aboutsummaryrefslogtreecommitdiffstats
path: root/src/transport/libp2p
diff options
context:
space:
mode:
authortv <tv@krebsco.de>2026-10-31 20:53:22 +0100
committertv <tv@krebsco.de>2026-10-31 20:53:22 +0100
commit9fa1c4601f3c62555903e549e976915169602b26 (patch)
treec005ac09f07f2959f69bae56f9343f9d8ffde6b5 /src/transport/libp2p
parenta08c430100edfba7b924c957f98294fd79de0e1b (diff)
libp2p: add prefer option
Diffstat (limited to 'src/transport/libp2p')
-rw-r--r--src/transport/libp2p/book.rs66
-rw-r--r--src/transport/libp2p/config.rs14
-rw-r--r--src/transport/libp2p/mod.rs4
-rw-r--r--src/transport/libp2p/tests.rs34
4 files changed, 110 insertions, 8 deletions
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() {