use std::collections::HashMap;
use std::net::{IpAddr, SocketAddr};
use crate::scope::{fold_ip, scope_of, Scope};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Direction {
Inbound,
Outbound,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Path {
Direct,
Relayed,
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy)]
pub struct SessionMeta {
pub direction: Direction,
pub path: Path,
pub remote: SocketAddr,
}
impl SessionMeta {
pub fn new(direction: Direction, path: Path, remote: SocketAddr) -> Self {
SessionMeta {
direction,
path,
remote,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Refusal {
Outbound,
Relayed,
Unusable,
RateLimited,
}
pub fn observe(meta: &SessionMeta) -> Result<SocketAddr, Refusal> {
if meta.direction != Direction::Inbound {
return Err(Refusal::Outbound);
}
if meta.path != Path::Direct {
return Err(Refusal::Relayed);
}
if scope_of(meta.remote) == Scope::NeverDialable {
return Err(Refusal::Unusable);
}
Ok(SocketAddr::new(
fold_ip(meta.remote.ip()),
meta.remote.port(),
))
}
pub const OBSERVE_PER_SESSION_PER_MINUTE: u32 = 6;
pub const OBSERVE_GLOBAL_PER_SECOND: u32 = 64;
pub const MAX_TRACKED_SOURCES: usize = 4096;
const SESSION_SOURCE_WINDOW_MS: u64 = 60_000;
const GLOBAL_WINDOW_MS: u64 = 1_000;
#[derive(Debug, Clone, Copy)]
struct TokenBucket {
tokens: u32,
window: u64,
last_seen_ms: u64,
}
impl TokenBucket {
fn new(capacity: u32, now_ms: u64, window_ms: u64) -> Self {
TokenBucket {
tokens: capacity,
window: now_ms / window_ms,
last_seen_ms: now_ms,
}
}
fn peek(&self, now_ms: u64, window_ms: u64) -> bool {
now_ms / window_ms != self.window || self.tokens > 0
}
fn spend(&mut self, capacity: u32, now_ms: u64, window_ms: u64) {
let window = now_ms / window_ms;
if window != self.window {
self.window = window;
self.tokens = capacity;
}
self.tokens = self.tokens.saturating_sub(1);
self.last_seen_ms = now_ms;
}
}
fn bucket_for<'m, K: std::hash::Hash + Eq + Clone>(
map: &'m mut HashMap<K, TokenBucket>,
key: &K,
capacity: u32,
now_ms: u64,
window_ms: u64,
) -> &'m mut TokenBucket {
if !map.contains_key(key) && map.len() >= MAX_TRACKED_SOURCES {
if let Some(victim) = map
.iter()
.min_by_key(|(_, b)| b.last_seen_ms)
.map(|(k, _)| k.clone())
{
map.remove(&victim);
}
}
map.entry(key.clone())
.or_insert_with(|| TokenBucket::new(capacity, now_ms, window_ms))
}
pub struct ObserveLimiter {
session_and_source_capacity: u32,
global_capacity: u32,
per_session: HashMap<String, TokenBucket>,
per_source: HashMap<IpAddr, TokenBucket>,
global: TokenBucket,
}
impl ObserveLimiter {
pub fn new(per_session_and_source_per_minute: u32, global_per_second: u32) -> Self {
ObserveLimiter {
session_and_source_capacity: per_session_and_source_per_minute,
global_capacity: global_per_second,
per_session: HashMap::new(),
per_source: HashMap::new(),
global: TokenBucket::new(global_per_second, 0, GLOBAL_WINDOW_MS),
}
}
pub fn allow(&mut self, session: &str, source: IpAddr, now_ms: u64) -> bool {
let source = fold_ip(source);
let session_key = session.to_string();
let capacity = self.session_and_source_capacity;
let session_bucket = bucket_for(
&mut self.per_session,
&session_key,
capacity,
now_ms,
SESSION_SOURCE_WINDOW_MS,
);
if !session_bucket.peek(now_ms, SESSION_SOURCE_WINDOW_MS) {
session_bucket.last_seen_ms = now_ms;
return false;
}
let source_bucket = bucket_for(
&mut self.per_source,
&source,
capacity,
now_ms,
SESSION_SOURCE_WINDOW_MS,
);
if !source_bucket.peek(now_ms, SESSION_SOURCE_WINDOW_MS) {
source_bucket.last_seen_ms = now_ms;
return false;
}
if !self.global.peek(now_ms, GLOBAL_WINDOW_MS) {
return false;
}
bucket_for(
&mut self.per_session,
&session_key,
capacity,
now_ms,
SESSION_SOURCE_WINDOW_MS,
)
.spend(capacity, now_ms, SESSION_SOURCE_WINDOW_MS);
bucket_for(
&mut self.per_source,
&source,
capacity,
now_ms,
SESSION_SOURCE_WINDOW_MS,
)
.spend(capacity, now_ms, SESSION_SOURCE_WINDOW_MS);
self.global
.spend(self.global_capacity, now_ms, GLOBAL_WINDOW_MS);
true
}
}
#[cfg(test)]
mod bounded_map_tests {
use super::*;
use std::net::Ipv4Addr;
#[test]
fn per_session_and_per_source_maps_stay_bounded_past_max_tracked_sources() {
let mut limiter =
ObserveLimiter::new(OBSERVE_PER_SESSION_PER_MINUTE, OBSERVE_GLOBAL_PER_SECOND);
for i in 0..(MAX_TRACKED_SOURCES as u64 + 5000) {
let src = IpAddr::V4(Ipv4Addr::new(
((i >> 24) & 0xff) as u8,
((i >> 16) & 0xff) as u8,
((i >> 8) & 0xff) as u8,
(i & 0xff) as u8,
));
limiter.allow(&format!("peer-{i}"), src, i);
}
assert!(
limiter.per_session.len() <= MAX_TRACKED_SOURCES,
"per-session map must stay bounded"
);
assert!(
limiter.per_source.len() <= MAX_TRACKED_SOURCES,
"per-source map must stay bounded"
);
}
}