diff --git a/Cargo.lock b/Cargo.lock index fcdef344fc..4d78fb5722 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -150,6 +150,18 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "attohttpc" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb8867f378f33f78a811a8eb9bf108ad99430d7aad43315dd9319c827ef6247" +dependencies = [ + "http", + "log", + "url", + "wildmatch", +] + [[package]] name = "atty" version = "0.2.14" @@ -1267,6 +1279,19 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "igd" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c4e7ee8b51e541486d7040883fe1f00e2a9954bcc24fd155b7e4f03ed4b93dd" +dependencies = [ + "attohttpc", + "log", + "rand", + "url", + "xmltree", +] + [[package]] name = "indexmap" version = "1.8.1" @@ -2803,6 +2828,7 @@ dependencies = [ "clap 3.1.15", "colored", "crossterm", + "igd", "num_cpus", "rand", "rayon", @@ -3867,6 +3893,12 @@ dependencies = [ "safe_arch", ] +[[package]] +name = "wildmatch" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f44b95f62d34113cf558c93511ac93027e03e9c29a60dd0fd70e6e025c7270a" + [[package]] name = "winapi" version = "0.3.9" @@ -3950,6 +3982,21 @@ dependencies = [ "winapi", ] +[[package]] +name = "xml-rs" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2d7d3948613f75c98fd9328cfdcc45acc4d360655289d0a7d4ec931392200a3" + +[[package]] +name = "xmltree" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7d8a75eaf6557bb84a65ace8609883db44a29951042ada9b393151532e41fcb" +dependencies = [ + "xml-rs", +] + [[package]] name = "zip" version = "0.6.2" diff --git a/Cargo.toml b/Cargo.toml index 005ee60f5b..19d3f4260e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,6 +49,9 @@ version = "2.0" version = "0.23" optional = true +[dependencies.igd] +version = "0.12" + [dependencies.num_cpus] version = "1" diff --git a/snarkos/node.rs b/snarkos/node.rs index 5bb434f1fd..ea38ed5443 100644 --- a/snarkos/node.rs +++ b/snarkos/node.rs @@ -64,6 +64,9 @@ pub struct Node { /// Specify the IP address and port for the node server. #[clap(parse(try_from_str), default_value = "0.0.0.0:4132", long = "node")] pub node: SocketAddr, + /// If the flag is set, the node will attempt to use UPnP to open its listening port. + #[structopt(long = "upnp")] + pub upnp: bool, /// Specify the IP address and port for the RPC server. #[clap(parse(try_from_str), default_value = "0.0.0.0:3032", long = "rpc")] pub rpc: SocketAddr, diff --git a/snarkos/server.rs b/snarkos/server.rs index 64096da70c..1fc48228dd 100644 --- a/snarkos/server.rs +++ b/snarkos/server.rs @@ -38,6 +38,7 @@ use snarkos_metrics as metrics; use tokio::sync::RwLock; use anyhow::Result; +use igd::{AddPortError, PortMappingProtocol}; use std::{net::SocketAddr, sync::Arc, time::Duration}; use tokio::{net::TcpListener, sync::oneshot, task}; @@ -64,6 +65,29 @@ impl Server { /// #[inline] pub async fn initialize(node: &Node, address: Option>, pool_ip: Option) -> Result { + // Use UPnP to detect the gateway, if the upnp option is enabled. + if node.upnp { + let gateway_search_opts = igd::SearchOptions { + timeout: Some(Duration::from_secs(1)), + ..Default::default() + }; + let gateway = igd::search_gateway(gateway_search_opts) + .map_err(|e| warn!("Can't obtain the node's gateway details: {}; perhaps it's not UPnP-enabled?", e)) + .ok(); + + if let Some(ref gateway) = gateway { + if let SocketAddr::V4(internal_addr) = node.node { + let external_port = internal_addr.port(); + + match gateway.add_port(PortMappingProtocol::TCP, external_port, internal_addr, 0, "snarkOS") { + Err(AddPortError::PortInUse) => debug!("Port {} is already forwarded", external_port), + Err(e) => error!("Can't map external port {} to address {}: {}", external_port, internal_addr, e), + Ok(_) => info!("Enabled port forwarding via UPnP"), + } + } + } + } + // Initialize a new TCP listener at the given IP. let (local_ip, listener) = match TcpListener::bind(node.node).await { Ok(listener) => (listener.local_addr().expect("Failed to fetch the local IP"), listener),