From c7e0c0d93ebdac6a63211913ced1e972ef23666a Mon Sep 17 00:00:00 2001 From: tv Date: Thu, 29 Oct 2026 00:07:11 +0100 Subject: libp2p-relay: doesn't speak continuum --- src/transport/libp2p.rs | 40 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/src/transport/libp2p.rs b/src/transport/libp2p.rs index c3e2d98..8746206 100644 --- a/src/transport/libp2p.rs +++ b/src/transport/libp2p.rs @@ -282,6 +282,9 @@ struct Book { lan: std::collections::HashSet, /// Peers that speak continuum (identify says so). continuum: std::collections::HashSet, + /// Peers identify says don't speak continuum, and whether they relay: not sent to + /// (`send_encoded`), so that a seed that's no node says so. + not_nodes: HashMap, /// Peers found on the LAN that speak continuum, with their address: seeds not /// handed out yet (`discovered_seeds`). discovered: Vec, @@ -532,6 +535,7 @@ impl Book { self.relays.remove(peer); self.public_relays.remove(peer); self.continuum.remove(peer); + self.not_nodes.remove(peer); self.lan.remove(peer); self.failed_relays.remove(peer); self.upgrades.remove(peer); @@ -1623,7 +1627,15 @@ async fn run( { book.public_relays.insert(peer_id, address); } + if !info.protocols.contains(&PROTOCOL) + && (book.not_nodes.len() < MAX_PEERS + || book.not_nodes.contains_key(&peer_id)) + { + let relays = info.protocols.contains(&::libp2p::relay::HOP_PROTOCOL_NAME); + book.not_nodes.insert(peer_id, relays); + } if info.protocols.contains(&PROTOCOL) { + book.not_nodes.remove(&peer_id); book.continuum.insert(peer_id); if book.lan.remove(&peer_id) { book.discover(peer_id); @@ -1886,6 +1898,17 @@ impl Transport for Libp2pTransport { .with_context(|| format!("{} is no libp2p peer ID", message.peer))?; let started = std::time::Instant::now(); self.connect(peer).await?; + // The protocol goes with the first data (`swarm_config`), so a peer that doesn't + // speak it seems to take the message: once identify has told, say so. + if let Some(relays) = lock(&self.book).not_nodes.get(&peer).copied() { + if relays { + bail!( + "{peer} is a relay, not a node: give a node behind it as the seed \ + (/p2p-circuit/p2p/, or /p2p/ with libp2p-relays)" + ); + } + bail!("{peer} doesn't speak continuum"); + } let connected = started.elapsed(); let mut control = self.control.clone(); let body = message.body.clone(); @@ -2314,14 +2337,23 @@ mod tests { let (from, _) = at_b.recv().await.unwrap(); assert_eq!(from, a.local_id()); - // The relay itself doesn't speak continuum: it takes no messages (a send to it - // isn't refused, though: the protocol goes with the first data, `swarm_config`, - // so the relay's refusal comes too late; nodes never send to it, as it never - // proves itself a node). + // The relay itself doesn't speak continuum: it takes no messages, and a send to + // it is refused once identify has told (the relay's own refusal comes too late: + // the protocol goes with the first data, `swarm_config`), saying what to give + // as a seed instead. assert_eq!( a.resolve_seed(&r_address.to_string()).await.unwrap(), r.local_id() ); + assert!( + eventually(|| lock(&a.book).not_nodes.get(&r.peer_id) == Some(&true)).await, + "identify didn't tell" + ); + let refused = a.send_message(r.local_id(), &status()).await.unwrap_err(); + assert!( + refused.to_string().contains("is a relay, not a node"), + "{refused:#}" + ); assert!( r.start_consumer(Box::new(|_, _, _| Box::pin(async { Ok(()) }))) .await -- cgit v1.3.1