Skip to main content

reth_net_nat/
lib.rs

1//! Helpers for resolving the external IP.
2//!
3//! ## Feature Flags
4//!
5//! - `serde` (default): Enable serde support
6
7#![doc(
8    html_logo_url = "https://raw.githubusercontent.com/paradigmxyz/reth/main/assets/reth-docs.png",
9    html_favicon_url = "https://avatars0.githubusercontent.com/u/97369466?s=256",
10    issue_tracker_base_url = "https://github.com/paradigmxyz/reth/issues/"
11)]
12#![cfg_attr(not(test), warn(unused_crate_dependencies))]
13#![cfg_attr(docsrs, feature(doc_cfg))]
14
15pub mod net_if;
16
17pub use net_if::{NetInterfaceError, DEFAULT_NET_IF_NAME};
18
19use std::{
20    fmt,
21    future::{poll_fn, Future},
22    net::{AddrParseError, IpAddr, ToSocketAddrs},
23    pin::Pin,
24    str::FromStr,
25    task::{Context, Poll},
26    time::Duration,
27};
28use tracing::debug;
29
30use crate::net_if::resolve_net_if_ip;
31#[cfg(feature = "serde")]
32use serde_with::{DeserializeFromStr, SerializeDisplay};
33
34/// URLs to `GET` the external IP address.
35///
36/// Taken from: <https://stackoverflow.com/questions/3253701/get-public-external-ip-address>
37const EXTERNAL_IP_APIS: &[&str] =
38    &["https://ipinfo.io/ip", "https://icanhazip.com", "https://ifconfig.me"];
39
40/// All builtin resolvers.
41#[derive(Debug, Clone, Eq, PartialEq, Default, Hash)]
42#[cfg_attr(feature = "serde", derive(SerializeDisplay, DeserializeFromStr))]
43pub enum NatResolver {
44    /// Resolve with any available resolver.
45    #[default]
46    Any,
47    /// Resolve external IP via `UPnP`.
48    Upnp,
49    /// Resolve external IP via a network request.
50    PublicIp,
51    /// Use the given [`IpAddr`]
52    ExternalIp(IpAddr),
53    /// Use the given domain name as the external address to expose to peers.
54    /// This is behaving essentially the same as [`NatResolver::ExternalIp`], but supports domain
55    /// names. Domain names are resolved to IP addresses using the OS's resolver. The first IP
56    /// address found is used.
57    /// This may be useful in docker bridge networks where containers are usually queried by DNS
58    /// instead of direct IP addresses.
59    /// Note: the domain shouldn't include a port number. Only the IP address is resolved.
60    ExternalAddr(String),
61    /// Resolve external IP via the network interface.
62    NetIf,
63    /// Resolve nothing
64    None,
65}
66
67impl NatResolver {
68    /// Attempts to produce an IP address (best effort).
69    ///
70    /// # Panics
71    ///
72    /// Network and interface resolution require an active Tokio runtime. Only
73    /// [`Self::ExternalIp`] and [`Self::None`] are guaranteed to work without one.
74    pub async fn external_addr(self) -> Option<IpAddr> {
75        external_addr_with(self).await
76    }
77
78    /// Returns the fixed ip, if it is [`NatResolver::ExternalIp`] or [`NatResolver::ExternalAddr`].
79    ///
80    /// In the case of [`NatResolver::ExternalAddr`], it will return the first IP address found for
81    /// the domain. This performs blocking DNS resolution; async callers should use
82    /// [`Self::external_addr`] instead.
83    pub fn as_external_ip(self, port: u16) -> Option<IpAddr> {
84        match self {
85            Self::ExternalIp(ip) => Some(ip),
86            Self::ExternalAddr(domain) => format!("{domain}:{port}")
87                .to_socket_addrs()
88                .ok()
89                .and_then(|mut addrs| addrs.next().map(|addr| addr.ip())),
90            _ => None,
91        }
92    }
93}
94
95impl fmt::Display for NatResolver {
96    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
97        match self {
98            Self::Any => f.write_str("any"),
99            Self::Upnp => f.write_str("upnp"),
100            Self::PublicIp => f.write_str("publicip"),
101            Self::ExternalIp(ip) => write!(f, "extip:{ip}"),
102            Self::ExternalAddr(domain) => write!(f, "extaddr:{domain}"),
103            Self::NetIf => f.write_str("netif"),
104            Self::None => f.write_str("none"),
105        }
106    }
107}
108
109/// Error when parsing a [`NatResolver`]
110#[derive(Debug, thiserror::Error)]
111pub enum ParseNatResolverError {
112    /// Failed to parse provided IP
113    #[error(transparent)]
114    AddrParseError(#[from] AddrParseError),
115    /// Failed to parse due to unknown variant
116    #[error("Unknown Nat Resolver variant: {0}")]
117    UnknownVariant(String),
118}
119
120impl FromStr for NatResolver {
121    type Err = ParseNatResolverError;
122
123    fn from_str(s: &str) -> Result<Self, Self::Err> {
124        let r = match s {
125            "any" => Self::Any,
126            "upnp" => Self::Upnp,
127            "none" => Self::None,
128            "publicip" | "public-ip" => Self::PublicIp,
129            "netif" => Self::NetIf,
130            s => {
131                if let Some(ip) = s.strip_prefix("extip:") {
132                    Self::ExternalIp(ip.parse()?)
133                } else if let Some(domain) = s.strip_prefix("extaddr:") {
134                    Self::ExternalAddr(domain.to_string())
135                } else {
136                    return Err(ParseNatResolverError::UnknownVariant(format!(
137                        "Unknown Nat Resolver: {s}"
138                    )));
139                }
140            }
141        };
142        Ok(r)
143    }
144}
145
146/// With this type you can resolve the external public IP address on an interval basis.
147///
148/// Keeps at most one resolution in flight and skips missed interval ticks.
149#[must_use = "Does nothing unless polled"]
150pub struct ResolveNatInterval {
151    resolver: NatResolver,
152    future: Option<Pin<Box<dyn Future<Output = Option<IpAddr>> + Send>>>,
153    interval: tokio::time::Interval,
154}
155
156impl fmt::Debug for ResolveNatInterval {
157    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
158        f.debug_struct("ResolveNatInterval")
159            .field("resolver", &self.resolver)
160            .field("future", &self.future.as_ref().map(drop))
161            .field("interval", &self.interval)
162            .finish()
163    }
164}
165
166impl ResolveNatInterval {
167    fn with_interval(resolver: NatResolver, mut interval: tokio::time::Interval) -> Self {
168        // Resolving once is sufficient after a delay; do not replay missed attempts in a burst.
169        interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
170        Self { resolver, future: None, interval }
171    }
172
173    /// Creates a new [`ResolveNatInterval`] that attempts to resolve the public IP with interval of
174    /// period. See also [`tokio::time::interval`]
175    #[track_caller]
176    pub fn interval(resolver: NatResolver, period: Duration) -> Self {
177        let interval = tokio::time::interval(period);
178        Self::with_interval(resolver, interval)
179    }
180
181    /// Creates a new [`ResolveNatInterval`] that attempts to resolve the public IP with interval of
182    /// period with the first attempt starting at `start`. See also [`tokio::time::interval_at`]
183    #[track_caller]
184    pub fn interval_at(
185        resolver: NatResolver,
186        start: tokio::time::Instant,
187        period: Duration,
188    ) -> Self {
189        let interval = tokio::time::interval_at(start, period);
190        Self::with_interval(resolver, interval)
191    }
192
193    /// Returns the resolver used by this interval
194    pub const fn resolver(&self) -> &NatResolver {
195        &self.resolver
196    }
197
198    /// Completes when the next [`IpAddr`] in the interval has been reached.
199    pub async fn tick(&mut self) -> Option<IpAddr> {
200        poll_fn(|cx| self.poll_tick(cx)).await
201    }
202
203    /// Polls for the next resolved [`IpAddr`] in the interval to be reached.
204    ///
205    /// This method can return the following values:
206    ///
207    ///  * `Poll::Pending` if the next [`IpAddr`] has not yet been resolved.
208    ///  * `Poll::Ready(Option<IpAddr>)` if the next [`IpAddr`] has been resolved. This returns
209    ///    `None` if the attempt was unsuccessful.
210    pub fn poll_tick(&mut self, cx: &mut Context<'_>) -> Poll<Option<IpAddr>> {
211        // Dropping a resolution future cannot cancel blocking work it has already started.
212        if self.interval.poll_tick(cx).is_ready() && self.future.is_none() {
213            self.future = Some(Box::pin(self.resolver.clone().external_addr()));
214        }
215
216        if let Some(mut fut) = self.future.take() {
217            match fut.as_mut().poll(cx) {
218                Poll::Ready(ip) => return Poll::Ready(ip),
219                Poll::Pending => self.future = Some(fut),
220            }
221        }
222
223        Poll::Pending
224    }
225}
226
227/// Attempts to produce an IP address with all builtin resolvers (best effort).
228///
229/// # Panics
230///
231/// Panics if polled outside a Tokio runtime.
232pub async fn external_ip() -> Option<IpAddr> {
233    external_addr_with(NatResolver::Any).await
234}
235
236/// Given a [`NatResolver`] attempts to produce an IP address (best effort).
237///
238/// # Panics
239///
240/// Network and interface resolution require an active Tokio runtime. Only
241/// [`NatResolver::ExternalIp`] and [`NatResolver::None`] are guaranteed to work without one.
242pub async fn external_addr_with(resolver: NatResolver) -> Option<IpAddr> {
243    match resolver {
244        NatResolver::Any | NatResolver::Upnp | NatResolver::PublicIp => resolve_external_ip().await,
245        NatResolver::ExternalIp(ip) => Some(ip),
246        NatResolver::NetIf => tokio::task::spawn_blocking(|| {
247            resolve_net_if_ip(DEFAULT_NET_IF_NAME)
248        })
249        .await
250        .inspect_err(|err| {
251            debug!(target: "net::nat", %err, "Failed to join network interface resolution task");
252        })
253        .ok()?
254        .inspect_err(|err| {
255            debug!(target: "net::nat",
256                 %err,
257                "Failed to resolve network interface IP"
258            );
259        })
260        .ok(),
261        NatResolver::ExternalAddr(domain) => tokio::net::lookup_host(format!("{domain}:0"))
262            .await
263            .inspect_err(|err| {
264                debug!(target: "net::nat", %err, %domain, "Failed to resolve external address");
265            })
266            .ok()
267            .and_then(|mut addrs| addrs.next().map(|addr| addr.ip())),
268        NatResolver::None => None,
269    }
270}
271
272async fn resolve_external_ip() -> Option<IpAddr> {
273    // Client setup can read system proxy and TLS configuration. Keep it off the task polling
274    // discovery; the requests themselves use async I/O and share the same client.
275    let client = tokio::task::spawn_blocking(|| {
276        reqwest::Client::builder().timeout(Duration::from_secs(10)).build()
277    })
278    .await
279    .inspect_err(|err| {
280        debug!(target: "net::nat", %err, "Failed to join external IP client setup task");
281    })
282    .ok()?
283    .inspect_err(|err| {
284        debug!(target: "net::nat", %err, "Failed to build external IP client");
285    })
286    .ok()?;
287    let futures =
288        EXTERNAL_IP_APIS.iter().map(|url| resolve_external_ip_url_res(&client, url)).map(Box::pin);
289    futures_util::future::select_ok(futures)
290        .await
291        .inspect_err(|err| {
292            debug!(target: "net::nat",
293            ?err,
294                external_ip_apis=?EXTERNAL_IP_APIS,
295                "Failed to resolve external IP from any API");
296        })
297        .ok()
298        .map(|(ip, _)| ip)
299}
300
301async fn resolve_external_ip_url_res(client: &reqwest::Client, url: &str) -> Result<IpAddr, ()> {
302    resolve_external_ip_url(client, url).await.ok_or(())
303}
304
305async fn resolve_external_ip_url(client: &reqwest::Client, url: &str) -> Option<IpAddr> {
306    let response = client.get(url).send().await.ok()?;
307    let response = response.error_for_status().ok()?;
308    let text = response.text().await.ok()?;
309    text.trim().parse().ok()
310}
311
312#[cfg(test)]
313mod tests {
314    use super::*;
315    use futures_util::FutureExt;
316    use std::net::{Ipv4Addr, Ipv6Addr};
317
318    #[tokio::test]
319    #[ignore]
320    async fn get_external_ip() {
321        reth_tracing::init_test_tracing();
322        let ip = external_ip().await;
323        dbg!(ip);
324    }
325
326    #[tokio::test]
327    #[ignore]
328    async fn get_external_ip_interval() {
329        reth_tracing::init_test_tracing();
330        let mut interval = ResolveNatInterval::interval(Default::default(), Duration::from_secs(5));
331
332        let ip = interval.tick().await;
333        dbg!(ip);
334        let ip = interval.tick().await;
335        dbg!(ip);
336    }
337
338    #[tokio::test(start_paused = true)]
339    async fn interval_preserves_pending_resolution() {
340        let period = Duration::from_secs(5);
341        let next_ip: IpAddr = "203.0.113.7".parse().unwrap();
342        for result in [Some("203.0.113.8".parse().unwrap()), None] {
343            let mut interval =
344                ResolveNatInterval::interval(NatResolver::ExternalIp(next_ip), period);
345            assert_eq!(interval.tick().await, Some(next_ip));
346
347            let (tx, rx) = tokio::sync::oneshot::channel();
348            interval.future = Some(Box::pin(async move { rx.await.unwrap() }));
349            assert!(interval.tick().now_or_never().is_none());
350
351            tokio::time::advance(period * 3).await;
352            assert!(interval.tick().now_or_never().is_none());
353            tx.send(result).expect("the pending resolution must not be dropped");
354            assert_eq!(interval.tick().await, result);
355
356            // The next attempt waits for its interval after either success or failure.
357            assert!(interval.tick().now_or_never().is_none());
358            tokio::time::advance(period).await;
359            assert_eq!(interval.tick().await, Some(next_ip));
360        }
361    }
362
363    #[tokio::test(start_paused = true)]
364    async fn interval_skips_missed_attempts() {
365        let period = Duration::from_secs(5);
366        let ip: IpAddr = "203.0.113.7".parse().unwrap();
367        let mut interval = ResolveNatInterval::interval(NatResolver::ExternalIp(ip), period);
368        assert_eq!(interval.tick().await, Some(ip));
369
370        tokio::time::advance(period * 3).await;
371        assert_eq!(interval.tick().await, Some(ip));
372        assert!(interval.tick().now_or_never().is_none());
373        tokio::time::advance(period).await;
374        assert_eq!(interval.tick().await, Some(ip));
375    }
376
377    #[test]
378    fn netif_resolution_does_not_block_the_runtime() {
379        let runtime = tokio::runtime::Builder::new_current_thread()
380            .enable_all()
381            .max_blocking_threads(1)
382            .build()
383            .unwrap();
384        let (release_tx, release_rx) = std::sync::mpsc::channel();
385        // Occupy the blocking pool so a lookup must yield until the worker is released.
386        let blocker = runtime.spawn_blocking(move || {
387            release_rx.recv_timeout(Duration::from_secs(5)).unwrap();
388        });
389        runtime.block_on(async {
390            let mut lookup = std::pin::pin!(NatResolver::NetIf.external_addr());
391            let pending = futures_util::poll!(&mut lookup).is_pending();
392            release_tx.send(()).unwrap();
393            assert!(pending, "network interface lookup must run on a blocking worker");
394            assert_eq!(lookup.await, resolve_net_if_ip(DEFAULT_NET_IF_NAME).ok());
395            blocker.await.unwrap();
396        });
397    }
398
399    #[test]
400    fn as_external_ip_test() {
401        let resolver = NatResolver::ExternalAddr("localhost".to_string());
402        let ip = resolver.as_external_ip(30303).expect("localhost should be resolvable");
403
404        if ip.is_ipv4() {
405            assert_eq!(ip, IpAddr::V4(Ipv4Addr::LOCALHOST));
406        } else {
407            assert_eq!(ip, IpAddr::V6(Ipv6Addr::LOCALHOST));
408        }
409    }
410
411    #[test]
412    fn test_from_str() {
413        assert_eq!(NatResolver::Any, "any".parse().unwrap());
414        assert_eq!(NatResolver::None, "none".parse().unwrap());
415
416        let ip = NatResolver::ExternalIp(IpAddr::V4(Ipv4Addr::UNSPECIFIED));
417        let s = "extip:0.0.0.0";
418        assert_eq!(ip, s.parse().unwrap());
419        assert_eq!(ip.to_string(), s);
420    }
421}