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:
Franz Kafka 2026-03-21 18:35:31 +00:00
parent 91ab76758c
commit 13040268d6
7 changed files with 657 additions and 18 deletions

View file

@ -73,7 +73,7 @@ func main() {
var subFS fs.FS = staticFS
mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.FS(subFS))))
// Apply middleware: rate limiter → CORS → handler.
// Apply middleware: global rate limit → burst rate limit → per-IP rate limit → CORS → handler.
var handler http.Handler = mux
handler = middleware.CORS(middleware.CORSConfig{
AllowedOrigins: cfg.CORS.AllowedOrigins,
@ -87,6 +87,16 @@ func main() {
Window: cfg.RateLimitWindow(),
CleanupInterval: cfg.RateLimitCleanupInterval(),
}, logger)(handler)
handler = middleware.GlobalRateLimit(middleware.GlobalRateLimitConfig{
Requests: cfg.GlobalRateLimit.Requests,
Window: cfg.GlobalRateLimitWindow(),
}, logger)(handler)
handler = middleware.BurstRateLimit(middleware.BurstRateLimitConfig{
Burst: cfg.BurstRateLimit.Burst,
BurstWindow: cfg.BurstWindow(),
Sustained: cfg.BurstRateLimit.Sustained,
SustainedWindow: cfg.SustainedWindow(),
}, logger)(handler)
addr := fmt.Sprintf(":%d", cfg.Server.Port)
logger.Info("searxng-go starting",