From 15086284f1260d1cbb8006e5ae528682a1776f08 Mon Sep 17 00:00:00 2001 From: Ryan Fowler Date: Mon, 27 Jul 2026 23:28:14 +0000 Subject: [PATCH 1/2] Enforce timeout for system DNS resolution --- src/net.rs | 83 +++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 64 insertions(+), 19 deletions(-) diff --git a/src/net.rs b/src/net.rs index 493971f..99a4344 100644 --- a/src/net.rs +++ b/src/net.rs @@ -122,26 +122,55 @@ pub(crate) async fn resolve_host_with_doh_tls( doh_tls_config: Option, timeout: TimeoutBudget, ) -> Result, FetchError> { - if let Ok(ip) = host.parse::() { - return Ok(vec![SocketAddr::new(ip, 0)]); - } - let Some(dns_server) = dns_server else { - return tokio::net::lookup_host((host, 0)) - .await - .map(|addrs| addrs.collect()) - .map_err(|err| FetchError::Runtime(format!("lookup {host}: {err}"))); - }; + resolve_host_with_doh_tls_using( + host, + dns_server, + doh_tls_config, + timeout, + |host| async move { + tokio::net::lookup_host((host, 0)) + .await + .map(|addrs| addrs.collect()) + }, + ) + .await +} - let addrs = if is_doh_dns_server(dns_server) { - let shared_doh = shared_doh_resolver(dns_server, host, timeout, doh_tls_config.as_ref())?; - resolve_doh_ips(host, dns_server, Some(&shared_doh), timeout).await? - } else { - crate::dns::custom::lookup_ips(dns_server, host, timeout.remaining()?).await? - }; - Ok(addrs - .into_iter() - .map(|addr| SocketAddr::new(addr, 0)) - .collect()) +async fn resolve_host_with_doh_tls_using( + host: &str, + dns_server: Option<&str>, + doh_tls_config: Option, + timeout: TimeoutBudget, + system_lookup: F, +) -> Result, FetchError> +where + F: FnOnce(String) -> Fut, + Fut: Future>>, +{ + timeout + .run(async move { + if let Ok(ip) = host.parse::() { + return Ok(vec![SocketAddr::new(ip, 0)]); + } + let Some(dns_server) = dns_server else { + return system_lookup(host.to_owned()) + .await + .map_err(|err| FetchError::Runtime(format!("lookup {host}: {err}"))); + }; + + let addrs = if is_doh_dns_server(dns_server) { + let shared_doh = + shared_doh_resolver(dns_server, host, timeout, doh_tls_config.as_ref())?; + resolve_doh_ips(host, dns_server, Some(&shared_doh), timeout).await? + } else { + crate::dns::custom::lookup_ips(dns_server, host, timeout.remaining()?).await? + }; + Ok(addrs + .into_iter() + .map(|addr| SocketAddr::new(addr, 0)) + .collect()) + }) + .await } async fn resolve_host_family( @@ -1408,6 +1437,22 @@ async fn timeout_fetch( mod tests { use super::*; + #[tokio::test] + async fn system_host_resolution_honors_timeout_budget() { + let timeout = Duration::from_millis(10); + let err = resolve_host_with_doh_tls_using( + "example.com", + None, + None, + TimeoutBudget::new(Some(timeout)), + |_| std::future::pending::>>(), + ) + .await + .unwrap_err(); + + assert_eq!(err.to_string(), "request timed out after 10ms"); + } + #[test] fn host_header_value_brackets_ipv6_literals() { let url = Url::parse("http://[::1]/").unwrap(); From b1a7be859cfe17ab310b94690875ad96db9bf067 Mon Sep 17 00:00:00 2001 From: Ryan Fowler Date: Tue, 28 Jul 2026 00:14:15 +0000 Subject: [PATCH 2/2] Avoid DNS timeout stack growth --- src/net.rs | 82 ++++++++++++++++++++++++------------------------------ 1 file changed, 37 insertions(+), 45 deletions(-) diff --git a/src/net.rs b/src/net.rs index 99a4344..b4388bb 100644 --- a/src/net.rs +++ b/src/net.rs @@ -122,53 +122,47 @@ pub(crate) async fn resolve_host_with_doh_tls( doh_tls_config: Option, timeout: TimeoutBudget, ) -> Result, FetchError> { - resolve_host_with_doh_tls_using( - host, - dns_server, - doh_tls_config, - timeout, - |host| async move { - tokio::net::lookup_host((host, 0)) - .await - .map(|addrs| addrs.collect()) - }, - ) - .await + if let Ok(ip) = host.parse::() { + return Ok(vec![SocketAddr::new(ip, 0)]); + } + let Some(dns_server) = dns_server else { + return resolve_system_host_with( + host, + timeout, + Box::pin(async move { + tokio::net::lookup_host((host, 0)) + .await + .map(|addrs| addrs.collect()) + }), + ) + .await; + }; + + let addrs = if is_doh_dns_server(dns_server) { + let shared_doh = shared_doh_resolver(dns_server, host, timeout, doh_tls_config.as_ref())?; + resolve_doh_ips(host, dns_server, Some(&shared_doh), timeout).await? + } else { + crate::dns::custom::lookup_ips(dns_server, host, timeout.remaining()?).await? + }; + Ok(addrs + .into_iter() + .map(|addr| SocketAddr::new(addr, 0)) + .collect()) } -async fn resolve_host_with_doh_tls_using( +type SystemLookupFuture<'a> = + Pin>> + Send + 'a>>; + +async fn resolve_system_host_with( host: &str, - dns_server: Option<&str>, - doh_tls_config: Option, timeout: TimeoutBudget, - system_lookup: F, -) -> Result, FetchError> -where - F: FnOnce(String) -> Fut, - Fut: Future>>, -{ + system_lookup: SystemLookupFuture<'_>, +) -> Result, FetchError> { timeout .run(async move { - if let Ok(ip) = host.parse::() { - return Ok(vec![SocketAddr::new(ip, 0)]); - } - let Some(dns_server) = dns_server else { - return system_lookup(host.to_owned()) - .await - .map_err(|err| FetchError::Runtime(format!("lookup {host}: {err}"))); - }; - - let addrs = if is_doh_dns_server(dns_server) { - let shared_doh = - shared_doh_resolver(dns_server, host, timeout, doh_tls_config.as_ref())?; - resolve_doh_ips(host, dns_server, Some(&shared_doh), timeout).await? - } else { - crate::dns::custom::lookup_ips(dns_server, host, timeout.remaining()?).await? - }; - Ok(addrs - .into_iter() - .map(|addr| SocketAddr::new(addr, 0)) - .collect()) + system_lookup + .await + .map_err(|err| FetchError::Runtime(format!("lookup {host}: {err}"))) }) .await } @@ -1440,12 +1434,10 @@ mod tests { #[tokio::test] async fn system_host_resolution_honors_timeout_budget() { let timeout = Duration::from_millis(10); - let err = resolve_host_with_doh_tls_using( + let err = resolve_system_host_with( "example.com", - None, - None, TimeoutBudget::new(Some(timeout)), - |_| std::future::pending::>>(), + Box::pin(std::future::pending::>>()), ) .await .unwrap_err();