From 3de19b529c374ea378f7589eef787736f2e6928d Mon Sep 17 00:00:00 2001 From: Darius Date: Mon, 25 Aug 2025 16:03:32 -0500 Subject: [PATCH 01/77] feat: implement autorelay --- src/behaviour.rs | 15 + src/behaviour/autorelay.rs | 421 +++++++++++++++++++++++++++++ src/behaviour/autorelay/handler.rs | 106 ++++++++ src/builder.rs | 25 ++ src/lib.rs | 1 + src/multiaddr_ext.rs | 12 + 6 files changed, 580 insertions(+) create mode 100644 src/behaviour/autorelay.rs create mode 100644 src/behaviour/autorelay/handler.rs create mode 100644 src/multiaddr_ext.rs diff --git a/src/behaviour.rs b/src/behaviour.rs index 18bdecf..74c24f4 100644 --- a/src/behaviour.rs +++ b/src/behaviour.rs @@ -1,3 +1,4 @@ +pub mod autorelay; pub mod dummy; pub mod peer_store; #[cfg(feature = "request-response")] @@ -55,6 +56,9 @@ where #[cfg(feature = "relay")] pub relay_client: Toggle, + #[cfg(feature = "relay")] + pub autorelay: Toggle, + #[cfg(not(target_arch = "wasm32"))] #[cfg(feature = "upnp")] pub upnp: Toggle, @@ -268,6 +272,15 @@ where } false => (None, None.into()), }; + #[cfg(feature = "relay")] + let autorelay = protocols + .autorelay + .then(|| { + let config_fn = config.autorelay_config; + let config = config_fn(autorelay::Config::default()); + autorelay::Behaviour::new_with_config(config) + }) + .into(); #[cfg(not(feature = "relay"))] let transport = None::<()>; @@ -359,6 +372,8 @@ where relay, #[cfg(feature = "relay")] relay_client, + #[cfg(feature = "relay")] + autorelay, #[cfg(feature = "stream")] stream, #[cfg(not(target_arch = "wasm32"))] diff --git a/src/behaviour/autorelay.rs b/src/behaviour/autorelay.rs new file mode 100644 index 0000000..bc10a49 --- /dev/null +++ b/src/behaviour/autorelay.rs @@ -0,0 +1,421 @@ +mod handler; + +use crate::behaviour::autorelay::handler::Out; +use crate::behaviour::dummy; +use crate::multiaddr_ext::MultiaddrExt; +use crate::prelude::swarm::derive_prelude::{ConnectionEstablished, PortUse}; +use crate::prelude::swarm::{ + AddressChange, CloseConnection, ConnectionClosed, ConnectionDenied, ExpiredListenAddr, + FromSwarm, ListenerClosed, ListenerError, THandler, THandlerInEvent, THandlerOutEvent, ToSwarm, +}; +use crate::prelude::transport::Endpoint; +use either::Either; +use indexmap::{IndexMap, IndexSet}; +use libp2p::core::transport::ListenerId; +use libp2p::multiaddr::Protocol; +use libp2p::swarm::{ConnectionId, ListenOpts, NetworkBehaviour, NewListenAddr}; +use libp2p::{Multiaddr, PeerId}; +use rand::prelude::IteratorRandom; +use std::collections::VecDeque; +use std::task::{Context, Poll, Waker}; + +#[derive(Default)] +pub struct Behaviour { + config: Config, + info: IndexMap>, + listener_to_info: IndexMap, + _static_relays: IndexSet, + events: VecDeque::ToSwarm, THandlerInEvent>>, + pending_target: IndexSet, + waker: Option, +} + +#[derive(Debug, Clone)] +#[non_exhaustive] +pub struct Config { + pub max_reservation: Option, + pub auto_reservation: bool, +} + +impl Default for Config { + fn default() -> Self { + Self { + max_reservation: None, + auto_reservation: true, + } + } +} + +#[derive(Debug)] +struct PeerInfo { + address: Multiaddr, + relay_supported: bool, + reservation_status: ReservationStatus, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum ReservationStatus { + Pending { id: ListenerId }, + Active { id: ListenerId }, + None, +} + +// impl ReservationStatus { +// pub fn disabled(self) -> bool { +// self == ReservationStatus::None +// } +// } + +impl Behaviour { + pub fn new_with_config(config: Config) -> Self { + Self { + config, + ..Default::default() + } + } + + // pub fn add_relay(&mut self, address: Multiaddr) -> bool { + // self.static_relays.insert(address) + // } + // + // pub fn remove_relay(&mut self, address: Multiaddr) -> bool { + // self.static_relays.shift_remove(&address) + // } + + pub fn enable_autorelay(&mut self) { + self.config.auto_reservation = true; + if let Some(waker) = self.waker.take() { + waker.wake(); + } + } + + pub fn disable_autorelay(&mut self) { + self.config.auto_reservation = false; + if let Some(waker) = self.waker.take() { + waker.wake(); + } + } + + pub fn get_all_supported_targets(&self) -> impl Iterator { + self.info + .iter() + .filter(|(_, infos)| infos.iter().any(|(_, info)| info.relay_supported)) + .map(|(peer_id, _)| peer_id) + } + + pub fn get_supported_targets(&self) -> impl Iterator { + self.info + .iter() + .filter(|(_, infos)| { + infos.iter().any(|(_, info)| { + info.relay_supported && info.reservation_status == ReservationStatus::None + }) + }) + .map(|(peer_id, _)| peer_id) + } + + fn disable_reservation(&mut self, id: ListenerId) { + let Some((peer_id, connection_id)) = self.listener_to_info.shift_remove(&id) else { + return; + }; + + let Some(connections) = self.info.get_mut(&peer_id) else { + return; + }; + + let Some(info) = connections.get_mut(&connection_id) else { + return; + }; + + match info.reservation_status { + ReservationStatus::Active { .. } => { + // TODO: Determine if we should disconnect then reconnect? + } + ReservationStatus::Pending { .. } => { + self.pending_target.shift_remove(&peer_id); + } + ReservationStatus::None => { + // FIXME: Unreachable? + } + } + + info.reservation_status = ReservationStatus::None; + } + + #[allow(clippy::manual_saturating_arithmetic)] + fn meet_reservation_target(&mut self) { + if !self.config.auto_reservation { + return; + } + + let max = self.config.max_reservation.unwrap_or(2) as usize; + + // if max target reservation is 0, this would be no different from disabling auto reservation + // to prevent a DoS due to acquiring multiple reservations from an unbound number of relays + if max == 0 { + return; + } + + let relayed_targets = self + .info + .iter() + .filter(|(_, info)| { + info.iter().any(|(_, info)| { + info.relay_supported + && matches!(info.reservation_status, ReservationStatus::Active { .. }) + }) + }) + .count(); + + if relayed_targets == max as usize { + return; + } + + let targets = self.get_supported_targets().copied().collect::>(); + + let pending_target_len = self.pending_target.len(); + + if pending_target_len >= max { + return; + } + + debug_assert!(pending_target_len < max); + + let targets_count = targets.len(); + + if targets_count == 0 { + return; + } + + let mut rng = rand::thread_rng(); + + let remaining_targets_needed = targets_count + .checked_sub(self.pending_target.len()) + .unwrap_or_default(); + + if remaining_targets_needed == 0 { + return; + } + + let targets = targets + .into_iter() + .choose_multiple(&mut rng, remaining_targets_needed); + + for peer_id in targets { + let connections = self.info.get_mut(&peer_id).expect("peer entry is valud"); + + let (connection_id, info) = connections + .iter_mut() + .choose(&mut rng) + .expect("connection is present"); + + assert_eq!(info.reservation_status, ReservationStatus::None); + + 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; + } + }; + + let relay_addr = addr_with_peer_id.with(Protocol::P2pCircuit); + + let opts = ListenOpts::new(relay_addr); + + let id = opts.listener_id(); + + info.reservation_status = ReservationStatus::Pending { id }; + self.listener_to_info.insert(id, (peer_id, *connection_id)); + self.events.push_back(ToSwarm::ListenOn { opts }); + self.pending_target.insert(peer_id); + if self.pending_target.len() == max { + break; + } + } + + assert!(self.pending_target.len() <= max); + } +} + +impl NetworkBehaviour for Behaviour { + type ConnectionHandler = Either; + type ToSwarm = (); + + 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::DummyHandler)) + } 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::DummyHandler)) + } else { + Ok(Either::Left(handler::Handler::default())) + } + } + + fn on_swarm_event(&mut self, event: FromSwarm) { + match event { + FromSwarm::ConnectionEstablished(ConnectionEstablished { + peer_id, + connection_id, + endpoint, + .. + }) => { + let connections = self.info.entry(peer_id).or_default(); + let addr = endpoint.get_remote_address().clone(); + let info = PeerInfo { + address: addr, + relay_supported: false, + reservation_status: ReservationStatus::None, + }; + connections.insert(connection_id, info); + } + FromSwarm::ConnectionClosed(ConnectionClosed { + peer_id, + connection_id, + .. + }) => { + let Some(connections) = self.info.get_mut(&peer_id) else { + return; + }; + + let _info = connections + .shift_remove(&connection_id) + .expect("connection was present"); + + // if matches!(info.reservation_status, ReservationStatus::Pending { .. }) { + // self.pending_target = self.pending_target.checked_sub(1).unwrap_or_default(); + // } + + if connections.is_empty() { + self.info.shift_remove(&peer_id); + } + } + FromSwarm::AddressChange(AddressChange { + peer_id, + connection_id, + old, + new, + }) => { + let old_addr = old.get_remote_address(); + let new_addr = new.get_remote_address(); + + debug_assert!(old_addr != new_addr); + + let Some(connections) = self.info.get_mut(&peer_id) else { + return; + }; + + let Some(info) = connections.get_mut(&connection_id) else { + return; + }; + + info.address = new_addr.clone(); + } + FromSwarm::NewListenAddr(NewListenAddr { listener_id, addr }) => { + let Some((peer_id, connection_id)) = self.listener_to_info.get(&listener_id) else { + return; + }; + + if !addr.iter().any(|protocol| protocol == Protocol::P2pCircuit) { + return; + } + + let Some(connections) = self.info.get_mut(peer_id) else { + return; + }; + + let Some(info) = connections.get_mut(connection_id) else { + return; + }; + + let ReservationStatus::Pending { id } = info.reservation_status else { + return; + }; + + info.reservation_status = ReservationStatus::Active { id }; + + debug_assert!(self.pending_target.shift_remove(peer_id)); + } + FromSwarm::ExpiredListenAddr(ExpiredListenAddr { listener_id, .. }) + | FromSwarm::ListenerError(ListenerError { listener_id, .. }) + | FromSwarm::ListenerClosed(ListenerClosed { listener_id, .. }) => { + self.disable_reservation(listener_id) + } + _ => {} + } + } + + fn on_connection_handler_event( + &mut self, + peer_id: PeerId, + connection_id: ConnectionId, + event: THandlerOutEvent, + ) { + let Either::Left(event) = event; + + let Some(connections) = self.info.get_mut(&peer_id) else { + return; + }; + + let Some(peer_info) = connections.get_mut(&connection_id) else { + return; + }; + + match event { + Out::Supported => { + peer_info.relay_supported = true; + self.meet_reservation_target(); + } + Out::Unsupported => { + peer_info.relay_supported = false; + // if there is a change in protocol support during an active reservation, + // we should disconnect to remove the reservation + if peer_info.reservation_status != ReservationStatus::None { + self.events.push_back(ToSwarm::CloseConnection { + peer_id, + connection: CloseConnection::One(connection_id), + }); + + // TODO: Determine if we should reconnect if this is the only connection + // if connections.len() == 1 { + // let addr = peer_info.address.clone(); + // let opts = DialOpts::peer_id(peer_id).addresses(vec![addr]).build(); + // self.events.push_back(ToSwarm::Dial { opts }); + // } + } + } + } + } + + 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/src/behaviour/autorelay/handler.rs b/src/behaviour/autorelay/handler.rs new file mode 100644 index 0000000..3c1f7f2 --- /dev/null +++ b/src/behaviour/autorelay/handler.rs @@ -0,0 +1,106 @@ +use std::{ + collections::VecDeque, + task::{Context, Poll}, +}; + +use libp2p::{ + core::upgrade::DeniedUpgrade, + swarm::{ + ConnectionHandler, ConnectionHandlerEvent, SubstreamProtocol, SupportedProtocols, + handler::ConnectionEvent, + }, +}; + +#[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| libp2p::relay::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/src/builder.rs b/src/builder.rs index 8cb1b68..6ba54a7 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -96,6 +96,9 @@ pub(crate) struct Config { pub autonat_v2_client_config: Box AutonatV2ClientConfig>, #[cfg(feature = "relay")] pub relay_server_config: Box RelayServerConfig>, + #[cfg(feature = "relay")] + pub autorelay_config: + Box behaviour::autorelay::Config>, #[cfg(feature = "identify")] pub identify_config: (String, Box IdentifyConfig>), #[cfg(feature = "request-response")] @@ -128,6 +131,8 @@ impl Default for Config { autonat_v2_client_config: Box::new(|config| config), #[cfg(feature = "relay")] relay_server_config: Box::new(|config| config), + #[cfg(feature = "relay")] + autorelay_config: Box::new(|config| config), #[cfg(feature = "identify")] identify_config: (String::from("/ipfs/id"), Box::new(|config| config)), #[cfg(feature = "request-response")] @@ -152,6 +157,8 @@ pub(crate) struct Protocols { pub(crate) relay_client: bool, #[cfg(feature = "relay")] pub(crate) relay_server: bool, + #[cfg(feature = "relay")] + pub(crate) autorelay: bool, #[cfg(feature = "dcutr")] #[cfg(not(target_arch = "wasm32"))] pub(crate) dcutr: bool, @@ -348,6 +355,24 @@ where self } + /// Enable autorelay + #[cfg(feature = "relay")] + pub fn with_autorelay(mut self) -> Self { + self.protocols.autorelay = true; + self + } + + /// Enable autorelay + #[cfg(feature = "relay")] + pub fn with_autorelay_with_config(mut self, f: F) -> Self + where + F: FnOnce(behaviour::autorelay::Config) -> behaviour::autorelay::Config + 'static, + { + self.config.autorelay_config = Box::new(f); + self.protocols.autorelay = true; + self + } + /// Enables DCuTR #[cfg(all(feature = "relay", feature = "dcutr"))] #[cfg(not(target_arch = "wasm32"))] diff --git a/src/lib.rs b/src/lib.rs index f4aa60f..dc55ad2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,6 +2,7 @@ pub mod behaviour; pub mod builder; pub mod error; pub mod handle; +mod multiaddr_ext; pub mod task; pub(crate) mod types; diff --git a/src/multiaddr_ext.rs b/src/multiaddr_ext.rs new file mode 100644 index 0000000..e9205bb --- /dev/null +++ b/src/multiaddr_ext.rs @@ -0,0 +1,12 @@ +use libp2p::Multiaddr; +use libp2p::multiaddr::Protocol; + +pub(crate) trait MultiaddrExt { + fn is_relayed(&self) -> bool; +} + +impl MultiaddrExt for Multiaddr { + fn is_relayed(&self) -> bool { + self.iter().any(|protocol| protocol == Protocol::P2pCircuit) + } +} From 98a709b7179c136ca4cd908e6ec1da9b671489f1 Mon Sep 17 00:00:00 2001 From: Darius Date: Tue, 26 Aug 2025 20:34:33 -0500 Subject: [PATCH 02/77] chore: use vec to track peerinfo --- src/behaviour/autorelay.rs | 217 ++++++++++++++++++++++++++----------- 1 file changed, 156 insertions(+), 61 deletions(-) diff --git a/src/behaviour/autorelay.rs b/src/behaviour/autorelay.rs index bc10a49..d02ffb5 100644 --- a/src/behaviour/autorelay.rs +++ b/src/behaviour/autorelay.rs @@ -22,9 +22,8 @@ use std::task::{Context, Poll, Waker}; #[derive(Default)] pub struct Behaviour { config: Config, - info: IndexMap>, + info: IndexMap>, listener_to_info: IndexMap, - _static_relays: IndexSet, events: VecDeque::ToSwarm, THandlerInEvent>>, pending_target: IndexSet, waker: Option, @@ -48,9 +47,11 @@ impl Default for Config { #[derive(Debug)] struct PeerInfo { + connection_id: Option, address: Multiaddr, relay_supported: bool, reservation_status: ReservationStatus, + static_info: bool, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -60,12 +61,6 @@ enum ReservationStatus { None, } -// impl ReservationStatus { -// pub fn disabled(self) -> bool { -// self == ReservationStatus::None -// } -// } - impl Behaviour { pub fn new_with_config(config: Config) -> Self { Self { @@ -74,16 +69,53 @@ impl Behaviour { } } - // pub fn add_relay(&mut self, address: Multiaddr) -> bool { - // self.static_relays.insert(address) - // } - // - // pub fn remove_relay(&mut self, address: Multiaddr) -> bool { - // self.static_relays.shift_remove(&address) - // } + pub fn add_static_relay(&mut self, peer_id: PeerId, address: Multiaddr) -> bool { + let infos = self.info.entry(peer_id).or_default(); + if let Some(info) = infos + .iter() + .position(|info| info.address == address) + .map(|index| &mut infos[index]) + { + if info.static_info { + return false; + } + info.static_info = true; + } else { + infos.push(PeerInfo { + connection_id: None, + address, + relay_supported: false, + reservation_status: ReservationStatus::None, + static_info: true, + }); + } + + true + } + + pub fn remove_static_relay(&mut self, peer_id: PeerId, address: Multiaddr) -> bool { + // Note that if there is an active reservation or a connection to the address, we will only set `static_info` to false, so it will be removed later on disconnection + // otherwise it will be removed + let Some(infos) = self.info.get_mut(&peer_id) else { + return false; + }; + + if let Some(index) = infos.iter_mut().position(|info| info.address == address) { + let info = &mut infos[index]; + if info.connection_id.is_some() { + info.static_info = false; + return true; + } + infos.remove(index); + return true; + } + + false + } pub fn enable_autorelay(&mut self) { self.config.auto_reservation = true; + self.meet_reservation_target(true); if let Some(waker) = self.waker.take() { waker.wake(); } @@ -99,7 +131,7 @@ impl Behaviour { pub fn get_all_supported_targets(&self) -> impl Iterator { self.info .iter() - .filter(|(_, infos)| infos.iter().any(|(_, info)| info.relay_supported)) + .filter(|(_, infos)| infos.iter().any(|info| info.relay_supported)) .map(|(peer_id, _)| peer_id) } @@ -107,7 +139,7 @@ impl Behaviour { self.info .iter() .filter(|(_, infos)| { - infos.iter().any(|(_, info)| { + infos.iter().any(|info| { info.relay_supported && info.reservation_status == ReservationStatus::None }) }) @@ -123,7 +155,10 @@ impl Behaviour { return; }; - let Some(info) = connections.get_mut(&connection_id) else { + let Some(info) = connections + .iter_mut() + .find(|info| info.connection_id.is_some_and(|id| id == connection_id)) + else { return; }; @@ -143,31 +178,34 @@ impl Behaviour { } #[allow(clippy::manual_saturating_arithmetic)] - fn meet_reservation_target(&mut self) { + fn meet_reservation_target(&mut self, auto: bool) { if !self.config.auto_reservation { return; } let max = self.config.max_reservation.unwrap_or(2) as usize; - // if max target reservation is 0, this would be no different from disabling auto reservation - // to prevent a DoS due to acquiring multiple reservations from an unbound number of relays - if max == 0 { + // if max target reservation is 0, this would be no different from disabling auto reservation, + // which would help prevent a possible DoS due to having multiple reservation acquired. + if max == 0 && auto { return; } + // TODO: check to determine if we have any active connections and if not, dial any static relays and let it be handled internally + // let have_connections = self.info.iter().any(|(_, infos)| infos.iter().any(|info| info.connection_id.is_some() && !info.static_info)); + let relayed_targets = self .info .iter() .filter(|(_, info)| { - info.iter().any(|(_, info)| { + info.iter().any(|info| { info.relay_supported && matches!(info.reservation_status, ReservationStatus::Active { .. }) }) }) .count(); - if relayed_targets == max as usize { + if relayed_targets == max { return; } @@ -204,8 +242,9 @@ impl Behaviour { for peer_id in targets { let connections = self.info.get_mut(&peer_id).expect("peer entry is valud"); - let (connection_id, info) = connections + let info = connections .iter_mut() + .filter(|info| info.connection_id.is_some()) .choose(&mut rng) .expect("connection is present"); @@ -226,7 +265,13 @@ impl Behaviour { let id = opts.listener_id(); info.reservation_status = ReservationStatus::Pending { id }; - self.listener_to_info.insert(id, (peer_id, *connection_id)); + self.listener_to_info.insert( + id, + ( + peer_id, + info.connection_id.expect("connection id is present"), + ), + ); self.events.push_back(ToSwarm::ListenOn { opts }); self.pending_target.insert(peer_id); if self.pending_target.len() == max { @@ -271,6 +316,27 @@ impl NetworkBehaviour for Behaviour { } } + fn handle_pending_outbound_connection( + &mut self, + _connection_id: ConnectionId, + maybe_peer: Option, + _addresses: &[Multiaddr], + _effective_role: Endpoint, + ) -> Result, ConnectionDenied> { + let Some(infos) = maybe_peer.and_then(|peer_id| self.info.get_mut(&peer_id)) else { + return Ok(vec![]); + }; + + // To prevent providing addresses from active connections, we will only focus on addresses added here that are considered to be fixed/static relays. + let addrs = infos + .iter() + .filter(|info| info.static_info) + .map(|info| info.address.clone()) + .collect::>(); + + Ok(addrs) + } + fn on_swarm_event(&mut self, event: FromSwarm) { match event { FromSwarm::ConnectionEstablished(ConnectionEstablished { @@ -279,33 +345,52 @@ impl NetworkBehaviour for Behaviour { endpoint, .. }) => { - let connections = self.info.entry(peer_id).or_default(); + let infos = self.info.entry(peer_id).or_default(); let addr = endpoint.get_remote_address().clone(); - let info = PeerInfo { - address: addr, - relay_supported: false, - reservation_status: ReservationStatus::None, - }; - connections.insert(connection_id, info); + // scan the infos to find the first entry without a connection id + + if let Some(index) = infos.iter().position(|info| info.connection_id.is_none()) { + let info = &mut infos[index]; + info.connection_id = Some(connection_id); + } else { + let info = PeerInfo { + connection_id: Some(connection_id), + address: addr, + relay_supported: false, + reservation_status: ReservationStatus::None, + static_info: false, + }; + infos.push(info); + } } FromSwarm::ConnectionClosed(ConnectionClosed { peer_id, connection_id, .. }) => { - let Some(connections) = self.info.get_mut(&peer_id) else { + let Some(infos) = self.info.get_mut(&peer_id) else { return; }; - let _info = connections - .shift_remove(&connection_id) - .expect("connection was present"); - - // if matches!(info.reservation_status, ReservationStatus::Pending { .. }) { - // self.pending_target = self.pending_target.checked_sub(1).unwrap_or_default(); - // } + infos + .iter() + .position(|info| { + info.connection_id.is_some_and(|id| id == connection_id) + && !info.static_info + }) + .map(|index| infos.remove(index)); + + // TODO: Determine if we should remove it here or leave it for the listener events to handle its removal + if let Some(listener_id) = self + .listener_to_info + .iter() + .find(|(_, (peer, conn_id))| peer_id.eq(peer) && connection_id.eq(conn_id)) + .map(|(id, _)| *id) + { + self.listener_to_info.shift_remove(&listener_id); + } - if connections.is_empty() { + if infos.is_empty() { self.info.shift_remove(&peer_id); } } @@ -320,33 +405,41 @@ impl NetworkBehaviour for Behaviour { debug_assert!(old_addr != new_addr); - let Some(connections) = self.info.get_mut(&peer_id) else { - return; - }; - - let Some(info) = connections.get_mut(&connection_id) else { - return; - }; + let info = self + .info + .get_mut(&peer_id) + .and_then(|infos| { + infos + .iter() + .position(|info| { + info.connection_id.is_some_and(|id| id == connection_id) + }) + .and_then(|index| infos.get_mut(index)) + }) + .expect("connection is present"); info.address = new_addr.clone(); } FromSwarm::NewListenAddr(NewListenAddr { listener_id, addr }) => { - let Some((peer_id, connection_id)) = self.listener_to_info.get(&listener_id) else { - return; - }; - + // we only care about any new relayed address if !addr.iter().any(|protocol| protocol == Protocol::P2pCircuit) { return; } - let Some(connections) = self.info.get_mut(peer_id) else { + let Some((peer_id, connection_id)) = self.listener_to_info.get(&listener_id) else { return; }; - let Some(info) = connections.get_mut(connection_id) else { + let Some(infos) = self.info.get_mut(peer_id) else { return; }; + let info = infos + .iter() + .position(|info| info.connection_id.is_some_and(|id| id == *connection_id)) + .and_then(|index| infos.get_mut(index)) + .expect("connection is present"); + let ReservationStatus::Pending { id } = info.reservation_status else { return; }; @@ -372,18 +465,20 @@ impl NetworkBehaviour for Behaviour { ) { let Either::Left(event) = event; - let Some(connections) = self.info.get_mut(&peer_id) else { + let Some(infos) = self.info.get_mut(&peer_id) else { return; }; - let Some(peer_info) = connections.get_mut(&connection_id) else { - return; - }; + let peer_info = infos + .iter() + .position(|info| info.connection_id.is_some_and(|id| id == connection_id)) + .and_then(|index| infos.get_mut(index)) + .expect("connection is present"); match event { Out::Supported => { peer_info.relay_supported = true; - self.meet_reservation_target(); + self.meet_reservation_target(true); } Out::Unsupported => { peer_info.relay_supported = false; @@ -396,7 +491,7 @@ impl NetworkBehaviour for Behaviour { }); // TODO: Determine if we should reconnect if this is the only connection - // if connections.len() == 1 { + // if infos.iter().filter(|info| info.connection_id.is_some()).count() == 1 { // let addr = peer_info.address.clone(); // let opts = DialOpts::peer_id(peer_id).addresses(vec![addr]).build(); // self.events.push_back(ToSwarm::Dial { opts }); From 9f71d45c0d5e54b002ae2c115bcbdb5fbeb8c7e9 Mon Sep 17 00:00:00 2001 From: Darius Date: Wed, 27 Aug 2025 20:51:04 -0500 Subject: [PATCH 03/77] chore: add a timer to cleanup capacity if it exceeds maximum amount (currently 100) --- src/behaviour/autorelay.rs | 42 +++++++++++++++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/src/behaviour/autorelay.rs b/src/behaviour/autorelay.rs index d02ffb5..7da3655 100644 --- a/src/behaviour/autorelay.rs +++ b/src/behaviour/autorelay.rs @@ -10,6 +10,8 @@ use crate::prelude::swarm::{ }; use crate::prelude::transport::Endpoint; use either::Either; +use futures::FutureExt; +use futures_timer::Delay; use indexmap::{IndexMap, IndexSet}; use libp2p::core::transport::ListenerId; use libp2p::multiaddr::Protocol; @@ -18,22 +20,40 @@ use libp2p::{Multiaddr, PeerId}; use rand::prelude::IteratorRandom; use std::collections::VecDeque; use std::task::{Context, Poll, Waker}; +use std::time::Duration; + +const MAX_CAP: usize = 100; -#[derive(Default)] pub struct Behaviour { config: Config, info: IndexMap>, listener_to_info: IndexMap, events: VecDeque::ToSwarm, THandlerInEvent>>, pending_target: IndexSet, + capacity_cleanup: Delay, waker: Option, } +impl Default for Behaviour { + fn default() -> Self { + Self { + config: Config::default(), + info: IndexMap::new(), + listener_to_info: IndexMap::new(), + events: VecDeque::new(), + pending_target: IndexSet::new(), + capacity_cleanup: Delay::new(Duration::from_secs(60)), + waker: None, + } + } +} + #[derive(Debug, Clone)] #[non_exhaustive] pub struct Config { pub max_reservation: Option, pub auto_reservation: bool, + pub capacity_cleanup_interval: Duration, } impl Default for Config { @@ -41,6 +61,10 @@ impl Default for Config { Self { max_reservation: None, auto_reservation: true, + capacity_cleanup_interval: Duration::from_secs(60), + } + } +} } } } @@ -509,6 +533,22 @@ impl NetworkBehaviour for Behaviour { return Poll::Ready(event); } + if self.capacity_cleanup.poll_unpin(cx).is_ready() { + if (self.events.is_empty() || self.events.len() < MAX_CAP) + && self.events.capacity() > MAX_CAP + { + self.events.shrink_to_fit(); + } + + if (self.info.is_empty() || self.info.len() < MAX_CAP) && self.info.capacity() > MAX_CAP + { + self.info.shrink_to_fit(); + } + + self.capacity_cleanup + .reset(self.config.capacity_cleanup_interval); + } + self.waker.replace(cx.waker().clone()); Poll::Pending From a9b31f061e2ce5907db550bc871426a4d4fa914f Mon Sep 17 00:00:00 2001 From: Darius Date: Thu, 28 Aug 2025 11:14:16 -0500 Subject: [PATCH 04/77] chore: add functionality for using autorelay --- src/handle.rs | 8 +++++ src/handle/relay.rs | 72 +++++++++++++++++++++++++++++++++++++++++++++ src/task.rs | 4 +++ src/task/relay.rs | 51 ++++++++++++++++++++++++++++++++ src/types.rs | 30 +++++++++++++++++++ 5 files changed, 165 insertions(+) create mode 100644 src/handle/relay.rs diff --git a/src/handle.rs b/src/handle.rs index 1d804a3..9abcda0 100644 --- a/src/handle.rs +++ b/src/handle.rs @@ -8,6 +8,7 @@ pub(crate) mod floodsub; #[cfg(feature = "gossipsub")] pub(crate) mod gossipsub; mod peer_store; +mod relay; #[cfg(feature = "rendezvous")] pub(crate) mod rendezvous; #[cfg(feature = "request-response")] @@ -27,6 +28,7 @@ use crate::handle::floodsub::ConnexaFloodsub; #[cfg(feature = "gossipsub")] use crate::handle::gossipsub::ConnexaGossipsub; use crate::handle::peer_store::ConnexaPeerstore; +use crate::handle::relay::ConnexaRelay; #[cfg(feature = "rendezvous")] use crate::handle::rendezvous::ConnexaRendezvous; #[cfg(feature = "request-response")] @@ -133,6 +135,12 @@ where ConnexaRendezvous::new(self) } + /// Returns a handle for relay functions + #[cfg(feature = "relay")] + pub fn relay(&self) -> ConnexaRelay<'_, T> { + ConnexaRelay::new(self) + } + /// Returns a handle to manage peer whitelist functionality pub fn whitelist(&self) -> ConnexaWhitelist<'_, T> { ConnexaWhitelist::new(self) diff --git a/src/handle/relay.rs b/src/handle/relay.rs new file mode 100644 index 0000000..3579ca8 --- /dev/null +++ b/src/handle/relay.rs @@ -0,0 +1,72 @@ +use crate::handle::Connexa; +use crate::prelude::{Multiaddr, PeerId}; +use crate::types::AutoRelayCommand; +use futures::channel::oneshot; +use std::io; + +pub struct ConnexaRelay<'a, T> { + connexa: &'a Connexa, +} + +impl<'a, T> ConnexaRelay<'a, T> +where + T: Send + Sync + 'static, +{ + pub(crate) fn new(connexa: &'a Connexa) -> Self { + Self { connexa } + } + + pub async fn add_static_relay(&self, peer_id: PeerId, addr: Multiaddr) -> io::Result { + let (tx, rx) = oneshot::channel(); + self.connexa + .to_task + .clone() + .send( + AutoRelayCommand::AddStaticRelay { + peer_id, + relay_addr: addr, + resp: tx, + } + .into(), + ) + .await?; + rx.await.map_err(io::Error::other)? + } + + pub async fn remove_static_relay(&self, peer_id: PeerId, addr: Multiaddr) -> io::Result { + let (tx, rx) = oneshot::channel(); + self.connexa + .to_task + .clone() + .send( + AutoRelayCommand::RemoveStaticRelay { + peer_id, + relay_addr: addr, + resp: tx, + } + .into(), + ) + .await?; + rx.await.map_err(io::Error::other)? + } + + pub async fn enable_auto_relay(&self) -> io::Result<()> { + let (tx, rx) = oneshot::channel(); + self.connexa + .to_task + .clone() + .send(AutoRelayCommand::EnableAutoRelay { resp: tx }.into()) + .await?; + rx.await.map_err(io::Error::other)? + } + + pub async fn disable_auto_relay(&self) -> io::Result<()> { + let (tx, rx) = oneshot::channel(); + self.connexa + .to_task + .clone() + .send(AutoRelayCommand::DisableAutoRelay { resp: tx }.into()) + .await?; + rx.await.map_err(io::Error::other)? + } +} diff --git a/src/task.rs b/src/task.rs index cf3e302..0f88d76 100644 --- a/src/task.rs +++ b/src/task.rs @@ -535,6 +535,10 @@ where Command::Autonat(autonat_command) => self.process_autonat_v1_command(autonat_command), #[cfg(feature = "kad")] Command::Dht(dht_command) => self.process_kademlia_command(dht_command), + #[cfg(feature = "relay")] + Command::AutoRelay(autorelay_command) => { + self.process_autorelay_commands(autorelay_command) + } #[cfg(feature = "stream")] Command::Stream(stream_command) => self.process_stream_command(stream_command), #[cfg(feature = "request-response")] diff --git a/src/task/relay.rs b/src/task/relay.rs index 0ff6adb..fbc715f 100644 --- a/src/task/relay.rs +++ b/src/task/relay.rs @@ -1,5 +1,6 @@ use crate::behaviour::peer_store::store::Store; use crate::task::ConnexaTask; +use crate::types::AutoRelayCommand; use libp2p::relay::{Event as RelayServerEvent, client::Event as RelayClientEvent}; use libp2p::swarm::NetworkBehaviour; use std::fmt::Debug; @@ -11,6 +12,56 @@ where C::ToSwarm: Debug, S: Store, { + pub fn process_autorelay_commands(&mut self, command: AutoRelayCommand) { + let swarm = self.swarm.as_mut().expect("swarm is still valid"); + match command { + AutoRelayCommand::AddStaticRelay { + peer_id, + relay_addr, + resp, + } => { + let Some(autorelay) = swarm.behaviour_mut().autorelay.as_mut() else { + let _ = resp.send(Err(std::io::Error::other("autorelay is not enabled"))); + return; + }; + + let _ = resp.send(Ok(autorelay.add_static_relay(peer_id, relay_addr))); + } + AutoRelayCommand::RemoveStaticRelay { + peer_id, + relay_addr, + resp, + } => { + let Some(autorelay) = swarm.behaviour_mut().autorelay.as_mut() else { + let _ = resp.send(Err(std::io::Error::other("autorelay is not enabled"))); + return; + }; + + let _ = resp.send(Ok(autorelay.remove_static_relay(peer_id, relay_addr))); + } + AutoRelayCommand::EnableAutoRelay { resp } => { + let Some(autorelay) = swarm.behaviour_mut().autorelay.as_mut() else { + let _ = resp.send(Err(std::io::Error::other("autorelay is not enabled"))); + return; + }; + + autorelay.enable_autorelay(); + + let _ = resp.send(Ok(())); + } + AutoRelayCommand::DisableAutoRelay { resp } => { + let Some(autorelay) = swarm.behaviour_mut().autorelay.as_mut() else { + let _ = resp.send(Err(std::io::Error::other("autorelay is not enabled"))); + return; + }; + + autorelay.disable_autorelay(); + + let _ = resp.send(Ok(())); + } + } + } + pub fn process_relay_client_event(&mut self, event: RelayClientEvent) { match event { RelayClientEvent::ReservationReqAccepted { diff --git a/src/types.rs b/src/types.rs index 1548b96..3ecccd3 100644 --- a/src/types.rs +++ b/src/types.rs @@ -41,6 +41,8 @@ pub enum Command { Rendezvous(RendezvousCommand), #[cfg(feature = "autonat")] Autonat(AutonatCommand), + #[cfg(feature = "relay")] + AutoRelay(AutoRelayCommand), Whitelist(WhitelistCommand), Blacklist(BlacklistCommand), ConnectionLimits(ConnectionLimitsCommand), @@ -103,6 +105,13 @@ impl From for Command { } } +#[cfg(feature = "relay")] +impl From for Command { + fn from(cmd: AutoRelayCommand) -> Self { + Command::AutoRelay(cmd) + } +} + impl From for Command { fn from(cmd: WhitelistCommand) -> Self { Command::Whitelist(cmd) @@ -387,6 +396,27 @@ pub enum DHTCommand { }, } +#[cfg(feature = "relay")] +#[derive(Debug)] +pub enum AutoRelayCommand { + AddStaticRelay { + peer_id: PeerId, + relay_addr: Multiaddr, + resp: oneshot::Sender>, + }, + RemoveStaticRelay { + peer_id: PeerId, + relay_addr: Multiaddr, + resp: oneshot::Sender>, + }, + EnableAutoRelay { + resp: oneshot::Sender>, + }, + DisableAutoRelay { + resp: oneshot::Sender>, + }, +} + #[cfg(feature = "request-response")] #[derive(Debug)] pub enum RequestResponseCommand { From 70f4d58caa4a3c60c84f7b9f821cd6b10616dbc6 Mon Sep 17 00:00:00 2001 From: Darius Date: Sat, 30 Aug 2025 17:02:36 -0400 Subject: [PATCH 05/77] chore: extend functionality, move cleanup duration to a const, use vecdeque to prioritize static relays when possible, ignore relayed peers (marking them as not supported), and remove redundant logic --- src/behaviour/autorelay.rs | 355 +++++++++++++++++++++++-------------- 1 file changed, 224 insertions(+), 131 deletions(-) diff --git a/src/behaviour/autorelay.rs b/src/behaviour/autorelay.rs index 7da3655..c9557a5 100644 --- a/src/behaviour/autorelay.rs +++ b/src/behaviour/autorelay.rs @@ -5,28 +5,34 @@ use crate::behaviour::dummy; use crate::multiaddr_ext::MultiaddrExt; use crate::prelude::swarm::derive_prelude::{ConnectionEstablished, PortUse}; use crate::prelude::swarm::{ - AddressChange, CloseConnection, ConnectionClosed, ConnectionDenied, ExpiredListenAddr, - FromSwarm, ListenerClosed, ListenerError, THandler, THandlerInEvent, THandlerOutEvent, ToSwarm, + AddressChange, CloseConnection, ConnectionClosed, ConnectionDenied, DialFailure, + ExpiredListenAddr, FromSwarm, ListenerClosed, ListenerError, THandler, THandlerInEvent, + THandlerOutEvent, ToSwarm, }; use crate::prelude::transport::Endpoint; use either::Either; use futures::FutureExt; use futures_timer::Delay; +use indexmap::map::Entry; use indexmap::{IndexMap, IndexSet}; use libp2p::core::transport::ListenerId; use libp2p::multiaddr::Protocol; +use libp2p::swarm::dial_opts::DialOpts; use libp2p::swarm::{ConnectionId, ListenOpts, NetworkBehaviour, NewListenAddr}; use libp2p::{Multiaddr, PeerId}; use rand::prelude::IteratorRandom; use std::collections::VecDeque; +use std::hash::{Hash, Hasher}; use std::task::{Context, Poll, Waker}; use std::time::Duration; const MAX_CAP: usize = 100; +const CLEANUP_INTERVAL: Duration = Duration::from_secs(60); pub struct Behaviour { config: Config, - info: IndexMap>, + info: IndexMap>, + static_relays: IndexMap>, listener_to_info: IndexMap, events: VecDeque::ToSwarm, THandlerInEvent>>, pending_target: IndexSet, @@ -39,10 +45,11 @@ impl Default for Behaviour { Self { config: Config::default(), info: IndexMap::new(), + static_relays: IndexMap::new(), listener_to_info: IndexMap::new(), events: VecDeque::new(), pending_target: IndexSet::new(), - capacity_cleanup: Delay::new(Duration::from_secs(60)), + capacity_cleanup: Delay::new(CLEANUP_INTERVAL), waker: None, } } @@ -53,7 +60,6 @@ impl Default for Behaviour { pub struct Config { pub max_reservation: Option, pub auto_reservation: bool, - pub capacity_cleanup_interval: Duration, } impl Default for Config { @@ -61,28 +67,53 @@ impl Default for Config { Self { max_reservation: None, auto_reservation: true, - capacity_cleanup_interval: Duration::from_secs(60), } } } + +#[derive(Debug, Clone)] +struct PeerInfo { + connection_id: ConnectionId, + address: Multiaddr, + relay_status: RelayStatus, +} + +impl PeerInfo { + /// Check to see if the address is from a relay and if so, automatically disqualify the connection + /// as we are not able to establish a reservation via multi-HOP + pub fn check_for_disqualifying_address(&mut self) -> bool { + match self.address.is_relayed() { + true => { + self.relay_status = RelayStatus::NotSupported; + true + } + false => { + self.relay_status = RelayStatus::None; + false + } } } } -#[derive(Debug)] -struct PeerInfo { - connection_id: Option, - address: Multiaddr, - relay_supported: bool, - reservation_status: ReservationStatus, - static_info: bool, +impl Hash for PeerInfo { + fn hash(&self, state: &mut H) { + self.connection_id.hash(state); + self.address.hash(state); + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum RelayStatus { + Supported { status: ReservationStatus }, + NotSupported, + None, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum ReservationStatus { + Idle, Pending { id: ListenerId }, Active { id: ListenerId }, - None, } impl Behaviour { @@ -94,47 +125,32 @@ impl Behaviour { } pub fn add_static_relay(&mut self, peer_id: PeerId, address: Multiaddr) -> bool { - let infos = self.info.entry(peer_id).or_default(); - if let Some(info) = infos - .iter() - .position(|info| info.address == address) - .map(|index| &mut infos[index]) - { - if info.static_info { - return false; - } - info.static_info = true; - } else { - infos.push(PeerInfo { - connection_id: None, - address, - relay_supported: false, - reservation_status: ReservationStatus::None, - static_info: true, - }); - } + let Ok(address) = address.with_p2p(peer_id) else { + return false; + }; - true + self.static_relays + .entry(peer_id) + .or_default() + .insert(address) } pub fn remove_static_relay(&mut self, peer_id: PeerId, address: Multiaddr) -> bool { - // Note that if there is an active reservation or a connection to the address, we will only set `static_info` to false, so it will be removed later on disconnection - // otherwise it will be removed - let Some(infos) = self.info.get_mut(&peer_id) else { + let Ok(address) = address.with_p2p(peer_id) else { return false; }; - if let Some(index) = infos.iter_mut().position(|info| info.address == address) { - let info = &mut infos[index]; - if info.connection_id.is_some() { - info.static_info = false; - return true; - } - infos.remove(index); - return true; + let Some(addrs) = self.static_relays.get_mut(&peer_id) else { + return false; + }; + + let removed = addrs.shift_remove(&address); + + if addrs.is_empty() { + self.static_relays.shift_remove(&peer_id); } - false + removed } pub fn enable_autorelay(&mut self) { @@ -155,16 +171,25 @@ impl Behaviour { pub fn get_all_supported_targets(&self) -> impl Iterator { self.info .iter() - .filter(|(_, infos)| infos.iter().any(|info| info.relay_supported)) + .filter(|(_, infos)| { + infos + .iter() + .any(|info| matches!(info.relay_status, RelayStatus::Supported { .. })) + }) .map(|(peer_id, _)| peer_id) } - pub fn get_supported_targets(&self) -> impl Iterator { + pub fn get_potential_targets(&self) -> impl Iterator { self.info .iter() .filter(|(_, infos)| { infos.iter().any(|info| { - info.relay_supported && info.reservation_status == ReservationStatus::None + matches!( + info.relay_status, + RelayStatus::Supported { + status: ReservationStatus::Idle + } + ) }) }) .map(|(peer_id, _)| peer_id) @@ -181,24 +206,78 @@ impl Behaviour { let Some(info) = connections .iter_mut() - .find(|info| info.connection_id.is_some_and(|id| id == connection_id)) + .find(|info| info.connection_id == connection_id) else { return; }; - match info.reservation_status { - ReservationStatus::Active { .. } => { + match info.relay_status { + RelayStatus::Supported { + status: ReservationStatus::Active { .. }, + } => { // TODO: Determine if we should disconnect then reconnect? } - ReservationStatus::Pending { .. } => { + RelayStatus::Supported { + status: ReservationStatus::Pending { .. }, + } => { self.pending_target.shift_remove(&peer_id); } - ReservationStatus::None => { - // FIXME: Unreachable? + RelayStatus::Supported { + status: ReservationStatus::Idle, } + | RelayStatus::NotSupported + | RelayStatus::None => {} } - info.reservation_status = ReservationStatus::None; + info.relay_status = RelayStatus::NotSupported; + } + + fn select_connection_for_reservation(&mut self, peer_id: &PeerId) -> bool { + if self.pending_target.contains(peer_id) { + return false; + } + + let Some(connections) = self.info.get_mut(peer_id) else { + return false; + }; + + if connections.is_empty() { + self.info.shift_remove(peer_id); + tracing::warn!(%peer_id, "no connections present. removing entry"); + return false; + } + + let mut rng = rand::thread_rng(); + + // TODO: Have a option to select either in order or at random + let info = connections + .iter_mut() + .choose(&mut rng) + .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.listener_to_info + .insert(id, (*peer_id, info.connection_id)); + self.events.push_back(ToSwarm::ListenOn { opts }); + self.pending_target.insert(*peer_id); + + true } #[allow(clippy::manual_saturating_arithmetic)] @@ -216,15 +295,34 @@ impl Behaviour { } // TODO: check to determine if we have any active connections and if not, dial any static relays and let it be handled internally - // let have_connections = self.info.iter().any(|(_, infos)| infos.iter().any(|info| info.connection_id.is_some() && !info.static_info)); + let have_connections = self.info.iter().any(|(_, infos)| !infos.is_empty()); + + if !have_connections && auto { + if self.static_relays.is_empty() { + // TODO: Emit an event informing swarm about being in need of relays? + // however this would require separate functions to add relays to the autorelay state and possibly confirm if theres any existing connections + return; + } + for (peer_id, addrs) in self.static_relays.iter() { + for addr in addrs.iter().cloned() { + let opts = DialOpts::peer_id(*peer_id).addresses(vec![addr]).build(); + self.events.push_back(ToSwarm::Dial { opts }); + } + } + return; + } let relayed_targets = self .info .iter() .filter(|(_, info)| { info.iter().any(|info| { - info.relay_supported - && matches!(info.reservation_status, ReservationStatus::Active { .. }) + matches!( + info.relay_status, + RelayStatus::Supported { + status: ReservationStatus::Active { .. } + } + ) }) }) .count(); @@ -233,7 +331,7 @@ impl Behaviour { return; } - let targets = self.get_supported_targets().copied().collect::>(); + let targets = self.get_potential_targets().copied().collect::>(); let pending_target_len = self.pending_target.len(); @@ -241,8 +339,6 @@ impl Behaviour { return; } - debug_assert!(pending_target_len < max); - let targets_count = targets.len(); if targets_count == 0 { @@ -264,40 +360,10 @@ impl Behaviour { .choose_multiple(&mut rng, remaining_targets_needed); for peer_id in targets { - let connections = self.info.get_mut(&peer_id).expect("peer entry is valud"); - - let info = connections - .iter_mut() - .filter(|info| info.connection_id.is_some()) - .choose(&mut rng) - .expect("connection is present"); - - assert_eq!(info.reservation_status, ReservationStatus::None); - - 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; - } - }; - - let relay_addr = addr_with_peer_id.with(Protocol::P2pCircuit); - - let opts = ListenOpts::new(relay_addr); - - let id = opts.listener_id(); + if !self.select_connection_for_reservation(&peer_id) { + continue; + } - info.reservation_status = ReservationStatus::Pending { id }; - self.listener_to_info.insert( - id, - ( - peer_id, - info.connection_id.expect("connection id is present"), - ), - ); - self.events.push_back(ToSwarm::ListenOn { opts }); - self.pending_target.insert(peer_id); if self.pending_target.len() == max { break; } @@ -347,16 +413,13 @@ impl NetworkBehaviour for Behaviour { _addresses: &[Multiaddr], _effective_role: Endpoint, ) -> Result, ConnectionDenied> { - let Some(infos) = maybe_peer.and_then(|peer_id| self.info.get_mut(&peer_id)) else { + let Some(addrs) = maybe_peer.and_then(|peer_id| self.static_relays.get_mut(&peer_id)) + else { return Ok(vec![]); }; // To prevent providing addresses from active connections, we will only focus on addresses added here that are considered to be fixed/static relays. - let addrs = infos - .iter() - .filter(|info| info.static_info) - .map(|info| info.address.clone()) - .collect::>(); + let addrs = addrs.iter().cloned().collect::>(); Ok(addrs) } @@ -371,20 +434,27 @@ impl NetworkBehaviour for Behaviour { }) => { let infos = self.info.entry(peer_id).or_default(); let addr = endpoint.get_remote_address().clone(); - // scan the infos to find the first entry without a connection id - if let Some(index) = infos.iter().position(|info| info.connection_id.is_none()) { - let info = &mut infos[index]; - info.connection_id = Some(connection_id); + let mut info = PeerInfo { + connection_id, + address: addr, + relay_status: RelayStatus::None, + }; + + // in the event that the address is from a peer going through a relay, automatically disqualify the connection + // from being used as a potential relay since there is no support for multi-HOP + if info.check_for_disqualifying_address() { + infos.push_back(info); } else { - let info = PeerInfo { - connection_id: Some(connection_id), - address: addr, - relay_supported: false, - reservation_status: ReservationStatus::None, - static_info: false, - }; - infos.push(info); + match self.static_relays.get(&peer_id) { + Some(addrs) if addrs.contains(&info.address) => { + // prioritize static relays so it would have a higher chance of being selected first + infos.push_front(info); + } + _ => { + infos.push_back(info); + } + } } } FromSwarm::ConnectionClosed(ConnectionClosed { @@ -398,10 +468,7 @@ impl NetworkBehaviour for Behaviour { infos .iter() - .position(|info| { - info.connection_id.is_some_and(|id| id == connection_id) - && !info.static_info - }) + .position(|info| info.connection_id == connection_id) .map(|index| infos.remove(index)); // TODO: Determine if we should remove it here or leave it for the listener events to handle its removal @@ -418,6 +485,26 @@ impl NetworkBehaviour for Behaviour { self.info.shift_remove(&peer_id); } } + FromSwarm::DialFailure(DialFailure { + peer_id, + connection_id, + error, + }) => { + tracing::error!(maybe_peer = ?peer_id, %connection_id, %error, "failed to dial peer"); + + let Some(peer_id) = peer_id else { + return; + }; + + let Some(infos) = self.info.get_mut(&peer_id) else { + return; + }; + + infos + .iter() + .position(|info| info.connection_id == connection_id) + .map(|index| infos.remove(index)); + } FromSwarm::AddressChange(AddressChange { peer_id, connection_id, @@ -435,9 +522,7 @@ impl NetworkBehaviour for Behaviour { .and_then(|infos| { infos .iter() - .position(|info| { - info.connection_id.is_some_and(|id| id == connection_id) - }) + .position(|info| info.connection_id == connection_id) .and_then(|index| infos.get_mut(index)) }) .expect("connection is present"); @@ -460,15 +545,20 @@ impl NetworkBehaviour for Behaviour { let info = infos .iter() - .position(|info| info.connection_id.is_some_and(|id| id == *connection_id)) + .position(|info| info.connection_id == *connection_id) .and_then(|index| infos.get_mut(index)) .expect("connection is present"); - let ReservationStatus::Pending { id } = info.reservation_status else { + let RelayStatus::Supported { + status: ReservationStatus::Pending { id }, + } = info.relay_status + else { return; }; - info.reservation_status = ReservationStatus::Active { id }; + info.relay_status = RelayStatus::Supported { + status: ReservationStatus::Active { id }, + }; debug_assert!(self.pending_target.shift_remove(peer_id)); } @@ -495,27 +585,31 @@ impl NetworkBehaviour for Behaviour { let peer_info = infos .iter() - .position(|info| info.connection_id.is_some_and(|id| id == connection_id)) + .position(|info| info.connection_id == connection_id) .and_then(|index| infos.get_mut(index)) .expect("connection is present"); match event { Out::Supported => { - peer_info.relay_supported = true; + peer_info.relay_status = RelayStatus::Supported { + status: ReservationStatus::Idle, + }; self.meet_reservation_target(true); } Out::Unsupported => { - peer_info.relay_supported = false; + let previous_status = peer_info.relay_status; + peer_info.relay_status = RelayStatus::NotSupported; + // if there is a change in protocol support during an active reservation, // we should disconnect to remove the reservation - if peer_info.reservation_status != ReservationStatus::None { + if matches!(previous_status, RelayStatus::Supported { .. }) { self.events.push_back(ToSwarm::CloseConnection { peer_id, connection: CloseConnection::One(connection_id), }); - // TODO: Determine if we should reconnect if this is the only connection - // if infos.iter().filter(|info| info.connection_id.is_some()).count() == 1 { + // if infos.len() == 1 { + // // TODO: Determine if we should reconnect if this is the only connection // let addr = peer_info.address.clone(); // let opts = DialOpts::peer_id(peer_id).addresses(vec![addr]).build(); // self.events.push_back(ToSwarm::Dial { opts }); @@ -545,8 +639,7 @@ impl NetworkBehaviour for Behaviour { self.info.shrink_to_fit(); } - self.capacity_cleanup - .reset(self.config.capacity_cleanup_interval); + self.capacity_cleanup.reset(CLEANUP_INTERVAL); } self.waker.replace(cx.waker().clone()); From 8735d30772e602f5f3caca1aa8ac16ed765d400e Mon Sep 17 00:00:00 2001 From: Darius Date: Sat, 30 Aug 2025 17:59:16 -0400 Subject: [PATCH 06/77] chore: add selection and use the correct amount for target --- src/behaviour/autorelay.rs | 41 ++++++++++++++++++++++++++++---------- 1 file changed, 30 insertions(+), 11 deletions(-) diff --git a/src/behaviour/autorelay.rs b/src/behaviour/autorelay.rs index c9557a5..b9b792c 100644 --- a/src/behaviour/autorelay.rs +++ b/src/behaviour/autorelay.rs @@ -62,6 +62,14 @@ pub struct Config { pub auto_reservation: bool, } +#[derive(Default, Debug, Clone, Copy)] +pub enum Selection { + #[default] + InOrder, + Random, + LowestLatency, +} + impl Default for Config { fn default() -> Self { Self { @@ -155,7 +163,7 @@ impl Behaviour { pub fn enable_autorelay(&mut self) { self.config.auto_reservation = true; - self.meet_reservation_target(true); + self.meet_reservation_target(true, Selection::Random); if let Some(waker) = self.waker.take() { waker.wake(); } @@ -249,7 +257,6 @@ impl Behaviour { let mut rng = rand::thread_rng(); - // TODO: Have a option to select either in order or at random let info = connections .iter_mut() .choose(&mut rng) @@ -281,7 +288,7 @@ impl Behaviour { } #[allow(clippy::manual_saturating_arithmetic)] - fn meet_reservation_target(&mut self, auto: bool) { + fn meet_reservation_target(&mut self, auto: bool, selection: Selection) { if !self.config.auto_reservation { return; } @@ -339,14 +346,12 @@ impl Behaviour { return; } - let targets_count = targets.len(); + let targets_count = std::cmp::min(targets.len(), max); if targets_count == 0 { return; } - let mut rng = rand::thread_rng(); - let remaining_targets_needed = targets_count .checked_sub(self.pending_target.len()) .unwrap_or_default(); @@ -355,11 +360,25 @@ impl Behaviour { return; } - let targets = targets - .into_iter() - .choose_multiple(&mut rng, remaining_targets_needed); + let mut rng = rand::thread_rng(); + + let new_targets = match selection { + Selection::InOrder => { + // TODO: Maybe use take and collect instead? + targets + .into_iter() + .take(remaining_targets_needed) + .collect::>() + } + Selection::Random => targets + .into_iter() + .choose_multiple(&mut rng, remaining_targets_needed), + Selection::LowestLatency => { + unimplemented!() + } + }; - for peer_id in targets { + for peer_id in new_targets { if !self.select_connection_for_reservation(&peer_id) { continue; } @@ -594,7 +613,7 @@ impl NetworkBehaviour for Behaviour { peer_info.relay_status = RelayStatus::Supported { status: ReservationStatus::Idle, }; - self.meet_reservation_target(true); + self.meet_reservation_target(true, Selection::InOrder); } Out::Unsupported => { let previous_status = peer_info.relay_status; From 62da5764a49498b4402f7e06b230dcf15180b3e8 Mon Sep 17 00:00:00 2001 From: Darius Date: Sat, 30 Aug 2025 21:24:00 -0400 Subject: [PATCH 07/77] chore: remove RelayStatus::None --- src/behaviour/autorelay.rs | 39 ++++++++++++++++++++------------------ 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/src/behaviour/autorelay.rs b/src/behaviour/autorelay.rs index b9b792c..9a2f70a 100644 --- a/src/behaviour/autorelay.rs +++ b/src/behaviour/autorelay.rs @@ -96,7 +96,7 @@ impl PeerInfo { true } false => { - self.relay_status = RelayStatus::None; + self.relay_status = RelayStatus::Pending; false } } @@ -114,7 +114,7 @@ impl Hash for PeerInfo { enum RelayStatus { Supported { status: ReservationStatus }, NotSupported, - None, + Pending, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -163,7 +163,7 @@ impl Behaviour { pub fn enable_autorelay(&mut self) { self.config.auto_reservation = true; - self.meet_reservation_target(true, Selection::Random); + self.meet_reservation_target(Selection::Random); if let Some(waker) = self.waker.take() { waker.wake(); } @@ -234,7 +234,7 @@ impl Behaviour { status: ReservationStatus::Idle, } | RelayStatus::NotSupported - | RelayStatus::None => {} + | RelayStatus::Pending => {} } info.relay_status = RelayStatus::NotSupported; @@ -288,7 +288,7 @@ impl Behaviour { } #[allow(clippy::manual_saturating_arithmetic)] - fn meet_reservation_target(&mut self, auto: bool, selection: Selection) { + fn meet_reservation_target(&mut self, selection: Selection) { if !self.config.auto_reservation { return; } @@ -297,14 +297,20 @@ impl Behaviour { // if max target reservation is 0, this would be no different from disabling auto reservation, // which would help prevent a possible DoS due to having multiple reservation acquired. - if max == 0 && auto { + if max == 0 { return; } // TODO: check to determine if we have any active connections and if not, dial any static relays and let it be handled internally - let have_connections = self.info.iter().any(|(_, infos)| !infos.is_empty()); - - if !have_connections && auto { + let no_supported_relays = self.info.is_empty() + || self.info.iter().all(|(_, infos)| { + !infos.is_empty() + || infos + .iter() + .all(|info| info.relay_status == RelayStatus::NotSupported) + }); + + if no_supported_relays { if self.static_relays.is_empty() { // TODO: Emit an event informing swarm about being in need of relays? // however this would require separate functions to add relays to the autorelay state and possibly confirm if theres any existing connections @@ -363,13 +369,10 @@ impl Behaviour { let mut rng = rand::thread_rng(); let new_targets = match selection { - Selection::InOrder => { - // TODO: Maybe use take and collect instead? - targets - .into_iter() - .take(remaining_targets_needed) - .collect::>() - } + Selection::InOrder => targets + .into_iter() + .take(remaining_targets_needed) + .collect::>(), Selection::Random => targets .into_iter() .choose_multiple(&mut rng, remaining_targets_needed), @@ -457,7 +460,7 @@ impl NetworkBehaviour for Behaviour { let mut info = PeerInfo { connection_id, address: addr, - relay_status: RelayStatus::None, + relay_status: RelayStatus::Pending, }; // in the event that the address is from a peer going through a relay, automatically disqualify the connection @@ -613,7 +616,7 @@ impl NetworkBehaviour for Behaviour { peer_info.relay_status = RelayStatus::Supported { status: ReservationStatus::Idle, }; - self.meet_reservation_target(true, Selection::InOrder); + self.meet_reservation_target(Selection::InOrder); } Out::Unsupported => { let previous_status = peer_info.relay_status; From dbc5a65142a298e0c9a074d9a0fc3250e5c638bf Mon Sep 17 00:00:00 2001 From: Darius Date: Sun, 31 Aug 2025 04:43:02 -0400 Subject: [PATCH 08/77] chore: use NonZeroU8, rename field, and add Selection::Peer --- src/behaviour/autorelay.rs | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/src/behaviour/autorelay.rs b/src/behaviour/autorelay.rs index 9a2f70a..cf7a189 100644 --- a/src/behaviour/autorelay.rs +++ b/src/behaviour/autorelay.rs @@ -13,7 +13,6 @@ use crate::prelude::transport::Endpoint; use either::Either; use futures::FutureExt; use futures_timer::Delay; -use indexmap::map::Entry; use indexmap::{IndexMap, IndexSet}; use libp2p::core::transport::ListenerId; use libp2p::multiaddr::Protocol; @@ -23,6 +22,7 @@ use libp2p::{Multiaddr, PeerId}; use rand::prelude::IteratorRandom; use std::collections::VecDeque; use std::hash::{Hash, Hasher}; +use std::num::NonZeroU8; use std::task::{Context, Poll, Waker}; use std::time::Duration; @@ -58,8 +58,8 @@ impl Default for Behaviour { #[derive(Debug, Clone)] #[non_exhaustive] pub struct Config { - pub max_reservation: Option, - pub auto_reservation: bool, + pub max_reservation: NonZeroU8, + pub enable_auto_relay: bool, } #[derive(Default, Debug, Clone, Copy)] @@ -68,13 +68,14 @@ pub enum Selection { InOrder, Random, LowestLatency, + Peer(PeerId), } impl Default for Config { fn default() -> Self { Self { - max_reservation: None, - auto_reservation: true, + max_reservation: NonZeroU8::new(2).expect("not zero"), + enable_auto_relay: true, } } } @@ -162,7 +163,7 @@ impl Behaviour { } pub fn enable_autorelay(&mut self) { - self.config.auto_reservation = true; + self.config.enable_auto_relay = true; self.meet_reservation_target(Selection::Random); if let Some(waker) = self.waker.take() { waker.wake(); @@ -170,7 +171,7 @@ impl Behaviour { } pub fn disable_autorelay(&mut self) { - self.config.auto_reservation = false; + self.config.enable_auto_relay = false; if let Some(waker) = self.waker.take() { waker.wake(); } @@ -289,17 +290,11 @@ impl Behaviour { #[allow(clippy::manual_saturating_arithmetic)] fn meet_reservation_target(&mut self, selection: Selection) { - if !self.config.auto_reservation { + if !self.config.enable_auto_relay { return; } - let max = self.config.max_reservation.unwrap_or(2) as usize; - - // if max target reservation is 0, this would be no different from disabling auto reservation, - // which would help prevent a possible DoS due to having multiple reservation acquired. - if max == 0 { - return; - } + let max = self.config.max_reservation.get() as usize; // TODO: check to determine if we have any active connections and if not, dial any static relays and let it be handled internally let no_supported_relays = self.info.is_empty() @@ -376,6 +371,7 @@ impl Behaviour { Selection::Random => targets .into_iter() .choose_multiple(&mut rng, remaining_targets_needed), + Selection::Peer(peer_id) => targets.into_iter().filter(|&id| id == peer_id).collect(), Selection::LowestLatency => { unimplemented!() } From 97218d4058ba92c45d1a2d9a4b07e5af651223ab Mon Sep 17 00:00:00 2001 From: Darius Date: Sun, 31 Aug 2025 17:38:03 -0400 Subject: [PATCH 09/77] chore: add additional trait members for multiaddr ext --- src/multiaddr_ext.rs | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/src/multiaddr_ext.rs b/src/multiaddr_ext.rs index e9205bb..689306d 100644 --- a/src/multiaddr_ext.rs +++ b/src/multiaddr_ext.rs @@ -3,10 +3,48 @@ use libp2p::multiaddr::Protocol; 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(|protocol| protocol == 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 acca3be6d7051c1541dbaddf841d7df142d804e6 Mon Sep 17 00:00:00 2001 From: Darius Date: Sun, 31 Aug 2025 17:40:45 -0400 Subject: [PATCH 10/77] chore: track external addresses, preventing relay reservations in the event of the node being reachable through other means --- src/behaviour/autorelay.rs | 31 ++++++++++++++++++++++++------- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/src/behaviour/autorelay.rs b/src/behaviour/autorelay.rs index cf7a189..1aadc19 100644 --- a/src/behaviour/autorelay.rs +++ b/src/behaviour/autorelay.rs @@ -17,7 +17,7 @@ use indexmap::{IndexMap, IndexSet}; use libp2p::core::transport::ListenerId; use libp2p::multiaddr::Protocol; use libp2p::swarm::dial_opts::DialOpts; -use libp2p::swarm::{ConnectionId, ListenOpts, NetworkBehaviour, NewListenAddr}; +use libp2p::swarm::{ConnectionId, ExternalAddresses, ListenOpts, NetworkBehaviour, NewListenAddr}; use libp2p::{Multiaddr, PeerId}; use rand::prelude::IteratorRandom; use std::collections::VecDeque; @@ -35,6 +35,7 @@ pub struct Behaviour { static_relays: IndexMap>, listener_to_info: IndexMap, events: VecDeque::ToSwarm, THandlerInEvent>>, + external_addresses: ExternalAddresses, pending_target: IndexSet, capacity_cleanup: Delay, waker: Option, @@ -50,6 +51,7 @@ impl Default for Behaviour { events: VecDeque::new(), pending_target: IndexSet::new(), capacity_cleanup: Delay::new(CLEANUP_INTERVAL), + external_addresses: ExternalAddresses::default(), waker: None, } } @@ -294,18 +296,32 @@ impl Behaviour { return; } + // 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 = self.config.max_reservation.get() as usize; // TODO: check to determine if we have any active connections and if not, dial any static relays and let it be handled internally - let no_supported_relays = self.info.is_empty() - || self.info.iter().all(|(_, infos)| { - !infos.is_empty() - || infos + let peers_not_supported = self.info.is_empty() + || self + .info + .iter() + .filter(|(_, infos)| { + infos .iter() .all(|info| info.relay_status == RelayStatus::NotSupported) - }); + }) + .count() + == 0; - if no_supported_relays { + if peers_not_supported { if self.static_relays.is_empty() { // TODO: Emit an event informing swarm about being in need of relays? // however this would require separate functions to add relays to the autorelay state and possibly confirm if theres any existing connections @@ -443,6 +459,7 @@ impl NetworkBehaviour for Behaviour { } fn on_swarm_event(&mut self, event: FromSwarm) { + let _change = self.external_addresses.on_swarm_event(&event); match event { FromSwarm::ConnectionEstablished(ConnectionEstablished { peer_id, From 0e8ffd21e1d044f16227df4ff2ac9c8784a72ec4 Mon Sep 17 00:00:00 2001 From: Darius Date: Sun, 31 Aug 2025 18:15:36 -0400 Subject: [PATCH 11/77] chore: remove config from behavior --- src/behaviour/autorelay.rs | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/behaviour/autorelay.rs b/src/behaviour/autorelay.rs index 1aadc19..f208b3d 100644 --- a/src/behaviour/autorelay.rs +++ b/src/behaviour/autorelay.rs @@ -30,7 +30,6 @@ const MAX_CAP: usize = 100; const CLEANUP_INTERVAL: Duration = Duration::from_secs(60); pub struct Behaviour { - config: Config, info: IndexMap>, static_relays: IndexMap>, listener_to_info: IndexMap, @@ -38,13 +37,15 @@ pub struct Behaviour { external_addresses: ExternalAddresses, pending_target: IndexSet, capacity_cleanup: Delay, + max_reservation: NonZeroU8, + override_autorelay: bool, + enable_auto_relay: bool, waker: Option, } impl Default for Behaviour { fn default() -> Self { Self { - config: Config::default(), info: IndexMap::new(), static_relays: IndexMap::new(), listener_to_info: IndexMap::new(), @@ -52,7 +53,10 @@ impl Default for Behaviour { pending_target: IndexSet::new(), capacity_cleanup: Delay::new(CLEANUP_INTERVAL), external_addresses: ExternalAddresses::default(), + override_autorelay: false, waker: None, + enable_auto_relay: true, + max_reservation: NonZeroU8::new(2).expect("not zero"), } } } @@ -130,7 +134,8 @@ enum ReservationStatus { impl Behaviour { pub fn new_with_config(config: Config) -> Self { Self { - config, + enable_auto_relay: config.enable_auto_relay, + max_reservation: config.max_reservation, ..Default::default() } } @@ -165,7 +170,7 @@ impl Behaviour { } pub fn enable_autorelay(&mut self) { - self.config.enable_auto_relay = true; + self.enable_auto_relay = true; self.meet_reservation_target(Selection::Random); if let Some(waker) = self.waker.take() { waker.wake(); @@ -173,7 +178,7 @@ impl Behaviour { } pub fn disable_autorelay(&mut self) { - self.config.enable_auto_relay = false; + self.enable_auto_relay = false; if let Some(waker) = self.waker.take() { waker.wake(); } @@ -292,7 +297,7 @@ impl Behaviour { #[allow(clippy::manual_saturating_arithmetic)] fn meet_reservation_target(&mut self, selection: Selection) { - if !self.config.enable_auto_relay { + if !self.enable_auto_relay { return; } @@ -306,7 +311,7 @@ impl Behaviour { return; } - let max = self.config.max_reservation.get() as usize; + let max = self.max_reservation.get() as usize; // TODO: check to determine if we have any active connections and if not, dial any static relays and let it be handled internally let peers_not_supported = self.info.is_empty() From 98388edab98294c6ae1c4433cb1a2a451a3041c9 Mon Sep 17 00:00:00 2001 From: Darius Date: Mon, 1 Sep 2025 10:29:36 -0400 Subject: [PATCH 12/77] chore: fix check, add backoff timer before enabling relay --- src/behaviour/autorelay.rs | 43 ++++++++++++++++++++++++++++---------- 1 file changed, 32 insertions(+), 11 deletions(-) diff --git a/src/behaviour/autorelay.rs b/src/behaviour/autorelay.rs index f208b3d..a2a948f 100644 --- a/src/behaviour/autorelay.rs +++ b/src/behaviour/autorelay.rs @@ -19,6 +19,7 @@ use libp2p::multiaddr::Protocol; use libp2p::swarm::dial_opts::DialOpts; use libp2p::swarm::{ConnectionId, ExternalAddresses, ListenOpts, NetworkBehaviour, NewListenAddr}; use libp2p::{Multiaddr, PeerId}; +use pollable_map::optional::Optional; use rand::prelude::IteratorRandom; use std::collections::VecDeque; use std::hash::{Hash, Hasher}; @@ -28,6 +29,7 @@ use std::time::Duration; const MAX_CAP: usize = 100; const CLEANUP_INTERVAL: Duration = Duration::from_secs(60); +const BACKOFF_INTERVAL: Duration = Duration::from_secs(5); pub struct Behaviour { info: IndexMap>, @@ -40,6 +42,7 @@ pub struct Behaviour { max_reservation: NonZeroU8, override_autorelay: bool, enable_auto_relay: bool, + backoff: Optional, waker: Option, } @@ -56,6 +59,7 @@ impl Default for Behaviour { override_autorelay: false, waker: None, enable_auto_relay: true, + backoff: Optional::default(), max_reservation: NonZeroU8::new(2).expect("not zero"), } } @@ -307,24 +311,22 @@ impl Behaviour { .external_addresses .iter() .any(|addr| addr.is_public() && !addr.is_relayed()) + && !self.override_autorelay { return; } let max = self.max_reservation.get() as usize; - // TODO: check to determine if we have any active connections and if not, dial any static relays and let it be handled internally let peers_not_supported = self.info.is_empty() - || self - .info - .iter() - .filter(|(_, infos)| { - infos - .iter() - .all(|info| info.relay_status == RelayStatus::NotSupported) + || self.info.iter().all(|(_, infos)| { + infos.iter().all(|info| { + matches!( + info.relay_status, + RelayStatus::NotSupported | RelayStatus::Pending + ) }) - .count() - == 0; + }); if peers_not_supported { if self.static_relays.is_empty() { @@ -464,7 +466,22 @@ 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 { + if self + .external_addresses + .iter() + .any(|addr| addr.is_public() && !addr.is_relayed()) + { + self.override_autorelay = true; + self.backoff.replace(Delay::new(BACKOFF_INTERVAL)); + } else if self.external_addresses.iter().any(|addr| !addr.is_public()) { + self.backoff.take(); + self.override_autorelay = false; + } + return; + } + match event { FromSwarm::ConnectionEstablished(ConnectionEstablished { peer_id, @@ -667,6 +684,10 @@ impl NetworkBehaviour for Behaviour { return Poll::Ready(event); } + if self.backoff.poll_unpin(cx).is_ready() { + self.meet_reservation_target(Selection::InOrder); + } + if self.capacity_cleanup.poll_unpin(cx).is_ready() { if (self.events.is_empty() || self.events.len() < MAX_CAP) && self.events.capacity() > MAX_CAP From 678ecf08a0d6504a6a854931ee1fba6f859d5fc8 Mon Sep 17 00:00:00 2001 From: Darius Date: Mon, 1 Sep 2025 23:50:46 -0400 Subject: [PATCH 13/77] chore: add logic to remove all reservation when an external address is added that is public --- src/behaviour/autorelay.rs | 64 ++++++++++++++++++++++++++++++-------- 1 file changed, 51 insertions(+), 13 deletions(-) diff --git a/src/behaviour/autorelay.rs b/src/behaviour/autorelay.rs index a2a948f..14c32f1 100644 --- a/src/behaviour/autorelay.rs +++ b/src/behaviour/autorelay.rs @@ -236,20 +236,57 @@ impl Behaviour { status: ReservationStatus::Active { .. }, } => { // TODO: Determine if we should disconnect then reconnect? + info.relay_status = RelayStatus::Supported { + status: ReservationStatus::Idle, + }; } RelayStatus::Supported { status: ReservationStatus::Pending { .. }, } => { + info.relay_status = RelayStatus::Supported { + status: ReservationStatus::Idle, + }; + } + RelayStatus::Pending => { self.pending_target.shift_remove(&peer_id); } RelayStatus::Supported { status: ReservationStatus::Idle, } - | RelayStatus::NotSupported - | RelayStatus::Pending => {} + | RelayStatus::NotSupported => {} } + } + + fn disable_all_reservations(&mut self) { + let relay_listeners = self + .listener_to_info + .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.info.get_mut(&peer_id).and_then(|connections| { + connections + .iter_mut() + .find(|info| info.connection_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, + }; - info.relay_status = RelayStatus::NotSupported; + self.events + .push_back(ToSwarm::RemoveListener { id: listener_id }); + } } fn select_connection_for_reservation(&mut self, peer_id: &PeerId) -> bool { @@ -320,12 +357,9 @@ impl Behaviour { let peers_not_supported = self.info.is_empty() || self.info.iter().all(|(_, infos)| { - infos.iter().all(|info| { - matches!( - info.relay_status, - RelayStatus::NotSupported | RelayStatus::Pending - ) - }) + infos + .iter() + .all(|info| info.relay_status == RelayStatus::NotSupported) }); if peers_not_supported { @@ -474,9 +508,15 @@ impl NetworkBehaviour for Behaviour { .any(|addr| addr.is_public() && !addr.is_relayed()) { self.override_autorelay = true; - self.backoff.replace(Delay::new(BACKOFF_INTERVAL)); - } else if self.external_addresses.iter().any(|addr| !addr.is_public()) { + self.disable_all_reservations(); self.backoff.take(); + } else if self.external_addresses.iter().count() == 0 + || self + .external_addresses + .iter() + .any(|addr| !addr.is_public() || addr.is_relayed()) + { + self.backoff.replace(Delay::new(BACKOFF_INTERVAL)); self.override_autorelay = false; } return; @@ -528,7 +568,6 @@ impl NetworkBehaviour for Behaviour { .position(|info| info.connection_id == connection_id) .map(|index| infos.remove(index)); - // TODO: Determine if we should remove it here or leave it for the listener events to handle its removal if let Some(listener_id) = self .listener_to_info .iter() @@ -656,7 +695,6 @@ impl NetworkBehaviour for Behaviour { Out::Unsupported => { let previous_status = peer_info.relay_status; peer_info.relay_status = RelayStatus::NotSupported; - // if there is a change in protocol support during an active reservation, // we should disconnect to remove the reservation if matches!(previous_status, RelayStatus::Supported { .. }) { From 9b3fc4115f9c4f55e8f5ca3e990da221792c59f6 Mon Sep 17 00:00:00 2001 From: Darius Date: Tue, 2 Sep 2025 08:16:39 -0400 Subject: [PATCH 14/77] chore: change map to use (PeerId, ConnectionId) as key --- src/behaviour/autorelay.rs | 155 ++++++++++++------------------------- 1 file changed, 50 insertions(+), 105 deletions(-) diff --git a/src/behaviour/autorelay.rs b/src/behaviour/autorelay.rs index 14c32f1..b7b28fc 100644 --- a/src/behaviour/autorelay.rs +++ b/src/behaviour/autorelay.rs @@ -32,7 +32,7 @@ const CLEANUP_INTERVAL: Duration = Duration::from_secs(60); const BACKOFF_INTERVAL: Duration = Duration::from_secs(5); pub struct Behaviour { - info: IndexMap>, + info: IndexMap<(PeerId, ConnectionId), PeerInfo>, static_relays: IndexMap>, listener_to_info: IndexMap, events: VecDeque::ToSwarm, THandlerInEvent>>, @@ -191,28 +191,22 @@ impl Behaviour { pub fn get_all_supported_targets(&self) -> impl Iterator { self.info .iter() - .filter(|(_, infos)| { - infos - .iter() - .any(|info| matches!(info.relay_status, RelayStatus::Supported { .. })) - }) - .map(|(peer_id, _)| peer_id) + .filter(|(_, info)| matches!(info.relay_status, RelayStatus::Supported { .. })) + .map(|((peer_id, _), _)| peer_id) } - pub fn get_potential_targets(&self) -> impl Iterator { + pub fn get_potential_targets(&self) -> impl Iterator { self.info .iter() - .filter(|(_, infos)| { - infos.iter().any(|info| { - matches!( - info.relay_status, - RelayStatus::Supported { - status: ReservationStatus::Idle - } - ) - }) + .filter(|(_, info)| { + matches!( + info.relay_status, + RelayStatus::Supported { + status: ReservationStatus::Idle + } + ) }) - .map(|(peer_id, _)| peer_id) + .map(|((peer_id, connection_id), _)| (peer_id, connection_id)) } fn disable_reservation(&mut self, id: ListenerId) { @@ -220,14 +214,7 @@ impl Behaviour { return; }; - let Some(connections) = self.info.get_mut(&peer_id) else { - return; - }; - - let Some(info) = connections - .iter_mut() - .find(|info| info.connection_id == connection_id) - else { + let Some(info) = self.info.get_mut(&(peer_id, connection_id)) else { return; }; @@ -265,11 +252,7 @@ impl Behaviour { .collect::>(); for (listener_id, peer_id, connection_id) in relay_listeners { - let Some(connection) = self.info.get_mut(&peer_id).and_then(|connections| { - connections - .iter_mut() - .find(|info| info.connection_id == connection_id) - }) else { + let Some(connection) = self.info.get_mut(&(peer_id, connection_id)) else { continue; }; @@ -289,26 +272,22 @@ impl Behaviour { } } - fn select_connection_for_reservation(&mut self, peer_id: &PeerId) -> bool { + fn select_connection_for_reservation( + &mut self, + (peer_id, connection_id): &(PeerId, ConnectionId), + ) -> bool { if self.pending_target.contains(peer_id) { return false; } - let Some(connections) = self.info.get_mut(peer_id) else { - return false; - }; - - if connections.is_empty() { - self.info.shift_remove(peer_id); + if self.info.is_empty() { tracing::warn!(%peer_id, "no connections present. removing entry"); return false; } - let mut rng = rand::thread_rng(); - - let info = connections - .iter_mut() - .choose(&mut rng) + let info = self + .info + .get_mut(&(*peer_id, *connection_id)) .expect("connection is present"); let addr_with_peer_id = match info.address.clone().with_p2p(*peer_id) { @@ -356,11 +335,10 @@ impl Behaviour { let max = self.max_reservation.get() as usize; let peers_not_supported = self.info.is_empty() - || self.info.iter().all(|(_, infos)| { - infos - .iter() - .all(|info| info.relay_status == RelayStatus::NotSupported) - }); + || self + .info + .iter() + .all(|(_, info)| info.relay_status == RelayStatus::NotSupported); if peers_not_supported { if self.static_relays.is_empty() { @@ -381,14 +359,12 @@ impl Behaviour { .info .iter() .filter(|(_, info)| { - info.iter().any(|info| { - matches!( - info.relay_status, - RelayStatus::Supported { - status: ReservationStatus::Active { .. } - } - ) - }) + matches!( + info.relay_status, + RelayStatus::Supported { + status: ReservationStatus::Active { .. } + } + ) }) .count(); @@ -396,7 +372,10 @@ impl Behaviour { return; } - let targets = self.get_potential_targets().copied().collect::>(); + let targets = self + .get_potential_targets() + .map(|(peer_id, connection_id)| (*peer_id, *connection_id)) + .collect::>(); let pending_target_len = self.pending_target.len(); @@ -428,14 +407,17 @@ impl Behaviour { Selection::Random => targets .into_iter() .choose_multiple(&mut rng, remaining_targets_needed), - Selection::Peer(peer_id) => targets.into_iter().filter(|&id| id == peer_id).collect(), + Selection::Peer(peer_id) => targets + .into_iter() + .filter(|(id, _)| *id == peer_id) + .collect(), Selection::LowestLatency => { unimplemented!() } }; - for peer_id in new_targets { - if !self.select_connection_for_reservation(&peer_id) { + for (peer_id, connection_id) in new_targets { + if !self.select_connection_for_reservation(&(peer_id, connection_id)) { continue; } @@ -529,7 +511,6 @@ impl NetworkBehaviour for Behaviour { endpoint, .. }) => { - let infos = self.info.entry(peer_id).or_default(); let addr = endpoint.get_remote_address().clone(); let mut info = PeerInfo { @@ -541,15 +522,15 @@ impl NetworkBehaviour for Behaviour { // in the event that the address is from a peer going through a relay, automatically disqualify the connection // from being used as a potential relay since there is no support for multi-HOP if info.check_for_disqualifying_address() { - infos.push_back(info); + self.info.insert((peer_id, connection_id), info); } else { match self.static_relays.get(&peer_id) { Some(addrs) if addrs.contains(&info.address) => { // prioritize static relays so it would have a higher chance of being selected first - infos.push_front(info); + self.info.insert_before(0, (peer_id, connection_id), info); } _ => { - infos.push_back(info); + self.info.insert((peer_id, connection_id), info); } } } @@ -559,14 +540,7 @@ impl NetworkBehaviour for Behaviour { connection_id, .. }) => { - let Some(infos) = self.info.get_mut(&peer_id) else { - return; - }; - - infos - .iter() - .position(|info| info.connection_id == connection_id) - .map(|index| infos.remove(index)); + self.info.shift_remove(&(peer_id, connection_id)); if let Some(listener_id) = self .listener_to_info @@ -576,10 +550,6 @@ impl NetworkBehaviour for Behaviour { { self.listener_to_info.shift_remove(&listener_id); } - - if infos.is_empty() { - self.info.shift_remove(&peer_id); - } } FromSwarm::DialFailure(DialFailure { peer_id, @@ -592,14 +562,7 @@ impl NetworkBehaviour for Behaviour { return; }; - let Some(infos) = self.info.get_mut(&peer_id) else { - return; - }; - - infos - .iter() - .position(|info| info.connection_id == connection_id) - .map(|index| infos.remove(index)); + self.info.shift_remove(&(peer_id, connection_id)); } FromSwarm::AddressChange(AddressChange { peer_id, @@ -614,13 +577,7 @@ impl NetworkBehaviour for Behaviour { let info = self .info - .get_mut(&peer_id) - .and_then(|infos| { - infos - .iter() - .position(|info| info.connection_id == connection_id) - .and_then(|index| infos.get_mut(index)) - }) + .get_mut(&(peer_id, connection_id)) .expect("connection is present"); info.address = new_addr.clone(); @@ -635,16 +592,10 @@ impl NetworkBehaviour for Behaviour { return; }; - let Some(infos) = self.info.get_mut(peer_id) else { + let Some(info) = self.info.get_mut(&(*peer_id, *connection_id)) else { return; }; - let info = infos - .iter() - .position(|info| info.connection_id == *connection_id) - .and_then(|index| infos.get_mut(index)) - .expect("connection is present"); - let RelayStatus::Supported { status: ReservationStatus::Pending { id }, } = info.relay_status @@ -675,16 +626,10 @@ impl NetworkBehaviour for Behaviour { ) { let Either::Left(event) = event; - let Some(infos) = self.info.get_mut(&peer_id) else { + let Some(peer_info) = self.info.get_mut(&(peer_id, connection_id)) else { return; }; - let peer_info = infos - .iter() - .position(|info| info.connection_id == connection_id) - .and_then(|index| infos.get_mut(index)) - .expect("connection is present"); - match event { Out::Supported => { peer_info.relay_status = RelayStatus::Supported { From cad738352937e52c720dc7fd9446d7053dca81b5 Mon Sep 17 00:00:00 2001 From: Darius Date: Wed, 3 Sep 2025 06:17:36 -0400 Subject: [PATCH 15/77] chore: track connection in pending target --- src/behaviour/autorelay.rs | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/src/behaviour/autorelay.rs b/src/behaviour/autorelay.rs index b7b28fc..e6696c6 100644 --- a/src/behaviour/autorelay.rs +++ b/src/behaviour/autorelay.rs @@ -37,7 +37,7 @@ pub struct Behaviour { listener_to_info: IndexMap, events: VecDeque::ToSwarm, THandlerInEvent>>, external_addresses: ExternalAddresses, - pending_target: IndexSet, + pending_target: IndexSet<(PeerId, ConnectionId)>, capacity_cleanup: Delay, max_reservation: NonZeroU8, override_autorelay: bool, @@ -233,11 +233,10 @@ impl Behaviour { info.relay_status = RelayStatus::Supported { status: ReservationStatus::Idle, }; + self.pending_target.shift_remove(&(peer_id, connection_id)); } - RelayStatus::Pending => { - self.pending_target.shift_remove(&peer_id); - } - RelayStatus::Supported { + RelayStatus::Pending + | RelayStatus::Supported { status: ReservationStatus::Idle, } | RelayStatus::NotSupported => {} @@ -274,9 +273,10 @@ 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) { + if self.pending_target.contains(&(peer_id, connection_id)) { return false; } @@ -287,10 +287,10 @@ impl Behaviour { let info = self .info - .get_mut(&(*peer_id, *connection_id)) + .get_mut(&(peer_id, connection_id)) .expect("connection is present"); - let addr_with_peer_id = match info.address.clone().with_p2p(*peer_id) { + 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"); @@ -308,9 +308,9 @@ impl Behaviour { status: ReservationStatus::Pending { id }, }; self.listener_to_info - .insert(id, (*peer_id, info.connection_id)); + .insert(id, (peer_id, info.connection_id)); self.events.push_back(ToSwarm::ListenOn { opts }); - self.pending_target.insert(*peer_id); + self.pending_target.insert((peer_id, connection_id)); true } @@ -417,7 +417,7 @@ impl Behaviour { }; for (peer_id, connection_id) in new_targets { - if !self.select_connection_for_reservation(&(peer_id, connection_id)) { + if !self.select_connection_for_reservation(peer_id, connection_id) { continue; } @@ -607,7 +607,10 @@ impl NetworkBehaviour for Behaviour { status: ReservationStatus::Active { id }, }; - debug_assert!(self.pending_target.shift_remove(peer_id)); + debug_assert!( + self.pending_target + .shift_remove(&(*peer_id, *connection_id)) + ); } FromSwarm::ExpiredListenAddr(ExpiredListenAddr { listener_id, .. }) | FromSwarm::ListenerError(ListenerError { listener_id, .. }) From 0a8f1f5ccdf6c38eb33c62430855ea67991436b2 Mon Sep 17 00:00:00 2001 From: Darius Date: Thu, 4 Sep 2025 14:31:17 -0400 Subject: [PATCH 16/77] chore: add feature checks --- src/behaviour.rs | 1 + src/handle.rs | 2 ++ src/multiaddr_ext.rs | 1 + 3 files changed, 4 insertions(+) diff --git a/src/behaviour.rs b/src/behaviour.rs index 74c24f4..2386e46 100644 --- a/src/behaviour.rs +++ b/src/behaviour.rs @@ -1,3 +1,4 @@ +#[cfg(feature = "relay")] pub mod autorelay; pub mod dummy; pub mod peer_store; diff --git a/src/handle.rs b/src/handle.rs index 9abcda0..c468536 100644 --- a/src/handle.rs +++ b/src/handle.rs @@ -8,6 +8,7 @@ pub(crate) mod floodsub; #[cfg(feature = "gossipsub")] pub(crate) mod gossipsub; mod peer_store; +#[cfg(feature = "relay")] mod relay; #[cfg(feature = "rendezvous")] pub(crate) mod rendezvous; @@ -28,6 +29,7 @@ use crate::handle::floodsub::ConnexaFloodsub; #[cfg(feature = "gossipsub")] use crate::handle::gossipsub::ConnexaGossipsub; use crate::handle::peer_store::ConnexaPeerstore; +#[cfg(feature = "relay")] use crate::handle::relay::ConnexaRelay; #[cfg(feature = "rendezvous")] use crate::handle::rendezvous::ConnexaRendezvous; diff --git a/src/multiaddr_ext.rs b/src/multiaddr_ext.rs index 689306d..6ab2aa1 100644 --- a/src/multiaddr_ext.rs +++ b/src/multiaddr_ext.rs @@ -1,6 +1,7 @@ use libp2p::Multiaddr; use libp2p::multiaddr::Protocol; +#[allow(dead_code)] pub(crate) trait MultiaddrExt { fn is_relayed(&self) -> bool; From 8140faeb2b6723a5512042e5fc3fc61f611bc179 Mon Sep 17 00:00:00 2001 From: Darius Date: Thu, 4 Sep 2025 14:34:32 -0400 Subject: [PATCH 17/77] chore: check active reservation when status change --- src/behaviour/autorelay.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/behaviour/autorelay.rs b/src/behaviour/autorelay.rs index e6696c6..00c723d 100644 --- a/src/behaviour/autorelay.rs +++ b/src/behaviour/autorelay.rs @@ -315,7 +315,6 @@ impl Behaviour { true } - #[allow(clippy::manual_saturating_arithmetic)] fn meet_reservation_target(&mut self, selection: Selection) { if !self.enable_auto_relay { return; @@ -645,7 +644,7 @@ impl NetworkBehaviour for Behaviour { peer_info.relay_status = RelayStatus::NotSupported; // if there is a change in protocol support during an active reservation, // we should disconnect to remove the reservation - if matches!(previous_status, RelayStatus::Supported { .. }) { + if matches!(previous_status, RelayStatus::Supported { status: ReservationStatus::Active { .. } }) { self.events.push_back(ToSwarm::CloseConnection { peer_id, connection: CloseConnection::One(connection_id), From d96f357029e514fd30b5378a15cd457c812e7564 Mon Sep 17 00:00:00 2001 From: Darius Date: Thu, 4 Sep 2025 14:35:56 -0400 Subject: [PATCH 18/77] chore: fmt --- src/behaviour/autorelay.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/behaviour/autorelay.rs b/src/behaviour/autorelay.rs index 00c723d..d6fbc25 100644 --- a/src/behaviour/autorelay.rs +++ b/src/behaviour/autorelay.rs @@ -644,7 +644,12 @@ impl NetworkBehaviour for Behaviour { peer_info.relay_status = RelayStatus::NotSupported; // if there is a change in protocol support during an active reservation, // we should disconnect to remove the reservation - if matches!(previous_status, RelayStatus::Supported { status: ReservationStatus::Active { .. } }) { + if matches!( + previous_status, + RelayStatus::Supported { + status: ReservationStatus::Active { .. } + } + ) { self.events.push_back(ToSwarm::CloseConnection { peer_id, connection: CloseConnection::One(connection_id), From 53c21d192fb8a009ec16baaea21e8f43088d648c Mon Sep 17 00:00:00 2001 From: Darius Date: Sun, 7 Sep 2025 00:06:41 -0400 Subject: [PATCH 19/77] chore: optimize dialing by providing all static addresses for a peer in the dialopt --- src/behaviour/autorelay.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/behaviour/autorelay.rs b/src/behaviour/autorelay.rs index d6fbc25..dd49330 100644 --- a/src/behaviour/autorelay.rs +++ b/src/behaviour/autorelay.rs @@ -346,10 +346,8 @@ impl Behaviour { return; } for (peer_id, addrs) in self.static_relays.iter() { - for addr in addrs.iter().cloned() { - let opts = DialOpts::peer_id(*peer_id).addresses(vec![addr]).build(); - self.events.push_back(ToSwarm::Dial { opts }); - } + let opts = DialOpts::peer_id(*peer_id).addresses(Vec::from_iter(addrs.clone())).build(); + self.events.push_back(ToSwarm::Dial { opts }); } return; } From f78c29310c1ecffd724162a05b7e03e19cc0373b Mon Sep 17 00:00:00 2001 From: Darius Date: Sun, 7 Sep 2025 19:48:17 -0400 Subject: [PATCH 20/77] chore: add selector for lowest latency --- src/behaviour/autorelay.rs | 60 ++++++++++++++++++++++++++++++++------ src/task/ping.rs | 12 ++++++++ 2 files changed, 63 insertions(+), 9 deletions(-) diff --git a/src/behaviour/autorelay.rs b/src/behaviour/autorelay.rs index dd49330..9e5116a 100644 --- a/src/behaviour/autorelay.rs +++ b/src/behaviour/autorelay.rs @@ -95,6 +95,7 @@ struct PeerInfo { connection_id: ConnectionId, address: Multiaddr, relay_status: RelayStatus, + latency: [Duration; 5], } impl PeerInfo { @@ -112,6 +113,16 @@ impl PeerInfo { } } } + + pub fn average_latency(&self) -> u128 { + let avg: u128 = self + .latency + .iter() + .map(|duration| duration.as_millis()) + .sum(); + let div = self.latency.iter().filter(|i| !i.is_zero()).count() as u128; + avg / div + } } impl Hash for PeerInfo { @@ -188,14 +199,28 @@ impl Behaviour { } } - pub fn get_all_supported_targets(&self) -> impl Iterator { + pub fn get_all_supported_targets(&self) -> impl Iterator { self.info .iter() .filter(|(_, info)| matches!(info.relay_status, RelayStatus::Supported { .. })) - .map(|((peer_id, _), _)| peer_id) + .map(|((peer_id, connection_id), _)| (peer_id, connection_id)) + } + + pub fn set_peer_ping( + &mut self, + peer_id: PeerId, + connection_id: ConnectionId, + duration: Duration, + ) { + let Some(info) = self.info.get_mut(&(peer_id, connection_id)) else { + return; + }; + + info.latency.rotate_left(1); + info.latency[4] = duration; } - pub fn get_potential_targets(&self) -> impl Iterator { + fn get_potential_targets(&self) -> impl Iterator { self.info .iter() .filter(|(_, info)| { @@ -206,7 +231,7 @@ impl Behaviour { } ) }) - .map(|((peer_id, connection_id), _)| (peer_id, connection_id)) + .map(|((peer_id, connection_id), info)| (peer_id, connection_id, info)) } fn disable_reservation(&mut self, id: ListenerId) { @@ -346,7 +371,9 @@ impl Behaviour { return; } for (peer_id, addrs) in self.static_relays.iter() { - let opts = DialOpts::peer_id(*peer_id).addresses(Vec::from_iter(addrs.clone())).build(); + let opts = DialOpts::peer_id(*peer_id) + .addresses(Vec::from_iter(addrs.clone())) + .build(); self.events.push_back(ToSwarm::Dial { opts }); } return; @@ -371,7 +398,7 @@ impl Behaviour { let targets = self .get_potential_targets() - .map(|(peer_id, connection_id)| (*peer_id, *connection_id)) + .map(|(peer_id, connection_id, info)| (*peer_id, *connection_id, info)) .collect::>(); let pending_target_len = self.pending_target.len(); @@ -399,17 +426,31 @@ impl Behaviour { let new_targets = match selection { Selection::InOrder => targets .into_iter() + .map(|(peer_id, connection_id, _)| (peer_id, connection_id)) .take(remaining_targets_needed) .collect::>(), Selection::Random => targets .into_iter() + .map(|(peer_id, connection_id, _)| (peer_id, connection_id)) .choose_multiple(&mut rng, remaining_targets_needed), Selection::Peer(peer_id) => targets .into_iter() - .filter(|(id, _)| *id == peer_id) - .collect(), + .filter(|(id, _, _)| *id == peer_id) + .map(|(peer_id, connection_id, _)| (peer_id, connection_id)) + .collect::>(), Selection::LowestLatency => { - unimplemented!() + let mut targets = targets; + targets.sort_by(|(_, _, info1), (_, _, info2)| { + let avg1 = info1.average_latency(); + let avg2 = info2.average_latency(); + avg1.cmp(&avg2) + }); + + targets + .into_iter() + .take(remaining_targets_needed) + .map(|(peer_id, connection_id, _)| (peer_id, connection_id)) + .collect::>() } }; @@ -514,6 +555,7 @@ impl NetworkBehaviour for Behaviour { connection_id, address: addr, relay_status: RelayStatus::Pending, + latency: [Duration::ZERO; 5], }; // in the event that the address is from a peer going through a relay, automatically disqualify the connection diff --git a/src/task/ping.rs b/src/task/ping.rs index b4e0069..07e6532 100644 --- a/src/task/ping.rs +++ b/src/task/ping.rs @@ -20,6 +20,18 @@ where match result { Ok(duration) => { tracing::info!("ping to {} at {} took {:?}", peer, connection, duration); + + #[cfg(feature = "relay")] + if let Some(autorelay) = self + .swarm + .as_mut() + .expect("swarm valid") + .behaviour_mut() + .autorelay + .as_mut() + { + autorelay.set_peer_ping(peer, connection, duration); + } } Err(e) => { // TODO: Possibly disconnect peer since if there is an error? From 54392878b1e014b1a2890f2d5394bd2ddd389dd7 Mon Sep 17 00:00:00 2001 From: Darius Date: Mon, 8 Sep 2025 06:50:39 -0400 Subject: [PATCH 21/77] chore: use immutable access to static relays --- src/behaviour/autorelay.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/behaviour/autorelay.rs b/src/behaviour/autorelay.rs index 9e5116a..30deb52 100644 --- a/src/behaviour/autorelay.rs +++ b/src/behaviour/autorelay.rs @@ -508,14 +508,14 @@ impl NetworkBehaviour for Behaviour { _addresses: &[Multiaddr], _effective_role: Endpoint, ) -> Result, ConnectionDenied> { - let Some(addrs) = maybe_peer.and_then(|peer_id| self.static_relays.get_mut(&peer_id)) + // To prevent providing addresses from active connections, we will only focus on addresses added here that are considered to be fixed/static relays. + let Some(addrs) = maybe_peer + .and_then(|peer_id| self.static_relays.get(&peer_id).cloned()) + .map(Vec::from_iter) else { return Ok(vec![]); }; - // To prevent providing addresses from active connections, we will only focus on addresses added here that are considered to be fixed/static relays. - let addrs = addrs.iter().cloned().collect::>(); - Ok(addrs) } From 12ddfb62bd466ac8f0fa8e4f50c4ca8b549d3390 Mon Sep 17 00:00:00 2001 From: Darius Date: Mon, 8 Sep 2025 06:57:03 -0400 Subject: [PATCH 22/77] chore: remove connection id from struct --- src/behaviour/autorelay.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/behaviour/autorelay.rs b/src/behaviour/autorelay.rs index 30deb52..920f230 100644 --- a/src/behaviour/autorelay.rs +++ b/src/behaviour/autorelay.rs @@ -92,7 +92,6 @@ impl Default for Config { #[derive(Debug, Clone)] struct PeerInfo { - connection_id: ConnectionId, address: Multiaddr, relay_status: RelayStatus, latency: [Duration; 5], @@ -127,7 +126,6 @@ impl PeerInfo { impl Hash for PeerInfo { fn hash(&self, state: &mut H) { - self.connection_id.hash(state); self.address.hash(state); } } @@ -332,8 +330,7 @@ impl Behaviour { info.relay_status = RelayStatus::Supported { status: ReservationStatus::Pending { id }, }; - self.listener_to_info - .insert(id, (peer_id, info.connection_id)); + self.listener_to_info.insert(id, (peer_id, connection_id)); self.events.push_back(ToSwarm::ListenOn { opts }); self.pending_target.insert((peer_id, connection_id)); @@ -552,7 +549,6 @@ impl NetworkBehaviour for Behaviour { let addr = endpoint.get_remote_address().clone(); let mut info = PeerInfo { - connection_id, address: addr, relay_status: RelayStatus::Pending, latency: [Duration::ZERO; 5], From a9f34fa86b83af16bf3b37479972684e8c8356ba Mon Sep 17 00:00:00 2001 From: Darius Date: Mon, 8 Sep 2025 08:28:47 -0400 Subject: [PATCH 23/77] chore: add autorelay example --- Cargo.toml | 2 +- examples/autorelay/Cargo.toml | 11 ++++++ examples/autorelay/src/main.rs | 61 ++++++++++++++++++++++++++++++++++ 3 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 examples/autorelay/Cargo.toml create mode 100644 examples/autorelay/src/main.rs diff --git a/Cargo.toml b/Cargo.toml index 07561e5..5640106 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,7 +52,7 @@ websocket = ["libp2p/websocket", "rcgen", "libp2p/websocket-websys"] webtransport = ["libp2p/webtransport-websys"] [workspace] -members = ["examples/browser-webrtc", "examples/custom-behaviour-and-context", "examples/custom-transport", "examples/distributed-key-value-store", "examples/file-sharing", "examples/floodsub", "examples/gossipsub", "examples/ipfs-kad", "examples/peer-store", "examples/relay", "examples/rendezvous", "examples/stream", "examples/upnp"] +members = [ "examples/autorelay","examples/browser-webrtc", "examples/custom-behaviour-and-context", "examples/custom-transport", "examples/distributed-key-value-store", "examples/file-sharing", "examples/floodsub", "examples/gossipsub", "examples/ipfs-kad", "examples/peer-store", "examples/relay", "examples/rendezvous", "examples/stream", "examples/upnp"] [workspace.dependencies] async-rt = "0.1.8" diff --git a/examples/autorelay/Cargo.toml b/examples/autorelay/Cargo.toml new file mode 100644 index 0000000..387430e --- /dev/null +++ b/examples/autorelay/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "autorelay" +version = "0.1.0" +edition = "2024" + +[dependencies] +tokio.workspace = true +futures.workspace = true +connexa = { path = "../../", default-features = false, features = ["ed25519", "rsa", "tcp", "quic", "yamux", "noise", "relay", "ping", "identify", "testing", "kad", "dns"] } +clap = { version = "4.5.39", features = ["derive"] } +tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } diff --git a/examples/autorelay/src/main.rs b/examples/autorelay/src/main.rs new file mode 100644 index 0000000..e43c6d2 --- /dev/null +++ b/examples/autorelay/src/main.rs @@ -0,0 +1,61 @@ +use connexa::prelude::swarm::SwarmEvent; +use connexa::prelude::{DefaultConnexaBuilder, Multiaddr, PeerId}; + +pub const BOOTSTRAP_NODES: &[(&str, &str)] = &[ + ( + "/ip4/104.131.131.82/tcp/4001", + "QmaCpDMGvV2BGHeYERUEnRQAwe3N8SzbUtfsmvsqQLuvuJ", + ), + ( + "/dnsaddr/bootstrap.libp2p.io", + "QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN", + ), + ( + "/dnsaddr/bootstrap.libp2p.io", + "QmQCU2EcMqAqQPR2i9bChDtGNJchTbq5TbXJJ16u19uLTa", + ), + ( + "/dnsaddr/bootstrap.libp2p.io", + "QmbLHAnMoJPWSCR5Zhtx6BHJX9KiKNN6tpvbUcqanj75Nb", + ), + ( + "/dnsaddr/bootstrap.libp2p.io", + "QmcZf59bWwK5XFi76CZX8cbJ4BhTzzA3gU1ZjYZcYW3dwt", + ), +]; + +#[tokio::main] +async fn main() -> std::io::Result<()> { + let connexa = DefaultConnexaBuilder::new_identity() + .enable_tcp() + .enable_quic() + .enable_dns() + .with_relay() + .with_autorelay() + .with_ping() + .with_identify() + .with_kademlia() + .set_swarm_event_callback(|_swarm, event, ()| { + if matches!( + event, + SwarmEvent::NewListenAddr { .. } + | SwarmEvent::ListenerError { .. } + | SwarmEvent::ListenerClosed { .. } + ) { + println!("{event:?}"); + } + }) + .build()?; + + for (addr, peer_id) in BOOTSTRAP_NODES { + let peer_id: PeerId = peer_id.parse().expect("valid peer id"); + let addr: Multiaddr = addr.parse().expect("valid addr"); + connexa.dht().add_address(peer_id, addr).await?; + } + + connexa.dht().bootstrap().await?; + + tokio::time::sleep(std::time::Duration::from_secs(10)).await; + + Ok(()) +} From c47abf6d6fd89ca477d1e0f6566c269ed397bd0b Mon Sep 17 00:00:00 2001 From: Darius Date: Mon, 8 Sep 2025 08:29:26 -0400 Subject: [PATCH 24/77] chore: use ConnexaBuilder::with_autorelay_with_config within ConnexaBuilder::with_autorelay --- src/builder.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/builder.rs b/src/builder.rs index 6ba54a7..7b2eb1a 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -357,9 +357,8 @@ where /// Enable autorelay #[cfg(feature = "relay")] - pub fn with_autorelay(mut self) -> Self { - self.protocols.autorelay = true; - self + pub fn with_autorelay(self) -> Self { + self.with_autorelay_with_config(|config| config) } /// Enable autorelay From df3722be676b146e70e31284786f6d869ffc1add Mon Sep 17 00:00:00 2001 From: Darius Date: Tue, 9 Sep 2025 05:18:55 -0400 Subject: [PATCH 25/77] chore: add additional pending check --- src/behaviour/autorelay.rs | 48 ++++++++++++++++++++++++-------------- 1 file changed, 31 insertions(+), 17 deletions(-) diff --git a/src/behaviour/autorelay.rs b/src/behaviour/autorelay.rs index 920f230..7de8bba 100644 --- a/src/behaviour/autorelay.rs +++ b/src/behaviour/autorelay.rs @@ -393,20 +393,33 @@ impl Behaviour { return; } + let pending_targets = self + .info + .iter() + .filter(|(_, info)| { + matches!( + info.relay_status, + RelayStatus::Supported { + status: ReservationStatus::Pending { .. } + } + ) + }) + .count(); + + if pending_targets == max { + return; + } + + let max = max - relayed_targets; + let targets = self .get_potential_targets() .map(|(peer_id, connection_id, info)| (*peer_id, *connection_id, info)) .collect::>(); - let pending_target_len = self.pending_target.len(); - - if pending_target_len >= max { - return; - } - let targets_count = std::cmp::min(targets.len(), max); - if targets_count == 0 { + if targets_count == 0 || max == 0 { return; } @@ -418,18 +431,19 @@ impl Behaviour { return; } - let mut rng = rand::thread_rng(); - let new_targets = match selection { Selection::InOrder => targets .into_iter() .map(|(peer_id, connection_id, _)| (peer_id, connection_id)) .take(remaining_targets_needed) .collect::>(), - Selection::Random => targets - .into_iter() - .map(|(peer_id, connection_id, _)| (peer_id, connection_id)) - .choose_multiple(&mut rng, remaining_targets_needed), + Selection::Random => { + let mut rng = rand::thread_rng(); + targets + .into_iter() + .map(|(peer_id, connection_id, _)| (peer_id, connection_id)) + .choose_multiple(&mut rng, remaining_targets_needed) + } Selection::Peer(peer_id) => targets .into_iter() .filter(|(id, _, _)| *id == peer_id) @@ -452,13 +466,13 @@ impl Behaviour { }; for (peer_id, connection_id) in new_targets { - if !self.select_connection_for_reservation(peer_id, connection_id) { - continue; - } - if self.pending_target.len() == max { break; } + + if !self.select_connection_for_reservation(peer_id, connection_id) { + continue; + } } assert!(self.pending_target.len() <= max); From 1392967c9dcd520c7f9f51410065995dc9b29102 Mon Sep 17 00:00:00 2001 From: Darius Date: Tue, 9 Sep 2025 15:36:46 -0400 Subject: [PATCH 26/77] chore: update example to grab external addresses --- examples/autorelay/src/main.rs | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/examples/autorelay/src/main.rs b/examples/autorelay/src/main.rs index e43c6d2..6bc85e0 100644 --- a/examples/autorelay/src/main.rs +++ b/examples/autorelay/src/main.rs @@ -35,16 +35,6 @@ async fn main() -> std::io::Result<()> { .with_ping() .with_identify() .with_kademlia() - .set_swarm_event_callback(|_swarm, event, ()| { - if matches!( - event, - SwarmEvent::NewListenAddr { .. } - | SwarmEvent::ListenerError { .. } - | SwarmEvent::ListenerClosed { .. } - ) { - println!("{event:?}"); - } - }) .build()?; for (addr, peer_id) in BOOTSTRAP_NODES { @@ -53,9 +43,13 @@ async fn main() -> std::io::Result<()> { connexa.dht().add_address(peer_id, addr).await?; } - connexa.dht().bootstrap().await?; - tokio::time::sleep(std::time::Duration::from_secs(10)).await; + let external_addrs = connexa.swarm().external_addresses().await?; + + for addr in external_addrs { + println!("- {}", addr); + } + Ok(()) } From 01784aa5fdda854fb8f790b3c10c5589079e9be2 Mon Sep 17 00:00:00 2001 From: Darius Date: Tue, 9 Sep 2025 15:37:07 -0400 Subject: [PATCH 27/77] chore: remove pending_target --- src/behaviour/autorelay.rs | 60 ++++++++++++++++++++++---------------- 1 file changed, 35 insertions(+), 25 deletions(-) diff --git a/src/behaviour/autorelay.rs b/src/behaviour/autorelay.rs index 7de8bba..4d3d769 100644 --- a/src/behaviour/autorelay.rs +++ b/src/behaviour/autorelay.rs @@ -37,7 +37,6 @@ pub struct Behaviour { listener_to_info: IndexMap, events: VecDeque::ToSwarm, THandlerInEvent>>, external_addresses: ExternalAddresses, - pending_target: IndexSet<(PeerId, ConnectionId)>, capacity_cleanup: Delay, max_reservation: NonZeroU8, override_autorelay: bool, @@ -53,7 +52,6 @@ impl Default for Behaviour { static_relays: IndexMap::new(), listener_to_info: IndexMap::new(), events: VecDeque::new(), - pending_target: IndexSet::new(), capacity_cleanup: Delay::new(CLEANUP_INTERVAL), external_addresses: ExternalAddresses::default(), override_autorelay: false, @@ -204,6 +202,24 @@ impl Behaviour { .map(|((peer_id, connection_id), _)| (peer_id, connection_id)) } + fn get_pending_reservations(&self) -> impl Iterator { + self.info + .iter() + .filter(|(_, info)| { + matches!( + info.relay_status, + RelayStatus::Supported { + status: ReservationStatus::Pending { .. } + } + ) + }) + .map(|((peer_id, connection_id), _)| (peer_id, connection_id)) + } + + fn get_pending_reservations_count(&self) -> usize { + self.get_pending_reservations().count() + } + pub fn set_peer_ping( &mut self, peer_id: PeerId, @@ -256,7 +272,6 @@ impl Behaviour { info.relay_status = RelayStatus::Supported { status: ReservationStatus::Idle, }; - self.pending_target.shift_remove(&(peer_id, connection_id)); } RelayStatus::Pending | RelayStatus::Supported { @@ -299,7 +314,19 @@ impl Behaviour { peer_id: PeerId, connection_id: ConnectionId, ) -> bool { - if self.pending_target.contains(&(peer_id, connection_id)) { + if self + .info + .get(&(peer_id, connection_id)) + .is_some_and(|info| { + matches!( + info.relay_status, + RelayStatus::Supported { + status: ReservationStatus::Pending { .. } + | ReservationStatus::Active { .. } + } + ) + }) + { return false; } @@ -332,7 +359,6 @@ impl Behaviour { }; self.listener_to_info.insert(id, (peer_id, connection_id)); self.events.push_back(ToSwarm::ListenOn { opts }); - self.pending_target.insert((peer_id, connection_id)); true } @@ -393,18 +419,7 @@ impl Behaviour { return; } - let pending_targets = self - .info - .iter() - .filter(|(_, info)| { - matches!( - info.relay_status, - RelayStatus::Supported { - status: ReservationStatus::Pending { .. } - } - ) - }) - .count(); + let pending_targets = self.get_pending_reservations_count(); if pending_targets == max { return; @@ -424,7 +439,7 @@ impl Behaviour { } let remaining_targets_needed = targets_count - .checked_sub(self.pending_target.len()) + .checked_sub(pending_targets) .unwrap_or_default(); if remaining_targets_needed == 0 { @@ -466,7 +481,7 @@ impl Behaviour { }; for (peer_id, connection_id) in new_targets { - if self.pending_target.len() == max { + if self.get_pending_reservations_count() == max { break; } @@ -475,7 +490,7 @@ impl Behaviour { } } - assert!(self.pending_target.len() <= max); + assert!(self.get_pending_reservations_count() <= max); } } @@ -655,11 +670,6 @@ impl NetworkBehaviour for Behaviour { info.relay_status = RelayStatus::Supported { status: ReservationStatus::Active { id }, }; - - debug_assert!( - self.pending_target - .shift_remove(&(*peer_id, *connection_id)) - ); } FromSwarm::ExpiredListenAddr(ExpiredListenAddr { listener_id, .. }) | FromSwarm::ListenerError(ListenerError { listener_id, .. }) From 159c4d71288bf880b576ee601f5d46deab9b90d4 Mon Sep 17 00:00:00 2001 From: Darius Date: Fri, 12 Sep 2025 05:38:44 -0400 Subject: [PATCH 28/77] chore: add relay namespace for future relay discovery --- src/task/relay.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/task/relay.rs b/src/task/relay.rs index fbc715f..b0ff205 100644 --- a/src/task/relay.rs +++ b/src/task/relay.rs @@ -5,6 +5,9 @@ use libp2p::relay::{Event as RelayServerEvent, client::Event as RelayClientEvent use libp2p::swarm::NetworkBehaviour; use std::fmt::Debug; +#[allow(dead_code)] +pub const RELAY_NAMESPACE: &[u8] = b"/libp2p/relay"; + impl ConnexaTask where X: Default + Send + 'static, From 4e56fafb50bad3bbff1c9ebaf4e6972e433edb25 Mon Sep 17 00:00:00 2001 From: Darius Date: Sat, 13 Sep 2025 23:07:58 -0400 Subject: [PATCH 29/77] chore: attempt to remove listener if status change instead of disconnecting. --- src/behaviour/autorelay.rs | 25 +++++++------------------ 1 file changed, 7 insertions(+), 18 deletions(-) diff --git a/src/behaviour/autorelay.rs b/src/behaviour/autorelay.rs index 4d3d769..5558ff3 100644 --- a/src/behaviour/autorelay.rs +++ b/src/behaviour/autorelay.rs @@ -703,24 +703,13 @@ impl NetworkBehaviour for Behaviour { let previous_status = peer_info.relay_status; peer_info.relay_status = RelayStatus::NotSupported; // if there is a change in protocol support during an active reservation, - // we should disconnect to remove the reservation - if matches!( - previous_status, - RelayStatus::Supported { - status: ReservationStatus::Active { .. } - } - ) { - self.events.push_back(ToSwarm::CloseConnection { - peer_id, - connection: CloseConnection::One(connection_id), - }); - - // if infos.len() == 1 { - // // TODO: Determine if we should reconnect if this is the only connection - // let addr = peer_info.address.clone(); - // let opts = DialOpts::peer_id(peer_id).addresses(vec![addr]).build(); - // self.events.push_back(ToSwarm::Dial { opts }); - // } + // we should remove the reservation if its not already removed + + if let RelayStatus::Supported { + status: ReservationStatus::Active { id }, + } = previous_status + { + self.events.push_back(ToSwarm::RemoveListener { id }); } } } From 28348e9ef9b590036c68e8c925b84110a94e2cc2 Mon Sep 17 00:00:00 2001 From: Darius Date: Sun, 14 Sep 2025 13:53:47 -0400 Subject: [PATCH 30/77] chore: add functions to add and list static relays --- src/behaviour/autorelay.rs | 15 +++++++++++++++ src/handle/relay.rs | 20 ++++++++++++++++++++ src/task/relay.rs | 18 ++++++++++++++++++ src/types.rs | 7 +++++++ 4 files changed, 60 insertions(+) diff --git a/src/behaviour/autorelay.rs b/src/behaviour/autorelay.rs index 5558ff3..ece291c 100644 --- a/src/behaviour/autorelay.rs +++ b/src/behaviour/autorelay.rs @@ -180,6 +180,21 @@ impl Behaviour { removed } + pub fn list_static_relays(&self) -> Vec<(PeerId, Vec)> { + self.static_relays + .iter() + .map(|(peer_id, addrs)| (*peer_id, Vec::from_iter(addrs.clone()))) + .collect() + } + + pub fn get_static_relay_addrs(&self, peer_id: PeerId) -> Vec { + let Some(addrs) = self.static_relays.get(&peer_id) else { + return vec![]; + }; + + Vec::from_iter(addrs.clone()) + } + pub fn enable_autorelay(&mut self) { self.enable_auto_relay = true; self.meet_reservation_target(Selection::Random); diff --git a/src/handle/relay.rs b/src/handle/relay.rs index 3579ca8..62d9767 100644 --- a/src/handle/relay.rs +++ b/src/handle/relay.rs @@ -50,6 +50,26 @@ where rx.await.map_err(io::Error::other)? } + pub async fn list_static_relays(&self) -> io::Result)>> { + let (tx, rx) = oneshot::channel(); + self.connexa + .to_task + .clone() + .send(AutoRelayCommand::ListStaticRelays { resp: tx }.into()) + .await?; + rx.await.map_err(io::Error::other)? + } + + pub async fn get_static_relay(&self, peer_id: PeerId) -> io::Result> { + let (tx, rx) = oneshot::channel(); + self.connexa + .to_task + .clone() + .send(AutoRelayCommand::GetStaticRelay { peer_id, resp: tx }.into()) + .await?; + rx.await.map_err(io::Error::other)? + } + pub async fn enable_auto_relay(&self) -> io::Result<()> { let (tx, rx) = oneshot::channel(); self.connexa diff --git a/src/task/relay.rs b/src/task/relay.rs index b0ff205..2dfc7d9 100644 --- a/src/task/relay.rs +++ b/src/task/relay.rs @@ -42,6 +42,24 @@ where let _ = resp.send(Ok(autorelay.remove_static_relay(peer_id, relay_addr))); } + AutoRelayCommand::ListStaticRelays { resp } => { + let Some(autorelay) = swarm.behaviour_mut().autorelay.as_mut() else { + let _ = resp.send(Err(std::io::Error::other("autorelay is not enabled"))); + return; + }; + + let list = autorelay.list_static_relays(); + let _ = resp.send(Ok(list)); + } + AutoRelayCommand::GetStaticRelay { peer_id, resp } => { + let Some(autorelay) = swarm.behaviour_mut().autorelay.as_mut() else { + let _ = resp.send(Err(std::io::Error::other("autorelay is not enabled"))); + return; + }; + + let addrs = autorelay.get_static_relay_addrs(peer_id); + let _ = resp.send(Ok(addrs)); + } AutoRelayCommand::EnableAutoRelay { resp } => { let Some(autorelay) = swarm.behaviour_mut().autorelay.as_mut() else { let _ = resp.send(Err(std::io::Error::other("autorelay is not enabled"))); diff --git a/src/types.rs b/src/types.rs index 3ecccd3..ac5e5d2 100644 --- a/src/types.rs +++ b/src/types.rs @@ -409,6 +409,13 @@ pub enum AutoRelayCommand { relay_addr: Multiaddr, resp: oneshot::Sender>, }, + ListStaticRelays { + resp: oneshot::Sender)>>>, + }, + GetStaticRelay { + peer_id: PeerId, + resp: oneshot::Sender>>, + }, EnableAutoRelay { resp: oneshot::Sender>, }, From 60df9dbc49e5139990dff38ac62cd46623527fd0 Mon Sep 17 00:00:00 2001 From: Darius Date: Mon, 15 Sep 2025 04:28:04 -0400 Subject: [PATCH 31/77] chore: add function to disable active relays --- src/behaviour/autorelay.rs | 7 +++++++ src/handle/relay.rs | 10 ++++++++++ src/task/relay.rs | 10 ++++++++++ src/types.rs | 3 +++ 4 files changed, 30 insertions(+) diff --git a/src/behaviour/autorelay.rs b/src/behaviour/autorelay.rs index ece291c..f932fdb 100644 --- a/src/behaviour/autorelay.rs +++ b/src/behaviour/autorelay.rs @@ -210,6 +210,13 @@ impl Behaviour { } } + pub fn disable_all_relays(&mut self) { + self.disable_all_reservations(); + if let Some(waker) = self.waker.take() { + waker.wake(); + } + } + pub fn get_all_supported_targets(&self) -> impl Iterator { self.info .iter() diff --git a/src/handle/relay.rs b/src/handle/relay.rs index 62d9767..5e332be 100644 --- a/src/handle/relay.rs +++ b/src/handle/relay.rs @@ -89,4 +89,14 @@ where .await?; rx.await.map_err(io::Error::other)? } + + pub async fn disable_relaya(&self) -> io::Result<()> { + let (tx, rx) = oneshot::channel(); + self.connexa + .to_task + .clone() + .send(AutoRelayCommand::DisableRelays { resp: tx }.into()) + .await?; + rx.await.map_err(io::Error::other)? + } } diff --git a/src/task/relay.rs b/src/task/relay.rs index 2dfc7d9..1259d0a 100644 --- a/src/task/relay.rs +++ b/src/task/relay.rs @@ -78,6 +78,16 @@ where autorelay.disable_autorelay(); + let _ = resp.send(Ok(())); + } + AutoRelayCommand::DisableRelays { resp } => { + let Some(autorelay) = swarm.behaviour_mut().autorelay.as_mut() else { + let _ = resp.send(Err(std::io::Error::other("autorelay is not enabled"))); + return; + }; + + autorelay.disable_autorelay(); + let _ = resp.send(Ok(())); } } diff --git a/src/types.rs b/src/types.rs index ac5e5d2..08b646e 100644 --- a/src/types.rs +++ b/src/types.rs @@ -409,6 +409,9 @@ pub enum AutoRelayCommand { relay_addr: Multiaddr, resp: oneshot::Sender>, }, + DisableRelays { + resp: oneshot::Sender>, + }, ListStaticRelays { resp: oneshot::Sender)>>>, }, From 883954c625ce1daaddc1fd55146da0ff0c89079e Mon Sep 17 00:00:00 2001 From: Darius Date: Mon, 15 Sep 2025 11:20:42 -0400 Subject: [PATCH 32/77] chore: add logging --- src/behaviour/autorelay.rs | 24 +++++++++++++++++++++++- src/behaviour/peer_store/store/memory.rs | 2 +- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/src/behaviour/autorelay.rs b/src/behaviour/autorelay.rs index f932fdb..249f148 100644 --- a/src/behaviour/autorelay.rs +++ b/src/behaviour/autorelay.rs @@ -312,6 +312,7 @@ impl Behaviour { for (listener_id, peer_id, connection_id) in relay_listeners { let Some(connection) = self.info.get_mut(&(peer_id, connection_id)) else { + tracing::warn!(%peer_id, %connection_id, "connection not found when it should have been present. skipping"); continue; }; @@ -328,6 +329,7 @@ impl Behaviour { self.events .push_back(ToSwarm::RemoveListener { id: listener_id }); + tracing::info!(%peer_id, %connection_id, ?listener_id, "removing relay listener"); } } @@ -349,6 +351,7 @@ impl Behaviour { ) }) { + tracing::warn!(%peer_id, %connection_id, "connection already has a reservation or pending reservation. skipping"); return false; } @@ -374,14 +377,16 @@ impl Behaviour { let opts = ListenOpts::new(relay_addr); + let addr = opts.address(); let id = opts.listener_id(); + tracing::info!(%peer_id, %connection_id, %addr, ?id, "new pending reservation"); + info.relay_status = RelayStatus::Supported { status: ReservationStatus::Pending { id }, }; self.listener_to_info.insert(id, (peer_id, connection_id)); self.events.push_back(ToSwarm::ListenOn { opts }); - true } @@ -398,6 +403,7 @@ impl Behaviour { .any(|addr| addr.is_public() && !addr.is_relayed()) && !self.override_autorelay { + tracing::trace!("local node reachable. autorelay will not run"); return; } @@ -413,6 +419,7 @@ impl Behaviour { if self.static_relays.is_empty() { // TODO: Emit an event informing swarm about being in need of relays? // however this would require separate functions to add relays to the autorelay state and possibly confirm if theres any existing connections + tracing::warn!("no relays present."); return; } for (peer_id, addrs) in self.static_relays.iter() { @@ -438,12 +445,14 @@ impl Behaviour { .count(); if relayed_targets == max { + tracing::warn!("max reservation reached. no more reservations will be made"); return; } let pending_targets = self.get_pending_reservations_count(); if pending_targets == max { + tracing::warn!("pending targets reached max target."); return; } @@ -457,6 +466,7 @@ impl Behaviour { let targets_count = std::cmp::min(targets.len(), max); if targets_count == 0 || max == 0 { + tracing::warn!("no potential targets to meet reservation target."); return; } @@ -465,6 +475,7 @@ impl Behaviour { .unwrap_or_default(); if remaining_targets_needed == 0 { + tracing::warn!("no potential targets to meet reservation target."); return; } @@ -575,6 +586,7 @@ impl NetworkBehaviour for Behaviour { .iter() .any(|addr| addr.is_public() && !addr.is_relayed()) { + tracing::info!("local node is reachable. disabling autorelay"); self.override_autorelay = true; self.disable_all_reservations(); self.backoff.take(); @@ -584,6 +596,7 @@ impl NetworkBehaviour for Behaviour { .iter() .any(|addr| !addr.is_public() || addr.is_relayed()) { + tracing::info!("local node is not reachable. enabling autorelay"); self.backoff.replace(Delay::new(BACKOFF_INTERVAL)); self.override_autorelay = false; } @@ -599,6 +612,8 @@ impl NetworkBehaviour for Behaviour { }) => { let addr = endpoint.get_remote_address().clone(); + tracing::trace!(%peer_id, %connection_id, %addr, "connection established"); + let mut info = PeerInfo { address: addr, relay_status: RelayStatus::Pending, @@ -626,6 +641,7 @@ impl NetworkBehaviour for Behaviour { connection_id, .. }) => { + tracing::trace!(%peer_id, %connection_id, "connection closed"); self.info.shift_remove(&(peer_id, connection_id)); if let Some(listener_id) = self @@ -667,6 +683,7 @@ impl NetworkBehaviour for Behaviour { .expect("connection is present"); info.address = new_addr.clone(); + tracing::trace!(%peer_id, %connection_id, %old_addr, %new_addr, "address changed"); } FromSwarm::NewListenAddr(NewListenAddr { listener_id, addr }) => { // we only care about any new relayed address @@ -679,6 +696,7 @@ impl NetworkBehaviour for Behaviour { }; let Some(info) = self.info.get_mut(&(*peer_id, *connection_id)) else { + tracing::warn!(%peer_id, %connection_id, "connection not found when it should have been present. skipping"); return; }; @@ -686,12 +704,15 @@ impl NetworkBehaviour for Behaviour { status: ReservationStatus::Pending { id }, } = info.relay_status else { + tracing::warn!(%peer_id, %connection_id, "connection doesnt have a pending reservation. skipping"); return; }; info.relay_status = RelayStatus::Supported { status: ReservationStatus::Active { id }, }; + + tracing::info!(%peer_id, %connection_id, %addr, %id, "active reservation with relay"); } FromSwarm::ExpiredListenAddr(ExpiredListenAddr { listener_id, .. }) | FromSwarm::ListenerError(ListenerError { listener_id, .. }) @@ -746,6 +767,7 @@ impl NetworkBehaviour for Behaviour { } if self.backoff.poll_unpin(cx).is_ready() { + tracing::debug!("attempting to meet reservation target after node became unreachable"); self.meet_reservation_target(Selection::InOrder); } diff --git a/src/behaviour/peer_store/store/memory.rs b/src/behaviour/peer_store/store/memory.rs index 6348e3a..d6ba14b 100644 --- a/src/behaviour/peer_store/store/memory.rs +++ b/src/behaviour/peer_store/store/memory.rs @@ -156,7 +156,7 @@ impl Store for MemoryStore { .entry(*peer_id) .or_default() .insert(remote_addr.clone()); - + self.timer.remove(&(*peer_id, remote_addr)); // TODO: determine if we should remove any failed addresses from the store to keep the entry up to date? } From f24f754ced9b3749a4ae8ca062a1e97957bd0917 Mon Sep 17 00:00:00 2001 From: Darius Date: Tue, 16 Sep 2025 07:45:46 -0400 Subject: [PATCH 33/77] chore: remove unused import --- src/behaviour/autorelay.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/behaviour/autorelay.rs b/src/behaviour/autorelay.rs index 249f148..8e73adc 100644 --- a/src/behaviour/autorelay.rs +++ b/src/behaviour/autorelay.rs @@ -5,9 +5,8 @@ use crate::behaviour::dummy; use crate::multiaddr_ext::MultiaddrExt; use crate::prelude::swarm::derive_prelude::{ConnectionEstablished, PortUse}; use crate::prelude::swarm::{ - AddressChange, CloseConnection, ConnectionClosed, ConnectionDenied, DialFailure, - ExpiredListenAddr, FromSwarm, ListenerClosed, ListenerError, THandler, THandlerInEvent, - THandlerOutEvent, ToSwarm, + AddressChange, ConnectionClosed, ConnectionDenied, DialFailure, ExpiredListenAddr, FromSwarm, + ListenerClosed, ListenerError, THandler, THandlerInEvent, THandlerOutEvent, ToSwarm, }; use crate::prelude::transport::Endpoint; use either::Either; From 8d00bfac7fa41b94e12d55982196e97b8a2138f3 Mon Sep 17 00:00:00 2001 From: Darius Date: Fri, 19 Sep 2025 09:04:31 -0500 Subject: [PATCH 34/77] chore: return early if address is not disqualified --- src/behaviour/autorelay.rs | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/behaviour/autorelay.rs b/src/behaviour/autorelay.rs index 8e73adc..b9b624c 100644 --- a/src/behaviour/autorelay.rs +++ b/src/behaviour/autorelay.rs @@ -623,15 +623,16 @@ impl NetworkBehaviour for Behaviour { // from being used as a potential relay since there is no support for multi-HOP if info.check_for_disqualifying_address() { self.info.insert((peer_id, connection_id), info); - } else { - match self.static_relays.get(&peer_id) { - Some(addrs) if addrs.contains(&info.address) => { - // prioritize static relays so it would have a higher chance of being selected first - self.info.insert_before(0, (peer_id, connection_id), info); - } - _ => { - self.info.insert((peer_id, connection_id), info); - } + return; + } + + match self.static_relays.get(&peer_id) { + Some(addrs) if addrs.contains(&info.address) => { + // prioritize static relays so it would have a higher chance of being selected first + self.info.insert_before(0, (peer_id, connection_id), info); + } + _ => { + self.info.insert((peer_id, connection_id), info); } } } From d171c459c385ee1d70d9ec2928fa78cc6e0f4fea Mon Sep 17 00:00:00 2001 From: Darius Date: Fri, 26 Sep 2025 10:20:54 -0500 Subject: [PATCH 35/77] chore: rename struct field for clarity --- src/behaviour/autorelay.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/behaviour/autorelay.rs b/src/behaviour/autorelay.rs index b9b624c..c7ed02c 100644 --- a/src/behaviour/autorelay.rs +++ b/src/behaviour/autorelay.rs @@ -33,7 +33,7 @@ const BACKOFF_INTERVAL: Duration = Duration::from_secs(5); pub struct Behaviour { info: IndexMap<(PeerId, ConnectionId), PeerInfo>, static_relays: IndexMap>, - listener_to_info: IndexMap, + connection_reservation: IndexMap, events: VecDeque::ToSwarm, THandlerInEvent>>, external_addresses: ExternalAddresses, capacity_cleanup: Delay, @@ -49,7 +49,7 @@ impl Default for Behaviour { Self { info: IndexMap::new(), static_relays: IndexMap::new(), - listener_to_info: IndexMap::new(), + connection_reservation: IndexMap::new(), events: VecDeque::new(), capacity_cleanup: Delay::new(CLEANUP_INTERVAL), external_addresses: ExternalAddresses::default(), @@ -270,7 +270,7 @@ impl Behaviour { } fn disable_reservation(&mut self, id: ListenerId) { - let Some((peer_id, connection_id)) = self.listener_to_info.shift_remove(&id) else { + let Some((peer_id, connection_id)) = self.connection_reservation.shift_remove(&id) else { return; }; @@ -304,7 +304,7 @@ impl Behaviour { fn disable_all_reservations(&mut self) { let relay_listeners = self - .listener_to_info + .connection_reservation .iter() .map(|(id, (peer_id, conn_id))| (*id, *peer_id, *conn_id)) .collect::>(); @@ -384,7 +384,7 @@ impl Behaviour { info.relay_status = RelayStatus::Supported { status: ReservationStatus::Pending { id }, }; - self.listener_to_info.insert(id, (peer_id, connection_id)); + self.connection_reservation.insert(id, (peer_id, connection_id)); self.events.push_back(ToSwarm::ListenOn { opts }); true } @@ -645,12 +645,12 @@ impl NetworkBehaviour for Behaviour { self.info.shift_remove(&(peer_id, connection_id)); if let Some(listener_id) = self - .listener_to_info + .connection_reservation .iter() .find(|(_, (peer, conn_id))| peer_id.eq(peer) && connection_id.eq(conn_id)) .map(|(id, _)| *id) { - self.listener_to_info.shift_remove(&listener_id); + self.connection_reservation.shift_remove(&listener_id); } } FromSwarm::DialFailure(DialFailure { @@ -691,7 +691,7 @@ impl NetworkBehaviour for Behaviour { return; } - let Some((peer_id, connection_id)) = self.listener_to_info.get(&listener_id) else { + let Some((peer_id, connection_id)) = self.connection_reservation.get(&listener_id) else { return; }; From 3871f242f38673229ef20e5fb7fae4e75853df7a Mon Sep 17 00:00:00 2001 From: Darius Date: Fri, 26 Sep 2025 10:21:10 -0500 Subject: [PATCH 36/77] chore: rename struct field for clarity --- src/behaviour/autorelay.rs | 48 +++++++++++++++++++------------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/src/behaviour/autorelay.rs b/src/behaviour/autorelay.rs index c7ed02c..da52f25 100644 --- a/src/behaviour/autorelay.rs +++ b/src/behaviour/autorelay.rs @@ -31,7 +31,7 @@ const CLEANUP_INTERVAL: Duration = Duration::from_secs(60); const BACKOFF_INTERVAL: Duration = Duration::from_secs(5); pub struct Behaviour { - info: IndexMap<(PeerId, ConnectionId), PeerInfo>, + connections: IndexMap<(PeerId, ConnectionId), PeerInfo>, static_relays: IndexMap>, connection_reservation: IndexMap, events: VecDeque::ToSwarm, THandlerInEvent>>, @@ -47,7 +47,7 @@ pub struct Behaviour { impl Default for Behaviour { fn default() -> Self { Self { - info: IndexMap::new(), + connections: IndexMap::new(), static_relays: IndexMap::new(), connection_reservation: IndexMap::new(), events: VecDeque::new(), @@ -217,14 +217,14 @@ impl Behaviour { } pub fn get_all_supported_targets(&self) -> impl Iterator { - self.info + self.connections .iter() .filter(|(_, info)| matches!(info.relay_status, RelayStatus::Supported { .. })) .map(|((peer_id, connection_id), _)| (peer_id, connection_id)) } fn get_pending_reservations(&self) -> impl Iterator { - self.info + self.connections .iter() .filter(|(_, info)| { matches!( @@ -247,7 +247,7 @@ impl Behaviour { connection_id: ConnectionId, duration: Duration, ) { - let Some(info) = self.info.get_mut(&(peer_id, connection_id)) else { + let Some(info) = self.connections.get_mut(&(peer_id, connection_id)) else { return; }; @@ -256,7 +256,7 @@ impl Behaviour { } fn get_potential_targets(&self) -> impl Iterator { - self.info + self.connections .iter() .filter(|(_, info)| { matches!( @@ -274,7 +274,7 @@ impl Behaviour { return; }; - let Some(info) = self.info.get_mut(&(peer_id, connection_id)) else { + let Some(info) = self.connections.get_mut(&(peer_id, connection_id)) else { return; }; @@ -310,7 +310,7 @@ impl Behaviour { .collect::>(); for (listener_id, peer_id, connection_id) in relay_listeners { - let Some(connection) = self.info.get_mut(&(peer_id, connection_id)) else { + let Some(connection) = self.connections.get_mut(&(peer_id, connection_id)) else { tracing::warn!(%peer_id, %connection_id, "connection not found when it should have been present. skipping"); continue; }; @@ -338,7 +338,7 @@ impl Behaviour { connection_id: ConnectionId, ) -> bool { if self - .info + .connections .get(&(peer_id, connection_id)) .is_some_and(|info| { matches!( @@ -354,13 +354,13 @@ impl Behaviour { return false; } - if self.info.is_empty() { + if self.connections.is_empty() { tracing::warn!(%peer_id, "no connections present. removing entry"); return false; } let info = self - .info + .connections .get_mut(&(peer_id, connection_id)) .expect("connection is present"); @@ -408,9 +408,9 @@ impl Behaviour { let max = self.max_reservation.get() as usize; - let peers_not_supported = self.info.is_empty() + let peers_not_supported = self.connections.is_empty() || self - .info + .connections .iter() .all(|(_, info)| info.relay_status == RelayStatus::NotSupported); @@ -431,7 +431,7 @@ impl Behaviour { } let relayed_targets = self - .info + .connections .iter() .filter(|(_, info)| { matches!( @@ -622,17 +622,17 @@ impl NetworkBehaviour for Behaviour { // in the event that the address is from a peer going through a relay, automatically disqualify the connection // from being used as a potential relay since there is no support for multi-HOP if info.check_for_disqualifying_address() { - self.info.insert((peer_id, connection_id), info); + self.connections.insert((peer_id, connection_id), info); return; } match self.static_relays.get(&peer_id) { Some(addrs) if addrs.contains(&info.address) => { // prioritize static relays so it would have a higher chance of being selected first - self.info.insert_before(0, (peer_id, connection_id), info); + self.connections.insert_before(0, (peer_id, connection_id), info); } _ => { - self.info.insert((peer_id, connection_id), info); + self.connections.insert((peer_id, connection_id), info); } } } @@ -642,7 +642,7 @@ impl NetworkBehaviour for Behaviour { .. }) => { tracing::trace!(%peer_id, %connection_id, "connection closed"); - self.info.shift_remove(&(peer_id, connection_id)); + self.connections.shift_remove(&(peer_id, connection_id)); if let Some(listener_id) = self .connection_reservation @@ -664,7 +664,7 @@ impl NetworkBehaviour for Behaviour { return; }; - self.info.shift_remove(&(peer_id, connection_id)); + self.connections.shift_remove(&(peer_id, connection_id)); } FromSwarm::AddressChange(AddressChange { peer_id, @@ -678,7 +678,7 @@ impl NetworkBehaviour for Behaviour { debug_assert!(old_addr != new_addr); let info = self - .info + .connections .get_mut(&(peer_id, connection_id)) .expect("connection is present"); @@ -695,7 +695,7 @@ impl NetworkBehaviour for Behaviour { return; }; - let Some(info) = self.info.get_mut(&(*peer_id, *connection_id)) else { + let Some(info) = self.connections.get_mut(&(*peer_id, *connection_id)) else { tracing::warn!(%peer_id, %connection_id, "connection not found when it should have been present. skipping"); return; }; @@ -731,7 +731,7 @@ impl NetworkBehaviour for Behaviour { ) { let Either::Left(event) = event; - let Some(peer_info) = self.info.get_mut(&(peer_id, connection_id)) else { + let Some(peer_info) = self.connections.get_mut(&(peer_id, connection_id)) else { return; }; @@ -778,9 +778,9 @@ impl NetworkBehaviour for Behaviour { self.events.shrink_to_fit(); } - if (self.info.is_empty() || self.info.len() < MAX_CAP) && self.info.capacity() > MAX_CAP + if (self.connections.is_empty() || self.connections.len() < MAX_CAP) && self.connections.capacity() > MAX_CAP { - self.info.shrink_to_fit(); + self.connections.shrink_to_fit(); } self.capacity_cleanup.reset(CLEANUP_INTERVAL); From a0fc9516046d67871ce9bae52954de0721783e79 Mon Sep 17 00:00:00 2001 From: Darius Date: Fri, 26 Sep 2025 10:21:18 -0500 Subject: [PATCH 37/77] chore: remove unused import --- examples/autorelay/src/main.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/autorelay/src/main.rs b/examples/autorelay/src/main.rs index 6bc85e0..9c3a99d 100644 --- a/examples/autorelay/src/main.rs +++ b/examples/autorelay/src/main.rs @@ -1,4 +1,3 @@ -use connexa::prelude::swarm::SwarmEvent; use connexa::prelude::{DefaultConnexaBuilder, Multiaddr, PeerId}; pub const BOOTSTRAP_NODES: &[(&str, &str)] = &[ From 7f1576f3a020b32239be897597bd15ada5b1e34b Mon Sep 17 00:00:00 2001 From: Darius Date: Fri, 26 Sep 2025 10:21:54 -0500 Subject: [PATCH 38/77] chore: fmt --- src/behaviour/autorelay.rs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/behaviour/autorelay.rs b/src/behaviour/autorelay.rs index da52f25..34df5c0 100644 --- a/src/behaviour/autorelay.rs +++ b/src/behaviour/autorelay.rs @@ -384,7 +384,8 @@ impl Behaviour { info.relay_status = RelayStatus::Supported { status: ReservationStatus::Pending { id }, }; - self.connection_reservation.insert(id, (peer_id, connection_id)); + self.connection_reservation + .insert(id, (peer_id, connection_id)); self.events.push_back(ToSwarm::ListenOn { opts }); true } @@ -629,7 +630,8 @@ impl NetworkBehaviour for Behaviour { match self.static_relays.get(&peer_id) { Some(addrs) if addrs.contains(&info.address) => { // prioritize static relays so it would have a higher chance of being selected first - self.connections.insert_before(0, (peer_id, connection_id), info); + self.connections + .insert_before(0, (peer_id, connection_id), info); } _ => { self.connections.insert((peer_id, connection_id), info); @@ -691,7 +693,8 @@ impl NetworkBehaviour for Behaviour { return; } - let Some((peer_id, connection_id)) = self.connection_reservation.get(&listener_id) else { + let Some((peer_id, connection_id)) = self.connection_reservation.get(&listener_id) + else { return; }; @@ -778,7 +781,8 @@ impl NetworkBehaviour for Behaviour { self.events.shrink_to_fit(); } - if (self.connections.is_empty() || self.connections.len() < MAX_CAP) && self.connections.capacity() > MAX_CAP + if (self.connections.is_empty() || self.connections.len() < MAX_CAP) + && self.connections.capacity() > MAX_CAP { self.connections.shrink_to_fit(); } From 18becdbe6fc796f0b92cac991f149e33f54cf897 Mon Sep 17 00:00:00 2001 From: Darius Clark Date: Mon, 9 Mar 2026 10:46:58 -0500 Subject: [PATCH 39/77] fix: correct private ipv6 address check --- src/multiaddr_ext.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/multiaddr_ext.rs b/src/multiaddr_ext.rs index 6ab2aa1..8f669ce 100644 --- a/src/multiaddr_ext.rs +++ b/src/multiaddr_ext.rs @@ -35,7 +35,7 @@ impl MultiaddrExt for Multiaddr { 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 + (ip.segments()[0] & 0xffc0) == 0xfe80 || (ip.segments()[0] & 0xfe00) == 0xfc00 } _ => false, }) From eb1704940b042d540160a0ad145caec87bdc070c Mon Sep 17 00:00:00 2001 From: Darius Clark Date: Mon, 9 Mar 2026 10:47:34 -0500 Subject: [PATCH 40/77] chore: return avg latency zero if there is no connections available --- src/behaviour/autorelay.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/behaviour/autorelay.rs b/src/behaviour/autorelay.rs index 34df5c0..73cc333 100644 --- a/src/behaviour/autorelay.rs +++ b/src/behaviour/autorelay.rs @@ -111,13 +111,16 @@ impl PeerInfo { } pub fn average_latency(&self) -> u128 { - let avg: u128 = self + let total_latency: u128 = self .latency .iter() .map(|duration| duration.as_millis()) .sum(); - let div = self.latency.iter().filter(|i| !i.is_zero()).count() as u128; - avg / div + let count = self.latency.iter().filter(|i| !i.is_zero()).count() as u128; + if count == 0 { + return 0; + } + total_latency / count } } From ef35ec87ab827fc133ab36b16711d680db823851 Mon Sep 17 00:00:00 2001 From: Darius Clark Date: Mon, 9 Mar 2026 10:47:56 -0500 Subject: [PATCH 41/77] chore: return false if connection isnt found --- src/behaviour/autorelay.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/behaviour/autorelay.rs b/src/behaviour/autorelay.rs index 73cc333..30a55e9 100644 --- a/src/behaviour/autorelay.rs +++ b/src/behaviour/autorelay.rs @@ -362,10 +362,10 @@ impl Behaviour { return false; } - let info = self - .connections - .get_mut(&(peer_id, connection_id)) - .expect("connection is present"); + let Some(info) = self.connections.get_mut(&(peer_id, connection_id)) else { + tracing::warn!(%peer_id, %connection_id, "connection not found. skipping"); + return false; + }; let addr_with_peer_id = match info.address.clone().with_p2p(peer_id) { Ok(addr) => addr, From 66dd54fef96b3b47e282392c54780f07e9d893e9 Mon Sep 17 00:00:00 2001 From: Darius Clark Date: Mon, 9 Mar 2026 10:48:18 -0500 Subject: [PATCH 42/77] chore: make MultiaddrExt public --- src/multiaddr_ext.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/multiaddr_ext.rs b/src/multiaddr_ext.rs index 8f669ce..0de27bc 100644 --- a/src/multiaddr_ext.rs +++ b/src/multiaddr_ext.rs @@ -1,8 +1,7 @@ use libp2p::Multiaddr; use libp2p::multiaddr::Protocol; -#[allow(dead_code)] -pub(crate) trait MultiaddrExt { +pub trait MultiaddrExt { fn is_relayed(&self) -> bool; fn is_public(&self) -> bool; From d0a87a51cdbe539e4318220c73c48405f0a87944 Mon Sep 17 00:00:00 2001 From: Darius Clark Date: Mon, 9 Mar 2026 11:00:04 -0500 Subject: [PATCH 43/77] chore: retry if status match specific conditions --- src/behaviour/autorelay.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/behaviour/autorelay.rs b/src/behaviour/autorelay.rs index 30a55e9..b23dbeb 100644 --- a/src/behaviour/autorelay.rs +++ b/src/behaviour/autorelay.rs @@ -281,7 +281,7 @@ impl Behaviour { return; }; - match info.relay_status { + let should_retry = match info.relay_status { RelayStatus::Supported { status: ReservationStatus::Active { .. }, } => { @@ -289,6 +289,7 @@ impl Behaviour { info.relay_status = RelayStatus::Supported { status: ReservationStatus::Idle, }; + true } RelayStatus::Supported { status: ReservationStatus::Pending { .. }, @@ -296,12 +297,17 @@ impl Behaviour { info.relay_status = RelayStatus::Supported { status: ReservationStatus::Idle, }; + true } RelayStatus::Pending | RelayStatus::Supported { status: ReservationStatus::Idle, } - | RelayStatus::NotSupported => {} + | RelayStatus::NotSupported => false, + }; + + if should_retry { + self.meet_reservation_target(Selection::InOrder); } } From 0f7929c8ff9e9b87e13a7782aa393ff15fb23b88 Mon Sep 17 00:00:00 2001 From: Darius Clark Date: Mon, 27 Apr 2026 09:11:33 -0500 Subject: [PATCH 44/77] chore: update libp2p --- Cargo.toml | 22 +++++++++++++++------- examples/upnp/src/main.rs | 8 +++++--- src/behaviour/request_response/codec.rs | 2 -- src/task/upnp.rs | 18 ++++++++++++++---- 4 files changed, 34 insertions(+), 16 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index d32047c..edf788d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -68,12 +68,12 @@ getrandom = { version = "0.2.15" } getrandom_03 = { version = "0.3.3", package = "getrandom" } hickory-resolver = "0.25.0-alpha.5" indexmap = "2.10.0" -libp2p = { version = "0.56.0" } -libp2p-allow-block-list = "0.6.0" -libp2p-connection-limits = "0.6.0" -libp2p-stream = { version = "0.4.0-alpha" } -libp2p-webrtc = { version = "=0.9.0-alpha.1", features = ["pem"] } -libp2p-webrtc-websys = "0.4.0" +libp2p = { version = "0.57.0" } +libp2p-allow-block-list = "0.7.0" +libp2p-connection-limits = "0.7.0" +libp2p-stream = { version = "0.5.0-alpha" } +libp2p-webrtc = { version = "=0.10.0-alpha", features = ["pem"] } +libp2p-webrtc-websys = "0.5.0" pollable-map = "0.1.7" pem = { version = "3.0.5" } rand = "0.8.5" @@ -121,9 +121,17 @@ tokio = { features = ["full"], workspace = true } futures-timer = { workspace = true, features = ["wasm-bindgen"] } getrandom = { workspace = true, features = ["js"] } getrandom_03 = { workspace = true, features = ["wasm_js"] } -libp2p = { features = ["macros", "serde", "wasm-bindgen"], workspace = true } +libp2p = { features = ["macros", "serde"], workspace = true } libp2p-webrtc-websys = { workspace = true, optional = true } send_wrapper.workspace = true serde-wasm-bindgen.workspace = true tokio = { default-features = false, features = ["sync", "macros"], workspace = true } wasm-bindgen-futures.workspace = true + +[patch.crates-io] +libp2p = { git = "https://github.com/libp2p/rust-libp2p.git", rev = "22fb4c7" } +libp2p-allow-block-list = { git = "https://github.com/libp2p/rust-libp2p.git", rev = "22fb4c7" } +libp2p-connection-limits = { git = "https://github.com/libp2p/rust-libp2p.git", rev = "22fb4c7" } +libp2p-stream = { git = "https://github.com/libp2p/rust-libp2p.git", rev = "22fb4c7" } +libp2p-webrtc = { git = "https://github.com/libp2p/rust-libp2p.git", rev = "22fb4c7" } +libp2p-webrtc-websys = { git = "https://github.com/libp2p/rust-libp2p.git", rev = "22fb4c7" } \ No newline at end of file diff --git a/examples/upnp/src/main.rs b/examples/upnp/src/main.rs index 7a4a1a3..c2f8057 100644 --- a/examples/upnp/src/main.rs +++ b/examples/upnp/src/main.rs @@ -13,9 +13,11 @@ async fn main() -> std::io::Result<()> { println!("New listen address: {addr}") } SwarmEvent::Behaviour(BehaviourEvent::Upnp(event)) => match event { - UpnpEvent::NewExternalAddr(addr) => println!("New external address: {addr}"), - UpnpEvent::ExpiredExternalAddr(addr) => { - println!("Expired external address: {addr}") + UpnpEvent::NewExternalAddr { external_addr, .. } => { + println!("New external address: {external_addr}") + } + UpnpEvent::ExpiredExternalAddr { external_addr, .. } => { + println!("Expired external address: {external_addr}") } UpnpEvent::GatewayNotFound => println!("Gateway not found"), UpnpEvent::NonRoutableGateway => println!("Gateway is not routable"), diff --git a/src/behaviour/request_response/codec.rs b/src/behaviour/request_response/codec.rs index 40a0585..6e3caca 100644 --- a/src/behaviour/request_response/codec.rs +++ b/src/behaviour/request_response/codec.rs @@ -1,4 +1,3 @@ -use async_trait::async_trait; use bytes::Bytes; use futures::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; use libp2p::StreamProtocol; @@ -18,7 +17,6 @@ impl Codec { } } -#[async_trait] impl libp2p::request_response::Codec for Codec { type Protocol = StreamProtocol; type Request = Bytes; diff --git a/src/task/upnp.rs b/src/task/upnp.rs index 84323dc..2b2c027 100644 --- a/src/task/upnp.rs +++ b/src/task/upnp.rs @@ -13,11 +13,21 @@ where { pub fn process_upnp_event(&mut self, event: UpnpEvent) { match event { - UpnpEvent::NewExternalAddr(addr) => { - tracing::info!(?addr, "upnp external address discovered"); + UpnpEvent::NewExternalAddr { + external_addr, + local_addr, + } => { + tracing::info!( + ?external_addr, + ?local_addr, + "upnp external address discovered" + ); } - UpnpEvent::ExpiredExternalAddr(addr) => { - tracing::info!(?addr, "upnp external address expired"); + UpnpEvent::ExpiredExternalAddr { + external_addr, + local_addr, + } => { + tracing::info!(?external_addr, ?local_addr, "upnp external address expired"); } UpnpEvent::GatewayNotFound => { tracing::warn!("upnp gateway not found"); From 6e29001a700798b3fd0fd07f6a67be50afe976a9 Mon Sep 17 00:00:00 2001 From: Darius Clark Date: Mon, 27 Apr 2026 09:45:18 -0500 Subject: [PATCH 45/77] feat: Add function to enable/disable relay server --- src/handle.rs | 9 +++++++++ src/handle/relay_server.rs | 32 ++++++++++++++++++++++++++++++++ src/task.rs | 6 +++++- src/task/relay.rs | 30 ++++++++++++++++++++++++++++++ src/types.rs | 18 ++++++++++++++++++ 5 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 src/handle/relay_server.rs diff --git a/src/handle.rs b/src/handle.rs index 1d804a3..9ea1393 100644 --- a/src/handle.rs +++ b/src/handle.rs @@ -8,6 +8,8 @@ pub(crate) mod floodsub; #[cfg(feature = "gossipsub")] pub(crate) mod gossipsub; mod peer_store; +#[cfg(feature = "relay")] +mod relay_server; #[cfg(feature = "rendezvous")] pub(crate) mod rendezvous; #[cfg(feature = "request-response")] @@ -27,6 +29,7 @@ use crate::handle::floodsub::ConnexaFloodsub; #[cfg(feature = "gossipsub")] use crate::handle::gossipsub::ConnexaGossipsub; use crate::handle::peer_store::ConnexaPeerstore; +use crate::handle::relay_server::ConnexaRelayServer; #[cfg(feature = "rendezvous")] use crate::handle::rendezvous::ConnexaRendezvous; #[cfg(feature = "request-response")] @@ -133,6 +136,12 @@ where ConnexaRendezvous::new(self) } + /// Returns a handle for relay server functions + #[cfg(feature = "relay")] + pub fn relay_server(&self) -> ConnexaRelayServer<'_, T> { + ConnexaRelayServer::new(self) + } + /// Returns a handle to manage peer whitelist functionality pub fn whitelist(&self) -> ConnexaWhitelist<'_, T> { ConnexaWhitelist::new(self) diff --git a/src/handle/relay_server.rs b/src/handle/relay_server.rs new file mode 100644 index 0000000..51fa80e --- /dev/null +++ b/src/handle/relay_server.rs @@ -0,0 +1,32 @@ +use crate::handle::Connexa; +use crate::types::RelayServerCommand; +use futures::TryFutureExt; +use libp2p::relay::Status as RelayServerStatus; + +#[derive(Copy, Clone)] +pub struct ConnexaRelayServer<'a, T = ()> { + connexa: &'a Connexa, +} + +impl<'a, T> ConnexaRelayServer<'a, T> +where + T: Send + Sync + 'static, +{ + pub(crate) fn new(connexa: &'a Connexa) -> Self { + Self { connexa } + } + + pub async fn change_status( + &self, + status: impl Into>, + ) -> std::io::Result<()> { + let (tx, rx) = futures::channel::oneshot::channel(); + let status = status.into(); + self.connexa + .to_task + .clone() + .send(RelayServerCommand::StatusChanged { status, resp: tx }.into()) + .await?; + rx.await.map_err(std::io::Error::other)? + } +} diff --git a/src/task.rs b/src/task.rs index 9be825a..db29f47 100644 --- a/src/task.rs +++ b/src/task.rs @@ -50,7 +50,7 @@ use futures::channel::{mpsc, oneshot}; use futures::future::BoxFuture; use futures::{FutureExt, StreamExt}; use futures_timer::Delay; -use indexmap::IndexMap; +use indexmap::{IndexMap, IndexSet}; #[cfg(feature = "gossipsub")] use libp2p::gossipsub::{MessageAcceptance, MessageId}; #[cfg(feature = "kad")] @@ -561,6 +561,10 @@ where Command::Rendezvous(rendezvous_command) => { self.process_rendezvous_command(rendezvous_command) } + #[cfg(feature = "relay")] + Command::RelayServer(relay_server_command) => { + self.process_relay_server_command(relay_server_command) + } Command::Custom(custom_command) => { (self.custom_task_callback)(swarm, &mut self.context, custom_command); } diff --git a/src/task/relay.rs b/src/task/relay.rs index 0ff6adb..46fc4d6 100644 --- a/src/task/relay.rs +++ b/src/task/relay.rs @@ -1,5 +1,6 @@ use crate::behaviour::peer_store::store::Store; use crate::task::ConnexaTask; +use crate::types::RelayServerCommand; use libp2p::relay::{Event as RelayServerEvent, client::Event as RelayClientEvent}; use libp2p::swarm::NetworkBehaviour; use std::fmt::Debug; @@ -69,7 +70,36 @@ where } => { tracing::warn!(%src_peer_id, %dst_peer_id, ?error, "relay server circuit closed"); } + RelayServerEvent::StatusChanged { status } => { + tracing::info!(?status, "relay server status changed"); + } _ => {} } } } + +impl ConnexaTask +where + X: Default + Send + 'static, + C: Send, + C::ToSwarm: Debug, + S: Store, +{ + pub fn process_relay_server_command(&mut self, command: RelayServerCommand) { + let swarm = self.swarm.as_mut().expect("swarm is active"); + match command { + RelayServerCommand::StatusChanged { status, resp } => { + let Some(relay) = swarm.behaviour_mut().relay.as_mut() else { + let _ = resp.send(Err(std::io::Error::other( + "relay server protocol is not enabled", + ))); + return; + }; + + relay.set_status(status); + + let _ = resp.send(Ok(())); + } + } + } +} diff --git a/src/types.rs b/src/types.rs index 099ad9e..f586cb4 100644 --- a/src/types.rs +++ b/src/types.rs @@ -44,6 +44,8 @@ pub enum Command { Rendezvous(RendezvousCommand), #[cfg(feature = "autonat")] Autonat(AutonatCommand), + #[cfg(feature = "relay")] + RelayServer(RelayServerCommand), Whitelist(WhitelistCommand), Blacklist(BlacklistCommand), ConnectionLimits(ConnectionLimitsCommand), @@ -106,6 +108,13 @@ impl From for Command { } } +#[cfg(feature = "relay")] +impl From for Command { + fn from(cmd: RelayServerCommand) -> Self { + Command::RelayServer(cmd) + } +} + impl From for Command { fn from(cmd: WhitelistCommand) -> Self { Command::Whitelist(cmd) @@ -493,6 +502,15 @@ pub enum RendezvousCommand { }, } +#[cfg(feature = "relay")] +#[derive(Debug)] +pub enum RelayServerCommand { + StatusChanged { + status: Option, + resp: oneshot::Sender>, + }, +} + #[cfg(feature = "kad")] #[derive(Clone, Debug)] pub enum DHTEvent { From 15e8a37dedc565c82c845a61d8d3c137e7e32e73 Mon Sep 17 00:00:00 2001 From: Darius Clark Date: Mon, 27 Apr 2026 09:48:00 -0500 Subject: [PATCH 46/77] chore: cleanup and fmt --- src/handle.rs | 2 +- src/handle/relay_server.rs | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/handle.rs b/src/handle.rs index 9ea1393..3733318 100644 --- a/src/handle.rs +++ b/src/handle.rs @@ -136,7 +136,7 @@ where ConnexaRendezvous::new(self) } - /// Returns a handle for relay server functions + /// Returns a handle for relay server functions #[cfg(feature = "relay")] pub fn relay_server(&self) -> ConnexaRelayServer<'_, T> { ConnexaRelayServer::new(self) diff --git a/src/handle/relay_server.rs b/src/handle/relay_server.rs index 51fa80e..24a9b99 100644 --- a/src/handle/relay_server.rs +++ b/src/handle/relay_server.rs @@ -1,6 +1,5 @@ use crate::handle::Connexa; use crate::types::RelayServerCommand; -use futures::TryFutureExt; use libp2p::relay::Status as RelayServerStatus; #[derive(Copy, Clone)] From ba27a2889db60cd2f1247a8f10926fa9d707344d Mon Sep 17 00:00:00 2001 From: Darius Clark Date: Mon, 27 Apr 2026 09:48:41 -0500 Subject: [PATCH 47/77] chore: point to branch to remove relay external addr when it is removed manually --- Cargo.toml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index edf788d..784e7d3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -129,9 +129,9 @@ tokio = { default-features = false, features = ["sync", "macros"], workspace = t wasm-bindgen-futures.workspace = true [patch.crates-io] -libp2p = { git = "https://github.com/libp2p/rust-libp2p.git", rev = "22fb4c7" } -libp2p-allow-block-list = { git = "https://github.com/libp2p/rust-libp2p.git", rev = "22fb4c7" } -libp2p-connection-limits = { git = "https://github.com/libp2p/rust-libp2p.git", rev = "22fb4c7" } -libp2p-stream = { git = "https://github.com/libp2p/rust-libp2p.git", rev = "22fb4c7" } -libp2p-webrtc = { git = "https://github.com/libp2p/rust-libp2p.git", rev = "22fb4c7" } -libp2p-webrtc-websys = { git = "https://github.com/libp2p/rust-libp2p.git", rev = "22fb4c7" } \ No newline at end of file +libp2p = { git = "https://github.com/dariusc93/rust-libp2p.git", branch = "fix/remove-ext-addr-relay" } +libp2p-allow-block-list = { git = "https://github.com/dariusc93/rust-libp2p.git", branch = "fix/remove-ext-addr-relay" } +libp2p-connection-limits = { git = "https://github.com/dariusc93/rust-libp2p.git", branch = "fix/remove-ext-addr-relay" } +libp2p-stream = { git = "https://github.com/dariusc93/rust-libp2p.git", branch = "fix/remove-ext-addr-relay" } +libp2p-webrtc = { git = "https://github.com/dariusc93/rust-libp2p.git", branch = "fix/remove-ext-addr-relay" } +libp2p-webrtc-websys = { git = "https://github.com/dariusc93/rust-libp2p.git", branch = "fix/remove-ext-addr-relay" } \ No newline at end of file From 8ccddf297d36c3f90f315eec1049cb2a3441fc9e Mon Sep 17 00:00:00 2001 From: Darius Clark Date: Mon, 27 Apr 2026 11:26:55 -0500 Subject: [PATCH 48/77] chore: set relay status if connection is disqualified --- src/behaviour/autorelay.rs | 1 + src/handle/relay.rs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/behaviour/autorelay.rs b/src/behaviour/autorelay.rs index b23dbeb..70b7c51 100644 --- a/src/behaviour/autorelay.rs +++ b/src/behaviour/autorelay.rs @@ -632,6 +632,7 @@ impl NetworkBehaviour for Behaviour { // in the event that the address is from a peer going through a relay, automatically disqualify the connection // from being used as a potential relay since there is no support for multi-HOP if info.check_for_disqualifying_address() { + info.relay_status = RelayStatus::NotSupported; self.connections.insert((peer_id, connection_id), info); return; } diff --git a/src/handle/relay.rs b/src/handle/relay.rs index 5e332be..f6cbed2 100644 --- a/src/handle/relay.rs +++ b/src/handle/relay.rs @@ -90,7 +90,7 @@ where rx.await.map_err(io::Error::other)? } - pub async fn disable_relaya(&self) -> io::Result<()> { + pub async fn disable_relays(&self) -> io::Result<()> { let (tx, rx) = oneshot::channel(); self.connexa .to_task From 6e4443ffc5f05f4f326e6edd4a8766b12ec6db45 Mon Sep 17 00:00:00 2001 From: Darius Clark Date: Tue, 28 Apr 2026 16:14:15 -0500 Subject: [PATCH 49/77] chore: correctly disable all relays --- src/task/relay.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/task/relay.rs b/src/task/relay.rs index 1588a17..5975872 100644 --- a/src/task/relay.rs +++ b/src/task/relay.rs @@ -87,7 +87,7 @@ where return; }; - autorelay.disable_autorelay(); + autorelay.disable_all_relays(); let _ = resp.send(Ok(())); } From 428d7d018233ae57dd5715ffd5252731fa49c128 Mon Sep 17 00:00:00 2001 From: Darius Clark Date: Wed, 29 Apr 2026 07:29:46 -0500 Subject: [PATCH 50/77] chore: remove redundant flag --- src/behaviour/autorelay.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/behaviour/autorelay.rs b/src/behaviour/autorelay.rs index 70b7c51..b23dbeb 100644 --- a/src/behaviour/autorelay.rs +++ b/src/behaviour/autorelay.rs @@ -632,7 +632,6 @@ impl NetworkBehaviour for Behaviour { // in the event that the address is from a peer going through a relay, automatically disqualify the connection // from being used as a potential relay since there is no support for multi-HOP if info.check_for_disqualifying_address() { - info.relay_status = RelayStatus::NotSupported; self.connections.insert((peer_id, connection_id), info); return; } From 7dfe534d02d04e91689bc6da7e52c2a9f0936f35 Mon Sep 17 00:00:00 2001 From: Darius Clark Date: Wed, 29 Apr 2026 10:17:57 -0500 Subject: [PATCH 51/77] chore: remove autorelay override flag --- src/behaviour/autorelay.rs | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/behaviour/autorelay.rs b/src/behaviour/autorelay.rs index b23dbeb..1d2b8c1 100644 --- a/src/behaviour/autorelay.rs +++ b/src/behaviour/autorelay.rs @@ -38,7 +38,6 @@ pub struct Behaviour { external_addresses: ExternalAddresses, capacity_cleanup: Delay, max_reservation: NonZeroU8, - override_autorelay: bool, enable_auto_relay: bool, backoff: Optional, waker: Option, @@ -53,7 +52,6 @@ impl Default for Behaviour { events: VecDeque::new(), capacity_cleanup: Delay::new(CLEANUP_INTERVAL), external_addresses: ExternalAddresses::default(), - override_autorelay: false, waker: None, enable_auto_relay: true, backoff: Optional::default(), @@ -410,7 +408,6 @@ impl Behaviour { .external_addresses .iter() .any(|addr| addr.is_public() && !addr.is_relayed()) - && !self.override_autorelay { tracing::trace!("local node reachable. autorelay will not run"); return; @@ -479,9 +476,7 @@ impl Behaviour { return; } - let remaining_targets_needed = targets_count - .checked_sub(pending_targets) - .unwrap_or_default(); + let remaining_targets_needed = targets_count.saturating_sub(pending_targets); if remaining_targets_needed == 0 { tracing::warn!("no potential targets to meet reservation target."); @@ -596,7 +591,6 @@ impl NetworkBehaviour for Behaviour { .any(|addr| addr.is_public() && !addr.is_relayed()) { tracing::info!("local node is reachable. disabling autorelay"); - self.override_autorelay = true; self.disable_all_reservations(); self.backoff.take(); } else if self.external_addresses.iter().count() == 0 @@ -607,7 +601,6 @@ impl NetworkBehaviour for Behaviour { { tracing::info!("local node is not reachable. enabling autorelay"); self.backoff.replace(Delay::new(BACKOFF_INTERVAL)); - self.override_autorelay = false; } return; } From 5e5a080ee370f70fe238e437c75678802bb559f5 Mon Sep 17 00:00:00 2001 From: Darius Clark Date: Fri, 1 May 2026 08:15:32 -0500 Subject: [PATCH 52/77] chore: add flag to remove active reservation when we are notified about protocol change --- src/behaviour/autorelay.rs | 14 ++++++++++---- src/task/relay.rs | 2 +- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/behaviour/autorelay.rs b/src/behaviour/autorelay.rs index 1d2b8c1..ad55541 100644 --- a/src/behaviour/autorelay.rs +++ b/src/behaviour/autorelay.rs @@ -41,6 +41,7 @@ pub struct Behaviour { enable_auto_relay: bool, backoff: Optional, waker: Option, + remove_active_reservation_on_unsupport: bool, } impl Default for Behaviour { @@ -54,6 +55,7 @@ impl Default for Behaviour { external_addresses: ExternalAddresses::default(), waker: None, enable_auto_relay: true, + remove_active_reservation_on_unsupport: true, backoff: Optional::default(), max_reservation: NonZeroU8::new(2).expect("not zero"), } @@ -65,6 +67,7 @@ impl Default for Behaviour { pub struct Config { pub max_reservation: NonZeroU8, pub enable_auto_relay: bool, + pub remove_active_reservation_on_unsupport: bool, } #[derive(Default, Debug, Clone, Copy)] @@ -81,6 +84,7 @@ impl Default for Config { Self { max_reservation: NonZeroU8::new(2).expect("not zero"), enable_auto_relay: true, + remove_active_reservation_on_unsupport: true, } } } @@ -147,6 +151,7 @@ impl Behaviour { Self { enable_auto_relay: config.enable_auto_relay, max_reservation: config.max_reservation, + remove_active_reservation_on_unsupport: config.remove_active_reservation_on_unsupport, ..Default::default() } } @@ -210,7 +215,7 @@ impl Behaviour { } } - pub fn disable_all_relays(&mut self) { + pub fn remove_existing_reservations(&mut self) { self.disable_all_reservations(); if let Some(waker) = self.waker.take() { waker.wake(); @@ -753,9 +758,10 @@ impl NetworkBehaviour for Behaviour { // if there is a change in protocol support during an active reservation, // we should remove the reservation if its not already removed - if let RelayStatus::Supported { - status: ReservationStatus::Active { id }, - } = previous_status + if self.remove_active_reservation_on_unsupport + && let RelayStatus::Supported { + status: ReservationStatus::Active { id } | ReservationStatus::Pending { id }, + } = previous_status { self.events.push_back(ToSwarm::RemoveListener { id }); } diff --git a/src/task/relay.rs b/src/task/relay.rs index 5975872..c718919 100644 --- a/src/task/relay.rs +++ b/src/task/relay.rs @@ -87,7 +87,7 @@ where return; }; - autorelay.disable_all_relays(); + autorelay.remove_existing_reservations(); let _ = resp.send(Ok(())); } From 57921b38f585439309626dc233f0b551f0226add Mon Sep 17 00:00:00 2001 From: Darius Clark Date: Fri, 1 May 2026 08:26:39 -0500 Subject: [PATCH 53/77] chore: add logs when unable to find reservation when disabling --- src/behaviour/autorelay.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/behaviour/autorelay.rs b/src/behaviour/autorelay.rs index ad55541..78cd729 100644 --- a/src/behaviour/autorelay.rs +++ b/src/behaviour/autorelay.rs @@ -277,10 +277,12 @@ impl Behaviour { fn disable_reservation(&mut self, id: ListenerId) { let Some((peer_id, connection_id)) = self.connection_reservation.shift_remove(&id) else { + tracing::error!(listener_id=%id, "could not find reservation with listener id."); return; }; let Some(info) = self.connections.get_mut(&(peer_id, connection_id)) else { + tracing::error!(%peer_id, %connection_id, listener_id=%id, "connection not found."); return; }; From ea24bde9227ce82e716ad88840117e6c95c989eb Mon Sep 17 00:00:00 2001 From: Darius Clark Date: Fri, 1 May 2026 08:27:02 -0500 Subject: [PATCH 54/77] chore: reduce allocation by iterating over map --- src/behaviour/autorelay.rs | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/src/behaviour/autorelay.rs b/src/behaviour/autorelay.rs index 78cd729..fbdab45 100644 --- a/src/behaviour/autorelay.rs +++ b/src/behaviour/autorelay.rs @@ -317,14 +317,9 @@ impl Behaviour { } fn disable_all_reservations(&mut self) { - let relay_listeners = self - .connection_reservation - .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 { + for (listener_id, peer_connection) in self.connection_reservation.iter() { + let (peer_id, connection_id) = peer_connection; + let Some(connection) = self.connections.get_mut(peer_connection) else { tracing::warn!(%peer_id, %connection_id, "connection not found when it should have been present. skipping"); continue; }; @@ -333,7 +328,7 @@ impl Behaviour { connection.relay_status, RelayStatus::Supported { status: ReservationStatus::Active { id } | ReservationStatus::Pending { id } - } if id == listener_id + } if id == *listener_id )); connection.relay_status = RelayStatus::Supported { @@ -341,7 +336,7 @@ impl Behaviour { }; self.events - .push_back(ToSwarm::RemoveListener { id: listener_id }); + .push_back(ToSwarm::RemoveListener { id: *listener_id }); tracing::info!(%peer_id, %connection_id, ?listener_id, "removing relay listener"); } } From fca25b203bf24b1f2959895dbe82d7b147eac0cc Mon Sep 17 00:00:00 2001 From: Darius Clark Date: Fri, 1 May 2026 08:27:23 -0500 Subject: [PATCH 55/77] chore: add event to indicate to swarm that no relay was found or available --- src/behaviour/autorelay.rs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/behaviour/autorelay.rs b/src/behaviour/autorelay.rs index fbdab45..8a9fa2e 100644 --- a/src/behaviour/autorelay.rs +++ b/src/behaviour/autorelay.rs @@ -425,9 +425,9 @@ impl Behaviour { if peers_not_supported { if self.static_relays.is_empty() { - // TODO: Emit an event informing swarm about being in need of relays? - // however this would require separate functions to add relays to the autorelay state and possibly confirm if theres any existing connections tracing::warn!("no relays present."); + self.events + .push_back(ToSwarm::GenerateEvent(Event::NoRelayAvailable)); return; } for (peer_id, addrs) in self.static_relays.iter() { @@ -533,9 +533,14 @@ impl Behaviour { } } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Event { + NoRelayAvailable, +} + impl NetworkBehaviour for Behaviour { type ConnectionHandler = Either; - type ToSwarm = (); + type ToSwarm = Event; fn handle_established_inbound_connection( &mut self, From 914c2094cf769b2356ab7372cd8920fd29218071 Mon Sep 17 00:00:00 2001 From: Darius Clark Date: Fri, 1 May 2026 14:54:48 -0500 Subject: [PATCH 56/77] chore: add additional events, remove hash derive --- src/behaviour/autorelay.rs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/behaviour/autorelay.rs b/src/behaviour/autorelay.rs index 8a9fa2e..7a614b1 100644 --- a/src/behaviour/autorelay.rs +++ b/src/behaviour/autorelay.rs @@ -126,12 +126,6 @@ impl PeerInfo { } } -impl Hash for PeerInfo { - fn hash(&self, state: &mut H) { - self.address.hash(state); - } -} - #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum RelayStatus { Supported { status: ReservationStatus }, @@ -203,6 +197,8 @@ impl Behaviour { pub fn enable_autorelay(&mut self) { self.enable_auto_relay = true; self.meet_reservation_target(Selection::Random); + self.events + .push_back(ToSwarm::GenerateEvent(Event::AutoRelayEnabled)); if let Some(waker) = self.waker.take() { waker.wake(); } @@ -210,6 +206,8 @@ impl Behaviour { pub fn disable_autorelay(&mut self) { self.enable_auto_relay = false; + self.events + .push_back(ToSwarm::GenerateEvent(Event::AutoRelayDisabled)); if let Some(waker) = self.waker.take() { waker.wake(); } @@ -324,7 +322,7 @@ impl Behaviour { continue; }; - assert!(matches!( + debug_assert!(matches!( connection.relay_status, RelayStatus::Supported { status: ReservationStatus::Active { id } | ReservationStatus::Pending { id } @@ -452,6 +450,8 @@ impl Behaviour { }) .count(); + tracing::info!(?relayed_targets, ?max, "relayed targets"); + if relayed_targets == max { tracing::warn!("max reservation reached. no more reservations will be made"); return; @@ -536,6 +536,8 @@ impl Behaviour { #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Event { NoRelayAvailable, + AutoRelayEnabled, + AutoRelayDisabled, } impl NetworkBehaviour for Behaviour { From 3b7e6c951c17b6df93878324eed26e21c8ddea7f Mon Sep 17 00:00:00 2001 From: Darius Clark Date: Mon, 11 May 2026 07:56:56 -0400 Subject: [PATCH 57/77] chore: use main rust-libp2p repo --- Cargo.toml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index cef31b0..4f7281e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -129,9 +129,9 @@ tokio = { default-features = false, features = ["sync", "macros"], workspace = t wasm-bindgen-futures.workspace = true [patch.crates-io] -libp2p = { git = "https://github.com/dariusc93/rust-libp2p.git", branch = "fix/remove-ext-addr-relay" } -libp2p-allow-block-list = { git = "https://github.com/dariusc93/rust-libp2p.git", branch = "fix/remove-ext-addr-relay" } -libp2p-connection-limits = { git = "https://github.com/dariusc93/rust-libp2p.git", branch = "fix/remove-ext-addr-relay" } -libp2p-stream = { git = "https://github.com/dariusc93/rust-libp2p.git", branch = "fix/remove-ext-addr-relay" } -libp2p-webrtc = { git = "https://github.com/dariusc93/rust-libp2p.git", branch = "fix/remove-ext-addr-relay" } -libp2p-webrtc-websys = { git = "https://github.com/dariusc93/rust-libp2p.git", branch = "fix/remove-ext-addr-relay" } \ No newline at end of file +libp2p = { git = "https://github.com/libp2p/rust-libp2p.git", branch = "master" } +libp2p-allow-block-list = { git = "https://github.com/libp2p/rust-libp2p.git", branch = "master" } +libp2p-connection-limits = { git = "https://github.com/libp2p/rust-libp2p.git", branch = "master" } +libp2p-stream = { git = "https://github.com/libp2p/rust-libp2p.git", branch = "master" } +libp2p-webrtc = { git = "https://github.com/libp2p/rust-libp2p.git", branch = "master" } +libp2p-webrtc-websys = { git = "https://github.com/libp2p/rust-libp2p.git", branch = "master" } \ No newline at end of file From 81cbfc0fbd0ffc7bbe22b48b62d09557db94c6e6 Mon Sep 17 00:00:00 2001 From: Darius Clark Date: Sun, 31 May 2026 07:54:28 -0500 Subject: [PATCH 58/77] refactor: change to mirror libp2p autorelay PR --- src/behaviour/autorelay.rs | 1093 ++++++++++++++-------------- src/behaviour/autorelay/handler.rs | 49 +- src/handle/relay.rs | 11 +- src/multiaddr_ext.rs | 14 + src/task/ping.rs | 22 +- src/task/relay.rs | 30 +- src/types.rs | 1 - 7 files changed, 636 insertions(+), 584 deletions(-) diff --git a/src/behaviour/autorelay.rs b/src/behaviour/autorelay.rs index 7a614b1..37548a1 100644 --- a/src/behaviour/autorelay.rs +++ b/src/behaviour/autorelay.rs @@ -1,128 +1,98 @@ -mod handler; +// TODO: Replace with builtin autorelay behaviour from libp2p. See https://github.com/libp2p/rust-libp2p/pull/6156 +use std::{ + collections::{HashMap, HashSet, VecDeque}, + num::NonZeroU8, + task::{Context, Poll, Waker}, + time::{Duration, Instant}, +}; use crate::behaviour::autorelay::handler::Out; -use crate::behaviour::dummy; use crate::multiaddr_ext::MultiaddrExt; -use crate::prelude::swarm::derive_prelude::{ConnectionEstablished, PortUse}; use crate::prelude::swarm::{ - AddressChange, ConnectionClosed, ConnectionDenied, DialFailure, ExpiredListenAddr, FromSwarm, - ListenerClosed, ListenerError, THandler, THandlerInEvent, THandlerOutEvent, ToSwarm, + 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 crate::prelude::transport::Endpoint; +use crate::prelude::{PeerId, Protocol}; use either::Either; -use futures::FutureExt; -use futures_timer::Delay; -use indexmap::{IndexMap, IndexSet}; -use libp2p::core::transport::ListenerId; -use libp2p::multiaddr::Protocol; -use libp2p::swarm::dial_opts::DialOpts; -use libp2p::swarm::{ConnectionId, ExternalAddresses, ListenOpts, NetworkBehaviour, NewListenAddr}; -use libp2p::{Multiaddr, PeerId}; -use pollable_map::optional::Optional; -use rand::prelude::IteratorRandom; -use std::collections::VecDeque; -use std::hash::{Hash, Hasher}; -use std::num::NonZeroU8; -use std::task::{Context, Poll, Waker}; -use std::time::Duration; - -const MAX_CAP: usize = 100; -const CLEANUP_INTERVAL: Duration = Duration::from_secs(60); -const BACKOFF_INTERVAL: Duration = Duration::from_secs(5); +use crate::prelude::swarm::derive_prelude::{ListenerId, PortUse}; + +mod handler; + +#[derive(Debug)] pub struct Behaviour { - connections: IndexMap<(PeerId, ConnectionId), PeerInfo>, - static_relays: IndexMap>, - connection_reservation: IndexMap, - events: VecDeque::ToSwarm, THandlerInEvent>>, + config: Config, + status: Status, + auto_status_change: bool, external_addresses: ExternalAddresses, - capacity_cleanup: Delay, - max_reservation: NonZeroU8, - enable_auto_relay: bool, - backoff: Optional, + events: VecDeque::ToSwarm, THandlerInEvent>>, + + connections: HashMap<(PeerId, ConnectionId), Connection>, + + reservations: HashMap, + + external_reservations: HashMap, + + static_relays: HashMap>, + + static_dial_cooldowns: HashMap, + + failure_counts: HashMap, + + previous_relays: VecDeque<(PeerId, Multiaddr, Instant)>, + + relays_available: bool, + waker: Option, - remove_active_reservation_on_unsupport: bool, } impl Default for Behaviour { fn default() -> Self { Self { - connections: IndexMap::new(), - static_relays: IndexMap::new(), - connection_reservation: IndexMap::new(), - events: VecDeque::new(), - capacity_cleanup: Delay::new(CLEANUP_INTERVAL), + 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(), + static_dial_cooldowns: HashMap::new(), + failure_counts: HashMap::new(), + previous_relays: VecDeque::new(), + relays_available: false, waker: None, - enable_auto_relay: true, - remove_active_reservation_on_unsupport: true, - backoff: Optional::default(), - max_reservation: NonZeroU8::new(2).expect("not zero"), } } } -#[derive(Debug, Clone)] -#[non_exhaustive] -pub struct Config { - pub max_reservation: NonZeroU8, - pub enable_auto_relay: bool, - pub remove_active_reservation_on_unsupport: bool, -} - -#[derive(Default, Debug, Clone, Copy)] -pub enum Selection { +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] +pub enum Status { #[default] - InOrder, - Random, - LowestLatency, - Peer(PeerId), + Enable, + Disable, } -impl Default for Config { - fn default() -> Self { - Self { - max_reservation: NonZeroU8::new(2).expect("not zero"), - enable_auto_relay: true, - remove_active_reservation_on_unsupport: true, - } - } -} - -#[derive(Debug, Clone)] -struct PeerInfo { +#[derive(Debug)] +struct Connection { address: Multiaddr, relay_status: RelayStatus, - latency: [Duration; 5], } -impl PeerInfo { - /// Check to see if the address is from a relay and if so, automatically disqualify the connection - /// as we are not able to establish a reservation via multi-HOP - pub fn check_for_disqualifying_address(&mut self) -> bool { - match self.address.is_relayed() { - true => { - self.relay_status = RelayStatus::NotSupported; - true - } - false => { - self.relay_status = RelayStatus::Pending; - false - } - } - } - - pub fn average_latency(&self) -> u128 { - let total_latency: u128 = self - .latency - .iter() - .map(|duration| duration.as_millis()) - .sum(); - let count = self.latency.iter().filter(|i| !i.is_zero()).count() as u128; - if count == 0 { - return 0; +impl Connection { + /// Mark relayed connection as not supported + pub(crate) fn disqualify_connection_if_relayed(&mut self) { + if self.address.is_relayed() { + self.relay_status = RelayStatus::NotSupported; } - total_latency / count } } @@ -138,410 +108,468 @@ enum ReservationStatus { Idle, Pending { id: ListenerId }, Active { id: ListenerId }, + Blacklisted, } -impl Behaviour { - pub fn new_with_config(config: Config) -> Self { +#[derive(Debug)] +pub struct Config { + max_reservations: NonZeroU8, + failure_cooldown: Duration, + failure_cooldown_max: Duration, + max_previous_relays: usize, + static_relays: HashMap, +} + +impl Default for Config { + fn default() -> Self { Self { - enable_auto_relay: config.enable_auto_relay, - max_reservation: config.max_reservation, - remove_active_reservation_on_unsupport: config.remove_active_reservation_on_unsupport, - ..Default::default() + max_reservations: NonZeroU8::new(2).unwrap(), + failure_cooldown: Duration::from_secs(30), + failure_cooldown_max: Duration::from_secs(10 * 60), + max_previous_relays: 16, + static_relays: HashMap::new(), } } +} - pub fn add_static_relay(&mut self, peer_id: PeerId, address: Multiaddr) -> bool { - let Ok(address) = address.with_p2p(peer_id) else { - return false; - }; - - self.static_relays - .entry(peer_id) - .or_default() - .insert(address) +impl Config { + pub fn set_max_reservations(mut self, max_reservations: NonZeroU8) -> Self { + self.max_reservations = max_reservations; + self } - pub fn remove_static_relay(&mut self, peer_id: PeerId, address: Multiaddr) -> bool { - let Ok(address) = address.with_p2p(peer_id) else { - return false; - }; - - let Some(addrs) = self.static_relays.get_mut(&peer_id) else { - return false; - }; - - let removed = addrs.shift_remove(&address); + pub fn set_failure_cooldown(mut self, duration: Duration) -> Self { + self.failure_cooldown = duration; + self + } - if addrs.is_empty() { - self.static_relays.shift_remove(&peer_id); - } + pub fn set_failure_cooldown_max(mut self, duration: Duration) -> Self { + self.failure_cooldown_max = duration; + self + } - removed + pub fn set_max_previous_relays(mut self, max: usize) -> Self { + self.max_previous_relays = max; + self } - pub fn list_static_relays(&self) -> Vec<(PeerId, Vec)> { - self.static_relays - .iter() - .map(|(peer_id, addrs)| (*peer_id, Vec::from_iter(addrs.clone()))) - .collect() + pub fn add_static_relay(mut self, peer_id: PeerId, address: Multiaddr) -> Self { + self.static_relays.insert(peer_id, address); + self } +} - pub fn get_static_relay_addrs(&self, peer_id: PeerId) -> Vec { - let Some(addrs) = self.static_relays.get(&peer_id) else { - return vec![]; - }; +#[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, +} - Vec::from_iter(addrs.clone()) +impl Behaviour { + 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 } - pub fn enable_autorelay(&mut self) { - self.enable_auto_relay = true; - self.meet_reservation_target(Selection::Random); - self.events - .push_back(ToSwarm::GenerateEvent(Event::AutoRelayEnabled)); - if let Some(waker) = self.waker.take() { - waker.wake(); + /// 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(); + } } - } - pub fn disable_autorelay(&mut self) { - self.enable_auto_relay = false; - self.events - .push_back(ToSwarm::GenerateEvent(Event::AutoRelayDisabled)); if let Some(waker) = self.waker.take() { waker.wake(); } } - pub fn remove_existing_reservations(&mut self) { - self.disable_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 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) -> bool { + if address.is_relayed() { + tracing::warn!(%peer_id, %address, "static relay address is relayed. ignoring."); + return false; + } + + 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 addrs = entry.clone(); + + if self.is_peer_idle(&peer_id) { + self.evict_for_static_peer(peer_id); + } + + if !self.queue_static_dial(peer_id, addrs) { + self.meet_reservation_target(); + } + if let Some(waker) = self.waker.take() { waker.wake(); } + + return true; } - pub fn get_all_supported_targets(&self) -> impl Iterator { - self.connections + /// 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() + } + + pub fn static_relays(&self) -> impl Iterator { + self.static_relays .iter() - .filter(|(_, info)| matches!(info.relay_status, RelayStatus::Supported { .. })) - .map(|((peer_id, connection_id), _)| (peer_id, connection_id)) + .map(|(peer, addrs)| (peer, addrs.as_slice())) } - fn get_pending_reservations(&self) -> impl Iterator { - self.connections + pub fn previous_relays(&self) -> impl Iterator { + self.previous_relays .iter() - .filter(|(_, info)| { - matches!( - info.relay_status, - RelayStatus::Supported { - status: ReservationStatus::Pending { .. } - } - ) - }) - .map(|((peer_id, connection_id), _)| (peer_id, connection_id)) + .map(|(peer, addr, ts)| (peer, addr, ts)) } - fn get_pending_reservations_count(&self) -> usize { - self.get_pending_reservations().count() + fn static_dial_in_cooldown(&self, peer_id: &PeerId) -> bool { + self.static_dial_cooldowns + .get(peer_id) + .is_some_and(|deadline| *deadline > Instant::now()) } - pub fn set_peer_ping( - &mut self, - peer_id: PeerId, - connection_id: ConnectionId, - duration: Duration, - ) { - let Some(info) = self.connections.get_mut(&(peer_id, connection_id)) else { + 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(addresses).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 { 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())); + } - info.latency.rotate_left(1); - info.latency[4] = duration; + fn forget_previous_relay(&mut self, peer_id: &PeerId) { + self.previous_relays.retain(|(p, _, _)| p != peer_id); } - fn get_potential_targets(&self) -> impl Iterator { - self.connections - .iter() - .filter(|(_, info)| { - matches!( - info.relay_status, - RelayStatus::Supported { - status: ReservationStatus::Idle - } - ) - }) - .map(|((peer_id, connection_id), info)| (peer_id, connection_id, info)) + 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 = 1u32 << exponent; + self.config + .failure_cooldown + .saturating_mul(scale) + .min(self.config.failure_cooldown_max) } - fn disable_reservation(&mut self, id: ListenerId) { - let Some((peer_id, connection_id)) = self.connection_reservation.shift_remove(&id) else { - tracing::error!(listener_id=%id, "could not find reservation with listener id."); - return; - }; + fn clear_failure(&mut self, peer_id: &PeerId) { + self.failure_counts.remove(peer_id); + } - let Some(info) = self.connections.get_mut(&(peer_id, connection_id)) else { - tracing::error!(%peer_id, %connection_id, listener_id=%id, "connection not found."); - return; - }; + fn determine_status_from_external_addresses(&mut self) { + let has_public_addr = self + .external_addresses + .iter() + .any(|addr| !addr.is_relayed()); - let should_retry = match info.relay_status { - RelayStatus::Supported { - status: ReservationStatus::Active { .. }, - } => { - // TODO: Determine if we should disconnect then reconnect? - info.relay_status = RelayStatus::Supported { - status: ReservationStatus::Idle, - }; - true - } - RelayStatus::Supported { - status: ReservationStatus::Pending { .. }, - } => { - info.relay_status = RelayStatus::Supported { - status: ReservationStatus::Idle, - }; - true - } - RelayStatus::Pending - | RelayStatus::Supported { - status: ReservationStatus::Idle, - } - | RelayStatus::NotSupported => false, + let new_status = match has_public_addr { + true => Status::Disable, + false => Status::Enable, }; - - if should_retry { - self.meet_reservation_target(Selection::InOrder); + 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 disable_all_reservations(&mut self) { - for (listener_id, peer_connection) in self.connection_reservation.iter() { - let (peer_id, connection_id) = peer_connection; - let Some(connection) = self.connections.get_mut(peer_connection) else { - tracing::warn!(%peer_id, %connection_id, "connection not found when it should have been present. skipping"); - continue; - }; + fn is_peer_idle(&self, peer_id: &PeerId) -> bool { + self.connections.iter().any(|((pid, _), info)| { + pid == peer_id + && info.relay_status + == RelayStatus::Supported { + status: ReservationStatus::Idle, + } + }) + } - debug_assert!(matches!( - connection.relay_status, - RelayStatus::Supported { - status: ReservationStatus::Active { id } | ReservationStatus::Pending { id } - } if id == *listener_id - )); + fn has_direct_connection(&self, peer_id: &PeerId) -> bool { + self.connections + .iter() + .any(|((pid, _), info)| pid == peer_id && !info.address.is_relayed()) + } - connection.relay_status = RelayStatus::Supported { - status: ReservationStatus::Idle, - }; + 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, _)| *listener_id) + { self.events - .push_back(ToSwarm::RemoveListener { id: *listener_id }); - tracing::info!(%peer_id, %connection_id, ?listener_id, "removing relay listener"); + .push_back(ToSwarm::RemoveListener { id: listener_id }); } } - fn select_connection_for_reservation( - &mut self, - peer_id: PeerId, - connection_id: ConnectionId, - ) -> bool { - if self + fn select_connection_for_reservation(&mut self, peer_id: PeerId, connection_id: ConnectionId) { + let info = self .connections - .get(&(peer_id, connection_id)) - .is_some_and(|info| { - matches!( - info.relay_status, - RelayStatus::Supported { - status: ReservationStatus::Pending { .. } - | ReservationStatus::Active { .. } - } - ) + .get_mut(&(peer_id, connection_id)) + .expect("connection is present"); + + if info.relay_status + != (RelayStatus::Supported { + status: ReservationStatus::Idle, }) { - tracing::warn!(%peer_id, %connection_id, "connection already has a reservation or pending reservation. skipping"); - return false; - } - - if self.connections.is_empty() { - tracing::warn!(%peer_id, "no connections present. removing entry"); - return false; + return; } - let Some(info) = self.connections.get_mut(&(peer_id, connection_id)) else { - tracing::warn!(%peer_id, %connection_id, "connection not found. skipping"); - return false; - }; - 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 addr = opts.address(); + let opts = ListenOpts::new(addr_with_peer_id.with(Protocol::P2pCircuit)); let id = opts.listener_id(); - tracing::info!(%peer_id, %connection_id, %addr, ?id, "new pending reservation"); - info.relay_status = RelayStatus::Supported { status: ReservationStatus::Pending { id }, }; - self.connection_reservation - .insert(id, (peer_id, connection_id)); + self.reservations.insert(id, (peer_id, connection_id)); self.events.push_back(ToSwarm::ListenOn { opts }); - true } - fn meet_reservation_target(&mut self, selection: Selection) { - if !self.enable_auto_relay { - return; - } - - // check to determine if there is a public external address that could possibly let us know the node - // is reachable - if self - .external_addresses + /// Removes all existing reservations. + pub fn remove_all_reservations(&mut self) { + let relay_listeners = self + .reservations .iter() - .any(|addr| addr.is_public() && !addr.is_relayed()) - { - tracing::trace!("local node reachable. autorelay will not run"); - return; - } - - let max = self.max_reservation.get() as usize; + .map(|(id, (peer_id, conn_id))| (*id, *peer_id, *conn_id)) + .collect::>(); - let peers_not_supported = self.connections.is_empty() - || self - .connections - .iter() - .all(|(_, info)| info.relay_status == RelayStatus::NotSupported); + for (listener_id, peer_id, connection_id) in relay_listeners { + let Some(connection) = self.connections.get_mut(&(peer_id, connection_id)) else { + continue; + }; - if peers_not_supported { - if self.static_relays.is_empty() { - tracing::warn!("no relays present."); - self.events - .push_back(ToSwarm::GenerateEvent(Event::NoRelayAvailable)); - return; - } - for (peer_id, addrs) in self.static_relays.iter() { - let opts = DialOpts::peer_id(*peer_id) - .addresses(Vec::from_iter(addrs.clone())) - .build(); - self.events.push_back(ToSwarm::Dial { opts }); + 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, + }; + + self.events + .push_back(ToSwarm::RemoveListener { id: listener_id }); + } + } + + fn disable_reservation(&mut self, id: ListenerId, failed: bool) { + if self.external_reservations.remove(&id).is_some() { + self.meet_reservation_target(); return; } - let relayed_targets = self + let Some((peer_id, connection_id)) = self.reservations.remove(&id) else { + return; + }; + + let Some(address) = self .connections - .iter() - .filter(|(_, info)| { + .get(&(peer_id, connection_id)) + .filter(|info| { matches!( info.relay_status, RelayStatus::Supported { status: ReservationStatus::Active { .. } + | ReservationStatus::Pending { .. } } ) }) - .count(); - - tracing::info!(?relayed_targets, ?max, "relayed targets"); - - if relayed_targets == max { - tracing::warn!("max reservation reached. no more reservations will be made"); + .map(|info| info.address.clone()) + else { + self.meet_reservation_target(); return; - } + }; - let pending_targets = self.get_pending_reservations_count(); + let blacklist_duration = failed.then(|| self.record_failure(peer_id)); - if pending_targets == max { - tracing::warn!("pending targets reached max target."); - return; + 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, + }; + } } - let max = max - relayed_targets; - - let targets = self - .get_potential_targets() - .map(|(peer_id, connection_id, info)| (*peer_id, *connection_id, info)) - .collect::>(); + self.record_previous_relay(peer_id, address); + self.meet_reservation_target(); + } - let targets_count = std::cmp::min(targets.len(), max); + fn covered_peers(&self) -> HashSet { + self.reservations + .values() + .map(|(peer_id, _)| *peer_id) + .chain(self.external_reservations.values().copied()) + .collect() + } - if targets_count == 0 || max == 0 { - tracing::warn!("no potential targets to meet reservation target."); + /// Meet the reservation target by selecting connections to establish a reservation. + fn meet_reservation_target(&mut self) { + if self.status == Status::Disable { return; } - let remaining_targets_needed = targets_count.saturating_sub(pending_targets); - - if remaining_targets_needed == 0 { - tracing::warn!("no potential targets to meet reservation target."); + 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; } - let new_targets = match selection { - Selection::InOrder => targets - .into_iter() - .map(|(peer_id, connection_id, _)| (peer_id, connection_id)) - .take(remaining_targets_needed) - .collect::>(), - Selection::Random => { - let mut rng = rand::thread_rng(); - targets - .into_iter() - .map(|(peer_id, connection_id, _)| (peer_id, connection_id)) - .choose_multiple(&mut rng, remaining_targets_needed) + 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; } - Selection::Peer(peer_id) => targets - .into_iter() - .filter(|(id, _, _)| *id == peer_id) - .map(|(peer_id, connection_id, _)| (peer_id, connection_id)) - .collect::>(), - Selection::LowestLatency => { - let mut targets = targets; - targets.sort_by(|(_, _, info1), (_, _, info2)| { - let avg1 = info1.average_latency(); - let avg2 = info2.average_latency(); - avg1.cmp(&avg2) - }); - - targets - .into_iter() - .take(remaining_targets_needed) - .map(|(peer_id, connection_id, _)| (peer_id, connection_id)) - .collect::>() + if info.relay_status + != (RelayStatus::Supported { + status: ReservationStatus::Idle, + }) + { + continue; } - }; - - for (peer_id, connection_id) in new_targets { - if self.get_pending_reservations_count() == max { - break; + 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); } + } - if !self.select_connection_for_reservation(peer_id, connection_id) { - continue; - } + let selected_candidates: Vec<(PeerId, ConnectionId)> = static_candidates + .into_iter() + .chain(candidates) + .take(budget) + .collect(); + + for (peer_id, connection_id) in selected_candidates { + self.select_connection_for_reservation(peer_id, connection_id); } - assert!(self.get_pending_reservations_count() <= max); + debug_assert!(self.covered_peers().len() <= max); } -} -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum Event { - NoRelayAvailable, - AutoRelayEnabled, - AutoRelayDisabled, + 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 { - type ConnectionHandler = Either; + type ConnectionHandler = Either; type ToSwarm = Event; fn handle_established_inbound_connection( @@ -552,7 +580,7 @@ impl NetworkBehaviour for Behaviour { _remote_addr: &Multiaddr, ) -> Result, ConnectionDenied> { if local_addr.is_relayed() { - Ok(Either::Right(dummy::DummyHandler)) + Ok(Either::Right(dummy::ConnectionHandler)) } else { Ok(Either::Left(handler::Handler::default())) } @@ -567,86 +595,40 @@ impl NetworkBehaviour for Behaviour { _port_use: PortUse, ) -> Result, ConnectionDenied> { if addr.is_relayed() { - Ok(Either::Right(dummy::DummyHandler)) + Ok(Either::Right(dummy::ConnectionHandler)) } else { Ok(Either::Left(handler::Handler::default())) } } - fn handle_pending_outbound_connection( - &mut self, - _connection_id: ConnectionId, - maybe_peer: Option, - _addresses: &[Multiaddr], - _effective_role: Endpoint, - ) -> Result, ConnectionDenied> { - // To prevent providing addresses from active connections, we will only focus on addresses added here that are considered to be fixed/static relays. - let Some(addrs) = maybe_peer - .and_then(|peer_id| self.static_relays.get(&peer_id).cloned()) - .map(Vec::from_iter) - else { - return Ok(vec![]); - }; - - Ok(addrs) - } - fn on_swarm_event(&mut self, event: FromSwarm) { let change = self.external_addresses.on_swarm_event(&event); - if change { - if self - .external_addresses - .iter() - .any(|addr| addr.is_public() && !addr.is_relayed()) - { - tracing::info!("local node is reachable. disabling autorelay"); - self.disable_all_reservations(); - self.backoff.take(); - } else if self.external_addresses.iter().count() == 0 - || self - .external_addresses - .iter() - .any(|addr| !addr.is_public() || addr.is_relayed()) - { - tracing::info!("local node is not reachable. enabling autorelay"); - self.backoff.replace(Delay::new(BACKOFF_INTERVAL)); - } - return; + + if self.auto_status_change && change { + self.determine_status_from_external_addresses(); } match event { FromSwarm::ConnectionEstablished(ConnectionEstablished { peer_id, - connection_id, endpoint, + connection_id, .. }) => { - let addr = endpoint.get_remote_address().clone(); - - tracing::trace!(%peer_id, %connection_id, %addr, "connection established"); + let remote_addr = endpoint.get_remote_address().clone(); - let mut info = PeerInfo { - address: addr, + let mut connection = Connection { + address: remote_addr, relay_status: RelayStatus::Pending, - latency: [Duration::ZERO; 5], }; - // in the event that the address is from a peer going through a relay, automatically disqualify the connection - // from being used as a potential relay since there is no support for multi-HOP - if info.check_for_disqualifying_address() { - self.connections.insert((peer_id, connection_id), info); - return; - } + connection.disqualify_connection_if_relayed(); - match self.static_relays.get(&peer_id) { - Some(addrs) if addrs.contains(&info.address) => { - // prioritize static relays so it would have a higher chance of being selected first - self.connections - .insert_before(0, (peer_id, connection_id), info); - } - _ => { - self.connections.insert((peer_id, connection_id), info); - } + 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 { @@ -654,84 +636,106 @@ impl NetworkBehaviour for Behaviour { connection_id, .. }) => { - tracing::trace!(%peer_id, %connection_id, "connection closed"); - self.connections.shift_remove(&(peer_id, connection_id)); - - if let Some(listener_id) = self - .connection_reservation - .iter() - .find(|(_, (peer, conn_id))| peer_id.eq(peer) && connection_id.eq(conn_id)) - .map(|(id, _)| *id) + let connection = self + .connections + .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 { - self.connection_reservation.shift_remove(&listener_id); + self.reservations.remove(&id); + self.meet_reservation_target(); } - } - FromSwarm::DialFailure(DialFailure { - peer_id, - connection_id, - error, - }) => { - tracing::error!(maybe_peer = ?peer_id, %connection_id, %error, "failed to dial peer"); - let Some(peer_id) = peer_id else { - return; - }; + if had_reservation { + self.record_previous_relay(peer_id, connection.address); + } - self.connections.shift_remove(&(peer_id, connection_id)); + if let Some(address) = self.static_relays.get(&peer_id).cloned() { + self.queue_static_dial(peer_id, address); + } + + self.update_relay_availability(); } FromSwarm::AddressChange(AddressChange { peer_id, connection_id, - old, + old: _, new, }) => { - let old_addr = old.get_remote_address(); - let new_addr = new.get_remote_address(); - - debug_assert!(old_addr != new_addr); - - let info = self + let connection = self .connections .get_mut(&(peer_id, connection_id)) - .expect("connection is present"); + .expect("valid connection"); - info.address = new_addr.clone(); - tracing::trace!(%peer_id, %connection_id, %old_addr, %new_addr, "address changed"); + let new_addr = new.get_remote_address(); + + 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.connection_reservation.get(&listener_id) - else { - return; - }; - - let Some(info) = self.connections.get_mut(&(*peer_id, *connection_id)) else { - tracing::warn!(%peer_id, %connection_id, "connection not found when it should have been present. skipping"); - return; - }; - - let RelayStatus::Supported { - status: ReservationStatus::Pending { id }, - } = info.relay_status - else { - tracing::warn!(%peer_id, %connection_id, "connection doesnt have a pending reservation. skipping"); + 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 }, + }; + self.forget_previous_relay(&peer_id); + self.clear_failure(&peer_id); + } return; - }; - - info.relay_status = RelayStatus::Supported { - status: ReservationStatus::Active { id }, - }; + } - tracing::info!(%peer_id, %connection_id, %addr, %id, "active reservation with relay"); + 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, false); } - FromSwarm::ExpiredListenAddr(ExpiredListenAddr { listener_id, .. }) - | FromSwarm::ListenerError(ListenerError { listener_id, .. }) - | FromSwarm::ListenerClosed(ListenerClosed { listener_id, .. }) => { - self.disable_reservation(listener_id) + FromSwarm::ListenerError(ListenerError { listener_id, .. }) => { + self.disable_reservation(listener_id, true); + } + FromSwarm::ListenerClosed(ListenerClosed { + listener_id, + reason, + .. + }) => { + 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"); + self.static_dial_cooldowns + .insert(peer_id, Instant::now() + self.config.failure_cooldown); } _ => {} } @@ -745,29 +749,57 @@ impl NetworkBehaviour for Behaviour { ) { let Either::Left(event) = event; - let Some(peer_info) = 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 => { - peer_info.relay_status = RelayStatus::Supported { - status: ReservationStatus::Idle, - }; - self.meet_reservation_target(Selection::InOrder); + if matches!( + connection.relay_status, + RelayStatus::Pending | RelayStatus::NotSupported + ) { + 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(); + } } Out::Unsupported => { - let previous_status = peer_info.relay_status; - peer_info.relay_status = RelayStatus::NotSupported; - // if there is a change in protocol support during an active reservation, - // we should remove the reservation if its not already removed - - if self.remove_active_reservation_on_unsupport - && let RelayStatus::Supported { - status: ReservationStatus::Active { id } | ReservationStatus::Pending { id }, - } = previous_status - { + let drop_listener = match connection.relay_status { + RelayStatus::Supported { + status: ReservationStatus::Pending { id } | ReservationStatus::Active { id }, + } => 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 => { + if matches!( + connection.relay_status, + RelayStatus::Supported { + status: ReservationStatus::Blacklisted + } + ) { + connection.relay_status = RelayStatus::Supported { + status: ReservationStatus::Idle, + }; + self.meet_reservation_target(); } } } @@ -781,28 +813,7 @@ impl NetworkBehaviour for Behaviour { return Poll::Ready(event); } - if self.backoff.poll_unpin(cx).is_ready() { - tracing::debug!("attempting to meet reservation target after node became unreachable"); - self.meet_reservation_target(Selection::InOrder); - } - - if self.capacity_cleanup.poll_unpin(cx).is_ready() { - if (self.events.is_empty() || self.events.len() < MAX_CAP) - && self.events.capacity() > MAX_CAP - { - self.events.shrink_to_fit(); - } - - if (self.connections.is_empty() || self.connections.len() < MAX_CAP) - && self.connections.capacity() > MAX_CAP - { - self.connections.shrink_to_fit(); - } - - self.capacity_cleanup.reset(CLEANUP_INTERVAL); - } - - self.waker.replace(cx.waker().clone()); + self.waker = Some(cx.waker().clone()); Poll::Pending } diff --git a/src/behaviour/autorelay/handler.rs b/src/behaviour/autorelay/handler.rs index 3c1f7f2..210a04f 100644 --- a/src/behaviour/autorelay/handler.rs +++ b/src/behaviour/autorelay/handler.rs @@ -1,15 +1,17 @@ use std::{ collections::VecDeque, task::{Context, Poll}, + time::Duration, }; -use libp2p::{ - core::upgrade::DeniedUpgrade, - swarm::{ - ConnectionHandler, ConnectionHandlerEvent, SubstreamProtocol, SupportedProtocols, - handler::ConnectionEvent, - }, +use crate::prelude::swarm::handler::ConnectionEvent; +use crate::prelude::swarm::{ + ConnectionHandler, ConnectionHandlerEvent, SubstreamProtocol, SupportedProtocols, }; +use crate::prelude::transport::upgrade::DeniedUpgrade; +use futures::FutureExt; +use futures_timer::Delay; +use libp2p::relay::HOP_PROTOCOL_NAME; #[derive(Default, Debug)] pub struct Handler { @@ -24,17 +26,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 +59,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, @@ -68,7 +84,7 @@ impl ConnectionHandler for Handler { let valid = self .supported_protocol .iter() - .any(|proto| libp2p::relay::HOP_PROTOCOL_NAME.eq(proto)); + .any(|proto| HOP_PROTOCOL_NAME.eq(proto)); match (valid, self.supported) { (true, false) => { @@ -78,6 +94,7 @@ impl ConnectionHandler for Handler { } (false, true) => { self.supported = false; + self.blacklist_timer = None; self.events .push_back(ConnectionHandlerEvent::NotifyBehaviour( Out::Unsupported, @@ -94,13 +111,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/src/handle/relay.rs b/src/handle/relay.rs index f6cbed2..69b3efe 100644 --- a/src/handle/relay.rs +++ b/src/handle/relay.rs @@ -33,19 +33,12 @@ where rx.await.map_err(io::Error::other)? } - pub async fn remove_static_relay(&self, peer_id: PeerId, addr: Multiaddr) -> io::Result { + pub async fn remove_static_relay(&self, peer_id: PeerId) -> io::Result { let (tx, rx) = oneshot::channel(); self.connexa .to_task .clone() - .send( - AutoRelayCommand::RemoveStaticRelay { - peer_id, - relay_addr: addr, - resp: tx, - } - .into(), - ) + .send(AutoRelayCommand::RemoveStaticRelay { peer_id, resp: tx }.into()) .await?; rx.await.map_err(io::Error::other)? } diff --git a/src/multiaddr_ext.rs b/src/multiaddr_ext.rs index b576f02..f371eb9 100644 --- a/src/multiaddr_ext.rs +++ b/src/multiaddr_ext.rs @@ -1,3 +1,4 @@ +use crate::prelude::PeerId; use libp2p::Multiaddr; use libp2p::multiaddr::Protocol; @@ -11,6 +12,7 @@ pub trait MultiaddrExt { fn is_private(&self) -> bool; fn is_unspecified(&self) -> bool; + fn relay_peer_id(&self) -> Option; } impl MultiaddrExt for Multiaddr { @@ -47,6 +49,18 @@ impl MultiaddrExt for Multiaddr { _ => false, }) } + + 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 + } } #[cfg(test)] diff --git a/src/task/ping.rs b/src/task/ping.rs index 07e6532..061efe0 100644 --- a/src/task/ping.rs +++ b/src/task/ping.rs @@ -21,17 +21,17 @@ where Ok(duration) => { tracing::info!("ping to {} at {} took {:?}", peer, connection, duration); - #[cfg(feature = "relay")] - if let Some(autorelay) = self - .swarm - .as_mut() - .expect("swarm valid") - .behaviour_mut() - .autorelay - .as_mut() - { - autorelay.set_peer_ping(peer, connection, duration); - } + // #[cfg(feature = "relay")] + // if let Some(autorelay) = self + // .swarm + // .as_mut() + // .expect("swarm valid") + // .behaviour_mut() + // .autorelay + // .as_mut() + // { + // autorelay.set_peer_ping(peer, connection, duration); + // } } Err(e) => { // TODO: Possibly disconnect peer since if there is an error? diff --git a/src/task/relay.rs b/src/task/relay.rs index c718919..5a3ea22 100644 --- a/src/task/relay.rs +++ b/src/task/relay.rs @@ -1,3 +1,4 @@ +use crate::behaviour::autorelay; use crate::behaviour::peer_store::store::Store; use crate::task::ConnexaTask; use crate::types::AutoRelayCommand; @@ -31,17 +32,13 @@ where let _ = resp.send(Ok(autorelay.add_static_relay(peer_id, relay_addr))); } - AutoRelayCommand::RemoveStaticRelay { - peer_id, - relay_addr, - resp, - } => { + AutoRelayCommand::RemoveStaticRelay { peer_id, resp } => { let Some(autorelay) = swarm.behaviour_mut().autorelay.as_mut() else { let _ = resp.send(Err(std::io::Error::other("autorelay is not enabled"))); return; }; - let _ = resp.send(Ok(autorelay.remove_static_relay(peer_id, relay_addr))); + let _ = resp.send(Ok(autorelay.remove_static_relay(&peer_id))); } AutoRelayCommand::ListStaticRelays { resp } => { let Some(autorelay) = swarm.behaviour_mut().autorelay.as_mut() else { @@ -49,7 +46,10 @@ where return; }; - let list = autorelay.list_static_relays(); + let list = autorelay + .static_relays() + .map(|(peer_id, addr)| (*peer_id, addr.to_vec())) + .collect::>(); let _ = resp.send(Ok(list)); } AutoRelayCommand::GetStaticRelay { peer_id, resp } => { @@ -58,8 +58,14 @@ where return; }; - let addrs = autorelay.get_static_relay_addrs(peer_id); - let _ = resp.send(Ok(addrs)); + let addr = autorelay + .static_relays() + .find(|(p, _)| **p == peer_id) + .map(|(_, addr)| addr.to_vec()) + .ok_or_else(|| { + std::io::Error::new(std::io::ErrorKind::NotFound, "static relay not found") + }); + let _ = resp.send(addr); } AutoRelayCommand::EnableAutoRelay { resp } => { let Some(autorelay) = swarm.behaviour_mut().autorelay.as_mut() else { @@ -67,7 +73,7 @@ where return; }; - autorelay.enable_autorelay(); + autorelay.set_status(Some(autorelay::Status::Enable)); let _ = resp.send(Ok(())); } @@ -77,7 +83,7 @@ where return; }; - autorelay.disable_autorelay(); + autorelay.set_status(Some(autorelay::Status::Disable)); let _ = resp.send(Ok(())); } @@ -87,7 +93,7 @@ where return; }; - autorelay.remove_existing_reservations(); + autorelay.remove_all_reservations(); let _ = resp.send(Ok(())); } diff --git a/src/types.rs b/src/types.rs index 4980a5e..7d06e55 100644 --- a/src/types.rs +++ b/src/types.rs @@ -458,7 +458,6 @@ pub enum AutoRelayCommand { }, RemoveStaticRelay { peer_id: PeerId, - relay_addr: Multiaddr, resp: oneshot::Sender>, }, DisableRelays { From da1379fe73b53a5d43dedd50c971d15a8d5ac32f Mon Sep 17 00:00:00 2001 From: Darius Clark Date: Sun, 31 May 2026 08:32:39 -0500 Subject: [PATCH 59/77] chore: point to master --- Cargo.toml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 784e7d3..508bb84 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -129,9 +129,9 @@ tokio = { default-features = false, features = ["sync", "macros"], workspace = t wasm-bindgen-futures.workspace = true [patch.crates-io] -libp2p = { git = "https://github.com/dariusc93/rust-libp2p.git", branch = "fix/remove-ext-addr-relay" } -libp2p-allow-block-list = { git = "https://github.com/dariusc93/rust-libp2p.git", branch = "fix/remove-ext-addr-relay" } -libp2p-connection-limits = { git = "https://github.com/dariusc93/rust-libp2p.git", branch = "fix/remove-ext-addr-relay" } -libp2p-stream = { git = "https://github.com/dariusc93/rust-libp2p.git", branch = "fix/remove-ext-addr-relay" } -libp2p-webrtc = { git = "https://github.com/dariusc93/rust-libp2p.git", branch = "fix/remove-ext-addr-relay" } -libp2p-webrtc-websys = { git = "https://github.com/dariusc93/rust-libp2p.git", branch = "fix/remove-ext-addr-relay" } \ No newline at end of file +libp2p = { git = "https://github.com/libp2p/rust-libp2p.git", branch = "master" } +libp2p-allow-block-list = { git = "https://github.com/libp2p/rust-libp2p.git", branch = "master" } +libp2p-connection-limits = { git = "https://github.com/libp2p/rust-libp2p.git", branch = "master" } +libp2p-stream = { git = "https://github.com/libp2p/rust-libp2p.git", branch = "master" } +libp2p-webrtc = { git = "https://github.com/libp2p/rust-libp2p.git", branch = "master" } +libp2p-webrtc-websys = { git = "https://github.com/libp2p/rust-libp2p.git", branch = "master" } \ No newline at end of file From ff9d0b573b83a94e3f4f4cc4cfd455ee3c2e42f0 Mon Sep 17 00:00:00 2001 From: Darius Clark Date: Sun, 31 May 2026 08:52:41 -0500 Subject: [PATCH 60/77] chore: update dns resolver and support Quad9 --- Cargo.toml | 2 +- src/builder/transport.rs | 18 +++++++++++++++--- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 508bb84..81c4ba6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -66,7 +66,7 @@ futures-timer = "3.0.0" futures-timeout = "0.1.3" getrandom = { version = "0.2.15" } getrandom_03 = { version = "0.3.3", package = "getrandom" } -hickory-resolver = "0.25.0-alpha.5" +hickory-resolver = "0.26.1" indexmap = "2.10.0" libp2p = { version = "0.57.0" } libp2p-allow-block-list = "0.7.0" diff --git a/src/builder/transport.rs b/src/builder/transport.rs index 75dddac..4e2c5f3 100644 --- a/src/builder/transport.rs +++ b/src/builder/transport.rs @@ -136,6 +136,8 @@ pub enum DnsResolver { /// Cloudflare DNS Resolver #[default] Cloudflare, + /// Quad9 DNS Resolver + Quad9, /// Local DNS Resolver Local, /// No DNS Resolver @@ -147,12 +149,22 @@ pub enum DnsResolver { impl From for (ResolverConfig, ResolverOpts) { fn from(value: DnsResolver) -> Self { match value { - DnsResolver::Google => (ResolverConfig::google(), Default::default()), - DnsResolver::Cloudflare => (ResolverConfig::cloudflare(), Default::default()), + DnsResolver::Google => ( + ResolverConfig::udp_and_tcp(&hickory_resolver::config::GOOGLE), + Default::default(), + ), + DnsResolver::Cloudflare => ( + ResolverConfig::udp_and_tcp(&hickory_resolver::config::CLOUDFLARE), + Default::default(), + ), + DnsResolver::Quad9 => ( + ResolverConfig::udp_and_tcp(&hickory_resolver::config::QUAD9), + Default::default(), + ), DnsResolver::Local => { hickory_resolver::system_conf::read_system_conf().unwrap_or_default() } - DnsResolver::None => (ResolverConfig::new(), Default::default()), + DnsResolver::None => (ResolverConfig::default(), Default::default()), } } } From 1b14e59257c691881f6d0a844427d58608d449d2 Mon Sep 17 00:00:00 2001 From: Darius Clark Date: Sun, 14 Jun 2026 12:15:58 -0400 Subject: [PATCH 61/77] chore: remove feature --- Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 85bbcfd..5a69270 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -59,7 +59,7 @@ indexeddb = ["dep:idb", "serde"] redb = ["dep:redb", "dep:cbor4ii", "serde"] sqlite = ["dep:sqlx", "dep:cbor4ii", "serde"] -webrtc =["dep:libp2p-webrtc", "dep:libp2p-webrtc-websys"] +webrtc = ["dep:libp2p-webrtc", "dep:libp2p-webrtc-websys"] websocket = ["libp2p/websocket", "rcgen", "dep:pem", "libp2p/websocket-websys"] webtransport = ["libp2p/webtransport-websys"] @@ -156,7 +156,7 @@ futures-timer = { workspace = true, features = ["wasm-bindgen"] } getrandom = { workspace = true, features = ["js"] } getrandom_03 = { workspace = true, features = ["wasm_js"] } idb = { workspace = true, optional = true } -libp2p = { features = ["macros", "serde", "wasm-bindgen"], workspace = true } +libp2p = { features = ["macros", "serde"], workspace = true } libp2p-webrtc-websys = { workspace = true, optional = true } send_wrapper = { workspace = true, features = ["futures"] } serde-wasm-bindgen.workspace = true From 62f89b0be28a93fbf8fdfe29a635deb3707220c7 Mon Sep 17 00:00:00 2001 From: Darius Clark Date: Sun, 14 Jun 2026 12:20:51 -0400 Subject: [PATCH 62/77] chore: wasm check --- src/handle.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/handle.rs b/src/handle.rs index 0110853..1d5e851 100644 --- a/src/handle.rs +++ b/src/handle.rs @@ -141,6 +141,7 @@ where } /// Returns a handle for relay server functions + #[cfg(not(target_arch = "wasm32"))] #[cfg(feature = "relay")] pub fn relay_server(&self) -> ConnexaRelayServer<'_, T> { ConnexaRelayServer::new(self) From 240658cd59784250a3012b4ccef9ab05f6798e49 Mon Sep 17 00:00:00 2001 From: Darius Clark Date: Sun, 14 Jun 2026 12:21:44 -0400 Subject: [PATCH 63/77] chore: check --- src/handle.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/handle.rs b/src/handle.rs index 1d5e851..5c09b71 100644 --- a/src/handle.rs +++ b/src/handle.rs @@ -30,6 +30,7 @@ use crate::handle::floodsub::ConnexaFloodsub; #[cfg(feature = "gossipsub")] use crate::handle::gossipsub::ConnexaGossipsub; use crate::handle::peer_store::ConnexaPeerstore; +#[cfg(not(target_arch = "wasm32"))] use crate::handle::relay_server::ConnexaRelayServer; #[cfg(feature = "rendezvous")] use crate::handle::rendezvous::ConnexaRendezvous; From 7e4306524a27726e2cc39f9419cf52830e88621d Mon Sep 17 00:00:00 2001 From: Darius Clark Date: Sun, 14 Jun 2026 12:40:59 -0400 Subject: [PATCH 64/77] chore: wasm check --- src/task/relay.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/task/relay.rs b/src/task/relay.rs index 13dca7e..9dc4b75 100644 --- a/src/task/relay.rs +++ b/src/task/relay.rs @@ -1,5 +1,6 @@ use crate::behaviour::peer_store::store::Store; use crate::task::ConnexaTask; +#[cfg(not(target_arch = "wasm32"))] use crate::types::RelayServerCommand; use libp2p::relay::{Event as RelayServerEvent, client::Event as RelayClientEvent}; use libp2p::swarm::NetworkBehaviour; @@ -33,6 +34,7 @@ where } } + #[cfg(not(target_arch = "wasm32"))] pub fn process_relay_server_event(&mut self, event: RelayServerEvent) { match event { RelayServerEvent::ReservationReqAccepted { @@ -78,6 +80,7 @@ where } } +#[cfg(not(target_arch = "wasm32"))] impl ConnexaTask where X: Default + Send + 'static, From cf88b3b71c9ca38cdff96459a807bcd2cb53a5fa Mon Sep 17 00:00:00 2001 From: Darius Clark Date: Sun, 14 Jun 2026 12:47:30 -0400 Subject: [PATCH 65/77] chore: resolve generic types --- src/handle.rs | 2 +- src/handle/relay_server.rs | 8 ++++---- src/task.rs | 1 + src/task/relay.rs | 2 +- src/types.rs | 1 + 5 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/handle.rs b/src/handle.rs index 5c09b71..1669104 100644 --- a/src/handle.rs +++ b/src/handle.rs @@ -144,7 +144,7 @@ where /// Returns a handle for relay server functions #[cfg(not(target_arch = "wasm32"))] #[cfg(feature = "relay")] - pub fn relay_server(&self) -> ConnexaRelayServer<'_, T> { + pub fn relay_server(&self) -> ConnexaRelayServer<'_, T, K> { ConnexaRelayServer::new(self) } diff --git a/src/handle/relay_server.rs b/src/handle/relay_server.rs index 24a9b99..78d1fcb 100644 --- a/src/handle/relay_server.rs +++ b/src/handle/relay_server.rs @@ -3,15 +3,15 @@ use crate::types::RelayServerCommand; use libp2p::relay::Status as RelayServerStatus; #[derive(Copy, Clone)] -pub struct ConnexaRelayServer<'a, T = ()> { - connexa: &'a Connexa, +pub struct ConnexaRelayServer<'a, T = (), K = crate::keystore::store::memory::MemoryKeystore> { + connexa: &'a Connexa, } -impl<'a, T> ConnexaRelayServer<'a, T> +impl<'a, T, K> ConnexaRelayServer<'a, T, K> where T: Send + Sync + 'static, { - pub(crate) fn new(connexa: &'a Connexa) -> Self { + pub(crate) fn new(connexa: &'a Connexa) -> Self { Self { connexa } } diff --git a/src/task.rs b/src/task.rs index 4287349..7f3a811 100644 --- a/src/task.rs +++ b/src/task.rs @@ -583,6 +583,7 @@ where Command::Rendezvous(rendezvous_command) => { self.process_rendezvous_command(rendezvous_command) } + #[cfg(not(target_arch = "wasm32"))] #[cfg(feature = "relay")] Command::RelayServer(relay_server_command) => { self.process_relay_server_command(relay_server_command) diff --git a/src/task/relay.rs b/src/task/relay.rs index 9dc4b75..793c9f6 100644 --- a/src/task/relay.rs +++ b/src/task/relay.rs @@ -81,7 +81,7 @@ where } #[cfg(not(target_arch = "wasm32"))] -impl ConnexaTask +impl ConnexaTask where X: Default + Send + 'static, C: Send, diff --git a/src/types.rs b/src/types.rs index 0e7c807..2aa69df 100644 --- a/src/types.rs +++ b/src/types.rs @@ -514,6 +514,7 @@ pub enum RendezvousCommand { }, } +#[cfg(not(target_arch = "wasm32"))] #[cfg(feature = "relay")] #[derive(Debug)] pub enum RelayServerCommand { From 622906ae756b9c1305eaacce61e95d693532a962 Mon Sep 17 00:00:00 2001 From: Darius Clark Date: Sun, 14 Jun 2026 13:31:57 -0400 Subject: [PATCH 66/77] chore: checks --- src/task/swarm.rs | 1 + src/types.rs | 2 ++ 2 files changed, 3 insertions(+) diff --git a/src/task/swarm.rs b/src/task/swarm.rs index f596b77..f75309b 100644 --- a/src/task/swarm.rs +++ b/src/task/swarm.rs @@ -291,6 +291,7 @@ where }; match event { + #[cfg(not(target_arch = "wasm32"))] #[cfg(feature = "relay")] BehaviourEvent::Relay(event) => self.process_relay_server_event(event), #[cfg(feature = "relay")] diff --git a/src/types.rs b/src/types.rs index 2aa69df..a0d1be6 100644 --- a/src/types.rs +++ b/src/types.rs @@ -46,6 +46,7 @@ pub enum Command { Rendezvous(RendezvousCommand), #[cfg(feature = "autonat")] Autonat(AutonatCommand), + #[cfg(not(target_arch = "wasm32"))] #[cfg(feature = "relay")] RelayServer(RelayServerCommand), Whitelist(WhitelistCommand), @@ -110,6 +111,7 @@ impl From for Command { } } +#[cfg(not(target_arch = "wasm32"))] #[cfg(feature = "relay")] impl From for Command { fn from(cmd: RelayServerCommand) -> Self { From 3208f42800443b5fec9441c65dde2c103cfb8609 Mon Sep 17 00:00:00 2001 From: Darius Clark Date: Sun, 14 Jun 2026 13:36:55 -0400 Subject: [PATCH 67/77] chore: check --- src/handle.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/handle.rs b/src/handle.rs index 1669104..0d7d7be 100644 --- a/src/handle.rs +++ b/src/handle.rs @@ -8,6 +8,8 @@ pub(crate) mod floodsub; #[cfg(feature = "gossipsub")] pub(crate) mod gossipsub; mod peer_store; + +#[cfg(not(target_arch = "wasm32"))] #[cfg(feature = "relay")] mod relay_server; #[cfg(feature = "rendezvous")] From fc6e9236c93dedfc3b2c4e7801e3a3c3ebce506a Mon Sep 17 00:00:00 2001 From: Darius Clark Date: Sun, 14 Jun 2026 13:57:01 -0400 Subject: [PATCH 68/77] chore: fix types --- src/handle.rs | 2 +- src/handle/relay.rs | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/handle.rs b/src/handle.rs index 74d9bb1..347dccd 100644 --- a/src/handle.rs +++ b/src/handle.rs @@ -150,7 +150,7 @@ where /// Returns a handle for relay functions #[cfg(feature = "relay")] - pub fn relay(&self) -> ConnexaRelay<'_, T> { + pub fn relay(&self) -> ConnexaRelay<'_, T, K> { ConnexaRelay::new(self) } diff --git a/src/handle/relay.rs b/src/handle/relay.rs index 69b3efe..fd20792 100644 --- a/src/handle/relay.rs +++ b/src/handle/relay.rs @@ -4,15 +4,15 @@ use crate::types::AutoRelayCommand; use futures::channel::oneshot; use std::io; -pub struct ConnexaRelay<'a, T> { - connexa: &'a Connexa, +pub struct ConnexaRelay<'a, T, K> { + connexa: &'a Connexa, } -impl<'a, T> ConnexaRelay<'a, T> +impl<'a, T, K> ConnexaRelay<'a, T, K> where T: Send + Sync + 'static, { - pub(crate) fn new(connexa: &'a Connexa) -> Self { + pub(crate) fn new(connexa: &'a Connexa) -> Self { Self { connexa } } From 14ff27b9ed6140f01a98b8b405c87d3a6820abbe Mon Sep 17 00:00:00 2001 From: Darius Clark Date: Sun, 14 Jun 2026 13:57:56 -0400 Subject: [PATCH 69/77] chore: fix example --- examples/autorelay/src/main.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/autorelay/src/main.rs b/examples/autorelay/src/main.rs index 9c3a99d..770f138 100644 --- a/examples/autorelay/src/main.rs +++ b/examples/autorelay/src/main.rs @@ -34,7 +34,8 @@ async fn main() -> std::io::Result<()> { .with_ping() .with_identify() .with_kademlia() - .build()?; + .build() + .await?; for (addr, peer_id) in BOOTSTRAP_NODES { let peer_id: PeerId = peer_id.parse().expect("valid peer id"); From ffd5159a651e45f49a95ee9405692828d525adbd Mon Sep 17 00:00:00 2001 From: Darius Clark Date: Sun, 14 Jun 2026 14:01:10 -0400 Subject: [PATCH 70/77] chore: correction --- src/handle.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/handle.rs b/src/handle.rs index 347dccd..187e0d1 100644 --- a/src/handle.rs +++ b/src/handle.rs @@ -8,10 +8,9 @@ pub(crate) mod floodsub; #[cfg(feature = "gossipsub")] pub(crate) mod gossipsub; mod peer_store; - -#[cfg(not(target_arch = "wasm32"))] #[cfg(feature = "relay")] mod relay; +#[cfg(not(target_arch = "wasm32"))] #[cfg(feature = "relay")] mod relay_server; #[cfg(feature = "rendezvous")] From 98e39be800cf03523330f17c9f7023a51234b87f Mon Sep 17 00:00:00 2001 From: Darius Clark Date: Sun, 14 Jun 2026 14:02:01 -0400 Subject: [PATCH 71/77] chore: correction --- src/types.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/types.rs b/src/types.rs index 25fec91..5259924 100644 --- a/src/types.rs +++ b/src/types.rs @@ -46,9 +46,9 @@ pub enum Command { Rendezvous(RendezvousCommand), #[cfg(feature = "autonat")] Autonat(AutonatCommand), - #[cfg(not(target_arch = "wasm32"))] #[cfg(feature = "relay")] AutoRelay(AutoRelayCommand), + #[cfg(not(target_arch = "wasm32"))] #[cfg(feature = "relay")] RelayServer(RelayServerCommand), Whitelist(WhitelistCommand), @@ -113,7 +113,6 @@ impl From for Command { } } -#[cfg(not(target_arch = "wasm32"))] #[cfg(feature = "relay")] impl From for Command { fn from(cmd: AutoRelayCommand) -> Self { @@ -121,6 +120,7 @@ impl From for Command { } } +#[cfg(not(target_arch = "wasm32"))] #[cfg(feature = "relay")] impl From for Command { fn from(cmd: RelayServerCommand) -> Self { From d8b729fa4c2e1fd7fe111432a15cd6b1e39ab4a8 Mon Sep 17 00:00:00 2001 From: Darius Clark Date: Sun, 14 Jun 2026 14:08:22 -0400 Subject: [PATCH 72/77] chore: update autorelay --- src/behaviour/autorelay.rs | 99 ++++++++++++++++-------------- src/behaviour/autorelay/handler.rs | 48 +++++++-------- 2 files changed, 75 insertions(+), 72 deletions(-) diff --git a/src/behaviour/autorelay.rs b/src/behaviour/autorelay.rs index 37548a1..3d70316 100644 --- a/src/behaviour/autorelay.rs +++ b/src/behaviour/autorelay.rs @@ -3,11 +3,12 @@ use std::{ collections::{HashMap, HashSet, VecDeque}, num::NonZeroU8, task::{Context, Poll, Waker}, - time::{Duration, Instant}, + time::Duration, }; use crate::behaviour::autorelay::handler::Out; use crate::multiaddr_ext::MultiaddrExt; +use crate::prelude::swarm::derive_prelude::{ListenerId, PortUse}; use crate::prelude::swarm::{ ExternalAddresses, ListenOpts, NewListenAddr, NotifyHandler, derive_prelude::{ @@ -21,8 +22,7 @@ use crate::prelude::swarm::{ use crate::prelude::transport::Endpoint; use crate::prelude::{PeerId, Protocol}; use either::Either; - -use crate::prelude::swarm::derive_prelude::{ListenerId, PortUse}; +use web_time::{Instant, SystemTime}; mod handler; @@ -46,7 +46,7 @@ pub struct Behaviour { failure_counts: HashMap, - previous_relays: VecDeque<(PeerId, Multiaddr, Instant)>, + previous_relays: VecDeque<(PeerId, Multiaddr, SystemTime)>, relays_available: bool, @@ -117,7 +117,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 { @@ -153,8 +153,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 } } @@ -177,8 +182,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 } @@ -213,10 +220,10 @@ impl Behaviour { /// 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) -> bool { + 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 false; + return; } let entry = self.static_relays.entry(peer_id).or_default(); @@ -225,21 +232,19 @@ impl Behaviour { } else { entry.push(address); } - let addrs = entry.clone(); + 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, addrs) { + if !self.queue_static_dial(peer_id, combined) { self.meet_reservation_target(); } if let Some(waker) = self.waker.take() { waker.wake(); } - - return true; } /// Remove peer as a static relay. @@ -255,7 +260,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)) @@ -289,7 +294,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) { @@ -388,7 +393,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; } }; @@ -404,7 +410,7 @@ impl Behaviour { } /// Removes all existing reservations. - pub fn remove_all_reservations(&mut self) { + fn remove_all_reservations(&mut self) { let relay_listeners = self .reservations .iter() @@ -511,8 +517,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; @@ -524,13 +530,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 @@ -636,10 +644,13 @@ 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; + }; + + if !self.connections.keys().any(|(pid, _)| *pid == peer_id) { + self.clear_failure(&peer_id); + } let had_reservation = matches!( connection.relay_status, @@ -662,8 +673,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(); @@ -674,10 +685,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(); @@ -690,10 +700,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, @@ -749,10 +759,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 => { diff --git a/src/behaviour/autorelay/handler.rs b/src/behaviour/autorelay/handler.rs index 210a04f..169e1ff 100644 --- a/src/behaviour/autorelay/handler.rs +++ b/src/behaviour/autorelay/handler.rs @@ -76,36 +76,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 328889e0414f016709942614d7d99aabc659fd21 Mon Sep 17 00:00:00 2001 From: Darius Clark Date: Sun, 14 Jun 2026 17:00:22 -0400 Subject: [PATCH 73/77] Revert "Merge branch 'chore/update-libp2p' into feat/autorelay" This reverts commit 6d2025e39ea7c50d6a404c4e1c7348aa5a322f11, reversing changes made to 45055d08d2482d6ebdc27b435b4ee7e5e67e8aa3. --- Cargo.toml | 2 +- src/builder/transport.rs | 18 +++--------------- src/handle.rs | 4 +--- src/handle/relay_server.rs | 8 ++++---- src/task.rs | 1 - src/task/relay.rs | 5 +---- src/task/swarm.rs | 1 - src/types.rs | 1 - 8 files changed, 10 insertions(+), 30 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 762bdd5..90e6dc0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -80,7 +80,7 @@ futures-timer = "3.0.0" futures-timeout = "0.1.3" getrandom = { version = "0.2.15" } getrandom_03 = { version = "0.3.3", package = "getrandom" } -hickory-resolver = "0.26.1" +hickory-resolver = "0.25.2" idb = "0.6.5" indexmap = "2.10.0" libp2p = { version = "0.57.0" } diff --git a/src/builder/transport.rs b/src/builder/transport.rs index f462d78..22b3171 100644 --- a/src/builder/transport.rs +++ b/src/builder/transport.rs @@ -163,8 +163,6 @@ pub enum DnsResolver { /// Cloudflare DNS Resolver #[default] Cloudflare, - /// Quad9 DNS Resolver - Quad9, /// Local DNS Resolver Local, /// No DNS Resolver @@ -176,22 +174,12 @@ pub enum DnsResolver { impl From for (ResolverConfig, ResolverOpts) { fn from(value: DnsResolver) -> Self { match value { - DnsResolver::Google => ( - ResolverConfig::udp_and_tcp(&hickory_resolver::config::GOOGLE), - Default::default(), - ), - DnsResolver::Cloudflare => ( - ResolverConfig::udp_and_tcp(&hickory_resolver::config::CLOUDFLARE), - Default::default(), - ), - DnsResolver::Quad9 => ( - ResolverConfig::udp_and_tcp(&hickory_resolver::config::QUAD9), - Default::default(), - ), + DnsResolver::Google => (ResolverConfig::google(), Default::default()), + DnsResolver::Cloudflare => (ResolverConfig::cloudflare(), Default::default()), DnsResolver::Local => { hickory_resolver::system_conf::read_system_conf().unwrap_or_default() } - DnsResolver::None => (ResolverConfig::default(), Default::default()), + DnsResolver::None => (ResolverConfig::new(), Default::default()), } } } diff --git a/src/handle.rs b/src/handle.rs index 187e0d1..0cb47c1 100644 --- a/src/handle.rs +++ b/src/handle.rs @@ -36,7 +36,6 @@ use crate::handle::peer_store::ConnexaPeerstore; #[cfg(feature = "relay")] use crate::handle::relay::ConnexaRelay; #[cfg(feature = "relay")] -#[cfg(not(target_arch = "wasm32"))] use crate::handle::relay_server::ConnexaRelayServer; #[cfg(feature = "rendezvous")] use crate::handle::rendezvous::ConnexaRendezvous; @@ -154,9 +153,8 @@ where } /// Returns a handle for relay server functions - #[cfg(not(target_arch = "wasm32"))] #[cfg(feature = "relay")] - pub fn relay_server(&self) -> ConnexaRelayServer<'_, T, K> { + pub fn relay_server(&self) -> ConnexaRelayServer<'_, T> { ConnexaRelayServer::new(self) } diff --git a/src/handle/relay_server.rs b/src/handle/relay_server.rs index 78d1fcb..24a9b99 100644 --- a/src/handle/relay_server.rs +++ b/src/handle/relay_server.rs @@ -3,15 +3,15 @@ use crate::types::RelayServerCommand; use libp2p::relay::Status as RelayServerStatus; #[derive(Copy, Clone)] -pub struct ConnexaRelayServer<'a, T = (), K = crate::keystore::store::memory::MemoryKeystore> { - connexa: &'a Connexa, +pub struct ConnexaRelayServer<'a, T = ()> { + connexa: &'a Connexa, } -impl<'a, T, K> ConnexaRelayServer<'a, T, K> +impl<'a, T> ConnexaRelayServer<'a, T> where T: Send + Sync + 'static, { - pub(crate) fn new(connexa: &'a Connexa) -> Self { + pub(crate) fn new(connexa: &'a Connexa) -> Self { Self { connexa } } diff --git a/src/task.rs b/src/task.rs index 18483d0..76ebda0 100644 --- a/src/task.rs +++ b/src/task.rs @@ -587,7 +587,6 @@ where Command::Rendezvous(rendezvous_command) => { self.process_rendezvous_command(rendezvous_command) } - #[cfg(not(target_arch = "wasm32"))] #[cfg(feature = "relay")] Command::RelayServer(relay_server_command) => { self.process_relay_server_command(relay_server_command) diff --git a/src/task/relay.rs b/src/task/relay.rs index 913c47d..a08aeca 100644 --- a/src/task/relay.rs +++ b/src/task/relay.rs @@ -2,7 +2,6 @@ use crate::behaviour::autorelay; use crate::behaviour::peer_store::store::Store; use crate::task::ConnexaTask; use crate::types::AutoRelayCommand; -#[cfg(not(target_arch = "wasm32"))] use crate::types::RelayServerCommand; use libp2p::relay::{Event as RelayServerEvent, client::Event as RelayClientEvent}; use libp2p::swarm::NetworkBehaviour; @@ -122,7 +121,6 @@ where } } - #[cfg(not(target_arch = "wasm32"))] pub fn process_relay_server_event(&mut self, event: RelayServerEvent) { match event { RelayServerEvent::ReservationReqAccepted { @@ -168,8 +166,7 @@ where } } -#[cfg(not(target_arch = "wasm32"))] -impl ConnexaTask +impl ConnexaTask where X: Default + Send + 'static, C: Send, diff --git a/src/task/swarm.rs b/src/task/swarm.rs index f75309b..f596b77 100644 --- a/src/task/swarm.rs +++ b/src/task/swarm.rs @@ -291,7 +291,6 @@ where }; match event { - #[cfg(not(target_arch = "wasm32"))] #[cfg(feature = "relay")] BehaviourEvent::Relay(event) => self.process_relay_server_event(event), #[cfg(feature = "relay")] diff --git a/src/types.rs b/src/types.rs index 5259924..47de30f 100644 --- a/src/types.rs +++ b/src/types.rs @@ -555,7 +555,6 @@ pub enum RendezvousCommand { }, } -#[cfg(not(target_arch = "wasm32"))] #[cfg(feature = "relay")] #[derive(Debug)] pub enum RelayServerCommand { From 5d8fc2de52d701a70bf9a3767c173ab640cf580c Mon Sep 17 00:00:00 2001 From: Darius Clark Date: Sun, 14 Jun 2026 17:16:48 -0400 Subject: [PATCH 74/77] chore: minor reversals and changes --- Cargo.toml | 22 ++++++------------ examples/upnp/src/main.rs | 9 ++++--- src/behaviour/autorelay.rs | 22 ++++++++++-------- src/behaviour/request_response/codec.rs | 2 ++ src/handle.rs | 11 --------- src/handle/relay_server.rs | 31 ------------------------- src/task.rs | 4 ---- src/task/relay.rs | 30 ------------------------ src/task/upnp.rs | 18 ++++---------- src/types.rs | 20 ---------------- 10 files changed, 29 insertions(+), 140 deletions(-) delete mode 100644 src/handle/relay_server.rs diff --git a/Cargo.toml b/Cargo.toml index 90e6dc0..ce9e1d4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -83,12 +83,12 @@ getrandom_03 = { version = "0.3.3", package = "getrandom" } hickory-resolver = "0.25.2" idb = "0.6.5" indexmap = "2.10.0" -libp2p = { version = "0.57.0" } -libp2p-allow-block-list = "0.7.0" -libp2p-connection-limits = "0.7.0" -libp2p-stream = { version = "0.5.0-alpha" } -libp2p-webrtc = { version = "=0.10.0-alpha", features = ["pem"] } -libp2p-webrtc-websys = "0.5.0" +libp2p = { version = "0.56.0" } +libp2p-allow-block-list = "0.6.0" +libp2p-connection-limits = "0.6.0" +libp2p-stream = { version = "=0.4.0-alpha" } +libp2p-webrtc = { version = "=0.9.0-alpha.1", features = ["pem"] } +libp2p-webrtc-websys = "0.4.0" other-error = "0.1.1" pollable-map = "0.1.7" parking_lot = "0.12" @@ -180,12 +180,4 @@ tokio = { default-features = false, features = ["sync", "macros"], workspace = t url = { workspace = true, optional = true } wasm-bindgen.workspace = true wasm-bindgen-futures.workspace = true -web-sys = { workspace = true, optional = true } - -[patch.crates-io] -libp2p = { git = "https://github.com/libp2p/rust-libp2p.git", branch = "master" } -libp2p-allow-block-list = { git = "https://github.com/libp2p/rust-libp2p.git", branch = "master" } -libp2p-connection-limits = { git = "https://github.com/libp2p/rust-libp2p.git", branch = "master" } -libp2p-stream = { git = "https://github.com/libp2p/rust-libp2p.git", branch = "master" } -libp2p-webrtc = { git = "https://github.com/libp2p/rust-libp2p.git", branch = "master" } -libp2p-webrtc-websys = { git = "https://github.com/libp2p/rust-libp2p.git", branch = "master" } \ No newline at end of file +web-sys = { workspace = true, optional = true } \ No newline at end of file diff --git a/examples/upnp/src/main.rs b/examples/upnp/src/main.rs index af34719..3f881a9 100644 --- a/examples/upnp/src/main.rs +++ b/examples/upnp/src/main.rs @@ -13,16 +13,15 @@ async fn main() -> std::io::Result<()> { println!("New listen address: {addr}") } SwarmEvent::Behaviour(BehaviourEvent::Upnp(event)) => match event { - UpnpEvent::NewExternalAddr { external_addr, .. } => { - println!("New external address: {external_addr}") - } - UpnpEvent::ExpiredExternalAddr { external_addr, .. } => { - println!("Expired external address: {external_addr}") + UpnpEvent::NewExternalAddr(addr) => println!("New external address: {addr}"), + UpnpEvent::ExpiredExternalAddr(addr) => { + println!("Expired external address: {addr}") } UpnpEvent::GatewayNotFound => println!("Gateway not found"), UpnpEvent::NonRoutableGateway => println!("Gateway is not routable"), }, _ => {} + _ => {} }) .build() .await?; diff --git a/src/behaviour/autorelay.rs b/src/behaviour/autorelay.rs index 3d70316..d974a63 100644 --- a/src/behaviour/autorelay.rs +++ b/src/behaviour/autorelay.rs @@ -1,11 +1,4 @@ // TODO: Replace with builtin autorelay behaviour from libp2p. See https://github.com/libp2p/rust-libp2p/pull/6156 -use std::{ - collections::{HashMap, HashSet, VecDeque}, - num::NonZeroU8, - task::{Context, Poll, Waker}, - time::Duration, -}; - use crate::behaviour::autorelay::handler::Out; use crate::multiaddr_ext::MultiaddrExt; use crate::prelude::swarm::derive_prelude::{ListenerId, PortUse}; @@ -22,6 +15,13 @@ use crate::prelude::swarm::{ use crate::prelude::transport::Endpoint; use crate::prelude::{PeerId, Protocol}; use either::Either; +use std::collections::BTreeMap; +use std::{ + collections::{HashMap, HashSet, VecDeque}, + num::NonZeroU8, + task::{Context, Poll, Waker}, + time::Duration, +}; use web_time::{Instant, SystemTime}; mod handler; @@ -220,10 +220,10 @@ impl Behaviour { /// 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) { + pub fn add_static_relay(&mut self, peer_id: PeerId, address: Multiaddr) -> bool { if address.is_relayed() { tracing::warn!(%peer_id, %address, "static relay address is relayed. ignoring."); - return; + return false; } let entry = self.static_relays.entry(peer_id).or_default(); @@ -245,6 +245,8 @@ impl Behaviour { if let Some(waker) = self.waker.take() { waker.wake(); } + + true } /// Remove peer as a static relay. @@ -410,7 +412,7 @@ impl Behaviour { } /// Removes all existing reservations. - fn remove_all_reservations(&mut self) { + pub fn remove_all_reservations(&mut self) { let relay_listeners = self .reservations .iter() diff --git a/src/behaviour/request_response/codec.rs b/src/behaviour/request_response/codec.rs index ec04907..fc775de 100644 --- a/src/behaviour/request_response/codec.rs +++ b/src/behaviour/request_response/codec.rs @@ -1,3 +1,4 @@ +use async_trait::async_trait; use bytes::Bytes; use futures::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; use libp2p::StreamProtocol; @@ -17,6 +18,7 @@ impl Codec { } } +#[async_trait] impl libp2p::request_response::Codec for Codec { type Protocol = StreamProtocol; type Request = Bytes; diff --git a/src/handle.rs b/src/handle.rs index 0cb47c1..9d68d3d 100644 --- a/src/handle.rs +++ b/src/handle.rs @@ -10,9 +10,6 @@ pub(crate) mod gossipsub; mod peer_store; #[cfg(feature = "relay")] mod relay; -#[cfg(not(target_arch = "wasm32"))] -#[cfg(feature = "relay")] -mod relay_server; #[cfg(feature = "rendezvous")] pub(crate) mod rendezvous; #[cfg(feature = "request-response")] @@ -35,8 +32,6 @@ use crate::handle::gossipsub::ConnexaGossipsub; use crate::handle::peer_store::ConnexaPeerstore; #[cfg(feature = "relay")] use crate::handle::relay::ConnexaRelay; -#[cfg(feature = "relay")] -use crate::handle::relay_server::ConnexaRelayServer; #[cfg(feature = "rendezvous")] use crate::handle::rendezvous::ConnexaRendezvous; #[cfg(feature = "request-response")] @@ -152,12 +147,6 @@ where ConnexaRelay::new(self) } - /// Returns a handle for relay server functions - #[cfg(feature = "relay")] - pub fn relay_server(&self) -> ConnexaRelayServer<'_, T> { - ConnexaRelayServer::new(self) - } - /// Returns a handle to manage peer whitelist functionality pub fn whitelist(&self) -> ConnexaWhitelist<'_, T, K> { ConnexaWhitelist::new(self) diff --git a/src/handle/relay_server.rs b/src/handle/relay_server.rs deleted file mode 100644 index 24a9b99..0000000 --- a/src/handle/relay_server.rs +++ /dev/null @@ -1,31 +0,0 @@ -use crate::handle::Connexa; -use crate::types::RelayServerCommand; -use libp2p::relay::Status as RelayServerStatus; - -#[derive(Copy, Clone)] -pub struct ConnexaRelayServer<'a, T = ()> { - connexa: &'a Connexa, -} - -impl<'a, T> ConnexaRelayServer<'a, T> -where - T: Send + Sync + 'static, -{ - pub(crate) fn new(connexa: &'a Connexa) -> Self { - Self { connexa } - } - - pub async fn change_status( - &self, - status: impl Into>, - ) -> std::io::Result<()> { - let (tx, rx) = futures::channel::oneshot::channel(); - let status = status.into(); - self.connexa - .to_task - .clone() - .send(RelayServerCommand::StatusChanged { status, resp: tx }.into()) - .await?; - rx.await.map_err(std::io::Error::other)? - } -} diff --git a/src/task.rs b/src/task.rs index 76ebda0..0e6e316 100644 --- a/src/task.rs +++ b/src/task.rs @@ -587,10 +587,6 @@ where Command::Rendezvous(rendezvous_command) => { self.process_rendezvous_command(rendezvous_command) } - #[cfg(feature = "relay")] - Command::RelayServer(relay_server_command) => { - self.process_relay_server_command(relay_server_command) - } Command::Custom(custom_command) => { (self.custom_task_callback)( swarm, diff --git a/src/task/relay.rs b/src/task/relay.rs index a08aeca..fea0ae2 100644 --- a/src/task/relay.rs +++ b/src/task/relay.rs @@ -2,7 +2,6 @@ use crate::behaviour::autorelay; use crate::behaviour::peer_store::store::Store; use crate::task::ConnexaTask; use crate::types::AutoRelayCommand; -use crate::types::RelayServerCommand; use libp2p::relay::{Event as RelayServerEvent, client::Event as RelayClientEvent}; use libp2p::swarm::NetworkBehaviour; use std::fmt::Debug; @@ -158,36 +157,7 @@ where } => { tracing::warn!(%src_peer_id, %dst_peer_id, ?error, "relay server circuit closed"); } - RelayServerEvent::StatusChanged { status } => { - tracing::info!(?status, "relay server status changed"); - } _ => {} } } } - -impl ConnexaTask -where - X: Default + Send + 'static, - C: Send, - C::ToSwarm: Debug, - S: Store, -{ - pub fn process_relay_server_command(&mut self, command: RelayServerCommand) { - let swarm = self.swarm.as_mut().expect("swarm is active"); - match command { - RelayServerCommand::StatusChanged { status, resp } => { - let Some(relay) = swarm.behaviour_mut().relay.as_mut() else { - let _ = resp.send(Err(std::io::Error::other( - "relay server protocol is not enabled", - ))); - return; - }; - - relay.set_status(status); - - let _ = resp.send(Ok(())); - } - } - } -} diff --git a/src/task/upnp.rs b/src/task/upnp.rs index 5ed3835..838c38f 100644 --- a/src/task/upnp.rs +++ b/src/task/upnp.rs @@ -13,21 +13,11 @@ where { pub fn process_upnp_event(&mut self, event: UpnpEvent) { match event { - UpnpEvent::NewExternalAddr { - external_addr, - local_addr, - } => { - tracing::info!( - ?external_addr, - ?local_addr, - "upnp external address discovered" - ); + UpnpEvent::NewExternalAddr(addr) => { + tracing::info!(?addr, "upnp external address discovered"); } - UpnpEvent::ExpiredExternalAddr { - external_addr, - local_addr, - } => { - tracing::info!(?external_addr, ?local_addr, "upnp external address expired"); + UpnpEvent::ExpiredExternalAddr(addr) => { + tracing::info!(?addr, "upnp external address expired"); } UpnpEvent::GatewayNotFound => { tracing::warn!("upnp gateway not found"); diff --git a/src/types.rs b/src/types.rs index 47de30f..edfea3a 100644 --- a/src/types.rs +++ b/src/types.rs @@ -48,9 +48,6 @@ pub enum Command { Autonat(AutonatCommand), #[cfg(feature = "relay")] AutoRelay(AutoRelayCommand), - #[cfg(not(target_arch = "wasm32"))] - #[cfg(feature = "relay")] - RelayServer(RelayServerCommand), Whitelist(WhitelistCommand), Blacklist(BlacklistCommand), ConnectionLimits(ConnectionLimitsCommand), @@ -120,14 +117,6 @@ impl From for Command { } } -#[cfg(not(target_arch = "wasm32"))] -#[cfg(feature = "relay")] -impl From for Command { - fn from(cmd: RelayServerCommand) -> Self { - Command::RelayServer(cmd) - } -} - impl From for Command { fn from(cmd: WhitelistCommand) -> Self { Command::Whitelist(cmd) @@ -555,15 +544,6 @@ pub enum RendezvousCommand { }, } -#[cfg(feature = "relay")] -#[derive(Debug)] -pub enum RelayServerCommand { - StatusChanged { - status: Option, - resp: oneshot::Sender>, - }, -} - #[cfg(feature = "kad")] #[derive(Clone, Debug)] pub enum DHTEvent { From be28b255e134956aabfb3c68f0a51ac86a387e1f Mon Sep 17 00:00:00 2001 From: Darius Clark Date: Sun, 14 Jun 2026 19:52:13 -0400 Subject: [PATCH 75/77] chore: track reservations and remove addresses apart of relay listener --- src/behaviour/autorelay.rs | 68 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/src/behaviour/autorelay.rs b/src/behaviour/autorelay.rs index d974a63..c3fe7fe 100644 --- a/src/behaviour/autorelay.rs +++ b/src/behaviour/autorelay.rs @@ -40,6 +40,10 @@ pub struct Behaviour { external_reservations: HashMap, + // placeholder for reservations. + // TODO: Removed once https://github.com/libp2p/rust-libp2p/pull/3222 is published or backported. + reservation_addrs: HashMap>, + static_relays: HashMap>, static_dial_cooldowns: HashMap, @@ -64,6 +68,7 @@ impl Default for Behaviour { connections: HashMap::new(), reservations: HashMap::new(), external_reservations: HashMap::new(), + reservation_addrs: HashMap::new(), static_relays: HashMap::new(), static_dial_cooldowns: HashMap::new(), failure_counts: HashMap::new(), @@ -443,6 +448,8 @@ impl Behaviour { } fn disable_reservation(&mut self, id: ListenerId, failed: bool) { + self.expire_reservation_addrs(id); + if self.external_reservations.remove(&id).is_some() { self.meet_reservation_target(); return; @@ -498,6 +505,62 @@ impl Behaviour { self.meet_reservation_target(); } + fn reconcile_reservation_addrs(&mut self) { + let confirmed: HashSet = self + .external_addresses + .iter() + .filter(|addr| addr.is_relayed()) + .cloned() + .collect(); + + for addrs in self.reservation_addrs.values_mut() { + addrs.retain(|addr| confirmed.contains(addr)); + } + self.reservation_addrs.retain(|_, addrs| !addrs.is_empty()); + + for addr in confirmed { + let Some(relay_peer) = addr.relay_peer_id() else { + continue; + }; + + let listeners = self + .reservations + .iter() + .filter(|(_, (peer_id, _))| *peer_id == relay_peer) + .map(|(id, _)| *id) + .chain( + self.external_reservations + .iter() + .filter(|(_, peer_id)| **peer_id == relay_peer) + .map(|(id, _)| *id), + ) + .collect::>(); + + for id in listeners { + self.reservation_addrs + .entry(id) + .or_default() + .insert(addr.clone()); + } + } + } + + fn expire_reservation_addrs(&mut self, id: ListenerId) { + let Some(addrs) = self.reservation_addrs.remove(&id) else { + return; + }; + + for addr in addrs { + let still_backed = self + .reservation_addrs + .values() + .any(|other| other.contains(&addr)); + if !still_backed { + self.events.push_back(ToSwarm::ExternalAddrExpired(addr)); + } + } + } + fn covered_peers(&self) -> HashSet { self.reservations .values() @@ -618,6 +681,10 @@ impl NetworkBehaviour for Behaviour { self.determine_status_from_external_addresses(); } + if change { + self.reconcile_reservation_addrs(); + } + match event { FromSwarm::ConnectionEstablished(ConnectionEstablished { peer_id, @@ -791,6 +858,7 @@ impl NetworkBehaviour for Behaviour { let lost_address = drop_listener.map(|_| connection.address.clone()); connection.relay_status = RelayStatus::NotSupported; if let Some(id) = drop_listener { + self.expire_reservation_addrs(id); self.reservations.remove(&id); self.events.push_back(ToSwarm::RemoveListener { id }); self.meet_reservation_target(); From 0d1f8605c28db722ce7ad95b4bf8d907ef780e1b Mon Sep 17 00:00:00 2001 From: Darius Clark Date: Sun, 14 Jun 2026 20:05:33 -0400 Subject: [PATCH 76/77] chore: port test over --- src/behaviour/autorelay.rs | 1103 ++++++++++++++++++++++++++++++++++++ 1 file changed, 1103 insertions(+) diff --git a/src/behaviour/autorelay.rs b/src/behaviour/autorelay.rs index c3fe7fe..8b5ed7d 100644 --- a/src/behaviour/autorelay.rs +++ b/src/behaviour/autorelay.rs @@ -897,3 +897,1106 @@ impl NetworkBehaviour for Behaviour { Poll::Pending } } + +#[cfg(test)] +mod tests { + use crate::behaviour::autorelay; + use futures::StreamExt; + use futures::{AsyncRead, AsyncWrite}; + use libp2p::core::muxing::StreamMuxerBox; + use libp2p::core::transport::{Boxed, MemoryTransport, OrTransport}; + use libp2p::core::upgrade; + use libp2p::multiaddr::Protocol; + use libp2p::swarm::{Config, ConnectionId, NetworkBehaviour, SwarmEvent}; + use libp2p::{Multiaddr, PeerId, Swarm, Transport, identify, identity, noise, relay, yamux}; + use std::collections::{HashMap, HashSet}; + use std::num::NonZeroU8; + use std::time::Duration; + + #[tokio::test] + async fn autorelay_respects_max_reservations() { + init_tracing(); + + 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).unwrap(); + client.dial(relay_b_addr).unwrap(); + + let mut accepted = 0usize; + let mut timeout = futures_timer::Delay::new(Duration::from_secs(20)); + 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"); + } + futures_timer::Delay::new(Duration::from_secs(2)).await; + break; + } + } + } + } + + assert_eq!( + accepted, 1, + "expected exactly one reservation, observed {accepted}" + ); + } + + #[tokio::test] + async fn autorelay_with_two_reservations_among_five_relays() { + 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 = build_client( + autorelay::Config::default().set_max_reservations(NonZeroU8::new(2).unwrap()), + ); + for (_, addr) in &relay_addrs { + client.dial(addr.clone()).unwrap(); + } + + let mut direct_conns: HashMap = HashMap::new(); + let mut reservations: HashSet = HashSet::new(); + + let mut sleep = futures_timer::Delay::new(Duration::from_secs(30)); + 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 mut sleep = futures_timer::Delay::new(Duration::from_secs(30)); + + 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() { + init_tracing(); + + 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).unwrap(); + client.dial(relay_b_addr).unwrap(); + + let mut confirmed: HashSet = HashSet::new(); + let mut sleep = futures_timer::Delay::new(Duration::from_secs(30)); + + 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 mut sleep = futures_timer::Delay::new(Duration::from_secs(15)); + + 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; + } + } + } + + #[tokio::test] + async fn autorelay_blacklists_failing_relay_and_retries_after_cooldown() { + init_tracing(); + + let (_, relay_addr) = spawn_rejecting_relay(); + + 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 = with_timeout( + wait_for_listener_failure(&mut client, cooldown * 5), + cooldown / 2, + ) + .await; + assert!( + early_retry.is_none(), + "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 mut sleep = futures_timer::Delay::new(timeout); + + 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(); + } + } + } + } + } + + #[tokio::test] + async fn autorelay_disabled_does_not_reserve() { + init_tracing(); + + let (_, relay_addr) = spawn_relay(); + + let mut client = build_client(autorelay::Config::default()); + client + .behaviour_mut() + .autorelay + .set_status(Some(autorelay::Status::Disable)); + client.dial(relay_addr).unwrap(); + + let observed = with_timeout( + wait_until(&mut client, Duration::from_secs(5), |event| { + matches!( + event, + SwarmEvent::Behaviour(ClientEvent::RelayClient( + relay::client::Event::ReservationReqAccepted { .. } + )) + ) + }), + Duration::from_secs(3), + ) + .await; + + assert!( + observed.is_none(), + "autorelay opened a reservation while disabled" + ); + } + + #[tokio::test] + async fn autorelay_re_enable_triggers_reservation() { + init_tracing(); + + let (_, relay_addr) = spawn_relay(); + + let mut client = build_client(autorelay::Config::default()); + client + .behaviour_mut() + .autorelay + .set_status(Some(autorelay::Status::Disable)); + client.dial(relay_addr).unwrap(); + + let mut sleep = futures_timer::Delay::new(Duration::from_secs(3)); + + 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(Some(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() { + init_tracing(); + + let (_, relay_addr) = spawn_relay(); + + 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(Some(autorelay::Status::Disable)); + + let mut sleep = futures_timer::Delay::new(Duration::from_secs(3)); + + 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"); + } + } + } + } + } + + #[tokio::test] + async fn autorelay_prefers_static_relay() { + init_tracing(); + + 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()), + ); + client + .behaviour_mut() + .autorelay + .set_status(Some(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 mut warmup = futures_timer::Delay::new(Duration::from_secs(3)); + loop { + tokio::select! { + _ = &mut warmup => break, + _ = client.select_next_some() => {} + } + } + + client + .behaviour_mut() + .autorelay + .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( + 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 remove_static_relay_preserves_active_reservation() { + init_tracing(); + + let (relay_peer, relay_addr) = spawn_relay(); + + 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 mut sleep = futures_timer::Delay::new(Duration::from_secs(3)); + + 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() { + init_tracing(); + + let (relay_peer, relay_addr) = spawn_relay(); + + let mut client = build_client(autorelay::Config::default()); + client + .behaviour_mut() + .autorelay + .add_static_relay(relay_peer, relay_addr); + + let conn_id = + wait_for_reservation_with_conn(&mut client, relay_peer, Duration::from_secs(15)).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 mut sleep = futures_timer::Delay::new(timeout); + + loop { + tokio::select! { + _ = &mut sleep => panic!("timeout waiting on predicate"), + ev = client.select_next_some() => { + if let Some(value) = extract(&ev) { + return value; + } + } + } + } + } + + #[tokio::test] + async fn autorelay_emits_relay_available_after_recovery() { + init_tracing(); + + 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_for_reservation_with_conn(&mut client, relay_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)) + ) + }) + .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() { + init_tracing(); + + 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(); + client.dial(relay_b_addr).unwrap(); + + let mut conns: HashMap = HashMap::new(); + let mut reserved: HashSet = HashSet::new(); + let mut sleep = futures_timer::Delay::new(Duration::from_secs(20)); + + 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 mut sleep = futures_timer::Delay::new(Duration::from_secs(5)); + + 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" + ); + } + + #[tokio::test] + async fn autorelay_resumes_after_public_address_removed() { + init_tracing(); + + let (relay_peer, relay_addr) = spawn_relay(); + + let mut client = build_client(autorelay::Config::default()); + client.dial(relay_addr).unwrap(); + + wait_for_reservation_from(&mut client, relay_peer, Duration::from_secs(15)).await; + + let public_addr = memory_addr(); + 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_for_reservation_from(&mut client, relay_peer, Duration::from_secs(15)).await; + } + + #[tokio::test] + async fn autorelay_manual_enable_ignores_public_address() { + init_tracing(); + + let (relay_peer, relay_addr) = spawn_relay(); + + let mut client = build_client(autorelay::Config::default()); + client + .behaviour_mut() + .autorelay + .set_status(Some(autorelay::Status::Enable)); + client.dial(relay_addr).unwrap(); + + wait_for_reservation_from(&mut client, relay_peer, Duration::from_secs(15)).await; + + client.add_external_address(memory_addr()); + + let mut sleep = futures_timer::Delay::new(Duration::from_secs(3)); + + 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_forgets_previous_relay_on_reacquire() { + init_tracing(); + + 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_for_reservation_with_conn(&mut client, relay_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)) + ) + }) + .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() { + init_tracing(); + + let peers_and_addrs: Vec<(PeerId, Multiaddr)> = (0..3).map(|_| spawn_relay()).collect(); + + let mut client = build_client( + autorelay::Config::default() + .set_max_reservations(NonZeroU8::new(1).unwrap()) + .set_max_previous_relays(2), + ); + + for (peer, addr) in &peers_and_addrs { + client.dial(addr.clone()).unwrap(); + + 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 + )) + ) + }) + .await; + } + + 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)); + } + + #[tokio::test] + async fn autorelay_static_relay_dial_cooldown_after_failure() { + init_tracing(); + + 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 = memory_addr(); + + 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 mut watch = futures_timer::Delay::new(cooldown / 2); + 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() { + futures_timer::Delay::new(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; + } + + #[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 mut sleep = futures_timer::Delay::new(timeout); + loop { + tokio::select! { + _ = &mut sleep => panic!("timeout waiting on predicate"), + ev = client.select_next_some() => { + if predicate(&ev) { + return; + } + } + } + } + } + + 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), + ..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); + + Swarm::new( + transport, + Relay { + 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(), + )), + }, + 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(noise::Config::new(identity).unwrap()) + .multiplex(yamux::Config::default()) + .boxed() + } + + #[derive(NetworkBehaviour)] + struct Relay { + relay: relay::Behaviour, + identify: identify::Behaviour, + } + + #[derive(NetworkBehaviour)] + struct Client { + relay_client: relay::client::Behaviour, + autorelay: autorelay::Behaviour, + identify: identify::Behaviour, + } +} From de5983abf2130e55397f356aeb97769f5881d2a6 Mon Sep 17 00:00:00 2001 From: Darius Clark Date: Sun, 14 Jun 2026 21:50:18 -0400 Subject: [PATCH 77/77] chore: update and add additional test --- src/behaviour/autorelay.rs | 147 +++++++++++++++++++++++++++++++++---- 1 file changed, 131 insertions(+), 16 deletions(-) diff --git a/src/behaviour/autorelay.rs b/src/behaviour/autorelay.rs index 8b5ed7d..87f42c8 100644 --- a/src/behaviour/autorelay.rs +++ b/src/behaviour/autorelay.rs @@ -792,6 +792,7 @@ impl NetworkBehaviour for Behaviour { if let Some(relay_peer_id) = addr.relay_peer_id() { self.external_reservations .insert(listener_id, relay_peer_id); + self.reconcile_reservation_addrs(); } } FromSwarm::ExpiredListenAddr(ExpiredListenAddr { listener_id, .. }) => { @@ -926,31 +927,44 @@ mod tests { client.dial(relay_a_addr).unwrap(); client.dial(relay_b_addr).unwrap(); - let mut accepted = 0usize; - let mut timeout = futures_timer::Delay::new(Duration::from_secs(20)); + let first = wait_until_some(&mut client, Duration::from_secs(20), |event| { + if let SwarmEvent::Behaviour(ClientEvent::RelayClient( + relay::client::Event::ReservationReqAccepted { + relay_peer_id, + renewal: false, + .. + }, + )) = event + { + Some(*relay_peer_id) + } else { + None + } + }) + .await; + assert!(first == relay_a_peer_id || first == relay_b_peer_id); + + let mut extra = 0usize; + let mut settle = futures_timer::Delay::new(Duration::from_secs(5)); loop { tokio::select! { - _ = &mut timeout => break, + _ = &mut settle => 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"); - } - futures_timer::Delay::new(Duration::from_secs(2)).await; - break; + if matches!( + ev, + SwarmEvent::Behaviour(ClientEvent::RelayClient( + relay::client::Event::ReservationReqAccepted { renewal: false, .. } + )) + ) { + extra += 1; } } } } assert_eq!( - accepted, 1, - "expected exactly one reservation, observed {accepted}" + extra, 0, + "autorelay opened {extra} reservation(s) beyond max_reservations=1" ); } @@ -1101,6 +1115,107 @@ mod tests { } } + #[tokio::test] + async fn autorelay_expires_circuit_address_once_on_connection_close() { + init_tracing(); + + let (relay_peer, relay_addr) = spawn_relay(); + + let mut client = build_client(autorelay::Config::default()); + client.dial(relay_addr).unwrap(); + + let (conn, circuit) = wait_until_some(&mut client, Duration::from_secs(15), { + let mut direct: Option = None; + move |event| match event { + SwarmEvent::ConnectionEstablished { + peer_id, + connection_id, + endpoint, + .. + } if *peer_id == relay_peer && !endpoint.is_relayed() => { + direct = Some(*connection_id); + None + } + SwarmEvent::ExternalAddrConfirmed { address } + if address.iter().any(|p| p == Protocol::P2pCircuit) => + { + direct.map(|c| (c, address.clone())) + } + _ => None, + } + }) + .await; + + assert!(client.close_connection(conn)); + + let mut expired = 0usize; + let mut settle = futures_timer::Delay::new(Duration::from_secs(5)); + loop { + tokio::select! { + _ = &mut settle => break, + ev = client.select_next_some() => { + if let SwarmEvent::ExternalAddrExpired { address } = &ev + && *address == circuit + { + expired += 1; + } + } + } + } + + assert_eq!( + expired, 1, + "expected exactly one expiry of the circuit address, got {expired}" + ); + } + + #[tokio::test] + async fn autorelay_expires_user_circuit_listen_on_removal() { + init_tracing(); + + let (relay_peer, relay_addr) = spawn_relay(); + + let mut client = build_client(autorelay::Config::default()); + client + .behaviour_mut() + .autorelay + .set_status(Some(autorelay::Status::Disable)); + + let circuit_listen = relay_addr + .with(Protocol::P2p(relay_peer)) + .with(Protocol::P2pCircuit); + let listener_id = client.listen_on(circuit_listen).unwrap(); + + let circuit = wait_until_some(&mut client, Duration::from_secs(15), |event| { + if let SwarmEvent::ExternalAddrConfirmed { address } = event + && address.iter().any(|p| p == Protocol::P2pCircuit) + { + Some(address.clone()) + } else { + None + } + }) + .await; + + wait_until(&mut client, Duration::from_secs(10), |event| { + matches!( + event, + SwarmEvent::NewListenAddr { address, .. } if *address == circuit + ) + }) + .await; + + assert!(client.remove_listener(listener_id)); + + wait_until(&mut client, Duration::from_secs(10), |event| { + matches!( + event, + SwarmEvent::ExternalAddrExpired { address } if *address == circuit + ) + }) + .await; + } + #[tokio::test] async fn autorelay_blacklists_failing_relay_and_retries_after_cooldown() { init_tracing();