use std::collections::HashMap;
use std::net::IpAddr;
use std::time::Instant;
const MAX_ATTEMPTS: usize = 5;
const WINDOW_SECS: u64 = 60;
const BAN_DURATION_SECS: u64 = 300;
#[derive(Debug, Clone)]
struct RateLimitEntry {
attempts: usize,
window_start: Instant,
banned_until: Option<Instant>,
}
#[derive(Debug, Default)]
pub struct RateLimiter {
entries: HashMap<IpAddr, RateLimitEntry>,
}
impl RateLimiter {
pub fn new() -> Self {
Self {
entries: HashMap::new(),
}
}
pub fn check_attempt(&mut self, ip: IpAddr) -> bool {
let now = Instant::now();
self.cleanup_expired(now);
let entry = self.entries.entry(ip).or_insert(RateLimitEntry {
attempts: 0,
window_start: now,
banned_until: None,
});
if let Some(banned_until) = entry.banned_until {
if now < banned_until {
log::warn!("Rejected auth attempt from banned IP: {}", ip);
return false;
}
entry.banned_until = None;
entry.attempts = 0;
entry.window_start = now;
}
if now.duration_since(entry.window_start).as_secs() > WINDOW_SECS {
entry.attempts = 0;
entry.window_start = now;
}
entry.attempts += 1;
if entry.attempts > MAX_ATTEMPTS {
entry.banned_until = Some(now + std::time::Duration::from_secs(BAN_DURATION_SECS));
log::warn!(
"IP {} banned for {} seconds after {} failed attempts",
ip,
BAN_DURATION_SECS,
entry.attempts
);
return false;
}
true
}
pub fn record_success(&mut self, ip: IpAddr) {
if let Some(entry) = self.entries.get_mut(&ip) {
entry.attempts = 0;
entry.banned_until = None;
}
}
fn cleanup_expired(&mut self, now: Instant) {
self.entries.retain(|_ip, entry| {
let expired = now.duration_since(entry.window_start).as_secs() > WINDOW_SECS + BAN_DURATION_SECS;
if let Some(banned_until) = entry.banned_until {
!expired || now < banned_until
} else {
!expired
}
});
}
pub fn tracked_count(&self) -> usize {
self.entries.len()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn test_ip() -> IpAddr {
"192.168.1.100".parse().unwrap()
}
#[test]
fn test_allows_first_five_attempts() {
let mut limiter = RateLimiter::new();
let ip = test_ip();
for _ in 0..5 {
assert!(limiter.check_attempt(ip));
}
}
#[test]
fn test_blocks_sixth_attempt() {
let mut limiter = RateLimiter::new();
let ip = test_ip();
for _ in 0..5 {
assert!(limiter.check_attempt(ip));
}
assert!(!limiter.check_attempt(ip));
}
#[test]
fn test_success_resets_counter() {
let mut limiter = RateLimiter::new();
let ip = test_ip();
for _ in 0..3 {
limiter.check_attempt(ip);
}
limiter.record_success(ip);
for _ in 0..5 {
assert!(limiter.check_attempt(ip));
}
}
#[test]
fn test_different_ips_independent() {
let mut limiter = RateLimiter::new();
let ip1: IpAddr = "10.0.0.1".parse().unwrap();
let ip2: IpAddr = "10.0.0.2".parse().unwrap();
for _ in 0..5 {
limiter.check_attempt(ip1);
}
assert!(!limiter.check_attempt(ip1));
assert!(limiter.check_attempt(ip2));
}
}