aboutsummaryrefslogtreecommitdiffstats
path: root/src/transport/libp2p
diff options
context:
space:
mode:
authortv <tv@krebsco.de>2026-11-01 05:00:43 +0100
committertv <tv@krebsco.de>2026-11-01 05:00:43 +0100
commit67a476525bf23fec7aa10a962668cc77cce6c685 (patch)
treea259ef08154e1bc11efee95b22d3f615f564538b /src/transport/libp2p
parentc08574fac02c781b615969943e3d0d95ee402537 (diff)
libp2p: dial newly announced peer addresses
Diffstat (limited to 'src/transport/libp2p')
-rw-r--r--src/transport/libp2p/behaviour.rs4
-rw-r--r--src/transport/libp2p/book.rs38
-rw-r--r--src/transport/libp2p/driver.rs22
-rw-r--r--src/transport/libp2p/mod.rs8
-rw-r--r--src/transport/libp2p/relays.rs23
-rw-r--r--src/transport/libp2p/tests.rs29
6 files changed, 118 insertions, 6 deletions
diff --git a/src/transport/libp2p/behaviour.rs b/src/transport/libp2p/behaviour.rs
index e9b156c..a2e8e91 100644
--- a/src/transport/libp2p/behaviour.rs
+++ b/src/transport/libp2p/behaviour.rs
@@ -244,7 +244,9 @@ impl Behaviour {
dht
});
let identify = ::libp2p::identify::Config::new(IDENTIFY_VERSION.to_owned(), key.public())
- .with_agent_version(format!("continuum/{}", env!("CARGO_PKG_VERSION")));
+ .with_agent_version(format!("continuum/{}", env!("CARGO_PKG_VERSION")))
+ // Connected peers hear of a new interface at once (`explore`).
+ .with_push_listen_addr_updates(true);
let relay = relay.then(|| {
::libp2p::relay::Behaviour::new(
peer_id,
diff --git a/src/transport/libp2p/book.rs b/src/transport/libp2p/book.rs
index 44b2694..7de4be7 100644
--- a/src/transport/libp2p/book.rs
+++ b/src/transport/libp2p/book.rs
@@ -41,6 +41,9 @@ pub(super) struct Book {
/// connection to the peer closes, as a peer restarted elsewhere is found by what
/// the others say.
pub(super) identified: HashMap<PeerId, Vec<Multiaddr>>,
+ /// Addresses of directly connected peers dialled for a better path (`explore`):
+ /// not again while connected, unless our own interfaces change.
+ pub(super) explored: HashMap<PeerId, std::collections::HashSet<Multiaddr>>,
pub(super) listening: Vec<Multiaddr>,
/// Where the router forwards to our listen addresses (UPnP, `Libp2pConfig::upnp`).
pub(super) mapped: Vec<Multiaddr>,
@@ -213,6 +216,7 @@ impl Book {
for peer in &stale {
self.addresses.remove(peer);
self.identified.remove(peer);
+ self.explored.remove(peer);
self.relays.remove(peer);
self.public_relays.remove(peer);
self.continuum.remove(peer);
@@ -475,6 +479,38 @@ impl Book {
addresses.truncate(ADDRESSES_PER_PEER);
}
+ /// The addresses `explore` dials `peer` at, noted as dialled: a node of the swarm
+ /// connected directly, at addresses of its not dialled so yet, and with `again`
+ /// not only those at an IP it isn't connected at (an inbound connection's remote
+ /// port isn't where the peer listens).
+ pub(super) fn to_explore(&mut self, peer: PeerId, again: bool) -> Vec<Multiaddr> {
+ if !self.continuum.contains(&peer)
+ || book_connection(&self.connections, &peer) != Some(Connection::Direct)
+ {
+ return Vec::new();
+ }
+ let used: Vec<Option<std::net::IpAddr>> = self
+ .connections
+ .values()
+ .filter(|(connected, relayed, _)| *connected == peer && !relayed)
+ .map(|(_, _, address)| ip_of(address))
+ .collect();
+ let tried = self.explored.entry(peer).or_default();
+ let candidates: Vec<Multiaddr> = self
+ .addresses
+ .get(&peer)
+ .into_iter()
+ .flatten()
+ .filter(|address| is_ip(address) && !left_out(&self.ignored, address))
+ .filter(|address| again || !used.contains(&ip_of(address)))
+ .filter(|address| !tried.contains(*address))
+ .take(EXPLORED_AT_ONCE)
+ .cloned()
+ .collect();
+ tried.extend(candidates.iter().cloned());
+ candidates
+ }
+
/// `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. Kept in order of `preference`: of a peer with many,
@@ -520,7 +556,7 @@ impl Book {
}
/// The IP address `address` starts with, if any.
-fn ip_of(address: &Multiaddr) -> Option<std::net::IpAddr> {
+pub(super) 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()),
diff --git a/src/transport/libp2p/driver.rs b/src/transport/libp2p/driver.rs
index 145592d..9b4259f 100644
--- a/src/transport/libp2p/driver.rs
+++ b/src/transport/libp2p/driver.rs
@@ -220,6 +220,7 @@ pub(super) async fn run(
book.rtts.remove(&connection_id);
if book.connection(&peer_id).is_none() {
book.identified.remove(&peer_id);
+ book.explored.remove(&peer_id);
}
}
let cause = match cause {
@@ -267,9 +268,23 @@ pub(super) async fn run(
if serving && !left_out && !is_local(&address) && !is_relayed(&address) {
swarm.add_external_address(address.clone());
}
- let mut book = lock(&book);
- if !book.listening.contains(&address) {
- book.listening.push(address);
+ let new_path = !left_out && !is_local(&address) && !is_relayed(&address);
+ {
+ let mut book = lock(&book);
+ if !book.listening.contains(&address) {
+ book.listening.push(address);
+ }
+ // A new interface (ethernet besides the WLAN, say): every
+ // address may be reached better now.
+ if new_path {
+ book.explored.clear();
+ }
+ }
+ if new_path {
+ let peers: Vec<PeerId> = swarm.connected_peers().copied().collect();
+ for peer in peers {
+ explore(&mut swarm, &book, peer, true);
+ }
}
}
SwarmEvent::ExpiredListenAddr { address, .. } => {
@@ -501,6 +516,7 @@ pub(super) async fn run(
}
}
upgrade(&mut swarm, &book, peer_id, relaying.local);
+ explore(&mut swarm, &book, peer_id, false);
}
SwarmEvent::Behaviour(BehaviourEvent::Mdns(
::libp2p::mdns::Event::Discovered(found),
diff --git a/src/transport/libp2p/mod.rs b/src/transport/libp2p/mod.rs
index 293558b..a4fefc3 100644
--- a/src/transport/libp2p/mod.rs
+++ b/src/transport/libp2p/mod.rs
@@ -32,7 +32,11 @@
//! all but one are closed a few seconds later (`DUPLICATE_GRACE`, `Book::duplicates`),
//! by the side with the smaller peer ID: it keeps the direct one with the shortest round
//! trip (libp2p's ping, on each connection), so a path over the LAN wins over one
-//! through a VPN, whichever opened first.
+//! through a VPN, whichever opened first. A peer connected directly is dialled again
+//! where it might be reached better (`explore`): at addresses it tells of later (a new
+//! interface: identify pushes them at once), and at all of them when this node gains
+//! an interface. Otherwise a connection made over the WLAN would stay there once
+//! ethernet comes.
//!
//! A relay says so where it publishes its addresses: next to each, the same address
//! ending in `/p2p-circuit` (where to reserve a slot), which other nodes find in the
@@ -166,6 +170,8 @@ const RELAY_PAUSE: Duration = Duration::from_secs(600);
/// (`upgrade`): first at once, then after a failure no sooner than this, doubling up to
/// `RELAY_PAUSE`.
const UPGRADE_RETRY: Duration = Duration::from_secs(30);
+/// A directly connected peer's addresses dialled at once for a better path (`explore`).
+const EXPLORED_AT_ONCE: usize = 4;
/// A peer nobody mentioned for this long (the member list, identify, mDNS, a
/// connection) is forgotten, unless connected: mostly old peer IDs of nodes restarted
/// without their libp2p key, whose new ones the member list has since. Members are
diff --git a/src/transport/libp2p/relays.rs b/src/transport/libp2p/relays.rs
index dca031c..41d7e1d 100644
--- a/src/transport/libp2p/relays.rs
+++ b/src/transport/libp2p/relays.rs
@@ -175,6 +175,29 @@ pub(super) fn upgrade(
}
}
+/// Dials `peer`, connected directly, at its other addresses, in case one is a better
+/// path: one that came since (a new interface of its, identify tells), or, `again`
+/// (one of ours came), every one, as our side may reach them better now, the one in
+/// use too. Each once while connected (`Book::explored`); a connection that opens is
+/// pinged at once, and of several the fastest is kept (`Book::duplicates`). Without
+/// it, a connection stays on the path it was made on (the WLAN) when a better one comes
+/// (ethernet): a connected peer isn't dialled again.
+pub(super) fn explore(swarm: &mut Swarm<Behaviour>, book: &Mutex<Book>, peer: PeerId, again: bool) {
+ let candidates = lock(book).to_explore(peer, again);
+ if candidates.is_empty() {
+ return;
+ }
+ debug!(peer = %Addressed(SCHEME, &peer), addresses = ?candidates, "Connected directly; dialling other addresses, for a better path");
+ if let Err(error) = swarm.dial(
+ DialOpts::peer_id(peer)
+ .addresses(candidates)
+ .condition(PeerCondition::Always)
+ .build(),
+ ) {
+ debug!(peer = %Addressed(SCHEME, &peer), error = %error, "Couldn't dial for a better path");
+ }
+}
+
/// Tries hole punching again with the peers it failed with that are due (`punching`).
/// libp2p punches as a relayed connection opens, started by the side that was dialled
/// through the relay; that side is where a punch's failure is told, so it is this one.
diff --git a/src/transport/libp2p/tests.rs b/src/transport/libp2p/tests.rs
index 3d11913..ed23b34 100644
--- a/src/transport/libp2p/tests.rs
+++ b/src/transport/libp2p/tests.rs
@@ -696,6 +696,35 @@ fn ignored_addresses_not_kept() {
assert!(!book.knows(them, &[via_vpn]));
}
+/// A node connected directly over the WLAN dials the ethernet address it later hears
+/// of, once; and when an interface of ours comes, every address again, the one in use
+/// too.
+#[test]
+fn better_paths_explored() {
+ let mut book = Book::default();
+ let them = peer();
+ let wlan: Multiaddr = "/ip4/192.168.1.7/tcp/9661".parse().unwrap();
+ let ethernet: Multiaddr = "/ip4/192.168.2.7/tcp/9661".parse().unwrap();
+ book.continuum.insert(them);
+ book.remember(them, wlan.clone());
+ // Connected (inbound: from another port) over the WLAN.
+ book.connections.insert(
+ ConnectionId::new_unchecked(1),
+ (them, false, "/ip4/192.168.1.7/tcp/40000".parse().unwrap()),
+ );
+ assert!(book.to_explore(them, false).is_empty());
+ book.identified(them, vec![wlan.clone(), ethernet.clone()]);
+ assert_eq!(book.to_explore(them, false), [ethernet.clone()]);
+ assert!(book.to_explore(them, false).is_empty());
+ // An interface of ours came (`driver`): all of them again.
+ book.explored.clear();
+ let mut again = book.to_explore(them, true);
+ again.sort();
+ let mut both = vec![wlan, ethernet];
+ both.sort();
+ assert_eq!(again, both);
+}
+
/// Peers nobody mentions any more are forgotten, but connected ones.
#[test]
fn stale_peers_are_forgotten() {