Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
48 changes: 47 additions & 1 deletion src/async_impl/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,8 @@ struct Config {
#[cfg(feature = "cookies")]
cookie_store: Option<Arc<dyn cookie::CookieStore>>,
hickory_dns: bool,
#[cfg(feature = "hickory-dns")]
ip_filter: fn(std::net::IpAddr) -> bool,
error: Option<crate::Error>,
https_only: bool,
#[cfg(feature = "http3")]
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -327,7 +331,7 @@ impl ClientBuilder {
let mut resolver: Arc<dyn Resolve> = 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"),
};
Expand Down Expand Up @@ -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,
}),
})
}
Expand Down Expand Up @@ -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).
Expand Down Expand Up @@ -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)),
Expand Down Expand Up @@ -2482,6 +2505,8 @@ struct ClientRef {
proxies: Arc<Vec<Proxy>>,
proxies_maybe_http_auth: bool,
https_only: bool,
#[cfg(feature = "hickory-dns")]
ip_filter: fn(IpAddr) -> bool,
}

impl ClientRef {
Expand Down Expand Up @@ -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()),
Expand Down Expand Up @@ -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"))]
Expand Down
27 changes: 25 additions & 2 deletions src/dns/hickory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<OnceCell<TokioAsyncResolver>>,
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)]
Expand All @@ -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)
})
Expand All @@ -47,7 +65,12 @@ impl Iterator for SocketAddrs {
type Item = SocketAddr;

fn next(&mut self) -> Option<Self::Item> {
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));
}
}
}
}

Expand Down
8 changes: 8 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,14 @@ impl Error {
if hyper_err.is_connect() {
return true;
}
} else {
#[cfg(feature = "hickory-dns")]
if err
.downcast_ref::<hickory_resolver::error::ResolveError>()
.is_some()
{
return true;
}
}

source = err.source();
Expand Down
Loading