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}