aboutsummaryrefslogtreecommitdiffstats
path: root/src/transport/libp2p.rs
diff options
context:
space:
mode:
authortv <tv@krebsco.de>2026-10-29 03:19:59 +0100
committertv <tv@krebsco.de>2026-10-29 03:19:59 +0100
commit8dca9d7ee4969d5f00a6858a2954825f5136148c (patch)
tree7f8a5542830a5efd70c0a3858534ca2eb34d4d8a /src/transport/libp2p.rs
parent66aa9dda9280f8ea5956be026b4b8f40740ed20c (diff)
libp2p: add LAN hole punching
Diffstat (limited to 'src/transport/libp2p.rs')
-rw-r--r--src/transport/libp2p.rs160
1 files changed, 156 insertions, 4 deletions
diff --git a/src/transport/libp2p.rs b/src/transport/libp2p.rs
index 9f83295..67ee6b5 100644
--- a/src/transport/libp2p.rs
+++ b/src/transport/libp2p.rs
@@ -22,8 +22,8 @@
//! connect directly (libp2p's DCUtR, hole punching); the relay carries the traffic until
//! that works, or for good if it doesn't, within its limits (`RELAY_CIRCUIT_DURATION`,
//! `RELAY_CIRCUIT_BYTES`; a circuit that ends is made again on the next send). Hole
-//! punching needs both sides to listen; where only one does, the other dials it
-//! directly once it knows where (`upgrade`).
+//! punching needs both sides to listen (`HolePunching`: at their LAN addresses too);
+//! where only one does, the other dials it directly once it knows where (`upgrade`).
//!
//! Of several connections to one peer (both dialled at once, or AutoNAT dialled back),
//! all but one are closed a few seconds later (`DUPLICATE_GRACE`, `Book::duplicates`),
@@ -925,7 +925,7 @@ struct Behaviour {
relay_client: ::libp2p::relay::client::Behaviour,
/// Only with `Libp2pConfig::relay`.
relay: ::libp2p::swarm::behaviour::toggle::Toggle<::libp2p::relay::Behaviour>,
- dcutr: ::libp2p::dcutr::Behaviour,
+ dcutr: HolePunching,
/// Each connection's round trip (`Book::rtts`): of several to one peer, the fastest
/// is kept.
ping: ::libp2p::ping::Behaviour,
@@ -938,6 +938,145 @@ struct Behaviour {
>,
}
+/// libp2p's hole punching (DCUtR), offering our listen addresses too. DCUtR offers only
+/// where others saw us (external address candidates, from identify): seen by a relay
+/// out on the internet, two nodes on one LAN are both at their router's public address,
+/// and punching through it needs the router to hairpin, which many don't. Their LAN
+/// addresses work, the firewalls on the way being stateful. Only DCUtR is told:
+/// AutoNAT would test every candidate, and fail on all of these.
+struct HolePunching {
+ inner: ::libp2p::dcutr::Behaviour,
+ /// Where we listen (`offered`).
+ listening: Vec<Multiaddr>,
+}
+
+/// Whether DCUtR offers our listen address `address` too: neither loopback nor
+/// link-local (as for publishing), nor through a relay.
+fn offered(address: &Multiaddr) -> bool {
+ !is_local(address) && !is_relayed(address)
+}
+
+impl HolePunching {
+ fn new(local: PeerId) -> Self {
+ Self {
+ inner: ::libp2p::dcutr::Behaviour::new(local),
+ listening: Vec::new(),
+ }
+ }
+
+ /// Tells DCUtR where we listen, as if others had seen us there. Again after every
+ /// other candidate, as DCUtR keeps the latest 20.
+ fn offer_listening(&mut self) {
+ use ::libp2p::swarm::{FromSwarm, NetworkBehaviour, NewExternalAddrCandidate};
+ for addr in &self.listening {
+ self.inner
+ .on_swarm_event(FromSwarm::NewExternalAddrCandidate(
+ NewExternalAddrCandidate { addr },
+ ));
+ }
+ }
+}
+
+impl ::libp2p::swarm::NetworkBehaviour for HolePunching {
+ type ConnectionHandler =
+ <::libp2p::dcutr::Behaviour as ::libp2p::swarm::NetworkBehaviour>::ConnectionHandler;
+ type ToSwarm = ::libp2p::dcutr::Event;
+
+ fn handle_pending_inbound_connection(
+ &mut self,
+ connection_id: ConnectionId,
+ local_addr: &Multiaddr,
+ remote_addr: &Multiaddr,
+ ) -> Result<(), ::libp2p::swarm::ConnectionDenied> {
+ self.inner
+ .handle_pending_inbound_connection(connection_id, local_addr, remote_addr)
+ }
+
+ fn handle_established_inbound_connection(
+ &mut self,
+ connection_id: ConnectionId,
+ peer: PeerId,
+ local_addr: &Multiaddr,
+ remote_addr: &Multiaddr,
+ ) -> Result<::libp2p::swarm::THandler<Self>, ::libp2p::swarm::ConnectionDenied> {
+ self.inner.handle_established_inbound_connection(
+ connection_id,
+ peer,
+ local_addr,
+ remote_addr,
+ )
+ }
+
+ fn handle_pending_outbound_connection(
+ &mut self,
+ connection_id: ConnectionId,
+ maybe_peer: Option<PeerId>,
+ addresses: &[Multiaddr],
+ effective_role: ::libp2p::core::Endpoint,
+ ) -> Result<Vec<Multiaddr>, ::libp2p::swarm::ConnectionDenied> {
+ self.inner.handle_pending_outbound_connection(
+ connection_id,
+ maybe_peer,
+ addresses,
+ effective_role,
+ )
+ }
+
+ fn handle_established_outbound_connection(
+ &mut self,
+ connection_id: ConnectionId,
+ peer: PeerId,
+ addr: &Multiaddr,
+ role_override: ::libp2p::core::Endpoint,
+ port_use: ::libp2p::core::transport::PortUse,
+ ) -> Result<::libp2p::swarm::THandler<Self>, ::libp2p::swarm::ConnectionDenied> {
+ self.inner.handle_established_outbound_connection(
+ connection_id,
+ peer,
+ addr,
+ role_override,
+ port_use,
+ )
+ }
+
+ fn on_swarm_event(&mut self, event: ::libp2p::swarm::FromSwarm) {
+ use ::libp2p::swarm::FromSwarm;
+ self.inner.on_swarm_event(event);
+ match event {
+ FromSwarm::NewListenAddr(new) if offered(new.addr) => {
+ if !self.listening.contains(new.addr) {
+ self.listening.push(new.addr.clone());
+ }
+ self.offer_listening();
+ }
+ FromSwarm::ExpiredListenAddr(expired) => {
+ self.listening.retain(|address| address != expired.addr);
+ }
+ FromSwarm::NewExternalAddrCandidate(_) => self.offer_listening(),
+ _ => {}
+ }
+ }
+
+ fn on_connection_handler_event(
+ &mut self,
+ peer_id: PeerId,
+ connection_id: ConnectionId,
+ event: ::libp2p::swarm::THandlerOutEvent<Self>,
+ ) {
+ self.inner
+ .on_connection_handler_event(peer_id, connection_id, event)
+ }
+
+ fn poll(
+ &mut self,
+ cx: &mut std::task::Context<'_>,
+ ) -> std::task::Poll<
+ ::libp2p::swarm::ToSwarm<Self::ToSwarm, ::libp2p::swarm::THandlerInEvent<Self>>,
+ > {
+ self.inner.poll(cx)
+ }
+}
+
/// What the behaviour has besides what every node has.
#[derive(Clone, Copy)]
struct Features {
@@ -1021,7 +1160,7 @@ impl Behaviour {
identify: ::libp2p::identify::Behaviour::new(identify),
relay_client,
relay: relay.into(),
- dcutr: ::libp2p::dcutr::Behaviour::new(peer_id),
+ dcutr: HolePunching::new(peer_id),
ping: ::libp2p::ping::Behaviour::new(::libp2p::ping::Config::new()),
autonat: ::libp2p::autonat::Behaviour::new(
peer_id,
@@ -2633,6 +2772,19 @@ mod tests {
}
}
+ #[test]
+ fn listen_addresses_offered_for_hole_punching() {
+ let parse = |text: &str| text.parse::<Multiaddr>().unwrap();
+ assert!(offered(&parse("/ip4/192.168.1.2/udp/9661/quic-v1")));
+ assert!(offered(&parse("/ip6/fd00::2/tcp/9661")));
+ assert!(!offered(&parse("/ip4/127.0.0.1/udp/9661/quic-v1")));
+ assert!(!offered(&parse("/ip6/fe80::1/tcp/9661")));
+ let relay = PeerId::random();
+ assert!(!offered(&parse(&format!(
+ "/ip4/192.168.1.2/tcp/9661/p2p/{relay}/p2p-circuit"
+ ))));
+ }
+
/// libp2p's nested dial errors come out as each address's cause, once.
#[test]
fn dial_errors_are_short() {