use std::net::{IpAddr, SocketAddr};
use std::time::Duration;
use rand::RngExt;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::{TcpStream, UdpSocket};
use super::wire::{self, Answer, Response};
pub const DEFAULT_PORT: u16 = 53;
const RESOLV_CONF: &str = "/etc/resolv.conf";
const RECV_BUFFER: usize = 4096;
const MAX_TCP_MESSAGE: usize = 65535;
pub struct Query {
pub name: String,
pub qtype: u16,
pub servers: Vec<SocketAddr>,
pub timeout: Duration,
pub tries: u32,
}
pub async fn resolve(q: &Query) -> Result<Vec<Answer>, String> {
if q.servers.is_empty() {
return Err("no nameservers configured".to_string());
}
let mut last = String::new();
for _ in 0..q.tries.max(1) {
for server in &q.servers {
match ask(server, q).await {
Ok(answers) => return Ok(answers),
Err(e) => last = format!("{server}: {e}"),
}
}
}
if q.tries > 1 {
return Err(format!("{last} (after {} tries)", q.tries));
}
Err(last)
}
async fn ask(server: &SocketAddr, q: &Query) -> Result<Vec<Answer>, String> {
let id = rand::rng().random::<u16>();
let message = wire::encode_query(id, &q.name, q.qtype)?;
let over_udp = exchange_udp(server, &message, id, q.qtype, q.timeout).await?;
if !over_udp.truncated {
return Ok(over_udp.answers);
}
Ok(exchange_tcp(server, &message, id, q.qtype, q.timeout)
.await?
.answers)
}
async fn exchange_udp(
server: &SocketAddr,
message: &[u8],
id: u16,
qtype: u16,
timeout: Duration,
) -> Result<Response, String> {
let bind: SocketAddr = if server.is_ipv6() {
"[::]:0".parse().expect("literal is a valid address")
} else {
"0.0.0.0:0".parse().expect("literal is a valid address")
};
let socket = UdpSocket::bind(bind)
.await
.map_err(|e| format!("bind: {e}"))?;
socket
.connect(*server)
.await
.map_err(|e| format!("connect: {e}"))?;
socket
.send(message)
.await
.map_err(|e| format!("send: {e}"))?;
let deadline = tokio::time::Instant::now() + timeout;
let mut buf = vec![0u8; RECV_BUFFER];
loop {
let read = tokio::time::timeout_at(deadline, socket.recv(&mut buf))
.await
.map_err(|_| format!("timeout after {}ms", timeout.as_millis()))?
.map_err(|e| format!("recv: {e}"))?;
if wire::message_id(&buf[..read]) != Some(id) {
continue;
}
return wire::decode_response(&buf[..read], qtype);
}
}
async fn exchange_tcp(
server: &SocketAddr,
message: &[u8],
id: u16,
qtype: u16,
timeout: Duration,
) -> Result<Response, String> {
let deadline = tokio::time::Instant::now() + timeout;
let expired = || format!("timeout after {}ms", timeout.as_millis());
let mut stream = tokio::time::timeout_at(deadline, TcpStream::connect(*server))
.await
.map_err(|_| expired())?
.map_err(|e| format!("tcp connect: {e}"))?;
let mut framed = (message.len() as u16).to_be_bytes().to_vec();
framed.extend_from_slice(message);
tokio::time::timeout_at(deadline, stream.write_all(&framed))
.await
.map_err(|_| expired())?
.map_err(|e| format!("tcp write: {e}"))?;
let mut length = [0u8; 2];
tokio::time::timeout_at(deadline, stream.read_exact(&mut length))
.await
.map_err(|_| expired())?
.map_err(|e| format!("tcp read: {e}"))?;
let len = u16::from_be_bytes(length) as usize;
if len == 0 || len > MAX_TCP_MESSAGE {
return Err(format!("tcp answer declares {len} bytes"));
}
let mut buf = vec![0u8; len];
tokio::time::timeout_at(deadline, stream.read_exact(&mut buf))
.await
.map_err(|_| expired())?
.map_err(|e| format!("tcp read: {e}"))?;
if wire::message_id(&buf) != Some(id) {
return Err("tcp answer carries the wrong transaction id".to_string());
}
wire::decode_response(&buf, qtype)
}
pub fn system_servers() -> Result<Vec<SocketAddr>, String> {
let text = std::fs::read_to_string(RESOLV_CONF)
.map_err(|e| format!("cannot read {RESOLV_CONF}: {e}"))?;
let servers = parse_resolv_conf(&text);
if servers.is_empty() {
return Err(format!("{RESOLV_CONF} names no usable nameserver"));
}
Ok(servers)
}
pub fn parse_resolv_conf(text: &str) -> Vec<SocketAddr> {
let mut servers = Vec::new();
for line in text.lines() {
let body = match line.find(['#', ';']) {
Some(at) => &line[..at],
None => line,
};
let mut fields = body.split_whitespace();
if fields.next() != Some("nameserver") {
continue;
}
let Some(address) = fields.next() else {
continue;
};
let bare = address.split_once('%').map_or(address, |(ip, _)| ip);
if let Ok(ip) = bare.parse::<IpAddr>() {
servers.push(SocketAddr::new(ip, DEFAULT_PORT));
}
}
servers
}
pub fn parse_server(spec: &str) -> Result<SocketAddr, String> {
let spec = spec.trim();
if let Ok(addr) = spec.parse::<SocketAddr>() {
return Ok(addr);
}
if let Ok(ip) = spec.parse::<IpAddr>() {
return Ok(SocketAddr::new(ip, DEFAULT_PORT));
}
Err(format!(
"'{spec}' is not a nameserver address — want an IP, 'IP:port', or '[v6]:port'"
))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn nameservers_are_read_in_the_order_the_file_lists_them() {
let conf = "\
# generated by something
search corp.example.com example.com
nameserver 10.0.0.53
options edns0 trust-ad
nameserver 10.0.1.53
domain corp.example.com
";
assert_eq!(
parse_resolv_conf(conf),
vec![
"10.0.0.53:53".parse::<SocketAddr>().unwrap(),
"10.0.1.53:53".parse::<SocketAddr>().unwrap(),
]
);
}
#[test]
fn comments_and_unusable_entries_are_skipped() {
let conf = "\
#nameserver 1.1.1.1
nameserver 8.8.8.8 # the one in use
; nameserver 9.9.9.9
nameserver not-an-address
nameserver
nameserver fe80::1%eth0
";
assert_eq!(
parse_resolv_conf(conf),
vec![
"8.8.8.8:53".parse::<SocketAddr>().unwrap(),
"[fe80::1]:53".parse::<SocketAddr>().unwrap(),
]
);
}
#[test]
fn a_server_without_a_port_gets_the_standard_one() {
assert_eq!(
parse_server("1.1.1.1").unwrap(),
"1.1.1.1:53".parse::<SocketAddr>().unwrap()
);
assert_eq!(
parse_server("1.1.1.1:5353").unwrap(),
"1.1.1.1:5353".parse::<SocketAddr>().unwrap()
);
assert_eq!(
parse_server("[2606:4700:4700::1111]:5353").unwrap(),
"[2606:4700:4700::1111]:5353".parse::<SocketAddr>().unwrap()
);
assert_eq!(
parse_server("2606:4700:4700::1111").unwrap(),
"[2606:4700:4700::1111]:53".parse::<SocketAddr>().unwrap()
);
assert!(parse_server("resolver.example.com").is_err());
}
}