From cefdacf9afc566d4309569a8eba407828b194a27 Mon Sep 17 00:00:00 2001 From: Benno Zeeman Date: Tue, 7 May 2024 10:06:30 +0200 Subject: [PATCH 01/10] feat(nat): add binary to probe for NAT status This binary will help a machine figure out its NAT status. For this to work a public machine runs the binary, while a peer asks the public machine for a dial-back. --- Cargo.lock | 47 +++ Cargo.toml | 1 + sn_nat_detection/Cargo.toml | 29 ++ sn_nat_detection/README.md | 41 +++ sn_nat_detection/src/behaviour/auto_nat.rs | 75 +++++ sn_nat_detection/src/behaviour/identify.rs | 57 ++++ sn_nat_detection/src/behaviour/mod.rs | 58 ++++ sn_nat_detection/src/main.rs | 347 +++++++++++++++++++++ 8 files changed, 655 insertions(+) create mode 100644 sn_nat_detection/Cargo.toml create mode 100644 sn_nat_detection/README.md create mode 100644 sn_nat_detection/src/behaviour/auto_nat.rs create mode 100644 sn_nat_detection/src/behaviour/identify.rs create mode 100644 sn_nat_detection/src/behaviour/mod.rs create mode 100644 sn_nat_detection/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index 7f5b7e1d35..aff938636b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1076,6 +1076,16 @@ dependencies = [ "clap_derive", ] +[[package]] +name = "clap-verbosity-flag" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb9b20c0dd58e4c2e991c8d203bbeb76c11304d1011659686b5b644bc29aa478" +dependencies = [ + "clap", + "log", +] + [[package]] name = "clap_builder" version = "4.5.2" @@ -3718,6 +3728,7 @@ dependencies = [ "getrandom 0.2.15", "instant", "libp2p-allow-block-list", + "libp2p-autonat", "libp2p-connection-limits", "libp2p-core", "libp2p-dcutr", @@ -3756,6 +3767,27 @@ dependencies = [ "void", ] +[[package]] +name = "libp2p-autonat" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d95151726170e41b591735bf95c42b888fe4aa14f65216a9fbf0edcc04510586" +dependencies = [ + "async-trait", + "asynchronous-codec 0.6.2", + "futures", + "futures-timer", + "instant", + "libp2p-core", + "libp2p-identity", + "libp2p-request-response", + "libp2p-swarm", + "quick-protobuf", + "quick-protobuf-codec 0.2.0", + "rand 0.8.5", + "tracing", +] + [[package]] name = "libp2p-connection-limits" version = "0.3.1" @@ -7134,6 +7166,21 @@ dependencies = [ "walkdir", ] +[[package]] +name = "sn_nat_detection" +version = "0.1.0" +dependencies = [ + "clap", + "clap-verbosity-flag", + "futures", + "libp2p", + "sn_networking", + "tokio", + "tracing", + "tracing-log 0.2.0", + "tracing-subscriber", +] + [[package]] name = "sn_networking" version = "0.15.2" diff --git a/Cargo.toml b/Cargo.toml index e3c96bc3ad..ba276d8871 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ members = [ "sn_faucet", "sn_logging", "sn_metrics", + "sn_nat_detection", "sn_networking", "sn_node", "node-launchpad", diff --git a/sn_nat_detection/Cargo.toml b/sn_nat_detection/Cargo.toml new file mode 100644 index 0000000000..b55ac6a5c6 --- /dev/null +++ b/sn_nat_detection/Cargo.toml @@ -0,0 +1,29 @@ +[package] +authors = ["MaidSafe Developers "] +description = "Safe Network AutoNAT probing" +edition = "2021" +homepage = "https://maidsafe.net" +license = "GPL-3.0" +name = "sn_nat_detection" +readme = "README.md" +repository = "https://github.com/maidsafe/safe_network" +version = "0.1.0" + +[[bin]] +name = "detect-nat" +path = "src/main.rs" + +[dependencies] +tokio = { version = "1.32.0", features = ["full"] } +clap = { version = "4.5.4", features = ["derive"] } +futures = "~0.3.13" +libp2p = { version = "0.53", features = ["tokio", "tcp", "noise", "yamux", "autonat", "identify", "macros"] } +tracing = { version = "~0.1.26" } +tracing-subscriber = { version = "0.3", features = ["env-filter"] } + +sn_networking = { path = "../sn_networking", version = "0.14.4" } +clap-verbosity-flag = "2.2.0" +tracing-log = "0.2.0" + +[lints] +workspace = true diff --git a/sn_nat_detection/README.md b/sn_nat_detection/README.md new file mode 100644 index 0000000000..60827bbcf3 --- /dev/null +++ b/sn_nat_detection/README.md @@ -0,0 +1,41 @@ +## Description + +This example consists of a client and a server, which demonstrate the usage of the AutoNAT and identify protocols in **libp2p**. + +## Usage + +### Client + +The client-side part of the example showcases the combination of the AutoNAT and identify protocols. +The identify protocol allows the local peer to determine its external addresses, which are then included in AutoNAT dial-back requests sent to the server. + +To run the client example, follow these steps: + +1. Start the server by following the instructions provided in the `examples/server` directory. + +2. Open a new terminal. + +3. Run the following command in the terminal: + ```sh + cargo run --bin autonat_client -- --server-address --server-peer-id --listen-port + ``` + Note: The `--listen-port` parameter is optional and allows you to specify a fixed port at which the local client should listen. + +### Server + +The server-side example demonstrates a basic AutoNAT server that supports the autonat and identify protocols. + +To start the server, follow these steps: + +1. Open a terminal. + +2. Run the following command: + ```sh + cargo run --bin autonat_server -- --listen-port + ``` + Note: The `--listen-port` parameter is optional and allows you to set a fixed port at which the local peer should listen. + +## Conclusion + +By combining the AutoNAT and identify protocols, the example showcases the establishment of direct connections between peers and the exchange of external address information. +Users can explore the provided client and server code to gain insights into the implementation details and functionality of **libp2p**. diff --git a/sn_nat_detection/src/behaviour/auto_nat.rs b/sn_nat_detection/src/behaviour/auto_nat.rs new file mode 100644 index 0000000000..2b30b2d664 --- /dev/null +++ b/sn_nat_detection/src/behaviour/auto_nat.rs @@ -0,0 +1,75 @@ +use libp2p::autonat; +use tracing::{debug, info, warn}; + +use crate::EventLoop; + +impl EventLoop { + pub(crate) fn on_event_autonat(&mut self, event: autonat::Event) { + match event { + autonat::Event::InboundProbe(event) => match event { + autonat::InboundProbeEvent::Request { + probe_id, + peer: peer_id, + addresses, + } => { + info!(?probe_id, %peer_id, ?addresses, "Received a request to probe peer") + } + autonat::InboundProbeEvent::Response { + probe_id, + peer: peer_id, + address, + } => { + debug!(?probe_id, %peer_id, ?address, "Successfully probed a peer"); + } + autonat::InboundProbeEvent::Error { + probe_id, + peer: peer_id, + error, + } => { + warn!(?probe_id, %peer_id, ?error, "Probing a peer failed") + } + }, + autonat::Event::OutboundProbe(event) => match event { + autonat::OutboundProbeEvent::Request { + probe_id, + peer: peer_id, + } => { + debug!(?probe_id, %peer_id, "Asking remote to probe us") + } + autonat::OutboundProbeEvent::Response { + probe_id, + peer: peer_id, + address, + } => { + info!(?probe_id, %peer_id, ?address, "Remote successfully probed (reached) us") + } + autonat::OutboundProbeEvent::Error { + probe_id, + peer: peer_id, + error, + } => { + // Ignore the `NoServer` error if we're a server ourselves. + if self.client_state.is_none() + && !matches!(error, autonat::OutboundProbeError::NoServer) + { + warn!( + ?probe_id, + ?peer_id, + ?error, + "A request for probing us has failed" + ) + } + } + }, + autonat::Event::StatusChanged { old, new } => { + info!( + ?new, + ?old, + confidence = self.swarm.behaviour().auto_nat.confidence(), + "AutoNAT status changed" + ); + self.check_state(); + } + } + } +} diff --git a/sn_nat_detection/src/behaviour/identify.rs b/sn_nat_detection/src/behaviour/identify.rs new file mode 100644 index 0000000000..3562ac69bc --- /dev/null +++ b/sn_nat_detection/src/behaviour/identify.rs @@ -0,0 +1,57 @@ +use libp2p::{autonat, identify}; +use sn_networking::multiaddr_is_global; +use tracing::{debug, info, warn}; + +use crate::{behaviour::PROTOCOL_VERSION, EventLoop}; + +impl EventLoop { + pub(crate) fn on_event_identify(&mut self, event: identify::Event) { + match event { + identify::Event::Received { peer_id, info } => { + debug!( + %peer_id, + protocols=?info.protocols, + observed_address=%info.observed_addr, + protocol_version=%info.protocol_version, + "Received peer info" + ); + + // Disconnect if peer has incompatible protocol version. + if info.protocol_version != PROTOCOL_VERSION { + warn!(%peer_id, "Incompatible protocol version. Disconnecting from peer."); + let _ = self.swarm.disconnect_peer_id(peer_id); + return; + } + + // Disconnect if peer has no AutoNAT support. + if !info + .protocols + .iter() + .any(|p| *p == autonat::DEFAULT_PROTOCOL_NAME) + { + warn!(%peer_id, "Peer does not support AutoNAT. Disconnecting from peer."); + let _ = self.swarm.disconnect_peer_id(peer_id); + #[allow(clippy::needless_return)] + return; + } + + info!(%peer_id, "Received peer info: confirmed it supports AutoNAT"); + + // If we're a client and the peer has (a) global listen address(es), + // add it as an AutoNAT server. + if self.client_state.is_some() { + for addr in info.listen_addrs.into_iter().filter(multiaddr_is_global) { + self.swarm + .behaviour_mut() + .auto_nat + .add_server(peer_id, Some(addr)); + } + } + self.check_state(); + } + identify::Event::Sent { .. } => { /* ignore */ } + identify::Event::Pushed { .. } => { /* ignore */ } + identify::Event::Error { .. } => { /* ignore */ } + } + } +} diff --git a/sn_nat_detection/src/behaviour/mod.rs b/sn_nat_detection/src/behaviour/mod.rs new file mode 100644 index 0000000000..6def7fdd84 --- /dev/null +++ b/sn_nat_detection/src/behaviour/mod.rs @@ -0,0 +1,58 @@ +use libp2p::swarm::NetworkBehaviour; +use libp2p::{autonat, identity}; +use std::time::Duration; + +use crate::CONFIDENCE_MAX; + +mod auto_nat; +mod identify; + +pub(crate) const PROTOCOL_VERSION: &str = "/sn_nat_detection/0.1.0"; + +#[derive(NetworkBehaviour)] +pub(crate) struct Behaviour { + pub identify: libp2p::identify::Behaviour, + pub auto_nat: autonat::Behaviour, +} + +impl Behaviour { + pub(crate) fn new(local_public_key: identity::PublicKey, client_mode: bool) -> Self { + let far_future = Duration::MAX / 10; // `MAX` on itself causes overflows. This is a workaround. + Self { + identify: libp2p::identify::Behaviour::new( + libp2p::identify::Config::new( + PROTOCOL_VERSION.to_string(), + local_public_key.clone(), + ) + // Exchange information every 5 minutes. + .with_interval(Duration::from_secs(5 * 60)), + ), + auto_nat: autonat::Behaviour::new( + local_public_key.to_peer_id(), + if client_mode { + autonat::Config { + // Use dialed peers for probing. + use_connected: true, + // Start probing 3 seconds after swarm init. This gives us time to connect to the dialed server. + boot_delay: Duration::from_secs(3), + // Reuse probe server immediately even if it's the only one. + throttle_server_period: Duration::ZERO, + retry_interval: Duration::from_secs(10), + // We do not want to refresh. + refresh_interval: far_future, + confidence_max: CONFIDENCE_MAX, + ..Default::default() + } + } else { + autonat::Config { + // Do not ask for dial-backs, essentially putting us in server mode. + use_connected: false, + // Never start probing, as we are a server. + boot_delay: far_future, + ..Default::default() + } + }, + ), + } + } +} diff --git a/sn_nat_detection/src/main.rs b/sn_nat_detection/src/main.rs new file mode 100644 index 0000000000..9d0885c3bf --- /dev/null +++ b/sn_nat_detection/src/main.rs @@ -0,0 +1,347 @@ +// Copyright 2021 Protocol Labs. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +#![doc = include_str!("../../README.md")] + +use clap::Parser; +use futures::StreamExt; +use libp2p::autonat::NatStatus; +use libp2p::core::{multiaddr::Protocol, Multiaddr}; +use libp2p::swarm::SwarmEvent; +use libp2p::{noise, tcp, yamux}; +use std::collections::HashSet; +use std::error::Error; +use std::net::Ipv4Addr; +use std::time::Duration; +use tracing::{debug, info, warn}; +use tracing_log::AsTrace; +use tracing_subscriber::layer::SubscriberExt; +use tracing_subscriber::util::SubscriberInitExt; +use tracing_subscriber::EnvFilter; + +use behaviour::{Behaviour, BehaviourEvent}; + +mod behaviour; + +const CONFIDENCE_MAX: usize = 2; +const RETRY_INTERVAL: Duration = Duration::from_secs(10); + +#[derive(Debug, Parser)] +#[clap(name = "libp2p autonat")] +struct Opt { + /// Port to listen on. + /// + /// `0` causes the OS to assign a random available port. + #[clap(long, short, default_value_t = 0)] + port: u16, + + /// Servers to send dial-back requests to, in a 'multiaddr' format. + /// + /// A multiaddr looks like `/ip4/1.2.3.4/tcp/1200/tcp` where `1.2.3.4` is the IP and `1200` is the port. + /// Alternatively, the address can be written as `1.2.3.4:1200`. + /// + /// This argument can be provided multiple times to connect to multiple peers. + #[clap(name = "SERVER", value_name = "multiaddr", value_delimiter = ',', value_parser = parse_peer_addr)] + server_addr: Vec, + + #[command(flatten)] + verbose: clap_verbosity_flag::Verbosity, +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + // Process command line arguments. + let opt = Opt::parse(); + + let registry = tracing_subscriber::registry().with(tracing_subscriber::fmt::layer()); + // Use `RUST_LOG` if set, else use the verbosity flag (where `-vvvv` is trace level). + let _ = if std::env::var_os("RUST_LOG").is_some() { + registry.with(EnvFilter::from_env("RUST_LOG")).try_init() + } else { + let filter = tracing_subscriber::filter::Targets::new().with_target( + env!("CARGO_BIN_NAME").replace('-', "_"), + opt.verbose.log_level_filter().as_trace(), + ); + registry.with(filter).try_init() + }; + + // If no servers are provided, we are in server mode. Conversely, with servers + // provided, we are in client mode. + let client_mode = !opt.server_addr.is_empty(); + + let mut swarm = libp2p::SwarmBuilder::with_new_identity() + .with_tokio() + .with_tcp( + tcp::Config::default(), + noise::Config::new, + yamux::Config::default, + )? + .with_behaviour(|key| Behaviour::new(key.public(), client_mode))? + // Make it so that we retry just before idling out, to prevent quickly disconnecting/connecting + // to the same server. + .with_swarm_config(|c| { + c.with_idle_connection_timeout(RETRY_INTERVAL + Duration::from_secs(2)) + }) + .build(); + + swarm.listen_on( + Multiaddr::empty() + .with(Protocol::Ip4(Ipv4Addr::UNSPECIFIED)) + .with(Protocol::Tcp(opt.port)), + )?; + + info!( + peer_id=%swarm.local_peer_id(), + "starting in {} mode", + if client_mode { "client" } else { "server" } + ); + + let event_loop = EventLoop::new(swarm, opt.server_addr); + // The main loop will exit once it has gained enough confidence in the NAT status. + let status = event_loop.run().await; + + match status { + NatStatus::Public(addr) => { + info!(%addr, "NAT is public"); + Ok(()) + } + NatStatus::Private => Err("NAT is private".into()), + NatStatus::Unknown => Err("NAT is unknown".into()), + } +} + +enum State { + // This is where we start dialing the servers. + Init(Vec), + // When we're dialing, we'll move on once we've connected to a server. + Dialing, + // We start probing until we have enough confidence (`usize`). + // Keep track of confidence to report on changes. + Probing(usize), + // With enough confidence reached, we should report back the status. + Done(NatStatus), +} + +struct EventLoop { + swarm: libp2p::Swarm, + // Interval with which to check the state of the program. (State is also checked on events.) + interval: tokio::time::Interval, + // When we are a client, we progress through different states. + client_state: Option, + // Keep track of candidate addresses to avoid logging duplicates. + candidate_addrs: HashSet, +} + +impl EventLoop { + fn new(swarm: libp2p::Swarm, servers: Vec) -> Self { + Self { + swarm, + interval: tokio::time::interval(Duration::from_secs(5)), + client_state: if servers.is_empty() { + None + } else { + Some(State::Init(servers)) + }, + candidate_addrs: HashSet::new(), + } + } + + /// Run the event loop until we have gained enough confidence in the NAT status. + async fn run(mut self) -> NatStatus { + loop { + // Process both events and check the state per the interval. + tokio::select! { + event = self.swarm.select_next_some() => self.on_event(event), + _ = self.interval.tick() => self.check_state(), + } + + // If we reached `Done` status, return the status. + if let Some(State::Done(status)) = self.client_state { + break status; + } + } + } + + // Called regularly to check the state of the program. + fn check_state(&mut self) { + let state = if let Some(state) = self.client_state.take() { + state + } else { + return; + }; + + match state { + State::Init(servers) => { + self.client_state = Some(State::Dialing); + + for addr in servers { + // `SwarmEvent::Dialing` is only triggered when peer ID is included, so + // we log here too to make sure we log that we're dialing a server. + if let Err(e) = self.swarm.dial(addr.clone()) { + warn!(%addr, ?e, "failed to dial server"); + } else { + info!(%addr, "dialing server"); + } + } + } + State::Dialing => { + let info = self.swarm.network_info(); + if info.num_peers() > 0 { + self.client_state = Some(State::Probing(0)); + } else { + self.client_state = Some(State::Dialing); + } + } + State::Probing(old_confidence) => { + let confidence = self.swarm.behaviour().auto_nat.confidence(); + let status = self.swarm.behaviour().auto_nat.nat_status(); + + if confidence == CONFIDENCE_MAX { + debug!(confidence, ?status, "probing complete"); + self.client_state = Some(State::Done(status)); + } else { + if confidence != old_confidence { + info!( + ?status, + %confidence, + "confidence in NAT status {}", + if confidence > old_confidence { + "increased" + } else { + "decreased" + } + ); + } + self.client_state = Some(State::Probing(confidence)); + } + } + State::Done(status) => { + // Nothing more to do + self.client_state = Some(State::Done(status)); + } + } + } + + fn on_event(&mut self, event: SwarmEvent) { + match event { + // We delegate the specific behaviour events to their respective methods. + SwarmEvent::Behaviour(event) => match event { + BehaviourEvent::Identify(event) => self.on_event_identify(event), + BehaviourEvent::AutoNat(event) => self.on_event_autonat(event), + }, + SwarmEvent::NewListenAddr { address, .. } => { + debug!(%address, "Listening on new address"); + } + SwarmEvent::NewExternalAddrCandidate { address } => { + // Only report on newly discovered addresses. + if self.candidate_addrs.insert(address.clone()) { + info!(%address, "New external address candidate"); + } + } + SwarmEvent::ExternalAddrConfirmed { address } => { + info!(%address, "External address confirmed"); + self.check_state(); + } + SwarmEvent::ExternalAddrExpired { address } => { + warn!(%address, "External address expired") + } + SwarmEvent::ConnectionEstablished { + peer_id, + num_established, + connection_id, + .. + } => { + debug!( + conn_id=%connection_id, + %peer_id, + count=num_established, + "Connected to peer{}", + if num_established.get() > 1 { + " (again)" + } else { + "" + } + ); + self.check_state(); + } + SwarmEvent::ConnectionClosed { + peer_id, + num_established, + connection_id, + cause, + .. + } => { + debug!(conn_id=%connection_id, %peer_id, count=num_established, ?cause, "Closed connection to peer"); + } + SwarmEvent::IncomingConnection { + local_addr, + send_back_addr, + connection_id, + .. + } => { + debug!(conn_id=%connection_id, %local_addr, %send_back_addr, "Incoming connection"); + } + SwarmEvent::IncomingConnectionError { + connection_id, + local_addr, + send_back_addr, + error, + .. + } => { + warn!(conn_id=%connection_id, %local_addr, %send_back_addr, ?error, "Incoming connection error"); + } + SwarmEvent::OutgoingConnectionError { + peer_id, + connection_id, + error, + .. + } => { + warn!(conn_id=%connection_id, ?peer_id, ?error, "Connection error"); + } + SwarmEvent::ExpiredListenAddr { .. } => { /* ignore */ } + SwarmEvent::ListenerClosed { .. } => { /* ignore */ } + SwarmEvent::ListenerError { .. } => { /* ignore */ } + SwarmEvent::Dialing { + peer_id, + connection_id, + } => { + info!(?peer_id, %connection_id, "Dialing peer"); + } + _ => warn!("Unknown SwarmEvent"), + } + } +} + +/// Parse strings like `1.2.3.4:1234` and `/ip4/1.2.3.4/tcp/1234` into a multiaddr. +fn parse_peer_addr(addr: &str) -> Result { + // Parse valid IPv4 socket address, e.g. `1.2.3.4:1234`. + if let Ok(addr) = addr.parse::() { + let multiaddr = Multiaddr::from(*addr.ip()).with(Protocol::Tcp(addr.port())); + + return Ok(multiaddr); + } + + // Parse any valid multiaddr string + if let Ok(addr) = addr.parse::() { + return Ok(addr); + } + + Err("could not parse address") +} From 121d173abdf379bd38e9b2f2e10fe59e0be511bd Mon Sep 17 00:00:00 2001 From: Benno Zeeman Date: Tue, 14 May 2024 18:43:42 +0200 Subject: [PATCH 02/10] fix(autonat): update sn_networking dep --- sn_nat_detection/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sn_nat_detection/Cargo.toml b/sn_nat_detection/Cargo.toml index b55ac6a5c6..cdd5e4f324 100644 --- a/sn_nat_detection/Cargo.toml +++ b/sn_nat_detection/Cargo.toml @@ -21,7 +21,7 @@ libp2p = { version = "0.53", features = ["tokio", "tcp", "noise", "yamux", "auto tracing = { version = "~0.1.26" } tracing-subscriber = { version = "0.3", features = ["env-filter"] } -sn_networking = { path = "../sn_networking", version = "0.14.4" } +sn_networking = { path = "../sn_networking", version = "0.15.2" } clap-verbosity-flag = "2.2.0" tracing-log = "0.2.0" From 1d1c5dedd52ebe0661ffe5f888dcc2f9fdb2e4ea Mon Sep 17 00:00:00 2001 From: Benno Zeeman Date: Tue, 14 May 2024 18:46:01 +0200 Subject: [PATCH 03/10] feat(nat): ignore new Swarm event --- sn_nat_detection/src/main.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sn_nat_detection/src/main.rs b/sn_nat_detection/src/main.rs index 9d0885c3bf..845eab4d3a 100644 --- a/sn_nat_detection/src/main.rs +++ b/sn_nat_detection/src/main.rs @@ -324,7 +324,8 @@ impl EventLoop { } => { info!(?peer_id, %connection_id, "Dialing peer"); } - _ => warn!("Unknown SwarmEvent"), + SwarmEvent::NewExternalAddrOfPeer { .. } => { /* ignore */ } + event => warn!(?event, "Unknown SwarmEvent"), } } } From 5d172ffba46d1f2467ed17e488767cb7b9a0a4b8 Mon Sep 17 00:00:00 2001 From: Benno Zeeman Date: Tue, 14 May 2024 18:52:59 +0200 Subject: [PATCH 04/10] docs(nat): add program docs for clap --help --- sn_nat_detection/src/main.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sn_nat_detection/src/main.rs b/sn_nat_detection/src/main.rs index 845eab4d3a..4c37d60466 100644 --- a/sn_nat_detection/src/main.rs +++ b/sn_nat_detection/src/main.rs @@ -43,6 +43,8 @@ mod behaviour; const CONFIDENCE_MAX: usize = 2; const RETRY_INTERVAL: Duration = Duration::from_secs(10); +/// A tool to detect NAT status of the machine. It can be run in server mode or client mode. +/// The program will exit with an error code if NAT status is determined to be private. #[derive(Debug, Parser)] #[clap(name = "libp2p autonat")] struct Opt { From a006a1ab544777726f48d8622d1a02beb488c984 Mon Sep 17 00:00:00 2001 From: Benno Zeeman Date: Wed, 15 May 2024 08:57:47 +0200 Subject: [PATCH 05/10] chore(nat): remove libp2p example license etc --- sn_nat_detection/README.md | 41 ------------------------------------ sn_nat_detection/src/main.rs | 26 ++++++----------------- 2 files changed, 6 insertions(+), 61 deletions(-) delete mode 100644 sn_nat_detection/README.md diff --git a/sn_nat_detection/README.md b/sn_nat_detection/README.md deleted file mode 100644 index 60827bbcf3..0000000000 --- a/sn_nat_detection/README.md +++ /dev/null @@ -1,41 +0,0 @@ -## Description - -This example consists of a client and a server, which demonstrate the usage of the AutoNAT and identify protocols in **libp2p**. - -## Usage - -### Client - -The client-side part of the example showcases the combination of the AutoNAT and identify protocols. -The identify protocol allows the local peer to determine its external addresses, which are then included in AutoNAT dial-back requests sent to the server. - -To run the client example, follow these steps: - -1. Start the server by following the instructions provided in the `examples/server` directory. - -2. Open a new terminal. - -3. Run the following command in the terminal: - ```sh - cargo run --bin autonat_client -- --server-address --server-peer-id --listen-port - ``` - Note: The `--listen-port` parameter is optional and allows you to specify a fixed port at which the local client should listen. - -### Server - -The server-side example demonstrates a basic AutoNAT server that supports the autonat and identify protocols. - -To start the server, follow these steps: - -1. Open a terminal. - -2. Run the following command: - ```sh - cargo run --bin autonat_server -- --listen-port - ``` - Note: The `--listen-port` parameter is optional and allows you to set a fixed port at which the local peer should listen. - -## Conclusion - -By combining the AutoNAT and identify protocols, the example showcases the establishment of direct connections between peers and the exchange of external address information. -Users can explore the provided client and server code to gain insights into the implementation details and functionality of **libp2p**. diff --git a/sn_nat_detection/src/main.rs b/sn_nat_detection/src/main.rs index 4c37d60466..1be1587691 100644 --- a/sn_nat_detection/src/main.rs +++ b/sn_nat_detection/src/main.rs @@ -1,24 +1,10 @@ -// Copyright 2021 Protocol Labs. +// Copyright 2024 MaidSafe.net limited. // -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the "Software"), -// to deal in the Software without restriction, including without limitation -// the rights to use, copy, modify, merge, publish, distribute, sublicense, -// and/or sell copies of the Software, and to permit persons to whom the -// Software is furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -// DEALINGS IN THE SOFTWARE. - -#![doc = include_str!("../../README.md")] +// This SAFE Network Software is licensed to you under The General Public License (GPL), version 3. +// Unless required by applicable law or agreed to in writing, the SAFE Network Software distributed +// under the GPL Licence is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. Please review the Licences for the specific language governing +// permissions and limitations relating to use of the SAFE Network Software. use clap::Parser; use futures::StreamExt; From 3c25a338f7c152fd6f03fcb068385b87548a3544 Mon Sep 17 00:00:00 2001 From: Benno Zeeman Date: Wed, 15 May 2024 09:08:50 +0200 Subject: [PATCH 06/10] refactor(nat): renames and cleanup --- sn_nat_detection/Cargo.toml | 9 ++++----- .../src/behaviour/{auto_nat.rs => autonat.rs} | 2 +- sn_nat_detection/src/behaviour/identify.rs | 2 +- sn_nat_detection/src/behaviour/mod.rs | 12 ++++++------ sn_nat_detection/src/main.rs | 6 +++--- 5 files changed, 15 insertions(+), 16 deletions(-) rename sn_nat_detection/src/behaviour/{auto_nat.rs => autonat.rs} (97%) diff --git a/sn_nat_detection/Cargo.toml b/sn_nat_detection/Cargo.toml index cdd5e4f324..974b04b69c 100644 --- a/sn_nat_detection/Cargo.toml +++ b/sn_nat_detection/Cargo.toml @@ -14,16 +14,15 @@ name = "detect-nat" path = "src/main.rs" [dependencies] -tokio = { version = "1.32.0", features = ["full"] } clap = { version = "4.5.4", features = ["derive"] } +clap-verbosity-flag = "2.2.0" futures = "~0.3.13" libp2p = { version = "0.53", features = ["tokio", "tcp", "noise", "yamux", "autonat", "identify", "macros"] } -tracing = { version = "~0.1.26" } -tracing-subscriber = { version = "0.3", features = ["env-filter"] } - sn_networking = { path = "../sn_networking", version = "0.15.2" } -clap-verbosity-flag = "2.2.0" +tokio = { version = "1.32.0", features = ["full"] } +tracing = { version = "~0.1.26" } tracing-log = "0.2.0" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } [lints] workspace = true diff --git a/sn_nat_detection/src/behaviour/auto_nat.rs b/sn_nat_detection/src/behaviour/autonat.rs similarity index 97% rename from sn_nat_detection/src/behaviour/auto_nat.rs rename to sn_nat_detection/src/behaviour/autonat.rs index 2b30b2d664..b978230935 100644 --- a/sn_nat_detection/src/behaviour/auto_nat.rs +++ b/sn_nat_detection/src/behaviour/autonat.rs @@ -65,7 +65,7 @@ impl EventLoop { info!( ?new, ?old, - confidence = self.swarm.behaviour().auto_nat.confidence(), + confidence = self.swarm.behaviour().autonat.confidence(), "AutoNAT status changed" ); self.check_state(); diff --git a/sn_nat_detection/src/behaviour/identify.rs b/sn_nat_detection/src/behaviour/identify.rs index 3562ac69bc..b2e73abd8e 100644 --- a/sn_nat_detection/src/behaviour/identify.rs +++ b/sn_nat_detection/src/behaviour/identify.rs @@ -43,7 +43,7 @@ impl EventLoop { for addr in info.listen_addrs.into_iter().filter(multiaddr_is_global) { self.swarm .behaviour_mut() - .auto_nat + .autonat .add_server(peer_id, Some(addr)); } } diff --git a/sn_nat_detection/src/behaviour/mod.rs b/sn_nat_detection/src/behaviour/mod.rs index 6def7fdd84..ed37406d24 100644 --- a/sn_nat_detection/src/behaviour/mod.rs +++ b/sn_nat_detection/src/behaviour/mod.rs @@ -1,10 +1,10 @@ +use libp2p::identity; use libp2p::swarm::NetworkBehaviour; -use libp2p::{autonat, identity}; use std::time::Duration; use crate::CONFIDENCE_MAX; -mod auto_nat; +mod autonat; mod identify; pub(crate) const PROTOCOL_VERSION: &str = "/sn_nat_detection/0.1.0"; @@ -12,7 +12,7 @@ pub(crate) const PROTOCOL_VERSION: &str = "/sn_nat_detection/0.1.0"; #[derive(NetworkBehaviour)] pub(crate) struct Behaviour { pub identify: libp2p::identify::Behaviour, - pub auto_nat: autonat::Behaviour, + pub autonat: libp2p::autonat::Behaviour, } impl Behaviour { @@ -27,10 +27,10 @@ impl Behaviour { // Exchange information every 5 minutes. .with_interval(Duration::from_secs(5 * 60)), ), - auto_nat: autonat::Behaviour::new( + autonat: libp2p::autonat::Behaviour::new( local_public_key.to_peer_id(), if client_mode { - autonat::Config { + libp2p::autonat::Config { // Use dialed peers for probing. use_connected: true, // Start probing 3 seconds after swarm init. This gives us time to connect to the dialed server. @@ -44,7 +44,7 @@ impl Behaviour { ..Default::default() } } else { - autonat::Config { + libp2p::autonat::Config { // Do not ask for dial-backs, essentially putting us in server mode. use_connected: false, // Never start probing, as we are a server. diff --git a/sn_nat_detection/src/main.rs b/sn_nat_detection/src/main.rs index 1be1587691..cf1bf212d5 100644 --- a/sn_nat_detection/src/main.rs +++ b/sn_nat_detection/src/main.rs @@ -198,8 +198,8 @@ impl EventLoop { } } State::Probing(old_confidence) => { - let confidence = self.swarm.behaviour().auto_nat.confidence(); - let status = self.swarm.behaviour().auto_nat.nat_status(); + let confidence = self.swarm.behaviour().autonat.confidence(); + let status = self.swarm.behaviour().autonat.nat_status(); if confidence == CONFIDENCE_MAX { debug!(confidence, ?status, "probing complete"); @@ -232,7 +232,7 @@ impl EventLoop { // We delegate the specific behaviour events to their respective methods. SwarmEvent::Behaviour(event) => match event { BehaviourEvent::Identify(event) => self.on_event_identify(event), - BehaviourEvent::AutoNat(event) => self.on_event_autonat(event), + BehaviourEvent::Autonat(event) => self.on_event_autonat(event), }, SwarmEvent::NewListenAddr { address, .. } => { debug!(%address, "Listening on new address"); From d56cc4a548eb78bbc0706ae7c84efeabeb89d67b Mon Sep 17 00:00:00 2001 From: Benno Zeeman Date: Wed, 15 May 2024 11:48:16 +0200 Subject: [PATCH 07/10] feat(nat): add UPnP behaviour --- sn_nat_detection/Cargo.toml | 2 +- sn_nat_detection/src/behaviour/mod.rs | 22 +++++++++++++--------- sn_nat_detection/src/behaviour/upnp.rs | 23 +++++++++++++++++++++++ sn_nat_detection/src/main.rs | 3 ++- 4 files changed, 39 insertions(+), 11 deletions(-) create mode 100644 sn_nat_detection/src/behaviour/upnp.rs diff --git a/sn_nat_detection/Cargo.toml b/sn_nat_detection/Cargo.toml index 974b04b69c..54ae2de56c 100644 --- a/sn_nat_detection/Cargo.toml +++ b/sn_nat_detection/Cargo.toml @@ -17,7 +17,7 @@ path = "src/main.rs" clap = { version = "4.5.4", features = ["derive"] } clap-verbosity-flag = "2.2.0" futures = "~0.3.13" -libp2p = { version = "0.53", features = ["tokio", "tcp", "noise", "yamux", "autonat", "identify", "macros"] } +libp2p = { version = "0.53", features = ["tokio", "tcp", "noise", "yamux", "autonat", "identify", "macros", "upnp"] } sn_networking = { path = "../sn_networking", version = "0.15.2" } tokio = { version = "1.32.0", features = ["full"] } tracing = { version = "~0.1.26" } diff --git a/sn_nat_detection/src/behaviour/mod.rs b/sn_nat_detection/src/behaviour/mod.rs index ed37406d24..83d5bb0e6f 100644 --- a/sn_nat_detection/src/behaviour/mod.rs +++ b/sn_nat_detection/src/behaviour/mod.rs @@ -1,4 +1,5 @@ use libp2p::identity; +use libp2p::swarm::behaviour::toggle::Toggle; use libp2p::swarm::NetworkBehaviour; use std::time::Duration; @@ -6,27 +7,21 @@ use crate::CONFIDENCE_MAX; mod autonat; mod identify; +mod upnp; pub(crate) const PROTOCOL_VERSION: &str = "/sn_nat_detection/0.1.0"; #[derive(NetworkBehaviour)] pub(crate) struct Behaviour { - pub identify: libp2p::identify::Behaviour, pub autonat: libp2p::autonat::Behaviour, + pub identify: libp2p::identify::Behaviour, + pub upnp: Toggle, } impl Behaviour { pub(crate) fn new(local_public_key: identity::PublicKey, client_mode: bool) -> Self { let far_future = Duration::MAX / 10; // `MAX` on itself causes overflows. This is a workaround. Self { - identify: libp2p::identify::Behaviour::new( - libp2p::identify::Config::new( - PROTOCOL_VERSION.to_string(), - local_public_key.clone(), - ) - // Exchange information every 5 minutes. - .with_interval(Duration::from_secs(5 * 60)), - ), autonat: libp2p::autonat::Behaviour::new( local_public_key.to_peer_id(), if client_mode { @@ -53,6 +48,15 @@ impl Behaviour { } }, ), + identify: libp2p::identify::Behaviour::new( + libp2p::identify::Config::new( + PROTOCOL_VERSION.to_string(), + local_public_key.clone(), + ) + // Exchange information every 5 minutes. + .with_interval(Duration::from_secs(5 * 60)), + ), + upnp: Toggle::from(Some(libp2p::upnp::tokio::Behaviour::default())), } } } diff --git a/sn_nat_detection/src/behaviour/upnp.rs b/sn_nat_detection/src/behaviour/upnp.rs new file mode 100644 index 0000000000..7b21d6c4ce --- /dev/null +++ b/sn_nat_detection/src/behaviour/upnp.rs @@ -0,0 +1,23 @@ +use libp2p::upnp; +use tracing::{debug, info, warn}; + +use crate::EventLoop; + +impl EventLoop { + pub(crate) fn on_event_upnp(&mut self, event: upnp::Event) { + match event { + upnp::Event::NewExternalAddr(addr) => { + info!(%addr, "UPnP: New external address detected"); + } + upnp::Event::ExpiredExternalAddr(addr) => { + debug!(%addr, "UPnP: External address expired"); + } + upnp::Event::GatewayNotFound => { + warn!("UPnP: Gateway not found"); + } + upnp::Event::NonRoutableGateway => { + warn!("UPnP: Gateway is not routable"); + } + } + } +} diff --git a/sn_nat_detection/src/main.rs b/sn_nat_detection/src/main.rs index cf1bf212d5..06a2f10bbc 100644 --- a/sn_nat_detection/src/main.rs +++ b/sn_nat_detection/src/main.rs @@ -233,6 +233,7 @@ impl EventLoop { SwarmEvent::Behaviour(event) => match event { BehaviourEvent::Identify(event) => self.on_event_identify(event), BehaviourEvent::Autonat(event) => self.on_event_autonat(event), + BehaviourEvent::Upnp(event) => self.on_event_upnp(event), }, SwarmEvent::NewListenAddr { address, .. } => { debug!(%address, "Listening on new address"); @@ -310,7 +311,7 @@ impl EventLoop { peer_id, connection_id, } => { - info!(?peer_id, %connection_id, "Dialing peer"); + info!(?peer_id, conn_id=%connection_id, "Dialing peer"); } SwarmEvent::NewExternalAddrOfPeer { .. } => { /* ignore */ } event => warn!(?event, "Unknown SwarmEvent"), From 33bfa402833b76feaaf0bd13ba40ec0f156f5d8b Mon Sep 17 00:00:00 2001 From: Benno Zeeman Date: Wed, 15 May 2024 12:38:01 +0200 Subject: [PATCH 08/10] feat(nat): retry nat detction with UPnP --- sn_nat_detection/src/behaviour/autonat.rs | 4 +- sn_nat_detection/src/behaviour/identify.rs | 4 +- sn_nat_detection/src/behaviour/mod.rs | 17 ++- sn_nat_detection/src/behaviour/upnp.rs | 11 +- sn_nat_detection/src/main.rs | 149 +++++++++++++++------ 5 files changed, 129 insertions(+), 56 deletions(-) diff --git a/sn_nat_detection/src/behaviour/autonat.rs b/sn_nat_detection/src/behaviour/autonat.rs index b978230935..be3e5834a4 100644 --- a/sn_nat_detection/src/behaviour/autonat.rs +++ b/sn_nat_detection/src/behaviour/autonat.rs @@ -1,9 +1,9 @@ use libp2p::autonat; use tracing::{debug, info, warn}; -use crate::EventLoop; +use crate::App; -impl EventLoop { +impl App { pub(crate) fn on_event_autonat(&mut self, event: autonat::Event) { match event { autonat::Event::InboundProbe(event) => match event { diff --git a/sn_nat_detection/src/behaviour/identify.rs b/sn_nat_detection/src/behaviour/identify.rs index b2e73abd8e..e65d4381ca 100644 --- a/sn_nat_detection/src/behaviour/identify.rs +++ b/sn_nat_detection/src/behaviour/identify.rs @@ -2,9 +2,9 @@ use libp2p::{autonat, identify}; use sn_networking::multiaddr_is_global; use tracing::{debug, info, warn}; -use crate::{behaviour::PROTOCOL_VERSION, EventLoop}; +use crate::{behaviour::PROTOCOL_VERSION, App}; -impl EventLoop { +impl App { pub(crate) fn on_event_identify(&mut self, event: identify::Event) { match event { identify::Event::Received { peer_id, info } => { diff --git a/sn_nat_detection/src/behaviour/mod.rs b/sn_nat_detection/src/behaviour/mod.rs index 83d5bb0e6f..4d28d42981 100644 --- a/sn_nat_detection/src/behaviour/mod.rs +++ b/sn_nat_detection/src/behaviour/mod.rs @@ -19,7 +19,11 @@ pub(crate) struct Behaviour { } impl Behaviour { - pub(crate) fn new(local_public_key: identity::PublicKey, client_mode: bool) -> Self { + pub(crate) fn new( + local_public_key: identity::PublicKey, + client_mode: bool, + upnp: bool, + ) -> Self { let far_future = Duration::MAX / 10; // `MAX` on itself causes overflows. This is a workaround. Self { autonat: libp2p::autonat::Behaviour::new( @@ -28,8 +32,13 @@ impl Behaviour { libp2p::autonat::Config { // Use dialed peers for probing. use_connected: true, - // Start probing 3 seconds after swarm init. This gives us time to connect to the dialed server. - boot_delay: Duration::from_secs(3), + // Start probing a few seconds after swarm init. This gives us time to connect to the dialed server. + // With UPnP enabled, give it a bit more time to possibly open up the port. + boot_delay: if upnp { + Duration::from_secs(7) + } else { + Duration::from_secs(3) + }, // Reuse probe server immediately even if it's the only one. throttle_server_period: Duration::ZERO, retry_interval: Duration::from_secs(10), @@ -56,7 +65,7 @@ impl Behaviour { // Exchange information every 5 minutes. .with_interval(Duration::from_secs(5 * 60)), ), - upnp: Toggle::from(Some(libp2p::upnp::tokio::Behaviour::default())), + upnp: upnp.then(libp2p::upnp::tokio::Behaviour::default).into(), } } } diff --git a/sn_nat_detection/src/behaviour/upnp.rs b/sn_nat_detection/src/behaviour/upnp.rs index 7b21d6c4ce..e5f7c8bcbd 100644 --- a/sn_nat_detection/src/behaviour/upnp.rs +++ b/sn_nat_detection/src/behaviour/upnp.rs @@ -1,9 +1,10 @@ use libp2p::upnp; -use tracing::{debug, info, warn}; +use tracing::{debug, info}; +use tracing_log::log::error; -use crate::EventLoop; +use crate::App; -impl EventLoop { +impl App { pub(crate) fn on_event_upnp(&mut self, event: upnp::Event) { match event { upnp::Event::NewExternalAddr(addr) => { @@ -13,10 +14,10 @@ impl EventLoop { debug!(%addr, "UPnP: External address expired"); } upnp::Event::GatewayNotFound => { - warn!("UPnP: Gateway not found"); + error!("UPnP: No gateway not found"); } upnp::Event::NonRoutableGateway => { - warn!("UPnP: Gateway is not routable"); + error!("UPnP: Gateway is not routable"); } } } diff --git a/sn_nat_detection/src/main.rs b/sn_nat_detection/src/main.rs index 06a2f10bbc..c2cefffaae 100644 --- a/sn_nat_detection/src/main.rs +++ b/sn_nat_detection/src/main.rs @@ -49,6 +49,10 @@ struct Opt { #[clap(name = "SERVER", value_name = "multiaddr", value_delimiter = ',', value_parser = parse_peer_addr)] server_addr: Vec, + /// Disable use of UPnP to open a port on the router, before detecting NAT status. + #[clap(long, short, default_value_t = false)] + no_upnp: bool, + #[command(flatten)] verbose: clap_verbosity_flag::Verbosity, } @@ -70,48 +74,39 @@ async fn main() -> Result<(), Box> { registry.with(filter).try_init() }; - // If no servers are provided, we are in server mode. Conversely, with servers - // provided, we are in client mode. - let client_mode = !opt.server_addr.is_empty(); - - let mut swarm = libp2p::SwarmBuilder::with_new_identity() - .with_tokio() - .with_tcp( - tcp::Config::default(), - noise::Config::new, - yamux::Config::default, - )? - .with_behaviour(|key| Behaviour::new(key.public(), client_mode))? - // Make it so that we retry just before idling out, to prevent quickly disconnecting/connecting - // to the same server. - .with_swarm_config(|c| { - c.with_idle_connection_timeout(RETRY_INTERVAL + Duration::from_secs(2)) - }) - .build(); - - swarm.listen_on( - Multiaddr::empty() - .with(Protocol::Ip4(Ipv4Addr::UNSPECIFIED)) - .with(Protocol::Tcp(opt.port)), - )?; - - info!( - peer_id=%swarm.local_peer_id(), - "starting in {} mode", - if client_mode { "client" } else { "server" } - ); - - let event_loop = EventLoop::new(swarm, opt.server_addr); - // The main loop will exit once it has gained enough confidence in the NAT status. - let status = event_loop.run().await; - - match status { - NatStatus::Public(addr) => { - info!(%addr, "NAT is public"); - Ok(()) + let mut builder = AppBuilder::new() + .servers(opt.server_addr) + .upnp(false) + .port(opt.port); + + // Run the program twice, to first detect NAT status without UPnP, + // and then with UPnP enabled. (Unless `--no-upnp` was given.) + let mut running_with_upnp = false; + loop { + let status = builder + .build()? + // The main loop will exit once it has gained enough confidence in the NAT status. + .run() + .await; + + match status { + NatStatus::Public(addr) => { + info!(%addr, "NAT is public{}", if running_with_upnp { " (with UPnP)" } else { "" }); + break Ok(()); + } + NatStatus::Private => { + // Unless `--no-upnp` is set, rerun the program with UPnP enabled. + if !opt.no_upnp && !running_with_upnp { + warn!("NAT is private, rerunning program with UPnP enabled in 2 seconds..."); + tokio::time::sleep(Duration::from_secs(2)).await; + builder = builder.upnp(true); + running_with_upnp = true; + } else { + break Err("NAT is private".into()); + } + } + NatStatus::Unknown => break Err("NAT is unknown".into()), } - NatStatus::Private => Err("NAT is private".into()), - NatStatus::Unknown => Err("NAT is unknown".into()), } } @@ -127,7 +122,7 @@ enum State { Done(NatStatus), } -struct EventLoop { +struct App { swarm: libp2p::Swarm, // Interval with which to check the state of the program. (State is also checked on events.) interval: tokio::time::Interval, @@ -137,7 +132,7 @@ struct EventLoop { candidate_addrs: HashSet, } -impl EventLoop { +impl App { fn new(swarm: libp2p::Swarm, servers: Vec) -> Self { Self { swarm, @@ -335,3 +330,71 @@ fn parse_peer_addr(addr: &str) -> Result { Err("could not parse address") } + +struct AppBuilder { + port: u16, + servers: Vec, + upnp: bool, +} + +impl AppBuilder { + fn new() -> Self { + Self { + port: 0, + upnp: false, + servers: vec![], + } + } + + fn port(mut self, port: u16) -> Self { + self.port = port; + self + } + + fn upnp(mut self, upnp: bool) -> Self { + self.upnp = upnp; + self + } + + fn servers(mut self, servers: Vec) -> Self { + self.servers = servers; + self + } + + fn build(&self) -> Result> { + // If no servers are provided, we are in server mode. Conversely, with servers + // provided, we are in client mode. + let client_mode = !self.servers.is_empty(); + + let mut swarm = libp2p::SwarmBuilder::with_new_identity() + .with_tokio() + .with_tcp( + tcp::Config::default(), + noise::Config::new, + yamux::Config::default, + )? + .with_behaviour(|key| Behaviour::new(key.public(), client_mode, self.upnp))? + // Make it so that we retry just before idling out, to prevent quickly disconnecting/connecting + // to the same server. + .with_swarm_config(|c| { + c.with_idle_connection_timeout(RETRY_INTERVAL + Duration::from_secs(2)) + }) + .build(); + + swarm.listen_on( + Multiaddr::empty() + .with(Protocol::Ip4(Ipv4Addr::UNSPECIFIED)) + .with(Protocol::Tcp(self.port)), + )?; + + info!( + peer_id=%swarm.local_peer_id(), + "starting in {} mode", + if client_mode { "client" } else { "server" } + ); + + let app = App::new(swarm, self.servers.clone()); + + Ok(app) + } +} From 5b5aef3cfa68b5b0ce8b41d5c033350fb065201e Mon Sep 17 00:00:00 2001 From: Benno Zeeman Date: Wed, 15 May 2024 17:01:28 +0200 Subject: [PATCH 09/10] chore(nat): small doc and code changes --- sn_nat_detection/Cargo.toml | 2 +- sn_nat_detection/src/behaviour/identify.rs | 1 - sn_nat_detection/src/main.rs | 8 +++++--- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/sn_nat_detection/Cargo.toml b/sn_nat_detection/Cargo.toml index 54ae2de56c..32a135d9c9 100644 --- a/sn_nat_detection/Cargo.toml +++ b/sn_nat_detection/Cargo.toml @@ -1,6 +1,6 @@ [package] authors = ["MaidSafe Developers "] -description = "Safe Network AutoNAT probing" +description = "Safe Network NAT detection tool" edition = "2021" homepage = "https://maidsafe.net" license = "GPL-3.0" diff --git a/sn_nat_detection/src/behaviour/identify.rs b/sn_nat_detection/src/behaviour/identify.rs index e65d4381ca..8489034039 100644 --- a/sn_nat_detection/src/behaviour/identify.rs +++ b/sn_nat_detection/src/behaviour/identify.rs @@ -31,7 +31,6 @@ impl App { { warn!(%peer_id, "Peer does not support AutoNAT. Disconnecting from peer."); let _ = self.swarm.disconnect_peer_id(peer_id); - #[allow(clippy::needless_return)] return; } diff --git a/sn_nat_detection/src/main.rs b/sn_nat_detection/src/main.rs index c2cefffaae..bf48a7e3a5 100644 --- a/sn_nat_detection/src/main.rs +++ b/sn_nat_detection/src/main.rs @@ -32,7 +32,7 @@ const RETRY_INTERVAL: Duration = Duration::from_secs(10); /// A tool to detect NAT status of the machine. It can be run in server mode or client mode. /// The program will exit with an error code if NAT status is determined to be private. #[derive(Debug, Parser)] -#[clap(name = "libp2p autonat")] +#[clap(version, author, about)] struct Opt { /// Port to listen on. /// @@ -40,7 +40,9 @@ struct Opt { #[clap(long, short, default_value_t = 0)] port: u16, - /// Servers to send dial-back requests to, in a 'multiaddr' format. + /// Servers to send dial-back requests to as a client, in a 'multiaddr' format. + /// + /// If no servers are provided, the program will run in server mode. /// /// A multiaddr looks like `/ip4/1.2.3.4/tcp/1200/tcp` where `1.2.3.4` is the IP and `1200` is the port. /// Alternatively, the address can be written as `1.2.3.4:1200`. @@ -196,7 +198,7 @@ impl App { let confidence = self.swarm.behaviour().autonat.confidence(); let status = self.swarm.behaviour().autonat.nat_status(); - if confidence == CONFIDENCE_MAX { + if confidence >= CONFIDENCE_MAX { debug!(confidence, ?status, "probing complete"); self.client_state = Some(State::Done(status)); } else { From 77efb04ba490130eba4908ed05f07279fba264e5 Mon Sep 17 00:00:00 2001 From: Benno Zeeman Date: Wed, 15 May 2024 17:14:09 +0200 Subject: [PATCH 10/10] feat(nat): use color-eyre and uniform error msg --- Cargo.lock | 1 + sn_nat_detection/Cargo.toml | 1 + sn_nat_detection/src/behaviour/upnp.rs | 8 ++++---- sn_nat_detection/src/main.rs | 22 ++++++++++++---------- 4 files changed, 18 insertions(+), 14 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index aff938636b..a610c52f85 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7172,6 +7172,7 @@ version = "0.1.0" dependencies = [ "clap", "clap-verbosity-flag", + "color-eyre", "futures", "libp2p", "sn_networking", diff --git a/sn_nat_detection/Cargo.toml b/sn_nat_detection/Cargo.toml index 32a135d9c9..a8365ccd9e 100644 --- a/sn_nat_detection/Cargo.toml +++ b/sn_nat_detection/Cargo.toml @@ -16,6 +16,7 @@ path = "src/main.rs" [dependencies] clap = { version = "4.5.4", features = ["derive"] } clap-verbosity-flag = "2.2.0" +color-eyre = { version = "0.6", default-features = false } futures = "~0.3.13" libp2p = { version = "0.53", features = ["tokio", "tcp", "noise", "yamux", "autonat", "identify", "macros", "upnp"] } sn_networking = { path = "../sn_networking", version = "0.15.2" } diff --git a/sn_nat_detection/src/behaviour/upnp.rs b/sn_nat_detection/src/behaviour/upnp.rs index e5f7c8bcbd..db39c5cbf9 100644 --- a/sn_nat_detection/src/behaviour/upnp.rs +++ b/sn_nat_detection/src/behaviour/upnp.rs @@ -8,16 +8,16 @@ impl App { pub(crate) fn on_event_upnp(&mut self, event: upnp::Event) { match event { upnp::Event::NewExternalAddr(addr) => { - info!(%addr, "UPnP: New external address detected"); + info!(%addr, "Successfully mapped UPnP port"); } upnp::Event::ExpiredExternalAddr(addr) => { - debug!(%addr, "UPnP: External address expired"); + debug!(%addr, "External UPnP port mapping expired"); } upnp::Event::GatewayNotFound => { - error!("UPnP: No gateway not found"); + error!("No UPnP gateway not found"); } upnp::Event::NonRoutableGateway => { - error!("UPnP: Gateway is not routable"); + error!("UPnP gateway is not routable"); } } } diff --git a/sn_nat_detection/src/main.rs b/sn_nat_detection/src/main.rs index bf48a7e3a5..dd721f2d21 100644 --- a/sn_nat_detection/src/main.rs +++ b/sn_nat_detection/src/main.rs @@ -7,13 +7,13 @@ // permissions and limitations relating to use of the SAFE Network Software. use clap::Parser; +use color_eyre::eyre::{eyre, Result}; use futures::StreamExt; use libp2p::autonat::NatStatus; use libp2p::core::{multiaddr::Protocol, Multiaddr}; use libp2p::swarm::SwarmEvent; use libp2p::{noise, tcp, yamux}; use std::collections::HashSet; -use std::error::Error; use std::net::Ipv4Addr; use std::time::Duration; use tracing::{debug, info, warn}; @@ -60,7 +60,9 @@ struct Opt { } #[tokio::main] -async fn main() -> Result<(), Box> { +async fn main() -> Result<()> { + color_eyre::install()?; + // Process command line arguments. let opt = Opt::parse(); @@ -104,10 +106,10 @@ async fn main() -> Result<(), Box> { builder = builder.upnp(true); running_with_upnp = true; } else { - break Err("NAT is private".into()); + break Err(eyre!("NAT is private")); } } - NatStatus::Unknown => break Err("NAT is unknown".into()), + NatStatus::Unknown => break Err(eyre!("NAT is unknown")), } } } @@ -180,9 +182,9 @@ impl App { // `SwarmEvent::Dialing` is only triggered when peer ID is included, so // we log here too to make sure we log that we're dialing a server. if let Err(e) = self.swarm.dial(addr.clone()) { - warn!(%addr, ?e, "failed to dial server"); + warn!(%addr, ?e, "Failed to dial server"); } else { - info!(%addr, "dialing server"); + info!(%addr, "Dialing server"); } } } @@ -206,7 +208,7 @@ impl App { info!( ?status, %confidence, - "confidence in NAT status {}", + "Confidence in NAT status {}", if confidence > old_confidence { "increased" } else { @@ -330,7 +332,7 @@ fn parse_peer_addr(addr: &str) -> Result { return Ok(addr); } - Err("could not parse address") + Err("Could not parse address") } struct AppBuilder { @@ -363,7 +365,7 @@ impl AppBuilder { self } - fn build(&self) -> Result> { + fn build(&self) -> Result { // If no servers are provided, we are in server mode. Conversely, with servers // provided, we are in client mode. let client_mode = !self.servers.is_empty(); @@ -391,7 +393,7 @@ impl AppBuilder { info!( peer_id=%swarm.local_peer_id(), - "starting in {} mode", + "Starting in {} mode", if client_mode { "client" } else { "server" } );