use hickory_resolver::{
TokioResolver,
config::{ConnectionConfig, NameServerConfig, ResolverConfig},
net::runtime::TokioRuntimeProvider,
proto::rr::{RData, RecordType},
};
use std::{net::IpAddr, sync::Arc};
use crate::prelude::*;
use cloudillo_types::address::AddressType;
const ROOT_SERVERS: [&str; 13] = [
"198.41.0.4", "199.9.14.201", "192.33.4.12", "199.7.91.13", "192.203.230.10", "192.5.5.241", "192.112.36.4", "198.97.190.53", "192.36.148.17", "192.58.128.30", "193.0.14.129", "199.7.83.42", "202.12.27.33", ];
pub struct DnsResolver {}
impl DnsResolver {
pub fn new() -> ClResult<Self> {
debug!("Created DNS resolver with {} root servers", ROOT_SERVERS.len());
Ok(Self {})
}
#[expect(clippy::unused_self, reason = "method for consistency")]
fn create_resolver_for_ns(&self, ns_ips: &[IpAddr]) -> ClResult<TokioResolver> {
let name_servers = ns_ips
.iter()
.map(|ip| NameServerConfig::new(*ip, true, vec![ConnectionConfig::udp()]))
.collect();
let config = ResolverConfig::from_parts(None, vec![], name_servers);
TokioResolver::builder_with_config(config, TokioRuntimeProvider::default())
.build()
.map_err(|e| Error::ValidationError(format!("dns resolver build: {e}")))
}
async fn resolve_ns_to_ips(
&self,
ns_names: &[String],
resolver: &TokioResolver,
) -> Vec<IpAddr> {
let mut ips = Vec::new();
for ns_name in ns_names {
if let Ok(lookup) = resolver.lookup_ip(ns_name.as_str()).await {
for ip in lookup.iter() {
ips.push(ip);
}
}
}
ips
}
async fn find_authoritative_ns(&self, domain: &str) -> ClResult<Vec<IpAddr>> {
let labels: Vec<&str> = domain.trim_end_matches('.').split('.').collect();
let mut current_ns_ips: Vec<IpAddr> =
ROOT_SERVERS.iter().filter_map(|ip| ip.parse().ok()).collect();
let mut current_resolver = self.create_resolver_for_ns(¤t_ns_ips)?;
for i in (0..labels.len()).rev() {
let subdomain = labels[i..].join(".") + ".";
debug!(subdomain = %subdomain, "Looking up NS for zone");
match current_resolver.lookup(subdomain.as_str(), RecordType::NS).await {
Ok(ns_lookup) => {
let mut ns_names: Vec<String> = Vec::new();
let mut glue_ips: Vec<IpAddr> = Vec::new();
for record in ns_lookup.answers().iter().chain(ns_lookup.authorities()) {
if let RData::NS(ns) = &record.data {
let ns_name = ns.0.to_string();
debug!(subdomain = %subdomain, ns = %ns_name, "Found NS record");
ns_names.push(ns_name);
}
}
for record in ns_lookup.additionals().iter().chain(ns_lookup.authorities()) {
match &record.data {
RData::A(a) => glue_ips.push(IpAddr::V4(a.0)),
RData::AAAA(aaaa) => glue_ips.push(IpAddr::V6(aaaa.0)),
_ => {}
}
}
if !ns_names.is_empty() {
let ns_ips = if glue_ips.is_empty() {
self.resolve_ns_to_ips(&ns_names, ¤t_resolver).await
} else {
glue_ips
};
if !ns_ips.is_empty() {
debug!(
subdomain = %subdomain,
ns_count = ns_ips.len(),
"Updated authoritative NS"
);
current_ns_ips = ns_ips;
current_resolver = self.create_resolver_for_ns(¤t_ns_ips)?;
}
}
}
Err(e) => {
debug!(
subdomain = %subdomain,
error = %e,
"No NS delegation at this level"
);
}
}
}
debug!(
domain = %domain,
ns_count = current_ns_ips.len(),
"Found authoritative nameservers"
);
Ok(current_ns_ips)
}
pub async fn resolve_a(&self, domain: &str) -> ClResult<Option<String>> {
debug!(domain = %domain, "Starting A record resolution from root");
let auth_ns = self.find_authoritative_ns(domain).await?;
if auth_ns.is_empty() {
warn!(domain = %domain, "Could not find authoritative nameservers");
return Ok(None);
}
let auth_resolver = self.create_resolver_for_ns(&auth_ns)?;
debug!(domain = %domain, "Querying A records from authoritative NS");
match auth_resolver.lookup(domain, RecordType::A).await {
Ok(lookup) => {
for record in lookup.answers() {
if let RData::A(a) = &record.data {
let ip = a.0.to_string();
debug!(domain = %domain, ip = %ip, "Found A record");
return Ok(Some(ip));
}
}
}
Err(e) => {
debug!(domain = %domain, error = %e, "A lookup failed");
}
}
Ok(None)
}
pub async fn resolve_cname(&self, domain: &str) -> ClResult<Option<String>> {
debug!(domain = %domain, "Starting CNAME record resolution from root");
let auth_ns = self.find_authoritative_ns(domain).await?;
if auth_ns.is_empty() {
warn!(domain = %domain, "Could not find authoritative nameservers");
return Ok(None);
}
let auth_resolver = self.create_resolver_for_ns(&auth_ns)?;
debug!(domain = %domain, "Querying CNAME records from authoritative NS");
match auth_resolver.lookup(domain, RecordType::CNAME).await {
Ok(lookup) => {
for record in lookup.answers() {
if let RData::CNAME(cname) = &record.data {
let target = cname.0.to_string().trim_end_matches('.').to_string();
debug!(domain = %domain, cname = %target, "Found CNAME record");
return Ok(Some(target));
}
}
}
Err(e) => {
debug!(domain = %domain, error = %e, "CNAME lookup failed");
}
}
Ok(None)
}
}
pub fn create_recursive_resolver() -> ClResult<Arc<DnsResolver>> {
Ok(Arc::new(DnsResolver::new()?))
}
pub async fn resolve_domain_addresses(
domain: &str,
resolver: &DnsResolver,
) -> ClResult<Option<String>> {
debug!(domain = %domain, "Resolving domain addresses");
if let Some(cname) = resolver.resolve_cname(domain).await? {
return Ok(Some(cname));
}
if let Some(ip) = resolver.resolve_a(domain).await? {
return Ok(Some(ip));
}
debug!(domain = %domain, "No DNS records found");
Ok(None)
}
pub async fn validate_domain_address(
domain: &str,
local_address: &[Box<str>],
resolver: &DnsResolver,
) -> ClResult<(String, AddressType)> {
if local_address.is_empty() {
return Err(Error::ValidationError("no local address configured".to_string()));
}
debug!(
domain = %domain,
local_addresses = ?local_address,
"Starting DNS validation with recursive resolver"
);
if let Some(resolved_cname) = resolver.resolve_cname(domain).await? {
for local_addr in local_address {
if resolved_cname.eq_ignore_ascii_case(local_addr.as_ref()) {
info!(
domain = %domain,
resolved_cname = %resolved_cname,
matched_local_address = %local_addr,
"Domain validated via CNAME record"
);
return Ok((resolved_cname, AddressType::Hostname));
}
}
warn!(
domain = %domain,
resolved_cname = %resolved_cname,
local_addresses = ?local_address,
"DNS CNAME record doesn't match local address"
);
return Err(Error::ValidationError("address".to_string()));
}
if let Some(resolved_ip) = resolver.resolve_a(domain).await? {
for local_addr in local_address {
if resolved_ip == local_addr.as_ref() {
info!(
domain = %domain,
resolved_ip = %resolved_ip,
matched_local_address = %local_addr,
"Domain validated via A record"
);
return Ok((resolved_ip, AddressType::Ipv4));
}
}
warn!(
domain = %domain,
resolved_ip = %resolved_ip,
local_addresses = ?local_address,
"DNS A record doesn't match local address"
);
return Err(Error::ValidationError("address".to_string()));
}
warn!(domain = %domain, "DNS validation failed: no CNAME or A record found");
Err(Error::ValidationError("nodns".to_string()))
}