Skip to main content

shell_tunnel/security/
rate_limit.rs

1//! Rate limiting implementation.
2
3use std::collections::HashMap;
4use std::net::IpAddr;
5use std::sync::RwLock;
6use std::time::{Duration, Instant};
7
8use axum::{
9    extract::{ConnectInfo, Request, State},
10    http::StatusCode,
11    middleware::Next,
12    response::{IntoResponse, Response},
13};
14
15/// Rate limiter configuration.
16#[derive(Debug, Clone)]
17pub struct RateLimitConfig {
18    /// Maximum requests per window.
19    pub max_requests: u32,
20    /// Time window duration.
21    pub window: Duration,
22    /// Whether rate limiting is enabled.
23    pub enabled: bool,
24    /// Maximum number of tracked IPs (memory limit).
25    pub max_tracked_ips: usize,
26}
27
28impl Default for RateLimitConfig {
29    fn default() -> Self {
30        Self {
31            max_requests: 100,
32            window: Duration::from_secs(60),
33            enabled: true,
34            max_tracked_ips: 10000,
35        }
36    }
37}
38
39impl RateLimitConfig {
40    /// Create a disabled rate limiter config.
41    pub fn disabled() -> Self {
42        Self {
43            enabled: false,
44            ..Default::default()
45        }
46    }
47
48    /// Create a strict rate limiter (10 req/min).
49    pub fn strict() -> Self {
50        Self {
51            max_requests: 10,
52            window: Duration::from_secs(60),
53            ..Default::default()
54        }
55    }
56
57    /// Create a relaxed rate limiter (1000 req/min).
58    pub fn relaxed() -> Self {
59        Self {
60            max_requests: 1000,
61            window: Duration::from_secs(60),
62            ..Default::default()
63        }
64    }
65
66    /// Custom rate limit.
67    pub fn custom(max_requests: u32, window_secs: u64) -> Self {
68        Self {
69            max_requests,
70            window: Duration::from_secs(window_secs),
71            ..Default::default()
72        }
73    }
74}
75
76/// Request record for an IP.
77#[derive(Debug, Clone)]
78struct RequestRecord {
79    /// Timestamps of requests in the current window.
80    timestamps: Vec<Instant>,
81}
82
83impl RequestRecord {
84    fn new() -> Self {
85        Self {
86            timestamps: Vec::new(),
87        }
88    }
89
90    /// Clean up old timestamps and return current count.
91    fn clean_and_count(&mut self, window: Duration) -> u32 {
92        let now = Instant::now();
93        let cutoff = now - window;
94
95        // Remove timestamps older than the window
96        self.timestamps.retain(|&t| t > cutoff);
97
98        self.timestamps.len() as u32
99    }
100
101    /// Record a new request.
102    fn record(&mut self) {
103        self.timestamps.push(Instant::now());
104    }
105}
106
107/// Thread-safe rate limiter.
108#[derive(Debug)]
109pub struct RateLimiter {
110    records: RwLock<HashMap<IpAddr, RequestRecord>>,
111    config: RateLimitConfig,
112    last_cleanup: RwLock<Instant>,
113}
114
115impl RateLimiter {
116    /// Create a new rate limiter.
117    pub fn new(config: RateLimitConfig) -> Self {
118        Self {
119            records: RwLock::new(HashMap::new()),
120            config,
121            last_cleanup: RwLock::new(Instant::now()),
122        }
123    }
124
125    /// Create a disabled rate limiter.
126    pub fn disabled() -> Self {
127        Self::new(RateLimitConfig::disabled())
128    }
129
130    /// Check if rate limiting is enabled.
131    pub fn is_enabled(&self) -> bool {
132        self.config.enabled
133    }
134
135    /// Check if a request from the given IP should be allowed.
136    ///
137    /// Returns `Ok(remaining)` if allowed, `Err(retry_after)` if rate limited.
138    pub fn check(&self, ip: IpAddr) -> Result<u32, Duration> {
139        if !self.config.enabled {
140            return Ok(self.config.max_requests);
141        }
142
143        // Periodic cleanup
144        self.maybe_cleanup();
145
146        let mut records = match self.records.write() {
147            Ok(r) => r,
148            Err(_) => return Ok(self.config.max_requests), // Fail open on lock error
149        };
150
151        let record = records.entry(ip).or_insert_with(RequestRecord::new);
152        let current_count = record.clean_and_count(self.config.window);
153
154        if current_count >= self.config.max_requests {
155            // Calculate retry-after
156            let oldest = record.timestamps.first().copied();
157            let retry_after = oldest
158                .map(|t| self.config.window.saturating_sub(t.elapsed()))
159                .unwrap_or(self.config.window);
160            return Err(retry_after);
161        }
162
163        // Record this request
164        record.record();
165        let remaining = self.config.max_requests - current_count - 1;
166
167        Ok(remaining)
168    }
169
170    /// Perform cleanup of old records if needed.
171    fn maybe_cleanup(&self) {
172        let should_cleanup = self
173            .last_cleanup
174            .read()
175            .map(|t| t.elapsed() > self.config.window * 2)
176            .unwrap_or(false);
177
178        if !should_cleanup {
179            return;
180        }
181
182        // Try to acquire write lock for cleanup
183        if let Ok(mut last) = self.last_cleanup.write() {
184            // Double-check after acquiring lock
185            if last.elapsed() <= self.config.window * 2 {
186                return;
187            }
188
189            *last = Instant::now();
190
191            if let Ok(mut records) = self.records.write() {
192                let cutoff = Instant::now() - self.config.window * 2;
193
194                // Remove IPs with no recent activity
195                records.retain(|_, record| {
196                    record
197                        .timestamps
198                        .last()
199                        .map(|&t| t > cutoff)
200                        .unwrap_or(false)
201                });
202
203                // If still too many, remove oldest entries
204                if records.len() > self.config.max_tracked_ips {
205                    let mut entries: Vec<_> = records
206                        .iter()
207                        .map(|(ip, r)| (*ip, r.timestamps.last().copied()))
208                        .collect();
209
210                    entries.sort_by_key(|(_, t)| *t);
211
212                    let to_remove = records.len() - self.config.max_tracked_ips;
213                    for (ip, _) in entries.into_iter().take(to_remove) {
214                        records.remove(&ip);
215                    }
216                }
217            }
218        }
219    }
220
221    /// Get current stats.
222    pub fn stats(&self) -> RateLimitStats {
223        let tracked_ips = self.records.read().map(|r| r.len()).unwrap_or(0);
224        RateLimitStats {
225            tracked_ips,
226            max_requests: self.config.max_requests,
227            window_secs: self.config.window.as_secs(),
228            enabled: self.config.enabled,
229        }
230    }
231}
232
233impl Default for RateLimiter {
234    fn default() -> Self {
235        Self::new(RateLimitConfig::default())
236    }
237}
238
239/// Rate limit statistics.
240#[derive(Debug, Clone)]
241pub struct RateLimitStats {
242    pub tracked_ips: usize,
243    pub max_requests: u32,
244    pub window_secs: u64,
245    pub enabled: bool,
246}
247
248/// Rate limit middleware for axum.
249pub async fn rate_limit_middleware(
250    State(limiter): State<std::sync::Arc<RateLimiter>>,
251    ConnectInfo(addr): ConnectInfo<std::net::SocketAddr>,
252    request: Request,
253    next: Next,
254) -> Response {
255    // Skip rate limiting for health endpoint
256    if request.uri().path() == "/health" {
257        return next.run(request).await;
258    }
259
260    match limiter.check(addr.ip()) {
261        Ok(remaining) => {
262            let mut response = next.run(request).await;
263
264            // Add rate limit headers
265            let headers = response.headers_mut();
266            headers.insert(
267                "X-RateLimit-Limit",
268                limiter.config.max_requests.to_string().parse().unwrap(),
269            );
270            headers.insert(
271                "X-RateLimit-Remaining",
272                remaining.to_string().parse().unwrap(),
273            );
274
275            response
276        }
277        Err(retry_after) => {
278            let mut response = (
279                StatusCode::TOO_MANY_REQUESTS,
280                "Rate limit exceeded. Please try again later.",
281            )
282                .into_response();
283
284            response.headers_mut().insert(
285                "Retry-After",
286                retry_after.as_secs().to_string().parse().unwrap(),
287            );
288            response.headers_mut().insert(
289                "X-RateLimit-Limit",
290                limiter.config.max_requests.to_string().parse().unwrap(),
291            );
292            response
293                .headers_mut()
294                .insert("X-RateLimit-Remaining", "0".parse().unwrap());
295
296            response
297        }
298    }
299}
300
301#[cfg(test)]
302mod tests {
303    use super::*;
304    use std::net::{Ipv4Addr, Ipv6Addr};
305
306    #[test]
307    fn test_rate_limit_config_default() {
308        let config = RateLimitConfig::default();
309        assert_eq!(config.max_requests, 100);
310        assert_eq!(config.window, Duration::from_secs(60));
311        assert!(config.enabled);
312    }
313
314    #[test]
315    fn test_rate_limit_config_disabled() {
316        let config = RateLimitConfig::disabled();
317        assert!(!config.enabled);
318    }
319
320    #[test]
321    fn test_rate_limit_config_custom() {
322        let config = RateLimitConfig::custom(50, 30);
323        assert_eq!(config.max_requests, 50);
324        assert_eq!(config.window, Duration::from_secs(30));
325    }
326
327    #[test]
328    fn test_rate_limiter_allows_requests() {
329        let limiter = RateLimiter::new(RateLimitConfig::custom(5, 60));
330        let ip = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1));
331
332        // First 5 requests should be allowed
333        for i in 0..5 {
334            let result = limiter.check(ip);
335            assert!(result.is_ok(), "Request {} should be allowed", i);
336        }
337    }
338
339    #[test]
340    fn test_rate_limiter_blocks_excess() {
341        let limiter = RateLimiter::new(RateLimitConfig::custom(3, 60));
342        let ip = IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1));
343
344        // First 3 requests allowed
345        assert!(limiter.check(ip).is_ok());
346        assert!(limiter.check(ip).is_ok());
347        assert!(limiter.check(ip).is_ok());
348
349        // 4th request should be blocked
350        let result = limiter.check(ip);
351        assert!(result.is_err());
352    }
353
354    #[test]
355    fn test_rate_limiter_different_ips() {
356        let limiter = RateLimiter::new(RateLimitConfig::custom(2, 60));
357        let ip1 = IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1));
358        let ip2 = IpAddr::V4(Ipv4Addr::new(10, 0, 0, 2));
359
360        // Each IP gets its own quota
361        assert!(limiter.check(ip1).is_ok());
362        assert!(limiter.check(ip1).is_ok());
363        assert!(limiter.check(ip1).is_err()); // ip1 blocked
364
365        assert!(limiter.check(ip2).is_ok()); // ip2 still allowed
366        assert!(limiter.check(ip2).is_ok());
367        assert!(limiter.check(ip2).is_err()); // ip2 now blocked
368    }
369
370    #[test]
371    fn test_rate_limiter_disabled() {
372        let limiter = RateLimiter::disabled();
373        let ip = IpAddr::V4(Ipv4Addr::new(1, 1, 1, 1));
374
375        // Should always allow when disabled
376        for _ in 0..100 {
377            assert!(limiter.check(ip).is_ok());
378        }
379    }
380
381    #[test]
382    fn test_rate_limiter_ipv6() {
383        let limiter = RateLimiter::new(RateLimitConfig::custom(2, 60));
384        let ip = IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1));
385
386        assert!(limiter.check(ip).is_ok());
387        assert!(limiter.check(ip).is_ok());
388        assert!(limiter.check(ip).is_err());
389    }
390
391    #[test]
392    fn test_rate_limiter_stats() {
393        let limiter = RateLimiter::new(RateLimitConfig::custom(10, 30));
394        let ip = IpAddr::V4(Ipv4Addr::new(8, 8, 8, 8));
395
396        limiter.check(ip).ok();
397
398        let stats = limiter.stats();
399        assert_eq!(stats.tracked_ips, 1);
400        assert_eq!(stats.max_requests, 10);
401        assert_eq!(stats.window_secs, 30);
402        assert!(stats.enabled);
403    }
404
405    #[test]
406    fn test_rate_limiter_remaining_count() {
407        let limiter = RateLimiter::new(RateLimitConfig::custom(5, 60));
408        let ip = IpAddr::V4(Ipv4Addr::new(172, 16, 0, 1));
409
410        assert_eq!(limiter.check(ip).unwrap(), 4); // 5-1 = 4 remaining
411        assert_eq!(limiter.check(ip).unwrap(), 3);
412        assert_eq!(limiter.check(ip).unwrap(), 2);
413        assert_eq!(limiter.check(ip).unwrap(), 1);
414        assert_eq!(limiter.check(ip).unwrap(), 0);
415        assert!(limiter.check(ip).is_err()); // Now blocked
416    }
417}