Skip to main content

fips_core/upper/
icmp_rate_limit.rs

1//! ICMP Packet Too Big rate limiting.
2//!
3//! Prevents ICMP flood from repeated oversized packets by rate-limiting
4//! ICMP Packet Too Big messages per source address.
5
6use std::collections::HashMap;
7use std::net::Ipv6Addr;
8use std::time::{Duration, Instant};
9
10/// Rate limiter for ICMP Packet Too Big messages.
11///
12/// Tracks the last time an ICMP PTB was sent to each source address
13/// and enforces a minimum interval between messages to prevent floods.
14pub struct IcmpRateLimiter {
15    /// Maps source IPv6 address to the last time we sent ICMP PTB to it.
16    last_sent: HashMap<Ipv6Addr, Instant>,
17    /// Minimum interval between ICMP messages to the same source.
18    min_interval: Duration,
19    /// Maximum age of entries before cleanup (prevents unbounded growth).
20    max_age: Duration,
21}
22
23impl IcmpRateLimiter {
24    /// Create a new rate limiter.
25    ///
26    /// Default: max 10 ICMP/sec per source (100ms interval).
27    pub fn new() -> Self {
28        Self {
29            last_sent: HashMap::new(),
30            min_interval: Duration::from_millis(100),
31            max_age: Duration::from_secs(10),
32        }
33    }
34
35    /// Create a rate limiter with custom interval.
36    pub fn with_interval(min_interval: Duration) -> Self {
37        Self {
38            last_sent: HashMap::new(),
39            min_interval,
40            max_age: Duration::from_secs(10),
41        }
42    }
43
44    /// Check if we should send an ICMP PTB to this source address.
45    ///
46    /// Returns true if enough time has passed since the last ICMP to this source,
47    /// or if this is the first ICMP to this source.
48    ///
49    /// If true is returned, the internal state is updated to record this send.
50    pub fn should_send(&mut self, src_addr: Ipv6Addr) -> bool {
51        let now = Instant::now();
52
53        // Check if we've sent to this source recently
54        if let Some(&last) = self.last_sent.get(&src_addr)
55            && now.duration_since(last) < self.min_interval
56        {
57            return false; // Too soon, rate limit
58        }
59
60        // Update last sent time
61        self.last_sent.insert(src_addr, now);
62
63        // Cleanup old entries to prevent unbounded growth
64        self.cleanup(now);
65
66        true
67    }
68
69    /// Remove entries older than max_age.
70    fn cleanup(&mut self, now: Instant) {
71        self.last_sent
72            .retain(|_, &mut last| now.duration_since(last) < self.max_age);
73    }
74
75    /// Get the number of tracked sources.
76    #[cfg(test)]
77    pub fn len(&self) -> usize {
78        self.last_sent.len()
79    }
80
81    /// Check if there are no tracked sources.
82    #[cfg(test)]
83    pub fn is_empty(&self) -> bool {
84        self.last_sent.is_empty()
85    }
86}
87
88impl Default for IcmpRateLimiter {
89    fn default() -> Self {
90        Self::new()
91    }
92}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97    use std::thread;
98
99    #[test]
100    fn test_first_send_allowed() {
101        let mut limiter = IcmpRateLimiter::new();
102        let addr: Ipv6Addr = "fd00::1".parse().unwrap();
103
104        assert!(limiter.should_send(addr));
105    }
106
107    #[test]
108    fn test_rapid_sends_rate_limited() {
109        let mut limiter = IcmpRateLimiter::new();
110        let addr: Ipv6Addr = "fd00::1".parse().unwrap();
111
112        // First send should succeed
113        assert!(limiter.should_send(addr));
114
115        // Immediate second send should be rate limited
116        assert!(!limiter.should_send(addr));
117        assert!(!limiter.should_send(addr));
118    }
119
120    #[test]
121    fn test_different_sources_independent() {
122        let mut limiter = IcmpRateLimiter::new();
123        let addr1: Ipv6Addr = "fd00::1".parse().unwrap();
124        let addr2: Ipv6Addr = "fd00::2".parse().unwrap();
125
126        // Both sources should be allowed independently
127        assert!(limiter.should_send(addr1));
128        assert!(limiter.should_send(addr2));
129
130        // But rapid resends to same source are limited
131        assert!(!limiter.should_send(addr1));
132        assert!(!limiter.should_send(addr2));
133    }
134
135    #[test]
136    fn test_send_allowed_after_interval() {
137        let mut limiter = IcmpRateLimiter::with_interval(Duration::from_millis(50));
138        let addr: Ipv6Addr = "fd00::1".parse().unwrap();
139
140        // First send
141        assert!(limiter.should_send(addr));
142
143        // Wait for interval to pass
144        thread::sleep(Duration::from_millis(60));
145
146        // Second send should now be allowed
147        assert!(limiter.should_send(addr));
148    }
149
150    #[test]
151    fn test_cleanup_removes_old_entries() {
152        let mut limiter = IcmpRateLimiter::new();
153        let addr1: Ipv6Addr = "fd00::1".parse().unwrap();
154        let addr2: Ipv6Addr = "fd00::2".parse().unwrap();
155
156        // Send to both addresses
157        assert!(limiter.should_send(addr1));
158        assert!(limiter.should_send(addr2));
159        assert_eq!(limiter.len(), 2);
160
161        // Manually trigger cleanup with a future timestamp
162        let future = Instant::now() + Duration::from_secs(11);
163        limiter.cleanup(future);
164
165        // All entries should be cleaned up
166        assert_eq!(limiter.len(), 0);
167    }
168
169    #[test]
170    fn test_cleanup_preserves_recent_entries() {
171        let mut limiter = IcmpRateLimiter::new();
172        let addr: Ipv6Addr = "fd00::1".parse().unwrap();
173
174        assert!(limiter.should_send(addr));
175        assert_eq!(limiter.len(), 1);
176
177        // Cleanup with current time shouldn't remove recent entry
178        limiter.cleanup(Instant::now());
179        assert_eq!(limiter.len(), 1);
180    }
181}