Skip to main content

simoom_h2/
budget.rs

1//! Sliding-window frame budgets per connection (plan §2.7, research §5).
2//!
3//! Each budget is an approximate sliding window built from two fixed buckets: the estimate is
4//! the current bucket plus the previous bucket weighted by how much of it still overlaps the
5//! window. It costs two counters and a timestamp, and never under-counts a burst by more than
6//! one bucket's worth of decay.
7
8/// Limits per window. `u32::MAX` disables a budget.
9#[derive(Clone, Copy, Debug, PartialEq, Eq)]
10pub struct Budgets {
11    /// Window length in milliseconds.
12    pub window_ms: u64,
13    /// RST_STREAM frames received (Rapid Reset, CVE-2023-44487; reset flood, CVE-2019-9514).
14    pub rst_received: u32,
15    /// RST_STREAM frames the peer made us send: protocol errors on streams and refused streams
16    /// (MadeYouReset, CVE-2025-8671; CVE-2019-9514). Resets the application asks for do not
17    /// count.
18    pub rst_sent: u32,
19    /// PING frames received (ping flood, CVE-2019-9512).
20    pub ping: u32,
21    /// SETTINGS frames received (settings flood, CVE-2019-9515).
22    pub settings: u32,
23    /// Empty DATA, HEADERS and CONTINUATION frames without END_STREAM or END_HEADERS (empty
24    /// frames flood, CVE-2019-9518).
25    pub empty_frames: u32,
26    /// PRIORITY frames received (resource loop, CVE-2019-9513).
27    pub priority: u32,
28    /// WINDOW_UPDATE frames with an increment under 1024 bytes (data dribble, CVE-2019-9511).
29    pub small_window_updates: u32,
30}
31
32impl Default for Budgets {
33    fn default() -> Self {
34        Budgets {
35            window_ms: 10_000,
36            rst_received: 200,
37            rst_sent: 200,
38            ping: 100,
39            settings: 100,
40            empty_frames: 200,
41            priority: 1000,
42            small_window_updates: 500,
43        }
44    }
45}
46
47#[derive(Clone, Copy, Debug, PartialEq, Eq)]
48pub(crate) enum Kind {
49    RstReceived = 0,
50    RstSent,
51    Ping,
52    Settings,
53    Empty,
54    Priority,
55    SmallWindowUpdate,
56}
57
58#[derive(Clone, Copy, Debug, Default)]
59struct Counter {
60    prev: u32,
61    cur: u32,
62    start: u64,
63}
64
65#[derive(Clone, Debug)]
66pub(crate) struct Meter {
67    limits: [u32; 7],
68    window: u64,
69    counters: [Counter; 7],
70}
71
72impl Meter {
73    pub(crate) fn new(b: &Budgets) -> Meter {
74        Meter {
75            limits: [b.rst_received, b.rst_sent, b.ping, b.settings, b.empty_frames, b.priority, b.small_window_updates],
76            window: b.window_ms.max(1),
77            counters: [Counter { prev: 0, cur: 0, start: 0 }; 7],
78        }
79    }
80
81    /// Counts one event at `now`; true when the budget is exceeded.
82    #[inline(never)]
83    pub(crate) fn hit(&mut self, kind: Kind, now: u64) -> bool {
84        let limit = self.limits[kind as usize];
85        if limit == u32::MAX {
86            return false;
87        }
88        let w = self.window;
89        let c = &mut self.counters[kind as usize];
90        let elapsed = now.saturating_sub(c.start);
91        if elapsed >= w {
92            c.prev = if elapsed >= 2 * w { 0 } else { c.cur };
93            c.cur = 0;
94            c.start = now - elapsed % w;
95        }
96        c.cur = c.cur.saturating_add(1);
97        let into = now.saturating_sub(c.start).min(w);
98        let estimate = (c.prev as u64 * (w - into)).div_ceil(w) + c.cur as u64;
99        estimate > limit as u64
100    }
101}
102
103#[cfg(test)]
104mod tests {
105    use super::*;
106
107    #[test]
108    fn sliding_window() {
109        let b = Budgets { window_ms: 1000, rst_received: 10, ..Budgets::default() };
110        let mut m = Meter::new(&b);
111        for i in 0..10 {
112            assert!(!m.hit(Kind::RstReceived, 5000 + i), "{i}");
113        }
114        assert!(m.hit(Kind::RstReceived, 5010));
115        // just after the bucket turns the previous one still weighs in
116        let mut m = Meter::new(&b);
117        for _ in 0..10 {
118            assert!(!m.hit(Kind::RstReceived, 999));
119        }
120        assert!(m.hit(Kind::RstReceived, 1001));
121        // after two windows it is forgotten
122        let mut m = Meter::new(&b);
123        for _ in 0..10 {
124            m.hit(Kind::RstReceived, 0);
125        }
126        assert!(!m.hit(Kind::RstReceived, 2500));
127        // a steady rate under the limit never trips
128        let mut m = Meter::new(&b);
129        for t in 0..10_000u64 {
130            if t % 125 == 0 {
131                assert!(!m.hit(Kind::RstReceived, t), "{t}");
132            }
133        }
134        // disabled
135        let mut m = Meter::new(&Budgets { ping: u32::MAX, ..b });
136        for _ in 0..100_000 {
137            assert!(!m.hit(Kind::Ping, 0));
138        }
139    }
140}