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