1#![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
15pub 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#[derive(Debug, Clone, Default, PartialEq, Eq)]
31pub struct BanList {
32 banned_ips: HashMap<IpAddr, Option<Instant>>,
34 banned_peers: HashMap<PeerId, Option<Instant>>,
36}
37
38impl BanList {
39 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 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 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 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 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 #[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 #[inline]
105 pub fn is_banned_ip(&self, ip: &IpAddr) -> bool {
106 self.banned_ips.contains_key(ip)
107 }
108
109 #[inline]
111 pub fn is_banned_peer(&self, peer_id: &PeerId) -> bool {
112 self.banned_peers.contains_key(peer_id)
113 }
114
115 pub fn unban_ip(&mut self, ip: &IpAddr) {
117 self.banned_ips.remove(ip);
118 }
119
120 pub fn unban_peer(&mut self, peer_id: &PeerId) {
122 self.banned_peers.remove(peer_id);
123 }
124
125 pub fn ban_ip_until(&mut self, ip: IpAddr, until: Instant) {
130 self.ban_ip_with(ip, Some(until));
131 }
132
133 pub fn ban_peer_until(&mut self, node_id: PeerId, until: Instant) {
137 self.ban_peer_with(node_id, Some(until));
138 }
139
140 pub fn ban_ip(&mut self, ip: IpAddr) {
144 self.ban_ip_with(ip, None);
145 }
146
147 pub fn ban_peer(&mut self, node_id: PeerId) {
149 self.ban_peer_with(node_id, None);
150 }
151
152 pub fn ban_peer_with(&mut self, node_id: PeerId, until: Option<Instant>) {
156 self.banned_peers
157 .entry(node_id)
158 .and_modify(|current| extend_ban(current, until))
159 .or_insert(until);
160 }
161
162 pub fn ban_ip_with(&mut self, ip: IpAddr, until: Option<Instant>) {
167 if is_global(&ip) {
168 self.banned_ips
169 .entry(ip)
170 .and_modify(|current| extend_ban(current, until))
171 .or_insert(until);
172 }
173 }
174}
175
176fn extend_ban(current: &mut Option<Instant>, until: Option<Instant>) {
178 *current = current.zip(until).map(|(current, until)| current.max(until));
179}
180
181#[cfg(test)]
182mod tests {
183 use super::*;
184
185 #[test]
186 fn can_ban_unban_peer() {
187 let peer = PeerId::repeat_byte(1);
188 let mut banlist = BanList::default();
189 banlist.ban_peer(peer);
190 assert!(banlist.is_banned_peer(&peer));
191 banlist.unban_peer(&peer);
192 assert!(!banlist.is_banned_peer(&peer));
193 }
194
195 #[test]
196 fn can_ban_unban_ip() {
197 let ip = IpAddr::from([1, 1, 1, 1]);
198 let mut banlist = BanList::default();
199 banlist.ban_ip(ip);
200 assert!(banlist.is_banned_ip(&ip));
201 banlist.unban_ip(&ip);
202 assert!(!banlist.is_banned_ip(&ip));
203 }
204
205 #[test]
206 fn cannot_ban_non_global() {
207 let mut ip = IpAddr::from([0, 0, 0, 0]);
208 let mut banlist = BanList::default();
209 banlist.ban_ip(ip);
210 assert!(!banlist.is_banned_ip(&ip));
211
212 ip = IpAddr::from([10, 0, 0, 0]);
213 banlist.ban_ip(ip);
214 assert!(!banlist.is_banned_ip(&ip));
215
216 ip = IpAddr::from([127, 0, 0, 0]);
217 banlist.ban_ip(ip);
218 assert!(!banlist.is_banned_ip(&ip));
219
220 ip = IpAddr::from([172, 17, 0, 0]);
221 banlist.ban_ip(ip);
222 assert!(!banlist.is_banned_ip(&ip));
223
224 ip = IpAddr::from([172, 16, 0, 0]);
225 banlist.ban_ip(ip);
226 assert!(!banlist.is_banned_ip(&ip));
227 }
228
229 #[test]
230 fn bans_only_extend_until_explicitly_removed() {
231 let now = Instant::now();
232 let later = now + std::time::Duration::from_secs(60);
233 let peer = PeerId::repeat_byte(1);
234 let ip = IpAddr::from([1, 1, 1, 1]);
235 for (initial, update, expected) in [
236 (None, Some(now), None),
237 (Some(later), Some(now), Some(later)),
238 (Some(now), Some(later), Some(later)),
239 (Some(now), None, None),
240 ] {
241 let mut bans = BanList::default();
242 bans.ban_peer_with(peer, initial);
243 bans.ban_ip_with(ip, initial);
244 bans.ban_peer_with(peer, update);
245 bans.ban_ip_with(ip, update);
246 assert_eq!(bans.banned_peers[&peer], expected);
247 assert_eq!(bans.banned_ips[&ip], expected);
248 bans.evict(now + std::time::Duration::from_secs(1));
249 assert!(bans.is_banned(&peer, &ip));
250 bans.unban_peer(&peer);
251 bans.unban_ip(&ip);
252 assert!(!bans.is_banned(&peer, &ip));
253 bans.ban_peer_until(peer, now);
254 bans.ban_ip_until(ip, now);
255 bans.evict(later);
256 assert!(!bans.is_banned(&peer, &ip));
257 }
258 }
259}
260
261#[derive(Debug, Clone, PartialEq, Eq)]
263pub struct IpFilter {
264 allowed_networks: Vec<ipnet::IpNet>,
267}
268
269impl IpFilter {
270 pub const fn new(allowed_networks: Vec<ipnet::IpNet>) -> Self {
274 Self { allowed_networks }
275 }
276
277 pub fn from_cidr_string(cidrs: &str) -> Result<Self, ipnet::AddrParseError> {
283 if cidrs.is_empty() {
284 return Ok(Self::allow_all())
285 }
286
287 let networks = cidrs
288 .split(',')
289 .map(|s| s.trim())
290 .filter(|s| !s.is_empty())
291 .map(ipnet::IpNet::from_str)
292 .collect::<Result<Vec<_>, _>>()?;
293
294 Ok(Self::new(networks))
295 }
296
297 pub const fn allow_all() -> Self {
299 Self { allowed_networks: Vec::new() }
300 }
301
302 pub fn is_allowed(&self, ip: &IpAddr) -> bool {
307 if self.allowed_networks.is_empty() {
309 return true
310 }
311
312 self.allowed_networks.iter().any(|net| net.contains(ip))
314 }
315
316 pub const fn has_restrictions(&self) -> bool {
318 !self.allowed_networks.is_empty()
319 }
320
321 pub fn allowed_networks(&self) -> &[ipnet::IpNet] {
323 &self.allowed_networks
324 }
325}
326
327impl Default for IpFilter {
328 fn default() -> Self {
329 Self::allow_all()
330 }
331}
332
333#[cfg(test)]
334mod ip_filter_tests {
335 use super::*;
336
337 #[test]
338 fn test_allow_all_filter() {
339 let filter = IpFilter::allow_all();
340 assert!(filter.is_allowed(&IpAddr::from([192, 168, 1, 1])));
341 assert!(filter.is_allowed(&IpAddr::from([10, 0, 0, 1])));
342 assert!(filter.is_allowed(&IpAddr::from([8, 8, 8, 8])));
343 assert!(!filter.has_restrictions());
344 }
345
346 #[test]
347 fn test_single_network_filter() {
348 let filter = IpFilter::from_cidr_string("192.168.0.0/16").unwrap();
349 assert!(filter.is_allowed(&IpAddr::from([192, 168, 1, 1])));
350 assert!(filter.is_allowed(&IpAddr::from([192, 168, 255, 255])));
351 assert!(!filter.is_allowed(&IpAddr::from([192, 169, 1, 1])));
352 assert!(!filter.is_allowed(&IpAddr::from([10, 0, 0, 1])));
353 assert!(filter.has_restrictions());
354 }
355
356 #[test]
357 fn test_multiple_networks_filter() {
358 let filter = IpFilter::from_cidr_string("192.168.0.0/16,10.0.0.0/8").unwrap();
359 assert!(filter.is_allowed(&IpAddr::from([192, 168, 1, 1])));
360 assert!(filter.is_allowed(&IpAddr::from([10, 5, 10, 20])));
361 assert!(filter.is_allowed(&IpAddr::from([10, 255, 255, 255])));
362 assert!(!filter.is_allowed(&IpAddr::from([172, 16, 0, 1])));
363 assert!(!filter.is_allowed(&IpAddr::from([8, 8, 8, 8])));
364 }
365
366 #[test]
367 fn test_ipv6_filter() {
368 let filter = IpFilter::from_cidr_string("2001:db8::/32").unwrap();
369 let ipv6_in_range: IpAddr = "2001:db8::1".parse().unwrap();
370 let ipv6_out_range: IpAddr = "2001:db9::1".parse().unwrap();
371
372 assert!(filter.is_allowed(&ipv6_in_range));
373 assert!(!filter.is_allowed(&ipv6_out_range));
374 }
375
376 #[test]
377 fn test_mixed_ipv4_ipv6_filter() {
378 let filter = IpFilter::from_cidr_string("192.168.0.0/16,2001:db8::/32").unwrap();
379
380 assert!(filter.is_allowed(&IpAddr::from([192, 168, 1, 1])));
381 let ipv6_in_range: IpAddr = "2001:db8::1".parse().unwrap();
382 assert!(filter.is_allowed(&ipv6_in_range));
383
384 assert!(!filter.is_allowed(&IpAddr::from([10, 0, 0, 1])));
385 let ipv6_out_range: IpAddr = "2001:db9::1".parse().unwrap();
386 assert!(!filter.is_allowed(&ipv6_out_range));
387 }
388
389 #[test]
390 fn test_empty_string() {
391 let filter = IpFilter::from_cidr_string("").unwrap();
392 assert!(filter.is_allowed(&IpAddr::from([192, 168, 1, 1])));
393 assert!(!filter.has_restrictions());
394 }
395
396 #[test]
397 fn test_invalid_cidr() {
398 assert!(IpFilter::from_cidr_string("invalid").is_err());
399 assert!(IpFilter::from_cidr_string("192.168.0.0/33").is_err());
400 assert!(IpFilter::from_cidr_string("192.168.0.0,10.0.0.0").is_err());
401 }
402
403 #[test]
404 fn test_whitespace_handling() {
405 let filter = IpFilter::from_cidr_string(" 192.168.0.0/16 , 10.0.0.0/8 ").unwrap();
406 assert!(filter.is_allowed(&IpAddr::from([192, 168, 1, 1])));
407 assert!(filter.is_allowed(&IpAddr::from([10, 0, 0, 1])));
408 assert!(!filter.is_allowed(&IpAddr::from([172, 16, 0, 1])));
409 }
410
411 #[test]
412 fn test_single_ip_as_cidr() {
413 let filter = IpFilter::from_cidr_string("192.168.1.100/32").unwrap();
414 assert!(filter.is_allowed(&IpAddr::from([192, 168, 1, 100])));
415 assert!(!filter.is_allowed(&IpAddr::from([192, 168, 1, 101])));
416 }
417}