async_ip/
lib.rs

1//! # Async IP Resolution
2//!
3//! A lightweight, asynchronous client for obtaining global IPv4 and IPv6 addresses.
4//! This crate provides reliable IP address resolution using multiple fallback services
5//! and concurrent requests for improved reliability.
6//!
7//! ## Features
8//!
9//! - Async IP address resolution
10//! - Support for both IPv4 and IPv6
11//! - Multiple fallback services
12//! - Concurrent resolution for improved reliability
13//! - Internal IP address detection
14//! - WebAssembly support
15//! - Custom HTTP client support
16//!
17//! ## Usage
18//!
19//! ```rust,no_run
20//! use async_ip::get_all;
21//! use citadel_io::tokio;
22//!
23//! #[tokio::main(flavor = "current_thread")]
24//! async fn main() -> Result<(), async_ip::IpRetrieveError> {
25//!     // Get both internal and external IP addresses
26//!     use reqwest::Client;
27//! let ip_info = get_all::<Client>(None).await?;
28//!     println!("External IPv6: {:?}", ip_info.external_ipv6);
29//!     println!("Internal IPv4: {:?}", ip_info.internal_ip);
30//!     Ok(())
31//! }
32//! ```
33//!
34//! ## Advanced Usage
35//!
36//! ```rust,no_run
37//! use async_ip::{get_all_multi_concurrent, get_default_client};
38//! use citadel_io::tokio;
39//!
40//! #[tokio::main(flavor = "current_thread")]
41//! async fn main() -> Result<(), async_ip::IpRetrieveError> {
42//!     // Use multiple services concurrently with a custom client
43//!     let client = get_default_client();
44//!     let ip_info = get_all_multi_concurrent(Some(client)).await?;
45//!     println!("External IPs: {:?}", ip_info);
46//!     Ok(())
47//! }
48//! ```
49//!
50//! ## WebAssembly Support
51//!
52//! When compiled with the `wasm` target, this crate uses a lightweight HTTP client
53//! suitable for WebAssembly environments. The functionality remains the same, but
54//! some features (like internal IP detection) may be limited.
55//!
56//! ## Error Handling
57//!
58//! The crate uses a custom `IpRetrieveError` type that wraps various error
59//! conditions that may occur during IP resolution, including network errors
60//! and parsing failures.
61
62#![deny(
63    missing_docs,
64    trivial_numeric_casts,
65    unused_extern_crates,
66    unused_import_braces,
67    variant_size_differences,
68    unused_features,
69    unused_results,
70    warnings
71)]
72
73use async_trait::async_trait;
74use auto_impl::auto_impl;
75#[cfg(not(target_family = "wasm"))]
76use reqwest::Client;
77use std::fmt::Formatter;
78use std::net::IpAddr;
79#[cfg(not(target_family = "wasm"))]
80use std::net::SocketAddr;
81use std::str::FromStr;
82
83// use http since it's 2-3x faster
84const URL_V6: &str = "http://api64.ipify.org";
85//const URL_V4: &str = "http://api.ipify.org";
86
87const URL_V6_1: &str = "http://ident.me";
88//const URL_V4_1: &str = "http://v4.ident.me";
89
90const URL_V6_2: &str = "http://v4v6.ipv6-test.com/api/myip.php";
91//const URL_V4_2: &str = "http://v4.ipv6-test.com/api/myip.php";
92
93#[derive(Debug, Clone)]
94#[cfg_attr(feature = "std", derive(serde::Serialize, serde::Deserialize))]
95/// All the ip addr info for this node
96pub struct IpAddressInfo {
97    /// internal addr
98    pub internal_ip: IpAddr,
99    /// external v6 addr
100    pub external_ipv6: Option<IpAddr>,
101}
102
103impl IpAddressInfo {
104    /// Returns localhost addr
105    pub fn localhost() -> Self {
106        let localhost = IpAddr::from_str("127.0.0.1").unwrap();
107        Self {
108            internal_ip: localhost,
109            external_ipv6: None,
110        }
111    }
112}
113
114/// Gets IP info concurrently using default multiple internal sources
115pub async fn get_all_multi_concurrent<T: AsyncHttpGetClient>(
116    client: Option<T>,
117) -> Result<IpAddressInfo, IpRetrieveError> {
118    get_all_multi_concurrent_from(client, &[URL_V6, URL_V6_1, URL_V6_2]).await
119}
120
121/// Uses multiple url addrs to obtain the information
122pub async fn get_all_multi_concurrent_from<T: AsyncHttpGetClient>(
123    client: Option<T>,
124    v6_addrs: &[&str],
125) -> Result<IpAddressInfo, IpRetrieveError> {
126    let client = client.map(|client| Box::new(client) as Box<dyn AsyncHttpGetClient>);
127    let client = &client.unwrap_or_else(|| Box::new(get_default_client()));
128    let internal_ipv4_future = get_internal_ip(false);
129    let external_ipv6_future = futures::future::select_ok(
130        v6_addrs
131            .iter()
132            .map(|addr| Box::pin(get_ip_from(Some(client), addr)))
133            .collect::<Vec<_>>(),
134    );
135
136    let (res0, res2) = citadel_io::tokio::join!(internal_ipv4_future, external_ipv6_future);
137    let internal_ipv4 =
138        res0.ok_or_else(|| IpRetrieveError::Error("Could not obtain internal IPv4".to_string()))?;
139    let external_ipv6 = res2.ok().map(|r| r.0);
140
141    Ok(IpAddressInfo {
142        internal_ip: internal_ipv4,
143        external_ipv6,
144    })
145}
146
147/// Returns all possible IPs for this node
148pub async fn get_all<T: AsyncHttpGetClient>(
149    client: Option<T>,
150) -> Result<IpAddressInfo, IpRetrieveError> {
151    get_all_from(client, URL_V6).await
152}
153
154/// Gets IP info concurrently using custom multiple internal sources
155pub async fn get_all_from<T: AsyncHttpGetClient>(
156    client: Option<T>,
157    v6_addr: &str,
158) -> Result<IpAddressInfo, IpRetrieveError> {
159    let client = client
160        .map(|client| Box::new(client) as Box<dyn AsyncHttpGetClient>)
161        .unwrap_or_else(|| Box::new(get_default_client()));
162    let internal_ipv4_future = get_internal_ip(false);
163    let external_ipv6_future = get_ip_from(Some(client), v6_addr);
164    let (res0, res2) = citadel_io::tokio::join!(internal_ipv4_future, external_ipv6_future);
165    let internal_ipv4 =
166        res0.ok_or_else(|| IpRetrieveError::Error("Could not obtain internal IPv4".to_string()))?;
167    let external_ipv6 = res2.ok();
168
169    Ok(IpAddressInfo {
170        internal_ip: internal_ipv4,
171        external_ipv6,
172    })
173}
174
175/// Asynchronously gets the IP address of this node. If `prefer_ipv6` is true, then the client will
176/// attempt to get the IP address; however, if the client is using an IPv4 address, that will be returned
177/// instead.
178///
179/// If a reqwest client is supplied, this function will use that client to get the information. None by default.
180pub async fn get_ip_from<T: AsyncHttpGetClient>(
181    client: Option<T>,
182    addr: &str,
183) -> Result<IpAddr, IpRetrieveError> {
184    let client = client
185        .map(|client| Box::new(client) as Box<dyn AsyncHttpGetClient>)
186        .unwrap_or_else(|| Box::new(get_default_client()));
187
188    let text = client.get(addr).await?;
189    IpAddr::from_str(text.as_str()).map_err(|err| IpRetrieveError::Error(err.to_string()))
190}
191
192/// Gets the internal IP address using DNS
193pub async fn get_internal_ip(ipv6: bool) -> Option<IpAddr> {
194    if ipv6 {
195        get_internal_ipv6().await
196    } else {
197        get_internal_ipv4().await
198    }
199}
200
201#[cfg(not(target_family = "wasm"))]
202/// Returns the internal ipv4 address of this node
203pub async fn get_internal_ipv4() -> Option<IpAddr> {
204    let socket = citadel_io::tokio::net::UdpSocket::bind(addr("0.0.0.0:0")?)
205        .await
206        .ok()?;
207    socket.connect(addr("8.8.8.8:80")?).await.ok()?;
208    socket.local_addr().ok().map(|sck| sck.ip())
209}
210
211#[cfg(target_family = "wasm")]
212async fn get_internal_ipv4() -> Option<IpAddr> {
213    None
214}
215
216#[cfg(not(target_family = "wasm"))]
217async fn get_internal_ipv6() -> Option<IpAddr> {
218    let socket = citadel_io::tokio::net::UdpSocket::bind(addr("[::]:0")?)
219        .await
220        .ok()?;
221    socket
222        .connect(addr("[2001:4860:4860::8888]:80")?)
223        .await
224        .ok()?;
225    socket.local_addr().ok().map(|sck| sck.ip())
226}
227
228#[cfg(target_family = "wasm")]
229async fn get_internal_ipv6() -> Option<IpAddr> {
230    None
231}
232
233#[cfg(not(target_family = "wasm"))]
234fn addr(addr: &str) -> Option<SocketAddr> {
235    SocketAddr::from_str(addr).ok()
236}
237
238#[cfg(not(target_family = "wasm"))]
239/// Returns a default client
240pub fn get_default_client() -> Client {
241    Client::builder().tcp_nodelay(true).build().unwrap()
242}
243#[cfg(target_family = "wasm")]
244/// Returns a default client
245fn get_default_client() -> UreqClient {
246    UreqClient
247}
248
249/// The default error type for this crate
250#[derive(Debug)]
251pub enum IpRetrieveError {
252    /// Generic wrapper
253    Error(String),
254}
255
256impl std::fmt::Display for IpRetrieveError {
257    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
258        match self {
259            IpRetrieveError::Error(err) => write!(f, "{}", err),
260        }
261    }
262}
263
264#[async_trait]
265#[auto_impl(Box, &)]
266/// An async http client
267pub trait AsyncHttpGetClient: Send + Sync {
268    /// Async Get
269    async fn get(&self, addr: &str) -> Result<String, IpRetrieveError>;
270}
271
272#[cfg(not(target_family = "wasm"))]
273#[async_trait]
274impl AsyncHttpGetClient for Client {
275    async fn get(&self, addr: &str) -> Result<String, IpRetrieveError> {
276        let resp = self
277            .get(addr)
278            .send()
279            .await
280            .map_err(|err| IpRetrieveError::Error(err.to_string()))?;
281
282        resp.text()
283            .await
284            .map_err(|err| IpRetrieveError::Error(err.to_string()))
285    }
286}
287
288#[async_trait]
289impl AsyncHttpGetClient for () {
290    async fn get(&self, _addr: &str) -> Result<String, IpRetrieveError> {
291        unimplemented!("Stub implementation for AsyncHttpGetClient")
292    }
293}
294
295#[cfg(target_family = "wasm")]
296/// Ureq client
297pub struct UreqClient;
298
299#[cfg(target_family = "wasm")]
300#[async_trait]
301impl AsyncHttpGetClient for UreqClient {
302    async fn get(&self, addr: &str) -> Result<String, IpRetrieveError> {
303        let addr = addr.to_string();
304        citadel_io::tokio::task::spawn_blocking(move || {
305            ureq::get(&addr)
306                .call()
307                .map_err(|err| IpRetrieveError::Error(err.to_string()))?
308                .into_string()
309                .map_err(|err| IpRetrieveError::Error(err.to_string()))
310        })
311        .await
312        .map_err(|err| IpRetrieveError::Error(err.to_string()))?
313    }
314}