├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ └── ci.yml ├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── examples ├── basic.rs ├── details.rs └── errors.rs └── src ├── dns.rs ├── error.rs ├── http.rs └── lib.rs /.github/ FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [avitex] 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: cargo 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "19:30" 8 | open-pull-requests-limit: 10 9 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | on: [push, pull_request] 3 | jobs: 4 | test: 5 | strategy: 6 | matrix: 7 | os: [ubuntu-latest] 8 | rust-toolchain: [stable, nightly] 9 | fail-fast: false 10 | runs-on: ${{ matrix.os }} 11 | steps: 12 | - name: Checkout code 13 | uses: actions/checkout@v2 14 | - name: Install Rust toolchain 15 | uses: actions-rs/toolchain@v1 16 | with: 17 | toolchain: ${{ matrix.rust-toolchain }} 18 | components: clippy, rustfmt 19 | override: true 20 | - name: Verify versions 21 | run: rustc --version && rustup --version && cargo --version 22 | - name: Cache build artifacts 23 | id: cache-cargo 24 | uses: actions/cache@v2 25 | with: 26 | path: | 27 | ~/.cargo/registry 28 | ~/.cargo/git 29 | target 30 | key: ${{ runner.os }}-cargo-${{ matrix.rust-toolchain }} 31 | - name: Test code with default features 32 | run: cargo test 33 | - name: Check code with only `tokio-dns-resolver` and `google` features enabled. 34 | run: cargo check --no-default-features --features tokio-dns-resolver,google 35 | - name: Check code with only `tokio-http-resolver` and `google` features enabled. 36 | run: cargo check --no-default-features --features tokio-http-resolver,google 37 | - name: Lint code 38 | if: ${{ matrix.rust-toolchain == 'stable' }} 39 | run: cargo fmt -- --check && cargo clippy --all-features 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | Cargo.lock -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "public-ip" 3 | version = "0.2.2" 4 | authors = ["avitex "] 5 | edition = "2018" 6 | rust-version = "1.52.0" 7 | description = "Find the public IP address of a device" 8 | documentation = "https://docs.rs/public-ip" 9 | homepage = "https://github.com/avitex/rust-public-ip" 10 | repository = "https://github.com/avitex/rust-public-ip" 11 | license = "MIT" 12 | categories = ["network-programming"] 13 | include = ["src/**/*", "examples/**/*", "README.md", "LICENSE", "Cargo.toml"] 14 | keywords = ["public", "external", "ip", "async"] 15 | 16 | [features] 17 | default = ["all-providers", "tokio-dns-resolver", "tokio-http-resolver", "https-openssl"] 18 | dns-resolver = ["trust-dns-proto"] 19 | http-resolver = ["http", "hyper", "hyper-system-resolver", "dns-lookup"] 20 | tokio-dns-resolver = ["dns-resolver", "tokio", "trust-dns-client", "trust-dns-proto/tokio-runtime"] 21 | tokio-http-resolver = ["http-resolver", "tokio", "hyper/runtime"] 22 | 23 | https-openssl = ["hyper-openssl", "openssl", "tower-layer"] 24 | https-rustls-webpki = ["hyper-rustls/webpki-roots"] 25 | https-rustls-native = ["hyper-rustls/rustls-native-certs"] 26 | 27 | all-providers = ["google", "opendns", "ipify-org", "my-ip-io", "myip-com", "seeip-org"] 28 | 29 | google = [] 30 | opendns = [] 31 | myip-com = [] 32 | my-ip-io = [] 33 | seeip-org = [] 34 | ipify-org = [] 35 | 36 | [dependencies] 37 | thiserror = "1" 38 | tracing = "0.1" 39 | tracing-futures = { version = "0.2", features = ["futures-03"] } 40 | pin-project-lite = "0.2" 41 | futures-core = { version = "0.3", default-features = false } 42 | futures-util = { version = "0.3", default-features = false, features = ["alloc"] } 43 | 44 | tokio = { version = "1", optional = true } 45 | tower-layer = { version = "0.3", optional = true } 46 | 47 | # DNS Resolver 48 | trust-dns-client = { version = "0.22", optional = true } 49 | trust-dns-proto = { version = "0.22", optional = true, default-features = false } 50 | 51 | # HTTP Resolver 52 | http = { version = "0.2", optional = true } 53 | dns-lookup = { version = "1", optional = true } 54 | hyper = { version = "0.14", features = ["client", "http1"], optional = true } 55 | hyper-system-resolver = { version = "0.5", default-features = false, optional = true } 56 | hyper-openssl = { version = "0.9", optional = true } 57 | hyper-rustls = { version = "0.23", features = ["rustls-native-certs"], optional = true } 58 | openssl = { version = "0.10", optional = true } 59 | 60 | [dev-dependencies] 61 | tokio = { version = "~1", features = ["macros"] } 62 | 63 | [package.metadata.docs.rs] 64 | all-features = true 65 | rustdoc-args = ["--cfg", "docsrs"] 66 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2020 James Dyson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://github.com/avitex/rust-public-ip/workflows/build/badge.svg)](https://github.com/avitex/rust-public-ip/actions?query=workflow:build) 2 | [![Crate](https://img.shields.io/crates/v/public-ip.svg)](https://crates.io/crates/public-ip) 3 | [![Docs](https://docs.rs/public-ip/badge.svg)](https://docs.rs/public-ip) 4 | 5 | # rust-public-ip 6 | 7 | **Find the public IP address of a device** 8 | Documentation hosted on [docs.rs](https://docs.rs/public-ip). 9 | 10 | ```toml 11 | public-ip = "0.2" 12 | ``` 13 | 14 | ## Example usage 15 | 16 | ```rust 17 | #[tokio::main] 18 | async fn main() { 19 | // Attempt to get an IP address and print it. 20 | if let Some(ip) = public_ip::addr().await { 21 | println!("public ip address: {:?}", ip); 22 | } else { 23 | println!("couldn't get an IP address"); 24 | } 25 | } 26 | ``` 27 | -------------------------------------------------------------------------------- /examples/basic.rs: -------------------------------------------------------------------------------- 1 | #[tokio::main] 2 | async fn main() { 3 | // Attempt to get an IP address and print it. 4 | if let Some(ip) = public_ip::addr().await { 5 | println!("public ip address: {:?}", ip); 6 | } else { 7 | println!("couldn't get an IP address"); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /examples/details.rs: -------------------------------------------------------------------------------- 1 | use std::any::Any; 2 | 3 | use public_ip::{dns, http, Version}; 4 | 5 | #[tokio::main] 6 | async fn main() { 7 | // List of resolvers to try and get an IP address from. 8 | let resolver = &[http::HTTP_IPIFY_ORG, dns::GOOGLE]; 9 | // Attempt to get an IP address and print it. 10 | if let Some((addr, details)) = public_ip::addr_with_details(resolver, Version::Any).await { 11 | // Downcast the HTTP details (if the resolution was from a HTTP resolver). 12 | if let Some(details) = ::downcast_ref::(details.as_ref()) { 13 | println!( 14 | "public ip address {:?} resolved from {} ({:?})", 15 | addr, 16 | details.uri(), 17 | details.server(), 18 | ); 19 | } 20 | // Downcast the DNS details (if the resolution was from a DNS resolver). 21 | if let Some(details) = ::downcast_ref::(details.as_ref()) { 22 | println!( 23 | "public ip address {:?} resolved from {} ({:?})", 24 | addr, 25 | details.name(), 26 | details.server(), 27 | ); 28 | } 29 | } else { 30 | println!("couldn't get an IP address"); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /examples/errors.rs: -------------------------------------------------------------------------------- 1 | use futures_util::{future, StreamExt, TryStreamExt}; 2 | use public_ip::{dns, http, Version}; 3 | 4 | #[tokio::main] 5 | async fn main() { 6 | // List of resolvers to try and get an IP address from. 7 | let resolver = &[http::HTTP_IPIFY_ORG, dns::GOOGLE]; 8 | let addr = public_ip::resolve(resolver, Version::Any) 9 | // For each error in the stream we print it out to STDERR (console). 10 | .inspect_err(|err| eprintln!("resolver error: {}", err)) 11 | // We filter out the errors and leave just the resolved addresses in the stream. 12 | .filter_map(|result| future::ready(result.ok())) 13 | // We get the first resolved address in the stream. 14 | .next() 15 | // Wait for the future to finish. 16 | .await 17 | // We remove the details of the resolution if we don't care about them. 18 | .map(|(addr, _details)| addr); 19 | 20 | dbg!(addr); 21 | } 22 | -------------------------------------------------------------------------------- /src/dns.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; 3 | use std::pin::Pin; 4 | use std::str; 5 | use std::task::{Context, Poll}; 6 | 7 | use futures_core::Stream; 8 | use futures_util::{future, ready, stream, StreamExt}; 9 | use pin_project_lite::pin_project; 10 | use tracing::trace_span; 11 | use tracing_futures::Instrument; 12 | use trust_dns_proto::{ 13 | error::{ProtoError, ProtoErrorKind}, 14 | op::Query, 15 | rr::{Name, RData, RecordType}, 16 | udp::UdpClientStream, 17 | xfer::{DnsHandle, DnsRequestOptions, DnsResponse}, 18 | }; 19 | 20 | #[cfg(feature = "tokio-dns-resolver")] 21 | use tokio::{net::UdpSocket, runtime::Handle}; 22 | #[cfg(feature = "tokio-dns-resolver")] 23 | use trust_dns_client::client::AsyncClient; 24 | 25 | use crate::{Resolutions, Version}; 26 | 27 | /////////////////////////////////////////////////////////////////////////////// 28 | // Hardcoded resolvers 29 | 30 | const DEFAULT_DNS_PORT: u16 = 53; 31 | 32 | /// All builtin DNS resolvers. 33 | pub const ALL: &dyn crate::Resolver<'static> = &&[ 34 | #[cfg(feature = "opendns")] 35 | OPENDNS, 36 | #[cfg(feature = "google")] 37 | GOOGLE, 38 | ]; 39 | 40 | /// Combined OpenDNS IPv4 and IPv6 options. 41 | #[cfg(feature = "opendns")] 42 | #[cfg_attr(docsrs, doc(cfg(feature = "opendns")))] 43 | pub const OPENDNS: &dyn crate::Resolver<'static> = &&[OPENDNS_V4, OPENDNS_V6]; 44 | 45 | /// OpenDNS IPv4 DNS resolver options. 46 | #[cfg(feature = "opendns")] 47 | #[cfg_attr(docsrs, doc(cfg(feature = "opendns")))] 48 | pub const OPENDNS_V4: &dyn crate::Resolver<'static> = &Resolver::new_static( 49 | "myip.opendns.com", 50 | &[ 51 | IpAddr::V4(Ipv4Addr::new(208, 67, 222, 222)), 52 | IpAddr::V4(Ipv4Addr::new(208, 67, 220, 220)), 53 | IpAddr::V4(Ipv4Addr::new(208, 67, 222, 220)), 54 | IpAddr::V4(Ipv4Addr::new(208, 67, 220, 222)), 55 | ], 56 | DEFAULT_DNS_PORT, 57 | QueryMethod::A, 58 | ); 59 | 60 | /// OpenDNS IPv6 DNS resolver options. 61 | #[cfg(feature = "opendns")] 62 | #[cfg_attr(docsrs, doc(cfg(feature = "opendns")))] 63 | pub const OPENDNS_V6: &dyn crate::Resolver<'static> = &Resolver::new_static( 64 | "myip.opendns.com", 65 | &[ 66 | // 2620:0:ccc::2 67 | IpAddr::V6(Ipv6Addr::new(9760, 0, 3276, 0, 0, 0, 0, 2)), 68 | // 2620:0:ccd::2 69 | IpAddr::V6(Ipv6Addr::new(9760, 0, 3277, 0, 0, 0, 0, 2)), 70 | ], 71 | DEFAULT_DNS_PORT, 72 | QueryMethod::AAAA, 73 | ); 74 | 75 | /// Combined Google DNS IPv4 and IPv6 options 76 | #[cfg(feature = "google")] 77 | #[cfg_attr(docsrs, doc(cfg(feature = "google")))] 78 | pub const GOOGLE: &dyn crate::Resolver<'static> = &&[GOOGLE_V4, GOOGLE_V6]; 79 | 80 | /// Google DNS IPv4 DNS resolver options 81 | #[cfg(feature = "google")] 82 | #[cfg_attr(docsrs, doc(cfg(feature = "google")))] 83 | pub const GOOGLE_V4: &dyn crate::Resolver<'static> = &Resolver::new_static( 84 | "o-o.myaddr.l.google.com", 85 | &[ 86 | IpAddr::V4(Ipv4Addr::new(216, 239, 32, 10)), 87 | IpAddr::V4(Ipv4Addr::new(216, 239, 34, 10)), 88 | IpAddr::V4(Ipv4Addr::new(216, 239, 36, 10)), 89 | IpAddr::V4(Ipv4Addr::new(216, 239, 38, 10)), 90 | ], 91 | DEFAULT_DNS_PORT, 92 | QueryMethod::TXT, 93 | ); 94 | 95 | /// Google DNS IPv6 DNS resolver options 96 | #[cfg(feature = "google")] 97 | #[cfg_attr(docsrs, doc(cfg(feature = "google")))] 98 | pub const GOOGLE_V6: &dyn crate::Resolver<'static> = &Resolver::new_static( 99 | "o-o.myaddr.l.google.com", 100 | &[ 101 | // 2001:4860:4802:32::a 102 | IpAddr::V6(Ipv6Addr::new(8193, 18528, 18434, 50, 0, 0, 0, 10)), 103 | // 2001:4860:4802:34::a 104 | IpAddr::V6(Ipv6Addr::new(8193, 18528, 18434, 52, 0, 0, 0, 10)), 105 | // 2001:4860:4802:36::a 106 | IpAddr::V6(Ipv6Addr::new(8193, 18528, 18434, 54, 0, 0, 0, 10)), 107 | // 2001:4860:4802:38::a 108 | IpAddr::V6(Ipv6Addr::new(8193, 18528, 18434, 56, 0, 0, 0, 10)), 109 | ], 110 | DEFAULT_DNS_PORT, 111 | QueryMethod::TXT, 112 | ); 113 | 114 | /////////////////////////////////////////////////////////////////////////////// 115 | // Error 116 | 117 | /// DNS resolver error. 118 | pub type Error = ProtoError; 119 | 120 | /////////////////////////////////////////////////////////////////////////////// 121 | // Details & options 122 | 123 | /// Details produced from a DNS resolution. 124 | #[derive(Debug, Clone)] 125 | pub struct Details { 126 | name: Name, 127 | server: SocketAddr, 128 | method: QueryMethod, 129 | } 130 | 131 | impl Details { 132 | /// DNS name used in the resolution of our IP address. 133 | #[must_use] 134 | pub fn name(&self) -> &Name { 135 | &self.name 136 | } 137 | 138 | /// DNS server used in the resolution of our IP address. 139 | #[must_use] 140 | pub fn server(&self) -> SocketAddr { 141 | self.server 142 | } 143 | 144 | /// The query method used in the resolution of our IP address. 145 | #[must_use] 146 | pub fn query_method(&self) -> QueryMethod { 147 | self.method 148 | } 149 | } 150 | 151 | /// Method used to query an IP address from a DNS server 152 | #[derive(Debug, Clone, Copy, PartialEq)] 153 | #[allow(clippy::upper_case_acronyms)] 154 | pub enum QueryMethod { 155 | /// The first queried `A` name record is extracted as our IP address. 156 | A, 157 | /// The first queried `AAAA` name record is extracted as our IP address. 158 | AAAA, 159 | /// The first `TXT` record is extracted and parsed as our IP address. 160 | TXT, 161 | } 162 | 163 | /////////////////////////////////////////////////////////////////////////////// 164 | // Resolver 165 | 166 | /// Options to build a DNS resolver. 167 | #[derive(Debug)] 168 | pub struct Resolver<'r> { 169 | port: u16, 170 | name: Cow<'r, str>, 171 | servers: Cow<'r, [IpAddr]>, 172 | method: QueryMethod, 173 | } 174 | 175 | impl<'r> Resolver<'r> { 176 | /// Create a new DNS resolver. 177 | pub fn new(name: N, servers: S, port: u16, method: QueryMethod) -> Self 178 | where 179 | N: Into>, 180 | S: Into>, 181 | { 182 | Self { 183 | port, 184 | name: name.into(), 185 | servers: servers.into(), 186 | method, 187 | } 188 | } 189 | } 190 | 191 | impl Resolver<'static> { 192 | /// Create a new DNS resolver from static options. 193 | #[must_use] 194 | pub const fn new_static( 195 | name: &'static str, 196 | servers: &'static [IpAddr], 197 | port: u16, 198 | method: QueryMethod, 199 | ) -> Self { 200 | Self { 201 | port, 202 | name: Cow::Borrowed(name), 203 | servers: Cow::Borrowed(servers), 204 | method, 205 | } 206 | } 207 | } 208 | 209 | impl<'r> crate::Resolver<'r> for Resolver<'r> { 210 | fn resolve(&self, version: Version) -> Resolutions<'r> { 211 | let port = self.port; 212 | let method = self.method; 213 | let name = match Name::from_ascii(self.name.as_ref()) { 214 | Ok(name) => name, 215 | Err(err) => return Box::pin(stream::once(future::ready(Err(crate::Error::new(err))))), 216 | }; 217 | let mut servers: Vec<_> = self 218 | .servers 219 | .iter() 220 | .copied() 221 | .filter(|addr| version.matches(*addr)) 222 | .collect(); 223 | let first_server = match servers.pop() { 224 | Some(server) => server, 225 | None => return Box::pin(stream::empty()), 226 | }; 227 | let record_type = match self.method { 228 | QueryMethod::A => RecordType::A, 229 | QueryMethod::AAAA => RecordType::AAAA, 230 | QueryMethod::TXT => RecordType::TXT, 231 | }; 232 | let span = trace_span!("dns resolver", ?version, ?method, %name, %port); 233 | let query = Query::query(name, record_type); 234 | let stream = resolve(first_server, port, query.clone(), method); 235 | let resolutions = DnsResolutions { 236 | port, 237 | version, 238 | query, 239 | method, 240 | servers, 241 | stream, 242 | }; 243 | Box::pin(resolutions.instrument(span)) 244 | } 245 | } 246 | 247 | /////////////////////////////////////////////////////////////////////////////// 248 | // Resolutions 249 | 250 | pin_project! { 251 | struct DnsResolutions<'r> { 252 | port: u16, 253 | version: Version, 254 | query: Query, 255 | method: QueryMethod, 256 | servers: Vec, 257 | #[pin] 258 | stream: Resolutions<'r>, 259 | } 260 | } 261 | 262 | impl<'r> Stream for DnsResolutions<'r> { 263 | type Item = Result<(IpAddr, crate::Details), crate::Error>; 264 | 265 | fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 266 | match ready!(self.as_mut().project().stream.poll_next(cx)) { 267 | Some(o) => Poll::Ready(Some(o)), 268 | None => self.servers.pop().map_or(Poll::Ready(None), |server| { 269 | self.stream = resolve(server, self.port, self.query.clone(), self.method); 270 | self.project().stream.poll_next(cx) 271 | }), 272 | } 273 | } 274 | } 275 | 276 | /////////////////////////////////////////////////////////////////////////////// 277 | // Client 278 | 279 | #[cfg(feature = "tokio-dns-resolver")] 280 | async fn dns_query( 281 | server: SocketAddr, 282 | query: Query, 283 | query_opts: DnsRequestOptions, 284 | ) -> Result { 285 | let handle = Handle::current(); 286 | let stream = UdpClientStream::::new(server); 287 | let (mut client, bg) = AsyncClient::connect(stream).await?; 288 | handle.spawn(bg); 289 | client 290 | .lookup(query, query_opts) 291 | .next() 292 | .await 293 | .transpose()? 294 | .ok_or_else(|| ProtoErrorKind::Message("expected a response").into()) 295 | } 296 | 297 | fn parse_dns_response( 298 | mut response: DnsResponse, 299 | method: QueryMethod, 300 | ) -> Result { 301 | let answer = match response.take_answers().into_iter().next() { 302 | Some(answer) => answer, 303 | None => return Err(crate::Error::Addr), 304 | }; 305 | match answer.into_data() { 306 | Some(RData::A(addr)) if method == QueryMethod::A => Ok(IpAddr::V4(addr)), 307 | Some(RData::AAAA(addr)) if method == QueryMethod::AAAA => Ok(IpAddr::V6(addr)), 308 | Some(RData::TXT(txt)) if method == QueryMethod::TXT => match txt.iter().next() { 309 | Some(addr_bytes) => Ok(str::from_utf8(&addr_bytes[..])?.parse()?), 310 | None => Err(crate::Error::Addr), 311 | }, 312 | _ => Err(ProtoError::from(ProtoErrorKind::Message("invalid response")).into()), 313 | } 314 | } 315 | 316 | fn resolve<'r>(server: IpAddr, port: u16, query: Query, method: QueryMethod) -> Resolutions<'r> { 317 | let fut = async move { 318 | let name = query.name().clone(); 319 | let server = SocketAddr::new(server, port); 320 | let mut query_opts = DnsRequestOptions::default(); 321 | query_opts.use_edns = true; 322 | let response = dns_query(server, query, query_opts).await?; 323 | let addr = parse_dns_response(response, method)?; 324 | let details = Box::new(Details { 325 | name, 326 | server, 327 | method, 328 | }); 329 | Ok((addr, crate::Details::from(details))) 330 | }; 331 | Box::pin(stream::once( 332 | fut.instrument(trace_span!("query server", %server)), 333 | )) 334 | } 335 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error as StdError; 2 | use std::fmt::Debug; 3 | use std::net::AddrParseError; 4 | use std::str::Utf8Error; 5 | 6 | use thiserror::Error; 7 | 8 | #[cfg(feature = "dns-resolver")] 9 | use crate::dns; 10 | #[cfg(feature = "http-resolver")] 11 | use crate::http; 12 | 13 | /// An error produced while attempting to resolve. 14 | #[derive(Debug, Error)] 15 | #[non_exhaustive] 16 | pub enum Error { 17 | /// No or invalid IP address string found. 18 | #[error("no or invalid IP address string found")] 19 | Addr, 20 | /// IP version not requested was returned. 21 | #[error("IP version not requested was returned")] 22 | Version, 23 | /// DNS resolver error. 24 | #[cfg(feature = "dns-resolver")] 25 | #[cfg_attr(docsrs, doc(cfg(feature = "dns-resolver")))] 26 | #[error("dns resolver: {0}")] 27 | Dns(dns::Error), 28 | /// HTTP resolver error. 29 | #[cfg(feature = "http-resolver")] 30 | #[cfg_attr(docsrs, doc(cfg(feature = "http-resolver")))] 31 | #[error("http resolver: {0}")] 32 | Http(http::Error), 33 | /// Other resolver error. 34 | #[error("other resolver: {0}")] 35 | Other(Box), 36 | } 37 | 38 | impl Error { 39 | /// Construct a new error. 40 | pub fn new(error: E) -> Self 41 | where 42 | E: StdError + Send + Sync + 'static, 43 | { 44 | Self::Other(Box::new(error)) 45 | } 46 | } 47 | 48 | #[cfg(feature = "dns-resolver")] 49 | impl From for Error { 50 | fn from(error: dns::Error) -> Self { 51 | Self::Dns(error) 52 | } 53 | } 54 | 55 | #[cfg(feature = "http-resolver")] 56 | impl From for Error { 57 | fn from(error: http::Error) -> Self { 58 | Self::Http(error) 59 | } 60 | } 61 | 62 | impl From for Error { 63 | fn from(_: Utf8Error) -> Self { 64 | Self::Addr 65 | } 66 | } 67 | 68 | impl From for Error { 69 | fn from(_: AddrParseError) -> Self { 70 | Self::Addr 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/http.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | use std::future::Future; 3 | use std::net::{IpAddr, SocketAddr}; 4 | use std::pin::Pin; 5 | use std::str; 6 | use std::task::{Context, Poll}; 7 | 8 | use futures_core::Stream; 9 | use futures_util::future::BoxFuture; 10 | use futures_util::{future, ready, stream}; 11 | use http::{Response, Uri}; 12 | use hyper::{ 13 | body::{self, Body, Buf}, 14 | client::Builder, 15 | }; 16 | use pin_project_lite::pin_project; 17 | use thiserror::Error; 18 | use tracing::trace_span; 19 | use tracing_futures::Instrument; 20 | 21 | #[cfg(feature = "tokio-http-resolver")] 22 | use hyper::client::connect::{HttpConnector, HttpInfo}; 23 | 24 | #[cfg(feature = "tokio-http-resolver")] 25 | type GaiResolver = hyper_system_resolver::system::Resolver; 26 | 27 | #[cfg(feature = "tower-layer")] 28 | use tower_layer::Layer; 29 | 30 | use crate::{Resolutions, Version}; 31 | 32 | /////////////////////////////////////////////////////////////////////////////// 33 | // Hardcoded resolvers 34 | 35 | /// All builtin HTTP/HTTPS resolvers. 36 | pub const ALL: &dyn crate::Resolver<'static> = &&[ 37 | HTTP, 38 | #[cfg(any( 39 | feature = "https-openssl", 40 | feature = "https-rustls-native", 41 | feature = "https-rustls-webpki" 42 | ))] 43 | HTTPS, 44 | ]; 45 | 46 | /// All builtin HTTP resolvers. 47 | pub const HTTP: &dyn crate::Resolver<'static> = &&[ 48 | #[cfg(feature = "ipify-org")] 49 | HTTP_IPIFY_ORG, 50 | ]; 51 | 52 | /// `http://api.ipify.org` HTTP resolver options 53 | #[cfg(feature = "ipify-org")] 54 | #[cfg_attr(docsrs, doc(cfg(feature = "ipify-org")))] 55 | pub const HTTP_IPIFY_ORG: &dyn crate::Resolver<'static> = 56 | &Resolver::new_static("http://api.ipify.org", ExtractMethod::PlainText); 57 | 58 | #[cfg(any( 59 | feature = "https-openssl", 60 | feature = "https-rustls-native", 61 | feature = "https-rustls-webpki" 62 | ))] 63 | #[cfg_attr( 64 | docsrs, 65 | doc(cfg(any( 66 | feature = "https-openssl", 67 | feature = "https-rustls-native", 68 | feature = "https-rustls-webpki" 69 | ))) 70 | )] 71 | /// All builtin HTTP resolvers. 72 | pub const HTTPS: &dyn crate::Resolver<'static> = &&[ 73 | #[cfg(feature = "ipify-org")] 74 | HTTPS_IPIFY_ORG, 75 | #[cfg(feature = "myip-com")] 76 | HTTPS_MYIP_COM, 77 | #[cfg(feature = "my-ip-io")] 78 | HTTPS_MY_IP_IO, 79 | #[cfg(feature = "seeip-org")] 80 | HTTPS_SEEIP_ORG, 81 | ]; 82 | 83 | /// `http://api.ipify.org` HTTP resolver options 84 | #[cfg(feature = "ipify-org")] 85 | #[cfg_attr(docsrs, doc(cfg(feature = "ipify-org")))] 86 | pub const HTTPS_IPIFY_ORG: &dyn crate::Resolver<'static> = 87 | &Resolver::new_static("https://api.ipify.org", ExtractMethod::PlainText); 88 | 89 | /// `https://api.myip.com` HTTPS resolver options 90 | #[cfg(feature = "myip-com")] 91 | #[cfg_attr(docsrs, doc(cfg(feature = "myip-com")))] 92 | pub const HTTPS_MYIP_COM: &dyn crate::Resolver<'static> = 93 | &Resolver::new_static("https://api.myip.com", ExtractMethod::ExtractJsonIpField); 94 | 95 | /// `https://api.my-ip.io/ip` HTTPS resolver options 96 | #[cfg(feature = "my-ip-io")] 97 | #[cfg_attr(docsrs, doc(cfg(feature = "my-ip-io")))] 98 | pub const HTTPS_MY_IP_IO: &dyn crate::Resolver<'static> = 99 | &Resolver::new_static("https://api.my-ip.io/ip", ExtractMethod::PlainText); 100 | 101 | /// `https://ip.seeip.org` HTTPS resolver options 102 | #[cfg(feature = "seeip-org")] 103 | #[cfg_attr(docsrs, doc(cfg(feature = "seeip-org")))] 104 | pub const HTTPS_SEEIP_ORG: &dyn crate::Resolver<'static> = 105 | &Resolver::new_static("https://ip.seeip.org", ExtractMethod::PlainText); 106 | 107 | /////////////////////////////////////////////////////////////////////////////// 108 | // Error 109 | 110 | /// HTTP resolver error 111 | #[derive(Debug, Error)] 112 | pub enum Error { 113 | /// Client error. 114 | #[error("{0}")] 115 | Client(hyper::Error), 116 | /// URI parsing error. 117 | #[error("{0}")] 118 | Uri(http::uri::InvalidUri), 119 | /// OpenSSL error. 120 | #[cfg(feature = "openssl")] 121 | #[error("{0}")] 122 | Openssl(openssl::error::ErrorStack), 123 | } 124 | 125 | /////////////////////////////////////////////////////////////////////////////// 126 | // Details & options 127 | 128 | /// A resolution produced from a HTTP resolver 129 | #[derive(Debug, Clone)] 130 | pub struct Details { 131 | uri: Uri, 132 | server: SocketAddr, 133 | method: ExtractMethod, 134 | } 135 | 136 | impl Details { 137 | /// URI used in the resolution of the associated IP address 138 | pub fn uri(&self) -> &Uri { 139 | &self.uri 140 | } 141 | 142 | /// HTTP server used in the resolution of our IP address. 143 | pub fn server(&self) -> SocketAddr { 144 | self.server 145 | } 146 | 147 | /// The extract method used in the resolution of the associated IP address 148 | pub fn extract_method(&self) -> ExtractMethod { 149 | self.method 150 | } 151 | } 152 | 153 | /// Method used to extract an IP address from a http response 154 | #[derive(Debug, Clone, Copy)] 155 | pub enum ExtractMethod { 156 | /// Parses the body with whitespace trimmed as the IP address. 157 | PlainText, 158 | /// Parses the body with double quotes and whitespace trimmed as the IP address. 159 | StripDoubleQuotes, 160 | /// Parses the value of the JSON property `"ip"` within the body as the IP address. 161 | /// 162 | /// Note this method does not validate the JSON. 163 | ExtractJsonIpField, 164 | } 165 | 166 | /////////////////////////////////////////////////////////////////////////////// 167 | // Resolver 168 | 169 | /// Options to build a HTTP resolver 170 | #[derive(Debug, Clone)] 171 | pub struct Resolver<'r> { 172 | uri: Cow<'r, str>, 173 | method: ExtractMethod, 174 | } 175 | 176 | impl<'r> Resolver<'r> { 177 | /// Create new HTTP resolver options 178 | pub fn new(uri: U, method: ExtractMethod) -> Self 179 | where 180 | U: Into>, 181 | { 182 | Self { 183 | uri: uri.into(), 184 | method, 185 | } 186 | } 187 | } 188 | 189 | impl Resolver<'static> { 190 | /// Create new HTTP resolver options from static 191 | #[must_use] 192 | pub const fn new_static(uri: &'static str, method: ExtractMethod) -> Self { 193 | Self { 194 | uri: Cow::Borrowed(uri), 195 | method, 196 | } 197 | } 198 | } 199 | 200 | /////////////////////////////////////////////////////////////////////////////// 201 | // Resolutions 202 | 203 | pin_project! { 204 | #[project = HttpResolutionsProj] 205 | enum HttpResolutions<'r> { 206 | HttpRequest { 207 | #[pin] 208 | response: BoxFuture<'r, Result<(IpAddr, crate::Details), crate::Error>>, 209 | }, 210 | Done, 211 | } 212 | } 213 | 214 | impl<'r> Stream for HttpResolutions<'r> { 215 | type Item = Result<(IpAddr, crate::Details), crate::Error>; 216 | 217 | fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 218 | match self.as_mut().project() { 219 | HttpResolutionsProj::HttpRequest { response } => { 220 | let response = ready!(response.poll(cx)); 221 | *self = HttpResolutions::Done; 222 | Poll::Ready(Some(response)) 223 | } 224 | HttpResolutionsProj::Done => Poll::Ready(None), 225 | } 226 | } 227 | } 228 | 229 | async fn resolve( 230 | version: Version, 231 | uri: Uri, 232 | method: ExtractMethod, 233 | ) -> Result<(IpAddr, crate::Details), crate::Error> { 234 | let response = http_get(version, uri.clone()).await?; 235 | // TODO 236 | let server = remote_addr(&response); 237 | let mut body = body::aggregate(response.into_body()) 238 | .await 239 | .map_err(Error::Client)?; 240 | let body = body.copy_to_bytes(body.remaining()); 241 | let body_str = str::from_utf8(body.as_ref())?; 242 | let address_str = match method { 243 | ExtractMethod::PlainText => body_str.trim(), 244 | ExtractMethod::ExtractJsonIpField => extract_json_ip_field(body_str)?, 245 | ExtractMethod::StripDoubleQuotes => body_str.trim().trim_matches('"'), 246 | }; 247 | let address = address_str.parse()?; 248 | let details = Box::new(Details { 249 | uri, 250 | server, 251 | method, 252 | }); 253 | Ok((address, crate::Details::from(details))) 254 | } 255 | 256 | impl<'r> crate::Resolver<'r> for Resolver<'r> { 257 | fn resolve(&self, version: Version) -> Resolutions<'r> { 258 | let method = self.method; 259 | let uri: Uri = match self.uri.as_ref().parse() { 260 | Ok(name) => name, 261 | Err(err) => return Box::pin(stream::once(future::ready(Err(crate::Error::new(err))))), 262 | }; 263 | let span = trace_span!("http resolver", ?version, ?method, %uri); 264 | let resolutions = HttpResolutions::HttpRequest { 265 | response: Box::pin(resolve(version, uri, method)), 266 | }; 267 | Box::pin(resolutions.instrument(span)) 268 | } 269 | } 270 | 271 | fn extract_json_ip_field(s: &str) -> Result<&str, crate::Error> { 272 | s.split_once(r#""ip":"#) 273 | .and_then(|(_, after_prop)| after_prop.split('"').nth(1)) 274 | .ok_or(crate::Error::Addr) 275 | } 276 | 277 | /////////////////////////////////////////////////////////////////////////////// 278 | // Client 279 | 280 | #[cfg(feature = "tokio-http-resolver")] 281 | fn http_connector(version: Version) -> HttpConnector { 282 | use dns_lookup::{AddrFamily, AddrInfoHints, SockType}; 283 | use hyper_system_resolver::system::System; 284 | let hints = match version { 285 | Version::V4 => AddrInfoHints { 286 | address: AddrFamily::Inet.into(), 287 | ..AddrInfoHints::default() 288 | }, 289 | Version::V6 => AddrInfoHints { 290 | address: AddrFamily::Inet6.into(), 291 | ..AddrInfoHints::default() 292 | }, 293 | Version::Any => AddrInfoHints { 294 | socktype: SockType::Stream.into(), 295 | ..AddrInfoHints::default() 296 | }, 297 | }; 298 | let system = System { 299 | addr_info_hints: Some(hints), 300 | service: None, 301 | }; 302 | HttpConnector::new_with_resolver(system.resolver()) 303 | } 304 | 305 | #[cfg(feature = "tokio-http-resolver")] 306 | async fn http_get(version: Version, uri: Uri) -> Result, Error> { 307 | let http = http_connector(version); 308 | 309 | #[cfg(any( 310 | feature = "https-openssl", 311 | feature = "https-rustls-native", 312 | feature = "https-rustls-webpki" 313 | ))] 314 | if uri.scheme() == Some(&http::uri::Scheme::HTTPS) { 315 | let mut http = http; 316 | http.enforce_http(false); 317 | 318 | #[cfg(feature = "https-openssl")] 319 | let connector = hyper_openssl::HttpsLayer::new() 320 | .map(|l| l.layer(http)) 321 | .map_err(Error::Openssl)?; 322 | 323 | #[cfg(feature = "https-rustls-native")] 324 | let connector = hyper_rustls::HttpsConnectorBuilder::new() 325 | .with_native_roots() 326 | .https_only() 327 | .enable_http1() 328 | .wrap_connector(http); 329 | 330 | #[cfg(feature = "https-rustls-webpki")] 331 | let connector = hyper_rustls::HttpsConnectorBuilder::new() 332 | .with_webpki_roots() 333 | .https_only() 334 | .enable_http1() 335 | .wrap_connector(http); 336 | 337 | return Builder::default() 338 | .build::<_, Body>(connector) 339 | .get(uri) 340 | .await 341 | .map_err(Error::Client); 342 | } 343 | 344 | Builder::default() 345 | .build::<_, Body>(http) 346 | .get(uri) 347 | .await 348 | .map_err(Error::Client) 349 | } 350 | 351 | #[cfg(feature = "tokio-http-resolver")] 352 | fn remote_addr(response: &Response) -> SocketAddr { 353 | response 354 | .extensions() 355 | .get::() 356 | .unwrap() 357 | .remote_addr() 358 | } 359 | 360 | #[cfg(test)] 361 | mod tests { 362 | use super::*; 363 | 364 | #[test] 365 | fn test_extract_json_ip_field() { 366 | const VALID: &str = r#"{ 367 | "ip": "123.123.123.123", 368 | }"#; 369 | 370 | const INVALID: &str = r#"{ 371 | "ipp": "123.123.123.123", 372 | }"#; 373 | 374 | const VALID_INVALID: &str = r#"{ 375 | "ip": "123.123.123.123", 376 | "ip": "321.321.321.321", 377 | }"#; 378 | 379 | assert_eq!(extract_json_ip_field(VALID).unwrap(), "123.123.123.123"); 380 | assert_eq!( 381 | extract_json_ip_field(VALID_INVALID).unwrap(), 382 | "123.123.123.123" 383 | ); 384 | assert!(matches!( 385 | extract_json_ip_field(INVALID).unwrap_err(), 386 | crate::Error::Addr 387 | )); 388 | } 389 | } 390 | -------------------------------------------------------------------------------- /src/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 | 33 | mod 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 | ))] 39 | compile_error!("tokio is not enabled and is the only supported runtime currently - consider creating a PR or issue"); 40 | 41 | #[cfg(any( 42 | all(feature = "https-openssl", feature = "https-rustls-native"), 43 | all(feature = "https-openssl", feature = "https-rustls-webpki"), 44 | all(feature = "https-rustls-native", feature = "https-rustls-webpki") 45 | ))] 46 | compile_error!("only one of https-openssl/https-rustls-native/https-rustls-webpki can be enabled"); 47 | 48 | /// DNS resolver support. 49 | #[cfg(feature = "dns-resolver")] 50 | #[cfg_attr(docsrs, doc(cfg(feature = "dns-resolver")))] 51 | pub mod dns; 52 | 53 | /// HTTP resolver support. 54 | #[cfg(feature = "http-resolver")] 55 | #[cfg_attr(docsrs, doc(cfg(feature = "http-resolver")))] 56 | pub mod http; 57 | 58 | use std::any::Any; 59 | use std::net::IpAddr; 60 | #[cfg(any(feature = "dns-resolver", feature = "http-resolver"))] 61 | use std::net::{Ipv4Addr, Ipv6Addr}; 62 | use std::pin::Pin; 63 | use std::slice; 64 | use std::task::{Context, Poll}; 65 | 66 | use futures_core::Stream; 67 | use futures_util::stream::{self, BoxStream, StreamExt, TryStreamExt}; 68 | use futures_util::{future, ready}; 69 | use pin_project_lite::pin_project; 70 | use tracing::trace_span; 71 | use tracing_futures::Instrument; 72 | 73 | pub use crate::error::Error; 74 | 75 | /// The details of a resolution. 76 | /// 77 | /// The internal details can be downcasted through [`Any`]. 78 | pub type Details = Box; 79 | 80 | /// A [`Stream`] of `Result<(IpAddr, Details), Error>`. 81 | pub type Resolutions<'a> = BoxStream<'a, Result<(IpAddr, Details), Error>>; 82 | 83 | /// All builtin resolvers. 84 | #[cfg(any(feature = "dns-resolver", feature = "http-resolver"))] 85 | #[cfg_attr( 86 | docsrs, 87 | doc(cfg(any(feature = "dns-resolver", feature = "http-resolver"))) 88 | )] 89 | pub const ALL: &dyn crate::Resolver<'static> = &&[ 90 | #[cfg(feature = "dns-resolver")] 91 | dns::ALL, 92 | #[cfg(feature = "http-resolver")] 93 | http::ALL, 94 | ]; 95 | 96 | /// The version of IP address to resolve. 97 | #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] 98 | #[non_exhaustive] 99 | pub enum Version { 100 | /// IPv4. 101 | V4, 102 | /// IPv6. 103 | V6, 104 | /// Any version of IP address. 105 | Any, 106 | } 107 | 108 | impl Version { 109 | /// Returns `true` if the provided IP address's version matches `self`. 110 | #[must_use] 111 | pub fn matches(self, addr: IpAddr) -> bool { 112 | self == Version::Any 113 | || (self == Version::V4 && addr.is_ipv4()) 114 | || (self == Version::V6 && addr.is_ipv6()) 115 | } 116 | } 117 | 118 | /////////////////////////////////////////////////////////////////////////////// 119 | 120 | /// Attempts to produce an IP address with all builtin resolvers (best effort). 121 | /// 122 | /// This function will attempt to resolve until the stream is empty and will 123 | /// drop/ignore any resolver errors. 124 | #[cfg(any(feature = "dns-resolver", feature = "http-resolver"))] 125 | #[cfg_attr( 126 | docsrs, 127 | doc(cfg(any(feature = "dns-resolver", feature = "http-resolver"))) 128 | )] 129 | pub async fn addr() -> Option { 130 | addr_with(ALL, Version::Any).await 131 | } 132 | 133 | /// Attempts to produce an IPv4 address with all builtin resolvers (best 134 | /// effort). 135 | /// 136 | /// This function will attempt to resolve until the stream is empty and will 137 | /// drop/ignore any resolver errors. 138 | #[cfg(any(feature = "dns-resolver", feature = "http-resolver"))] 139 | #[cfg_attr( 140 | docsrs, 141 | doc(cfg(any(feature = "dns-resolver", feature = "http-resolver"))) 142 | )] 143 | pub async fn addr_v4() -> Option { 144 | addr_with(ALL, Version::V4).await.map(|addr| match addr { 145 | IpAddr::V4(addr) => addr, 146 | IpAddr::V6(_) => unreachable!(), 147 | }) 148 | } 149 | 150 | /// Attempts to produce an IPv6 address with all builtin resolvers (best 151 | /// effort). 152 | /// 153 | /// This function will attempt to resolve until the stream is empty and will 154 | /// drop/ignore any resolver errors. 155 | #[cfg(any(feature = "dns-resolver", feature = "http-resolver"))] 156 | #[cfg_attr( 157 | docsrs, 158 | doc(cfg(any(feature = "dns-resolver", feature = "http-resolver"))) 159 | )] 160 | pub async fn addr_v6() -> Option { 161 | addr_with(ALL, Version::V6).await.map(|addr| match addr { 162 | IpAddr::V6(addr) => addr, 163 | IpAddr::V4(_) => unreachable!(), 164 | }) 165 | } 166 | 167 | /// Given a [`Resolver`] and requested [`Version`], attempts to produce an IP 168 | /// address (best effort). 169 | /// 170 | /// This function will attempt to resolve until the stream is empty and will 171 | /// drop/ignore any resolver errors. 172 | pub async fn addr_with(resolver: impl Resolver<'_>, version: Version) -> Option { 173 | addr_with_details(resolver, version) 174 | .await 175 | .map(|(addr, _)| addr) 176 | } 177 | 178 | /// Given a [`Resolver`] and requested [`Version`], attempts to produce an IP 179 | /// address along with the details of how it was resolved (best effort). 180 | /// 181 | /// This function will attempt to resolve until the stream is empty and will 182 | /// drop/ignore any resolver errors. 183 | pub async fn addr_with_details( 184 | resolver: impl Resolver<'_>, 185 | version: Version, 186 | ) -> Option<(IpAddr, Details)> { 187 | resolve(resolver, version) 188 | .filter_map(|result| future::ready(result.ok())) 189 | .next() 190 | .await 191 | } 192 | 193 | /// Given a [`Resolver`] and requested [`Version`], produces a stream of [`Resolutions`]. 194 | /// 195 | /// This function also protects against a resolver returning a IP address with a 196 | /// version that was not requested. 197 | pub fn resolve<'r>(resolver: impl Resolver<'r>, version: Version) -> Resolutions<'r> { 198 | let stream = resolver.resolve(version).and_then(move |(addr, details)| { 199 | // If a resolver returns a version not matching the one we requested 200 | // this is an error so it is skipped. 201 | let result = if version.matches(addr) { 202 | Ok((addr, details)) 203 | } else { 204 | Err(Error::Version) 205 | }; 206 | future::ready(result) 207 | }); 208 | Box::pin(stream.instrument(trace_span!("resolve public ip address"))) 209 | } 210 | 211 | /////////////////////////////////////////////////////////////////////////////// 212 | 213 | /// Trait implemented by IP address resolver. 214 | pub trait Resolver<'a>: Send + Sync { 215 | /// Resolves a stream of IP addresses with a given [`Version`]. 216 | fn resolve(&self, version: Version) -> Resolutions<'a>; 217 | } 218 | 219 | impl<'r> Resolver<'r> for &'r dyn Resolver<'r> { 220 | fn resolve(&self, version: Version) -> Resolutions<'r> { 221 | (**self).resolve(version) 222 | } 223 | } 224 | 225 | impl<'r, R> Resolver<'r> for &'r [R] 226 | where 227 | R: Resolver<'r>, 228 | { 229 | fn resolve(&self, version: Version) -> Resolutions<'r> { 230 | pin_project! { 231 | struct DynSliceResolver<'r, R> { 232 | version: Version, 233 | resolvers: slice::Iter<'r, R>, 234 | #[pin] 235 | stream: Resolutions<'r>, 236 | } 237 | } 238 | 239 | impl<'r, R> Stream for DynSliceResolver<'r, R> 240 | where 241 | R: Resolver<'r>, 242 | { 243 | type Item = Result<(IpAddr, Details), Error>; 244 | 245 | fn poll_next( 246 | mut self: Pin<&mut Self>, 247 | cx: &mut Context<'_>, 248 | ) -> Poll> { 249 | match ready!(self.as_mut().project().stream.poll_next(cx)) { 250 | Some(o) => Poll::Ready(Some(o)), 251 | None => self.resolvers.next().map_or(Poll::Ready(None), |next| { 252 | self.stream = next.resolve(self.version); 253 | self.project().stream.poll_next(cx) 254 | }), 255 | } 256 | } 257 | } 258 | 259 | let mut resolvers = self.iter(); 260 | let first_resolver = resolvers.next(); 261 | Box::pin(DynSliceResolver { 262 | version, 263 | resolvers, 264 | stream: match first_resolver { 265 | Some(first) => first.resolve(version), 266 | None => Box::pin(stream::empty()), 267 | }, 268 | }) 269 | } 270 | } 271 | 272 | macro_rules! resolver_array { 273 | () => { 274 | resolver_array!( 275 | 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 276 | ); 277 | }; 278 | ($($n:expr),*) => { 279 | $( 280 | impl<'r> Resolver<'r> for &'r [&'r dyn Resolver<'r>; $n] { 281 | fn resolve(&self, version: Version) -> Resolutions<'r> { 282 | Resolver::resolve(&&self[..], version) 283 | } 284 | } 285 | )* 286 | } 287 | } 288 | 289 | resolver_array!(); 290 | --------------------------------------------------------------------------------