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    pub async fn external_addr(self) -> Option<IpAddr> {
70        external_addr_with(self).await
71    }
72
73    /// Returns the fixed ip, if it is [`NatResolver::ExternalIp`] or [`NatResolver::ExternalAddr`].
74    ///
75    /// In the case of [`NatResolver::ExternalAddr`], it will return the first IP address found for
76    /// the domain.
77    pub fn as_external_ip(self, port: u16) -> Option<IpAddr> {
78        match self {
79            Self::ExternalIp(ip) => Some(ip),
80            Self::ExternalAddr(domain) => format!("{domain}:{port}")
81                .to_socket_addrs()
82                .ok()
83                .and_then(|mut addrs| addrs.next().map(|addr| addr.ip())),
84            _ => None,
85        }
86    }
87}
88
89impl fmt::Display for NatResolver {
90    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
91        match self {
92            Self::Any => f.write_str("any"),
93            Self::Upnp => f.write_str("upnp"),
94            Self::PublicIp => f.write_str("publicip"),
95            Self::ExternalIp(ip) => write!(f, "extip:{ip}"),
96            Self::ExternalAddr(domain) => write!(f, "extaddr:{domain}"),
97            Self::NetIf => f.write_str("netif"),
98            Self::None => f.write_str("none"),
99        }
100    }
101}
102
103/// Error when parsing a [`NatResolver`]
104#[derive(Debug, thiserror::Error)]
105pub enum ParseNatResolverError {
106    /// Failed to parse provided IP
107    #[error(transparent)]
108    AddrParseError(#[from] AddrParseError),
109    /// Failed to parse due to unknown variant
110    #[error("Unknown Nat Resolver variant: {0}")]
111    UnknownVariant(String),
112}
113
114impl FromStr for NatResolver {
115    type Err = ParseNatResolverError;
116
117    fn from_str(s: &str) -> Result<Self, Self::Err> {
118        let r = match s {
119            "any" => Self::Any,
120            "upnp" => Self::Upnp,
121            "none" => Self::None,
122            "publicip" | "public-ip" => Self::PublicIp,
123            "netif" => Self::NetIf,
124            s => {
125                if let Some(ip) = s.strip_prefix("extip:") {
126                    Self::ExternalIp(ip.parse()?)
127                } else if let Some(domain) = s.strip_prefix("extaddr:") {
128                    Self::ExternalAddr(domain.to_string())
129                } else {
130                    return Err(ParseNatResolverError::UnknownVariant(format!(
131                        "Unknown Nat Resolver: {s}"
132                    )));
133                }
134            }
135        };
136        Ok(r)
137    }
138}
139
140/// With this type you can resolve the external public IP address on an interval basis.
141#[must_use = "Does nothing unless polled"]
142pub struct ResolveNatInterval {
143    resolver: NatResolver,
144    future: Option<Pin<Box<dyn Future<Output = Option<IpAddr>> + Send>>>,
145    interval: tokio::time::Interval,
146}
147
148impl fmt::Debug for ResolveNatInterval {
149    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
150        f.debug_struct("ResolveNatInterval")
151            .field("resolver", &self.resolver)
152            .field("future", &self.future.as_ref().map(drop))
153            .field("interval", &self.interval)
154            .finish()
155    }
156}
157
158impl ResolveNatInterval {
159    fn with_interval(resolver: NatResolver, interval: tokio::time::Interval) -> Self {
160        Self { resolver, future: None, interval }
161    }
162
163    /// Creates a new [`ResolveNatInterval`] that attempts to resolve the public IP with interval of
164    /// period. See also [`tokio::time::interval`]
165    #[track_caller]
166    pub fn interval(resolver: NatResolver, period: Duration) -> Self {
167        let interval = tokio::time::interval(period);
168        Self::with_interval(resolver, interval)
169    }
170
171    /// Creates a new [`ResolveNatInterval`] that attempts to resolve the public IP with interval of
172    /// period with the first attempt starting at `start`. See also [`tokio::time::interval_at`]
173    #[track_caller]
174    pub fn interval_at(
175        resolver: NatResolver,
176        start: tokio::time::Instant,
177        period: Duration,
178    ) -> Self {
179        let interval = tokio::time::interval_at(start, period);
180        Self::with_interval(resolver, interval)
181    }
182
183    /// Returns the resolver used by this interval
184    pub const fn resolver(&self) -> &NatResolver {
185        &self.resolver
186    }
187
188    /// Completes when the next [`IpAddr`] in the interval has been reached.
189    pub async fn tick(&mut self) -> Option<IpAddr> {
190        poll_fn(|cx| self.poll_tick(cx)).await
191    }
192
193    /// Polls for the next resolved [`IpAddr`] in the interval to be reached.
194    ///
195    /// This method can return the following values:
196    ///
197    ///  * `Poll::Pending` if the next [`IpAddr`] has not yet been resolved.
198    ///  * `Poll::Ready(Option<IpAddr>)` if the next [`IpAddr`] has been resolved. This returns
199    ///    `None` if the attempt was unsuccessful.
200    pub fn poll_tick(&mut self, cx: &mut Context<'_>) -> Poll<Option<IpAddr>> {
201        if self.interval.poll_tick(cx).is_ready() {
202            self.future = Some(Box::pin(self.resolver.clone().external_addr()));
203        }
204
205        if let Some(mut fut) = self.future.take() {
206            match fut.as_mut().poll(cx) {
207                Poll::Ready(ip) => return Poll::Ready(ip),
208                Poll::Pending => self.future = Some(fut),
209            }
210        }
211
212        Poll::Pending
213    }
214}
215
216/// Attempts to produce an IP address with all builtin resolvers (best effort).
217pub async fn external_ip() -> Option<IpAddr> {
218    external_addr_with(NatResolver::Any).await
219}
220
221/// Given a [`NatResolver`] attempts to produce an IP address (best effort).
222pub async fn external_addr_with(resolver: NatResolver) -> Option<IpAddr> {
223    match resolver {
224        NatResolver::Any | NatResolver::Upnp | NatResolver::PublicIp => resolve_external_ip().await,
225        NatResolver::ExternalIp(ip) => Some(ip),
226        NatResolver::NetIf => resolve_net_if_ip(DEFAULT_NET_IF_NAME)
227            .inspect_err(|err| {
228                debug!(target: "net::nat",
229                     %err,
230                    "Failed to resolve network interface IP"
231                );
232            })
233            .ok(),
234        NatResolver::ExternalAddr(domain) => {
235            domain.to_socket_addrs().ok().and_then(|mut addrs| addrs.next().map(|addr| addr.ip()))
236        }
237        NatResolver::None => None,
238    }
239}
240
241async fn resolve_external_ip() -> Option<IpAddr> {
242    let futures = EXTERNAL_IP_APIS.iter().copied().map(resolve_external_ip_url_res).map(Box::pin);
243    futures_util::future::select_ok(futures)
244        .await
245        .inspect_err(|err| {
246            debug!(target: "net::nat",
247            ?err,
248                external_ip_apis=?EXTERNAL_IP_APIS,
249                "Failed to resolve external IP from any API");
250        })
251        .ok()
252        .map(|(ip, _)| ip)
253}
254
255async fn resolve_external_ip_url_res(url: &str) -> Result<IpAddr, ()> {
256    resolve_external_ip_url(url).await.ok_or(())
257}
258
259async fn resolve_external_ip_url(url: &str) -> Option<IpAddr> {
260    let client = reqwest::Client::builder().timeout(Duration::from_secs(10)).build().ok()?;
261    let response = client.get(url).send().await.ok()?;
262    let response = response.error_for_status().ok()?;
263    let text = response.text().await.ok()?;
264    text.trim().parse().ok()
265}
266
267#[cfg(test)]
268mod tests {
269    use super::*;
270    use std::net::{Ipv4Addr, Ipv6Addr};
271
272    #[tokio::test]
273    #[ignore]
274    async fn get_external_ip() {
275        reth_tracing::init_test_tracing();
276        let ip = external_ip().await;
277        dbg!(ip);
278    }
279
280    #[tokio::test]
281    #[ignore]
282    async fn get_external_ip_interval() {
283        reth_tracing::init_test_tracing();
284        let mut interval = ResolveNatInterval::interval(Default::default(), Duration::from_secs(5));
285
286        let ip = interval.tick().await;
287        dbg!(ip);
288        let ip = interval.tick().await;
289        dbg!(ip);
290    }
291
292    #[test]
293    fn as_external_ip_test() {
294        let resolver = NatResolver::ExternalAddr("localhost".to_string());
295        let ip = resolver.as_external_ip(30303).expect("localhost should be resolvable");
296
297        if ip.is_ipv4() {
298            assert_eq!(ip, IpAddr::V4(Ipv4Addr::LOCALHOST));
299        } else {
300            assert_eq!(ip, IpAddr::V6(Ipv6Addr::LOCALHOST));
301        }
302    }
303
304    #[test]
305    fn test_from_str() {
306        assert_eq!(NatResolver::Any, "any".parse().unwrap());
307        assert_eq!(NatResolver::None, "none".parse().unwrap());
308
309        let ip = NatResolver::ExternalIp(IpAddr::V4(Ipv4Addr::UNSPECIFIED));
310        let s = "extip:0.0.0.0";
311        assert_eq!(ip, s.parse().unwrap());
312        assert_eq!(ip.to_string(), s);
313    }
314}