public_ip/
lib.rs

1//! Crate for resolving a devices' own public IP address.
2//!
3//! ```
4//! #[tokio::main]
5//! async fn main() {
6//!     // Attempt to get an IP address and print it.
7//!     if let Some(ip) = public_ip::addr().await {
8//!         println!("public ip address: {:?}", ip);
9//!     } else {
10//!         println!("couldn't get an IP address");
11//!     }
12//! }
13//! ```
14
15#![cfg_attr(docsrs, feature(doc_cfg))]
16#![cfg_attr(doc, deny(rustdoc::all))]
17#![forbid(trivial_casts, trivial_numeric_casts, unstable_features)]
18#![deny(
19    unused,
20    missing_docs,
21    rust_2018_idioms,
22    future_incompatible,
23    clippy::all,
24    clippy::correctness,
25    clippy::style,
26    clippy::complexity,
27    clippy::perf,
28    clippy::pedantic,
29    clippy::cargo
30)]
31#![allow(clippy::needless_pass_by_value)]
32
33mod error;
34
35#[cfg(any(
36    all(feature = "dns-resolver", not(feature = "tokio-dns-resolver")),
37    all(feature = "http-resolver", not(feature = "tokio-http-resolver"))
38))]
39compile_error!("tokio is not enabled and is the only supported runtime currently - consider creating a PR or issue");
40
41/// DNS resolver support.
42#[cfg(feature = "dns-resolver")]
43#[cfg_attr(docsrs, doc(cfg(feature = "dns-resolver")))]
44pub mod dns;
45
46/// HTTP resolver support.
47#[cfg(feature = "http-resolver")]
48#[cfg_attr(docsrs, doc(cfg(feature = "http-resolver")))]
49pub mod http;
50
51use std::any::Any;
52use std::net::IpAddr;
53#[cfg(any(feature = "dns-resolver", feature = "http-resolver"))]
54use std::net::{Ipv4Addr, Ipv6Addr};
55use std::pin::Pin;
56use std::slice;
57use std::task::{Context, Poll};
58
59use futures_core::Stream;
60use futures_util::stream::{self, BoxStream, StreamExt, TryStreamExt};
61use futures_util::{future, ready};
62use pin_project_lite::pin_project;
63use tracing::trace_span;
64use tracing_futures::Instrument;
65
66pub use crate::error::Error;
67
68/// The details of a resolution.
69///
70/// The internal details can be downcasted through [`Any`].
71pub type Details = Box<dyn Any + Send + Sync + 'static>;
72
73/// A [`Stream`] of `Result<(IpAddr, Details), Error>`.
74pub type Resolutions<'a> = BoxStream<'a, Result<(IpAddr, Details), Error>>;
75
76/// All builtin resolvers.
77#[cfg(any(feature = "dns-resolver", feature = "http-resolver"))]
78#[cfg_attr(
79    docsrs,
80    doc(cfg(any(feature = "dns-resolver", feature = "http-resolver")))
81)]
82pub const ALL: &dyn crate::Resolver<'static> = &&[
83    #[cfg(feature = "dns-resolver")]
84    dns::ALL,
85    #[cfg(feature = "http-resolver")]
86    http::ALL,
87];
88
89/// The version of IP address to resolve.
90#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
91#[non_exhaustive]
92pub enum Version {
93    /// IPv4.
94    V4,
95    /// IPv6.
96    V6,
97    /// Any version of IP address.
98    Any,
99}
100
101impl Version {
102    /// Returns `true` if the provided IP address's version matches `self`.
103    #[must_use]
104    pub fn matches(self, addr: IpAddr) -> bool {
105        self == Version::Any
106            || (self == Version::V4 && addr.is_ipv4())
107            || (self == Version::V6 && addr.is_ipv6())
108    }
109}
110
111///////////////////////////////////////////////////////////////////////////////
112
113/// Attempts to produce an IP address with all builtin resolvers (best effort).
114///
115/// This function will attempt to resolve until the stream is empty and will
116/// drop/ignore any resolver errors.
117#[cfg(any(feature = "dns-resolver", feature = "http-resolver"))]
118#[cfg_attr(
119    docsrs,
120    doc(cfg(any(feature = "dns-resolver", feature = "http-resolver")))
121)]
122pub async fn addr() -> Option<IpAddr> {
123    addr_with(ALL, Version::Any).await
124}
125
126/// Attempts to produce an IPv4 address with all builtin resolvers (best
127/// effort).
128///
129/// This function will attempt to resolve until the stream is empty and will
130/// drop/ignore any resolver errors.
131#[cfg(any(feature = "dns-resolver", feature = "http-resolver"))]
132#[cfg_attr(
133    docsrs,
134    doc(cfg(any(feature = "dns-resolver", feature = "http-resolver")))
135)]
136pub async fn addr_v4() -> Option<Ipv4Addr> {
137    addr_with(ALL, Version::V4).await.map(|addr| match addr {
138        IpAddr::V4(addr) => addr,
139        IpAddr::V6(_) => unreachable!(),
140    })
141}
142
143/// Attempts to produce an IPv6 address with all builtin resolvers (best
144/// effort).
145///
146/// This function will attempt to resolve until the stream is empty and will
147/// drop/ignore any resolver errors.
148#[cfg(any(feature = "dns-resolver", feature = "http-resolver"))]
149#[cfg_attr(
150    docsrs,
151    doc(cfg(any(feature = "dns-resolver", feature = "http-resolver")))
152)]
153pub async fn addr_v6() -> Option<Ipv6Addr> {
154    addr_with(ALL, Version::V6).await.map(|addr| match addr {
155        IpAddr::V6(addr) => addr,
156        IpAddr::V4(_) => unreachable!(),
157    })
158}
159
160/// Given a [`Resolver`] and requested [`Version`], attempts to produce an IP
161/// address (best effort).
162///
163/// This function will attempt to resolve until the stream is empty and will
164/// drop/ignore any resolver errors.
165pub async fn addr_with(resolver: impl Resolver<'_>, version: Version) -> Option<IpAddr> {
166    addr_with_details(resolver, version)
167        .await
168        .map(|(addr, _)| addr)
169}
170
171/// Given a [`Resolver`] and requested [`Version`], attempts to produce an IP
172/// address along with the details of how it was resolved (best effort).
173///
174/// This function will attempt to resolve until the stream is empty and will
175/// drop/ignore any resolver errors.
176pub async fn addr_with_details(
177    resolver: impl Resolver<'_>,
178    version: Version,
179) -> Option<(IpAddr, Details)> {
180    resolve(resolver, version)
181        .filter_map(|result| future::ready(result.ok()))
182        .next()
183        .await
184}
185
186/// Given a [`Resolver`] and requested [`Version`], produces a stream of [`Resolutions`].
187///
188/// This function also protects against a resolver returning a IP address with a
189/// version that was not requested.
190pub fn resolve<'r>(resolver: impl Resolver<'r>, version: Version) -> Resolutions<'r> {
191    let stream = resolver.resolve(version).and_then(move |(addr, details)| {
192        // If a resolver returns a version not matching the one we requested
193        // this is an error so it is skipped.
194        let result = if version.matches(addr) {
195            Ok((addr, details))
196        } else {
197            Err(Error::Version)
198        };
199        future::ready(result)
200    });
201    Box::pin(stream.instrument(trace_span!("resolve public ip address")))
202}
203
204///////////////////////////////////////////////////////////////////////////////
205
206/// Trait implemented by IP address resolver.
207pub trait Resolver<'a>: Send + Sync {
208    /// Resolves a stream of IP addresses with a given [`Version`].
209    fn resolve(&self, version: Version) -> Resolutions<'a>;
210}
211
212impl<'r> Resolver<'r> for &'r dyn Resolver<'r> {
213    fn resolve(&self, version: Version) -> Resolutions<'r> {
214        (**self).resolve(version)
215    }
216}
217
218impl<'r, R> Resolver<'r> for &'r [R]
219where
220    R: Resolver<'r>,
221{
222    fn resolve(&self, version: Version) -> Resolutions<'r> {
223        pin_project! {
224            struct DynSliceResolver<'r, R> {
225                version: Version,
226                resolvers: slice::Iter<'r, R>,
227                #[pin]
228                stream: Resolutions<'r>,
229            }
230        }
231
232        impl<'r, R> Stream for DynSliceResolver<'r, R>
233        where
234            R: Resolver<'r>,
235        {
236            type Item = Result<(IpAddr, Details), Error>;
237
238            fn poll_next(
239                mut self: Pin<&mut Self>,
240                cx: &mut Context<'_>,
241            ) -> Poll<Option<Self::Item>> {
242                match ready!(self.as_mut().project().stream.poll_next(cx)) {
243                    Some(o) => Poll::Ready(Some(o)),
244                    None => self.resolvers.next().map_or(Poll::Ready(None), |next| {
245                        self.stream = next.resolve(self.version);
246                        self.project().stream.poll_next(cx)
247                    }),
248                }
249            }
250        }
251
252        let mut resolvers = self.iter();
253        let first_resolver = resolvers.next();
254        Box::pin(DynSliceResolver {
255            version,
256            resolvers,
257            stream: match first_resolver {
258                Some(first) => first.resolve(version),
259                None => Box::pin(stream::empty()),
260            },
261        })
262    }
263}
264
265macro_rules! resolver_array {
266    () => {
267        resolver_array!(
268            0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16
269        );
270    };
271    ($($n:expr),*) => {
272        $(
273            impl<'r> Resolver<'r> for &'r [&'r dyn Resolver<'r>; $n] {
274                fn resolve(&self, version: Version) -> Resolutions<'r> {
275                    Resolver::resolve(&&self[..], version)
276                }
277            }
278        )*
279    }
280}
281
282resolver_array!();