diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 37a324c98..d66a4e082 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -298,9 +298,10 @@ jobs: cargo update -p tokio --precise 1.29.1 cargo update -p tokio-util --precise 0.7.11 cargo update -p idna_adapter --precise 1.1.0 - cargo update -p hashbrown@0.15.2 --precise 0.15.0 + cargo update -p hashbrown --precise 0.15.0 cargo update -p native-tls --precise 0.2.13 cargo update -p once_cell --precise 1.20.3 + cargo update -p tracing-core --precise 0.1.33 - uses: Swatinem/rust-cache@v2 diff --git a/Cargo.toml b/Cargo.toml index 1123e8ffc..1bf903edb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -141,7 +141,7 @@ tokio-native-tls = { version = "0.3.0", optional = true } # rustls-tls hyper-rustls = { version = "0.27.0", default-features = false, optional = true, features = ["http1", "tls12"] } rustls = { version = "0.23.4", optional = true, default-features = false, features = ["std", "tls12"] } -rustls-pki-types = { version = "1.1.0", features = ["alloc"] ,optional = true } +rustls-pki-types = { version = "1.11.0", features = ["alloc"], optional = true } tokio-rustls = { version = "0.26", optional = true, default-features = false, features = ["tls12"] } webpki-roots = { version = "0.26.0", optional = true } rustls-native-certs = { version = "0.8.0", optional = true } diff --git a/src/async_impl/client.rs b/src/async_impl/client.rs index 02266c31e..859bacd66 100644 --- a/src/async_impl/client.rs +++ b/src/async_impl/client.rs @@ -164,6 +164,8 @@ struct Config { #[cfg(feature = "cookies")] cookie_store: Option>, hickory_dns: bool, + #[cfg(feature = "hickory-dns")] + ip_filter: fn(std::net::IpAddr) -> bool, error: Option, https_only: bool, #[cfg(feature = "http3")] @@ -270,6 +272,8 @@ impl ClientBuilder { interface: None, nodelay: true, hickory_dns: cfg!(feature = "hickory-dns"), + #[cfg(feature = "hickory-dns")] + ip_filter: |_| true, #[cfg(feature = "cookies")] cookie_store: None, https_only: false, @@ -327,7 +331,7 @@ impl ClientBuilder { let mut resolver: Arc = match config.hickory_dns { false => Arc::new(GaiResolver::new()), #[cfg(feature = "hickory-dns")] - true => Arc::new(HickoryDnsResolver::default()), + true => Arc::new(HickoryDnsResolver::new(config.ip_filter)), #[cfg(not(feature = "hickory-dns"))] true => unreachable!("hickory-dns shouldn't be enabled unless the feature is"), }; @@ -843,6 +847,8 @@ impl ClientBuilder { proxies, proxies_maybe_http_auth, https_only: config.https_only, + #[cfg(feature = "hickory-dns")] + ip_filter: config.ip_filter, }), }) } @@ -1889,6 +1895,17 @@ impl ClientBuilder { } } + /// Adds a filter for valid IP addresses during DNS lookup. + /// + /// # Optional + /// + /// This requires the optional `hickory-dns` feature to be enabled. + #[cfg(feature = "hickory-dns")] + pub fn ip_filter(mut self, filter: fn(std::net::IpAddr) -> bool) -> ClientBuilder { + self.config.ip_filter = filter; + self + } + /// Override DNS resolution for specific domains to a particular IP address. /// /// Set the port to `0` to use the conventional port for the given scheme (e.g. 80 for http). @@ -2211,6 +2228,12 @@ impl Client { } } + #[cfg(feature = "hickory-dns")] + if let Err(err) = validate_url(self.inner.ip_filter, &url) { + return Pending { + inner: PendingInner::Error(Some(err)), + }; + } let uri = match try_uri(&url) { Ok(uri) => uri, _ => return Pending::new_err(error::url_invalid_uri(url)), @@ -2482,6 +2505,8 @@ struct ClientRef { proxies: Arc>, proxies_maybe_http_auth: bool, https_only: bool, + #[cfg(feature = "hickory-dns")] + ip_filter: fn(IpAddr) -> bool, } impl ClientRef { @@ -2853,6 +2878,12 @@ impl Future for PendingRequest { std::mem::replace(self.as_mut().headers(), HeaderMap::new()); remove_sensitive_headers(&mut headers, &self.url, &self.urls); + + #[cfg(feature = "hickory-dns")] + if let Err(err) = validate_url(self.client.ip_filter, &self.url) { + return Poll::Ready(Err(err)); + } + let uri = try_uri(&self.url)?; let body = match self.body { Some(Some(ref body)) => Body::reusable(body.clone()), @@ -2951,6 +2982,21 @@ fn add_cookie_header(headers: &mut HeaderMap, cookie_store: &dyn cookie::CookieS } } +#[cfg(feature = "hickory-dns")] +fn validate_url(ip_filter: fn(IpAddr) -> bool, url: &Url) -> Result<(), crate::Error> { + let is_valid_ip = match url.host() { + Some(url::Host::Ipv4(ip)) => (ip_filter)(IpAddr::V4(ip)), + Some(url::Host::Ipv6(ip)) => (ip_filter)(IpAddr::V6(ip)), + _ => true, + }; + + if !is_valid_ip { + let e = hickory_resolver::error::ResolveError::from("destination is restricted"); + return Err(crate::Error::new(crate::error::Kind::Request, Some(e))); + } + Ok(()) +} + #[cfg(test)] mod tests { #![cfg(not(feature = "rustls-tls-manual-roots-no-provider"))] diff --git a/src/dns/hickory.rs b/src/dns/hickory.rs index a94160b2d..756c85210 100644 --- a/src/dns/hickory.rs +++ b/src/dns/hickory.rs @@ -13,16 +13,27 @@ use std::sync::Arc; use super::{Addrs, Name, Resolve, Resolving}; /// Wrapper around an `AsyncResolver`, which implements the `Resolve` trait. -#[derive(Debug, Default, Clone)] +#[derive(Debug, Clone)] pub(crate) struct HickoryDnsResolver { /// Since we might not have been called in the context of a /// Tokio Runtime in initialization, so we must delay the actual /// construction of the resolver. state: Arc>, + filter: fn(std::net::IpAddr) -> bool, } struct SocketAddrs { iter: LookupIpIntoIter, + filter: fn(std::net::IpAddr) -> bool, +} + +impl HickoryDnsResolver { + pub fn new(filter: fn(std::net::IpAddr) -> bool) -> Self { + Self { + state: Default::default(), + filter, + } + } } #[derive(Debug)] @@ -32,11 +43,18 @@ impl Resolve for HickoryDnsResolver { fn resolve(&self, name: Name) -> Resolving { let resolver = self.clone(); Box::pin(async move { + let filter = resolver.filter; let resolver = resolver.state.get_or_try_init(new_resolver)?; let lookup = resolver.lookup_ip(name.as_str()).await?; + if !lookup.iter().any(filter) { + let e = hickory_resolver::error::ResolveError::from("destination is restricted"); + return Err(e.into()); + } + let addrs: Addrs = Box::new(SocketAddrs { iter: lookup.into_iter(), + filter, }); Ok(addrs) }) @@ -47,7 +65,12 @@ impl Iterator for SocketAddrs { type Item = SocketAddr; fn next(&mut self) -> Option { - self.iter.next().map(|ip_addr| SocketAddr::new(ip_addr, 0)) + loop { + let ip_addr = self.iter.next()?; + if (self.filter)(ip_addr) { + return Some(SocketAddr::new(ip_addr, 0)); + } + } } } diff --git a/src/error.rs b/src/error.rs index fd044814d..cf71bc145 100644 --- a/src/error.rs +++ b/src/error.rs @@ -131,6 +131,14 @@ impl Error { if hyper_err.is_connect() { return true; } + } else { + #[cfg(feature = "hickory-dns")] + if err + .downcast_ref::() + .is_some() + { + return true; + } } source = err.source();