1mod overrides;
15mod rate;
16mod settings;
17
18use {
19 crate::types::NodeId,
20 krabka_metadata::MetadataImage,
21 overrides::OverrideAddresses,
22 rate::CreationRates,
23 std::{
24 collections::HashMap,
25 hash,
26 net::IpAddr,
27 sync::{Arc, Mutex, MutexGuard, PoisonError},
28 time::{Duration, Instant},
29 },
30};
31pub use {
32 overrides::IpOverrides,
33 settings::{
34 ConnectionLimits, ConnectionQuotaSettings, MAX_CONNECTION_CREATION_RATE, MAX_CONNECTIONS,
35 MAX_CONNECTIONS_PER_IP, MAX_CONNECTIONS_PER_IP_OVERRIDES,
36 },
37};
38
39#[derive(Debug, Clone)]
42pub struct ConnectionQuotas {
43 pub settings: ConnectionQuotaSettings,
44 pub per_ip_overrides: IpOverrides,
45}
46
47impl ConnectionQuotas {
48 #[must_use]
50 pub fn max_connections_for(&self, ip: IpAddr) -> usize {
51 self.per_ip_overrides
52 .get(&ip.to_canonical())
53 .copied()
54 .unwrap_or(self.settings.max_connections_per_ip)
55 }
56}
57
58#[derive(Debug, Clone, Copy, PartialEq, Eq)]
60pub struct AcceptingListener<'a> {
61 pub name: &'a str,
64 pub protected: bool,
68}
69
70#[derive(Debug, Default)]
72struct Counts {
73 total: usize,
74 per_ip: HashMap<IpAddr, usize>,
75 per_listener: HashMap<String, usize>,
76}
77
78#[derive(Clone)]
82pub struct ConnectionLimiter {
83 configured: ConnectionLimits,
84 counts: Arc<Mutex<Counts>>,
85 rates: Arc<Mutex<CreationRates>>,
86 overrides: Arc<OverrideAddresses>,
87 applied: Arc<Mutex<HashMap<String, ConnectionQuotaSettings>>>,
90}
91
92#[derive(Debug, Clone, Copy, PartialEq, Eq)]
94pub enum ConnectionLimit {
95 Global,
97 Listener,
99 PerIp,
101}
102
103fn lock<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
106 mutex.lock().unwrap_or_else(PoisonError::into_inner)
107}
108
109impl ConnectionLimiter {
110 #[must_use]
113 pub fn new(max_connections: usize, max_connections_per_ip: usize) -> Self {
114 Self {
115 configured: ConnectionLimits {
116 max_connections,
117 max_connections_per_ip,
118 },
119 counts: Arc::default(),
120 rates: Arc::default(),
121 overrides: Arc::default(),
122 applied: Arc::default(),
123 }
124 }
125
126 #[must_use]
128 pub const fn limits(&self) -> ConnectionLimits { self.configured }
129
130 #[must_use]
136 pub fn quotas(
137 &self,
138 image: &MetadataImage,
139 node_id: NodeId,
140 listener: &str,
141 ) -> ConnectionQuotas {
142 let name = listener.to_ascii_uppercase();
143 let mut applied = lock(&self.applied);
144 let settings = ConnectionQuotaSettings::resolve(
145 image,
146 node_id,
147 listener,
148 self.configured,
149 applied.get(&name),
150 );
151 applied.insert(name, settings.clone());
152 drop(applied);
153 let per_ip_overrides = self.overrides.addresses(&settings.per_ip_overrides);
154 ConnectionQuotas {
155 settings,
156 per_ip_overrides,
157 }
158 }
159
160 #[must_use]
166 pub fn creation_delay(
167 &self,
168 listener: AcceptingListener<'_>,
169 quotas: &ConnectionQuotas,
170 now: Instant,
171 max_delay: Duration,
172 ) -> Duration {
173 lock(&self.rates).admit(
174 listener.name,
175 listener.protected,
176 (
177 quotas.settings.max_connection_creation_rate,
178 quotas.settings.listener_max_connection_creation_rate,
179 ),
180 now,
181 max_delay,
182 )
183 }
184
185 pub fn refund_creation(&self, listener: AcceptingListener<'_>, quotas: &ConnectionQuotas) {
189 lock(&self.rates).refund(
190 listener.name,
191 listener.protected,
192 (
193 quotas.settings.max_connection_creation_rate,
194 quotas.settings.listener_max_connection_creation_rate,
195 ),
196 );
197 }
198
199 pub fn try_acquire_within(
213 &self,
214 ip: IpAddr,
215 listener: AcceptingListener<'_>,
216 quotas: &ConnectionQuotas,
217 ) -> Result<ConnectionGuard, ConnectionLimit> {
218 let name = listener.name.to_ascii_uppercase();
219 let mut counts = lock(&self.counts);
220 let on_listener = counts.per_listener.get(&name).copied().unwrap_or(0);
221 if on_listener >= quotas.settings.listener_max_connections {
222 return Err(ConnectionLimit::Listener);
223 }
224 if !listener.protected && counts.total >= quotas.settings.max_connections {
225 return Err(ConnectionLimit::Global);
226 }
227 let from_ip = counts.per_ip.get(&ip).copied().unwrap_or(0);
228 if from_ip >= quotas.max_connections_for(ip) {
229 return Err(ConnectionLimit::PerIp);
230 }
231 counts.total += 1;
232 *counts.per_ip.entry(ip).or_default() += 1;
233 *counts.per_listener.entry(name.clone()).or_default() += 1;
234 drop(counts);
235 Ok(ConnectionGuard {
236 limiter: self.clone(),
237 ip,
238 listener: name,
239 })
240 }
241
242 #[must_use]
244 pub fn total(&self) -> usize { lock(&self.counts).total }
245}
246
247pub struct ConnectionGuard {
254 limiter: ConnectionLimiter,
255 ip: IpAddr,
256 listener: String,
257}
258
259fn release<K: Eq + hash::Hash>(map: &mut HashMap<K, usize>, key: &K) {
261 if let Some(count) = map.get_mut(key) {
262 *count -= 1;
263 if *count == 0 {
264 map.remove(key);
265 }
266 }
267}
268
269impl Drop for ConnectionGuard {
270 fn drop(&mut self) {
271 let mut counts = lock(&self.limiter.counts);
272 counts.total -= 1;
273 release(&mut counts.per_ip, &self.ip);
274 release(&mut counts.per_listener, &self.listener);
275 }
276}
277
278#[cfg(test)]
279mod tests {
280 use {super::*, assert2::check, std::collections::BTreeMap};
281
282 const PLAINTEXT: AcceptingListener<'static> = AcceptingListener {
283 name: "PLAINTEXT",
284 protected: false,
285 };
286
287 const A: &str = "10.0.0.1";
288 const B: &str = "10.0.0.2";
289
290 fn ip(address: &str) -> IpAddr { address.parse().unwrap() }
291
292 fn quotas(customise: impl FnOnce(&mut ConnectionQuotas)) -> ConnectionQuotas {
294 let mut quotas = ConnectionQuotas {
295 settings: ConnectionQuotaSettings {
296 max_connections: usize::MAX,
297 max_connections_per_ip: usize::MAX,
298 per_ip_overrides: BTreeMap::new(),
299 listener_max_connections: usize::MAX,
300 max_connection_creation_rate: None,
301 listener_max_connection_creation_rate: None,
302 },
303 per_ip_overrides: Arc::default(),
304 };
305 customise(&mut quotas);
306 quotas
307 }
308
309 impl ConnectionLimiter {
310 fn per_ip_count(&self, ip: IpAddr) -> usize {
311 lock(&self.counts).per_ip.get(&ip).copied().unwrap_or(0)
312 }
313
314 fn listener_count(&self, name: &str) -> usize {
315 lock(&self.counts)
316 .per_listener
317 .get(name)
318 .copied()
319 .unwrap_or(0)
320 }
321 }
322
323 #[test]
324 fn a_guard_counts_and_releases_the_total_the_address_and_the_listener() {
325 let limiter = ConnectionLimiter::new(usize::MAX, usize::MAX);
326 let unlimited = quotas(|_| {});
327 let first = limiter
328 .try_acquire_within(ip(A), PLAINTEXT, &unlimited)
329 .unwrap();
330 let second = limiter
331 .try_acquire_within(ip(A), PLAINTEXT, &unlimited)
332 .unwrap();
333 check!(
334 (
335 limiter.total(),
336 limiter.per_ip_count(ip(A)),
337 limiter.listener_count("PLAINTEXT")
338 ) == (2, 2, 2)
339 );
340 drop((first, second));
341 check!(limiter.total() == 0);
342 check!(lock(&limiter.counts).per_ip.is_empty());
343 check!(lock(&limiter.counts).per_listener.is_empty());
344 }
345
346 #[yare::parameterized(
349 global_cap = { |q: &mut ConnectionQuotas| q.settings.max_connections = 1, B, PLAINTEXT, Some(ConnectionLimit::Global) },
350 protected_listener_skips_global_cap = {
351 |q: &mut ConnectionQuotas| q.settings.max_connections = 1,
352 B,
353 AcceptingListener { name: "REPLICATION", protected: true },
354 None
355 },
356 listener_cap = { |q: &mut ConnectionQuotas| q.settings.listener_max_connections = 1, B, PLAINTEXT, Some(ConnectionLimit::Listener) },
357 listener_cap_ignores_case = {
358 |q: &mut ConnectionQuotas| q.settings.listener_max_connections = 1,
359 B,
360 AcceptingListener { name: "plaintext", protected: false },
361 Some(ConnectionLimit::Listener)
362 },
363 listener_cap_counts_its_own_listener = {
364 |q: &mut ConnectionQuotas| q.settings.listener_max_connections = 1,
365 B,
366 AcceptingListener { name: "EXTERNAL", protected: false },
367 None
368 },
369 per_ip_cap = { |q: &mut ConnectionQuotas| q.settings.max_connections_per_ip = 1, A, PLAINTEXT, Some(ConnectionLimit::PerIp) },
370 per_ip_cap_other_address = { |q: &mut ConnectionQuotas| q.settings.max_connections_per_ip = 1, B, PLAINTEXT, None },
371 override_raises_the_cap = {
372 |q: &mut ConnectionQuotas| {
373 q.settings.max_connections_per_ip = 1;
374 q.per_ip_overrides = Arc::new(HashMap::from([(ip(A), 2)]));
375 },
376 A,
377 PLAINTEXT,
378 None
379 },
380 override_of_zero_refuses_the_address = {
381 |q: &mut ConnectionQuotas| q.per_ip_overrides = Arc::new(HashMap::from([(ip(B), 0)])),
382 B,
383 PLAINTEXT,
384 Some(ConnectionLimit::PerIp)
385 },
386 )]
387 fn the_next_connection_meets_each_cap(
388 customise: fn(&mut ConnectionQuotas),
389 from: &str,
390 listener: AcceptingListener<'static>,
391 refused: Option<ConnectionLimit>,
392 ) {
393 let limiter = ConnectionLimiter::new(usize::MAX, usize::MAX);
394 let _held = limiter
395 .try_acquire_within(ip(A), PLAINTEXT, "as(|_| {}))
396 .unwrap();
397 let next = limiter.try_acquire_within(ip(from), listener, "as(customise));
398 assert2::assert!(next.as_ref().err().copied() == refused);
399 assert2::assert!(limiter.total() == 1 + usize::from(refused.is_none()));
401 }
402
403 #[test]
404 fn a_v4_mapped_peer_meets_its_v4_override() {
405 let quotas = quotas(|q| q.per_ip_overrides = Arc::new(HashMap::from([(ip(A), 0)])));
406 assert2::assert!(quotas.max_connections_for(ip("::ffff:10.0.0.1")) == 0);
407 }
408}