use std::collections::HashMap;
use std::net::IpAddr;
use std::sync::Mutex;
use crate::error::AppError;
pub const MAX_PER_WINDOW: u64 = 30;
pub const WINDOW_SECS: u64 = 60;
pub const MAX_TRACKED_IPS: usize = 10_000;
#[derive(Debug, Clone, Copy)]
struct Bucket {
count: u64,
window_start: u64,
}
#[derive(Debug, Default)]
pub struct IpRateLimiter {
buckets: Mutex<HashMap<IpAddr, Bucket>>,
}
impl IpRateLimiter {
pub fn new() -> Self {
Self::default()
}
pub fn try_consume(&self, ip: IpAddr, now: u64) -> Result<(), AppError> {
let mut buckets = self
.buckets
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
if buckets.len() >= MAX_TRACKED_IPS {
buckets.clear();
}
let entry = buckets.entry(ip).or_insert(Bucket {
count: 0,
window_start: now,
});
if now.saturating_sub(entry.window_start) >= WINDOW_SECS {
entry.count = 0;
entry.window_start = now;
}
if entry.count >= MAX_PER_WINDOW {
return Err(AppError::Validation(format!(
"IP rate limit exceeded ({MAX_PER_WINDOW} requests per {WINDOW_SECS}s); try again later",
)));
}
entry.count += 1;
Ok(())
}
#[cfg(test)]
pub fn count(&self, ip: IpAddr) -> u64 {
let buckets = self.buckets.lock().unwrap();
buckets.get(&ip).map(|b| b.count).unwrap_or(0)
}
}
pub fn resolve_client_ip(peer: IpAddr, xff: Option<&str>, trusted_proxies: &[String]) -> IpAddr {
if trusted_proxies.is_empty() {
return peer;
}
let trusted: Vec<IpAddr> = trusted_proxies
.iter()
.filter_map(|s| s.parse::<IpAddr>().ok())
.collect();
if !trusted.contains(&peer) {
return peer;
}
let Some(xff) = xff else {
return peer;
};
let parsed: Vec<IpAddr> = xff
.split(',')
.filter_map(|s| s.trim().parse::<IpAddr>().ok())
.collect();
if parsed.is_empty() {
return peer;
}
for candidate in parsed.iter().rev() {
if !trusted.contains(candidate) {
return *candidate;
}
}
parsed[0]
}
#[cfg(test)]
mod tests {
use super::*;
use std::net::Ipv4Addr;
fn ip(s: &str) -> IpAddr {
s.parse().unwrap()
}
#[test]
fn allows_under_cap() {
let l = IpRateLimiter::new();
let p = IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4));
for _ in 0..MAX_PER_WINDOW {
l.try_consume(p, 1000).unwrap();
}
assert_eq!(l.count(p), MAX_PER_WINDOW);
}
#[test]
fn rejects_over_cap() {
let l = IpRateLimiter::new();
let p = IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4));
for _ in 0..MAX_PER_WINDOW {
l.try_consume(p, 1000).unwrap();
}
let err = l.try_consume(p, 1000).unwrap_err();
assert!(matches!(err, AppError::Validation(ref m) if m.contains("rate limit")));
}
#[test]
fn window_resets_after_expiry() {
let l = IpRateLimiter::new();
let p = IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4));
for _ in 0..MAX_PER_WINDOW {
l.try_consume(p, 1000).unwrap();
}
assert!(l.try_consume(p, 1000).is_err());
l.try_consume(p, 1000 + WINDOW_SECS).unwrap();
assert_eq!(l.count(p), 1);
}
#[test]
fn distinct_ips_independent() {
let l = IpRateLimiter::new();
let a = IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4));
let b = IpAddr::V4(Ipv4Addr::new(5, 6, 7, 8));
for _ in 0..MAX_PER_WINDOW {
l.try_consume(a, 1000).unwrap();
}
assert!(l.try_consume(a, 1000).is_err());
l.try_consume(b, 1000).unwrap();
}
#[test]
fn resolve_no_trusted_proxies_returns_peer() {
let peer = ip("203.0.113.1");
assert_eq!(resolve_client_ip(peer, Some("198.51.100.1"), &[]), peer);
}
#[test]
fn resolve_peer_not_in_trusted_returns_peer() {
let peer = ip("203.0.113.1");
let trusted = vec!["10.0.0.1".to_string()];
assert_eq!(
resolve_client_ip(peer, Some("198.51.100.1"), &trusted),
peer
);
}
#[test]
fn resolve_trusted_peer_picks_xff_client() {
let peer = ip("10.0.0.1");
let trusted = vec!["10.0.0.1".to_string()];
assert_eq!(
resolve_client_ip(peer, Some("198.51.100.1"), &trusted),
ip("198.51.100.1")
);
}
#[test]
fn resolve_walks_past_trusted_chain() {
let peer = ip("10.0.0.2");
let trusted = vec!["10.0.0.1".to_string(), "10.0.0.2".to_string()];
assert_eq!(
resolve_client_ip(peer, Some("198.51.100.1, 10.0.0.1, 10.0.0.2"), &trusted,),
ip("198.51.100.1")
);
}
#[test]
fn resolve_attacker_spoof_ignored() {
let peer = ip("203.0.113.99"); let trusted = vec!["10.0.0.1".to_string()];
assert_eq!(
resolve_client_ip(peer, Some("198.51.100.5, 10.0.0.1"), &trusted,),
peer
);
}
#[test]
fn resolve_malformed_xff_falls_back_to_peer() {
let peer = ip("10.0.0.1");
let trusted = vec!["10.0.0.1".to_string()];
assert_eq!(resolve_client_ip(peer, Some("not-an-ip"), &trusted), peer);
}
#[test]
fn resolve_no_xff_returns_peer() {
let peer = ip("10.0.0.1");
let trusted = vec!["10.0.0.1".to_string()];
assert_eq!(resolve_client_ip(peer, None, &trusted), peer);
}
}