Files
fips/src/node/rate_limit.rs
T
Johnathan Corgan b8a1f322c2 Module reorganization and clippy cleanup
Move single-consumer modules into node/:
- rate_limit.rs, wire.rs, dns.rs — exclusively used by node subsystem
- Reduces top-level lib.rs from 16 to 13 modules

Split large files into focused subdirectories:
- noise.rs (1475 lines) → noise/{mod, handshake, session, replay, tests}.rs
- tree.rs (1479 lines) → tree/{mod, coordinate, declaration, state, tests}.rs
- bloom.rs (849 lines) → bloom/{mod, filter, state, tests}.rs
- All public APIs re-exported from mod.rs, no external import changes

Remove unused rate_limit defaults:
- HANDSHAKE_TIMEOUT_SECS, MAX_PENDING_INBOUND constants
- Default constructor eliminated in favor of with_params() taking config values

Fix all clippy warnings across codebase:
- Remove .clone() on Copy types, collapse nested ifs, replace match-return-None
  with ?, remove/gate unused code, fix loop indexing, remove unnecessary casts
- Box large PeerSlot enum variants to reduce size disparity
- cargo clippy --all-targets now reports zero warnings
2026-02-15 15:07:42 +00:00

405 lines
11 KiB
Rust

//! Rate Limiting for FIPS Protocol
//!
//! Provides token bucket rate limiting for protecting against DoS attacks,
//! particularly on the Noise handshake path where msg1 processing involves
//! expensive cryptographic operations.
//!
//! ## Design
//!
//! - Token bucket algorithm with configurable burst and refill rate
//! - Global rate limit (not per-source, since UDP sources are spoofable)
//! - Applied before expensive DH operations in handshake processing
//!
//! ## Default Parameters
//!
//! - Burst capacity: 100 tokens (max concurrent handshakes)
//! - Refill rate: 10 tokens/second (sustained handshake rate)
//! - This allows handling burst traffic while limiting sustained attack impact
use std::time::Instant;
/// Default burst capacity (max tokens).
pub const DEFAULT_BURST_CAPACITY: u32 = 100;
/// Default refill rate (tokens per second).
pub const DEFAULT_REFILL_RATE: f64 = 10.0;
/// Token bucket rate limiter.
///
/// Uses a classic token bucket algorithm where tokens are consumed for each
/// operation and refilled at a constant rate. When tokens are exhausted,
/// operations are rate-limited until tokens refill.
#[derive(Debug, Clone)]
pub struct TokenBucket {
/// Maximum number of tokens (burst capacity).
capacity: u32,
/// Current number of available tokens (may be fractional during refill).
tokens: f64,
/// Tokens added per second.
refill_rate: f64,
/// Last time tokens were refilled.
last_refill: Instant,
}
impl TokenBucket {
/// Create a new token bucket with default parameters.
///
/// - Burst capacity: 100 tokens
/// - Refill rate: 10 tokens/second
pub fn new() -> Self {
Self::with_params(DEFAULT_BURST_CAPACITY, DEFAULT_REFILL_RATE)
}
/// Create a token bucket with custom parameters.
///
/// # Arguments
///
/// * `capacity` - Maximum number of tokens (burst capacity)
/// * `refill_rate` - Tokens added per second
pub fn with_params(capacity: u32, refill_rate: f64) -> Self {
Self {
capacity,
tokens: capacity as f64,
refill_rate,
last_refill: Instant::now(),
}
}
/// Try to consume one token.
///
/// Returns `true` if a token was available and consumed, `false` if
/// rate limited (no tokens available).
pub fn try_acquire(&mut self) -> bool {
self.try_acquire_n(1)
}
/// Try to consume n tokens.
///
/// Returns `true` if n tokens were available and consumed, `false` if
/// rate limited (insufficient tokens).
pub fn try_acquire_n(&mut self, n: u32) -> bool {
self.refill();
if self.tokens >= n as f64 {
self.tokens -= n as f64;
true
} else {
false
}
}
/// Check if tokens are available without consuming them.
#[cfg(test)]
pub fn available(&mut self) -> bool {
self.refill();
self.tokens >= 1.0
}
/// Get the current number of available tokens.
#[cfg(test)]
pub fn tokens(&mut self) -> f64 {
self.refill();
self.tokens
}
/// Get the capacity (max tokens).
#[cfg(test)]
pub fn capacity(&self) -> u32 {
self.capacity
}
/// Refill tokens based on elapsed time.
fn refill(&mut self) {
let now = Instant::now();
let elapsed = now.duration_since(self.last_refill);
let elapsed_secs = elapsed.as_secs_f64();
// Add tokens based on time elapsed
self.tokens += elapsed_secs * self.refill_rate;
// Cap at capacity
if self.tokens > self.capacity as f64 {
self.tokens = self.capacity as f64;
}
self.last_refill = now;
}
/// Reset to full capacity.
#[cfg(test)]
pub fn reset(&mut self) {
self.tokens = self.capacity as f64;
self.last_refill = Instant::now();
}
/// Time until the next token is available.
///
/// Returns `Duration::ZERO` if tokens are available, otherwise the
/// estimated time until one token will be available.
#[cfg(test)]
pub fn time_until_available(&mut self) -> std::time::Duration {
self.refill();
if self.tokens >= 1.0 {
std::time::Duration::ZERO
} else {
let needed = 1.0 - self.tokens;
let secs = needed / self.refill_rate;
std::time::Duration::from_secs_f64(secs)
}
}
}
impl Default for TokenBucket {
fn default() -> Self {
Self::new()
}
}
/// Rate limiter for handshake message 1 processing.
///
/// Combines token bucket rate limiting with connection counting to
/// protect against DoS attacks on the handshake path.
#[derive(Debug)]
pub struct HandshakeRateLimiter {
/// Token bucket for rate limiting.
bucket: TokenBucket,
/// Current count of pending inbound connections.
pending_count: usize,
/// Maximum pending inbound connections.
max_pending: usize,
}
impl HandshakeRateLimiter {
/// Create a handshake rate limiter with the given parameters.
pub fn with_params(bucket: TokenBucket, max_pending: usize) -> Self {
Self {
bucket,
pending_count: 0,
max_pending,
}
}
/// Check if a new handshake can be started.
///
/// Returns `true` if:
/// - Token bucket has available tokens (rate limit not exceeded)
/// - Pending connection count is below maximum
///
/// Does NOT consume a token - call `start_handshake` for that.
#[cfg(test)]
pub fn can_start_handshake(&mut self) -> bool {
self.bucket.available() && self.pending_count < self.max_pending
}
/// Start a new handshake, consuming a token and incrementing pending count.
///
/// Returns `true` if the handshake was allowed, `false` if rate limited.
pub fn start_handshake(&mut self) -> bool {
if self.pending_count >= self.max_pending {
return false;
}
if self.bucket.try_acquire() {
self.pending_count += 1;
true
} else {
false
}
}
/// Mark a handshake as complete (successful or failed).
///
/// Decrements the pending connection count.
pub fn complete_handshake(&mut self) {
if self.pending_count > 0 {
self.pending_count -= 1;
}
}
/// Get the current pending connection count.
#[cfg(test)]
pub fn pending_count(&self) -> usize {
self.pending_count
}
/// Get a reference to the token bucket.
#[cfg(test)]
pub fn bucket(&self) -> &TokenBucket {
&self.bucket
}
/// Reset the rate limiter.
#[cfg(test)]
pub fn reset(&mut self) {
self.bucket.reset();
self.pending_count = 0;
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::thread;
use std::time::Duration;
#[test]
fn test_token_bucket_basic() {
let mut bucket = TokenBucket::with_params(10, 1.0);
// Should have full capacity
assert_eq!(bucket.capacity(), 10);
assert!(bucket.tokens() >= 9.9); // Allow for timing
// Consume all tokens
for _ in 0..10 {
assert!(bucket.try_acquire());
}
// Should be empty
assert!(!bucket.try_acquire());
assert!(!bucket.available());
}
#[test]
fn test_token_bucket_refill() {
let mut bucket = TokenBucket::with_params(10, 100.0); // 100 tokens/sec
// Drain completely
for _ in 0..10 {
bucket.try_acquire();
}
assert!(!bucket.available());
// Wait for refill
thread::sleep(Duration::from_millis(50)); // Should refill ~5 tokens
// Should have tokens now
let tokens = bucket.tokens();
assert!((4.0..=6.0).contains(&tokens), "tokens: {}", tokens);
}
#[test]
fn test_token_bucket_try_acquire_n() {
let mut bucket = TokenBucket::with_params(10, 1.0);
// Acquire 5
assert!(bucket.try_acquire_n(5));
assert!(bucket.tokens() >= 4.9 && bucket.tokens() <= 5.1);
// Acquire 5 more
assert!(bucket.try_acquire_n(5));
// Can't acquire more
assert!(!bucket.try_acquire_n(1));
}
#[test]
fn test_token_bucket_reset() {
let mut bucket = TokenBucket::with_params(10, 1.0);
// Drain
for _ in 0..10 {
bucket.try_acquire();
}
// Reset
bucket.reset();
// Should be full again
assert!(bucket.tokens() >= 9.9);
}
#[test]
fn test_token_bucket_time_until_available() {
let mut bucket = TokenBucket::with_params(10, 10.0); // 10 tokens/sec
// When full, should be zero
assert_eq!(bucket.time_until_available(), Duration::ZERO);
// Drain completely
for _ in 0..10 {
bucket.try_acquire();
}
// Should need ~100ms for one token at 10/sec
let wait = bucket.time_until_available();
assert!(wait.as_millis() >= 90 && wait.as_millis() <= 110);
}
#[test]
fn test_handshake_rate_limiter_basic() {
let mut limiter = HandshakeRateLimiter::with_params(TokenBucket::new(), 100);
assert!(limiter.can_start_handshake());
assert_eq!(limiter.pending_count(), 0);
// Start a handshake
assert!(limiter.start_handshake());
assert_eq!(limiter.pending_count(), 1);
// Complete it
limiter.complete_handshake();
assert_eq!(limiter.pending_count(), 0);
}
#[test]
fn test_handshake_rate_limiter_max_pending() {
let bucket = TokenBucket::with_params(1000, 100.0);
let mut limiter = HandshakeRateLimiter::with_params(bucket, 3);
// Start 3 handshakes
assert!(limiter.start_handshake());
assert!(limiter.start_handshake());
assert!(limiter.start_handshake());
// Fourth should fail (max pending)
assert!(!limiter.can_start_handshake());
assert!(!limiter.start_handshake());
// Complete one
limiter.complete_handshake();
// Now should be able to start another
assert!(limiter.can_start_handshake());
assert!(limiter.start_handshake());
}
#[test]
fn test_handshake_rate_limiter_token_exhaustion() {
let bucket = TokenBucket::with_params(5, 0.0); // No refill
let mut limiter = HandshakeRateLimiter::with_params(bucket, 100);
// Start 5 handshakes (exhausts tokens)
for _ in 0..5 {
assert!(limiter.start_handshake());
}
// Complete them all
for _ in 0..5 {
limiter.complete_handshake();
}
// Tokens exhausted, even though pending is 0
assert!(!limiter.can_start_handshake());
assert!(!limiter.start_handshake());
}
#[test]
fn test_handshake_rate_limiter_reset() {
let mut limiter = HandshakeRateLimiter::with_params(TokenBucket::new(), 100);
// Start some handshakes
limiter.start_handshake();
limiter.start_handshake();
assert_eq!(limiter.pending_count(), 2);
// Reset
limiter.reset();
assert_eq!(limiter.pending_count(), 0);
assert!(limiter.bucket().tokens >= DEFAULT_BURST_CAPACITY as f64 - 0.1);
}
}