aboutsummaryrefslogtreecommitdiffstats
path: root/src/transport/libp2p/routes.rs
diff options
context:
space:
mode:
authortv <tv@krebsco.de>2026-11-01 05:22:38 +0100
committertv <tv@krebsco.de>2026-11-01 05:22:38 +0100
commit6bab0c4af15988e3c0d4e38de3ee9c1f8c4a8be2 (patch)
tree088b1c5156ce237f0be885f620b28a7d1252b333 /src/transport/libp2p/routes.rs
parent35eade856a9b445920ec07eacdeaf153239d8ec9 (diff)
libp2p: autoignore unroutable address ranges
Diffstat (limited to 'src/transport/libp2p/routes.rs')
-rw-r--r--src/transport/libp2p/routes.rs199
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
+}