1use std::net::IpAddr;
12use std::str::FromStr;
13use std::sync::Arc;
14use std::time::Duration;
15
16use hickory_resolver::config::{
17 NameServerConfig, ResolveHosts, ResolverConfig, ResolverOpts, ServerOrderingStrategy, GOOGLE,
18};
19use hickory_resolver::net::runtime::TokioRuntimeProvider;
20use hickory_resolver::net::NetError;
21use hickory_resolver::proto::dnssec::PublicKey;
22use hickory_resolver::proto::rr::rdata::CAA;
23use hickory_resolver::proto::rr::{RData as HickoryRData, RecordType as HickoryRecordType};
24use hickory_resolver::TokioResolver;
25use tracing::{debug, instrument};
26
27use super::nameserver::{NameserverProtocol, NameserverSpec};
28use super::records::{DnsRecord, RecordData, RecordType};
29use crate::error::{Result, SeerError};
30use crate::validation::normalize_domain;
31
32fn dns_lookup_or_empty<T>(
36 result: std::result::Result<T, NetError>,
37 record_type: &str,
38) -> Result<Option<T>> {
39 match result {
40 Ok(response) => Ok(Some(response)),
41 Err(e) if e.is_no_records_found() => Ok(None),
42 Err(e) => Err(SeerError::DnsError(format!(
43 "{} lookup failed: {}",
44 record_type, e
45 ))),
46 }
47}
48
49const DEFAULT_TIMEOUT: Duration = Duration::from_secs(5);
52
53pub(crate) fn apply_standard_opts(opts: &mut ResolverOpts, timeout: Duration) {
61 opts.timeout = timeout;
62 opts.attempts = 2;
63 opts.use_hosts_file = ResolveHosts::Never;
64 opts.server_ordering_strategy = ServerOrderingStrategy::UserProvidedOrder;
85}
86
87fn build_resolver(config: ResolverConfig, timeout: Duration) -> Result<TokioResolver> {
96 let mut builder = TokioResolver::builder_with_config(config, TokioRuntimeProvider::default());
97 apply_standard_opts(builder.options_mut(), timeout);
98 builder
99 .build()
100 .map_err(|e| SeerError::DnsError(format!("failed to construct DNS resolver: {}", e)))
101}
102
103fn build_default_resolver(timeout: Duration) -> TokioResolver {
110 build_resolver(ResolverConfig::udp_and_tcp(&GOOGLE), timeout)
111 .expect("default resolver build cannot fail with the bundled webpki root store")
112}
113
114fn build_upstream_config(
124 spec: &NameserverSpec,
125 ips: &[IpAddr],
126 port_override: Option<u16>,
127) -> ResolverConfig {
128 let mut config = ResolverConfig::from_parts(None, vec![], vec![]);
129 let port = port_override.unwrap_or(spec.port);
130 for ip in ips {
131 let mut ns = match spec.protocol {
132 NameserverProtocol::Udp => NameServerConfig::udp(*ip),
133 NameserverProtocol::Tls => NameServerConfig::tls(*ip, Arc::from(spec.tls_name())),
134 NameserverProtocol::Https => NameServerConfig::https(
135 *ip,
136 Arc::from(spec.tls_name()),
137 spec.path.as_deref().map(Arc::from),
138 ),
139 };
140 for connection in &mut ns.connections {
141 connection.port = port;
142 }
143 config.add_name_server(ns);
144 }
145 config
146}
147
148#[derive(Clone)]
156pub struct DnsResolver {
157 timeout: Duration,
158 default_resolver: TokioResolver,
161 port_override: Option<u16>,
166 allow_private_hosts: bool,
171}
172
173impl std::fmt::Debug for DnsResolver {
174 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
175 f.debug_struct("DnsResolver")
176 .field("timeout", &self.timeout)
177 .finish()
178 }
179}
180
181impl Default for DnsResolver {
182 fn default() -> Self {
183 Self::new()
184 }
185}
186
187impl DnsResolver {
188 pub fn new() -> Self {
190 Self {
191 timeout: DEFAULT_TIMEOUT,
192 default_resolver: build_default_resolver(DEFAULT_TIMEOUT),
193 port_override: None,
194 allow_private_hosts: false,
195 }
196 }
197
198 pub fn from_config(config: &crate::config::SeerConfig) -> Self {
210 Self::new().with_timeout(config.dns_timeout())
211 }
212
213 #[cfg(test)]
215 pub(crate) fn allowing_private_hosts(mut self) -> Self {
216 self.allow_private_hosts = true;
217 self
218 }
219
220 #[cfg(test)]
223 pub(crate) fn with_port(mut self, port: u16) -> Self {
224 self.port_override = Some(port);
225 self
226 }
227
228 pub fn with_timeout(mut self, timeout: Duration) -> Self {
232 self.timeout = timeout;
233 self.default_resolver = build_default_resolver(timeout);
234 self
235 }
236
237 async fn create_custom_resolver(&self, nameserver: &str) -> Result<TokioResolver> {
238 let spec = NameserverSpec::parse(nameserver)?;
243
244 let ips: Vec<IpAddr> = if let Ok(ip) = spec.host.parse::<IpAddr>() {
253 vec![ip]
254 } else {
255 let response = self
256 .default_resolver
257 .lookup_ip(spec.host.as_str())
258 .await
259 .map_err(|e| {
260 SeerError::DnsError(format!(
261 "failed to resolve nameserver hostname {}: {}",
262 spec.host, e
263 ))
264 })?;
265 let resolved: Vec<IpAddr> = response.iter().collect();
266 if resolved.is_empty() {
267 return Err(SeerError::DnsError(format!(
268 "nameserver {} did not resolve to any addresses",
269 spec.host
270 )));
271 }
272 resolved
273 };
274
275 if !self.allow_private_hosts {
282 for ip in &ips {
283 if let Some(reason) = crate::validation::describe_reserved_ip(ip) {
284 return Err(SeerError::DnsError(format!(
285 "nameserver {} blocked: {}",
286 nameserver, reason
287 )));
288 }
289 }
290 }
291
292 build_resolver(
293 build_upstream_config(&spec, &ips, self.port_override),
294 self.timeout,
295 )
296 }
297
298 #[instrument(skip(self), fields(domain = %domain, record_type = %record_type))]
308 pub async fn resolve(
309 &self,
310 domain: &str,
311 record_type: RecordType,
312 nameserver: Option<&str>,
313 ) -> Result<Vec<DnsRecord>> {
314 let custom_resolver;
316 let resolver = if let Some(ns) = nameserver {
317 custom_resolver = self.create_custom_resolver(ns).await?;
318 &custom_resolver
319 } else {
320 &self.default_resolver
321 };
322 let domain = prepare_query(domain, record_type)?;
323
324 debug!(nameserver = nameserver.unwrap_or("system"), "Resolving DNS");
325
326 match record_type {
327 RecordType::SRV => match parse_srv_query(&domain) {
328 Some((service, protocol, name)) => {
330 self.resolve_srv_core(resolver, &service, &protocol, &name)
331 .await
332 }
333 None => Err(srv_format_error()),
336 },
337 RecordType::ANY => self.resolve_any(resolver, &domain).await,
338 single => self.resolve_type(resolver, &domain, single).await,
339 }
340 }
341
342 #[instrument(skip(self), fields(domain = %domain, service = %service, protocol = %protocol))]
350 pub async fn resolve_srv(
351 &self,
352 service: &str,
353 protocol: &str,
354 domain: &str,
355 nameserver: Option<&str>,
356 ) -> Result<Vec<DnsRecord>> {
357 let domain = normalize_domain(domain)?;
360 let custom_resolver;
361 let resolver = if let Some(ns) = nameserver {
362 custom_resolver = self.create_custom_resolver(ns).await?;
363 &custom_resolver
364 } else {
365 &self.default_resolver
366 };
367 self.resolve_srv_core(resolver, service, protocol, &domain)
368 .await
369 }
370
371 async fn resolve_srv_core(
378 &self,
379 resolver: &TokioResolver,
380 service: &str,
381 protocol: &str,
382 domain: &str,
383 ) -> Result<Vec<DnsRecord>> {
384 if !is_valid_srv_label(service) {
385 return Err(SeerError::InvalidInput(format!(
386 "invalid SRV service name: {}",
387 service
388 )));
389 }
390 if !is_valid_srv_label(protocol) {
391 return Err(SeerError::InvalidInput(format!(
392 "invalid SRV protocol name: {}",
393 protocol
394 )));
395 }
396
397 let query_name = format!("_{}._{}.{}", service, protocol, domain);
398
399 let Some(response) = dns_lookup_or_empty(
400 resolver.lookup(&query_name, HickoryRecordType::SRV).await,
401 "SRV",
402 )?
403 else {
404 return Ok(vec![]);
405 };
406
407 let records = response
408 .answers()
409 .iter()
410 .filter_map(|record| {
411 if let HickoryRData::SRV(srv) = &record.data {
412 Some(DnsRecord {
413 name: query_name.clone(),
414 record_type: RecordType::SRV,
415 ttl: record.ttl,
416 data: RecordData::SRV {
417 priority: srv.priority,
418 weight: srv.weight,
419 port: srv.port,
420 target: srv.target.to_string(),
421 },
422 })
423 } else {
424 None
425 }
426 })
427 .collect();
428
429 Ok(records)
430 }
431
432 async fn resolve_type(
440 &self,
441 resolver: &TokioResolver,
442 domain: &str,
443 record_type: RecordType,
444 ) -> Result<Vec<DnsRecord>> {
445 match record_type {
446 RecordType::PTR => {
449 let query = if let Ok(ip) = IpAddr::from_str(domain) {
450 reverse_dns_name(&ip)
451 } else {
452 domain.to_string()
453 };
454 self.resolve_records(resolver, &query, RecordType::PTR)
455 .await
456 }
457 single => self.resolve_records(resolver, domain, single).await,
458 }
459 }
460
461 async fn resolve_records(
469 &self,
470 resolver: &TokioResolver,
471 domain: &str,
472 record_type: RecordType,
473 ) -> Result<Vec<DnsRecord>> {
474 let Some(wire_type) = wire_type(record_type) else {
475 return Err(unsupported_record_type(record_type));
476 };
477
478 let Some(response) = dns_lookup_or_empty(
479 resolver.lookup(domain, wire_type).await,
480 &record_type.to_string(),
481 )?
482 else {
483 return Ok(vec![]);
484 };
485
486 let mut records: Vec<DnsRecord> = response
487 .answers()
488 .iter()
489 .filter_map(|record| {
490 convert_rdata(record_type, &record.data).map(|data| DnsRecord {
491 name: domain.to_string(),
492 record_type,
493 ttl: record.ttl,
494 data,
495 })
496 })
497 .collect();
498
499 if record_type == RecordType::MX {
500 records.sort_by_key(|r| match &r.data {
501 RecordData::MX { preference, .. } => *preference,
502 _ => 0,
503 });
504 }
505
506 Ok(records)
507 }
508
509 async fn resolve_any(&self, resolver: &TokioResolver, domain: &str) -> Result<Vec<DnsRecord>> {
510 let record_types = [
515 RecordType::A,
516 RecordType::AAAA,
517 RecordType::MX,
518 RecordType::NS,
519 RecordType::TXT,
520 RecordType::SOA,
521 RecordType::CAA,
522 ];
523
524 let results = futures::future::join_all(
525 record_types
526 .into_iter()
527 .map(|record_type| self.resolve_type(resolver, domain, record_type)),
528 )
529 .await;
530
531 let mut all_records = Vec::new();
536 let mut any_ok = false;
537 let mut last_err = None;
538 for result in results {
539 match result {
540 Ok(records) => {
541 any_ok = true;
542 all_records.extend(records);
543 }
544 Err(e) => last_err = Some(e),
545 }
546 }
547
548 match last_err {
549 Some(e) if !any_ok => Err(e),
550 _ => Ok(all_records),
551 }
552 }
553}
554
555#[derive(Debug, Clone, Copy, PartialEq, Eq)]
560pub enum DnsPresence {
561 Present,
563 Absent,
565 Unknown,
567}
568
569fn classify_ns_presence(result: &Result<Vec<DnsRecord>>) -> DnsPresence {
574 match result {
575 Ok(records) if records.is_empty() => DnsPresence::Absent,
576 Ok(_) => DnsPresence::Present,
577 Err(_) => DnsPresence::Unknown,
578 }
579}
580
581impl DnsResolver {
582 pub async fn presence(&self, domain: &str) -> DnsPresence {
590 classify_ns_presence(&self.resolve(domain, RecordType::NS, None).await)
591 }
592}
593
594fn prepare_query(domain: &str, record_type: RecordType) -> Result<String> {
607 if record_type == RecordType::PTR {
608 if let Ok(ip) = IpAddr::from_str(domain.trim()) {
609 return Ok(ip.to_string());
610 }
611 }
612 normalize_domain(domain)
613}
614
615pub(crate) fn parse_srv_query(name: &str) -> Option<(String, String, String)> {
620 let mut parts = name.splitn(3, '.');
621 let service = parts.next()?.strip_prefix('_')?;
622 let protocol = parts.next()?.strip_prefix('_')?;
623 let rest = parts.next()?;
624 if service.is_empty() || protocol.is_empty() || rest.is_empty() {
625 return None;
626 }
627 Some((service.to_string(), protocol.to_string(), rest.to_string()))
628}
629
630pub(crate) fn srv_format_error() -> SeerError {
634 SeerError::InvalidInput(
635 "SRV records require service name format: _service._proto.name".to_string(),
636 )
637}
638
639fn reverse_dns_name(ip: &IpAddr) -> String {
640 match ip {
641 IpAddr::V4(addr) => {
642 let octets = addr.octets();
643 format!(
644 "{}.{}.{}.{}.in-addr.arpa",
645 octets[3], octets[2], octets[1], octets[0]
646 )
647 }
648 IpAddr::V6(addr) => {
649 let segments = addr.segments();
650 let mut result = String::with_capacity(72);
652 let mut first = true;
653 for segment in segments.iter().rev() {
654 for shift in [0, 4, 8, 12] {
655 if !first {
656 result.push('.');
657 }
658 first = false;
659 let nibble = (segment >> shift) & 0xF;
660 result
661 .push(char::from_digit(nibble as u32, 16).expect("nibble is always 0-15"));
662 }
663 }
664 result.push_str(".ip6.arpa");
665 result
666 }
667 }
668}
669
670fn parse_caa(caa: &CAA) -> (u8, String, String) {
671 let flags = if caa.issuer_critical { 128 } else { 0 };
678 let tag = caa.tag.clone();
679 let value = String::from_utf8_lossy(&caa.value).to_string();
680 (flags, tag, value)
681}
682
683fn wire_type(record_type: RecordType) -> Option<HickoryRecordType> {
689 Some(match record_type {
690 RecordType::A => HickoryRecordType::A,
691 RecordType::AAAA => HickoryRecordType::AAAA,
692 RecordType::CNAME => HickoryRecordType::CNAME,
693 RecordType::MX => HickoryRecordType::MX,
694 RecordType::NS => HickoryRecordType::NS,
695 RecordType::TXT => HickoryRecordType::TXT,
696 RecordType::SOA => HickoryRecordType::SOA,
697 RecordType::PTR => HickoryRecordType::PTR,
698 RecordType::CAA => HickoryRecordType::CAA,
699 RecordType::DNSKEY => HickoryRecordType::DNSKEY,
700 RecordType::DS => HickoryRecordType::DS,
701 RecordType::TLSA => HickoryRecordType::TLSA,
707 RecordType::SSHFP => HickoryRecordType::SSHFP,
708 RecordType::NAPTR => HickoryRecordType::NAPTR,
709 RecordType::SRV | RecordType::ANY => return None,
710 })
711}
712
713fn unsupported_record_type(record_type: RecordType) -> SeerError {
716 SeerError::DnsError(format!("unsupported record type: {}", record_type))
717}
718
719fn hex_upper(bytes: &[u8]) -> String {
722 bytes.iter().map(|b| format!("{:02X}", b)).collect()
723}
724
725fn convert_rdata(record_type: RecordType, data: &HickoryRData) -> Option<RecordData> {
732 use hickory_resolver::proto::dnssec::rdata::DNSSECRData;
733
734 match (record_type, data) {
735 (RecordType::A, HickoryRData::A(addr)) => Some(RecordData::A {
736 address: addr.0.to_string(),
737 }),
738 (RecordType::AAAA, HickoryRData::AAAA(addr)) => Some(RecordData::AAAA {
739 address: addr.0.to_string(),
740 }),
741 (RecordType::CNAME, HickoryRData::CNAME(cname)) => Some(RecordData::CNAME {
742 target: cname.0.to_string(),
743 }),
744 (RecordType::MX, HickoryRData::MX(mx)) => Some(RecordData::MX {
745 preference: mx.preference,
746 exchange: mx.exchange.to_string(),
747 }),
748 (RecordType::NS, HickoryRData::NS(ns)) => Some(RecordData::NS {
749 nameserver: ns.0.to_string(),
750 }),
751 (RecordType::TXT, HickoryRData::TXT(txt)) => Some(RecordData::TXT {
752 text: txt
753 .txt_data
754 .iter()
755 .map(|data| String::from_utf8_lossy(data).to_string())
756 .collect::<Vec<_>>()
757 .join(""),
758 }),
759 (RecordType::SOA, HickoryRData::SOA(soa)) => Some(RecordData::SOA {
760 mname: soa.mname.to_string(),
761 rname: soa.rname.to_string(),
762 serial: soa.serial,
763 refresh: soa.refresh as u32,
769 retry: soa.retry as u32,
770 expire: soa.expire as u32,
771 minimum: soa.minimum,
772 }),
773 (RecordType::PTR, HickoryRData::PTR(ptr)) => Some(RecordData::PTR {
774 target: ptr.0.to_string(),
775 }),
776 (RecordType::CAA, HickoryRData::CAA(caa)) => {
777 let (flags, tag, value) = parse_caa(caa);
778 Some(RecordData::CAA { flags, tag, value })
779 }
780 (RecordType::DNSKEY, HickoryRData::DNSSEC(DNSSECRData::DNSKEY(dnskey))) => {
781 use base64::{engine::general_purpose::STANDARD, Engine};
782 let public_key_buf = dnskey.public_key();
783 Some(RecordData::DNSKEY {
784 flags: dnskey.flags(),
785 protocol: 3,
787 algorithm: u8::from(public_key_buf.algorithm()),
788 public_key: STANDARD.encode(public_key_buf.public_bytes()),
789 })
790 }
791 (RecordType::DS, HickoryRData::DNSSEC(DNSSECRData::DS(ds))) => Some(RecordData::DS {
792 key_tag: ds.key_tag(),
793 algorithm: u8::from(ds.algorithm()),
794 digest_type: u8::from(ds.digest_type()),
795 digest: hex_upper(ds.digest()),
796 }),
797 (RecordType::TLSA, HickoryRData::TLSA(tlsa)) => Some(RecordData::TLSA {
798 cert_usage: u8::from(tlsa.cert_usage),
799 selector: u8::from(tlsa.selector),
800 matching: u8::from(tlsa.matching),
801 cert_data: hex_upper(&tlsa.cert_data),
802 }),
803 (RecordType::SSHFP, HickoryRData::SSHFP(sshfp)) => Some(RecordData::SSHFP {
804 algorithm: u8::from(sshfp.algorithm),
805 fingerprint_type: u8::from(sshfp.fingerprint_type),
806 fingerprint: hex_upper(&sshfp.fingerprint),
807 }),
808 (RecordType::NAPTR, HickoryRData::NAPTR(naptr)) => Some(RecordData::NAPTR {
812 order: naptr.order,
813 preference: naptr.preference,
814 flags: String::from_utf8_lossy(&naptr.flags).into_owned(),
815 services: String::from_utf8_lossy(&naptr.services).into_owned(),
816 regexp: String::from_utf8_lossy(&naptr.regexp).into_owned(),
817 replacement: naptr.replacement.to_string(),
818 }),
819 _ => None,
820 }
821}
822
823fn is_valid_srv_label(label: &str) -> bool {
825 !label.is_empty()
826 && label.len() <= 63
827 && label.chars().all(|c| c.is_ascii_alphanumeric() || c == '-')
828 && !label.starts_with('-')
829 && !label.ends_with('-')
830}
831
832#[cfg(test)]
833mod tests {
834 use super::*;
842
843 #[test]
858 fn standard_opts_pin_server_order_so_ipv4_is_tried_first() {
859 let mut opts = ResolverOpts::default();
860 apply_standard_opts(&mut opts, Duration::from_secs(5));
861 assert_eq!(
862 opts.server_ordering_strategy,
863 ServerOrderingStrategy::UserProvidedOrder,
864 "QueryStatistics ordering is nondeterministic in a short-lived \
865 process and can draw only black-holed IPv6 servers"
866 );
867 }
868
869 #[test]
874 fn default_config_lists_enough_ipv4_servers_before_any_ipv6() {
875 let config = ResolverConfig::udp_and_tcp(&GOOGLE);
876 let ips: Vec<_> = config.name_servers().iter().map(|ns| ns.ip).collect();
877 let concurrent = ResolverOpts::default().num_concurrent_reqs.max(1);
878 assert!(
879 ips.len() > concurrent,
880 "expected more servers than the concurrency window: {ips:?}"
881 );
882 for (i, ip) in ips.iter().take(concurrent).enumerate() {
883 assert!(
884 ip.is_ipv4(),
885 "server {i} in the concurrency window is not IPv4: {ips:?}"
886 );
887 }
888 }
889
890 #[test]
891 fn from_config_applies_dns_timeout() {
892 let mut config = crate::config::SeerConfig::default();
893 config.timeouts.dns_secs = 9;
894 let resolver = DnsResolver::from_config(&config);
895 assert_eq!(resolver.timeout, Duration::from_secs(9));
896 }
897 use std::net::{Ipv4Addr, Ipv6Addr};
898
899 #[test]
902 fn record_type_from_str_accepts_lowercase() {
903 assert_eq!(RecordType::from_str("a").unwrap(), RecordType::A);
904 assert_eq!(RecordType::from_str("mx").unwrap(), RecordType::MX);
905 assert_eq!(RecordType::from_str("cname").unwrap(), RecordType::CNAME);
906 assert_eq!(RecordType::from_str("dnskey").unwrap(), RecordType::DNSKEY);
907 }
908
909 #[test]
910 fn record_type_from_str_accepts_mixed_case() {
911 assert_eq!(RecordType::from_str("Mx").unwrap(), RecordType::MX);
912 assert_eq!(RecordType::from_str("cNaMe").unwrap(), RecordType::CNAME);
913 }
914
915 #[test]
916 fn record_type_from_str_rejects_whitespace_padded() {
917 assert!(RecordType::from_str(" A").is_err());
921 assert!(RecordType::from_str("A ").is_err());
922 assert!(RecordType::from_str("\tA\n").is_err());
923 }
924
925 #[test]
926 fn record_type_from_str_rejects_unknown() {
927 assert!(RecordType::from_str("NOTAREAL").is_err());
928 assert!(RecordType::from_str("A1").is_err());
929 assert!(RecordType::from_str("").is_err());
930 }
931
932 #[test]
933 fn record_type_from_str_accepts_star_as_any() {
934 assert_eq!(RecordType::from_str("*").unwrap(), RecordType::ANY);
935 assert_eq!(RecordType::from_str("ANY").unwrap(), RecordType::ANY);
936 assert_eq!(RecordType::from_str("any").unwrap(), RecordType::ANY);
937 }
938
939 #[test]
942 fn srv_label_accepts_alphanumeric_and_hyphen() {
943 assert!(is_valid_srv_label("http"));
944 assert!(is_valid_srv_label("ldap-tls"));
945 assert!(is_valid_srv_label("a1"));
946 assert!(is_valid_srv_label("tcp"));
947 }
948
949 #[test]
950 fn srv_label_rejects_empty() {
951 assert!(!is_valid_srv_label(""));
952 }
953
954 #[test]
955 fn srv_label_rejects_leading_or_trailing_hyphen() {
956 assert!(!is_valid_srv_label("-http"));
957 assert!(!is_valid_srv_label("http-"));
958 assert!(!is_valid_srv_label("-"));
959 }
960
961 #[test]
962 fn srv_label_rejects_dots() {
963 assert!(!is_valid_srv_label("http.evil"));
966 assert!(!is_valid_srv_label("a.b"));
967 }
968
969 #[test]
970 fn srv_label_rejects_special_chars() {
971 assert!(!is_valid_srv_label("http evil"));
972 assert!(!is_valid_srv_label("http/evil"));
973 assert!(!is_valid_srv_label("http\0"));
974 assert!(!is_valid_srv_label("http\n"));
975 }
976
977 #[test]
978 fn srv_label_rejects_over_63_chars() {
979 let too_long = "a".repeat(64);
980 assert!(!is_valid_srv_label(&too_long));
981 let exactly_63 = "a".repeat(63);
982 assert!(is_valid_srv_label(&exactly_63));
983 }
984
985 #[test]
988 fn classify_ns_presence_absent_on_empty_ok() {
989 let r: Result<Vec<DnsRecord>> = Ok(vec![]);
991 assert_eq!(classify_ns_presence(&r), DnsPresence::Absent);
992 }
993
994 #[test]
995 fn classify_ns_presence_present_on_records() {
996 let rec = DnsRecord {
997 name: "example.test.".to_string(),
998 record_type: RecordType::NS,
999 ttl: 3600,
1000 data: RecordData::NS {
1001 nameserver: "ns1.example.net.".to_string(),
1002 },
1003 };
1004 let r: Result<Vec<DnsRecord>> = Ok(vec![rec]);
1005 assert_eq!(classify_ns_presence(&r), DnsPresence::Present);
1006 }
1007
1008 #[test]
1009 fn classify_ns_presence_unknown_on_error() {
1010 let r: Result<Vec<DnsRecord>> = Err(SeerError::DnsError("servfail".to_string()));
1011 assert_eq!(classify_ns_presence(&r), DnsPresence::Unknown);
1012 }
1013
1014 #[test]
1017 fn reverse_dns_name_formats_ipv4_correctly() {
1018 let ip: IpAddr = Ipv4Addr::new(192, 0, 2, 1).into();
1019 assert_eq!(reverse_dns_name(&ip), "1.2.0.192.in-addr.arpa");
1020 }
1021
1022 #[test]
1023 fn reverse_dns_name_formats_ipv6_correctly() {
1024 let ip: IpAddr = Ipv6Addr::LOCALHOST.into();
1026 let name = reverse_dns_name(&ip);
1027 assert!(
1028 name.ends_with(".ip6.arpa"),
1029 "must end with .ip6.arpa; got: {}",
1030 name
1031 );
1032 assert!(
1034 name.starts_with("1."),
1035 "expected '1.' prefix, got: {}",
1036 name
1037 );
1038 assert_eq!(name.len(), 72);
1040 }
1041
1042 #[test]
1045 fn resolver_new_has_default_timeout() {
1046 let r = DnsResolver::new();
1047 assert_eq!(r.timeout, DEFAULT_TIMEOUT);
1048 }
1049
1050 #[test]
1051 fn resolver_with_timeout_overrides_default() {
1052 let custom = Duration::from_secs(42);
1053 let r = DnsResolver::new().with_timeout(custom);
1054 assert_eq!(r.timeout, custom);
1055 }
1056
1057 #[test]
1058 fn resolver_default_matches_new() {
1059 let a = DnsResolver::default();
1060 let b = DnsResolver::new();
1061 assert_eq!(a.timeout, b.timeout);
1062 }
1063
1064 #[tokio::test]
1067 async fn custom_resolver_rejects_invalid_input() {
1068 let r = DnsResolver::new();
1073 let err = r.create_custom_resolver("..").await.unwrap_err();
1074 let msg = err.to_string().to_lowercase();
1075 assert!(
1076 msg.contains("dns resolution failed") || msg.contains("invalid"),
1077 "expected resolution failure, got: {}",
1078 msg
1079 );
1080 }
1081
1082 #[tokio::test]
1083 async fn custom_resolver_rejects_private_ipv4() {
1084 let r = DnsResolver::new();
1087 for reserved in ["127.0.0.1", "10.0.0.1", "192.168.1.1", "169.254.169.254"] {
1088 let err = r.create_custom_resolver(reserved).await.unwrap_err();
1089 let msg = err.to_string().to_lowercase();
1090 assert!(
1091 msg.contains("blocked") || msg.contains("reserved"),
1092 "reserved IP {} must be rejected, got error: {}",
1093 reserved,
1094 msg
1095 );
1096 }
1097 }
1098
1099 #[tokio::test]
1100 async fn custom_resolver_rejects_loopback_ipv6() {
1101 let r = DnsResolver::new();
1102 let err = r.create_custom_resolver("::1").await.unwrap_err();
1103 let msg = err.to_string().to_lowercase();
1104 assert!(
1105 msg.contains("blocked") || msg.contains("reserved"),
1106 "::1 must be rejected, got error: {}",
1107 msg
1108 );
1109 }
1110
1111 #[tokio::test]
1112 async fn custom_resolver_accepts_public_ipv4() {
1113 let r = DnsResolver::new();
1115 let result = r.create_custom_resolver("8.8.8.8").await;
1116 assert!(
1117 result.is_ok(),
1118 "8.8.8.8 must be accepted as a public nameserver, got: {:?}",
1119 result.err()
1120 );
1121 }
1122
1123 #[tokio::test]
1129 async fn custom_resolver_rejects_private_ip_for_dot_and_doh() {
1130 let r = DnsResolver::new();
1131 for reserved in [
1132 "tls://127.0.0.1",
1133 "tls://192.168.1.1:853",
1134 "tls://[::1]",
1135 "https://10.0.0.1/dns-query",
1136 "https://169.254.169.254",
1137 "https://[fd00::1]:443/dns-query",
1138 ] {
1139 let err = r.create_custom_resolver(reserved).await.unwrap_err();
1140 let msg = err.to_string().to_lowercase();
1141 assert!(
1142 msg.contains("blocked") || msg.contains("reserved"),
1143 "reserved spec {} must be rejected, got error: {}",
1144 reserved,
1145 msg
1146 );
1147 }
1148 }
1149
1150 #[tokio::test]
1151 async fn custom_resolver_accepts_public_dot_and_doh_literals() {
1152 let r = DnsResolver::new();
1155 for spec in ["tls://1.1.1.1", "https://8.8.8.8/dns-query"] {
1156 let result = r.create_custom_resolver(spec).await;
1157 assert!(
1158 result.is_ok(),
1159 "{} must be accepted, got: {:?}",
1160 spec,
1161 result.err()
1162 );
1163 }
1164 }
1165
1166 #[tokio::test]
1167 async fn custom_resolver_rejects_unknown_scheme() {
1168 let r = DnsResolver::new();
1169 let err = r.create_custom_resolver("ftp://8.8.8.8").await.unwrap_err();
1170 assert!(
1171 matches!(err, SeerError::InvalidInput(_)),
1172 "unknown scheme must be an input error, got: {err:?}"
1173 );
1174 }
1175
1176 fn spec(s: &str) -> NameserverSpec {
1179 NameserverSpec::parse(s).unwrap_or_else(|e| panic!("{s:?} must parse: {e}"))
1180 }
1181
1182 #[test]
1183 fn upstream_config_udp_defaults() {
1184 use hickory_resolver::config::ProtocolConfig;
1185
1186 let ip: IpAddr = "8.8.8.8".parse().unwrap();
1187 let config = build_upstream_config(&spec("8.8.8.8"), &[ip], None);
1188 let servers = config.name_servers();
1189 assert_eq!(servers.len(), 1);
1190 assert_eq!(servers[0].ip, ip);
1191 assert_eq!(servers[0].connections.len(), 1);
1192 assert_eq!(servers[0].connections[0].port, 53);
1193 assert!(matches!(
1194 servers[0].connections[0].protocol,
1195 ProtocolConfig::Udp
1196 ));
1197 }
1198
1199 #[test]
1200 fn upstream_config_udp_explicit_port() {
1201 let ip: IpAddr = "9.9.9.9".parse().unwrap();
1202 let config = build_upstream_config(&spec("9.9.9.9:5353"), &[ip], None);
1203 assert_eq!(config.name_servers()[0].connections[0].port, 5353);
1204 }
1205
1206 #[test]
1207 fn upstream_config_tls_sets_protocol_port_and_server_name() {
1208 use hickory_resolver::config::ProtocolConfig;
1209
1210 let ip: IpAddr = "9.9.9.9".parse().unwrap();
1211 let config = build_upstream_config(&spec("tls://dns.quad9.net"), &[ip], None);
1212 let ns = &config.name_servers()[0];
1213 assert_eq!(ns.ip, ip);
1214 assert_eq!(ns.connections.len(), 1);
1215 assert_eq!(ns.connections[0].port, 853);
1216 match &ns.connections[0].protocol {
1217 ProtocolConfig::Tls { server_name } => {
1218 assert_eq!(&**server_name, "dns.quad9.net");
1219 }
1220 other => panic!("expected Tls protocol, got {other:?}"),
1221 }
1222 }
1223
1224 #[test]
1225 fn upstream_config_tls_ip_literal_uses_ip_as_server_name() {
1226 use hickory_resolver::config::ProtocolConfig;
1227
1228 let ip: IpAddr = "1.1.1.1".parse().unwrap();
1229 let config = build_upstream_config(&spec("tls://1.1.1.1"), &[ip], None);
1230 match &config.name_servers()[0].connections[0].protocol {
1231 ProtocolConfig::Tls { server_name } => assert_eq!(&**server_name, "1.1.1.1"),
1232 other => panic!("expected Tls protocol, got {other:?}"),
1233 }
1234 }
1235
1236 #[test]
1237 fn upstream_config_https_sets_protocol_port_path_and_server_name() {
1238 use hickory_resolver::config::ProtocolConfig;
1239
1240 let ip: IpAddr = "104.16.248.249".parse().unwrap();
1241 let config = build_upstream_config(&spec("https://cloudflare-dns.com"), &[ip], None);
1242 let ns = &config.name_servers()[0];
1243 assert_eq!(ns.connections[0].port, 443);
1244 match &ns.connections[0].protocol {
1245 ProtocolConfig::Https { server_name, path } => {
1246 assert_eq!(&**server_name, "cloudflare-dns.com");
1247 assert_eq!(&**path, "/dns-query");
1248 }
1249 other => panic!("expected Https protocol, got {other:?}"),
1250 }
1251 }
1252
1253 #[test]
1254 fn upstream_config_https_custom_port_and_path() {
1255 use hickory_resolver::config::ProtocolConfig;
1256
1257 let ip: IpAddr = "8.8.8.8".parse().unwrap();
1258 let config = build_upstream_config(&spec("https://dns.google:8443/resolve"), &[ip], None);
1259 let ns = &config.name_servers()[0];
1260 assert_eq!(ns.connections[0].port, 8443);
1261 match &ns.connections[0].protocol {
1262 ProtocolConfig::Https { server_name, path } => {
1263 assert_eq!(&**server_name, "dns.google");
1264 assert_eq!(&**path, "/resolve");
1265 }
1266 other => panic!("expected Https protocol, got {other:?}"),
1267 }
1268 }
1269
1270 #[test]
1271 fn upstream_config_multiple_ips_share_spec() {
1272 use hickory_resolver::config::ProtocolConfig;
1275
1276 let ips: Vec<IpAddr> = vec![
1277 "9.9.9.9".parse().unwrap(),
1278 "149.112.112.112".parse().unwrap(),
1279 ];
1280 let config = build_upstream_config(&spec("tls://dns.quad9.net"), &ips, None);
1281 let servers = config.name_servers();
1282 assert_eq!(servers.len(), 2);
1283 for (ns, expected_ip) in servers.iter().zip(&ips) {
1284 assert_eq!(&ns.ip, expected_ip);
1285 assert_eq!(ns.connections[0].port, 853);
1286 assert!(matches!(
1287 &ns.connections[0].protocol,
1288 ProtocolConfig::Tls { server_name } if &**server_name == "dns.quad9.net"
1289 ));
1290 }
1291 }
1292
1293 #[test]
1294 fn upstream_config_test_port_override_wins() {
1295 let ip: IpAddr = "127.0.0.1".parse().unwrap();
1297 let config = build_upstream_config(&spec("127.0.0.1"), &[ip], Some(9999));
1298 assert_eq!(config.name_servers()[0].connections[0].port, 9999);
1299 }
1300
1301 #[tokio::test]
1304 #[ignore = "live network — DoT query against Cloudflare"]
1305 async fn live_resolve_over_dot() {
1306 let r = DnsResolver::new();
1307 let records = r
1308 .resolve("example.com", RecordType::A, Some("tls://1.1.1.1"))
1309 .await
1310 .expect("DoT lookup should succeed");
1311 assert!(!records.is_empty(), "expected A records over DoT");
1312 }
1313
1314 #[tokio::test]
1315 #[ignore = "live network — DoH query against Cloudflare"]
1316 async fn live_resolve_over_doh() {
1317 let r = DnsResolver::new();
1318 let records = r
1319 .resolve(
1320 "example.com",
1321 RecordType::A,
1322 Some("https://cloudflare-dns.com/dns-query"),
1323 )
1324 .await
1325 .expect("DoH lookup should succeed");
1326 assert!(!records.is_empty(), "expected A records over DoH");
1327 }
1328
1329 #[tokio::test]
1332 async fn resolve_srv_rejects_invalid_service_label() {
1333 let r = DnsResolver::new();
1334 let result = r.resolve_srv("http.evil", "tcp", "example.com", None).await;
1336 assert!(result.is_err());
1337 let msg = result.unwrap_err().to_string().to_lowercase();
1338 assert!(
1339 msg.contains("invalid srv service"),
1340 "expected SRV service validation error, got: {}",
1341 msg
1342 );
1343 }
1344
1345 #[tokio::test]
1346 async fn resolve_srv_rejects_invalid_protocol_label() {
1347 let r = DnsResolver::new();
1348 let result = r.resolve_srv("http", "tcp.evil", "example.com", None).await;
1349 assert!(result.is_err());
1350 let msg = result.unwrap_err().to_string().to_lowercase();
1351 assert!(
1352 msg.contains("invalid srv protocol"),
1353 "expected SRV protocol validation error, got: {}",
1354 msg
1355 );
1356 }
1357
1358 #[tokio::test]
1359 async fn resolve_srv_normalizes_and_validates_domain_input() {
1360 let r = DnsResolver::new();
1366 let result = r
1367 .resolve_srv("http", "tcp", "not a valid domain", None)
1368 .await;
1369 assert!(
1370 matches!(result, Err(SeerError::InvalidDomain(_))),
1371 "expected InvalidDomain from domain validation, got: {result:?}"
1372 );
1373 }
1374
1375 #[tokio::test]
1378 async fn resolve_normalizes_uppercase_domain_input() {
1379 let r = DnsResolver::new();
1384 let result = r.resolve(".bad.example", RecordType::A, None).await;
1385 assert!(result.is_err(), "leading-dot domain must be rejected");
1386 }
1387
1388 #[test]
1393 fn parse_srv_query_extracts_service_proto_and_name() {
1394 assert_eq!(
1395 parse_srv_query("_sip._tcp.example.com"),
1396 Some((
1397 "sip".to_string(),
1398 "tcp".to_string(),
1399 "example.com".to_string()
1400 ))
1401 );
1402 }
1403
1404 #[test]
1405 fn parse_srv_query_keeps_multilabel_domain() {
1406 assert_eq!(
1407 parse_srv_query("_sip._tcp.sip.voice.google.com"),
1408 Some((
1409 "sip".to_string(),
1410 "tcp".to_string(),
1411 "sip.voice.google.com".to_string()
1412 ))
1413 );
1414 }
1415
1416 #[test]
1417 fn parse_srv_query_rejects_bare_domain() {
1418 assert_eq!(parse_srv_query("example.com"), None);
1419 }
1420
1421 #[test]
1422 fn parse_srv_query_rejects_missing_proto_label() {
1423 assert_eq!(parse_srv_query("_sip.example.com"), None);
1425 }
1426
1427 #[tokio::test]
1428 async fn resolve_rejects_bare_domain_for_srv_as_input_error() {
1429 let r = DnsResolver::new();
1434 let err = r
1435 .resolve("example.com", RecordType::SRV, None)
1436 .await
1437 .expect_err("bare-domain SRV must error");
1438 assert!(
1439 matches!(err, SeerError::InvalidInput(_)),
1440 "bare-domain SRV should be an input error, got: {err:?}"
1441 );
1442 assert!(err.to_string().contains("_service._proto"));
1443 }
1444
1445 #[tokio::test]
1446 #[ignore = "live network"]
1447 async fn resolve_srv_via_dig_style_name_returns_records() {
1448 let r = DnsResolver::new();
1451 let records = r
1452 .resolve("_caldavs._tcp.google.com", RecordType::SRV, None)
1453 .await
1454 .expect("dig-style SRV lookup should succeed");
1455 assert!(!records.is_empty(), "expected SRV records");
1456 assert!(records.iter().all(|r| r.record_type == RecordType::SRV));
1457 }
1458
1459 #[tokio::test]
1460 #[ignore = "live network"]
1461 async fn resolve_naptr_returns_records() {
1462 let r = DnsResolver::new();
1464 let records = r
1465 .resolve("sip2sip.info", RecordType::NAPTR, None)
1466 .await
1467 .expect("NAPTR lookup should succeed");
1468 assert!(!records.is_empty(), "expected NAPTR records");
1469 assert!(records.iter().all(|r| r.record_type == RecordType::NAPTR));
1470 }
1471
1472 #[test]
1475 fn prepare_query_passes_ipv6_literal_through_for_ptr() {
1476 let out = prepare_query("2606:4700:4700::1111", RecordType::PTR).unwrap();
1482 assert_eq!(out, "2606:4700:4700::1111");
1483 }
1484
1485 #[test]
1486 fn prepare_query_passes_ipv6_loopback_through_for_ptr() {
1487 let out = prepare_query("::1", RecordType::PTR).unwrap();
1488 assert_eq!(out, "::1");
1489 }
1490
1491 #[test]
1492 fn prepare_query_passes_ipv4_literal_through_for_ptr() {
1493 let out = prepare_query("8.8.8.8", RecordType::PTR).unwrap();
1494 assert_eq!(out, "8.8.8.8");
1495 }
1496
1497 #[test]
1498 fn prepare_query_normalizes_non_ip_ptr_names() {
1499 let out = prepare_query("1.1.1.1.in-addr.arpa", RecordType::PTR).unwrap();
1501 assert_eq!(out, "1.1.1.1.in-addr.arpa");
1502 }
1503
1504 #[test]
1505 fn prepare_query_normalizes_domains_for_non_ptr() {
1506 let out = prepare_query("HTTPS://WWW.Example.com/path", RecordType::A).unwrap();
1507 assert_eq!(out, "example.com");
1508 }
1509
1510 use crate::dns::test_support::{mock_dns_resolver, spawn_mock_dns, MockMode};
1521
1522 async fn mock_zone_lookup(record_type: RecordType, domain: &str) -> Vec<DnsRecord> {
1523 let port = spawn_mock_dns(MockMode::Zone).await;
1524 mock_dns_resolver(port)
1525 .resolve(domain, record_type, Some("127.0.0.1"))
1526 .await
1527 .unwrap_or_else(|e| panic!("{record_type} lookup against mock must succeed: {e}"))
1528 }
1529
1530 #[tokio::test]
1531 async fn mock_resolve_a_returns_addresses() {
1532 let records = mock_zone_lookup(RecordType::A, "seer.test").await;
1533 assert_eq!(records.len(), 2);
1534 assert!(records.iter().all(|r| r.record_type == RecordType::A));
1535 assert_eq!(records[0].name, "seer.test");
1536 assert_eq!(records[0].ttl, 300);
1537 let addresses: Vec<String> = records
1538 .iter()
1539 .map(|r| match &r.data {
1540 RecordData::A { address } => address.clone(),
1541 other => panic!("expected A data, got {other:?}"),
1542 })
1543 .collect();
1544 assert!(addresses.contains(&"192.0.2.1".to_string()));
1545 assert!(addresses.contains(&"192.0.2.2".to_string()));
1546 }
1547
1548 #[tokio::test]
1549 async fn mock_resolve_mx_sorts_by_preference() {
1550 let records = mock_zone_lookup(RecordType::MX, "seer.test").await;
1551 let prefs: Vec<u16> = records
1552 .iter()
1553 .map(|r| match &r.data {
1554 RecordData::MX { preference, .. } => *preference,
1555 other => panic!("expected MX data, got {other:?}"),
1556 })
1557 .collect();
1558 assert_eq!(prefs, vec![10, 20, 30]);
1560 assert!(matches!(
1561 &records[0].data,
1562 RecordData::MX { exchange, .. } if exchange == "a.mail.seer.test."
1563 ));
1564 }
1565
1566 #[tokio::test]
1567 async fn mock_resolve_txt_joins_character_strings() {
1568 let records = mock_zone_lookup(RecordType::TXT, "seer.test").await;
1569 assert_eq!(records.len(), 1);
1570 match &records[0].data {
1571 RecordData::TXT { text } => assert_eq!(text, "v=spf1 -all"),
1572 other => panic!("expected TXT data, got {other:?}"),
1573 }
1574 }
1575
1576 #[tokio::test]
1577 async fn mock_resolve_soa_maps_all_fields() {
1578 let records = mock_zone_lookup(RecordType::SOA, "seer.test").await;
1579 assert_eq!(records.len(), 1);
1580 match &records[0].data {
1581 RecordData::SOA {
1582 mname,
1583 rname,
1584 serial,
1585 refresh,
1586 retry,
1587 expire,
1588 minimum,
1589 } => {
1590 assert_eq!(mname, "ns1.seer.test.");
1591 assert_eq!(rname, "hostmaster.seer.test.");
1592 assert_eq!(*serial, 2026070101);
1593 assert_eq!(*refresh, 7200);
1594 assert_eq!(*retry, 3600);
1595 assert_eq!(*expire, 1209600);
1596 assert_eq!(*minimum, 300);
1597 }
1598 other => panic!("expected SOA data, got {other:?}"),
1599 }
1600 }
1601
1602 #[tokio::test]
1603 async fn mock_resolve_caa_maps_flags_tag_and_value() {
1604 let records = mock_zone_lookup(RecordType::CAA, "seer.test").await;
1605 assert_eq!(records.len(), 2);
1606 let by_tag = |wanted: &str| {
1607 records
1608 .iter()
1609 .find_map(|r| match &r.data {
1610 RecordData::CAA { flags, tag, value } if tag == wanted => {
1611 Some((*flags, value.clone()))
1612 }
1613 _ => None,
1614 })
1615 .unwrap_or_else(|| panic!("expected a CAA record with tag {wanted}"))
1616 };
1617 assert_eq!(by_tag("issue"), (0, "letsencrypt.org".to_string()));
1619 assert_eq!(
1620 by_tag("iodef"),
1621 (128, "mailto:security@seer.test".to_string())
1622 );
1623 }
1624
1625 #[tokio::test]
1626 async fn mock_resolve_tlsa_hex_encodes_cert_data() {
1627 let records = mock_zone_lookup(RecordType::TLSA, "_443._tcp.seer.test").await;
1628 assert_eq!(records.len(), 1);
1629 match &records[0].data {
1630 RecordData::TLSA {
1631 cert_usage,
1632 selector,
1633 matching,
1634 cert_data,
1635 } => {
1636 assert_eq!((*cert_usage, *selector, *matching), (3, 1, 1));
1637 assert_eq!(cert_data, "ABCD01");
1638 }
1639 other => panic!("expected TLSA data, got {other:?}"),
1640 }
1641 }
1642
1643 #[tokio::test]
1644 async fn mock_resolve_sshfp_hex_encodes_fingerprint() {
1645 let records = mock_zone_lookup(RecordType::SSHFP, "seer.test").await;
1646 assert_eq!(records.len(), 1);
1647 match &records[0].data {
1648 RecordData::SSHFP {
1649 algorithm,
1650 fingerprint_type,
1651 fingerprint,
1652 } => {
1653 assert_eq!((*algorithm, *fingerprint_type), (4, 2));
1654 assert_eq!(fingerprint, "DEADBEEF");
1655 }
1656 other => panic!("expected SSHFP data, got {other:?}"),
1657 }
1658 }
1659
1660 #[tokio::test]
1661 async fn mock_resolve_naptr_decodes_character_strings() {
1662 let records = mock_zone_lookup(RecordType::NAPTR, "seer.test").await;
1663 assert_eq!(records.len(), 1);
1664 match &records[0].data {
1665 RecordData::NAPTR {
1666 order,
1667 preference,
1668 flags,
1669 services,
1670 regexp,
1671 replacement,
1672 } => {
1673 assert_eq!((*order, *preference), (100, 50));
1674 assert_eq!(flags, "U");
1675 assert_eq!(services, "E2U+sip");
1676 assert_eq!(regexp, "!^.*$!sip:info@seer.test!");
1677 assert_eq!(replacement, ".");
1678 }
1679 other => panic!("expected NAPTR data, got {other:?}"),
1680 }
1681 }
1682
1683 #[tokio::test]
1684 async fn mock_resolve_srv_via_dig_style_name() {
1685 let records = mock_zone_lookup(RecordType::SRV, "_sip._tcp.seer.test").await;
1686 assert_eq!(records.len(), 1);
1687 assert_eq!(records[0].name, "_sip._tcp.seer.test");
1688 match &records[0].data {
1689 RecordData::SRV {
1690 priority,
1691 weight,
1692 port,
1693 target,
1694 } => {
1695 assert_eq!((*priority, *weight, *port), (10, 5, 5060));
1696 assert_eq!(target, "sipserver.seer.test.");
1697 }
1698 other => panic!("expected SRV data, got {other:?}"),
1699 }
1700 }
1701
1702 #[tokio::test]
1703 async fn mock_resolve_ptr_transforms_ip_literal() {
1704 let records = mock_zone_lookup(RecordType::PTR, "192.0.2.1").await;
1705 assert_eq!(records.len(), 1);
1706 assert_eq!(records[0].name, "1.2.0.192.in-addr.arpa");
1708 assert!(matches!(
1709 &records[0].data,
1710 RecordData::PTR { target } if target == "ptr.seer.test."
1711 ));
1712 }
1713
1714 #[tokio::test]
1715 async fn mock_resolve_any_aggregates_multiple_types() {
1716 let records = mock_zone_lookup(RecordType::ANY, "seer.test").await;
1717 for expected in [
1719 RecordType::A,
1720 RecordType::AAAA,
1721 RecordType::MX,
1722 RecordType::NS,
1723 RecordType::TXT,
1724 RecordType::SOA,
1725 RecordType::CAA,
1726 ] {
1727 assert!(
1728 records.iter().any(|r| r.record_type == expected),
1729 "ANY must include {expected} records"
1730 );
1731 }
1732 assert_eq!(records.len(), 11);
1734 }
1735
1736 #[tokio::test]
1737 async fn mock_nodata_folds_to_empty_and_classifies_absent() {
1738 let port = spawn_mock_dns(MockMode::NoData).await;
1739 let result = mock_dns_resolver(port)
1740 .resolve("seer.test", RecordType::NS, Some("127.0.0.1"))
1741 .await;
1742 assert!(
1743 matches!(&result, Ok(records) if records.is_empty()),
1744 "NODATA must fold to Ok(vec![]), got: {result:?}"
1745 );
1746 assert_eq!(classify_ns_presence(&result), DnsPresence::Absent);
1747 }
1748
1749 #[tokio::test]
1750 async fn mock_nxdomain_folds_to_empty_and_classifies_absent() {
1751 let port = spawn_mock_dns(MockMode::Nxdomain).await;
1752 let result = mock_dns_resolver(port)
1753 .resolve("seer.test", RecordType::NS, Some("127.0.0.1"))
1754 .await;
1755 assert!(
1756 matches!(&result, Ok(records) if records.is_empty()),
1757 "NXDOMAIN must fold to Ok(vec![]), got: {result:?}"
1758 );
1759 assert_eq!(classify_ns_presence(&result), DnsPresence::Absent);
1760 }
1761
1762 #[tokio::test]
1763 async fn mock_timeout_errors_and_classifies_unknown() {
1764 let port = spawn_mock_dns(MockMode::Ignore).await;
1765 let resolver = DnsResolver::new()
1767 .with_timeout(Duration::from_millis(200))
1768 .allowing_private_hosts()
1769 .with_port(port);
1770 let result = resolver
1771 .resolve("seer.test", RecordType::NS, Some("127.0.0.1"))
1772 .await;
1773 match &result {
1774 Err(SeerError::DnsError(_)) => {}
1775 other => panic!("unanswered query must surface a DnsError, got: {other:?}"),
1776 }
1777 assert_eq!(classify_ns_presence(&result), DnsPresence::Unknown);
1778 }
1779
1780 #[tokio::test]
1781 async fn resolve_type_rejects_composite_types_consistently() {
1782 let r = DnsResolver::new();
1787 for composite in [RecordType::SRV, RecordType::ANY] {
1788 let err = r
1789 .resolve_type(&r.default_resolver, "seer.test", composite)
1790 .await
1791 .expect_err("composite types must be rejected by resolve_type");
1792 assert_eq!(
1793 err.to_string(),
1794 unsupported_record_type(composite).to_string()
1795 );
1796 }
1797 }
1798}