1use hickory_resolver::{
10 TokioResolver,
11 config::{ConnectionConfig, NameServerConfig, ResolverConfig},
12 net::runtime::TokioRuntimeProvider,
13 proto::rr::{RData, RecordType},
14};
15use std::{net::IpAddr, sync::Arc};
16
17use crate::prelude::*;
18use cloudillo_types::address::AddressType;
19
20const ROOT_SERVERS: [&str; 13] = [
22 "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", ];
36
37pub struct DnsResolver {}
39
40impl DnsResolver {
41 pub fn new() -> ClResult<Self> {
43 debug!("Created DNS resolver with {} root servers", ROOT_SERVERS.len());
44 Ok(Self {})
45 }
46
47 #[expect(clippy::unused_self, reason = "method for consistency")]
49 fn create_resolver_for_ns(&self, ns_ips: &[IpAddr]) -> ClResult<TokioResolver> {
50 let name_servers = ns_ips
51 .iter()
52 .map(|ip| NameServerConfig::new(*ip, true, vec![ConnectionConfig::udp()]))
53 .collect();
54 let config = ResolverConfig::from_parts(None, vec![], name_servers);
55 TokioResolver::builder_with_config(config, TokioRuntimeProvider::default())
56 .build()
57 .map_err(|e| Error::ValidationError(format!("dns resolver build: {e}")))
58 }
59
60 async fn resolve_ns_to_ips(
62 &self,
63 ns_names: &[String],
64 resolver: &TokioResolver,
65 ) -> Vec<IpAddr> {
66 let mut ips = Vec::new();
67 for ns_name in ns_names {
68 if let Ok(lookup) = resolver.lookup_ip(ns_name.as_str()).await {
69 for ip in lookup.iter() {
70 ips.push(ip);
71 }
72 }
73 }
74 ips
75 }
76
77 async fn find_authoritative_ns(&self, domain: &str) -> ClResult<Vec<IpAddr>> {
79 let labels: Vec<&str> = domain.trim_end_matches('.').split('.').collect();
80
81 let mut current_ns_ips: Vec<IpAddr> =
83 ROOT_SERVERS.iter().filter_map(|ip| ip.parse().ok()).collect();
84
85 let mut current_resolver = self.create_resolver_for_ns(¤t_ns_ips)?;
86
87 for i in (0..labels.len()).rev() {
89 let subdomain = labels[i..].join(".") + ".";
90
91 debug!(subdomain = %subdomain, "Looking up NS for zone");
92
93 match current_resolver.lookup(subdomain.as_str(), RecordType::NS).await {
95 Ok(ns_lookup) => {
96 let mut ns_names: Vec<String> = Vec::new();
97 let mut glue_ips: Vec<IpAddr> = Vec::new();
98
99 for record in ns_lookup.answers().iter().chain(ns_lookup.authorities()) {
101 if let RData::NS(ns) = &record.data {
102 let ns_name = ns.0.to_string();
103 debug!(subdomain = %subdomain, ns = %ns_name, "Found NS record");
104 ns_names.push(ns_name);
105 }
106 }
107
108 for record in ns_lookup.additionals().iter().chain(ns_lookup.authorities()) {
111 match &record.data {
112 RData::A(a) => glue_ips.push(IpAddr::V4(a.0)),
113 RData::AAAA(aaaa) => glue_ips.push(IpAddr::V6(aaaa.0)),
114 _ => {}
115 }
116 }
117
118 if !ns_names.is_empty() {
119 let ns_ips = if glue_ips.is_empty() {
121 self.resolve_ns_to_ips(&ns_names, ¤t_resolver).await
122 } else {
123 glue_ips
124 };
125
126 if !ns_ips.is_empty() {
127 debug!(
128 subdomain = %subdomain,
129 ns_count = ns_ips.len(),
130 "Updated authoritative NS"
131 );
132 current_ns_ips = ns_ips;
133 current_resolver = self.create_resolver_for_ns(¤t_ns_ips)?;
134 }
135 }
136 }
137 Err(e) => {
138 debug!(
140 subdomain = %subdomain,
141 error = %e,
142 "No NS delegation at this level"
143 );
144 }
145 }
146 }
147
148 debug!(
149 domain = %domain,
150 ns_count = current_ns_ips.len(),
151 "Found authoritative nameservers"
152 );
153
154 Ok(current_ns_ips)
155 }
156
157 pub async fn resolve_a(&self, domain: &str) -> ClResult<Option<String>> {
159 debug!(domain = %domain, "Starting A record resolution from root");
160
161 let auth_ns = self.find_authoritative_ns(domain).await?;
162 if auth_ns.is_empty() {
163 warn!(domain = %domain, "Could not find authoritative nameservers");
164 return Ok(None);
165 }
166
167 let auth_resolver = self.create_resolver_for_ns(&auth_ns)?;
168
169 debug!(domain = %domain, "Querying A records from authoritative NS");
170 match auth_resolver.lookup(domain, RecordType::A).await {
171 Ok(lookup) => {
172 for record in lookup.answers() {
173 if let RData::A(a) = &record.data {
174 let ip = a.0.to_string();
175 debug!(domain = %domain, ip = %ip, "Found A record");
176 return Ok(Some(ip));
177 }
178 }
179 }
180 Err(e) => {
181 debug!(domain = %domain, error = %e, "A lookup failed");
182 }
183 }
184
185 Ok(None)
186 }
187
188 pub async fn resolve_cname(&self, domain: &str) -> ClResult<Option<String>> {
190 debug!(domain = %domain, "Starting CNAME record resolution from root");
191
192 let auth_ns = self.find_authoritative_ns(domain).await?;
193 if auth_ns.is_empty() {
194 warn!(domain = %domain, "Could not find authoritative nameservers");
195 return Ok(None);
196 }
197
198 let auth_resolver = self.create_resolver_for_ns(&auth_ns)?;
199
200 debug!(domain = %domain, "Querying CNAME records from authoritative NS");
201 match auth_resolver.lookup(domain, RecordType::CNAME).await {
202 Ok(lookup) => {
203 for record in lookup.answers() {
204 if let RData::CNAME(cname) = &record.data {
205 let target = cname.0.to_string().trim_end_matches('.').to_string();
206 debug!(domain = %domain, cname = %target, "Found CNAME record");
207 return Ok(Some(target));
208 }
209 }
210 }
211 Err(e) => {
212 debug!(domain = %domain, error = %e, "CNAME lookup failed");
213 }
214 }
215
216 Ok(None)
217 }
218}
219
220pub fn create_recursive_resolver() -> ClResult<Arc<DnsResolver>> {
222 Ok(Arc::new(DnsResolver::new()?))
223}
224
225pub async fn resolve_domain_addresses(
228 domain: &str,
229 resolver: &DnsResolver,
230) -> ClResult<Option<String>> {
231 debug!(domain = %domain, "Resolving domain addresses");
232
233 if let Some(cname) = resolver.resolve_cname(domain).await? {
235 return Ok(Some(cname));
236 }
237 if let Some(ip) = resolver.resolve_a(domain).await? {
238 return Ok(Some(ip));
239 }
240
241 debug!(domain = %domain, "No DNS records found");
242 Ok(None)
243}
244
245pub async fn validate_domain_address(
248 domain: &str,
249 local_address: &[Box<str>],
250 resolver: &DnsResolver,
251) -> ClResult<(String, AddressType)> {
252 if local_address.is_empty() {
253 return Err(Error::ValidationError("no local address configured".to_string()));
254 }
255
256 debug!(
257 domain = %domain,
258 local_addresses = ?local_address,
259 "Starting DNS validation with recursive resolver"
260 );
261
262 if let Some(resolved_cname) = resolver.resolve_cname(domain).await? {
264 for local_addr in local_address {
265 if resolved_cname.eq_ignore_ascii_case(local_addr.as_ref()) {
266 info!(
267 domain = %domain,
268 resolved_cname = %resolved_cname,
269 matched_local_address = %local_addr,
270 "Domain validated via CNAME record"
271 );
272 return Ok((resolved_cname, AddressType::Hostname));
273 }
274 }
275 warn!(
276 domain = %domain,
277 resolved_cname = %resolved_cname,
278 local_addresses = ?local_address,
279 "DNS CNAME record doesn't match local address"
280 );
281 return Err(Error::ValidationError("address".to_string()));
282 }
283
284 if let Some(resolved_ip) = resolver.resolve_a(domain).await? {
286 for local_addr in local_address {
287 if resolved_ip == local_addr.as_ref() {
288 info!(
289 domain = %domain,
290 resolved_ip = %resolved_ip,
291 matched_local_address = %local_addr,
292 "Domain validated via A record"
293 );
294 return Ok((resolved_ip, AddressType::Ipv4));
295 }
296 }
297 warn!(
298 domain = %domain,
299 resolved_ip = %resolved_ip,
300 local_addresses = ?local_address,
301 "DNS A record doesn't match local address"
302 );
303 return Err(Error::ValidationError("address".to_string()));
304 }
305
306 warn!(domain = %domain, "DNS validation failed: no CNAME or A record found");
308 Err(Error::ValidationError("nodns".to_string()))
309}
310
311