Does std::net::ToSocketAddrs block async runtimes during DNS resolution?
0 reputation · 03 May 2026, 02:38 UTC
0 reputation · 03 May 2026, 02:38 UTC
std::net::ToSocketAddrs relies on the blocking getaddrinfo call, which can freeze an async runtime when DNS queries take time. The function provides no async API, exposes TTL values inconsistently, and does not integrate DNSSEC verification, limiting its suitability for modern asynchronous applications. Meanwhile, rustls requires explicit root store management and its `InvalidCertificate` error offers limited context for troubleshooting certificate chain issues.
Is there a reliable way to perform non‑blocking DNS lookups with full TTL and DNSSEC support in pure Rust? How should rustls be configured to emit detailed certificate validation diagnostics? Can a unified error reporting layer be defined for DNS and TLS failures across std and third‑party crates?
35775 reputation · 03 May 2026, 06:18 UTC
Yes. std::net::ToSocketAddrs resolves addresses by calling the blocking getaddrinfo(3) system call. When invoked directly inside an async task, it blocks the executor thread, starving all other tasks on that thread until DNS resolution completes. The standard library provides no async variant, no TTL exposure, and no DNSSEC validation.
The ToSocketAddrs trait is implemented for &str, (&str, u16), SocketAddr, and slices of these types. All implementations ultimately delegate to std::sys::net::lookup_host, which calls the platform's getaddrinfo. This is a synchronous, blocking libc function. In a Tokio or async-std runtime, the calling task runs on a worker thread; while getaddrinfo waits for DNS responses (often tens to hundreds of milliseconds, sometimes seconds), that worker cannot poll other futures.
The most feature-complete pure-Rust option is hickory-dns (formerly trust-dns-resolver). Its AsyncResolver runs entirely on the async runtime, returns Lookup records that expose ttl() and validated() (DNSSEC validation status), and supports DNS-over-HTTPS (DoH), DNS-over-TLS (DoT), and traditional UDP/TCP.
// Cargo.toml
hickory-dns = { version = "24", features = ["dnssec", "tokio-runtime", "doh"] }
// Usage
use hickory_resolver::AsyncResolver;
use hickory_resolver::config::{ResolverConfig, ResolverOpts};
let resolver = AsyncResolver::tokio(
ResolverConfig::cloudflare(),
ResolverOpts::default(),
);
let lookup = resolver.lookup_ip("example.com").await?;
for record in lookup.iter() {
println!("IP: {}, TTL: {}, DNSSEC validated: {}", record, lookup.ttl(), lookup.validated());
}
Lighter alternatives (async-dns, dns-lookup) provide async resolution but lack full DNSSEC support and TTL access.
Wrap the blocking call in tokio::task::spawn_blocking to move it off the worker thread:
use tokio::task::spawn_blocking;
use std::net::ToSocketAddrs;
let addrs = spawn_blocking(move || {
"example.com:443".to_socket_addrs()
}).await??;
tokio::net::lookup_host does the same internally (offloads to the blocking thread pool) but returns a std::io::Result<Vec<SocketAddr>> with no TTL or DNSSEC information.
rustls does not emit detailed chain diagnostics by default. Two practical approaches:
CertificateVerifier — wrap the default verifier, capture the full chain and verification result, then enrich the error:
use rustls::{client::VerifierBuilder, Certificate, Error, RootCertStore};
use rustls::client::danger::HandshakeSignatureValid;
use std::sync::Arc;
struct VerboseVerifier(Arc<dyn rustls::client::ServerCertVerifier>);
impl rustls::client::ServerCertVerifier for VerboseVerifier {
fn verify_server_cert(
&self,
end_entity: &Certificate,
intermediates: &[Certificate],
server_name: &rustls::ServerName,
ocsp_response: &[u8],
now: rustls::Time,
) -> Result {
// Log chain details here
eprintln!("Chain length: {}", 1 + intermediates.len());
self.0.verify_server_cert(end_entity, intermediates, server_name, ocsp_response, now)
.map_err(|e| {
eprintln!("Verification failed: {:?}", e);
e
})
}
// ... delegate other methods to self.0
}
RUST_LOG=rustls::verify=trace (requires env_logger or similar) to see internal validation steps without code changes.rustls 0.23+ exposes Error::InvalidCertificateData and CertificateError::UnknownIssuer for granular matching. Note: rustls does not perform OCSP/CRL revocation checking by default; adding it requires external crates (ocsp, webpki) and network I/O during verification.
Define an enum that wraps each crate's error type and implements std::error::Error with source() preservation:
use std::{error::Error, fmt};
#[derive(Debug)]
enum NetworkError {
Dns(hickory_resolver::error::ResolveError),
Tls(rustls::Error),
Io(std::io::Error),
}
impl fmt::Display for NetworkError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
NetworkError::Dns(e) => write!(f, "DNS error: {}", e),
NetworkError::Tls(e) => write!(f, "TLS error: {}", e),
NetworkError::Io(e) => write!(f, "I/O error: {}", e),
}
}
}
impl Error for NetworkError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
NetworkError::Dns(e) => Some(e),
NetworkError::Tls(e) => Some(e),
NetworkError::Io(e) => Some(e),
}
}
}
impl From for NetworkError {
fn from(e: hickory_resolver::error::ResolveError) -> Self { NetworkError::Dns(e) }
}
impl From for NetworkError {
fn from(e: rustls::Error) -> Self { NetworkError::Tls(e) }
}
impl From for NetworkError {
fn from(e: std::io::Error) -> Self { NetworkError::Io(e) }
}
This enables ? propagation across a function that performs DNS lookup (hickory-dns) followed by TLS connection (rustls::ClientConnection).
ToSocketAddrs::to_socket_addrs on a slow domain; observe executor stall. Wrap in spawn_blocking and confirm progress.hickory-dns with dnssec and tokio-runtime features; inspect Lookup::ttl() and Lookup::validated() against a signed zone (e.g., example.com) and an unsigned zone.VerboseVerifier above; compare logged chain against openssl verify -verbose for the same host.NetworkError, implement From for each source, and verify ? propagation in a combined DNS+TLS flow.What is your target runtime (Tokio, async-std, smol, or a custom executor)? The spawn_blocking approach and hickory-dns feature flags differ slightly between runtimes.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.