From eb22c19d2dae0ccca1fc5b9f07f25ca09c55a153 Mon Sep 17 00:00:00 2001 From: Darius Date: Tue, 2 Sep 2025 09:18:41 -0400 Subject: [PATCH 01/51] feat: implement autorelay behaviour --- protocols/relay/src/autorelay.rs | 433 +++++++++++++++++++++++ protocols/relay/src/autorelay/handler.rs | 106 ++++++ protocols/relay/src/lib.rs | 1 + protocols/relay/src/multiaddr_ext.rs | 38 ++ 4 files changed, 578 insertions(+) create mode 100644 protocols/relay/src/autorelay.rs create mode 100644 protocols/relay/src/autorelay/handler.rs diff --git a/protocols/relay/src/autorelay.rs b/protocols/relay/src/autorelay.rs new file mode 100644 index 00000000000..27bc66f47f3 --- /dev/null +++ b/protocols/relay/src/autorelay.rs @@ -0,0 +1,433 @@ +use crate::autorelay::handler::Out; +use crate::multiaddr_ext::MultiaddrExt; +use either::Either; +use libp2p_core::multiaddr::Protocol; +use libp2p_core::transport::{ListenerId, PortUse}; +use libp2p_core::Endpoint; +use libp2p_identity::PeerId; +use libp2p_swarm::derive_prelude::{ + AddressChange, ConnectionClosed, ConnectionDenied, ConnectionEstablished, ConnectionId, + DialFailure, ExpiredListenAddr, FromSwarm, ListenerClosed, ListenerError, Multiaddr, + NetworkBehaviour, THandler, THandlerInEvent, THandlerOutEvent, ToSwarm, +}; +use libp2p_swarm::{dummy, ExternalAddresses, ListenOpts, NewListenAddr}; +use std::collections::{HashMap, HashSet, VecDeque}; +use std::num::NonZeroU8; +use std::task::{Context, Poll, Waker}; + +mod handler; + +#[derive(Default, Debug)] +pub struct Behaviour { + config: Config, + external_addresses: ExternalAddresses, + events: VecDeque::ToSwarm, THandlerInEvent>>, + + connections: HashMap<(PeerId, ConnectionId), Connection>, + + pending_reservation: HashMap, + pending_target: HashSet<(PeerId, ConnectionId)>, + + waker: Option, +} + +#[derive(Debug)] +struct Connection { + address: Multiaddr, + relay_status: RelayStatus, +} + +impl Connection { + /// Mark relayed connection as not supported + pub fn disqualify_connection(&mut self) -> bool { + match self.address.is_relayed() { + true => { + self.relay_status = RelayStatus::NotSupported; + true + } + false => { + self.relay_status = RelayStatus::Pending; + false + } + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum RelayStatus { + Supported { status: ReservationStatus }, + NotSupported, + Pending, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum ReservationStatus { + Idle, + Pending { id: ListenerId }, + Active { id: ListenerId }, +} + +#[derive(Debug)] +pub struct Config { + max_reservations: NonZeroU8, +} + +impl Default for Config { + fn default() -> Self { + Self { + max_reservations: NonZeroU8::new(2).unwrap(), + } + } +} + +impl Config { + pub fn set_max_reservations(mut self, max_reservations: u8) -> Self { + assert!(max_reservations > 0); + self.max_reservations = NonZeroU8::new(max_reservations).expect("greater than zero"); + self + } +} + +#[derive(Debug)] +#[non_exhaustive] +pub enum Event {} + +impl Behaviour { + + pub fn new_with_config(config: Config) -> Self { + Self { + config, + ..Default::default() + } + } + + fn select_connection_for_reservation( + &mut self, + (peer_id, connection_id): (PeerId, ConnectionId), + ) -> bool { + if self.pending_target.contains(&(peer_id, connection_id)) { + return false; + } + + let info = self + .connections + .get_mut(&(peer_id, connection_id)) + .expect("connection is present"); + + let addr_with_peer_id = match info.address.clone().with_p2p(peer_id) { + Ok(addr) => addr, + Err(addr) => { + tracing::warn!(%addr, "address unexpectedly contains a different peer id than the connection"); + return false; + } + }; + + let relay_addr = addr_with_peer_id.with(Protocol::P2pCircuit); + + let opts = ListenOpts::new(relay_addr); + + let id = opts.listener_id(); + + info.relay_status = RelayStatus::Supported { + status: ReservationStatus::Pending { id }, + }; + self.pending_reservation + .insert(id, (peer_id, connection_id)); + self.events.push_back(ToSwarm::ListenOn { opts }); + self.pending_target.insert((peer_id, connection_id)); + + true + } + + fn meet_reservation_target(&mut self) { + // check to determine if there is a public external address that could possibly let us know the node + // is reachable + if self + .external_addresses + .iter() + .any(|addr| addr.is_public() && !addr.is_relayed()) + { + return; + } + + let max_reservation = self.config.max_reservations.get() as usize; + + let peers_not_supported = self.connections.is_empty() + || self + .connections + .iter() + .all(|(_, connection)| connection.relay_status == RelayStatus::NotSupported); + + if peers_not_supported { + return; + } + + let relayed_targets = self + .connections + .iter() + .filter(|(_, info)| { + matches!( + info.relay_status, + RelayStatus::Supported { + status: ReservationStatus::Active { .. } + } + ) + }) + .count(); + + if relayed_targets == max_reservation { + return; + } + + let pending_target_len = self.pending_target.len(); + + if pending_target_len >= max_reservation { + return; + } + + let possible_targets = self + .connections + .iter() + .filter(|(_, info)| { + matches!( + info.relay_status, + RelayStatus::Supported { + status: ReservationStatus::Idle + } + ) + }) + .map(|((peer_id, connection_id), _)| (*peer_id, *connection_id)) + .collect::>(); + + let targets_count = std::cmp::min(possible_targets.len(), max_reservation); + + if targets_count == 0 { + return; + } + + let remaining_targets_needed = targets_count + .checked_sub(self.pending_target.len()) + .unwrap_or_default(); + + if remaining_targets_needed == 0 { + return; + } + + for (peer_id, connection_id) in possible_targets + .iter() + .copied() + .take(remaining_targets_needed) + { + if !self.select_connection_for_reservation((peer_id, connection_id)) { + continue; + } + + if self.pending_target.len() == max_reservation { + break; + } + } + + debug_assert!(self.pending_target.len() <= max_reservation); + } + + fn active_reservations(&self) -> usize { + self.connections.values().filter(|info| matches!(info.relay_status, RelayStatus::Supported { status: ReservationStatus::Active { .. } })).count() + } + + // fn on_connection_established( + // &mut self, + // peer_id: PeerId, + // connection_id: ConnectionId, + // connection_id: ConnectionId, + // address: Multiaddr, + // ) { + // } + // + // fn on_connection_closed(&mut self, peer_id: PeerId, connection_id: ConnectionId) {} + // + // fn on_address_change( + // &mut self, + // peer_id: PeerId, + // connection_id: ConnectionId, + // old_addr: &Multiaddr, + // new_addr: &Multiaddr, + // ) { + // } + // + // fn on_new_listen_addr(&mut self, listener_id: ListenerId, addr: &Multiaddr) {} + // fn on_expired_listen_addr(&mut self, listener_id: ListenerId) {} + // fn on_listener_error(&mut self, listener_id: ListenerId, error: &std::io::Error) {} + // fn on_listener_closed(&mut self, listener_id: ListenerId) {} +} + +impl NetworkBehaviour for Behaviour { + type ConnectionHandler = Either; + type ToSwarm = Event; + + fn handle_established_inbound_connection( + &mut self, + _connection_id: ConnectionId, + _peer: PeerId, + local_addr: &Multiaddr, + _remote_addr: &Multiaddr, + ) -> Result, ConnectionDenied> { + if local_addr.is_relayed() { + Ok(Either::Right(dummy::ConnectionHandler)) + } else { + Ok(Either::Left(handler::Handler::default())) + } + } + + fn handle_established_outbound_connection( + &mut self, + _connection_id: ConnectionId, + _peer: PeerId, + addr: &Multiaddr, + _role_override: Endpoint, + _port_use: PortUse, + ) -> Result, ConnectionDenied> { + if addr.is_relayed() { + Ok(Either::Right(dummy::ConnectionHandler)) + } else { + Ok(Either::Left(handler::Handler::default())) + } + } + + fn on_swarm_event(&mut self, event: FromSwarm) { + let _change = self.external_addresses.on_swarm_event(&event); + + match event { + FromSwarm::ConnectionEstablished(ConnectionEstablished { + peer_id, + endpoint, + connection_id, + .. + }) => { + let remote_addr = endpoint.get_remote_address().clone(); + + let mut connection = Connection { + address: remote_addr, + relay_status: RelayStatus::Pending, + }; + + connection.disqualify_connection(); + + self.connections + .insert((peer_id, connection_id), connection); + } + FromSwarm::ConnectionClosed(ConnectionClosed { + peer_id, + connection_id, + .. + }) => { + let connection = self + .connections + .remove(&(peer_id, connection_id)) + .expect("valid connection"); + + let id = match connection.relay_status { + RelayStatus::Supported { status: ReservationStatus::Active { id } } => id, + RelayStatus::Supported { status: ReservationStatus::Pending { id } } => id, + _ => return, + }; + + self.pending_reservation.remove(&id); + self.pending_target.remove(&(peer_id, connection_id)); + + let max_reservation = self.config.max_reservations.get() as usize; + + let active_reservations = self.active_reservations(); + + if active_reservations < max_reservation { + self.meet_reservation_target(); + } + } + FromSwarm::AddressChange(AddressChange { + peer_id, + connection_id, + old, + new, + }) => { + let connection = self + .connections + .get_mut(&(peer_id, connection_id)) + .expect("valid connection"); + + let old_addr = old.get_remote_address(); + let new_addr = new.get_remote_address(); + + debug_assert!(old_addr != new_addr); + + connection.address = new_addr.clone(); + } + FromSwarm::NewListenAddr(NewListenAddr { listener_id, addr }) => { + // we only care about any new relayed address + if !addr.iter().any(|protocol| protocol == Protocol::P2pCircuit) { + return; + } + + let Some((peer_id, connection_id)) = self.pending_reservation.remove(&listener_id) + else { + return; + }; + + let connection = self + .connections + .get_mut(&(peer_id, connection_id)) + .expect("valid connection"); + + let RelayStatus::Supported { + status: ReservationStatus::Pending { id }, + } = connection.relay_status + else { + return; + }; + + connection.relay_status = RelayStatus::Supported { + status: ReservationStatus::Active { id }, + }; + } + FromSwarm::ExpiredListenAddr(_) => {} + FromSwarm::ListenerError(_) => {} + FromSwarm::ListenerClosed(_) => {} + _ => {} + } + } + + fn on_connection_handler_event( + &mut self, + peer_id: PeerId, + connection_id: ConnectionId, + event: THandlerOutEvent, + ) { + let Either::Left(event) = event; + + let Some(connection) = self.connections.get_mut(&(peer_id, connection_id)) else { + return; + }; + + match event { + Out::Supported => { + connection.relay_status = RelayStatus::Supported { + status: ReservationStatus::Idle, + }; + self.meet_reservation_target(); + } + Out::Unsupported => { + let _previous_status = connection.relay_status; + connection.relay_status = RelayStatus::NotSupported; + } + } + } + + fn poll( + &mut self, + cx: &mut Context<'_>, + ) -> Poll>> { + if let Some(event) = self.events.pop_front() { + return Poll::Ready(event); + } + self.waker.replace(cx.waker().clone()); + Poll::Pending + } +} diff --git a/protocols/relay/src/autorelay/handler.rs b/protocols/relay/src/autorelay/handler.rs new file mode 100644 index 00000000000..4306e53b7bd --- /dev/null +++ b/protocols/relay/src/autorelay/handler.rs @@ -0,0 +1,106 @@ +use std::{ + collections::VecDeque, + task::{Context, Poll}, +}; + +use libp2p_core::upgrade::DeniedUpgrade; + +use crate::HOP_PROTOCOL_NAME; +use libp2p_swarm::{ + handler::ConnectionEvent, ConnectionHandler, ConnectionHandlerEvent, SubstreamProtocol, + SupportedProtocols, +}; + +#[derive(Default, Debug)] +pub struct Handler { + events: VecDeque< + ConnectionHandlerEvent< + ::OutboundProtocol, + ::OutboundOpenInfo, + ::ToBehaviour, + >, + >, + + supported: bool, + + supported_protocol: SupportedProtocols, +} + +#[derive(Debug, Copy, Clone)] +pub enum Out { + Supported, + Unsupported, +} + +#[allow(deprecated)] +impl ConnectionHandler for Handler { + type FromBehaviour = (); + type ToBehaviour = Out; + type InboundProtocol = DeniedUpgrade; + type OutboundProtocol = DeniedUpgrade; + type InboundOpenInfo = (); + type OutboundOpenInfo = (); + + fn listen_protocol(&self) -> SubstreamProtocol { + SubstreamProtocol::new(DeniedUpgrade, ()) + } + + fn connection_keep_alive(&self) -> bool { + false + } + + fn on_behaviour_event(&mut self, _event: Self::FromBehaviour) {} + + fn on_connection_event( + &mut self, + event: ConnectionEvent< + Self::InboundProtocol, + Self::OutboundProtocol, + Self::InboundOpenInfo, + Self::OutboundOpenInfo, + >, + ) { + match event { + ConnectionEvent::RemoteProtocolsChange(protocol) + | ConnectionEvent::LocalProtocolsChange(protocol) => { + let change = self.supported_protocol.on_protocols_change(protocol); + if change { + let valid = self + .supported_protocol + .iter() + .any(|proto| HOP_PROTOCOL_NAME.eq(proto)); + + match (valid, self.supported) { + (true, false) => { + self.supported = true; + self.events + .push_back(ConnectionHandlerEvent::NotifyBehaviour(Out::Supported)); + } + (false, true) => { + self.supported = false; + self.events + .push_back(ConnectionHandlerEvent::NotifyBehaviour( + Out::Unsupported, + )); + } + (true, true) => {} + _ => {} + } + } + } + _ => {} + } + } + + fn poll( + &mut self, + _: &mut Context<'_>, + ) -> Poll< + ConnectionHandlerEvent, + > { + if let Some(event) = self.events.pop_front() { + return Poll::Ready(event); + } + Poll::Pending + } +} diff --git a/protocols/relay/src/lib.rs b/protocols/relay/src/lib.rs index 515fb40ef4b..6dbdc0e8673 100644 --- a/protocols/relay/src/lib.rs +++ b/protocols/relay/src/lib.rs @@ -23,6 +23,7 @@ #![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] +pub mod autorelay; mod behaviour; mod copy_future; mod multiaddr_ext; diff --git a/protocols/relay/src/multiaddr_ext.rs b/protocols/relay/src/multiaddr_ext.rs index 7c06eb7eab0..44e9dc5ac08 100644 --- a/protocols/relay/src/multiaddr_ext.rs +++ b/protocols/relay/src/multiaddr_ext.rs @@ -2,10 +2,48 @@ use libp2p_core::{multiaddr::Protocol, Multiaddr}; pub(crate) trait MultiaddrExt { fn is_relayed(&self) -> bool; + + fn is_public(&self) -> bool; + + fn is_loopback(&self) -> bool; + + fn is_private(&self) -> bool; + + fn is_unspecified(&self) -> bool; } impl MultiaddrExt for Multiaddr { fn is_relayed(&self) -> bool { self.iter().any(|p| p == Protocol::P2pCircuit) } + + fn is_public(&self) -> bool { + !self.is_private() && !self.is_loopback() && !self.is_unspecified() + } + + fn is_loopback(&self) -> bool { + self.iter().any(|proto| match proto { + Protocol::Ip4(ip) => ip.is_loopback(), + Protocol::Ip6(ip) => ip.is_loopback(), + _ => false, + }) + } + + fn is_private(&self) -> bool { + self.iter().any(|proto| match proto { + Protocol::Ip4(ip) => ip.is_private(), + Protocol::Ip6(ip) => { + (ip.segments()[0] & 0xffc0) != 0xfe80 && (ip.segments()[0] & 0xfe00) != 0xfc00 + } + _ => false, + }) + } + + fn is_unspecified(&self) -> bool { + self.iter().any(|proto| match proto { + Protocol::Ip4(ip) => ip.is_unspecified(), + Protocol::Ip6(ip) => ip.is_unspecified(), + _ => false, + }) + } } From b1c1cdc468f3d03cc98900328de833349701047e Mon Sep 17 00:00:00 2001 From: Darius Date: Tue, 2 Sep 2025 09:31:11 -0400 Subject: [PATCH 02/51] chore: update cargo.toml and add changelog entry --- Cargo.lock | 2 +- Cargo.toml | 2 +- protocols/relay/CHANGELOG.md | 4 ++++ protocols/relay/Cargo.toml | 2 +- 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b088d0da44f..2d254c1a07c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2959,7 +2959,7 @@ dependencies = [ [[package]] name = "libp2p-relay" -version = "0.21.0" +version = "0.21.1" dependencies = [ "asynchronous-codec", "bytes", diff --git a/Cargo.toml b/Cargo.toml index 527d20c27e4..6d3a460cc58 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -97,7 +97,7 @@ libp2p-ping = { version = "0.47.0", path = "protocols/ping" } libp2p-plaintext = { version = "0.43.0", path = "transports/plaintext" } libp2p-pnet = { version = "0.26.0", path = "transports/pnet" } libp2p-quic = { version = "0.13.0", path = "transports/quic" } -libp2p-relay = { version = "0.21.0", path = "protocols/relay" } +libp2p-relay = { version = "0.21.1", path = "protocols/relay" } libp2p-rendezvous = { version = "0.17.0", path = "protocols/rendezvous" } libp2p-request-response = { version = "0.29.0", path = "protocols/request-response" } libp2p-server = { version = "0.12.7", path = "misc/server" } diff --git a/protocols/relay/CHANGELOG.md b/protocols/relay/CHANGELOG.md index 0f17112a76e..822476eea25 100644 --- a/protocols/relay/CHANGELOG.md +++ b/protocols/relay/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.21.1 +- Implements autorelay that would make a reservation as soon as a connection reports supporting HOP protocol. + See [PR XXXX](https://github.com/libp2p/rust-libp2p/pull/6067/files) + ## 0.21.0 diff --git a/protocols/relay/Cargo.toml b/protocols/relay/Cargo.toml index 6124744cb0d..3871abbcf8a 100644 --- a/protocols/relay/Cargo.toml +++ b/protocols/relay/Cargo.toml @@ -3,7 +3,7 @@ name = "libp2p-relay" edition.workspace = true rust-version = { workspace = true } description = "Communications relaying for libp2p" -version = "0.21.0" +version = "0.21.1" authors = ["Parity Technologies ", "Max Inden "] license = "MIT" repository = "https://github.com/libp2p/rust-libp2p" From cb2fbab0f96ce9873b453cacf113a557d1c27a73 Mon Sep 17 00:00:00 2001 From: Darius Date: Tue, 2 Sep 2025 09:44:37 -0400 Subject: [PATCH 03/51] chore: rename function --- protocols/relay/src/autorelay.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/protocols/relay/src/autorelay.rs b/protocols/relay/src/autorelay.rs index 27bc66f47f3..fa67e5effc8 100644 --- a/protocols/relay/src/autorelay.rs +++ b/protocols/relay/src/autorelay.rs @@ -39,7 +39,7 @@ struct Connection { impl Connection { /// Mark relayed connection as not supported - pub fn disqualify_connection(&mut self) -> bool { + pub(crate) fn disqualify_connection_if_relayed(&mut self) -> bool { match self.address.is_relayed() { true => { self.relay_status = RelayStatus::NotSupported; @@ -310,7 +310,7 @@ impl NetworkBehaviour for Behaviour { relay_status: RelayStatus::Pending, }; - connection.disqualify_connection(); + connection.disqualify_connection_if_relayed(); self.connections .insert((peer_id, connection_id), connection); From 40a05829f3d97bb58e914c6ceee8ca9ce0f068e5 Mon Sep 17 00:00:00 2001 From: Darius Date: Tue, 2 Sep 2025 09:47:03 -0400 Subject: [PATCH 04/51] chore: remofe let/else condition --- protocols/relay/src/autorelay.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/protocols/relay/src/autorelay.rs b/protocols/relay/src/autorelay.rs index fa67e5effc8..788e768a833 100644 --- a/protocols/relay/src/autorelay.rs +++ b/protocols/relay/src/autorelay.rs @@ -402,9 +402,7 @@ impl NetworkBehaviour for Behaviour { ) { let Either::Left(event) = event; - let Some(connection) = self.connections.get_mut(&(peer_id, connection_id)) else { - return; - }; + let connection = self.connections.get_mut(&(peer_id, connection_id)).expect("valid connection"); match event { Out::Supported => { From eb7a6c7250fe130e0b6c649928b559414a7eadf0 Mon Sep 17 00:00:00 2001 From: Darius Date: Tue, 2 Sep 2025 10:21:58 -0400 Subject: [PATCH 05/51] chore: remove redundant trait members --- protocols/relay/src/autorelay.rs | 14 ++++++++-- protocols/relay/src/multiaddr_ext.rs | 38 ---------------------------- 2 files changed, 12 insertions(+), 40 deletions(-) diff --git a/protocols/relay/src/autorelay.rs b/protocols/relay/src/autorelay.rs index 788e768a833..f23dc61757c 100644 --- a/protocols/relay/src/autorelay.rs +++ b/protocols/relay/src/autorelay.rs @@ -145,7 +145,7 @@ impl Behaviour { if self .external_addresses .iter() - .any(|addr| addr.is_public() && !addr.is_relayed()) + .any(|addr| !addr.is_relayed()) { return; } @@ -294,7 +294,17 @@ impl NetworkBehaviour for Behaviour { } fn on_swarm_event(&mut self, event: FromSwarm) { - let _change = self.external_addresses.on_swarm_event(&event); + let change = self.external_addresses.on_swarm_event(&event); + + if change + && self + .external_addresses + .iter() + .any(|addr| !addr.is_relayed()) + { + self.remove_all_reservations(); + return; + } match event { FromSwarm::ConnectionEstablished(ConnectionEstablished { diff --git a/protocols/relay/src/multiaddr_ext.rs b/protocols/relay/src/multiaddr_ext.rs index 44e9dc5ac08..7c06eb7eab0 100644 --- a/protocols/relay/src/multiaddr_ext.rs +++ b/protocols/relay/src/multiaddr_ext.rs @@ -2,48 +2,10 @@ use libp2p_core::{multiaddr::Protocol, Multiaddr}; pub(crate) trait MultiaddrExt { fn is_relayed(&self) -> bool; - - fn is_public(&self) -> bool; - - fn is_loopback(&self) -> bool; - - fn is_private(&self) -> bool; - - fn is_unspecified(&self) -> bool; } impl MultiaddrExt for Multiaddr { fn is_relayed(&self) -> bool { self.iter().any(|p| p == Protocol::P2pCircuit) } - - fn is_public(&self) -> bool { - !self.is_private() && !self.is_loopback() && !self.is_unspecified() - } - - fn is_loopback(&self) -> bool { - self.iter().any(|proto| match proto { - Protocol::Ip4(ip) => ip.is_loopback(), - Protocol::Ip6(ip) => ip.is_loopback(), - _ => false, - }) - } - - fn is_private(&self) -> bool { - self.iter().any(|proto| match proto { - Protocol::Ip4(ip) => ip.is_private(), - Protocol::Ip6(ip) => { - (ip.segments()[0] & 0xffc0) != 0xfe80 && (ip.segments()[0] & 0xfe00) != 0xfc00 - } - _ => false, - }) - } - - fn is_unspecified(&self) -> bool { - self.iter().any(|proto| match proto { - Protocol::Ip4(ip) => ip.is_unspecified(), - Protocol::Ip6(ip) => ip.is_unspecified(), - _ => false, - }) - } } From 60b69315bcee49544a131f742139952a542933cb Mon Sep 17 00:00:00 2001 From: Darius Date: Tue, 2 Sep 2025 11:19:11 -0400 Subject: [PATCH 06/51] chore: remove unused imports --- protocols/relay/src/autorelay.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/protocols/relay/src/autorelay.rs b/protocols/relay/src/autorelay.rs index f23dc61757c..e926668e88e 100644 --- a/protocols/relay/src/autorelay.rs +++ b/protocols/relay/src/autorelay.rs @@ -7,8 +7,8 @@ use libp2p_core::Endpoint; use libp2p_identity::PeerId; use libp2p_swarm::derive_prelude::{ AddressChange, ConnectionClosed, ConnectionDenied, ConnectionEstablished, ConnectionId, - DialFailure, ExpiredListenAddr, FromSwarm, ListenerClosed, ListenerError, Multiaddr, - NetworkBehaviour, THandler, THandlerInEvent, THandlerOutEvent, ToSwarm, + ExpiredListenAddr, FromSwarm, ListenerClosed, ListenerError, Multiaddr, NetworkBehaviour, + THandler, THandlerInEvent, THandlerOutEvent, ToSwarm, }; use libp2p_swarm::{dummy, ExternalAddresses, ListenOpts, NewListenAddr}; use std::collections::{HashMap, HashSet, VecDeque}; From 8ed974efeb0286e497aa50ea6139be423c4fd590 Mon Sep 17 00:00:00 2001 From: Darius Date: Tue, 2 Sep 2025 11:26:01 -0400 Subject: [PATCH 07/51] chore: mark status accordingly when listener errors, closes or expires. --- protocols/relay/src/autorelay.rs | 113 +++++++++++++++++++++++++++---- 1 file changed, 98 insertions(+), 15 deletions(-) diff --git a/protocols/relay/src/autorelay.rs b/protocols/relay/src/autorelay.rs index e926668e88e..bcf038d2945 100644 --- a/protocols/relay/src/autorelay.rs +++ b/protocols/relay/src/autorelay.rs @@ -25,7 +25,7 @@ pub struct Behaviour { connections: HashMap<(PeerId, ConnectionId), Connection>, - pending_reservation: HashMap, + reservations: HashMap, pending_target: HashSet<(PeerId, ConnectionId)>, waker: Option, @@ -93,7 +93,6 @@ impl Config { pub enum Event {} impl Behaviour { - pub fn new_with_config(config: Config) -> Self { Self { config, @@ -103,7 +102,7 @@ impl Behaviour { fn select_connection_for_reservation( &mut self, - (peer_id, connection_id): (PeerId, ConnectionId), + peer_id: PeerId, connection_id: ConnectionId ) -> bool { if self.pending_target.contains(&(peer_id, connection_id)) { return false; @@ -131,14 +130,75 @@ impl Behaviour { info.relay_status = RelayStatus::Supported { status: ReservationStatus::Pending { id }, }; - self.pending_reservation - .insert(id, (peer_id, connection_id)); + self.reservations.insert(id, (peer_id, connection_id)); self.events.push_back(ToSwarm::ListenOn { opts }); self.pending_target.insert((peer_id, connection_id)); true } + fn remove_all_reservations(&mut self) { + let relay_listeners = self + .reservations + .iter() + .map(|(id, (peer_id, conn_id))| (*id, *peer_id, *conn_id)) + .collect::>(); + + for (listener_id, peer_id, connection_id) in relay_listeners { + let Some(connection) = self.connections.get_mut(&(peer_id, connection_id)) else { + continue; + }; + + assert!(matches!( + connection.relay_status, + RelayStatus::Supported { + status: ReservationStatus::Active { id } | ReservationStatus::Pending { id } + } if id == listener_id + )); + + connection.relay_status = RelayStatus::Supported { + status: ReservationStatus::Idle, + }; + + self.events + .push_back(ToSwarm::RemoveListener { id: listener_id }); + } + } + + fn disable_reservation(&mut self, id: ListenerId) { + let Some((peer_id, connection_id)) = self.reservations.remove(&id) else { + return; + }; + + let Some(connection) = self.connections.get_mut(&(peer_id, connection_id)) else { + return; + }; + + match connection.relay_status { + RelayStatus::Supported { + status: ReservationStatus::Active { .. }, + } => { + connection.relay_status = RelayStatus::Supported { + status: ReservationStatus::Idle, + }; + } + RelayStatus::Supported { + status: ReservationStatus::Pending { .. }, + } => { + connection.relay_status = RelayStatus::Supported { + status: ReservationStatus::Idle, + }; + } + RelayStatus::Pending => { + self.pending_target.remove(&(peer_id, connection_id)); + } + RelayStatus::Supported { + status: ReservationStatus::Idle, + } + | RelayStatus::NotSupported => {} + } + } + fn meet_reservation_target(&mut self) { // check to determine if there is a public external address that could possibly let us know the node // is reachable @@ -218,7 +278,7 @@ impl Behaviour { .copied() .take(remaining_targets_needed) { - if !self.select_connection_for_reservation((peer_id, connection_id)) { + if !self.select_connection_for_reservation(peer_id, connection_id) { continue; } @@ -231,7 +291,17 @@ impl Behaviour { } fn active_reservations(&self) -> usize { - self.connections.values().filter(|info| matches!(info.relay_status, RelayStatus::Supported { status: ReservationStatus::Active { .. } })).count() + self.connections + .values() + .filter(|info| { + matches!( + info.relay_status, + RelayStatus::Supported { + status: ReservationStatus::Active { .. } + } + ) + }) + .count() } // fn on_connection_established( @@ -336,12 +406,16 @@ impl NetworkBehaviour for Behaviour { .expect("valid connection"); let id = match connection.relay_status { - RelayStatus::Supported { status: ReservationStatus::Active { id } } => id, - RelayStatus::Supported { status: ReservationStatus::Pending { id } } => id, + RelayStatus::Supported { + status: ReservationStatus::Active { id }, + } => id, + RelayStatus::Supported { + status: ReservationStatus::Pending { id }, + } => id, _ => return, }; - self.pending_reservation.remove(&id); + self.reservations.remove(&id); self.pending_target.remove(&(peer_id, connection_id)); let max_reservation = self.config.max_reservations.get() as usize; @@ -376,7 +450,7 @@ impl NetworkBehaviour for Behaviour { return; } - let Some((peer_id, connection_id)) = self.pending_reservation.remove(&listener_id) + let Some((peer_id, connection_id)) = self.reservations.get(&listener_id).copied() else { return; }; @@ -397,9 +471,15 @@ impl NetworkBehaviour for Behaviour { status: ReservationStatus::Active { id }, }; } - FromSwarm::ExpiredListenAddr(_) => {} - FromSwarm::ListenerError(_) => {} - FromSwarm::ListenerClosed(_) => {} + FromSwarm::ExpiredListenAddr(ExpiredListenAddr { listener_id, .. }) => { + self.disable_reservation(listener_id); + } + FromSwarm::ListenerError(ListenerError { listener_id, .. }) => { + self.disable_reservation(listener_id); + } + FromSwarm::ListenerClosed(ListenerClosed { listener_id, .. }) => { + self.disable_reservation(listener_id); + } _ => {} } } @@ -412,7 +492,10 @@ impl NetworkBehaviour for Behaviour { ) { let Either::Left(event) = event; - let connection = self.connections.get_mut(&(peer_id, connection_id)).expect("valid connection"); + let connection = self + .connections + .get_mut(&(peer_id, connection_id)) + .expect("valid connection"); match event { Out::Supported => { From ed6481379b0e8d3f35ad885b587eff3d595d8f31 Mon Sep 17 00:00:00 2001 From: Darius Date: Tue, 2 Sep 2025 11:26:24 -0400 Subject: [PATCH 08/51] chore: fmt --- protocols/relay/src/autorelay.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/protocols/relay/src/autorelay.rs b/protocols/relay/src/autorelay.rs index bcf038d2945..c6e54f51787 100644 --- a/protocols/relay/src/autorelay.rs +++ b/protocols/relay/src/autorelay.rs @@ -102,7 +102,8 @@ impl Behaviour { fn select_connection_for_reservation( &mut self, - peer_id: PeerId, connection_id: ConnectionId + peer_id: PeerId, + connection_id: ConnectionId, ) -> bool { if self.pending_target.contains(&(peer_id, connection_id)) { return false; From bc76458f37604276d778bd4ae1a0f5eee104e37f Mon Sep 17 00:00:00 2001 From: Darius Date: Tue, 2 Sep 2025 11:28:17 -0400 Subject: [PATCH 09/51] chore: fmt --- protocols/relay/src/autorelay.rs | 37 ++++++++++++++---------- protocols/relay/src/autorelay/handler.rs | 4 +-- 2 files changed, 24 insertions(+), 17 deletions(-) diff --git a/protocols/relay/src/autorelay.rs b/protocols/relay/src/autorelay.rs index c6e54f51787..73714d85bd7 100644 --- a/protocols/relay/src/autorelay.rs +++ b/protocols/relay/src/autorelay.rs @@ -1,19 +1,26 @@ -use crate::autorelay::handler::Out; -use crate::multiaddr_ext::MultiaddrExt; +use std::{ + collections::{HashMap, HashSet, VecDeque}, + num::NonZeroU8, + task::{Context, Poll, Waker}, +}; + use either::Either; -use libp2p_core::multiaddr::Protocol; -use libp2p_core::transport::{ListenerId, PortUse}; -use libp2p_core::Endpoint; +use libp2p_core::{ + multiaddr::Protocol, + transport::{ListenerId, PortUse}, + Endpoint, +}; use libp2p_identity::PeerId; -use libp2p_swarm::derive_prelude::{ - AddressChange, ConnectionClosed, ConnectionDenied, ConnectionEstablished, ConnectionId, - ExpiredListenAddr, FromSwarm, ListenerClosed, ListenerError, Multiaddr, NetworkBehaviour, - THandler, THandlerInEvent, THandlerOutEvent, ToSwarm, +use libp2p_swarm::{ + derive_prelude::{ + AddressChange, ConnectionClosed, ConnectionDenied, ConnectionEstablished, ConnectionId, + ExpiredListenAddr, FromSwarm, ListenerClosed, ListenerError, Multiaddr, NetworkBehaviour, + THandler, THandlerInEvent, THandlerOutEvent, ToSwarm, + }, + dummy, ExternalAddresses, ListenOpts, NewListenAddr, }; -use libp2p_swarm::{dummy, ExternalAddresses, ListenOpts, NewListenAddr}; -use std::collections::{HashMap, HashSet, VecDeque}; -use std::num::NonZeroU8; -use std::task::{Context, Poll, Waker}; + +use crate::{autorelay::handler::Out, multiaddr_ext::MultiaddrExt}; mod handler; @@ -201,8 +208,8 @@ impl Behaviour { } fn meet_reservation_target(&mut self) { - // check to determine if there is a public external address that could possibly let us know the node - // is reachable + // check to determine if there is a public external address that could possibly let us know + // the node is reachable if self .external_addresses .iter() diff --git a/protocols/relay/src/autorelay/handler.rs b/protocols/relay/src/autorelay/handler.rs index 4306e53b7bd..8f4c633a75d 100644 --- a/protocols/relay/src/autorelay/handler.rs +++ b/protocols/relay/src/autorelay/handler.rs @@ -4,13 +4,13 @@ use std::{ }; use libp2p_core::upgrade::DeniedUpgrade; - -use crate::HOP_PROTOCOL_NAME; use libp2p_swarm::{ handler::ConnectionEvent, ConnectionHandler, ConnectionHandlerEvent, SubstreamProtocol, SupportedProtocols, }; +use crate::HOP_PROTOCOL_NAME; + #[derive(Default, Debug)] pub struct Handler { events: VecDeque< From bb02c3824b7d9fc055980bafe3fb306d47560919 Mon Sep 17 00:00:00 2001 From: Darius Date: Tue, 2 Sep 2025 20:15:22 -0400 Subject: [PATCH 10/51] chore: remove pending target --- protocols/relay/src/autorelay.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/protocols/relay/src/autorelay.rs b/protocols/relay/src/autorelay.rs index 73714d85bd7..4e44dfc3ffb 100644 --- a/protocols/relay/src/autorelay.rs +++ b/protocols/relay/src/autorelay.rs @@ -196,11 +196,10 @@ impl Behaviour { connection.relay_status = RelayStatus::Supported { status: ReservationStatus::Idle, }; - } - RelayStatus::Pending => { self.pending_target.remove(&(peer_id, connection_id)); } - RelayStatus::Supported { + RelayStatus::Pending + | RelayStatus::Supported { status: ReservationStatus::Idle, } | RelayStatus::NotSupported => {} From c0e50c0073d533198117788785907dbdadd2e02c Mon Sep 17 00:00:00 2001 From: Darius Date: Wed, 10 Sep 2025 16:58:02 -0400 Subject: [PATCH 11/51] chore: remove pending target and add functions to check relay status and get pending amount --- protocols/relay/src/autorelay.rs | 68 +++++++++++++++++++++----------- 1 file changed, 44 insertions(+), 24 deletions(-) diff --git a/protocols/relay/src/autorelay.rs b/protocols/relay/src/autorelay.rs index 4e44dfc3ffb..f6027910130 100644 --- a/protocols/relay/src/autorelay.rs +++ b/protocols/relay/src/autorelay.rs @@ -33,7 +33,6 @@ pub struct Behaviour { connections: HashMap<(PeerId, ConnectionId), Connection>, reservations: HashMap, - pending_target: HashSet<(PeerId, ConnectionId)>, waker: Option, } @@ -58,6 +57,28 @@ impl Connection { } } } + + pub(crate) fn is_reservation_confirmed(&self) -> bool { + matches!( + self.relay_status, + RelayStatus::Supported { + status: ReservationStatus::Active { .. } + } + ) + } + + pub(crate) fn is_confirmation_pending(&self) -> bool { + self.relay_status == RelayStatus::Pending + } + + pub(crate) fn is_reservation_pending(&self) -> bool { + matches!( + self.relay_status, + RelayStatus::Supported { + status: ReservationStatus::Pending { .. } + } + ) + } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -107,20 +128,27 @@ impl Behaviour { } } + fn get_pending_reservations(&self) -> usize { + self.connections + .values() + .filter(|info| info.is_reservation_pending()) + .count() + } + fn select_connection_for_reservation( &mut self, peer_id: PeerId, connection_id: ConnectionId, ) -> bool { - if self.pending_target.contains(&(peer_id, connection_id)) { - return false; - } - let info = self .connections .get_mut(&(peer_id, connection_id)) .expect("connection is present"); + if !info.is_confirmation_pending() { + return false; + } + let addr_with_peer_id = match info.address.clone().with_p2p(peer_id) { Ok(addr) => addr, Err(addr) => { @@ -140,7 +168,6 @@ impl Behaviour { }; self.reservations.insert(id, (peer_id, connection_id)); self.events.push_back(ToSwarm::ListenOn { opts }); - self.pending_target.insert((peer_id, connection_id)); true } @@ -196,7 +223,6 @@ impl Behaviour { connection.relay_status = RelayStatus::Supported { status: ReservationStatus::Idle, }; - self.pending_target.remove(&(peer_id, connection_id)); } RelayStatus::Pending | RelayStatus::Supported { @@ -246,9 +272,9 @@ impl Behaviour { return; } - let pending_target_len = self.pending_target.len(); + let pending_target_len = self.get_pending_reservations(); - if pending_target_len >= max_reservation { + if pending_target_len == max_reservation { return; } @@ -256,12 +282,10 @@ impl Behaviour { .connections .iter() .filter(|(_, info)| { - matches!( - info.relay_status, - RelayStatus::Supported { - status: ReservationStatus::Idle + info.relay_status + == RelayStatus::Supported { + status: ReservationStatus::Idle, } - ) }) .map(|((peer_id, connection_id), _)| (*peer_id, *connection_id)) .collect::>(); @@ -273,7 +297,7 @@ impl Behaviour { } let remaining_targets_needed = targets_count - .checked_sub(self.pending_target.len()) + .checked_sub(pending_target_len) .unwrap_or_default(); if remaining_targets_needed == 0 { @@ -289,12 +313,12 @@ impl Behaviour { continue; } - if self.pending_target.len() == max_reservation { + if self.get_pending_reservations() == max_reservation { break; } } - debug_assert!(self.pending_target.len() <= max_reservation); + debug_assert!(self.get_pending_reservations() <= max_reservation); } fn active_reservations(&self) -> usize { @@ -423,7 +447,6 @@ impl NetworkBehaviour for Behaviour { }; self.reservations.remove(&id); - self.pending_target.remove(&(peer_id, connection_id)); let max_reservation = self.config.max_reservations.get() as usize; @@ -467,15 +490,12 @@ impl NetworkBehaviour for Behaviour { .get_mut(&(peer_id, connection_id)) .expect("valid connection"); - let RelayStatus::Supported { - status: ReservationStatus::Pending { id }, - } = connection.relay_status - else { + if !connection.is_reservation_confirmed() { return; - }; + } connection.relay_status = RelayStatus::Supported { - status: ReservationStatus::Active { id }, + status: ReservationStatus::Active { id: listener_id }, }; } FromSwarm::ExpiredListenAddr(ExpiredListenAddr { listener_id, .. }) => { From 709a6f6267e5e01a0d092815c3c5ae953be847af Mon Sep 17 00:00:00 2001 From: Darius Clark Date: Fri, 12 Sep 2025 05:51:01 -0400 Subject: [PATCH 12/51] Update protocols/relay/CHANGELOG.md --- protocols/relay/CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/protocols/relay/CHANGELOG.md b/protocols/relay/CHANGELOG.md index 782e1598efa..4bc2ec39b8d 100644 --- a/protocols/relay/CHANGELOG.md +++ b/protocols/relay/CHANGELOG.md @@ -5,7 +5,6 @@ - reduce allocations by replacing `get_or_insert` with `get_or_insert_with` See [PR 6136](https://github.com/libp2p/rust-libp2p/pull/6136) - ## 0.21.0 From 43f37f8c30452ca7fd27eb603df6b8d80b3d2f7a Mon Sep 17 00:00:00 2001 From: Darius Clark Date: Thu, 14 May 2026 08:01:11 -0400 Subject: [PATCH 13/51] chore: overhaul existing code; redo reservation checks --- Cargo.lock | 1 + protocols/relay/Cargo.toml | 1 + protocols/relay/src/autorelay.rs | 351 ++++++---------- protocols/relay/src/autorelay/handler.rs | 4 +- protocols/relay/src/multiaddr_ext.rs | 14 + protocols/relay/tests/autorelay.rs | 498 +++++++++++++++++++++++ 6 files changed, 635 insertions(+), 234 deletions(-) create mode 100644 protocols/relay/tests/autorelay.rs diff --git a/Cargo.lock b/Cargo.lock index db35f1b9f21..7e9d3456521 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3090,6 +3090,7 @@ dependencies = [ "futures-bounded", "futures-timer", "libp2p-core", + "libp2p-identify", "libp2p-identity", "libp2p-ping", "libp2p-plaintext", diff --git a/protocols/relay/Cargo.toml b/protocols/relay/Cargo.toml index 779778ce7af..e4aef6e5c80 100644 --- a/protocols/relay/Cargo.toml +++ b/protocols/relay/Cargo.toml @@ -29,6 +29,7 @@ thiserror = { workspace = true } tracing = { workspace = true } [dev-dependencies] +libp2p-identify = { workspace = true } libp2p-identity = { workspace = true, features = ["rand"] } libp2p-ping = { workspace = true } libp2p-plaintext = { workspace = true } diff --git a/protocols/relay/src/autorelay.rs b/protocols/relay/src/autorelay.rs index f6027910130..5d688ebb860 100644 --- a/protocols/relay/src/autorelay.rs +++ b/protocols/relay/src/autorelay.rs @@ -6,18 +6,19 @@ use std::{ use either::Either; use libp2p_core::{ + Endpoint, multiaddr::Protocol, transport::{ListenerId, PortUse}, - Endpoint, }; use libp2p_identity::PeerId; use libp2p_swarm::{ + ExternalAddresses, ListenOpts, NewListenAddr, derive_prelude::{ AddressChange, ConnectionClosed, ConnectionDenied, ConnectionEstablished, ConnectionId, - ExpiredListenAddr, FromSwarm, ListenerClosed, ListenerError, Multiaddr, NetworkBehaviour, - THandler, THandlerInEvent, THandlerOutEvent, ToSwarm, + ExpiredListenAddr, ExternalAddrConfirmed, FromSwarm, ListenerClosed, ListenerError, + Multiaddr, NetworkBehaviour, THandler, THandlerInEvent, THandlerOutEvent, ToSwarm, }, - dummy, ExternalAddresses, ListenOpts, NewListenAddr, + dummy, }; use crate::{autorelay::handler::Out, multiaddr_ext::MultiaddrExt}; @@ -34,6 +35,8 @@ pub struct Behaviour { reservations: HashMap, + external_reservations: HashMap, + waker: Option, } @@ -45,40 +48,12 @@ struct Connection { impl Connection { /// Mark relayed connection as not supported - pub(crate) fn disqualify_connection_if_relayed(&mut self) -> bool { - match self.address.is_relayed() { - true => { - self.relay_status = RelayStatus::NotSupported; - true - } - false => { - self.relay_status = RelayStatus::Pending; - false - } + pub(crate) fn disqualify_connection_if_relayed(&mut self) { + if self.address.is_relayed() { + self.relay_status = RelayStatus::NotSupported; } } - pub(crate) fn is_reservation_confirmed(&self) -> bool { - matches!( - self.relay_status, - RelayStatus::Supported { - status: ReservationStatus::Active { .. } - } - ) - } - - pub(crate) fn is_confirmation_pending(&self) -> bool { - self.relay_status == RelayStatus::Pending - } - - pub(crate) fn is_reservation_pending(&self) -> bool { - matches!( - self.relay_status, - RelayStatus::Supported { - status: ReservationStatus::Pending { .. } - } - ) - } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -128,39 +103,29 @@ impl Behaviour { } } - fn get_pending_reservations(&self) -> usize { - self.connections - .values() - .filter(|info| info.is_reservation_pending()) - .count() - } - - fn select_connection_for_reservation( - &mut self, - peer_id: PeerId, - connection_id: ConnectionId, - ) -> bool { + fn select_connection_for_reservation(&mut self, peer_id: PeerId, connection_id: ConnectionId) { let info = self .connections .get_mut(&(peer_id, connection_id)) .expect("connection is present"); - if !info.is_confirmation_pending() { - return false; + if info.relay_status + != (RelayStatus::Supported { + status: ReservationStatus::Idle, + }) + { + return; } let addr_with_peer_id = match info.address.clone().with_p2p(peer_id) { Ok(addr) => addr, Err(addr) => { tracing::warn!(%addr, "address unexpectedly contains a different peer id than the connection"); - return false; + return; } }; - let relay_addr = addr_with_peer_id.with(Protocol::P2pCircuit); - - let opts = ListenOpts::new(relay_addr); - + let opts = ListenOpts::new(addr_with_peer_id.with(Protocol::P2pCircuit)); let id = opts.listener_id(); info.relay_status = RelayStatus::Supported { @@ -168,8 +133,6 @@ impl Behaviour { }; self.reservations.insert(id, (peer_id, connection_id)); self.events.push_back(ToSwarm::ListenOn { opts }); - - true } fn remove_all_reservations(&mut self) { @@ -184,12 +147,14 @@ impl Behaviour { continue; }; - assert!(matches!( + if !matches!( connection.relay_status, RelayStatus::Supported { status: ReservationStatus::Active { id } | ReservationStatus::Pending { id } } if id == listener_id - )); + ) { + continue; + } connection.relay_status = RelayStatus::Supported { status: ReservationStatus::Idle, @@ -201,40 +166,40 @@ impl Behaviour { } fn disable_reservation(&mut self, id: ListenerId) { - let Some((peer_id, connection_id)) = self.reservations.remove(&id) else { + if self.external_reservations.remove(&id).is_some() { + self.meet_reservation_target(); return; - }; + } - let Some(connection) = self.connections.get_mut(&(peer_id, connection_id)) else { + let Some((peer_id, connection_id)) = self.reservations.remove(&id) else { return; }; - match connection.relay_status { - RelayStatus::Supported { - status: ReservationStatus::Active { .. }, - } => { - connection.relay_status = RelayStatus::Supported { - status: ReservationStatus::Idle, - }; - } - RelayStatus::Supported { - status: ReservationStatus::Pending { .. }, - } => { - connection.relay_status = RelayStatus::Supported { - status: ReservationStatus::Idle, - }; - } - RelayStatus::Pending - | RelayStatus::Supported { + if let Some(connection) = self.connections.get_mut(&(peer_id, connection_id)) + && matches!( + connection.relay_status, + RelayStatus::Supported { + status: ReservationStatus::Active { .. } | ReservationStatus::Pending { .. } + } + ) + { + connection.relay_status = RelayStatus::Supported { status: ReservationStatus::Idle, - } - | RelayStatus::NotSupported => {} + }; } + + self.meet_reservation_target(); + } + + fn covered_peers(&self) -> HashSet { + self.reservations + .values() + .map(|(peer_id, _)| *peer_id) + .chain(self.external_reservations.values().copied()) + .collect() } fn meet_reservation_target(&mut self) { - // check to determine if there is a public external address that could possibly let us know - // the node is reachable if self .external_addresses .iter() @@ -243,122 +208,37 @@ impl Behaviour { return; } - let max_reservation = self.config.max_reservations.get() as usize; - - let peers_not_supported = self.connections.is_empty() - || self - .connections - .iter() - .all(|(_, connection)| connection.relay_status == RelayStatus::NotSupported); - - if peers_not_supported { - return; - } - - let relayed_targets = self - .connections - .iter() - .filter(|(_, info)| { - matches!( - info.relay_status, - RelayStatus::Supported { - status: ReservationStatus::Active { .. } - } - ) - }) - .count(); - - if relayed_targets == max_reservation { - return; - } - - let pending_target_len = self.get_pending_reservations(); - - if pending_target_len == max_reservation { - return; - } - - let possible_targets = self - .connections - .iter() - .filter(|(_, info)| { - info.relay_status - == RelayStatus::Supported { - status: ReservationStatus::Idle, - } - }) - .map(|((peer_id, connection_id), _)| (*peer_id, *connection_id)) - .collect::>(); - - let targets_count = std::cmp::min(possible_targets.len(), max_reservation); - - if targets_count == 0 { - return; - } - - let remaining_targets_needed = targets_count - .checked_sub(pending_target_len) - .unwrap_or_default(); - - if remaining_targets_needed == 0 { + let max = self.config.max_reservations.get() as usize; + let covered = self.covered_peers(); + let budget = max.saturating_sub(covered.len()); + if budget == 0 { return; } - for (peer_id, connection_id) in possible_targets - .iter() - .copied() - .take(remaining_targets_needed) - { - if !self.select_connection_for_reservation(peer_id, connection_id) { + let mut candidates = HashMap::new(); + for ((peer_id, connection_id), info) in self.connections.iter() { + if covered.contains(peer_id) { continue; } - - if self.get_pending_reservations() == max_reservation { - break; + if info.relay_status + == (RelayStatus::Supported { + status: ReservationStatus::Idle, + }) + { + candidates.entry(*peer_id).or_insert(*connection_id); } } - debug_assert!(self.get_pending_reservations() <= max_reservation); - } + for (peer_id, connection_id) in candidates.into_iter().take(budget) { + self.select_connection_for_reservation(peer_id, connection_id); + } - fn active_reservations(&self) -> usize { - self.connections - .values() - .filter(|info| { - matches!( - info.relay_status, - RelayStatus::Supported { - status: ReservationStatus::Active { .. } - } - ) - }) - .count() - } + debug_assert!(self.covered_peers().len() <= max); - // fn on_connection_established( - // &mut self, - // peer_id: PeerId, - // connection_id: ConnectionId, - // connection_id: ConnectionId, - // address: Multiaddr, - // ) { - // } - // - // fn on_connection_closed(&mut self, peer_id: PeerId, connection_id: ConnectionId) {} - // - // fn on_address_change( - // &mut self, - // peer_id: PeerId, - // connection_id: ConnectionId, - // old_addr: &Multiaddr, - // new_addr: &Multiaddr, - // ) { - // } - // - // fn on_new_listen_addr(&mut self, listener_id: ListenerId, addr: &Multiaddr) {} - // fn on_expired_listen_addr(&mut self, listener_id: ListenerId) {} - // fn on_listener_error(&mut self, listener_id: ListenerId, error: &std::io::Error) {} - // fn on_listener_closed(&mut self, listener_id: ListenerId) {} + if let Some(waker) = self.waker.take() { + waker.wake(); + } + } } impl NetworkBehaviour for Behaviour { @@ -395,13 +275,10 @@ impl NetworkBehaviour for Behaviour { } fn on_swarm_event(&mut self, event: FromSwarm) { - let change = self.external_addresses.on_swarm_event(&event); + self.external_addresses.on_swarm_event(&event); - if change - && self - .external_addresses - .iter() - .any(|addr| !addr.is_relayed()) + if let FromSwarm::ExternalAddrConfirmed(ExternalAddrConfirmed { addr }) = &event + && !addr.is_relayed() { self.remove_all_reservations(); return; @@ -436,23 +313,11 @@ impl NetworkBehaviour for Behaviour { .remove(&(peer_id, connection_id)) .expect("valid connection"); - let id = match connection.relay_status { - RelayStatus::Supported { - status: ReservationStatus::Active { id }, - } => id, - RelayStatus::Supported { - status: ReservationStatus::Pending { id }, - } => id, - _ => return, - }; - - self.reservations.remove(&id); - - let max_reservation = self.config.max_reservations.get() as usize; - - let active_reservations = self.active_reservations(); - - if active_reservations < max_reservation { + if let RelayStatus::Supported { + status: ReservationStatus::Active { id } | ReservationStatus::Pending { id }, + } = connection.relay_status + { + self.reservations.remove(&id); self.meet_reservation_target(); } } @@ -475,28 +340,35 @@ impl NetworkBehaviour for Behaviour { connection.address = new_addr.clone(); } FromSwarm::NewListenAddr(NewListenAddr { listener_id, addr }) => { - // we only care about any new relayed address - if !addr.iter().any(|protocol| protocol == Protocol::P2pCircuit) { + if !addr.is_relayed() { return; } - let Some((peer_id, connection_id)) = self.reservations.get(&listener_id).copied() - else { - return; - }; - - let connection = self - .connections - .get_mut(&(peer_id, connection_id)) - .expect("valid connection"); - - if !connection.is_reservation_confirmed() { + if let Some((peer_id, connection_id)) = + self.reservations.get(&listener_id).copied() + { + let connection = self + .connections + .get_mut(&(peer_id, connection_id)) + .expect("valid connection"); + + if matches!( + connection.relay_status, + RelayStatus::Supported { + status: ReservationStatus::Pending { id } + } if id == listener_id + ) { + connection.relay_status = RelayStatus::Supported { + status: ReservationStatus::Active { id: listener_id }, + }; + } return; } - connection.relay_status = RelayStatus::Supported { - status: ReservationStatus::Active { id: listener_id }, - }; + if let Some(relay_peer_id) = addr.relay_peer_id() { + self.external_reservations + .insert(listener_id, relay_peer_id); + } } FromSwarm::ExpiredListenAddr(ExpiredListenAddr { listener_id, .. }) => { self.disable_reservation(listener_id); @@ -526,14 +398,29 @@ impl NetworkBehaviour for Behaviour { match event { Out::Supported => { - connection.relay_status = RelayStatus::Supported { - status: ReservationStatus::Idle, - }; - self.meet_reservation_target(); + if matches!( + connection.relay_status, + RelayStatus::Pending | RelayStatus::NotSupported + ) { + connection.relay_status = RelayStatus::Supported { + status: ReservationStatus::Idle, + }; + self.meet_reservation_target(); + } } Out::Unsupported => { - let _previous_status = connection.relay_status; + let drop_listener = match connection.relay_status { + RelayStatus::Supported { + status: ReservationStatus::Pending { id } | ReservationStatus::Active { id }, + } => Some(id), + _ => None, + }; connection.relay_status = RelayStatus::NotSupported; + if let Some(id) = drop_listener { + self.reservations.remove(&id); + self.events.push_back(ToSwarm::RemoveListener { id }); + self.meet_reservation_target(); + } } } } diff --git a/protocols/relay/src/autorelay/handler.rs b/protocols/relay/src/autorelay/handler.rs index 8f4c633a75d..18fcdfd7196 100644 --- a/protocols/relay/src/autorelay/handler.rs +++ b/protocols/relay/src/autorelay/handler.rs @@ -5,8 +5,8 @@ use std::{ use libp2p_core::upgrade::DeniedUpgrade; use libp2p_swarm::{ - handler::ConnectionEvent, ConnectionHandler, ConnectionHandlerEvent, SubstreamProtocol, - SupportedProtocols, + ConnectionHandler, ConnectionHandlerEvent, SubstreamProtocol, SupportedProtocols, + handler::ConnectionEvent, }; use crate::HOP_PROTOCOL_NAME; diff --git a/protocols/relay/src/multiaddr_ext.rs b/protocols/relay/src/multiaddr_ext.rs index f9a1f71b4dc..365143e5dcb 100644 --- a/protocols/relay/src/multiaddr_ext.rs +++ b/protocols/relay/src/multiaddr_ext.rs @@ -1,11 +1,25 @@ use libp2p_core::{Multiaddr, multiaddr::Protocol}; +use libp2p_identity::PeerId; pub(crate) trait MultiaddrExt { fn is_relayed(&self) -> bool; + fn relay_peer_id(&self) -> Option; } impl MultiaddrExt for Multiaddr { fn is_relayed(&self) -> bool { self.iter().any(|p| p == Protocol::P2pCircuit) } + + fn relay_peer_id(&self) -> Option { + let mut last_p2p = None; + for proto in self.iter() { + match proto { + Protocol::P2p(peer) => last_p2p = Some(peer), + Protocol::P2pCircuit => return last_p2p, + _ => {} + } + } + None + } } diff --git a/protocols/relay/tests/autorelay.rs b/protocols/relay/tests/autorelay.rs new file mode 100644 index 00000000000..85b281c7b06 --- /dev/null +++ b/protocols/relay/tests/autorelay.rs @@ -0,0 +1,498 @@ +use std::{ + collections::{HashMap, HashSet}, + time::Duration, +}; + +use futures::{ + io::{AsyncRead, AsyncWrite}, + stream::StreamExt, +}; +use libp2p_core::{ + multiaddr::{Multiaddr, Protocol}, + muxing::StreamMuxerBox, + transport::{Boxed, MemoryTransport, Transport, choice::OrTransport}, + upgrade, +}; +use libp2p_identify as identify; +use libp2p_identity as identity; +use libp2p_identity::PeerId; +use libp2p_plaintext as plaintext; +use libp2p_relay::{self as relay, autorelay}; +use libp2p_swarm::{Config, ConnectionId, NetworkBehaviour, Swarm, SwarmEvent}; +use tracing_subscriber::EnvFilter; + +#[tokio::test] +async fn autorelay_reserves_when_peer_supports_hop() { + let _ = tracing_subscriber::fmt() + .with_env_filter(EnvFilter::from_default_env()) + .try_init(); + + let mut relay = build_relay(); + let relay_addr = Multiaddr::empty().with(Protocol::Memory(rand::random::())); + let relay_peer_id = *relay.local_peer_id(); + + relay.listen_on(relay_addr.clone()).unwrap(); + relay.add_external_address(relay_addr.clone()); + tokio::spawn(async move { + relay.collect::>().await; + }); + + let mut client = build_client(autorelay::Config::default()); + let client_peer_id = *client.local_peer_id(); + client.dial(relay_addr.clone()).unwrap(); + + let expected_relayed = relay_addr + .with(Protocol::P2p(relay_peer_id)) + .with(Protocol::P2pCircuit) + .with(Protocol::P2p(client_peer_id)); + + wait_until(&mut client, Duration::from_secs(20), { + let mut new_listen_addr = false; + let mut reservation_accepted = false; + move |event| { + match event { + SwarmEvent::NewListenAddr { address, .. } if address == &expected_relayed => { + new_listen_addr = true; + } + SwarmEvent::Behaviour(ClientEvent::RelayClient( + relay::client::Event::ReservationReqAccepted { .. }, + )) => { + reservation_accepted = true; + } + _ => {} + } + new_listen_addr && reservation_accepted + } + }) + .await; +} + +#[tokio::test] +async fn autorelay_respects_max_reservations() { + let _ = tracing_subscriber::fmt() + .with_env_filter(EnvFilter::from_default_env()) + .try_init(); + + let mut relay_a = build_relay(); + let relay_a_addr = Multiaddr::empty().with(Protocol::Memory(rand::random::())); + let relay_a_peer_id = *relay_a.local_peer_id(); + relay_a.listen_on(relay_a_addr.clone()).unwrap(); + relay_a.add_external_address(relay_a_addr.clone()); + tokio::spawn(async move { + relay_a.collect::>().await; + }); + + let mut relay_b = build_relay(); + let relay_b_addr = Multiaddr::empty().with(Protocol::Memory(rand::random::())); + let relay_b_peer_id = *relay_b.local_peer_id(); + relay_b.listen_on(relay_b_addr.clone()).unwrap(); + relay_b.add_external_address(relay_b_addr.clone()); + tokio::spawn(async move { + relay_b.collect::>().await; + }); + + let mut client = build_client(autorelay::Config::default().set_max_reservations(1)); + client.dial(relay_a_addr.clone()).unwrap(); + client.dial(relay_b_addr.clone()).unwrap(); + + let mut accepted = 0usize; + let timeout = tokio::time::sleep(Duration::from_secs(20)); + tokio::pin!(timeout); + loop { + tokio::select! { + _ = &mut timeout => break, + ev = client.select_next_some() => { + if let SwarmEvent::Behaviour(ClientEvent::RelayClient( + relay::client::Event::ReservationReqAccepted { relay_peer_id, .. }, + )) = ev + { + assert!(relay_peer_id == relay_a_peer_id || relay_peer_id == relay_b_peer_id); + accepted += 1; + if accepted > 1 { + panic!("autorelay opened more reservations than max_reservations=1"); + } + tokio::time::sleep(Duration::from_secs(2)).await; + break; + } + } + } + } + + assert_eq!( + accepted, 1, + "expected exactly one reservation, observed {accepted}" + ); +} + +#[tokio::test] +async fn autorelay_refills_after_connection_drop() { + let _ = tracing_subscriber::fmt() + .with_env_filter(EnvFilter::from_default_env()) + .try_init(); + + let mut relay_a = build_relay(); + let relay_a_addr = Multiaddr::empty().with(Protocol::Memory(rand::random::())); + let relay_a_peer_id = *relay_a.local_peer_id(); + relay_a.listen_on(relay_a_addr.clone()).unwrap(); + relay_a.add_external_address(relay_a_addr.clone()); + tokio::spawn(async move { + relay_a.collect::>().await; + }); + + let mut relay_b = build_relay(); + let relay_b_addr = Multiaddr::empty().with(Protocol::Memory(rand::random::())); + let relay_b_peer_id = *relay_b.local_peer_id(); + relay_b.listen_on(relay_b_addr.clone()).unwrap(); + relay_b.add_external_address(relay_b_addr.clone()); + tokio::spawn(async move { + relay_b.collect::>().await; + }); + + let mut client = build_client(autorelay::Config::default().set_max_reservations(1)); + client.dial(relay_a_addr.clone()).unwrap(); + client.dial(relay_b_addr.clone()).unwrap(); + + let (first_relay, first_conn) = wait_for_reservation_from_either( + &mut client, + relay_a_peer_id, + relay_b_peer_id, + Duration::from_secs(20), + ) + .await; + + assert!( + client.close_connection(first_conn), + "first reservation connection should exist" + ); + + let other_relay = if first_relay == relay_a_peer_id { + relay_b_peer_id + } else { + relay_a_peer_id + }; + + wait_until(&mut client, Duration::from_secs(20), { + let mut got = false; + move |event| { + if let SwarmEvent::Behaviour(ClientEvent::RelayClient( + relay::client::Event::ReservationReqAccepted { relay_peer_id, .. }, + )) = event + && *relay_peer_id == other_relay + { + got = true; + } + got + } + }) + .await; +} + +#[tokio::test] +async fn autorelay_with_two_reservations_among_five_relays() { + let _ = tracing_subscriber::fmt() + .with_env_filter(EnvFilter::from_default_env()) + .try_init(); + + let mut relay_addrs: Vec<(PeerId, Multiaddr)> = Vec::with_capacity(5); + for _ in 0..5 { + let mut relay = build_relay(); + let addr = Multiaddr::empty().with(Protocol::Memory(rand::random::())); + let peer_id = *relay.local_peer_id(); + relay.listen_on(addr.clone()).unwrap(); + relay.add_external_address(addr.clone()); + relay_addrs.push((peer_id, addr.clone())); + tokio::spawn(async move { + relay.collect::>().await; + }); + } + + let relay_peers: HashSet = relay_addrs.iter().map(|(p, _)| *p).collect(); + + let mut client = build_client(autorelay::Config::default().set_max_reservations(2)); + for (_, addr) in &relay_addrs { + client.dial(addr.clone()).unwrap(); + } + + let mut direct_conns: HashMap = HashMap::new(); + let mut reservations: HashSet = HashSet::new(); + + let sleep = tokio::time::sleep(Duration::from_secs(30)); + tokio::pin!(sleep); + loop { + tokio::select! { + _ = &mut sleep => panic!( + "timeout: got {} reservations, expected 2", + reservations.len() + ), + ev = client.select_next_some() => match ev { + SwarmEvent::ConnectionEstablished { + peer_id, connection_id, endpoint, .. + } if !endpoint.is_relayed() && relay_peers.contains(&peer_id) => { + direct_conns.insert(peer_id, connection_id); + } + SwarmEvent::Behaviour(ClientEvent::RelayClient( + relay::client::Event::ReservationReqAccepted { + relay_peer_id, + renewal: false, + .. + }, + )) => { + reservations.insert(relay_peer_id); + } + _ => {} + } + } + if reservations.len() == 2 { + break; + } + } + + let drop_peer = *reservations.iter().next().expect("two reservations held"); + let keep_peer = reservations + .iter() + .find(|p| **p != drop_peer) + .copied() + .expect("two reservations held"); + let drop_conn = *direct_conns + .get(&drop_peer) + .expect("direct connection observed"); + + assert!( + client.close_connection(drop_conn), + "should close the relay connection holding a reservation" + ); + + let sleep = tokio::time::sleep(Duration::from_secs(30)); + tokio::pin!(sleep); + loop { + tokio::select! { + _ = &mut sleep => panic!("timeout waiting for replacement reservation"), + ev = client.select_next_some() => { + if let SwarmEvent::Behaviour(ClientEvent::RelayClient( + relay::client::Event::ReservationReqAccepted { + relay_peer_id, + renewal: false, + .. + }, + )) = ev + && relay_peer_id != keep_peer + && relay_peer_id != drop_peer + { + return; + } + } + } + } +} + +#[tokio::test] +async fn autorelay_drops_reservations_when_public_address_appears() { + let _ = tracing_subscriber::fmt() + .with_env_filter(EnvFilter::from_default_env()) + .try_init(); + + let mut relay_a = build_relay(); + let relay_a_addr = Multiaddr::empty().with(Protocol::Memory(rand::random::())); + relay_a.listen_on(relay_a_addr.clone()).unwrap(); + relay_a.add_external_address(relay_a_addr.clone()); + tokio::spawn(async move { + relay_a.collect::>().await; + }); + + let mut relay_b = build_relay(); + let relay_b_addr = Multiaddr::empty().with(Protocol::Memory(rand::random::())); + relay_b.listen_on(relay_b_addr.clone()).unwrap(); + relay_b.add_external_address(relay_b_addr.clone()); + tokio::spawn(async move { + relay_b.collect::>().await; + }); + + let mut client = build_client(autorelay::Config::default().set_max_reservations(2)); + client.dial(relay_a_addr.clone()).unwrap(); + client.dial(relay_b_addr.clone()).unwrap(); + + let mut confirmed: HashSet = HashSet::new(); + let sleep = tokio::time::sleep(Duration::from_secs(30)); + tokio::pin!(sleep); + loop { + tokio::select! { + _ = &mut sleep => panic!( + "timeout: got {} confirmed external addresses, expected 2", + confirmed.len() + ), + ev = client.select_next_some() => { + if let SwarmEvent::ExternalAddrConfirmed { address } = ev + && address.iter().any(|p| p == Protocol::P2pCircuit) + { + confirmed.insert(address); + } + } + } + if confirmed.len() == 2 { + break; + } + } + + let public_addr = Multiaddr::empty().with(Protocol::Memory(rand::random::())); + client.add_external_address(public_addr); + + let mut expired: HashSet = HashSet::new(); + let sleep = tokio::time::sleep(Duration::from_secs(15)); + tokio::pin!(sleep); + loop { + tokio::select! { + _ = &mut sleep => panic!( + "timeout: only {}/{} relayed addresses expired", + expired.len(), + confirmed.len() + ), + ev = client.select_next_some() => { + if let SwarmEvent::ExternalAddrExpired { address } = ev + && confirmed.contains(&address) + { + expired.insert(address); + } + } + } + if expired == confirmed { + break; + } + } +} + +async fn wait_for_reservation_from_either( + client: &mut Swarm, + peer_a: PeerId, + peer_b: PeerId, + timeout: Duration, +) -> (PeerId, ConnectionId) { + let sleep = tokio::time::sleep(timeout); + tokio::pin!(sleep); + let mut direct_conns: HashMap = HashMap::new(); + loop { + tokio::select! { + _ = &mut sleep => panic!("timeout waiting for reservation from either relay"), + ev = client.select_next_some() => { + match ev { + SwarmEvent::ConnectionEstablished { + peer_id, + connection_id, + endpoint, + .. + } if (peer_id == peer_a || peer_id == peer_b) + && !endpoint.is_relayed() => + { + direct_conns.insert(peer_id, connection_id); + } + SwarmEvent::Behaviour(ClientEvent::RelayClient( + relay::client::Event::ReservationReqAccepted { relay_peer_id, .. }, + )) if relay_peer_id == peer_a || relay_peer_id == peer_b => { + let conn_id = direct_conns + .get(&relay_peer_id) + .copied() + .expect("direct connection to relay was observed"); + return (relay_peer_id, conn_id); + } + _ => {} + } + } + } + } +} + +async fn wait_until(client: &mut Swarm, timeout: Duration, mut predicate: F) +where + F: FnMut(&SwarmEvent) -> bool, +{ + let sleep = tokio::time::sleep(timeout); + tokio::pin!(sleep); + loop { + tokio::select! { + _ = &mut sleep => panic!("timeout waiting on predicate"), + ev = client.select_next_some() => { + if predicate(&ev) { + return; + } + } + } + } +} + +fn build_relay() -> Swarm { + let local_key = identity::Keypair::generate_ed25519(); + let local_peer_id = local_key.public().to_peer_id(); + let transport = upgrade_transport(MemoryTransport::default().boxed(), &local_key); + + Swarm::new( + transport, + Relay { + relay: relay::Behaviour::new( + local_peer_id, + relay::Config { + reservation_duration: Duration::from_secs(60), + ..Default::default() + }, + ), + identify: identify::Behaviour::new(identify::Config::new( + "/autorelay-test/1.0.0".to_owned(), + local_key.public(), + )), + }, + local_peer_id, + Config::with_tokio_executor(), + ) +} + +fn build_client(autorelay_config: autorelay::Config) -> Swarm { + let local_key = identity::Keypair::generate_ed25519(); + let local_peer_id = local_key.public().to_peer_id(); + let (relay_transport, relay_client) = relay::client::new(local_peer_id); + + let transport = upgrade_transport( + OrTransport::new(relay_transport, MemoryTransport::default()).boxed(), + &local_key, + ); + + Swarm::new( + transport, + Client { + relay_client, + autorelay: autorelay::Behaviour::new_with_config(autorelay_config), + identify: identify::Behaviour::new(identify::Config::new( + "/autorelay-test/1.0.0".to_owned(), + local_key.public(), + )), + }, + local_peer_id, + Config::with_tokio_executor(), + ) +} + +fn upgrade_transport( + transport: Boxed, + identity: &identity::Keypair, +) -> Boxed<(PeerId, StreamMuxerBox)> +where + StreamSink: AsyncRead + AsyncWrite + Send + Unpin + 'static, +{ + transport + .upgrade(upgrade::Version::V1) + .authenticate(plaintext::Config::new(identity)) + .multiplex(libp2p_yamux::Config::default()) + .boxed() +} + +#[derive(NetworkBehaviour)] +#[behaviour(prelude = "libp2p_swarm::derive_prelude")] +struct Relay { + relay: relay::Behaviour, + identify: identify::Behaviour, +} + +#[derive(NetworkBehaviour)] +#[behaviour(prelude = "libp2p_swarm::derive_prelude")] +struct Client { + relay_client: relay::client::Behaviour, + autorelay: autorelay::Behaviour, + identify: identify::Behaviour, +} From 1c6091f4298a5d69fe028486497bfa839d6b8387 Mon Sep 17 00:00:00 2001 From: Darius Clark Date: Sat, 16 May 2026 12:42:56 -0400 Subject: [PATCH 14/51] chore: add example --- Cargo.lock | 12 +++ Cargo.toml | 1 + examples/relay-client/Cargo.toml | 20 +++++ examples/relay-client/README.md | 17 ++++ examples/relay-client/src/main.rs | 134 ++++++++++++++++++++++++++++++ 5 files changed, 184 insertions(+) create mode 100644 examples/relay-client/Cargo.toml create mode 100644 examples/relay-client/README.md create mode 100644 examples/relay-client/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index 7e9d3456521..6b57e4c51f4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4703,6 +4703,18 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +[[package]] +name = "relay-client-example" +version = "0.1.0" +dependencies = [ + "clap", + "futures", + "libp2p", + "tokio", + "tracing", + "tracing-subscriber", +] + [[package]] name = "relay-server-example" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 3ab8f676a7e..289d962feb2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ members = [ "examples/ipfs-private", "examples/metrics", "examples/ping", + "examples/relay-client", "examples/relay-server", "examples/rendezvous", "examples/stream", diff --git a/examples/relay-client/Cargo.toml b/examples/relay-client/Cargo.toml new file mode 100644 index 00000000000..67bacd17005 --- /dev/null +++ b/examples/relay-client/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "relay-client-example" +version = "0.1.0" +edition.workspace = true +publish = false +license = "MIT" + +[package.metadata.release] +release = false + +[dependencies] +clap = { version = "4.6.1", features = ["derive"] } +futures = { workspace = true } +libp2p = { path = "../../libp2p", features = ["dns", "identify", "macros", "noise", "ping", "quic", "relay", "tcp", "tokio", "yamux"] } +tokio = { workspace = true, features = ["macros", "rt-multi-thread", "signal"] } +tracing = { workspace = true } +tracing-subscriber = { workspace = true, features = ["env-filter"] } + +[lints] +workspace = true diff --git a/examples/relay-client/README.md b/examples/relay-client/README.md new file mode 100644 index 00000000000..74b109ea4ec --- /dev/null +++ b/examples/relay-client/README.md @@ -0,0 +1,17 @@ +## Description + +A small relay client that demonstrates the autorelay. + +## Run the client + +In another terminal, pointing it at the relay: + +``` +cargo run \ + --secret-key-seed 1 \ + --relay /ip4/$RELAY_IP/tcp/$PORT/p2p/$RELAY_PEERID \ + --max-reservations 2 +``` + +Provide `relay` multiple times to point at additional relays; autorelay will +pick among them up to `max-reservations`. \ No newline at end of file diff --git a/examples/relay-client/src/main.rs b/examples/relay-client/src/main.rs new file mode 100644 index 00000000000..864de6a00a1 --- /dev/null +++ b/examples/relay-client/src/main.rs @@ -0,0 +1,134 @@ +use std::{error::Error, num::NonZeroU8}; + +use clap::Parser; +use futures::stream::StreamExt; +use libp2p::{ + core::multiaddr::Multiaddr, + identify, identity, noise, ping, + relay::{self, autorelay}, + swarm::{NetworkBehaviour, SwarmEvent}, + tcp, yamux, +}; +use tracing_subscriber::EnvFilter; + +#[derive(Debug, Parser)] +#[command(name = "libp2p relay client")] +struct Opts { + /// Fixed value used to derive a deterministic peer id. + #[arg(long)] + secret_key_seed: u8, + + /// List of relay addresses + #[arg(long = "relay", required = true)] + relays: Vec, + + /// Maximum number of relay reservations autorelay should maintain. + #[arg(long, default_value_t = 2)] + max_reservations: u8, +} + +#[derive(NetworkBehaviour)] +struct Behaviour { + relay_client: relay::client::Behaviour, + autorelay: autorelay::Behaviour, + identify: identify::Behaviour, + ping: ping::Behaviour, +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + let _ = tracing_subscriber::fmt() + .with_env_filter(EnvFilter::from_default_env()) + .try_init(); + + let opts = Opts::parse(); + + let max_reservations = NonZeroU8::new(opts.max_reservations) + .ok_or("--max-reservations must be greater than zero")?; + let autorelay_config = autorelay::Config::default().set_max_reservations(max_reservations.get()); + + let mut swarm = libp2p::SwarmBuilder::with_existing_identity(generate_ed25519(opts.secret_key_seed)) + .with_tokio() + .with_tcp( + tcp::Config::default().nodelay(true), + noise::Config::new, + yamux::Config::default, + )? + .with_quic() + .with_dns()? + .with_relay_client(noise::Config::new, yamux::Config::default)? + .with_behaviour(|keypair, relay_behaviour| Behaviour { + relay_client: relay_behaviour, + autorelay: autorelay::Behaviour::new_with_config(autorelay_config), + identify: identify::Behaviour::new(identify::Config::new( + "/autorelay-example/0.1.0".to_owned(), + keypair.public(), + )), + ping: ping::Behaviour::new(ping::Config::new()), + })? + .build(); + + swarm.listen_on("/ip4/0.0.0.0/tcp/0".parse()?)?; + swarm.listen_on("/ip4/0.0.0.0/udp/0/quic-v1".parse()?)?; + + let local_peer_id = *swarm.local_peer_id(); + tracing::info!(%local_peer_id, "Local peer id"); + + for addr in &opts.relays { + tracing::info!(%addr, "Dialing relay"); + swarm.dial(addr.clone())?; + } + + loop { + tokio::select! { + event = swarm.select_next_some() => match event { + SwarmEvent::NewListenAddr { address, .. } => { + tracing::info!(%address, "Listening"); + } + SwarmEvent::ConnectionEstablished { + peer_id, endpoint, .. + } => { + tracing::info!(peer=%peer_id, address=%endpoint.get_remote_address(), "Connected"); + } + SwarmEvent::ConnectionClosed { peer_id, .. } => { + tracing::info!(peer=%peer_id, "Disconnected"); + } + SwarmEvent::ExternalAddrConfirmed { address } => { + tracing::info!(%address, "External address confirmed"); + } + SwarmEvent::ExternalAddrExpired { address } => { + tracing::info!(%address, "External address expired"); + } + SwarmEvent::Behaviour(BehaviourEvent::RelayClient( + relay::client::Event::ReservationReqAccepted { + relay_peer_id, renewal, .. + }, + )) => { + if renewal { + tracing::info!(%relay_peer_id, "Reservation renewed"); + } else { + tracing::info!(%relay_peer_id, "Reservation accepted"); + } + } + SwarmEvent::Behaviour(BehaviourEvent::RelayClient(event)) => { + tracing::debug!(?event, "Relay client event"); + } + SwarmEvent::Behaviour(BehaviourEvent::Identify(identify::Event::Received { + peer_id, info, .. + })) => { + tracing::debug!(peer=%peer_id, protocols=?info.protocols, "Identify received"); + } + SwarmEvent::Behaviour(BehaviourEvent::Identify(_)) => {} + SwarmEvent::Behaviour(BehaviourEvent::Ping(_)) => {} + SwarmEvent::Behaviour(BehaviourEvent::Autorelay(_)) => {} + _ => {} + }, + } + } +} + +fn generate_ed25519(secret_key_seed: u8) -> identity::Keypair { + let mut bytes = [0u8; 32]; + bytes[0] = secret_key_seed; + identity::Keypair::ed25519_from_bytes(bytes).expect("only errors on wrong length") +} From 414222c46024166d3220b94859f8691f4e5a55bd Mon Sep 17 00:00:00 2001 From: Darius Clark Date: Sat, 16 May 2026 15:01:57 -0400 Subject: [PATCH 15/51] chore: misc changes --- examples/relay-client/src/main.rs | 2 +- protocols/relay/src/autorelay.rs | 21 +++++---------------- protocols/relay/tests/autorelay.rs | 9 +++++---- 3 files changed, 11 insertions(+), 21 deletions(-) diff --git a/examples/relay-client/src/main.rs b/examples/relay-client/src/main.rs index 864de6a00a1..1599efdf5a3 100644 --- a/examples/relay-client/src/main.rs +++ b/examples/relay-client/src/main.rs @@ -45,7 +45,7 @@ async fn main() -> Result<(), Box> { let max_reservations = NonZeroU8::new(opts.max_reservations) .ok_or("--max-reservations must be greater than zero")?; - let autorelay_config = autorelay::Config::default().set_max_reservations(max_reservations.get()); + let autorelay_config = autorelay::Config::default().set_max_reservations(max_reservations); let mut swarm = libp2p::SwarmBuilder::with_existing_identity(generate_ed25519(opts.secret_key_seed)) .with_tokio() diff --git a/protocols/relay/src/autorelay.rs b/protocols/relay/src/autorelay.rs index 5d688ebb860..386f811ab3f 100644 --- a/protocols/relay/src/autorelay.rs +++ b/protocols/relay/src/autorelay.rs @@ -1,7 +1,7 @@ use std::{ collections::{HashMap, HashSet, VecDeque}, num::NonZeroU8, - task::{Context, Poll, Waker}, + task::{Context, Poll}, }; use either::Either; @@ -36,8 +36,6 @@ pub struct Behaviour { reservations: HashMap, external_reservations: HashMap, - - waker: Option, } #[derive(Debug)] @@ -84,9 +82,8 @@ impl Default for Config { } impl Config { - pub fn set_max_reservations(mut self, max_reservations: u8) -> Self { - assert!(max_reservations > 0); - self.max_reservations = NonZeroU8::new(max_reservations).expect("greater than zero"); + pub fn set_max_reservations(mut self, max_reservations: NonZeroU8) -> Self { + self.max_reservations = max_reservations; self } } @@ -234,10 +231,6 @@ impl Behaviour { } debug_assert!(self.covered_peers().len() <= max); - - if let Some(waker) = self.waker.take() { - waker.wake(); - } } } @@ -324,7 +317,7 @@ impl NetworkBehaviour for Behaviour { FromSwarm::AddressChange(AddressChange { peer_id, connection_id, - old, + old: _, new, }) => { let connection = self @@ -332,11 +325,8 @@ impl NetworkBehaviour for Behaviour { .get_mut(&(peer_id, connection_id)) .expect("valid connection"); - let old_addr = old.get_remote_address(); let new_addr = new.get_remote_address(); - debug_assert!(old_addr != new_addr); - connection.address = new_addr.clone(); } FromSwarm::NewListenAddr(NewListenAddr { listener_id, addr }) => { @@ -427,12 +417,11 @@ impl NetworkBehaviour for Behaviour { fn poll( &mut self, - cx: &mut Context<'_>, + _cx: &mut Context<'_>, ) -> Poll>> { if let Some(event) = self.events.pop_front() { return Poll::Ready(event); } - self.waker.replace(cx.waker().clone()); Poll::Pending } } diff --git a/protocols/relay/tests/autorelay.rs b/protocols/relay/tests/autorelay.rs index 85b281c7b06..4af961a6426 100644 --- a/protocols/relay/tests/autorelay.rs +++ b/protocols/relay/tests/autorelay.rs @@ -1,5 +1,6 @@ use std::{ collections::{HashMap, HashSet}, + num::NonZeroU8, time::Duration, }; @@ -91,7 +92,7 @@ async fn autorelay_respects_max_reservations() { relay_b.collect::>().await; }); - let mut client = build_client(autorelay::Config::default().set_max_reservations(1)); + let mut client = build_client(autorelay::Config::default().set_max_reservations(NonZeroU8::new(1).unwrap())); client.dial(relay_a_addr.clone()).unwrap(); client.dial(relay_b_addr.clone()).unwrap(); @@ -148,7 +149,7 @@ async fn autorelay_refills_after_connection_drop() { relay_b.collect::>().await; }); - let mut client = build_client(autorelay::Config::default().set_max_reservations(1)); + let mut client = build_client(autorelay::Config::default().set_max_reservations(NonZeroU8::new(1).unwrap())); client.dial(relay_a_addr.clone()).unwrap(); client.dial(relay_b_addr.clone()).unwrap(); @@ -208,7 +209,7 @@ async fn autorelay_with_two_reservations_among_five_relays() { let relay_peers: HashSet = relay_addrs.iter().map(|(p, _)| *p).collect(); - let mut client = build_client(autorelay::Config::default().set_max_reservations(2)); + let mut client = build_client(autorelay::Config::default().set_max_reservations(NonZeroU8::new(2).unwrap())); for (_, addr) in &relay_addrs { client.dial(addr.clone()).unwrap(); } @@ -307,7 +308,7 @@ async fn autorelay_drops_reservations_when_public_address_appears() { relay_b.collect::>().await; }); - let mut client = build_client(autorelay::Config::default().set_max_reservations(2)); + let mut client = build_client(autorelay::Config::default().set_max_reservations(NonZeroU8::new(2).unwrap())); client.dial(relay_a_addr.clone()).unwrap(); client.dial(relay_b_addr.clone()).unwrap(); From 94a8dcd560926d873b49fb7840855e5a7f8fe405 Mon Sep 17 00:00:00 2001 From: Darius Clark Date: Sat, 16 May 2026 15:02:39 -0400 Subject: [PATCH 16/51] chore: fmt --- protocols/relay/src/autorelay.rs | 6 ++---- protocols/relay/tests/autorelay.rs | 12 ++++++++---- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/protocols/relay/src/autorelay.rs b/protocols/relay/src/autorelay.rs index 386f811ab3f..4b731954c77 100644 --- a/protocols/relay/src/autorelay.rs +++ b/protocols/relay/src/autorelay.rs @@ -46,12 +46,11 @@ struct Connection { impl Connection { /// Mark relayed connection as not supported - pub(crate) fn disqualify_connection_if_relayed(&mut self) { + pub(crate) fn disqualify_connection_if_relayed(&mut self) { if self.address.is_relayed() { self.relay_status = RelayStatus::NotSupported; } } - } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -334,8 +333,7 @@ impl NetworkBehaviour for Behaviour { return; } - if let Some((peer_id, connection_id)) = - self.reservations.get(&listener_id).copied() + if let Some((peer_id, connection_id)) = self.reservations.get(&listener_id).copied() { let connection = self .connections diff --git a/protocols/relay/tests/autorelay.rs b/protocols/relay/tests/autorelay.rs index 4af961a6426..279846aaaeb 100644 --- a/protocols/relay/tests/autorelay.rs +++ b/protocols/relay/tests/autorelay.rs @@ -92,7 +92,8 @@ async fn autorelay_respects_max_reservations() { relay_b.collect::>().await; }); - let mut client = build_client(autorelay::Config::default().set_max_reservations(NonZeroU8::new(1).unwrap())); + let mut client = + build_client(autorelay::Config::default().set_max_reservations(NonZeroU8::new(1).unwrap())); client.dial(relay_a_addr.clone()).unwrap(); client.dial(relay_b_addr.clone()).unwrap(); @@ -149,7 +150,8 @@ async fn autorelay_refills_after_connection_drop() { relay_b.collect::>().await; }); - let mut client = build_client(autorelay::Config::default().set_max_reservations(NonZeroU8::new(1).unwrap())); + let mut client = + build_client(autorelay::Config::default().set_max_reservations(NonZeroU8::new(1).unwrap())); client.dial(relay_a_addr.clone()).unwrap(); client.dial(relay_b_addr.clone()).unwrap(); @@ -209,7 +211,8 @@ async fn autorelay_with_two_reservations_among_five_relays() { let relay_peers: HashSet = relay_addrs.iter().map(|(p, _)| *p).collect(); - let mut client = build_client(autorelay::Config::default().set_max_reservations(NonZeroU8::new(2).unwrap())); + let mut client = + build_client(autorelay::Config::default().set_max_reservations(NonZeroU8::new(2).unwrap())); for (_, addr) in &relay_addrs { client.dial(addr.clone()).unwrap(); } @@ -308,7 +311,8 @@ async fn autorelay_drops_reservations_when_public_address_appears() { relay_b.collect::>().await; }); - let mut client = build_client(autorelay::Config::default().set_max_reservations(NonZeroU8::new(2).unwrap())); + let mut client = + build_client(autorelay::Config::default().set_max_reservations(NonZeroU8::new(2).unwrap())); client.dial(relay_a_addr.clone()).unwrap(); client.dial(relay_b_addr.clone()).unwrap(); From f0bdcc0343b640a214fe45258b7d9bb07a6dc7ca Mon Sep 17 00:00:00 2001 From: Darius Clark Date: Sat, 16 May 2026 19:45:19 -0400 Subject: [PATCH 17/51] chore: add a backoff to prevent using the relay for a specific duration after an error or failed reservation --- protocols/relay/src/autorelay.rs | 55 +++++++++++++--- protocols/relay/src/autorelay/handler.rs | 36 +++++++++- protocols/relay/tests/autorelay.rs | 83 ++++++++++++++++++++++-- 3 files changed, 155 insertions(+), 19 deletions(-) diff --git a/protocols/relay/src/autorelay.rs b/protocols/relay/src/autorelay.rs index 4b731954c77..b5dc8a2af83 100644 --- a/protocols/relay/src/autorelay.rs +++ b/protocols/relay/src/autorelay.rs @@ -2,6 +2,7 @@ use std::{ collections::{HashMap, HashSet, VecDeque}, num::NonZeroU8, task::{Context, Poll}, + time::Duration, }; use either::Either; @@ -12,7 +13,7 @@ use libp2p_core::{ }; use libp2p_identity::PeerId; use libp2p_swarm::{ - ExternalAddresses, ListenOpts, NewListenAddr, + ExternalAddresses, ListenOpts, NewListenAddr, NotifyHandler, derive_prelude::{ AddressChange, ConnectionClosed, ConnectionDenied, ConnectionEstablished, ConnectionId, ExpiredListenAddr, ExternalAddrConfirmed, FromSwarm, ListenerClosed, ListenerError, @@ -65,17 +66,20 @@ enum ReservationStatus { Idle, Pending { id: ListenerId }, Active { id: ListenerId }, + Blacklisted, } #[derive(Debug)] pub struct Config { max_reservations: NonZeroU8, + failure_cooldown: Duration, } impl Default for Config { fn default() -> Self { Self { max_reservations: NonZeroU8::new(2).unwrap(), + failure_cooldown: Duration::from_secs(30), } } } @@ -85,6 +89,11 @@ impl Config { self.max_reservations = max_reservations; self } + + pub fn set_failure_cooldown(mut self, duration: Duration) -> Self { + self.failure_cooldown = duration; + self + } } #[derive(Debug)] @@ -161,7 +170,7 @@ impl Behaviour { } } - fn disable_reservation(&mut self, id: ListenerId) { + fn disable_reservation(&mut self, id: ListenerId, failed: bool) { if self.external_reservations.remove(&id).is_some() { self.meet_reservation_target(); return; @@ -179,9 +188,22 @@ impl Behaviour { } ) { - connection.relay_status = RelayStatus::Supported { - status: ReservationStatus::Idle, - }; + if failed { + connection.relay_status = RelayStatus::Supported { + status: ReservationStatus::Blacklisted, + }; + self.events.push_back(ToSwarm::NotifyHandler { + peer_id, + handler: NotifyHandler::One(connection_id), + event: Either::Left(handler::In::Blacklist { + duration: self.config.failure_cooldown, + }), + }); + } else { + connection.relay_status = RelayStatus::Supported { + status: ReservationStatus::Idle, + }; + } } self.meet_reservation_target(); @@ -359,13 +381,15 @@ impl NetworkBehaviour for Behaviour { } } FromSwarm::ExpiredListenAddr(ExpiredListenAddr { listener_id, .. }) => { - self.disable_reservation(listener_id); + self.disable_reservation(listener_id, false); } FromSwarm::ListenerError(ListenerError { listener_id, .. }) => { - self.disable_reservation(listener_id); + self.disable_reservation(listener_id, true); } - FromSwarm::ListenerClosed(ListenerClosed { listener_id, .. }) => { - self.disable_reservation(listener_id); + FromSwarm::ListenerClosed(ListenerClosed { + listener_id, reason, .. + }) => { + self.disable_reservation(listener_id, reason.is_err()); } _ => {} } @@ -410,6 +434,19 @@ impl NetworkBehaviour for Behaviour { self.meet_reservation_target(); } } + Out::BlacklistExpired => { + if matches!( + connection.relay_status, + RelayStatus::Supported { + status: ReservationStatus::Blacklisted + } + ) { + connection.relay_status = RelayStatus::Supported { + status: ReservationStatus::Idle, + }; + self.meet_reservation_target(); + } + } } } diff --git a/protocols/relay/src/autorelay/handler.rs b/protocols/relay/src/autorelay/handler.rs index 18fcdfd7196..885f74314cc 100644 --- a/protocols/relay/src/autorelay/handler.rs +++ b/protocols/relay/src/autorelay/handler.rs @@ -1,8 +1,11 @@ use std::{ collections::VecDeque, task::{Context, Poll}, + time::Duration, }; +use futures::FutureExt; +use futures_timer::Delay; use libp2p_core::upgrade::DeniedUpgrade; use libp2p_swarm::{ ConnectionHandler, ConnectionHandlerEvent, SubstreamProtocol, SupportedProtocols, @@ -24,17 +27,25 @@ pub struct Handler { supported: bool, supported_protocol: SupportedProtocols, + + blacklist_timer: Option, +} + +#[derive(Debug, Copy, Clone)] +pub enum In { + Blacklist { duration: Duration }, } #[derive(Debug, Copy, Clone)] pub enum Out { Supported, Unsupported, + BlacklistExpired, } #[allow(deprecated)] impl ConnectionHandler for Handler { - type FromBehaviour = (); + type FromBehaviour = In; type ToBehaviour = Out; type InboundProtocol = DeniedUpgrade; type OutboundProtocol = DeniedUpgrade; @@ -49,7 +60,13 @@ impl ConnectionHandler for Handler { false } - fn on_behaviour_event(&mut self, _event: Self::FromBehaviour) {} + fn on_behaviour_event(&mut self, event: Self::FromBehaviour) { + match event { + In::Blacklist { duration } => { + self.blacklist_timer = Some(Delay::new(duration)); + } + } + } fn on_connection_event( &mut self, @@ -78,6 +95,7 @@ impl ConnectionHandler for Handler { } (false, true) => { self.supported = false; + self.blacklist_timer = None; self.events .push_back(ConnectionHandlerEvent::NotifyBehaviour( Out::Unsupported, @@ -94,13 +112,25 @@ impl ConnectionHandler for Handler { fn poll( &mut self, - _: &mut Context<'_>, + cx: &mut Context<'_>, ) -> Poll< ConnectionHandlerEvent, > { if let Some(event) = self.events.pop_front() { return Poll::Ready(event); } + + if let Some(timer) = self.blacklist_timer.as_mut() + && timer.poll_unpin(cx).is_ready() + { + self.blacklist_timer = None; + if self.supported { + return Poll::Ready(ConnectionHandlerEvent::NotifyBehaviour( + Out::BlacklistExpired, + )); + } + } + Poll::Pending } } diff --git a/protocols/relay/tests/autorelay.rs b/protocols/relay/tests/autorelay.rs index 279846aaaeb..04c208505c3 100644 --- a/protocols/relay/tests/autorelay.rs +++ b/protocols/relay/tests/autorelay.rs @@ -365,6 +365,67 @@ async fn autorelay_drops_reservations_when_public_address_appears() { } } +#[tokio::test] +async fn autorelay_blacklists_failing_relay_and_retries_after_cooldown() { + let _ = tracing_subscriber::fmt() + .with_env_filter(EnvFilter::from_default_env()) + .try_init(); + + let mut relay = build_rejecting_relay(); + let relay_addr = Multiaddr::empty().with(Protocol::Memory(rand::random::())); + relay.listen_on(relay_addr.clone()).unwrap(); + relay.add_external_address(relay_addr.clone()); + tokio::spawn(async move { + relay.collect::>().await; + }); + + let cooldown = Duration::from_secs(1); + let mut client = build_client( + autorelay::Config::default() + .set_max_reservations(NonZeroU8::new(1).unwrap()) + .set_failure_cooldown(cooldown), + ); + client.dial(relay_addr).unwrap(); + + let first_failure_at = wait_for_listener_failure(&mut client, Duration::from_secs(10)).await; + + let early_retry = tokio::time::timeout( + cooldown / 2, + wait_for_listener_failure(&mut client, cooldown * 5), + ) + .await; + assert!( + early_retry.is_err(), + "autorelay retried during the cooldown window" + ); + + let second_failure_at = + wait_for_listener_failure(&mut client, cooldown * 5).await; + let elapsed = second_failure_at.duration_since(first_failure_at); + assert!( + elapsed >= cooldown, + "retry should respect cooldown (elapsed {elapsed:?}, cooldown {cooldown:?})" + ); +} + +async fn wait_for_listener_failure( + client: &mut Swarm, + timeout: Duration, +) -> std::time::Instant { + let sleep = tokio::time::sleep(timeout); + tokio::pin!(sleep); + loop { + tokio::select! { + _ = &mut sleep => panic!("timeout waiting for listener failure"), + ev = client.select_next_some() => { + if let SwarmEvent::ListenerClosed { reason: Err(_), .. } = ev { + return std::time::Instant::now(); + } + } + } + } +} + async fn wait_for_reservation_from_either( client: &mut Swarm, peer_a: PeerId, @@ -424,6 +485,20 @@ where } fn build_relay() -> Swarm { + build_relay_with_config(relay::Config { + reservation_duration: Duration::from_secs(60), + ..Default::default() + }) +} + +fn build_rejecting_relay() -> Swarm { + build_relay_with_config(relay::Config { + max_reservations: 0, + ..Default::default() + }) +} + +fn build_relay_with_config(config: relay::Config) -> Swarm { let local_key = identity::Keypair::generate_ed25519(); let local_peer_id = local_key.public().to_peer_id(); let transport = upgrade_transport(MemoryTransport::default().boxed(), &local_key); @@ -431,13 +506,7 @@ fn build_relay() -> Swarm { Swarm::new( transport, Relay { - relay: relay::Behaviour::new( - local_peer_id, - relay::Config { - reservation_duration: Duration::from_secs(60), - ..Default::default() - }, - ), + relay: relay::Behaviour::new(local_peer_id, config), identify: identify::Behaviour::new(identify::Config::new( "/autorelay-test/1.0.0".to_owned(), local_key.public(), From 1f83b52b817a06e58f7023b46fc1ad7074ad971e Mon Sep 17 00:00:00 2001 From: Darius Clark Date: Sat, 16 May 2026 19:53:34 -0400 Subject: [PATCH 18/51] chore: fmt --- protocols/relay/src/autorelay.rs | 4 +++- protocols/relay/tests/autorelay.rs | 3 +-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/protocols/relay/src/autorelay.rs b/protocols/relay/src/autorelay.rs index b5dc8a2af83..a823a1ce4b2 100644 --- a/protocols/relay/src/autorelay.rs +++ b/protocols/relay/src/autorelay.rs @@ -387,7 +387,9 @@ impl NetworkBehaviour for Behaviour { self.disable_reservation(listener_id, true); } FromSwarm::ListenerClosed(ListenerClosed { - listener_id, reason, .. + listener_id, + reason, + .. }) => { self.disable_reservation(listener_id, reason.is_err()); } diff --git a/protocols/relay/tests/autorelay.rs b/protocols/relay/tests/autorelay.rs index 04c208505c3..d02442b34b1 100644 --- a/protocols/relay/tests/autorelay.rs +++ b/protocols/relay/tests/autorelay.rs @@ -399,8 +399,7 @@ async fn autorelay_blacklists_failing_relay_and_retries_after_cooldown() { "autorelay retried during the cooldown window" ); - let second_failure_at = - wait_for_listener_failure(&mut client, cooldown * 5).await; + let second_failure_at = wait_for_listener_failure(&mut client, cooldown * 5).await; let elapsed = second_failure_at.duration_since(first_failure_at); assert!( elapsed >= cooldown, From 05540513d88291771ffdf5d9999210397cccb3b7 Mon Sep 17 00:00:00 2001 From: Darius Clark Date: Sat, 16 May 2026 19:57:23 -0400 Subject: [PATCH 19/51] chore: fmt --- examples/relay-client/src/main.rs | 43 ++++++++++++++++--------------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/examples/relay-client/src/main.rs b/examples/relay-client/src/main.rs index 1599efdf5a3..88a011a377a 100644 --- a/examples/relay-client/src/main.rs +++ b/examples/relay-client/src/main.rs @@ -47,26 +47,27 @@ async fn main() -> Result<(), Box> { .ok_or("--max-reservations must be greater than zero")?; let autorelay_config = autorelay::Config::default().set_max_reservations(max_reservations); - let mut swarm = libp2p::SwarmBuilder::with_existing_identity(generate_ed25519(opts.secret_key_seed)) - .with_tokio() - .with_tcp( - tcp::Config::default().nodelay(true), - noise::Config::new, - yamux::Config::default, - )? - .with_quic() - .with_dns()? - .with_relay_client(noise::Config::new, yamux::Config::default)? - .with_behaviour(|keypair, relay_behaviour| Behaviour { - relay_client: relay_behaviour, - autorelay: autorelay::Behaviour::new_with_config(autorelay_config), - identify: identify::Behaviour::new(identify::Config::new( - "/autorelay-example/0.1.0".to_owned(), - keypair.public(), - )), - ping: ping::Behaviour::new(ping::Config::new()), - })? - .build(); + let mut swarm = + libp2p::SwarmBuilder::with_existing_identity(generate_ed25519(opts.secret_key_seed)) + .with_tokio() + .with_tcp( + tcp::Config::default().nodelay(true), + noise::Config::new, + yamux::Config::default, + )? + .with_quic() + .with_dns()? + .with_relay_client(noise::Config::new, yamux::Config::default)? + .with_behaviour(|keypair, relay_behaviour| Behaviour { + relay_client: relay_behaviour, + autorelay: autorelay::Behaviour::new_with_config(autorelay_config), + identify: identify::Behaviour::new(identify::Config::new( + "/autorelay-example/0.1.0".to_owned(), + keypair.public(), + )), + ping: ping::Behaviour::new(ping::Config::new()), + })? + .build(); swarm.listen_on("/ip4/0.0.0.0/tcp/0".parse()?)?; swarm.listen_on("/ip4/0.0.0.0/udp/0/quic-v1".parse()?)?; @@ -78,7 +79,7 @@ async fn main() -> Result<(), Box> { tracing::info!(%addr, "Dialing relay"); swarm.dial(addr.clone())?; } - + loop { tokio::select! { event = swarm.select_next_some() => match event { From adab09995e10ca427f6f938a817e4629de9d39cf Mon Sep 17 00:00:00 2001 From: Darius Clark Date: Sat, 16 May 2026 21:30:59 -0400 Subject: [PATCH 20/51] chore: update changeling --- protocols/relay/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/protocols/relay/CHANGELOG.md b/protocols/relay/CHANGELOG.md index 874f825df58..7162a81f48c 100644 --- a/protocols/relay/CHANGELOG.md +++ b/protocols/relay/CHANGELOG.md @@ -9,7 +9,7 @@ - Expire external address when a relay listener is closed without a replacement reservation. See [PR 6285](https://github.com/libp2p/rust-libp2p/pull/6285). - Implements autorelay that would make a reservation as soon as a connection reports supporting HOP protocol. - See [PR XXXX](https://github.com/libp2p/rust-libp2p/pull/XXXX) + See [PR 6156](https://github.com/libp2p/rust-libp2p/pull/6156) ## 0.21.1 - reduce allocations by replacing `get_or_insert` with `get_or_insert_with` From 0ce7695b4d842abfa3e9ee6aa4d4d6111db925ca Mon Sep 17 00:00:00 2001 From: Darius Clark Date: Sun, 17 May 2026 11:58:56 -0400 Subject: [PATCH 21/51] chore: add function to enable/disable autorelay --- protocols/relay/src/autorelay.rs | 37 +++++++- protocols/relay/tests/autorelay.rs | 144 +++++++++++++++++++++++++++++ 2 files changed, 179 insertions(+), 2 deletions(-) diff --git a/protocols/relay/src/autorelay.rs b/protocols/relay/src/autorelay.rs index a823a1ce4b2..431651d3b2d 100644 --- a/protocols/relay/src/autorelay.rs +++ b/protocols/relay/src/autorelay.rs @@ -4,7 +4,7 @@ use std::{ task::{Context, Poll}, time::Duration, }; - +use std::task::Waker; use either::Either; use libp2p_core::{ Endpoint, @@ -29,6 +29,7 @@ mod handler; #[derive(Default, Debug)] pub struct Behaviour { config: Config, + status: Status, external_addresses: ExternalAddresses, events: VecDeque::ToSwarm, THandlerInEvent>>, @@ -37,6 +38,14 @@ pub struct Behaviour { reservations: HashMap, external_reservations: HashMap, + waker: Option +} + +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] +pub enum Status { + #[default] + Enable, + Disable, } #[derive(Debug)] @@ -108,6 +117,23 @@ impl Behaviour { } } + pub fn status(&self) -> Status { + self.status + } + + pub fn set_status(&mut self, status: Status) { + if self.status == status { + return; + } + self.status = status; + if status == Status::Enable { + self.meet_reservation_target(); + if let Some(waker) = self.waker.take() { + waker.wake(); + } + } + } + fn select_connection_for_reservation(&mut self, peer_id: PeerId, connection_id: ConnectionId) { let info = self .connections @@ -218,6 +244,10 @@ impl Behaviour { } fn meet_reservation_target(&mut self) { + if self.status == Status::Disable { + return; + } + if self .external_addresses .iter() @@ -454,11 +484,14 @@ impl NetworkBehaviour for Behaviour { fn poll( &mut self, - _cx: &mut Context<'_>, + cx: &mut Context<'_>, ) -> Poll>> { if let Some(event) = self.events.pop_front() { return Poll::Ready(event); } + + self.waker = Some(cx.waker().clone()); + Poll::Pending } } diff --git a/protocols/relay/tests/autorelay.rs b/protocols/relay/tests/autorelay.rs index d02442b34b1..8966543b6fb 100644 --- a/protocols/relay/tests/autorelay.rs +++ b/protocols/relay/tests/autorelay.rs @@ -425,6 +425,150 @@ async fn wait_for_listener_failure( } } +#[tokio::test] +async fn autorelay_disabled_does_not_reserve() { + let _ = tracing_subscriber::fmt() + .with_env_filter(EnvFilter::from_default_env()) + .try_init(); + + let mut relay = build_relay(); + let relay_addr = Multiaddr::empty().with(Protocol::Memory(rand::random::())); + relay.listen_on(relay_addr.clone()).unwrap(); + relay.add_external_address(relay_addr.clone()); + tokio::spawn(async move { + relay.collect::>().await; + }); + + let mut client = build_client(autorelay::Config::default()); + client + .behaviour_mut() + .autorelay + .set_status(autorelay::Status::Disable); + client.dial(relay_addr).unwrap(); + + let observed = tokio::time::timeout( + Duration::from_secs(3), + wait_until(&mut client, Duration::from_secs(5), |event| { + matches!( + event, + SwarmEvent::Behaviour(ClientEvent::RelayClient( + relay::client::Event::ReservationReqAccepted { .. } + )) + ) + }), + ) + .await; + + assert!( + observed.is_err(), + "autorelay opened a reservation while disabled" + ); +} + +#[tokio::test] +async fn autorelay_re_enable_triggers_reservation() { + let _ = tracing_subscriber::fmt() + .with_env_filter(EnvFilter::from_default_env()) + .try_init(); + + let mut relay = build_relay(); + let relay_addr = Multiaddr::empty().with(Protocol::Memory(rand::random::())); + relay.listen_on(relay_addr.clone()).unwrap(); + relay.add_external_address(relay_addr.clone()); + tokio::spawn(async move { + relay.collect::>().await; + }); + + let mut client = build_client(autorelay::Config::default()); + client + .behaviour_mut() + .autorelay + .set_status(autorelay::Status::Disable); + client.dial(relay_addr).unwrap(); + + let sleep = tokio::time::sleep(Duration::from_secs(3)); + tokio::pin!(sleep); + loop { + tokio::select! { + _ = &mut sleep => break, + ev = client.select_next_some() => { + if matches!( + ev, + SwarmEvent::Behaviour(ClientEvent::RelayClient( + relay::client::Event::ReservationReqAccepted { .. } + )) + ) { + panic!("autorelay reserved while disabled"); + } + } + } + } + + client + .behaviour_mut() + .autorelay + .set_status(autorelay::Status::Enable); + + wait_until(&mut client, Duration::from_secs(10), |event| { + matches!( + event, + SwarmEvent::Behaviour(ClientEvent::RelayClient( + relay::client::Event::ReservationReqAccepted { .. } + )) + ) + }) + .await; +} + +#[tokio::test] +async fn autorelay_disable_preserves_active_reservation() { + let _ = tracing_subscriber::fmt() + .with_env_filter(EnvFilter::from_default_env()) + .try_init(); + + let mut relay = build_relay(); + let relay_addr = Multiaddr::empty().with(Protocol::Memory(rand::random::())); + relay.listen_on(relay_addr.clone()).unwrap(); + relay.add_external_address(relay_addr.clone()); + tokio::spawn(async move { + relay.collect::>().await; + }); + + let mut client = build_client(autorelay::Config::default()); + client.dial(relay_addr).unwrap(); + + wait_until(&mut client, Duration::from_secs(20), |event| { + matches!( + event, + SwarmEvent::Behaviour(ClientEvent::RelayClient( + relay::client::Event::ReservationReqAccepted { .. } + )) + ) + }) + .await; + + client + .behaviour_mut() + .autorelay + .set_status(autorelay::Status::Disable); + + let sleep = tokio::time::sleep(Duration::from_secs(3)); + tokio::pin!(sleep); + loop { + tokio::select! { + _ = &mut sleep => break, + ev = client.select_next_some() => { + if let SwarmEvent::ListenerClosed { reason: Err(_), .. } = ev { + panic!("disabling autorelay dropped an active reservation"); + } + if let SwarmEvent::ExternalAddrExpired { .. } = ev { + panic!("disabling autorelay expired an external address"); + } + } + } + } +} + async fn wait_for_reservation_from_either( client: &mut Swarm, peer_a: PeerId, From 4163c1062132dcf08b45da42d72ddd826b445769 Mon Sep 17 00:00:00 2001 From: Darius Clark Date: Wed, 20 May 2026 07:22:54 -0400 Subject: [PATCH 22/51] chore: add support for static relays and add status change event --- protocols/relay/src/autorelay.rs | 112 ++++++++++--- protocols/relay/tests/autorelay.rs | 255 +++++++++++++++++++++++++++++ 2 files changed, 347 insertions(+), 20 deletions(-) diff --git a/protocols/relay/src/autorelay.rs b/protocols/relay/src/autorelay.rs index 431651d3b2d..d4829a4bd18 100644 --- a/protocols/relay/src/autorelay.rs +++ b/protocols/relay/src/autorelay.rs @@ -1,10 +1,3 @@ -use std::{ - collections::{HashMap, HashSet, VecDeque}, - num::NonZeroU8, - task::{Context, Poll}, - time::Duration, -}; -use std::task::Waker; use either::Either; use libp2p_core::{ Endpoint, @@ -16,11 +9,20 @@ use libp2p_swarm::{ ExternalAddresses, ListenOpts, NewListenAddr, NotifyHandler, derive_prelude::{ AddressChange, ConnectionClosed, ConnectionDenied, ConnectionEstablished, ConnectionId, - ExpiredListenAddr, ExternalAddrConfirmed, FromSwarm, ListenerClosed, ListenerError, - Multiaddr, NetworkBehaviour, THandler, THandlerInEvent, THandlerOutEvent, ToSwarm, + DialFailure, ExpiredListenAddr, ExternalAddrConfirmed, FromSwarm, ListenerClosed, + ListenerError, Multiaddr, NetworkBehaviour, THandler, THandlerInEvent, THandlerOutEvent, + ToSwarm, }, + dial_opts::DialOpts, dummy, }; +use std::task::Waker; +use std::{ + collections::{HashMap, HashSet, VecDeque}, + num::NonZeroU8, + task::{Context, Poll}, + time::Duration, +}; use crate::{autorelay::handler::Out, multiaddr_ext::MultiaddrExt}; @@ -38,7 +40,10 @@ pub struct Behaviour { reservations: HashMap, external_reservations: HashMap, - waker: Option + + static_relays: HashMap, + + waker: Option, } #[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] @@ -107,7 +112,9 @@ impl Config { #[derive(Debug)] #[non_exhaustive] -pub enum Event {} +pub enum Event { + StatusChanged { status: Status }, +} impl Behaviour { pub fn new_with_config(config: Config) -> Self { @@ -117,23 +124,58 @@ impl Behaviour { } } - pub fn status(&self) -> Status { - self.status - } - + /// Sets the status of the local node to enable or disable autorelay. pub fn set_status(&mut self, status: Status) { if self.status == status { return; } self.status = status; + self.events.push_back(ToSwarm::GenerateEvent(Event::StatusChanged { status })); if status == Status::Enable { self.meet_reservation_target(); - if let Some(waker) = self.waker.take() { - waker.wake(); - } + } + if let Some(waker) = self.waker.take() { + waker.wake(); } } + /// Register a peer as a static relay. + /// + /// This will dial and establish a connection to the peer if it doesn't already have a direct connection. + pub fn add_static_relay(&mut self, peer_id: PeerId, address: Multiaddr) { + if address.is_relayed() { + tracing::warn!(%peer_id, %address, "static relay address is relayed. ignoring."); + return; + } + self.static_relays.insert(peer_id, address.clone()); + + if !self.has_direct_connection(&peer_id) { + let opts = DialOpts::peer_id(peer_id).addresses(vec![address]).build(); + self.events.push_back(ToSwarm::Dial { opts }); + } + + self.meet_reservation_target(); + if let Some(waker) = self.waker.take() { + waker.wake(); + } + } + + /// Remove peer as a static relay. + /// This will not close any connections or terminate any existing reservation with the relay + pub fn remove_static_relay(&mut self, peer_id: &PeerId) -> bool { + self.static_relays.remove(peer_id).is_some() + } + + pub fn static_relays(&self) -> impl Iterator { + self.static_relays.iter() + } + + fn has_direct_connection(&self, peer_id: &PeerId) -> bool { + self.connections + .iter() + .any(|((pid, _), info)| pid == peer_id && !info.address.is_relayed()) + } + fn select_connection_for_reservation(&mut self, peer_id: PeerId, connection_id: ConnectionId) { let info = self .connections @@ -166,6 +208,7 @@ impl Behaviour { self.events.push_back(ToSwarm::ListenOn { opts }); } + /// Removes all existing reservations. fn remove_all_reservations(&mut self) { let relay_listeners = self .reservations @@ -243,6 +286,7 @@ impl Behaviour { .collect() } + /// Meet the reservation target by selecting connections to establish a reservation. fn meet_reservation_target(&mut self) { if self.status == Status::Disable { return; @@ -263,21 +307,35 @@ impl Behaviour { return; } + let mut static_candidates = Vec::new(); let mut candidates = HashMap::new(); for ((peer_id, connection_id), info) in self.connections.iter() { if covered.contains(peer_id) { continue; } if info.relay_status - == (RelayStatus::Supported { + != (RelayStatus::Supported { status: ReservationStatus::Idle, }) { + continue; + } + if self.static_relays.contains_key(peer_id) { + if !static_candidates.iter().any(|(p, _)| p == peer_id) { + static_candidates.push((*peer_id, *connection_id)); + } + } else { candidates.entry(*peer_id).or_insert(*connection_id); } } - for (peer_id, connection_id) in candidates.into_iter().take(budget) { + let picks: Vec<(PeerId, ConnectionId)> = static_candidates + .into_iter() + .chain(candidates) + .take(budget) + .collect(); + + for (peer_id, connection_id) in picks { self.select_connection_for_reservation(peer_id, connection_id); } @@ -364,6 +422,13 @@ impl NetworkBehaviour for Behaviour { self.reservations.remove(&id); self.meet_reservation_target(); } + + if let Some(address) = self.static_relays.get(&peer_id).cloned() + && !self.has_direct_connection(&peer_id) + { + let opts = DialOpts::peer_id(peer_id).addresses(vec![address]).build(); + self.events.push_back(ToSwarm::Dial { opts }); + } } FromSwarm::AddressChange(AddressChange { peer_id, @@ -423,6 +488,13 @@ impl NetworkBehaviour for Behaviour { }) => { self.disable_reservation(listener_id, reason.is_err()); } + FromSwarm::DialFailure(DialFailure { + peer_id: Some(peer_id), + error, + .. + }) if self.static_relays.contains_key(&peer_id) => { + tracing::warn!(%peer_id, %error, "dial to static relay failed"); + } _ => {} } } diff --git a/protocols/relay/tests/autorelay.rs b/protocols/relay/tests/autorelay.rs index 8966543b6fb..c11993512c8 100644 --- a/protocols/relay/tests/autorelay.rs +++ b/protocols/relay/tests/autorelay.rs @@ -569,6 +569,261 @@ async fn autorelay_disable_preserves_active_reservation() { } } +#[tokio::test] +async fn autorelay_prefers_static_relay() { + let _ = tracing_subscriber::fmt() + .with_env_filter(EnvFilter::from_default_env()) + .try_init(); + + let mut opportunistic = build_relay(); + let opportunistic_addr = Multiaddr::empty().with(Protocol::Memory(rand::random::())); + let opportunistic_peer = *opportunistic.local_peer_id(); + opportunistic.listen_on(opportunistic_addr.clone()).unwrap(); + opportunistic.add_external_address(opportunistic_addr.clone()); + tokio::spawn(async move { + opportunistic.collect::>().await; + }); + + let mut staticr = build_relay(); + let static_addr = Multiaddr::empty().with(Protocol::Memory(rand::random::())); + let static_peer = *staticr.local_peer_id(); + staticr.listen_on(static_addr.clone()).unwrap(); + staticr.add_external_address(static_addr.clone()); + tokio::spawn(async move { + staticr.collect::>().await; + }); + + let mut client = + build_client(autorelay::Config::default().set_max_reservations(NonZeroU8::new(1).unwrap())); + client + .behaviour_mut() + .autorelay + .set_status(autorelay::Status::Disable); + + client.dial(opportunistic_addr).unwrap(); + client + .behaviour_mut() + .autorelay + .add_static_relay(static_peer, static_addr); + + // Let both connections establish and identify exchanges complete. + let warmup = tokio::time::sleep(Duration::from_secs(3)); + tokio::pin!(warmup); + loop { + tokio::select! { + _ = &mut warmup => break, + _ = client.select_next_some() => {} + } + } + + client + .behaviour_mut() + .autorelay + .set_status(autorelay::Status::Enable); + + let accepted_peer = wait_until_some(&mut client, Duration::from_secs(15), |event| { + if let SwarmEvent::Behaviour(ClientEvent::RelayClient( + relay::client::Event::ReservationReqAccepted { relay_peer_id, .. }, + )) = event + { + Some(*relay_peer_id) + } else { + None + } + }) + .await; + + assert_eq!( + accepted_peer, static_peer, + "autorelay should pick the static relay over the opportunistic one" + ); + assert_ne!(accepted_peer, opportunistic_peer); +} + +#[tokio::test] +async fn add_static_relay_dials_and_reserves() { + let _ = tracing_subscriber::fmt() + .with_env_filter(EnvFilter::from_default_env()) + .try_init(); + + let mut relay = build_relay(); + let relay_addr = Multiaddr::empty().with(Protocol::Memory(rand::random::())); + let relay_peer = *relay.local_peer_id(); + relay.listen_on(relay_addr.clone()).unwrap(); + relay.add_external_address(relay_addr.clone()); + tokio::spawn(async move { + relay.collect::>().await; + }); + + let mut client = build_client(autorelay::Config::default()); + client + .behaviour_mut() + .autorelay + .add_static_relay(relay_peer, relay_addr); + + let accepted_peer = wait_until_some(&mut client, Duration::from_secs(15), |event| { + if let SwarmEvent::Behaviour(ClientEvent::RelayClient( + relay::client::Event::ReservationReqAccepted { relay_peer_id, .. }, + )) = event + { + Some(*relay_peer_id) + } else { + None + } + }) + .await; + + assert_eq!(accepted_peer, relay_peer); +} + +#[tokio::test] +async fn remove_static_relay_preserves_active_reservation() { + let _ = tracing_subscriber::fmt() + .with_env_filter(EnvFilter::from_default_env()) + .try_init(); + + let mut relay = build_relay(); + let relay_addr = Multiaddr::empty().with(Protocol::Memory(rand::random::())); + let relay_peer = *relay.local_peer_id(); + relay.listen_on(relay_addr.clone()).unwrap(); + relay.add_external_address(relay_addr.clone()); + tokio::spawn(async move { + relay.collect::>().await; + }); + + let mut client = build_client(autorelay::Config::default()); + client + .behaviour_mut() + .autorelay + .add_static_relay(relay_peer, relay_addr); + + wait_until(&mut client, Duration::from_secs(15), |event| { + matches!( + event, + SwarmEvent::Behaviour(ClientEvent::RelayClient( + relay::client::Event::ReservationReqAccepted { .. } + )) + ) + }) + .await; + + assert!( + client + .behaviour_mut() + .autorelay + .remove_static_relay(&relay_peer) + ); + + let sleep = tokio::time::sleep(Duration::from_secs(3)); + tokio::pin!(sleep); + loop { + tokio::select! { + _ = &mut sleep => break, + ev = client.select_next_some() => { + if let SwarmEvent::ListenerClosed { reason: Err(_), .. } = ev { + panic!("removing static relay dropped an active reservation"); + } + if let SwarmEvent::ExternalAddrExpired { .. } = ev { + panic!("removing static relay expired an external address"); + } + } + } + } +} + +#[tokio::test] +async fn static_relay_redials_after_connection_drop() { + let _ = tracing_subscriber::fmt() + .with_env_filter(EnvFilter::from_default_env()) + .try_init(); + + let mut relay = build_relay(); + let relay_addr = Multiaddr::empty().with(Protocol::Memory(rand::random::())); + let relay_peer = *relay.local_peer_id(); + relay.listen_on(relay_addr.clone()).unwrap(); + relay.add_external_address(relay_addr.clone()); + tokio::spawn(async move { + relay.collect::>().await; + }); + + let mut client = build_client(autorelay::Config::default()); + client + .behaviour_mut() + .autorelay + .add_static_relay(relay_peer, relay_addr); + + let conn_id = wait_until_some(&mut client, Duration::from_secs(15), { + let mut established_conn: Option = None; + let mut reservation_seen = false; + move |event| { + match event { + SwarmEvent::ConnectionEstablished { + peer_id, + connection_id, + endpoint, + .. + } if *peer_id == relay_peer && !endpoint.is_relayed() => { + established_conn = Some(*connection_id); + } + SwarmEvent::Behaviour(ClientEvent::RelayClient( + relay::client::Event::ReservationReqAccepted { relay_peer_id, .. }, + )) if *relay_peer_id == relay_peer => { + reservation_seen = true; + } + _ => {} + } + if reservation_seen { + established_conn + } else { + None + } + } + }) + .await; + + assert!(client.close_connection(conn_id)); + + wait_until(&mut client, Duration::from_secs(20), { + let mut redialed = false; + let mut reserved_again = false; + move |event| { + match event { + SwarmEvent::ConnectionEstablished { + peer_id, endpoint, .. + } if *peer_id == relay_peer && !endpoint.is_relayed() => { + redialed = true; + } + SwarmEvent::Behaviour(ClientEvent::RelayClient( + relay::client::Event::ReservationReqAccepted { relay_peer_id, .. }, + )) if *relay_peer_id == relay_peer => { + reserved_again = true; + } + _ => {} + } + redialed && reserved_again + } + }) + .await; +} + +async fn wait_until_some(client: &mut Swarm, timeout: Duration, mut extract: F) -> T +where + F: FnMut(&SwarmEvent) -> Option, +{ + let sleep = tokio::time::sleep(timeout); + tokio::pin!(sleep); + loop { + tokio::select! { + _ = &mut sleep => panic!("timeout waiting on predicate"), + ev = client.select_next_some() => { + if let Some(value) = extract(&ev) { + return value; + } + } + } + } +} + async fn wait_for_reservation_from_either( client: &mut Swarm, peer_a: PeerId, From d72174169ae258924c6ccd19f983813dc6b522fc Mon Sep 17 00:00:00 2001 From: Darius Clark Date: Wed, 20 May 2026 08:20:24 -0400 Subject: [PATCH 23/51] chore: notify swarm about relay availability --- protocols/relay/src/autorelay.rs | 36 ++++- protocols/relay/tests/autorelay.rs | 205 +++++++++++++++++++++++++++++ 2 files changed, 239 insertions(+), 2 deletions(-) diff --git a/protocols/relay/src/autorelay.rs b/protocols/relay/src/autorelay.rs index d4829a4bd18..310a0a1eda2 100644 --- a/protocols/relay/src/autorelay.rs +++ b/protocols/relay/src/autorelay.rs @@ -43,6 +43,8 @@ pub struct Behaviour { static_relays: HashMap, + relays_available: bool, + waker: Option, } @@ -113,7 +115,12 @@ impl Config { #[derive(Debug)] #[non_exhaustive] pub enum Event { + /// The status of the local node has changed. StatusChanged { status: Status }, + /// No connected peer supports the HOP protocol. + NoRelaysAvailable, + /// At least one connected peer supports the HOP protocol. + RelaysAvailable, } impl Behaviour { @@ -329,18 +336,39 @@ impl Behaviour { } } - let picks: Vec<(PeerId, ConnectionId)> = static_candidates + let selected_candidates: Vec<(PeerId, ConnectionId)> = static_candidates .into_iter() .chain(candidates) .take(budget) .collect(); - for (peer_id, connection_id) in picks { + for (peer_id, connection_id) in selected_candidates { self.select_connection_for_reservation(peer_id, connection_id); } debug_assert!(self.covered_peers().len() <= max); } + + fn update_relay_availability(&mut self) { + let has_hop_peer = self + .connections + .values() + .any(|info| matches!(info.relay_status, RelayStatus::Supported { .. })); + + match (has_hop_peer, self.relays_available) { + (true, false) => { + self.relays_available = true; + self.events + .push_back(ToSwarm::GenerateEvent(Event::RelaysAvailable)); + } + (false, true) => { + self.relays_available = false; + self.events + .push_back(ToSwarm::GenerateEvent(Event::NoRelaysAvailable)); + } + _ => {} + } + } } impl NetworkBehaviour for Behaviour { @@ -429,6 +457,8 @@ impl NetworkBehaviour for Behaviour { let opts = DialOpts::peer_id(peer_id).addresses(vec![address]).build(); self.events.push_back(ToSwarm::Dial { opts }); } + + self.update_relay_availability(); } FromSwarm::AddressChange(AddressChange { peer_id, @@ -522,6 +552,7 @@ impl NetworkBehaviour for Behaviour { status: ReservationStatus::Idle, }; self.meet_reservation_target(); + self.update_relay_availability(); } } Out::Unsupported => { @@ -537,6 +568,7 @@ impl NetworkBehaviour for Behaviour { self.events.push_back(ToSwarm::RemoveListener { id }); self.meet_reservation_target(); } + self.update_relay_availability(); } Out::BlacklistExpired => { if matches!( diff --git a/protocols/relay/tests/autorelay.rs b/protocols/relay/tests/autorelay.rs index c11993512c8..e415c1acffc 100644 --- a/protocols/relay/tests/autorelay.rs +++ b/protocols/relay/tests/autorelay.rs @@ -824,6 +824,211 @@ where } } +#[tokio::test] +async fn autorelay_emits_no_relays_available_after_losing_last_relay() { + let _ = tracing_subscriber::fmt() + .with_env_filter(EnvFilter::from_default_env()) + .try_init(); + + let mut relay = build_relay(); + let relay_addr = Multiaddr::empty().with(Protocol::Memory(rand::random::())); + let relay_peer = *relay.local_peer_id(); + relay.listen_on(relay_addr.clone()).unwrap(); + relay.add_external_address(relay_addr.clone()); + tokio::spawn(async move { + relay.collect::>().await; + }); + + let mut client = build_client(autorelay::Config::default()); + client.dial(relay_addr).unwrap(); + + let conn_id = wait_until_some(&mut client, Duration::from_secs(15), { + let mut established: Option = None; + let mut reserved = false; + move |event| { + match event { + SwarmEvent::ConnectionEstablished { + peer_id, + connection_id, + endpoint, + .. + } if *peer_id == relay_peer && !endpoint.is_relayed() => { + established = Some(*connection_id); + } + SwarmEvent::Behaviour(ClientEvent::RelayClient( + relay::client::Event::ReservationReqAccepted { relay_peer_id, .. }, + )) if *relay_peer_id == relay_peer => { + reserved = true; + } + _ => {} + } + if reserved { established } else { None } + } + }) + .await; + + assert!(client.close_connection(conn_id)); + + wait_until(&mut client, Duration::from_secs(10), |event| { + matches!( + event, + SwarmEvent::Behaviour(ClientEvent::Autorelay(autorelay::Event::NoRelaysAvailable)) + ) + }) + .await; +} + +#[tokio::test] +async fn autorelay_emits_relay_available_after_recovery() { + let _ = tracing_subscriber::fmt() + .with_env_filter(EnvFilter::from_default_env()) + .try_init(); + + let mut relay = build_relay(); + let relay_addr = Multiaddr::empty().with(Protocol::Memory(rand::random::())); + let relay_peer = *relay.local_peer_id(); + relay.listen_on(relay_addr.clone()).unwrap(); + relay.add_external_address(relay_addr.clone()); + tokio::spawn(async move { + relay.collect::>().await; + }); + + let mut client = build_client(autorelay::Config::default()); + client.dial(relay_addr.clone()).unwrap(); + + let conn_id = wait_until_some(&mut client, Duration::from_secs(15), { + let mut established: Option = None; + let mut reserved = false; + move |event| { + match event { + SwarmEvent::ConnectionEstablished { + peer_id, + connection_id, + endpoint, + .. + } if *peer_id == relay_peer && !endpoint.is_relayed() => { + established = Some(*connection_id); + } + SwarmEvent::Behaviour(ClientEvent::RelayClient( + relay::client::Event::ReservationReqAccepted { relay_peer_id, .. }, + )) if *relay_peer_id == relay_peer => { + reserved = true; + } + _ => {} + } + if reserved { established } else { None } + } + }) + .await; + + assert!(client.close_connection(conn_id)); + + wait_until(&mut client, Duration::from_secs(10), |event| { + matches!( + event, + SwarmEvent::Behaviour(ClientEvent::Autorelay(autorelay::Event::NoRelaysAvailable)) + ) + }) + .await; + + client.dial(relay_addr).unwrap(); + + wait_until(&mut client, Duration::from_secs(15), |event| { + matches!( + event, + SwarmEvent::Behaviour(ClientEvent::Autorelay(autorelay::Event::RelaysAvailable)) + ) + }) + .await; +} + +#[tokio::test] +async fn autorelay_no_relays_available_is_edge_triggered() { + let _ = tracing_subscriber::fmt() + .with_env_filter(EnvFilter::from_default_env()) + .try_init(); + + let mut relay_a = build_relay(); + let relay_a_addr = Multiaddr::empty().with(Protocol::Memory(rand::random::())); + let relay_a_peer = *relay_a.local_peer_id(); + relay_a.listen_on(relay_a_addr.clone()).unwrap(); + relay_a.add_external_address(relay_a_addr.clone()); + tokio::spawn(async move { + relay_a.collect::>().await; + }); + + let mut relay_b = build_relay(); + let relay_b_addr = Multiaddr::empty().with(Protocol::Memory(rand::random::())); + let relay_b_peer = *relay_b.local_peer_id(); + relay_b.listen_on(relay_b_addr.clone()).unwrap(); + relay_b.add_external_address(relay_b_addr.clone()); + tokio::spawn(async move { + relay_b.collect::>().await; + }); + + let mut client = build_client(autorelay::Config::default()); + client.dial(relay_a_addr).unwrap(); + client.dial(relay_b_addr).unwrap(); + + let mut conns: HashMap = HashMap::new(); + let mut reserved: HashSet = HashSet::new(); + let sleep = tokio::time::sleep(Duration::from_secs(20)); + tokio::pin!(sleep); + loop { + tokio::select! { + _ = &mut sleep => panic!("did not get both reservations in time"), + ev = client.select_next_some() => match ev { + SwarmEvent::ConnectionEstablished { + peer_id, connection_id, endpoint, .. + } if !endpoint.is_relayed() + && (peer_id == relay_a_peer || peer_id == relay_b_peer) => + { + conns.insert(peer_id, connection_id); + } + SwarmEvent::Behaviour(ClientEvent::RelayClient( + relay::client::Event::ReservationReqAccepted { relay_peer_id, .. } + )) if relay_peer_id == relay_a_peer || relay_peer_id == relay_b_peer => { + reserved.insert(relay_peer_id); + } + _ => {} + } + } + if reserved.len() == 2 { + break; + } + } + + let conn_a = *conns.get(&relay_a_peer).unwrap(); + let conn_b = *conns.get(&relay_b_peer).unwrap(); + + assert!(client.close_connection(conn_a)); + assert!(client.close_connection(conn_b)); + + let mut starved_count = 0usize; + let sleep = tokio::time::sleep(Duration::from_secs(5)); + tokio::pin!(sleep); + loop { + tokio::select! { + _ = &mut sleep => break, + ev = client.select_next_some() => { + if matches!( + ev, + SwarmEvent::Behaviour(ClientEvent::Autorelay( + autorelay::Event::NoRelaysAvailable + )) + ) { + starved_count += 1; + } + } + } + } + + assert_eq!( + starved_count, 1, + "NoRelaysAvailable should fire exactly once across multiple meet_reservation_target invocations" + ); +} + async fn wait_for_reservation_from_either( client: &mut Swarm, peer_a: PeerId, From d2a271944a92545a7ec0aa132e099de4b269aa5f Mon Sep 17 00:00:00 2001 From: Darius Clark Date: Wed, 20 May 2026 12:50:21 -0400 Subject: [PATCH 24/51] chore: perform a check on empty external addr events --- protocols/relay/src/autorelay.rs | 33 +++++++++----------- protocols/relay/tests/autorelay.rs | 49 ++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 19 deletions(-) diff --git a/protocols/relay/src/autorelay.rs b/protocols/relay/src/autorelay.rs index 310a0a1eda2..1e237bbb33e 100644 --- a/protocols/relay/src/autorelay.rs +++ b/protocols/relay/src/autorelay.rs @@ -5,17 +5,12 @@ use libp2p_core::{ transport::{ListenerId, PortUse}, }; use libp2p_identity::PeerId; -use libp2p_swarm::{ - ExternalAddresses, ListenOpts, NewListenAddr, NotifyHandler, - derive_prelude::{ - AddressChange, ConnectionClosed, ConnectionDenied, ConnectionEstablished, ConnectionId, - DialFailure, ExpiredListenAddr, ExternalAddrConfirmed, FromSwarm, ListenerClosed, - ListenerError, Multiaddr, NetworkBehaviour, THandler, THandlerInEvent, THandlerOutEvent, - ToSwarm, - }, - dial_opts::DialOpts, - dummy, -}; +use libp2p_swarm::{ExternalAddresses, ListenOpts, NewListenAddr, NotifyHandler, derive_prelude::{ + AddressChange, ConnectionClosed, ConnectionDenied, ConnectionEstablished, ConnectionId, + DialFailure, ExpiredListenAddr, ExternalAddrConfirmed, FromSwarm, ListenerClosed, + ListenerError, Multiaddr, NetworkBehaviour, THandler, THandlerInEvent, THandlerOutEvent, + ToSwarm, +}, dial_opts::DialOpts, dummy, ExternalAddrExpired}; use std::task::Waker; use std::{ collections::{HashMap, HashSet, VecDeque}, @@ -137,7 +132,8 @@ impl Behaviour { return; } self.status = status; - self.events.push_back(ToSwarm::GenerateEvent(Event::StatusChanged { status })); + self.events + .push_back(ToSwarm::GenerateEvent(Event::StatusChanged { status })); if status == Status::Enable { self.meet_reservation_target(); } @@ -407,14 +403,13 @@ impl NetworkBehaviour for Behaviour { fn on_swarm_event(&mut self, event: FromSwarm) { self.external_addresses.on_swarm_event(&event); - if let FromSwarm::ExternalAddrConfirmed(ExternalAddrConfirmed { addr }) = &event - && !addr.is_relayed() - { - self.remove_all_reservations(); - return; - } - match event { + FromSwarm::ExternalAddrConfirmed(ExternalAddrConfirmed { addr }) if !addr.is_relayed() => { + self.remove_all_reservations(); + } + FromSwarm::ExternalAddrExpired(_) => { + self.meet_reservation_target(); + } FromSwarm::ConnectionEstablished(ConnectionEstablished { peer_id, endpoint, diff --git a/protocols/relay/tests/autorelay.rs b/protocols/relay/tests/autorelay.rs index e415c1acffc..78f8fd00be9 100644 --- a/protocols/relay/tests/autorelay.rs +++ b/protocols/relay/tests/autorelay.rs @@ -1029,6 +1029,55 @@ async fn autorelay_no_relays_available_is_edge_triggered() { ); } +#[tokio::test] +async fn autorelay_resumes_after_public_address_removed() { + let _ = tracing_subscriber::fmt() + .with_env_filter(EnvFilter::from_default_env()) + .try_init(); + + let mut relay = build_relay(); + let relay_addr = Multiaddr::empty().with(Protocol::Memory(rand::random::())); + let relay_peer = *relay.local_peer_id(); + relay.listen_on(relay_addr.clone()).unwrap(); + relay.add_external_address(relay_addr.clone()); + tokio::spawn(async move { + relay.collect::>().await; + }); + + let mut client = build_client(autorelay::Config::default()); + client.dial(relay_addr).unwrap(); + + wait_until(&mut client, Duration::from_secs(15), |event| { + matches!( + event, + SwarmEvent::Behaviour(ClientEvent::RelayClient( + relay::client::Event::ReservationReqAccepted { .. } + )) + ) + }) + .await; + + let public_addr = Multiaddr::empty().with(Protocol::Memory(rand::random::())); + client.add_external_address(public_addr.clone()); + + wait_until(&mut client, Duration::from_secs(10), |event| { + matches!(event, SwarmEvent::ExternalAddrExpired { .. }) + }) + .await; + + client.remove_external_address(&public_addr); + + wait_until(&mut client, Duration::from_secs(15), |event| { + matches!( + event, + SwarmEvent::Behaviour(ClientEvent::RelayClient( + relay::client::Event::ReservationReqAccepted { relay_peer_id, .. } + )) if *relay_peer_id == relay_peer + ) + }) + .await; +} + async fn wait_for_reservation_from_either( client: &mut Swarm, peer_a: PeerId, From 2ae86549617bed496036bf9a55c7039efc988788 Mon Sep 17 00:00:00 2001 From: Darius Clark Date: Thu, 21 May 2026 08:49:23 -0400 Subject: [PATCH 25/51] chore: allow overriding autorelay with status --- protocols/relay/src/autorelay.rs | 98 +++++++++++++++++++++--------- protocols/relay/tests/autorelay.rs | 12 ++-- 2 files changed, 76 insertions(+), 34 deletions(-) diff --git a/protocols/relay/src/autorelay.rs b/protocols/relay/src/autorelay.rs index 1e237bbb33e..74cbd9f1b35 100644 --- a/protocols/relay/src/autorelay.rs +++ b/protocols/relay/src/autorelay.rs @@ -7,10 +7,10 @@ use libp2p_core::{ use libp2p_identity::PeerId; use libp2p_swarm::{ExternalAddresses, ListenOpts, NewListenAddr, NotifyHandler, derive_prelude::{ AddressChange, ConnectionClosed, ConnectionDenied, ConnectionEstablished, ConnectionId, - DialFailure, ExpiredListenAddr, ExternalAddrConfirmed, FromSwarm, ListenerClosed, + DialFailure, ExpiredListenAddr, FromSwarm, ListenerClosed, ListenerError, Multiaddr, NetworkBehaviour, THandler, THandlerInEvent, THandlerOutEvent, ToSwarm, -}, dial_opts::DialOpts, dummy, ExternalAddrExpired}; +}, dial_opts::DialOpts, dummy}; use std::task::Waker; use std::{ collections::{HashMap, HashSet, VecDeque}, @@ -23,10 +23,11 @@ use crate::{autorelay::handler::Out, multiaddr_ext::MultiaddrExt}; mod handler; -#[derive(Default, Debug)] +#[derive(Debug)] pub struct Behaviour { config: Config, status: Status, + auto_status_change: bool, external_addresses: ExternalAddresses, events: VecDeque::ToSwarm, THandlerInEvent>>, @@ -43,6 +44,24 @@ pub struct Behaviour { waker: Option, } +impl Default for Behaviour { + fn default() -> Self { + Self { + config: Config::default(), + status: Status::Enable, + auto_status_change: true, + external_addresses: ExternalAddresses::default(), + events: VecDeque::new(), + connections: HashMap::new(), + reservations: HashMap::new(), + external_reservations: HashMap::new(), + static_relays: HashMap::new(), + relays_available: false, + waker: None, + } + } +} + #[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] pub enum Status { #[default] @@ -126,22 +145,55 @@ impl Behaviour { } } - /// Sets the status of the local node to enable or disable autorelay. - pub fn set_status(&mut self, status: Status) { - if self.status == status { - return; - } - self.status = status; - self.events - .push_back(ToSwarm::GenerateEvent(Event::StatusChanged { status })); - if status == Status::Enable { - self.meet_reservation_target(); + /// Sets the autorelay status. + pub fn set_status(&mut self, status: Option) { + match status { + Some(status) => { + self.auto_status_change = false; + if self.status != status { + self.status = status; + self.events + .push_back(ToSwarm::GenerateEvent(Event::StatusChanged { status })); + if status == Status::Enable { + self.meet_reservation_target(); + } + } + } + None => { + self.auto_status_change = true; + self.determine_status_from_external_addresses(); + } } + if let Some(waker) = self.waker.take() { waker.wake(); } } + fn determine_status_from_external_addresses(&mut self) { + let has_public_addr = self + .external_addresses + .iter() + .any(|addr| !addr.is_relayed()); + let new_status = if has_public_addr { + Status::Disable + } else { + Status::Enable + }; + if new_status == self.status { + return; + } + self.status = new_status; + self.events + .push_back(ToSwarm::GenerateEvent(Event::StatusChanged { + status: new_status, + })); + match new_status { + Status::Enable => self.meet_reservation_target(), + Status::Disable => self.remove_all_reservations(), + } + } + /// Register a peer as a static relay. /// /// This will dial and establish a connection to the peer if it doesn't already have a direct connection. @@ -295,14 +347,6 @@ impl Behaviour { return; } - if self - .external_addresses - .iter() - .any(|addr| !addr.is_relayed()) - { - return; - } - let max = self.config.max_reservations.get() as usize; let covered = self.covered_peers(); let budget = max.saturating_sub(covered.len()); @@ -401,15 +445,13 @@ impl NetworkBehaviour for Behaviour { } fn on_swarm_event(&mut self, event: FromSwarm) { - self.external_addresses.on_swarm_event(&event); + let change = self.external_addresses.on_swarm_event(&event); + + if self.auto_status_change && change { + self.determine_status_from_external_addresses(); + } match event { - FromSwarm::ExternalAddrConfirmed(ExternalAddrConfirmed { addr }) if !addr.is_relayed() => { - self.remove_all_reservations(); - } - FromSwarm::ExternalAddrExpired(_) => { - self.meet_reservation_target(); - } FromSwarm::ConnectionEstablished(ConnectionEstablished { peer_id, endpoint, diff --git a/protocols/relay/tests/autorelay.rs b/protocols/relay/tests/autorelay.rs index 78f8fd00be9..55933591502 100644 --- a/protocols/relay/tests/autorelay.rs +++ b/protocols/relay/tests/autorelay.rs @@ -443,7 +443,7 @@ async fn autorelay_disabled_does_not_reserve() { client .behaviour_mut() .autorelay - .set_status(autorelay::Status::Disable); + .set_status(Some(autorelay::Status::Disable)); client.dial(relay_addr).unwrap(); let observed = tokio::time::timeout( @@ -483,7 +483,7 @@ async fn autorelay_re_enable_triggers_reservation() { client .behaviour_mut() .autorelay - .set_status(autorelay::Status::Disable); + .set_status(Some(autorelay::Status::Disable)); client.dial(relay_addr).unwrap(); let sleep = tokio::time::sleep(Duration::from_secs(3)); @@ -507,7 +507,7 @@ async fn autorelay_re_enable_triggers_reservation() { client .behaviour_mut() .autorelay - .set_status(autorelay::Status::Enable); + .set_status(Some(autorelay::Status::Enable)); wait_until(&mut client, Duration::from_secs(10), |event| { matches!( @@ -550,7 +550,7 @@ async fn autorelay_disable_preserves_active_reservation() { client .behaviour_mut() .autorelay - .set_status(autorelay::Status::Disable); + .set_status(Some(autorelay::Status::Disable)); let sleep = tokio::time::sleep(Duration::from_secs(3)); tokio::pin!(sleep); @@ -598,7 +598,7 @@ async fn autorelay_prefers_static_relay() { client .behaviour_mut() .autorelay - .set_status(autorelay::Status::Disable); + .set_status(Some(autorelay::Status::Disable)); client.dial(opportunistic_addr).unwrap(); client @@ -619,7 +619,7 @@ async fn autorelay_prefers_static_relay() { client .behaviour_mut() .autorelay - .set_status(autorelay::Status::Enable); + .set_status(Some(autorelay::Status::Enable)); let accepted_peer = wait_until_some(&mut client, Duration::from_secs(15), |event| { if let SwarmEvent::Behaviour(ClientEvent::RelayClient( From 31be754831ed6efd6197bfabc1a9ceb824f018fb Mon Sep 17 00:00:00 2001 From: Darius Clark Date: Fri, 22 May 2026 20:21:46 -0500 Subject: [PATCH 26/51] chore: cleanup --- protocols/relay/src/autorelay.rs | 49 ++++++++++++++++---------------- 1 file changed, 25 insertions(+), 24 deletions(-) diff --git a/protocols/relay/src/autorelay.rs b/protocols/relay/src/autorelay.rs index 74cbd9f1b35..fd6e7578621 100644 --- a/protocols/relay/src/autorelay.rs +++ b/protocols/relay/src/autorelay.rs @@ -170,33 +170,10 @@ impl Behaviour { } } - fn determine_status_from_external_addresses(&mut self) { - let has_public_addr = self - .external_addresses - .iter() - .any(|addr| !addr.is_relayed()); - let new_status = if has_public_addr { - Status::Disable - } else { - Status::Enable - }; - if new_status == self.status { - return; - } - self.status = new_status; - self.events - .push_back(ToSwarm::GenerateEvent(Event::StatusChanged { - status: new_status, - })); - match new_status { - Status::Enable => self.meet_reservation_target(), - Status::Disable => self.remove_all_reservations(), - } - } - /// Register a peer as a static relay. /// /// This will dial and establish a connection to the peer if it doesn't already have a direct connection. + /// Note: Peers that are through a relay cannot be used as a static peer pub fn add_static_relay(&mut self, peer_id: PeerId, address: Multiaddr) { if address.is_relayed() { tracing::warn!(%peer_id, %address, "static relay address is relayed. ignoring."); @@ -225,6 +202,30 @@ impl Behaviour { self.static_relays.iter() } + + fn determine_status_from_external_addresses(&mut self) { + let has_public_addr = self + .external_addresses + .iter() + .any(|addr| !addr.is_relayed()); + + let new_status = match has_public_addr { + true => Status::Disable, + false => Status::Enable + }; + if new_status != self.status { + self.status = new_status; + self.events + .push_back(ToSwarm::GenerateEvent(Event::StatusChanged { + status: new_status, + })); + match new_status { + Status::Enable => self.meet_reservation_target(), + Status::Disable => self.remove_all_reservations(), + } + } + } + fn has_direct_connection(&self, peer_id: &PeerId) -> bool { self.connections .iter() From f38498253acd9cc6b3a7608b1ef2ef5a6273b2ef Mon Sep 17 00:00:00 2001 From: Darius Clark Date: Sat, 23 May 2026 14:49:01 -0500 Subject: [PATCH 27/51] chore: record previous successful relays --- protocols/relay/src/autorelay.rs | 59 +++++- protocols/relay/tests/autorelay.rs | 298 +++++++++++++++++++++++++++++ 2 files changed, 356 insertions(+), 1 deletion(-) diff --git a/protocols/relay/src/autorelay.rs b/protocols/relay/src/autorelay.rs index fd6e7578621..d323cea97ff 100644 --- a/protocols/relay/src/autorelay.rs +++ b/protocols/relay/src/autorelay.rs @@ -16,7 +16,7 @@ use std::{ collections::{HashMap, HashSet, VecDeque}, num::NonZeroU8, task::{Context, Poll}, - time::Duration, + time::{Duration, Instant}, }; use crate::{autorelay::handler::Out, multiaddr_ext::MultiaddrExt}; @@ -39,6 +39,8 @@ pub struct Behaviour { static_relays: HashMap, + previous_relays: VecDeque<(PeerId, Multiaddr, Instant)>, + relays_available: bool, waker: Option, @@ -56,6 +58,7 @@ impl Default for Behaviour { reservations: HashMap::new(), external_reservations: HashMap::new(), static_relays: HashMap::new(), + previous_relays: VecDeque::new(), relays_available: false, waker: None, } @@ -103,6 +106,7 @@ enum ReservationStatus { pub struct Config { max_reservations: NonZeroU8, failure_cooldown: Duration, + max_previous_relays: usize, } impl Default for Config { @@ -110,6 +114,7 @@ impl Default for Config { Self { max_reservations: NonZeroU8::new(2).unwrap(), failure_cooldown: Duration::from_secs(30), + max_previous_relays: 16, } } } @@ -124,6 +129,11 @@ impl Config { self.failure_cooldown = duration; self } + + pub fn set_max_previous_relays(mut self, max: usize) -> Self { + self.max_previous_relays = max; + self + } } #[derive(Debug)] @@ -202,6 +212,29 @@ impl Behaviour { self.static_relays.iter() } + pub fn previous_relays(&self) -> impl Iterator { + self.previous_relays + .iter() + .map(|(peer, addr, ts)| (peer, addr, ts)) + } + + fn record_previous_relay(&mut self, peer_id: PeerId, address: Multiaddr) { + let max = self.config.max_previous_relays; + if max == 0 { + return; + } + self.previous_relays + .retain(|(p, _, _)| *p != peer_id); + if self.previous_relays.len() >= max { + self.previous_relays.pop_front(); + } + self.previous_relays + .push_back((peer_id, address, Instant::now())); + } + + fn forget_previous_relay(&mut self, peer_id: &PeerId) { + self.previous_relays.retain(|(p, _, _)| p != peer_id); + } fn determine_status_from_external_addresses(&mut self) { let has_public_addr = self @@ -305,6 +338,7 @@ impl Behaviour { return; }; + let mut connection_address = None; if let Some(connection) = self.connections.get_mut(&(peer_id, connection_id)) && matches!( connection.relay_status, @@ -313,6 +347,7 @@ impl Behaviour { } ) { + connection_address = Some(connection.address.clone()); if failed { connection.relay_status = RelayStatus::Supported { status: ReservationStatus::Blacklisted, @@ -331,6 +366,10 @@ impl Behaviour { } } + if let Some(address) = connection_address { + self.record_previous_relay(peer_id, address); + } + self.meet_reservation_target(); } @@ -481,6 +520,15 @@ impl NetworkBehaviour for Behaviour { .remove(&(peer_id, connection_id)) .expect("valid connection"); + let had_reservation = matches!( + connection.relay_status, + RelayStatus::Supported { + status: ReservationStatus::Active { .. } + | ReservationStatus::Pending { .. } + | ReservationStatus::Blacklisted + } + ); + if let RelayStatus::Supported { status: ReservationStatus::Active { id } | ReservationStatus::Pending { id }, } = connection.relay_status @@ -489,6 +537,10 @@ impl NetworkBehaviour for Behaviour { self.meet_reservation_target(); } + if had_reservation { + self.record_previous_relay(peer_id, connection.address.clone()); + } + if let Some(address) = self.static_relays.get(&peer_id).cloned() && !self.has_direct_connection(&peer_id) { @@ -534,6 +586,7 @@ impl NetworkBehaviour for Behaviour { connection.relay_status = RelayStatus::Supported { status: ReservationStatus::Active { id: listener_id }, }; + self.forget_previous_relay(&peer_id); } return; } @@ -600,12 +653,16 @@ impl NetworkBehaviour for Behaviour { } => Some(id), _ => None, }; + let lost_address = drop_listener.map(|_| connection.address.clone()); connection.relay_status = RelayStatus::NotSupported; if let Some(id) = drop_listener { self.reservations.remove(&id); self.events.push_back(ToSwarm::RemoveListener { id }); self.meet_reservation_target(); } + if let Some(address) = lost_address { + self.record_previous_relay(peer_id, address); + } self.update_relay_availability(); } Out::BlacklistExpired => { diff --git a/protocols/relay/tests/autorelay.rs b/protocols/relay/tests/autorelay.rs index 55933591502..39ecc1b14a0 100644 --- a/protocols/relay/tests/autorelay.rs +++ b/protocols/relay/tests/autorelay.rs @@ -1078,6 +1078,304 @@ async fn autorelay_resumes_after_public_address_removed() { .await; } +#[tokio::test] +async fn autorelay_manual_enable_ignores_public_address() { + let _ = tracing_subscriber::fmt() + .with_env_filter(EnvFilter::from_default_env()) + .try_init(); + + let mut relay = build_relay(); + let relay_addr = Multiaddr::empty().with(Protocol::Memory(rand::random::())); + let relay_peer = *relay.local_peer_id(); + relay.listen_on(relay_addr.clone()).unwrap(); + relay.add_external_address(relay_addr.clone()); + tokio::spawn(async move { + relay.collect::>().await; + }); + + let mut client = build_client(autorelay::Config::default()); + client + .behaviour_mut() + .autorelay + .set_status(Some(autorelay::Status::Enable)); + client.dial(relay_addr).unwrap(); + + wait_until(&mut client, Duration::from_secs(15), |event| { + matches!( + event, + SwarmEvent::Behaviour(ClientEvent::RelayClient( + relay::client::Event::ReservationReqAccepted { relay_peer_id, .. } + )) if *relay_peer_id == relay_peer + ) + }) + .await; + + let public_addr = Multiaddr::empty().with(Protocol::Memory(rand::random::())); + client.add_external_address(public_addr); + + let sleep = tokio::time::sleep(Duration::from_secs(3)); + tokio::pin!(sleep); + loop { + tokio::select! { + _ = &mut sleep => break, + ev = client.select_next_some() => { + if let SwarmEvent::ListenerClosed { reason: Err(_), .. } = ev { + panic!("manual-Enable autorelay dropped reservation after public addr appeared"); + } + if let SwarmEvent::ExternalAddrExpired { address } = &ev + && address.iter().any(|p| p == Protocol::P2pCircuit) + { + panic!("manual-Enable autorelay expired the relayed external address"); + } + if let SwarmEvent::Behaviour(ClientEvent::Autorelay( + autorelay::Event::StatusChanged { status: autorelay::Status::Disable }, + )) = ev + { + panic!("manual-Enable autorelay flipped to Disable on public addr"); + } + } + } + } +} + +#[tokio::test] +async fn autorelay_records_previous_relay_after_reservation_loss() { + let _ = tracing_subscriber::fmt() + .with_env_filter(EnvFilter::from_default_env()) + .try_init(); + + let mut relay = build_relay(); + let relay_addr = Multiaddr::empty().with(Protocol::Memory(rand::random::())); + let relay_peer = *relay.local_peer_id(); + relay.listen_on(relay_addr.clone()).unwrap(); + relay.add_external_address(relay_addr.clone()); + tokio::spawn(async move { + relay.collect::>().await; + }); + + let mut client = build_client(autorelay::Config::default()); + client.dial(relay_addr).unwrap(); + + let conn_id = wait_until_some(&mut client, Duration::from_secs(15), { + let mut established: Option = None; + let mut reserved = false; + move |event| { + match event { + SwarmEvent::ConnectionEstablished { + peer_id, + connection_id, + endpoint, + .. + } if *peer_id == relay_peer && !endpoint.is_relayed() => { + established = Some(*connection_id); + } + SwarmEvent::Behaviour(ClientEvent::RelayClient( + relay::client::Event::ReservationReqAccepted { relay_peer_id, .. }, + )) if *relay_peer_id == relay_peer => { + reserved = true; + } + _ => {} + } + if reserved { established } else { None } + } + }) + .await; + + assert!(client.close_connection(conn_id)); + + wait_until(&mut client, Duration::from_secs(10), |event| { + matches!( + event, + SwarmEvent::Behaviour(ClientEvent::Autorelay(autorelay::Event::NoRelaysAvailable)) + ) + }) + .await; + + let previous: Vec = client + .behaviour() + .autorelay + .previous_relays() + .map(|(p, _, _)| *p) + .collect(); + assert!( + previous.contains(&relay_peer), + "expected {relay_peer} in previous_relays, got {previous:?}" + ); +} + +#[tokio::test] +async fn autorelay_forgets_previous_relay_on_reacquire() { + let _ = tracing_subscriber::fmt() + .with_env_filter(EnvFilter::from_default_env()) + .try_init(); + + let mut relay = build_relay(); + let relay_addr = Multiaddr::empty().with(Protocol::Memory(rand::random::())); + let relay_peer = *relay.local_peer_id(); + relay.listen_on(relay_addr.clone()).unwrap(); + relay.add_external_address(relay_addr.clone()); + tokio::spawn(async move { + relay.collect::>().await; + }); + + let mut client = build_client(autorelay::Config::default()); + client.dial(relay_addr.clone()).unwrap(); + + let conn_id = wait_until_some(&mut client, Duration::from_secs(15), { + let mut established: Option = None; + let mut reserved = false; + move |event| { + match event { + SwarmEvent::ConnectionEstablished { + peer_id, + connection_id, + endpoint, + .. + } if *peer_id == relay_peer && !endpoint.is_relayed() => { + established = Some(*connection_id); + } + SwarmEvent::Behaviour(ClientEvent::RelayClient( + relay::client::Event::ReservationReqAccepted { relay_peer_id, .. }, + )) if *relay_peer_id == relay_peer => { + reserved = true; + } + _ => {} + } + if reserved { established } else { None } + } + }) + .await; + + assert!(client.close_connection(conn_id)); + + wait_until(&mut client, Duration::from_secs(10), |event| { + matches!( + event, + SwarmEvent::Behaviour(ClientEvent::Autorelay(autorelay::Event::NoRelaysAvailable)) + ) + }) + .await; + + assert!( + client + .behaviour() + .autorelay + .previous_relays() + .any(|(p, _, _)| *p == relay_peer), + "expected {relay_peer} in previous_relays after loss" + ); + + client.dial(relay_addr).unwrap(); + + wait_until(&mut client, Duration::from_secs(15), |event| { + matches!( + event, + SwarmEvent::NewListenAddr { address, .. } if address.iter().any(|p| p == Protocol::P2pCircuit) + ) + }) + .await; + + let previous: Vec = client + .behaviour() + .autorelay + .previous_relays() + .map(|(p, _, _)| *p) + .collect(); + assert!( + !previous.contains(&relay_peer), + "expected {relay_peer} to be removed from previous_relays after re-acquire, got {previous:?}" + ); +} + +#[tokio::test] +async fn autorelay_previous_relays_is_bounded() { + let _ = tracing_subscriber::fmt() + .with_env_filter(EnvFilter::from_default_env()) + .try_init(); + + let mut peers_and_addrs: Vec<(PeerId, Multiaddr)> = Vec::with_capacity(3); + for _ in 0..3 { + let mut relay = build_relay(); + let addr = Multiaddr::empty().with(Protocol::Memory(rand::random::())); + let peer_id = *relay.local_peer_id(); + relay.listen_on(addr.clone()).unwrap(); + relay.add_external_address(addr.clone()); + peers_and_addrs.push((peer_id, addr.clone())); + tokio::spawn(async move { + relay.collect::>().await; + }); + } + + let mut client = build_client( + autorelay::Config::default() + .set_max_reservations(NonZeroU8::new(1).unwrap()) + .set_max_previous_relays(2), + ); + + for (idx, (peer, addr)) in peers_and_addrs.iter().enumerate() { + client.dial(addr.clone()).unwrap(); + + let conn_id = wait_until_some(&mut client, Duration::from_secs(15), { + let target_peer = *peer; + let mut established: Option = None; + let mut reserved = false; + move |event| { + match event { + SwarmEvent::ConnectionEstablished { + peer_id, + connection_id, + endpoint, + .. + } if *peer_id == target_peer && !endpoint.is_relayed() => { + established = Some(*connection_id); + } + SwarmEvent::Behaviour(ClientEvent::RelayClient( + relay::client::Event::ReservationReqAccepted { relay_peer_id, .. }, + )) if *relay_peer_id == target_peer => { + reserved = true; + } + _ => {} + } + if reserved { established } else { None } + } + }) + .await; + + assert!(client.close_connection(conn_id)); + + wait_until(&mut client, Duration::from_secs(10), |event| { + matches!( + event, + SwarmEvent::Behaviour(ClientEvent::Autorelay( + autorelay::Event::NoRelaysAvailable + )) + ) + }) + .await; + + let _ = idx; + } + + let previous: Vec = client + .behaviour() + .autorelay + .previous_relays() + .map(|(p, _, _)| *p) + .collect(); + + assert_eq!( + previous.len(), + 2, + "expected previous_relays to be bounded to 2, got {previous:?}" + ); + assert!( + !previous.contains(&peers_and_addrs[0].0), + "oldest relay should have been evicted: {previous:?}" + ); + assert!(previous.contains(&peers_and_addrs[1].0)); + assert!(previous.contains(&peers_and_addrs[2].0)); +} + async fn wait_for_reservation_from_either( client: &mut Swarm, peer_a: PeerId, From 252b2f5cfd3adc6ba315d77b2841a1a301d949e3 Mon Sep 17 00:00:00 2001 From: Darius Clark Date: Sun, 24 May 2026 06:33:44 -0500 Subject: [PATCH 28/51] chore: add backoff for static relay --- protocols/relay/src/autorelay.rs | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/protocols/relay/src/autorelay.rs b/protocols/relay/src/autorelay.rs index d323cea97ff..eb13f7f8ffe 100644 --- a/protocols/relay/src/autorelay.rs +++ b/protocols/relay/src/autorelay.rs @@ -39,6 +39,8 @@ pub struct Behaviour { static_relays: HashMap, + static_dial_cooldowns: HashMap, + previous_relays: VecDeque<(PeerId, Multiaddr, Instant)>, relays_available: bool, @@ -58,6 +60,7 @@ impl Default for Behaviour { reservations: HashMap::new(), external_reservations: HashMap::new(), static_relays: HashMap::new(), + static_dial_cooldowns: HashMap::new(), previous_relays: VecDeque::new(), relays_available: false, waker: None, @@ -191,7 +194,7 @@ impl Behaviour { } self.static_relays.insert(peer_id, address.clone()); - if !self.has_direct_connection(&peer_id) { + if !self.has_direct_connection(&peer_id) && !self.static_dial_in_cooldown(&peer_id) { let opts = DialOpts::peer_id(peer_id).addresses(vec![address]).build(); self.events.push_back(ToSwarm::Dial { opts }); } @@ -205,6 +208,7 @@ impl Behaviour { /// Remove peer as a static relay. /// This will not close any connections or terminate any existing reservation with the relay pub fn remove_static_relay(&mut self, peer_id: &PeerId) -> bool { + self.static_dial_cooldowns.remove(peer_id); self.static_relays.remove(peer_id).is_some() } @@ -218,6 +222,12 @@ impl Behaviour { .map(|(peer, addr, ts)| (peer, addr, ts)) } + fn static_dial_in_cooldown(&self, peer_id: &PeerId) -> bool { + self.static_dial_cooldowns + .get(peer_id) + .is_some_and(|deadline| *deadline > Instant::now()) + } + fn record_previous_relay(&mut self, peer_id: PeerId, address: Multiaddr) { let max = self.config.max_previous_relays; if max == 0 { @@ -509,6 +519,10 @@ impl NetworkBehaviour for Behaviour { self.connections .insert((peer_id, connection_id), connection); + + if self.static_relays.contains_key(&peer_id) { + self.static_dial_cooldowns.remove(&peer_id); + } } FromSwarm::ConnectionClosed(ConnectionClosed { peer_id, @@ -543,6 +557,7 @@ impl NetworkBehaviour for Behaviour { if let Some(address) = self.static_relays.get(&peer_id).cloned() && !self.has_direct_connection(&peer_id) + && !self.static_dial_in_cooldown(&peer_id) { let opts = DialOpts::peer_id(peer_id).addresses(vec![address]).build(); self.events.push_back(ToSwarm::Dial { opts }); @@ -615,6 +630,8 @@ impl NetworkBehaviour for Behaviour { .. }) if self.static_relays.contains_key(&peer_id) => { tracing::warn!(%peer_id, %error, "dial to static relay failed"); + self.static_dial_cooldowns + .insert(peer_id, Instant::now() + self.config.failure_cooldown); } _ => {} } From e436da5d648b0a9cdc6d5b429a812fa1e3adf83f Mon Sep 17 00:00:00 2001 From: Darius Clark Date: Sun, 24 May 2026 18:33:18 -0500 Subject: [PATCH 29/51] chore: add backoff on failure --- protocols/relay/src/autorelay.rs | 60 ++++++++++++++++++++++++-------- 1 file changed, 45 insertions(+), 15 deletions(-) diff --git a/protocols/relay/src/autorelay.rs b/protocols/relay/src/autorelay.rs index eb13f7f8ffe..108d26ee04e 100644 --- a/protocols/relay/src/autorelay.rs +++ b/protocols/relay/src/autorelay.rs @@ -41,6 +41,8 @@ pub struct Behaviour { static_dial_cooldowns: HashMap, + failure_counts: HashMap, + previous_relays: VecDeque<(PeerId, Multiaddr, Instant)>, relays_available: bool, @@ -61,6 +63,7 @@ impl Default for Behaviour { external_reservations: HashMap::new(), static_relays: HashMap::new(), static_dial_cooldowns: HashMap::new(), + failure_counts: HashMap::new(), previous_relays: VecDeque::new(), relays_available: false, waker: None, @@ -109,6 +112,7 @@ enum ReservationStatus { pub struct Config { max_reservations: NonZeroU8, failure_cooldown: Duration, + failure_cooldown_max: Duration, max_previous_relays: usize, } @@ -117,6 +121,7 @@ impl Default for Config { Self { max_reservations: NonZeroU8::new(2).unwrap(), failure_cooldown: Duration::from_secs(30), + failure_cooldown_max: Duration::from_secs(10 * 60), max_previous_relays: 16, } } @@ -133,6 +138,11 @@ impl Config { self } + pub fn set_failure_cooldown_max(mut self, duration: Duration) -> Self { + self.failure_cooldown_max = duration; + self + } + pub fn set_max_previous_relays(mut self, max: usize) -> Self { self.max_previous_relays = max; self @@ -246,6 +256,22 @@ impl Behaviour { self.previous_relays.retain(|(p, _, _)| p != peer_id); } + fn record_failure(&mut self, peer_id: PeerId) -> Duration { + let attempts = self.failure_counts.entry(peer_id).or_insert(0); + *attempts = attempts.saturating_add(1); + let exponent = attempts.saturating_sub(1).min(20); + let scale = 1u64.checked_shl(exponent).unwrap_or(u64::MAX); + let base = self.config.failure_cooldown; + let scaled = base + .checked_mul(scale.min(u32::MAX as u64) as u32) + .unwrap_or(self.config.failure_cooldown_max); + scaled.min(self.config.failure_cooldown_max) + } + + fn clear_failure(&mut self, peer_id: &PeerId) { + self.failure_counts.remove(peer_id); + } + fn determine_status_from_external_addresses(&mut self) { let has_public_addr = self .external_addresses @@ -349,6 +375,8 @@ impl Behaviour { }; let mut connection_address = None; + let mut blacklist_duration = failed.then(|| self.record_failure(peer_id)); + if let Some(connection) = self.connections.get_mut(&(peer_id, connection_id)) && matches!( connection.relay_status, @@ -358,21 +386,22 @@ impl Behaviour { ) { connection_address = Some(connection.address.clone()); - if failed { - connection.relay_status = RelayStatus::Supported { - status: ReservationStatus::Blacklisted, - }; - self.events.push_back(ToSwarm::NotifyHandler { - peer_id, - handler: NotifyHandler::One(connection_id), - event: Either::Left(handler::In::Blacklist { - duration: self.config.failure_cooldown, - }), - }); - } else { - connection.relay_status = RelayStatus::Supported { - status: ReservationStatus::Idle, - }; + match blacklist_duration { + Some(duration) => { + connection.relay_status = RelayStatus::Supported { + status: ReservationStatus::Blacklisted, + }; + self.events.push_back(ToSwarm::NotifyHandler { + peer_id, + handler: NotifyHandler::One(connection_id), + event: Either::Left(handler::In::Blacklist { duration }), + }); + }, + None => { + connection.relay_status = RelayStatus::Supported { + status: ReservationStatus::Idle, + }; + }, } } @@ -602,6 +631,7 @@ impl NetworkBehaviour for Behaviour { status: ReservationStatus::Active { id: listener_id }, }; self.forget_previous_relay(&peer_id); + self.clear_failure(&peer_id); } return; } From 05f55d9e1b421d3e75dbc885ef994c0b78ba52f3 Mon Sep 17 00:00:00 2001 From: Darius Clark Date: Sun, 24 May 2026 18:35:12 -0500 Subject: [PATCH 30/51] chore: update test --- protocols/relay/src/autorelay.rs | 4 +- protocols/relay/tests/autorelay.rs | 73 ++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+), 2 deletions(-) diff --git a/protocols/relay/src/autorelay.rs b/protocols/relay/src/autorelay.rs index 108d26ee04e..e9949ba96ed 100644 --- a/protocols/relay/src/autorelay.rs +++ b/protocols/relay/src/autorelay.rs @@ -375,8 +375,8 @@ impl Behaviour { }; let mut connection_address = None; - let mut blacklist_duration = failed.then(|| self.record_failure(peer_id)); - + let blacklist_duration = failed.then(|| self.record_failure(peer_id)); + if let Some(connection) = self.connections.get_mut(&(peer_id, connection_id)) && matches!( connection.relay_status, diff --git a/protocols/relay/tests/autorelay.rs b/protocols/relay/tests/autorelay.rs index 39ecc1b14a0..5ce6601b604 100644 --- a/protocols/relay/tests/autorelay.rs +++ b/protocols/relay/tests/autorelay.rs @@ -1376,6 +1376,79 @@ async fn autorelay_previous_relays_is_bounded() { assert!(previous.contains(&peers_and_addrs[2].0)); } +#[tokio::test] +async fn autorelay_static_relay_dial_cooldown_after_failure() { + let _ = tracing_subscriber::fmt() + .with_env_filter(EnvFilter::from_default_env()) + .try_init(); + + let cooldown = Duration::from_secs(2); + let mut client = + build_client(autorelay::Config::default().set_failure_cooldown(cooldown)); + + let unreachable_peer = PeerId::random(); + let unreachable_addr = Multiaddr::empty().with(Protocol::Memory(rand::random::())); + + client + .behaviour_mut() + .autorelay + .add_static_relay(unreachable_peer, unreachable_addr.clone()); + + wait_until(&mut client, Duration::from_secs(5), |event| { + matches!( + event, + SwarmEvent::OutgoingConnectionError { peer_id: Some(p), .. } if *p == unreachable_peer + ) + }) + .await; + + let first_failure_at = std::time::Instant::now(); + + client + .behaviour_mut() + .autorelay + .add_static_relay(unreachable_peer, unreachable_addr.clone()); + + let mut redialed = false; + let watch = tokio::time::sleep(cooldown / 2); + tokio::pin!(watch); + loop { + tokio::select! { + _ = &mut watch => break, + ev = client.select_next_some() => { + if matches!( + ev, + SwarmEvent::OutgoingConnectionError { peer_id: Some(p), .. } if p == unreachable_peer + ) { + redialed = true; + break; + } + } + } + } + assert!(!redialed, "autorelay redialed within cooldown"); + + let remaining = cooldown + .checked_sub(first_failure_at.elapsed()) + .unwrap_or_default(); + if !remaining.is_zero() { + tokio::time::sleep(remaining + Duration::from_millis(200)).await; + } + + client + .behaviour_mut() + .autorelay + .add_static_relay(unreachable_peer, unreachable_addr); + + wait_until(&mut client, Duration::from_secs(5), |event| { + matches!( + event, + SwarmEvent::OutgoingConnectionError { peer_id: Some(p), .. } if *p == unreachable_peer + ) + }) + .await; +} + async fn wait_for_reservation_from_either( client: &mut Swarm, peer_a: PeerId, From e3aa8a51ae6faa4aca901a8b7d5c7e59f8084b39 Mon Sep 17 00:00:00 2001 From: Darius Clark Date: Sun, 24 May 2026 20:18:22 -0500 Subject: [PATCH 31/51] chore: cleanup --- protocols/relay/src/autorelay.rs | 68 ++++++++++++++++++-------------- 1 file changed, 38 insertions(+), 30 deletions(-) diff --git a/protocols/relay/src/autorelay.rs b/protocols/relay/src/autorelay.rs index e9949ba96ed..e753bcc1983 100644 --- a/protocols/relay/src/autorelay.rs +++ b/protocols/relay/src/autorelay.rs @@ -374,41 +374,49 @@ impl Behaviour { return; }; - let mut connection_address = None; + let Some(address) = self + .connections + .get(&(peer_id, connection_id)) + .filter(|info| { + matches!( + info.relay_status, + RelayStatus::Supported { + status: ReservationStatus::Active { .. } + | ReservationStatus::Pending { .. } + } + ) + }) + .map(|info| info.address.clone()) + else { + self.meet_reservation_target(); + return; + }; + let blacklist_duration = failed.then(|| self.record_failure(peer_id)); - if let Some(connection) = self.connections.get_mut(&(peer_id, connection_id)) - && matches!( - connection.relay_status, - RelayStatus::Supported { - status: ReservationStatus::Active { .. } | ReservationStatus::Pending { .. } - } - ) - { - connection_address = Some(connection.address.clone()); - match blacklist_duration { - Some(duration) => { - connection.relay_status = RelayStatus::Supported { - status: ReservationStatus::Blacklisted, - }; - self.events.push_back(ToSwarm::NotifyHandler { - peer_id, - handler: NotifyHandler::One(connection_id), - event: Either::Left(handler::In::Blacklist { duration }), - }); - }, - None => { - connection.relay_status = RelayStatus::Supported { - status: ReservationStatus::Idle, - }; - }, + let connection = self + .connections + .get_mut(&(peer_id, connection_id)) + .expect("connection is tracked"); + match blacklist_duration { + Some(duration) => { + connection.relay_status = RelayStatus::Supported { + status: ReservationStatus::Blacklisted, + }; + self.events.push_back(ToSwarm::NotifyHandler { + peer_id, + handler: NotifyHandler::One(connection_id), + event: Either::Left(handler::In::Blacklist { duration }), + }); + } + None => { + connection.relay_status = RelayStatus::Supported { + status: ReservationStatus::Idle, + }; } } - if let Some(address) = connection_address { - self.record_previous_relay(peer_id, address); - } - + self.record_previous_relay(peer_id, address); self.meet_reservation_target(); } From 4a22b25bf9f775d517e4f9461fe982c27dba87f3 Mon Sep 17 00:00:00 2001 From: Darius Clark Date: Tue, 26 May 2026 05:48:17 -0500 Subject: [PATCH 32/51] chore: prioritize static relays --- protocols/relay/src/autorelay.rs | 81 +++++++++++++++++++++++++++----- 1 file changed, 70 insertions(+), 11 deletions(-) diff --git a/protocols/relay/src/autorelay.rs b/protocols/relay/src/autorelay.rs index e753bcc1983..499cedbd8f3 100644 --- a/protocols/relay/src/autorelay.rs +++ b/protocols/relay/src/autorelay.rs @@ -5,12 +5,16 @@ use libp2p_core::{ transport::{ListenerId, PortUse}, }; use libp2p_identity::PeerId; -use libp2p_swarm::{ExternalAddresses, ListenOpts, NewListenAddr, NotifyHandler, derive_prelude::{ - AddressChange, ConnectionClosed, ConnectionDenied, ConnectionEstablished, ConnectionId, - DialFailure, ExpiredListenAddr, FromSwarm, ListenerClosed, - ListenerError, Multiaddr, NetworkBehaviour, THandler, THandlerInEvent, THandlerOutEvent, - ToSwarm, -}, dial_opts::DialOpts, dummy}; +use libp2p_swarm::{ + ExternalAddresses, ListenOpts, NewListenAddr, NotifyHandler, + derive_prelude::{ + AddressChange, ConnectionClosed, ConnectionDenied, ConnectionEstablished, ConnectionId, + DialFailure, ExpiredListenAddr, FromSwarm, ListenerClosed, ListenerError, Multiaddr, + NetworkBehaviour, THandler, THandlerInEvent, THandlerOutEvent, ToSwarm, + }, + dial_opts::DialOpts, + dummy, +}; use std::task::Waker; use std::{ collections::{HashMap, HashSet, VecDeque}, @@ -114,6 +118,7 @@ pub struct Config { failure_cooldown: Duration, failure_cooldown_max: Duration, max_previous_relays: usize, + static_relays: HashMap, } impl Default for Config { @@ -123,6 +128,7 @@ impl Default for Config { failure_cooldown: Duration::from_secs(30), failure_cooldown_max: Duration::from_secs(10 * 60), max_previous_relays: 16, + static_relays: HashMap::new(), } } } @@ -147,6 +153,11 @@ impl Config { self.max_previous_relays = max; self } + + pub fn add_static_relay(mut self, peer_id: PeerId, address: Multiaddr) -> Self { + self.static_relays.insert(peer_id, address); + self + } } #[derive(Debug)] @@ -161,11 +172,16 @@ pub enum Event { } impl Behaviour { - pub fn new_with_config(config: Config) -> Self { - Self { + pub fn new_with_config(mut config: Config) -> Self { + let initial_static_relays = std::mem::take(&mut config.static_relays); + let mut behaviour = Self { config, ..Default::default() + }; + for (peer_id, address) in initial_static_relays { + behaviour.add_static_relay(peer_id, address); } + behaviour } /// Sets the autorelay status. @@ -204,9 +220,17 @@ impl Behaviour { } self.static_relays.insert(peer_id, address.clone()); + if self.is_peer_idle(&peer_id) { + self.evict_for_static_peer(peer_id); + } + if !self.has_direct_connection(&peer_id) && !self.static_dial_in_cooldown(&peer_id) { let opts = DialOpts::peer_id(peer_id).addresses(vec![address]).build(); self.events.push_back(ToSwarm::Dial { opts }); + if let Some(waker) = self.waker.take() { + waker.wake(); + } + return; } self.meet_reservation_target(); @@ -243,8 +267,7 @@ impl Behaviour { if max == 0 { return; } - self.previous_relays - .retain(|(p, _, _)| *p != peer_id); + self.previous_relays.retain(|(p, _, _)| *p != peer_id); if self.previous_relays.len() >= max { self.previous_relays.pop_front(); } @@ -280,7 +303,7 @@ impl Behaviour { let new_status = match has_public_addr { true => Status::Disable, - false => Status::Enable + false => Status::Enable, }; if new_status != self.status { self.status = new_status; @@ -295,12 +318,45 @@ impl Behaviour { } } + fn is_peer_idle(&self, peer_id: &PeerId) -> bool { + self.connections + .iter() + .filter(|((pid, _), _)| pid == peer_id) + .any(|((_, _), info)| { + info.relay_status + == RelayStatus::Supported { + status: ReservationStatus::Idle, + } + }) + } + fn has_direct_connection(&self, peer_id: &PeerId) -> bool { self.connections .iter() .any(|((pid, _), info)| pid == peer_id && !info.address.is_relayed()) } + fn evict_for_static_peer(&mut self, new_static: PeerId) { + let covered = self.covered_peers(); + if covered.contains(&new_static) { + return; + } + let max = self.config.max_reservations.get() as usize; + if covered.len() < max { + return; + } + + if let Some((listener_id, ..)) = self + .reservations + .iter() + .find(|(_, (peer_id, _))| !self.static_relays.contains_key(peer_id)) + .map(|(listener_id, (peer_id, connection_id))| (*listener_id, *peer_id, *connection_id)) + { + self.events + .push_back(ToSwarm::RemoveListener { id: listener_id }); + } + } + fn select_connection_for_reservation(&mut self, peer_id: PeerId, connection_id: ConnectionId) { let info = self .connections @@ -697,6 +753,9 @@ impl NetworkBehaviour for Behaviour { connection.relay_status = RelayStatus::Supported { status: ReservationStatus::Idle, }; + if self.static_relays.contains_key(&peer_id) { + self.evict_for_static_peer(peer_id); + } self.meet_reservation_target(); self.update_relay_availability(); } From efa27683d7ae066046665a077c8f45395aa1b4bc Mon Sep 17 00:00:00 2001 From: Darius Clark Date: Tue, 26 May 2026 07:26:22 -0500 Subject: [PATCH 33/51] chore: cleanup test --- protocols/relay/tests/autorelay.rs | 910 +++++++---------------------- 1 file changed, 206 insertions(+), 704 deletions(-) diff --git a/protocols/relay/tests/autorelay.rs b/protocols/relay/tests/autorelay.rs index 5ce6601b604..77becfe7bce 100644 --- a/protocols/relay/tests/autorelay.rs +++ b/protocols/relay/tests/autorelay.rs @@ -1,5 +1,6 @@ use std::{ collections::{HashMap, HashSet}, + future::Future, num::NonZeroU8, time::Duration, }; @@ -22,84 +23,20 @@ use libp2p_relay::{self as relay, autorelay}; use libp2p_swarm::{Config, ConnectionId, NetworkBehaviour, Swarm, SwarmEvent}; use tracing_subscriber::EnvFilter; -#[tokio::test] -async fn autorelay_reserves_when_peer_supports_hop() { - let _ = tracing_subscriber::fmt() - .with_env_filter(EnvFilter::from_default_env()) - .try_init(); - - let mut relay = build_relay(); - let relay_addr = Multiaddr::empty().with(Protocol::Memory(rand::random::())); - let relay_peer_id = *relay.local_peer_id(); - - relay.listen_on(relay_addr.clone()).unwrap(); - relay.add_external_address(relay_addr.clone()); - tokio::spawn(async move { - relay.collect::>().await; - }); - - let mut client = build_client(autorelay::Config::default()); - let client_peer_id = *client.local_peer_id(); - client.dial(relay_addr.clone()).unwrap(); - - let expected_relayed = relay_addr - .with(Protocol::P2p(relay_peer_id)) - .with(Protocol::P2pCircuit) - .with(Protocol::P2p(client_peer_id)); - - wait_until(&mut client, Duration::from_secs(20), { - let mut new_listen_addr = false; - let mut reservation_accepted = false; - move |event| { - match event { - SwarmEvent::NewListenAddr { address, .. } if address == &expected_relayed => { - new_listen_addr = true; - } - SwarmEvent::Behaviour(ClientEvent::RelayClient( - relay::client::Event::ReservationReqAccepted { .. }, - )) => { - reservation_accepted = true; - } - _ => {} - } - new_listen_addr && reservation_accepted - } - }) - .await; -} - #[tokio::test] async fn autorelay_respects_max_reservations() { - let _ = tracing_subscriber::fmt() - .with_env_filter(EnvFilter::from_default_env()) - .try_init(); + init_tracing(); - let mut relay_a = build_relay(); - let relay_a_addr = Multiaddr::empty().with(Protocol::Memory(rand::random::())); - let relay_a_peer_id = *relay_a.local_peer_id(); - relay_a.listen_on(relay_a_addr.clone()).unwrap(); - relay_a.add_external_address(relay_a_addr.clone()); - tokio::spawn(async move { - relay_a.collect::>().await; - }); - - let mut relay_b = build_relay(); - let relay_b_addr = Multiaddr::empty().with(Protocol::Memory(rand::random::())); - let relay_b_peer_id = *relay_b.local_peer_id(); - relay_b.listen_on(relay_b_addr.clone()).unwrap(); - relay_b.add_external_address(relay_b_addr.clone()); - tokio::spawn(async move { - relay_b.collect::>().await; - }); + let (relay_a_peer_id, relay_a_addr) = spawn_relay(); + let (relay_b_peer_id, relay_b_addr) = spawn_relay(); let mut client = build_client(autorelay::Config::default().set_max_reservations(NonZeroU8::new(1).unwrap())); - client.dial(relay_a_addr.clone()).unwrap(); - client.dial(relay_b_addr.clone()).unwrap(); + client.dial(relay_a_addr).unwrap(); + client.dial(relay_b_addr).unwrap(); let mut accepted = 0usize; - let timeout = tokio::time::sleep(Duration::from_secs(20)); - tokio::pin!(timeout); + let mut timeout = futures_timer::Delay::new(Duration::from_secs(20)); loop { tokio::select! { _ = &mut timeout => break, @@ -113,7 +50,7 @@ async fn autorelay_respects_max_reservations() { if accepted > 1 { panic!("autorelay opened more reservations than max_reservations=1"); } - tokio::time::sleep(Duration::from_secs(2)).await; + futures_timer::Delay::new(Duration::from_secs(2)).await; break; } } @@ -126,89 +63,11 @@ async fn autorelay_respects_max_reservations() { ); } -#[tokio::test] -async fn autorelay_refills_after_connection_drop() { - let _ = tracing_subscriber::fmt() - .with_env_filter(EnvFilter::from_default_env()) - .try_init(); - - let mut relay_a = build_relay(); - let relay_a_addr = Multiaddr::empty().with(Protocol::Memory(rand::random::())); - let relay_a_peer_id = *relay_a.local_peer_id(); - relay_a.listen_on(relay_a_addr.clone()).unwrap(); - relay_a.add_external_address(relay_a_addr.clone()); - tokio::spawn(async move { - relay_a.collect::>().await; - }); - - let mut relay_b = build_relay(); - let relay_b_addr = Multiaddr::empty().with(Protocol::Memory(rand::random::())); - let relay_b_peer_id = *relay_b.local_peer_id(); - relay_b.listen_on(relay_b_addr.clone()).unwrap(); - relay_b.add_external_address(relay_b_addr.clone()); - tokio::spawn(async move { - relay_b.collect::>().await; - }); - - let mut client = - build_client(autorelay::Config::default().set_max_reservations(NonZeroU8::new(1).unwrap())); - client.dial(relay_a_addr.clone()).unwrap(); - client.dial(relay_b_addr.clone()).unwrap(); - - let (first_relay, first_conn) = wait_for_reservation_from_either( - &mut client, - relay_a_peer_id, - relay_b_peer_id, - Duration::from_secs(20), - ) - .await; - - assert!( - client.close_connection(first_conn), - "first reservation connection should exist" - ); - - let other_relay = if first_relay == relay_a_peer_id { - relay_b_peer_id - } else { - relay_a_peer_id - }; - - wait_until(&mut client, Duration::from_secs(20), { - let mut got = false; - move |event| { - if let SwarmEvent::Behaviour(ClientEvent::RelayClient( - relay::client::Event::ReservationReqAccepted { relay_peer_id, .. }, - )) = event - && *relay_peer_id == other_relay - { - got = true; - } - got - } - }) - .await; -} - #[tokio::test] async fn autorelay_with_two_reservations_among_five_relays() { - let _ = tracing_subscriber::fmt() - .with_env_filter(EnvFilter::from_default_env()) - .try_init(); - - let mut relay_addrs: Vec<(PeerId, Multiaddr)> = Vec::with_capacity(5); - for _ in 0..5 { - let mut relay = build_relay(); - let addr = Multiaddr::empty().with(Protocol::Memory(rand::random::())); - let peer_id = *relay.local_peer_id(); - relay.listen_on(addr.clone()).unwrap(); - relay.add_external_address(addr.clone()); - relay_addrs.push((peer_id, addr.clone())); - tokio::spawn(async move { - relay.collect::>().await; - }); - } + init_tracing(); + let relay_addrs: Vec<(PeerId, Multiaddr)> = (0..5).map(|_| spawn_relay()).collect(); let relay_peers: HashSet = relay_addrs.iter().map(|(p, _)| *p).collect(); let mut client = @@ -220,8 +79,7 @@ async fn autorelay_with_two_reservations_among_five_relays() { let mut direct_conns: HashMap = HashMap::new(); let mut reservations: HashSet = HashSet::new(); - let sleep = tokio::time::sleep(Duration::from_secs(30)); - tokio::pin!(sleep); + let mut sleep = futures_timer::Delay::new(Duration::from_secs(30)); loop { tokio::select! { _ = &mut sleep => panic!( @@ -266,8 +124,8 @@ async fn autorelay_with_two_reservations_among_five_relays() { "should close the relay connection holding a reservation" ); - let sleep = tokio::time::sleep(Duration::from_secs(30)); - tokio::pin!(sleep); + let mut sleep = futures_timer::Delay::new(Duration::from_secs(30)); + loop { tokio::select! { _ = &mut sleep => panic!("timeout waiting for replacement reservation"), @@ -291,34 +149,19 @@ async fn autorelay_with_two_reservations_among_five_relays() { #[tokio::test] async fn autorelay_drops_reservations_when_public_address_appears() { - let _ = tracing_subscriber::fmt() - .with_env_filter(EnvFilter::from_default_env()) - .try_init(); + init_tracing(); - let mut relay_a = build_relay(); - let relay_a_addr = Multiaddr::empty().with(Protocol::Memory(rand::random::())); - relay_a.listen_on(relay_a_addr.clone()).unwrap(); - relay_a.add_external_address(relay_a_addr.clone()); - tokio::spawn(async move { - relay_a.collect::>().await; - }); - - let mut relay_b = build_relay(); - let relay_b_addr = Multiaddr::empty().with(Protocol::Memory(rand::random::())); - relay_b.listen_on(relay_b_addr.clone()).unwrap(); - relay_b.add_external_address(relay_b_addr.clone()); - tokio::spawn(async move { - relay_b.collect::>().await; - }); + let (_, relay_a_addr) = spawn_relay(); + let (_, relay_b_addr) = spawn_relay(); let mut client = build_client(autorelay::Config::default().set_max_reservations(NonZeroU8::new(2).unwrap())); - client.dial(relay_a_addr.clone()).unwrap(); - client.dial(relay_b_addr.clone()).unwrap(); + client.dial(relay_a_addr).unwrap(); + client.dial(relay_b_addr).unwrap(); let mut confirmed: HashSet = HashSet::new(); - let sleep = tokio::time::sleep(Duration::from_secs(30)); - tokio::pin!(sleep); + let mut sleep = futures_timer::Delay::new(Duration::from_secs(30)); + loop { tokio::select! { _ = &mut sleep => panic!( @@ -342,8 +185,8 @@ async fn autorelay_drops_reservations_when_public_address_appears() { client.add_external_address(public_addr); let mut expired: HashSet = HashSet::new(); - let sleep = tokio::time::sleep(Duration::from_secs(15)); - tokio::pin!(sleep); + let mut sleep = futures_timer::Delay::new(Duration::from_secs(15)); + loop { tokio::select! { _ = &mut sleep => panic!( @@ -367,17 +210,9 @@ async fn autorelay_drops_reservations_when_public_address_appears() { #[tokio::test] async fn autorelay_blacklists_failing_relay_and_retries_after_cooldown() { - let _ = tracing_subscriber::fmt() - .with_env_filter(EnvFilter::from_default_env()) - .try_init(); + init_tracing(); - let mut relay = build_rejecting_relay(); - let relay_addr = Multiaddr::empty().with(Protocol::Memory(rand::random::())); - relay.listen_on(relay_addr.clone()).unwrap(); - relay.add_external_address(relay_addr.clone()); - tokio::spawn(async move { - relay.collect::>().await; - }); + let (_, relay_addr) = spawn_rejecting_relay(); let cooldown = Duration::from_secs(1); let mut client = build_client( @@ -389,13 +224,13 @@ async fn autorelay_blacklists_failing_relay_and_retries_after_cooldown() { let first_failure_at = wait_for_listener_failure(&mut client, Duration::from_secs(10)).await; - let early_retry = tokio::time::timeout( - cooldown / 2, + let early_retry = with_timeout( wait_for_listener_failure(&mut client, cooldown * 5), + cooldown / 2, ) .await; assert!( - early_retry.is_err(), + early_retry.is_none(), "autorelay retried during the cooldown window" ); @@ -411,8 +246,8 @@ async fn wait_for_listener_failure( client: &mut Swarm, timeout: Duration, ) -> std::time::Instant { - let sleep = tokio::time::sleep(timeout); - tokio::pin!(sleep); + let mut sleep = futures_timer::Delay::new(timeout); + loop { tokio::select! { _ = &mut sleep => panic!("timeout waiting for listener failure"), @@ -427,17 +262,9 @@ async fn wait_for_listener_failure( #[tokio::test] async fn autorelay_disabled_does_not_reserve() { - let _ = tracing_subscriber::fmt() - .with_env_filter(EnvFilter::from_default_env()) - .try_init(); + init_tracing(); - let mut relay = build_relay(); - let relay_addr = Multiaddr::empty().with(Protocol::Memory(rand::random::())); - relay.listen_on(relay_addr.clone()).unwrap(); - relay.add_external_address(relay_addr.clone()); - tokio::spawn(async move { - relay.collect::>().await; - }); + let (_, relay_addr) = spawn_relay(); let mut client = build_client(autorelay::Config::default()); client @@ -446,8 +273,7 @@ async fn autorelay_disabled_does_not_reserve() { .set_status(Some(autorelay::Status::Disable)); client.dial(relay_addr).unwrap(); - let observed = tokio::time::timeout( - Duration::from_secs(3), + let observed = with_timeout( wait_until(&mut client, Duration::from_secs(5), |event| { matches!( event, @@ -456,28 +282,21 @@ async fn autorelay_disabled_does_not_reserve() { )) ) }), + Duration::from_secs(3), ) .await; assert!( - observed.is_err(), + observed.is_none(), "autorelay opened a reservation while disabled" ); } #[tokio::test] async fn autorelay_re_enable_triggers_reservation() { - let _ = tracing_subscriber::fmt() - .with_env_filter(EnvFilter::from_default_env()) - .try_init(); + init_tracing(); - let mut relay = build_relay(); - let relay_addr = Multiaddr::empty().with(Protocol::Memory(rand::random::())); - relay.listen_on(relay_addr.clone()).unwrap(); - relay.add_external_address(relay_addr.clone()); - tokio::spawn(async move { - relay.collect::>().await; - }); + let (_, relay_addr) = spawn_relay(); let mut client = build_client(autorelay::Config::default()); client @@ -486,8 +305,8 @@ async fn autorelay_re_enable_triggers_reservation() { .set_status(Some(autorelay::Status::Disable)); client.dial(relay_addr).unwrap(); - let sleep = tokio::time::sleep(Duration::from_secs(3)); - tokio::pin!(sleep); + let mut sleep = futures_timer::Delay::new(Duration::from_secs(3)); + loop { tokio::select! { _ = &mut sleep => break, @@ -522,17 +341,9 @@ async fn autorelay_re_enable_triggers_reservation() { #[tokio::test] async fn autorelay_disable_preserves_active_reservation() { - let _ = tracing_subscriber::fmt() - .with_env_filter(EnvFilter::from_default_env()) - .try_init(); + init_tracing(); - let mut relay = build_relay(); - let relay_addr = Multiaddr::empty().with(Protocol::Memory(rand::random::())); - relay.listen_on(relay_addr.clone()).unwrap(); - relay.add_external_address(relay_addr.clone()); - tokio::spawn(async move { - relay.collect::>().await; - }); + let (_, relay_addr) = spawn_relay(); let mut client = build_client(autorelay::Config::default()); client.dial(relay_addr).unwrap(); @@ -552,8 +363,8 @@ async fn autorelay_disable_preserves_active_reservation() { .autorelay .set_status(Some(autorelay::Status::Disable)); - let sleep = tokio::time::sleep(Duration::from_secs(3)); - tokio::pin!(sleep); + let mut sleep = futures_timer::Delay::new(Duration::from_secs(3)); + loop { tokio::select! { _ = &mut sleep => break, @@ -571,27 +382,10 @@ async fn autorelay_disable_preserves_active_reservation() { #[tokio::test] async fn autorelay_prefers_static_relay() { - let _ = tracing_subscriber::fmt() - .with_env_filter(EnvFilter::from_default_env()) - .try_init(); + init_tracing(); - let mut opportunistic = build_relay(); - let opportunistic_addr = Multiaddr::empty().with(Protocol::Memory(rand::random::())); - let opportunistic_peer = *opportunistic.local_peer_id(); - opportunistic.listen_on(opportunistic_addr.clone()).unwrap(); - opportunistic.add_external_address(opportunistic_addr.clone()); - tokio::spawn(async move { - opportunistic.collect::>().await; - }); - - let mut staticr = build_relay(); - let static_addr = Multiaddr::empty().with(Protocol::Memory(rand::random::())); - let static_peer = *staticr.local_peer_id(); - staticr.listen_on(static_addr.clone()).unwrap(); - staticr.add_external_address(static_addr.clone()); - tokio::spawn(async move { - staticr.collect::>().await; - }); + let (opportunistic_peer, opportunistic_addr) = spawn_relay(); + let (static_peer, static_addr) = spawn_relay(); let mut client = build_client(autorelay::Config::default().set_max_reservations(NonZeroU8::new(1).unwrap())); @@ -607,8 +401,7 @@ async fn autorelay_prefers_static_relay() { .add_static_relay(static_peer, static_addr); // Let both connections establish and identify exchanges complete. - let warmup = tokio::time::sleep(Duration::from_secs(3)); - tokio::pin!(warmup); + let mut warmup = futures_timer::Delay::new(Duration::from_secs(3)); loop { tokio::select! { _ = &mut warmup => break, @@ -640,56 +433,11 @@ async fn autorelay_prefers_static_relay() { assert_ne!(accepted_peer, opportunistic_peer); } -#[tokio::test] -async fn add_static_relay_dials_and_reserves() { - let _ = tracing_subscriber::fmt() - .with_env_filter(EnvFilter::from_default_env()) - .try_init(); - - let mut relay = build_relay(); - let relay_addr = Multiaddr::empty().with(Protocol::Memory(rand::random::())); - let relay_peer = *relay.local_peer_id(); - relay.listen_on(relay_addr.clone()).unwrap(); - relay.add_external_address(relay_addr.clone()); - tokio::spawn(async move { - relay.collect::>().await; - }); - - let mut client = build_client(autorelay::Config::default()); - client - .behaviour_mut() - .autorelay - .add_static_relay(relay_peer, relay_addr); - - let accepted_peer = wait_until_some(&mut client, Duration::from_secs(15), |event| { - if let SwarmEvent::Behaviour(ClientEvent::RelayClient( - relay::client::Event::ReservationReqAccepted { relay_peer_id, .. }, - )) = event - { - Some(*relay_peer_id) - } else { - None - } - }) - .await; - - assert_eq!(accepted_peer, relay_peer); -} - #[tokio::test] async fn remove_static_relay_preserves_active_reservation() { - let _ = tracing_subscriber::fmt() - .with_env_filter(EnvFilter::from_default_env()) - .try_init(); + init_tracing(); - let mut relay = build_relay(); - let relay_addr = Multiaddr::empty().with(Protocol::Memory(rand::random::())); - let relay_peer = *relay.local_peer_id(); - relay.listen_on(relay_addr.clone()).unwrap(); - relay.add_external_address(relay_addr.clone()); - tokio::spawn(async move { - relay.collect::>().await; - }); + let (relay_peer, relay_addr) = spawn_relay(); let mut client = build_client(autorelay::Config::default()); client @@ -714,8 +462,8 @@ async fn remove_static_relay_preserves_active_reservation() { .remove_static_relay(&relay_peer) ); - let sleep = tokio::time::sleep(Duration::from_secs(3)); - tokio::pin!(sleep); + let mut sleep = futures_timer::Delay::new(Duration::from_secs(3)); + loop { tokio::select! { _ = &mut sleep => break, @@ -733,18 +481,9 @@ async fn remove_static_relay_preserves_active_reservation() { #[tokio::test] async fn static_relay_redials_after_connection_drop() { - let _ = tracing_subscriber::fmt() - .with_env_filter(EnvFilter::from_default_env()) - .try_init(); + init_tracing(); - let mut relay = build_relay(); - let relay_addr = Multiaddr::empty().with(Protocol::Memory(rand::random::())); - let relay_peer = *relay.local_peer_id(); - relay.listen_on(relay_addr.clone()).unwrap(); - relay.add_external_address(relay_addr.clone()); - tokio::spawn(async move { - relay.collect::>().await; - }); + let (relay_peer, relay_addr) = spawn_relay(); let mut client = build_client(autorelay::Config::default()); client @@ -752,34 +491,8 @@ async fn static_relay_redials_after_connection_drop() { .autorelay .add_static_relay(relay_peer, relay_addr); - let conn_id = wait_until_some(&mut client, Duration::from_secs(15), { - let mut established_conn: Option = None; - let mut reservation_seen = false; - move |event| { - match event { - SwarmEvent::ConnectionEstablished { - peer_id, - connection_id, - endpoint, - .. - } if *peer_id == relay_peer && !endpoint.is_relayed() => { - established_conn = Some(*connection_id); - } - SwarmEvent::Behaviour(ClientEvent::RelayClient( - relay::client::Event::ReservationReqAccepted { relay_peer_id, .. }, - )) if *relay_peer_id == relay_peer => { - reservation_seen = true; - } - _ => {} - } - if reservation_seen { - established_conn - } else { - None - } - } - }) - .await; + let conn_id = + wait_for_reservation_with_conn(&mut client, relay_peer, Duration::from_secs(15)).await; assert!(client.close_connection(conn_id)); @@ -810,8 +523,8 @@ async fn wait_until_some(client: &mut Swarm, timeout: Duration, mu where F: FnMut(&SwarmEvent) -> Option, { - let sleep = tokio::time::sleep(timeout); - tokio::pin!(sleep); + let mut sleep = futures_timer::Delay::new(timeout); + loop { tokio::select! { _ = &mut sleep => panic!("timeout waiting on predicate"), @@ -824,102 +537,17 @@ where } } -#[tokio::test] -async fn autorelay_emits_no_relays_available_after_losing_last_relay() { - let _ = tracing_subscriber::fmt() - .with_env_filter(EnvFilter::from_default_env()) - .try_init(); - - let mut relay = build_relay(); - let relay_addr = Multiaddr::empty().with(Protocol::Memory(rand::random::())); - let relay_peer = *relay.local_peer_id(); - relay.listen_on(relay_addr.clone()).unwrap(); - relay.add_external_address(relay_addr.clone()); - tokio::spawn(async move { - relay.collect::>().await; - }); - - let mut client = build_client(autorelay::Config::default()); - client.dial(relay_addr).unwrap(); - - let conn_id = wait_until_some(&mut client, Duration::from_secs(15), { - let mut established: Option = None; - let mut reserved = false; - move |event| { - match event { - SwarmEvent::ConnectionEstablished { - peer_id, - connection_id, - endpoint, - .. - } if *peer_id == relay_peer && !endpoint.is_relayed() => { - established = Some(*connection_id); - } - SwarmEvent::Behaviour(ClientEvent::RelayClient( - relay::client::Event::ReservationReqAccepted { relay_peer_id, .. }, - )) if *relay_peer_id == relay_peer => { - reserved = true; - } - _ => {} - } - if reserved { established } else { None } - } - }) - .await; - - assert!(client.close_connection(conn_id)); - - wait_until(&mut client, Duration::from_secs(10), |event| { - matches!( - event, - SwarmEvent::Behaviour(ClientEvent::Autorelay(autorelay::Event::NoRelaysAvailable)) - ) - }) - .await; -} - #[tokio::test] async fn autorelay_emits_relay_available_after_recovery() { - let _ = tracing_subscriber::fmt() - .with_env_filter(EnvFilter::from_default_env()) - .try_init(); + init_tracing(); - let mut relay = build_relay(); - let relay_addr = Multiaddr::empty().with(Protocol::Memory(rand::random::())); - let relay_peer = *relay.local_peer_id(); - relay.listen_on(relay_addr.clone()).unwrap(); - relay.add_external_address(relay_addr.clone()); - tokio::spawn(async move { - relay.collect::>().await; - }); + let (relay_peer, relay_addr) = spawn_relay(); let mut client = build_client(autorelay::Config::default()); client.dial(relay_addr.clone()).unwrap(); - let conn_id = wait_until_some(&mut client, Duration::from_secs(15), { - let mut established: Option = None; - let mut reserved = false; - move |event| { - match event { - SwarmEvent::ConnectionEstablished { - peer_id, - connection_id, - endpoint, - .. - } if *peer_id == relay_peer && !endpoint.is_relayed() => { - established = Some(*connection_id); - } - SwarmEvent::Behaviour(ClientEvent::RelayClient( - relay::client::Event::ReservationReqAccepted { relay_peer_id, .. }, - )) if *relay_peer_id == relay_peer => { - reserved = true; - } - _ => {} - } - if reserved { established } else { None } - } - }) - .await; + let conn_id = + wait_for_reservation_with_conn(&mut client, relay_peer, Duration::from_secs(15)).await; assert!(client.close_connection(conn_id)); @@ -944,27 +572,10 @@ async fn autorelay_emits_relay_available_after_recovery() { #[tokio::test] async fn autorelay_no_relays_available_is_edge_triggered() { - let _ = tracing_subscriber::fmt() - .with_env_filter(EnvFilter::from_default_env()) - .try_init(); + init_tracing(); - let mut relay_a = build_relay(); - let relay_a_addr = Multiaddr::empty().with(Protocol::Memory(rand::random::())); - let relay_a_peer = *relay_a.local_peer_id(); - relay_a.listen_on(relay_a_addr.clone()).unwrap(); - relay_a.add_external_address(relay_a_addr.clone()); - tokio::spawn(async move { - relay_a.collect::>().await; - }); - - let mut relay_b = build_relay(); - let relay_b_addr = Multiaddr::empty().with(Protocol::Memory(rand::random::())); - let relay_b_peer = *relay_b.local_peer_id(); - relay_b.listen_on(relay_b_addr.clone()).unwrap(); - relay_b.add_external_address(relay_b_addr.clone()); - tokio::spawn(async move { - relay_b.collect::>().await; - }); + let (relay_a_peer, relay_a_addr) = spawn_relay(); + let (relay_b_peer, relay_b_addr) = spawn_relay(); let mut client = build_client(autorelay::Config::default()); client.dial(relay_a_addr).unwrap(); @@ -972,8 +583,8 @@ async fn autorelay_no_relays_available_is_edge_triggered() { let mut conns: HashMap = HashMap::new(); let mut reserved: HashSet = HashSet::new(); - let sleep = tokio::time::sleep(Duration::from_secs(20)); - tokio::pin!(sleep); + let mut sleep = futures_timer::Delay::new(Duration::from_secs(20)); + loop { tokio::select! { _ = &mut sleep => panic!("did not get both reservations in time"), @@ -1005,8 +616,8 @@ async fn autorelay_no_relays_available_is_edge_triggered() { assert!(client.close_connection(conn_b)); let mut starved_count = 0usize; - let sleep = tokio::time::sleep(Duration::from_secs(5)); - tokio::pin!(sleep); + let mut sleep = futures_timer::Delay::new(Duration::from_secs(5)); + loop { tokio::select! { _ = &mut sleep => break, @@ -1031,33 +642,16 @@ async fn autorelay_no_relays_available_is_edge_triggered() { #[tokio::test] async fn autorelay_resumes_after_public_address_removed() { - let _ = tracing_subscriber::fmt() - .with_env_filter(EnvFilter::from_default_env()) - .try_init(); + init_tracing(); - let mut relay = build_relay(); - let relay_addr = Multiaddr::empty().with(Protocol::Memory(rand::random::())); - let relay_peer = *relay.local_peer_id(); - relay.listen_on(relay_addr.clone()).unwrap(); - relay.add_external_address(relay_addr.clone()); - tokio::spawn(async move { - relay.collect::>().await; - }); + let (relay_peer, relay_addr) = spawn_relay(); let mut client = build_client(autorelay::Config::default()); client.dial(relay_addr).unwrap(); - wait_until(&mut client, Duration::from_secs(15), |event| { - matches!( - event, - SwarmEvent::Behaviour(ClientEvent::RelayClient( - relay::client::Event::ReservationReqAccepted { .. } - )) - ) - }) - .await; + wait_for_reservation_from(&mut client, relay_peer, Duration::from_secs(15)).await; - let public_addr = Multiaddr::empty().with(Protocol::Memory(rand::random::())); + let public_addr = memory_addr(); client.add_external_address(public_addr.clone()); wait_until(&mut client, Duration::from_secs(10), |event| { @@ -1067,31 +661,14 @@ async fn autorelay_resumes_after_public_address_removed() { client.remove_external_address(&public_addr); - wait_until(&mut client, Duration::from_secs(15), |event| { - matches!( - event, - SwarmEvent::Behaviour(ClientEvent::RelayClient( - relay::client::Event::ReservationReqAccepted { relay_peer_id, .. } - )) if *relay_peer_id == relay_peer - ) - }) - .await; + wait_for_reservation_from(&mut client, relay_peer, Duration::from_secs(15)).await; } #[tokio::test] async fn autorelay_manual_enable_ignores_public_address() { - let _ = tracing_subscriber::fmt() - .with_env_filter(EnvFilter::from_default_env()) - .try_init(); + init_tracing(); - let mut relay = build_relay(); - let relay_addr = Multiaddr::empty().with(Protocol::Memory(rand::random::())); - let relay_peer = *relay.local_peer_id(); - relay.listen_on(relay_addr.clone()).unwrap(); - relay.add_external_address(relay_addr.clone()); - tokio::spawn(async move { - relay.collect::>().await; - }); + let (relay_peer, relay_addr) = spawn_relay(); let mut client = build_client(autorelay::Config::default()); client @@ -1100,21 +677,12 @@ async fn autorelay_manual_enable_ignores_public_address() { .set_status(Some(autorelay::Status::Enable)); client.dial(relay_addr).unwrap(); - wait_until(&mut client, Duration::from_secs(15), |event| { - matches!( - event, - SwarmEvent::Behaviour(ClientEvent::RelayClient( - relay::client::Event::ReservationReqAccepted { relay_peer_id, .. } - )) if *relay_peer_id == relay_peer - ) - }) - .await; + wait_for_reservation_from(&mut client, relay_peer, Duration::from_secs(15)).await; - let public_addr = Multiaddr::empty().with(Protocol::Memory(rand::random::())); - client.add_external_address(public_addr); + client.add_external_address(memory_addr()); + + let mut sleep = futures_timer::Delay::new(Duration::from_secs(3)); - let sleep = tokio::time::sleep(Duration::from_secs(3)); - tokio::pin!(sleep); loop { tokio::select! { _ = &mut sleep => break, @@ -1138,113 +706,17 @@ async fn autorelay_manual_enable_ignores_public_address() { } } -#[tokio::test] -async fn autorelay_records_previous_relay_after_reservation_loss() { - let _ = tracing_subscriber::fmt() - .with_env_filter(EnvFilter::from_default_env()) - .try_init(); - - let mut relay = build_relay(); - let relay_addr = Multiaddr::empty().with(Protocol::Memory(rand::random::())); - let relay_peer = *relay.local_peer_id(); - relay.listen_on(relay_addr.clone()).unwrap(); - relay.add_external_address(relay_addr.clone()); - tokio::spawn(async move { - relay.collect::>().await; - }); - - let mut client = build_client(autorelay::Config::default()); - client.dial(relay_addr).unwrap(); - - let conn_id = wait_until_some(&mut client, Duration::from_secs(15), { - let mut established: Option = None; - let mut reserved = false; - move |event| { - match event { - SwarmEvent::ConnectionEstablished { - peer_id, - connection_id, - endpoint, - .. - } if *peer_id == relay_peer && !endpoint.is_relayed() => { - established = Some(*connection_id); - } - SwarmEvent::Behaviour(ClientEvent::RelayClient( - relay::client::Event::ReservationReqAccepted { relay_peer_id, .. }, - )) if *relay_peer_id == relay_peer => { - reserved = true; - } - _ => {} - } - if reserved { established } else { None } - } - }) - .await; - - assert!(client.close_connection(conn_id)); - - wait_until(&mut client, Duration::from_secs(10), |event| { - matches!( - event, - SwarmEvent::Behaviour(ClientEvent::Autorelay(autorelay::Event::NoRelaysAvailable)) - ) - }) - .await; - - let previous: Vec = client - .behaviour() - .autorelay - .previous_relays() - .map(|(p, _, _)| *p) - .collect(); - assert!( - previous.contains(&relay_peer), - "expected {relay_peer} in previous_relays, got {previous:?}" - ); -} - #[tokio::test] async fn autorelay_forgets_previous_relay_on_reacquire() { - let _ = tracing_subscriber::fmt() - .with_env_filter(EnvFilter::from_default_env()) - .try_init(); + init_tracing(); - let mut relay = build_relay(); - let relay_addr = Multiaddr::empty().with(Protocol::Memory(rand::random::())); - let relay_peer = *relay.local_peer_id(); - relay.listen_on(relay_addr.clone()).unwrap(); - relay.add_external_address(relay_addr.clone()); - tokio::spawn(async move { - relay.collect::>().await; - }); + let (relay_peer, relay_addr) = spawn_relay(); let mut client = build_client(autorelay::Config::default()); client.dial(relay_addr.clone()).unwrap(); - let conn_id = wait_until_some(&mut client, Duration::from_secs(15), { - let mut established: Option = None; - let mut reserved = false; - move |event| { - match event { - SwarmEvent::ConnectionEstablished { - peer_id, - connection_id, - endpoint, - .. - } if *peer_id == relay_peer && !endpoint.is_relayed() => { - established = Some(*connection_id); - } - SwarmEvent::Behaviour(ClientEvent::RelayClient( - relay::client::Event::ReservationReqAccepted { relay_peer_id, .. }, - )) if *relay_peer_id == relay_peer => { - reserved = true; - } - _ => {} - } - if reserved { established } else { None } - } - }) - .await; + let conn_id = + wait_for_reservation_with_conn(&mut client, relay_peer, Duration::from_secs(15)).await; assert!(client.close_connection(conn_id)); @@ -1289,22 +761,9 @@ async fn autorelay_forgets_previous_relay_on_reacquire() { #[tokio::test] async fn autorelay_previous_relays_is_bounded() { - let _ = tracing_subscriber::fmt() - .with_env_filter(EnvFilter::from_default_env()) - .try_init(); + init_tracing(); - let mut peers_and_addrs: Vec<(PeerId, Multiaddr)> = Vec::with_capacity(3); - for _ in 0..3 { - let mut relay = build_relay(); - let addr = Multiaddr::empty().with(Protocol::Memory(rand::random::())); - let peer_id = *relay.local_peer_id(); - relay.listen_on(addr.clone()).unwrap(); - relay.add_external_address(addr.clone()); - peers_and_addrs.push((peer_id, addr.clone())); - tokio::spawn(async move { - relay.collect::>().await; - }); - } + let peers_and_addrs: Vec<(PeerId, Multiaddr)> = (0..3).map(|_| spawn_relay()).collect(); let mut client = build_client( autorelay::Config::default() @@ -1312,48 +771,21 @@ async fn autorelay_previous_relays_is_bounded() { .set_max_previous_relays(2), ); - for (idx, (peer, addr)) in peers_and_addrs.iter().enumerate() { + for (peer, addr) in &peers_and_addrs { client.dial(addr.clone()).unwrap(); - let conn_id = wait_until_some(&mut client, Duration::from_secs(15), { - let target_peer = *peer; - let mut established: Option = None; - let mut reserved = false; - move |event| { - match event { - SwarmEvent::ConnectionEstablished { - peer_id, - connection_id, - endpoint, - .. - } if *peer_id == target_peer && !endpoint.is_relayed() => { - established = Some(*connection_id); - } - SwarmEvent::Behaviour(ClientEvent::RelayClient( - relay::client::Event::ReservationReqAccepted { relay_peer_id, .. }, - )) if *relay_peer_id == target_peer => { - reserved = true; - } - _ => {} - } - if reserved { established } else { None } - } - }) - .await; + let conn_id = + wait_for_reservation_with_conn(&mut client, *peer, Duration::from_secs(15)).await; assert!(client.close_connection(conn_id)); wait_until(&mut client, Duration::from_secs(10), |event| { matches!( event, - SwarmEvent::Behaviour(ClientEvent::Autorelay( - autorelay::Event::NoRelaysAvailable - )) + SwarmEvent::Behaviour(ClientEvent::Autorelay(autorelay::Event::NoRelaysAvailable)) ) }) .await; - - let _ = idx; } let previous: Vec = client @@ -1378,16 +810,13 @@ async fn autorelay_previous_relays_is_bounded() { #[tokio::test] async fn autorelay_static_relay_dial_cooldown_after_failure() { - let _ = tracing_subscriber::fmt() - .with_env_filter(EnvFilter::from_default_env()) - .try_init(); + init_tracing(); let cooldown = Duration::from_secs(2); - let mut client = - build_client(autorelay::Config::default().set_failure_cooldown(cooldown)); + let mut client = build_client(autorelay::Config::default().set_failure_cooldown(cooldown)); let unreachable_peer = PeerId::random(); - let unreachable_addr = Multiaddr::empty().with(Protocol::Memory(rand::random::())); + let unreachable_addr = memory_addr(); client .behaviour_mut() @@ -1410,8 +839,7 @@ async fn autorelay_static_relay_dial_cooldown_after_failure() { .add_static_relay(unreachable_peer, unreachable_addr.clone()); let mut redialed = false; - let watch = tokio::time::sleep(cooldown / 2); - tokio::pin!(watch); + let mut watch = futures_timer::Delay::new(cooldown / 2); loop { tokio::select! { _ = &mut watch => break, @@ -1432,7 +860,7 @@ async fn autorelay_static_relay_dial_cooldown_after_failure() { .checked_sub(first_failure_at.elapsed()) .unwrap_or_default(); if !remaining.is_zero() { - tokio::time::sleep(remaining + Duration::from_millis(200)).await; + futures_timer::Delay::new(remaining + Duration::from_millis(200)).await; } client @@ -1449,52 +877,46 @@ async fn autorelay_static_relay_dial_cooldown_after_failure() { .await; } -async fn wait_for_reservation_from_either( - client: &mut Swarm, - peer_a: PeerId, - peer_b: PeerId, - timeout: Duration, -) -> (PeerId, ConnectionId) { - let sleep = tokio::time::sleep(timeout); - tokio::pin!(sleep); - let mut direct_conns: HashMap = HashMap::new(); - loop { - tokio::select! { - _ = &mut sleep => panic!("timeout waiting for reservation from either relay"), - ev = client.select_next_some() => { - match ev { - SwarmEvent::ConnectionEstablished { - peer_id, - connection_id, - endpoint, - .. - } if (peer_id == peer_a || peer_id == peer_b) - && !endpoint.is_relayed() => - { - direct_conns.insert(peer_id, connection_id); - } - SwarmEvent::Behaviour(ClientEvent::RelayClient( - relay::client::Event::ReservationReqAccepted { relay_peer_id, .. }, - )) if relay_peer_id == peer_a || relay_peer_id == peer_b => { - let conn_id = direct_conns - .get(&relay_peer_id) - .copied() - .expect("direct connection to relay was observed"); - return (relay_peer_id, conn_id); - } - _ => {} - } - } +#[tokio::test] +async fn autorelay_evicts_discovered_peers_for_static() { + init_tracing(); + + let (opp_a_peer, opp_a_addr) = spawn_relay(); + let (opp_b_peer, opp_b_addr) = spawn_relay(); + let (static_peer, static_addr) = spawn_relay(); + + let mut client = + build_client(autorelay::Config::default().set_max_reservations(NonZeroU8::new(1).unwrap())); + + client.dial(opp_a_addr).unwrap(); + client.dial(opp_b_addr).unwrap(); + + wait_until_some(&mut client, Duration::from_secs(20), |event| { + if let SwarmEvent::Behaviour(ClientEvent::RelayClient( + relay::client::Event::ReservationReqAccepted { relay_peer_id, .. }, + )) = event + && (*relay_peer_id == opp_a_peer || *relay_peer_id == opp_b_peer) + { + Some(*relay_peer_id) + } else { + None } - } + }) + .await; + + client + .behaviour_mut() + .autorelay + .add_static_relay(static_peer, static_addr); + + wait_for_reservation_from(&mut client, static_peer, Duration::from_secs(20)).await; } async fn wait_until(client: &mut Swarm, timeout: Duration, mut predicate: F) where F: FnMut(&SwarmEvent) -> bool, { - let sleep = tokio::time::sleep(timeout); - tokio::pin!(sleep); + let mut sleep = futures_timer::Delay::new(timeout); loop { tokio::select! { _ = &mut sleep => panic!("timeout waiting on predicate"), @@ -1507,6 +929,86 @@ where } } +async fn with_timeout(future: F, timeout: Duration) -> Option { + use futures::future::Either; + let timer = futures_timer::Delay::new(timeout); + futures::pin_mut!(future); + match futures::future::select(future, timer).await { + Either::Left((output, _)) => Some(output), + Either::Right(_) => None, + } +} + +async fn wait_for_reservation_from(client: &mut Swarm, peer: PeerId, timeout: Duration) { + wait_until(client, timeout, |event| { + matches!( + event, + SwarmEvent::Behaviour(ClientEvent::RelayClient( + relay::client::Event::ReservationReqAccepted { relay_peer_id, .. } + )) if *relay_peer_id == peer + ) + }) + .await; +} + +async fn wait_for_reservation_with_conn( + client: &mut Swarm, + peer: PeerId, + timeout: Duration, +) -> ConnectionId { + wait_until_some(client, timeout, { + let mut established: Option = None; + let mut reserved = false; + move |event| { + match event { + SwarmEvent::ConnectionEstablished { + peer_id, + connection_id, + endpoint, + .. + } if *peer_id == peer && !endpoint.is_relayed() => { + established = Some(*connection_id); + } + SwarmEvent::Behaviour(ClientEvent::RelayClient( + relay::client::Event::ReservationReqAccepted { relay_peer_id, .. }, + )) if *relay_peer_id == peer => { + reserved = true; + } + _ => {} + } + if reserved { established } else { None } + } + }) + .await +} + +fn init_tracing() { + let _ = tracing_subscriber::fmt() + .with_env_filter(EnvFilter::from_default_env()) + .try_init(); +} + +fn memory_addr() -> Multiaddr { + Multiaddr::empty().with(Protocol::Memory(rand::random::())) +} + +fn spawn_relay() -> (PeerId, Multiaddr) { + spawn_relay_swarm(build_relay()) +} + +fn spawn_rejecting_relay() -> (PeerId, Multiaddr) { + spawn_relay_swarm(build_rejecting_relay()) +} + +fn spawn_relay_swarm(mut relay: Swarm) -> (PeerId, Multiaddr) { + let addr = memory_addr(); + let peer = *relay.local_peer_id(); + relay.listen_on(addr.clone()).unwrap(); + relay.add_external_address(addr.clone()); + tokio::spawn(relay.collect::>()); + (peer, addr) +} + fn build_relay() -> Swarm { build_relay_with_config(relay::Config { reservation_duration: Duration::from_secs(60), From 0b9109ad49b58553990f832ff6f692acd6891693 Mon Sep 17 00:00:00 2001 From: Darius Clark Date: Tue, 26 May 2026 08:24:20 -0500 Subject: [PATCH 34/51] chore: misc cleanup and linting --- protocols/relay/src/autorelay.rs | 73 +++++++++++++++----------------- 1 file changed, 35 insertions(+), 38 deletions(-) diff --git a/protocols/relay/src/autorelay.rs b/protocols/relay/src/autorelay.rs index 499cedbd8f3..419c0c8e824 100644 --- a/protocols/relay/src/autorelay.rs +++ b/protocols/relay/src/autorelay.rs @@ -1,3 +1,10 @@ +use std::{ + collections::{HashMap, HashSet, VecDeque}, + num::NonZeroU8, + task::{Context, Poll, Waker}, + time::{Duration, Instant}, +}; + use either::Either; use libp2p_core::{ Endpoint, @@ -15,13 +22,6 @@ use libp2p_swarm::{ dial_opts::DialOpts, dummy, }; -use std::task::Waker; -use std::{ - collections::{HashMap, HashSet, VecDeque}, - num::NonZeroU8, - task::{Context, Poll}, - time::{Duration, Instant}, -}; use crate::{autorelay::handler::Out, multiaddr_ext::MultiaddrExt}; @@ -211,8 +211,9 @@ impl Behaviour { /// Register a peer as a static relay. /// - /// This will dial and establish a connection to the peer if it doesn't already have a direct connection. - /// Note: Peers that are through a relay cannot be used as a static peer + /// This will dial and establish a connection to the peer if it doesn't already have a direct + /// connection. + /// Note that peers that are through a relay cannot be used as a static peer pub fn add_static_relay(&mut self, peer_id: PeerId, address: Multiaddr) { if address.is_relayed() { tracing::warn!(%peer_id, %address, "static relay address is relayed. ignoring."); @@ -224,16 +225,10 @@ impl Behaviour { self.evict_for_static_peer(peer_id); } - if !self.has_direct_connection(&peer_id) && !self.static_dial_in_cooldown(&peer_id) { - let opts = DialOpts::peer_id(peer_id).addresses(vec![address]).build(); - self.events.push_back(ToSwarm::Dial { opts }); - if let Some(waker) = self.waker.take() { - waker.wake(); - } - return; + if !self.queue_static_dial(peer_id, address) { + self.meet_reservation_target(); } - self.meet_reservation_target(); if let Some(waker) = self.waker.take() { waker.wake(); } @@ -262,6 +257,15 @@ impl Behaviour { .is_some_and(|deadline| *deadline > Instant::now()) } + fn queue_static_dial(&mut self, peer_id: PeerId, address: Multiaddr) -> bool { + if self.has_direct_connection(&peer_id) || self.static_dial_in_cooldown(&peer_id) { + return false; + } + let opts = DialOpts::peer_id(peer_id).addresses(vec![address]).build(); + self.events.push_back(ToSwarm::Dial { opts }); + true + } + fn record_previous_relay(&mut self, peer_id: PeerId, address: Multiaddr) { let max = self.config.max_previous_relays; if max == 0 { @@ -283,12 +287,11 @@ impl Behaviour { let attempts = self.failure_counts.entry(peer_id).or_insert(0); *attempts = attempts.saturating_add(1); let exponent = attempts.saturating_sub(1).min(20); - let scale = 1u64.checked_shl(exponent).unwrap_or(u64::MAX); - let base = self.config.failure_cooldown; - let scaled = base - .checked_mul(scale.min(u32::MAX as u64) as u32) - .unwrap_or(self.config.failure_cooldown_max); - scaled.min(self.config.failure_cooldown_max) + let scale = 1u32 << exponent; + self.config + .failure_cooldown + .saturating_mul(scale) + .min(self.config.failure_cooldown_max) } fn clear_failure(&mut self, peer_id: &PeerId) { @@ -319,15 +322,13 @@ impl Behaviour { } fn is_peer_idle(&self, peer_id: &PeerId) -> bool { - self.connections - .iter() - .filter(|((pid, _), _)| pid == peer_id) - .any(|((_, _), info)| { - info.relay_status + self.connections.iter().any(|((pid, _), info)| { + pid == peer_id + && info.relay_status == RelayStatus::Supported { status: ReservationStatus::Idle, } - }) + }) } fn has_direct_connection(&self, peer_id: &PeerId) -> bool { @@ -346,11 +347,11 @@ impl Behaviour { return; } - if let Some((listener_id, ..)) = self + if let Some(listener_id) = self .reservations .iter() .find(|(_, (peer_id, _))| !self.static_relays.contains_key(peer_id)) - .map(|(listener_id, (peer_id, connection_id))| (*listener_id, *peer_id, *connection_id)) + .map(|(listener_id, _)| *listener_id) { self.events .push_back(ToSwarm::RemoveListener { id: listener_id }); @@ -645,15 +646,11 @@ impl NetworkBehaviour for Behaviour { } if had_reservation { - self.record_previous_relay(peer_id, connection.address.clone()); + self.record_previous_relay(peer_id, connection.address); } - if let Some(address) = self.static_relays.get(&peer_id).cloned() - && !self.has_direct_connection(&peer_id) - && !self.static_dial_in_cooldown(&peer_id) - { - let opts = DialOpts::peer_id(peer_id).addresses(vec![address]).build(); - self.events.push_back(ToSwarm::Dial { opts }); + if let Some(address) = self.static_relays.get(&peer_id).cloned() { + self.queue_static_dial(peer_id, address); } self.update_relay_availability(); From 0f3545b68271fa942c091c5cd47f67fecdf9c0a7 Mon Sep 17 00:00:00 2001 From: Darius Clark Date: Tue, 26 May 2026 08:40:37 -0500 Subject: [PATCH 35/51] chore: update example --- examples/relay-client/src/main.rs | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/examples/relay-client/src/main.rs b/examples/relay-client/src/main.rs index 88a011a377a..b577b92b878 100644 --- a/examples/relay-client/src/main.rs +++ b/examples/relay-client/src/main.rs @@ -4,7 +4,9 @@ use clap::Parser; use futures::stream::StreamExt; use libp2p::{ core::multiaddr::Multiaddr, - identify, identity, noise, ping, + identify, + identity::Keypair, + noise, ping, relay::{self, autorelay}, swarm::{NetworkBehaviour, SwarmEvent}, tcp, yamux, @@ -16,7 +18,7 @@ use tracing_subscriber::EnvFilter; struct Opts { /// Fixed value used to derive a deterministic peer id. #[arg(long)] - secret_key_seed: u8, + secret_key_seed: Option, /// List of relay addresses #[arg(long = "relay", required = true)] @@ -121,15 +123,22 @@ async fn main() -> Result<(), Box> { } SwarmEvent::Behaviour(BehaviourEvent::Identify(_)) => {} SwarmEvent::Behaviour(BehaviourEvent::Ping(_)) => {} - SwarmEvent::Behaviour(BehaviourEvent::Autorelay(_)) => {} + SwarmEvent::Behaviour(BehaviourEvent::Autorelay(e)) => { + tracing::debug!(?e, "Autorelay event"); + } _ => {} }, } } } -fn generate_ed25519(secret_key_seed: u8) -> identity::Keypair { - let mut bytes = [0u8; 32]; - bytes[0] = secret_key_seed; - identity::Keypair::ed25519_from_bytes(bytes).expect("only errors on wrong length") +fn generate_ed25519(secret_key_seed: Option) -> Keypair { + match secret_key_seed { + Some(secret_key_seed) => { + let mut bytes = [0u8; 32]; + bytes[0] = secret_key_seed; + Keypair::ed25519_from_bytes(bytes).expect("only errors on wrong length") + } + None => Keypair::generate_ed25519(), + } } From 89068023ff7b98c18aa991173c88d57d6c5b6669 Mon Sep 17 00:00:00 2001 From: Darius Clark Date: Sun, 31 May 2026 07:39:06 -0500 Subject: [PATCH 36/51] chore: support multiple addresses for a static peer --- protocols/relay/src/autorelay.rs | 49 ++++++++++++++++++++++---------- 1 file changed, 34 insertions(+), 15 deletions(-) diff --git a/protocols/relay/src/autorelay.rs b/protocols/relay/src/autorelay.rs index 419c0c8e824..5e08e2c1ce0 100644 --- a/protocols/relay/src/autorelay.rs +++ b/protocols/relay/src/autorelay.rs @@ -41,7 +41,7 @@ pub struct Behaviour { external_reservations: HashMap, - static_relays: HashMap, + static_relays: HashMap>, static_dial_cooldowns: HashMap, @@ -118,7 +118,7 @@ pub struct Config { failure_cooldown: Duration, failure_cooldown_max: Duration, max_previous_relays: usize, - static_relays: HashMap, + static_relays: HashMap>, } impl Default for Config { @@ -154,8 +154,13 @@ impl Config { self } - pub fn add_static_relay(mut self, peer_id: PeerId, address: Multiaddr) -> Self { - self.static_relays.insert(peer_id, address); + pub fn add_static_relay(mut self, peer_id: PeerId, addresses: Vec) -> Self { + let entry = self.static_relays.entry(peer_id).or_default(); + for addr in addresses { + if !entry.contains(&addr) { + entry.push(addr); + } + } self } } @@ -178,8 +183,10 @@ impl Behaviour { config, ..Default::default() }; - for (peer_id, address) in initial_static_relays { - behaviour.add_static_relay(peer_id, address); + for (peer_id, addresses) in initial_static_relays { + for address in addresses { + behaviour.add_static_relay(peer_id, address); + } } behaviour } @@ -219,13 +226,20 @@ impl Behaviour { tracing::warn!(%peer_id, %address, "static relay address is relayed. ignoring."); return; } - self.static_relays.insert(peer_id, address.clone()); + + let entry = self.static_relays.entry(peer_id).or_default(); + if entry.contains(&address) { + tracing::warn!(%peer_id, %address, "static relay address already exist"); + } else { + entry.push(address); + } + let combined = entry.clone(); if self.is_peer_idle(&peer_id) { self.evict_for_static_peer(peer_id); } - if !self.queue_static_dial(peer_id, address) { + if !self.queue_static_dial(peer_id, combined) { self.meet_reservation_target(); } @@ -241,8 +255,10 @@ impl Behaviour { self.static_relays.remove(peer_id).is_some() } - pub fn static_relays(&self) -> impl Iterator { - self.static_relays.iter() + pub fn static_relays(&self) -> impl Iterator { + self.static_relays + .iter() + .map(|(peer, addrs)| (peer, addrs.as_slice())) } pub fn previous_relays(&self) -> impl Iterator { @@ -257,11 +273,14 @@ impl Behaviour { .is_some_and(|deadline| *deadline > Instant::now()) } - fn queue_static_dial(&mut self, peer_id: PeerId, address: Multiaddr) -> bool { - if self.has_direct_connection(&peer_id) || self.static_dial_in_cooldown(&peer_id) { + fn queue_static_dial(&mut self, peer_id: PeerId, addresses: Vec) -> bool { + if addresses.is_empty() + || self.has_direct_connection(&peer_id) + || self.static_dial_in_cooldown(&peer_id) + { return false; } - let opts = DialOpts::peer_id(peer_id).addresses(vec![address]).build(); + let opts = DialOpts::peer_id(peer_id).addresses(addresses).build(); self.events.push_back(ToSwarm::Dial { opts }); true } @@ -649,8 +668,8 @@ impl NetworkBehaviour for Behaviour { self.record_previous_relay(peer_id, connection.address); } - if let Some(address) = self.static_relays.get(&peer_id).cloned() { - self.queue_static_dial(peer_id, address); + if let Some(addresses) = self.static_relays.get(&peer_id).cloned() { + self.queue_static_dial(peer_id, addresses); } self.update_relay_availability(); From aa31f50863d8dc9d7e30b63ad672a17b5468fc20 Mon Sep 17 00:00:00 2001 From: Darius Clark Date: Thu, 4 Jun 2026 06:53:32 -0500 Subject: [PATCH 37/51] chore: use `ConnectionEvent::RemoteProtocolChange` only --- protocols/relay/src/autorelay/handler.rs | 48 +++++++++++------------- 1 file changed, 21 insertions(+), 27 deletions(-) diff --git a/protocols/relay/src/autorelay/handler.rs b/protocols/relay/src/autorelay/handler.rs index 885f74314cc..a9cd91db041 100644 --- a/protocols/relay/src/autorelay/handler.rs +++ b/protocols/relay/src/autorelay/handler.rs @@ -77,36 +77,30 @@ impl ConnectionHandler for Handler { Self::OutboundOpenInfo, >, ) { - match event { - ConnectionEvent::RemoteProtocolsChange(protocol) - | ConnectionEvent::LocalProtocolsChange(protocol) => { - let change = self.supported_protocol.on_protocols_change(protocol); - if change { - let valid = self - .supported_protocol - .iter() - .any(|proto| HOP_PROTOCOL_NAME.eq(proto)); - - match (valid, self.supported) { - (true, false) => { - self.supported = true; - self.events - .push_back(ConnectionHandlerEvent::NotifyBehaviour(Out::Supported)); - } - (false, true) => { - self.supported = false; - self.blacklist_timer = None; - self.events - .push_back(ConnectionHandlerEvent::NotifyBehaviour( - Out::Unsupported, - )); - } - (true, true) => {} - _ => {} + if let ConnectionEvent::RemoteProtocolsChange(protocol) = event { + let change = self.supported_protocol.on_protocols_change(protocol); + if change { + let valid = self + .supported_protocol + .iter() + .any(|proto| HOP_PROTOCOL_NAME.eq(proto)); + + match (valid, self.supported) { + (true, false) => { + self.supported = true; + self.events + .push_back(ConnectionHandlerEvent::NotifyBehaviour(Out::Supported)); + } + (false, true) => { + self.supported = false; + self.blacklist_timer = None; + self.events + .push_back(ConnectionHandlerEvent::NotifyBehaviour(Out::Unsupported)); } + (true, true) => {} + _ => {} } } - _ => {} } } From f984fb3f5706050dc6d3d34200bfdf420e200895 Mon Sep 17 00:00:00 2001 From: Darius Clark Date: Thu, 4 Jun 2026 06:54:05 -0500 Subject: [PATCH 38/51] chore: mark peer connection as not supported if peer id is diff --- protocols/relay/src/autorelay.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/protocols/relay/src/autorelay.rs b/protocols/relay/src/autorelay.rs index 5e08e2c1ce0..829058e161c 100644 --- a/protocols/relay/src/autorelay.rs +++ b/protocols/relay/src/autorelay.rs @@ -394,7 +394,8 @@ impl Behaviour { let addr_with_peer_id = match info.address.clone().with_p2p(peer_id) { Ok(addr) => addr, Err(addr) => { - tracing::warn!(%addr, "address unexpectedly contains a different peer id than the connection"); + tracing::warn!(%addr, "address unexpectedly contains a different peer id than the connection; marking relay connection ineligible"); + info.relay_status = RelayStatus::NotSupported; return; } }; From 3d73abb661f3cb8da838f3fb96907b808fa2197d Mon Sep 17 00:00:00 2001 From: Darius Clark Date: Thu, 4 Jun 2026 06:54:34 -0500 Subject: [PATCH 39/51] chore: return early if connection doesnt exist --- protocols/relay/src/autorelay.rs | 29 +++++++++++++---------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/protocols/relay/src/autorelay.rs b/protocols/relay/src/autorelay.rs index 829058e161c..e1ba6234a75 100644 --- a/protocols/relay/src/autorelay.rs +++ b/protocols/relay/src/autorelay.rs @@ -643,10 +643,9 @@ impl NetworkBehaviour for Behaviour { connection_id, .. }) => { - let connection = self - .connections - .remove(&(peer_id, connection_id)) - .expect("valid connection"); + let Some(connection) = self.connections.remove(&(peer_id, connection_id)) else { + return; + }; let had_reservation = matches!( connection.relay_status, @@ -681,10 +680,9 @@ impl NetworkBehaviour for Behaviour { old: _, new, }) => { - let connection = self - .connections - .get_mut(&(peer_id, connection_id)) - .expect("valid connection"); + let Some(connection) = self.connections.get_mut(&(peer_id, connection_id)) else { + return; + }; let new_addr = new.get_remote_address(); @@ -697,10 +695,10 @@ impl NetworkBehaviour for Behaviour { if let Some((peer_id, connection_id)) = self.reservations.get(&listener_id).copied() { - let connection = self - .connections - .get_mut(&(peer_id, connection_id)) - .expect("valid connection"); + let Some(connection) = self.connections.get_mut(&(peer_id, connection_id)) + else { + return; + }; if matches!( connection.relay_status, @@ -756,10 +754,9 @@ impl NetworkBehaviour for Behaviour { ) { let Either::Left(event) = event; - let connection = self - .connections - .get_mut(&(peer_id, connection_id)) - .expect("valid connection"); + let Some(connection) = self.connections.get_mut(&(peer_id, connection_id)) else { + return; + }; match event { Out::Supported => { From 2615a51fd979ca0748b4e15ed819c4947269f34e Mon Sep 17 00:00:00 2001 From: Darius Clark Date: Thu, 4 Jun 2026 07:06:20 -0500 Subject: [PATCH 40/51] chore: cleanup failures --- protocols/relay/src/autorelay.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/protocols/relay/src/autorelay.rs b/protocols/relay/src/autorelay.rs index e1ba6234a75..422daa2ccc1 100644 --- a/protocols/relay/src/autorelay.rs +++ b/protocols/relay/src/autorelay.rs @@ -647,6 +647,10 @@ impl NetworkBehaviour for Behaviour { return; }; + if !self.connections.keys().any(|(pid, _)| *pid == peer_id) { + self.clear_failure(&peer_id); + } + let had_reservation = matches!( connection.relay_status, RelayStatus::Supported { From 81c98895d461b4f31ffd43d017ed44864069cbca Mon Sep 17 00:00:00 2001 From: Darius Clark Date: Thu, 4 Jun 2026 07:13:33 -0500 Subject: [PATCH 41/51] chore: determinate candidates selection through lowest connection per peer --- protocols/relay/src/autorelay.rs | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/protocols/relay/src/autorelay.rs b/protocols/relay/src/autorelay.rs index 422daa2ccc1..73df4a08dda 100644 --- a/protocols/relay/src/autorelay.rs +++ b/protocols/relay/src/autorelay.rs @@ -1,5 +1,5 @@ use std::{ - collections::{HashMap, HashSet, VecDeque}, + collections::{BTreeMap, HashMap, HashSet, VecDeque}, num::NonZeroU8, task::{Context, Poll, Waker}, time::{Duration, Instant}, @@ -518,8 +518,8 @@ impl Behaviour { return; } - let mut static_candidates = Vec::new(); - let mut candidates = HashMap::new(); + let mut static_candidates = BTreeMap::new(); + let mut candidates: BTreeMap<_, ConnectionId> = BTreeMap::new(); for ((peer_id, connection_id), info) in self.connections.iter() { if covered.contains(peer_id) { continue; @@ -531,13 +531,15 @@ impl Behaviour { { continue; } - if self.static_relays.contains_key(peer_id) { - if !static_candidates.iter().any(|(p, _)| p == peer_id) { - static_candidates.push((*peer_id, *connection_id)); - } + let bucket = if self.static_relays.contains_key(peer_id) { + &mut static_candidates } else { - candidates.entry(*peer_id).or_insert(*connection_id); - } + &mut candidates + }; + bucket + .entry(*peer_id) + .and_modify(|existing| *existing = (*existing).min(*connection_id)) + .or_insert(*connection_id); } let selected_candidates: Vec<(PeerId, ConnectionId)> = static_candidates From 2252585b4427728c3f82ba723d45732bfe65b5d7 Mon Sep 17 00:00:00 2001 From: Darius Clark Date: Fri, 5 Jun 2026 09:34:39 -0500 Subject: [PATCH 42/51] chore: use web_time::Instant --- protocols/relay/src/autorelay.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/protocols/relay/src/autorelay.rs b/protocols/relay/src/autorelay.rs index 73df4a08dda..ea74b8e5516 100644 --- a/protocols/relay/src/autorelay.rs +++ b/protocols/relay/src/autorelay.rs @@ -2,9 +2,11 @@ use std::{ collections::{BTreeMap, HashMap, HashSet, VecDeque}, num::NonZeroU8, task::{Context, Poll, Waker}, - time::{Duration, Instant}, + time::Duration, }; +use web_time::Instant; + use either::Either; use libp2p_core::{ Endpoint, From bbc841277e1cb7c121a74e986a77431919c838e0 Mon Sep 17 00:00:00 2001 From: Darius Clark Date: Fri, 5 Jun 2026 09:40:01 -0500 Subject: [PATCH 43/51] chore: use SystemTime for previous relays --- protocols/relay/src/autorelay.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/protocols/relay/src/autorelay.rs b/protocols/relay/src/autorelay.rs index ea74b8e5516..14dfeb6fa6f 100644 --- a/protocols/relay/src/autorelay.rs +++ b/protocols/relay/src/autorelay.rs @@ -5,7 +5,7 @@ use std::{ time::Duration, }; -use web_time::Instant; +use web_time::{Instant, SystemTime}; use either::Either; use libp2p_core::{ @@ -49,7 +49,7 @@ pub struct Behaviour { failure_counts: HashMap, - previous_relays: VecDeque<(PeerId, Multiaddr, Instant)>, + previous_relays: VecDeque<(PeerId, Multiaddr, SystemTime)>, relays_available: bool, @@ -263,7 +263,7 @@ impl Behaviour { .map(|(peer, addrs)| (peer, addrs.as_slice())) } - pub fn previous_relays(&self) -> impl Iterator { + pub fn previous_relays(&self) -> impl Iterator { self.previous_relays .iter() .map(|(peer, addr, ts)| (peer, addr, ts)) @@ -297,7 +297,7 @@ impl Behaviour { self.previous_relays.pop_front(); } self.previous_relays - .push_back((peer_id, address, Instant::now())); + .push_back((peer_id, address, SystemTime::now())); } fn forget_previous_relay(&mut self, peer_id: &PeerId) { From b843545a3f8c2440f9f398afacb71eccf639c70c Mon Sep 17 00:00:00 2001 From: Darius Clark Date: Mon, 8 Jun 2026 21:41:13 -0500 Subject: [PATCH 44/51] chore: fmt --- protocols/relay/src/autorelay.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/protocols/relay/src/autorelay.rs b/protocols/relay/src/autorelay.rs index 14dfeb6fa6f..c39f0a7a783 100644 --- a/protocols/relay/src/autorelay.rs +++ b/protocols/relay/src/autorelay.rs @@ -5,8 +5,6 @@ use std::{ time::Duration, }; -use web_time::{Instant, SystemTime}; - use either::Either; use libp2p_core::{ Endpoint, @@ -24,6 +22,7 @@ use libp2p_swarm::{ dial_opts::DialOpts, dummy, }; +use web_time::{Instant, SystemTime}; use crate::{autorelay::handler::Out, multiaddr_ext::MultiaddrExt}; From 6884c65cf642c9f6dda2e0dc891fa27cb787d966 Mon Sep 17 00:00:00 2001 From: Darius Clark Date: Mon, 15 Jun 2026 15:18:30 -0400 Subject: [PATCH 45/51] chore: remove allow(deprecated) --- protocols/relay/src/autorelay/handler.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/protocols/relay/src/autorelay/handler.rs b/protocols/relay/src/autorelay/handler.rs index a9cd91db041..b5778bdd551 100644 --- a/protocols/relay/src/autorelay/handler.rs +++ b/protocols/relay/src/autorelay/handler.rs @@ -43,7 +43,6 @@ pub enum Out { BlacklistExpired, } -#[allow(deprecated)] impl ConnectionHandler for Handler { type FromBehaviour = In; type ToBehaviour = Out; From b778f2ec024d405b817ce442477e199276798136 Mon Sep 17 00:00:00 2001 From: Darius Clark Date: Mon, 15 Jun 2026 19:29:22 -0400 Subject: [PATCH 46/51] chore: perform reservation cool down against peer instead of connection --- protocols/relay/src/autorelay.rs | 110 +++++++++++++++-------- protocols/relay/src/autorelay/handler.rs | 34 +------ 2 files changed, 76 insertions(+), 68 deletions(-) diff --git a/protocols/relay/src/autorelay.rs b/protocols/relay/src/autorelay.rs index c39f0a7a783..3aafe84a15a 100644 --- a/protocols/relay/src/autorelay.rs +++ b/protocols/relay/src/autorelay.rs @@ -6,6 +6,8 @@ use std::{ }; use either::Either; +use futures::FutureExt; +use futures_timer::Delay; use libp2p_core::{ Endpoint, multiaddr::Protocol, @@ -13,7 +15,7 @@ use libp2p_core::{ }; use libp2p_identity::PeerId; use libp2p_swarm::{ - ExternalAddresses, ListenOpts, NewListenAddr, NotifyHandler, + ExternalAddresses, ListenOpts, NewListenAddr, derive_prelude::{ AddressChange, ConnectionClosed, ConnectionDenied, ConnectionEstablished, ConnectionId, DialFailure, ExpiredListenAddr, FromSwarm, ListenerClosed, ListenerError, Multiaddr, @@ -48,10 +50,14 @@ pub struct Behaviour { failure_counts: HashMap, + reservation_cooldowns: HashMap, + previous_relays: VecDeque<(PeerId, Multiaddr, SystemTime)>, relays_available: bool, + cooldown_wakeup: Option<(Instant, Delay)>, + waker: Option, } @@ -69,8 +75,10 @@ impl Default for Behaviour { static_relays: HashMap::new(), static_dial_cooldowns: HashMap::new(), failure_counts: HashMap::new(), + reservation_cooldowns: HashMap::new(), previous_relays: VecDeque::new(), relays_available: false, + cooldown_wakeup: None, waker: None, } } @@ -110,7 +118,6 @@ enum ReservationStatus { Idle, Pending { id: ListenerId }, Active { id: ListenerId }, - Blacklisted, } #[derive(Debug)] @@ -316,6 +323,47 @@ impl Behaviour { fn clear_failure(&mut self, peer_id: &PeerId) { self.failure_counts.remove(peer_id); + self.reservation_cooldowns.remove(peer_id); + } + + fn reservation_in_cooldown(&self, peer_id: &PeerId) -> bool { + self.reservation_cooldowns + .get(peer_id) + .is_some_and(|deadline| *deadline > Instant::now()) + } + + fn poll_reservation_cooldowns(&mut self, cx: &mut Context<'_>) -> bool { + let now = Instant::now(); + + if self + .reservation_cooldowns + .values() + .any(|deadline| *deadline <= now) + { + self.reservation_cooldowns + .retain(|_, deadline| *deadline > now); + self.cooldown_wakeup = None; + self.meet_reservation_target(); + return true; + } + + match self.reservation_cooldowns.values().copied().min() { + Some(deadline) => { + if self.cooldown_wakeup.as_ref().map(|(at, _)| *at) != Some(deadline) { + let delay = Delay::new(deadline.saturating_duration_since(now)); + self.cooldown_wakeup = Some((deadline, delay)); + } + } + None => { + self.cooldown_wakeup = None; + return false; + } + } + + match self.cooldown_wakeup.as_mut() { + Some((_, timer)) => timer.poll_unpin(cx).is_ready(), + None => false, + } } fn determine_status_from_external_addresses(&mut self) { @@ -470,28 +518,19 @@ impl Behaviour { return; }; - let blacklist_duration = failed.then(|| self.record_failure(peer_id)); + let cooldown_duration = failed.then(|| self.record_failure(peer_id)); let connection = self .connections .get_mut(&(peer_id, connection_id)) .expect("connection is tracked"); - match blacklist_duration { - Some(duration) => { - connection.relay_status = RelayStatus::Supported { - status: ReservationStatus::Blacklisted, - }; - self.events.push_back(ToSwarm::NotifyHandler { - peer_id, - handler: NotifyHandler::One(connection_id), - event: Either::Left(handler::In::Blacklist { duration }), - }); - } - None => { - connection.relay_status = RelayStatus::Supported { - status: ReservationStatus::Idle, - }; - } + connection.relay_status = RelayStatus::Supported { + status: ReservationStatus::Idle, + }; + + if let Some(duration) = cooldown_duration { + self.reservation_cooldowns + .insert(peer_id, Instant::now() + duration); } self.record_previous_relay(peer_id, address); @@ -525,6 +564,9 @@ impl Behaviour { if covered.contains(peer_id) { continue; } + if self.reservation_in_cooldown(peer_id) { + continue; + } if info.relay_status != (RelayStatus::Supported { status: ReservationStatus::Idle, @@ -659,7 +701,6 @@ impl NetworkBehaviour for Behaviour { RelayStatus::Supported { status: ReservationStatus::Active { .. } | ReservationStatus::Pending { .. } - | ReservationStatus::Blacklisted } ); @@ -800,19 +841,6 @@ impl NetworkBehaviour for Behaviour { } self.update_relay_availability(); } - Out::BlacklistExpired => { - if matches!( - connection.relay_status, - RelayStatus::Supported { - status: ReservationStatus::Blacklisted - } - ) { - connection.relay_status = RelayStatus::Supported { - status: ReservationStatus::Idle, - }; - self.meet_reservation_target(); - } - } } } @@ -820,12 +848,18 @@ impl NetworkBehaviour for Behaviour { &mut self, cx: &mut Context<'_>, ) -> Poll>> { - if let Some(event) = self.events.pop_front() { - return Poll::Ready(event); - } + loop { + if let Some(event) = self.events.pop_front() { + return Poll::Ready(event); + } + + if self.poll_reservation_cooldowns(cx) { + continue; + } - self.waker = Some(cx.waker().clone()); + self.waker = Some(cx.waker().clone()); - Poll::Pending + return Poll::Pending; + } } } diff --git a/protocols/relay/src/autorelay/handler.rs b/protocols/relay/src/autorelay/handler.rs index b5778bdd551..e1ef47dac3a 100644 --- a/protocols/relay/src/autorelay/handler.rs +++ b/protocols/relay/src/autorelay/handler.rs @@ -1,11 +1,9 @@ use std::{ collections::VecDeque, + convert::Infallible, task::{Context, Poll}, - time::Duration, }; -use futures::FutureExt; -use futures_timer::Delay; use libp2p_core::upgrade::DeniedUpgrade; use libp2p_swarm::{ ConnectionHandler, ConnectionHandlerEvent, SubstreamProtocol, SupportedProtocols, @@ -27,24 +25,16 @@ pub struct Handler { supported: bool, supported_protocol: SupportedProtocols, - - blacklist_timer: Option, -} - -#[derive(Debug, Copy, Clone)] -pub enum In { - Blacklist { duration: Duration }, } #[derive(Debug, Copy, Clone)] pub enum Out { Supported, Unsupported, - BlacklistExpired, } impl ConnectionHandler for Handler { - type FromBehaviour = In; + type FromBehaviour = Infallible; type ToBehaviour = Out; type InboundProtocol = DeniedUpgrade; type OutboundProtocol = DeniedUpgrade; @@ -60,11 +50,7 @@ impl ConnectionHandler for Handler { } fn on_behaviour_event(&mut self, event: Self::FromBehaviour) { - match event { - In::Blacklist { duration } => { - self.blacklist_timer = Some(Delay::new(duration)); - } - } + match event {} } fn on_connection_event( @@ -92,7 +78,6 @@ impl ConnectionHandler for Handler { } (false, true) => { self.supported = false; - self.blacklist_timer = None; self.events .push_back(ConnectionHandlerEvent::NotifyBehaviour(Out::Unsupported)); } @@ -105,7 +90,7 @@ impl ConnectionHandler for Handler { fn poll( &mut self, - cx: &mut Context<'_>, + _cx: &mut Context<'_>, ) -> Poll< ConnectionHandlerEvent, > { @@ -113,17 +98,6 @@ impl ConnectionHandler for Handler { return Poll::Ready(event); } - if let Some(timer) = self.blacklist_timer.as_mut() - && timer.poll_unpin(cx).is_ready() - { - self.blacklist_timer = None; - if self.supported { - return Poll::Ready(ConnectionHandlerEvent::NotifyBehaviour( - Out::BlacklistExpired, - )); - } - } - Poll::Pending } } From 5b7509c6fa72eb772d338a1b6f11e7204c927f4e Mon Sep 17 00:00:00 2001 From: Darius Clark Date: Fri, 19 Jun 2026 09:53:59 -0400 Subject: [PATCH 47/51] chore: add logging --- protocols/relay/src/autorelay.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/protocols/relay/src/autorelay.rs b/protocols/relay/src/autorelay.rs index 3aafe84a15a..8e048529bae 100644 --- a/protocols/relay/src/autorelay.rs +++ b/protocols/relay/src/autorelay.rs @@ -408,19 +408,22 @@ impl Behaviour { fn evict_for_static_peer(&mut self, new_static: PeerId) { let covered = self.covered_peers(); if covered.contains(&new_static) { + tracing::debug!(%new_static, "peer is already covered by a reservation"); return; } let max = self.config.max_reservations.get() as usize; if covered.len() < max { + tracing::debug!(%new_static, "free reservation slot available. no eviction needed"); return; } - if let Some(listener_id) = self + if let Some((peer_id, listener_id)) = self .reservations .iter() .find(|(_, (peer_id, _))| !self.static_relays.contains_key(peer_id)) - .map(|(listener_id, _)| *listener_id) + .map(|(listener_id, (peer_id, _))| (peer_id, *listener_id)) { + tracing::debug!(%peer_id, %listener_id, "evicting peer to for static relay"); self.events .push_back(ToSwarm::RemoveListener { id: listener_id }); } From cbcfe37b1fc9d46a77bd50a4150b0a4e84d642aa Mon Sep 17 00:00:00 2001 From: Darius Clark Date: Fri, 19 Jun 2026 10:11:03 -0400 Subject: [PATCH 48/51] chore: switch relay_peer_id from trait member to fn --- protocols/relay/src/autorelay.rs | 3 ++- protocols/relay/src/multiaddr_ext.rs | 21 ++++++++++----------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/protocols/relay/src/autorelay.rs b/protocols/relay/src/autorelay.rs index 8e048529bae..ec47a68f00c 100644 --- a/protocols/relay/src/autorelay.rs +++ b/protocols/relay/src/autorelay.rs @@ -27,6 +27,7 @@ use libp2p_swarm::{ use web_time::{Instant, SystemTime}; use crate::{autorelay::handler::Out, multiaddr_ext::MultiaddrExt}; +use crate::multiaddr_ext::relay_peer_id; mod handler; @@ -766,7 +767,7 @@ impl NetworkBehaviour for Behaviour { return; } - if let Some(relay_peer_id) = addr.relay_peer_id() { + if let Some(relay_peer_id) = relay_peer_id(&addr) { self.external_reservations .insert(listener_id, relay_peer_id); } diff --git a/protocols/relay/src/multiaddr_ext.rs b/protocols/relay/src/multiaddr_ext.rs index 365143e5dcb..851fc8c4d65 100644 --- a/protocols/relay/src/multiaddr_ext.rs +++ b/protocols/relay/src/multiaddr_ext.rs @@ -3,23 +3,22 @@ use libp2p_identity::PeerId; pub(crate) trait MultiaddrExt { fn is_relayed(&self) -> bool; - fn relay_peer_id(&self) -> Option; } impl MultiaddrExt for Multiaddr { fn is_relayed(&self) -> bool { self.iter().any(|p| p == Protocol::P2pCircuit) } +} - fn relay_peer_id(&self) -> Option { - let mut last_p2p = None; - for proto in self.iter() { - match proto { - Protocol::P2p(peer) => last_p2p = Some(peer), - Protocol::P2pCircuit => return last_p2p, - _ => {} - } +pub(crate) fn relay_peer_id(addr: &Multiaddr) -> Option { + let mut last_p2p = None; + for proto in addr.iter() { + match proto { + Protocol::P2p(peer) => last_p2p = Some(peer), + Protocol::P2pCircuit => return last_p2p, + _ => {} } - None } -} + None +} \ No newline at end of file From 0c1a579fe22f8581f2ebf18f9f83cd520db03a45 Mon Sep 17 00:00:00 2001 From: Darius Clark Date: Fri, 19 Jun 2026 10:19:42 -0400 Subject: [PATCH 49/51] chore: fmt --- protocols/relay/src/autorelay.rs | 2 +- protocols/relay/src/multiaddr_ext.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/protocols/relay/src/autorelay.rs b/protocols/relay/src/autorelay.rs index ec47a68f00c..87966ff5035 100644 --- a/protocols/relay/src/autorelay.rs +++ b/protocols/relay/src/autorelay.rs @@ -26,8 +26,8 @@ use libp2p_swarm::{ }; use web_time::{Instant, SystemTime}; -use crate::{autorelay::handler::Out, multiaddr_ext::MultiaddrExt}; use crate::multiaddr_ext::relay_peer_id; +use crate::{autorelay::handler::Out, multiaddr_ext::MultiaddrExt}; mod handler; diff --git a/protocols/relay/src/multiaddr_ext.rs b/protocols/relay/src/multiaddr_ext.rs index 851fc8c4d65..d1f66cb032c 100644 --- a/protocols/relay/src/multiaddr_ext.rs +++ b/protocols/relay/src/multiaddr_ext.rs @@ -21,4 +21,4 @@ pub(crate) fn relay_peer_id(addr: &Multiaddr) -> Option { } } None -} \ No newline at end of file +} From ca2817da9e2f45a213a00582dde843b5124bbe71 Mon Sep 17 00:00:00 2001 From: Darius Clark Date: Fri, 19 Jun 2026 10:35:19 -0400 Subject: [PATCH 50/51] chore: fmt --- protocols/relay/src/autorelay.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/protocols/relay/src/autorelay.rs b/protocols/relay/src/autorelay.rs index 87966ff5035..eeb0e9b59f7 100644 --- a/protocols/relay/src/autorelay.rs +++ b/protocols/relay/src/autorelay.rs @@ -26,8 +26,10 @@ use libp2p_swarm::{ }; use web_time::{Instant, SystemTime}; -use crate::multiaddr_ext::relay_peer_id; -use crate::{autorelay::handler::Out, multiaddr_ext::MultiaddrExt}; +use crate::{ + autorelay::handler::Out, + multiaddr_ext::{MultiaddrExt, relay_peer_id}, +}; mod handler; From afdc276d1c398717f9594cd46802decc54ab45c3 Mon Sep 17 00:00:00 2001 From: Darius Clark Date: Fri, 19 Jun 2026 10:40:53 -0400 Subject: [PATCH 51/51] chore: linting --- protocols/relay/src/autorelay.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/protocols/relay/src/autorelay.rs b/protocols/relay/src/autorelay.rs index eeb0e9b59f7..402efce559a 100644 --- a/protocols/relay/src/autorelay.rs +++ b/protocols/relay/src/autorelay.rs @@ -769,7 +769,7 @@ impl NetworkBehaviour for Behaviour { return; } - if let Some(relay_peer_id) = relay_peer_id(&addr) { + if let Some(relay_peer_id) = relay_peer_id(addr) { self.external_reservations .insert(listener_id, relay_peer_id); }