1use 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
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(
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 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 async fn find_authoritative_ns(&self, domain: &str) -> ClResult<Vec<IpAddr>> {
81 let labels: Vec<&str> = domain.trim_end_matches('.').split('.').collect();
82
83 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(¤t_ns_ips)?;
88
89 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 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 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 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 let ns_ips = if glue_ips.is_empty() {
123 self.resolve_ns_to_ips(&ns_names, ¤t_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(¤t_ns_ips)?;
136 }
137 }
138 }
139 Err(e) => {
140 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 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 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
222pub fn create_recursive_resolver() -> ClResult<Arc<DnsResolver>> {
224 Ok(Arc::new(DnsResolver::new()?))
225}
226
227pub async fn resolve_domain_addresses(
230 domain: &str,
231 resolver: &DnsResolver,
232) -> ClResult<Option<String>> {
233 debug!(domain = %domain, "Resolving domain addresses");
234
235 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
247pub 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 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 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 warn!(domain = %domain, "DNS validation failed: no CNAME or A record found");
310 Err(Error::ValidationError("nodns".to_string()))
311}
312
313