diff --git a/Cargo.lock b/Cargo.lock index d8fe496de..ef078bab0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -173,6 +173,18 @@ dependencies = [ "critical-section", ] +[[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" @@ -1933,6 +1945,23 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb56e1aa765b4b4f3aadfab769793b7087bb03a4ea4920644a6d238e2df5b9ed" +[[package]] +name = "igd" +version = "0.12.0" +source = "git+https://github.com/stevefan1999-personal/rust-igd#c2d1f83eb1612a462962453cb0703bc93258b173" +dependencies = [ + "attohttpc", + "bytes", + "futures", + "http", + "hyper", + "log", + "rand 0.8.5", + "tokio", + "url", + "xmltree", +] + [[package]] name = "impl-codec" version = "0.6.0" @@ -3579,6 +3608,18 @@ dependencies = [ "reth-primitives", ] +[[package]] +name = "reth-net-nat" +version = "0.1.0" +dependencies = [ + "igd", + "pin-project-lite", + "public-ip", + "reth-tracing", + "tokio", + "tracing", +] + [[package]] name = "reth-network" version = "0.1.0" @@ -5251,6 +5292,12 @@ dependencies = [ "webpki", ] +[[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" @@ -5418,6 +5465,21 @@ dependencies = [ "tap", ] +[[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 = "zeroize" version = "1.5.7" diff --git a/Cargo.toml b/Cargo.toml index e16e4de02..10869b4f5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,7 @@ members = [ "crates/net/ecies", "crates/net/eth-wire", "crates/net/discv4", + "crates/net/nat", "crates/net/network", "crates/net/ipc", "crates/net/rpc", diff --git a/crates/net/nat/Cargo.toml b/crates/net/nat/Cargo.toml new file mode 100644 index 000000000..9ddec0ffa --- /dev/null +++ b/crates/net/nat/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "reth-net-nat" +version = "0.1.0" +edition = "2021" +license = "Apache-2.0" +repository = "https://github.com/paradigmxyz/reth" +description = """ +Helpers for working around NAT +""" + +[dependencies] + +# nat +public-ip = "0.2" +## fork of rust-igd with ipv6 support: https://github.com/sbstp/rust-igd/issues/47 +igd = { git = "https://github.com/stevefan1999-personal/rust-igd", features = [ + "aio", + "tokio1", +] } + +# misc +tracing = "0.1" +pin-project-lite = "0.2.9" + +[dev-dependencies] +reth-tracing = { path = "../../tracing" } +tokio = { version = "1", features = ["macros"] } diff --git a/crates/net/nat/src/lib.rs b/crates/net/nat/src/lib.rs new file mode 100644 index 000000000..206de577d --- /dev/null +++ b/crates/net/nat/src/lib.rs @@ -0,0 +1,137 @@ +#![warn(missing_docs, unused_crate_dependencies)] +#![deny(unused_must_use, rust_2018_idioms)] +#![doc(test( + no_crate_inject, + attr(deny(warnings, rust_2018_idioms), allow(dead_code, unused_variables)) +))] + +//! Helpers for resolving the external IP. + +use igd::aio::search_gateway; +use pin_project_lite::pin_project; +use std::{ + future::Future, + net::IpAddr, + pin::Pin, + task::{ready, Context, Poll}, +}; +use tracing::warn; + +/// All builtin resolvers. +#[derive(Debug, Clone, Copy, Eq, PartialEq, Default, Hash)] +pub enum NatResolver { + /// Resolve with any available resolver. + #[default] + Any, + /// Resolve via Upnp + Upnp, + /// Resolve external IP via [public_ip::Resolver] + ExternalIp, +} + +// === impl NatResolver === + +impl NatResolver { + /// Attempts to produce an IP address (best effort). + pub async fn external_addr(self) -> Option { + external_addr_with(self).await + } +} + +/// Attempts to produce an IP address with all builtin resolvers (best effort). +pub async fn external_ip() -> Option { + external_addr_with(NatResolver::Any).await +} + +/// Given a [`NatResolver`] attempts to produce an IP address (best effort). +pub async fn external_addr_with(resolver: NatResolver) -> Option { + match resolver { + NatResolver::Any => { + ResolveAny { + upnp: Some(Box::pin(resolve_external_ip_upnp())), + external: Some(Box::pin(resolve_external_ip())), + } + .await + } + NatResolver::Upnp => resolve_external_ip_upnp().await, + NatResolver::ExternalIp => resolve_external_ip().await, + } +} + +type ResolveFut = Pin>>>; + +pin_project! { + /// A future that resolves the first ip via all configured resolvers + struct ResolveAny { + #[pin] + upnp: Option, + #[pin] + external: Option, + } +} + +impl Future for ResolveAny { + type Output = Option; + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let mut this = self.as_mut().project(); + + if let Some(upnp) = this.upnp.as_mut().as_pin_mut() { + // if upnp is configured we prefer it over http and dns resolvers + let ip = ready!(upnp.poll(cx)); + this.upnp.set(None); + if ip.is_some() { + return Poll::Ready(ip) + } + } + + if let Some(upnp) = this.external.as_mut().as_pin_mut() { + if let Poll::Ready(ip) = upnp.poll(cx) { + this.external.set(None); + if ip.is_some() { + return Poll::Ready(ip) + } + } + } + + if this.upnp.is_none() && this.external.is_none() { + return Poll::Ready(None) + } + + Poll::Pending + } +} + +async fn resolve_external_ip_upnp() -> Option { + search_gateway(Default::default()) + .await + .map_err(|err| { + warn!(target: "net::nat", ?err, "failed to find upnp gateway"); + err + }) + .ok()? + .get_external_ip() + .await + .map_err(|err| { + warn!(target: "net::nat", ?err, "failed to resolve external ip via upnp gateway"); + err + }) + .ok() +} + +async fn resolve_external_ip() -> Option { + public_ip::addr().await +} + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + #[ignore] + async fn get_external_ip() { + reth_tracing::init_tracing(); + let ip = external_ip().await; + dbg!(ip); + } +}