Skip to main content

zebra_network/peer_set/initialize/
recent_by_ip.rs

1//! A set of IPs from recent connection attempts.
2
3use std::{
4    collections::{HashMap, VecDeque},
5    net::IpAddr,
6    time::{Duration, Instant},
7};
8
9use crate::{constants, protocol::external::connection_limit_key};
10
11#[cfg(test)]
12mod tests;
13
14#[derive(Debug)]
15/// Tracks recently attempted inbound connections, keyed per IPv4 address and
16/// per IPv6 `/64` subnet: one machine with a `/64` can originate many IPv6
17/// addresses, and every address it uses must count against a single limit.
18pub struct RecentByIp {
19    /// The list of connection limit keys in decreasing connection age order.
20    pub by_time: VecDeque<(IpAddr, Instant)>,
21
22    /// Stores the number of recently attempted inbound connections per connection limit key.
23    pub by_ip: HashMap<IpAddr, usize>,
24
25    /// The maximum number of peer connections Zebra will keep for a given IP address
26    /// before it drops any additional peer connections with that IP.
27    pub max_connections_per_ip: usize,
28
29    /// The duration to wait after an entry is added before removing it.
30    pub time_limit: Duration,
31}
32
33impl Default for RecentByIp {
34    fn default() -> Self {
35        Self::new(None, None)
36    }
37}
38
39impl RecentByIp {
40    /// Creates a new [`RecentByIp`]
41    pub fn new(time_limit: Option<Duration>, max_connections_per_ip: Option<usize>) -> Self {
42        let (by_time, by_ip) = Default::default();
43        Self {
44            by_time,
45            by_ip,
46            time_limit: time_limit.unwrap_or(constants::MIN_PEER_RECONNECTION_DELAY),
47            max_connections_per_ip: max_connections_per_ip
48                .unwrap_or(constants::DEFAULT_MAX_CONNS_PER_IP),
49        }
50    }
51
52    /// Prunes outdated entries, checks if there's a recent inbound connection
53    /// attempt from the same IPv4 address or IPv6 `/64` subnet, and adds the
54    /// entry to `by_time` and `by_ip` if needed.
55    ///
56    /// Returns true if the recently attempted inbound connection count is past the configured limit.
57    pub fn is_past_limit_or_add(&mut self, ip: IpAddr) -> bool {
58        let now = Instant::now();
59        self.prune_by_time(now);
60
61        let limit_key = connection_limit_key(ip);
62        let count = self.by_ip.entry(limit_key).or_default();
63        if *count >= self.max_connections_per_ip {
64            true
65        } else {
66            *count += 1;
67            self.by_time.push_back((limit_key, now));
68            false
69        }
70    }
71
72    /// Prunes entries older than `time_limit`, decrementing or removing their counts in `by_ip`.
73    fn prune_by_time(&mut self, now: Instant) {
74        // Currently saturates to zero:
75        // <https://doc.rust-lang.org/std/time/struct.Instant.html#monotonicity>
76        //
77        // This discards the whole structure if the time limit is very large,
78        // which is unexpected, but stops this list growing without limit.
79        // After the handshake, the peer set will remove any duplicate connections over the limit.
80        let age_limit = now - self.time_limit;
81
82        // `by_time` must be sorted for this to work.
83        let split_off_idx = self.by_time.partition_point(|&(_, time)| time <= age_limit);
84
85        let updated_by_time = self.by_time.split_off(split_off_idx);
86
87        for (ip, _) in &self.by_time {
88            if let Some(count) = self.by_ip.get_mut(ip) {
89                *count -= 1;
90                if *count == 0 {
91                    self.by_ip.remove(ip);
92                }
93            }
94        }
95
96        self.by_time = updated_by_time;
97    }
98}