postrust_proxy/ratelimit/
limiter.rs1use crate::config::RateLimitDefaults;
4use crate::ratelimit::TokenBucket;
5use dashmap::DashMap;
6use parking_lot::Mutex;
7use sha2::{Digest, Sha256};
8use std::net::{IpAddr, Ipv6Addr};
9use std::sync::Arc;
10use std::time::{Duration, Instant};
11use tokio_util::sync::CancellationToken;
12use tracing::{debug, info, warn};
13use uuid::Uuid;
14
15#[derive(Clone, Debug, Hash, PartialEq, Eq)]
17pub enum RateLimitKey {
18 Ip(IpAddr),
21 Header(String),
25 Route(uuid::Uuid),
27 Global,
29}
30
31impl RateLimitKey {
32 pub fn client_ip(ip: IpAddr) -> Self {
43 match ip.to_canonical() {
44 IpAddr::V4(v4) => RateLimitKey::Ip(IpAddr::V4(v4)),
45 IpAddr::V6(v6) => {
46 let s = v6.segments();
47 RateLimitKey::Ip(IpAddr::V6(Ipv6Addr::new(
48 s[0], s[1], s[2], s[3], 0, 0, 0, 0,
49 )))
50 }
51 }
52 }
53
54 pub fn header_value(value: &[u8]) -> Self {
61 RateLimitKey::Header(hex::encode(Sha256::digest(value)))
62 }
63}
64
65pub fn per_second(requests: u32, window_secs: u32) -> f64 {
70 f64::from(requests) / f64::from(window_secs.max(1))
71}
72
73type BucketId = (Uuid, RateLimitKey);
80
81pub struct RateLimiter {
90 buckets: DashMap<BucketId, TokenBucket>,
92 defaults: RateLimitDefaults,
94 max_keys: usize,
96 last_full_sweep: Mutex<Option<Instant>>,
99}
100
101const DEFAULT_SCOPE: Uuid = Uuid::nil();
104
105impl RateLimiter {
106 pub fn new(defaults: RateLimitDefaults) -> Self {
108 Self {
109 buckets: DashMap::new(),
110 max_keys: defaults.max_tracked_keys.max(1),
111 defaults,
112 last_full_sweep: Mutex::new(None),
113 }
114 }
115
116 pub fn check(&self, key: RateLimitKey) -> bool {
120 let rate = per_second(self.defaults.requests, self.defaults.window_secs);
121 self.check_with_config(DEFAULT_SCOPE, key, rate, self.defaults.burst)
122 }
123
124 pub fn check_with_config(
130 &self,
131 scope: Uuid,
132 key: RateLimitKey,
133 rate_per_sec: f64,
134 burst: u32,
135 ) -> bool {
136 let id = (scope, key);
137 if let Some(bucket) = self.buckets.get(&id) {
138 return bucket.try_acquire();
139 }
140
141 if self.buckets.len() >= self.max_keys && !self.make_room() {
142 warn!(
146 max_keys = self.max_keys,
147 "Rate limiter is tracking as many clients as it may; refusing a new one"
148 );
149 return false;
150 }
151
152 self.buckets
153 .entry(id)
154 .or_insert_with(|| TokenBucket::new(u64::from(burst), rate_per_sec))
155 .try_acquire()
156 }
157
158 pub fn remaining(&self, key: &RateLimitKey) -> Option<u64> {
160 self.buckets
161 .get(&(DEFAULT_SCOPE, key.clone()))
162 .map(|bucket| bucket.available())
163 }
164
165 pub fn tracked_keys(&self) -> usize {
167 self.buckets.len()
168 }
169
170 pub async fn start_cleanup(self: Arc<Self>, cancel_token: CancellationToken) {
172 let cleanup_interval = Duration::from_secs(60);
173
174 info!("Rate limiter cleanup task started");
175
176 loop {
177 tokio::select! {
178 _ = cancel_token.cancelled() => {
179 info!("Rate limiter cleanup task stopped");
180 break;
181 }
182 _ = tokio::time::sleep(cleanup_interval) => {
183 self.cleanup_expired();
184 }
185 }
186 }
187 }
188
189 fn make_room(&self) -> bool {
191 {
192 let mut last = self.last_full_sweep.lock();
193 let now = Instant::now();
194 if last.is_some_and(|at| now.duration_since(at) < Duration::from_secs(1)) {
195 return self.buckets.len() < self.max_keys;
196 }
197 *last = Some(now);
198 }
199 self.cleanup_expired();
200 self.buckets.len() < self.max_keys
201 }
202
203 fn cleanup_expired(&self) {
205 let before = self.buckets.len();
206 self.buckets.retain(|_, bucket| !bucket.is_full());
207 let removed = before.saturating_sub(self.buckets.len());
208 if removed > 0 {
209 debug!("Rate limiter cleanup: removed {} refilled entries", removed);
210 }
211 }
212}
213
214impl Default for RateLimiter {
215 fn default() -> Self {
216 Self::new(RateLimitDefaults::default())
217 }
218}
219
220#[cfg(test)]
221mod tests {
222 use super::*;
223 use std::net::Ipv4Addr;
224
225 fn defaults(requests: u32, window_secs: u32, burst: u32) -> RateLimitDefaults {
226 RateLimitDefaults {
227 requests,
228 window_secs,
229 burst,
230 ..RateLimitDefaults::default()
231 }
232 }
233
234 #[test]
235 fn test_rate_limiter_by_ip() {
236 let limiter = RateLimiter::new(defaults(600, 60, 5));
237
238 let ip1 = RateLimitKey::Ip(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1)));
239 let ip2 = RateLimitKey::Ip(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 2)));
240
241 for _ in 0..5 {
243 assert!(limiter.check(ip1.clone()));
244 assert!(limiter.check(ip2.clone()));
245 }
246
247 assert!(!limiter.check(ip1.clone()));
249 assert!(!limiter.check(ip2.clone()));
250 }
251
252 #[test]
253 fn test_rate_limiter_remaining() {
254 let limiter = RateLimiter::new(defaults(600, 60, 10));
255
256 let key = RateLimitKey::Global;
257 assert!(limiter.check(key.clone()));
258
259 let remaining = limiter.remaining(&key).unwrap();
260 assert!(remaining < 10);
261 }
262
263 #[test]
264 fn the_rate_is_a_fraction() {
265 assert_eq!(per_second(3, 60), 0.05);
266 assert_eq!(per_second(600, 60), 10.0);
267 assert_eq!(per_second(5, 0), 5.0);
269 }
270
271 #[test]
272 fn scopes_do_not_share_a_bucket() {
273 let limiter = RateLimiter::default();
276 let ip = RateLimitKey::client_ip("10.0.0.1".parse().unwrap());
277 let strict = Uuid::new_v4();
278 let generous = Uuid::new_v4();
279
280 assert!(limiter.check_with_config(strict, ip.clone(), 0.01, 1));
281 assert!(!limiter.check_with_config(strict, ip.clone(), 0.01, 1));
282
283 for _ in 0..100 {
286 assert!(limiter.check_with_config(generous, ip.clone(), 0.01, 100));
287 }
288 assert!(!limiter.check_with_config(generous, ip.clone(), 0.01, 100));
289 }
290
291 #[test]
292 fn an_ipv6_client_is_keyed_by_its_slash_64() {
293 let a = RateLimitKey::client_ip("2001:db8:1:2:aaaa::1".parse().unwrap());
294 let b = RateLimitKey::client_ip("2001:db8:1:2:ffff:1:2:3".parse().unwrap());
295 let other = RateLimitKey::client_ip("2001:db8:1:3::1".parse().unwrap());
296 assert_eq!(a, b);
297 assert_ne!(a, other);
298 }
299
300 #[test]
301 fn a_mapped_ipv4_client_is_keyed_as_ipv4() {
302 let mapped = RateLimitKey::client_ip("::ffff:192.0.2.1".parse().unwrap());
304 let plain = RateLimitKey::client_ip("192.0.2.1".parse().unwrap());
305 let other = RateLimitKey::client_ip("::ffff:192.0.2.2".parse().unwrap());
306 assert_eq!(mapped, plain);
307 assert_ne!(mapped, other);
308 }
309
310 #[test]
311 fn a_header_value_is_stored_as_a_digest() {
312 let key = RateLimitKey::header_value(&[b'k'; 8192]);
313 match key {
314 RateLimitKey::Header(digest) => assert_eq!(digest.len(), 64),
315 other => panic!("expected a header key, got {other:?}"),
316 }
317 assert_eq!(
318 RateLimitKey::header_value(b"a"),
319 RateLimitKey::header_value(b"a")
320 );
321 assert_ne!(
322 RateLimitKey::header_value(b"a"),
323 RateLimitKey::header_value(b"b")
324 );
325 }
326
327 #[test]
328 fn the_table_is_capped() {
329 let limiter = RateLimiter::new(RateLimitDefaults {
330 max_tracked_keys: 3,
331 ..RateLimitDefaults::default()
332 });
333 let scope = Uuid::new_v4();
334 let key = |n: u8| RateLimitKey::Ip(IpAddr::V4(Ipv4Addr::new(10, 0, 0, n)));
335
336 for n in 0..3 {
338 assert!(limiter.check_with_config(scope, key(n), 0.0001, 1));
339 }
340 assert_eq!(limiter.tracked_keys(), 3);
341
342 assert!(!limiter.check_with_config(scope, key(3), 0.0001, 1));
345 assert_eq!(limiter.tracked_keys(), 3);
346
347 assert!(!limiter.check_with_config(scope, key(0), 0.0001, 1));
349 }
350
351 #[test]
352 fn a_full_table_makes_room_by_dropping_refilled_buckets() {
353 let limiter = RateLimiter::new(RateLimitDefaults {
354 max_tracked_keys: 2,
355 ..RateLimitDefaults::default()
356 });
357 let scope = Uuid::new_v4();
358 let key = |n: u8| RateLimitKey::Ip(IpAddr::V4(Ipv4Addr::new(10, 0, 0, n)));
359
360 assert!(limiter.check_with_config(scope, key(0), 1e9, 2));
363 assert!(limiter.check_with_config(scope, key(1), 1e9, 2));
364 std::thread::sleep(Duration::from_millis(5));
365
366 assert!(limiter.check_with_config(scope, key(2), 1e9, 2));
367 assert!(limiter.tracked_keys() <= 2);
368 }
369
370 #[test]
371 fn cleanup_forgets_only_refilled_buckets() {
372 let limiter = RateLimiter::default();
373 let scope = Uuid::new_v4();
374 let spent = RateLimitKey::Ip(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)));
375 let refilled = RateLimitKey::Ip(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 2)));
376
377 assert!(limiter.check_with_config(scope, spent.clone(), 0.0001, 1));
378 assert!(limiter.check_with_config(scope, refilled, 1e9, 1));
379 std::thread::sleep(Duration::from_millis(5));
380
381 limiter.cleanup_expired();
382 assert_eq!(limiter.tracked_keys(), 1);
383 assert!(!limiter.check_with_config(scope, spent, 0.0001, 1));
385 }
386}