diff options
| author | tv <tv@krebsco.de> | 2026-10-29 05:33:37 +0100 |
|---|---|---|
| committer | tv <tv@krebsco.de> | 2026-10-29 05:33:37 +0100 |
| commit | badd492779e401b5ef4719c14291bcf86df8c80d (patch) | |
| tree | 7e276220bddded4c7aab4e12f31818b8bff8fbbb /src/transport/libp2p.rs | |
| parent | e36d7c78565560af223950186256896d05f58010 (diff) | |
libp2p: retry punching
Diffstat (limited to 'src/transport/libp2p.rs')
| -rw-r--r-- | src/transport/libp2p.rs | 141 |
1 files changed, 126 insertions, 15 deletions
diff --git a/src/transport/libp2p.rs b/src/transport/libp2p.rs index 5419f46..a5541f6 100644 --- a/src/transport/libp2p.rs +++ b/src/transport/libp2p.rs @@ -20,7 +20,8 @@ //! (`--libp2p-relays`), on which it reserves a slot; its address through the relay //! (`<relay>/p2p-circuit/p2p/<it>`) is published with the others. Over a relayed connection, both sides then try to //! connect directly (libp2p's DCUtR, hole punching); the relay carries the traffic until -//! that works, or for good if it doesn't, within its limits (`RELAY_CIRCUIT_DURATION`, +//! that works, or for good if it doesn't (punched again now and then, `PUNCH_RETRIES`), +//! within its limits (`RELAY_CIRCUIT_DURATION`, //! `RELAY_CIRCUIT_BYTES`; a circuit that ends is made again on the next send). Hole //! punching needs both sides to listen (`HolePunching`: at their LAN addresses too); //! where only one does, the other dials it directly once it knows where (`upgrade`). @@ -156,6 +157,19 @@ const PRUNE_EVERY: Duration = Duration::from_secs(60); /// the last one opened: long enough for what it was dialled for, and for each to be /// pinged (at once, as it opens). Looked for every `DEDUPE_EVERY`. const DUPLICATE_GRACE: Duration = Duration::from_secs(10); +/// The same while a peer is connected only through relays: a new relayed connection is +/// a hole punch under way (`PUNCH_RETRIES`), which takes its tries (up to a QUIC +/// handshake's 5 s each, three times) on that connection. +const PUNCH_GRACE: Duration = Duration::from_secs(60); +/// After libp2p's hole punching gave up on a peer (three tries, as the relayed +/// connection opened), it tries again after these, the last one over and over: what +/// failed may have been passing (a NAT's mapping, a busy link). Each time through a new +/// connection through the relay, on which the peer starts the punch (`retry_punches`). +const PUNCH_RETRIES: [Duration; 3] = [ + Duration::from_secs(60), + Duration::from_secs(300), + Duration::from_secs(1800), +]; const DEDUPE_EVERY: Duration = Duration::from_secs(5); /// How long a message of `bytes` may take on a stream. @@ -296,6 +310,9 @@ struct Book { upgrades: HashMap<PeerId, (tokio::time::Instant, Duration)>, /// The shortest round trip measured on each open connection (libp2p's ping). rtts: HashMap<ConnectionId, Duration>, + /// Peers whose hole punching failed: how often in a row, and when to try again + /// (`PUNCH_RETRIES`). + punching: HashMap<PeerId, (usize, tokio::time::Instant)>, /// Open connections: to whom, whether through a relay, and the remote address. connections: HashMap<ConnectionId, (PeerId, bool, Multiaddr)>, /// What AutoNAT says, and the address it confirmed while public. @@ -454,16 +471,7 @@ fn count_dial_failure(failures: &mut DialFailures, error: &::libp2p::swarm::Dial impl Book { /// How we are connected to `peer`: directly if any connection is direct. fn connection(&self, peer: &PeerId) -> Option<Connection> { - let mut found = None; - for (connected, relayed, _) in self.connections.values() { - if connected == peer { - if !relayed { - return Some(Connection::Direct); - } - found = Some(Connection::Relayed); - } - } - found + book_connection(&self.connections, peer) } /// The connections to `peer` to close, of several: all but one, the oldest direct @@ -567,6 +575,7 @@ impl Book { self.lan.remove(peer); self.failed_relays.remove(peer); self.upgrades.remove(peer); + self.punching.remove(peer); self.seen.remove(peer); } stale.len() @@ -972,6 +981,24 @@ impl ::libp2p::swarm::NetworkBehaviour for Ignoring { } } +/// How `peer` is connected, of `connections` (`Book::connections`): directly if one is +/// direct. +fn book_connection( + connections: &HashMap<ConnectionId, (PeerId, bool, Multiaddr)>, + peer: &PeerId, +) -> Option<Connection> { + let mut found = None; + for (connected, relayed, _) in connections.values() { + if connected == peer { + if !relayed { + return Some(Connection::Direct); + } + found = Some(Connection::Relayed); + } + } + found +} + fn dialable(known: &[Multiaddr], local: PeerId) -> Vec<Multiaddr> { known .iter() @@ -1612,6 +1639,67 @@ fn upgrade(swarm: &mut Swarm<Behaviour>, book: &Mutex<Book>, peer: PeerId, local ); } +/// 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. +/// A new relayed connection starts a new punch: we dial the peer through a relay if it +/// has a slot on one; else we close our relayed connections to it, and its next message +/// (a probe, within a minute) dials us through ours again. Peers connected directly by +/// now are dropped; those not connected now are kept, failures and all. +fn retry_punches(swarm: &mut Swarm<Behaviour>, book: &Mutex<Book>, local: PeerId) { + let now = tokio::time::Instant::now(); + let mut close: Vec<ConnectionId> = Vec::new(); + let due: Vec<(PeerId, usize, Vec<Multiaddr>)> = { + let mut book = lock(book); + let book = &mut *book; + let mut due = Vec::new(); + book.punching.retain(|peer, (failed, next)| { + match book_connection(&book.connections, peer) { + Some(Connection::Direct) => return false, + // Kept, failures and all: one we closed to have it dial anew comes back. + None => return true, + Some(Connection::Relayed) if now < *next => return true, + Some(Connection::Relayed) => {} + } + let through: Vec<Multiaddr> = book + .addresses + .get(peer) + .map(|known| dialable(known, local)) + .unwrap_or_default() + .into_iter() + .filter(is_relayed) + .collect(); + // Until its punch fails again (or the peer tries from its side). + *next = now + PUNCH_RETRIES[(*failed).min(PUNCH_RETRIES.len() - 1)]; + if through.is_empty() { + close.extend( + book.connections + .iter() + .filter(|(_, (connected, relayed, _))| connected == peer && *relayed) + .map(|(id, _)| *id), + ); + } else { + due.push((*peer, *failed, through)); + } + true + }); + due + }; + for connection in close { + debug!(connection = %connection, "Hole punching again: closing a relayed connection, for the peer to dial anew"); + swarm.close_connection(connection); + } + for (peer, failed, through) in due { + debug!(peer = %Addressed(SCHEME, &peer), failed, "Hole punching again, through a new relayed connection"); + let _ = swarm.dial( + DialOpts::peer_id(peer) + .addresses(through) + .condition(PeerCondition::Always) + .build(), + ); + } +} + /// Asks the DHT (with `--libp2p-public`) where `peer` is, for the next attempt. fn find_on_dht(swarm: &mut Swarm<Behaviour>, peer: PeerId) { if let Some(dht) = swarm.behaviour_mut().kad.as_mut() { @@ -1687,6 +1775,7 @@ async fn run( } _ = check.tick() => { pick_relays(&mut swarm, &book, &relaying, &nat, &mut picked); + retry_punches(&mut swarm, &book, relaying.local); } command = commands.recv() => match command { None => break, @@ -1779,11 +1868,17 @@ async fn run( ); if !relayed { book.upgrades.remove(&peer_id); + book.punching.remove(&peer_id); } if relaying.local < peer_id && book.connections.values().filter(|(connected, _, _)| *connected == peer_id).count() > 1 { - dedupe_due.insert(peer_id, tokio::time::Instant::now() + DUPLICATE_GRACE); + // Only through relays: maybe a punch under way. + let grace = match book.connection(&peer_id) { + Some(Connection::Relayed) => PUNCH_GRACE, + _ => DUPLICATE_GRACE, + }; + dedupe_due.insert(peer_id, tokio::time::Instant::now() + grace); } debug!( peer = %Addressed(SCHEME, &peer_id), @@ -2005,12 +2100,28 @@ async fn run( }, SwarmEvent::Behaviour(BehaviourEvent::Dcutr(event)) => match event.result { Ok(_) => { - lock(&book).counts.hole_punches.succeeded += 1; + let mut book = lock(&book); + book.counts.hole_punches.succeeded += 1; + book.punching.remove(&event.remote_peer_id); + drop(book); info!(peer = %Addressed(SCHEME, &event.remote_peer_id), "Connected directly to a peer behind a relay"); } Err(error) => { - lock(&book).counts.hole_punches.failed += 1; - debug!(peer = %Addressed(SCHEME, &event.remote_peer_id), error = %error, "Couldn't connect directly to a peer behind a relay; the relay carries on"); + let retry = { + let mut book = lock(&book); + book.counts.hole_punches.failed += 1; + let failed = book + .punching + .get(&event.remote_peer_id) + .map_or(0, |(failed, _)| *failed); + let retry = PUNCH_RETRIES[failed.min(PUNCH_RETRIES.len() - 1)]; + book.punching.insert( + event.remote_peer_id, + (failed + 1, tokio::time::Instant::now() + retry), + ); + retry + }; + debug!(peer = %Addressed(SCHEME, &event.remote_peer_id), error = %error, retry = ?retry, "Couldn't connect directly to a peer behind a relay; the relay carries on"); } }, SwarmEvent::Behaviour(BehaviourEvent::Identify( |
