aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authortv <tv@krebsco.de>2026-10-29 04:32:42 +0100
committertv <tv@krebsco.de>2026-10-29 04:32:42 +0100
commit355c85940b387c292cc65e2a291b6990d4ac6234 (patch)
tree92348e4a1def1d1792577db303c0db6e4ad480b2
parenta17bc7e17109bc680f841498007adf72be3c735f (diff)
libp2p: add UPnP support
-rw-r--r--Cargo.toml2
-rw-r--r--src/config/mod.rs10
-rw-r--r--src/main.rs1
-rw-r--r--src/simtest/identity.rs1
-rw-r--r--src/transport/libp2p.rs56
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
@@ -292,6 +292,13 @@ pub(crate) const OPTIONS: &[OptionSpec] = &[
"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,
"BOOL",
@@ -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<PathBuf>,
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<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,
})