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))]
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
34const EXTERNAL_IP_APIS: &[&str] =
38 &["https://ipinfo.io/ip", "https://icanhazip.com", "https://ifconfig.me"];
39
40#[derive(Debug, Clone, 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 ExternalAddr(String),
61 NetIf,
63 None,
65}
66
67impl NatResolver {
68 pub async fn external_addr(self) -> Option<IpAddr> {
75 external_addr_with(self).await
76 }
77
78 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#[derive(Debug, thiserror::Error)]
111pub enum ParseNatResolverError {
112 #[error(transparent)]
114 AddrParseError(#[from] AddrParseError),
115 #[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#[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 interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
170 Self { resolver, future: None, interval }
171 }
172
173 #[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 #[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 pub const fn resolver(&self) -> &NatResolver {
195 &self.resolver
196 }
197
198 pub async fn tick(&mut self) -> Option<IpAddr> {
200 poll_fn(|cx| self.poll_tick(cx)).await
201 }
202
203 pub fn poll_tick(&mut self, cx: &mut Context<'_>) -> Poll<Option<IpAddr>> {
211 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
227pub async fn external_ip() -> Option<IpAddr> {
233 external_addr_with(NatResolver::Any).await
234}
235
236pub 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 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 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 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}