1use std::net::IpAddr;
2use std::str::FromStr;
3use std::time::Duration;
4
5use hickory_resolver::config::{NameServerConfig, ResolveHosts, ResolverConfig, GOOGLE};
6use hickory_resolver::net::runtime::TokioRuntimeProvider;
7use hickory_resolver::net::NetError;
8use hickory_resolver::proto::dnssec::PublicKey;
9use hickory_resolver::proto::rr::rdata::CAA;
10use hickory_resolver::proto::rr::{RData as HickoryRData, RecordType as HickoryRecordType};
11use hickory_resolver::TokioResolver;
12use tracing::{debug, instrument};
13
14use super::records::{DnsRecord, RecordData, RecordType};
15use crate::error::{Result, SeerError};
16use crate::validation::normalize_domain;
17
18fn dns_lookup_or_empty<T>(
22 result: std::result::Result<T, NetError>,
23 record_type: &str,
24) -> Result<Option<T>> {
25 match result {
26 Ok(response) => Ok(Some(response)),
27 Err(e) if e.is_no_records_found() => Ok(None),
28 Err(e) => Err(SeerError::DnsError(format!(
29 "{} lookup failed: {}",
30 record_type, e
31 ))),
32 }
33}
34
35const DEFAULT_TIMEOUT: Duration = Duration::from_secs(5);
38
39fn build_resolver(config: ResolverConfig, timeout: Duration) -> TokioResolver {
46 let mut builder = TokioResolver::builder_with_config(config, TokioRuntimeProvider::default());
47 {
48 let opts = builder.options_mut();
49 opts.timeout = timeout;
50 opts.attempts = 2;
51 opts.use_hosts_file = ResolveHosts::Never;
52 }
53 builder
54 .build()
55 .expect("hickory resolver build is infallible without TLS features")
56}
57
58#[derive(Clone)]
64pub struct DnsResolver {
65 timeout: Duration,
66 default_resolver: TokioResolver,
69}
70
71impl std::fmt::Debug for DnsResolver {
72 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
73 f.debug_struct("DnsResolver")
74 .field("timeout", &self.timeout)
75 .finish()
76 }
77}
78
79impl Default for DnsResolver {
80 fn default() -> Self {
81 Self::new()
82 }
83}
84
85impl DnsResolver {
86 pub fn new() -> Self {
88 Self {
89 timeout: DEFAULT_TIMEOUT,
90 default_resolver: build_resolver(ResolverConfig::udp_and_tcp(&GOOGLE), DEFAULT_TIMEOUT),
91 }
92 }
93
94 pub fn with_timeout(mut self, timeout: Duration) -> Self {
98 self.timeout = timeout;
99 self.default_resolver = build_resolver(ResolverConfig::udp_and_tcp(&GOOGLE), timeout);
100 self
101 }
102
103 async fn create_custom_resolver(&self, nameserver: &str) -> Result<TokioResolver> {
104 let ips: Vec<IpAddr> = if let Ok(ip) = nameserver.parse::<IpAddr>() {
112 vec![ip]
113 } else {
114 let response = self
115 .default_resolver
116 .lookup_ip(nameserver)
117 .await
118 .map_err(|e| {
119 SeerError::DnsError(format!(
120 "failed to resolve nameserver hostname {}: {}",
121 nameserver, e
122 ))
123 })?;
124 let resolved: Vec<IpAddr> = response.iter().collect();
125 if resolved.is_empty() {
126 return Err(SeerError::DnsError(format!(
127 "nameserver {} did not resolve to any addresses",
128 nameserver
129 )));
130 }
131 resolved
132 };
133
134 for ip in &ips {
138 if let Some(reason) = crate::validation::describe_reserved_ip(ip) {
139 return Err(SeerError::DnsError(format!(
140 "nameserver {} blocked: {}",
141 nameserver, reason
142 )));
143 }
144 }
145
146 let mut config = ResolverConfig::from_parts(None, vec![], vec![]);
151 for ip in ips {
152 config.add_name_server(NameServerConfig::udp(ip));
153 }
154
155 Ok(build_resolver(config, self.timeout))
156 }
157
158 #[instrument(skip(self), fields(domain = %domain, record_type = %record_type))]
165 pub async fn resolve(
166 &self,
167 domain: &str,
168 record_type: RecordType,
169 nameserver: Option<&str>,
170 ) -> Result<Vec<DnsRecord>> {
171 let custom_resolver;
173 let resolver = if let Some(ns) = nameserver {
174 custom_resolver = self.create_custom_resolver(ns).await?;
175 &custom_resolver
176 } else {
177 &self.default_resolver
178 };
179 let domain = normalize_domain(domain)?;
180
181 debug!(nameserver = nameserver.unwrap_or("system"), "Resolving DNS");
182
183 match record_type {
184 RecordType::A => self.resolve_a(resolver, &domain).await,
185 RecordType::AAAA => self.resolve_aaaa(resolver, &domain).await,
186 RecordType::CNAME => self.resolve_cname(resolver, &domain).await,
187 RecordType::MX => self.resolve_mx(resolver, &domain).await,
188 RecordType::NS => self.resolve_ns(resolver, &domain).await,
189 RecordType::TXT => self.resolve_txt(resolver, &domain).await,
190 RecordType::SOA => self.resolve_soa(resolver, &domain).await,
191 RecordType::PTR => self.resolve_ptr(resolver, &domain).await,
192 RecordType::SRV => Err(SeerError::DnsError(
193 "SRV records require service name format: _service._proto.name".to_string(),
194 )),
195 RecordType::CAA => self.resolve_caa(resolver, &domain).await,
196 RecordType::DNSKEY => self.resolve_dnskey(resolver, &domain).await,
197 RecordType::DS => self.resolve_ds(resolver, &domain).await,
198 RecordType::TLSA => self.resolve_tlsa(resolver, &domain).await,
199 RecordType::SSHFP => self.resolve_sshfp(resolver, &domain).await,
200 RecordType::ANY => self.resolve_any(resolver, &domain).await,
201 _ => Err(SeerError::DnsError(format!(
202 "Record type {} not implemented",
203 record_type
204 ))),
205 }
206 }
207
208 #[instrument(skip(self), fields(domain = %domain, service = %service, protocol = %protocol))]
216 pub async fn resolve_srv(
217 &self,
218 service: &str,
219 protocol: &str,
220 domain: &str,
221 nameserver: Option<&str>,
222 ) -> Result<Vec<DnsRecord>> {
223 if !is_valid_srv_label(service) {
225 return Err(SeerError::DnsError(format!(
226 "invalid SRV service name: {}",
227 service
228 )));
229 }
230 if !is_valid_srv_label(protocol) {
231 return Err(SeerError::DnsError(format!(
232 "invalid SRV protocol name: {}",
233 protocol
234 )));
235 }
236
237 let custom_resolver;
238 let resolver = if let Some(ns) = nameserver {
239 custom_resolver = self.create_custom_resolver(ns).await?;
240 &custom_resolver
241 } else {
242 &self.default_resolver
243 };
244 let query_name = format!("_{}._{}.{}", service, protocol, domain);
245
246 let Some(response) = dns_lookup_or_empty(
247 resolver.lookup(&query_name, HickoryRecordType::SRV).await,
248 "SRV",
249 )?
250 else {
251 return Ok(vec![]);
252 };
253
254 let records = response
255 .answers()
256 .iter()
257 .filter_map(|record| {
258 if let HickoryRData::SRV(srv) = &record.data {
259 Some(DnsRecord {
260 name: query_name.clone(),
261 record_type: RecordType::SRV,
262 ttl: record.ttl,
263 data: RecordData::SRV {
264 priority: srv.priority,
265 weight: srv.weight,
266 port: srv.port,
267 target: srv.target.to_string(),
268 },
269 })
270 } else {
271 None
272 }
273 })
274 .collect();
275
276 Ok(records)
277 }
278
279 async fn resolve_a(&self, resolver: &TokioResolver, domain: &str) -> Result<Vec<DnsRecord>> {
280 let Some(response) =
281 dns_lookup_or_empty(resolver.lookup(domain, HickoryRecordType::A).await, "A")?
282 else {
283 return Ok(vec![]);
284 };
285
286 let records = response
287 .answers()
288 .iter()
289 .filter_map(|record| {
290 if let HickoryRData::A(addr) = &record.data {
291 Some(DnsRecord {
292 name: domain.to_string(),
293 record_type: RecordType::A,
294 ttl: record.ttl,
295 data: RecordData::A {
296 address: addr.0.to_string(),
297 },
298 })
299 } else {
300 None
301 }
302 })
303 .collect();
304
305 Ok(records)
306 }
307
308 async fn resolve_aaaa(&self, resolver: &TokioResolver, domain: &str) -> Result<Vec<DnsRecord>> {
309 let Some(response) = dns_lookup_or_empty(
310 resolver.lookup(domain, HickoryRecordType::AAAA).await,
311 "AAAA",
312 )?
313 else {
314 return Ok(vec![]);
315 };
316
317 let records = response
318 .answers()
319 .iter()
320 .filter_map(|record| {
321 if let HickoryRData::AAAA(addr) = &record.data {
322 Some(DnsRecord {
323 name: domain.to_string(),
324 record_type: RecordType::AAAA,
325 ttl: record.ttl,
326 data: RecordData::AAAA {
327 address: addr.0.to_string(),
328 },
329 })
330 } else {
331 None
332 }
333 })
334 .collect();
335
336 Ok(records)
337 }
338
339 async fn resolve_cname(
340 &self,
341 resolver: &TokioResolver,
342 domain: &str,
343 ) -> Result<Vec<DnsRecord>> {
344 let Some(response) = dns_lookup_or_empty(
345 resolver.lookup(domain, HickoryRecordType::CNAME).await,
346 "CNAME",
347 )?
348 else {
349 return Ok(vec![]);
350 };
351
352 let records = response
353 .answers()
354 .iter()
355 .filter_map(|record| {
356 if let HickoryRData::CNAME(cname) = &record.data {
357 Some(DnsRecord {
358 name: domain.to_string(),
359 record_type: RecordType::CNAME,
360 ttl: record.ttl,
361 data: RecordData::CNAME {
362 target: cname.0.to_string(),
363 },
364 })
365 } else {
366 None
367 }
368 })
369 .collect();
370
371 Ok(records)
372 }
373
374 async fn resolve_mx(&self, resolver: &TokioResolver, domain: &str) -> Result<Vec<DnsRecord>> {
375 let Some(response) =
376 dns_lookup_or_empty(resolver.lookup(domain, HickoryRecordType::MX).await, "MX")?
377 else {
378 return Ok(vec![]);
379 };
380
381 let mut records: Vec<DnsRecord> = response
382 .answers()
383 .iter()
384 .filter_map(|record| {
385 if let HickoryRData::MX(mx) = &record.data {
386 Some(DnsRecord {
387 name: domain.to_string(),
388 record_type: RecordType::MX,
389 ttl: record.ttl,
390 data: RecordData::MX {
391 preference: mx.preference,
392 exchange: mx.exchange.to_string(),
393 },
394 })
395 } else {
396 None
397 }
398 })
399 .collect();
400
401 records.sort_by_key(|r| {
402 if let RecordData::MX { preference, .. } = &r.data {
403 *preference
404 } else {
405 0
406 }
407 });
408
409 Ok(records)
410 }
411
412 async fn resolve_ns(&self, resolver: &TokioResolver, domain: &str) -> Result<Vec<DnsRecord>> {
413 let Some(response) =
414 dns_lookup_or_empty(resolver.lookup(domain, HickoryRecordType::NS).await, "NS")?
415 else {
416 return Ok(vec![]);
417 };
418
419 let records = response
420 .answers()
421 .iter()
422 .filter_map(|record| {
423 if let HickoryRData::NS(ns) = &record.data {
424 Some(DnsRecord {
425 name: domain.to_string(),
426 record_type: RecordType::NS,
427 ttl: record.ttl,
428 data: RecordData::NS {
429 nameserver: ns.0.to_string(),
430 },
431 })
432 } else {
433 None
434 }
435 })
436 .collect();
437
438 Ok(records)
439 }
440
441 async fn resolve_txt(&self, resolver: &TokioResolver, domain: &str) -> Result<Vec<DnsRecord>> {
442 let Some(response) =
443 dns_lookup_or_empty(resolver.lookup(domain, HickoryRecordType::TXT).await, "TXT")?
444 else {
445 return Ok(vec![]);
446 };
447
448 let records = response
449 .answers()
450 .iter()
451 .filter_map(|record| {
452 if let HickoryRData::TXT(txt) = &record.data {
453 let text = txt
454 .txt_data
455 .iter()
456 .map(|data| String::from_utf8_lossy(data).to_string())
457 .collect::<Vec<_>>()
458 .join("");
459
460 Some(DnsRecord {
461 name: domain.to_string(),
462 record_type: RecordType::TXT,
463 ttl: record.ttl,
464 data: RecordData::TXT { text },
465 })
466 } else {
467 None
468 }
469 })
470 .collect();
471
472 Ok(records)
473 }
474
475 async fn resolve_soa(&self, resolver: &TokioResolver, domain: &str) -> Result<Vec<DnsRecord>> {
476 let Some(response) =
477 dns_lookup_or_empty(resolver.lookup(domain, HickoryRecordType::SOA).await, "SOA")?
478 else {
479 return Ok(vec![]);
480 };
481
482 let records = response
483 .answers()
484 .iter()
485 .filter_map(|record| {
486 if let HickoryRData::SOA(soa) = &record.data {
487 Some(DnsRecord {
488 name: domain.to_string(),
489 record_type: RecordType::SOA,
490 ttl: record.ttl,
491 data: RecordData::SOA {
492 mname: soa.mname.to_string(),
493 rname: soa.rname.to_string(),
494 serial: soa.serial,
495 refresh: soa.refresh.try_into().unwrap_or(0),
496 retry: soa.retry.try_into().unwrap_or(0),
497 expire: soa.expire.try_into().unwrap_or(0),
498 minimum: soa.minimum,
499 },
500 })
501 } else {
502 None
503 }
504 })
505 .collect();
506
507 Ok(records)
508 }
509
510 async fn resolve_ptr(&self, resolver: &TokioResolver, query: &str) -> Result<Vec<DnsRecord>> {
511 let query = if let Ok(ip) = IpAddr::from_str(query) {
513 reverse_dns_name(&ip)
514 } else {
515 query.to_string()
516 };
517
518 let Some(response) =
519 dns_lookup_or_empty(resolver.lookup(&query, HickoryRecordType::PTR).await, "PTR")?
520 else {
521 return Ok(vec![]);
522 };
523
524 let records = response
525 .answers()
526 .iter()
527 .filter_map(|record| {
528 if let HickoryRData::PTR(ptr) = &record.data {
529 Some(DnsRecord {
530 name: query.clone(),
531 record_type: RecordType::PTR,
532 ttl: record.ttl,
533 data: RecordData::PTR {
534 target: ptr.0.to_string(),
535 },
536 })
537 } else {
538 None
539 }
540 })
541 .collect();
542
543 Ok(records)
544 }
545
546 async fn resolve_caa(&self, resolver: &TokioResolver, domain: &str) -> Result<Vec<DnsRecord>> {
547 let Some(response) =
548 dns_lookup_or_empty(resolver.lookup(domain, HickoryRecordType::CAA).await, "CAA")?
549 else {
550 return Ok(vec![]);
551 };
552
553 let records = response
554 .answers()
555 .iter()
556 .filter_map(|record| {
557 if let HickoryRData::CAA(caa) = &record.data {
558 let (flags, tag, value) = parse_caa(caa);
559 Some(DnsRecord {
560 name: domain.to_string(),
561 record_type: RecordType::CAA,
562 ttl: record.ttl,
563 data: RecordData::CAA { flags, tag, value },
564 })
565 } else {
566 None
567 }
568 })
569 .collect();
570
571 Ok(records)
572 }
573
574 async fn resolve_dnskey(
575 &self,
576 resolver: &TokioResolver,
577 domain: &str,
578 ) -> Result<Vec<DnsRecord>> {
579 use hickory_resolver::proto::dnssec::rdata::DNSSECRData;
580
581 let Some(response) = dns_lookup_or_empty(
582 resolver.lookup(domain, HickoryRecordType::DNSKEY).await,
583 "DNSKEY",
584 )?
585 else {
586 return Ok(vec![]);
587 };
588
589 let records = response
590 .answers()
591 .iter()
592 .filter_map(|record| {
593 if let HickoryRData::DNSSEC(DNSSECRData::DNSKEY(dnskey)) = &record.data {
594 use base64::{engine::general_purpose::STANDARD, Engine};
595 let public_key_buf = dnskey.public_key();
596 let public_key = STANDARD.encode(public_key_buf.public_bytes());
597 Some(DnsRecord {
598 name: domain.to_string(),
599 record_type: RecordType::DNSKEY,
600 ttl: record.ttl,
601 data: RecordData::DNSKEY {
602 flags: dnskey.flags(),
603 protocol: 3,
605 algorithm: u8::from(public_key_buf.algorithm()),
606 public_key,
607 },
608 })
609 } else {
610 None
611 }
612 })
613 .collect();
614
615 Ok(records)
616 }
617
618 async fn resolve_ds(&self, resolver: &TokioResolver, domain: &str) -> Result<Vec<DnsRecord>> {
619 use hickory_resolver::proto::dnssec::rdata::DNSSECRData;
620
621 let Some(response) =
622 dns_lookup_or_empty(resolver.lookup(domain, HickoryRecordType::DS).await, "DS")?
623 else {
624 return Ok(vec![]);
625 };
626
627 let records = response
628 .answers()
629 .iter()
630 .filter_map(|record| {
631 if let HickoryRData::DNSSEC(DNSSECRData::DS(ds)) = &record.data {
632 let digest = ds
633 .digest()
634 .iter()
635 .map(|b| format!("{:02X}", b))
636 .collect::<String>();
637 Some(DnsRecord {
638 name: domain.to_string(),
639 record_type: RecordType::DS,
640 ttl: record.ttl,
641 data: RecordData::DS {
642 key_tag: ds.key_tag(),
643 algorithm: u8::from(ds.algorithm()),
644 digest_type: u8::from(ds.digest_type()),
645 digest,
646 },
647 })
648 } else {
649 None
650 }
651 })
652 .collect();
653
654 Ok(records)
655 }
656
657 async fn resolve_tlsa(&self, resolver: &TokioResolver, domain: &str) -> Result<Vec<DnsRecord>> {
658 let Some(response) = dns_lookup_or_empty(
664 resolver.lookup(domain, HickoryRecordType::TLSA).await,
665 "TLSA",
666 )?
667 else {
668 return Ok(vec![]);
669 };
670
671 let records = response
672 .answers()
673 .iter()
674 .filter_map(|record| {
675 if let HickoryRData::TLSA(tlsa) = &record.data {
676 let cert_data = tlsa
677 .cert_data
678 .iter()
679 .map(|b| format!("{:02X}", b))
680 .collect::<String>();
681 Some(DnsRecord {
682 name: domain.to_string(),
683 record_type: RecordType::TLSA,
684 ttl: record.ttl,
685 data: RecordData::TLSA {
686 cert_usage: u8::from(tlsa.cert_usage),
687 selector: u8::from(tlsa.selector),
688 matching: u8::from(tlsa.matching),
689 cert_data,
690 },
691 })
692 } else {
693 None
694 }
695 })
696 .collect();
697
698 Ok(records)
699 }
700
701 async fn resolve_sshfp(
702 &self,
703 resolver: &TokioResolver,
704 domain: &str,
705 ) -> Result<Vec<DnsRecord>> {
706 let Some(response) = dns_lookup_or_empty(
707 resolver.lookup(domain, HickoryRecordType::SSHFP).await,
708 "SSHFP",
709 )?
710 else {
711 return Ok(vec![]);
712 };
713
714 let records = response
715 .answers()
716 .iter()
717 .filter_map(|record| {
718 if let HickoryRData::SSHFP(sshfp) = &record.data {
719 let fingerprint = sshfp
720 .fingerprint
721 .iter()
722 .map(|b| format!("{:02X}", b))
723 .collect::<String>();
724 Some(DnsRecord {
725 name: domain.to_string(),
726 record_type: RecordType::SSHFP,
727 ttl: record.ttl,
728 data: RecordData::SSHFP {
729 algorithm: u8::from(sshfp.algorithm),
730 fingerprint_type: u8::from(sshfp.fingerprint_type),
731 fingerprint,
732 },
733 })
734 } else {
735 None
736 }
737 })
738 .collect();
739
740 Ok(records)
741 }
742
743 async fn resolve_any(&self, resolver: &TokioResolver, domain: &str) -> Result<Vec<DnsRecord>> {
744 let mut all_records = Vec::new();
745
746 let record_types = [
748 RecordType::A,
749 RecordType::AAAA,
750 RecordType::MX,
751 RecordType::NS,
752 RecordType::TXT,
753 RecordType::SOA,
754 RecordType::CAA,
755 ];
756
757 for record_type in record_types {
758 match self.resolve_type(resolver, domain, record_type).await {
759 Ok(records) => all_records.extend(records),
760 Err(_) => continue, }
762 }
763
764 Ok(all_records)
765 }
766
767 async fn resolve_type(
768 &self,
769 resolver: &TokioResolver,
770 domain: &str,
771 record_type: RecordType,
772 ) -> Result<Vec<DnsRecord>> {
773 match record_type {
774 RecordType::A => self.resolve_a(resolver, domain).await,
775 RecordType::AAAA => self.resolve_aaaa(resolver, domain).await,
776 RecordType::CNAME => self.resolve_cname(resolver, domain).await,
777 RecordType::MX => self.resolve_mx(resolver, domain).await,
778 RecordType::NS => self.resolve_ns(resolver, domain).await,
779 RecordType::TXT => self.resolve_txt(resolver, domain).await,
780 RecordType::SOA => self.resolve_soa(resolver, domain).await,
781 RecordType::CAA => self.resolve_caa(resolver, domain).await,
782 RecordType::DNSKEY => self.resolve_dnskey(resolver, domain).await,
783 RecordType::DS => self.resolve_ds(resolver, domain).await,
784 _ => Err(SeerError::DnsError("unsupported record type".to_string())),
785 }
786 }
787}
788
789#[derive(Debug, Clone, Copy, PartialEq, Eq)]
794pub enum DnsPresence {
795 Present,
797 Absent,
799 Unknown,
801}
802
803fn classify_ns_presence(result: &Result<Vec<DnsRecord>>) -> DnsPresence {
808 match result {
809 Ok(records) if records.is_empty() => DnsPresence::Absent,
810 Ok(_) => DnsPresence::Present,
811 Err(_) => DnsPresence::Unknown,
812 }
813}
814
815impl DnsResolver {
816 pub async fn presence(&self, domain: &str) -> DnsPresence {
824 classify_ns_presence(&self.resolve(domain, RecordType::NS, None).await)
825 }
826}
827
828fn reverse_dns_name(ip: &IpAddr) -> String {
831 match ip {
832 IpAddr::V4(addr) => {
833 let octets = addr.octets();
834 format!(
835 "{}.{}.{}.{}.in-addr.arpa",
836 octets[3], octets[2], octets[1], octets[0]
837 )
838 }
839 IpAddr::V6(addr) => {
840 let segments = addr.segments();
841 let mut result = String::with_capacity(72);
843 let mut first = true;
844 for segment in segments.iter().rev() {
845 for shift in [0, 4, 8, 12] {
846 if !first {
847 result.push('.');
848 }
849 first = false;
850 let nibble = (segment >> shift) & 0xF;
851 result
852 .push(char::from_digit(nibble as u32, 16).expect("nibble is always 0-15"));
853 }
854 }
855 result.push_str(".ip6.arpa");
856 result
857 }
858 }
859}
860
861fn parse_caa(caa: &CAA) -> (u8, String, String) {
862 let flags = if caa.issuer_critical { 128 } else { 0 };
869 let tag = caa.tag.clone();
870 let value = String::from_utf8_lossy(&caa.value).to_string();
871 (flags, tag, value)
872}
873
874fn is_valid_srv_label(label: &str) -> bool {
876 !label.is_empty()
877 && label.len() <= 63
878 && label.chars().all(|c| c.is_ascii_alphanumeric() || c == '-')
879 && !label.starts_with('-')
880 && !label.ends_with('-')
881}
882
883#[cfg(test)]
884mod tests {
885 use super::*;
895 use std::net::{Ipv4Addr, Ipv6Addr};
896
897 #[test]
900 fn record_type_from_str_accepts_lowercase() {
901 assert_eq!(RecordType::from_str("a").unwrap(), RecordType::A);
902 assert_eq!(RecordType::from_str("mx").unwrap(), RecordType::MX);
903 assert_eq!(RecordType::from_str("cname").unwrap(), RecordType::CNAME);
904 assert_eq!(RecordType::from_str("dnskey").unwrap(), RecordType::DNSKEY);
905 }
906
907 #[test]
908 fn record_type_from_str_accepts_mixed_case() {
909 assert_eq!(RecordType::from_str("Mx").unwrap(), RecordType::MX);
910 assert_eq!(RecordType::from_str("cNaMe").unwrap(), RecordType::CNAME);
911 }
912
913 #[test]
914 fn record_type_from_str_rejects_whitespace_padded() {
915 assert!(RecordType::from_str(" A").is_err());
919 assert!(RecordType::from_str("A ").is_err());
920 assert!(RecordType::from_str("\tA\n").is_err());
921 }
922
923 #[test]
924 fn record_type_from_str_rejects_unknown() {
925 assert!(RecordType::from_str("NOTAREAL").is_err());
926 assert!(RecordType::from_str("A1").is_err());
927 assert!(RecordType::from_str("").is_err());
928 }
929
930 #[test]
931 fn record_type_from_str_accepts_star_as_any() {
932 assert_eq!(RecordType::from_str("*").unwrap(), RecordType::ANY);
933 assert_eq!(RecordType::from_str("ANY").unwrap(), RecordType::ANY);
934 assert_eq!(RecordType::from_str("any").unwrap(), RecordType::ANY);
935 }
936
937 #[test]
940 fn srv_label_accepts_alphanumeric_and_hyphen() {
941 assert!(is_valid_srv_label("http"));
942 assert!(is_valid_srv_label("ldap-tls"));
943 assert!(is_valid_srv_label("a1"));
944 assert!(is_valid_srv_label("tcp"));
945 }
946
947 #[test]
948 fn srv_label_rejects_empty() {
949 assert!(!is_valid_srv_label(""));
950 }
951
952 #[test]
953 fn srv_label_rejects_leading_or_trailing_hyphen() {
954 assert!(!is_valid_srv_label("-http"));
955 assert!(!is_valid_srv_label("http-"));
956 assert!(!is_valid_srv_label("-"));
957 }
958
959 #[test]
960 fn srv_label_rejects_dots() {
961 assert!(!is_valid_srv_label("http.evil"));
964 assert!(!is_valid_srv_label("a.b"));
965 }
966
967 #[test]
968 fn srv_label_rejects_special_chars() {
969 assert!(!is_valid_srv_label("http evil"));
970 assert!(!is_valid_srv_label("http/evil"));
971 assert!(!is_valid_srv_label("http\0"));
972 assert!(!is_valid_srv_label("http\n"));
973 }
974
975 #[test]
976 fn srv_label_rejects_over_63_chars() {
977 let too_long = "a".repeat(64);
978 assert!(!is_valid_srv_label(&too_long));
979 let exactly_63 = "a".repeat(63);
980 assert!(is_valid_srv_label(&exactly_63));
981 }
982
983 #[test]
986 fn classify_ns_presence_absent_on_empty_ok() {
987 let r: Result<Vec<DnsRecord>> = Ok(vec![]);
989 assert_eq!(classify_ns_presence(&r), DnsPresence::Absent);
990 }
991
992 #[test]
993 fn classify_ns_presence_present_on_records() {
994 let rec = DnsRecord {
995 name: "example.test.".to_string(),
996 record_type: RecordType::NS,
997 ttl: 3600,
998 data: RecordData::NS {
999 nameserver: "ns1.example.net.".to_string(),
1000 },
1001 };
1002 let r: Result<Vec<DnsRecord>> = Ok(vec![rec]);
1003 assert_eq!(classify_ns_presence(&r), DnsPresence::Present);
1004 }
1005
1006 #[test]
1007 fn classify_ns_presence_unknown_on_error() {
1008 let r: Result<Vec<DnsRecord>> = Err(SeerError::DnsError("servfail".to_string()));
1009 assert_eq!(classify_ns_presence(&r), DnsPresence::Unknown);
1010 }
1011
1012 #[test]
1015 fn reverse_dns_name_formats_ipv4_correctly() {
1016 let ip: IpAddr = Ipv4Addr::new(192, 0, 2, 1).into();
1017 assert_eq!(reverse_dns_name(&ip), "1.2.0.192.in-addr.arpa");
1018 }
1019
1020 #[test]
1021 fn reverse_dns_name_formats_ipv6_correctly() {
1022 let ip: IpAddr = Ipv6Addr::LOCALHOST.into();
1024 let name = reverse_dns_name(&ip);
1025 assert!(
1026 name.ends_with(".ip6.arpa"),
1027 "must end with .ip6.arpa; got: {}",
1028 name
1029 );
1030 assert!(
1032 name.starts_with("1."),
1033 "expected '1.' prefix, got: {}",
1034 name
1035 );
1036 assert_eq!(name.len(), 72);
1038 }
1039
1040 #[test]
1043 fn resolver_new_has_default_timeout() {
1044 let r = DnsResolver::new();
1045 assert_eq!(r.timeout, DEFAULT_TIMEOUT);
1046 }
1047
1048 #[test]
1049 fn resolver_with_timeout_overrides_default() {
1050 let custom = Duration::from_secs(42);
1051 let r = DnsResolver::new().with_timeout(custom);
1052 assert_eq!(r.timeout, custom);
1053 }
1054
1055 #[test]
1056 fn resolver_default_matches_new() {
1057 let a = DnsResolver::default();
1058 let b = DnsResolver::new();
1059 assert_eq!(a.timeout, b.timeout);
1060 }
1061
1062 #[tokio::test]
1065 async fn custom_resolver_rejects_invalid_input() {
1066 let r = DnsResolver::new();
1071 let err = r.create_custom_resolver("..").await.unwrap_err();
1072 let msg = err.to_string().to_lowercase();
1073 assert!(
1074 msg.contains("dns resolution failed") || msg.contains("invalid"),
1075 "expected resolution failure, got: {}",
1076 msg
1077 );
1078 }
1079
1080 #[tokio::test]
1081 async fn custom_resolver_rejects_private_ipv4() {
1082 let r = DnsResolver::new();
1085 for reserved in ["127.0.0.1", "10.0.0.1", "192.168.1.1", "169.254.169.254"] {
1086 let err = r.create_custom_resolver(reserved).await.unwrap_err();
1087 let msg = err.to_string().to_lowercase();
1088 assert!(
1089 msg.contains("blocked") || msg.contains("reserved"),
1090 "reserved IP {} must be rejected, got error: {}",
1091 reserved,
1092 msg
1093 );
1094 }
1095 }
1096
1097 #[tokio::test]
1098 async fn custom_resolver_rejects_loopback_ipv6() {
1099 let r = DnsResolver::new();
1100 let err = r.create_custom_resolver("::1").await.unwrap_err();
1101 let msg = err.to_string().to_lowercase();
1102 assert!(
1103 msg.contains("blocked") || msg.contains("reserved"),
1104 "::1 must be rejected, got error: {}",
1105 msg
1106 );
1107 }
1108
1109 #[tokio::test]
1110 async fn custom_resolver_accepts_public_ipv4() {
1111 let r = DnsResolver::new();
1113 let result = r.create_custom_resolver("8.8.8.8").await;
1114 assert!(
1115 result.is_ok(),
1116 "8.8.8.8 must be accepted as a public nameserver, got: {:?}",
1117 result.err()
1118 );
1119 }
1120
1121 #[tokio::test]
1124 async fn resolve_srv_rejects_invalid_service_label() {
1125 let r = DnsResolver::new();
1126 let result = r.resolve_srv("http.evil", "tcp", "example.com", None).await;
1128 assert!(result.is_err());
1129 let msg = result.unwrap_err().to_string().to_lowercase();
1130 assert!(
1131 msg.contains("invalid srv service"),
1132 "expected SRV service validation error, got: {}",
1133 msg
1134 );
1135 }
1136
1137 #[tokio::test]
1138 async fn resolve_srv_rejects_invalid_protocol_label() {
1139 let r = DnsResolver::new();
1140 let result = r.resolve_srv("http", "tcp.evil", "example.com", None).await;
1141 assert!(result.is_err());
1142 let msg = result.unwrap_err().to_string().to_lowercase();
1143 assert!(
1144 msg.contains("invalid srv protocol"),
1145 "expected SRV protocol validation error, got: {}",
1146 msg
1147 );
1148 }
1149
1150 #[tokio::test]
1153 async fn resolve_normalizes_uppercase_domain_input() {
1154 let r = DnsResolver::new();
1159 let result = r.resolve(".bad.example", RecordType::A, None).await;
1160 assert!(result.is_err(), "leading-dot domain must be rejected");
1161 }
1162
1163 #[tokio::test]
1166 async fn resolve_rejects_srv_record_type_without_srv_helper() {
1167 let r = DnsResolver::new();
1170 let result = r.resolve("example.com", RecordType::SRV, None).await;
1171 assert!(result.is_err());
1172 let msg = result.unwrap_err().to_string();
1173 assert!(
1174 msg.contains("SRV records require service name format"),
1175 "expected helpful SRV error, got: {}",
1176 msg
1177 );
1178 }
1179}