1use std::net::IpAddr;
12use std::str::FromStr;
13use std::sync::Arc;
14use std::time::Duration;
15
16use hickory_resolver::config::{NameServerConfig, ResolveHosts, ResolverConfig, GOOGLE};
17use hickory_resolver::net::runtime::TokioRuntimeProvider;
18use hickory_resolver::net::NetError;
19use hickory_resolver::proto::dnssec::PublicKey;
20use hickory_resolver::proto::rr::rdata::CAA;
21use hickory_resolver::proto::rr::{RData as HickoryRData, RecordType as HickoryRecordType};
22use hickory_resolver::TokioResolver;
23use tracing::{debug, instrument};
24
25use super::nameserver::{NameserverProtocol, NameserverSpec};
26use super::records::{DnsRecord, RecordData, RecordType};
27use crate::error::{Result, SeerError};
28use crate::validation::normalize_domain;
29
30fn dns_lookup_or_empty<T>(
34 result: std::result::Result<T, NetError>,
35 record_type: &str,
36) -> Result<Option<T>> {
37 match result {
38 Ok(response) => Ok(Some(response)),
39 Err(e) if e.is_no_records_found() => Ok(None),
40 Err(e) => Err(SeerError::DnsError(format!(
41 "{} lookup failed: {}",
42 record_type, e
43 ))),
44 }
45}
46
47const DEFAULT_TIMEOUT: Duration = Duration::from_secs(5);
50
51fn build_resolver(config: ResolverConfig, timeout: Duration) -> Result<TokioResolver> {
60 let mut builder = TokioResolver::builder_with_config(config, TokioRuntimeProvider::default());
61 {
62 let opts = builder.options_mut();
63 opts.timeout = timeout;
64 opts.attempts = 2;
65 opts.use_hosts_file = ResolveHosts::Never;
66 }
67 builder
68 .build()
69 .map_err(|e| SeerError::DnsError(format!("failed to construct DNS resolver: {}", e)))
70}
71
72fn build_default_resolver(timeout: Duration) -> TokioResolver {
79 build_resolver(ResolverConfig::udp_and_tcp(&GOOGLE), timeout)
80 .expect("default resolver build cannot fail with the bundled webpki root store")
81}
82
83fn build_upstream_config(
93 spec: &NameserverSpec,
94 ips: &[IpAddr],
95 port_override: Option<u16>,
96) -> ResolverConfig {
97 let mut config = ResolverConfig::from_parts(None, vec![], vec![]);
98 let port = port_override.unwrap_or(spec.port);
99 for ip in ips {
100 let mut ns = match spec.protocol {
101 NameserverProtocol::Udp => NameServerConfig::udp(*ip),
102 NameserverProtocol::Tls => NameServerConfig::tls(*ip, Arc::from(spec.tls_name())),
103 NameserverProtocol::Https => NameServerConfig::https(
104 *ip,
105 Arc::from(spec.tls_name()),
106 spec.path.as_deref().map(Arc::from),
107 ),
108 };
109 for connection in &mut ns.connections {
110 connection.port = port;
111 }
112 config.add_name_server(ns);
113 }
114 config
115}
116
117#[derive(Clone)]
125pub struct DnsResolver {
126 timeout: Duration,
127 default_resolver: TokioResolver,
130 port_override: Option<u16>,
135 allow_private_hosts: bool,
140}
141
142impl std::fmt::Debug for DnsResolver {
143 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
144 f.debug_struct("DnsResolver")
145 .field("timeout", &self.timeout)
146 .finish()
147 }
148}
149
150impl Default for DnsResolver {
151 fn default() -> Self {
152 Self::new()
153 }
154}
155
156impl DnsResolver {
157 pub fn new() -> Self {
159 Self {
160 timeout: DEFAULT_TIMEOUT,
161 default_resolver: build_default_resolver(DEFAULT_TIMEOUT),
162 port_override: None,
163 allow_private_hosts: false,
164 }
165 }
166
167 pub fn from_config(config: &crate::config::SeerConfig) -> Self {
179 Self::new().with_timeout(config.dns_timeout())
180 }
181
182 #[cfg(test)]
184 pub(crate) fn allowing_private_hosts(mut self) -> Self {
185 self.allow_private_hosts = true;
186 self
187 }
188
189 #[cfg(test)]
192 pub(crate) fn with_port(mut self, port: u16) -> Self {
193 self.port_override = Some(port);
194 self
195 }
196
197 pub fn with_timeout(mut self, timeout: Duration) -> Self {
201 self.timeout = timeout;
202 self.default_resolver = build_default_resolver(timeout);
203 self
204 }
205
206 async fn create_custom_resolver(&self, nameserver: &str) -> Result<TokioResolver> {
207 let spec = NameserverSpec::parse(nameserver)?;
212
213 let ips: Vec<IpAddr> = if let Ok(ip) = spec.host.parse::<IpAddr>() {
222 vec![ip]
223 } else {
224 let response = self
225 .default_resolver
226 .lookup_ip(spec.host.as_str())
227 .await
228 .map_err(|e| {
229 SeerError::DnsError(format!(
230 "failed to resolve nameserver hostname {}: {}",
231 spec.host, e
232 ))
233 })?;
234 let resolved: Vec<IpAddr> = response.iter().collect();
235 if resolved.is_empty() {
236 return Err(SeerError::DnsError(format!(
237 "nameserver {} did not resolve to any addresses",
238 spec.host
239 )));
240 }
241 resolved
242 };
243
244 if !self.allow_private_hosts {
251 for ip in &ips {
252 if let Some(reason) = crate::validation::describe_reserved_ip(ip) {
253 return Err(SeerError::DnsError(format!(
254 "nameserver {} blocked: {}",
255 nameserver, reason
256 )));
257 }
258 }
259 }
260
261 build_resolver(
262 build_upstream_config(&spec, &ips, self.port_override),
263 self.timeout,
264 )
265 }
266
267 #[instrument(skip(self), fields(domain = %domain, record_type = %record_type))]
277 pub async fn resolve(
278 &self,
279 domain: &str,
280 record_type: RecordType,
281 nameserver: Option<&str>,
282 ) -> Result<Vec<DnsRecord>> {
283 let custom_resolver;
285 let resolver = if let Some(ns) = nameserver {
286 custom_resolver = self.create_custom_resolver(ns).await?;
287 &custom_resolver
288 } else {
289 &self.default_resolver
290 };
291 let domain = prepare_query(domain, record_type)?;
292
293 debug!(nameserver = nameserver.unwrap_or("system"), "Resolving DNS");
294
295 match record_type {
296 RecordType::SRV => match parse_srv_query(&domain) {
297 Some((service, protocol, name)) => {
299 self.resolve_srv_core(resolver, &service, &protocol, &name)
300 .await
301 }
302 None => Err(srv_format_error()),
305 },
306 RecordType::ANY => self.resolve_any(resolver, &domain).await,
307 single => self.resolve_type(resolver, &domain, single).await,
308 }
309 }
310
311 #[instrument(skip(self), fields(domain = %domain, service = %service, protocol = %protocol))]
319 pub async fn resolve_srv(
320 &self,
321 service: &str,
322 protocol: &str,
323 domain: &str,
324 nameserver: Option<&str>,
325 ) -> Result<Vec<DnsRecord>> {
326 let custom_resolver;
327 let resolver = if let Some(ns) = nameserver {
328 custom_resolver = self.create_custom_resolver(ns).await?;
329 &custom_resolver
330 } else {
331 &self.default_resolver
332 };
333 self.resolve_srv_core(resolver, service, protocol, domain)
334 .await
335 }
336
337 async fn resolve_srv_core(
344 &self,
345 resolver: &TokioResolver,
346 service: &str,
347 protocol: &str,
348 domain: &str,
349 ) -> Result<Vec<DnsRecord>> {
350 if !is_valid_srv_label(service) {
351 return Err(SeerError::InvalidInput(format!(
352 "invalid SRV service name: {}",
353 service
354 )));
355 }
356 if !is_valid_srv_label(protocol) {
357 return Err(SeerError::InvalidInput(format!(
358 "invalid SRV protocol name: {}",
359 protocol
360 )));
361 }
362
363 let query_name = format!("_{}._{}.{}", service, protocol, domain);
364
365 let Some(response) = dns_lookup_or_empty(
366 resolver.lookup(&query_name, HickoryRecordType::SRV).await,
367 "SRV",
368 )?
369 else {
370 return Ok(vec![]);
371 };
372
373 let records = response
374 .answers()
375 .iter()
376 .filter_map(|record| {
377 if let HickoryRData::SRV(srv) = &record.data {
378 Some(DnsRecord {
379 name: query_name.clone(),
380 record_type: RecordType::SRV,
381 ttl: record.ttl,
382 data: RecordData::SRV {
383 priority: srv.priority,
384 weight: srv.weight,
385 port: srv.port,
386 target: srv.target.to_string(),
387 },
388 })
389 } else {
390 None
391 }
392 })
393 .collect();
394
395 Ok(records)
396 }
397
398 async fn resolve_type(
406 &self,
407 resolver: &TokioResolver,
408 domain: &str,
409 record_type: RecordType,
410 ) -> Result<Vec<DnsRecord>> {
411 match record_type {
412 RecordType::PTR => {
415 let query = if let Ok(ip) = IpAddr::from_str(domain) {
416 reverse_dns_name(&ip)
417 } else {
418 domain.to_string()
419 };
420 self.resolve_records(resolver, &query, RecordType::PTR)
421 .await
422 }
423 single => self.resolve_records(resolver, domain, single).await,
424 }
425 }
426
427 async fn resolve_records(
435 &self,
436 resolver: &TokioResolver,
437 domain: &str,
438 record_type: RecordType,
439 ) -> Result<Vec<DnsRecord>> {
440 let Some(wire_type) = wire_type(record_type) else {
441 return Err(unsupported_record_type(record_type));
442 };
443
444 let Some(response) = dns_lookup_or_empty(
445 resolver.lookup(domain, wire_type).await,
446 &record_type.to_string(),
447 )?
448 else {
449 return Ok(vec![]);
450 };
451
452 let mut records: Vec<DnsRecord> = response
453 .answers()
454 .iter()
455 .filter_map(|record| {
456 convert_rdata(record_type, &record.data).map(|data| DnsRecord {
457 name: domain.to_string(),
458 record_type,
459 ttl: record.ttl,
460 data,
461 })
462 })
463 .collect();
464
465 if record_type == RecordType::MX {
466 records.sort_by_key(|r| match &r.data {
467 RecordData::MX { preference, .. } => *preference,
468 _ => 0,
469 });
470 }
471
472 Ok(records)
473 }
474
475 async fn resolve_any(&self, resolver: &TokioResolver, domain: &str) -> Result<Vec<DnsRecord>> {
476 let record_types = [
481 RecordType::A,
482 RecordType::AAAA,
483 RecordType::MX,
484 RecordType::NS,
485 RecordType::TXT,
486 RecordType::SOA,
487 RecordType::CAA,
488 ];
489
490 let results = futures::future::join_all(
491 record_types
492 .into_iter()
493 .map(|record_type| self.resolve_type(resolver, domain, record_type)),
494 )
495 .await;
496
497 let mut all_records = Vec::new();
502 let mut any_ok = false;
503 let mut last_err = None;
504 for result in results {
505 match result {
506 Ok(records) => {
507 any_ok = true;
508 all_records.extend(records);
509 }
510 Err(e) => last_err = Some(e),
511 }
512 }
513
514 match last_err {
515 Some(e) if !any_ok => Err(e),
516 _ => Ok(all_records),
517 }
518 }
519}
520
521#[derive(Debug, Clone, Copy, PartialEq, Eq)]
526pub enum DnsPresence {
527 Present,
529 Absent,
531 Unknown,
533}
534
535fn classify_ns_presence(result: &Result<Vec<DnsRecord>>) -> DnsPresence {
540 match result {
541 Ok(records) if records.is_empty() => DnsPresence::Absent,
542 Ok(_) => DnsPresence::Present,
543 Err(_) => DnsPresence::Unknown,
544 }
545}
546
547impl DnsResolver {
548 pub async fn presence(&self, domain: &str) -> DnsPresence {
556 classify_ns_presence(&self.resolve(domain, RecordType::NS, None).await)
557 }
558}
559
560fn prepare_query(domain: &str, record_type: RecordType) -> Result<String> {
573 if record_type == RecordType::PTR {
574 if let Ok(ip) = IpAddr::from_str(domain.trim()) {
575 return Ok(ip.to_string());
576 }
577 }
578 normalize_domain(domain)
579}
580
581pub(crate) fn parse_srv_query(name: &str) -> Option<(String, String, String)> {
586 let mut parts = name.splitn(3, '.');
587 let service = parts.next()?.strip_prefix('_')?;
588 let protocol = parts.next()?.strip_prefix('_')?;
589 let rest = parts.next()?;
590 if service.is_empty() || protocol.is_empty() || rest.is_empty() {
591 return None;
592 }
593 Some((service.to_string(), protocol.to_string(), rest.to_string()))
594}
595
596pub(crate) fn srv_format_error() -> SeerError {
600 SeerError::InvalidInput(
601 "SRV records require service name format: _service._proto.name".to_string(),
602 )
603}
604
605fn reverse_dns_name(ip: &IpAddr) -> String {
606 match ip {
607 IpAddr::V4(addr) => {
608 let octets = addr.octets();
609 format!(
610 "{}.{}.{}.{}.in-addr.arpa",
611 octets[3], octets[2], octets[1], octets[0]
612 )
613 }
614 IpAddr::V6(addr) => {
615 let segments = addr.segments();
616 let mut result = String::with_capacity(72);
618 let mut first = true;
619 for segment in segments.iter().rev() {
620 for shift in [0, 4, 8, 12] {
621 if !first {
622 result.push('.');
623 }
624 first = false;
625 let nibble = (segment >> shift) & 0xF;
626 result
627 .push(char::from_digit(nibble as u32, 16).expect("nibble is always 0-15"));
628 }
629 }
630 result.push_str(".ip6.arpa");
631 result
632 }
633 }
634}
635
636fn parse_caa(caa: &CAA) -> (u8, String, String) {
637 let flags = if caa.issuer_critical { 128 } else { 0 };
644 let tag = caa.tag.clone();
645 let value = String::from_utf8_lossy(&caa.value).to_string();
646 (flags, tag, value)
647}
648
649fn wire_type(record_type: RecordType) -> Option<HickoryRecordType> {
655 Some(match record_type {
656 RecordType::A => HickoryRecordType::A,
657 RecordType::AAAA => HickoryRecordType::AAAA,
658 RecordType::CNAME => HickoryRecordType::CNAME,
659 RecordType::MX => HickoryRecordType::MX,
660 RecordType::NS => HickoryRecordType::NS,
661 RecordType::TXT => HickoryRecordType::TXT,
662 RecordType::SOA => HickoryRecordType::SOA,
663 RecordType::PTR => HickoryRecordType::PTR,
664 RecordType::CAA => HickoryRecordType::CAA,
665 RecordType::DNSKEY => HickoryRecordType::DNSKEY,
666 RecordType::DS => HickoryRecordType::DS,
667 RecordType::TLSA => HickoryRecordType::TLSA,
673 RecordType::SSHFP => HickoryRecordType::SSHFP,
674 RecordType::NAPTR => HickoryRecordType::NAPTR,
675 RecordType::SRV | RecordType::ANY => return None,
676 })
677}
678
679fn unsupported_record_type(record_type: RecordType) -> SeerError {
682 SeerError::DnsError(format!("unsupported record type: {}", record_type))
683}
684
685fn hex_upper(bytes: &[u8]) -> String {
688 bytes.iter().map(|b| format!("{:02X}", b)).collect()
689}
690
691fn convert_rdata(record_type: RecordType, data: &HickoryRData) -> Option<RecordData> {
698 use hickory_resolver::proto::dnssec::rdata::DNSSECRData;
699
700 match (record_type, data) {
701 (RecordType::A, HickoryRData::A(addr)) => Some(RecordData::A {
702 address: addr.0.to_string(),
703 }),
704 (RecordType::AAAA, HickoryRData::AAAA(addr)) => Some(RecordData::AAAA {
705 address: addr.0.to_string(),
706 }),
707 (RecordType::CNAME, HickoryRData::CNAME(cname)) => Some(RecordData::CNAME {
708 target: cname.0.to_string(),
709 }),
710 (RecordType::MX, HickoryRData::MX(mx)) => Some(RecordData::MX {
711 preference: mx.preference,
712 exchange: mx.exchange.to_string(),
713 }),
714 (RecordType::NS, HickoryRData::NS(ns)) => Some(RecordData::NS {
715 nameserver: ns.0.to_string(),
716 }),
717 (RecordType::TXT, HickoryRData::TXT(txt)) => Some(RecordData::TXT {
718 text: txt
719 .txt_data
720 .iter()
721 .map(|data| String::from_utf8_lossy(data).to_string())
722 .collect::<Vec<_>>()
723 .join(""),
724 }),
725 (RecordType::SOA, HickoryRData::SOA(soa)) => Some(RecordData::SOA {
726 mname: soa.mname.to_string(),
727 rname: soa.rname.to_string(),
728 serial: soa.serial,
729 refresh: soa.refresh as u32,
735 retry: soa.retry as u32,
736 expire: soa.expire as u32,
737 minimum: soa.minimum,
738 }),
739 (RecordType::PTR, HickoryRData::PTR(ptr)) => Some(RecordData::PTR {
740 target: ptr.0.to_string(),
741 }),
742 (RecordType::CAA, HickoryRData::CAA(caa)) => {
743 let (flags, tag, value) = parse_caa(caa);
744 Some(RecordData::CAA { flags, tag, value })
745 }
746 (RecordType::DNSKEY, HickoryRData::DNSSEC(DNSSECRData::DNSKEY(dnskey))) => {
747 use base64::{engine::general_purpose::STANDARD, Engine};
748 let public_key_buf = dnskey.public_key();
749 Some(RecordData::DNSKEY {
750 flags: dnskey.flags(),
751 protocol: 3,
753 algorithm: u8::from(public_key_buf.algorithm()),
754 public_key: STANDARD.encode(public_key_buf.public_bytes()),
755 })
756 }
757 (RecordType::DS, HickoryRData::DNSSEC(DNSSECRData::DS(ds))) => Some(RecordData::DS {
758 key_tag: ds.key_tag(),
759 algorithm: u8::from(ds.algorithm()),
760 digest_type: u8::from(ds.digest_type()),
761 digest: hex_upper(ds.digest()),
762 }),
763 (RecordType::TLSA, HickoryRData::TLSA(tlsa)) => Some(RecordData::TLSA {
764 cert_usage: u8::from(tlsa.cert_usage),
765 selector: u8::from(tlsa.selector),
766 matching: u8::from(tlsa.matching),
767 cert_data: hex_upper(&tlsa.cert_data),
768 }),
769 (RecordType::SSHFP, HickoryRData::SSHFP(sshfp)) => Some(RecordData::SSHFP {
770 algorithm: u8::from(sshfp.algorithm),
771 fingerprint_type: u8::from(sshfp.fingerprint_type),
772 fingerprint: hex_upper(&sshfp.fingerprint),
773 }),
774 (RecordType::NAPTR, HickoryRData::NAPTR(naptr)) => Some(RecordData::NAPTR {
778 order: naptr.order,
779 preference: naptr.preference,
780 flags: String::from_utf8_lossy(&naptr.flags).into_owned(),
781 services: String::from_utf8_lossy(&naptr.services).into_owned(),
782 regexp: String::from_utf8_lossy(&naptr.regexp).into_owned(),
783 replacement: naptr.replacement.to_string(),
784 }),
785 _ => None,
786 }
787}
788
789fn is_valid_srv_label(label: &str) -> bool {
791 !label.is_empty()
792 && label.len() <= 63
793 && label.chars().all(|c| c.is_ascii_alphanumeric() || c == '-')
794 && !label.starts_with('-')
795 && !label.ends_with('-')
796}
797
798#[cfg(test)]
799mod tests {
800 use super::*;
808
809 #[test]
810 fn from_config_applies_dns_timeout() {
811 let mut config = crate::config::SeerConfig::default();
812 config.timeouts.dns_secs = 9;
813 let resolver = DnsResolver::from_config(&config);
814 assert_eq!(resolver.timeout, Duration::from_secs(9));
815 }
816 use std::net::{Ipv4Addr, Ipv6Addr};
817
818 #[test]
821 fn record_type_from_str_accepts_lowercase() {
822 assert_eq!(RecordType::from_str("a").unwrap(), RecordType::A);
823 assert_eq!(RecordType::from_str("mx").unwrap(), RecordType::MX);
824 assert_eq!(RecordType::from_str("cname").unwrap(), RecordType::CNAME);
825 assert_eq!(RecordType::from_str("dnskey").unwrap(), RecordType::DNSKEY);
826 }
827
828 #[test]
829 fn record_type_from_str_accepts_mixed_case() {
830 assert_eq!(RecordType::from_str("Mx").unwrap(), RecordType::MX);
831 assert_eq!(RecordType::from_str("cNaMe").unwrap(), RecordType::CNAME);
832 }
833
834 #[test]
835 fn record_type_from_str_rejects_whitespace_padded() {
836 assert!(RecordType::from_str(" A").is_err());
840 assert!(RecordType::from_str("A ").is_err());
841 assert!(RecordType::from_str("\tA\n").is_err());
842 }
843
844 #[test]
845 fn record_type_from_str_rejects_unknown() {
846 assert!(RecordType::from_str("NOTAREAL").is_err());
847 assert!(RecordType::from_str("A1").is_err());
848 assert!(RecordType::from_str("").is_err());
849 }
850
851 #[test]
852 fn record_type_from_str_accepts_star_as_any() {
853 assert_eq!(RecordType::from_str("*").unwrap(), RecordType::ANY);
854 assert_eq!(RecordType::from_str("ANY").unwrap(), RecordType::ANY);
855 assert_eq!(RecordType::from_str("any").unwrap(), RecordType::ANY);
856 }
857
858 #[test]
861 fn srv_label_accepts_alphanumeric_and_hyphen() {
862 assert!(is_valid_srv_label("http"));
863 assert!(is_valid_srv_label("ldap-tls"));
864 assert!(is_valid_srv_label("a1"));
865 assert!(is_valid_srv_label("tcp"));
866 }
867
868 #[test]
869 fn srv_label_rejects_empty() {
870 assert!(!is_valid_srv_label(""));
871 }
872
873 #[test]
874 fn srv_label_rejects_leading_or_trailing_hyphen() {
875 assert!(!is_valid_srv_label("-http"));
876 assert!(!is_valid_srv_label("http-"));
877 assert!(!is_valid_srv_label("-"));
878 }
879
880 #[test]
881 fn srv_label_rejects_dots() {
882 assert!(!is_valid_srv_label("http.evil"));
885 assert!(!is_valid_srv_label("a.b"));
886 }
887
888 #[test]
889 fn srv_label_rejects_special_chars() {
890 assert!(!is_valid_srv_label("http evil"));
891 assert!(!is_valid_srv_label("http/evil"));
892 assert!(!is_valid_srv_label("http\0"));
893 assert!(!is_valid_srv_label("http\n"));
894 }
895
896 #[test]
897 fn srv_label_rejects_over_63_chars() {
898 let too_long = "a".repeat(64);
899 assert!(!is_valid_srv_label(&too_long));
900 let exactly_63 = "a".repeat(63);
901 assert!(is_valid_srv_label(&exactly_63));
902 }
903
904 #[test]
907 fn classify_ns_presence_absent_on_empty_ok() {
908 let r: Result<Vec<DnsRecord>> = Ok(vec![]);
910 assert_eq!(classify_ns_presence(&r), DnsPresence::Absent);
911 }
912
913 #[test]
914 fn classify_ns_presence_present_on_records() {
915 let rec = DnsRecord {
916 name: "example.test.".to_string(),
917 record_type: RecordType::NS,
918 ttl: 3600,
919 data: RecordData::NS {
920 nameserver: "ns1.example.net.".to_string(),
921 },
922 };
923 let r: Result<Vec<DnsRecord>> = Ok(vec![rec]);
924 assert_eq!(classify_ns_presence(&r), DnsPresence::Present);
925 }
926
927 #[test]
928 fn classify_ns_presence_unknown_on_error() {
929 let r: Result<Vec<DnsRecord>> = Err(SeerError::DnsError("servfail".to_string()));
930 assert_eq!(classify_ns_presence(&r), DnsPresence::Unknown);
931 }
932
933 #[test]
936 fn reverse_dns_name_formats_ipv4_correctly() {
937 let ip: IpAddr = Ipv4Addr::new(192, 0, 2, 1).into();
938 assert_eq!(reverse_dns_name(&ip), "1.2.0.192.in-addr.arpa");
939 }
940
941 #[test]
942 fn reverse_dns_name_formats_ipv6_correctly() {
943 let ip: IpAddr = Ipv6Addr::LOCALHOST.into();
945 let name = reverse_dns_name(&ip);
946 assert!(
947 name.ends_with(".ip6.arpa"),
948 "must end with .ip6.arpa; got: {}",
949 name
950 );
951 assert!(
953 name.starts_with("1."),
954 "expected '1.' prefix, got: {}",
955 name
956 );
957 assert_eq!(name.len(), 72);
959 }
960
961 #[test]
964 fn resolver_new_has_default_timeout() {
965 let r = DnsResolver::new();
966 assert_eq!(r.timeout, DEFAULT_TIMEOUT);
967 }
968
969 #[test]
970 fn resolver_with_timeout_overrides_default() {
971 let custom = Duration::from_secs(42);
972 let r = DnsResolver::new().with_timeout(custom);
973 assert_eq!(r.timeout, custom);
974 }
975
976 #[test]
977 fn resolver_default_matches_new() {
978 let a = DnsResolver::default();
979 let b = DnsResolver::new();
980 assert_eq!(a.timeout, b.timeout);
981 }
982
983 #[tokio::test]
986 async fn custom_resolver_rejects_invalid_input() {
987 let r = DnsResolver::new();
992 let err = r.create_custom_resolver("..").await.unwrap_err();
993 let msg = err.to_string().to_lowercase();
994 assert!(
995 msg.contains("dns resolution failed") || msg.contains("invalid"),
996 "expected resolution failure, got: {}",
997 msg
998 );
999 }
1000
1001 #[tokio::test]
1002 async fn custom_resolver_rejects_private_ipv4() {
1003 let r = DnsResolver::new();
1006 for reserved in ["127.0.0.1", "10.0.0.1", "192.168.1.1", "169.254.169.254"] {
1007 let err = r.create_custom_resolver(reserved).await.unwrap_err();
1008 let msg = err.to_string().to_lowercase();
1009 assert!(
1010 msg.contains("blocked") || msg.contains("reserved"),
1011 "reserved IP {} must be rejected, got error: {}",
1012 reserved,
1013 msg
1014 );
1015 }
1016 }
1017
1018 #[tokio::test]
1019 async fn custom_resolver_rejects_loopback_ipv6() {
1020 let r = DnsResolver::new();
1021 let err = r.create_custom_resolver("::1").await.unwrap_err();
1022 let msg = err.to_string().to_lowercase();
1023 assert!(
1024 msg.contains("blocked") || msg.contains("reserved"),
1025 "::1 must be rejected, got error: {}",
1026 msg
1027 );
1028 }
1029
1030 #[tokio::test]
1031 async fn custom_resolver_accepts_public_ipv4() {
1032 let r = DnsResolver::new();
1034 let result = r.create_custom_resolver("8.8.8.8").await;
1035 assert!(
1036 result.is_ok(),
1037 "8.8.8.8 must be accepted as a public nameserver, got: {:?}",
1038 result.err()
1039 );
1040 }
1041
1042 #[tokio::test]
1048 async fn custom_resolver_rejects_private_ip_for_dot_and_doh() {
1049 let r = DnsResolver::new();
1050 for reserved in [
1051 "tls://127.0.0.1",
1052 "tls://192.168.1.1:853",
1053 "tls://[::1]",
1054 "https://10.0.0.1/dns-query",
1055 "https://169.254.169.254",
1056 "https://[fd00::1]:443/dns-query",
1057 ] {
1058 let err = r.create_custom_resolver(reserved).await.unwrap_err();
1059 let msg = err.to_string().to_lowercase();
1060 assert!(
1061 msg.contains("blocked") || msg.contains("reserved"),
1062 "reserved spec {} must be rejected, got error: {}",
1063 reserved,
1064 msg
1065 );
1066 }
1067 }
1068
1069 #[tokio::test]
1070 async fn custom_resolver_accepts_public_dot_and_doh_literals() {
1071 let r = DnsResolver::new();
1074 for spec in ["tls://1.1.1.1", "https://8.8.8.8/dns-query"] {
1075 let result = r.create_custom_resolver(spec).await;
1076 assert!(
1077 result.is_ok(),
1078 "{} must be accepted, got: {:?}",
1079 spec,
1080 result.err()
1081 );
1082 }
1083 }
1084
1085 #[tokio::test]
1086 async fn custom_resolver_rejects_unknown_scheme() {
1087 let r = DnsResolver::new();
1088 let err = r.create_custom_resolver("ftp://8.8.8.8").await.unwrap_err();
1089 assert!(
1090 matches!(err, SeerError::InvalidInput(_)),
1091 "unknown scheme must be an input error, got: {err:?}"
1092 );
1093 }
1094
1095 fn spec(s: &str) -> NameserverSpec {
1098 NameserverSpec::parse(s).unwrap_or_else(|e| panic!("{s:?} must parse: {e}"))
1099 }
1100
1101 #[test]
1102 fn upstream_config_udp_defaults() {
1103 use hickory_resolver::config::ProtocolConfig;
1104
1105 let ip: IpAddr = "8.8.8.8".parse().unwrap();
1106 let config = build_upstream_config(&spec("8.8.8.8"), &[ip], None);
1107 let servers = config.name_servers();
1108 assert_eq!(servers.len(), 1);
1109 assert_eq!(servers[0].ip, ip);
1110 assert_eq!(servers[0].connections.len(), 1);
1111 assert_eq!(servers[0].connections[0].port, 53);
1112 assert!(matches!(
1113 servers[0].connections[0].protocol,
1114 ProtocolConfig::Udp
1115 ));
1116 }
1117
1118 #[test]
1119 fn upstream_config_udp_explicit_port() {
1120 let ip: IpAddr = "9.9.9.9".parse().unwrap();
1121 let config = build_upstream_config(&spec("9.9.9.9:5353"), &[ip], None);
1122 assert_eq!(config.name_servers()[0].connections[0].port, 5353);
1123 }
1124
1125 #[test]
1126 fn upstream_config_tls_sets_protocol_port_and_server_name() {
1127 use hickory_resolver::config::ProtocolConfig;
1128
1129 let ip: IpAddr = "9.9.9.9".parse().unwrap();
1130 let config = build_upstream_config(&spec("tls://dns.quad9.net"), &[ip], None);
1131 let ns = &config.name_servers()[0];
1132 assert_eq!(ns.ip, ip);
1133 assert_eq!(ns.connections.len(), 1);
1134 assert_eq!(ns.connections[0].port, 853);
1135 match &ns.connections[0].protocol {
1136 ProtocolConfig::Tls { server_name } => {
1137 assert_eq!(&**server_name, "dns.quad9.net");
1138 }
1139 other => panic!("expected Tls protocol, got {other:?}"),
1140 }
1141 }
1142
1143 #[test]
1144 fn upstream_config_tls_ip_literal_uses_ip_as_server_name() {
1145 use hickory_resolver::config::ProtocolConfig;
1146
1147 let ip: IpAddr = "1.1.1.1".parse().unwrap();
1148 let config = build_upstream_config(&spec("tls://1.1.1.1"), &[ip], None);
1149 match &config.name_servers()[0].connections[0].protocol {
1150 ProtocolConfig::Tls { server_name } => assert_eq!(&**server_name, "1.1.1.1"),
1151 other => panic!("expected Tls protocol, got {other:?}"),
1152 }
1153 }
1154
1155 #[test]
1156 fn upstream_config_https_sets_protocol_port_path_and_server_name() {
1157 use hickory_resolver::config::ProtocolConfig;
1158
1159 let ip: IpAddr = "104.16.248.249".parse().unwrap();
1160 let config = build_upstream_config(&spec("https://cloudflare-dns.com"), &[ip], None);
1161 let ns = &config.name_servers()[0];
1162 assert_eq!(ns.connections[0].port, 443);
1163 match &ns.connections[0].protocol {
1164 ProtocolConfig::Https { server_name, path } => {
1165 assert_eq!(&**server_name, "cloudflare-dns.com");
1166 assert_eq!(&**path, "/dns-query");
1167 }
1168 other => panic!("expected Https protocol, got {other:?}"),
1169 }
1170 }
1171
1172 #[test]
1173 fn upstream_config_https_custom_port_and_path() {
1174 use hickory_resolver::config::ProtocolConfig;
1175
1176 let ip: IpAddr = "8.8.8.8".parse().unwrap();
1177 let config = build_upstream_config(&spec("https://dns.google:8443/resolve"), &[ip], None);
1178 let ns = &config.name_servers()[0];
1179 assert_eq!(ns.connections[0].port, 8443);
1180 match &ns.connections[0].protocol {
1181 ProtocolConfig::Https { server_name, path } => {
1182 assert_eq!(&**server_name, "dns.google");
1183 assert_eq!(&**path, "/resolve");
1184 }
1185 other => panic!("expected Https protocol, got {other:?}"),
1186 }
1187 }
1188
1189 #[test]
1190 fn upstream_config_multiple_ips_share_spec() {
1191 use hickory_resolver::config::ProtocolConfig;
1194
1195 let ips: Vec<IpAddr> = vec![
1196 "9.9.9.9".parse().unwrap(),
1197 "149.112.112.112".parse().unwrap(),
1198 ];
1199 let config = build_upstream_config(&spec("tls://dns.quad9.net"), &ips, None);
1200 let servers = config.name_servers();
1201 assert_eq!(servers.len(), 2);
1202 for (ns, expected_ip) in servers.iter().zip(&ips) {
1203 assert_eq!(&ns.ip, expected_ip);
1204 assert_eq!(ns.connections[0].port, 853);
1205 assert!(matches!(
1206 &ns.connections[0].protocol,
1207 ProtocolConfig::Tls { server_name } if &**server_name == "dns.quad9.net"
1208 ));
1209 }
1210 }
1211
1212 #[test]
1213 fn upstream_config_test_port_override_wins() {
1214 let ip: IpAddr = "127.0.0.1".parse().unwrap();
1216 let config = build_upstream_config(&spec("127.0.0.1"), &[ip], Some(9999));
1217 assert_eq!(config.name_servers()[0].connections[0].port, 9999);
1218 }
1219
1220 #[tokio::test]
1223 #[ignore = "live network — DoT query against Cloudflare"]
1224 async fn live_resolve_over_dot() {
1225 let r = DnsResolver::new();
1226 let records = r
1227 .resolve("example.com", RecordType::A, Some("tls://1.1.1.1"))
1228 .await
1229 .expect("DoT lookup should succeed");
1230 assert!(!records.is_empty(), "expected A records over DoT");
1231 }
1232
1233 #[tokio::test]
1234 #[ignore = "live network — DoH query against Cloudflare"]
1235 async fn live_resolve_over_doh() {
1236 let r = DnsResolver::new();
1237 let records = r
1238 .resolve(
1239 "example.com",
1240 RecordType::A,
1241 Some("https://cloudflare-dns.com/dns-query"),
1242 )
1243 .await
1244 .expect("DoH lookup should succeed");
1245 assert!(!records.is_empty(), "expected A records over DoH");
1246 }
1247
1248 #[tokio::test]
1251 async fn resolve_srv_rejects_invalid_service_label() {
1252 let r = DnsResolver::new();
1253 let result = r.resolve_srv("http.evil", "tcp", "example.com", None).await;
1255 assert!(result.is_err());
1256 let msg = result.unwrap_err().to_string().to_lowercase();
1257 assert!(
1258 msg.contains("invalid srv service"),
1259 "expected SRV service validation error, got: {}",
1260 msg
1261 );
1262 }
1263
1264 #[tokio::test]
1265 async fn resolve_srv_rejects_invalid_protocol_label() {
1266 let r = DnsResolver::new();
1267 let result = r.resolve_srv("http", "tcp.evil", "example.com", None).await;
1268 assert!(result.is_err());
1269 let msg = result.unwrap_err().to_string().to_lowercase();
1270 assert!(
1271 msg.contains("invalid srv protocol"),
1272 "expected SRV protocol validation error, got: {}",
1273 msg
1274 );
1275 }
1276
1277 #[tokio::test]
1280 async fn resolve_normalizes_uppercase_domain_input() {
1281 let r = DnsResolver::new();
1286 let result = r.resolve(".bad.example", RecordType::A, None).await;
1287 assert!(result.is_err(), "leading-dot domain must be rejected");
1288 }
1289
1290 #[test]
1295 fn parse_srv_query_extracts_service_proto_and_name() {
1296 assert_eq!(
1297 parse_srv_query("_sip._tcp.example.com"),
1298 Some((
1299 "sip".to_string(),
1300 "tcp".to_string(),
1301 "example.com".to_string()
1302 ))
1303 );
1304 }
1305
1306 #[test]
1307 fn parse_srv_query_keeps_multilabel_domain() {
1308 assert_eq!(
1309 parse_srv_query("_sip._tcp.sip.voice.google.com"),
1310 Some((
1311 "sip".to_string(),
1312 "tcp".to_string(),
1313 "sip.voice.google.com".to_string()
1314 ))
1315 );
1316 }
1317
1318 #[test]
1319 fn parse_srv_query_rejects_bare_domain() {
1320 assert_eq!(parse_srv_query("example.com"), None);
1321 }
1322
1323 #[test]
1324 fn parse_srv_query_rejects_missing_proto_label() {
1325 assert_eq!(parse_srv_query("_sip.example.com"), None);
1327 }
1328
1329 #[tokio::test]
1330 async fn resolve_rejects_bare_domain_for_srv_as_input_error() {
1331 let r = DnsResolver::new();
1336 let err = r
1337 .resolve("example.com", RecordType::SRV, None)
1338 .await
1339 .expect_err("bare-domain SRV must error");
1340 assert!(
1341 matches!(err, SeerError::InvalidInput(_)),
1342 "bare-domain SRV should be an input error, got: {err:?}"
1343 );
1344 assert!(err.to_string().contains("_service._proto"));
1345 }
1346
1347 #[tokio::test]
1348 #[ignore = "live network"]
1349 async fn resolve_srv_via_dig_style_name_returns_records() {
1350 let r = DnsResolver::new();
1353 let records = r
1354 .resolve("_caldavs._tcp.google.com", RecordType::SRV, None)
1355 .await
1356 .expect("dig-style SRV lookup should succeed");
1357 assert!(!records.is_empty(), "expected SRV records");
1358 assert!(records.iter().all(|r| r.record_type == RecordType::SRV));
1359 }
1360
1361 #[tokio::test]
1362 #[ignore = "live network"]
1363 async fn resolve_naptr_returns_records() {
1364 let r = DnsResolver::new();
1366 let records = r
1367 .resolve("sip2sip.info", RecordType::NAPTR, None)
1368 .await
1369 .expect("NAPTR lookup should succeed");
1370 assert!(!records.is_empty(), "expected NAPTR records");
1371 assert!(records.iter().all(|r| r.record_type == RecordType::NAPTR));
1372 }
1373
1374 #[test]
1377 fn prepare_query_passes_ipv6_literal_through_for_ptr() {
1378 let out = prepare_query("2606:4700:4700::1111", RecordType::PTR).unwrap();
1384 assert_eq!(out, "2606:4700:4700::1111");
1385 }
1386
1387 #[test]
1388 fn prepare_query_passes_ipv6_loopback_through_for_ptr() {
1389 let out = prepare_query("::1", RecordType::PTR).unwrap();
1390 assert_eq!(out, "::1");
1391 }
1392
1393 #[test]
1394 fn prepare_query_passes_ipv4_literal_through_for_ptr() {
1395 let out = prepare_query("8.8.8.8", RecordType::PTR).unwrap();
1396 assert_eq!(out, "8.8.8.8");
1397 }
1398
1399 #[test]
1400 fn prepare_query_normalizes_non_ip_ptr_names() {
1401 let out = prepare_query("1.1.1.1.in-addr.arpa", RecordType::PTR).unwrap();
1403 assert_eq!(out, "1.1.1.1.in-addr.arpa");
1404 }
1405
1406 #[test]
1407 fn prepare_query_normalizes_domains_for_non_ptr() {
1408 let out = prepare_query("HTTPS://WWW.Example.com/path", RecordType::A).unwrap();
1409 assert_eq!(out, "example.com");
1410 }
1411
1412 use hickory_resolver::proto::op::{Message, OpCode, ResponseCode};
1423 use hickory_resolver::proto::rr::rdata as wire;
1424 use hickory_resolver::proto::rr::rdata::{sshfp, tlsa};
1425 use hickory_resolver::proto::rr::{Name, Record};
1426 use tokio::net::UdpSocket;
1427
1428 #[derive(Clone, Copy)]
1430 enum MockMode {
1431 Zone,
1433 Nxdomain,
1435 NoData,
1437 Ignore,
1439 }
1440
1441 fn name(s: &str) -> Name {
1442 Name::from_ascii(s).expect("valid test name")
1443 }
1444
1445 fn zone_answers(qname: &str, qtype: HickoryRecordType) -> Vec<HickoryRData> {
1448 match (qname.trim_end_matches('.'), qtype) {
1449 ("seer.test", HickoryRecordType::A) => vec![
1450 HickoryRData::A(wire::A(Ipv4Addr::new(192, 0, 2, 1))),
1451 HickoryRData::A(wire::A(Ipv4Addr::new(192, 0, 2, 2))),
1452 ],
1453 ("seer.test", HickoryRecordType::AAAA) => vec![HickoryRData::AAAA(wire::AAAA(
1454 "2001:db8::1".parse().expect("valid IPv6 literal"),
1455 ))],
1456 ("seer.test", HickoryRecordType::MX) => vec![
1458 HickoryRData::MX(wire::MX::new(30, name("c.mail.seer.test."))),
1459 HickoryRData::MX(wire::MX::new(10, name("a.mail.seer.test."))),
1460 HickoryRData::MX(wire::MX::new(20, name("b.mail.seer.test."))),
1461 ],
1462 ("seer.test", HickoryRecordType::NS) => {
1463 vec![HickoryRData::NS(wire::NS(name("ns1.seer.test.")))]
1464 }
1465 ("seer.test", HickoryRecordType::TXT) => vec![HickoryRData::TXT(wire::TXT::new(vec![
1467 "v=spf1 ".to_string(),
1468 "-all".to_string(),
1469 ]))],
1470 ("seer.test", HickoryRecordType::SOA) => vec![HickoryRData::SOA(wire::SOA::new(
1471 name("ns1.seer.test."),
1472 name("hostmaster.seer.test."),
1473 2026070101,
1474 7200,
1475 3600,
1476 1209600,
1477 300,
1478 ))],
1479 ("seer.test", HickoryRecordType::CAA) => vec![
1482 HickoryRData::CAA(CAA::new_issue(false, Some(name("letsencrypt.org")), vec![])),
1483 HickoryRData::CAA(CAA::new_iodef(
1484 true,
1485 url::Url::parse("mailto:security@seer.test").expect("valid iodef URL"),
1486 )),
1487 ],
1488 ("_443._tcp.seer.test", HickoryRecordType::TLSA) => {
1489 vec![HickoryRData::TLSA(wire::TLSA::new(
1490 tlsa::CertUsage::from(3),
1491 tlsa::Selector::from(1),
1492 tlsa::Matching::from(1),
1493 vec![0xAB, 0xCD, 0x01],
1494 ))]
1495 }
1496 ("seer.test", HickoryRecordType::SSHFP) => {
1497 vec![HickoryRData::SSHFP(wire::SSHFP::new(
1498 sshfp::Algorithm::from(4),
1499 sshfp::FingerprintType::from(2),
1500 vec![0xDE, 0xAD, 0xBE, 0xEF],
1501 ))]
1502 }
1503 ("seer.test", HickoryRecordType::NAPTR) => {
1504 vec![HickoryRData::NAPTR(wire::NAPTR::new(
1505 100,
1506 50,
1507 b"U".to_vec().into_boxed_slice(),
1508 b"E2U+sip".to_vec().into_boxed_slice(),
1509 b"!^.*$!sip:info@seer.test!".to_vec().into_boxed_slice(),
1510 Name::root(),
1511 ))]
1512 }
1513 ("_sip._tcp.seer.test", HickoryRecordType::SRV) => vec![HickoryRData::SRV(
1514 wire::SRV::new(10, 5, 5060, name("sipserver.seer.test.")),
1515 )],
1516 ("1.2.0.192.in-addr.arpa", HickoryRecordType::PTR) => {
1517 vec![HickoryRData::PTR(wire::PTR(name("ptr.seer.test.")))]
1518 }
1519 _ => vec![],
1520 }
1521 }
1522
1523 async fn spawn_mock_dns(mode: MockMode) -> u16 {
1527 let socket = UdpSocket::bind("127.0.0.1:0").await.expect("bind mock DNS");
1528 let port = socket.local_addr().expect("mock DNS local addr").port();
1529 tokio::spawn(async move {
1530 let mut buf = [0u8; 4096];
1531 loop {
1532 let Ok((len, src)) = socket.recv_from(&mut buf).await else {
1533 return;
1534 };
1535 if matches!(mode, MockMode::Ignore) {
1536 continue;
1537 }
1538 let Ok(request) = Message::from_vec(&buf[..len]) else {
1539 continue;
1540 };
1541 let mut response = Message::response(request.metadata.id, OpCode::Query);
1542 response.metadata.recursion_desired = request.metadata.recursion_desired;
1543 response.metadata.recursion_available = true;
1544 for query in &request.queries {
1547 response.add_query(query.clone());
1548 }
1549 match mode {
1550 MockMode::Zone => {
1551 if let Some(query) = request.queries.first() {
1552 for rdata in zone_answers(&query.name.to_string(), query.query_type) {
1553 response.add_answer(Record::from_rdata(
1554 query.name.clone(),
1555 300,
1556 rdata,
1557 ));
1558 }
1559 }
1560 }
1561 MockMode::Nxdomain => {
1562 response.metadata.response_code = ResponseCode::NXDomain;
1563 }
1564 MockMode::NoData | MockMode::Ignore => {}
1565 }
1566 let Ok(bytes) = response.to_vec() else {
1567 continue;
1568 };
1569 let _ = socket.send_to(&bytes, src).await;
1570 }
1571 });
1572 port
1573 }
1574
1575 fn mock_dns_resolver(port: u16) -> DnsResolver {
1576 DnsResolver::new()
1577 .with_timeout(Duration::from_millis(500))
1578 .allowing_private_hosts()
1579 .with_port(port)
1580 }
1581
1582 async fn mock_zone_lookup(record_type: RecordType, domain: &str) -> Vec<DnsRecord> {
1583 let port = spawn_mock_dns(MockMode::Zone).await;
1584 mock_dns_resolver(port)
1585 .resolve(domain, record_type, Some("127.0.0.1"))
1586 .await
1587 .unwrap_or_else(|e| panic!("{record_type} lookup against mock must succeed: {e}"))
1588 }
1589
1590 #[tokio::test]
1591 async fn mock_resolve_a_returns_addresses() {
1592 let records = mock_zone_lookup(RecordType::A, "seer.test").await;
1593 assert_eq!(records.len(), 2);
1594 assert!(records.iter().all(|r| r.record_type == RecordType::A));
1595 assert_eq!(records[0].name, "seer.test");
1596 assert_eq!(records[0].ttl, 300);
1597 let addresses: Vec<String> = records
1598 .iter()
1599 .map(|r| match &r.data {
1600 RecordData::A { address } => address.clone(),
1601 other => panic!("expected A data, got {other:?}"),
1602 })
1603 .collect();
1604 assert!(addresses.contains(&"192.0.2.1".to_string()));
1605 assert!(addresses.contains(&"192.0.2.2".to_string()));
1606 }
1607
1608 #[tokio::test]
1609 async fn mock_resolve_mx_sorts_by_preference() {
1610 let records = mock_zone_lookup(RecordType::MX, "seer.test").await;
1611 let prefs: Vec<u16> = records
1612 .iter()
1613 .map(|r| match &r.data {
1614 RecordData::MX { preference, .. } => *preference,
1615 other => panic!("expected MX data, got {other:?}"),
1616 })
1617 .collect();
1618 assert_eq!(prefs, vec![10, 20, 30]);
1620 assert!(matches!(
1621 &records[0].data,
1622 RecordData::MX { exchange, .. } if exchange == "a.mail.seer.test."
1623 ));
1624 }
1625
1626 #[tokio::test]
1627 async fn mock_resolve_txt_joins_character_strings() {
1628 let records = mock_zone_lookup(RecordType::TXT, "seer.test").await;
1629 assert_eq!(records.len(), 1);
1630 match &records[0].data {
1631 RecordData::TXT { text } => assert_eq!(text, "v=spf1 -all"),
1632 other => panic!("expected TXT data, got {other:?}"),
1633 }
1634 }
1635
1636 #[tokio::test]
1637 async fn mock_resolve_soa_maps_all_fields() {
1638 let records = mock_zone_lookup(RecordType::SOA, "seer.test").await;
1639 assert_eq!(records.len(), 1);
1640 match &records[0].data {
1641 RecordData::SOA {
1642 mname,
1643 rname,
1644 serial,
1645 refresh,
1646 retry,
1647 expire,
1648 minimum,
1649 } => {
1650 assert_eq!(mname, "ns1.seer.test.");
1651 assert_eq!(rname, "hostmaster.seer.test.");
1652 assert_eq!(*serial, 2026070101);
1653 assert_eq!(*refresh, 7200);
1654 assert_eq!(*retry, 3600);
1655 assert_eq!(*expire, 1209600);
1656 assert_eq!(*minimum, 300);
1657 }
1658 other => panic!("expected SOA data, got {other:?}"),
1659 }
1660 }
1661
1662 #[tokio::test]
1663 async fn mock_resolve_caa_maps_flags_tag_and_value() {
1664 let records = mock_zone_lookup(RecordType::CAA, "seer.test").await;
1665 assert_eq!(records.len(), 2);
1666 let by_tag = |wanted: &str| {
1667 records
1668 .iter()
1669 .find_map(|r| match &r.data {
1670 RecordData::CAA { flags, tag, value } if tag == wanted => {
1671 Some((*flags, value.clone()))
1672 }
1673 _ => None,
1674 })
1675 .unwrap_or_else(|| panic!("expected a CAA record with tag {wanted}"))
1676 };
1677 assert_eq!(by_tag("issue"), (0, "letsencrypt.org".to_string()));
1679 assert_eq!(
1680 by_tag("iodef"),
1681 (128, "mailto:security@seer.test".to_string())
1682 );
1683 }
1684
1685 #[tokio::test]
1686 async fn mock_resolve_tlsa_hex_encodes_cert_data() {
1687 let records = mock_zone_lookup(RecordType::TLSA, "_443._tcp.seer.test").await;
1688 assert_eq!(records.len(), 1);
1689 match &records[0].data {
1690 RecordData::TLSA {
1691 cert_usage,
1692 selector,
1693 matching,
1694 cert_data,
1695 } => {
1696 assert_eq!((*cert_usage, *selector, *matching), (3, 1, 1));
1697 assert_eq!(cert_data, "ABCD01");
1698 }
1699 other => panic!("expected TLSA data, got {other:?}"),
1700 }
1701 }
1702
1703 #[tokio::test]
1704 async fn mock_resolve_sshfp_hex_encodes_fingerprint() {
1705 let records = mock_zone_lookup(RecordType::SSHFP, "seer.test").await;
1706 assert_eq!(records.len(), 1);
1707 match &records[0].data {
1708 RecordData::SSHFP {
1709 algorithm,
1710 fingerprint_type,
1711 fingerprint,
1712 } => {
1713 assert_eq!((*algorithm, *fingerprint_type), (4, 2));
1714 assert_eq!(fingerprint, "DEADBEEF");
1715 }
1716 other => panic!("expected SSHFP data, got {other:?}"),
1717 }
1718 }
1719
1720 #[tokio::test]
1721 async fn mock_resolve_naptr_decodes_character_strings() {
1722 let records = mock_zone_lookup(RecordType::NAPTR, "seer.test").await;
1723 assert_eq!(records.len(), 1);
1724 match &records[0].data {
1725 RecordData::NAPTR {
1726 order,
1727 preference,
1728 flags,
1729 services,
1730 regexp,
1731 replacement,
1732 } => {
1733 assert_eq!((*order, *preference), (100, 50));
1734 assert_eq!(flags, "U");
1735 assert_eq!(services, "E2U+sip");
1736 assert_eq!(regexp, "!^.*$!sip:info@seer.test!");
1737 assert_eq!(replacement, ".");
1738 }
1739 other => panic!("expected NAPTR data, got {other:?}"),
1740 }
1741 }
1742
1743 #[tokio::test]
1744 async fn mock_resolve_srv_via_dig_style_name() {
1745 let records = mock_zone_lookup(RecordType::SRV, "_sip._tcp.seer.test").await;
1746 assert_eq!(records.len(), 1);
1747 assert_eq!(records[0].name, "_sip._tcp.seer.test");
1748 match &records[0].data {
1749 RecordData::SRV {
1750 priority,
1751 weight,
1752 port,
1753 target,
1754 } => {
1755 assert_eq!((*priority, *weight, *port), (10, 5, 5060));
1756 assert_eq!(target, "sipserver.seer.test.");
1757 }
1758 other => panic!("expected SRV data, got {other:?}"),
1759 }
1760 }
1761
1762 #[tokio::test]
1763 async fn mock_resolve_ptr_transforms_ip_literal() {
1764 let records = mock_zone_lookup(RecordType::PTR, "192.0.2.1").await;
1765 assert_eq!(records.len(), 1);
1766 assert_eq!(records[0].name, "1.2.0.192.in-addr.arpa");
1768 assert!(matches!(
1769 &records[0].data,
1770 RecordData::PTR { target } if target == "ptr.seer.test."
1771 ));
1772 }
1773
1774 #[tokio::test]
1775 async fn mock_resolve_any_aggregates_multiple_types() {
1776 let records = mock_zone_lookup(RecordType::ANY, "seer.test").await;
1777 for expected in [
1779 RecordType::A,
1780 RecordType::AAAA,
1781 RecordType::MX,
1782 RecordType::NS,
1783 RecordType::TXT,
1784 RecordType::SOA,
1785 RecordType::CAA,
1786 ] {
1787 assert!(
1788 records.iter().any(|r| r.record_type == expected),
1789 "ANY must include {expected} records"
1790 );
1791 }
1792 assert_eq!(records.len(), 11);
1794 }
1795
1796 #[tokio::test]
1797 async fn mock_nodata_folds_to_empty_and_classifies_absent() {
1798 let port = spawn_mock_dns(MockMode::NoData).await;
1799 let result = mock_dns_resolver(port)
1800 .resolve("seer.test", RecordType::NS, Some("127.0.0.1"))
1801 .await;
1802 assert!(
1803 matches!(&result, Ok(records) if records.is_empty()),
1804 "NODATA must fold to Ok(vec![]), got: {result:?}"
1805 );
1806 assert_eq!(classify_ns_presence(&result), DnsPresence::Absent);
1807 }
1808
1809 #[tokio::test]
1810 async fn mock_nxdomain_folds_to_empty_and_classifies_absent() {
1811 let port = spawn_mock_dns(MockMode::Nxdomain).await;
1812 let result = mock_dns_resolver(port)
1813 .resolve("seer.test", RecordType::NS, Some("127.0.0.1"))
1814 .await;
1815 assert!(
1816 matches!(&result, Ok(records) if records.is_empty()),
1817 "NXDOMAIN must fold to Ok(vec![]), got: {result:?}"
1818 );
1819 assert_eq!(classify_ns_presence(&result), DnsPresence::Absent);
1820 }
1821
1822 #[tokio::test]
1823 async fn mock_timeout_errors_and_classifies_unknown() {
1824 let port = spawn_mock_dns(MockMode::Ignore).await;
1825 let resolver = DnsResolver::new()
1827 .with_timeout(Duration::from_millis(200))
1828 .allowing_private_hosts()
1829 .with_port(port);
1830 let result = resolver
1831 .resolve("seer.test", RecordType::NS, Some("127.0.0.1"))
1832 .await;
1833 match &result {
1834 Err(SeerError::DnsError(_)) => {}
1835 other => panic!("unanswered query must surface a DnsError, got: {other:?}"),
1836 }
1837 assert_eq!(classify_ns_presence(&result), DnsPresence::Unknown);
1838 }
1839
1840 #[tokio::test]
1841 async fn resolve_type_rejects_composite_types_consistently() {
1842 let r = DnsResolver::new();
1847 for composite in [RecordType::SRV, RecordType::ANY] {
1848 let err = r
1849 .resolve_type(&r.default_resolver, "seer.test", composite)
1850 .await
1851 .expect_err("composite types must be rejected by resolve_type");
1852 assert_eq!(
1853 err.to_string(),
1854 unsupported_record_type(composite).to_string()
1855 );
1856 }
1857 }
1858}