diff options
| author | tv <tv@krebsco.de> | 2026-10-31 20:53:22 +0100 |
|---|---|---|
| committer | tv <tv@krebsco.de> | 2026-10-31 20:53:22 +0100 |
| commit | fb20fcc6d1036fe6de86f2b4388fb55951d9c6ac (patch) | |
| tree | b03791f51e1c22c83b388a33b43c100b9ce68867 /src | |
| parent | 78a927449c87225a4d811f8c1a419584e45e5109 (diff) | |
libp2p: add prefer option
Diffstat (limited to 'src')
| -rw-r--r-- | src/config/mod.rs | 10 | ||||
| -rw-r--r-- | src/simtest/identity.rs | 1 | ||||
| -rw-r--r-- | src/transport/libp2p/book.rs | 66 | ||||
| -rw-r--r-- | src/transport/libp2p/config.rs | 14 | ||||
| -rw-r--r-- | src/transport/libp2p/mod.rs | 4 | ||||
| -rw-r--r-- | src/transport/libp2p/tests.rs | 34 |
6 files changed, 121 insertions, 8 deletions
diff --git a/src/config/mod.rs b/src/config/mod.rs index 2dfefcf..c45f1d0 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -296,6 +296,13 @@ pub(crate) const OPTIONS: &[OptionSpec] = &[ "With the libp2p transport: address ranges libp2p leaves out, e.g. 10.244.0.0/16 and 42::/16 (VPNs): addresses there are neither published (but announced ones) nor offered for hole punching nor dialled, and connections there are refused, whoever dials.", ), option( + "libp2p-prefer", + Kind::List, + "RANGE,[RANGE...]", + None, + "With the libp2p transport: address ranges that matter most, best first, e.g. the LAN's 10.23.0.0/16: of the many addresses a node listening everywhere has, those there are published first, and of a peer's, kept first and dialled with the others. Without, public addresses come first.", + ), + option( "libp2p-relay", Kind::Flag, "BOOL", @@ -507,6 +514,9 @@ pub(crate) struct Settings { pub(crate) libp2p_ignore: Vec<String>, #[serde(default)] #[cfg_attr(not(feature = "libp2p"), allow(dead_code))] + pub(crate) libp2p_prefer: Vec<String>, + #[serde(default)] + #[cfg_attr(not(feature = "libp2p"), allow(dead_code))] pub(crate) libp2p_public: bool, #[cfg_attr(not(feature = "libp2p"), allow(dead_code))] pub(crate) libp2p_key_file: Option<PathBuf>, diff --git a/src/simtest/identity.rs b/src/simtest/identity.rs index 6290ba7..5ed2052 100644 --- a/src/simtest/identity.rs +++ b/src/simtest/identity.rs @@ -224,6 +224,7 @@ async fn nodes_over_libp2p() { mdns: false, upnp: false, ignore: Vec::new(), + prefer: Vec::new(), public: false, memory: true, }) diff --git a/src/transport/libp2p/book.rs b/src/transport/libp2p/book.rs index ceb9d27..b495719 100644 --- a/src/transport/libp2p/book.rs +++ b/src/transport/libp2p/book.rs @@ -48,6 +48,8 @@ pub(super) struct Book { pub(super) announced: Vec<Multiaddr>, /// `Libp2pConfig::ignore`: not published (but announced), not dialled. pub(super) ignored: Vec<IpRange>, + /// `Libp2pConfig::prefer`: addresses there first (`preference`). + pub(super) preferred: Vec<IpRange>, /// `Libp2pConfig::relay`: this node relays, and says so. pub(super) serving: bool, /// Relays named to pick (configured, listed), by peer: picked first. @@ -265,6 +267,7 @@ impl Book { } } direct.retain(|address| !first.contains(address)); + direct.sort_by_key(|address| self.preferred_range(address)); first.extend(direct); first.retain(|address| !ignored(&self.ignored, address)); first @@ -469,9 +472,11 @@ impl Book { /// `peer`, connected, identified itself at `listen` (`published`): its direct /// addresses are those from now on, while connected (`remember`); those through - /// relays and by name stay. + /// relays and by name stay. Kept in order of `preference`: of a peer with many, + /// the ones that matter stay (`ADDRESSES_PER_PEER`). pub(super) fn identified(&mut self, peer: PeerId, listen: Vec<Multiaddr>) { - let listen: Vec<Multiaddr> = listen.into_iter().map(without_peer).collect(); + let mut listen: Vec<Multiaddr> = listen.into_iter().map(without_peer).collect(); + listen.sort_by_key(|address| self.preference(address)); if let Some(known) = self.addresses.get_mut(&peer) { known.retain(|address| !is_ip(address) || listen.contains(address)); } @@ -483,6 +488,63 @@ impl Book { } } +impl Book { + /// Where a peer's `address` comes, first first: in a range `libp2p-prefer` names + /// (by the range's place), then public, then through a relay or by name, then a + /// private one (another LAN, a container bridge). + fn preference(&self, address: &Multiaddr) -> (u8, usize) { + let range = self.preferred_range(address); + if range != usize::MAX { + return (0, range); + } + match ip_of(address).filter(|_| is_ip(address)) { + Some(ip) if is_public(ip) => (1, 0), + Some(_) => (3, 0), + None => (2, 0), + } + } + + /// The place of the first range of `libp2p-prefer` that has `address`; for an + /// address none has, `usize::MAX`: after them. + fn preferred_range(&self, address: &Multiaddr) -> usize { + ip_of(address) + .filter(|_| is_ip(address)) + .and_then(|ip| self.preferred.iter().position(|range| range.contains(ip))) + .unwrap_or(usize::MAX) + } +} + +/// The IP address `address` starts with, if any. +fn ip_of(address: &Multiaddr) -> Option<std::net::IpAddr> { + match address.iter().next() { + Some(Protocol::Ip4(ip)) => Some(ip.into()), + Some(Protocol::Ip6(ip)) => Some(ip.into()), + _ => None, + } +} + +/// Whether `ip` can be reached from the internet, as far as its range tells. +fn is_public(ip: std::net::IpAddr) -> bool { + use std::net::IpAddr; + match ip { + IpAddr::V4(ip) => { + // 100.64.0.0/10: carrier-grade NAT. + let shared = ip.octets()[0] == 100 && (ip.octets()[1] & 0xc0) == 64; + !(ip.is_private() + || ip.is_loopback() + || ip.is_link_local() + || ip.is_unspecified() + || shared) + } + IpAddr::V6(ip) => { + !(ip.is_loopback() + || ip.is_unspecified() + || ip.is_unique_local() + || ip.is_unicast_link_local()) + } + } +} + /// Whether `address` is a direct one at an IP address (not through a relay, nor a name). pub(super) fn is_ip(address: &Multiaddr) -> bool { matches!( diff --git a/src/transport/libp2p/config.rs b/src/transport/libp2p/config.rs index 719f2e8..8b172ba 100644 --- a/src/transport/libp2p/config.rs +++ b/src/transport/libp2p/config.rs @@ -23,6 +23,8 @@ pub(crate) struct Libp2pConfig { pub(crate) upnp: bool, /// Address ranges neither published nor dialled, nor connected over (`Ignoring`). pub(crate) ignore: Vec<IpRange>, + /// Address ranges that matter most, best first (`Book::preference`). + pub(crate) prefer: Vec<IpRange>, /// Use public infrastructure: the IPFS DHT, public relays. pub(crate) public: bool, /// libp2p's in-memory transport instead of TCP and QUIC: for tests. @@ -41,11 +43,11 @@ pub(super) fn parse_addresses(option: &str, addresses: &[String]) -> Result<Vec< .collect() } -/// The ranges of `libp2p-ignore`. -pub(super) fn parse_ranges(ranges: &[String]) -> Result<Vec<IpRange>> { +/// The ranges of option `option`. +pub(super) fn parse_ranges(option: &str, ranges: &[String]) -> Result<Vec<IpRange>> { ranges .iter() - .map(|range| range.parse().context("libp2p-ignore")) + .map(|range| range.parse().with_context(|| option.to_owned())) .collect() } @@ -57,7 +59,8 @@ pub(crate) fn check(settings: &crate::config::Settings) -> Result<()> { parse_addresses("libp2p-listen", &settings.libp2p_listen)?; parse_addresses("libp2p-announce", &settings.libp2p_announce)?; parse_addresses("libp2p-relays", &settings.libp2p_relays)?; - parse_ranges(&settings.libp2p_ignore)?; + parse_ranges("libp2p-ignore", &settings.libp2p_ignore)?; + parse_ranges("libp2p-prefer", &settings.libp2p_prefer)?; if settings.relay_only { if settings.libp2p_listen.is_empty() { bail!("relay-only needs libp2p-listen: where nodes reach the relay"); @@ -88,7 +91,8 @@ impl Libp2pConfig { continuum: !relay_only, mdns: settings.libp2p_mdns, upnp: settings.libp2p_upnp, - ignore: parse_ranges(&settings.libp2p_ignore)?, + ignore: parse_ranges("libp2p-ignore", &settings.libp2p_ignore)?, + prefer: parse_ranges("libp2p-prefer", &settings.libp2p_prefer)?, public: settings.libp2p_public, memory: false, }) diff --git a/src/transport/libp2p/mod.rs b/src/transport/libp2p/mod.rs index f1006e3..0e15a09 100644 --- a/src/transport/libp2p/mod.rs +++ b/src/transport/libp2p/mod.rs @@ -45,7 +45,8 @@ //! //! With `--libp2p-ignore`, address ranges are left out (VPNs): not published, not told //! peers by identify (`Identifying`), not offered for hole punching, not dialled, and -//! connections there refused (`Ignoring`). +//! connections there refused (`Ignoring`). With `--libp2p-prefer`, addresses in the +//! ranges named come first, where only so many are kept (`Book::preference`). //! //! With `--libp2p-upnp`, a node asks its router to forward its listen ports (UPnP), and //! publishes the router's public address for them with its own (`Book::direct`): then @@ -239,6 +240,7 @@ impl Libp2pTransport { let mut book = Book { announced: config.announce, ignored: config.ignore.clone(), + preferred: config.prefer.clone(), serving: config.relay, ..Book::default() }; diff --git a/src/transport/libp2p/tests.rs b/src/transport/libp2p/tests.rs index 12e4fce..9eb2808 100644 --- a/src/transport/libp2p/tests.rs +++ b/src/transport/libp2p/tests.rs @@ -32,6 +32,7 @@ fn memory_node_with( mdns: false, upnp: false, ignore: Vec::new(), + prefer: Vec::new(), public: false, memory: true, }) @@ -179,6 +180,7 @@ async fn announced_addresses_are_published_instead() { mdns: false, upnp: false, ignore: Vec::new(), + prefer: Vec::new(), public: false, memory: true, }) @@ -384,6 +386,7 @@ async fn a_relay_that_is_no_node() { mdns: false, upnp: false, ignore: Vec::new(), + prefer: Vec::new(), public: false, memory: true, }) @@ -601,6 +604,37 @@ fn identify_replaces_direct_addresses() { assert!(book.knows(them, &[old])); } +/// Of a peer with more addresses than are kept, those that matter stay: in the ranges +/// `libp2p-prefer` names first, then public ones, then through relays, then other +/// private ones. +#[test] +fn identified_addresses_preferred_first() { + let mut book = Book { + preferred: vec!["10.23.0.0/16".parse().unwrap()], + ..Book::default() + }; + let (relay, them) = (peer(), peer()); + let lan: Multiaddr = "/ip4/10.23.7.7/tcp/9661".parse().unwrap(); + let public: Multiaddr = "/ip4/198.51.100.7/tcp/9661".parse().unwrap(); + let relayed: Multiaddr = format!("/ip4/192.0.2.1/tcp/9661/p2p/{relay}/p2p-circuit") + .parse() + .unwrap(); + let bridges: Vec<Multiaddr> = (0..ADDRESSES_PER_PEER) + .map(|i| format!("/ip4/172.17.{i}.1/tcp/9661").parse().unwrap()) + .collect(); + let mut listen = bridges.clone(); + listen.extend([relayed.clone(), public.clone(), lan.clone()]); + + book.identified(them, listen); + let known = &book.addresses[&them]; + assert_eq!(known.len(), ADDRESSES_PER_PEER); + assert_eq!(known[..3], [lan.clone(), public, relayed]); + + // Our own: those listened on in the preferred ranges published first. + book.listening = vec![bridges[0].clone(), lan.clone()]; + assert_eq!(book.direct(), [lan, bridges[0].clone()]); +} + /// Peers nobody mentions any more are forgotten, but connected ones. #[test] fn stale_peers_are_forgotten() { |
