diff --git a/crates/cli/commands/src/p2p/mod.rs b/crates/cli/commands/src/p2p/mod.rs index d255f5fd77..31fa408db6 100644 --- a/crates/cli/commands/src/p2p/mod.rs +++ b/crates/cli/commands/src/p2p/mod.rs @@ -189,7 +189,7 @@ impl DownloadArgs { let net = NetworkConfigBuilder::::new(p2p_secret_key) .peer_config(config.peers_config_with_basic_nodes_from_file(None)) - .external_ip_resolver(self.network.nat) + .external_ip_resolver(self.network.nat.clone()) .network_id(self.network.network_id) .boot_nodes(boot_nodes.clone()) .apply(|builder| { diff --git a/crates/net/discv4/src/config.rs b/crates/net/discv4/src/config.rs index fdc5ba5de5..a6a7211f47 100644 --- a/crates/net/discv4/src/config.rs +++ b/crates/net/discv4/src/config.rs @@ -93,7 +93,7 @@ impl Discv4Config { /// Returns the corresponding [`ResolveNatInterval`], if a [`NatResolver`] and an interval was /// configured pub fn resolve_external_ip_interval(&self) -> Option { - let resolver = self.external_ip_resolver?; + let resolver = self.external_ip_resolver.clone()?; let interval = self.resolve_external_ip_interval?; Some(ResolveNatInterval::interval_at(resolver, tokio::time::Instant::now(), interval)) } @@ -275,10 +275,7 @@ impl Discv4ConfigBuilder { } /// Configures if and how the external IP of the node should be resolved. - pub const fn external_ip_resolver( - &mut self, - external_ip_resolver: Option, - ) -> &mut Self { + pub fn external_ip_resolver(&mut self, external_ip_resolver: Option) -> &mut Self { self.config.external_ip_resolver = external_ip_resolver; self } diff --git a/crates/net/discv4/src/lib.rs b/crates/net/discv4/src/lib.rs index 83106cbbe6..0daad65d5a 100644 --- a/crates/net/discv4/src/lib.rs +++ b/crates/net/discv4/src/lib.rs @@ -625,10 +625,13 @@ impl Discv4Service { self.lookup_interval = tokio::time::interval(duration); } - /// Sets the external Ip to the configured external IP if [`NatResolver::ExternalIp`]. + /// Sets the external Ip to the configured external IP if [`NatResolver::ExternalIp`] or + /// [`NatResolver::ExternalAddr`]. In the case of [`NatResolver::ExternalAddr`], it will return + /// the first IP address found for the domain associated with the discv4 UDP port. fn resolve_external_ip(&mut self) { if let Some(r) = &self.resolve_external_ip_interval && - let Some(external_ip) = r.resolver().as_external_ip() + let Some(external_ip) = + r.resolver().clone().as_external_ip(self.local_node_record.udp_port) { self.set_external_ip_addr(external_ip); } diff --git a/crates/net/nat/src/lib.rs b/crates/net/nat/src/lib.rs index edea0fb644..83b24f2ac5 100644 --- a/crates/net/nat/src/lib.rs +++ b/crates/net/nat/src/lib.rs @@ -19,7 +19,7 @@ pub use net_if::{NetInterfaceError, DEFAULT_NET_IF_NAME}; use std::{ fmt, future::{poll_fn, Future}, - net::{AddrParseError, IpAddr}, + net::{AddrParseError, IpAddr, ToSocketAddrs}, pin::Pin, str::FromStr, task::{Context, Poll}, @@ -38,7 +38,7 @@ const EXTERNAL_IP_APIS: &[&str] = &["https://ipinfo.io/ip", "https://icanhazip.com", "https://ifconfig.me"]; /// All builtin resolvers. -#[derive(Debug, Clone, Copy, Eq, PartialEq, Default, Hash)] +#[derive(Debug, Clone, Eq, PartialEq, Default, Hash)] #[cfg_attr(feature = "serde", derive(SerializeDisplay, DeserializeFromStr))] pub enum NatResolver { /// Resolve with any available resolver. @@ -50,6 +50,14 @@ pub enum NatResolver { PublicIp, /// Use the given [`IpAddr`] ExternalIp(IpAddr), + /// Use the given domain name as the external address to expose to peers. + /// This is behaving essentially the same as [`NatResolver::ExternalIp`], but supports domain + /// names. Domain names are resolved to IP addresses using the OS's resolver. The first IP + /// address found is used. + /// This may be useful in docker bridge networks where containers are usually queried by DNS + /// instead of direct IP addresses. + /// Note: the domain shouldn't include a port number. Only the IP address is resolved. + ExternalAddr(String), /// Resolve external IP via the network interface. NetIf, /// Resolve nothing @@ -62,10 +70,17 @@ impl NatResolver { external_addr_with(self).await } - /// Returns the external ip, if it is [`NatResolver::ExternalIp`] - pub const fn as_external_ip(self) -> Option { + /// Returns the fixed ip, if it is [`NatResolver::ExternalIp`] or [`NatResolver::ExternalAddr`]. + /// + /// In the case of [`NatResolver::ExternalAddr`], it will return the first IP address found for + /// the domain. + pub fn as_external_ip(self, port: u16) -> Option { match self { Self::ExternalIp(ip) => Some(ip), + Self::ExternalAddr(domain) => format!("{domain}:{port}") + .to_socket_addrs() + .ok() + .and_then(|mut addrs| addrs.next().map(|addr| addr.ip())), _ => None, } } @@ -78,6 +93,7 @@ impl fmt::Display for NatResolver { Self::Upnp => f.write_str("upnp"), Self::PublicIp => f.write_str("publicip"), Self::ExternalIp(ip) => write!(f, "extip:{ip}"), + Self::ExternalAddr(domain) => write!(f, "extaddr:{domain}"), Self::NetIf => f.write_str("netif"), Self::None => f.write_str("none"), } @@ -106,12 +122,15 @@ impl FromStr for NatResolver { "publicip" | "public-ip" => Self::PublicIp, "netif" => Self::NetIf, s => { - let Some(ip) = s.strip_prefix("extip:") else { + if let Some(ip) = s.strip_prefix("extip:") { + Self::ExternalIp(ip.parse()?) + } else if let Some(domain) = s.strip_prefix("extaddr:") { + Self::ExternalAddr(domain.to_string()) + } else { return Err(ParseNatResolverError::UnknownVariant(format!( "Unknown Nat Resolver: {s}" - ))) - }; - Self::ExternalIp(ip.parse()?) + ))); + } } }; Ok(r) @@ -180,7 +199,7 @@ impl ResolveNatInterval { /// `None` if the attempt was unsuccessful. pub fn poll_tick(&mut self, cx: &mut Context<'_>) -> Poll> { if self.interval.poll_tick(cx).is_ready() { - self.future = Some(Box::pin(self.resolver.external_addr())); + self.future = Some(Box::pin(self.resolver.clone().external_addr())); } if let Some(mut fut) = self.future.take() { @@ -212,6 +231,9 @@ pub async fn external_addr_with(resolver: NatResolver) -> Option { ); }) .ok(), + NatResolver::ExternalAddr(domain) => { + domain.to_socket_addrs().ok().and_then(|mut addrs| addrs.next().map(|addr| addr.ip())) + } NatResolver::None => None, } } @@ -245,7 +267,7 @@ async fn resolve_external_ip_url(url: &str) -> Option { #[cfg(test)] mod tests { use super::*; - use std::net::Ipv4Addr; + use std::net::{Ipv4Addr, Ipv6Addr}; #[tokio::test] #[ignore] @@ -267,6 +289,18 @@ mod tests { dbg!(ip); } + #[test] + fn as_external_ip_test() { + let resolver = NatResolver::ExternalAddr("localhost".to_string()); + let ip = resolver.as_external_ip(30303).expect("localhost should be resolvable"); + + if ip.is_ipv4() { + assert_eq!(ip, IpAddr::V4(Ipv4Addr::LOCALHOST)); + } else { + assert_eq!(ip, IpAddr::V6(Ipv6Addr::LOCALHOST)); + } + } + #[test] fn test_from_str() { assert_eq!(NatResolver::Any, "any".parse().unwrap()); diff --git a/crates/net/network/src/config.rs b/crates/net/network/src/config.rs index 047970aac0..93223fabcd 100644 --- a/crates/net/network/src/config.rs +++ b/crates/net/network/src/config.rs @@ -433,7 +433,7 @@ impl NetworkConfigBuilder { pub fn external_ip_resolver(mut self, resolver: NatResolver) -> Self { self.discovery_v4_builder .get_or_insert_with(Discv4Config::builder) - .external_ip_resolver(Some(resolver)); + .external_ip_resolver(Some(resolver.clone())); self.nat = Some(resolver); self } @@ -484,7 +484,7 @@ impl NetworkConfigBuilder { } // Disable nat - pub const fn disable_nat(mut self) -> Self { + pub fn disable_nat(mut self) -> Self { self.nat = None; self } @@ -579,7 +579,7 @@ impl NetworkConfigBuilder { } /// Sets the NAT resolver for external IP. - pub const fn add_nat(mut self, nat: Option) -> Self { + pub fn add_nat(mut self, nat: Option) -> Self { self.nat = nat; self } diff --git a/crates/net/network/src/network.rs b/crates/net/network/src/network.rs index f804144a4e..93e14ec04e 100644 --- a/crates/net/network/src/network.rs +++ b/crates/net/network/src/network.rs @@ -237,7 +237,9 @@ impl PeersInfo for NetworkHandle { discv4.node_record() } else if let Some(discv5) = self.inner.discv5.as_ref() { // for disv5 we must check if we have an external ip configured - if let Some(external) = self.inner.nat.and_then(|nat| nat.as_external_ip()) { + if let Some(external) = + self.inner.nat.clone().and_then(|nat| nat.as_external_ip(discv5.local_port())) + { NodeRecord::new((external, discv5.local_port()).into(), *self.peer_id()) } else { // use the node record that discv5 tracks or use localhost @@ -252,9 +254,11 @@ impl PeersInfo for NetworkHandle { // also use the tcp port .with_tcp_port(self.inner.listener_address.lock().port()) } else { - let external_ip = self.inner.nat.and_then(|nat| nat.as_external_ip()); - let mut socket_addr = *self.inner.listener_address.lock(); + + let external_ip = + self.inner.nat.clone().and_then(|nat| nat.as_external_ip(socket_addr.port())); + if let Some(ip) = external_ip { // if able to resolve external ip, use it instead and also set the local address socket_addr.set_ip(ip) diff --git a/crates/node/core/src/args/network.rs b/crates/node/core/src/args/network.rs index 2d8f7dc3dd..3da236f169 100644 --- a/crates/node/core/src/args/network.rs +++ b/crates/node/core/src/args/network.rs @@ -337,7 +337,7 @@ impl NetworkArgs { // Configure basic network stack NetworkConfigBuilder::::new(secret_key) - .external_ip_resolver(self.nat) + .external_ip_resolver(self.nat.clone()) .sessions_config( SessionsConfig::default().with_upscaled_event_buffer(peers_config.max_peers()), ) @@ -399,7 +399,7 @@ impl NetworkArgs { } /// Configures the [`NatResolver`] - pub const fn with_nat_resolver(mut self, nat: NatResolver) -> Self { + pub fn with_nat_resolver(mut self, nat: NatResolver) -> Self { self.nat = nat; self }