feat: add global and burst rate limiters
Three layers of rate limiting, all disabled by default, opt-in via config: 1. Per-IP (existing): 30 req/min per IP 2. Global: server-wide limit across all IPs - Lock-free atomic counter for minimal overhead - Returns 503 when exceeded - Prevents pool exhaustion from distributed attacks 3. Burst: per-IP burst + sustained windows - Blocks rapid-fire abuse within seconds - Returns 429 with X-RateLimit-Reason header - Example: 5 req/5s burst, 60 req/min sustained Config: [global_rate_limit] requests = 0 # disabled by default window = "1m" [burst_rate_limit] burst = 0 # disabled by default burst_window = "5s" sustained = 0 sustained_window = "1m" Env overrides: GLOBAL_RATE_LIMIT_REQUESTS, GLOBAL_RATE_LIMIT_WINDOW, BURST_RATE_LIMIT_BURST, BURST_RATE_LIMIT_BURST_WINDOW, BURST_RATE_LIMIT_SUSTAINED, BURST_RATE_LIMIT_SUSTAINED_WINDOW Full test coverage: concurrent lock-free test, window expiry, disabled states, IP isolation, burst vs sustained distinction.
This commit is contained in:
parent
91ab76758c
commit
13040268d6
7 changed files with 657 additions and 18 deletions
|
|
@ -45,13 +45,12 @@ func RateLimit(cfg RateLimitConfig, logger *slog.Logger) func(http.Handler) http
|
|||
}
|
||||
|
||||
limiter := &ipLimiter{
|
||||
requests: requests,
|
||||
window: window,
|
||||
clients: make(map[string]*bucket),
|
||||
logger: logger,
|
||||
requests: requests,
|
||||
window: window,
|
||||
clients: make(map[string]*bucket),
|
||||
logger: logger,
|
||||
}
|
||||
|
||||
// Background cleanup of stale buckets.
|
||||
go limiter.cleanup(cleanup)
|
||||
|
||||
return func(next http.Handler) http.Handler {
|
||||
|
|
@ -122,13 +121,9 @@ func (l *ipLimiter) cleanup(interval time.Duration) {
|
|||
}
|
||||
|
||||
func extractIP(r *http.Request) string {
|
||||
// Trust X-Forwarded-For / X-Real-IP if behind a proxy.
|
||||
if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
|
||||
// First IP in the chain is the client.
|
||||
if idx := len(xff); idx > 0 {
|
||||
parts := strings.SplitN(xff, ",", 2)
|
||||
return strings.TrimSpace(parts[0])
|
||||
}
|
||||
parts := strings.SplitN(xff, ",", 2)
|
||||
return strings.TrimSpace(parts[0])
|
||||
}
|
||||
if rip := r.Header.Get("X-Real-IP"); rip != "" {
|
||||
return strings.TrimSpace(rip)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue