rightkit_daemon/
fail_limit.rs1use std::collections::HashMap;
15use std::time::{Duration, Instant};
16
17pub const WINDOW: Duration = Duration::from_secs(60);
19pub const BLOCK: Duration = Duration::from_secs(60);
21pub const CONN_MAX_FAILS: u32 = 5;
23pub const GLOBAL_MAX_FAILS: u32 = 30;
25pub const MAX_ENTRIES: usize = 256;
27
28#[derive(Default)]
29struct Bucket {
30 start: Option<Instant>,
31 fails: u32,
32 blocked_until: Option<Instant>,
33}
34
35impl Bucket {
36 fn blocked(&self, now: Instant) -> bool {
37 self.blocked_until.is_some_and(|t| now < t)
38 }
39
40 fn window_open(&self, now: Instant) -> bool {
41 self.start
42 .is_some_and(|s| now.saturating_duration_since(s) < WINDOW)
43 }
44
45 fn idle(&self, now: Instant) -> bool {
47 !self.blocked(now) && !self.window_open(now)
48 }
49
50 fn fail(&mut self, now: Instant, max: u32) {
51 if !self.window_open(now) {
52 self.start = Some(now);
53 self.fails = 0;
54 }
55 self.fails += 1;
56 if self.fails >= max {
57 self.blocked_until = Some(now + BLOCK);
58 self.start = None;
59 self.fails = 0;
60 }
61 }
62}
63
64#[derive(Default)]
66pub struct FailLimiter {
67 conns: HashMap<u64, Bucket>,
68 global: Bucket,
69}
70
71impl FailLimiter {
72 pub fn new() -> Self {
73 Self::default()
74 }
75
76 pub fn refuses_unverified(&self, conn: u64, now: Instant) -> bool {
80 self.global.blocked(now) || self.conns.get(&conn).is_some_and(|b| b.blocked(now))
81 }
82
83 pub fn record_failure(&mut self, conn: u64, now: Instant) {
85 self.global.fail(now, GLOBAL_MAX_FAILS);
86 if !self.conns.contains_key(&conn) && self.conns.len() >= MAX_ENTRIES {
87 self.conns.retain(|_, b| !b.idle(now));
88 if self.conns.len() >= MAX_ENTRIES {
89 return;
91 }
92 }
93 self.conns
94 .entry(conn)
95 .or_default()
96 .fail(now, CONN_MAX_FAILS);
97 }
98
99 pub fn forget(&mut self, conn: u64, now: Instant) {
101 if self.conns.get(&conn).is_some_and(|b| b.idle(now)) {
102 self.conns.remove(&conn);
103 }
104 }
105
106 #[cfg(test)]
108 pub fn entries(&self) -> usize {
109 self.conns.len()
110 }
111}
112
113#[cfg(test)]
114mod tests {
115 use super::*;
116
117 #[test]
118 fn a_connection_is_blocked_after_its_limit_and_not_before() {
119 let mut l = FailLimiter::new();
120 let t0 = Instant::now();
121 for _ in 0..CONN_MAX_FAILS - 1 {
122 l.record_failure(1, t0);
123 }
124 assert!(!l.refuses_unverified(1, t0));
125 l.record_failure(1, t0);
126 assert!(l.refuses_unverified(1, t0));
127 assert!(
128 !l.refuses_unverified(2, t0),
129 "another connection is not blocked by this one"
130 );
131 }
132
133 #[test]
134 fn the_block_lapses_after_block_duration() {
135 let mut l = FailLimiter::new();
136 let t0 = Instant::now();
137 for _ in 0..CONN_MAX_FAILS {
138 l.record_failure(1, t0);
139 }
140 assert!(l.refuses_unverified(1, t0 + BLOCK - Duration::from_millis(1)));
141 assert!(!l.refuses_unverified(1, t0 + BLOCK));
142 }
143
144 #[test]
145 fn failures_outside_the_window_do_not_add_up() {
146 let mut l = FailLimiter::new();
147 let t0 = Instant::now();
148 for i in 0..(CONN_MAX_FAILS * 3) {
149 l.record_failure(1, t0 + WINDOW * i);
150 }
151 assert!(!l.refuses_unverified(1, t0 + WINDOW * (CONN_MAX_FAILS * 3)));
152 }
153
154 #[test]
155 fn the_global_bucket_refuses_unverified_attempts_from_every_connection() {
156 let mut l = FailLimiter::new();
157 let t0 = Instant::now();
158 for conn in 0..GLOBAL_MAX_FAILS as u64 {
159 l.record_failure(conn, t0);
160 }
161 assert!(
162 l.refuses_unverified(10_000, t0),
163 "a fresh connection's failed secret is refused too"
164 );
165 assert!(!l.refuses_unverified(10_000, t0 + BLOCK));
166 }
167
168 #[test]
169 fn a_connection_block_does_not_count_toward_the_global_bucket() {
170 let mut l = FailLimiter::new();
171 let t0 = Instant::now();
172 for _ in 0..CONN_MAX_FAILS {
174 l.record_failure(1, t0);
175 }
176 assert!(l.refuses_unverified(1, t0));
177 assert!(
178 !l.refuses_unverified(2, t0),
179 "one peer's bad secrets do not refuse other peers"
180 );
181 }
182
183 #[test]
184 fn forget_drops_an_idle_entry_only() {
185 let mut l = FailLimiter::new();
186 let t0 = Instant::now();
187 l.record_failure(1, t0);
188 assert_eq!(l.entries(), 1);
189 l.forget(1, t0);
191 assert_eq!(l.entries(), 1);
192 l.forget(1, t0 + WINDOW);
194 assert_eq!(l.entries(), 0);
195 }
196}