v0.4.1 authentication impelemented, not tested and docs not updated
This commit is contained in:
+385
-85
@@ -14,11 +14,32 @@ import (
|
||||
"github.com/lixenwraith/log"
|
||||
)
|
||||
|
||||
// DenialReason indicates why a request was denied
|
||||
type DenialReason string
|
||||
|
||||
const (
|
||||
// IPv4Only is the enforcement message for IPv6 rejection
|
||||
IPv4Only = "IPv4-only (IPv6 not supported)"
|
||||
)
|
||||
|
||||
const (
|
||||
ReasonAllowed DenialReason = ""
|
||||
ReasonBlacklisted DenialReason = "IP denied by blacklist"
|
||||
ReasonNotWhitelisted DenialReason = "IP not in whitelist"
|
||||
ReasonRateLimited DenialReason = "Rate limit exceeded"
|
||||
ReasonConnectionLimited DenialReason = "Connection limit exceeded"
|
||||
ReasonInvalidIP DenialReason = "Invalid IP address"
|
||||
)
|
||||
|
||||
// NetLimiter manages net limiting for a transport
|
||||
type NetLimiter struct {
|
||||
config config.NetLimitConfig
|
||||
logger *log.Logger
|
||||
|
||||
// IP Access Control Lists
|
||||
ipWhitelist []*net.IPNet
|
||||
ipBlacklist []*net.IPNet
|
||||
|
||||
// Per-IP limiters
|
||||
ipLimiters map[string]*ipLimiter
|
||||
ipMu sync.RWMutex
|
||||
@@ -27,17 +48,22 @@ type NetLimiter struct {
|
||||
globalLimiter *TokenBucket
|
||||
|
||||
// Connection tracking
|
||||
ipConnections map[string]*atomic.Int64
|
||||
ipConnections map[string]*connTracker
|
||||
connMu sync.RWMutex
|
||||
|
||||
// Statistics
|
||||
totalRequests atomic.Uint64
|
||||
blockedRequests atomic.Uint64
|
||||
uniqueIPs atomic.Uint64
|
||||
totalRequests atomic.Uint64
|
||||
blockedByBlacklist atomic.Uint64
|
||||
blockedByWhitelist atomic.Uint64
|
||||
blockedByRateLimit atomic.Uint64
|
||||
blockedByConnLimit atomic.Uint64
|
||||
blockedByInvalidIP atomic.Uint64
|
||||
uniqueIPs atomic.Uint64
|
||||
|
||||
// Cleanup
|
||||
lastCleanup time.Time
|
||||
cleanupMu sync.Mutex
|
||||
lastCleanup time.Time
|
||||
cleanupMu sync.Mutex
|
||||
cleanupActive atomic.Bool
|
||||
|
||||
// Lifecycle management
|
||||
ctx context.Context
|
||||
@@ -51,9 +77,20 @@ type ipLimiter struct {
|
||||
connections atomic.Int64
|
||||
}
|
||||
|
||||
// Connection tracking with activity timestamp
|
||||
type connTracker struct {
|
||||
connections atomic.Int64
|
||||
lastSeen time.Time
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
// Creates a new net limiter
|
||||
func NewNetLimiter(cfg config.NetLimitConfig, logger *log.Logger) *NetLimiter {
|
||||
if !cfg.Enabled {
|
||||
// Return nil only if nothing is configured
|
||||
hasACL := len(cfg.IPWhitelist) > 0 || len(cfg.IPBlacklist) > 0
|
||||
hasRateLimit := cfg.Enabled
|
||||
|
||||
if !hasACL && !hasRateLimit {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -65,28 +102,39 @@ func NewNetLimiter(cfg config.NetLimitConfig, logger *log.Logger) *NetLimiter {
|
||||
|
||||
l := &NetLimiter{
|
||||
config: cfg,
|
||||
ipLimiters: make(map[string]*ipLimiter),
|
||||
ipConnections: make(map[string]*atomic.Int64),
|
||||
lastCleanup: time.Now(),
|
||||
logger: logger,
|
||||
ipWhitelist: make([]*net.IPNet, 0),
|
||||
ipBlacklist: make([]*net.IPNet, 0),
|
||||
ipLimiters: make(map[string]*ipLimiter),
|
||||
ipConnections: make(map[string]*connTracker),
|
||||
lastCleanup: time.Now(),
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
cleanupDone: make(chan struct{}),
|
||||
}
|
||||
|
||||
// Create global limiter if not using per-IP limiting
|
||||
if cfg.LimitBy == "global" {
|
||||
// Parse IP lists
|
||||
l.parseIPLists(cfg)
|
||||
|
||||
// Create global limiter if configured
|
||||
if cfg.Enabled && cfg.LimitBy == "global" {
|
||||
l.globalLimiter = NewTokenBucket(
|
||||
float64(cfg.BurstSize),
|
||||
cfg.RequestsPerSecond,
|
||||
)
|
||||
}
|
||||
|
||||
// Start cleanup goroutine
|
||||
go l.cleanupLoop()
|
||||
// Start cleanup goroutine only if rate limiting is enabled
|
||||
if cfg.Enabled {
|
||||
go l.cleanupLoop()
|
||||
}
|
||||
|
||||
l.logger.Info("msg", "Net limiter initialized",
|
||||
logger.Info("msg", "Net limiter initialized",
|
||||
"component", "netlimit",
|
||||
"acl_enabled", hasACL,
|
||||
"rate_limiting", cfg.Enabled,
|
||||
"whitelist_rules", len(l.ipWhitelist),
|
||||
"blacklist_rules", len(l.ipBlacklist),
|
||||
"requests_per_second", cfg.RequestsPerSecond,
|
||||
"burst_size", cfg.BurstSize,
|
||||
"limit_by", cfg.LimitBy)
|
||||
@@ -94,6 +142,120 @@ func NewNetLimiter(cfg config.NetLimitConfig, logger *log.Logger) *NetLimiter {
|
||||
return l
|
||||
}
|
||||
|
||||
// parseIPLists parses and validates IP whitelist/blacklist
|
||||
func (l *NetLimiter) parseIPLists(cfg config.NetLimitConfig) {
|
||||
// Parse whitelist
|
||||
for _, entry := range cfg.IPWhitelist {
|
||||
if ipNet := l.parseIPEntry(entry, "whitelist"); ipNet != nil {
|
||||
l.ipWhitelist = append(l.ipWhitelist, ipNet)
|
||||
}
|
||||
}
|
||||
|
||||
// Parse blacklist
|
||||
for _, entry := range cfg.IPBlacklist {
|
||||
if ipNet := l.parseIPEntry(entry, "blacklist"); ipNet != nil {
|
||||
l.ipBlacklist = append(l.ipBlacklist, ipNet)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// parseIPEntry parses a single IP or CIDR entry
|
||||
func (l *NetLimiter) parseIPEntry(entry, listType string) *net.IPNet {
|
||||
// Handle single IP
|
||||
if !strings.Contains(entry, "/") {
|
||||
ip := net.ParseIP(entry)
|
||||
if ip == nil {
|
||||
l.logger.Warn("msg", "Invalid IP entry",
|
||||
"component", "netlimit",
|
||||
"list", listType,
|
||||
"entry", entry)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Reject IPv6
|
||||
if ip.To4() == nil {
|
||||
l.logger.Warn("msg", "IPv6 address rejected",
|
||||
"component", "netlimit",
|
||||
"list", listType,
|
||||
"entry", entry,
|
||||
"reason", IPv4Only)
|
||||
return nil
|
||||
}
|
||||
|
||||
return &net.IPNet{IP: ip.To4(), Mask: net.CIDRMask(32, 32)}
|
||||
}
|
||||
|
||||
// Parse CIDR
|
||||
ipAddr, ipNet, err := net.ParseCIDR(entry)
|
||||
if err != nil {
|
||||
l.logger.Warn("msg", "Invalid CIDR entry",
|
||||
"component", "netlimit",
|
||||
"list", listType,
|
||||
"entry", entry,
|
||||
"error", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Reject IPv6 CIDR
|
||||
if ipAddr.To4() == nil {
|
||||
l.logger.Warn("msg", "IPv6 CIDR rejected",
|
||||
"component", "netlimit",
|
||||
"list", listType,
|
||||
"entry", entry,
|
||||
"reason", IPv4Only)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Ensure mask is IPv4
|
||||
_, bits := ipNet.Mask.Size()
|
||||
if bits != 32 {
|
||||
l.logger.Warn("msg", "Non-IPv4 CIDR mask rejected",
|
||||
"component", "netlimit",
|
||||
"list", listType,
|
||||
"entry", entry,
|
||||
"mask_bits", bits,
|
||||
"reason", IPv4Only)
|
||||
return nil
|
||||
}
|
||||
|
||||
return &net.IPNet{IP: ipAddr.To4(), Mask: ipNet.Mask}
|
||||
}
|
||||
|
||||
// checkIPAccess checks if an IP is allowed by ACLs
|
||||
func (l *NetLimiter) checkIPAccess(ip net.IP) DenialReason {
|
||||
// 1. Check blacklist first (deny takes precedence)
|
||||
for _, ipNet := range l.ipBlacklist {
|
||||
if ipNet.Contains(ip) {
|
||||
l.blockedByBlacklist.Add(1)
|
||||
l.logger.Debug("msg", "IP denied by blacklist",
|
||||
"component", "netlimit",
|
||||
"ip", ip.String(),
|
||||
"rule", ipNet.String())
|
||||
return ReasonBlacklisted
|
||||
}
|
||||
}
|
||||
|
||||
// 2. If whitelist is configured, IP must be in it
|
||||
if len(l.ipWhitelist) > 0 {
|
||||
for _, ipNet := range l.ipWhitelist {
|
||||
if ipNet.Contains(ip) {
|
||||
l.logger.Debug("msg", "IP allowed by whitelist",
|
||||
"component", "netlimit",
|
||||
"ip", ip.String(),
|
||||
"rule", ipNet.String())
|
||||
return ReasonAllowed
|
||||
}
|
||||
}
|
||||
l.blockedByWhitelist.Add(1)
|
||||
l.logger.Debug("msg", "IP not in whitelist",
|
||||
"component", "netlimit",
|
||||
"ip", ip.String())
|
||||
return ReasonNotWhitelisted
|
||||
}
|
||||
|
||||
return ReasonAllowed
|
||||
}
|
||||
|
||||
func (l *NetLimiter) Shutdown() {
|
||||
if l == nil {
|
||||
return
|
||||
@@ -121,9 +283,9 @@ func (l *NetLimiter) CheckHTTP(remoteAddr string) (allowed bool, statusCode int6
|
||||
|
||||
l.totalRequests.Add(1)
|
||||
|
||||
ip, _, err := net.SplitHostPort(remoteAddr)
|
||||
// Parse IP address
|
||||
ipStr, _, err := net.SplitHostPort(remoteAddr)
|
||||
if err != nil {
|
||||
// If we can't parse the IP, allow the request but log
|
||||
l.logger.Warn("msg", "Failed to parse remote addr",
|
||||
"component", "netlimit",
|
||||
"remote_addr", remoteAddr,
|
||||
@@ -131,56 +293,82 @@ func (l *NetLimiter) CheckHTTP(remoteAddr string) (allowed bool, statusCode int6
|
||||
return true, 0, ""
|
||||
}
|
||||
|
||||
// Only supporting ipv4
|
||||
if !isIPv4(ip) {
|
||||
// Block non-IPv4 addresses to prevent complications
|
||||
l.blockedRequests.Add(1)
|
||||
l.logger.Warn("msg", "Non-IPv4 address blocked",
|
||||
ip := net.ParseIP(ipStr)
|
||||
if ip == nil {
|
||||
l.blockedByInvalidIP.Add(1)
|
||||
l.logger.Warn("msg", "Failed to parse IP",
|
||||
"component", "netlimit",
|
||||
"ip", ip)
|
||||
return false, 403, "IPv4 only"
|
||||
"ip", ipStr)
|
||||
return false, 403, string(ReasonInvalidIP)
|
||||
}
|
||||
|
||||
// Check connection limit for streaming endpoint
|
||||
// Reject IPv6 connections
|
||||
if !isIPv4(ip) {
|
||||
l.blockedByInvalidIP.Add(1)
|
||||
l.logger.Warn("msg", "IPv6 connection rejected",
|
||||
"component", "netlimit",
|
||||
"ip", ipStr,
|
||||
"reason", IPv4Only)
|
||||
return false, 403, IPv4Only
|
||||
}
|
||||
|
||||
// Normalize to IPv4 representation
|
||||
ip = ip.To4()
|
||||
|
||||
// Check IP access control
|
||||
if reason := l.checkIPAccess(ip); reason != ReasonAllowed {
|
||||
return false, 403, string(reason)
|
||||
}
|
||||
|
||||
// If rate limiting is not enabled, allow
|
||||
if !l.config.Enabled {
|
||||
return true, 0, ""
|
||||
}
|
||||
|
||||
// Check connection limits
|
||||
if l.config.MaxConnectionsPerIP > 0 {
|
||||
l.connMu.RLock()
|
||||
counter, exists := l.ipConnections[ip]
|
||||
tracker, exists := l.ipConnections[ipStr]
|
||||
l.connMu.RUnlock()
|
||||
|
||||
if exists && counter.Load() >= l.config.MaxConnectionsPerIP {
|
||||
l.blockedRequests.Add(1)
|
||||
if exists && tracker.connections.Load() >= l.config.MaxConnectionsPerIP {
|
||||
l.blockedByConnLimit.Add(1)
|
||||
statusCode = l.config.ResponseCode
|
||||
if statusCode == 0 {
|
||||
statusCode = 429
|
||||
}
|
||||
message = "Connection limit exceeded"
|
||||
|
||||
l.logger.Warn("msg", "Connection limit exceeded",
|
||||
"component", "netlimit",
|
||||
"ip", ip,
|
||||
"connections", counter.Load(),
|
||||
"limit", l.config.MaxConnectionsPerIP)
|
||||
|
||||
return false, statusCode, message
|
||||
return false, statusCode, string(ReasonConnectionLimited)
|
||||
}
|
||||
}
|
||||
|
||||
// Check net limit
|
||||
allowed = l.checkLimit(ip)
|
||||
if !allowed {
|
||||
l.blockedRequests.Add(1)
|
||||
// Check rate limit
|
||||
if !l.checkLimit(ipStr) {
|
||||
l.blockedByRateLimit.Add(1)
|
||||
statusCode = l.config.ResponseCode
|
||||
if statusCode == 0 {
|
||||
statusCode = 429
|
||||
}
|
||||
message = l.config.ResponseMessage
|
||||
if message == "" {
|
||||
message = "Net limit exceeded"
|
||||
message = string(ReasonRateLimited)
|
||||
}
|
||||
l.logger.Debug("msg", "Request net limited", "ip", ip)
|
||||
return false, statusCode, message
|
||||
}
|
||||
|
||||
return allowed, statusCode, message
|
||||
return true, 0, ""
|
||||
}
|
||||
|
||||
// Update connection activity
|
||||
func (l *NetLimiter) updateConnectionActivity(ip string) {
|
||||
l.connMu.RLock()
|
||||
tracker, exists := l.ipConnections[ip]
|
||||
l.connMu.RUnlock()
|
||||
|
||||
if exists {
|
||||
tracker.mu.Lock()
|
||||
tracker.lastSeen = time.Now()
|
||||
tracker.mu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
// Checks if a TCP connection should be allowed
|
||||
@@ -194,32 +382,45 @@ func (l *NetLimiter) CheckTCP(remoteAddr net.Addr) bool {
|
||||
// Extract IP from TCP addr
|
||||
tcpAddr, ok := remoteAddr.(*net.TCPAddr)
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
|
||||
ip := tcpAddr.IP.String()
|
||||
|
||||
// Only supporting ipv4
|
||||
if !isIPv4(ip) {
|
||||
l.blockedRequests.Add(1)
|
||||
l.logger.Warn("msg", "Non-IPv4 TCP connection blocked",
|
||||
"component", "netlimit",
|
||||
"ip", ip)
|
||||
l.blockedByInvalidIP.Add(1)
|
||||
return false
|
||||
}
|
||||
|
||||
allowed := l.checkLimit(ip)
|
||||
if !allowed {
|
||||
l.blockedRequests.Add(1)
|
||||
l.logger.Debug("msg", "TCP connection net limited", "ip", ip)
|
||||
// Reject IPv6 connections
|
||||
if !isIPv4(tcpAddr.IP) {
|
||||
l.blockedByInvalidIP.Add(1)
|
||||
l.logger.Warn("msg", "IPv6 TCP connection rejected",
|
||||
"component", "netlimit",
|
||||
"ip", tcpAddr.IP.String(),
|
||||
"reason", IPv4Only)
|
||||
return false
|
||||
}
|
||||
|
||||
return allowed
|
||||
// Normalize to IPv4 representation
|
||||
ip := tcpAddr.IP.To4()
|
||||
|
||||
// Check IP access control
|
||||
if reason := l.checkIPAccess(ip); reason != ReasonAllowed {
|
||||
return false
|
||||
}
|
||||
|
||||
// If rate limiting is not enabled, allow
|
||||
if !l.config.Enabled {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check rate limit
|
||||
ipStr := tcpAddr.IP.String()
|
||||
if !l.checkLimit(ipStr) {
|
||||
l.blockedByRateLimit.Add(1)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func isIPv4(ip string) bool {
|
||||
// Simple check: IPv4 addresses contain dots, IPv6 contain colons
|
||||
return strings.Contains(ip, ".") && !strings.Contains(ip, ":")
|
||||
func isIPv4(ip net.IP) bool {
|
||||
return ip.To4() != nil
|
||||
}
|
||||
|
||||
// Tracks a new connection for an IP
|
||||
@@ -230,23 +431,44 @@ func (l *NetLimiter) AddConnection(remoteAddr string) {
|
||||
|
||||
ip, _, err := net.SplitHostPort(remoteAddr)
|
||||
if err != nil {
|
||||
l.logger.Warn("msg", "Failed to parse remote address in AddConnection",
|
||||
"component", "netlimit",
|
||||
"remote_addr", remoteAddr,
|
||||
"error", err)
|
||||
return
|
||||
}
|
||||
|
||||
// IP validation
|
||||
parsedIP := net.ParseIP(ip)
|
||||
if parsedIP == nil {
|
||||
l.logger.Warn("msg", "Failed to parse IP in AddConnection",
|
||||
"component", "netlimit",
|
||||
"ip", ip)
|
||||
return
|
||||
}
|
||||
|
||||
// Only supporting ipv4
|
||||
if !isIPv4(ip) {
|
||||
if !isIPv4(parsedIP) {
|
||||
return
|
||||
}
|
||||
|
||||
l.connMu.Lock()
|
||||
counter, exists := l.ipConnections[ip]
|
||||
tracker, exists := l.ipConnections[ip]
|
||||
if !exists {
|
||||
counter = &atomic.Int64{}
|
||||
l.ipConnections[ip] = counter
|
||||
// Create new tracker with timestamp
|
||||
tracker = &connTracker{
|
||||
lastSeen: time.Now(),
|
||||
}
|
||||
l.ipConnections[ip] = tracker
|
||||
}
|
||||
l.connMu.Unlock()
|
||||
|
||||
newCount := counter.Add(1)
|
||||
newCount := tracker.connections.Add(1)
|
||||
// Update activity timestamp
|
||||
tracker.mu.Lock()
|
||||
tracker.lastSeen = time.Now()
|
||||
tracker.mu.Unlock()
|
||||
|
||||
l.logger.Debug("msg", "Connection added",
|
||||
"ip", ip,
|
||||
"connections", newCount)
|
||||
@@ -260,20 +482,33 @@ func (l *NetLimiter) RemoveConnection(remoteAddr string) {
|
||||
|
||||
ip, _, err := net.SplitHostPort(remoteAddr)
|
||||
if err != nil {
|
||||
l.logger.Warn("msg", "Failed to parse remote address in RemoveConnection",
|
||||
"component", "netlimit",
|
||||
"remote_addr", remoteAddr,
|
||||
"error", err)
|
||||
return
|
||||
}
|
||||
|
||||
// IP validation
|
||||
parsedIP := net.ParseIP(ip)
|
||||
if parsedIP == nil {
|
||||
l.logger.Warn("msg", "Failed to parse IP in RemoveConnection",
|
||||
"component", "netlimit",
|
||||
"ip", ip)
|
||||
return
|
||||
}
|
||||
|
||||
// Only supporting ipv4
|
||||
if !isIPv4(ip) {
|
||||
if !isIPv4(parsedIP) {
|
||||
return
|
||||
}
|
||||
|
||||
l.connMu.RLock()
|
||||
counter, exists := l.ipConnections[ip]
|
||||
tracker, exists := l.ipConnections[ip]
|
||||
l.connMu.RUnlock()
|
||||
|
||||
if exists {
|
||||
newCount := counter.Add(-1)
|
||||
newCount := tracker.connections.Add(-1)
|
||||
l.logger.Debug("msg", "Connection removed",
|
||||
"ip", ip,
|
||||
"connections", newCount)
|
||||
@@ -281,7 +516,7 @@ func (l *NetLimiter) RemoveConnection(remoteAddr string) {
|
||||
if newCount <= 0 {
|
||||
// Clean up if no more connections
|
||||
l.connMu.Lock()
|
||||
if counter.Load() <= 0 {
|
||||
if tracker.connections.Load() <= 0 {
|
||||
delete(l.ipConnections, ip)
|
||||
}
|
||||
l.connMu.Unlock()
|
||||
@@ -292,9 +527,7 @@ func (l *NetLimiter) RemoveConnection(remoteAddr string) {
|
||||
// Returns net limiter statistics
|
||||
func (l *NetLimiter) GetStats() map[string]any {
|
||||
if l == nil {
|
||||
return map[string]any{
|
||||
"enabled": false,
|
||||
}
|
||||
return map[string]any{"enabled": false}
|
||||
}
|
||||
|
||||
l.ipMu.RLock()
|
||||
@@ -303,18 +536,36 @@ func (l *NetLimiter) GetStats() map[string]any {
|
||||
|
||||
l.connMu.RLock()
|
||||
totalConnections := 0
|
||||
for _, counter := range l.ipConnections {
|
||||
totalConnections += int(counter.Load())
|
||||
for _, tracker := range l.ipConnections {
|
||||
totalConnections += int(tracker.connections.Load())
|
||||
}
|
||||
l.connMu.RUnlock()
|
||||
|
||||
totalBlocked := l.blockedByBlacklist.Load() +
|
||||
l.blockedByWhitelist.Load() +
|
||||
l.blockedByRateLimit.Load() +
|
||||
l.blockedByConnLimit.Load() +
|
||||
l.blockedByInvalidIP.Load()
|
||||
|
||||
return map[string]any{
|
||||
"enabled": true,
|
||||
"total_requests": l.totalRequests.Load(),
|
||||
"blocked_requests": l.blockedRequests.Load(),
|
||||
"enabled": true,
|
||||
"total_requests": l.totalRequests.Load(),
|
||||
"total_blocked": totalBlocked,
|
||||
"blocked_breakdown": map[string]uint64{
|
||||
"blacklist": l.blockedByBlacklist.Load(),
|
||||
"whitelist": l.blockedByWhitelist.Load(),
|
||||
"rate_limit": l.blockedByRateLimit.Load(),
|
||||
"conn_limit": l.blockedByConnLimit.Load(),
|
||||
"invalid_ip": l.blockedByInvalidIP.Load(),
|
||||
},
|
||||
"active_ips": activeIPs,
|
||||
"total_connections": totalConnections,
|
||||
"config": map[string]any{
|
||||
"acl": map[string]int{
|
||||
"whitelist_rules": len(l.ipWhitelist),
|
||||
"blacklist_rules": len(l.ipBlacklist),
|
||||
},
|
||||
"rate_limit": map[string]any{
|
||||
"enabled": l.config.Enabled,
|
||||
"requests_per_second": l.config.RequestsPerSecond,
|
||||
"burst_size": l.config.BurstSize,
|
||||
"limit_by": l.config.LimitBy,
|
||||
@@ -324,6 +575,15 @@ func (l *NetLimiter) GetStats() map[string]any {
|
||||
|
||||
// Performs the actual net limit check
|
||||
func (l *NetLimiter) checkLimit(ip string) bool {
|
||||
// Validate IP format
|
||||
parsedIP := net.ParseIP(ip)
|
||||
if parsedIP == nil || !isIPv4(parsedIP) {
|
||||
l.logger.Warn("msg", "Invalid or non-IPv4 address in rate limiter",
|
||||
"component", "netlimit",
|
||||
"ip", ip)
|
||||
return false
|
||||
}
|
||||
|
||||
// Maybe run cleanup
|
||||
l.maybeCleanup()
|
||||
|
||||
@@ -358,10 +618,10 @@ func (l *NetLimiter) checkLimit(ip string) bool {
|
||||
// Check connection limit if configured
|
||||
if l.config.MaxConnectionsPerIP > 0 {
|
||||
l.connMu.RLock()
|
||||
counter, exists := l.ipConnections[ip]
|
||||
tracker, exists := l.ipConnections[ip]
|
||||
l.connMu.RUnlock()
|
||||
|
||||
if exists && counter.Load() >= l.config.MaxConnectionsPerIP {
|
||||
if exists && tracker.connections.Load() >= l.config.MaxConnectionsPerIP {
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -379,14 +639,27 @@ func (l *NetLimiter) checkLimit(ip string) bool {
|
||||
// Runs cleanup if enough time has passed
|
||||
func (l *NetLimiter) maybeCleanup() {
|
||||
l.cleanupMu.Lock()
|
||||
defer l.cleanupMu.Unlock()
|
||||
|
||||
// Check if enough time has passed
|
||||
if time.Since(l.lastCleanup) < 30*time.Second {
|
||||
l.cleanupMu.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
// Check if cleanup already running
|
||||
if !l.cleanupActive.CompareAndSwap(false, true) {
|
||||
l.cleanupMu.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
l.lastCleanup = time.Now()
|
||||
go l.cleanup()
|
||||
l.cleanupMu.Unlock()
|
||||
|
||||
// Run cleanup async
|
||||
go func() {
|
||||
defer l.cleanupActive.Store(false)
|
||||
l.cleanup()
|
||||
}()
|
||||
}
|
||||
|
||||
// Removes stale IP limiters
|
||||
@@ -397,6 +670,8 @@ func (l *NetLimiter) cleanup() {
|
||||
l.ipMu.Lock()
|
||||
defer l.ipMu.Unlock()
|
||||
|
||||
// Clean up rate limiters
|
||||
l.ipMu.Lock()
|
||||
cleaned := 0
|
||||
for ip, lim := range l.ipLimiters {
|
||||
if now.Sub(lim.lastSeen) > staleTimeout {
|
||||
@@ -404,12 +679,37 @@ func (l *NetLimiter) cleanup() {
|
||||
cleaned++
|
||||
}
|
||||
}
|
||||
l.ipMu.Unlock()
|
||||
|
||||
if cleaned > 0 {
|
||||
l.logger.Debug("msg", "Cleaned up stale IP limiters",
|
||||
"component", "netlimit",
|
||||
"cleaned", cleaned,
|
||||
"remaining", len(l.ipLimiters))
|
||||
}
|
||||
|
||||
// Clean up stale connection trackers
|
||||
l.connMu.Lock()
|
||||
connCleaned := 0
|
||||
for ip, tracker := range l.ipConnections {
|
||||
tracker.mu.Lock()
|
||||
lastSeen := tracker.lastSeen
|
||||
tracker.mu.Unlock()
|
||||
|
||||
// Remove if no activity for 5 minutes AND no active connections
|
||||
if now.Sub(lastSeen) > staleTimeout && tracker.connections.Load() <= 0 {
|
||||
delete(l.ipConnections, ip)
|
||||
connCleaned++
|
||||
}
|
||||
}
|
||||
l.connMu.Unlock()
|
||||
|
||||
if connCleaned > 0 {
|
||||
l.logger.Debug("msg", "Cleaned up stale connection trackers",
|
||||
"component", "netlimit",
|
||||
"cleaned", connCleaned,
|
||||
"remaining", len(l.ipConnections))
|
||||
}
|
||||
}
|
||||
|
||||
// Runs periodic cleanup
|
||||
|
||||
Reference in New Issue
Block a user