use std::collections::VecDeque;
use std::net::IpAddr;
use std::sync::Mutex;
use std::time::{Duration, Instant};
const BAN_DURATION: Duration = Duration::from_mins(10);
const BAN_CAPACITY: usize = 100;
#[derive(Debug)]
pub struct IpBanList {
banned: Mutex<VecDeque<(IpAddr, Instant)>>,
duration: Duration,
capacity: usize,
}
impl Default for IpBanList {
fn default() -> Self {
Self::new(BAN_DURATION, BAN_CAPACITY)
}
}
impl IpBanList {
fn new(duration: Duration, capacity: usize) -> Self {
Self {
banned: Mutex::new(VecDeque::new()),
duration,
capacity,
}
}
pub fn add(&self, ip: IpAddr) {
let expiry = Instant::now() + self.duration;
let mut banned = self.banned.lock().expect("ban mutex should not be poisoned");
if banned.len() == self.capacity {
banned.pop_front();
}
banned.push_back((ip, expiry));
}
pub fn banned_until(&self, ip: IpAddr) -> Option<Instant> {
let now = Instant::now();
let banned = self.banned.lock().expect("ban mutex should not be poisoned");
banned
.iter()
.rev()
.find(|(banned_ip, _)| *banned_ip == ip)
.map(|(_, expiry)| *expiry)
.filter(|expiry| *expiry > now)
}
}
#[cfg(test)]
mod tests {
use std::net::Ipv4Addr;
use super::*;
fn ip(n: u8) -> IpAddr {
IpAddr::V4(Ipv4Addr::new(127, 0, 0, n))
}
#[test]
fn unknown_ip_is_not_banned() {
let ban = IpBanList::default();
assert!(ban.banned_until(ip(1)).is_none());
}
#[test]
fn banned_ip_reports_expiry() {
let ban = IpBanList::new(Duration::from_secs(600), 16);
let before = Instant::now();
ban.add(ip(1));
let after = Instant::now();
let until = ban.banned_until(ip(1)).expect("ip should be banned");
assert!(until >= before + Duration::from_secs(600));
assert!(until <= after + Duration::from_secs(600));
assert!(ban.banned_until(ip(2)).is_none());
}
#[test]
fn expired_ban_is_ignored() {
let ban = IpBanList::new(Duration::ZERO, 16);
ban.add(ip(1));
assert!(ban.banned_until(ip(1)).is_none());
}
#[test]
fn most_recent_ban_wins_over_stale_entry() {
let ban = IpBanList::new(Duration::from_secs(600), 16);
let now = Instant::now();
{
let mut banned = ban.banned.lock().unwrap();
banned.push_back((ip(1), now));
banned.push_back((ip(1), now + Duration::from_secs(600)));
}
assert!(ban.banned_until(ip(1)).is_some());
}
#[test]
fn oldest_entry_is_evicted_at_capacity() {
let ban = IpBanList::new(Duration::from_secs(600), 2);
ban.add(ip(1));
ban.add(ip(2));
ban.add(ip(3));
assert!(ban.banned_until(ip(1)).is_none());
assert!(ban.banned_until(ip(2)).is_some());
assert!(ban.banned_until(ip(3)).is_some());
}
}