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::*,
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::{parse_address_type, 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	fn create_resolver_for_ns(&self, ns_ips: &[IpAddr]) -> ClResult<TokioResolver> {
49		let mut config = ResolverConfig::new();
50		for ip in ns_ips {
51			let socket_addr = SocketAddr::new(*ip, 53);
52			config.add_name_server(NameServerConfig::new(socket_addr, Protocol::Udp));
53		}
54		Ok(TokioResolver::builder_with_config(config, TokioConnectionProvider::default()).build())
55	}
56
57	/// Resolve NS record hostnames to IP addresses using the given resolver
58	async fn resolve_ns_to_ips(
59		&self,
60		ns_names: &[String],
61		resolver: &TokioResolver,
62	) -> Vec<IpAddr> {
63		let mut ips = Vec::new();
64		for ns_name in ns_names {
65			if let Ok(lookup) = resolver.lookup_ip(ns_name.as_str()).await {
66				for ip in lookup.iter() {
67					ips.push(ip);
68				}
69			}
70		}
71		ips
72	}
73
74	/// Find authoritative nameservers for a domain by walking down from root
75	async fn find_authoritative_ns(&self, domain: &str) -> ClResult<Vec<IpAddr>> {
76		let labels: Vec<&str> = domain.trim_end_matches('.').split('.').collect();
77
78		// Start with root servers
79		let mut current_ns_ips: Vec<IpAddr> =
80			ROOT_SERVERS.iter().filter_map(|ip| ip.parse().ok()).collect();
81
82		let mut current_resolver = self.create_resolver_for_ns(&current_ns_ips)?;
83
84		// Walk down the domain tree
85		for i in (0..labels.len()).rev() {
86			let subdomain = labels[i..].join(".") + ".";
87
88			debug!(subdomain = %subdomain, "Looking up NS for zone");
89
90			// Query NS records for this level
91			match current_resolver.lookup(subdomain.as_str(), RecordType::NS).await {
92				Ok(ns_lookup) => {
93					let mut ns_names: Vec<String> = Vec::new();
94					let mut glue_ips: Vec<IpAddr> = Vec::new();
95
96					// Collect NS names
97					for record in ns_lookup.record_iter() {
98						if let Some(ns) = record.data().as_ns() {
99							let ns_name = ns.0.to_string();
100							debug!(subdomain = %subdomain, ns = %ns_name, "Found NS record");
101							ns_names.push(ns_name);
102						}
103					}
104
105					// Check for glue records (A/AAAA in additional section)
106					for record in ns_lookup.record_iter() {
107						if let Some(a) = record.data().as_a() {
108							glue_ips.push(IpAddr::V4(a.0));
109						}
110						if let Some(aaaa) = record.data().as_aaaa() {
111							glue_ips.push(IpAddr::V6(aaaa.0));
112						}
113					}
114
115					if !ns_names.is_empty() {
116						// Resolve NS names to IPs if no glue records
117						let ns_ips = if glue_ips.is_empty() {
118							self.resolve_ns_to_ips(&ns_names, &current_resolver).await
119						} else {
120							glue_ips
121						};
122
123						if !ns_ips.is_empty() {
124							debug!(
125								subdomain = %subdomain,
126								ns_count = ns_ips.len(),
127								"Updated authoritative NS"
128							);
129							current_ns_ips = ns_ips;
130							current_resolver = self.create_resolver_for_ns(&current_ns_ips)?;
131						}
132					}
133				}
134				Err(e) => {
135					// NS lookup failed - this is normal for non-delegated subdomains
136					debug!(
137						subdomain = %subdomain,
138						error = %e,
139						"No NS delegation at this level"
140					);
141				}
142			}
143		}
144
145		debug!(
146			domain = %domain,
147			ns_count = current_ns_ips.len(),
148			"Found authoritative nameservers"
149		);
150
151		Ok(current_ns_ips)
152	}
153
154	/// Resolve a domain to A record
155	pub async fn resolve_a(&self, domain: &str) -> ClResult<Option<String>> {
156		debug!(domain = %domain, "Starting A record resolution from root");
157
158		let auth_ns = self.find_authoritative_ns(domain).await?;
159		if auth_ns.is_empty() {
160			warn!(domain = %domain, "Could not find authoritative nameservers");
161			return Ok(None);
162		}
163
164		let auth_resolver = self.create_resolver_for_ns(&auth_ns)?;
165
166		debug!(domain = %domain, "Querying A records from authoritative NS");
167		match auth_resolver.lookup(domain, RecordType::A).await {
168			Ok(lookup) => {
169				for record in lookup.record_iter() {
170					if let Some(a) = record.data().as_a() {
171						let ip = a.0.to_string();
172						debug!(domain = %domain, ip = %ip, "Found A record");
173						return Ok(Some(ip));
174					}
175				}
176			}
177			Err(e) => {
178				debug!(domain = %domain, error = %e, "A lookup failed");
179			}
180		}
181
182		Ok(None)
183	}
184
185	/// Resolve a domain to CNAME record
186	pub async fn resolve_cname(&self, domain: &str) -> ClResult<Option<String>> {
187		debug!(domain = %domain, "Starting CNAME record resolution from root");
188
189		let auth_ns = self.find_authoritative_ns(domain).await?;
190		if auth_ns.is_empty() {
191			warn!(domain = %domain, "Could not find authoritative nameservers");
192			return Ok(None);
193		}
194
195		let auth_resolver = self.create_resolver_for_ns(&auth_ns)?;
196
197		debug!(domain = %domain, "Querying CNAME records from authoritative NS");
198		match auth_resolver.lookup(domain, RecordType::CNAME).await {
199			Ok(lookup) => {
200				for record in lookup.record_iter() {
201					if let Some(cname) = record.data().as_cname() {
202						let target = cname.0.to_string().trim_end_matches('.').to_string();
203						debug!(domain = %domain, cname = %target, "Found CNAME record");
204						return Ok(Some(target));
205					}
206				}
207			}
208			Err(e) => {
209				debug!(domain = %domain, error = %e, "CNAME lookup failed");
210			}
211		}
212
213		Ok(None)
214	}
215}
216
217/// Create a recursive DNS resolver that starts from root nameservers
218pub fn create_recursive_resolver() -> ClResult<Arc<DnsResolver>> {
219	Ok(Arc::new(DnsResolver::new()?))
220}
221
222/// Resolve domain addresses from DNS (without validation)
223/// Uses CNAME lookup (returns hostname target)
224pub async fn resolve_domain_addresses(
225	domain: &str,
226	resolver: &DnsResolver,
227) -> ClResult<Option<String>> {
228	debug!(domain = %domain, "Resolving domain addresses");
229
230	// Try CNAME first, then A
231	if let Some(cname) = resolver.resolve_cname(domain).await? {
232		return Ok(Some(cname));
233	}
234	if let Some(ip) = resolver.resolve_a(domain).await? {
235		return Ok(Some(ip));
236	}
237
238	debug!(domain = %domain, "No DNS records found");
239	Ok(None)
240}
241
242/// Validate a domain against local address using DNS
243/// Checks A records if local_address is IP, CNAME if local_address is hostname
244pub async fn validate_domain_address(
245	domain: &str,
246	local_address: &[Box<str>],
247	resolver: &DnsResolver,
248) -> ClResult<(String, AddressType)> {
249	if local_address.is_empty() {
250		return Err(Error::ValidationError("no local address configured".to_string()));
251	}
252
253	// Determine what record type to check based on local address type
254	let local_addr_type = parse_address_type(local_address[0].as_ref())?;
255
256	debug!(
257		domain = %domain,
258		local_addresses = ?local_address,
259		local_addr_type = %local_addr_type,
260		"Starting DNS validation with recursive resolver"
261	);
262
263	match local_addr_type {
264		AddressType::Ipv4 => {
265			// Local address is IP - check A record
266			if let Some(resolved_ip) = resolver.resolve_a(domain).await? {
267				for local_addr in local_address {
268					if resolved_ip == local_addr.as_ref() {
269						info!(
270							domain = %domain,
271							resolved_ip = %resolved_ip,
272							matched_local_address = %local_addr,
273							"Domain validated via A record"
274						);
275						return Ok((resolved_ip, AddressType::Ipv4));
276					}
277				}
278				warn!(
279					domain = %domain,
280					resolved_ip = %resolved_ip,
281					local_addresses = ?local_address,
282					"DNS A record doesn't match local address"
283				);
284				return Err(Error::ValidationError("address".to_string()));
285			}
286			warn!(domain = %domain, "DNS validation failed: no A record found");
287			Err(Error::ValidationError("nodns".to_string()))
288		}
289		AddressType::Hostname => {
290			// Local address is hostname - check CNAME record
291			if let Some(resolved_cname) = resolver.resolve_cname(domain).await? {
292				for local_addr in local_address {
293					if resolved_cname.eq_ignore_ascii_case(local_addr.as_ref()) {
294						info!(
295							domain = %domain,
296							resolved_cname = %resolved_cname,
297							matched_local_address = %local_addr,
298							"Domain validated via CNAME record"
299						);
300						return Ok((resolved_cname, AddressType::Hostname));
301					}
302				}
303				warn!(
304					domain = %domain,
305					resolved_cname = %resolved_cname,
306					local_addresses = ?local_address,
307					"DNS CNAME record doesn't match local address"
308				);
309				return Err(Error::ValidationError("address".to_string()));
310			}
311			warn!(domain = %domain, "DNS validation failed: no CNAME record found");
312			Err(Error::ValidationError("nodns".to_string()))
313		}
314		AddressType::Ipv6 => {
315			// IPv6 not supported for validation
316			Err(Error::ValidationError("IPv6 local address not supported".to_string()))
317		}
318	}
319}
320
321// vim: ts=4