diff options
| author | tv <tv@krebsco.de> | 2026-10-29 04:32:42 +0100 |
|---|---|---|
| committer | tv <tv@krebsco.de> | 2026-10-29 04:32:42 +0100 |
| commit | 355c85940b387c292cc65e2a291b6990d4ac6234 (patch) | |
| tree | 92348e4a1def1d1792577db303c0db6e4ad480b2 /src/transport | |
| parent | a17bc7e17109bc680f841498007adf72be3c735f (diff) | |
libp2p: add UPnP support
Diffstat (limited to 'src/transport')
| -rw-r--r-- | src/transport/libp2p.rs | 56 |
1 files changed, 54 insertions, 2 deletions
diff --git a/src/transport/libp2p.rs b/src/transport/libp2p.rs index 9b7c666..6602c2e 100644 --- a/src/transport/libp2p.rs +++ b/src/transport/libp2p.rs @@ -40,6 +40,10 @@ //! AutoNAT asks connected peers (nodes of the swarm) to dial it back. Once AutoNAT says //! it can be dialled, it lets the relays it picked go. //! +//! With `--libp2p-upnp`, a node asks its router to forward its listen ports (UPnP), and +//! publishes the router's public address for them with its own (`Book::direct`): then +//! it can be dialled from elsewhere, whatever the other side's NAT. +//! //! With `--libp2p-mdns`, a node finds libp2p peers on its LAN (multicast DNS). Their //! LAN addresses are noted, and those that turn out to speak continuum (identify says //! so) are seeds while the node joins (`discovered_seeds`): nodes on one LAN find each @@ -173,6 +177,8 @@ pub(crate) struct Libp2pConfig { pub(crate) continuum: bool, /// Find peers on the LAN (mDNS). pub(crate) mdns: bool, + /// Ask the router to forward our listen ports (UPnP), and publish where. + pub(crate) upnp: bool, /// Use public infrastructure: the IPFS DHT, public relays. pub(crate) public: bool, /// libp2p's in-memory transport instead of TCP and QUIC: for tests. @@ -255,6 +261,8 @@ struct NamedRelay { struct Book { addresses: HashMap<PeerId, Vec<Multiaddr>>, listening: Vec<Multiaddr>, + /// Where the router forwards to our listen addresses (UPnP, `Libp2pConfig::upnp`). + mapped: Vec<Multiaddr>, /// `Libp2pConfig::announce`. announced: Vec<Multiaddr>, /// `Libp2pConfig::relay`: this node relays, and says so. @@ -571,7 +579,8 @@ impl Book { } /// Where others could dial us directly: the announced addresses, or else those we - /// listen on (`published`), without the peer ID. + /// listen on (`published`) and where the router forwards to them (`mapped`), without + /// the peer ID. fn direct(&self) -> Vec<Multiaddr> { if !self.announced.is_empty() { return self.announced.clone(); @@ -582,7 +591,15 @@ impl Book { .filter(|address| !is_relayed(address)) .cloned() .collect(); - published(&direct) + let mut direct = published(&direct); + direct.extend( + self.mapped + .iter() + .filter(|address| !direct.contains(address)) + .cloned() + .collect::<Vec<_>>(), + ); + direct } } @@ -939,6 +956,9 @@ struct Behaviour { autonat: ::libp2p::autonat::Behaviour, /// Only with `Libp2pConfig::mdns`. mdns: ::libp2p::swarm::behaviour::toggle::Toggle<::libp2p::mdns::tokio::Behaviour>, + /// Only with `Libp2pConfig::upnp`: maps our listen ports on the router, and confirms + /// the mapped addresses as ours (identify then tells them). + upnp: ::libp2p::swarm::behaviour::toggle::Toggle<::libp2p::upnp::tokio::Behaviour>, /// Only with `Libp2pConfig::public`: the IPFS DHT. kad: ::libp2p::swarm::behaviour::toggle::Toggle< ::libp2p::kad::Behaviour<::libp2p::kad::store::MemoryStore>, @@ -1095,6 +1115,7 @@ impl ::libp2p::swarm::NetworkBehaviour for HolePunching { struct Features { relay: bool, mdns: bool, + upnp: bool, public: bool, } @@ -1103,6 +1124,7 @@ impl Features { Self { relay: config.relay, mdns: config.mdns, + upnp: config.upnp, public: config.public, } } @@ -1129,6 +1151,7 @@ impl Behaviour { let Features { relay, mdns, + upnp, public, } = features; let peer_id = key.public().to_peer_id(); @@ -1184,6 +1207,7 @@ impl Behaviour { }, ), mdns: mdns.into(), + upnp: upnp.then(::libp2p::upnp::tokio::Behaviour::default).into(), kad: kad.into(), }) } @@ -1772,6 +1796,31 @@ async fn run( trace!(peer = %Addressed(SCHEME, &peer), connection = %connection, error = %error, "Connection's ping failed"); } }, + SwarmEvent::Behaviour(BehaviourEvent::Upnp(event)) => match event { + ::libp2p::upnp::Event::NewExternalAddr { + local_addr, + external_addr: address, + } => { + info!(address = %address, to = %local_addr, "The router forwards to us (UPnP)"); + let mut book = lock(&book); + if !book.mapped.contains(&address) { + book.mapped.push(address); + } + } + ::libp2p::upnp::Event::ExpiredExternalAddr { + local_addr, + external_addr: address, + } => { + info!(address = %address, to = %local_addr, "The router no longer forwards to us (UPnP)"); + lock(&book).mapped.retain(|mapped| *mapped != address); + } + ::libp2p::upnp::Event::GatewayNotFound => { + info!("No router that maps ports (UPnP) answered"); + } + ::libp2p::upnp::Event::NonRoutableGateway => { + warn!("The router that maps ports (UPnP) is behind another NAT itself; its mappings don't reach us from outside"); + } + }, SwarmEvent::Behaviour(BehaviourEvent::Dcutr(event)) => match event.result { Ok(_) => { lock(&book).counts.hole_punches.succeeded += 1; @@ -2242,6 +2291,7 @@ mod tests { relay, continuum: true, mdns: false, + upnp: false, public: false, memory: true, }) @@ -2388,6 +2438,7 @@ mod tests { relay: false, continuum: true, mdns: false, + upnp: false, public: false, memory: true, }) @@ -2525,6 +2576,7 @@ mod tests { relay: true, continuum: false, mdns: false, + upnp: false, public: false, memory: true, }) |
