use std::net::IpAddr;
use std::sync::Arc;
use std::time::{Duration, Instant};
use hickory_resolver::config::{ConnectionConfig, NameServerConfig, ProtocolConfig, ResolverOpts};
use hickory_resolver::net::runtime::TokioRuntimeProvider;
use hickory_resolver::net::xfer::{DnsHandle, FirstAnswer};
use hickory_resolver::net::NetError;
use hickory_resolver::proto::op::{DnsRequest, DnsRequestOptions, DnsResponse, Message, Query};
use hickory_resolver::proto::rr::{Name, RecordType as HickoryRecordType};
use hickory_resolver::{ConnectionProvider, PoolContext, TlsConfig};
use tracing::debug;
use super::delegation::is_local_no_route;
use super::resolver::{apply_standard_opts, RESOLVER_ATTEMPTS};
pub(crate) const QUERY_BUDGET_TIMEOUTS: u32 = RESOLVER_ATTEMPTS as u32;
#[derive(Clone)]
pub(crate) struct Transport {
cx: Arc<PoolContext>,
}
impl Transport {
pub(crate) fn new(timeout: Duration) -> Self {
let mut options = ResolverOpts::default();
apply_standard_opts(&mut options, timeout);
let tls =
TlsConfig::new().expect("TLS config cannot fail with the bundled webpki root store");
Self {
cx: Arc::new(PoolContext::new(options, tls)),
}
}
pub(crate) fn timeout(&self) -> Duration {
self.cx.options.timeout
}
pub(crate) fn request(
&self,
name: Name,
record_type: HickoryRecordType,
recursion_desired: bool,
) -> DnsRequest {
let opts = &self.cx.options;
let mut options = DnsRequestOptions::default();
options.recursion_desired = recursion_desired;
options.use_edns = opts.edns0;
options.edns_payload_len = opts.edns_payload_len;
options.case_randomization = opts.case_randomization;
DnsRequest::from_query(Query::query(name, record_type), options)
}
pub(crate) async fn exchange(
&self,
ip: IpAddr,
connection: &ConnectionConfig,
request: &DnsRequest,
) -> Result<Message, NetError> {
let provider = TokioRuntimeProvider::default();
let exchange = async {
let response = self.send(&provider, ip, connection, request).await?;
if !(response.metadata.truncation && connection.protocol == ProtocolConfig::Udp) {
return Ok(response.into_message());
}
debug!(%ip, "truncated UDP reply, repeating the query over TCP");
let response = self
.send(&provider, ip, &tcp_like(connection), request)
.await?;
Ok(response.into_message())
};
tokio::time::timeout(self.timeout(), exchange)
.await
.unwrap_or(Err(NetError::Timeout))
}
async fn send(
&self,
provider: &TokioRuntimeProvider,
ip: IpAddr,
connection: &ConnectionConfig,
request: &DnsRequest,
) -> Result<DnsResponse, NetError> {
let handle = provider.new_connection(ip, connection, &self.cx)?.await?;
handle.send(request.clone()).first_answer().await
}
pub(crate) async fn query(
&self,
servers: &[NameServerConfig],
request: &DnsRequest,
) -> Result<Message, NoResponse> {
let budget = self.timeout().saturating_mul(QUERY_BUDGET_TIMEOUTS);
let started = Instant::now();
let mut failures = Vec::new();
let mut unasked = None;
for (index, server) in servers.iter().enumerate() {
let remaining = budget.saturating_sub(started.elapsed());
if remaining.is_zero() {
unasked = Some(format!(
"gave up after {budget:?} with {} more server(s) unasked",
servers.len() - index
));
break;
}
let Some(connection) = preferred_connection(server) else {
continue;
};
debug!(ip = %server.ip, "querying");
let attempt = self.exchange(server.ip, connection, request);
match tokio::time::timeout(remaining, attempt).await {
Ok(Ok(message)) => return Ok(message),
Ok(Err(e)) => failures.push((server.ip, transport_reason(&e))),
Err(_) => failures.push((server.ip, "timed out".to_string())),
}
}
Err(NoResponse { failures, unasked })
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct NoResponse {
failures: Vec<(IpAddr, String)>,
unasked: Option<String>,
}
impl NoResponse {
#[cfg(test)]
pub(crate) fn timed_out(ip: IpAddr) -> Self {
Self {
failures: vec![(ip, "timed out".to_string())],
unasked: None,
}
}
pub(crate) fn reason(&self) -> String {
match (self.failures.first(), &self.unasked) {
(Some((_, reason)), _) => reason.clone(),
(None, Some(note)) => note.clone(),
(None, None) => NO_NAMESERVER.to_string(),
}
}
}
const NO_NAMESERVER: &str = "no nameserver to query";
impl std::fmt::Display for NoResponse {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let parts: Vec<String> = self
.failures
.iter()
.map(|(ip, reason)| format!("{ip}: {reason}"))
.chain(self.unasked.clone())
.collect();
if parts.is_empty() {
f.write_str(NO_NAMESERVER)
} else {
f.write_str(&parts.join("; "))
}
}
}
fn preferred_connection(server: &NameServerConfig) -> Option<&ConnectionConfig> {
server
.connections
.iter()
.find(|c| c.protocol == ProtocolConfig::Udp)
.or_else(|| server.connections.first())
}
fn tcp_like(udp: &ConnectionConfig) -> ConnectionConfig {
let mut tcp = ConnectionConfig::tcp();
tcp.port = udp.port;
tcp.bind_addr = udp.bind_addr;
tcp
}
pub(crate) fn transport_reason(err: &NetError) -> String {
match err {
NetError::Timeout => "timed out".to_string(),
NetError::Io(io) if is_local_no_route(io) => format!("no route from this host ({io})"),
other => other.to_string(),
}
}
#[cfg(test)]
mod tests {
use std::sync::Mutex;
use hickory_resolver::config::ResolverConfig;
use hickory_resolver::proto::op::ResponseCode;
use hickory_resolver::proto::rr::RData;
use super::*;
use crate::dns::test_support::{
a_rdata, fq_name, spawn_mock_dns, spawn_mock_dns_fn, spawn_mock_dns_fn_with_tcp, MockMode,
MockReply,
};
const LOOPBACK: IpAddr = IpAddr::V4(std::net::Ipv4Addr::LOCALHOST);
fn transport() -> Transport {
Transport::new(Duration::from_millis(300))
}
fn request_for(transport: &Transport, name: &str) -> DnsRequest {
transport.request(fq_name(name), HickoryRecordType::A, true)
}
fn udp_server(port: u16) -> NameServerConfig {
let mut server = NameServerConfig::udp(LOOPBACK);
server.connections[0].port = port;
server
}
fn addresses(message: &Message) -> Vec<String> {
message
.answers
.iter()
.filter_map(|r| match &r.data {
RData::A(a) => Some(a.0.to_string()),
_ => None,
})
.collect()
}
#[test]
fn request_is_built_as_the_resolver_builds_one() {
let transport = transport();
let request = transport.request(fq_name("www.seer.test"), HickoryRecordType::AAAA, true);
assert!(request.metadata.recursion_desired);
assert_eq!(request.queries.len(), 1);
assert_eq!(request.queries[0].name(), &fq_name("www.seer.test"));
assert_eq!(request.queries[0].query_type(), HickoryRecordType::AAAA);
let edns = request
.edns
.as_ref()
.expect("EDNS(0), as the resolver sends");
assert_eq!(edns.max_payload(), ResolverOpts::default().edns_payload_len);
assert!(
!edns.flags().dnssec_ok,
"seer does not ask for DNSSEC records"
);
let hop = transport.request(fq_name("seer.test"), HickoryRecordType::NS, false);
assert!(!hop.metadata.recursion_desired);
assert_eq!(transport.timeout(), Duration::from_millis(300));
}
#[test]
fn a_server_is_asked_over_udp_first_else_its_only_connection() {
let google = ResolverConfig::udp_and_tcp(&hickory_resolver::config::GOOGLE);
for server in google.name_servers() {
assert_eq!(
preferred_connection(server).map(|c| &c.protocol),
Some(&ProtocolConfig::Udp)
);
}
let tls = NameServerConfig::tls(LOOPBACK, Arc::from("dns.seer.test"));
assert!(matches!(
preferred_connection(&tls).map(|c| &c.protocol),
Some(ProtocolConfig::Tls { server_name }) if &**server_name == "dns.seer.test"
));
let https = NameServerConfig::https(LOOPBACK, Arc::from("dns.seer.test"), None);
assert!(matches!(
preferred_connection(&https).map(|c| &c.protocol),
Some(ProtocolConfig::Https { path, .. }) if &**path == "/dns-query"
));
let mut udp = ConnectionConfig::udp();
udp.port = 5353;
let tcp = tcp_like(&udp);
assert_eq!(tcp.protocol, ProtocolConfig::Tcp);
assert_eq!(
tcp.port, 5353,
"a truncated reply is repeated on the same port"
);
}
#[tokio::test]
async fn an_error_rcode_is_a_response_not_a_failure() {
let port = spawn_mock_dns_fn(|_, _| MockReply::Rcode(ResponseCode::ServFail)).await;
let transport = transport();
let request = request_for(&transport, "www.seer.test");
let message = transport
.query(&[udp_server(port)], &request)
.await
.expect("SERVFAIL is what the server said");
assert_eq!(message.metadata.response_code, ResponseCode::ServFail);
assert!(message.metadata.recursion_available);
}
#[tokio::test]
async fn a_truncated_udp_reply_is_repeated_over_tcp() {
let calls = Arc::new(Mutex::new(0));
let seen = Arc::clone(&calls);
let port = spawn_mock_dns_fn_with_tcp(move |_, _| {
let mut n = seen.lock().unwrap();
*n += 1;
if *n == 1 {
MockReply::Truncated
} else {
MockReply::Answer(vec![a_rdata([192, 0, 2, 7])])
}
})
.await;
let transport = transport();
let request = request_for(&transport, "www.seer.test");
let message = transport
.exchange(LOOPBACK, &udp_server(port).connections[0], &request)
.await
.expect("the TCP repeat answers");
assert!(!message.metadata.truncation);
assert_eq!(addresses(&message), ["192.0.2.7"]);
assert_eq!(*calls.lock().unwrap(), 2, "one UDP query, one TCP query");
}
#[tokio::test]
async fn a_server_that_never_answers_passes_the_query_to_the_next() {
let silent = spawn_mock_dns(MockMode::Ignore).await;
let answering =
spawn_mock_dns_fn(|_, _| MockReply::Answer(vec![a_rdata([192, 0, 2, 9])])).await;
let transport = transport();
let request = request_for(&transport, "www.seer.test");
let started = Instant::now();
let message = transport
.query(&[udp_server(silent), udp_server(answering)], &request)
.await
.expect("the second server answers");
assert_eq!(addresses(&message), ["192.0.2.9"]);
assert!(
started.elapsed() < Duration::from_millis(600),
"{:?}",
started.elapsed()
);
}
#[tokio::test]
async fn the_whole_list_shares_one_deadline() {
let silent = spawn_mock_dns(MockMode::Ignore).await;
let answering =
spawn_mock_dns_fn(|_, _| MockReply::Answer(vec![a_rdata([192, 0, 2, 9])])).await;
let transport = transport();
let request = request_for(&transport, "www.seer.test");
let started = Instant::now();
let servers = [
udp_server(silent),
udp_server(silent),
udp_server(answering),
];
let err = transport
.query(&servers, &request)
.await
.expect_err("the deadline ends the list");
let elapsed = started.elapsed();
assert!(elapsed >= Duration::from_millis(600), "{elapsed:?}");
assert!(elapsed < Duration::from_millis(900), "{elapsed:?}");
assert_eq!(
err.to_string(),
"127.0.0.1: timed out; 127.0.0.1: timed out; gave up after 600ms with 1 more \
server(s) unasked"
);
assert_eq!(err.reason(), "timed out");
let none = transport.query(&[], &request).await.unwrap_err();
assert_eq!(none.to_string(), "no nameserver to query");
assert_eq!(none.reason(), "no nameserver to query");
}
}