Skip to content

Commit fc6efaf

Browse files
fix(dcutr): exchange address candidates
In a serious of refactorings, we seem to have introduced a bug where we where exchanged the _external_ addresses of our node as part of `libp2p-dcutr`. This is ironically quite pointless. If we have external addresses, then there is no need for hole-punching (i.e. DCUtR). Instead of gathering external addresses, we use an LRU cache to store our observed addresses. Repeatedly observed addresses will be tried first which should increase the success rate of a hole-punch. Resolves: #4496. Pull-Request: #4624.
1 parent 5b1cc3b commit fc6efaf

File tree

9 files changed

+119
-48
lines changed

9 files changed

+119
-48
lines changed

Cargo.lock

Lines changed: 11 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

examples/dcutr/src/main.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -156,8 +156,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
156156
info: identify::Info { observed_addr, .. },
157157
..
158158
})) => {
159-
info!("Relay told us our public address: {:?}", observed_addr);
160-
swarm.add_external_address(observed_addr);
159+
info!("Relay told us our observed address: {observed_addr}");
161160
learned_observed_addr = true;
162161
}
163162
event => panic!("{event:?}"),

hole-punching-tests/src/main.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,6 @@ async fn client_connect_to_relay(
211211
..
212212
})) => {
213213
log::info!("Relay told us our public address: {observed_addr}");
214-
swarm.add_external_address(observed_addr);
215214
break;
216215
}
217216
SwarmEvent::ConnectionEstablished { connection_id, .. }

protocols/dcutr/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@
55

66
[PR 4558]: https://github.com/libp2p/rust-libp2p/pull/4558
77

8+
- Exchange address _candidates_ instead of external addresses in `CONNECT`.
9+
If hole-punching wasn't working properly for you until now, this might be the reason why.
10+
See [PR 4624](https://github.com/libp2p/rust-libp2p/pull/4624).
11+
812
## 0.10.0
913

1014
- Raise MSRV to 1.65.

protocols/dcutr/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ quick-protobuf = "0.8"
2424
quick-protobuf-codec = { workspace = true }
2525
thiserror = "1.0"
2626
void = "1"
27+
lru = "0.11.1"
2728

2829
[dev-dependencies]
2930
async-std = { version = "1.12.0", features = ["attributes"] }

protocols/dcutr/src/behaviour_impl.rs renamed to protocols/dcutr/src/behaviour.rs

Lines changed: 47 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,13 @@ use libp2p_identity::PeerId;
2929
use libp2p_swarm::behaviour::{ConnectionClosed, DialFailure, FromSwarm};
3030
use libp2p_swarm::dial_opts::{self, DialOpts};
3131
use libp2p_swarm::{
32-
dummy, ConnectionDenied, ConnectionHandler, ConnectionId, THandler, THandlerOutEvent,
33-
};
34-
use libp2p_swarm::{
35-
ExternalAddresses, NetworkBehaviour, NotifyHandler, StreamUpgradeError, THandlerInEvent,
36-
ToSwarm,
32+
dummy, ConnectionDenied, ConnectionHandler, ConnectionId, NewExternalAddrCandidate, THandler,
33+
THandlerOutEvent,
3734
};
35+
use libp2p_swarm::{NetworkBehaviour, NotifyHandler, StreamUpgradeError, THandlerInEvent, ToSwarm};
36+
use lru::LruCache;
3837
use std::collections::{HashMap, HashSet, VecDeque};
38+
use std::num::NonZeroUsize;
3939
use std::task::{Context, Poll};
4040
use thiserror::Error;
4141
use void::Void;
@@ -79,9 +79,7 @@ pub struct Behaviour {
7979
/// All direct (non-relayed) connections.
8080
direct_connections: HashMap<PeerId, HashSet<ConnectionId>>,
8181

82-
external_addresses: ExternalAddresses,
83-
84-
local_peer_id: PeerId,
82+
address_candidates: Candidates,
8583

8684
direct_to_relayed_connections: HashMap<ConnectionId, ConnectionId>,
8785

@@ -95,20 +93,14 @@ impl Behaviour {
9593
Behaviour {
9694
queued_events: Default::default(),
9795
direct_connections: Default::default(),
98-
external_addresses: Default::default(),
99-
local_peer_id,
96+
address_candidates: Candidates::new(local_peer_id),
10097
direct_to_relayed_connections: Default::default(),
10198
outgoing_direct_connection_attempts: Default::default(),
10299
}
103100
}
104101

105102
fn observed_addresses(&self) -> Vec<Multiaddr> {
106-
self.external_addresses
107-
.iter()
108-
.filter(|a| !a.iter().any(|p| p == Protocol::P2pCircuit))
109-
.cloned()
110-
.map(|a| a.with(Protocol::P2p(self.local_peer_id)))
111-
.collect()
103+
self.address_candidates.iter().cloned().collect()
112104
}
113105

114106
fn on_dial_failure(
@@ -359,13 +351,14 @@ impl NetworkBehaviour for Behaviour {
359351
}
360352

361353
fn on_swarm_event(&mut self, event: FromSwarm) {
362-
self.external_addresses.on_swarm_event(&event);
363-
364354
match event {
365355
FromSwarm::ConnectionClosed(connection_closed) => {
366356
self.on_connection_closed(connection_closed)
367357
}
368358
FromSwarm::DialFailure(dial_failure) => self.on_dial_failure(dial_failure),
359+
FromSwarm::NewExternalAddrCandidate(NewExternalAddrCandidate { addr }) => {
360+
self.address_candidates.add(addr.clone());
361+
}
369362
FromSwarm::AddressChange(_)
370363
| FromSwarm::ConnectionEstablished(_)
371364
| FromSwarm::ListenFailure(_)
@@ -374,13 +367,48 @@ impl NetworkBehaviour for Behaviour {
374367
| FromSwarm::ExpiredListenAddr(_)
375368
| FromSwarm::ListenerError(_)
376369
| FromSwarm::ListenerClosed(_)
377-
| FromSwarm::NewExternalAddrCandidate(_)
378370
| FromSwarm::ExternalAddrExpired(_)
379371
| FromSwarm::ExternalAddrConfirmed(_) => {}
380372
}
381373
}
382374
}
383375

376+
/// Stores our address candidates.
377+
///
378+
/// We use an [`LruCache`] to favor addresses that are reported more often.
379+
/// When attempting a hole-punch, we will try more frequent addresses first.
380+
/// Most of these addresses will come from observations by other nodes (via e.g. the identify protocol).
381+
/// More common observations mean a more likely stable port-mapping and thus a higher chance of a successful hole-punch.
382+
struct Candidates {
383+
inner: LruCache<Multiaddr, ()>,
384+
me: PeerId,
385+
}
386+
387+
impl Candidates {
388+
fn new(me: PeerId) -> Self {
389+
Self {
390+
inner: LruCache::new(NonZeroUsize::new(20).expect("20 > 0")),
391+
me,
392+
}
393+
}
394+
395+
fn add(&mut self, mut address: Multiaddr) {
396+
if is_relayed(&address) {
397+
return;
398+
}
399+
400+
if address.iter().last() != Some(Protocol::P2p(self.me)) {
401+
address.push(Protocol::P2p(self.me));
402+
}
403+
404+
self.inner.push(address, ());
405+
}
406+
407+
fn iter(&self) -> impl Iterator<Item = &Multiaddr> {
408+
self.inner.iter().map(|(a, _)| a)
409+
}
410+
}
411+
384412
fn is_relayed(addr: &Multiaddr) -> bool {
385413
addr.iter().any(|p| p == Protocol::P2pCircuit)
386414
}

protocols/dcutr/src/handler/relayed.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020

2121
//! [`ConnectionHandler`] handling relayed connection potentially upgraded to a direct connection.
2222
23-
use crate::behaviour_impl::MAX_NUMBER_OF_UPGRADE_ATTEMPTS;
23+
use crate::behaviour::MAX_NUMBER_OF_UPGRADE_ATTEMPTS;
2424
use crate::protocol;
2525
use either::Either;
2626
use futures::future;

protocols/dcutr/src/lib.rs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
2424
#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))]
2525

26-
mod behaviour_impl; // TODO: Rename back `behaviour` once deprecation symbols are removed.
26+
mod behaviour;
2727
mod handler;
2828
mod protocol;
2929

@@ -33,9 +33,7 @@ mod proto {
3333
pub(crate) use self::holepunch::pb::{mod_HolePunch::*, HolePunch};
3434
}
3535

36-
pub use behaviour_impl::Behaviour;
37-
pub use behaviour_impl::Error;
38-
pub use behaviour_impl::Event;
36+
pub use behaviour::{Behaviour, Error, Event};
3937
pub use protocol::PROTOCOL_NAME;
4038
pub mod inbound {
4139
pub use crate::protocol::inbound::UpgradeError;

protocols/dcutr/tests/lib.rs

Lines changed: 52 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ use libp2p_core::multiaddr::{Multiaddr, Protocol};
2222
use libp2p_core::transport::upgrade::Version;
2323
use libp2p_core::transport::{MemoryTransport, Transport};
2424
use libp2p_dcutr as dcutr;
25+
use libp2p_identify as identify;
2526
use libp2p_identity as identity;
2627
use libp2p_identity::PeerId;
2728
use libp2p_plaintext as plaintext;
@@ -38,10 +39,19 @@ async fn connect() {
3839
let mut dst = build_client();
3940
let mut src = build_client();
4041

41-
// Have all swarms listen on a local memory address.
42-
let (relay_addr, _) = relay.listen().await;
43-
let (dst_addr, _) = dst.listen().await;
44-
src.listen().await;
42+
// Have all swarms listen on a local TCP address.
43+
let (memory_addr, relay_addr) = relay.listen().await;
44+
relay.remove_external_address(&memory_addr);
45+
relay.add_external_address(relay_addr.clone());
46+
47+
let (dst_mem_addr, dst_tcp_addr) = dst.listen().await;
48+
let (src_mem_addr, _) = src.listen().await;
49+
50+
dst.remove_external_address(&dst_mem_addr);
51+
src.remove_external_address(&src_mem_addr);
52+
53+
assert!(src.external_addresses().next().is_none());
54+
assert!(dst.external_addresses().next().is_none());
4555

4656
let relay_peer_id = *relay.local_peer_id();
4757
let dst_peer_id = *dst.local_peer_id();
@@ -80,11 +90,12 @@ async fn connect() {
8090
break;
8191
}
8292
}
93+
ClientEvent::Identify(_) => {}
8394
other => panic!("Unexpected event: {other:?}."),
8495
}
8596
}
8697

87-
let dst_addr = dst_addr.with(Protocol::P2p(dst_peer_id));
98+
let dst_addr = dst_tcp_addr.with(Protocol::P2p(dst_peer_id));
8899

89100
let established_conn_id = src
90101
.wait(move |e| match e {
@@ -109,20 +120,33 @@ async fn connect() {
109120
assert_eq!(established_conn_id, reported_conn_id);
110121
}
111122

112-
fn build_relay() -> Swarm<relay::Behaviour> {
123+
fn build_relay() -> Swarm<Relay> {
113124
Swarm::new_ephemeral(|identity| {
114125
let local_peer_id = identity.public().to_peer_id();
115126

116-
relay::Behaviour::new(
117-
local_peer_id,
118-
relay::Config {
119-
reservation_duration: Duration::from_secs(2),
120-
..Default::default()
121-
},
122-
)
127+
Relay {
128+
relay: relay::Behaviour::new(
129+
local_peer_id,
130+
relay::Config {
131+
reservation_duration: Duration::from_secs(2),
132+
..Default::default()
133+
},
134+
),
135+
identify: identify::Behaviour::new(identify::Config::new(
136+
"/relay".to_owned(),
137+
identity.public(),
138+
)),
139+
}
123140
})
124141
}
125142

143+
#[derive(NetworkBehaviour)]
144+
#[behaviour(prelude = "libp2p_swarm::derive_prelude")]
145+
struct Relay {
146+
relay: relay::Behaviour,
147+
identify: identify::Behaviour,
148+
}
149+
126150
fn build_client() -> Swarm<Client> {
127151
let local_key = identity::Keypair::generate_ed25519();
128152
let local_peer_id = local_key.public().to_peer_id();
@@ -142,6 +166,10 @@ fn build_client() -> Swarm<Client> {
142166
Client {
143167
relay: behaviour,
144168
dcutr: dcutr::Behaviour::new(local_peer_id),
169+
identify: identify::Behaviour::new(identify::Config::new(
170+
"/client".to_owned(),
171+
local_key.public(),
172+
)),
145173
},
146174
local_peer_id,
147175
Config::with_async_std_executor(),
@@ -153,6 +181,7 @@ fn build_client() -> Swarm<Client> {
153181
struct Client {
154182
relay: relay::client::Behaviour,
155183
dcutr: dcutr::Behaviour,
184+
identify: identify::Behaviour,
156185
}
157186

158187
async fn wait_for_reservation(
@@ -163,14 +192,16 @@ async fn wait_for_reservation(
163192
) {
164193
let mut new_listen_addr_for_relayed_addr = false;
165194
let mut reservation_req_accepted = false;
195+
let mut addr_observed = false;
196+
166197
loop {
198+
if new_listen_addr_for_relayed_addr && reservation_req_accepted && addr_observed {
199+
break;
200+
}
201+
167202
match client.next_swarm_event().await {
168-
SwarmEvent::NewListenAddr { address, .. } if address != client_addr => {}
169203
SwarmEvent::NewListenAddr { address, .. } if address == client_addr => {
170204
new_listen_addr_for_relayed_addr = true;
171-
if reservation_req_accepted {
172-
break;
173-
}
174205
}
175206
SwarmEvent::Behaviour(ClientEvent::Relay(
176207
relay::client::Event::ReservationReqAccepted {
@@ -180,15 +211,16 @@ async fn wait_for_reservation(
180211
},
181212
)) if relay_peer_id == peer_id && renewal == is_renewal => {
182213
reservation_req_accepted = true;
183-
if new_listen_addr_for_relayed_addr {
184-
break;
185-
}
186214
}
187215
SwarmEvent::Dialing {
188216
peer_id: Some(peer_id),
189217
..
190218
} if peer_id == relay_peer_id => {}
191219
SwarmEvent::ConnectionEstablished { peer_id, .. } if peer_id == relay_peer_id => {}
220+
SwarmEvent::Behaviour(ClientEvent::Identify(identify::Event::Received { .. })) => {
221+
addr_observed = true;
222+
}
223+
SwarmEvent::Behaviour(ClientEvent::Identify(_)) => {}
192224
e => panic!("{e:?}"),
193225
}
194226
}

0 commit comments

Comments
 (0)