chore: resolve external IP with just an http request (#8516)

This commit is contained in:
DaniPopes
2024-05-30 18:40:23 +03:00
committed by GitHub
parent 716976f0d1
commit 9a08ad7844
3 changed files with 41 additions and 343 deletions

View File

@ -12,16 +12,11 @@ description = "Helpers for working around NAT"
workspace = true
[dependencies]
# nat
public-ip = "0.2"
igd-next = { workspace = true, features = ["aio_tokio"] }
# misc
tracing.workspace = true
pin-project-lite = "0.2.9"
tokio = { workspace = true, features = ["time"] }
thiserror.workspace = true
futures-util.workspace = true
reqwest.workspace = true
serde_with = { workspace = true, optional = true }
thiserror.workspace = true
tokio = { workspace = true, features = ["time"] }
[dev-dependencies]
reth-tracing.workspace = true

View File

@ -12,22 +12,25 @@
#![cfg_attr(not(test), warn(unused_crate_dependencies))]
#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))]
use igd_next::aio::tokio::search_gateway;
use pin_project_lite::pin_project;
use std::{
fmt,
future::{poll_fn, Future},
net::{AddrParseError, IpAddr},
pin::Pin,
str::FromStr,
task::{ready, Context, Poll},
task::{Context, Poll},
time::Duration,
};
use tracing::debug;
#[cfg(feature = "serde")]
use serde_with::{DeserializeFromStr, SerializeDisplay};
/// URLs to `GET` the external IP address.
///
/// Taken from: <https://stackoverflow.com/questions/3253701/get-public-external-ip-address>
const EXTERNAL_IP_APIS: &[&str] =
&["http://ipinfo.io/ip", "http://icanhazip.com", "http://ifconfig.me"];
/// All builtin resolvers.
#[derive(Debug, Clone, Copy, Eq, PartialEq, Default, Hash)]
#[cfg_attr(feature = "serde", derive(SerializeDisplay, DeserializeFromStr))]
@ -35,9 +38,9 @@ pub enum NatResolver {
/// Resolve with any available resolver.
#[default]
Any,
/// Resolve via Upnp
/// Resolve external IP via UPnP.
Upnp,
/// Resolve external IP via [public_ip::Resolver]
/// Resolve external IP via a network request.
PublicIp,
/// Use the given [IpAddr]
ExternalIp(IpAddr),
@ -45,8 +48,6 @@ pub enum NatResolver {
None,
}
// === impl NatResolver ===
impl NatResolver {
/// Attempts to produce an IP address (best effort).
pub async fn external_addr(self) -> Option<IpAddr> {
@ -103,12 +104,10 @@ impl FromStr for NatResolver {
#[must_use = "Does nothing unless polled"]
pub struct ResolveNatInterval {
resolver: NatResolver,
future: Option<ResolveFut>,
future: Option<Pin<Box<dyn Future<Output = Option<IpAddr>> + Send>>>,
interval: tokio::time::Interval,
}
// === impl ResolveNatInterval ===
impl fmt::Debug for ResolveNatInterval {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ResolveNatInterval")
@ -146,8 +145,7 @@ impl ResolveNatInterval {
/// Completes when the next [IpAddr] in the interval has been reached.
pub async fn tick(&mut self) -> Option<IpAddr> {
let ip = poll_fn(|cx| self.poll_tick(cx));
ip.await
poll_fn(|cx| self.poll_tick(cx)).await
}
/// Polls for the next resolved [IpAddr] in the interval to be reached.
@ -165,9 +163,7 @@ impl ResolveNatInterval {
if let Some(mut fut) = self.future.take() {
match fut.as_mut().poll(cx) {
Poll::Ready(ip) => return Poll::Ready(ip),
Poll::Pending => {
self.future = Some(fut);
}
Poll::Pending => self.future = Some(fut),
}
}
@ -183,83 +179,26 @@ pub async fn external_ip() -> Option<IpAddr> {
/// Given a [`NatResolver`] attempts to produce an IP address (best effort).
pub async fn external_addr_with(resolver: NatResolver) -> Option<IpAddr> {
match resolver {
NatResolver::Any => {
ResolveAny {
upnp: Some(Box::pin(resolve_external_ip_upnp())),
external: Some(Box::pin(resolve_external_ip())),
}
.await
}
NatResolver::Upnp => resolve_external_ip_upnp().await,
NatResolver::PublicIp => resolve_external_ip().await,
NatResolver::Any | NatResolver::Upnp | NatResolver::PublicIp => resolve_external_ip().await,
NatResolver::ExternalIp(ip) => Some(ip),
NatResolver::None => None,
}
}
type ResolveFut = Pin<Box<dyn Future<Output = Option<IpAddr>> + Send>>;
pin_project! {
/// A future that resolves the first ip via all configured resolvers
struct ResolveAny {
#[pin]
upnp: Option<ResolveFut>,
#[pin]
external: Option<ResolveFut>,
}
}
impl Future for ResolveAny {
type Output = Option<IpAddr>;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let mut this = self.as_mut().project();
if let Some(upnp) = this.upnp.as_mut().as_pin_mut() {
// if upnp is configured we prefer it over http and dns resolvers
let ip = ready!(upnp.poll(cx));
this.upnp.set(None);
if ip.is_some() {
return Poll::Ready(ip)
}
}
if let Some(upnp) = this.external.as_mut().as_pin_mut() {
if let Poll::Ready(ip) = upnp.poll(cx) {
this.external.set(None);
if ip.is_some() {
return Poll::Ready(ip)
}
}
}
if this.upnp.is_none() && this.external.is_none() {
return Poll::Ready(None)
}
Poll::Pending
}
}
async fn resolve_external_ip_upnp() -> Option<IpAddr> {
search_gateway(Default::default())
.await
.map_err(|err| {
debug!(target: "net::nat", %err, "Failed to resolve external IP via UPnP: failed to find gateway");
err
})
.ok()?
.get_external_ip()
.await
.map_err(|err| {
debug!(target: "net::nat", %err, "Failed to resolve external IP via UPnP");
err
})
.ok()
}
async fn resolve_external_ip() -> Option<IpAddr> {
public_ip::addr().await
let futures = EXTERNAL_IP_APIS.iter().copied().map(resolve_external_ip_url_res).map(Box::pin);
futures_util::future::select_ok(futures).await.ok().map(|(res, _)| res)
}
async fn resolve_external_ip_url_res(url: &str) -> Result<IpAddr, ()> {
resolve_external_ip_url(url).await.ok_or(())
}
async fn resolve_external_ip_url(url: &str) -> Option<IpAddr> {
let response = reqwest::get(url).await.ok()?;
let response = response.error_for_status().ok()?;
let text = response.text().await.ok()?;
text.trim().parse().ok()
}
#[cfg(test)]