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
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 #[cfg(test)]
175 test_default_nameserver: Option<String>,
176}
177
178impl std::fmt::Debug for DnsResolver {
179 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
180 f.debug_struct("DnsResolver")
181 .field("timeout", &self.timeout)
182 .finish()
183 }
184}
185
186impl Default for DnsResolver {
187 fn default() -> Self {
188 Self::new()
189 }
190}
191
192impl DnsResolver {
193 pub fn new() -> Self {
195 Self {
196 timeout: DEFAULT_TIMEOUT,
197 default_resolver: build_default_resolver(DEFAULT_TIMEOUT),
198 port_override: None,
199 allow_private_hosts: false,
200 #[cfg(test)]
201 test_default_nameserver: None,
202 }
203 }
204
205 pub fn from_config(config: &crate::config::SeerConfig) -> Self {
217 Self::new().with_timeout(config.dns_timeout())
218 }
219
220 #[cfg(test)]
222 pub(crate) fn allowing_private_hosts(mut self) -> Self {
223 self.allow_private_hosts = true;
224 self
225 }
226
227 #[cfg(test)]
230 pub(crate) fn with_port(mut self, port: u16) -> Self {
231 self.port_override = Some(port);
232 self
233 }
234
235 #[cfg(test)]
238 pub(crate) fn with_default_nameserver(mut self, nameserver: &str) -> Self {
239 self.test_default_nameserver = Some(nameserver.to_string());
240 self
241 }
242
243 #[cfg(test)]
247 fn effective_nameserver<'a>(&'a self, nameserver: Option<&'a str>) -> Option<&'a str> {
248 nameserver.or(self.test_default_nameserver.as_deref())
249 }
250
251 #[cfg(not(test))]
252 fn effective_nameserver<'a>(&self, nameserver: Option<&'a str>) -> Option<&'a str> {
253 nameserver
254 }
255
256 pub fn with_timeout(mut self, timeout: Duration) -> Self {
260 self.timeout = timeout;
261 self.default_resolver = build_default_resolver(timeout);
262 self
263 }
264
265 async fn create_custom_resolver(&self, nameserver: &str) -> Result<TokioResolver> {
266 let spec = NameserverSpec::parse(nameserver)?;
271
272 let ips: Vec<IpAddr> = if let Ok(ip) = spec.host.parse::<IpAddr>() {
281 vec![ip]
282 } else {
283 let response = self
284 .default_resolver
285 .lookup_ip(spec.host.as_str())
286 .await
287 .map_err(|e| {
288 SeerError::DnsError(format!(
289 "failed to resolve nameserver hostname {}: {}",
290 spec.host, e
291 ))
292 })?;
293 let resolved: Vec<IpAddr> = response.iter().collect();
294 if resolved.is_empty() {
295 return Err(SeerError::DnsError(format!(
296 "nameserver {} did not resolve to any addresses",
297 spec.host
298 )));
299 }
300 resolved
301 };
302
303 if !self.allow_private_hosts {
310 for ip in &ips {
311 if let Some(reason) = crate::validation::describe_reserved_ip(ip) {
312 return Err(SeerError::DnsError(format!(
313 "nameserver {} blocked: {}",
314 nameserver, reason
315 )));
316 }
317 }
318 }
319
320 build_resolver(
321 build_upstream_config(&spec, &ips, self.port_override),
322 self.timeout,
323 )
324 }
325
326 #[instrument(skip(self), fields(domain = %domain, record_type = %record_type))]
336 pub async fn resolve(
337 &self,
338 domain: &str,
339 record_type: RecordType,
340 nameserver: Option<&str>,
341 ) -> Result<Vec<DnsRecord>> {
342 let custom_resolver;
344 let resolver = if let Some(ns) = self.effective_nameserver(nameserver) {
345 custom_resolver = self.create_custom_resolver(ns).await?;
346 &custom_resolver
347 } else {
348 &self.default_resolver
349 };
350 let domain = prepare_query(domain, record_type)?;
351
352 debug!(nameserver = nameserver.unwrap_or("system"), "Resolving DNS");
353
354 match record_type {
355 RecordType::SRV => match parse_srv_query(&domain) {
356 Some((service, protocol, name)) => {
358 self.resolve_srv_core(resolver, &service, &protocol, &name)
359 .await
360 }
361 None => Err(srv_format_error()),
364 },
365 RecordType::ANY => self.resolve_any(resolver, &domain).await,
366 single => self.resolve_type(resolver, &domain, single).await,
367 }
368 }
369
370 #[instrument(skip(self), fields(domain = %domain, service = %service, protocol = %protocol))]
378 pub async fn resolve_srv(
379 &self,
380 service: &str,
381 protocol: &str,
382 domain: &str,
383 nameserver: Option<&str>,
384 ) -> Result<Vec<DnsRecord>> {
385 let domain = normalize_host(domain)?;
390 let custom_resolver;
391 let resolver = if let Some(ns) = self.effective_nameserver(nameserver) {
392 custom_resolver = self.create_custom_resolver(ns).await?;
393 &custom_resolver
394 } else {
395 &self.default_resolver
396 };
397 self.resolve_srv_core(resolver, service, protocol, &domain)
398 .await
399 }
400
401 async fn resolve_srv_core(
408 &self,
409 resolver: &TokioResolver,
410 service: &str,
411 protocol: &str,
412 domain: &str,
413 ) -> Result<Vec<DnsRecord>> {
414 if !is_valid_srv_label(service) {
415 return Err(SeerError::InvalidInput(format!(
416 "invalid SRV service name: {}",
417 service
418 )));
419 }
420 if !is_valid_srv_label(protocol) {
421 return Err(SeerError::InvalidInput(format!(
422 "invalid SRV protocol name: {}",
423 protocol
424 )));
425 }
426
427 let query_name = format!("_{}._{}.{}", service, protocol, domain);
428
429 let Some(response) = dns_lookup_or_empty(
430 resolver.lookup(&query_name, HickoryRecordType::SRV).await,
431 "SRV",
432 )?
433 else {
434 return Ok(vec![]);
435 };
436
437 let records = response
438 .answers()
439 .iter()
440 .filter_map(|record| {
441 if let HickoryRData::SRV(srv) = &record.data {
442 Some(DnsRecord {
443 name: query_name.clone(),
444 record_type: RecordType::SRV,
445 ttl: record.ttl,
446 data: RecordData::SRV {
447 priority: srv.priority,
448 weight: srv.weight,
449 port: srv.port,
450 target: srv.target.to_string(),
451 },
452 })
453 } else {
454 None
455 }
456 })
457 .collect();
458
459 Ok(records)
460 }
461
462 async fn resolve_type(
470 &self,
471 resolver: &TokioResolver,
472 domain: &str,
473 record_type: RecordType,
474 ) -> Result<Vec<DnsRecord>> {
475 match record_type {
476 RecordType::PTR => {
479 let query = if let Ok(ip) = IpAddr::from_str(domain) {
480 reverse_dns_name(&ip)
481 } else {
482 domain.to_string()
483 };
484 self.resolve_records(resolver, &query, RecordType::PTR)
485 .await
486 }
487 single => self.resolve_records(resolver, domain, single).await,
488 }
489 }
490
491 async fn resolve_records(
499 &self,
500 resolver: &TokioResolver,
501 domain: &str,
502 record_type: RecordType,
503 ) -> Result<Vec<DnsRecord>> {
504 let Some(wire_type) = wire_type(record_type) else {
505 return Err(unsupported_record_type(record_type));
506 };
507
508 let Some(response) = dns_lookup_or_empty(
509 resolver.lookup(domain, wire_type).await,
510 &record_type.to_string(),
511 )?
512 else {
513 return Ok(vec![]);
514 };
515
516 let mut records: Vec<DnsRecord> = response
517 .answers()
518 .iter()
519 .filter_map(|record| {
520 convert_rdata(record_type, &record.data).map(|data| DnsRecord {
521 name: domain.to_string(),
522 record_type,
523 ttl: record.ttl,
524 data,
525 })
526 })
527 .collect();
528
529 if record_type == RecordType::MX {
530 records.sort_by_key(|r| match &r.data {
531 RecordData::MX { preference, .. } => *preference,
532 _ => 0,
533 });
534 }
535
536 Ok(records)
537 }
538
539 async fn resolve_any(&self, resolver: &TokioResolver, domain: &str) -> Result<Vec<DnsRecord>> {
540 let record_types = [
545 RecordType::A,
546 RecordType::AAAA,
547 RecordType::MX,
548 RecordType::NS,
549 RecordType::TXT,
550 RecordType::SOA,
551 RecordType::CAA,
552 ];
553
554 let results = futures::future::join_all(
555 record_types
556 .into_iter()
557 .map(|record_type| self.resolve_type(resolver, domain, record_type)),
558 )
559 .await;
560
561 let mut all_records = Vec::new();
566 let mut any_ok = false;
567 let mut last_err = None;
568 for result in results {
569 match result {
570 Ok(records) => {
571 any_ok = true;
572 all_records.extend(records);
573 }
574 Err(e) => last_err = Some(e),
575 }
576 }
577
578 match last_err {
579 Some(e) if !any_ok => Err(e),
580 _ => Ok(all_records),
581 }
582 }
583}
584
585#[derive(Debug, Clone, Copy, PartialEq, Eq)]
590pub enum DnsPresence {
591 Present,
593 Absent,
595 Unknown,
597}
598
599fn classify_ns_presence(result: &Result<Vec<DnsRecord>>) -> DnsPresence {
604 match result {
605 Ok(records) if records.is_empty() => DnsPresence::Absent,
606 Ok(_) => DnsPresence::Present,
607 Err(_) => DnsPresence::Unknown,
608 }
609}
610
611impl DnsResolver {
612 pub async fn presence(&self, domain: &str) -> DnsPresence {
620 let Ok(apex) = normalize_domain(domain) else {
624 return DnsPresence::Unknown;
625 };
626 classify_ns_presence(&self.resolve(&apex, RecordType::NS, None).await)
627 }
628}
629
630pub(crate) fn prepare_query(domain: &str, record_type: RecordType) -> Result<String> {
651 prepare_query_with(domain, record_type, normalize_host)
652}
653
654fn prepare_query_with(
658 domain: &str,
659 record_type: RecordType,
660 normalize: impl Fn(&str) -> Result<String>,
661) -> Result<String> {
662 if record_type == RecordType::PTR {
663 if let Ok(ip) = IpAddr::from_str(domain.trim()) {
664 normalize(&reverse_dns_name(&ip))?;
670 return Ok(ip.to_string());
671 }
672 }
673 normalize(domain)
674}
675
676pub(crate) fn parse_srv_query(name: &str) -> Option<(String, String, String)> {
681 let mut parts = name.splitn(3, '.');
682 let service = parts.next()?.strip_prefix('_')?;
683 let protocol = parts.next()?.strip_prefix('_')?;
684 let rest = parts.next()?;
685 if service.is_empty() || protocol.is_empty() || rest.is_empty() {
686 return None;
687 }
688 Some((service.to_string(), protocol.to_string(), rest.to_string()))
689}
690
691pub(crate) fn srv_format_error() -> SeerError {
695 SeerError::InvalidInput(
696 "SRV records require service name format: _service._proto.name".to_string(),
697 )
698}
699
700fn reverse_dns_name(ip: &IpAddr) -> String {
701 match ip {
702 IpAddr::V4(addr) => {
703 let octets = addr.octets();
704 format!(
705 "{}.{}.{}.{}.in-addr.arpa",
706 octets[3], octets[2], octets[1], octets[0]
707 )
708 }
709 IpAddr::V6(addr) => {
710 let segments = addr.segments();
711 let mut result = String::with_capacity(72);
713 let mut first = true;
714 for segment in segments.iter().rev() {
715 for shift in [0, 4, 8, 12] {
716 if !first {
717 result.push('.');
718 }
719 first = false;
720 let nibble = (segment >> shift) & 0xF;
721 result
722 .push(char::from_digit(nibble as u32, 16).expect("nibble is always 0-15"));
723 }
724 }
725 result.push_str(".ip6.arpa");
726 result
727 }
728 }
729}
730
731fn parse_caa(caa: &CAA) -> (u8, String, String) {
732 let flags = caa.flags();
742 let tag = caa.tag.clone();
743 let value = String::from_utf8_lossy(&caa.value).to_string();
744 (flags, tag, value)
745}
746
747fn wire_type(record_type: RecordType) -> Option<HickoryRecordType> {
753 Some(match record_type {
754 RecordType::A => HickoryRecordType::A,
755 RecordType::AAAA => HickoryRecordType::AAAA,
756 RecordType::CNAME => HickoryRecordType::CNAME,
757 RecordType::MX => HickoryRecordType::MX,
758 RecordType::NS => HickoryRecordType::NS,
759 RecordType::TXT => HickoryRecordType::TXT,
760 RecordType::SOA => HickoryRecordType::SOA,
761 RecordType::PTR => HickoryRecordType::PTR,
762 RecordType::CAA => HickoryRecordType::CAA,
763 RecordType::DNSKEY => HickoryRecordType::DNSKEY,
764 RecordType::DS => HickoryRecordType::DS,
765 RecordType::TLSA => HickoryRecordType::TLSA,
771 RecordType::SSHFP => HickoryRecordType::SSHFP,
772 RecordType::NAPTR => HickoryRecordType::NAPTR,
773 RecordType::SRV | RecordType::ANY => return None,
774 })
775}
776
777fn unsupported_record_type(record_type: RecordType) -> SeerError {
780 SeerError::DnsError(format!("unsupported record type: {}", record_type))
781}
782
783fn hex_upper(bytes: &[u8]) -> String {
786 bytes.iter().map(|b| format!("{:02X}", b)).collect()
787}
788
789fn convert_rdata(record_type: RecordType, data: &HickoryRData) -> Option<RecordData> {
796 use hickory_resolver::proto::dnssec::rdata::DNSSECRData;
797
798 match (record_type, data) {
799 (RecordType::A, HickoryRData::A(addr)) => Some(RecordData::A {
800 address: addr.0.to_string(),
801 }),
802 (RecordType::AAAA, HickoryRData::AAAA(addr)) => Some(RecordData::AAAA {
803 address: addr.0.to_string(),
804 }),
805 (RecordType::CNAME, HickoryRData::CNAME(cname)) => Some(RecordData::CNAME {
806 target: cname.0.to_string(),
807 }),
808 (RecordType::MX, HickoryRData::MX(mx)) => Some(RecordData::MX {
809 preference: mx.preference,
810 exchange: mx.exchange.to_string(),
811 }),
812 (RecordType::NS, HickoryRData::NS(ns)) => Some(RecordData::NS {
813 nameserver: ns.0.to_string(),
814 }),
815 (RecordType::TXT, HickoryRData::TXT(txt)) => Some(RecordData::TXT {
816 text: txt
817 .txt_data
818 .iter()
819 .map(|data| String::from_utf8_lossy(data).to_string())
820 .collect::<Vec<_>>()
821 .join(""),
822 }),
823 (RecordType::SOA, HickoryRData::SOA(soa)) => Some(RecordData::SOA {
824 mname: soa.mname.to_string(),
825 rname: soa.rname.to_string(),
826 serial: soa.serial,
827 refresh: soa.refresh as u32,
833 retry: soa.retry as u32,
834 expire: soa.expire as u32,
835 minimum: soa.minimum,
836 }),
837 (RecordType::PTR, HickoryRData::PTR(ptr)) => Some(RecordData::PTR {
838 target: ptr.0.to_string(),
839 }),
840 (RecordType::CAA, HickoryRData::CAA(caa)) => {
841 let (flags, tag, value) = parse_caa(caa);
842 Some(RecordData::CAA { flags, tag, value })
843 }
844 (RecordType::DNSKEY, HickoryRData::DNSSEC(DNSSECRData::DNSKEY(dnskey))) => {
845 use base64::{engine::general_purpose::STANDARD, Engine};
846 let public_key_buf = dnskey.public_key();
847 Some(RecordData::DNSKEY {
848 flags: dnskey.flags(),
849 protocol: 3,
851 algorithm: u8::from(public_key_buf.algorithm()),
852 public_key: STANDARD.encode(public_key_buf.public_bytes()),
853 })
854 }
855 (RecordType::DS, HickoryRData::DNSSEC(DNSSECRData::DS(ds))) => Some(RecordData::DS {
856 key_tag: ds.key_tag(),
857 algorithm: u8::from(ds.algorithm()),
858 digest_type: u8::from(ds.digest_type()),
859 digest: hex_upper(ds.digest()),
860 }),
861 (RecordType::TLSA, HickoryRData::TLSA(tlsa)) => Some(RecordData::TLSA {
862 cert_usage: u8::from(tlsa.cert_usage),
863 selector: u8::from(tlsa.selector),
864 matching: u8::from(tlsa.matching),
865 cert_data: hex_upper(&tlsa.cert_data),
866 }),
867 (RecordType::SSHFP, HickoryRData::SSHFP(sshfp)) => Some(RecordData::SSHFP {
868 algorithm: u8::from(sshfp.algorithm),
869 fingerprint_type: u8::from(sshfp.fingerprint_type),
870 fingerprint: hex_upper(&sshfp.fingerprint),
871 }),
872 (RecordType::NAPTR, HickoryRData::NAPTR(naptr)) => Some(RecordData::NAPTR {
876 order: naptr.order,
877 preference: naptr.preference,
878 flags: String::from_utf8_lossy(&naptr.flags).into_owned(),
879 services: String::from_utf8_lossy(&naptr.services).into_owned(),
880 regexp: String::from_utf8_lossy(&naptr.regexp).into_owned(),
881 replacement: naptr.replacement.to_string(),
882 }),
883 _ => None,
884 }
885}
886
887fn is_valid_srv_label(label: &str) -> bool {
889 !label.is_empty()
890 && label.len() <= 63
891 && label.chars().all(|c| c.is_ascii_alphanumeric() || c == '-')
892 && !label.starts_with('-')
893 && !label.ends_with('-')
894}
895
896#[cfg(test)]
897mod tests {
898 use super::*;
906
907 #[test]
922 fn standard_opts_pin_server_order_so_ipv4_is_tried_first() {
923 let mut opts = ResolverOpts::default();
924 apply_standard_opts(&mut opts, Duration::from_secs(5));
925 assert_eq!(
926 opts.server_ordering_strategy,
927 ServerOrderingStrategy::UserProvidedOrder,
928 "QueryStatistics ordering is nondeterministic in a short-lived \
929 process and can draw only black-holed IPv6 servers"
930 );
931 }
932
933 #[test]
938 fn default_config_lists_enough_ipv4_servers_before_any_ipv6() {
939 let config = ResolverConfig::udp_and_tcp(&GOOGLE);
940 let ips: Vec<_> = config.name_servers().iter().map(|ns| ns.ip).collect();
941 let concurrent = ResolverOpts::default().num_concurrent_reqs.max(1);
942 assert!(
943 ips.len() > concurrent,
944 "expected more servers than the concurrency window: {ips:?}"
945 );
946 for (i, ip) in ips.iter().take(concurrent).enumerate() {
947 assert!(
948 ip.is_ipv4(),
949 "server {i} in the concurrency window is not IPv4: {ips:?}"
950 );
951 }
952 }
953
954 #[test]
955 fn from_config_applies_dns_timeout() {
956 let mut config = crate::config::SeerConfig::default();
957 config.timeouts.dns_secs = 9;
958 let resolver = DnsResolver::from_config(&config);
959 assert_eq!(resolver.timeout, Duration::from_secs(9));
960 }
961 use std::net::{Ipv4Addr, Ipv6Addr};
962
963 #[test]
966 fn record_type_from_str_accepts_lowercase() {
967 assert_eq!(RecordType::from_str("a").unwrap(), RecordType::A);
968 assert_eq!(RecordType::from_str("mx").unwrap(), RecordType::MX);
969 assert_eq!(RecordType::from_str("cname").unwrap(), RecordType::CNAME);
970 assert_eq!(RecordType::from_str("dnskey").unwrap(), RecordType::DNSKEY);
971 }
972
973 #[test]
974 fn record_type_from_str_accepts_mixed_case() {
975 assert_eq!(RecordType::from_str("Mx").unwrap(), RecordType::MX);
976 assert_eq!(RecordType::from_str("cNaMe").unwrap(), RecordType::CNAME);
977 }
978
979 #[test]
980 fn record_type_from_str_rejects_whitespace_padded() {
981 assert!(RecordType::from_str(" A").is_err());
985 assert!(RecordType::from_str("A ").is_err());
986 assert!(RecordType::from_str("\tA\n").is_err());
987 }
988
989 #[test]
990 fn record_type_from_str_rejects_unknown() {
991 assert!(RecordType::from_str("NOTAREAL").is_err());
992 assert!(RecordType::from_str("A1").is_err());
993 assert!(RecordType::from_str("").is_err());
994 }
995
996 #[test]
997 fn record_type_from_str_accepts_star_as_any() {
998 assert_eq!(RecordType::from_str("*").unwrap(), RecordType::ANY);
999 assert_eq!(RecordType::from_str("ANY").unwrap(), RecordType::ANY);
1000 assert_eq!(RecordType::from_str("any").unwrap(), RecordType::ANY);
1001 }
1002
1003 #[test]
1006 fn srv_label_accepts_alphanumeric_and_hyphen() {
1007 assert!(is_valid_srv_label("http"));
1008 assert!(is_valid_srv_label("ldap-tls"));
1009 assert!(is_valid_srv_label("a1"));
1010 assert!(is_valid_srv_label("tcp"));
1011 }
1012
1013 #[test]
1014 fn srv_label_rejects_empty() {
1015 assert!(!is_valid_srv_label(""));
1016 }
1017
1018 #[test]
1019 fn srv_label_rejects_leading_or_trailing_hyphen() {
1020 assert!(!is_valid_srv_label("-http"));
1021 assert!(!is_valid_srv_label("http-"));
1022 assert!(!is_valid_srv_label("-"));
1023 }
1024
1025 #[test]
1026 fn srv_label_rejects_dots() {
1027 assert!(!is_valid_srv_label("http.evil"));
1030 assert!(!is_valid_srv_label("a.b"));
1031 }
1032
1033 #[test]
1034 fn srv_label_rejects_special_chars() {
1035 assert!(!is_valid_srv_label("http evil"));
1036 assert!(!is_valid_srv_label("http/evil"));
1037 assert!(!is_valid_srv_label("http\0"));
1038 assert!(!is_valid_srv_label("http\n"));
1039 }
1040
1041 #[test]
1042 fn srv_label_rejects_over_63_chars() {
1043 let too_long = "a".repeat(64);
1044 assert!(!is_valid_srv_label(&too_long));
1045 let exactly_63 = "a".repeat(63);
1046 assert!(is_valid_srv_label(&exactly_63));
1047 }
1048
1049 #[test]
1052 fn classify_ns_presence_absent_on_empty_ok() {
1053 let r: Result<Vec<DnsRecord>> = Ok(vec![]);
1055 assert_eq!(classify_ns_presence(&r), DnsPresence::Absent);
1056 }
1057
1058 #[test]
1059 fn classify_ns_presence_present_on_records() {
1060 let rec = DnsRecord {
1061 name: "example.test.".to_string(),
1062 record_type: RecordType::NS,
1063 ttl: 3600,
1064 data: RecordData::NS {
1065 nameserver: "ns1.example.net.".to_string(),
1066 },
1067 };
1068 let r: Result<Vec<DnsRecord>> = Ok(vec![rec]);
1069 assert_eq!(classify_ns_presence(&r), DnsPresence::Present);
1070 }
1071
1072 #[test]
1073 fn classify_ns_presence_unknown_on_error() {
1074 let r: Result<Vec<DnsRecord>> = Err(SeerError::DnsError("servfail".to_string()));
1075 assert_eq!(classify_ns_presence(&r), DnsPresence::Unknown);
1076 }
1077
1078 #[test]
1081 fn reverse_dns_name_formats_ipv4_correctly() {
1082 let ip: IpAddr = Ipv4Addr::new(192, 0, 2, 1).into();
1083 assert_eq!(reverse_dns_name(&ip), "1.2.0.192.in-addr.arpa");
1084 }
1085
1086 #[test]
1087 fn reverse_dns_name_formats_ipv6_correctly() {
1088 let ip: IpAddr = Ipv6Addr::LOCALHOST.into();
1090 let name = reverse_dns_name(&ip);
1091 assert!(
1092 name.ends_with(".ip6.arpa"),
1093 "must end with .ip6.arpa; got: {}",
1094 name
1095 );
1096 assert!(
1098 name.starts_with("1."),
1099 "expected '1.' prefix, got: {}",
1100 name
1101 );
1102 assert_eq!(name.len(), 72);
1104 }
1105
1106 #[test]
1109 fn resolver_new_has_default_timeout() {
1110 let r = DnsResolver::new();
1111 assert_eq!(r.timeout, DEFAULT_TIMEOUT);
1112 }
1113
1114 #[test]
1115 fn resolver_with_timeout_overrides_default() {
1116 let custom = Duration::from_secs(42);
1117 let r = DnsResolver::new().with_timeout(custom);
1118 assert_eq!(r.timeout, custom);
1119 }
1120
1121 #[test]
1122 fn resolver_default_matches_new() {
1123 let a = DnsResolver::default();
1124 let b = DnsResolver::new();
1125 assert_eq!(a.timeout, b.timeout);
1126 }
1127
1128 #[tokio::test]
1131 async fn custom_resolver_rejects_invalid_input() {
1132 let r = DnsResolver::new();
1137 let err = r.create_custom_resolver("..").await.unwrap_err();
1138 let msg = err.to_string().to_lowercase();
1139 assert!(
1140 msg.contains("dns resolution failed") || msg.contains("invalid"),
1141 "expected resolution failure, got: {}",
1142 msg
1143 );
1144 }
1145
1146 #[tokio::test]
1147 async fn custom_resolver_rejects_private_ipv4() {
1148 let r = DnsResolver::new();
1151 for reserved in ["127.0.0.1", "10.0.0.1", "192.168.1.1", "169.254.169.254"] {
1152 let err = r.create_custom_resolver(reserved).await.unwrap_err();
1153 let msg = err.to_string().to_lowercase();
1154 assert!(
1155 msg.contains("blocked") || msg.contains("reserved"),
1156 "reserved IP {} must be rejected, got error: {}",
1157 reserved,
1158 msg
1159 );
1160 }
1161 }
1162
1163 #[tokio::test]
1164 async fn custom_resolver_rejects_loopback_ipv6() {
1165 let r = DnsResolver::new();
1166 let err = r.create_custom_resolver("::1").await.unwrap_err();
1167 let msg = err.to_string().to_lowercase();
1168 assert!(
1169 msg.contains("blocked") || msg.contains("reserved"),
1170 "::1 must be rejected, got error: {}",
1171 msg
1172 );
1173 }
1174
1175 #[tokio::test]
1176 async fn custom_resolver_accepts_public_ipv4() {
1177 let r = DnsResolver::new();
1179 let result = r.create_custom_resolver("8.8.8.8").await;
1180 assert!(
1181 result.is_ok(),
1182 "8.8.8.8 must be accepted as a public nameserver, got: {:?}",
1183 result.err()
1184 );
1185 }
1186
1187 #[tokio::test]
1193 async fn custom_resolver_rejects_private_ip_for_dot_and_doh() {
1194 let r = DnsResolver::new();
1195 for reserved in [
1196 "tls://127.0.0.1",
1197 "tls://192.168.1.1:853",
1198 "tls://[::1]",
1199 "https://10.0.0.1/dns-query",
1200 "https://169.254.169.254",
1201 "tls://[fd00::1]:853",
1202 ] {
1203 let err = r.create_custom_resolver(reserved).await.unwrap_err();
1204 let msg = err.to_string().to_lowercase();
1205 assert!(
1206 msg.contains("blocked") || msg.contains("reserved"),
1207 "reserved spec {} must be rejected, got error: {}",
1208 reserved,
1209 msg
1210 );
1211 }
1212 let err = r
1215 .create_custom_resolver("https://[fd00::1]:443/dns-query")
1216 .await
1217 .unwrap_err();
1218 assert!(matches!(err, SeerError::InvalidInput(_)), "{err:?}");
1219 }
1220
1221 #[tokio::test]
1222 async fn custom_resolver_accepts_public_dot_and_doh_literals() {
1223 let r = DnsResolver::new();
1226 for spec in ["tls://1.1.1.1", "https://8.8.8.8/dns-query"] {
1227 let result = r.create_custom_resolver(spec).await;
1228 assert!(
1229 result.is_ok(),
1230 "{} must be accepted, got: {:?}",
1231 spec,
1232 result.err()
1233 );
1234 }
1235 }
1236
1237 #[tokio::test]
1238 async fn custom_resolver_rejects_unknown_scheme() {
1239 let r = DnsResolver::new();
1240 let err = r.create_custom_resolver("ftp://8.8.8.8").await.unwrap_err();
1241 assert!(
1242 matches!(err, SeerError::InvalidInput(_)),
1243 "unknown scheme must be an input error, got: {err:?}"
1244 );
1245 }
1246
1247 fn spec(s: &str) -> NameserverSpec {
1250 NameserverSpec::parse(s).unwrap_or_else(|e| panic!("{s:?} must parse: {e}"))
1251 }
1252
1253 #[test]
1254 fn upstream_config_udp_defaults() {
1255 use hickory_resolver::config::ProtocolConfig;
1256
1257 let ip: IpAddr = "8.8.8.8".parse().unwrap();
1258 let config = build_upstream_config(&spec("8.8.8.8"), &[ip], None);
1259 let servers = config.name_servers();
1260 assert_eq!(servers.len(), 1);
1261 assert_eq!(servers[0].ip, ip);
1262 assert_eq!(servers[0].connections.len(), 1);
1263 assert_eq!(servers[0].connections[0].port, 53);
1264 assert!(matches!(
1265 servers[0].connections[0].protocol,
1266 ProtocolConfig::Udp
1267 ));
1268 }
1269
1270 #[test]
1271 fn upstream_config_udp_explicit_port() {
1272 let ip: IpAddr = "9.9.9.9".parse().unwrap();
1273 let config = build_upstream_config(&spec("9.9.9.9:5353"), &[ip], None);
1274 assert_eq!(config.name_servers()[0].connections[0].port, 5353);
1275 }
1276
1277 #[test]
1278 fn upstream_config_tls_sets_protocol_port_and_server_name() {
1279 use hickory_resolver::config::ProtocolConfig;
1280
1281 let ip: IpAddr = "9.9.9.9".parse().unwrap();
1282 let config = build_upstream_config(&spec("tls://dns.quad9.net"), &[ip], None);
1283 let ns = &config.name_servers()[0];
1284 assert_eq!(ns.ip, ip);
1285 assert_eq!(ns.connections.len(), 1);
1286 assert_eq!(ns.connections[0].port, 853);
1287 match &ns.connections[0].protocol {
1288 ProtocolConfig::Tls { server_name } => {
1289 assert_eq!(&**server_name, "dns.quad9.net");
1290 }
1291 other => panic!("expected Tls protocol, got {other:?}"),
1292 }
1293 }
1294
1295 #[test]
1296 fn upstream_config_tls_ip_literal_uses_ip_as_server_name() {
1297 use hickory_resolver::config::ProtocolConfig;
1298
1299 let ip: IpAddr = "1.1.1.1".parse().unwrap();
1300 let config = build_upstream_config(&spec("tls://1.1.1.1"), &[ip], None);
1301 match &config.name_servers()[0].connections[0].protocol {
1302 ProtocolConfig::Tls { server_name } => assert_eq!(&**server_name, "1.1.1.1"),
1303 other => panic!("expected Tls protocol, got {other:?}"),
1304 }
1305 }
1306
1307 #[test]
1308 fn upstream_config_https_sets_protocol_port_path_and_server_name() {
1309 use hickory_resolver::config::ProtocolConfig;
1310
1311 let ip: IpAddr = "104.16.248.249".parse().unwrap();
1312 let config = build_upstream_config(&spec("https://cloudflare-dns.com"), &[ip], None);
1313 let ns = &config.name_servers()[0];
1314 assert_eq!(ns.connections[0].port, 443);
1315 match &ns.connections[0].protocol {
1316 ProtocolConfig::Https { server_name, path } => {
1317 assert_eq!(&**server_name, "cloudflare-dns.com");
1318 assert_eq!(&**path, "/dns-query");
1319 }
1320 other => panic!("expected Https protocol, got {other:?}"),
1321 }
1322 }
1323
1324 #[test]
1325 fn upstream_config_https_custom_port_and_path() {
1326 use hickory_resolver::config::ProtocolConfig;
1327
1328 let ip: IpAddr = "8.8.8.8".parse().unwrap();
1329 let config = build_upstream_config(&spec("https://dns.google:8443/resolve"), &[ip], None);
1330 let ns = &config.name_servers()[0];
1331 assert_eq!(ns.connections[0].port, 8443);
1332 match &ns.connections[0].protocol {
1333 ProtocolConfig::Https { server_name, path } => {
1334 assert_eq!(&**server_name, "dns.google");
1335 assert_eq!(&**path, "/resolve");
1336 }
1337 other => panic!("expected Https protocol, got {other:?}"),
1338 }
1339 }
1340
1341 #[test]
1342 fn upstream_config_multiple_ips_share_spec() {
1343 use hickory_resolver::config::ProtocolConfig;
1346
1347 let ips: Vec<IpAddr> = vec![
1348 "9.9.9.9".parse().unwrap(),
1349 "149.112.112.112".parse().unwrap(),
1350 ];
1351 let config = build_upstream_config(&spec("tls://dns.quad9.net"), &ips, None);
1352 let servers = config.name_servers();
1353 assert_eq!(servers.len(), 2);
1354 for (ns, expected_ip) in servers.iter().zip(&ips) {
1355 assert_eq!(&ns.ip, expected_ip);
1356 assert_eq!(ns.connections[0].port, 853);
1357 assert!(matches!(
1358 &ns.connections[0].protocol,
1359 ProtocolConfig::Tls { server_name } if &**server_name == "dns.quad9.net"
1360 ));
1361 }
1362 }
1363
1364 #[test]
1365 fn upstream_config_test_port_override_wins() {
1366 let ip: IpAddr = "127.0.0.1".parse().unwrap();
1368 let config = build_upstream_config(&spec("127.0.0.1"), &[ip], Some(9999));
1369 assert_eq!(config.name_servers()[0].connections[0].port, 9999);
1370 }
1371
1372 #[tokio::test]
1375 #[ignore = "live network — DoT query against Cloudflare"]
1376 async fn live_resolve_over_dot() {
1377 let r = DnsResolver::new();
1378 let records = r
1379 .resolve("example.com", RecordType::A, Some("tls://1.1.1.1"))
1380 .await
1381 .expect("DoT lookup should succeed");
1382 assert!(!records.is_empty(), "expected A records over DoT");
1383 }
1384
1385 #[tokio::test]
1386 #[ignore = "live network — DoH query against Cloudflare"]
1387 async fn live_resolve_over_doh() {
1388 let r = DnsResolver::new();
1389 let records = r
1390 .resolve(
1391 "example.com",
1392 RecordType::A,
1393 Some("https://cloudflare-dns.com/dns-query"),
1394 )
1395 .await
1396 .expect("DoH lookup should succeed");
1397 assert!(!records.is_empty(), "expected A records over DoH");
1398 }
1399
1400 #[tokio::test]
1403 async fn resolve_srv_rejects_invalid_service_label() {
1404 let r = DnsResolver::new();
1405 let result = r.resolve_srv("http.evil", "tcp", "example.com", None).await;
1407 assert!(result.is_err());
1408 let msg = result.unwrap_err().to_string().to_lowercase();
1409 assert!(
1410 msg.contains("invalid srv service"),
1411 "expected SRV service validation error, got: {}",
1412 msg
1413 );
1414 }
1415
1416 #[tokio::test]
1417 async fn resolve_srv_rejects_invalid_protocol_label() {
1418 let r = DnsResolver::new();
1419 let result = r.resolve_srv("http", "tcp.evil", "example.com", None).await;
1420 assert!(result.is_err());
1421 let msg = result.unwrap_err().to_string().to_lowercase();
1422 assert!(
1423 msg.contains("invalid srv protocol"),
1424 "expected SRV protocol validation error, got: {}",
1425 msg
1426 );
1427 }
1428
1429 #[tokio::test]
1430 async fn resolve_srv_normalizes_and_validates_domain_input() {
1431 let r = DnsResolver::new();
1437 let result = r
1438 .resolve_srv("http", "tcp", "not a valid domain", None)
1439 .await;
1440 assert!(
1441 matches!(result, Err(SeerError::InvalidDomain(_))),
1442 "expected InvalidDomain from domain validation, got: {result:?}"
1443 );
1444 }
1445
1446 #[tokio::test]
1449 async fn resolve_normalizes_uppercase_domain_input() {
1450 let r = DnsResolver::new();
1455 let result = r.resolve(".bad.example", RecordType::A, None).await;
1456 assert!(result.is_err(), "leading-dot domain must be rejected");
1457 }
1458
1459 #[test]
1464 fn parse_srv_query_extracts_service_proto_and_name() {
1465 assert_eq!(
1466 parse_srv_query("_sip._tcp.example.com"),
1467 Some((
1468 "sip".to_string(),
1469 "tcp".to_string(),
1470 "example.com".to_string()
1471 ))
1472 );
1473 }
1474
1475 #[test]
1476 fn parse_srv_query_keeps_multilabel_domain() {
1477 assert_eq!(
1478 parse_srv_query("_sip._tcp.sip.voice.google.com"),
1479 Some((
1480 "sip".to_string(),
1481 "tcp".to_string(),
1482 "sip.voice.google.com".to_string()
1483 ))
1484 );
1485 }
1486
1487 #[test]
1488 fn parse_srv_query_rejects_bare_domain() {
1489 assert_eq!(parse_srv_query("example.com"), None);
1490 }
1491
1492 #[test]
1493 fn parse_srv_query_rejects_missing_proto_label() {
1494 assert_eq!(parse_srv_query("_sip.example.com"), None);
1496 }
1497
1498 #[tokio::test]
1499 async fn resolve_rejects_bare_domain_for_srv_as_input_error() {
1500 let r = DnsResolver::new();
1505 let err = r
1506 .resolve("example.com", RecordType::SRV, None)
1507 .await
1508 .expect_err("bare-domain SRV must error");
1509 assert!(
1510 matches!(err, SeerError::InvalidInput(_)),
1511 "bare-domain SRV should be an input error, got: {err:?}"
1512 );
1513 assert!(err.to_string().contains("_service._proto"));
1514 }
1515
1516 #[tokio::test]
1517 #[ignore = "live network"]
1518 async fn resolve_srv_via_dig_style_name_returns_records() {
1519 let r = DnsResolver::new();
1522 let records = r
1523 .resolve("_caldavs._tcp.google.com", RecordType::SRV, None)
1524 .await
1525 .expect("dig-style SRV lookup should succeed");
1526 assert!(!records.is_empty(), "expected SRV records");
1527 assert!(records.iter().all(|r| r.record_type == RecordType::SRV));
1528 }
1529
1530 #[tokio::test]
1531 #[ignore = "live network"]
1532 async fn resolve_naptr_returns_records() {
1533 let r = DnsResolver::new();
1535 let records = r
1536 .resolve("sip2sip.info", RecordType::NAPTR, None)
1537 .await
1538 .expect("NAPTR lookup should succeed");
1539 assert!(!records.is_empty(), "expected NAPTR records");
1540 assert!(records.iter().all(|r| r.record_type == RecordType::NAPTR));
1541 }
1542
1543 #[test]
1546 fn prepare_query_passes_ipv6_literal_through_for_ptr() {
1547 let out = prepare_query("2606:4700:4700::1111", RecordType::PTR).unwrap();
1553 assert_eq!(out, "2606:4700:4700::1111");
1554 }
1555
1556 #[test]
1557 fn prepare_query_passes_ipv6_loopback_through_for_ptr() {
1558 let out = prepare_query("::1", RecordType::PTR).unwrap();
1559 assert_eq!(out, "::1");
1560 }
1561
1562 #[test]
1563 fn prepare_query_passes_ipv4_literal_through_for_ptr() {
1564 let out = prepare_query("8.8.8.8", RecordType::PTR).unwrap();
1565 assert_eq!(out, "8.8.8.8");
1566 }
1567
1568 #[test]
1569 fn prepare_query_normalizes_non_ip_ptr_names() {
1570 let out = prepare_query("1.1.1.1.in-addr.arpa", RecordType::PTR).unwrap();
1572 assert_eq!(out, "1.1.1.1.in-addr.arpa");
1573 }
1574
1575 #[test]
1576 fn prepare_query_keeps_www_for_record_queries() {
1577 let out = prepare_query("HTTPS://WWW.Example.com/path", RecordType::A).unwrap();
1581 assert_eq!(out, "www.example.com");
1582 let out = prepare_query("www.example.com", RecordType::CNAME).unwrap();
1583 assert_eq!(out, "www.example.com");
1584 }
1585
1586 #[test]
1587 fn prepare_query_still_normalizes_scheme_port_path_and_case() {
1588 let out = prepare_query("https://API.Example.COM:8443/v1?q=1#frag", RecordType::A).unwrap();
1589 assert_eq!(out, "api.example.com");
1590 let out = prepare_query("example.com.", RecordType::MX).unwrap();
1591 assert_eq!(out, "example.com");
1592 assert!(prepare_query("not a domain", RecordType::A).is_err());
1593 }
1594
1595 #[test]
1596 fn prepare_query_runs_ptr_ip_literal_through_the_normalizer() {
1597 let deny_arpa = |name: &str| -> Result<String> {
1603 if name.ends_with(".arpa") {
1604 Err(SeerError::DomainNotAllowed {
1605 domain: name.to_string(),
1606 tld: "arpa".to_string(),
1607 })
1608 } else {
1609 Ok(name.to_string())
1610 }
1611 };
1612 for ip in ["192.0.2.1", "2606:4700:4700::1111"] {
1613 let err = prepare_query_with(ip, RecordType::PTR, deny_arpa)
1614 .expect_err("PTR for an IP literal must obey the normalizer");
1615 assert!(matches!(err, SeerError::DomainNotAllowed { .. }), "{err:?}");
1616 }
1617 assert_eq!(
1619 prepare_query_with("192.0.2.1", RecordType::PTR, normalize_host).unwrap(),
1620 "192.0.2.1"
1621 );
1622 }
1623
1624 #[test]
1625 fn parse_caa_keeps_reserved_flag_bits() {
1626 let mut caa = CAA::new_issue(
1629 true,
1630 Some(hickory_resolver::proto::rr::Name::from_ascii("letsencrypt.org").unwrap()),
1631 vec![],
1632 );
1633 caa.reserved_flags = 0x01;
1634 let (flags, tag, value) = parse_caa(&caa);
1635 assert_eq!(flags, 0x81);
1636 assert_eq!(tag, "issue");
1637 assert_eq!(value, "letsencrypt.org");
1638 }
1639
1640 use crate::dns::test_support::{
1651 mock_dns_resolver, mock_dns_resolver_default, spawn_mock_dns, spawn_mock_dns_fn, MockMode,
1652 MockReply,
1653 };
1654
1655 async fn mock_zone_lookup(record_type: RecordType, domain: &str) -> Vec<DnsRecord> {
1656 let port = spawn_mock_dns(MockMode::Zone).await;
1657 mock_dns_resolver(port)
1658 .resolve(domain, record_type, Some("127.0.0.1"))
1659 .await
1660 .unwrap_or_else(|e| panic!("{record_type} lookup against mock must succeed: {e}"))
1661 }
1662
1663 #[tokio::test]
1664 async fn mock_resolve_a_returns_addresses() {
1665 let records = mock_zone_lookup(RecordType::A, "seer.test").await;
1666 assert_eq!(records.len(), 2);
1667 assert!(records.iter().all(|r| r.record_type == RecordType::A));
1668 assert_eq!(records[0].name, "seer.test");
1669 assert_eq!(records[0].ttl, 300);
1670 let addresses: Vec<String> = records
1671 .iter()
1672 .map(|r| match &r.data {
1673 RecordData::A { address } => address.clone(),
1674 other => panic!("expected A data, got {other:?}"),
1675 })
1676 .collect();
1677 assert!(addresses.contains(&"192.0.2.1".to_string()));
1678 assert!(addresses.contains(&"192.0.2.2".to_string()));
1679 }
1680
1681 #[tokio::test]
1682 async fn mock_resolve_mx_sorts_by_preference() {
1683 let records = mock_zone_lookup(RecordType::MX, "seer.test").await;
1684 let prefs: Vec<u16> = records
1685 .iter()
1686 .map(|r| match &r.data {
1687 RecordData::MX { preference, .. } => *preference,
1688 other => panic!("expected MX data, got {other:?}"),
1689 })
1690 .collect();
1691 assert_eq!(prefs, vec![10, 20, 30]);
1693 assert!(matches!(
1694 &records[0].data,
1695 RecordData::MX { exchange, .. } if exchange == "a.mail.seer.test."
1696 ));
1697 }
1698
1699 #[tokio::test]
1700 async fn mock_resolve_txt_joins_character_strings() {
1701 let records = mock_zone_lookup(RecordType::TXT, "seer.test").await;
1702 assert_eq!(records.len(), 1);
1703 match &records[0].data {
1704 RecordData::TXT { text } => assert_eq!(text, "v=spf1 -all"),
1705 other => panic!("expected TXT data, got {other:?}"),
1706 }
1707 }
1708
1709 #[tokio::test]
1710 async fn mock_resolve_soa_maps_all_fields() {
1711 let records = mock_zone_lookup(RecordType::SOA, "seer.test").await;
1712 assert_eq!(records.len(), 1);
1713 match &records[0].data {
1714 RecordData::SOA {
1715 mname,
1716 rname,
1717 serial,
1718 refresh,
1719 retry,
1720 expire,
1721 minimum,
1722 } => {
1723 assert_eq!(mname, "ns1.seer.test.");
1724 assert_eq!(rname, "hostmaster.seer.test.");
1725 assert_eq!(*serial, 2026070101);
1726 assert_eq!(*refresh, 7200);
1727 assert_eq!(*retry, 3600);
1728 assert_eq!(*expire, 1209600);
1729 assert_eq!(*minimum, 300);
1730 }
1731 other => panic!("expected SOA data, got {other:?}"),
1732 }
1733 }
1734
1735 #[tokio::test]
1736 async fn mock_resolve_caa_maps_flags_tag_and_value() {
1737 let records = mock_zone_lookup(RecordType::CAA, "seer.test").await;
1738 assert_eq!(records.len(), 2);
1739 let by_tag = |wanted: &str| {
1740 records
1741 .iter()
1742 .find_map(|r| match &r.data {
1743 RecordData::CAA { flags, tag, value } if tag == wanted => {
1744 Some((*flags, value.clone()))
1745 }
1746 _ => None,
1747 })
1748 .unwrap_or_else(|| panic!("expected a CAA record with tag {wanted}"))
1749 };
1750 assert_eq!(by_tag("issue"), (0, "letsencrypt.org".to_string()));
1752 assert_eq!(
1753 by_tag("iodef"),
1754 (128, "mailto:security@seer.test".to_string())
1755 );
1756 }
1757
1758 #[tokio::test]
1759 async fn mock_resolve_tlsa_hex_encodes_cert_data() {
1760 let records = mock_zone_lookup(RecordType::TLSA, "_443._tcp.seer.test").await;
1761 assert_eq!(records.len(), 1);
1762 match &records[0].data {
1763 RecordData::TLSA {
1764 cert_usage,
1765 selector,
1766 matching,
1767 cert_data,
1768 } => {
1769 assert_eq!((*cert_usage, *selector, *matching), (3, 1, 1));
1770 assert_eq!(cert_data, "ABCD01");
1771 }
1772 other => panic!("expected TLSA data, got {other:?}"),
1773 }
1774 }
1775
1776 #[tokio::test]
1777 async fn mock_resolve_sshfp_hex_encodes_fingerprint() {
1778 let records = mock_zone_lookup(RecordType::SSHFP, "seer.test").await;
1779 assert_eq!(records.len(), 1);
1780 match &records[0].data {
1781 RecordData::SSHFP {
1782 algorithm,
1783 fingerprint_type,
1784 fingerprint,
1785 } => {
1786 assert_eq!((*algorithm, *fingerprint_type), (4, 2));
1787 assert_eq!(fingerprint, "DEADBEEF");
1788 }
1789 other => panic!("expected SSHFP data, got {other:?}"),
1790 }
1791 }
1792
1793 #[tokio::test]
1794 async fn mock_resolve_naptr_decodes_character_strings() {
1795 let records = mock_zone_lookup(RecordType::NAPTR, "seer.test").await;
1796 assert_eq!(records.len(), 1);
1797 match &records[0].data {
1798 RecordData::NAPTR {
1799 order,
1800 preference,
1801 flags,
1802 services,
1803 regexp,
1804 replacement,
1805 } => {
1806 assert_eq!((*order, *preference), (100, 50));
1807 assert_eq!(flags, "U");
1808 assert_eq!(services, "E2U+sip");
1809 assert_eq!(regexp, "!^.*$!sip:info@seer.test!");
1810 assert_eq!(replacement, ".");
1811 }
1812 other => panic!("expected NAPTR data, got {other:?}"),
1813 }
1814 }
1815
1816 #[tokio::test]
1817 async fn mock_resolve_srv_via_dig_style_name() {
1818 let records = mock_zone_lookup(RecordType::SRV, "_sip._tcp.seer.test").await;
1819 assert_eq!(records.len(), 1);
1820 assert_eq!(records[0].name, "_sip._tcp.seer.test");
1821 match &records[0].data {
1822 RecordData::SRV {
1823 priority,
1824 weight,
1825 port,
1826 target,
1827 } => {
1828 assert_eq!((*priority, *weight, *port), (10, 5, 5060));
1829 assert_eq!(target, "sipserver.seer.test.");
1830 }
1831 other => panic!("expected SRV data, got {other:?}"),
1832 }
1833 }
1834
1835 #[tokio::test]
1836 async fn mock_resolve_ptr_transforms_ip_literal() {
1837 let records = mock_zone_lookup(RecordType::PTR, "192.0.2.1").await;
1838 assert_eq!(records.len(), 1);
1839 assert_eq!(records[0].name, "1.2.0.192.in-addr.arpa");
1841 assert!(matches!(
1842 &records[0].data,
1843 RecordData::PTR { target } if target == "ptr.seer.test."
1844 ));
1845 }
1846
1847 #[tokio::test]
1848 async fn mock_resolve_keeps_www_label() {
1849 let records = mock_zone_lookup(RecordType::CNAME, "www.seer.test").await;
1852 assert_eq!(records.len(), 1, "{records:?}");
1853 assert_eq!(records[0].name, "www.seer.test");
1854 assert_eq!(records[0].data.to_string(), "edge.cdn.test.");
1855 }
1856
1857 #[tokio::test]
1858 async fn mock_resolve_srv_keeps_www_label() {
1859 let port = spawn_mock_dns_fn(|qname, qtype| match (qname, qtype) {
1860 ("_sip._tcp.www.seer.test", HickoryRecordType::SRV) => {
1861 MockReply::Answer(vec![HickoryRData::SRV(
1862 hickory_resolver::proto::rr::rdata::SRV::new(
1863 10,
1864 5,
1865 5060,
1866 hickory_resolver::proto::rr::Name::from_ascii("sip.seer.test.").unwrap(),
1867 ),
1868 )])
1869 }
1870 _ => MockReply::NoData,
1871 })
1872 .await;
1873 let records = mock_dns_resolver(port)
1874 .resolve_srv("sip", "tcp", "www.seer.test", Some("127.0.0.1"))
1875 .await
1876 .expect("SRV against mock");
1877 assert_eq!(records.len(), 1, "{records:?}");
1878 assert_eq!(records[0].name, "_sip._tcp.www.seer.test");
1879 }
1880
1881 #[tokio::test]
1882 async fn mock_presence_still_probes_the_apex_for_www_input() {
1883 let port = spawn_mock_dns_fn(|qname, qtype| match (qname, qtype) {
1886 ("seer.test", HickoryRecordType::NS) => MockReply::Answer(vec![HickoryRData::NS(
1887 hickory_resolver::proto::rr::rdata::NS(
1888 hickory_resolver::proto::rr::Name::from_ascii("ns1.seer.test.").unwrap(),
1889 ),
1890 )]),
1891 ("www.seer.test", HickoryRecordType::NS) => MockReply::NoDataWithSoa("seer.test"),
1894 _ => MockReply::NxDomain,
1895 })
1896 .await;
1897 let resolver = mock_dns_resolver_default(port);
1898 assert_eq!(
1899 resolver.presence("www.seer.test").await,
1900 DnsPresence::Present
1901 );
1902 }
1903
1904 #[tokio::test]
1905 async fn mock_resolve_ptr_transforms_ipv6_literal() {
1906 let records = mock_zone_lookup(RecordType::PTR, "2606:4700:4700::1111").await;
1907 assert_eq!(records.len(), 1, "{records:?}");
1908 assert_eq!(records[0].data.to_string(), "one.one.one.one.");
1909 }
1910
1911 #[tokio::test]
1912 async fn mock_resolve_any_aggregates_multiple_types() {
1913 let records = mock_zone_lookup(RecordType::ANY, "seer.test").await;
1914 for expected in [
1916 RecordType::A,
1917 RecordType::AAAA,
1918 RecordType::MX,
1919 RecordType::NS,
1920 RecordType::TXT,
1921 RecordType::SOA,
1922 RecordType::CAA,
1923 ] {
1924 assert!(
1925 records.iter().any(|r| r.record_type == expected),
1926 "ANY must include {expected} records"
1927 );
1928 }
1929 assert_eq!(records.len(), 11);
1931 }
1932
1933 #[tokio::test]
1934 async fn mock_nodata_folds_to_empty_and_classifies_absent() {
1935 let port = spawn_mock_dns(MockMode::NoData).await;
1936 let result = mock_dns_resolver(port)
1937 .resolve("seer.test", RecordType::NS, Some("127.0.0.1"))
1938 .await;
1939 assert!(
1940 matches!(&result, Ok(records) if records.is_empty()),
1941 "NODATA must fold to Ok(vec![]), got: {result:?}"
1942 );
1943 assert_eq!(classify_ns_presence(&result), DnsPresence::Absent);
1944 }
1945
1946 #[tokio::test]
1947 async fn mock_nxdomain_folds_to_empty_and_classifies_absent() {
1948 let port = spawn_mock_dns(MockMode::Nxdomain).await;
1949 let result = mock_dns_resolver(port)
1950 .resolve("seer.test", RecordType::NS, Some("127.0.0.1"))
1951 .await;
1952 assert!(
1953 matches!(&result, Ok(records) if records.is_empty()),
1954 "NXDOMAIN must fold to Ok(vec![]), got: {result:?}"
1955 );
1956 assert_eq!(classify_ns_presence(&result), DnsPresence::Absent);
1957 }
1958
1959 #[tokio::test]
1960 async fn mock_timeout_errors_and_classifies_unknown() {
1961 let port = spawn_mock_dns(MockMode::Ignore).await;
1962 let resolver = DnsResolver::new()
1964 .with_timeout(Duration::from_millis(200))
1965 .allowing_private_hosts()
1966 .with_port(port);
1967 let result = resolver
1968 .resolve("seer.test", RecordType::NS, Some("127.0.0.1"))
1969 .await;
1970 match &result {
1971 Err(SeerError::DnsError(_)) => {}
1972 other => panic!("unanswered query must surface a DnsError, got: {other:?}"),
1973 }
1974 assert_eq!(classify_ns_presence(&result), DnsPresence::Unknown);
1975 }
1976
1977 #[tokio::test]
1978 async fn resolve_type_rejects_composite_types_consistently() {
1979 let r = DnsResolver::new();
1984 for composite in [RecordType::SRV, RecordType::ANY] {
1985 let err = r
1986 .resolve_type(&r.default_resolver, "seer.test", composite)
1987 .await
1988 .expect_err("composite types must be rejected by resolve_type");
1989 assert_eq!(
1990 err.to_string(),
1991 unsupported_record_type(composite).to_string()
1992 );
1993 }
1994 }
1995}