From 4e8f16d7dd72689b9e25b39b4519297cc332bce1 Mon Sep 17 00:00:00 2001 From: Darius Clark Date: Fri, 6 Mar 2026 12:32:57 -0600 Subject: [PATCH 1/5] fix: remove external address when is it closed on relay --- protocols/relay/src/priv_client.rs | 51 +++++++++++++- protocols/relay/src/priv_client/transport.rs | 2 + protocols/relay/tests/lib.rs | 74 ++++++++++++++++++++ 3 files changed, 125 insertions(+), 2 deletions(-) diff --git a/protocols/relay/src/priv_client.rs b/protocols/relay/src/priv_client.rs index 58e28b14913..a251b3b67c0 100644 --- a/protocols/relay/src/priv_client.rs +++ b/protocols/relay/src/priv_client.rs @@ -40,10 +40,14 @@ use futures::{ ready, stream::StreamExt, }; -use libp2p_core::{multiaddr::Protocol, transport::PortUse, Endpoint, Multiaddr}; +use libp2p_core::{ + multiaddr::Protocol, + transport::{ListenerId, PortUse}, + Endpoint, Multiaddr, +}; use libp2p_identity::PeerId; use libp2p_swarm::{ - behaviour::{ConnectionClosed, ConnectionEstablished, FromSwarm}, + behaviour::{ConnectionClosed, ConnectionEstablished, FromSwarm, ListenerClosed}, dial_opts::DialOpts, dummy, ConnectionDenied, ConnectionHandler, ConnectionId, DialFailure, NetworkBehaviour, NotifyHandler, Stream, THandler, THandlerInEvent, THandlerOutEvent, ToSwarm, @@ -99,6 +103,12 @@ pub struct Behaviour { /// `/p2p-circuit` address we reserved on it. reservation_addresses: HashMap, + /// Stores the [`ListenerId`] to the [`ConnectionId`] of the relay connection + /// that the listener's reservation is on. This allows us to expire external addresses + /// when a listener closes, but only if no other listener still holds a + /// reservation for the same address. + listener_id_to_connection_id: HashMap, + /// Queue of actions to return when polled. queued_actions: VecDeque>>, @@ -113,6 +123,7 @@ pub fn new(local_peer_id: PeerId) -> (Transport, Behaviour) { from_transport, directly_connected_peers: Default::default(), reservation_addresses: Default::default(), + listener_id_to_connection_id: Default::default(), queued_actions: Default::default(), pending_handler_commands: Default::default(), }; @@ -147,6 +158,8 @@ impl Behaviour { unreachable!("`on_connection_closed` for unconnected peer.") } }; + self.listener_id_to_connection_id + .retain(|_, cid| *cid != connection_id); if let Some((addr, ReservationStatus::Confirmed)) = self.reservation_addresses.remove(&connection_id) { @@ -155,6 +168,32 @@ impl Behaviour { } } } + + fn on_listener_closed(&mut self, ListenerClosed { listener_id, .. }: ListenerClosed) { + let Some(connection_id) = self.listener_id_to_connection_id.remove(&listener_id) else { + return; + }; + + // Only expire the external address if no other listener still holds + // a reservation on the same connection. During reservation replacement the new listener is + // registered before the old one closes, so there will still be another entry, so we should + // not emit the event to expire the address address. + let listenr_and_connection = self + .listener_id_to_connection_id + .values() + .any(|cid| *cid == connection_id); + + if listenr_and_connection { + return; + } + + if let Some((addr, ReservationStatus::Confirmed)) = + self.reservation_addresses.remove(&connection_id) + { + self.queued_actions + .push_back(ToSwarm::ExternalAddrExpired(addr)); + } + } } impl NetworkBehaviour for Behaviour { @@ -221,6 +260,9 @@ impl NetworkBehaviour for Behaviour { FromSwarm::ConnectionClosed(connection_closed) => { self.on_connection_closed(connection_closed) } + FromSwarm::ListenerClosed(listener_closed) => { + self.on_listener_closed(listener_closed) + } FromSwarm::DialFailure(DialFailure { connection_id, .. }) => { self.reservation_addresses.remove(&connection_id); self.pending_handler_commands.remove(&connection_id); @@ -284,6 +326,7 @@ impl NetworkBehaviour for Behaviour { let action = match ready!(self.from_transport.poll_next_unpin(cx)) { Some(transport::TransportToBehaviourMsg::ListenReq { + listener_id, relay_peer_id, relay_addr, to_listener, @@ -294,6 +337,8 @@ impl NetworkBehaviour for Behaviour { .and_then(|cs| cs.first()) { Some(connection_id) => { + self.listener_id_to_connection_id + .insert(listener_id, *connection_id); self.reservation_addresses.insert( *connection_id, ( @@ -318,6 +363,8 @@ impl NetworkBehaviour for Behaviour { .build(); let relayed_connection_id = opts.connection_id(); + self.listener_id_to_connection_id + .insert(listener_id, relayed_connection_id); self.reservation_addresses.insert( relayed_connection_id, ( diff --git a/protocols/relay/src/priv_client/transport.rs b/protocols/relay/src/priv_client/transport.rs index c5c17fd5137..b4500924f26 100644 --- a/protocols/relay/src/priv_client/transport.rs +++ b/protocols/relay/src/priv_client/transport.rs @@ -154,6 +154,7 @@ impl libp2p_core::Transport for Transport { let (to_listener, from_behaviour) = mpsc::channel(0); self.pending_to_behaviour .push_back(TransportToBehaviourMsg::ListenReq { + listener_id, relay_peer_id, relay_addr, to_listener, @@ -459,6 +460,7 @@ pub(crate) enum TransportToBehaviourMsg { }, /// Listen for incoming relayed connections via relay node. ListenReq { + listener_id: ListenerId, relay_peer_id: PeerId, relay_addr: Multiaddr, to_listener: mpsc::Sender, diff --git a/protocols/relay/tests/lib.rs b/protocols/relay/tests/lib.rs index 7258bbd7e48..58126a60bbe 100644 --- a/protocols/relay/tests/lib.rs +++ b/protocols/relay/tests/lib.rs @@ -184,6 +184,80 @@ async fn new_reservation_to_same_relay_replaces_old() { } } +#[tokio::test] +async fn closing_relay_listener_expires_external_address() { + let _ = tracing_subscriber::fmt() + .with_env_filter(EnvFilter::from_default_env()) + .try_init(); + + let relay_addr = Multiaddr::empty().with(Protocol::Memory(rand::random::())); + let mut relay = build_relay(); + let relay_peer_id = *relay.local_peer_id(); + + relay.listen_on(relay_addr.clone()).unwrap(); + relay.add_external_address(relay_addr.clone()); + tokio::spawn(async move { + relay.collect::>().await; + }); + + let mut client = build_client(); + let client_peer_id = *client.local_peer_id(); + let client_addr = relay_addr + .with(Protocol::P2p(relay_peer_id)) + .with(Protocol::P2pCircuit); + let client_addr_with_peer_id = client_addr.clone().with(Protocol::P2p(client_peer_id)); + + let listener_id = client.listen_on(client_addr.clone()).unwrap(); + + // Wait for connection to relay. + assert!(wait_for_dial(&mut client, relay_peer_id).await); + + // Wait for reservation to be accepted. + wait_for_reservation( + &mut client, + client_addr_with_peer_id.clone(), + relay_peer_id, + false, // No renewal. + ) + .await; + + // Now remove the relay listener. + assert!(client.remove_listener(listener_id)); + + // Expect the listener to close and the external address to expire. + let mut listener_closed = false; + let mut external_addr_expired = false; + loop { + match client.select_next_some().await { + SwarmEvent::ListenerClosed { + listener_id: closed_id, + addresses, + .. + } => { + assert_eq!(closed_id, listener_id); + assert_eq!(addresses, vec![client_addr_with_peer_id.clone()]); + listener_closed = true; + if external_addr_expired { + break; + } + } + SwarmEvent::ExternalAddrExpired { address } => { + assert_eq!(address, client_addr_with_peer_id); + external_addr_expired = true; + if listener_closed { + break; + } + } + SwarmEvent::Behaviour(ClientEvent::Ping(_)) => {} + SwarmEvent::ExpiredListenAddr { .. } => {} + e => panic!("{e:?}"), + } + } + + assert!(listener_closed); + assert!(external_addr_expired); +} + #[tokio::test] async fn connect() { let _ = tracing_subscriber::fmt() From 0ea37ea50463955a9a0bb49f44d7104f5ec80358 Mon Sep 17 00:00:00 2001 From: Darius Clark Date: Fri, 6 Mar 2026 14:27:44 -0600 Subject: [PATCH 2/5] chore: update changelog --- Cargo.lock | 2 +- Cargo.toml | 2 +- protocols/relay/CHANGELOG.md | 5 +++++ protocols/relay/Cargo.toml | 2 +- 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 599c9b367cb..9db9c86c22a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2978,7 +2978,7 @@ dependencies = [ [[package]] name = "libp2p-relay" -version = "0.21.1" +version = "0.21.2" dependencies = [ "asynchronous-codec", "bytes", diff --git a/Cargo.toml b/Cargo.toml index 104025d12f1..af05f9219a0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -97,7 +97,7 @@ libp2p-ping = { version = "0.47.0", path = "protocols/ping" } libp2p-plaintext = { version = "0.43.0", path = "transports/plaintext" } libp2p-pnet = { version = "0.26.0", path = "transports/pnet" } libp2p-quic = { version = "0.13.0", path = "transports/quic" } -libp2p-relay = { version = "0.21.1", path = "protocols/relay" } +libp2p-relay = { version = "0.21.2", path = "protocols/relay" } libp2p-rendezvous = { version = "0.17.0", path = "protocols/rendezvous" } libp2p-request-response = { version = "0.29.0", path = "protocols/request-response" } libp2p-server = { version = "0.12.7", path = "misc/server" } diff --git a/protocols/relay/CHANGELOG.md b/protocols/relay/CHANGELOG.md index fde8a2a6807..ae0d1167674 100644 --- a/protocols/relay/CHANGELOG.md +++ b/protocols/relay/CHANGELOG.md @@ -1,3 +1,8 @@ +## 0.21.2 + +- Expire external address when a relay listener is closed without a replacement reservation. + See [PR 6285](https://github.com/libp2p/rust-libp2p/pull/6285). + ## 0.21.1 - reduce allocations by replacing `get_or_insert` with `get_or_insert_with` diff --git a/protocols/relay/Cargo.toml b/protocols/relay/Cargo.toml index 3871abbcf8a..7cbcf2d04ae 100644 --- a/protocols/relay/Cargo.toml +++ b/protocols/relay/Cargo.toml @@ -3,7 +3,7 @@ name = "libp2p-relay" edition.workspace = true rust-version = { workspace = true } description = "Communications relaying for libp2p" -version = "0.21.1" +version = "0.21.2" authors = ["Parity Technologies ", "Max Inden "] license = "MIT" repository = "https://github.com/libp2p/rust-libp2p" From 70266ec71a338eac57ed0353ed4dfad11da4006e Mon Sep 17 00:00:00 2001 From: Darius Clark Date: Fri, 6 Mar 2026 14:29:14 -0600 Subject: [PATCH 3/5] chore: fmt --- protocols/relay/src/priv_client.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/protocols/relay/src/priv_client.rs b/protocols/relay/src/priv_client.rs index a251b3b67c0..33d81df5a64 100644 --- a/protocols/relay/src/priv_client.rs +++ b/protocols/relay/src/priv_client.rs @@ -260,9 +260,7 @@ impl NetworkBehaviour for Behaviour { FromSwarm::ConnectionClosed(connection_closed) => { self.on_connection_closed(connection_closed) } - FromSwarm::ListenerClosed(listener_closed) => { - self.on_listener_closed(listener_closed) - } + FromSwarm::ListenerClosed(listener_closed) => self.on_listener_closed(listener_closed), FromSwarm::DialFailure(DialFailure { connection_id, .. }) => { self.reservation_addresses.remove(&connection_id); self.pending_handler_commands.remove(&connection_id); From 26b6b4b7b875ba54c25a4cdb08cc84f2fbe04c59 Mon Sep 17 00:00:00 2001 From: Darius Clark Date: Tue, 21 Apr 2026 06:52:03 -0500 Subject: [PATCH 4/5] fix: correct Cargo.lock --- Cargo.lock | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b95dae6231f..2ef0595e38e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3007,11 +3007,7 @@ dependencies = [ [[package]] name = "libp2p-relay" -<<<<<<< HEAD -version = "0.21.2" -======= version = "0.22.0" ->>>>>>> master dependencies = [ "asynchronous-codec", "bytes", From 03cbfa69f88260d354ed7e897d8483b54e4fa40f Mon Sep 17 00:00:00 2001 From: Darius Clark Date: Tue, 28 Apr 2026 09:37:15 -0500 Subject: [PATCH 5/5] chore: break loop when all conditions are met --- protocols/relay/tests/lib.rs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/protocols/relay/tests/lib.rs b/protocols/relay/tests/lib.rs index efbfc777b95..35e276727e1 100644 --- a/protocols/relay/tests/lib.rs +++ b/protocols/relay/tests/lib.rs @@ -237,21 +237,18 @@ async fn closing_relay_listener_expires_external_address() { assert_eq!(closed_id, listener_id); assert_eq!(addresses, vec![client_addr_with_peer_id.clone()]); listener_closed = true; - if external_addr_expired { - break; - } } SwarmEvent::ExternalAddrExpired { address } => { assert_eq!(address, client_addr_with_peer_id); external_addr_expired = true; - if listener_closed { - break; - } } SwarmEvent::Behaviour(ClientEvent::Ping(_)) => {} SwarmEvent::ExpiredListenAddr { .. } => {} e => panic!("{e:?}"), } + if listener_closed && external_addr_expired { + break; + } } assert!(listener_closed);