Skip to content
Merged
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
62 changes: 51 additions & 11 deletions src/dns/svcb/system.rs
Original file line number Diff line number Diff line change
Expand Up @@ -244,10 +244,9 @@ async fn lookup_with_resolv_conf(
host: &str,
budget: TimeoutBudget,
) -> Result<Vec<SvcbRecord>, FetchError> {
let contents = match tokio::fs::read_to_string("/etc/resolv.conf").await {
Ok(contents) => contents,
Err(_) => return Ok(Vec::new()),
};
let contents = tokio::fs::read_to_string("/etc/resolv.conf")
.await
.map_err(|err| FetchError::Runtime(format!("read /etc/resolv.conf: {err}")))?;
lookup_with_resolv_conf_config(host, budget, &parse_resolv_conf(&contents)).await
}

Expand All @@ -258,7 +257,9 @@ async fn lookup_with_resolv_conf_config(
config: &ResolvConf,
) -> Result<Vec<SvcbRecord>, FetchError> {
if config.nameservers.is_empty() || config.attempts == 0 {
return Ok(Vec::new());
return Err(FetchError::Runtime(
"system DNS resolver has no usable nameservers or attempts".to_string(),
));
}
let start = if config.rotate {
RESOLVER_ROTATION.fetch_add(1, Ordering::Relaxed) % config.nameservers.len()
Expand Down Expand Up @@ -368,6 +369,7 @@ fn lookup_https_records_blocking(
type DNSServiceFlags = c_uint;

const K_DNS_SERVICE_ERR_NO_ERROR: DNSServiceErrorType = 0;
const K_DNS_SERVICE_ERR_NO_SUCH_RECORD: DNSServiceErrorType = -65554;
const K_DNS_SERVICE_FLAGS_MORE_COMING: DNSServiceFlags = 1;

type DNSServiceQueryRecordReply = unsafe extern "C" fn(
Expand Down Expand Up @@ -436,6 +438,10 @@ fn lookup_https_records_blocking(
return;
}
let state = unsafe { &mut *(context.cast::<QueryState>()) };
if error_code == K_DNS_SERVICE_ERR_NO_SUCH_RECORD {
state.finished = true;
return;
}
if error_code != K_DNS_SERVICE_ERR_NO_ERROR {
state.error = Some(format!("system HTTPS record lookup failed: {error_code}"));
state.finished = true;
Expand Down Expand Up @@ -480,12 +486,16 @@ fn lookup_https_records_blocking(
)
};
if status != K_DNS_SERVICE_ERR_NO_ERROR {
return Ok(Vec::new());
return Err(FetchError::Runtime(format!(
"system DNS query setup failed with status {status}"
)));
}
let _guard = DnsServiceRefGuard(sd_ref);
let fd = unsafe { DNSServiceRefSockFD(sd_ref) };
if fd < 0 {
return Ok(Vec::new());
return Err(FetchError::Runtime(
"system DNS query returned an invalid socket".to_string(),
));
}

let deadline = timeout.and_then(|timeout| Instant::now().checked_add(timeout));
Expand All @@ -494,7 +504,9 @@ fn lookup_https_records_blocking(
break;
}
let Some(timeout_ms) = poll_timeout_ms(deadline) else {
return Ok(Vec::new());
return Err(FetchError::Runtime(
"system DNS query timed out".to_string(),
));
};
let mut pollfd = libc::pollfd {
fd,
Expand All @@ -503,7 +515,9 @@ fn lookup_https_records_blocking(
};
let ready = unsafe { libc::poll(&mut pollfd, 1, timeout_ms) };
if ready == 0 {
return Ok(Vec::new());
return Err(FetchError::Runtime(
"system DNS query timed out".to_string(),
));
}
if ready < 0 {
return Err(FetchError::Runtime(format!(
Expand All @@ -513,7 +527,9 @@ fn lookup_https_records_blocking(
}
let status = unsafe { DNSServiceProcessResult(sd_ref) };
if status != K_DNS_SERVICE_ERR_NO_ERROR {
return Ok(Vec::new());
return Err(FetchError::Runtime(format!(
"system DNS response processing failed with status {status}"
)));
}
}

Expand Down Expand Up @@ -567,7 +583,17 @@ fn lookup_https_records_blocking(
std::ptr::null_mut(),
)
};
if status != 0 || records.is_null() {
const DNS_ERROR_RCODE_NAME_ERROR: u32 = 9003;
const DNS_INFO_NO_RECORDS: u32 = 9501;
if status == DNS_ERROR_RCODE_NAME_ERROR || status == DNS_INFO_NO_RECORDS {
return Ok(Vec::new());
}
if status != 0 {
return Err(FetchError::Runtime(format!(
"system DNS query failed with status {status}"
)));
}
if records.is_null() {
return Ok(Vec::new());
}
let _guard = DnsRecordListGuard(records);
Expand Down Expand Up @@ -794,6 +820,20 @@ mod tests {
assert!(config.rotate);
}

#[cfg(all(unix, not(target_os = "macos")))]
#[tokio::test]
async fn empty_resolver_config_is_a_lookup_failure() {
let err = lookup_with_resolv_conf_config(
"example.com",
TimeoutBudget::new(Some(std::time::Duration::from_secs(1))),
&ResolvConf::default(),
)
.await
.unwrap_err();

assert!(err.to_string().contains("no usable nameservers"));
}

#[cfg(all(unix, not(target_os = "macos")))]
#[tokio::test]
async fn resolv_conf_lookup_fails_over_to_second_nameserver() {
Expand Down
77 changes: 44 additions & 33 deletions src/http/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,7 @@ async fn resolve_dns_for_client_inner(
// through a proxy; auto_http3_allowed already blocks that path.
if effective_proxy.is_some_and(|proxy| !proxy.uses_local_target_dns()) {
if need_ech_svcb {
let https_records = if let Some(dns_server) = cli.dns_server.as_deref() {
let (https_records, _) = if let Some(dns_server) = cli.dns_server.as_deref() {
lookup_ech_https_records(cli, Some(dns_server), host, timeout).await?
} else {
lookup_ech_https_records(cli, None, host, timeout).await?
Expand Down Expand Up @@ -313,14 +313,15 @@ async fn resolve_dns_for_client_inner(

if let Some(dns_server) = cli.dns_server.as_deref() {
let start = Instant::now();
let (addrs, https_lookup) = if need_ech_svcb {
let (addrs, https_lookup, https_lookup_succeeded) = if need_ech_svcb {
// ECH requires HTTPS records; don't use the abort-early auto-H3
// pattern which may discard them before the SVCB query finishes.
let (addrs, https_records) = tokio::join!(
lookup_custom_ips_with_doh_tls(cli, dns_server, host, timeout),
lookup_ech_https_records(cli, Some(dns_server), host, timeout),
);
(addrs, https_records?)
let (https_records, succeeded) = https_records?;
(addrs, https_records, succeeded)
} else if auto_http3 && custom::dns_server_is_authenticated(dns_server, !cli.insecure)? {
// Authenticated HTTPS lookup failures must gate service access.
// Run address and service discovery together, but do not abort or
Expand All @@ -329,24 +330,27 @@ async fn resolve_dns_for_client_inner(
lookup_custom_ips_with_doh_tls(cli, dns_server, host, timeout),
lookup_authenticated_auto_http3_https_records(cli, dns_server, host, timeout),
);
(addrs, https_records?)
(addrs, https_records?, true)
} else if let Some(auto_http3_budget) = auto_http3_discovery {
let https = spawn_auto_http3_https_records(
Some(dns_server.to_string()),
host.to_string(),
Some(auto_http3_budget),
);
let addrs = lookup_custom_ips_with_doh_tls(cli, dns_server, host, timeout).await;
let https_records = if addrs.is_err() {
https.await.unwrap_or_else(|_| empty_https_lookup(host))
let (https_records, succeeded) = if addrs.is_err() {
https
.await
.unwrap_or_else(|_| (empty_https_lookup(host), false))
} else {
take_finished_auto_http3_https_records(https, host).await
};
(addrs, https_records)
(addrs, https_records, succeeded)
} else {
(
lookup_custom_ips_with_doh_tls(cli, dns_server, host, timeout).await,
empty_https_lookup(host),
false,
)
};
let addrs = resolve_custom_alias_fallback(
Expand All @@ -366,7 +370,7 @@ async fn resolve_dns_for_client_inner(
&https_lookup.fallback_target,
&socket_addrs,
);
if auto_http3 {
if auto_http3 && https_lookup_succeeded {
Http3Cache::new()
.store_https_records(url, Some(dns_server), &https_records)
.await;
Expand Down Expand Up @@ -408,22 +412,23 @@ async fn resolve_dns_for_client_inner(
});
let start = Instant::now();
let lookup = tokio::net::lookup_host((host, port));
let (socket_addrs, https_records) = if need_ech_svcb {
let (socket_addrs, https_records, https_lookup_succeeded) = if need_ech_svcb {
// ECH requires HTTPS records; await the SVCB query properly instead
// of using the abort-early auto-H3 pattern.
let (socket_addrs, https_records) =
tokio::join!(lookup, lookup_ech_https_records(cli, None, host, timeout),);
(
socket_addrs
.map(|addrs| addrs.collect::<Vec<_>>())
.map_err(|err| FetchError::Runtime(format!("lookup {host}: {err}"))),
https_records?,
)
let socket_addrs = socket_addrs
.map(|addrs| addrs.collect::<Vec<_>>())
.map_err(|err| FetchError::Runtime(format!("lookup {host}: {err}")));
let (https_records, succeeded) = https_records?;
(socket_addrs, https_records, succeeded)
} else if let Some(auto_http3_budget) = auto_http3_discovery {
let https = spawn_auto_http3_https_records(None, host.to_string(), Some(auto_http3_budget));
let socket_addrs = lookup.await;
let https_records = if socket_addrs.is_err() {
https.await.unwrap_or_else(|_| empty_https_lookup(host))
let (https_records, succeeded) = if socket_addrs.is_err() {
https
.await
.unwrap_or_else(|_| (empty_https_lookup(host), false))
} else {
take_finished_auto_http3_https_records(https, host).await
};
Expand All @@ -432,6 +437,7 @@ async fn resolve_dns_for_client_inner(
.map(|addrs| addrs.collect::<Vec<_>>())
.map_err(|err| FetchError::Runtime(format!("lookup {host}: {err}"))),
https_records,
succeeded,
)
} else {
(
Expand All @@ -440,6 +446,7 @@ async fn resolve_dns_for_client_inner(
.map(|addrs| addrs.collect::<Vec<_>>())
.map_err(|err| FetchError::Runtime(format!("lookup {host}: {err}"))),
empty_https_lookup(host),
false,
)
};
let socket_addrs = resolve_system_alias_fallback(
Expand All @@ -455,7 +462,7 @@ async fn resolve_dns_for_client_inner(
let addrs = dns_timing_addrs(socket_addrs.iter().map(|addr| addr.ip()));
let auto_http3_config =
auto_http3_config_for_records(&https_records, &effective_host, &socket_addrs);
if auto_http3 {
if auto_http3 && https_lookup_succeeded {
Http3Cache::new()
.store_https_records(url, None, &https_records)
.await;
Expand Down Expand Up @@ -563,29 +570,30 @@ async fn lookup_auto_http3_https_records(
dns_server: Option<&str>,
host: &str,
discovery_budget: Option<AutoHttp3DiscoveryBudget>,
) -> HttpsLookup {
) -> (HttpsLookup, bool) {
let Some(timeout) = discovery_budget.and_then(AutoHttp3DiscoveryBudget::remaining) else {
return empty_https_lookup(host);
return (empty_https_lookup(host), false);
};
let resolver = dns_server
.map(HttpsRecordResolver::Custom)
.unwrap_or(HttpsRecordResolver::System);
tokio::time::timeout(
match tokio::time::timeout(
timeout,
crate::dns::svcb::lookup_https_records(resolver, host, Some(timeout)),
)
.await
.ok()
.and_then(Result::ok)
.unwrap_or_else(|| empty_https_lookup(host))
{
Ok(Ok(lookup)) => (lookup, true),
Ok(Err(_)) | Err(_) => (empty_https_lookup(host), false),
}
}

async fn lookup_ech_https_records(
cli: &Cli,
dns_server: Option<&str>,
host: &str,
timeout: TimeoutBudget,
) -> Result<HttpsLookup, FetchError> {
) -> Result<(HttpsLookup, bool), FetchError> {
let ech_timeout = timeout.remaining()?.unwrap_or(Duration::from_secs(5));
let resolver = dns_server
.map(HttpsRecordResolver::Custom)
Expand All @@ -599,14 +607,14 @@ async fn lookup_ech_https_records(
))
.await
{
Ok(lookup) => Ok(lookup),
Ok(lookup) => Ok((lookup, true)),
Err(err) => {
let authenticated = dns_server
.map(|server| custom::dns_server_is_authenticated(server, !cli.insecure))
.transpose()?
.unwrap_or(false);
crate::tls::ech::handle_ech_discovery_error(cli, err, authenticated)?;
Ok(empty_https_lookup(host))
Ok((empty_https_lookup(host), false))
}
}
}
Expand All @@ -632,24 +640,26 @@ fn spawn_auto_http3_https_records(
dns_server: Option<String>,
host: String,
discovery_budget: Option<AutoHttp3DiscoveryBudget>,
) -> JoinHandle<HttpsLookup> {
) -> JoinHandle<(HttpsLookup, bool)> {
tokio::spawn(async move {
lookup_auto_http3_https_records(dns_server.as_deref(), &host, discovery_budget).await
})
}

async fn take_finished_auto_http3_https_records(
handle: JoinHandle<HttpsLookup>,
handle: JoinHandle<(HttpsLookup, bool)>,
host: &str,
) -> HttpsLookup {
) -> (HttpsLookup, bool) {
if !handle.is_finished() {
tokio::task::yield_now().await;
}
if handle.is_finished() {
handle.await.unwrap_or_else(|_| empty_https_lookup(host))
handle
.await
.unwrap_or_else(|_| (empty_https_lookup(host), false))
} else {
handle.abort();
empty_https_lookup(host)
(empty_https_lookup(host), false)
}
}

Expand Down Expand Up @@ -789,7 +799,8 @@ pub(crate) async fn resolve_websocket_ech_mode(
if host.parse::<IpAddr>().is_ok() {
return Ok(None);
}
let records = lookup_ech_https_records(cli, cli.dns_server.as_deref(), host, timeout).await?;
let (records, _) =
lookup_ech_https_records(cli, cli.dns_server.as_deref(), host, timeout).await?;
let candidates = ech_candidates_from_records(&records.records);
crate::tls::ech::resolve_ech_mode(cli, &candidates)
}
Expand Down
Loading