use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use std::sync::Arc;
use std::time::Duration;
use hickory_resolver::config::{
ConnectionConfig, NameServerConfig, ProtocolConfig, ResolverConfig, ResolverOpts,
};
use hickory_resolver::net::runtime::TokioRuntimeProvider;
use hickory_resolver::proto::rr::RecordType;
use hickory_resolver::{Resolver, TokioResolver};
use crate::error::Error;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DnsVerdict {
InUse,
Absent,
NoAnswer,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct DnsRecords {
pub by_type: Vec<(String, Vec<String>)>,
}
impl DnsRecords {
#[must_use]
pub fn is_empty(&self) -> bool {
self.by_type.is_empty()
}
}
const RECORD_TYPES: &[(RecordType, &str)] = &[
(RecordType::A, "a"),
(RecordType::AAAA, "aaaa"),
(RecordType::NS, "ns"),
(RecordType::MX, "mx"),
(RecordType::TXT, "txt"),
(RecordType::SOA, "soa"),
];
pub(crate) fn build(timeout: Duration) -> Result<TokioResolver, Error> {
let mut options = ResolverOpts::default();
options.timeout = (timeout / 4).max(Duration::from_secs(2));
options.attempts = 2;
options.try_tcp_on_error = true;
if let Ok(builder) = Resolver::builder_tokio()
&& let Ok(resolver) = builder.with_options(options.clone()).build()
{
return Ok(resolver);
}
let servers = [
IpAddr::V4(Ipv4Addr::new(1, 1, 1, 1)),
IpAddr::V4(Ipv4Addr::new(8, 8, 8, 8)),
]
.into_iter()
.map(|ip| {
NameServerConfig::new(
ip,
true,
vec![
ConnectionConfig::new(ProtocolConfig::Udp),
ConnectionConfig::new(ProtocolConfig::Tcp),
],
)
})
.collect();
let config = ResolverConfig::from_parts(None, Vec::new(), servers);
Resolver::builder_with_config(config, TokioRuntimeProvider::default())
.with_options(options)
.build()
.map_err(|source| Error::NetworkUnreachable {
source: Box::new(source),
})
}
pub(crate) async fn query(resolver: &TokioResolver, domain: &str) -> DnsVerdict {
let mut saw_absence = false;
for record_type in [RecordType::NS, RecordType::SOA, RecordType::A] {
match resolver.lookup(domain, record_type).await {
Ok(answer) if !answer.answers().is_empty() => return DnsVerdict::InUse,
Ok(_) => saw_absence = true,
Err(error) => {
if looks_absent(&error.to_string()) {
saw_absence = true;
} else {
return DnsVerdict::NoAnswer;
}
}
}
}
if saw_absence {
DnsVerdict::Absent
} else {
DnsVerdict::NoAnswer
}
}
pub async fn dns_records(resolver: &TokioResolver, domain: &str) -> DnsRecords {
let mut by_type = Vec::new();
for (record_type, label) in RECORD_TYPES {
let Ok(answer) = resolver.lookup(domain, *record_type).await else {
continue;
};
let mut values: Vec<String> = answer
.answers()
.iter()
.filter(|record| record.record_type() == *record_type)
.map(|record| {
let line = record.to_string();
line.split_whitespace()
.skip(4)
.collect::<Vec<_>>()
.join(" ")
})
.filter(|value| !value.is_empty())
.collect();
values.sort_unstable();
values.dedup();
if !values.is_empty() {
by_type.push(((*label).to_owned(), values));
}
}
DnsRecords { by_type }
}
fn looks_absent(message: &str) -> bool {
let message = message.to_lowercase();
let absent = message.contains("no records resolved")
|| message.contains("nxdomain")
|| message.contains("no record");
let unreachable = message.contains("timed out")
|| message.contains("timeout")
|| message.contains("connection")
|| message.contains("no connections");
absent && !unreachable
}
#[derive(Debug)]
pub(crate) struct HttpResolver(TokioResolver);
impl HttpResolver {
#[must_use]
pub(crate) fn new(resolver: &TokioResolver) -> Arc<Self> {
Arc::new(Self(resolver.clone()))
}
}
impl reqwest::dns::Resolve for HttpResolver {
fn resolve(&self, name: reqwest::dns::Name) -> reqwest::dns::Resolving {
let resolver = self.0.clone();
Box::pin(async move {
let resolved = resolver.lookup_ip(name.as_str()).await?;
let sockets: Vec<SocketAddr> =
resolved.iter().map(|ip| SocketAddr::new(ip, 0)).collect();
let addresses: reqwest::dns::Addrs = Box::new(sockets.into_iter());
Ok(addresses)
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_resolver_builds_without_panicking() {
assert!(build(Duration::from_secs(8)).is_ok());
}
#[test]
fn absence_is_told_apart_from_unreachability() {
assert!(looks_absent("no records resolved for Query"));
assert!(looks_absent("NXDOMAIN reported by server"));
assert!(!looks_absent("request timed out"));
assert!(!looks_absent("no connections available"));
assert!(!looks_absent("connection refused"));
assert!(!looks_absent("no records resolved: request timed out"));
}
#[test]
fn dns_has_no_way_to_say_available() {
for verdict in [DnsVerdict::InUse, DnsVerdict::Absent, DnsVerdict::NoAnswer] {
assert!(matches!(
verdict,
DnsVerdict::InUse | DnsVerdict::Absent | DnsVerdict::NoAnswer
));
}
}
#[test]
fn empty_records_report_empty() {
assert!(DnsRecords::default().is_empty());
let filled = DnsRecords {
by_type: vec![("a".to_owned(), vec!["127.0.0.1".to_owned()])],
};
assert!(!filled.is_empty());
}
}