1use crate::response::Response;
49use serde::{Deserialize, Serialize};
50use std::collections::HashMap;
51use std::net::IpAddr;
52use std::sync::atomic::{AtomicUsize, Ordering};
53use std::sync::{Arc, Mutex};
54use std::time::{Duration, Instant};
55use thiserror::Error;
56
57#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
59pub struct Limit {
60 pub max_requests: u32,
62 pub window_ms: u64,
64}
65
66#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
68pub struct RouteQuota {
69 pub route: String,
71 pub limit: Limit,
73}
74
75#[derive(
77 Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize,
78)]
79#[serde(default)]
80pub struct AbusePolicy {
81 pub burst: Option<Limit>,
83 pub route_quotas: Vec<RouteQuota>,
85 pub global: Option<Limit>,
87 pub trusted: Vec<String>,
90 pub exempt_routes: Vec<String>,
92}
93
94#[derive(Clone, Debug, PartialEq, Eq, Error)]
96pub enum AbuseError {
97 #[error(
99 "{0}: max_requests and window_ms must be greater than zero"
100 )]
101 InvalidLimit(String),
102 #[error("invalid trusted address `{0}`")]
104 InvalidTrusted(String),
105 #[error("duplicate quota for route `{0}`")]
107 DuplicateQuota(String),
108}
109
110impl AbusePolicy {
111 pub fn validate(&self) -> Result<(), AbuseError> {
118 self.validate_limits()?;
119 if let Some(bad) =
120 self.trusted.iter().find(|t| parse_cidr(t).is_none())
121 {
122 return Err(AbuseError::InvalidTrusted(bad.clone()));
123 }
124 for (i, quota) in self.route_quotas.iter().enumerate() {
125 if self.route_quotas[..i]
126 .iter()
127 .any(|q| q.route == quota.route)
128 {
129 return Err(AbuseError::DuplicateQuota(
130 quota.route.clone(),
131 ));
132 }
133 }
134 Ok(())
135 }
136
137 fn validate_limits(&self) -> Result<(), AbuseError> {
138 let named =
139 |name: &str, limit: &Limit| (name.to_string(), *limit);
140 let limits = self
141 .burst
142 .iter()
143 .map(|l| named("burst", l))
144 .chain(self.global.iter().map(|l| named("global", l)))
145 .chain(self.route_quotas.iter().map(|q| {
146 named(&format!("route `{}`", q.route), &q.limit)
147 }));
148 for (name, limit) in limits {
149 if limit.max_requests == 0 || limit.window_ms == 0 {
150 return Err(AbuseError::InvalidLimit(name));
151 }
152 }
153 Ok(())
154 }
155
156 fn is_trusted(&self, ip: IpAddr) -> bool {
157 self.trusted
158 .iter()
159 .filter_map(|t| parse_cidr(t))
160 .any(|(net, bits)| in_network(ip, net, bits))
161 }
162}
163
164#[derive(Clone, Copy, Debug, PartialEq, Eq)]
166pub enum AbuseReason {
167 Burst,
169 RouteQuota,
171 Global,
173}
174
175#[derive(Clone, Debug, PartialEq, Eq)]
177pub struct AbuseEvent {
178 pub reason: AbuseReason,
180 pub scope: String,
182 pub client: IpAddr,
184 pub retry_after_secs: u64,
186}
187
188impl AbuseEvent {
189 pub fn to_log_line(&self) -> String {
191 let (reason, action) = match self.reason {
192 AbuseReason::Burst => ("burst", "throttled"),
193 AbuseReason::RouteQuota => ("route_quota", "throttled"),
194 AbuseReason::Global => ("global", "shed"),
195 };
196 format!(
197 "abuse reason={reason} scope={} action={action} client={}",
198 self.scope, self.client
199 )
200 }
201
202 pub(crate) fn response(&self) -> Response {
204 let mut response = match self.reason {
205 AbuseReason::Global => Response::new(
206 503,
207 "SERVICE UNAVAILABLE",
208 b"Server busy".to_vec(),
209 ),
210 _ => Response::new(
211 429,
212 "TOO MANY REQUESTS",
213 b"Rate limit exceeded".to_vec(),
214 ),
215 };
216 response.add_header("Content-Type", "text/plain");
217 response.add_header(
218 "Retry-After",
219 &self.retry_after_secs.to_string(),
220 );
221 response
222 }
223}
224
225type Check = (AbuseReason, String, Option<IpAddr>, Limit);
227
228type Windows = HashMap<(String, Option<IpAddr>), (Instant, u32)>;
230
231const MAX_TRACKED: usize = 65_536;
233
234#[derive(Clone, Debug, Serialize, Deserialize)]
236#[serde(try_from = "AbusePolicy", into = "AbusePolicy")]
237pub(crate) struct AbuseGuard {
238 policy: AbusePolicy,
239 windows: Arc<Mutex<Windows>>,
240}
241
242impl PartialEq for AbuseGuard {
243 fn eq(&self, other: &Self) -> bool {
244 self.policy == other.policy
245 }
246}
247
248impl Eq for AbuseGuard {}
249
250impl TryFrom<AbusePolicy> for AbuseGuard {
251 type Error = AbuseError;
252
253 fn try_from(policy: AbusePolicy) -> Result<Self, Self::Error> {
254 policy.validate()?;
255 Ok(Self {
256 policy,
257 windows: Arc::default(),
258 })
259 }
260}
261
262impl From<AbuseGuard> for AbusePolicy {
263 fn from(guard: AbuseGuard) -> Self {
264 guard.policy
265 }
266}
267
268impl AbuseGuard {
269 pub(crate) fn policy(&self) -> &AbusePolicy {
270 &self.policy
271 }
272
273 pub(crate) fn check(
276 &self,
277 client: IpAddr,
278 route: Option<&str>,
279 ) -> Option<AbuseEvent> {
280 let policy = &self.policy;
281 if route.is_some_and(|r| {
282 policy.exempt_routes.iter().any(|e| e == r)
283 }) {
284 return None;
285 }
286 let now = Instant::now();
287 let mut windows = match self.windows.lock() {
288 Ok(guard) => guard,
289 Err(poisoned) => poisoned.into_inner(),
290 };
291 prune(&mut windows, now, self.longest_window());
292 let checks = self.checks(client, route);
293 checks.into_iter().flatten().find_map(
294 |(reason, scope, key, limit)| {
295 let retry = hit(
296 &mut windows,
297 (scope.clone(), key),
298 limit,
299 now,
300 )?;
301 Some(AbuseEvent {
302 reason,
303 scope,
304 client,
305 retry_after_secs: retry,
306 })
307 },
308 )
309 }
310
311 fn longest_window(&self) -> Duration {
313 let policy = &self.policy;
314 let longest = policy
315 .burst
316 .iter()
317 .chain(policy.global.iter())
318 .chain(policy.route_quotas.iter().map(|q| &q.limit))
319 .map(|l| l.window_ms)
320 .max()
321 .unwrap_or(0);
322 Duration::from_millis(longest)
323 }
324
325 #[cfg(test)]
327 pub(crate) fn tracked(&self) -> usize {
328 self.windows.lock().map_or(0, |w| w.len())
329 }
330
331 fn checks(
334 &self,
335 client: IpAddr,
336 route: Option<&str>,
337 ) -> [Option<Check>; 3] {
338 let policy = &self.policy;
339 let quota = route.and_then(|r| {
340 policy.route_quotas.iter().find(|q| q.route == r)
341 });
342 [
343 policy.burst.map(|l| {
344 (
345 AbuseReason::Burst,
346 "client".to_string(),
347 Some(client),
348 l,
349 )
350 }),
351 quota.map(|q| {
352 let scope = format!("route:{}", q.route);
353 (AbuseReason::RouteQuota, scope, Some(client), q.limit)
354 }),
355 policy.global.filter(|_| !policy.is_trusted(client)).map(
356 |l| {
357 (AbuseReason::Global, "global".to_string(), None, l)
358 },
359 ),
360 ]
361 }
362}
363
364fn hit(
367 windows: &mut Windows,
368 key: (String, Option<IpAddr>),
369 limit: Limit,
370 now: Instant,
371) -> Option<u64> {
372 let length = Duration::from_millis(limit.window_ms);
373 let entry = windows.entry(key).or_insert((now, 0));
374 if now.duration_since(entry.0) >= length {
375 *entry = (now, 0);
376 }
377 if entry.1 >= limit.max_requests {
378 let left = length.saturating_sub(now.duration_since(entry.0));
379 return Some(left.as_secs_f64().ceil().max(1.0) as u64);
380 }
381 entry.1 += 1;
382 None
383}
384
385fn prune(windows: &mut Windows, now: Instant, longest: Duration) {
390 if windows.len() < MAX_TRACKED {
391 return;
392 }
393 windows
394 .retain(|_, (start, _)| now.duration_since(*start) < longest);
395 if windows.len() >= MAX_TRACKED / 2 {
396 log::warn!(
397 "abuse counters reset: {} live windows exceed the cap",
398 windows.len()
399 );
400 windows.clear();
401 }
402}
403
404fn parse_cidr(value: &str) -> Option<(IpAddr, u8)> {
406 let (address, bits) = match value.split_once('/') {
407 Some((address, bits)) => {
408 (address, Some(bits.parse::<u8>().ok()?))
409 }
410 None => (value, None),
411 };
412 let address: IpAddr = address.trim().parse().ok()?;
413 let max = if address.is_ipv4() { 32 } else { 128 };
414 let bits = bits.unwrap_or(max);
415 (bits <= max).then_some((address, bits))
416}
417
418fn in_network(ip: IpAddr, net: IpAddr, bits: u8) -> bool {
419 let (ip, net, width) = match (ip, net) {
420 (IpAddr::V4(a), IpAddr::V4(b)) => {
421 (u128::from(a.to_bits()), u128::from(b.to_bits()), 32)
422 }
423 (IpAddr::V6(a), IpAddr::V6(b)) => {
424 (a.to_bits(), b.to_bits(), 128)
425 }
426 _ => return false,
427 };
428 let shift = width - u32::from(bits);
429 shift >= width || (ip >> shift) == (net >> shift)
430}
431
432static BURST: AtomicUsize = AtomicUsize::new(0);
433static ROUTE_QUOTA: AtomicUsize = AtomicUsize::new(0);
434static GLOBAL: AtomicUsize = AtomicUsize::new(0);
435
436#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
438pub struct AbuseStats {
439 pub burst: usize,
441 pub route_quota: usize,
443 pub global: usize,
445}
446
447pub fn stats() -> AbuseStats {
449 AbuseStats {
450 burst: BURST.load(Ordering::Relaxed),
451 route_quota: ROUTE_QUOTA.load(Ordering::Relaxed),
452 global: GLOBAL.load(Ordering::Relaxed),
453 }
454}
455
456pub(crate) fn metrics_lines() -> String {
458 let s = stats();
459 format!(
460 "http_handle_abuse_total{{reason=\"burst\"}} {}\nhttp_handle_abuse_total{{reason=\"route_quota\"}} {}\nhttp_handle_abuse_total{{reason=\"global\"}} {}\n",
461 s.burst, s.route_quota, s.global
462 )
463}
464
465pub(crate) fn record(event: &AbuseEvent) {
467 log::warn!("{}", event.to_log_line());
468 let counter = match event.reason {
469 AbuseReason::Burst => &BURST,
470 AbuseReason::RouteQuota => &ROUTE_QUOTA,
471 AbuseReason::Global => &GLOBAL,
472 };
473 let _ = counter.fetch_add(1, Ordering::Relaxed);
474}