bindcar/
rate_limit.rs

1//! Rate limiting middleware for HTTP requests
2//!
3//! This module provides rate limiting functionality using the tower-governor crate,
4//! which implements the Generic Cell Rate Algorithm (GCRA). Rate limits are
5//! keyed on the real TCP peer IP ([`PeerIpKeyExtractor`]) rather than the
6//! spoofable `X-Forwarded-For` family of headers, so a client cannot evade the
7//! limit or exhaust another client's bucket by forging a forwarding header.
8
9use std::time::Duration;
10
11/// Smallest burst that can absorb one zone replay without tripping the limiter.
12///
13/// When a BIND9 Pod is replaced it comes back with an empty zone directory, and
14/// the operator immediately replays everything it should be serving: create the
15/// zone, freeze it, apply each record, thaw, notify, then poll status. That is
16/// comfortably more than a handful of calls, and it arrives as one burst per
17/// zone. A burst below this guarantees HTTP 429 on every Pod restart — which the
18/// operator answers with exponential backoff, turning a ~30s recovery into ~130s.
19pub const MIN_BURST_FOR_ZONE_REPLAY: u32 = 50;
20
21// Re-export commonly used types for convenience. These are the only part of
22// this module that touches the HTTP stack — `RateLimitConfig` below is pure
23// configuration and stays available to a library-only consumer.
24#[cfg(feature = "server")]
25pub use tower_governor::{
26    governor::GovernorConfigBuilder, key_extractor::PeerIpKeyExtractor, GovernorLayer,
27};
28
29/// Rate limiting configuration
30#[derive(Debug, Clone)]
31pub struct RateLimitConfig {
32    /// Maximum requests per period
33    pub requests_per_period: u32,
34    /// Period duration in seconds
35    pub period_secs: u64,
36    /// Burst size (max requests at once)
37    pub burst_size: u32,
38    /// Whether rate limiting is enabled
39    pub enabled: bool,
40}
41
42impl Default for RateLimitConfig {
43    fn default() -> Self {
44        Self {
45            requests_per_period: 600,
46            period_secs: 60,
47            // Sized for the operator's zone-replay burst, not for a browser.
48            // bindcar's authenticated API has exactly one legitimate caller —
49            // the operator's ServiceAccount, enforced by TokenReview — so a
50            // burst this size is not an abuse surface, while a small one breaks
51            // recovery.
52            burst_size: MIN_BURST_FOR_ZONE_REPLAY,
53            enabled: true,
54        }
55    }
56}
57
58impl RateLimitConfig {
59    /// Create configuration from environment variables
60    ///
61    /// Environment variables:
62    /// - `RATE_LIMIT_ENABLED`: Enable/disable rate limiting (default: true)
63    /// - `RATE_LIMIT_REQUESTS`: Max requests per period (default: 600)
64    /// - `RATE_LIMIT_PERIOD_SECS`: Period in seconds (default: 60)
65    /// - `RATE_LIMIT_BURST`: Burst size (default: 50)
66    pub fn from_env() -> Self {
67        let enabled = std::env::var("RATE_LIMIT_ENABLED")
68            .ok()
69            .and_then(|v| v.parse().ok())
70            .unwrap_or(true);
71
72        let requests_per_period = std::env::var("RATE_LIMIT_REQUESTS")
73            .ok()
74            .and_then(|v| v.parse().ok())
75            .unwrap_or(600);
76
77        let period_secs = std::env::var("RATE_LIMIT_PERIOD_SECS")
78            .ok()
79            .and_then(|v| v.parse().ok())
80            .unwrap_or(60);
81
82        let burst_size = std::env::var("RATE_LIMIT_BURST")
83            .ok()
84            .and_then(|v| v.parse().ok())
85            .unwrap_or(MIN_BURST_FOR_ZONE_REPLAY);
86
87        Self {
88            requests_per_period,
89            period_secs,
90            burst_size,
91            enabled,
92        }
93    }
94
95    /// The interval at which governor replenishes one request cell.
96    ///
97    /// Governor is configured by *period per cell*, not by requests per period.
98    /// Converting with `requests_per_period / period_secs` in integer arithmetic
99    /// silently floors the budget — the 100-per-60s default became 1 request per
100    /// second, a 40% cut, and any budget under one request per second collapsed
101    /// to the same 1/s floor. Dividing in milliseconds keeps the configured rate.
102    ///
103    /// # Returns
104    ///
105    /// The delay between replenished cells; never zero, so governor always has a
106    /// valid period even for an absurdly large `requests_per_period`.
107    #[must_use]
108    pub fn replenish_period(&self) -> Duration {
109        let millis = self
110            .period_secs
111            .saturating_mul(1_000)
112            .checked_div(u64::from(self.requests_per_period))
113            .unwrap_or(1)
114            .max(1);
115        Duration::from_millis(millis)
116    }
117
118    /// Validate configuration values
119    pub fn validate(&self) -> Result<(), String> {
120        if self.requests_per_period == 0 {
121            return Err("requests_per_period must be greater than 0".to_string());
122        }
123
124        if self.period_secs == 0 {
125            return Err("period_secs must be greater than 0".to_string());
126        }
127
128        if self.burst_size == 0 {
129            return Err("burst_size must be greater than 0".to_string());
130        }
131
132        Ok(())
133    }
134}