waf_detection/
rate_limit.rs1use std::sync::Arc;
5
6use tracing::warn;
7use waf_core::{
8 BucketParams, Clock, Config, Decision, Phase, RateLimitAction, RateLimitKey, RateLimitState,
9 RequestContext, WafModule,
10};
11
12pub struct RateLimitModule {
17 enabled: bool,
18 params: BucketParams,
19 action: RateLimitAction,
20 score: u32,
21 key: RateLimitKey,
22 state: RateLimitState,
23}
24
25impl Default for RateLimitModule {
26 fn default() -> Self {
27 Self::with_state(RateLimitState::new())
28 }
29}
30
31impl RateLimitModule {
32 pub fn new() -> Self {
33 Self::default()
34 }
35
36 pub fn with_clock(clock: Arc<dyn Clock>) -> Self {
39 Self::with_state(RateLimitState::in_memory_with_clock(clock, DEFAULT_MAX_TRACKED_KEYS))
40 }
41
42 pub fn with_state(state: RateLimitState) -> Self {
45 Self {
46 enabled: false,
47 params: BucketParams { capacity: 0.0, refill_per_sec: 0.0 },
48 action: RateLimitAction::Block,
49 score: 0,
50 key: RateLimitKey::ClientIp,
51 state,
52 }
53 }
54
55 fn key_for(&self, ctx: &RequestContext) -> String {
56 match self.key {
57 RateLimitKey::ClientIp => ctx.client_ip.to_string(),
60 }
61 }
62
63 fn retry_after(&self, tokens: f64) -> u64 {
65 if self.params.refill_per_sec <= 0.0 {
66 return 1;
67 }
68 ((1.0 - tokens) / self.params.refill_per_sec).ceil().max(1.0) as u64
69 }
70}
71
72const DEFAULT_MAX_TRACKED_KEYS: usize = 100_000;
76
77impl WafModule for RateLimitModule {
78 fn id(&self) -> &str {
79 "rate_limit"
80 }
81
82 fn phase(&self) -> Phase {
83 Phase::Connection
84 }
85
86 fn init(&mut self, cfg: &Config) {
87 let rl = &cfg.rate_limit;
88 self.enabled = rl.enabled;
89 let window = rl.window_seconds.max(1) as f64;
90 self.params = BucketParams {
91 capacity: rl.burst.unwrap_or(rl.requests).max(1) as f64,
92 refill_per_sec: rl.requests as f64 / window,
93 };
94 self.action = rl.action;
95 self.score = rl.score;
96 self.key = rl.key;
97 }
98
99 fn inspect(&self, ctx: &RequestContext) -> Decision {
100 if !self.enabled {
101 return Decision::Allow;
102 }
103
104 let key = self.key_for(ctx);
105
106 let outcome = self.state.try_acquire(&key, 1.0, self.params);
108 if outcome.allowed {
109 return Decision::Allow;
110 }
111 let tokens_after = outcome.tokens_remaining;
112
113 match self.action {
115 RateLimitAction::Block => {
116 let retry = self.retry_after(tokens_after);
117 warn!(
118 request_id = %ctx.request_id,
119 key = %key,
120 retry_after = retry,
121 "rate limit exceeded (block)"
122 );
123 Decision::Reject {
124 rule_id: "rate-limit".to_string(),
125 reason: "rate limit exceeded".to_string(),
126 status: 429,
127 retry_after: Some(retry),
128 }
129 }
130 RateLimitAction::Score => {
131 warn!(
132 request_id = %ctx.request_id,
133 key = %key,
134 points = self.score,
135 "rate limit exceeded (score)"
136 );
137 Decision::Score {
138 rule_id: "rate-limit-exceeded".to_string(),
139 points: self.score,
140 }
141 }
142 }
143 }
144}