use std::collections::hash_map::RandomState;
use std::hash::{BuildHasher, Hasher};
use std::net::IpAddr;
use std::time::{SystemTime, UNIX_EPOCH};
use parking_lot::Mutex;
use plecto_host::{Bucket, apply_bucket};
use crate::manifest::{RateLimitKeyKind, RouteRateLimit};
const IP_SLOTS: usize = 1 << 16;
fn now_millis() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_millis() as u64)
.unwrap_or(0)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RateLimitDecision {
Allow,
Limit { retry_after_ms: u64 },
}
pub(crate) struct NativeRateLimit {
spec: Bucket,
state: LimiterState,
}
impl std::fmt::Debug for NativeRateLimit {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let key = match self.state {
LimiterState::Route(_) => "route",
LimiterState::ClientIp { .. } => "client-ip",
};
f.debug_struct("NativeRateLimit")
.field("capacity", &self.spec.capacity)
.field("refill_tokens", &self.spec.refill_tokens)
.field("key", &key)
.finish()
}
}
enum LimiterState {
Route(Mutex<(u64, u64)>),
ClientIp {
slots: Box<[Mutex<(u64, u64)>]>,
hasher: RandomState,
},
}
impl NativeRateLimit {
pub(crate) fn new(cfg: RouteRateLimit) -> Self {
let spec = Bucket {
capacity: cfg.burst,
refill_tokens: cfg.rate,
refill_interval_ms: 1000,
};
let state = match cfg.key {
RateLimitKeyKind::Route => LimiterState::Route(Mutex::new((0, 0))),
RateLimitKeyKind::ClientIp => LimiterState::ClientIp {
slots: (0..IP_SLOTS).map(|_| Mutex::new((0, 0))).collect(),
hasher: RandomState::new(),
},
};
Self { spec, state }
}
pub(crate) fn check(&self, peer: IpAddr) -> RateLimitDecision {
self.check_at(peer, now_millis())
}
#[allow(clippy::indexing_slicing)]
fn check_at(&self, peer: IpAddr, now_ms: u64) -> RateLimitDecision {
let cell = match &self.state {
LimiterState::Route(cell) => cell,
LimiterState::ClientIp { slots, hasher } => {
let slot = slot_for_ip(peer, hasher, slots.len());
debug_assert!(slot < slots.len());
&slots[slot]
}
};
let mut guard = cell.lock();
let (next, acq) = apply_bucket(Some(*guard), 1, self.spec, now_ms);
*guard = next;
if acq.allowed {
RateLimitDecision::Allow
} else {
RateLimitDecision::Limit {
retry_after_ms: acq.retry_after_ms,
}
}
}
}
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, len: usize) -> usize {
let mut h = hasher.build_hasher();
h.write(&ip_key_bytes(peer));
(h.finish() as usize) & (len - 1)
}
#[cfg(test)]
mod tests {
use super::*;
use std::net::{Ipv4Addr, Ipv6Addr};
fn ip(s: &str) -> IpAddr {
s.parse().unwrap()
}
fn route_limit(rate: u64, burst: u64, key: RateLimitKeyKind) -> NativeRateLimit {
NativeRateLimit::new(RouteRateLimit { rate, burst, key })
}
#[test]
fn route_bucket_starts_full_drains_then_refills() {
let rl = route_limit(2, 3, RateLimitKeyKind::Route);
let peer = ip("203.0.113.7");
let t0 = 1_000_000;
for _ in 0..3 {
assert_eq!(rl.check_at(peer, t0), RateLimitDecision::Allow);
}
match rl.check_at(peer, t0) {
RateLimitDecision::Limit { retry_after_ms } => {
assert_eq!(retry_after_ms, 1000);
}
RateLimitDecision::Allow => panic!("4th request over a burst of 3 must be limited"),
}
assert_eq!(rl.check_at(peer, t0 + 1000), RateLimitDecision::Allow);
assert_eq!(rl.check_at(peer, t0 + 1000), RateLimitDecision::Allow);
assert_eq!(
rl.check_at(peer, t0 + 1000),
RateLimitDecision::Limit {
retry_after_ms: 1000
},
"only `rate` (2) tokens refill per interval, capped well under the 4th"
);
}
#[test]
fn client_ip_drains_one_peer_independently() {
let rl = route_limit(1, 2, RateLimitKeyKind::ClientIp);
let a = ip("198.51.100.1");
let t0 = 5_000_000;
assert_eq!(rl.check_at(a, t0), RateLimitDecision::Allow);
assert_eq!(rl.check_at(a, t0), RateLimitDecision::Allow);
assert!(matches!(
rl.check_at(a, t0),
RateLimitDecision::Limit { .. }
));
}
#[test]
fn client_ip_isolates_distinct_peers() {
let rl = route_limit(1, 1, RateLimitKeyKind::ClientIp);
let t0 = 7_000_000;
let drained = ip("192.0.2.255");
assert_eq!(rl.check_at(drained, t0), RateLimitDecision::Allow);
assert!(
matches!(rl.check_at(drained, t0), RateLimitDecision::Limit { .. }),
"the drained peer is now limited"
);
let allowed = (0..=255u8)
.map(|n| IpAddr::V4(Ipv4Addr::new(192, 0, 2, n)))
.filter(|p| *p != drained)
.filter(|p| rl.check_at(*p, t0) == RateLimitDecision::Allow)
.count();
assert!(
allowed >= 250,
"distinct peers get independent buckets (got {allowed}/255 allowed)"
);
}
#[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));
}
}