From 525f28a67ddfb6ac3eca8e681dbb4356266f022c Mon Sep 17 00:00:00 2001 From: Matthias Seitz Date: Wed, 28 Dec 2022 16:14:07 +0100 Subject: [PATCH] feat(net): integrate external public ip auto discovery (#632) * feat(net): integrate external public ip auto discovery * Update crates/net/discv4/src/config.rs Co-authored-by: Georgios Konstantopoulos * rename var Co-authored-by: Georgios Konstantopoulos --- Cargo.lock | 2 +- crates/net/discv4/Cargo.toml | 4 +- crates/net/discv4/src/config.rs | 32 +++++++++++++ crates/net/discv4/src/lib.rs | 26 +++++++++-- crates/net/discv4/src/mock.rs | 2 +- crates/net/nat/Cargo.toml | 1 + crates/net/nat/src/lib.rs | 83 ++++++++++++++++++++++++++++++++- 7 files changed, 141 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 15e7a558e..a6f0c1ac3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3514,9 +3514,9 @@ dependencies = [ "enr 0.7.0", "generic-array", "hex", - "public-ip", "rand 0.8.5", "reth-net-common", + "reth-net-nat", "reth-primitives", "reth-rlp", "reth-rlp-derive", diff --git a/crates/net/discv4/Cargo.toml b/crates/net/discv4/Cargo.toml index b9ee1c75d..39bbeb971 100644 --- a/crates/net/discv4/Cargo.toml +++ b/crates/net/discv4/Cargo.toml @@ -6,7 +6,7 @@ license = "Apache-2.0" repository = "https://github.com/paradigmxyz/reth" readme = "README.md" description = """ -Ethereum network support +Ethereum network discovery """ [dependencies] @@ -15,6 +15,7 @@ reth-primitives = { path = "../../primitives" } reth-rlp = { path = "../../common/rlp", features = ["enr"] } reth-rlp-derive = { path = "../../common/rlp-derive" } reth-net-common = { path = "../common" } +reth-net-nat = { path = "../nat" } # ethereum discv5 = { git = "https://github.com/sigp/discv5" } @@ -34,7 +35,6 @@ bytes = "1.2" tracing = "0.1" thiserror = "1.0" hex = "0.4" -public-ip = "0.2" rand = { version = "0.8", optional = true } generic-array = "0.14" diff --git a/crates/net/discv4/src/config.rs b/crates/net/discv4/src/config.rs index f0832dcfc..9a58cf30d 100644 --- a/crates/net/discv4/src/config.rs +++ b/crates/net/discv4/src/config.rs @@ -5,6 +5,7 @@ use bytes::{Bytes, BytesMut}; use reth_net_common::ban_list::BanList; +use reth_net_nat::{NatResolver, ResolveNatInterval}; use reth_primitives::NodeRecord; use reth_rlp::Encodable; use std::{ @@ -54,6 +55,11 @@ pub struct Discv4Config { pub enable_eip868: bool, /// Additional pairs to include in The [`Enr`](enr::Enr) if EIP-868 extension is enabled pub additional_eip868_rlp_pairs: HashMap, Bytes>, + /// If configured, try to resolve public ip + pub external_ip_resolver: Option, + /// If configured and a `external_ip_resolver` is configured, try to resolve the external ip + /// using this interval. + pub resolve_external_ip_interval: Option, } impl Discv4Config { @@ -85,6 +91,14 @@ impl Discv4Config { } self } + + /// 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 interval = self.resolve_external_ip_interval?; + Some(ResolveNatInterval::interval(resolver, interval)) + } } impl Default for Discv4Config { @@ -113,6 +127,9 @@ impl Default for Discv4Config { enable_lookup: true, enable_eip868: true, additional_eip868_rlp_pairs: Default::default(), + external_ip_resolver: Some(Default::default()), + /// By default retry public IP using a 5min interval + resolve_external_ip_interval: Some(Duration::from_secs(60 * 5)), } } } @@ -247,6 +264,21 @@ impl Discv4ConfigBuilder { self } + /// Configures if and how the external IP of the node should be resolved. + pub fn external_ip_resolver(&mut self, external_ip_resolver: Option) -> &mut Self { + self.config.external_ip_resolver = external_ip_resolver; + self + } + + /// Sets the interval at which the external IP is to be resolved. + pub fn resolve_external_ip_interval( + &mut self, + resolve_external_ip_interval: Option, + ) -> &mut Self { + self.config.resolve_external_ip_interval = resolve_external_ip_interval; + self + } + /// Returns the configured [`Discv4Config`] pub fn build(&self) -> Discv4Config { self.config.clone() diff --git a/crates/net/discv4/src/lib.rs b/crates/net/discv4/src/lib.rs index ec7b40dc4..ce958b0ec 100644 --- a/crates/net/discv4/src/lib.rs +++ b/crates/net/discv4/src/lib.rs @@ -49,7 +49,7 @@ use tokio::{ time::Interval, }; use tokio_stream::{wrappers::ReceiverStream, Stream, StreamExt}; -use tracing::{debug, trace, warn}; +use tracing::{debug, info, trace, warn}; pub mod bootnodes; pub mod error; @@ -67,8 +67,9 @@ pub use reth_primitives::NodeRecord; #[cfg(any(test, feature = "mock"))] pub mod mock; +use reth_net_nat::ResolveNatInterval; /// reexport to get public ip. -pub use public_ip; +pub use reth_net_nat::{external_ip, NatResolver}; /// The default port for discv4 via UDP /// @@ -361,6 +362,8 @@ pub struct Discv4Service { evict_expired_requests_interval: Interval, /// Interval when to resend pings. ping_interval: Interval, + /// The interval at which to attempt resolving external IP again. + resolve_external_ip_interval: Option, /// How this services is configured config: Discv4Config, } @@ -453,8 +456,9 @@ impl Discv4Service { lookup_interval: self_lookup_interval, ping_interval, evict_expired_requests_interval, - config, lookup_rotator, + resolve_external_ip_interval: config.resolve_external_ip_interval(), + config, } } @@ -467,6 +471,16 @@ impl Discv4Service { } } + /// Sets the given ip address as the node's external IP in the node record announced in + /// discovery + pub fn set_external_ip_addr(&mut self, external_ip: IpAddr) { + if self.local_node_record.address != external_ip { + info!(target : "discv4", ?external_ip, "Updating external ip"); + self.local_node_record.address = external_ip; + let _ = self.local_eip_868_enr.set_ip(external_ip, &self.secret_key); + } + } + /// Returns the address of the UDP socket pub fn local_addr(&self) -> SocketAddr { self.local_address @@ -1251,6 +1265,12 @@ impl Discv4Service { self.re_ping_oldest(); } + if let Some(Poll::Ready(Some(ip))) = + self.resolve_external_ip_interval.as_mut().map(|r| r.poll_tick(cx)) + { + self.set_external_ip_addr(ip); + } + // process all incoming commands if let Some(mut rx) = self.commands_rx.take() { let mut is_done = false; diff --git a/crates/net/discv4/src/mock.rs b/crates/net/discv4/src/mock.rs index 91eee1dda..53b4778fa 100644 --- a/crates/net/discv4/src/mock.rs +++ b/crates/net/discv4/src/mock.rs @@ -216,7 +216,7 @@ pub async fn create_discv4_with_config(config: Discv4Config) -> (Discv4, Discv4S let socket = SocketAddr::from_str("0.0.0.0:0").unwrap(); let (secret_key, pk) = SECP256K1.generate_keypair(&mut rng); let id = PeerId::from_slice(&pk.serialize_uncompressed()[1..]); - let external_addr = public_ip::addr().await.unwrap_or_else(|| socket.ip()); + let external_addr = reth_net_nat::external_ip().await.unwrap_or_else(|| socket.ip()); let local_enr = NodeRecord { address: external_addr, tcp_port: socket.port(), udp_port: socket.port(), id }; Discv4::bind(socket, local_enr, secret_key, config).await.unwrap() diff --git a/crates/net/nat/Cargo.toml b/crates/net/nat/Cargo.toml index 9ddec0ffa..4d320ac0c 100644 --- a/crates/net/nat/Cargo.toml +++ b/crates/net/nat/Cargo.toml @@ -21,6 +21,7 @@ igd = { git = "https://github.com/stevefan1999-personal/rust-igd", features = [ # misc tracing = "0.1" pin-project-lite = "0.2.9" +tokio = { version = "1", features = ["time"] } [dev-dependencies] reth-tracing = { path = "../../tracing" } diff --git a/crates/net/nat/src/lib.rs b/crates/net/nat/src/lib.rs index 206de577d..6fad17a2e 100644 --- a/crates/net/nat/src/lib.rs +++ b/crates/net/nat/src/lib.rs @@ -10,10 +10,11 @@ use igd::aio::search_gateway; use pin_project_lite::pin_project; use std::{ - future::Future, + future::{poll_fn, Future}, net::IpAddr, pin::Pin, task::{ready, Context, Poll}, + time::Duration, }; use tracing::warn; @@ -38,6 +39,72 @@ impl NatResolver { } } +/// With this type you can resolve the external public IP address on an interval basis. +#[must_use = "Does nothing unless polled"] +pub struct ResolveNatInterval { + resolver: NatResolver, + future: Option, + interval: tokio::time::Interval, +} + +// === impl ResolveNatInterval === + +impl ResolveNatInterval { + fn with_interval(resolver: NatResolver, interval: tokio::time::Interval) -> Self { + Self { resolver, future: None, interval } + } + + /// Creates a new [ResolveNatInterval] that attempts to resolve the public IP with interval of + /// period. See also [tokio::time::interval] + #[track_caller] + pub fn interval(resolver: NatResolver, period: Duration) -> Self { + let interval = tokio::time::interval(period); + Self::with_interval(resolver, interval) + } + + /// Creates a new [ResolveNatInterval] that attempts to resolve the public IP with interval of + /// period with the first attempt starting at `sart`. See also [tokio::time::interval_at] + #[track_caller] + pub fn interval_at( + resolver: NatResolver, + start: tokio::time::Instant, + period: Duration, + ) -> Self { + let interval = tokio::time::interval_at(start, period); + Self::with_interval(resolver, interval) + } + + /// Completes when the next [IpAddr] in the interval has been reached. + pub async fn tick(&mut self) -> Option { + let ip = poll_fn(|cx| self.poll_tick(cx)); + ip.await + } + + /// Polls for the next resolved [IpAddr] in the interval to be reached. + /// + /// This method can return the following values: + /// + /// * `Poll::Pending` if the next [IpAddr] has not yet been resolved. + /// * `Poll::Ready(Option)` if the next [IpAddr] has been resolved. This returns `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())); + } + + if let Some(mut fut) = self.future.take() { + match fut.as_mut().poll(cx) { + Poll::Ready(ip) => return Poll::Ready(ip), + Poll::Pending => { + self.future = Some(fut); + } + } + } + + Poll::Pending + } +} + /// Attempts to produce an IP address with all builtin resolvers (best effort). pub async fn external_ip() -> Option { external_addr_with(NatResolver::Any).await @@ -58,7 +125,7 @@ pub async fn external_addr_with(resolver: NatResolver) -> Option { } } -type ResolveFut = Pin>>>; +type ResolveFut = Pin> + Send>>; pin_project! { /// A future that resolves the first ip via all configured resolvers @@ -134,4 +201,16 @@ mod tests { let ip = external_ip().await; dbg!(ip); } + + #[tokio::test] + #[ignore] + async fn get_external_ip_interval() { + reth_tracing::init_tracing(); + let mut interval = ResolveNatInterval::interval(Default::default(), Duration::from_secs(5)); + + let ip = interval.tick().await; + dbg!(ip); + let ip = interval.tick().await; + dbg!(ip); + } }