reth_net_banlist/
lib.rs

1//! Support for banning peers.
2
3#![doc(
4    html_logo_url = "https://raw.githubusercontent.com/paradigmxyz/reth/main/assets/reth-docs.png",
5    html_favicon_url = "https://avatars0.githubusercontent.com/u/97369466?s=256",
6    issue_tracker_base_url = "https://github.com/paradigmxyz/reth/issues/"
7)]
8#![cfg_attr(not(test), warn(unused_crate_dependencies))]
9#![cfg_attr(docsrs, feature(doc_cfg))]
10
11type PeerId = alloy_primitives::B512;
12
13use std::{collections::HashMap, net::IpAddr, str::FromStr, time::Instant};
14
15/// Determines whether or not the IP is globally routable.
16/// Should be replaced with [`IpAddr::is_global`](std::net::IpAddr::is_global) once it is stable.
17pub const fn is_global(ip: &IpAddr) -> bool {
18    if ip.is_unspecified() || ip.is_loopback() {
19        return false
20    }
21
22    match ip {
23        IpAddr::V4(ip) => !ip.is_private() && !ip.is_link_local(),
24        IpAddr::V6(_) => true,
25    }
26}
27
28/// Stores peers that should be taken out of circulation either indefinitely or until a certain
29/// timestamp
30#[derive(Debug, Clone, Default, PartialEq, Eq)]
31pub struct BanList {
32    /// A set of IPs whose packets get dropped instantly.
33    banned_ips: HashMap<IpAddr, Option<Instant>>,
34    /// A set of [`PeerId`] whose packets get dropped instantly.
35    banned_peers: HashMap<PeerId, Option<Instant>>,
36}
37
38impl BanList {
39    /// Creates a new ban list that bans the given peers and ips indefinitely.
40    pub fn new(
41        banned_peers: impl IntoIterator<Item = PeerId>,
42        banned_ips: impl IntoIterator<Item = IpAddr>,
43    ) -> Self {
44        Self::new_with_timeout(
45            banned_peers.into_iter().map(|peer| (peer, None)).collect(),
46            banned_ips.into_iter().map(|ip| (ip, None)).collect(),
47        )
48    }
49
50    /// Creates a new ban list that bans the given peers and ips with an optional timeout.
51    pub const fn new_with_timeout(
52        banned_peers: HashMap<PeerId, Option<Instant>>,
53        banned_ips: HashMap<IpAddr, Option<Instant>>,
54    ) -> Self {
55        Self { banned_ips, banned_peers }
56    }
57
58    /// Removes all peers that are no longer banned.
59    pub fn evict_peers(&mut self, now: Instant) -> Vec<PeerId> {
60        let mut evicted = Vec::new();
61        self.banned_peers.retain(|peer, until| {
62            if let Some(until) = until &&
63                now > *until
64            {
65                evicted.push(*peer);
66                return false
67            }
68            true
69        });
70        evicted
71    }
72
73    /// Removes all ip addresses that are no longer banned.
74    pub fn evict_ips(&mut self, now: Instant) -> Vec<IpAddr> {
75        let mut evicted = Vec::new();
76        self.banned_ips.retain(|peer, until| {
77            if let Some(until) = until &&
78                now > *until
79            {
80                evicted.push(*peer);
81                return false
82            }
83            true
84        });
85        evicted
86    }
87
88    /// Removes all entries that should no longer be banned.
89    ///
90    /// Returns the evicted entries.
91    pub fn evict(&mut self, now: Instant) -> (Vec<IpAddr>, Vec<PeerId>) {
92        let ips = self.evict_ips(now);
93        let peers = self.evict_peers(now);
94        (ips, peers)
95    }
96
97    /// Returns true if either the given peer id _or_ ip address is banned.
98    #[inline]
99    pub fn is_banned(&self, peer_id: &PeerId, ip: &IpAddr) -> bool {
100        self.is_banned_peer(peer_id) || self.is_banned_ip(ip)
101    }
102
103    /// checks the ban list to see if it contains the given ip
104    #[inline]
105    pub fn is_banned_ip(&self, ip: &IpAddr) -> bool {
106        self.banned_ips.contains_key(ip)
107    }
108
109    /// checks the ban list to see if it contains the given ip
110    #[inline]
111    pub fn is_banned_peer(&self, peer_id: &PeerId) -> bool {
112        self.banned_peers.contains_key(peer_id)
113    }
114
115    /// Unbans the ip address
116    pub fn unban_ip(&mut self, ip: &IpAddr) {
117        self.banned_ips.remove(ip);
118    }
119
120    /// Unbans the ip address
121    pub fn unban_peer(&mut self, peer_id: &PeerId) {
122        self.banned_peers.remove(peer_id);
123    }
124
125    /// Bans the IP until the timestamp.
126    ///
127    /// This does not ban non-global IPs.
128    /// If the IP is already banned, the timeout will be updated to the new value.
129    pub fn ban_ip_until(&mut self, ip: IpAddr, until: Instant) {
130        self.ban_ip_with(ip, Some(until));
131    }
132
133    /// Bans the peer until the timestamp.
134    ///
135    /// If the peer is already banned, the timeout will be updated to the new value.
136    pub fn ban_peer_until(&mut self, node_id: PeerId, until: Instant) {
137        self.ban_peer_with(node_id, Some(until));
138    }
139
140    /// Bans the IP indefinitely.
141    ///
142    /// This does not ban non-global IPs.
143    pub fn ban_ip(&mut self, ip: IpAddr) {
144        self.ban_ip_with(ip, None);
145    }
146
147    /// Bans the peer indefinitely,
148    pub fn ban_peer(&mut self, node_id: PeerId) {
149        self.ban_peer_with(node_id, None);
150    }
151
152    /// Bans the peer indefinitely or until the given timeout.
153    ///
154    /// If the peer is already banned, the timeout will be updated to the new value.
155    pub fn ban_peer_with(&mut self, node_id: PeerId, until: Option<Instant>) {
156        self.banned_peers.insert(node_id, until);
157    }
158
159    /// Bans the ip indefinitely or until the given timeout.
160    ///
161    /// This does not ban non-global IPs.
162    /// If the IP is already banned, the timeout will be updated to the new value.
163    pub fn ban_ip_with(&mut self, ip: IpAddr, until: Option<Instant>) {
164        if is_global(&ip) {
165            self.banned_ips.insert(ip, until);
166        }
167    }
168}
169
170#[cfg(test)]
171mod tests {
172    use super::*;
173
174    #[test]
175    fn can_ban_unban_peer() {
176        let peer = PeerId::new([1; 64]);
177        let mut banlist = BanList::default();
178        banlist.ban_peer(peer);
179        assert!(banlist.is_banned_peer(&peer));
180        banlist.unban_peer(&peer);
181        assert!(!banlist.is_banned_peer(&peer));
182    }
183
184    #[test]
185    fn can_ban_unban_ip() {
186        let ip = IpAddr::from([1, 1, 1, 1]);
187        let mut banlist = BanList::default();
188        banlist.ban_ip(ip);
189        assert!(banlist.is_banned_ip(&ip));
190        banlist.unban_ip(&ip);
191        assert!(!banlist.is_banned_ip(&ip));
192    }
193
194    #[test]
195    fn cannot_ban_non_global() {
196        let mut ip = IpAddr::from([0, 0, 0, 0]);
197        let mut banlist = BanList::default();
198        banlist.ban_ip(ip);
199        assert!(!banlist.is_banned_ip(&ip));
200
201        ip = IpAddr::from([10, 0, 0, 0]);
202        banlist.ban_ip(ip);
203        assert!(!banlist.is_banned_ip(&ip));
204
205        ip = IpAddr::from([127, 0, 0, 0]);
206        banlist.ban_ip(ip);
207        assert!(!banlist.is_banned_ip(&ip));
208
209        ip = IpAddr::from([172, 17, 0, 0]);
210        banlist.ban_ip(ip);
211        assert!(!banlist.is_banned_ip(&ip));
212
213        ip = IpAddr::from([172, 16, 0, 0]);
214        banlist.ban_ip(ip);
215        assert!(!banlist.is_banned_ip(&ip));
216    }
217}
218
219/// IP filter for restricting network communication to specific IP ranges using CIDR notation.
220#[derive(Debug, Clone, PartialEq, Eq)]
221pub struct IpFilter {
222    /// List of allowed IP networks in CIDR notation.
223    /// If empty, all IPs are allowed.
224    allowed_networks: Vec<ipnet::IpNet>,
225}
226
227impl IpFilter {
228    /// Creates a new IP filter with the given CIDR networks.
229    ///
230    /// If the list is empty, all IPs will be allowed.
231    pub const fn new(allowed_networks: Vec<ipnet::IpNet>) -> Self {
232        Self { allowed_networks }
233    }
234
235    /// Creates an IP filter from a comma-separated list of CIDR networks.
236    ///
237    /// # Errors
238    ///
239    /// Returns an error if any of the CIDR strings cannot be parsed.
240    pub fn from_cidr_string(cidrs: &str) -> Result<Self, ipnet::AddrParseError> {
241        if cidrs.is_empty() {
242            return Ok(Self::allow_all())
243        }
244
245        let networks = cidrs
246            .split(',')
247            .map(|s| s.trim())
248            .filter(|s| !s.is_empty())
249            .map(ipnet::IpNet::from_str)
250            .collect::<Result<Vec<_>, _>>()?;
251
252        Ok(Self::new(networks))
253    }
254
255    /// Creates a filter that allows all IPs.
256    pub const fn allow_all() -> Self {
257        Self { allowed_networks: Vec::new() }
258    }
259
260    /// Checks if the given IP address is allowed by this filter.
261    ///
262    /// Returns `true` if the filter is empty (allows all) or if the IP is within
263    /// any of the allowed networks.
264    pub fn is_allowed(&self, ip: &IpAddr) -> bool {
265        // If no restrictions are set, allow all IPs
266        if self.allowed_networks.is_empty() {
267            return true
268        }
269
270        // Check if the IP is within any of the allowed networks
271        self.allowed_networks.iter().any(|net| net.contains(ip))
272    }
273
274    /// Returns `true` if this filter has restrictions (i.e., not allowing all IPs).
275    pub const fn has_restrictions(&self) -> bool {
276        !self.allowed_networks.is_empty()
277    }
278
279    /// Returns the list of allowed networks.
280    pub fn allowed_networks(&self) -> &[ipnet::IpNet] {
281        &self.allowed_networks
282    }
283}
284
285impl Default for IpFilter {
286    fn default() -> Self {
287        Self::allow_all()
288    }
289}
290
291#[cfg(test)]
292mod ip_filter_tests {
293    use super::*;
294
295    #[test]
296    fn test_allow_all_filter() {
297        let filter = IpFilter::allow_all();
298        assert!(filter.is_allowed(&IpAddr::from([192, 168, 1, 1])));
299        assert!(filter.is_allowed(&IpAddr::from([10, 0, 0, 1])));
300        assert!(filter.is_allowed(&IpAddr::from([8, 8, 8, 8])));
301        assert!(!filter.has_restrictions());
302    }
303
304    #[test]
305    fn test_single_network_filter() {
306        let filter = IpFilter::from_cidr_string("192.168.0.0/16").unwrap();
307        assert!(filter.is_allowed(&IpAddr::from([192, 168, 1, 1])));
308        assert!(filter.is_allowed(&IpAddr::from([192, 168, 255, 255])));
309        assert!(!filter.is_allowed(&IpAddr::from([192, 169, 1, 1])));
310        assert!(!filter.is_allowed(&IpAddr::from([10, 0, 0, 1])));
311        assert!(filter.has_restrictions());
312    }
313
314    #[test]
315    fn test_multiple_networks_filter() {
316        let filter = IpFilter::from_cidr_string("192.168.0.0/16,10.0.0.0/8").unwrap();
317        assert!(filter.is_allowed(&IpAddr::from([192, 168, 1, 1])));
318        assert!(filter.is_allowed(&IpAddr::from([10, 5, 10, 20])));
319        assert!(filter.is_allowed(&IpAddr::from([10, 255, 255, 255])));
320        assert!(!filter.is_allowed(&IpAddr::from([172, 16, 0, 1])));
321        assert!(!filter.is_allowed(&IpAddr::from([8, 8, 8, 8])));
322    }
323
324    #[test]
325    fn test_ipv6_filter() {
326        let filter = IpFilter::from_cidr_string("2001:db8::/32").unwrap();
327        let ipv6_in_range: IpAddr = "2001:db8::1".parse().unwrap();
328        let ipv6_out_range: IpAddr = "2001:db9::1".parse().unwrap();
329
330        assert!(filter.is_allowed(&ipv6_in_range));
331        assert!(!filter.is_allowed(&ipv6_out_range));
332    }
333
334    #[test]
335    fn test_mixed_ipv4_ipv6_filter() {
336        let filter = IpFilter::from_cidr_string("192.168.0.0/16,2001:db8::/32").unwrap();
337
338        assert!(filter.is_allowed(&IpAddr::from([192, 168, 1, 1])));
339        let ipv6_in_range: IpAddr = "2001:db8::1".parse().unwrap();
340        assert!(filter.is_allowed(&ipv6_in_range));
341
342        assert!(!filter.is_allowed(&IpAddr::from([10, 0, 0, 1])));
343        let ipv6_out_range: IpAddr = "2001:db9::1".parse().unwrap();
344        assert!(!filter.is_allowed(&ipv6_out_range));
345    }
346
347    #[test]
348    fn test_empty_string() {
349        let filter = IpFilter::from_cidr_string("").unwrap();
350        assert!(filter.is_allowed(&IpAddr::from([192, 168, 1, 1])));
351        assert!(!filter.has_restrictions());
352    }
353
354    #[test]
355    fn test_invalid_cidr() {
356        assert!(IpFilter::from_cidr_string("invalid").is_err());
357        assert!(IpFilter::from_cidr_string("192.168.0.0/33").is_err());
358        assert!(IpFilter::from_cidr_string("192.168.0.0,10.0.0.0").is_err());
359    }
360
361    #[test]
362    fn test_whitespace_handling() {
363        let filter = IpFilter::from_cidr_string(" 192.168.0.0/16 , 10.0.0.0/8 ").unwrap();
364        assert!(filter.is_allowed(&IpAddr::from([192, 168, 1, 1])));
365        assert!(filter.is_allowed(&IpAddr::from([10, 0, 0, 1])));
366        assert!(!filter.is_allowed(&IpAddr::from([172, 16, 0, 1])));
367    }
368
369    #[test]
370    fn test_single_ip_as_cidr() {
371        let filter = IpFilter::from_cidr_string("192.168.1.100/32").unwrap();
372        assert!(filter.is_allowed(&IpAddr::from([192, 168, 1, 100])));
373        assert!(!filter.is_allowed(&IpAddr::from([192, 168, 1, 101])));
374    }
375}