use std::collections::HashMap;
use std::time::{Duration, Instant};
pub const WINDOW: Duration = Duration::from_secs(60);
pub const BLOCK: Duration = Duration::from_secs(60);
pub const CONN_MAX_FAILS: u32 = 5;
pub const GLOBAL_MAX_FAILS: u32 = 30;
pub const MAX_ENTRIES: usize = 256;
#[derive(Default)]
struct Bucket {
start: Option<Instant>,
fails: u32,
blocked_until: Option<Instant>,
}
impl Bucket {
fn blocked(&self, now: Instant) -> bool {
self.blocked_until.is_some_and(|t| now < t)
}
fn window_open(&self, now: Instant) -> bool {
self.start
.is_some_and(|s| now.saturating_duration_since(s) < WINDOW)
}
fn idle(&self, now: Instant) -> bool {
!self.blocked(now) && !self.window_open(now)
}
fn fail(&mut self, now: Instant, max: u32) {
if !self.window_open(now) {
self.start = Some(now);
self.fails = 0;
}
self.fails += 1;
if self.fails >= max {
self.blocked_until = Some(now + BLOCK);
self.start = None;
self.fails = 0;
}
}
}
#[derive(Default)]
pub struct FailLimiter {
conns: HashMap<u64, Bucket>,
global: Bucket,
}
impl FailLimiter {
pub fn new() -> Self {
Self::default()
}
pub fn refuses_unverified(&self, conn: u64, now: Instant) -> bool {
self.global.blocked(now) || self.conns.get(&conn).is_some_and(|b| b.blocked(now))
}
pub fn record_failure(&mut self, conn: u64, now: Instant) {
self.global.fail(now, GLOBAL_MAX_FAILS);
if !self.conns.contains_key(&conn) && self.conns.len() >= MAX_ENTRIES {
self.conns.retain(|_, b| !b.idle(now));
if self.conns.len() >= MAX_ENTRIES {
return;
}
}
self.conns
.entry(conn)
.or_default()
.fail(now, CONN_MAX_FAILS);
}
pub fn forget(&mut self, conn: u64, now: Instant) {
if self.conns.get(&conn).is_some_and(|b| b.idle(now)) {
self.conns.remove(&conn);
}
}
}