package server import ( "net" "net/http" "sync" "time" ) // ipLimiter is a fixed-window per-client rate limiter keyed by remote IP. It is // deliberately minimal: no bursting, no warm-up, just a hard cap per window to // keep an open relay from being flooded. type ipLimiter struct { mu sync.Mutex limit int window time.Duration hits map[string]int reset map[string]time.Time } func newIPLimiter(limit int, window time.Duration) *ipLimiter { return &ipLimiter{ limit: limit, window: window, hits: make(map[string]int), reset: make(map[string]time.Time), } } // allow reports whether the client may proceed, incrementing its windowed count. func (l *ipLimiter) allow(ip string) bool { l.mu.Lock() defer l.mu.Unlock() now := time.Now() until, ok := l.reset[ip] if !ok || now.After(until) { l.hits[ip] = 0 l.reset[ip] = now.Add(l.window) } else if now.Sub(until) > l.window*2 { // Lazy cleanup of long-idle entries. delete(l.hits, ip) delete(l.reset, ip) l.hits[ip] = 0 l.reset[ip] = now.Add(l.window) } if l.hits[ip] >= l.limit { return false } l.hits[ip]++ return true } // clientIP returns the request's remote IP, stripping a port if present. // With trustProxy set (Config.TrustProxy) the left-most X-Forwarded-For // entry wins: the relay sits behind a reverse proxy and every socket would // otherwise share the proxy's address. The header is attacker-controlled, // which is exactly why trusting it is an explicit operator choice. func clientIP(r *http.Request, trustProxy bool) string { if trustProxy { if xff := r.Header.Get("X-Forwarded-For"); xff != "" { if i := indexByte(xff, ','); i >= 0 { xff = xff[:i] } return trimSpace(xff) } } host, _, err := net.SplitHostPort(r.RemoteAddr) if err != nil { return r.RemoteAddr } return host } func indexByte(s string, b byte) int { for i := 0; i < len(s); i++ { if s[i] == b { return i } } return -1 } func trimSpace(s string) string { for len(s) > 0 && (s[0] == ' ' || s[0] == '\t') { s = s[1:] } for len(s) > 0 && (s[len(s)-1] == ' ' || s[len(s)-1] == '\t') { s = s[:len(s)-1] } return s }