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