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, normalize_host};
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
114pub(crate) fn single_server_config(ip: IpAddr, port: u16, tcp_fallback: bool) -> ResolverConfig {
117 let mut ns = if tcp_fallback {
118 NameServerConfig::udp_and_tcp(ip)
119 } else {
120 NameServerConfig::udp(ip)
121 };
122 for connection in &mut ns.connections {
123 connection.port = port;
124 }
125 let mut config = ResolverConfig::from_parts(None, vec![], vec![]);
126 config.add_name_server(ns);
127 config
128}
129
130pub(crate) fn google_or_pinned(upstream: Option<(IpAddr, u16)>) -> ResolverConfig {
133 match upstream {
134 None => ResolverConfig::udp_and_tcp(&GOOGLE),
135 Some((ip, port)) => single_server_config(ip, port, false),
136 }
137}
138
139pub(crate) fn fqdn(name: &str) -> String {
142 if name.ends_with('.') {
143 name.to_string()
144 } else {
145 format!("{name}.")
146 }
147}
148
149fn build_upstream_config(
159 spec: &NameserverSpec,
160 ips: &[IpAddr],
161 port_override: Option<u16>,
162) -> ResolverConfig {
163 let mut config = ResolverConfig::from_parts(None, vec![], vec![]);
164 let port = port_override.unwrap_or(spec.port);
165 let ordered = ips
176 .iter()
177 .filter(|ip| ip.is_ipv4())
178 .chain(ips.iter().filter(|ip| ip.is_ipv6()));
179 for ip in ordered {
180 let mut ns = match spec.protocol {
181 NameserverProtocol::Udp => NameServerConfig::udp(*ip),
182 NameserverProtocol::Tls => NameServerConfig::tls(*ip, Arc::from(spec.tls_name())),
183 NameserverProtocol::Https => NameServerConfig::https(
184 *ip,
185 Arc::from(spec.tls_name()),
186 spec.path.as_deref().map(Arc::from),
187 ),
188 };
189 for connection in &mut ns.connections {
190 connection.port = port;
191 }
192 config.add_name_server(ns);
193 }
194 config
195}
196
197#[derive(Clone)]
205pub struct DnsResolver {
206 timeout: Duration,
207 default_resolver: TokioResolver,
210 port_override: Option<u16>,
215 allow_private_hosts: bool,
220 #[cfg(test)]
224 test_default_nameserver: Option<String>,
225}
226
227impl std::fmt::Debug for DnsResolver {
228 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
229 f.debug_struct("DnsResolver")
230 .field("timeout", &self.timeout)
231 .finish()
232 }
233}
234
235impl Default for DnsResolver {
236 fn default() -> Self {
237 Self::new()
238 }
239}
240
241impl DnsResolver {
242 pub fn new() -> Self {
244 Self {
245 timeout: DEFAULT_TIMEOUT,
246 default_resolver: build_default_resolver(DEFAULT_TIMEOUT),
247 port_override: None,
248 allow_private_hosts: false,
249 #[cfg(test)]
250 test_default_nameserver: None,
251 }
252 }
253
254 pub fn from_config(config: &crate::config::SeerConfig) -> Self {
266 Self::new().with_timeout(config.dns_timeout())
267 }
268
269 #[cfg(test)]
271 pub(crate) fn timeout(&self) -> Duration {
272 self.timeout
273 }
274
275 #[cfg(test)]
277 pub(crate) fn allowing_private_hosts(mut self) -> Self {
278 self.allow_private_hosts = true;
279 self
280 }
281
282 #[cfg(test)]
285 pub(crate) fn with_port(mut self, port: u16) -> Self {
286 self.port_override = Some(port);
287 self
288 }
289
290 #[cfg(test)]
293 pub(crate) fn with_default_nameserver(mut self, nameserver: &str) -> Self {
294 self.test_default_nameserver = Some(nameserver.to_string());
295 self
296 }
297
298 #[cfg(test)]
302 fn effective_nameserver<'a>(&'a self, nameserver: Option<&'a str>) -> Option<&'a str> {
303 nameserver.or(self.test_default_nameserver.as_deref())
304 }
305
306 #[cfg(not(test))]
307 fn effective_nameserver<'a>(&self, nameserver: Option<&'a str>) -> Option<&'a str> {
308 nameserver
309 }
310
311 pub fn with_timeout(mut self, timeout: Duration) -> Self {
315 self.timeout = timeout;
316 self.default_resolver = build_default_resolver(timeout);
317 self
318 }
319
320 async fn create_custom_resolver(&self, nameserver: &str) -> Result<TokioResolver> {
321 let spec = NameserverSpec::parse(nameserver)?;
326
327 let ips: Vec<IpAddr> = if let Ok(ip) = spec.host.parse::<IpAddr>() {
336 vec![ip]
337 } else {
338 let response = self
339 .default_resolver
340 .lookup_ip(spec.host.as_str())
341 .await
342 .map_err(|e| {
343 SeerError::DnsError(format!(
344 "failed to resolve nameserver hostname {}: {}",
345 spec.host, e
346 ))
347 })?;
348 let resolved: Vec<IpAddr> = response.iter().collect();
349 if resolved.is_empty() {
350 return Err(SeerError::DnsError(format!(
351 "nameserver {} did not resolve to any addresses",
352 spec.host
353 )));
354 }
355 resolved
356 };
357
358 if !self.allow_private_hosts {
365 for ip in &ips {
366 if let Some(reason) = crate::validation::describe_reserved_ip(ip) {
367 return Err(SeerError::DnsError(format!(
368 "nameserver {} blocked: {}",
369 nameserver, reason
370 )));
371 }
372 }
373 }
374
375 build_resolver(
376 build_upstream_config(&spec, &ips, self.port_override),
377 self.timeout,
378 )
379 }
380
381 #[instrument(skip(self), fields(domain = %domain, record_type = %record_type))]
391 pub async fn resolve(
392 &self,
393 domain: &str,
394 record_type: RecordType,
395 nameserver: Option<&str>,
396 ) -> Result<Vec<DnsRecord>> {
397 let custom_resolver;
399 let resolver = if let Some(ns) = self.effective_nameserver(nameserver) {
400 custom_resolver = self.create_custom_resolver(ns).await?;
401 &custom_resolver
402 } else {
403 &self.default_resolver
404 };
405 let domain = prepare_query(domain, record_type)?;
406
407 debug!(nameserver = nameserver.unwrap_or("system"), "Resolving DNS");
408
409 match record_type {
410 RecordType::SRV => match parse_srv_query(&domain) {
411 Some((service, protocol, name)) => {
413 self.resolve_srv_core(resolver, &service, &protocol, &name)
414 .await
415 }
416 None => Err(srv_format_error()),
419 },
420 RecordType::ANY => self.resolve_any(resolver, &domain).await,
421 single => self.resolve_type(resolver, &domain, single).await,
422 }
423 }
424
425 async fn resolve_srv_core(
432 &self,
433 resolver: &TokioResolver,
434 service: &str,
435 protocol: &str,
436 domain: &str,
437 ) -> Result<Vec<DnsRecord>> {
438 if !is_valid_srv_label(service) {
439 return Err(SeerError::InvalidInput(format!(
440 "invalid SRV service name: {}",
441 service
442 )));
443 }
444 if !is_valid_srv_label(protocol) {
445 return Err(SeerError::InvalidInput(format!(
446 "invalid SRV protocol name: {}",
447 protocol
448 )));
449 }
450
451 let query_name = format!("_{}._{}.{}", service, protocol, domain);
452
453 let Some(response) = dns_lookup_or_empty(
454 resolver.lookup(&query_name, HickoryRecordType::SRV).await,
455 "SRV",
456 )?
457 else {
458 return Ok(vec![]);
459 };
460
461 let records = response
462 .answers()
463 .iter()
464 .filter_map(|record| {
465 if let HickoryRData::SRV(srv) = &record.data {
466 Some(DnsRecord {
467 name: query_name.clone(),
468 record_type: RecordType::SRV,
469 ttl: record.ttl,
470 data: RecordData::SRV {
471 priority: srv.priority,
472 weight: srv.weight,
473 port: srv.port,
474 target: srv.target.to_string(),
475 },
476 })
477 } else {
478 None
479 }
480 })
481 .collect();
482
483 Ok(records)
484 }
485
486 async fn resolve_type(
494 &self,
495 resolver: &TokioResolver,
496 domain: &str,
497 record_type: RecordType,
498 ) -> Result<Vec<DnsRecord>> {
499 match record_type {
500 RecordType::PTR => {
503 let query = if let Ok(ip) = IpAddr::from_str(domain) {
504 reverse_dns_name(&ip)
505 } else {
506 domain.to_string()
507 };
508 self.resolve_records(resolver, &query, RecordType::PTR)
509 .await
510 }
511 single => self.resolve_records(resolver, domain, single).await,
512 }
513 }
514
515 async fn resolve_records(
523 &self,
524 resolver: &TokioResolver,
525 domain: &str,
526 record_type: RecordType,
527 ) -> Result<Vec<DnsRecord>> {
528 let Some(wire_type) = wire_type(record_type) else {
529 return Err(unsupported_record_type(record_type));
530 };
531
532 let Some(response) = dns_lookup_or_empty(
533 resolver.lookup(domain, wire_type).await,
534 &record_type.to_string(),
535 )?
536 else {
537 return Ok(vec![]);
538 };
539
540 let mut records: Vec<DnsRecord> = response
541 .answers()
542 .iter()
543 .filter_map(|record| {
544 convert_rdata(record_type, &record.data).map(|data| DnsRecord {
545 name: domain.to_string(),
546 record_type,
547 ttl: record.ttl,
548 data,
549 })
550 })
551 .collect();
552
553 if record_type == RecordType::MX {
554 records.sort_by_key(|r| match &r.data {
555 RecordData::MX { preference, .. } => *preference,
556 _ => 0,
557 });
558 }
559
560 Ok(records)
561 }
562
563 async fn resolve_any(&self, resolver: &TokioResolver, domain: &str) -> Result<Vec<DnsRecord>> {
564 let record_types = [
569 RecordType::A,
570 RecordType::AAAA,
571 RecordType::MX,
572 RecordType::NS,
573 RecordType::TXT,
574 RecordType::SOA,
575 RecordType::CAA,
576 ];
577
578 let results = futures::future::join_all(
579 record_types
580 .into_iter()
581 .map(|record_type| self.resolve_type(resolver, domain, record_type)),
582 )
583 .await;
584
585 let mut all_records = Vec::new();
590 let mut any_ok = false;
591 let mut last_err = None;
592 for result in results {
593 match result {
594 Ok(records) => {
595 any_ok = true;
596 all_records.extend(records);
597 }
598 Err(e) => last_err = Some(e),
599 }
600 }
601
602 match last_err {
603 Some(e) if !any_ok => Err(e),
604 _ => Ok(all_records),
605 }
606 }
607}
608
609#[derive(Debug, Clone, Copy, PartialEq, Eq)]
614pub enum DnsPresence {
615 Present,
617 Absent,
619 Unknown,
621}
622
623fn classify_ns_presence(result: &Result<Vec<DnsRecord>>) -> DnsPresence {
628 match result {
629 Ok(records) if records.is_empty() => DnsPresence::Absent,
630 Ok(_) => DnsPresence::Present,
631 Err(_) => DnsPresence::Unknown,
632 }
633}
634
635impl DnsResolver {
636 pub async fn presence(&self, domain: &str) -> DnsPresence {
644 let Ok(apex) = normalize_domain(domain) else {
648 return DnsPresence::Unknown;
649 };
650 classify_ns_presence(&self.resolve(&apex, RecordType::NS, None).await)
651 }
652}
653
654pub(crate) fn prepare_query(domain: &str, record_type: RecordType) -> Result<String> {
675 prepare_query_with(domain, record_type, normalize_host)
676}
677
678fn prepare_query_with(
682 domain: &str,
683 record_type: RecordType,
684 normalize: impl Fn(&str) -> Result<String>,
685) -> Result<String> {
686 if record_type == RecordType::PTR {
687 if let Ok(ip) = IpAddr::from_str(domain.trim()) {
688 normalize(&reverse_dns_name(&ip))?;
694 return Ok(ip.to_string());
695 }
696 }
697 normalize(domain)
698}
699
700pub(crate) fn parse_srv_query(name: &str) -> Option<(String, String, String)> {
705 let mut parts = name.splitn(3, '.');
706 let service = parts.next()?.strip_prefix('_')?;
707 let protocol = parts.next()?.strip_prefix('_')?;
708 let rest = parts.next()?;
709 if service.is_empty() || protocol.is_empty() || rest.is_empty() {
710 return None;
711 }
712 Some((service.to_string(), protocol.to_string(), rest.to_string()))
713}
714
715pub(crate) fn srv_format_error() -> SeerError {
719 SeerError::InvalidInput(
720 "SRV records require service name format: _service._proto.name".to_string(),
721 )
722}
723
724fn reverse_dns_name(ip: &IpAddr) -> String {
725 match ip {
726 IpAddr::V4(addr) => {
727 let octets = addr.octets();
728 format!(
729 "{}.{}.{}.{}.in-addr.arpa",
730 octets[3], octets[2], octets[1], octets[0]
731 )
732 }
733 IpAddr::V6(addr) => {
734 let segments = addr.segments();
735 let mut result = String::with_capacity(72);
737 let mut first = true;
738 for segment in segments.iter().rev() {
739 for shift in [0, 4, 8, 12] {
740 if !first {
741 result.push('.');
742 }
743 first = false;
744 let nibble = (segment >> shift) & 0xF;
745 result
746 .push(char::from_digit(nibble as u32, 16).expect("nibble is always 0-15"));
747 }
748 }
749 result.push_str(".ip6.arpa");
750 result
751 }
752 }
753}
754
755fn parse_caa(caa: &CAA) -> (u8, String, String) {
756 let flags = caa.flags();
766 let tag = caa.tag.clone();
767 let value = String::from_utf8_lossy(&caa.value).to_string();
768 (flags, tag, value)
769}
770
771fn wire_type(record_type: RecordType) -> Option<HickoryRecordType> {
777 Some(match record_type {
778 RecordType::A => HickoryRecordType::A,
779 RecordType::AAAA => HickoryRecordType::AAAA,
780 RecordType::CNAME => HickoryRecordType::CNAME,
781 RecordType::MX => HickoryRecordType::MX,
782 RecordType::NS => HickoryRecordType::NS,
783 RecordType::TXT => HickoryRecordType::TXT,
784 RecordType::SOA => HickoryRecordType::SOA,
785 RecordType::PTR => HickoryRecordType::PTR,
786 RecordType::CAA => HickoryRecordType::CAA,
787 RecordType::DNSKEY => HickoryRecordType::DNSKEY,
788 RecordType::DS => HickoryRecordType::DS,
789 RecordType::TLSA => HickoryRecordType::TLSA,
795 RecordType::SSHFP => HickoryRecordType::SSHFP,
796 RecordType::NAPTR => HickoryRecordType::NAPTR,
797 RecordType::SRV | RecordType::ANY => return None,
798 })
799}
800
801fn unsupported_record_type(record_type: RecordType) -> SeerError {
804 SeerError::DnsError(format!("unsupported record type: {}", record_type))
805}
806
807fn hex_upper(bytes: &[u8]) -> String {
810 bytes.iter().map(|b| format!("{:02X}", b)).collect()
811}
812
813fn convert_rdata(record_type: RecordType, data: &HickoryRData) -> Option<RecordData> {
820 use hickory_resolver::proto::dnssec::rdata::DNSSECRData;
821
822 match (record_type, data) {
823 (RecordType::A, HickoryRData::A(addr)) => Some(RecordData::A {
824 address: addr.0.to_string(),
825 }),
826 (RecordType::AAAA, HickoryRData::AAAA(addr)) => Some(RecordData::AAAA {
827 address: addr.0.to_string(),
828 }),
829 (RecordType::CNAME, HickoryRData::CNAME(cname)) => Some(RecordData::CNAME {
830 target: cname.0.to_string(),
831 }),
832 (RecordType::MX, HickoryRData::MX(mx)) => Some(RecordData::MX {
833 preference: mx.preference,
834 exchange: mx.exchange.to_string(),
835 }),
836 (RecordType::NS, HickoryRData::NS(ns)) => Some(RecordData::NS {
837 nameserver: ns.0.to_string(),
838 }),
839 (RecordType::TXT, HickoryRData::TXT(txt)) => Some(RecordData::TXT {
840 text: txt
841 .txt_data
842 .iter()
843 .map(|data| String::from_utf8_lossy(data).to_string())
844 .collect::<Vec<_>>()
845 .join(""),
846 }),
847 (RecordType::SOA, HickoryRData::SOA(soa)) => Some(RecordData::SOA {
848 mname: soa.mname.to_string(),
849 rname: soa.rname.to_string(),
850 serial: soa.serial,
851 refresh: soa.refresh as u32,
857 retry: soa.retry as u32,
858 expire: soa.expire as u32,
859 minimum: soa.minimum,
860 }),
861 (RecordType::PTR, HickoryRData::PTR(ptr)) => Some(RecordData::PTR {
862 target: ptr.0.to_string(),
863 }),
864 (RecordType::CAA, HickoryRData::CAA(caa)) => {
865 let (flags, tag, value) = parse_caa(caa);
866 Some(RecordData::CAA { flags, tag, value })
867 }
868 (RecordType::DNSKEY, HickoryRData::DNSSEC(DNSSECRData::DNSKEY(dnskey))) => {
869 use base64::{engine::general_purpose::STANDARD, Engine};
870 let public_key_buf = dnskey.public_key();
871 Some(RecordData::DNSKEY {
872 flags: dnskey.flags(),
873 protocol: 3,
875 algorithm: u8::from(public_key_buf.algorithm()),
876 public_key: STANDARD.encode(public_key_buf.public_bytes()),
877 })
878 }
879 (RecordType::DS, HickoryRData::DNSSEC(DNSSECRData::DS(ds))) => Some(RecordData::DS {
880 key_tag: ds.key_tag(),
881 algorithm: u8::from(ds.algorithm()),
882 digest_type: u8::from(ds.digest_type()),
883 digest: hex_upper(ds.digest()),
884 }),
885 (RecordType::TLSA, HickoryRData::TLSA(tlsa)) => Some(RecordData::TLSA {
886 cert_usage: u8::from(tlsa.cert_usage),
887 selector: u8::from(tlsa.selector),
888 matching: u8::from(tlsa.matching),
889 cert_data: hex_upper(&tlsa.cert_data),
890 }),
891 (RecordType::SSHFP, HickoryRData::SSHFP(sshfp)) => Some(RecordData::SSHFP {
892 algorithm: u8::from(sshfp.algorithm),
893 fingerprint_type: u8::from(sshfp.fingerprint_type),
894 fingerprint: hex_upper(&sshfp.fingerprint),
895 }),
896 (RecordType::NAPTR, HickoryRData::NAPTR(naptr)) => Some(RecordData::NAPTR {
900 order: naptr.order,
901 preference: naptr.preference,
902 flags: String::from_utf8_lossy(&naptr.flags).into_owned(),
903 services: String::from_utf8_lossy(&naptr.services).into_owned(),
904 regexp: String::from_utf8_lossy(&naptr.regexp).into_owned(),
905 replacement: naptr.replacement.to_string(),
906 }),
907 _ => None,
908 }
909}
910
911fn is_valid_srv_label(label: &str) -> bool {
913 !label.is_empty()
914 && label.len() <= 63
915 && label.chars().all(|c| c.is_ascii_alphanumeric() || c == '-')
916 && !label.starts_with('-')
917 && !label.ends_with('-')
918}
919
920#[cfg(test)]
921mod tests {
922 use super::*;
930
931 #[test]
946 fn standard_opts_pin_server_order_so_ipv4_is_tried_first() {
947 let mut opts = ResolverOpts::default();
948 apply_standard_opts(&mut opts, Duration::from_secs(5));
949 assert_eq!(
950 opts.server_ordering_strategy,
951 ServerOrderingStrategy::UserProvidedOrder,
952 "QueryStatistics ordering is nondeterministic in a short-lived \
953 process and can draw only black-holed IPv6 servers"
954 );
955 }
956
957 #[test]
962 fn default_config_lists_enough_ipv4_servers_before_any_ipv6() {
963 let config = ResolverConfig::udp_and_tcp(&GOOGLE);
964 let ips: Vec<_> = config.name_servers().iter().map(|ns| ns.ip).collect();
965 let concurrent = ResolverOpts::default().num_concurrent_reqs.max(1);
966 assert!(
967 ips.len() > concurrent,
968 "expected more servers than the concurrency window: {ips:?}"
969 );
970 for (i, ip) in ips.iter().take(concurrent).enumerate() {
971 assert!(
972 ip.is_ipv4(),
973 "server {i} in the concurrency window is not IPv4: {ips:?}"
974 );
975 }
976 }
977
978 #[test]
979 fn from_config_applies_dns_timeout() {
980 let mut config = crate::config::SeerConfig::default();
981 config.timeouts.dns_secs = 9;
982 let resolver = DnsResolver::from_config(&config);
983 assert_eq!(resolver.timeout, Duration::from_secs(9));
984 }
985 use std::net::{Ipv4Addr, Ipv6Addr};
986
987 #[test]
990 fn record_type_from_str_accepts_lowercase() {
991 assert_eq!(RecordType::from_str("a").unwrap(), RecordType::A);
992 assert_eq!(RecordType::from_str("mx").unwrap(), RecordType::MX);
993 assert_eq!(RecordType::from_str("cname").unwrap(), RecordType::CNAME);
994 assert_eq!(RecordType::from_str("dnskey").unwrap(), RecordType::DNSKEY);
995 }
996
997 #[test]
998 fn record_type_from_str_accepts_mixed_case() {
999 assert_eq!(RecordType::from_str("Mx").unwrap(), RecordType::MX);
1000 assert_eq!(RecordType::from_str("cNaMe").unwrap(), RecordType::CNAME);
1001 }
1002
1003 #[test]
1004 fn record_type_from_str_rejects_whitespace_padded() {
1005 assert!(RecordType::from_str(" A").is_err());
1009 assert!(RecordType::from_str("A ").is_err());
1010 assert!(RecordType::from_str("\tA\n").is_err());
1011 }
1012
1013 #[test]
1014 fn record_type_from_str_rejects_unknown() {
1015 assert!(RecordType::from_str("NOTAREAL").is_err());
1016 assert!(RecordType::from_str("A1").is_err());
1017 assert!(RecordType::from_str("").is_err());
1018 }
1019
1020 #[test]
1021 fn record_type_from_str_accepts_star_as_any() {
1022 assert_eq!(RecordType::from_str("*").unwrap(), RecordType::ANY);
1023 assert_eq!(RecordType::from_str("ANY").unwrap(), RecordType::ANY);
1024 assert_eq!(RecordType::from_str("any").unwrap(), RecordType::ANY);
1025 }
1026
1027 #[test]
1030 fn srv_label_accepts_alphanumeric_and_hyphen() {
1031 assert!(is_valid_srv_label("http"));
1032 assert!(is_valid_srv_label("ldap-tls"));
1033 assert!(is_valid_srv_label("a1"));
1034 assert!(is_valid_srv_label("tcp"));
1035 }
1036
1037 #[test]
1038 fn srv_label_rejects_empty() {
1039 assert!(!is_valid_srv_label(""));
1040 }
1041
1042 #[test]
1043 fn srv_label_rejects_leading_or_trailing_hyphen() {
1044 assert!(!is_valid_srv_label("-http"));
1045 assert!(!is_valid_srv_label("http-"));
1046 assert!(!is_valid_srv_label("-"));
1047 }
1048
1049 #[test]
1050 fn srv_label_rejects_dots() {
1051 assert!(!is_valid_srv_label("http.evil"));
1054 assert!(!is_valid_srv_label("a.b"));
1055 }
1056
1057 #[test]
1058 fn srv_label_rejects_special_chars() {
1059 assert!(!is_valid_srv_label("http evil"));
1060 assert!(!is_valid_srv_label("http/evil"));
1061 assert!(!is_valid_srv_label("http\0"));
1062 assert!(!is_valid_srv_label("http\n"));
1063 }
1064
1065 #[test]
1066 fn srv_label_rejects_over_63_chars() {
1067 let too_long = "a".repeat(64);
1068 assert!(!is_valid_srv_label(&too_long));
1069 let exactly_63 = "a".repeat(63);
1070 assert!(is_valid_srv_label(&exactly_63));
1071 }
1072
1073 #[test]
1076 fn classify_ns_presence_absent_on_empty_ok() {
1077 let r: Result<Vec<DnsRecord>> = Ok(vec![]);
1079 assert_eq!(classify_ns_presence(&r), DnsPresence::Absent);
1080 }
1081
1082 #[test]
1083 fn classify_ns_presence_present_on_records() {
1084 let rec = DnsRecord {
1085 name: "example.test.".to_string(),
1086 record_type: RecordType::NS,
1087 ttl: 3600,
1088 data: RecordData::NS {
1089 nameserver: "ns1.example.net.".to_string(),
1090 },
1091 };
1092 let r: Result<Vec<DnsRecord>> = Ok(vec![rec]);
1093 assert_eq!(classify_ns_presence(&r), DnsPresence::Present);
1094 }
1095
1096 #[test]
1097 fn classify_ns_presence_unknown_on_error() {
1098 let r: Result<Vec<DnsRecord>> = Err(SeerError::DnsError("servfail".to_string()));
1099 assert_eq!(classify_ns_presence(&r), DnsPresence::Unknown);
1100 }
1101
1102 #[test]
1105 fn reverse_dns_name_formats_ipv4_correctly() {
1106 let ip: IpAddr = Ipv4Addr::new(192, 0, 2, 1).into();
1107 assert_eq!(reverse_dns_name(&ip), "1.2.0.192.in-addr.arpa");
1108 }
1109
1110 #[test]
1111 fn reverse_dns_name_formats_ipv6_correctly() {
1112 let ip: IpAddr = Ipv6Addr::LOCALHOST.into();
1114 let name = reverse_dns_name(&ip);
1115 assert!(
1116 name.ends_with(".ip6.arpa"),
1117 "must end with .ip6.arpa; got: {}",
1118 name
1119 );
1120 assert!(
1122 name.starts_with("1."),
1123 "expected '1.' prefix, got: {}",
1124 name
1125 );
1126 assert_eq!(name.len(), 72);
1128 }
1129
1130 #[test]
1133 fn resolver_new_has_default_timeout() {
1134 let r = DnsResolver::new();
1135 assert_eq!(r.timeout, DEFAULT_TIMEOUT);
1136 }
1137
1138 #[test]
1139 fn resolver_with_timeout_overrides_default() {
1140 let custom = Duration::from_secs(42);
1141 let r = DnsResolver::new().with_timeout(custom);
1142 assert_eq!(r.timeout, custom);
1143 }
1144
1145 #[test]
1146 fn resolver_default_matches_new() {
1147 let a = DnsResolver::default();
1148 let b = DnsResolver::new();
1149 assert_eq!(a.timeout, b.timeout);
1150 }
1151
1152 #[tokio::test]
1155 async fn custom_resolver_rejects_invalid_input() {
1156 let r = DnsResolver::new();
1161 let err = r.create_custom_resolver("..").await.unwrap_err();
1162 let msg = err.to_string().to_lowercase();
1163 assert!(
1164 msg.contains("dns resolution failed") || msg.contains("invalid"),
1165 "expected resolution failure, got: {}",
1166 msg
1167 );
1168 }
1169
1170 #[tokio::test]
1171 async fn custom_resolver_rejects_private_ipv4() {
1172 let r = DnsResolver::new();
1175 for reserved in ["127.0.0.1", "10.0.0.1", "192.168.1.1", "169.254.169.254"] {
1176 let err = r.create_custom_resolver(reserved).await.unwrap_err();
1177 let msg = err.to_string().to_lowercase();
1178 assert!(
1179 msg.contains("blocked") || msg.contains("reserved"),
1180 "reserved IP {} must be rejected, got error: {}",
1181 reserved,
1182 msg
1183 );
1184 }
1185 }
1186
1187 #[tokio::test]
1188 async fn custom_resolver_rejects_loopback_ipv6() {
1189 let r = DnsResolver::new();
1190 let err = r.create_custom_resolver("::1").await.unwrap_err();
1191 let msg = err.to_string().to_lowercase();
1192 assert!(
1193 msg.contains("blocked") || msg.contains("reserved"),
1194 "::1 must be rejected, got error: {}",
1195 msg
1196 );
1197 }
1198
1199 #[tokio::test]
1200 async fn custom_resolver_accepts_public_ipv4() {
1201 let r = DnsResolver::new();
1203 let result = r.create_custom_resolver("8.8.8.8").await;
1204 assert!(
1205 result.is_ok(),
1206 "8.8.8.8 must be accepted as a public nameserver, got: {:?}",
1207 result.err()
1208 );
1209 }
1210
1211 #[tokio::test]
1217 async fn custom_resolver_rejects_private_ip_for_dot_and_doh() {
1218 let r = DnsResolver::new();
1219 for reserved in [
1220 "tls://127.0.0.1",
1221 "tls://192.168.1.1:853",
1222 "tls://[::1]",
1223 "https://10.0.0.1/dns-query",
1224 "https://169.254.169.254",
1225 "tls://[fd00::1]:853",
1226 ] {
1227 let err = r.create_custom_resolver(reserved).await.unwrap_err();
1228 let msg = err.to_string().to_lowercase();
1229 assert!(
1230 msg.contains("blocked") || msg.contains("reserved"),
1231 "reserved spec {} must be rejected, got error: {}",
1232 reserved,
1233 msg
1234 );
1235 }
1236 let err = r
1239 .create_custom_resolver("https://[fd00::1]:443/dns-query")
1240 .await
1241 .unwrap_err();
1242 assert!(matches!(err, SeerError::InvalidInput(_)), "{err:?}");
1243 }
1244
1245 #[tokio::test]
1246 async fn custom_resolver_accepts_public_dot_and_doh_literals() {
1247 let r = DnsResolver::new();
1250 for spec in ["tls://1.1.1.1", "https://8.8.8.8/dns-query"] {
1251 let result = r.create_custom_resolver(spec).await;
1252 assert!(
1253 result.is_ok(),
1254 "{} must be accepted, got: {:?}",
1255 spec,
1256 result.err()
1257 );
1258 }
1259 }
1260
1261 #[tokio::test]
1262 async fn custom_resolver_rejects_unknown_scheme() {
1263 let r = DnsResolver::new();
1264 let err = r.create_custom_resolver("ftp://8.8.8.8").await.unwrap_err();
1265 assert!(
1266 matches!(err, SeerError::InvalidInput(_)),
1267 "unknown scheme must be an input error, got: {err:?}"
1268 );
1269 }
1270
1271 fn spec(s: &str) -> NameserverSpec {
1274 NameserverSpec::parse(s).unwrap_or_else(|e| panic!("{s:?} must parse: {e}"))
1275 }
1276
1277 #[test]
1278 fn upstream_config_udp_defaults() {
1279 use hickory_resolver::config::ProtocolConfig;
1280
1281 let ip: IpAddr = "8.8.8.8".parse().unwrap();
1282 let config = build_upstream_config(&spec("8.8.8.8"), &[ip], None);
1283 let servers = config.name_servers();
1284 assert_eq!(servers.len(), 1);
1285 assert_eq!(servers[0].ip, ip);
1286 assert_eq!(servers[0].connections.len(), 1);
1287 assert_eq!(servers[0].connections[0].port, 53);
1288 assert!(matches!(
1289 servers[0].connections[0].protocol,
1290 ProtocolConfig::Udp
1291 ));
1292 }
1293
1294 #[test]
1295 fn upstream_config_udp_explicit_port() {
1296 let ip: IpAddr = "9.9.9.9".parse().unwrap();
1297 let config = build_upstream_config(&spec("9.9.9.9:5353"), &[ip], None);
1298 assert_eq!(config.name_servers()[0].connections[0].port, 5353);
1299 }
1300
1301 #[test]
1302 fn upstream_config_tls_sets_protocol_port_and_server_name() {
1303 use hickory_resolver::config::ProtocolConfig;
1304
1305 let ip: IpAddr = "9.9.9.9".parse().unwrap();
1306 let config = build_upstream_config(&spec("tls://dns.quad9.net"), &[ip], None);
1307 let ns = &config.name_servers()[0];
1308 assert_eq!(ns.ip, ip);
1309 assert_eq!(ns.connections.len(), 1);
1310 assert_eq!(ns.connections[0].port, 853);
1311 match &ns.connections[0].protocol {
1312 ProtocolConfig::Tls { server_name } => {
1313 assert_eq!(&**server_name, "dns.quad9.net");
1314 }
1315 other => panic!("expected Tls protocol, got {other:?}"),
1316 }
1317 }
1318
1319 #[test]
1320 fn upstream_config_tls_ip_literal_uses_ip_as_server_name() {
1321 use hickory_resolver::config::ProtocolConfig;
1322
1323 let ip: IpAddr = "1.1.1.1".parse().unwrap();
1324 let config = build_upstream_config(&spec("tls://1.1.1.1"), &[ip], None);
1325 match &config.name_servers()[0].connections[0].protocol {
1326 ProtocolConfig::Tls { server_name } => assert_eq!(&**server_name, "1.1.1.1"),
1327 other => panic!("expected Tls protocol, got {other:?}"),
1328 }
1329 }
1330
1331 #[test]
1332 fn upstream_config_https_sets_protocol_port_path_and_server_name() {
1333 use hickory_resolver::config::ProtocolConfig;
1334
1335 let ip: IpAddr = "104.16.248.249".parse().unwrap();
1336 let config = build_upstream_config(&spec("https://cloudflare-dns.com"), &[ip], None);
1337 let ns = &config.name_servers()[0];
1338 assert_eq!(ns.connections[0].port, 443);
1339 match &ns.connections[0].protocol {
1340 ProtocolConfig::Https { server_name, path } => {
1341 assert_eq!(&**server_name, "cloudflare-dns.com");
1342 assert_eq!(&**path, "/dns-query");
1343 }
1344 other => panic!("expected Https protocol, got {other:?}"),
1345 }
1346 }
1347
1348 #[test]
1349 fn upstream_config_https_custom_port_and_path() {
1350 use hickory_resolver::config::ProtocolConfig;
1351
1352 let ip: IpAddr = "8.8.8.8".parse().unwrap();
1353 let config = build_upstream_config(&spec("https://dns.google:8443/resolve"), &[ip], None);
1354 let ns = &config.name_servers()[0];
1355 assert_eq!(ns.connections[0].port, 8443);
1356 match &ns.connections[0].protocol {
1357 ProtocolConfig::Https { server_name, path } => {
1358 assert_eq!(&**server_name, "dns.google");
1359 assert_eq!(&**path, "/resolve");
1360 }
1361 other => panic!("expected Https protocol, got {other:?}"),
1362 }
1363 }
1364
1365 #[test]
1366 fn upstream_config_multiple_ips_share_spec() {
1367 use hickory_resolver::config::ProtocolConfig;
1370
1371 let ips: Vec<IpAddr> = vec![
1372 "9.9.9.9".parse().unwrap(),
1373 "149.112.112.112".parse().unwrap(),
1374 ];
1375 let config = build_upstream_config(&spec("tls://dns.quad9.net"), &ips, None);
1376 let servers = config.name_servers();
1377 assert_eq!(servers.len(), 2);
1378 for (ns, expected_ip) in servers.iter().zip(&ips) {
1379 assert_eq!(&ns.ip, expected_ip);
1380 assert_eq!(ns.connections[0].port, 853);
1381 assert!(matches!(
1382 &ns.connections[0].protocol,
1383 ProtocolConfig::Tls { server_name } if &**server_name == "dns.quad9.net"
1384 ));
1385 }
1386 }
1387
1388 #[test]
1396 fn upstream_config_orders_ipv4_before_ipv6() {
1397 let ips: Vec<IpAddr> = [
1399 "2606:4700::6810:f8f9",
1400 "2606:4700::6810:f9f9",
1401 "104.16.248.249",
1402 "104.16.249.249",
1403 ]
1404 .iter()
1405 .map(|s| s.parse().unwrap())
1406 .collect();
1407 let config =
1408 build_upstream_config(&spec("https://cloudflare-dns.com/dns-query"), &ips, None);
1409 let order: Vec<IpAddr> = config.name_servers().iter().map(|ns| ns.ip).collect();
1410 assert_eq!(
1411 order,
1412 [ips[2], ips[3], ips[0], ips[1]],
1413 "IPv4 first, each family in resolved order, nothing dropped"
1414 );
1415 let concurrent = ResolverOpts::default().num_concurrent_reqs.max(1);
1416 assert!(
1417 order.iter().take(concurrent).all(IpAddr::is_ipv4),
1418 "the parallel window must be all IPv4: {order:?}"
1419 );
1420 }
1421
1422 #[test]
1423 fn upstream_config_test_port_override_wins() {
1424 let ip: IpAddr = "127.0.0.1".parse().unwrap();
1426 let config = build_upstream_config(&spec("127.0.0.1"), &[ip], Some(9999));
1427 assert_eq!(config.name_servers()[0].connections[0].port, 9999);
1428 }
1429
1430 #[tokio::test]
1433 #[ignore = "live network — DoT query against Cloudflare"]
1434 async fn live_resolve_over_dot() {
1435 let r = DnsResolver::new();
1436 let records = r
1437 .resolve("example.com", RecordType::A, Some("tls://1.1.1.1"))
1438 .await
1439 .expect("DoT lookup should succeed");
1440 assert!(!records.is_empty(), "expected A records over DoT");
1441 }
1442
1443 #[tokio::test]
1444 #[ignore = "live network — DoH query against Cloudflare"]
1445 async fn live_resolve_over_doh() {
1446 let r = DnsResolver::new();
1447 let records = r
1448 .resolve(
1449 "example.com",
1450 RecordType::A,
1451 Some("https://cloudflare-dns.com/dns-query"),
1452 )
1453 .await
1454 .expect("DoH lookup should succeed");
1455 assert!(!records.is_empty(), "expected A records over DoH");
1456 }
1457
1458 #[tokio::test]
1461 async fn resolve_srv_rejects_invalid_service_label() {
1462 let r = DnsResolver::new();
1463 let result = r
1465 .resolve_srv_core(&r.default_resolver, "http.evil", "tcp", "example.com")
1466 .await;
1467 assert!(result.is_err());
1468 let msg = result.unwrap_err().to_string().to_lowercase();
1469 assert!(
1470 msg.contains("invalid srv service"),
1471 "expected SRV service validation error, got: {}",
1472 msg
1473 );
1474 }
1475
1476 #[tokio::test]
1477 async fn resolve_srv_rejects_invalid_protocol_label() {
1478 let r = DnsResolver::new();
1479 let result = r
1480 .resolve_srv_core(&r.default_resolver, "http", "tcp.evil", "example.com")
1481 .await;
1482 assert!(result.is_err());
1483 let msg = result.unwrap_err().to_string().to_lowercase();
1484 assert!(
1485 msg.contains("invalid srv protocol"),
1486 "expected SRV protocol validation error, got: {}",
1487 msg
1488 );
1489 }
1490
1491 #[tokio::test]
1492 async fn resolve_srv_normalizes_and_validates_domain_input() {
1493 let r = DnsResolver::new();
1497 let result = r
1498 .resolve("_http._tcp.not a valid domain", RecordType::SRV, None)
1499 .await;
1500 assert!(
1501 matches!(result, Err(SeerError::InvalidDomain(_))),
1502 "expected InvalidDomain from domain validation, got: {result:?}"
1503 );
1504 }
1505
1506 #[tokio::test]
1509 async fn resolve_normalizes_uppercase_domain_input() {
1510 let r = DnsResolver::new();
1515 let result = r.resolve(".bad.example", RecordType::A, None).await;
1516 assert!(result.is_err(), "leading-dot domain must be rejected");
1517 }
1518
1519 #[test]
1524 fn parse_srv_query_extracts_service_proto_and_name() {
1525 assert_eq!(
1526 parse_srv_query("_sip._tcp.example.com"),
1527 Some((
1528 "sip".to_string(),
1529 "tcp".to_string(),
1530 "example.com".to_string()
1531 ))
1532 );
1533 }
1534
1535 #[test]
1536 fn parse_srv_query_keeps_multilabel_domain() {
1537 assert_eq!(
1538 parse_srv_query("_sip._tcp.sip.voice.google.com"),
1539 Some((
1540 "sip".to_string(),
1541 "tcp".to_string(),
1542 "sip.voice.google.com".to_string()
1543 ))
1544 );
1545 }
1546
1547 #[test]
1548 fn fqdn_appends_root_dot_once() {
1549 assert_eq!(fqdn("example.com"), "example.com.");
1550 assert_eq!(fqdn("example.com."), "example.com.");
1551 }
1552
1553 #[test]
1554 fn parse_srv_query_rejects_bare_domain() {
1555 assert_eq!(parse_srv_query("example.com"), None);
1556 }
1557
1558 #[test]
1559 fn parse_srv_query_rejects_missing_proto_label() {
1560 assert_eq!(parse_srv_query("_sip.example.com"), None);
1562 }
1563
1564 #[tokio::test]
1565 async fn resolve_rejects_bare_domain_for_srv_as_input_error() {
1566 let r = DnsResolver::new();
1571 let err = r
1572 .resolve("example.com", RecordType::SRV, None)
1573 .await
1574 .expect_err("bare-domain SRV must error");
1575 assert!(
1576 matches!(err, SeerError::InvalidInput(_)),
1577 "bare-domain SRV should be an input error, got: {err:?}"
1578 );
1579 assert!(err.to_string().contains("_service._proto"));
1580 }
1581
1582 #[tokio::test]
1583 #[ignore = "live network"]
1584 async fn resolve_srv_via_dig_style_name_returns_records() {
1585 let r = DnsResolver::new();
1588 let records = r
1589 .resolve("_caldavs._tcp.google.com", RecordType::SRV, None)
1590 .await
1591 .expect("dig-style SRV lookup should succeed");
1592 assert!(!records.is_empty(), "expected SRV records");
1593 assert!(records.iter().all(|r| r.record_type == RecordType::SRV));
1594 }
1595
1596 #[tokio::test]
1597 #[ignore = "live network"]
1598 async fn resolve_naptr_returns_records() {
1599 let r = DnsResolver::new();
1601 let records = r
1602 .resolve("sip2sip.info", RecordType::NAPTR, None)
1603 .await
1604 .expect("NAPTR lookup should succeed");
1605 assert!(!records.is_empty(), "expected NAPTR records");
1606 assert!(records.iter().all(|r| r.record_type == RecordType::NAPTR));
1607 }
1608
1609 #[test]
1612 fn prepare_query_passes_ipv6_literal_through_for_ptr() {
1613 let out = prepare_query("2606:4700:4700::1111", RecordType::PTR).unwrap();
1619 assert_eq!(out, "2606:4700:4700::1111");
1620 }
1621
1622 #[test]
1623 fn prepare_query_passes_ipv6_loopback_through_for_ptr() {
1624 let out = prepare_query("::1", RecordType::PTR).unwrap();
1625 assert_eq!(out, "::1");
1626 }
1627
1628 #[test]
1629 fn prepare_query_passes_ipv4_literal_through_for_ptr() {
1630 let out = prepare_query("8.8.8.8", RecordType::PTR).unwrap();
1631 assert_eq!(out, "8.8.8.8");
1632 }
1633
1634 #[test]
1635 fn prepare_query_normalizes_non_ip_ptr_names() {
1636 let out = prepare_query("1.1.1.1.in-addr.arpa", RecordType::PTR).unwrap();
1638 assert_eq!(out, "1.1.1.1.in-addr.arpa");
1639 }
1640
1641 #[test]
1642 fn prepare_query_keeps_www_for_record_queries() {
1643 let out = prepare_query("HTTPS://WWW.Example.com/path", RecordType::A).unwrap();
1647 assert_eq!(out, "www.example.com");
1648 let out = prepare_query("www.example.com", RecordType::CNAME).unwrap();
1649 assert_eq!(out, "www.example.com");
1650 }
1651
1652 #[test]
1653 fn prepare_query_still_normalizes_scheme_port_path_and_case() {
1654 let out = prepare_query("https://API.Example.COM:8443/v1?q=1#frag", RecordType::A).unwrap();
1655 assert_eq!(out, "api.example.com");
1656 let out = prepare_query("example.com.", RecordType::MX).unwrap();
1657 assert_eq!(out, "example.com");
1658 assert!(prepare_query("not a domain", RecordType::A).is_err());
1659 }
1660
1661 #[test]
1662 fn prepare_query_runs_ptr_ip_literal_through_the_normalizer() {
1663 let deny_arpa = |name: &str| -> Result<String> {
1669 if name.ends_with(".arpa") {
1670 Err(SeerError::DomainNotAllowed {
1671 domain: name.to_string(),
1672 tld: "arpa".to_string(),
1673 })
1674 } else {
1675 Ok(name.to_string())
1676 }
1677 };
1678 for ip in ["192.0.2.1", "2606:4700:4700::1111"] {
1679 let err = prepare_query_with(ip, RecordType::PTR, deny_arpa)
1680 .expect_err("PTR for an IP literal must obey the normalizer");
1681 assert!(matches!(err, SeerError::DomainNotAllowed { .. }), "{err:?}");
1682 }
1683 assert_eq!(
1685 prepare_query_with("192.0.2.1", RecordType::PTR, normalize_host).unwrap(),
1686 "192.0.2.1"
1687 );
1688 }
1689
1690 #[test]
1691 fn parse_caa_keeps_reserved_flag_bits() {
1692 let mut caa = CAA::new_issue(
1695 true,
1696 Some(hickory_resolver::proto::rr::Name::from_ascii("letsencrypt.org").unwrap()),
1697 vec![],
1698 );
1699 caa.reserved_flags = 0x01;
1700 let (flags, tag, value) = parse_caa(&caa);
1701 assert_eq!(flags, 0x81);
1702 assert_eq!(tag, "issue");
1703 assert_eq!(value, "letsencrypt.org");
1704 }
1705
1706 use crate::dns::test_support::{
1717 mock_dns_resolver, mock_dns_resolver_default, spawn_mock_dns, spawn_mock_dns_fn, MockMode,
1718 MockReply,
1719 };
1720
1721 async fn mock_zone_lookup(record_type: RecordType, domain: &str) -> Vec<DnsRecord> {
1722 let port = spawn_mock_dns(MockMode::Zone).await;
1723 mock_dns_resolver(port)
1724 .resolve(domain, record_type, Some("127.0.0.1"))
1725 .await
1726 .unwrap_or_else(|e| panic!("{record_type} lookup against mock must succeed: {e}"))
1727 }
1728
1729 #[tokio::test]
1730 async fn mock_resolve_a_returns_addresses() {
1731 let records = mock_zone_lookup(RecordType::A, "seer.test").await;
1732 assert_eq!(records.len(), 2);
1733 assert!(records.iter().all(|r| r.record_type == RecordType::A));
1734 assert_eq!(records[0].name, "seer.test");
1735 assert_eq!(records[0].ttl, 300);
1736 let addresses: Vec<String> = records
1737 .iter()
1738 .map(|r| match &r.data {
1739 RecordData::A { address } => address.clone(),
1740 other => panic!("expected A data, got {other:?}"),
1741 })
1742 .collect();
1743 assert!(addresses.contains(&"192.0.2.1".to_string()));
1744 assert!(addresses.contains(&"192.0.2.2".to_string()));
1745 }
1746
1747 #[tokio::test]
1748 async fn mock_resolve_mx_sorts_by_preference() {
1749 let records = mock_zone_lookup(RecordType::MX, "seer.test").await;
1750 let prefs: Vec<u16> = records
1751 .iter()
1752 .map(|r| match &r.data {
1753 RecordData::MX { preference, .. } => *preference,
1754 other => panic!("expected MX data, got {other:?}"),
1755 })
1756 .collect();
1757 assert_eq!(prefs, vec![10, 20, 30]);
1759 assert!(matches!(
1760 &records[0].data,
1761 RecordData::MX { exchange, .. } if exchange == "a.mail.seer.test."
1762 ));
1763 }
1764
1765 #[tokio::test]
1766 async fn mock_resolve_txt_joins_character_strings() {
1767 let records = mock_zone_lookup(RecordType::TXT, "seer.test").await;
1768 assert_eq!(records.len(), 1);
1769 match &records[0].data {
1770 RecordData::TXT { text } => assert_eq!(text, "v=spf1 -all"),
1771 other => panic!("expected TXT data, got {other:?}"),
1772 }
1773 }
1774
1775 #[tokio::test]
1776 async fn mock_resolve_soa_maps_all_fields() {
1777 let records = mock_zone_lookup(RecordType::SOA, "seer.test").await;
1778 assert_eq!(records.len(), 1);
1779 match &records[0].data {
1780 RecordData::SOA {
1781 mname,
1782 rname,
1783 serial,
1784 refresh,
1785 retry,
1786 expire,
1787 minimum,
1788 } => {
1789 assert_eq!(mname, "ns1.seer.test.");
1790 assert_eq!(rname, "hostmaster.seer.test.");
1791 assert_eq!(*serial, 2026070101);
1792 assert_eq!(*refresh, 7200);
1793 assert_eq!(*retry, 3600);
1794 assert_eq!(*expire, 1209600);
1795 assert_eq!(*minimum, 300);
1796 }
1797 other => panic!("expected SOA data, got {other:?}"),
1798 }
1799 }
1800
1801 #[tokio::test]
1802 async fn mock_resolve_caa_maps_flags_tag_and_value() {
1803 let records = mock_zone_lookup(RecordType::CAA, "seer.test").await;
1804 assert_eq!(records.len(), 2);
1805 let by_tag = |wanted: &str| {
1806 records
1807 .iter()
1808 .find_map(|r| match &r.data {
1809 RecordData::CAA { flags, tag, value } if tag == wanted => {
1810 Some((*flags, value.clone()))
1811 }
1812 _ => None,
1813 })
1814 .unwrap_or_else(|| panic!("expected a CAA record with tag {wanted}"))
1815 };
1816 assert_eq!(by_tag("issue"), (0, "letsencrypt.org".to_string()));
1818 assert_eq!(
1819 by_tag("iodef"),
1820 (128, "mailto:security@seer.test".to_string())
1821 );
1822 }
1823
1824 #[tokio::test]
1825 async fn mock_resolve_tlsa_hex_encodes_cert_data() {
1826 let records = mock_zone_lookup(RecordType::TLSA, "_443._tcp.seer.test").await;
1827 assert_eq!(records.len(), 1);
1828 match &records[0].data {
1829 RecordData::TLSA {
1830 cert_usage,
1831 selector,
1832 matching,
1833 cert_data,
1834 } => {
1835 assert_eq!((*cert_usage, *selector, *matching), (3, 1, 1));
1836 assert_eq!(cert_data, "ABCD01");
1837 }
1838 other => panic!("expected TLSA data, got {other:?}"),
1839 }
1840 }
1841
1842 #[tokio::test]
1843 async fn mock_resolve_sshfp_hex_encodes_fingerprint() {
1844 let records = mock_zone_lookup(RecordType::SSHFP, "seer.test").await;
1845 assert_eq!(records.len(), 1);
1846 match &records[0].data {
1847 RecordData::SSHFP {
1848 algorithm,
1849 fingerprint_type,
1850 fingerprint,
1851 } => {
1852 assert_eq!((*algorithm, *fingerprint_type), (4, 2));
1853 assert_eq!(fingerprint, "DEADBEEF");
1854 }
1855 other => panic!("expected SSHFP data, got {other:?}"),
1856 }
1857 }
1858
1859 #[tokio::test]
1860 async fn mock_resolve_naptr_decodes_character_strings() {
1861 let records = mock_zone_lookup(RecordType::NAPTR, "seer.test").await;
1862 assert_eq!(records.len(), 1);
1863 match &records[0].data {
1864 RecordData::NAPTR {
1865 order,
1866 preference,
1867 flags,
1868 services,
1869 regexp,
1870 replacement,
1871 } => {
1872 assert_eq!((*order, *preference), (100, 50));
1873 assert_eq!(flags, "U");
1874 assert_eq!(services, "E2U+sip");
1875 assert_eq!(regexp, "!^.*$!sip:info@seer.test!");
1876 assert_eq!(replacement, ".");
1877 }
1878 other => panic!("expected NAPTR data, got {other:?}"),
1879 }
1880 }
1881
1882 #[tokio::test]
1883 async fn mock_resolve_srv_via_dig_style_name() {
1884 let records = mock_zone_lookup(RecordType::SRV, "_sip._tcp.seer.test").await;
1885 assert_eq!(records.len(), 1);
1886 assert_eq!(records[0].name, "_sip._tcp.seer.test");
1887 match &records[0].data {
1888 RecordData::SRV {
1889 priority,
1890 weight,
1891 port,
1892 target,
1893 } => {
1894 assert_eq!((*priority, *weight, *port), (10, 5, 5060));
1895 assert_eq!(target, "sipserver.seer.test.");
1896 }
1897 other => panic!("expected SRV data, got {other:?}"),
1898 }
1899 }
1900
1901 #[tokio::test]
1902 async fn mock_resolve_ptr_transforms_ip_literal() {
1903 let records = mock_zone_lookup(RecordType::PTR, "192.0.2.1").await;
1904 assert_eq!(records.len(), 1);
1905 assert_eq!(records[0].name, "1.2.0.192.in-addr.arpa");
1907 assert!(matches!(
1908 &records[0].data,
1909 RecordData::PTR { target } if target == "ptr.seer.test."
1910 ));
1911 }
1912
1913 #[tokio::test]
1914 async fn mock_resolve_keeps_www_label() {
1915 let records = mock_zone_lookup(RecordType::CNAME, "www.seer.test").await;
1918 assert_eq!(records.len(), 1, "{records:?}");
1919 assert_eq!(records[0].name, "www.seer.test");
1920 assert_eq!(records[0].data.to_string(), "edge.cdn.test.");
1921 }
1922
1923 #[tokio::test]
1924 async fn mock_resolve_srv_keeps_www_label() {
1925 let port = spawn_mock_dns_fn(|qname, qtype| match (qname, qtype) {
1926 ("_sip._tcp.www.seer.test", HickoryRecordType::SRV) => {
1927 MockReply::Answer(vec![HickoryRData::SRV(
1928 hickory_resolver::proto::rr::rdata::SRV::new(
1929 10,
1930 5,
1931 5060,
1932 hickory_resolver::proto::rr::Name::from_ascii("sip.seer.test.").unwrap(),
1933 ),
1934 )])
1935 }
1936 _ => MockReply::NoData,
1937 })
1938 .await;
1939 let records = mock_dns_resolver(port)
1940 .resolve(
1941 "_sip._tcp.www.seer.test",
1942 RecordType::SRV,
1943 Some("127.0.0.1"),
1944 )
1945 .await
1946 .expect("SRV against mock");
1947 assert_eq!(records.len(), 1, "{records:?}");
1948 assert_eq!(records[0].name, "_sip._tcp.www.seer.test");
1949 }
1950
1951 #[tokio::test]
1952 async fn mock_presence_still_probes_the_apex_for_www_input() {
1953 let port = spawn_mock_dns_fn(|qname, qtype| match (qname, qtype) {
1956 ("seer.test", HickoryRecordType::NS) => MockReply::Answer(vec![HickoryRData::NS(
1957 hickory_resolver::proto::rr::rdata::NS(
1958 hickory_resolver::proto::rr::Name::from_ascii("ns1.seer.test.").unwrap(),
1959 ),
1960 )]),
1961 ("www.seer.test", HickoryRecordType::NS) => MockReply::NoDataWithSoa("seer.test"),
1964 _ => MockReply::NxDomain,
1965 })
1966 .await;
1967 let resolver = mock_dns_resolver_default(port);
1968 assert_eq!(
1969 resolver.presence("www.seer.test").await,
1970 DnsPresence::Present
1971 );
1972 }
1973
1974 #[tokio::test]
1975 async fn mock_resolve_ptr_transforms_ipv6_literal() {
1976 let records = mock_zone_lookup(RecordType::PTR, "2606:4700:4700::1111").await;
1977 assert_eq!(records.len(), 1, "{records:?}");
1978 assert_eq!(records[0].data.to_string(), "one.one.one.one.");
1979 }
1980
1981 #[tokio::test]
1982 async fn mock_resolve_any_aggregates_multiple_types() {
1983 let records = mock_zone_lookup(RecordType::ANY, "seer.test").await;
1984 for expected in [
1986 RecordType::A,
1987 RecordType::AAAA,
1988 RecordType::MX,
1989 RecordType::NS,
1990 RecordType::TXT,
1991 RecordType::SOA,
1992 RecordType::CAA,
1993 ] {
1994 assert!(
1995 records.iter().any(|r| r.record_type == expected),
1996 "ANY must include {expected} records"
1997 );
1998 }
1999 assert_eq!(records.len(), 11);
2001 }
2002
2003 #[tokio::test]
2004 async fn mock_nodata_folds_to_empty_and_classifies_absent() {
2005 let port = spawn_mock_dns(MockMode::NoData).await;
2006 let result = mock_dns_resolver(port)
2007 .resolve("seer.test", RecordType::NS, Some("127.0.0.1"))
2008 .await;
2009 assert!(
2010 matches!(&result, Ok(records) if records.is_empty()),
2011 "NODATA must fold to Ok(vec![]), got: {result:?}"
2012 );
2013 assert_eq!(classify_ns_presence(&result), DnsPresence::Absent);
2014 }
2015
2016 #[tokio::test]
2017 async fn mock_nxdomain_folds_to_empty_and_classifies_absent() {
2018 let port = spawn_mock_dns(MockMode::Nxdomain).await;
2019 let result = mock_dns_resolver(port)
2020 .resolve("seer.test", RecordType::NS, Some("127.0.0.1"))
2021 .await;
2022 assert!(
2023 matches!(&result, Ok(records) if records.is_empty()),
2024 "NXDOMAIN must fold to Ok(vec![]), got: {result:?}"
2025 );
2026 assert_eq!(classify_ns_presence(&result), DnsPresence::Absent);
2027 }
2028
2029 #[tokio::test]
2030 async fn mock_timeout_errors_and_classifies_unknown() {
2031 let port = spawn_mock_dns(MockMode::Ignore).await;
2032 let resolver = DnsResolver::new()
2034 .with_timeout(Duration::from_millis(200))
2035 .allowing_private_hosts()
2036 .with_port(port);
2037 let result = resolver
2038 .resolve("seer.test", RecordType::NS, Some("127.0.0.1"))
2039 .await;
2040 match &result {
2041 Err(SeerError::DnsError(_)) => {}
2042 other => panic!("unanswered query must surface a DnsError, got: {other:?}"),
2043 }
2044 assert_eq!(classify_ns_presence(&result), DnsPresence::Unknown);
2045 }
2046
2047 #[tokio::test]
2048 async fn resolve_type_rejects_composite_types_consistently() {
2049 let r = DnsResolver::new();
2054 for composite in [RecordType::SRV, RecordType::ANY] {
2055 let err = r
2056 .resolve_type(&r.default_resolver, "seer.test", composite)
2057 .await
2058 .expect_err("composite types must be rejected by resolve_type");
2059 assert_eq!(
2060 err.to_string(),
2061 unsupported_record_type(composite).to_string()
2062 );
2063 }
2064 }
2065}