zebra_network/ban_list.rs
1//! The set of banned peer groups.
2
3use std::{collections::HashMap, net::IpAddr, sync::Arc};
4
5use tokio::time::Instant;
6
7use crate::{constants, protocol::external::connection_limit_key};
8
9#[cfg(test)]
10mod tests;
11
12/// The peer groups Zebra has banned for misbehaviour, and when each was banned.
13///
14/// Entries are keyed by peer group — one IPv4 address, or one IPv6 `/64` subnet
15/// — so a peer cannot dodge its ban by reconnecting from another address it
16/// already controls. Bans lapse after
17/// [`BAN_DURATION`](constants::BAN_DURATION).
18///
19/// # Security
20///
21/// This type owns both of those rules. It deliberately does not expose the
22/// underlying map: a caller doing its own lookup would have to remember to map
23/// the address to its peer group *and* to check the ban's age, and getting
24/// either wrong silently stops bans being enforced. Query it with
25/// [`BanList::is_banned`].
26///
27/// # Correctness
28///
29/// Cloning is cheap, and a clone is a snapshot: the map is shared behind an
30/// [`Arc`] and copied only when a ban is added. Snapshots stay correct as bans
31/// lapse, because [`BanList::is_banned`] checks each entry's age when it is
32/// queried, so holders don't need to be sent a new snapshot.
33#[derive(Clone, Debug, Default, PartialEq, Eq)]
34pub struct BanList {
35 /// The time each banned peer group was banned.
36 banned_at: Arc<HashMap<IpAddr, Instant>>,
37}
38
39impl BanList {
40 /// Returns `true` if `ip`'s peer group is banned, and the ban has not
41 /// lapsed.
42 pub fn is_banned(&self, ip: IpAddr) -> bool {
43 self.banned_at
44 .get(&connection_limit_key(ip))
45 .is_some_and(|banned_at| !Self::has_lapsed(*banned_at, Instant::now()))
46 }
47
48 /// Bans `ip`'s peer group, starting now.
49 ///
50 /// Re-banning an already-banned group extends its ban for another full
51 /// [`BAN_DURATION`](constants::BAN_DURATION).
52 pub(crate) fn ban(&mut self, ip: IpAddr) {
53 let now = Instant::now();
54 let banned_at = Arc::make_mut(&mut self.banned_at);
55
56 // Drop lapsed bans, so they don't occupy the slots that active bans
57 // need, and so snapshots stay small.
58 banned_at.retain(|_group, entry| !Self::has_lapsed(*entry, now));
59
60 // Inserting an already-banned group overwrites its ban time, which is
61 // exactly the refresh we want.
62 banned_at.insert(connection_limit_key(ip), now);
63
64 while banned_at.len() > constants::MAX_BANNED_IPS {
65 let oldest = banned_at
66 .iter()
67 .min_by_key(|(_group, entry)| **entry)
68 .map(|(group, _entry)| *group)
69 .expect("the map is over the limit, so it is not empty");
70 banned_at.remove(&oldest);
71 }
72 }
73
74 /// Returns `true` if a ban applied at `banned_at` has lapsed by `now`.
75 fn has_lapsed(banned_at: Instant, now: Instant) -> bool {
76 // Instants are monotonic, so `now` is normally at or after `banned_at`.
77 // Saturating to zero treats a clock oddity as "just banned" rather than
78 // "lapsed", which fails closed.
79 now.saturating_duration_since(banned_at) >= constants::BAN_DURATION
80 }
81
82 /// Returns the number of banned peer groups, including any whose bans have
83 /// lapsed but have not been pruned yet.
84 pub fn len(&self) -> usize {
85 self.banned_at.len()
86 }
87
88 /// Returns `true` if no peer group is banned.
89 pub fn is_empty(&self) -> bool {
90 self.banned_at.is_empty()
91 }
92}