Skip to main content

cloudillo_core/
dns.rs

1//! DNS resolver module with true recursive resolution from root nameservers
2//!
3//! This module provides DNS resolution capabilities using manual recursive
4//! resolution starting from root nameservers.
5
6use hickory_resolver::{
7	config::{NameServerConfig, ResolverConfig},
8	name_server::TokioConnectionProvider,
9	proto::{rr::RecordType, xfer::Protocol},
10	TokioResolver,
11};
12use std::{
13	net::{IpAddr, SocketAddr},
14	sync::Arc,
15};
16
17use crate::prelude::*;
18use cloudillo_types::address::AddressType;
19
20/// Root nameserver IPs - the 13 official ICANN root servers
21const ROOT_SERVERS: [&str; 13] = [
22	"198.41.0.4",     // A.ROOT-SERVERS.NET
23	"199.9.14.201",   // B.ROOT-SERVERS.NET
24	"192.33.4.12",    // C.ROOT-SERVERS.NET
25	"199.7.91.13",    // D.ROOT-SERVERS.NET
26	"192.203.230.10", // E.ROOT-SERVERS.NET
27	"192.5.5.241",    // F.ROOT-SERVERS.NET
28	"192.112.36.4",   // G.ROOT-SERVERS.NET
29	"198.97.190.53",  // H.ROOT-SERVERS.NET
30	"192.36.148.17",  // I.ROOT-SERVERS.NET
31	"192.58.128.30",  // J.ROOT-SERVERS.NET
32	"193.0.14.129",   // K.ROOT-SERVERS.NET
33	"199.7.83.42",    // L.ROOT-SERVERS.NET
34	"202.12.27.33",   // M.ROOT-SERVERS.NET
35];
36
37/// DNS Resolver wrapper that performs recursive resolution from root servers
38pub struct DnsResolver {}
39
40impl DnsResolver {
41	/// Create a new DNS resolver configured with root servers
42	pub fn new() -> ClResult<Self> {
43		debug!("Created DNS resolver with {} root servers", ROOT_SERVERS.len());
44		Ok(Self {})
45	}
46
47	/// Create a resolver configured to query specific nameservers
48	#[expect(
49		clippy::unused_self,
50		clippy::unnecessary_wraps,
51		reason = "method for consistency; Result for future error handling"
52	)]
53	fn create_resolver_for_ns(&self, ns_ips: &[IpAddr]) -> ClResult<TokioResolver> {
54		let mut config = ResolverConfig::new();
55		for ip in ns_ips {
56			let socket_addr = SocketAddr::new(*ip, 53);
57			config.add_name_server(NameServerConfig::new(socket_addr, Protocol::Udp));
58		}
59		Ok(TokioResolver::builder_with_config(config, TokioConnectionProvider::default()).build())
60	}
61
62	/// Resolve NS record hostnames to IP addresses using the given resolver
63	async fn resolve_ns_to_ips(
64		&self,
65		ns_names: &[String],
66		resolver: &TokioResolver,
67	) -> Vec<IpAddr> {
68		let mut ips = Vec::new();
69		for ns_name in ns_names {
70			if let Ok(lookup) = resolver.lookup_ip(ns_name.as_str()).await {
71				for ip in lookup.iter() {
72					ips.push(ip);
73				}
74			}
75		}
76		ips
77	}
78
79	/// Find authoritative nameservers for a domain by walking down from root
80	async fn find_authoritative_ns(&self, domain: &str) -> ClResult<Vec<IpAddr>> {
81		let labels: Vec<&str> = domain.trim_end_matches('.').split('.').collect();
82
83		// Start with root servers
84		let mut current_ns_ips: Vec<IpAddr> =
85			ROOT_SERVERS.iter().filter_map(|ip| ip.parse().ok()).collect();
86
87		let mut current_resolver = self.create_resolver_for_ns(&current_ns_ips)?;
88
89		// Walk down the domain tree
90		for i in (0..labels.len()).rev() {
91			let subdomain = labels[i..].join(".") + ".";
92
93			debug!(subdomain = %subdomain, "Looking up NS for zone");
94
95			// Query NS records for this level
96			match current_resolver.lookup(subdomain.as_str(), RecordType::NS).await {
97				Ok(ns_lookup) => {
98					let mut ns_names: Vec<String> = Vec::new();
99					let mut glue_ips: Vec<IpAddr> = Vec::new();
100
101					// Collect NS names
102					for record in ns_lookup.record_iter() {
103						if let Some(ns) = record.data().as_ns() {
104							let ns_name = ns.0.to_string();
105							debug!(subdomain = %subdomain, ns = %ns_name, "Found NS record");
106							ns_names.push(ns_name);
107						}
108					}
109
110					// Check for glue records (A/AAAA in additional section)
111					for record in ns_lookup.record_iter() {
112						if let Some(a) = record.data().as_a() {
113							glue_ips.push(IpAddr::V4(a.0));
114						}
115						if let Some(aaaa) = record.data().as_aaaa() {
116							glue_ips.push(IpAddr::V6(aaaa.0));
117						}
118					}
119
120					if !ns_names.is_empty() {
121						// Resolve NS names to IPs if no glue records
122						let ns_ips = if glue_ips.is_empty() {
123							self.resolve_ns_to_ips(&ns_names, &current_resolver).await
124						} else {
125							glue_ips
126						};
127
128						if !ns_ips.is_empty() {
129							debug!(
130								subdomain = %subdomain,
131								ns_count = ns_ips.len(),
132								"Updated authoritative NS"
133							);
134							current_ns_ips = ns_ips;
135							current_resolver = self.create_resolver_for_ns(&current_ns_ips)?;
136						}
137					}
138				}
139				Err(e) => {
140					// NS lookup failed - this is normal for non-delegated subdomains
141					debug!(
142						subdomain = %subdomain,
143						error = %e,
144						"No NS delegation at this level"
145					);
146				}
147			}
148		}
149
150		debug!(
151			domain = %domain,
152			ns_count = current_ns_ips.len(),
153			"Found authoritative nameservers"
154		);
155
156		Ok(current_ns_ips)
157	}
158
159	/// Resolve a domain to A record
160	pub async fn resolve_a(&self, domain: &str) -> ClResult<Option<String>> {
161		debug!(domain = %domain, "Starting A record resolution from root");
162
163		let auth_ns = self.find_authoritative_ns(domain).await?;
164		if auth_ns.is_empty() {
165			warn!(domain = %domain, "Could not find authoritative nameservers");
166			return Ok(None);
167		}
168
169		let auth_resolver = self.create_resolver_for_ns(&auth_ns)?;
170
171		debug!(domain = %domain, "Querying A records from authoritative NS");
172		match auth_resolver.lookup(domain, RecordType::A).await {
173			Ok(lookup) => {
174				for record in lookup.record_iter() {
175					if let Some(a) = record.data().as_a() {
176						let ip = a.0.to_string();
177						debug!(domain = %domain, ip = %ip, "Found A record");
178						return Ok(Some(ip));
179					}
180				}
181			}
182			Err(e) => {
183				debug!(domain = %domain, error = %e, "A lookup failed");
184			}
185		}
186
187		Ok(None)
188	}
189
190	/// Resolve a domain to CNAME record
191	pub async fn resolve_cname(&self, domain: &str) -> ClResult<Option<String>> {
192		debug!(domain = %domain, "Starting CNAME record resolution from root");
193
194		let auth_ns = self.find_authoritative_ns(domain).await?;
195		if auth_ns.is_empty() {
196			warn!(domain = %domain, "Could not find authoritative nameservers");
197			return Ok(None);
198		}
199
200		let auth_resolver = self.create_resolver_for_ns(&auth_ns)?;
201
202		debug!(domain = %domain, "Querying CNAME records from authoritative NS");
203		match auth_resolver.lookup(domain, RecordType::CNAME).await {
204			Ok(lookup) => {
205				for record in lookup.record_iter() {
206					if let Some(cname) = record.data().as_cname() {
207						let target = cname.0.to_string().trim_end_matches('.').to_string();
208						debug!(domain = %domain, cname = %target, "Found CNAME record");
209						return Ok(Some(target));
210					}
211				}
212			}
213			Err(e) => {
214				debug!(domain = %domain, error = %e, "CNAME lookup failed");
215			}
216		}
217
218		Ok(None)
219	}
220}
221
222/// Create a recursive DNS resolver that starts from root nameservers
223pub fn create_recursive_resolver() -> ClResult<Arc<DnsResolver>> {
224	Ok(Arc::new(DnsResolver::new()?))
225}
226
227/// Resolve domain addresses from DNS (without validation)
228/// Uses CNAME lookup (returns hostname target)
229pub async fn resolve_domain_addresses(
230	domain: &str,
231	resolver: &DnsResolver,
232) -> ClResult<Option<String>> {
233	debug!(domain = %domain, "Resolving domain addresses");
234
235	// Try CNAME first, then A
236	if let Some(cname) = resolver.resolve_cname(domain).await? {
237		return Ok(Some(cname));
238	}
239	if let Some(ip) = resolver.resolve_a(domain).await? {
240		return Ok(Some(ip));
241	}
242
243	debug!(domain = %domain, "No DNS records found");
244	Ok(None)
245}
246
247/// Validate a domain against local address using DNS
248/// Checks both CNAME and A records regardless of local address type
249pub async fn validate_domain_address(
250	domain: &str,
251	local_address: &[Box<str>],
252	resolver: &DnsResolver,
253) -> ClResult<(String, AddressType)> {
254	if local_address.is_empty() {
255		return Err(Error::ValidationError("no local address configured".to_string()));
256	}
257
258	debug!(
259		domain = %domain,
260		local_addresses = ?local_address,
261		"Starting DNS validation with recursive resolver"
262	);
263
264	// Try CNAME first
265	if let Some(resolved_cname) = resolver.resolve_cname(domain).await? {
266		for local_addr in local_address {
267			if resolved_cname.eq_ignore_ascii_case(local_addr.as_ref()) {
268				info!(
269					domain = %domain,
270					resolved_cname = %resolved_cname,
271					matched_local_address = %local_addr,
272					"Domain validated via CNAME record"
273				);
274				return Ok((resolved_cname, AddressType::Hostname));
275			}
276		}
277		warn!(
278			domain = %domain,
279			resolved_cname = %resolved_cname,
280			local_addresses = ?local_address,
281			"DNS CNAME record doesn't match local address"
282		);
283		return Err(Error::ValidationError("address".to_string()));
284	}
285
286	// Try A record
287	if let Some(resolved_ip) = resolver.resolve_a(domain).await? {
288		for local_addr in local_address {
289			if resolved_ip == local_addr.as_ref() {
290				info!(
291					domain = %domain,
292					resolved_ip = %resolved_ip,
293					matched_local_address = %local_addr,
294					"Domain validated via A record"
295				);
296				return Ok((resolved_ip, AddressType::Ipv4));
297			}
298		}
299		warn!(
300			domain = %domain,
301			resolved_ip = %resolved_ip,
302			local_addresses = ?local_address,
303			"DNS A record doesn't match local address"
304		);
305		return Err(Error::ValidationError("address".to_string()));
306	}
307
308	// Neither CNAME nor A record found
309	warn!(domain = %domain, "DNS validation failed: no CNAME or A record found");
310	Err(Error::ValidationError("nodns".to_string()))
311}
312
313// vim: ts=4