use std::collections::hash_map::RandomState;
use std::hash::{BuildHasher, Hasher};
use std::net::IpAddr;
use std::sync::Arc;
use std::sync::atomic::{AtomicU32, Ordering};
const IP_SLOTS: usize = 1 << 16;
pub(crate) struct PerIpConnLimit {
slots: Box<[AtomicU32]>,
hasher: RandomState,
limit: u32,
}
impl PerIpConnLimit {
pub(crate) fn new(limit: u32) -> Self {
Self {
slots: (0..IP_SLOTS).map(|_| AtomicU32::new(0)).collect(),
hasher: RandomState::new(),
limit,
}
}
pub(crate) fn try_acquire(limiter: &Arc<Self>, peer: IpAddr) -> Option<PerIpConnGuard> {
let slot = slot_for_ip(peer, &limiter.hasher);
let counter = limiter.slots.get(slot)?;
let mut cur = counter.load(Ordering::Relaxed);
loop {
if cur >= limiter.limit {
return None;
}
match counter.compare_exchange_weak(cur, cur + 1, Ordering::AcqRel, Ordering::Relaxed) {
Ok(_) => {
return Some(PerIpConnGuard {
limiter: limiter.clone(),
slot,
});
}
Err(actual) => cur = actual,
}
}
}
}
pub(crate) struct PerIpConnGuard {
limiter: Arc<PerIpConnLimit>,
slot: usize,
}
impl Drop for PerIpConnGuard {
fn drop(&mut self) {
if let Some(counter) = self.limiter.slots.get(self.slot) {
counter.fetch_sub(1, Ordering::AcqRel);
}
}
}
fn ip_key_bytes(peer: IpAddr) -> [u8; 8] {
let peer = match peer {
IpAddr::V6(v6) => match v6.to_ipv4_mapped() {
Some(v4) => IpAddr::V4(v4),
None => IpAddr::V6(v6),
},
v4 => v4,
};
match peer {
IpAddr::V4(v4) => {
let o = v4.octets();
[o[0], o[1], o[2], o[3], 0, 0, 0, 0]
}
IpAddr::V6(v6) => {
let o = v6.octets();
[o[0], o[1], o[2], o[3], o[4], o[5], o[6], o[7]]
}
}
}
fn slot_for_ip(peer: IpAddr, hasher: &RandomState) -> usize {
let mut h = hasher.build_hasher();
h.write(&ip_key_bytes(peer));
(h.finish() as usize) & (IP_SLOTS - 1)
}
#[cfg(test)]
mod tests {
use super::*;
use std::net::{Ipv4Addr, Ipv6Addr};
fn ip(s: &str) -> IpAddr {
s.parse().unwrap()
}
#[test]
fn admits_up_to_the_limit_then_refuses() {
let limiter = Arc::new(PerIpConnLimit::new(2));
let peer = ip("203.0.113.7");
let g1 = PerIpConnLimit::try_acquire(&limiter, peer);
let g2 = PerIpConnLimit::try_acquire(&limiter, peer);
let g3 = PerIpConnLimit::try_acquire(&limiter, peer);
assert!(g1.is_some(), "1st connection must be admitted");
assert!(
g2.is_some(),
"2nd connection (at the limit) must be admitted"
);
assert!(g3.is_none(), "3rd connection must be refused (limit is 2)");
}
#[test]
fn releasing_a_guard_frees_a_slot() {
let limiter = Arc::new(PerIpConnLimit::new(1));
let peer = ip("203.0.113.7");
let g1 = PerIpConnLimit::try_acquire(&limiter, peer).expect("1st admitted");
assert!(
PerIpConnLimit::try_acquire(&limiter, peer).is_none(),
"at the limit while g1 is held"
);
drop(g1);
assert!(
PerIpConnLimit::try_acquire(&limiter, peer).is_some(),
"dropping the guard must free the slot"
);
}
#[test]
fn distinct_peers_get_independent_counters() {
let limiter = Arc::new(PerIpConnLimit::new(1));
let drained = ip("192.0.2.255");
let _g = PerIpConnLimit::try_acquire(&limiter, drained).expect("drained peer admitted");
assert!(PerIpConnLimit::try_acquire(&limiter, drained).is_none());
let allowed = (0..=255u8)
.map(|n| IpAddr::V4(Ipv4Addr::new(192, 0, 2, n)))
.filter(|p| *p != drained)
.filter(|p| PerIpConnLimit::try_acquire(&limiter, *p).is_some())
.count();
assert!(
allowed >= 250,
"distinct peers get independent slots (got {allowed}/255 admitted)"
);
}
#[test]
fn ipv4_mapped_v6_collapses_to_v4_key() {
let v4 = ip("198.51.100.9");
let mapped = IpAddr::V6(Ipv4Addr::new(198, 51, 100, 9).to_ipv6_mapped());
assert_eq!(ip_key_bytes(v4), ip_key_bytes(mapped));
}
#[test]
fn ipv6_key_is_the_64_prefix() {
let a = IpAddr::V6("2001:db8:abcd:1::1".parse::<Ipv6Addr>().unwrap());
let b = IpAddr::V6(
"2001:db8:abcd:1:ffff:ffff:ffff:ffff"
.parse::<Ipv6Addr>()
.unwrap(),
);
let other = IpAddr::V6("2001:db8:abcd:2::1".parse::<Ipv6Addr>().unwrap());
assert_eq!(ip_key_bytes(a), ip_key_bytes(b));
assert_ne!(ip_key_bytes(a), ip_key_bytes(other));
}
#[test]
fn ipv6_64_prefix_shares_one_slot_across_the_limit() {
let limiter = Arc::new(PerIpConnLimit::new(1));
let a = ip("2001:db8:abcd:1::1");
let b = ip("2001:db8:abcd:1::2");
let _g = PerIpConnLimit::try_acquire(&limiter, a).expect("first address admitted");
assert!(
PerIpConnLimit::try_acquire(&limiter, b).is_none(),
"a same-/64 address must share the exhausted slot"
);
}
}