From 355c85940b387c292cc65e2a291b6990d4ac6234 Mon Sep 17 00:00:00 2001 From: tv Date: Thu, 29 Oct 2026 04:32:42 +0100 Subject: libp2p: add UPnP support --- Cargo.toml | 2 +- src/config/mod.rs | 10 +++++++++ src/main.rs | 1 + src/simtest/identity.rs | 1 + src/transport/libp2p.rs | 56 +++++++++++++++++++++++++++++++++++++++++++++++-- 5 files changed, 67 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 7ea4643..db5503d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,7 +40,7 @@ bytes = "1.12" clap = "4.6" dashmap = "6.2" futures = { version = "0.3", optional = true } -libp2p = { version = "0.57", optional = true, features = ["tokio", "tcp", "quic", "dns", "noise", "yamux", "ed25519", "identify", "ping", "macros", "relay", "dcutr", "autonat", "mdns", "kad"] } +libp2p = { version = "0.57", optional = true, features = ["tokio", "tcp", "quic", "dns", "noise", "yamux", "ed25519", "identify", "ping", "macros", "relay", "dcutr", "upnp", "autonat", "mdns", "kad"] } libp2p-stream = { version = "0.5.0-alpha", optional = true } ed25519-dalek = "3" getrandom = "0.4" diff --git a/src/config/mod.rs b/src/config/mod.rs index cfe2e7e..cb4bf19 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -291,6 +291,13 @@ pub(crate) const OPTIONS: &[OptionSpec] = &[ Some("false"), "With the libp2p transport: find nodes on the LAN (multicast DNS, IPv4). Those that speak continuum are seeds while joining, so nodes on one LAN find each other without configured seeds; a swarm key keeps a new node from joining another swarm on the same LAN.", ), + option( + "libp2p-upnp", + Kind::Flag, + "BOOL", + Some("false"), + "With the libp2p transport: ask the router to forward this node's listen ports (UPnP), and publish where (the router's public address), so that nodes elsewhere can dial it: behind a home router, no relay or hole punching is needed then. Opens those ports on the router for as long as the node runs.", + ), option( "libp2p-public", Kind::Flag, @@ -458,6 +465,9 @@ pub(crate) struct Settings { pub(crate) libp2p_mdns: bool, #[serde(default)] #[cfg_attr(not(feature = "libp2p"), allow(dead_code))] + pub(crate) libp2p_upnp: bool, + #[serde(default)] + #[cfg_attr(not(feature = "libp2p"), allow(dead_code))] pub(crate) libp2p_public: bool, #[cfg_attr(not(feature = "libp2p"), allow(dead_code))] pub(crate) libp2p_key_file: Option, diff --git a/src/main.rs b/src/main.rs index 8af1c29..1803f15 100644 --- a/src/main.rs +++ b/src/main.rs @@ -391,6 +391,7 @@ fn libp2p_config( relay: args.libp2p_relay || relay_only, continuum: !relay_only, mdns: args.libp2p_mdns, + upnp: args.libp2p_upnp, public: args.libp2p_public, memory: false, }) diff --git a/src/simtest/identity.rs b/src/simtest/identity.rs index 9f2bff3..13c596a 100644 --- a/src/simtest/identity.rs +++ b/src/simtest/identity.rs @@ -221,6 +221,7 @@ async fn nodes_over_libp2p() { relay: false, continuum: true, mdns: false, + upnp: false, public: false, memory: true, }) 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>, listening: Vec, + /// Where the router forwards to our listen addresses (UPnP, `Libp2pConfig::upnp`). + mapped: Vec, /// `Libp2pConfig::announce`. announced: Vec, /// `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 { 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::>(), + ); + 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, }) -- cgit v1.3.1