diff options
| author | tv <tv@krebsco.de> | 2026-11-01 05:22:38 +0100 |
|---|---|---|
| committer | tv <tv@krebsco.de> | 2026-11-01 05:22:38 +0100 |
| commit | 6bab0c4af15988e3c0d4e38de3ee9c1f8c4a8be2 (patch) | |
| tree | 088b1c5156ce237f0be885f620b28a7d1252b333 /src/transport/libp2p/routes.rs | |
| parent | 35eade856a9b445920ec07eacdeaf153239d8ec9 (diff) | |
libp2p: autoignore unroutable address ranges
Diffstat (limited to 'src/transport/libp2p/routes.rs')
| -rw-r--r-- | src/transport/libp2p/routes.rs | 199 |
1 files changed, 199 insertions, 0 deletions
diff --git a/src/transport/libp2p/routes.rs b/src/transport/libp2p/routes.rs new file mode 100644 index 0000000..05e14e8 --- /dev/null +++ b/src/transport/libp2p/routes.rs @@ -0,0 +1,199 @@ +//! Addresses without a route from here, left out as if `--libp2p-ignore` named them: a +//! node without IPv6 to the internet doesn't keep, show or dial peers' public IPv6 +//! addresses; one without a route to another LAN, that LAN's. And with +//! `--libp2p-internet-hosts`, whether there's a route to the internet at all, per +//! family: a node without one doesn't publish its own public addresses of that family. +//! +//! Whether there is a route is asked of the operating system: a UDP socket "connected" +//! to the address (no packet is sent) fails where the routing table has none. Asked +//! per range (`ROUTE_PREFIX_V4`, `ROUTE_PREFIX_V6`) of the addresses heard of, and +//! asked again every `ROUTES_RECHECK`: a range gets a route (IPv6 comes up), and its +//! addresses are taken again as they're heard of next (the member list, identify); +//! one loses its route, and the addresses there are dropped. + +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, UdpSocket}; + +use super::*; + +/// The ranges routes are asked for: an IPv4 /24 or an IPv6 /64, as far as a route +/// usually differs within them. +const ROUTE_PREFIX_V4: u8 = 24; +const ROUTE_PREFIX_V6: u8 = 64; +/// How long an answer is taken for one. +const ROUTES_RECHECK: Duration = Duration::from_secs(30); +/// Ranges remembered at most. +const MAX_RANGES: usize = 1024; +/// How long resolving `--libp2p-internet-hosts` may take, each time. +const RESOLVE_WITHIN: Duration = Duration::from_secs(5); + +/// Whether the routing table has a route to `ip`. +pub(super) fn has_route(ip: IpAddr) -> bool { + let any: SocketAddr = match ip { + IpAddr::V4(_) => (Ipv4Addr::UNSPECIFIED, 0).into(), + IpAddr::V6(_) => (Ipv6Addr::UNSPECIFIED, 0).into(), + }; + UdpSocket::bind(any) + .and_then(|socket| socket.connect((ip, 9))) + .is_ok() +} + +/// The answers, by range, and the question asked (`has_route`, or a stand-in in tests). +pub(super) struct Routes { + known: HashMap<IpRange, (bool, tokio::time::Instant)>, + /// Whether there is a route to the internet, per family (IPv4, IPv6), as last + /// asked (`internet`): own public addresses are published only then. True where + /// nobody asks (no `--libp2p-internet-hosts` of that family). + internet: [bool; 2], + /// `--libp2p-internet-hosts`: names or addresses on the internet, whose routes + /// tell whether there is one to the internet at all. + pub(super) internet_hosts: Vec<String>, + probe: fn(IpAddr) -> bool, +} + +impl Default for Routes { + fn default() -> Self { + // Tests don't depend on the host's routes; those that care set `probe`. + #[cfg(test)] + let probe: fn(IpAddr) -> bool = |_| true; + #[cfg(not(test))] + let probe: fn(IpAddr) -> bool = has_route; + Self::with_probe(probe) + } +} + +/// The range of `ip` routes are asked for. +fn range_of(ip: IpAddr) -> IpRange { + let prefix = if ip.is_ipv4() { + ROUTE_PREFIX_V4 + } else { + ROUTE_PREFIX_V6 + }; + IpRange::masked(ip, prefix) +} + +impl Routes { + /// With `--libp2p-internet-hosts` `internet_hosts`. + pub(super) fn asking(internet_hosts: Vec<String>) -> Self { + Self { + internet_hosts, + ..Self::default() + } + } + + pub(super) fn with_probe(probe: fn(IpAddr) -> bool) -> Self { + Self { + known: HashMap::new(), + internet: [true, true], + internet_hosts: Vec::new(), + probe, + } + } + + /// Whether `ip` has a route from here: asked if not known lately, told when the + /// answer is news (debug). + pub(super) fn routable(&mut self, ip: IpAddr) -> bool { + let range = range_of(ip); + let now = tokio::time::Instant::now(); + if let Some((routable, at)) = self.known.get(&range) + && now.saturating_duration_since(*at) < ROUTES_RECHECK + { + return *routable; + } + let routable = (self.probe)(ip); + let before = self.known.get(&range).map(|(routable, _)| *routable); + if before.is_none() && self.known.len() >= MAX_RANGES { + self.known.clear(); + } + self.known.insert(range, (routable, now)); + announce(range, before, routable); + routable + } + + /// Asks again for the ranges known; returns those that lost their route, whose + /// addresses are to go. + pub(super) fn recheck(&mut self) -> Vec<IpRange> { + let now = tokio::time::Instant::now(); + let mut lost = Vec::new(); + for (range, (routable, at)) in &mut self.known { + let now_routable = (self.probe)(range.network); + if *routable != now_routable { + announce(*range, Some(*routable), now_routable); + if !now_routable { + lost.push(*range); + } + } + *routable = now_routable; + *at = now; + } + lost + } + + /// Whether there's a route to the internet, per family, by the addresses + /// `--libp2p-internet-hosts` resolved to (`resolve_internet_hosts`): one of them + /// with a route is enough. A family none of them has isn't judged (taken as having + /// one). + pub(super) fn internet(&mut self, hosts: &[IpAddr]) { + for (family, name) in ["IPv4", "IPv6"].into_iter().enumerate() { + let of_family: Vec<IpAddr> = hosts + .iter() + .copied() + .filter(|ip| usize::from(ip.is_ipv6()) == family) + .collect(); + let routable = of_family.is_empty() || of_family.iter().any(|ip| (self.probe)(*ip)); + if routable != self.internet[family] { + if routable { + debug!( + family = name, + "A route to the internet again: own public addresses published again" + ); + } else { + debug!( + family = name, + "No route to the internet: own public addresses not published" + ); + } + } + self.internet[family] = routable; + } + } + + /// Whether an own address `ip` is worth publishing: not a public one of a family + /// with no route to the internet. + pub(super) fn publishable(&self, ip: IpAddr) -> bool { + !is_public(ip) || self.internet[usize::from(ip.is_ipv6())] + } +} + +fn announce(range: IpRange, before: Option<bool>, routable: bool) { + match (before, routable) { + (None | Some(true), false) => { + debug!(range = %range, "No route to an address range: its addresses left out until there is"); + } + (Some(false), true) => { + debug!(range = %range, "A route to an address range again: its addresses taken again"); + } + _ => {} + } +} + +/// The addresses `hosts` (names or addresses) resolve to, within `RESOLVE_WITHIN`; a +/// name that doesn't resolve is left out (told at debug level). +pub(super) async fn resolve_internet_hosts(hosts: &[String]) -> Vec<IpAddr> { + let mut addresses = Vec::new(); + for host in hosts { + if let Ok(ip) = host.parse::<IpAddr>() { + addresses.push(ip); + continue; + } + match tokio::time::timeout(RESOLVE_WITHIN, tokio::net::lookup_host((host.as_str(), 0))) + .await + { + Ok(Ok(found)) => addresses.extend(found.map(|address| address.ip())), + Ok(Err(error)) => { + debug!(host = %host, error = %error, "An internet host didn't resolve") + } + Err(_) => debug!(host = %host, "An internet host didn't resolve in time"), + } + } + addresses +} |
