diff options
| author | murilo ijanc | 2026-03-24 15:04:03 -0300 |
|---|---|---|
| committer | murilo ijanc | 2026-03-24 15:04:03 -0300 |
| commit | 9821aabf0b50d2487b07502d3d2cd89e7d62bdbe (patch) | |
| tree | 53da095ff90cc755bac3d4bf699172b5e8cd07d6 /src/ratelimit.rs | |
| download | tesseras-dht-e908bc01403f4b8ef2a65fa6be43716fd1c6e003.tar.gz | |
Initial commitv0.1.0
NAT-aware Kademlia DHT library for peer-to-peer networks.
Features:
- Distributed key-value storage (iterative FIND_NODE, FIND_VALUE, STORE)
- NAT traversal via DTUN hole-punching and proxy relay
- Reliable Datagram Protocol (RDP) with 7-state connection machine
- Datagram transport with automatic fragmentation/reassembly
- Ed25519 packet authentication
- 256-bit node IDs (Ed25519 public keys)
- Rate limiting, ban list, and eclipse attack mitigation
- Persistence and metrics
- OpenBSD and Linux support
Diffstat (limited to 'src/ratelimit.rs')
| -rw-r--r-- | src/ratelimit.rs | 136 |
1 files changed, 136 insertions, 0 deletions
diff --git a/src/ratelimit.rs b/src/ratelimit.rs new file mode 100644 index 0000000..691b30f --- /dev/null +++ b/src/ratelimit.rs @@ -0,0 +1,136 @@ +//! Per-IP rate limiting with token bucket algorithm. +//! +//! Limits the number of inbound messages processed per +//! source IP address per second. Prevents a single +//! source from overwhelming the node. + +use std::collections::HashMap; +use std::net::IpAddr; +use std::time::Instant; + +/// Default rate: 50 messages per second per IP. +pub const DEFAULT_RATE: f64 = 50.0; + +/// Default burst: 100 messages. +pub const DEFAULT_BURST: u32 = 100; + +/// Stale bucket cleanup threshold. +const STALE_SECS: u64 = 60; + +struct Bucket { + tokens: f64, + last_refill: Instant, +} + +/// Per-IP token bucket rate limiter. +pub struct RateLimiter { + buckets: HashMap<IpAddr, Bucket>, + rate: f64, + burst: u32, + last_cleanup: Instant, +} + +impl RateLimiter { + /// Create a new rate limiter. + /// + /// - `rate`: tokens added per second per IP. + /// - `burst`: maximum tokens (burst capacity). + pub fn new(rate: f64, burst: u32) -> Self { + Self { + buckets: HashMap::new(), + rate, + burst, + last_cleanup: Instant::now(), + } + } + + /// Check if a message from `ip` should be allowed. + /// + /// Returns `true` if allowed (token consumed), + /// `false` if rate-limited (drop the message). + pub fn allow(&mut self, ip: IpAddr) -> bool { + let now = Instant::now(); + let burst = self.burst as f64; + let rate = self.rate; + + let bucket = self.buckets.entry(ip).or_insert(Bucket { + tokens: burst, + last_refill: now, + }); + + // Refill tokens based on elapsed time + let elapsed = now.duration_since(bucket.last_refill).as_secs_f64(); + bucket.tokens = (bucket.tokens + elapsed * rate).min(burst); + bucket.last_refill = now; + + if bucket.tokens >= 1.0 { + bucket.tokens -= 1.0; + true + } else { + false + } + } + + /// Remove stale buckets (no activity for 60s). + pub fn cleanup(&mut self) { + if self.last_cleanup.elapsed().as_secs() < STALE_SECS { + return; + } + self.last_cleanup = Instant::now(); + let cutoff = Instant::now(); + self.buckets.retain(|_, b| { + cutoff.duration_since(b.last_refill).as_secs() < STALE_SECS + }); + } + + /// Number of tracked IPs. + pub fn tracked_count(&self) -> usize { + self.buckets.len() + } +} + +impl Default for RateLimiter { + fn default() -> Self { + Self::new(DEFAULT_RATE, DEFAULT_BURST) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn allow_within_burst() { + let mut rl = RateLimiter::new(10.0, 5); + let ip: IpAddr = "1.2.3.4".parse().unwrap(); + + // First 5 should be allowed (burst) + for _ in 0..5 { + assert!(rl.allow(ip)); + } + + // 6th should be denied + assert!(!rl.allow(ip)); + } + + #[test] + fn different_ips_independent() { + let mut rl = RateLimiter::new(1.0, 1); + let ip1: IpAddr = "1.2.3.4".parse().unwrap(); + let ip2: IpAddr = "5.6.7.8".parse().unwrap(); + assert!(rl.allow(ip1)); + assert!(rl.allow(ip2)); + + // Both exhausted + assert!(!rl.allow(ip1)); + assert!(!rl.allow(ip2)); + } + + #[test] + fn tracked_count() { + let mut rl = RateLimiter::default(); + let ip: IpAddr = "1.2.3.4".parse().unwrap(); + rl.allow(ip); + assert_eq!(rl.tracked_count(), 1); + } +} |