1use std::net::IpAddr;
12use std::str::FromStr;
13use std::time::Duration;
14
15use hickory_resolver::config::{NameServerConfig, ResolveHosts, ResolverConfig, GOOGLE};
16use hickory_resolver::net::runtime::TokioRuntimeProvider;
17use hickory_resolver::net::NetError;
18use hickory_resolver::proto::dnssec::PublicKey;
19use hickory_resolver::proto::rr::rdata::CAA;
20use hickory_resolver::proto::rr::{RData as HickoryRData, RecordType as HickoryRecordType};
21use hickory_resolver::TokioResolver;
22use tracing::{debug, instrument};
23
24use super::records::{DnsRecord, RecordData, RecordType};
25use crate::error::{Result, SeerError};
26use crate::validation::normalize_domain;
27
28fn dns_lookup_or_empty<T>(
32 result: std::result::Result<T, NetError>,
33 record_type: &str,
34) -> Result<Option<T>> {
35 match result {
36 Ok(response) => Ok(Some(response)),
37 Err(e) if e.is_no_records_found() => Ok(None),
38 Err(e) => Err(SeerError::DnsError(format!(
39 "{} lookup failed: {}",
40 record_type, e
41 ))),
42 }
43}
44
45const DEFAULT_TIMEOUT: Duration = Duration::from_secs(5);
48
49fn build_resolver(config: ResolverConfig, timeout: Duration) -> TokioResolver {
56 let mut builder = TokioResolver::builder_with_config(config, TokioRuntimeProvider::default());
57 {
58 let opts = builder.options_mut();
59 opts.timeout = timeout;
60 opts.attempts = 2;
61 opts.use_hosts_file = ResolveHosts::Never;
62 }
63 builder
64 .build()
65 .expect("hickory resolver build is infallible without TLS features")
66}
67
68#[derive(Clone)]
74pub struct DnsResolver {
75 timeout: Duration,
76 default_resolver: TokioResolver,
79}
80
81impl std::fmt::Debug for DnsResolver {
82 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
83 f.debug_struct("DnsResolver")
84 .field("timeout", &self.timeout)
85 .finish()
86 }
87}
88
89impl Default for DnsResolver {
90 fn default() -> Self {
91 Self::new()
92 }
93}
94
95impl DnsResolver {
96 pub fn new() -> Self {
98 Self {
99 timeout: DEFAULT_TIMEOUT,
100 default_resolver: build_resolver(ResolverConfig::udp_and_tcp(&GOOGLE), DEFAULT_TIMEOUT),
101 }
102 }
103
104 pub fn with_timeout(mut self, timeout: Duration) -> Self {
108 self.timeout = timeout;
109 self.default_resolver = build_resolver(ResolverConfig::udp_and_tcp(&GOOGLE), timeout);
110 self
111 }
112
113 async fn create_custom_resolver(&self, nameserver: &str) -> Result<TokioResolver> {
114 let ips: Vec<IpAddr> = if let Ok(ip) = nameserver.parse::<IpAddr>() {
122 vec![ip]
123 } else {
124 let response = self
125 .default_resolver
126 .lookup_ip(nameserver)
127 .await
128 .map_err(|e| {
129 SeerError::DnsError(format!(
130 "failed to resolve nameserver hostname {}: {}",
131 nameserver, e
132 ))
133 })?;
134 let resolved: Vec<IpAddr> = response.iter().collect();
135 if resolved.is_empty() {
136 return Err(SeerError::DnsError(format!(
137 "nameserver {} did not resolve to any addresses",
138 nameserver
139 )));
140 }
141 resolved
142 };
143
144 for ip in &ips {
148 if let Some(reason) = crate::validation::describe_reserved_ip(ip) {
149 return Err(SeerError::DnsError(format!(
150 "nameserver {} blocked: {}",
151 nameserver, reason
152 )));
153 }
154 }
155
156 let mut config = ResolverConfig::from_parts(None, vec![], vec![]);
161 for ip in ips {
162 config.add_name_server(NameServerConfig::udp(ip));
163 }
164
165 Ok(build_resolver(config, self.timeout))
166 }
167
168 #[instrument(skip(self), fields(domain = %domain, record_type = %record_type))]
175 pub async fn resolve(
176 &self,
177 domain: &str,
178 record_type: RecordType,
179 nameserver: Option<&str>,
180 ) -> Result<Vec<DnsRecord>> {
181 let custom_resolver;
183 let resolver = if let Some(ns) = nameserver {
184 custom_resolver = self.create_custom_resolver(ns).await?;
185 &custom_resolver
186 } else {
187 &self.default_resolver
188 };
189 let domain = prepare_query(domain, record_type)?;
190
191 debug!(nameserver = nameserver.unwrap_or("system"), "Resolving DNS");
192
193 match record_type {
194 RecordType::A => self.resolve_a(resolver, &domain).await,
195 RecordType::AAAA => self.resolve_aaaa(resolver, &domain).await,
196 RecordType::CNAME => self.resolve_cname(resolver, &domain).await,
197 RecordType::MX => self.resolve_mx(resolver, &domain).await,
198 RecordType::NS => self.resolve_ns(resolver, &domain).await,
199 RecordType::TXT => self.resolve_txt(resolver, &domain).await,
200 RecordType::SOA => self.resolve_soa(resolver, &domain).await,
201 RecordType::PTR => self.resolve_ptr(resolver, &domain).await,
202 RecordType::SRV => match parse_srv_query(&domain) {
203 Some((service, protocol, name)) => {
205 self.resolve_srv_core(resolver, &service, &protocol, &name)
206 .await
207 }
208 None => Err(srv_format_error()),
211 },
212 RecordType::CAA => self.resolve_caa(resolver, &domain).await,
213 RecordType::DNSKEY => self.resolve_dnskey(resolver, &domain).await,
214 RecordType::DS => self.resolve_ds(resolver, &domain).await,
215 RecordType::TLSA => self.resolve_tlsa(resolver, &domain).await,
216 RecordType::SSHFP => self.resolve_sshfp(resolver, &domain).await,
217 RecordType::NAPTR => self.resolve_naptr(resolver, &domain).await,
218 RecordType::ANY => self.resolve_any(resolver, &domain).await,
219 }
220 }
221
222 #[instrument(skip(self), fields(domain = %domain, service = %service, protocol = %protocol))]
230 pub async fn resolve_srv(
231 &self,
232 service: &str,
233 protocol: &str,
234 domain: &str,
235 nameserver: Option<&str>,
236 ) -> Result<Vec<DnsRecord>> {
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 self.resolve_srv_core(resolver, service, protocol, domain)
245 .await
246 }
247
248 async fn resolve_srv_core(
255 &self,
256 resolver: &TokioResolver,
257 service: &str,
258 protocol: &str,
259 domain: &str,
260 ) -> Result<Vec<DnsRecord>> {
261 if !is_valid_srv_label(service) {
262 return Err(SeerError::InvalidInput(format!(
263 "invalid SRV service name: {}",
264 service
265 )));
266 }
267 if !is_valid_srv_label(protocol) {
268 return Err(SeerError::InvalidInput(format!(
269 "invalid SRV protocol name: {}",
270 protocol
271 )));
272 }
273
274 let query_name = format!("_{}._{}.{}", service, protocol, domain);
275
276 let Some(response) = dns_lookup_or_empty(
277 resolver.lookup(&query_name, HickoryRecordType::SRV).await,
278 "SRV",
279 )?
280 else {
281 return Ok(vec![]);
282 };
283
284 let records = response
285 .answers()
286 .iter()
287 .filter_map(|record| {
288 if let HickoryRData::SRV(srv) = &record.data {
289 Some(DnsRecord {
290 name: query_name.clone(),
291 record_type: RecordType::SRV,
292 ttl: record.ttl,
293 data: RecordData::SRV {
294 priority: srv.priority,
295 weight: srv.weight,
296 port: srv.port,
297 target: srv.target.to_string(),
298 },
299 })
300 } else {
301 None
302 }
303 })
304 .collect();
305
306 Ok(records)
307 }
308
309 async fn resolve_a(&self, resolver: &TokioResolver, domain: &str) -> Result<Vec<DnsRecord>> {
310 let Some(response) =
311 dns_lookup_or_empty(resolver.lookup(domain, HickoryRecordType::A).await, "A")?
312 else {
313 return Ok(vec![]);
314 };
315
316 let records = response
317 .answers()
318 .iter()
319 .filter_map(|record| {
320 if let HickoryRData::A(addr) = &record.data {
321 Some(DnsRecord {
322 name: domain.to_string(),
323 record_type: RecordType::A,
324 ttl: record.ttl,
325 data: RecordData::A {
326 address: addr.0.to_string(),
327 },
328 })
329 } else {
330 None
331 }
332 })
333 .collect();
334
335 Ok(records)
336 }
337
338 async fn resolve_aaaa(&self, resolver: &TokioResolver, domain: &str) -> Result<Vec<DnsRecord>> {
339 let Some(response) = dns_lookup_or_empty(
340 resolver.lookup(domain, HickoryRecordType::AAAA).await,
341 "AAAA",
342 )?
343 else {
344 return Ok(vec![]);
345 };
346
347 let records = response
348 .answers()
349 .iter()
350 .filter_map(|record| {
351 if let HickoryRData::AAAA(addr) = &record.data {
352 Some(DnsRecord {
353 name: domain.to_string(),
354 record_type: RecordType::AAAA,
355 ttl: record.ttl,
356 data: RecordData::AAAA {
357 address: addr.0.to_string(),
358 },
359 })
360 } else {
361 None
362 }
363 })
364 .collect();
365
366 Ok(records)
367 }
368
369 async fn resolve_cname(
370 &self,
371 resolver: &TokioResolver,
372 domain: &str,
373 ) -> Result<Vec<DnsRecord>> {
374 let Some(response) = dns_lookup_or_empty(
375 resolver.lookup(domain, HickoryRecordType::CNAME).await,
376 "CNAME",
377 )?
378 else {
379 return Ok(vec![]);
380 };
381
382 let records = response
383 .answers()
384 .iter()
385 .filter_map(|record| {
386 if let HickoryRData::CNAME(cname) = &record.data {
387 Some(DnsRecord {
388 name: domain.to_string(),
389 record_type: RecordType::CNAME,
390 ttl: record.ttl,
391 data: RecordData::CNAME {
392 target: cname.0.to_string(),
393 },
394 })
395 } else {
396 None
397 }
398 })
399 .collect();
400
401 Ok(records)
402 }
403
404 async fn resolve_mx(&self, resolver: &TokioResolver, domain: &str) -> Result<Vec<DnsRecord>> {
405 let Some(response) =
406 dns_lookup_or_empty(resolver.lookup(domain, HickoryRecordType::MX).await, "MX")?
407 else {
408 return Ok(vec![]);
409 };
410
411 let mut records: Vec<DnsRecord> = response
412 .answers()
413 .iter()
414 .filter_map(|record| {
415 if let HickoryRData::MX(mx) = &record.data {
416 Some(DnsRecord {
417 name: domain.to_string(),
418 record_type: RecordType::MX,
419 ttl: record.ttl,
420 data: RecordData::MX {
421 preference: mx.preference,
422 exchange: mx.exchange.to_string(),
423 },
424 })
425 } else {
426 None
427 }
428 })
429 .collect();
430
431 records.sort_by_key(|r| {
432 if let RecordData::MX { preference, .. } = &r.data {
433 *preference
434 } else {
435 0
436 }
437 });
438
439 Ok(records)
440 }
441
442 async fn resolve_ns(&self, resolver: &TokioResolver, domain: &str) -> Result<Vec<DnsRecord>> {
443 let Some(response) =
444 dns_lookup_or_empty(resolver.lookup(domain, HickoryRecordType::NS).await, "NS")?
445 else {
446 return Ok(vec![]);
447 };
448
449 let records = response
450 .answers()
451 .iter()
452 .filter_map(|record| {
453 if let HickoryRData::NS(ns) = &record.data {
454 Some(DnsRecord {
455 name: domain.to_string(),
456 record_type: RecordType::NS,
457 ttl: record.ttl,
458 data: RecordData::NS {
459 nameserver: ns.0.to_string(),
460 },
461 })
462 } else {
463 None
464 }
465 })
466 .collect();
467
468 Ok(records)
469 }
470
471 async fn resolve_txt(&self, resolver: &TokioResolver, domain: &str) -> Result<Vec<DnsRecord>> {
472 let Some(response) =
473 dns_lookup_or_empty(resolver.lookup(domain, HickoryRecordType::TXT).await, "TXT")?
474 else {
475 return Ok(vec![]);
476 };
477
478 let records = response
479 .answers()
480 .iter()
481 .filter_map(|record| {
482 if let HickoryRData::TXT(txt) = &record.data {
483 let text = txt
484 .txt_data
485 .iter()
486 .map(|data| String::from_utf8_lossy(data).to_string())
487 .collect::<Vec<_>>()
488 .join("");
489
490 Some(DnsRecord {
491 name: domain.to_string(),
492 record_type: RecordType::TXT,
493 ttl: record.ttl,
494 data: RecordData::TXT { text },
495 })
496 } else {
497 None
498 }
499 })
500 .collect();
501
502 Ok(records)
503 }
504
505 async fn resolve_soa(&self, resolver: &TokioResolver, domain: &str) -> Result<Vec<DnsRecord>> {
506 let Some(response) =
507 dns_lookup_or_empty(resolver.lookup(domain, HickoryRecordType::SOA).await, "SOA")?
508 else {
509 return Ok(vec![]);
510 };
511
512 let records = response
513 .answers()
514 .iter()
515 .filter_map(|record| {
516 if let HickoryRData::SOA(soa) = &record.data {
517 Some(DnsRecord {
518 name: domain.to_string(),
519 record_type: RecordType::SOA,
520 ttl: record.ttl,
521 data: RecordData::SOA {
522 mname: soa.mname.to_string(),
523 rname: soa.rname.to_string(),
524 serial: soa.serial,
525 refresh: soa.refresh as u32,
532 retry: soa.retry as u32,
533 expire: soa.expire as u32,
534 minimum: soa.minimum,
535 },
536 })
537 } else {
538 None
539 }
540 })
541 .collect();
542
543 Ok(records)
544 }
545
546 async fn resolve_ptr(&self, resolver: &TokioResolver, query: &str) -> Result<Vec<DnsRecord>> {
547 let query = if let Ok(ip) = IpAddr::from_str(query) {
549 reverse_dns_name(&ip)
550 } else {
551 query.to_string()
552 };
553
554 let Some(response) =
555 dns_lookup_or_empty(resolver.lookup(&query, HickoryRecordType::PTR).await, "PTR")?
556 else {
557 return Ok(vec![]);
558 };
559
560 let records = response
561 .answers()
562 .iter()
563 .filter_map(|record| {
564 if let HickoryRData::PTR(ptr) = &record.data {
565 Some(DnsRecord {
566 name: query.clone(),
567 record_type: RecordType::PTR,
568 ttl: record.ttl,
569 data: RecordData::PTR {
570 target: ptr.0.to_string(),
571 },
572 })
573 } else {
574 None
575 }
576 })
577 .collect();
578
579 Ok(records)
580 }
581
582 async fn resolve_caa(&self, resolver: &TokioResolver, domain: &str) -> Result<Vec<DnsRecord>> {
583 let Some(response) =
584 dns_lookup_or_empty(resolver.lookup(domain, HickoryRecordType::CAA).await, "CAA")?
585 else {
586 return Ok(vec![]);
587 };
588
589 let records = response
590 .answers()
591 .iter()
592 .filter_map(|record| {
593 if let HickoryRData::CAA(caa) = &record.data {
594 let (flags, tag, value) = parse_caa(caa);
595 Some(DnsRecord {
596 name: domain.to_string(),
597 record_type: RecordType::CAA,
598 ttl: record.ttl,
599 data: RecordData::CAA { flags, tag, value },
600 })
601 } else {
602 None
603 }
604 })
605 .collect();
606
607 Ok(records)
608 }
609
610 async fn resolve_dnskey(
611 &self,
612 resolver: &TokioResolver,
613 domain: &str,
614 ) -> Result<Vec<DnsRecord>> {
615 use hickory_resolver::proto::dnssec::rdata::DNSSECRData;
616
617 let Some(response) = dns_lookup_or_empty(
618 resolver.lookup(domain, HickoryRecordType::DNSKEY).await,
619 "DNSKEY",
620 )?
621 else {
622 return Ok(vec![]);
623 };
624
625 let records = response
626 .answers()
627 .iter()
628 .filter_map(|record| {
629 if let HickoryRData::DNSSEC(DNSSECRData::DNSKEY(dnskey)) = &record.data {
630 use base64::{engine::general_purpose::STANDARD, Engine};
631 let public_key_buf = dnskey.public_key();
632 let public_key = STANDARD.encode(public_key_buf.public_bytes());
633 Some(DnsRecord {
634 name: domain.to_string(),
635 record_type: RecordType::DNSKEY,
636 ttl: record.ttl,
637 data: RecordData::DNSKEY {
638 flags: dnskey.flags(),
639 protocol: 3,
641 algorithm: u8::from(public_key_buf.algorithm()),
642 public_key,
643 },
644 })
645 } else {
646 None
647 }
648 })
649 .collect();
650
651 Ok(records)
652 }
653
654 async fn resolve_ds(&self, resolver: &TokioResolver, domain: &str) -> Result<Vec<DnsRecord>> {
655 use hickory_resolver::proto::dnssec::rdata::DNSSECRData;
656
657 let Some(response) =
658 dns_lookup_or_empty(resolver.lookup(domain, HickoryRecordType::DS).await, "DS")?
659 else {
660 return Ok(vec![]);
661 };
662
663 let records = response
664 .answers()
665 .iter()
666 .filter_map(|record| {
667 if let HickoryRData::DNSSEC(DNSSECRData::DS(ds)) = &record.data {
668 let digest = ds
669 .digest()
670 .iter()
671 .map(|b| format!("{:02X}", b))
672 .collect::<String>();
673 Some(DnsRecord {
674 name: domain.to_string(),
675 record_type: RecordType::DS,
676 ttl: record.ttl,
677 data: RecordData::DS {
678 key_tag: ds.key_tag(),
679 algorithm: u8::from(ds.algorithm()),
680 digest_type: u8::from(ds.digest_type()),
681 digest,
682 },
683 })
684 } else {
685 None
686 }
687 })
688 .collect();
689
690 Ok(records)
691 }
692
693 async fn resolve_tlsa(&self, resolver: &TokioResolver, domain: &str) -> Result<Vec<DnsRecord>> {
694 let Some(response) = dns_lookup_or_empty(
700 resolver.lookup(domain, HickoryRecordType::TLSA).await,
701 "TLSA",
702 )?
703 else {
704 return Ok(vec![]);
705 };
706
707 let records = response
708 .answers()
709 .iter()
710 .filter_map(|record| {
711 if let HickoryRData::TLSA(tlsa) = &record.data {
712 let cert_data = tlsa
713 .cert_data
714 .iter()
715 .map(|b| format!("{:02X}", b))
716 .collect::<String>();
717 Some(DnsRecord {
718 name: domain.to_string(),
719 record_type: RecordType::TLSA,
720 ttl: record.ttl,
721 data: RecordData::TLSA {
722 cert_usage: u8::from(tlsa.cert_usage),
723 selector: u8::from(tlsa.selector),
724 matching: u8::from(tlsa.matching),
725 cert_data,
726 },
727 })
728 } else {
729 None
730 }
731 })
732 .collect();
733
734 Ok(records)
735 }
736
737 async fn resolve_sshfp(
738 &self,
739 resolver: &TokioResolver,
740 domain: &str,
741 ) -> Result<Vec<DnsRecord>> {
742 let Some(response) = dns_lookup_or_empty(
743 resolver.lookup(domain, HickoryRecordType::SSHFP).await,
744 "SSHFP",
745 )?
746 else {
747 return Ok(vec![]);
748 };
749
750 let records = response
751 .answers()
752 .iter()
753 .filter_map(|record| {
754 if let HickoryRData::SSHFP(sshfp) = &record.data {
755 let fingerprint = sshfp
756 .fingerprint
757 .iter()
758 .map(|b| format!("{:02X}", b))
759 .collect::<String>();
760 Some(DnsRecord {
761 name: domain.to_string(),
762 record_type: RecordType::SSHFP,
763 ttl: record.ttl,
764 data: RecordData::SSHFP {
765 algorithm: u8::from(sshfp.algorithm),
766 fingerprint_type: u8::from(sshfp.fingerprint_type),
767 fingerprint,
768 },
769 })
770 } else {
771 None
772 }
773 })
774 .collect();
775
776 Ok(records)
777 }
778
779 async fn resolve_naptr(
780 &self,
781 resolver: &TokioResolver,
782 domain: &str,
783 ) -> Result<Vec<DnsRecord>> {
784 let Some(response) = dns_lookup_or_empty(
785 resolver.lookup(domain, HickoryRecordType::NAPTR).await,
786 "NAPTR",
787 )?
788 else {
789 return Ok(vec![]);
790 };
791
792 let records = response
793 .answers()
794 .iter()
795 .filter_map(|record| {
796 if let HickoryRData::NAPTR(naptr) = &record.data {
797 Some(DnsRecord {
798 name: domain.to_string(),
799 record_type: RecordType::NAPTR,
800 ttl: record.ttl,
801 data: RecordData::NAPTR {
805 order: naptr.order,
806 preference: naptr.preference,
807 flags: String::from_utf8_lossy(&naptr.flags).into_owned(),
808 services: String::from_utf8_lossy(&naptr.services).into_owned(),
809 regexp: String::from_utf8_lossy(&naptr.regexp).into_owned(),
810 replacement: naptr.replacement.to_string(),
811 },
812 })
813 } else {
814 None
815 }
816 })
817 .collect();
818
819 Ok(records)
820 }
821
822 async fn resolve_any(&self, resolver: &TokioResolver, domain: &str) -> Result<Vec<DnsRecord>> {
823 let record_types = [
828 RecordType::A,
829 RecordType::AAAA,
830 RecordType::MX,
831 RecordType::NS,
832 RecordType::TXT,
833 RecordType::SOA,
834 RecordType::CAA,
835 ];
836
837 let results = futures::future::join_all(
838 record_types
839 .into_iter()
840 .map(|record_type| self.resolve_type(resolver, domain, record_type)),
841 )
842 .await;
843
844 let mut all_records = Vec::new();
849 let mut any_ok = false;
850 let mut last_err = None;
851 for result in results {
852 match result {
853 Ok(records) => {
854 any_ok = true;
855 all_records.extend(records);
856 }
857 Err(e) => last_err = Some(e),
858 }
859 }
860
861 match last_err {
862 Some(e) if !any_ok => Err(e),
863 _ => Ok(all_records),
864 }
865 }
866
867 async fn resolve_type(
868 &self,
869 resolver: &TokioResolver,
870 domain: &str,
871 record_type: RecordType,
872 ) -> Result<Vec<DnsRecord>> {
873 match record_type {
874 RecordType::A => self.resolve_a(resolver, domain).await,
875 RecordType::AAAA => self.resolve_aaaa(resolver, domain).await,
876 RecordType::CNAME => self.resolve_cname(resolver, domain).await,
877 RecordType::MX => self.resolve_mx(resolver, domain).await,
878 RecordType::NS => self.resolve_ns(resolver, domain).await,
879 RecordType::TXT => self.resolve_txt(resolver, domain).await,
880 RecordType::SOA => self.resolve_soa(resolver, domain).await,
881 RecordType::CAA => self.resolve_caa(resolver, domain).await,
882 RecordType::DNSKEY => self.resolve_dnskey(resolver, domain).await,
883 RecordType::DS => self.resolve_ds(resolver, domain).await,
884 _ => Err(SeerError::DnsError("unsupported record type".to_string())),
885 }
886 }
887}
888
889#[derive(Debug, Clone, Copy, PartialEq, Eq)]
894pub enum DnsPresence {
895 Present,
897 Absent,
899 Unknown,
901}
902
903fn classify_ns_presence(result: &Result<Vec<DnsRecord>>) -> DnsPresence {
908 match result {
909 Ok(records) if records.is_empty() => DnsPresence::Absent,
910 Ok(_) => DnsPresence::Present,
911 Err(_) => DnsPresence::Unknown,
912 }
913}
914
915impl DnsResolver {
916 pub async fn presence(&self, domain: &str) -> DnsPresence {
924 classify_ns_presence(&self.resolve(domain, RecordType::NS, None).await)
925 }
926}
927
928fn prepare_query(domain: &str, record_type: RecordType) -> Result<String> {
941 if record_type == RecordType::PTR {
942 if let Ok(ip) = IpAddr::from_str(domain.trim()) {
943 return Ok(ip.to_string());
944 }
945 }
946 normalize_domain(domain)
947}
948
949pub(crate) fn parse_srv_query(name: &str) -> Option<(String, String, String)> {
954 let mut parts = name.splitn(3, '.');
955 let service = parts.next()?.strip_prefix('_')?;
956 let protocol = parts.next()?.strip_prefix('_')?;
957 let rest = parts.next()?;
958 if service.is_empty() || protocol.is_empty() || rest.is_empty() {
959 return None;
960 }
961 Some((service.to_string(), protocol.to_string(), rest.to_string()))
962}
963
964pub(crate) fn srv_format_error() -> SeerError {
968 SeerError::InvalidInput(
969 "SRV records require service name format: _service._proto.name".to_string(),
970 )
971}
972
973fn reverse_dns_name(ip: &IpAddr) -> String {
974 match ip {
975 IpAddr::V4(addr) => {
976 let octets = addr.octets();
977 format!(
978 "{}.{}.{}.{}.in-addr.arpa",
979 octets[3], octets[2], octets[1], octets[0]
980 )
981 }
982 IpAddr::V6(addr) => {
983 let segments = addr.segments();
984 let mut result = String::with_capacity(72);
986 let mut first = true;
987 for segment in segments.iter().rev() {
988 for shift in [0, 4, 8, 12] {
989 if !first {
990 result.push('.');
991 }
992 first = false;
993 let nibble = (segment >> shift) & 0xF;
994 result
995 .push(char::from_digit(nibble as u32, 16).expect("nibble is always 0-15"));
996 }
997 }
998 result.push_str(".ip6.arpa");
999 result
1000 }
1001 }
1002}
1003
1004fn parse_caa(caa: &CAA) -> (u8, String, String) {
1005 let flags = if caa.issuer_critical { 128 } else { 0 };
1012 let tag = caa.tag.clone();
1013 let value = String::from_utf8_lossy(&caa.value).to_string();
1014 (flags, tag, value)
1015}
1016
1017fn is_valid_srv_label(label: &str) -> bool {
1019 !label.is_empty()
1020 && label.len() <= 63
1021 && label.chars().all(|c| c.is_ascii_alphanumeric() || c == '-')
1022 && !label.starts_with('-')
1023 && !label.ends_with('-')
1024}
1025
1026#[cfg(test)]
1027mod tests {
1028 use super::*;
1038 use std::net::{Ipv4Addr, Ipv6Addr};
1039
1040 #[test]
1043 fn record_type_from_str_accepts_lowercase() {
1044 assert_eq!(RecordType::from_str("a").unwrap(), RecordType::A);
1045 assert_eq!(RecordType::from_str("mx").unwrap(), RecordType::MX);
1046 assert_eq!(RecordType::from_str("cname").unwrap(), RecordType::CNAME);
1047 assert_eq!(RecordType::from_str("dnskey").unwrap(), RecordType::DNSKEY);
1048 }
1049
1050 #[test]
1051 fn record_type_from_str_accepts_mixed_case() {
1052 assert_eq!(RecordType::from_str("Mx").unwrap(), RecordType::MX);
1053 assert_eq!(RecordType::from_str("cNaMe").unwrap(), RecordType::CNAME);
1054 }
1055
1056 #[test]
1057 fn record_type_from_str_rejects_whitespace_padded() {
1058 assert!(RecordType::from_str(" A").is_err());
1062 assert!(RecordType::from_str("A ").is_err());
1063 assert!(RecordType::from_str("\tA\n").is_err());
1064 }
1065
1066 #[test]
1067 fn record_type_from_str_rejects_unknown() {
1068 assert!(RecordType::from_str("NOTAREAL").is_err());
1069 assert!(RecordType::from_str("A1").is_err());
1070 assert!(RecordType::from_str("").is_err());
1071 }
1072
1073 #[test]
1074 fn record_type_from_str_accepts_star_as_any() {
1075 assert_eq!(RecordType::from_str("*").unwrap(), RecordType::ANY);
1076 assert_eq!(RecordType::from_str("ANY").unwrap(), RecordType::ANY);
1077 assert_eq!(RecordType::from_str("any").unwrap(), RecordType::ANY);
1078 }
1079
1080 #[test]
1083 fn srv_label_accepts_alphanumeric_and_hyphen() {
1084 assert!(is_valid_srv_label("http"));
1085 assert!(is_valid_srv_label("ldap-tls"));
1086 assert!(is_valid_srv_label("a1"));
1087 assert!(is_valid_srv_label("tcp"));
1088 }
1089
1090 #[test]
1091 fn srv_label_rejects_empty() {
1092 assert!(!is_valid_srv_label(""));
1093 }
1094
1095 #[test]
1096 fn srv_label_rejects_leading_or_trailing_hyphen() {
1097 assert!(!is_valid_srv_label("-http"));
1098 assert!(!is_valid_srv_label("http-"));
1099 assert!(!is_valid_srv_label("-"));
1100 }
1101
1102 #[test]
1103 fn srv_label_rejects_dots() {
1104 assert!(!is_valid_srv_label("http.evil"));
1107 assert!(!is_valid_srv_label("a.b"));
1108 }
1109
1110 #[test]
1111 fn srv_label_rejects_special_chars() {
1112 assert!(!is_valid_srv_label("http evil"));
1113 assert!(!is_valid_srv_label("http/evil"));
1114 assert!(!is_valid_srv_label("http\0"));
1115 assert!(!is_valid_srv_label("http\n"));
1116 }
1117
1118 #[test]
1119 fn srv_label_rejects_over_63_chars() {
1120 let too_long = "a".repeat(64);
1121 assert!(!is_valid_srv_label(&too_long));
1122 let exactly_63 = "a".repeat(63);
1123 assert!(is_valid_srv_label(&exactly_63));
1124 }
1125
1126 #[test]
1129 fn classify_ns_presence_absent_on_empty_ok() {
1130 let r: Result<Vec<DnsRecord>> = Ok(vec![]);
1132 assert_eq!(classify_ns_presence(&r), DnsPresence::Absent);
1133 }
1134
1135 #[test]
1136 fn classify_ns_presence_present_on_records() {
1137 let rec = DnsRecord {
1138 name: "example.test.".to_string(),
1139 record_type: RecordType::NS,
1140 ttl: 3600,
1141 data: RecordData::NS {
1142 nameserver: "ns1.example.net.".to_string(),
1143 },
1144 };
1145 let r: Result<Vec<DnsRecord>> = Ok(vec![rec]);
1146 assert_eq!(classify_ns_presence(&r), DnsPresence::Present);
1147 }
1148
1149 #[test]
1150 fn classify_ns_presence_unknown_on_error() {
1151 let r: Result<Vec<DnsRecord>> = Err(SeerError::DnsError("servfail".to_string()));
1152 assert_eq!(classify_ns_presence(&r), DnsPresence::Unknown);
1153 }
1154
1155 #[test]
1158 fn reverse_dns_name_formats_ipv4_correctly() {
1159 let ip: IpAddr = Ipv4Addr::new(192, 0, 2, 1).into();
1160 assert_eq!(reverse_dns_name(&ip), "1.2.0.192.in-addr.arpa");
1161 }
1162
1163 #[test]
1164 fn reverse_dns_name_formats_ipv6_correctly() {
1165 let ip: IpAddr = Ipv6Addr::LOCALHOST.into();
1167 let name = reverse_dns_name(&ip);
1168 assert!(
1169 name.ends_with(".ip6.arpa"),
1170 "must end with .ip6.arpa; got: {}",
1171 name
1172 );
1173 assert!(
1175 name.starts_with("1."),
1176 "expected '1.' prefix, got: {}",
1177 name
1178 );
1179 assert_eq!(name.len(), 72);
1181 }
1182
1183 #[test]
1186 fn resolver_new_has_default_timeout() {
1187 let r = DnsResolver::new();
1188 assert_eq!(r.timeout, DEFAULT_TIMEOUT);
1189 }
1190
1191 #[test]
1192 fn resolver_with_timeout_overrides_default() {
1193 let custom = Duration::from_secs(42);
1194 let r = DnsResolver::new().with_timeout(custom);
1195 assert_eq!(r.timeout, custom);
1196 }
1197
1198 #[test]
1199 fn resolver_default_matches_new() {
1200 let a = DnsResolver::default();
1201 let b = DnsResolver::new();
1202 assert_eq!(a.timeout, b.timeout);
1203 }
1204
1205 #[tokio::test]
1208 async fn custom_resolver_rejects_invalid_input() {
1209 let r = DnsResolver::new();
1214 let err = r.create_custom_resolver("..").await.unwrap_err();
1215 let msg = err.to_string().to_lowercase();
1216 assert!(
1217 msg.contains("dns resolution failed") || msg.contains("invalid"),
1218 "expected resolution failure, got: {}",
1219 msg
1220 );
1221 }
1222
1223 #[tokio::test]
1224 async fn custom_resolver_rejects_private_ipv4() {
1225 let r = DnsResolver::new();
1228 for reserved in ["127.0.0.1", "10.0.0.1", "192.168.1.1", "169.254.169.254"] {
1229 let err = r.create_custom_resolver(reserved).await.unwrap_err();
1230 let msg = err.to_string().to_lowercase();
1231 assert!(
1232 msg.contains("blocked") || msg.contains("reserved"),
1233 "reserved IP {} must be rejected, got error: {}",
1234 reserved,
1235 msg
1236 );
1237 }
1238 }
1239
1240 #[tokio::test]
1241 async fn custom_resolver_rejects_loopback_ipv6() {
1242 let r = DnsResolver::new();
1243 let err = r.create_custom_resolver("::1").await.unwrap_err();
1244 let msg = err.to_string().to_lowercase();
1245 assert!(
1246 msg.contains("blocked") || msg.contains("reserved"),
1247 "::1 must be rejected, got error: {}",
1248 msg
1249 );
1250 }
1251
1252 #[tokio::test]
1253 async fn custom_resolver_accepts_public_ipv4() {
1254 let r = DnsResolver::new();
1256 let result = r.create_custom_resolver("8.8.8.8").await;
1257 assert!(
1258 result.is_ok(),
1259 "8.8.8.8 must be accepted as a public nameserver, got: {:?}",
1260 result.err()
1261 );
1262 }
1263
1264 #[tokio::test]
1267 async fn resolve_srv_rejects_invalid_service_label() {
1268 let r = DnsResolver::new();
1269 let result = r.resolve_srv("http.evil", "tcp", "example.com", None).await;
1271 assert!(result.is_err());
1272 let msg = result.unwrap_err().to_string().to_lowercase();
1273 assert!(
1274 msg.contains("invalid srv service"),
1275 "expected SRV service validation error, got: {}",
1276 msg
1277 );
1278 }
1279
1280 #[tokio::test]
1281 async fn resolve_srv_rejects_invalid_protocol_label() {
1282 let r = DnsResolver::new();
1283 let result = r.resolve_srv("http", "tcp.evil", "example.com", None).await;
1284 assert!(result.is_err());
1285 let msg = result.unwrap_err().to_string().to_lowercase();
1286 assert!(
1287 msg.contains("invalid srv protocol"),
1288 "expected SRV protocol validation error, got: {}",
1289 msg
1290 );
1291 }
1292
1293 #[tokio::test]
1296 async fn resolve_normalizes_uppercase_domain_input() {
1297 let r = DnsResolver::new();
1302 let result = r.resolve(".bad.example", RecordType::A, None).await;
1303 assert!(result.is_err(), "leading-dot domain must be rejected");
1304 }
1305
1306 #[test]
1311 fn parse_srv_query_extracts_service_proto_and_name() {
1312 assert_eq!(
1313 parse_srv_query("_sip._tcp.example.com"),
1314 Some((
1315 "sip".to_string(),
1316 "tcp".to_string(),
1317 "example.com".to_string()
1318 ))
1319 );
1320 }
1321
1322 #[test]
1323 fn parse_srv_query_keeps_multilabel_domain() {
1324 assert_eq!(
1325 parse_srv_query("_sip._tcp.sip.voice.google.com"),
1326 Some((
1327 "sip".to_string(),
1328 "tcp".to_string(),
1329 "sip.voice.google.com".to_string()
1330 ))
1331 );
1332 }
1333
1334 #[test]
1335 fn parse_srv_query_rejects_bare_domain() {
1336 assert_eq!(parse_srv_query("example.com"), None);
1337 }
1338
1339 #[test]
1340 fn parse_srv_query_rejects_missing_proto_label() {
1341 assert_eq!(parse_srv_query("_sip.example.com"), None);
1343 }
1344
1345 #[tokio::test]
1346 async fn resolve_rejects_bare_domain_for_srv_as_input_error() {
1347 let r = DnsResolver::new();
1352 let err = r
1353 .resolve("example.com", RecordType::SRV, None)
1354 .await
1355 .expect_err("bare-domain SRV must error");
1356 assert!(
1357 matches!(err, SeerError::InvalidInput(_)),
1358 "bare-domain SRV should be an input error, got: {err:?}"
1359 );
1360 assert!(err.to_string().contains("_service._proto"));
1361 }
1362
1363 #[tokio::test]
1364 #[ignore = "live network"]
1365 async fn resolve_srv_via_dig_style_name_returns_records() {
1366 let r = DnsResolver::new();
1369 let records = r
1370 .resolve("_caldavs._tcp.google.com", RecordType::SRV, None)
1371 .await
1372 .expect("dig-style SRV lookup should succeed");
1373 assert!(!records.is_empty(), "expected SRV records");
1374 assert!(records.iter().all(|r| r.record_type == RecordType::SRV));
1375 }
1376
1377 #[tokio::test]
1378 #[ignore = "live network"]
1379 async fn resolve_naptr_returns_records() {
1380 let r = DnsResolver::new();
1382 let records = r
1383 .resolve("sip2sip.info", RecordType::NAPTR, None)
1384 .await
1385 .expect("NAPTR lookup should succeed");
1386 assert!(!records.is_empty(), "expected NAPTR records");
1387 assert!(records.iter().all(|r| r.record_type == RecordType::NAPTR));
1388 }
1389
1390 #[test]
1393 fn prepare_query_passes_ipv6_literal_through_for_ptr() {
1394 let out = prepare_query("2606:4700:4700::1111", RecordType::PTR).unwrap();
1400 assert_eq!(out, "2606:4700:4700::1111");
1401 }
1402
1403 #[test]
1404 fn prepare_query_passes_ipv6_loopback_through_for_ptr() {
1405 let out = prepare_query("::1", RecordType::PTR).unwrap();
1406 assert_eq!(out, "::1");
1407 }
1408
1409 #[test]
1410 fn prepare_query_passes_ipv4_literal_through_for_ptr() {
1411 let out = prepare_query("8.8.8.8", RecordType::PTR).unwrap();
1412 assert_eq!(out, "8.8.8.8");
1413 }
1414
1415 #[test]
1416 fn prepare_query_normalizes_non_ip_ptr_names() {
1417 let out = prepare_query("1.1.1.1.in-addr.arpa", RecordType::PTR).unwrap();
1419 assert_eq!(out, "1.1.1.1.in-addr.arpa");
1420 }
1421
1422 #[test]
1423 fn prepare_query_normalizes_domains_for_non_ptr() {
1424 let out = prepare_query("HTTPS://WWW.Example.com/path", RecordType::A).unwrap();
1425 assert_eq!(out, "example.com");
1426 }
1427}