You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
126 lines
3.6 KiB
126 lines
3.6 KiB
package acl |
|
|
|
import ( |
|
"sync" |
|
"time" |
|
) |
|
|
|
// ThrottleState tracks accumulated delay for an identity (IP or pubkey) |
|
type ThrottleState struct { |
|
AccumulatedDelay time.Duration |
|
LastEventTime time.Time |
|
} |
|
|
|
// ProgressiveThrottle implements linear delay with time decay. |
|
// Each event adds perEvent delay, and delay decays at 1:1 ratio with elapsed time. |
|
// This creates a natural rate limit that averages to 1 event per perEvent interval. |
|
type ProgressiveThrottle struct { |
|
mu sync.Mutex |
|
ipStates map[string]*ThrottleState |
|
pubkeyStates map[string]*ThrottleState |
|
perEvent time.Duration // delay increment per event (default 200ms) |
|
maxDelay time.Duration // cap (default 60s) |
|
} |
|
|
|
// NewProgressiveThrottle creates a new throttle with the given parameters. |
|
// perEvent is the delay added per event (e.g., 200ms). |
|
// maxDelay is the maximum accumulated delay cap (e.g., 60s). |
|
func NewProgressiveThrottle(perEvent, maxDelay time.Duration) *ProgressiveThrottle { |
|
return &ProgressiveThrottle{ |
|
ipStates: make(map[string]*ThrottleState), |
|
pubkeyStates: make(map[string]*ThrottleState), |
|
perEvent: perEvent, |
|
maxDelay: maxDelay, |
|
} |
|
} |
|
|
|
// GetDelay returns accumulated delay for this identity and updates state. |
|
// It tracks both IP and pubkey independently and returns the maximum of both. |
|
// This prevents evasion via different pubkeys from same IP or vice versa. |
|
func (pt *ProgressiveThrottle) GetDelay(ip, pubkeyHex string) time.Duration { |
|
pt.mu.Lock() |
|
defer pt.mu.Unlock() |
|
|
|
now := time.Now() |
|
var ipDelay, pubkeyDelay time.Duration |
|
|
|
if ip != "" { |
|
ipDelay = pt.updateState(pt.ipStates, ip, now) |
|
} |
|
if pubkeyHex != "" { |
|
pubkeyDelay = pt.updateState(pt.pubkeyStates, pubkeyHex, now) |
|
} |
|
|
|
// Return max of both to prevent evasion |
|
if ipDelay > pubkeyDelay { |
|
return ipDelay |
|
} |
|
return pubkeyDelay |
|
} |
|
|
|
// updateState calculates and updates the delay for a single identity. |
|
// The algorithm: |
|
// 1. Decay: subtract elapsed time from accumulated delay (1:1 ratio) |
|
// 2. Add: add perEvent for this new event |
|
// 3. Cap: limit to maxDelay |
|
func (pt *ProgressiveThrottle) updateState(states map[string]*ThrottleState, key string, now time.Time) time.Duration { |
|
state, exists := states[key] |
|
if !exists { |
|
// First event from this identity |
|
states[key] = &ThrottleState{ |
|
AccumulatedDelay: pt.perEvent, |
|
LastEventTime: now, |
|
} |
|
return pt.perEvent |
|
} |
|
|
|
// Decay: subtract elapsed time (1:1 ratio) |
|
elapsed := now.Sub(state.LastEventTime) |
|
state.AccumulatedDelay -= elapsed |
|
if state.AccumulatedDelay < 0 { |
|
state.AccumulatedDelay = 0 |
|
} |
|
|
|
// Add new event's delay |
|
state.AccumulatedDelay += pt.perEvent |
|
state.LastEventTime = now |
|
|
|
// Cap at max |
|
if state.AccumulatedDelay > pt.maxDelay { |
|
state.AccumulatedDelay = pt.maxDelay |
|
} |
|
|
|
return state.AccumulatedDelay |
|
} |
|
|
|
// Cleanup removes entries that have fully decayed (no remaining delay). |
|
// This should be called periodically to prevent unbounded memory growth. |
|
func (pt *ProgressiveThrottle) Cleanup() { |
|
pt.mu.Lock() |
|
defer pt.mu.Unlock() |
|
|
|
now := time.Now() |
|
|
|
// Remove IP entries that have fully decayed |
|
for k, v := range pt.ipStates { |
|
elapsed := now.Sub(v.LastEventTime) |
|
if elapsed >= v.AccumulatedDelay { |
|
delete(pt.ipStates, k) |
|
} |
|
} |
|
|
|
// Remove pubkey entries that have fully decayed |
|
for k, v := range pt.pubkeyStates { |
|
elapsed := now.Sub(v.LastEventTime) |
|
if elapsed >= v.AccumulatedDelay { |
|
delete(pt.pubkeyStates, k) |
|
} |
|
} |
|
} |
|
|
|
// Stats returns the current number of tracked IPs and pubkeys (for monitoring) |
|
func (pt *ProgressiveThrottle) Stats() (ipCount, pubkeyCount int) { |
|
pt.mu.Lock() |
|
defer pt.mu.Unlock() |
|
return len(pt.ipStates), len(pt.pubkeyStates) |
|
}
|
|
|