1#![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
34const EXTERNAL_IP_APIS: &[&str] =
38 &["https://ipinfo.io/ip", "https://icanhazip.com", "https://ifconfig.me"];
39
40#[derive(Debug, Clone, Copy, Eq, PartialEq, Default, Hash)]
42#[cfg_attr(feature = "serde", derive(SerializeDisplay, DeserializeFromStr))]
43pub enum NatResolver {
44 #[default]
46 Any,
47 Upnp,
49 PublicIp,
51 ExternalIp(IpAddr),
53 NetIf,
55 None,
57}
58
59impl NatResolver {
60 pub async fn external_addr(self) -> Option<IpAddr> {
62 external_addr_with(self).await
63 }
64
65 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#[derive(Debug, thiserror::Error)]
89pub enum ParseNatResolverError {
90 #[error(transparent)]
92 AddrParseError(#[from] AddrParseError),
93 #[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#[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 #[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 #[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 pub async fn tick(&mut self) -> Option<IpAddr> {
166 poll_fn(|cx| self.poll_tick(cx)).await
167 }
168
169 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
192pub async fn external_ip() -> Option<IpAddr> {
194 external_addr_with(NatResolver::Any).await
195}
196
197pub 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}