1use std::borrow::Cow;
28use std::collections::HashMap;
29use std::net::IpAddr;
30use std::pin::pin;
31use std::str::FromStr;
32use std::sync::Arc;
33use std::time::{Duration, Instant};
34
35use futures::future::{self, Either};
36use hickory_resolver::config::{
37 ConnectionConfig, NameServerConfig, ProtocolConfig, ResolveHosts, ResolverConfig, ResolverOpts,
38 ServerOrderingStrategy, GOOGLE,
39};
40use hickory_resolver::lookup::Lookup;
41use hickory_resolver::net::runtime::TokioRuntimeProvider;
42use hickory_resolver::net::NetError;
43use hickory_resolver::proto::dnssec::PublicKey;
44use hickory_resolver::proto::rr::rdata::svcb::{SvcParamKey, SvcParamValue, SVCB};
45use hickory_resolver::proto::rr::rdata::CAA;
46use hickory_resolver::proto::rr::{
47 Name, RData as HickoryRData, Record, RecordType as HickoryRecordType,
48};
49use hickory_resolver::proto::serialize::binary::BinEncodable;
50use hickory_resolver::TokioResolver;
51use tracing::{debug, instrument};
52
53use super::nameserver::{NameserverProtocol, NameserverSpec};
54use super::query::{
55 answered_locally, dedupe_records, fold_any, merge_any, random_probe_label, wildcard_outcome,
56 wildcard_probe_name, DnsQueryResult, Exchange, SubQuery, WildcardProbe,
57};
58use super::records::{DnsRecord, RecordData, RecordType, SvcParam};
59use super::transport::{NoResponse, Transport};
60use super::DEFAULT_DNS_TIMEOUT;
61use crate::error::{Result, SeerError};
62use crate::validation::{normalize_domain, normalize_query_name};
63
64pub(crate) const RESOLVER_ATTEMPTS: usize = 2;
68
69const PROBE_GRACE_DIVISOR: u32 = 5;
75
76const DEFAULT_UPSTREAM: &str = "default (Google Public DNS)";
78
79const ANY_TYPES: [RecordType; 11] = [
86 RecordType::A,
87 RecordType::AAAA,
88 RecordType::CNAME,
89 RecordType::MX,
90 RecordType::NS,
91 RecordType::TXT,
92 RecordType::SOA,
93 RecordType::CAA,
94 RecordType::HTTPS,
95 RecordType::DS,
96 RecordType::DNSKEY,
97];
98
99pub(crate) fn apply_standard_opts(opts: &mut ResolverOpts, timeout: Duration) {
107 opts.timeout = timeout;
108 opts.attempts = RESOLVER_ATTEMPTS;
109 opts.use_hosts_file = ResolveHosts::Never;
110 opts.server_ordering_strategy = ServerOrderingStrategy::UserProvidedOrder;
131}
132
133fn build_resolver(config: ResolverConfig, timeout: Duration) -> Result<TokioResolver> {
142 let mut builder = TokioResolver::builder_with_config(config, TokioRuntimeProvider::default());
143 apply_standard_opts(builder.options_mut(), timeout);
144 builder
145 .build()
146 .map_err(|e| SeerError::DnsError(format!("failed to construct DNS resolver: {}", e)))
147}
148
149pub(crate) fn build_plain_resolver(config: ResolverConfig, timeout: Duration) -> TokioResolver {
156 build_resolver(config, timeout)
157 .expect("plain UDP/TCP resolver build cannot fail with the bundled webpki root store")
158}
159
160pub(crate) async fn lookup_with_tcp_fallback(
175 resolver: &TokioResolver,
176 config: &ResolverConfig,
177 timeout: Duration,
178 name: &str,
179 wire_type: HickoryRecordType,
180) -> std::result::Result<Lookup, NetError> {
181 match resolver.lookup(name, wire_type).await {
182 Err(NetError::Truncated) => {
183 debug!(name, "truncated answer abandoned over UDP; asking over TCP");
184 match build_resolver(tcp_only(config), timeout) {
185 Ok(tcp) => tcp.lookup(name, wire_type).await,
186 Err(e) => {
187 debug!(error = %e, "cannot build the TCP resolver");
188 Err(NetError::Truncated)
189 }
190 }
191 }
192 other => other,
193 }
194}
195
196struct ResolveTarget<'a> {
200 resolver: Cow<'a, TokioResolver>,
201 config: ResolverConfig,
202}
203
204const CUSTOM_RESOLVER_TTL: Duration = Duration::from_secs(60);
209
210const MAX_CUSTOM_RESOLVERS: usize = 32;
213
214type CustomSlot = Arc<std::sync::Mutex<Option<(TokioResolver, ResolverConfig)>>>;
221
222#[derive(Default)]
233struct CustomResolvers {
234 slots: std::sync::Mutex<HashMap<String, (Instant, CustomSlot)>>,
235}
236
237impl CustomResolvers {
238 fn slot(&self, spec: &str) -> CustomSlot {
242 let now = Instant::now();
243 let mut slots = self.slots.lock().unwrap_or_else(|e| e.into_inner());
246 if let Some((created, slot)) = slots.get(spec) {
247 if now.duration_since(*created) < CUSTOM_RESOLVER_TTL {
248 return slot.clone();
249 }
250 }
251 slots.retain(|_, (created, _)| now.duration_since(*created) < CUSTOM_RESOLVER_TTL);
252 if slots.len() >= MAX_CUSTOM_RESOLVERS {
253 let oldest = slots
254 .iter()
255 .min_by_key(|(_, (created, _))| *created)
256 .map(|(key, _)| key.clone());
257 if let Some(oldest) = oldest {
258 slots.remove(&oldest);
259 }
260 }
261 let slot = CustomSlot::default();
262 slots.insert(spec.to_string(), (now, slot.clone()));
263 slot
264 }
265}
266
267fn tcp_only(config: &ResolverConfig) -> ResolverConfig {
270 let mut tcp =
271 ResolverConfig::from_parts(config.domain().cloned(), config.search().to_vec(), vec![]);
272 for server in config.name_servers() {
273 let mut server = server.clone();
274 let udp_ports: Vec<u16> = server
275 .connections
276 .iter()
277 .filter(|c| matches!(c.protocol, ProtocolConfig::Udp))
278 .map(|c| c.port)
279 .collect();
280 server
281 .connections
282 .retain(|c| !matches!(c.protocol, ProtocolConfig::Udp));
283 for port in udp_ports {
284 let has_tcp = server
285 .connections
286 .iter()
287 .any(|c| matches!(c.protocol, ProtocolConfig::Tcp) && c.port == port);
288 if !has_tcp {
289 let mut connection = ConnectionConfig::tcp();
290 connection.port = port;
291 server.connections.push(connection);
292 }
293 }
294 tcp.add_name_server(server);
295 }
296 tcp
297}
298
299fn build_default_resolver(timeout: Duration) -> TokioResolver {
304 build_plain_resolver(default_upstream_config(), timeout)
305}
306
307fn default_upstream_config() -> ResolverConfig {
311 ResolverConfig::udp_and_tcp(&GOOGLE)
312}
313
314pub(crate) fn single_server_config(ip: IpAddr, port: u16, tcp_fallback: bool) -> ResolverConfig {
317 let mut ns = if tcp_fallback {
318 NameServerConfig::udp_and_tcp(ip)
319 } else {
320 NameServerConfig::udp(ip)
321 };
322 for connection in &mut ns.connections {
323 connection.port = port;
324 }
325 let mut config = ResolverConfig::from_parts(None, vec![], vec![]);
326 config.add_name_server(ns);
327 config
328}
329
330pub(crate) fn google_or_pinned(upstream: Option<(IpAddr, u16)>) -> ResolverConfig {
333 match upstream {
334 None => default_upstream_config(),
335 Some((ip, port)) => single_server_config(ip, port, false),
336 }
337}
338
339pub(crate) fn fqdn(name: &str) -> String {
342 if name.ends_with('.') {
343 name.to_string()
344 } else {
345 format!("{name}.")
346 }
347}
348
349fn build_upstream_config(
359 spec: &NameserverSpec,
360 ips: &[IpAddr],
361 port_override: Option<u16>,
362) -> ResolverConfig {
363 let mut config = ResolverConfig::from_parts(None, vec![], vec![]);
364 let port = port_override.unwrap_or(spec.port);
365 let mut ordered = ips.to_vec();
376 crate::net::ipv4_first(&mut ordered, |ip| *ip);
377 for ip in ordered {
378 let mut ns = match spec.protocol {
379 NameserverProtocol::Udp => NameServerConfig::udp(ip),
380 NameserverProtocol::Tls => NameServerConfig::tls(ip, Arc::from(spec.tls_name())),
381 NameserverProtocol::Https => NameServerConfig::https(
382 ip,
383 Arc::from(spec.tls_name()),
384 spec.path.as_deref().map(Arc::from),
385 ),
386 };
387 for connection in &mut ns.connections {
388 connection.port = port;
389 }
390 config.add_name_server(ns);
391 }
392 config
393}
394
395#[derive(Clone)]
403pub struct DnsResolver {
404 timeout: Duration,
405 default_resolver: TokioResolver,
408 transport: Transport,
411 port_override: Option<u16>,
416 allow_private_hosts: bool,
421 custom: Arc<CustomResolvers>,
424 #[cfg(test)]
428 test_default_nameserver: Option<String>,
429}
430
431impl std::fmt::Debug for DnsResolver {
432 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
433 f.debug_struct("DnsResolver")
434 .field("timeout", &self.timeout)
435 .finish()
436 }
437}
438
439impl Default for DnsResolver {
440 fn default() -> Self {
441 Self::new()
442 }
443}
444
445impl DnsResolver {
446 pub fn new() -> Self {
448 Self::with_timeout_only(DEFAULT_DNS_TIMEOUT)
449 }
450
451 fn with_timeout_only(timeout: Duration) -> Self {
454 Self {
455 timeout,
456 default_resolver: build_default_resolver(timeout),
457 transport: Transport::new(timeout),
458 port_override: None,
459 allow_private_hosts: false,
460 custom: Arc::default(),
461 #[cfg(test)]
462 test_default_nameserver: None,
463 }
464 }
465
466 pub fn from_config(config: &crate::config::SeerConfig) -> Self {
477 Self::with_timeout_only(config.dns_timeout())
478 }
479
480 #[cfg(test)]
482 pub(crate) fn timeout(&self) -> Duration {
483 self.timeout
484 }
485
486 #[cfg(test)]
488 pub(crate) fn allowing_private_hosts(mut self) -> Self {
489 self.allow_private_hosts = true;
490 self.custom = Arc::default();
491 self
492 }
493
494 #[cfg(test)]
497 pub(crate) fn with_port(mut self, port: u16) -> Self {
498 self.port_override = Some(port);
499 self.custom = Arc::default();
500 self
501 }
502
503 #[cfg(test)]
506 pub(crate) fn with_default_nameserver(mut self, nameserver: &str) -> Self {
507 self.test_default_nameserver = Some(nameserver.to_string());
508 self
509 }
510
511 #[cfg(test)]
515 pub(crate) fn with_default_upstream_port(mut self, port: u16) -> Self {
516 self.default_resolver = build_resolver(
517 single_server_config(std::net::Ipv4Addr::LOCALHOST.into(), port, false),
518 self.timeout,
519 )
520 .expect("a plain UDP resolver builds");
521 self.custom = Arc::default();
522 self
523 }
524
525 #[cfg(test)]
529 fn effective_nameserver<'a>(&'a self, nameserver: Option<&'a str>) -> Option<&'a str> {
530 nameserver.or(self.test_default_nameserver.as_deref())
531 }
532
533 #[cfg(not(test))]
534 fn effective_nameserver<'a>(&self, nameserver: Option<&'a str>) -> Option<&'a str> {
535 nameserver
536 }
537
538 pub fn with_timeout(mut self, timeout: Duration) -> Self {
542 self.timeout = timeout;
543 self.default_resolver = build_default_resolver(timeout);
544 self.transport = Transport::new(timeout);
545 self.custom = Arc::default();
546 self
547 }
548
549 #[cfg(test)]
552 async fn create_custom_resolver(&self, nameserver: &str) -> Result<TokioResolver> {
553 Ok(self.custom_target(nameserver).await?.resolver.into_owned())
554 }
555
556 async fn memoized_custom_target(&self, nameserver: &str) -> Result<ResolveTarget<'static>> {
560 let slot = self.custom.slot(nameserver);
563 let installed = slot.lock().unwrap_or_else(|e| e.into_inner()).clone();
564 let (resolver, config) = match installed {
565 Some(built) => built,
566 None => {
567 let target = self.custom_target(nameserver).await?;
568 slot.lock()
569 .unwrap_or_else(|e| e.into_inner())
570 .get_or_insert_with(|| (target.resolver.into_owned(), target.config))
571 .clone()
572 }
573 };
574 Ok(ResolveTarget {
575 resolver: Cow::Owned(resolver),
576 config,
577 })
578 }
579
580 async fn custom_target(&self, nameserver: &str) -> Result<ResolveTarget<'static>> {
583 let config = self.custom_upstream_config(nameserver).await?;
584 Ok(ResolveTarget {
585 resolver: Cow::Owned(build_resolver(config.clone(), self.timeout)?),
586 config,
587 })
588 }
589
590 fn default_target(&self) -> ResolveTarget<'_> {
592 ResolveTarget {
593 resolver: Cow::Borrowed(&self.default_resolver),
594 config: default_upstream_config(),
595 }
596 }
597
598 async fn custom_upstream_config(&self, nameserver: &str) -> Result<ResolverConfig> {
606 let spec = NameserverSpec::parse(nameserver)?;
611
612 let ips: Vec<IpAddr> = if let Ok(ip) = spec.host.parse::<IpAddr>() {
621 vec![ip]
622 } else {
623 let lookup = self.default_resolver.lookup_ip(spec.host.as_str());
626 let response = tokio::time::timeout(self.timeout, lookup)
627 .await
628 .unwrap_or(Err(NetError::Timeout))
629 .map_err(|e| {
630 SeerError::DnsError(format!(
631 "failed to resolve nameserver hostname {}: {}",
632 spec.host, e
633 ))
634 })?;
635 let resolved: Vec<IpAddr> = response.iter().collect();
636 if resolved.is_empty() {
637 return Err(SeerError::DnsError(format!(
638 "nameserver {} did not resolve to any addresses",
639 spec.host
640 )));
641 }
642 resolved
643 };
644
645 self.vet_nameserver_ips(nameserver, &ips)?;
646 Ok(build_upstream_config(&spec, &ips, self.port_override))
647 }
648
649 fn vet_nameserver_ips(&self, nameserver: &str, ips: &[IpAddr]) -> Result<()> {
656 if self.allow_private_hosts {
657 return Ok(());
658 }
659 for ip in ips {
660 if let Some(reason) = crate::validation::describe_reserved_ip(ip) {
661 return Err(SeerError::DnsError(format!(
662 "nameserver {} blocked: {}",
663 nameserver, reason
664 )));
665 }
666 }
667 Ok(())
668 }
669
670 fn vet_nameserver_offline(&self, nameserver: &str) -> Result<()> {
676 let spec = NameserverSpec::parse(nameserver)?;
677 match spec.host.parse::<IpAddr>() {
678 Ok(ip) => self.vet_nameserver_ips(nameserver, &[ip]),
679 Err(_) => Ok(()),
680 }
681 }
682
683 async fn upstream(&self, nameserver: Option<&str>, fresh: bool) -> Result<ResolveTarget<'_>> {
687 Ok(match self.effective_nameserver(nameserver) {
688 Some(ns) if fresh => self.custom_target(ns).await?,
689 Some(ns) => self.memoized_custom_target(ns).await?,
690 None => self.default_target(),
691 })
692 }
693
694 async fn query_upstream(&self, name: &str, nameserver: Option<&str>) -> Result<Upstream<'_>> {
702 if answered_locally(name) {
703 if let Some(ns) = self.effective_nameserver(nameserver) {
704 self.vet_nameserver_offline(ns)?;
705 }
706 return Ok(Upstream::Local(&self.default_resolver));
707 }
708 let deadline = self.transport.deadline();
709 let config = match self.effective_nameserver(nameserver) {
710 Some(ns) => self.custom_upstream_config(ns).await?,
711 None => default_upstream_config(),
712 };
713 Ok(Upstream::Direct {
714 config,
715 transport: &self.transport,
716 deadline,
717 })
718 }
719
720 #[instrument(skip(self), fields(domain = %domain, record_type = %record_type))]
769 pub async fn query(
770 &self,
771 domain: &str,
772 record_type: RecordType,
773 nameserver: Option<&str>,
774 ) -> Result<DnsQueryResult> {
775 let domain = prepare_query(domain, record_type)?;
777 let name = wire_query_name(&domain, record_type)?;
778 let upstream = self.query_upstream(&name, nameserver).await?;
779
780 debug!(
781 nameserver = nameserver.unwrap_or(DEFAULT_UPSTREAM),
782 "Querying DNS"
783 );
784
785 let (exchange, query_time, wildcard) = if record_type == RecordType::ANY {
786 let started = Instant::now();
787 let exchange = merge_any(exchange_any(&upstream, &name).await)?;
788 (exchange, started.elapsed(), None)
789 } else {
790 let grace = self.timeout / PROBE_GRACE_DIVISOR;
791 exchange_with_probe(&upstream, &name, record_type, grace).await?
792 };
793 Ok(exchange.into_result(name, record_type, nameserver, wildcard, query_time))
794 }
795
796 pub(crate) async fn query_server(
806 &self,
807 domain: &str,
808 record_type: RecordType,
809 nameserver: &str,
810 ) -> Result<ServerReply> {
811 let domain = prepare_query(domain, record_type)?;
812 let name = wire_query_name(&domain, record_type)?;
813 if answered_locally(&name) {
814 return self
816 .query(&domain, record_type, Some(nameserver))
817 .await
818 .map(ServerReply::Response);
819 }
820 let upstream = self.query_upstream(&name, Some(nameserver)).await?;
821
822 let started = Instant::now();
823 let exchange = if record_type == RecordType::ANY {
824 let replies = exchange_any(&upstream, &name).await;
825 if replies.iter().all(|(_, reply)| matches!(reply, Ok(Err(_)))) {
828 if let Some((_, Ok(Err(why)))) = replies.first() {
829 return Ok(ServerReply::Silent(why.reason()));
830 }
831 }
832 merge_any(replies)?
833 } else {
834 match upstream.try_exchange(&name, record_type).await? {
835 Ok(exchange) => exchange,
836 Err(why) => return Ok(ServerReply::Silent(why.reason())),
837 }
838 };
839 Ok(ServerReply::Response(exchange.into_result(
840 name,
841 record_type,
842 Some(nameserver),
843 None,
844 started.elapsed(),
845 )))
846 }
847
848 #[instrument(skip(self), fields(domain = %domain, record_type = %record_type))]
867 pub async fn resolve(
868 &self,
869 domain: &str,
870 record_type: RecordType,
871 nameserver: Option<&str>,
872 ) -> Result<Vec<DnsRecord>> {
873 self.resolve_on(domain, record_type, nameserver, false)
874 .await
875 }
876
877 pub(crate) async fn resolve_uncached(
883 &self,
884 domain: &str,
885 record_type: RecordType,
886 nameserver: Option<&str>,
887 ) -> Result<Vec<DnsRecord>> {
888 self.resolve_on(domain, record_type, nameserver, true).await
889 }
890
891 async fn resolve_on(
894 &self,
895 domain: &str,
896 record_type: RecordType,
897 nameserver: Option<&str>,
898 fresh: bool,
899 ) -> Result<Vec<DnsRecord>> {
900 let domain = prepare_query(domain, record_type)?;
902 let target = self.upstream(nameserver, fresh).await?;
903
904 debug!(
905 nameserver = nameserver.unwrap_or(DEFAULT_UPSTREAM),
906 "Resolving DNS"
907 );
908
909 match record_type {
910 RecordType::SRV => match parse_srv_query(&domain) {
911 Some((service, protocol, name)) => {
913 self.resolve_srv_core(&target, &service, &protocol, &name)
914 .await
915 }
916 None => Err(srv_format_error()),
919 },
920 RecordType::ANY => self.resolve_any(&target, &domain).await,
921 single => self.resolve_type(&target, &domain, single).await,
922 }
923 }
924
925 async fn resolve_srv_core(
932 &self,
933 target: &ResolveTarget<'_>,
934 service: &str,
935 protocol: &str,
936 domain: &str,
937 ) -> Result<Vec<DnsRecord>> {
938 let query_name = srv_query_name(service, protocol, domain)?;
939 self.resolve_records(target, &query_name, RecordType::SRV)
940 .await
941 }
942
943 async fn resolve_type(
951 &self,
952 target: &ResolveTarget<'_>,
953 domain: &str,
954 record_type: RecordType,
955 ) -> Result<Vec<DnsRecord>> {
956 match record_type {
957 RecordType::SRV | RecordType::ANY => Err(unsupported_record_type(record_type)),
958 RecordType::PTR => {
961 self.resolve_records(target, &ptr_query_name(domain), RecordType::PTR)
962 .await
963 }
964 single => self.resolve_records(target, domain, single).await,
965 }
966 }
967
968 async fn resolve_records(
977 &self,
978 target: &ResolveTarget<'_>,
979 domain: &str,
980 record_type: RecordType,
981 ) -> Result<Vec<DnsRecord>> {
982 let Some(wire_type) = wire_type(record_type) else {
983 return Err(unsupported_record_type(record_type));
984 };
985
986 let response = match lookup_with_tcp_fallback(
987 &target.resolver,
988 &target.config,
989 self.timeout,
990 domain,
991 wire_type,
992 )
993 .await
994 {
995 Ok(response) => response,
996 Err(e) if e.is_no_records_found() => return Ok(vec![]),
997 Err(e) => {
998 return Err(SeerError::DnsError(format!(
999 "{record_type} lookup failed: {e}"
1000 )))
1001 }
1002 };
1003
1004 let mut records: Vec<DnsRecord> = response
1005 .answers()
1006 .iter()
1007 .filter_map(|record| {
1008 convert_rdata(record_type, &record.data, NameSpelling::Display).map(|data| {
1009 DnsRecord {
1010 name: domain.to_string(),
1011 record_type,
1012 ttl: record.ttl,
1013 data,
1014 }
1015 })
1016 })
1017 .collect();
1018
1019 if record_type == RecordType::MX {
1020 records.sort_by_key(|r| match &r.data {
1021 RecordData::MX { preference, .. } => *preference,
1022 _ => 0,
1023 });
1024 }
1025
1026 Ok(records)
1027 }
1028
1029 async fn resolve_any(
1030 &self,
1031 target: &ResolveTarget<'_>,
1032 domain: &str,
1033 ) -> Result<Vec<DnsRecord>> {
1034 let results = future::join_all(ANY_TYPES.into_iter().map(|record_type| async move {
1039 (
1040 record_type,
1041 self.resolve_type(target, domain, record_type).await,
1042 )
1043 }))
1044 .await;
1045
1046 let (record_sets, _failed) = fold_any(results)?;
1050 Ok(dedupe_records(record_sets.into_iter().flatten()))
1051 }
1052}
1053
1054enum Upstream<'a> {
1056 Local(&'a TokioResolver),
1059 Direct {
1063 config: ResolverConfig,
1064 transport: &'a Transport,
1065 deadline: tokio::time::Instant,
1066 },
1067}
1068
1069impl Upstream<'_> {
1070 async fn exchange(&self, name: &str, record_type: RecordType) -> Result<Exchange> {
1073 self.try_exchange(name, record_type)
1074 .await?
1075 .map_err(|why| SeerError::DnsError(format!("{record_type} lookup failed: {why}")))
1076 }
1077
1078 async fn try_exchange(
1081 &self,
1082 name: &str,
1083 record_type: RecordType,
1084 ) -> Result<std::result::Result<Exchange, NoResponse>> {
1085 let wire = wire_type(record_type).ok_or_else(|| unsupported_record_type(record_type))?;
1086 let exchange = match self {
1087 Upstream::Local(resolver) => {
1088 Exchange::from_local(resolver.lookup(name, wire).await, record_type)?
1089 }
1090 Upstream::Direct {
1091 config,
1092 transport,
1093 deadline,
1094 } => {
1095 let qname = Name::from_ascii(fqdn(name))
1096 .map_err(|e| SeerError::InvalidDomain(format!("{name}: {e}")))?;
1097 let request = transport.request(qname, wire, true);
1098 match transport
1099 .query(config.name_servers(), &request, *deadline)
1100 .await
1101 {
1102 Ok(response) => Exchange::from_message(&response),
1103 Err(why) => return Ok(Err(why)),
1104 }
1105 }
1106 };
1107 Ok(Ok(exchange.ordered(name, record_type)))
1108 }
1109}
1110
1111#[derive(Debug)]
1113pub(crate) enum ServerReply {
1114 Response(DnsQueryResult),
1116 Silent(String),
1119}
1120
1121async fn exchange_any(upstream: &Upstream<'_>, name: &str) -> Vec<(RecordType, SubQuery)> {
1125 future::join_all(ANY_TYPES.into_iter().map(|record_type| async move {
1126 (record_type, upstream.try_exchange(name, record_type).await)
1127 }))
1128 .await
1129}
1130
1131async fn exchange_with_probe(
1141 upstream: &Upstream<'_>,
1142 name: &str,
1143 record_type: RecordType,
1144 grace: Duration,
1145) -> Result<(Exchange, Duration, Option<WildcardProbe>)> {
1146 let main = async {
1147 let started = Instant::now();
1148 let result = upstream.exchange(name, record_type).await;
1149 (result, started.elapsed())
1150 };
1151 let Some(probe_name) = wildcard_probe_name(name, record_type, &random_probe_label()) else {
1152 let (result, elapsed) = main.await;
1153 return Ok((result?, elapsed, None));
1154 };
1155 let probe = upstream.exchange(&probe_name, record_type);
1156
1157 let main = pin!(main);
1158 let probe = pin!(probe);
1159 let ((result, elapsed), probe) = match future::select(main, probe).await {
1160 Either::Left(((result, elapsed), probe)) => {
1161 let attachable = matches!(&result, Ok(answer) if answer.has_records(record_type));
1162 let probe = if attachable {
1166 tokio::time::timeout(grace, probe).await.ok()
1167 } else {
1168 None
1169 };
1170 ((result, elapsed), probe)
1171 }
1172 Either::Right((probe, main)) => (main.await, Some(probe)),
1173 };
1174 let answer = result?;
1175 let wildcard =
1176 probe.and_then(|probe| wildcard_outcome(&probe_name, record_type, &answer, probe));
1177 Ok((answer, elapsed, wildcard))
1178}
1179
1180#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1185pub enum DnsPresence {
1186 Present,
1188 Absent,
1190 Unknown,
1192}
1193
1194fn classify_ns_presence(result: &Result<Vec<DnsRecord>>) -> DnsPresence {
1199 match result {
1200 Ok(records) if records.is_empty() => DnsPresence::Absent,
1201 Ok(_) => DnsPresence::Present,
1202 Err(_) => DnsPresence::Unknown,
1203 }
1204}
1205
1206impl DnsResolver {
1207 pub async fn presence(&self, domain: &str) -> DnsPresence {
1215 let Ok(apex) = normalize_domain(domain) else {
1219 return DnsPresence::Unknown;
1220 };
1221 classify_ns_presence(&self.resolve(&apex, RecordType::NS, None).await)
1222 }
1223}
1224
1225pub(crate) fn prepare_query(domain: &str, record_type: RecordType) -> Result<String> {
1248 prepare_query_with(domain, record_type, normalize_query_name)
1249}
1250
1251fn prepare_query_with(
1255 domain: &str,
1256 record_type: RecordType,
1257 normalize: impl Fn(&str) -> Result<String>,
1258) -> Result<String> {
1259 if record_type == RecordType::PTR {
1260 if let Ok(ip) = IpAddr::from_str(domain.trim()) {
1261 normalize(&reverse_dns_name(&ip))?;
1267 return Ok(ip.to_string());
1268 }
1269 }
1270 normalize(domain)
1271}
1272
1273pub(crate) fn parse_srv_query(name: &str) -> Option<(String, String, String)> {
1278 let mut parts = name.splitn(3, '.');
1279 let service = parts.next()?.strip_prefix('_')?;
1280 let protocol = parts.next()?.strip_prefix('_')?;
1281 let rest = parts.next()?;
1282 if service.is_empty() || protocol.is_empty() || rest.is_empty() {
1283 return None;
1284 }
1285 Some((service.to_string(), protocol.to_string(), rest.to_string()))
1286}
1287
1288fn srv_query_name(service: &str, protocol: &str, domain: &str) -> Result<String> {
1292 if !is_valid_srv_label(service) {
1293 return Err(SeerError::InvalidInput(format!(
1294 "invalid SRV service name: {}",
1295 service
1296 )));
1297 }
1298 if !is_valid_srv_label(protocol) {
1299 return Err(SeerError::InvalidInput(format!(
1300 "invalid SRV protocol name: {}",
1301 protocol
1302 )));
1303 }
1304 Ok(format!("_{}._{}.{}", service, protocol, domain))
1305}
1306
1307fn ptr_query_name(domain: &str) -> String {
1310 match IpAddr::from_str(domain) {
1311 Ok(ip) => reverse_dns_name(&ip),
1312 Err(_) => domain.to_string(),
1313 }
1314}
1315
1316pub(crate) fn wire_query_name(domain: &str, record_type: RecordType) -> Result<String> {
1322 match record_type {
1323 RecordType::SRV => {
1324 let (service, protocol, name) = parse_srv_query(domain).ok_or_else(srv_format_error)?;
1325 srv_query_name(&service, &protocol, &name)
1326 }
1327 RecordType::PTR => Ok(ptr_query_name(domain)),
1328 _ => Ok(domain.to_string()),
1329 }
1330}
1331
1332pub(crate) fn srv_format_error() -> SeerError {
1336 SeerError::InvalidInput(
1337 "SRV records require service name format: _service._proto.name".to_string(),
1338 )
1339}
1340
1341fn reverse_dns_name(ip: &IpAddr) -> String {
1342 match ip {
1343 IpAddr::V4(addr) => {
1344 let octets = addr.octets();
1345 format!(
1346 "{}.{}.{}.{}.in-addr.arpa",
1347 octets[3], octets[2], octets[1], octets[0]
1348 )
1349 }
1350 IpAddr::V6(addr) => {
1351 let segments = addr.segments();
1352 let mut result = String::with_capacity(72);
1354 let mut first = true;
1355 for segment in segments.iter().rev() {
1356 for shift in [0, 4, 8, 12] {
1357 if !first {
1358 result.push('.');
1359 }
1360 first = false;
1361 let nibble = (segment >> shift) & 0xF;
1362 result
1363 .push(char::from_digit(nibble as u32, 16).expect("nibble is always 0-15"));
1364 }
1365 }
1366 result.push_str(".ip6.arpa");
1367 result
1368 }
1369 }
1370}
1371
1372fn parse_caa(caa: &CAA) -> (u8, String, String) {
1373 let flags = caa.flags();
1383 let tag = caa.tag.clone();
1384 let value = String::from_utf8_lossy(&caa.value).to_string();
1385 (flags, tag, value)
1386}
1387
1388fn parse_svcb(svcb: &SVCB, names: NameSpelling) -> (u16, String, Vec<SvcParam>) {
1395 let params = svcb
1396 .svc_params
1397 .iter()
1398 .map(|(key, value)| SvcParam {
1399 key: svc_param_key_name(*key),
1400 value: svc_param_value(value),
1401 })
1402 .collect();
1403 (svcb.svc_priority, names.spell(&svcb.target_name), params)
1404}
1405
1406fn svc_param_key_name(key: SvcParamKey) -> String {
1409 match key {
1410 SvcParamKey::Mandatory => "mandatory".to_string(),
1411 SvcParamKey::Alpn => "alpn".to_string(),
1412 SvcParamKey::NoDefaultAlpn => "no-default-alpn".to_string(),
1413 SvcParamKey::Port => "port".to_string(),
1414 SvcParamKey::Ipv4Hint => "ipv4hint".to_string(),
1415 SvcParamKey::EchConfigList => "ech".to_string(),
1416 SvcParamKey::Ipv6Hint => "ipv6hint".to_string(),
1417 SvcParamKey::Key(_) | SvcParamKey::Key65535 | SvcParamKey::Unknown(_) => {
1418 format!("key{}", u16::from(key))
1419 }
1420 }
1421}
1422
1423fn svc_param_value(value: &SvcParamValue) -> String {
1427 use base64::{engine::general_purpose::STANDARD, Engine};
1428 fn join<T: std::fmt::Display>(items: impl Iterator<Item = T>) -> String {
1429 items
1430 .map(|item| item.to_string())
1431 .collect::<Vec<_>>()
1432 .join(",")
1433 }
1434 match value {
1435 SvcParamValue::Mandatory(keys) => join(keys.0.iter().map(|k| svc_param_key_name(*k))),
1436 SvcParamValue::Alpn(alpn) => join(alpn.0.iter().map(|id| {
1442 let listed = id.replace('\\', "\\\\").replace(',', "\\,");
1443 escape_char_string(listed.as_bytes())
1444 })),
1445 SvcParamValue::NoDefaultAlpn => String::new(),
1446 SvcParamValue::Port(port) => port.to_string(),
1447 SvcParamValue::Ipv4Hint(hint) => join(hint.0.iter().map(|a| a.0)),
1448 SvcParamValue::EchConfigList(ech) => STANDARD.encode(&ech.0),
1449 SvcParamValue::Ipv6Hint(hint) => join(hint.0.iter().map(|aaaa| aaaa.0)),
1450 SvcParamValue::Unknown(opaque) => escape_char_string(&opaque.0),
1451 }
1452}
1453
1454fn escape_char_string(bytes: &[u8]) -> String {
1459 let mut out = String::with_capacity(bytes.len());
1460 for &b in bytes {
1461 match b {
1462 b'"' | b'\\' => {
1463 out.push('\\');
1464 out.push(char::from(b));
1465 }
1466 0x21..=0x7E => out.push(char::from(b)),
1467 _ => out.push_str(&format!("\\{b:03}")),
1468 }
1469 }
1470 out
1471}
1472
1473pub(crate) fn wire_type(record_type: RecordType) -> Option<HickoryRecordType> {
1480 Some(match record_type {
1481 RecordType::A => HickoryRecordType::A,
1482 RecordType::AAAA => HickoryRecordType::AAAA,
1483 RecordType::CNAME => HickoryRecordType::CNAME,
1484 RecordType::MX => HickoryRecordType::MX,
1485 RecordType::NS => HickoryRecordType::NS,
1486 RecordType::TXT => HickoryRecordType::TXT,
1487 RecordType::SOA => HickoryRecordType::SOA,
1488 RecordType::PTR => HickoryRecordType::PTR,
1489 RecordType::SRV => HickoryRecordType::SRV,
1490 RecordType::CAA => HickoryRecordType::CAA,
1491 RecordType::DNSKEY => HickoryRecordType::DNSKEY,
1492 RecordType::DS => HickoryRecordType::DS,
1493 RecordType::CDS => HickoryRecordType::CDS,
1496 RecordType::CDNSKEY => HickoryRecordType::CDNSKEY,
1497 RecordType::HTTPS => HickoryRecordType::HTTPS,
1499 RecordType::SVCB => HickoryRecordType::SVCB,
1500 RecordType::TLSA => HickoryRecordType::TLSA,
1506 RecordType::SSHFP => HickoryRecordType::SSHFP,
1507 RecordType::NAPTR => HickoryRecordType::NAPTR,
1508 RecordType::ANY => return None,
1509 })
1510}
1511
1512fn unsupported_record_type(record_type: RecordType) -> SeerError {
1515 SeerError::DnsError(format!("unsupported record type: {}", record_type))
1516}
1517
1518fn hex_upper(bytes: &[u8]) -> String {
1521 bytes.iter().map(|b| format!("{:02X}", b)).collect()
1522}
1523
1524#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1528pub(crate) enum NameSpelling {
1529 Display,
1532 Ascii,
1537}
1538
1539impl NameSpelling {
1540 fn spell(self, name: &Name) -> String {
1541 match self {
1542 NameSpelling::Display => name.to_string(),
1543 NameSpelling::Ascii => name.to_ascii(),
1544 }
1545 }
1546}
1547
1548pub(crate) fn convert_rdata(
1556 record_type: RecordType,
1557 data: &HickoryRData,
1558 names: NameSpelling,
1559) -> Option<RecordData> {
1560 use hickory_resolver::proto::dnssec::rdata::DNSSECRData;
1561
1562 match (record_type, data) {
1563 (RecordType::A, HickoryRData::A(addr)) => Some(RecordData::A {
1564 address: addr.0.to_string(),
1565 }),
1566 (RecordType::AAAA, HickoryRData::AAAA(addr)) => Some(RecordData::AAAA {
1567 address: addr.0.to_string(),
1568 }),
1569 (RecordType::CNAME, HickoryRData::CNAME(cname)) => Some(RecordData::CNAME {
1570 target: names.spell(&cname.0),
1571 }),
1572 (RecordType::MX, HickoryRData::MX(mx)) => Some(RecordData::MX {
1573 preference: mx.preference,
1574 exchange: names.spell(&mx.exchange),
1575 }),
1576 (RecordType::NS, HickoryRData::NS(ns)) => Some(RecordData::NS {
1577 nameserver: names.spell(&ns.0),
1578 }),
1579 (RecordType::TXT, HickoryRData::TXT(txt)) => Some(RecordData::txt(
1580 txt.txt_data
1581 .iter()
1582 .map(|data| String::from_utf8_lossy(data).into_owned())
1583 .collect(),
1584 )),
1585 (RecordType::SOA, HickoryRData::SOA(soa)) => Some(RecordData::SOA {
1586 mname: names.spell(&soa.mname),
1587 rname: names.spell(&soa.rname),
1588 serial: soa.serial,
1589 refresh: soa.refresh as u32,
1595 retry: soa.retry as u32,
1596 expire: soa.expire as u32,
1597 minimum: soa.minimum,
1598 }),
1599 (RecordType::PTR, HickoryRData::PTR(ptr)) => Some(RecordData::PTR {
1600 target: names.spell(&ptr.0),
1601 }),
1602 (RecordType::SRV, HickoryRData::SRV(srv)) => Some(RecordData::SRV {
1603 priority: srv.priority,
1604 weight: srv.weight,
1605 port: srv.port,
1606 target: names.spell(&srv.target),
1607 }),
1608 (RecordType::CAA, HickoryRData::CAA(caa)) => {
1609 let (flags, tag, value) = parse_caa(caa);
1610 Some(RecordData::CAA { flags, tag, value })
1611 }
1612 (RecordType::DNSKEY, HickoryRData::DNSSEC(DNSSECRData::DNSKEY(dnskey))) => {
1613 use base64::{engine::general_purpose::STANDARD, Engine};
1614 let public_key_buf = dnskey.public_key();
1615 Some(RecordData::DNSKEY {
1616 flags: dnskey.flags(),
1617 protocol: 3,
1619 algorithm: u8::from(public_key_buf.algorithm()),
1620 public_key: STANDARD.encode(public_key_buf.public_bytes()),
1621 })
1622 }
1623 (RecordType::DS, HickoryRData::DNSSEC(DNSSECRData::DS(ds))) => Some(RecordData::DS {
1624 key_tag: ds.key_tag(),
1625 algorithm: u8::from(ds.algorithm()),
1626 digest_type: u8::from(ds.digest_type()),
1627 digest: hex_upper(ds.digest()),
1628 }),
1629 (RecordType::CDS, HickoryRData::DNSSEC(DNSSECRData::CDS(cds))) => Some(RecordData::CDS {
1631 key_tag: cds.key_tag(),
1632 algorithm: cds.algorithm().map_or(0, u8::from),
1633 digest_type: u8::from(cds.digest_type()),
1634 digest: hex_upper(cds.digest()),
1635 }),
1636 (RecordType::CDNSKEY, HickoryRData::DNSSEC(DNSSECRData::CDNSKEY(cdnskey))) => {
1637 use base64::{engine::general_purpose::STANDARD, Engine};
1638 let rdata = cdnskey.to_bytes().ok()?;
1645 Some(RecordData::CDNSKEY {
1646 flags: cdnskey.flags(),
1647 protocol: 3,
1649 algorithm: cdnskey.algorithm().map_or(0, u8::from),
1650 public_key: STANDARD.encode(rdata.get(4..)?),
1651 })
1652 }
1653 (RecordType::HTTPS, HickoryRData::HTTPS(https)) => {
1654 let (priority, target, params) = parse_svcb(&https.0, names);
1655 Some(RecordData::HTTPS {
1656 priority,
1657 target,
1658 params,
1659 })
1660 }
1661 (RecordType::SVCB, HickoryRData::SVCB(svcb)) => {
1662 let (priority, target, params) = parse_svcb(svcb, names);
1663 Some(RecordData::SVCB {
1664 priority,
1665 target,
1666 params,
1667 })
1668 }
1669 (RecordType::TLSA, HickoryRData::TLSA(tlsa)) => Some(RecordData::TLSA {
1670 cert_usage: u8::from(tlsa.cert_usage),
1671 selector: u8::from(tlsa.selector),
1672 matching: u8::from(tlsa.matching),
1673 cert_data: hex_upper(&tlsa.cert_data),
1674 }),
1675 (RecordType::SSHFP, HickoryRData::SSHFP(sshfp)) => Some(RecordData::SSHFP {
1676 algorithm: u8::from(sshfp.algorithm),
1677 fingerprint_type: u8::from(sshfp.fingerprint_type),
1678 fingerprint: hex_upper(&sshfp.fingerprint),
1679 }),
1680 (RecordType::NAPTR, HickoryRData::NAPTR(naptr)) => Some(RecordData::NAPTR {
1684 order: naptr.order,
1685 preference: naptr.preference,
1686 flags: String::from_utf8_lossy(&naptr.flags).into_owned(),
1687 services: String::from_utf8_lossy(&naptr.services).into_owned(),
1688 regexp: String::from_utf8_lossy(&naptr.regexp).into_owned(),
1689 replacement: names.spell(&naptr.replacement),
1690 }),
1691 _ => None,
1692 }
1693}
1694
1695pub(crate) fn from_wire_type(wire: HickoryRecordType) -> Option<RecordType> {
1700 RecordType::ALL
1701 .iter()
1702 .copied()
1703 .find(|t| wire_type(*t) == Some(wire))
1704}
1705
1706pub(crate) fn to_dns_record(record: &Record) -> Option<DnsRecord> {
1716 let record_type = from_wire_type(record.record_type())?;
1717 let data = convert_rdata(record_type, &record.data, NameSpelling::Ascii)?;
1718 Some(DnsRecord {
1719 name: owner_name(&record.name),
1720 record_type,
1721 ttl: record.ttl,
1722 data,
1723 })
1724}
1725
1726fn owner_name(name: &Name) -> String {
1731 let text = name.to_ascii();
1732 match text.strip_suffix('.') {
1733 Some(rest) if !rest.is_empty() => rest.to_string(),
1734 _ => text,
1735 }
1736}
1737
1738fn is_valid_srv_label(label: &str) -> bool {
1740 !label.is_empty()
1741 && label.len() <= 63
1742 && label.chars().all(|c| c.is_ascii_alphanumeric() || c == '-')
1743 && !label.starts_with('-')
1744 && !label.ends_with('-')
1745}
1746
1747#[cfg(test)]
1748mod tests {
1749 use super::*;
1757
1758 #[test]
1773 fn standard_opts_pin_server_order_so_ipv4_is_tried_first() {
1774 let mut opts = ResolverOpts::default();
1775 apply_standard_opts(&mut opts, Duration::from_secs(5));
1776 assert_eq!(
1777 opts.server_ordering_strategy,
1778 ServerOrderingStrategy::UserProvidedOrder,
1779 "QueryStatistics ordering is nondeterministic in a short-lived \
1780 process and can draw only black-holed IPv6 servers"
1781 );
1782 }
1783
1784 #[test]
1789 fn default_config_lists_enough_ipv4_servers_before_any_ipv6() {
1790 let config = ResolverConfig::udp_and_tcp(&GOOGLE);
1791 let ips: Vec<_> = config.name_servers().iter().map(|ns| ns.ip).collect();
1792 let concurrent = ResolverOpts::default().num_concurrent_reqs.max(1);
1793 assert!(
1794 ips.len() > concurrent,
1795 "expected more servers than the concurrency window: {ips:?}"
1796 );
1797 for (i, ip) in ips.iter().take(concurrent).enumerate() {
1798 assert!(
1799 ip.is_ipv4(),
1800 "server {i} in the concurrency window is not IPv4: {ips:?}"
1801 );
1802 }
1803 }
1804
1805 #[test]
1806 fn from_config_applies_dns_timeout() {
1807 let mut config = crate::config::SeerConfig::default();
1808 config.timeouts.dns_secs = 9;
1809 let resolver = DnsResolver::from_config(&config);
1810 assert_eq!(resolver.timeout, Duration::from_secs(9));
1811 assert_eq!(resolver.transport.timeout(), Duration::from_secs(9));
1812 let rebuilt = DnsResolver::new().with_timeout(Duration::from_secs(3));
1813 assert_eq!(rebuilt.transport.timeout(), Duration::from_secs(3));
1814 }
1815
1816 #[test]
1817 fn transport_budget_is_the_resolvers_attempts() {
1818 let mut opts = ResolverOpts::default();
1819 apply_standard_opts(&mut opts, Duration::from_secs(5));
1820 assert_eq!(
1821 opts.attempts as u32,
1822 crate::dns::transport::QUERY_BUDGET_TIMEOUTS
1823 );
1824 }
1825 use std::net::{Ipv4Addr, Ipv6Addr};
1826
1827 #[test]
1830 fn srv_label_accepts_alphanumeric_and_hyphen() {
1831 assert!(is_valid_srv_label("http"));
1832 assert!(is_valid_srv_label("ldap-tls"));
1833 assert!(is_valid_srv_label("a1"));
1834 assert!(is_valid_srv_label("tcp"));
1835 }
1836
1837 #[test]
1838 fn srv_label_rejects_empty() {
1839 assert!(!is_valid_srv_label(""));
1840 }
1841
1842 #[test]
1843 fn srv_label_rejects_leading_or_trailing_hyphen() {
1844 assert!(!is_valid_srv_label("-http"));
1845 assert!(!is_valid_srv_label("http-"));
1846 assert!(!is_valid_srv_label("-"));
1847 }
1848
1849 #[test]
1850 fn srv_label_rejects_dots() {
1851 assert!(!is_valid_srv_label("http.evil"));
1854 assert!(!is_valid_srv_label("a.b"));
1855 }
1856
1857 #[test]
1858 fn srv_label_rejects_special_chars() {
1859 assert!(!is_valid_srv_label("http evil"));
1860 assert!(!is_valid_srv_label("http/evil"));
1861 assert!(!is_valid_srv_label("http\0"));
1862 assert!(!is_valid_srv_label("http\n"));
1863 }
1864
1865 #[test]
1866 fn srv_label_rejects_over_63_chars() {
1867 let too_long = "a".repeat(64);
1868 assert!(!is_valid_srv_label(&too_long));
1869 let exactly_63 = "a".repeat(63);
1870 assert!(is_valid_srv_label(&exactly_63));
1871 }
1872
1873 #[test]
1876 fn classify_ns_presence_absent_on_empty_ok() {
1877 let r: Result<Vec<DnsRecord>> = Ok(vec![]);
1879 assert_eq!(classify_ns_presence(&r), DnsPresence::Absent);
1880 }
1881
1882 #[test]
1883 fn classify_ns_presence_present_on_records() {
1884 let rec = DnsRecord {
1885 name: "example.test.".to_string(),
1886 record_type: RecordType::NS,
1887 ttl: 3600,
1888 data: RecordData::NS {
1889 nameserver: "ns1.example.net.".to_string(),
1890 },
1891 };
1892 let r: Result<Vec<DnsRecord>> = Ok(vec![rec]);
1893 assert_eq!(classify_ns_presence(&r), DnsPresence::Present);
1894 }
1895
1896 #[test]
1897 fn classify_ns_presence_unknown_on_error() {
1898 let r: Result<Vec<DnsRecord>> = Err(SeerError::DnsError("servfail".to_string()));
1899 assert_eq!(classify_ns_presence(&r), DnsPresence::Unknown);
1900 }
1901
1902 #[test]
1905 fn reverse_dns_name_formats_ipv4_correctly() {
1906 let ip: IpAddr = Ipv4Addr::new(192, 0, 2, 1).into();
1907 assert_eq!(reverse_dns_name(&ip), "1.2.0.192.in-addr.arpa");
1908 }
1909
1910 #[test]
1911 fn reverse_dns_name_formats_ipv6_correctly() {
1912 let ip: IpAddr = Ipv6Addr::LOCALHOST.into();
1914 let name = reverse_dns_name(&ip);
1915 assert!(
1916 name.ends_with(".ip6.arpa"),
1917 "must end with .ip6.arpa; got: {}",
1918 name
1919 );
1920 assert!(
1922 name.starts_with("1."),
1923 "expected '1.' prefix, got: {}",
1924 name
1925 );
1926 assert_eq!(name.len(), 72);
1928 }
1929
1930 #[tokio::test]
1933 async fn custom_resolver_rejects_invalid_input() {
1934 let r = DnsResolver::new();
1939 let err = r.create_custom_resolver("..").await.unwrap_err();
1940 let msg = err.to_string().to_lowercase();
1941 assert!(
1942 msg.contains("dns resolution failed") || msg.contains("invalid"),
1943 "expected resolution failure, got: {}",
1944 msg
1945 );
1946 }
1947
1948 #[tokio::test]
1949 async fn custom_resolver_rejects_private_ipv4() {
1950 let r = DnsResolver::new();
1953 for reserved in ["127.0.0.1", "10.0.0.1", "192.168.1.1", "169.254.169.254"] {
1954 let err = r.create_custom_resolver(reserved).await.unwrap_err();
1955 let msg = err.to_string().to_lowercase();
1956 assert!(
1957 msg.contains("blocked") || msg.contains("reserved"),
1958 "reserved IP {} must be rejected, got error: {}",
1959 reserved,
1960 msg
1961 );
1962 }
1963 }
1964
1965 #[tokio::test]
1966 async fn custom_resolver_rejects_loopback_ipv6() {
1967 let r = DnsResolver::new();
1968 let err = r.create_custom_resolver("::1").await.unwrap_err();
1969 let msg = err.to_string().to_lowercase();
1970 assert!(
1971 msg.contains("blocked") || msg.contains("reserved"),
1972 "::1 must be rejected, got error: {}",
1973 msg
1974 );
1975 }
1976
1977 #[tokio::test]
1978 async fn custom_resolver_accepts_public_ipv4() {
1979 let r = DnsResolver::new();
1981 let result = r.create_custom_resolver("8.8.8.8").await;
1982 assert!(
1983 result.is_ok(),
1984 "8.8.8.8 must be accepted as a public nameserver, got: {:?}",
1985 result.err()
1986 );
1987 }
1988
1989 #[tokio::test]
1995 async fn custom_resolver_rejects_private_ip_for_dot_and_doh() {
1996 let r = DnsResolver::new();
1997 for reserved in [
1998 "tls://127.0.0.1",
1999 "tls://192.168.1.1:853",
2000 "tls://[::1]",
2001 "https://10.0.0.1/dns-query",
2002 "https://169.254.169.254",
2003 "tls://[fd00::1]:853",
2004 ] {
2005 let err = r.create_custom_resolver(reserved).await.unwrap_err();
2006 let msg = err.to_string().to_lowercase();
2007 assert!(
2008 msg.contains("blocked") || msg.contains("reserved"),
2009 "reserved spec {} must be rejected, got error: {}",
2010 reserved,
2011 msg
2012 );
2013 }
2014 let err = r
2017 .create_custom_resolver("https://[fd00::1]:443/dns-query")
2018 .await
2019 .unwrap_err();
2020 assert!(matches!(err, SeerError::InvalidInput(_)), "{err:?}");
2021 }
2022
2023 #[tokio::test]
2024 async fn custom_resolver_accepts_public_dot_and_doh_literals() {
2025 let r = DnsResolver::new();
2028 for spec in ["tls://1.1.1.1", "https://8.8.8.8/dns-query"] {
2029 let result = r.create_custom_resolver(spec).await;
2030 assert!(
2031 result.is_ok(),
2032 "{} must be accepted, got: {:?}",
2033 spec,
2034 result.err()
2035 );
2036 }
2037 }
2038
2039 #[tokio::test]
2040 async fn custom_resolver_rejects_unknown_scheme() {
2041 let r = DnsResolver::new();
2042 let err = r.create_custom_resolver("ftp://8.8.8.8").await.unwrap_err();
2043 assert!(
2044 matches!(err, SeerError::InvalidInput(_)),
2045 "unknown scheme must be an input error, got: {err:?}"
2046 );
2047 }
2048
2049 fn spec(s: &str) -> NameserverSpec {
2052 NameserverSpec::parse(s).unwrap_or_else(|e| panic!("{s:?} must parse: {e}"))
2053 }
2054
2055 #[test]
2056 fn tcp_only_swaps_udp_for_tcp_on_the_same_port() {
2057 let default = tcp_only(&default_upstream_config());
2058 assert!(!default.name_servers().is_empty());
2059 for server in default.name_servers() {
2060 assert_eq!(server.connections.len(), 1, "{server:?}");
2061 assert!(matches!(
2062 server.connections[0].protocol,
2063 ProtocolConfig::Tcp
2064 ));
2065 assert_eq!(server.connections[0].port, 53);
2066 }
2067
2068 let udp_only = tcp_only(&single_server_config(
2069 IpAddr::from([192, 0, 2, 1]),
2070 5353,
2071 false,
2072 ));
2073 let server = &udp_only.name_servers()[0];
2074 assert_eq!(server.connections.len(), 1);
2075 assert!(matches!(
2076 server.connections[0].protocol,
2077 ProtocolConfig::Tcp
2078 ));
2079 assert_eq!(server.connections[0].port, 5353);
2080 }
2081
2082 #[test]
2083 fn tcp_only_keeps_encrypted_transports() {
2084 let spec = NameserverSpec::parse("tls://1.1.1.1").unwrap();
2085 let config = build_upstream_config(&spec, &[IpAddr::from([1, 1, 1, 1])], None);
2086 let tcp = tcp_only(&config);
2087 assert_eq!(
2088 format!("{:?}", tcp.name_servers()),
2089 format!("{:?}", config.name_servers())
2090 );
2091 }
2092
2093 #[test]
2094 fn upstream_config_udp_defaults() {
2095 use hickory_resolver::config::ProtocolConfig;
2096
2097 let ip: IpAddr = "8.8.8.8".parse().unwrap();
2098 let config = build_upstream_config(&spec("8.8.8.8"), &[ip], None);
2099 let servers = config.name_servers();
2100 assert_eq!(servers.len(), 1);
2101 assert_eq!(servers[0].ip, ip);
2102 assert_eq!(servers[0].connections.len(), 1);
2103 assert_eq!(servers[0].connections[0].port, 53);
2104 assert!(matches!(
2105 servers[0].connections[0].protocol,
2106 ProtocolConfig::Udp
2107 ));
2108 }
2109
2110 #[test]
2111 fn upstream_config_udp_explicit_port() {
2112 let ip: IpAddr = "9.9.9.9".parse().unwrap();
2113 let config = build_upstream_config(&spec("9.9.9.9:5353"), &[ip], None);
2114 assert_eq!(config.name_servers()[0].connections[0].port, 5353);
2115 }
2116
2117 #[test]
2118 fn upstream_config_tls_sets_protocol_port_and_server_name() {
2119 use hickory_resolver::config::ProtocolConfig;
2120
2121 let ip: IpAddr = "9.9.9.9".parse().unwrap();
2122 let config = build_upstream_config(&spec("tls://dns.quad9.net"), &[ip], None);
2123 let ns = &config.name_servers()[0];
2124 assert_eq!(ns.ip, ip);
2125 assert_eq!(ns.connections.len(), 1);
2126 assert_eq!(ns.connections[0].port, 853);
2127 match &ns.connections[0].protocol {
2128 ProtocolConfig::Tls { server_name } => {
2129 assert_eq!(&**server_name, "dns.quad9.net");
2130 }
2131 other => panic!("expected Tls protocol, got {other:?}"),
2132 }
2133 }
2134
2135 #[test]
2136 fn upstream_config_tls_ip_literal_uses_ip_as_server_name() {
2137 use hickory_resolver::config::ProtocolConfig;
2138
2139 let ip: IpAddr = "1.1.1.1".parse().unwrap();
2140 let config = build_upstream_config(&spec("tls://1.1.1.1"), &[ip], None);
2141 match &config.name_servers()[0].connections[0].protocol {
2142 ProtocolConfig::Tls { server_name } => assert_eq!(&**server_name, "1.1.1.1"),
2143 other => panic!("expected Tls protocol, got {other:?}"),
2144 }
2145 }
2146
2147 #[test]
2148 fn upstream_config_https_sets_protocol_port_path_and_server_name() {
2149 use hickory_resolver::config::ProtocolConfig;
2150
2151 let ip: IpAddr = "104.16.248.249".parse().unwrap();
2152 let config = build_upstream_config(&spec("https://cloudflare-dns.com"), &[ip], None);
2153 let ns = &config.name_servers()[0];
2154 assert_eq!(ns.connections[0].port, 443);
2155 match &ns.connections[0].protocol {
2156 ProtocolConfig::Https { server_name, path } => {
2157 assert_eq!(&**server_name, "cloudflare-dns.com");
2158 assert_eq!(&**path, "/dns-query");
2159 }
2160 other => panic!("expected Https protocol, got {other:?}"),
2161 }
2162 }
2163
2164 #[test]
2165 fn upstream_config_https_custom_port_and_path() {
2166 use hickory_resolver::config::ProtocolConfig;
2167
2168 let ip: IpAddr = "8.8.8.8".parse().unwrap();
2169 let config = build_upstream_config(&spec("https://dns.google:8443/resolve"), &[ip], None);
2170 let ns = &config.name_servers()[0];
2171 assert_eq!(ns.connections[0].port, 8443);
2172 match &ns.connections[0].protocol {
2173 ProtocolConfig::Https { server_name, path } => {
2174 assert_eq!(&**server_name, "dns.google");
2175 assert_eq!(&**path, "/resolve");
2176 }
2177 other => panic!("expected Https protocol, got {other:?}"),
2178 }
2179 }
2180
2181 #[test]
2182 fn upstream_config_multiple_ips_share_spec() {
2183 use hickory_resolver::config::ProtocolConfig;
2186
2187 let ips: Vec<IpAddr> = vec![
2188 "9.9.9.9".parse().unwrap(),
2189 "149.112.112.112".parse().unwrap(),
2190 ];
2191 let config = build_upstream_config(&spec("tls://dns.quad9.net"), &ips, None);
2192 let servers = config.name_servers();
2193 assert_eq!(servers.len(), 2);
2194 for (ns, expected_ip) in servers.iter().zip(&ips) {
2195 assert_eq!(&ns.ip, expected_ip);
2196 assert_eq!(ns.connections[0].port, 853);
2197 assert!(matches!(
2198 &ns.connections[0].protocol,
2199 ProtocolConfig::Tls { server_name } if &**server_name == "dns.quad9.net"
2200 ));
2201 }
2202 }
2203
2204 #[test]
2212 fn upstream_config_orders_ipv4_before_ipv6() {
2213 let ips: Vec<IpAddr> = [
2215 "2606:4700::6810:f8f9",
2216 "2606:4700::6810:f9f9",
2217 "104.16.248.249",
2218 "104.16.249.249",
2219 ]
2220 .iter()
2221 .map(|s| s.parse().unwrap())
2222 .collect();
2223 let config =
2224 build_upstream_config(&spec("https://cloudflare-dns.com/dns-query"), &ips, None);
2225 let order: Vec<IpAddr> = config.name_servers().iter().map(|ns| ns.ip).collect();
2226 assert_eq!(
2227 order,
2228 [ips[2], ips[3], ips[0], ips[1]],
2229 "IPv4 first, each family in resolved order, nothing dropped"
2230 );
2231 let concurrent = ResolverOpts::default().num_concurrent_reqs.max(1);
2232 assert!(
2233 order.iter().take(concurrent).all(IpAddr::is_ipv4),
2234 "the parallel window must be all IPv4: {order:?}"
2235 );
2236 }
2237
2238 #[test]
2239 fn upstream_config_test_port_override_wins() {
2240 let ip: IpAddr = "127.0.0.1".parse().unwrap();
2242 let config = build_upstream_config(&spec("127.0.0.1"), &[ip], Some(9999));
2243 assert_eq!(config.name_servers()[0].connections[0].port, 9999);
2244 }
2245
2246 #[tokio::test]
2247 async fn query_asks_dot_and_doh_servers_through_the_vetted_config() {
2248 use hickory_resolver::config::ProtocolConfig;
2254
2255 let r = DnsResolver::new();
2256 let dot = r.custom_upstream_config("tls://1.1.1.1").await.unwrap();
2257 let [server] = dot.name_servers() else {
2258 panic!("one server per address: {dot:?}");
2259 };
2260 assert_eq!(server.ip, IpAddr::from([1, 1, 1, 1]));
2261 assert_eq!(server.connections.len(), 1);
2262 assert_eq!(server.connections[0].port, 853);
2263 assert!(matches!(
2264 &server.connections[0].protocol,
2265 ProtocolConfig::Tls { server_name } if &**server_name == "1.1.1.1"
2266 ));
2267
2268 let doh = r
2269 .custom_upstream_config("https://8.8.8.8/dns-query")
2270 .await
2271 .unwrap();
2272 let [server] = doh.name_servers() else {
2273 panic!("one server per address: {doh:?}");
2274 };
2275 assert_eq!(server.connections[0].port, 443);
2276 assert!(matches!(
2277 &server.connections[0].protocol,
2278 ProtocolConfig::Https { server_name, path }
2279 if &**server_name == "8.8.8.8" && &**path == "/dns-query"
2280 ));
2281
2282 let udp = r.custom_upstream_config("9.9.9.9").await.unwrap();
2285 assert_eq!(udp.name_servers()[0].connections[0].port, 53);
2286 assert!(matches!(
2287 udp.name_servers()[0].connections[0].protocol,
2288 ProtocolConfig::Udp
2289 ));
2290 let Upstream::Direct { config, .. } = r.query_upstream("seer.test", None).await.unwrap()
2292 else {
2293 panic!("a public name is sent to the default upstream");
2294 };
2295 assert!(config.name_servers()[0].ip.is_ipv4());
2296 }
2297
2298 #[tokio::test]
2301 #[ignore = "live network — DoT query against Cloudflare"]
2302 async fn live_query_over_dot() {
2303 let result = DnsResolver::new()
2304 .query("example.com", RecordType::A, Some("tls://1.1.1.1"))
2305 .await
2306 .expect("DoT query should get a response");
2307 assert_eq!(result.status, DnsStatus::NoError);
2308 assert!(result.flags.contains(&"qr".to_string()), "{result:?}");
2309 assert!(result.records().count() > 0, "expected A records over DoT");
2310 }
2311
2312 #[tokio::test]
2313 #[ignore = "live network — DoH query against Cloudflare"]
2314 async fn live_query_over_doh() {
2315 let result = DnsResolver::new()
2316 .query(
2317 "example.com",
2318 RecordType::A,
2319 Some("https://cloudflare-dns.com/dns-query"),
2320 )
2321 .await
2322 .expect("DoH query should get a response");
2323 assert_eq!(result.status, DnsStatus::NoError);
2324 assert!(result.records().count() > 0, "expected A records over DoH");
2325 }
2326
2327 #[tokio::test]
2328 #[ignore = "live network — NXDOMAIN keeps its header"]
2329 async fn live_query_nxdomain_has_flags_and_the_soa() {
2330 let result = DnsResolver::new()
2331 .query("seer-does-not-exist.example", RecordType::A, None)
2333 .await
2334 .expect("a negative answer is a result");
2335 assert_eq!(result.status, DnsStatus::NxDomain);
2336 assert!(result.flags.contains(&"qr".to_string()), "{result:?}");
2337 assert!(
2338 result
2339 .authority
2340 .iter()
2341 .any(|r| r.record_type == RecordType::SOA),
2342 "{result:?}"
2343 );
2344 }
2345
2346 #[tokio::test]
2347 #[ignore = "live network — DoT query against Cloudflare"]
2348 async fn live_resolve_over_dot() {
2349 let r = DnsResolver::new();
2350 let records = r
2351 .resolve("example.com", RecordType::A, Some("tls://1.1.1.1"))
2352 .await
2353 .expect("DoT lookup should succeed");
2354 assert!(!records.is_empty(), "expected A records over DoT");
2355 }
2356
2357 #[tokio::test]
2358 #[ignore = "live network — DoH query against Cloudflare"]
2359 async fn live_resolve_over_doh() {
2360 let r = DnsResolver::new();
2361 let records = r
2362 .resolve(
2363 "example.com",
2364 RecordType::A,
2365 Some("https://cloudflare-dns.com/dns-query"),
2366 )
2367 .await
2368 .expect("DoH lookup should succeed");
2369 assert!(!records.is_empty(), "expected A records over DoH");
2370 }
2371
2372 #[tokio::test]
2375 async fn resolve_srv_rejects_invalid_service_label() {
2376 let r = DnsResolver::new();
2377 let result = r
2379 .resolve_srv_core(&r.default_target(), "http.evil", "tcp", "example.com")
2380 .await;
2381 assert!(result.is_err());
2382 let msg = result.unwrap_err().to_string().to_lowercase();
2383 assert!(
2384 msg.contains("invalid srv service"),
2385 "expected SRV service validation error, got: {}",
2386 msg
2387 );
2388 }
2389
2390 #[tokio::test]
2391 async fn resolve_srv_rejects_invalid_protocol_label() {
2392 let r = DnsResolver::new();
2393 let result = r
2394 .resolve_srv_core(&r.default_target(), "http", "tcp.evil", "example.com")
2395 .await;
2396 assert!(result.is_err());
2397 let msg = result.unwrap_err().to_string().to_lowercase();
2398 assert!(
2399 msg.contains("invalid srv protocol"),
2400 "expected SRV protocol validation error, got: {}",
2401 msg
2402 );
2403 }
2404
2405 #[tokio::test]
2406 async fn resolve_srv_normalizes_and_validates_domain_input() {
2407 let r = DnsResolver::new();
2411 let result = r
2412 .resolve("_http._tcp.not a valid domain", RecordType::SRV, None)
2413 .await;
2414 assert!(
2415 matches!(result, Err(SeerError::InvalidDomain(_))),
2416 "expected InvalidDomain from domain validation, got: {result:?}"
2417 );
2418 }
2419
2420 #[tokio::test]
2423 async fn resolve_normalizes_uppercase_domain_input() {
2424 let r = DnsResolver::new();
2429 let result = r.resolve(".bad.example", RecordType::A, None).await;
2430 assert!(result.is_err(), "leading-dot domain must be rejected");
2431 }
2432
2433 #[test]
2438 fn parse_srv_query_extracts_service_proto_and_name() {
2439 assert_eq!(
2440 parse_srv_query("_sip._tcp.example.com"),
2441 Some((
2442 "sip".to_string(),
2443 "tcp".to_string(),
2444 "example.com".to_string()
2445 ))
2446 );
2447 }
2448
2449 #[test]
2450 fn parse_srv_query_keeps_multilabel_domain() {
2451 assert_eq!(
2452 parse_srv_query("_sip._tcp.sip.voice.google.com"),
2453 Some((
2454 "sip".to_string(),
2455 "tcp".to_string(),
2456 "sip.voice.google.com".to_string()
2457 ))
2458 );
2459 }
2460
2461 #[test]
2462 fn fqdn_appends_root_dot_once() {
2463 assert_eq!(fqdn("example.com"), "example.com.");
2464 assert_eq!(fqdn("example.com."), "example.com.");
2465 }
2466
2467 #[test]
2468 fn parse_srv_query_rejects_bare_domain() {
2469 assert_eq!(parse_srv_query("example.com"), None);
2470 }
2471
2472 #[test]
2473 fn parse_srv_query_rejects_missing_proto_label() {
2474 assert_eq!(parse_srv_query("_sip.example.com"), None);
2476 }
2477
2478 #[tokio::test]
2479 async fn resolve_rejects_bare_domain_for_srv_as_input_error() {
2480 let r = DnsResolver::new();
2485 let err = r
2486 .resolve("example.com", RecordType::SRV, None)
2487 .await
2488 .expect_err("bare-domain SRV must error");
2489 assert!(
2490 matches!(err, SeerError::InvalidInput(_)),
2491 "bare-domain SRV should be an input error, got: {err:?}"
2492 );
2493 assert!(err.to_string().contains("_service._proto"));
2494 }
2495
2496 #[tokio::test]
2497 #[ignore = "live network"]
2498 async fn resolve_srv_via_dig_style_name_returns_records() {
2499 let r = DnsResolver::new();
2502 let records = r
2503 .resolve("_caldavs._tcp.google.com", RecordType::SRV, None)
2504 .await
2505 .expect("dig-style SRV lookup should succeed");
2506 assert!(!records.is_empty(), "expected SRV records");
2507 assert!(records.iter().all(|r| r.record_type == RecordType::SRV));
2508 }
2509
2510 #[tokio::test]
2511 #[ignore = "live network"]
2512 async fn resolve_naptr_returns_records() {
2513 let r = DnsResolver::new();
2515 let records = r
2516 .resolve("sip2sip.info", RecordType::NAPTR, None)
2517 .await
2518 .expect("NAPTR lookup should succeed");
2519 assert!(!records.is_empty(), "expected NAPTR records");
2520 assert!(records.iter().all(|r| r.record_type == RecordType::NAPTR));
2521 }
2522
2523 #[test]
2526 fn prepare_query_passes_ipv6_literal_through_for_ptr() {
2527 let out = prepare_query("2606:4700:4700::1111", RecordType::PTR).unwrap();
2533 assert_eq!(out, "2606:4700:4700::1111");
2534 }
2535
2536 #[test]
2537 fn prepare_query_passes_ipv6_loopback_through_for_ptr() {
2538 let out = prepare_query("::1", RecordType::PTR).unwrap();
2539 assert_eq!(out, "::1");
2540 }
2541
2542 #[test]
2543 fn prepare_query_passes_ipv4_literal_through_for_ptr() {
2544 let out = prepare_query("8.8.8.8", RecordType::PTR).unwrap();
2545 assert_eq!(out, "8.8.8.8");
2546 }
2547
2548 #[test]
2549 fn prepare_query_normalizes_non_ip_ptr_names() {
2550 let out = prepare_query("1.1.1.1.in-addr.arpa", RecordType::PTR).unwrap();
2552 assert_eq!(out, "1.1.1.1.in-addr.arpa");
2553 }
2554
2555 #[test]
2556 fn prepare_query_keeps_www_for_record_queries() {
2557 let out = prepare_query("HTTPS://WWW.Example.com/path", RecordType::A).unwrap();
2561 assert_eq!(out, "www.example.com");
2562 let out = prepare_query("www.example.com", RecordType::CNAME).unwrap();
2563 assert_eq!(out, "www.example.com");
2564 }
2565
2566 #[test]
2567 fn prepare_query_still_normalizes_scheme_port_path_and_case() {
2568 let out = prepare_query("https://API.Example.COM:8443/v1?q=1#frag", RecordType::A).unwrap();
2569 assert_eq!(out, "api.example.com");
2570 let out = prepare_query("example.com.", RecordType::MX).unwrap();
2571 assert_eq!(out, "example.com");
2572 assert!(prepare_query("not a domain", RecordType::A).is_err());
2573 }
2574
2575 #[test]
2576 fn prepare_query_accepts_a_leading_wildcard_label() {
2577 let out = prepare_query("*.Example.com.", RecordType::A).unwrap();
2581 assert_eq!(out, "*.example.com");
2582 let out = prepare_query("*.1.168.192.in-addr.arpa", RecordType::PTR).unwrap();
2583 assert_eq!(out, "*.1.168.192.in-addr.arpa");
2584 assert!(prepare_query("a.*.example.com", RecordType::A).is_err());
2586 assert!(prepare_query("a*.example.com", RecordType::A).is_err());
2587 }
2588
2589 #[test]
2590 fn prepare_query_runs_ptr_ip_literal_through_the_normalizer() {
2591 let deny_arpa = |name: &str| -> Result<String> {
2597 if name.ends_with(".arpa") {
2598 Err(SeerError::DomainNotAllowed {
2599 domain: name.to_string(),
2600 tld: "arpa".to_string(),
2601 })
2602 } else {
2603 Ok(name.to_string())
2604 }
2605 };
2606 for ip in ["192.0.2.1", "2606:4700:4700::1111"] {
2607 let err = prepare_query_with(ip, RecordType::PTR, deny_arpa)
2608 .expect_err("PTR for an IP literal must obey the normalizer");
2609 assert!(matches!(err, SeerError::DomainNotAllowed { .. }), "{err:?}");
2610 }
2611 assert_eq!(
2613 prepare_query_with("192.0.2.1", RecordType::PTR, normalize_query_name).unwrap(),
2614 "192.0.2.1"
2615 );
2616 }
2617
2618 #[test]
2619 fn from_wire_type_inverts_wire_type_for_every_single_type() {
2620 for record_type in RecordType::ALL.iter().copied() {
2621 match wire_type(record_type) {
2622 Some(wire) => assert_eq!(from_wire_type(wire), Some(record_type)),
2623 None => assert_eq!(record_type, RecordType::ANY),
2624 }
2625 }
2626 assert_eq!(
2629 from_wire_type(HickoryRecordType::SRV),
2630 Some(RecordType::SRV)
2631 );
2632 assert_eq!(from_wire_type(HickoryRecordType::RRSIG), None);
2633 }
2634
2635 #[test]
2636 fn to_dns_record_keeps_the_real_owner_and_type() {
2637 let cname = Record::from_rdata(
2638 Name::from_ascii("www.seer.test.").unwrap(),
2639 300,
2640 HickoryRData::CNAME(hickory_resolver::proto::rr::rdata::CNAME(
2641 Name::from_ascii("edge.cdn.test.").unwrap(),
2642 )),
2643 );
2644 let converted = to_dns_record(&cname).expect("CNAME is modeled");
2645 assert_eq!(converted.name, "www.seer.test");
2646 assert_eq!(converted.record_type, RecordType::CNAME);
2647 assert_eq!(converted.ttl, 300);
2648 assert_eq!(converted.data.to_string(), "edge.cdn.test.");
2649
2650 let a = Record::from_rdata(
2651 Name::from_ascii("edge.cdn.test.").unwrap(),
2652 60,
2653 HickoryRData::A(hickory_resolver::proto::rr::rdata::A(Ipv4Addr::new(
2654 192, 0, 2, 7,
2655 ))),
2656 );
2657 let converted = to_dns_record(&a).expect("A is modeled");
2658 assert_eq!(converted.name, "edge.cdn.test");
2659 assert_eq!(converted.record_type, RecordType::A);
2660
2661 assert_eq!(owner_name(&Name::root()), ".");
2662 assert_eq!(
2664 owner_name(&Name::from_utf8("bücher.seer.test.").unwrap()),
2665 "xn--bcher-kva.seer.test"
2666 );
2667 }
2668
2669 #[test]
2670 fn to_dns_record_spells_rdata_names_as_a_labels_where_resolve_keeps_unicode() {
2671 use hickory_resolver::proto::rr::rdata::{CNAME, MX, SOA, SRV};
2672 let idn = |name: &str| Name::from_utf8(name).unwrap();
2673 let data = |rdata: HickoryRData| {
2674 let record = Record::from_rdata(idn("xn--bcher-kva.seer.test."), 300, rdata);
2675 to_dns_record(&record).expect("modeled").data.to_string()
2676 };
2677 assert_eq!(
2681 data(HickoryRData::CNAME(CNAME(idn("edge.café.test.")))),
2682 "edge.xn--caf-dma.test."
2683 );
2684 assert_eq!(
2685 data(HickoryRData::MX(MX::new(10, idn("mail.café.test.")))),
2686 "10 mail.xn--caf-dma.test."
2687 );
2688 assert_eq!(
2689 data(HickoryRData::SRV(SRV::new(
2690 1,
2691 5,
2692 443,
2693 idn("sip.café.test.")
2694 ))),
2695 "1 5 443 sip.xn--caf-dma.test."
2696 );
2697 assert_eq!(
2698 data(HickoryRData::SOA(SOA::new(
2699 idn("ns1.café.test."),
2700 idn("hostmaster.café.test."),
2701 1,
2702 7200,
2703 3600,
2704 1209600,
2705 300,
2706 ))),
2707 "ns1.xn--caf-dma.test. hostmaster.xn--caf-dma.test. 1 7200 3600 1209600 300"
2708 );
2709
2710 let cname = HickoryRData::CNAME(CNAME(idn("edge.café.test.")));
2712 assert_eq!(
2713 convert_rdata(RecordType::CNAME, &cname, NameSpelling::Display)
2714 .expect("modeled")
2715 .to_string(),
2716 "edge.café.test."
2717 );
2718 }
2719
2720 #[test]
2721 fn parse_caa_keeps_reserved_flag_bits() {
2722 let mut caa = CAA::new_issue(
2725 true,
2726 Some(Name::from_ascii("letsencrypt.org").unwrap()),
2727 vec![],
2728 );
2729 caa.reserved_flags = 0x01;
2730 let (flags, tag, value) = parse_caa(&caa);
2731 assert_eq!(flags, 0x81);
2732 assert_eq!(tag, "issue");
2733 assert_eq!(value, "letsencrypt.org");
2734 }
2735
2736 #[test]
2737 fn alpn_ids_are_escaped_at_both_levels() {
2738 use hickory_resolver::proto::rr::rdata::svcb::Alpn;
2739 let alpn = |ids: &[&str]| {
2740 svc_param_value(&SvcParamValue::Alpn(Alpn(
2741 ids.iter().map(|id| id.to_string()).collect(),
2742 )))
2743 };
2744 assert_eq!(alpn(&["h3", "h2"]), "h3,h2");
2745 assert_eq!(alpn(&["a,b"]), r"a\\,b");
2750 assert_eq!(alpn(&[r"x\y"]), r"x\\\\y");
2751 assert_eq!(alpn(&[r"f\oo,bar", "h2"]), r"f\\\\oo\\,bar,h2");
2753 }
2754
2755 use crate::dns::query::DnsStatus;
2766 use crate::dns::test_support::{
2767 a_rdata, cname_rdata, mock_dns_resolver, mock_dns_resolver_default, record, spawn_mock_dns,
2768 spawn_mock_dns_fn, spawn_mock_dns_fn_with_tcp, spawn_mock_dns_split, MockMode, MockReply,
2769 };
2770 use hickory_resolver::proto::op::ResponseCode;
2771
2772 async fn mock_zone_lookup(record_type: RecordType, domain: &str) -> Vec<DnsRecord> {
2773 let port = spawn_mock_dns(MockMode::Zone).await;
2774 mock_dns_resolver(port)
2775 .resolve(domain, record_type, Some("127.0.0.1"))
2776 .await
2777 .unwrap_or_else(|e| panic!("{record_type} lookup against mock must succeed: {e}"))
2778 }
2779
2780 async fn counting_server() -> (u16, Arc<std::sync::atomic::AtomicUsize>) {
2783 use std::sync::atomic::{AtomicUsize, Ordering};
2784 let queries = Arc::new(AtomicUsize::new(0));
2785 let seen = queries.clone();
2786 let port = spawn_mock_dns_fn(move |_, _| {
2787 seen.fetch_add(1, Ordering::SeqCst);
2788 MockReply::Answer(vec![a_rdata([192, 0, 2, 1])])
2789 })
2790 .await;
2791 (port, queries)
2792 }
2793
2794 #[tokio::test]
2799 async fn a_custom_nameservers_resolver_is_reused() {
2800 use std::sync::atomic::Ordering;
2801 let (port, queries) = counting_server().await;
2802 let resolver = mock_dns_resolver(port);
2803 let ns = Some("127.0.0.1");
2804
2805 let clone = resolver.clone();
2807 let (first, second) = tokio::join!(
2808 resolver.resolve("seer.test", RecordType::A, ns),
2809 clone.resolve("seer.test", RecordType::A, ns),
2810 );
2811 assert_eq!(first.unwrap().len(), 1);
2812 assert_eq!(second.unwrap().len(), 1);
2813 for _ in 0..10 {
2814 resolver
2815 .resolve("seer.test", RecordType::A, ns)
2816 .await
2817 .unwrap();
2818 }
2819 assert_eq!(queries.load(Ordering::SeqCst), 1);
2820 }
2821
2822 #[tokio::test]
2824 async fn resolve_uncached_asks_the_server_every_time() {
2825 use std::sync::atomic::Ordering;
2826 let (port, queries) = counting_server().await;
2827 let resolver = mock_dns_resolver(port);
2828 resolver
2829 .resolve("seer.test", RecordType::A, Some("127.0.0.1"))
2830 .await
2831 .unwrap();
2832 for _ in 0..3 {
2833 resolver
2834 .resolve_uncached("seer.test", RecordType::A, Some("127.0.0.1"))
2835 .await
2836 .unwrap();
2837 }
2838 assert_eq!(queries.load(Ordering::SeqCst), 4);
2839 }
2840
2841 #[test]
2844 fn the_custom_resolver_memo_is_bounded() {
2845 let memo = CustomResolvers::default();
2846 let first = memo.slot("ns0");
2847 assert!(Arc::ptr_eq(&first, &memo.slot("ns0")));
2848 for i in 1..=MAX_CUSTOM_RESOLVERS {
2849 memo.slot(&format!("ns{i}"));
2850 }
2851 let slots = memo.slots.lock().unwrap();
2852 assert_eq!(slots.len(), MAX_CUSTOM_RESOLVERS);
2853 assert!(!slots.contains_key("ns0"), "the oldest is dropped");
2854 }
2855
2856 #[tokio::test]
2857 async fn mock_resolve_a_returns_addresses() {
2858 let records = mock_zone_lookup(RecordType::A, "seer.test").await;
2859 assert_eq!(records.len(), 2);
2860 assert!(records.iter().all(|r| r.record_type == RecordType::A));
2861 assert_eq!(records[0].name, "seer.test");
2862 assert_eq!(records[0].ttl, 300);
2863 let addresses: Vec<String> = records
2864 .iter()
2865 .map(|r| match &r.data {
2866 RecordData::A { address } => address.clone(),
2867 other => panic!("expected A data, got {other:?}"),
2868 })
2869 .collect();
2870 assert!(addresses.contains(&"192.0.2.1".to_string()));
2871 assert!(addresses.contains(&"192.0.2.2".to_string()));
2872 }
2873
2874 #[tokio::test]
2875 async fn mock_resolve_mx_sorts_by_preference() {
2876 let records = mock_zone_lookup(RecordType::MX, "seer.test").await;
2877 let prefs: Vec<u16> = records
2878 .iter()
2879 .map(|r| match &r.data {
2880 RecordData::MX { preference, .. } => *preference,
2881 other => panic!("expected MX data, got {other:?}"),
2882 })
2883 .collect();
2884 assert_eq!(prefs, vec![10, 20, 30]);
2886 assert!(matches!(
2887 &records[0].data,
2888 RecordData::MX { exchange, .. } if exchange == "a.mail.seer.test."
2889 ));
2890 }
2891
2892 #[tokio::test]
2893 async fn mock_resolve_txt_joins_character_strings() {
2894 let records = mock_zone_lookup(RecordType::TXT, "seer.test").await;
2895 assert_eq!(records.len(), 1);
2896 match &records[0].data {
2897 RecordData::TXT { text, strings } => {
2898 assert_eq!(text, "v=spf1 -all");
2899 assert_eq!(strings, &["v=spf1 ", "-all"], "the strings as sent");
2900 }
2901 other => panic!("expected TXT data, got {other:?}"),
2902 }
2903 assert_eq!(records[0].data.to_string(), r#""v=spf1 " "-all""#);
2904 }
2905
2906 #[tokio::test]
2907 async fn mock_resolve_soa_maps_all_fields() {
2908 let records = mock_zone_lookup(RecordType::SOA, "seer.test").await;
2909 assert_eq!(records.len(), 1);
2910 match &records[0].data {
2911 RecordData::SOA {
2912 mname,
2913 rname,
2914 serial,
2915 refresh,
2916 retry,
2917 expire,
2918 minimum,
2919 } => {
2920 assert_eq!(mname, "ns1.seer.test.");
2921 assert_eq!(rname, "hostmaster.seer.test.");
2922 assert_eq!(*serial, 2026070101);
2923 assert_eq!(*refresh, 7200);
2924 assert_eq!(*retry, 3600);
2925 assert_eq!(*expire, 1209600);
2926 assert_eq!(*minimum, 300);
2927 }
2928 other => panic!("expected SOA data, got {other:?}"),
2929 }
2930 }
2931
2932 #[tokio::test]
2933 async fn mock_resolve_caa_maps_flags_tag_and_value() {
2934 let records = mock_zone_lookup(RecordType::CAA, "seer.test").await;
2935 assert_eq!(records.len(), 2);
2936 let by_tag = |wanted: &str| {
2937 records
2938 .iter()
2939 .find_map(|r| match &r.data {
2940 RecordData::CAA { flags, tag, value } if tag == wanted => {
2941 Some((*flags, value.clone()))
2942 }
2943 _ => None,
2944 })
2945 .unwrap_or_else(|| panic!("expected a CAA record with tag {wanted}"))
2946 };
2947 assert_eq!(by_tag("issue"), (0, "letsencrypt.org".to_string()));
2949 assert_eq!(
2950 by_tag("iodef"),
2951 (128, "mailto:security@seer.test".to_string())
2952 );
2953 }
2954
2955 #[tokio::test]
2956 async fn mock_resolve_tlsa_hex_encodes_cert_data() {
2957 let records = mock_zone_lookup(RecordType::TLSA, "_443._tcp.seer.test").await;
2958 assert_eq!(records.len(), 1);
2959 match &records[0].data {
2960 RecordData::TLSA {
2961 cert_usage,
2962 selector,
2963 matching,
2964 cert_data,
2965 } => {
2966 assert_eq!((*cert_usage, *selector, *matching), (3, 1, 1));
2967 assert_eq!(cert_data, "ABCD01");
2968 }
2969 other => panic!("expected TLSA data, got {other:?}"),
2970 }
2971 }
2972
2973 #[tokio::test]
2974 async fn mock_resolve_sshfp_hex_encodes_fingerprint() {
2975 let records = mock_zone_lookup(RecordType::SSHFP, "seer.test").await;
2976 assert_eq!(records.len(), 1);
2977 match &records[0].data {
2978 RecordData::SSHFP {
2979 algorithm,
2980 fingerprint_type,
2981 fingerprint,
2982 } => {
2983 assert_eq!((*algorithm, *fingerprint_type), (4, 2));
2984 assert_eq!(fingerprint, "DEADBEEF");
2985 }
2986 other => panic!("expected SSHFP data, got {other:?}"),
2987 }
2988 }
2989
2990 #[tokio::test]
2991 async fn mock_resolve_naptr_decodes_character_strings() {
2992 let records = mock_zone_lookup(RecordType::NAPTR, "seer.test").await;
2993 assert_eq!(records.len(), 1);
2994 match &records[0].data {
2995 RecordData::NAPTR {
2996 order,
2997 preference,
2998 flags,
2999 services,
3000 regexp,
3001 replacement,
3002 } => {
3003 assert_eq!((*order, *preference), (100, 50));
3004 assert_eq!(flags, "U");
3005 assert_eq!(services, "E2U+sip");
3006 assert_eq!(regexp, "!^.*$!sip:info@seer.test!");
3007 assert_eq!(replacement, ".");
3008 }
3009 other => panic!("expected NAPTR data, got {other:?}"),
3010 }
3011 }
3012
3013 #[tokio::test]
3014 async fn mock_resolve_srv_via_dig_style_name() {
3015 let records = mock_zone_lookup(RecordType::SRV, "_sip._tcp.seer.test").await;
3016 assert_eq!(records.len(), 1);
3017 assert_eq!(records[0].name, "_sip._tcp.seer.test");
3018 match &records[0].data {
3019 RecordData::SRV {
3020 priority,
3021 weight,
3022 port,
3023 target,
3024 } => {
3025 assert_eq!((*priority, *weight, *port), (10, 5, 5060));
3026 assert_eq!(target, "sipserver.seer.test.");
3027 }
3028 other => panic!("expected SRV data, got {other:?}"),
3029 }
3030 }
3031
3032 #[tokio::test]
3033 async fn mock_resolve_ptr_transforms_ip_literal() {
3034 let records = mock_zone_lookup(RecordType::PTR, "192.0.2.1").await;
3035 assert_eq!(records.len(), 1);
3036 assert_eq!(records[0].name, "1.2.0.192.in-addr.arpa");
3038 assert!(matches!(
3039 &records[0].data,
3040 RecordData::PTR { target } if target == "ptr.seer.test."
3041 ));
3042 }
3043
3044 #[tokio::test]
3045 async fn mock_resolve_keeps_www_label() {
3046 let records = mock_zone_lookup(RecordType::CNAME, "www.seer.test").await;
3049 assert_eq!(records.len(), 1, "{records:?}");
3050 assert_eq!(records[0].name, "www.seer.test");
3051 assert_eq!(records[0].data.to_string(), "edge.cdn.test.");
3052 }
3053
3054 #[tokio::test]
3055 async fn mock_resolve_srv_keeps_www_label() {
3056 let port = spawn_mock_dns_fn(|qname, qtype| match (qname, qtype) {
3057 ("_sip._tcp.www.seer.test", HickoryRecordType::SRV) => {
3058 MockReply::Answer(vec![HickoryRData::SRV(
3059 hickory_resolver::proto::rr::rdata::SRV::new(
3060 10,
3061 5,
3062 5060,
3063 Name::from_ascii("sip.seer.test.").unwrap(),
3064 ),
3065 )])
3066 }
3067 _ => MockReply::NoData,
3068 })
3069 .await;
3070 let records = mock_dns_resolver(port)
3071 .resolve(
3072 "_sip._tcp.www.seer.test",
3073 RecordType::SRV,
3074 Some("127.0.0.1"),
3075 )
3076 .await
3077 .expect("SRV against mock");
3078 assert_eq!(records.len(), 1, "{records:?}");
3079 assert_eq!(records[0].name, "_sip._tcp.www.seer.test");
3080 }
3081
3082 #[tokio::test]
3083 async fn mock_resolve_queries_a_wildcard_owner_name() {
3084 let port = spawn_mock_dns_fn(|qname, qtype| match (qname, qtype) {
3087 ("*.seer.test", HickoryRecordType::A) => MockReply::Answer(vec![HickoryRData::A(
3088 hickory_resolver::proto::rr::rdata::A(Ipv4Addr::new(192, 0, 2, 42)),
3089 )]),
3090 _ => MockReply::NoData,
3091 })
3092 .await;
3093 let records = mock_dns_resolver(port)
3094 .resolve("*.seer.test", RecordType::A, Some("127.0.0.1"))
3095 .await
3096 .expect("wildcard A against mock");
3097 assert_eq!(records.len(), 1, "{records:?}");
3098 assert_eq!(records[0].name, "*.seer.test");
3099 assert_eq!(records[0].data.to_string(), "192.0.2.42");
3100 }
3101
3102 #[tokio::test]
3103 async fn mock_presence_still_probes_the_apex_for_www_input() {
3104 let port = spawn_mock_dns_fn(|qname, qtype| match (qname, qtype) {
3107 ("seer.test", HickoryRecordType::NS) => MockReply::Answer(vec![HickoryRData::NS(
3108 hickory_resolver::proto::rr::rdata::NS(Name::from_ascii("ns1.seer.test.").unwrap()),
3109 )]),
3110 ("www.seer.test", HickoryRecordType::NS) => MockReply::NoDataWithSoa("seer.test"),
3113 _ => MockReply::NxDomain,
3114 })
3115 .await;
3116 let resolver = mock_dns_resolver_default(port);
3117 assert_eq!(
3118 resolver.presence("www.seer.test").await,
3119 DnsPresence::Present
3120 );
3121 }
3122
3123 #[tokio::test]
3124 async fn mock_resolve_ptr_transforms_ipv6_literal() {
3125 let records = mock_zone_lookup(RecordType::PTR, "2606:4700:4700::1111").await;
3126 assert_eq!(records.len(), 1, "{records:?}");
3127 assert_eq!(records[0].data.to_string(), "one.one.one.one.");
3128 }
3129
3130 #[tokio::test]
3131 async fn mock_resolve_any_aggregates_multiple_types() {
3132 let records = mock_zone_lookup(RecordType::ANY, "seer.test").await;
3133 for expected in [
3136 RecordType::A,
3137 RecordType::AAAA,
3138 RecordType::MX,
3139 RecordType::NS,
3140 RecordType::TXT,
3141 RecordType::SOA,
3142 RecordType::CAA,
3143 RecordType::HTTPS,
3144 RecordType::DNSKEY,
3145 ] {
3146 assert!(
3147 records.iter().any(|r| r.record_type == expected),
3148 "ANY must include {expected} records"
3149 );
3150 }
3151 assert_eq!(records.len(), 13);
3154 assert!(records.iter().all(|r| r.name == "seer.test"));
3155 assert!(!records.iter().any(|r| r.record_type == RecordType::CDS));
3157 }
3158
3159 #[tokio::test]
3160 async fn mock_resolve_converts_service_bindings() {
3161 let records = mock_zone_lookup(RecordType::HTTPS, "seer.test").await;
3162 assert_eq!(records.len(), 1, "{records:?}");
3163 let RecordData::HTTPS {
3164 priority,
3165 target,
3166 params,
3167 } = &records[0].data
3168 else {
3169 panic!("expected HTTPS data, got {:?}", records[0].data);
3170 };
3171 assert_eq!((*priority, target.as_str()), (1, "."));
3172 let pairs: Vec<(&str, &str)> = params
3173 .iter()
3174 .map(|p| (p.key.as_str(), p.value.as_str()))
3175 .collect();
3176 assert_eq!(
3177 pairs,
3178 [
3179 ("alpn", "h3,h2"),
3180 ("port", "8443"),
3181 ("ipv4hint", "192.0.2.1,192.0.2.2"),
3182 ("ech", "AAH+"),
3183 ("ipv6hint", "2001:db8::1"),
3184 ("key65333", "ex\\0321"),
3187 ]
3188 );
3189 assert_eq!(
3190 records[0].data.to_string(),
3191 "1 . alpn=\"h3,h2\" port=8443 ipv4hint=192.0.2.1,192.0.2.2 ech=AAH+ \
3192 ipv6hint=2001:db8::1 key65333=ex\\0321"
3193 );
3194
3195 let alias = mock_zone_lookup(RecordType::HTTPS, "alias.seer.test").await;
3196 assert_eq!(alias.len(), 1, "{alias:?}");
3197 assert_eq!(alias[0].data.to_string(), "0 pool.seer.test.");
3198
3199 let svcb = mock_zone_lookup(RecordType::SVCB, "_8443._foo.seer.test").await;
3200 assert_eq!(svcb.len(), 1, "{svcb:?}");
3201 assert_eq!(svcb[0].record_type, RecordType::SVCB);
3202 assert_eq!(
3203 svcb[0].data.to_string(),
3204 "2 svc.seer.test. mandatory=alpn,port alpn=\"foo\" no-default-alpn port=8443"
3205 );
3206 }
3207
3208 #[tokio::test]
3209 async fn mock_resolve_converts_child_dnssec_records() {
3210 let cds = mock_zone_lookup(RecordType::CDS, "seer.test").await;
3212 let shown: Vec<String> = cds.iter().map(|r| r.data.to_string()).collect();
3213 assert_eq!(shown, ["2371 15 2 ABCDEF", "0 0 0 00"]);
3214 assert!(cds.iter().all(|r| r.record_type == RecordType::CDS));
3215
3216 let cdnskey = mock_zone_lookup(RecordType::CDNSKEY, "seer.test").await;
3217 let shown: Vec<String> = cdnskey.iter().map(|r| r.data.to_string()).collect();
3218 assert_eq!(
3219 shown,
3220 [
3221 "257 3 15 BwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwc=",
3222 "0 3 0 AA==",
3224 ]
3225 );
3226 }
3227
3228 #[tokio::test]
3229 async fn mock_resolve_keeps_only_the_requested_type_behind_a_cname() {
3230 let port = spawn_mock_dns_fn(|qname, qtype| match (qname, qtype) {
3234 ("www.seer.test", HickoryRecordType::A) => MockReply::Records(vec![
3235 record("www.seer.test.", 300, cname_rdata("edge.cdn.test.")),
3236 record("edge.cdn.test.", 60, a_rdata([192, 0, 2, 7])),
3237 ]),
3238 _ => MockReply::NxDomain,
3239 })
3240 .await;
3241 let records = mock_dns_resolver(port)
3242 .resolve("www.seer.test", RecordType::A, Some("127.0.0.1"))
3243 .await
3244 .expect("A behind a CNAME");
3245 assert_eq!(
3246 records,
3247 [DnsRecord {
3248 name: "www.seer.test".to_string(),
3249 record_type: RecordType::A,
3250 ttl: 60,
3251 data: RecordData::A {
3252 address: "192.0.2.7".to_string(),
3253 },
3254 }]
3255 );
3256 }
3257
3258 fn is_probe(qname: &str) -> bool {
3261 qname.starts_with("seer-probe-")
3262 }
3263
3264 type Asked = Arc<std::sync::Mutex<Vec<(String, HickoryRecordType)>>>;
3266
3267 async fn spawn_recording<F>(mut handler: F) -> (u16, Asked)
3269 where
3270 F: FnMut(&str, HickoryRecordType) -> MockReply + Send + 'static,
3271 {
3272 let asked: Asked = Arc::default();
3273 let log = Arc::clone(&asked);
3274 let port = spawn_mock_dns_fn(move |qname, qtype| {
3275 log.lock().unwrap().push((qname.to_string(), qtype));
3276 handler(qname, qtype)
3277 })
3278 .await;
3279 (port, asked)
3280 }
3281
3282 fn probes_sent(asked: &Asked) -> usize {
3283 asked
3284 .lock()
3285 .unwrap()
3286 .iter()
3287 .filter(|(name, _)| is_probe(name))
3288 .count()
3289 }
3290
3291 async fn mock_query(port: u16, name: &str, record_type: RecordType) -> DnsQueryResult {
3292 mock_dns_resolver(port)
3293 .query(name, record_type, Some("127.0.0.1"))
3294 .await
3295 .unwrap_or_else(|e| panic!("{record_type} query against mock must succeed: {e}"))
3296 }
3297
3298 fn shown(records: &[&DnsRecord]) -> Vec<(String, RecordType, String)> {
3299 records
3300 .iter()
3301 .map(|r| (r.name.clone(), r.record_type, r.data.to_string()))
3302 .collect()
3303 }
3304
3305 fn row(name: &str, record_type: RecordType, data: &str) -> (String, RecordType, String) {
3306 (name.to_string(), record_type, data.to_string())
3307 }
3308
3309 #[tokio::test]
3310 async fn mock_query_reports_the_cname_chain_under_real_owners() {
3311 let port = spawn_mock_dns_fn(|qname, qtype| match (qname, qtype) {
3312 ("www.seer.test", HickoryRecordType::A) => MockReply::Records(vec![
3313 record("www.seer.test.", 300, cname_rdata("edge.cdn.test.")),
3314 record("edge.cdn.test.", 120, cname_rdata("origin.cdn.test.")),
3315 record("origin.cdn.test.", 60, a_rdata([192, 0, 2, 7])),
3316 record("origin.cdn.test.", 60, a_rdata([192, 0, 2, 8])),
3317 ]),
3318 _ => MockReply::NxDomain,
3319 })
3320 .await;
3321 let result = mock_query(port, "WWW.Seer.test", RecordType::A).await;
3322
3323 assert_eq!(result.name, "www.seer.test");
3324 assert_eq!(result.record_type, RecordType::A);
3325 assert_eq!(result.server.as_deref(), Some("127.0.0.1"));
3326 assert_eq!(result.status, DnsStatus::NoError);
3327 assert_eq!(result.flags, ["qr", "rd", "ra"]);
3328 assert_eq!(
3329 shown(&result.cname_chain().collect::<Vec<_>>()),
3330 [
3331 row("www.seer.test", RecordType::CNAME, "edge.cdn.test."),
3332 row("edge.cdn.test", RecordType::CNAME, "origin.cdn.test."),
3333 ]
3334 );
3335 assert_eq!(
3336 shown(&result.records().collect::<Vec<_>>()),
3337 [
3338 row("origin.cdn.test", RecordType::A, "192.0.2.7"),
3339 row("origin.cdn.test", RecordType::A, "192.0.2.8"),
3340 ]
3341 );
3342 assert_eq!(result.answers[2].ttl, 60);
3343 assert!(result.authority.is_empty());
3344 assert!(!result.is_nodata());
3345 let wildcard = result
3347 .wildcard
3348 .expect("probe ran for a name below seer.test");
3349 assert!(is_probe(&wildcard.probe_name), "{wildcard:?}");
3350 assert!(wildcard.probe_name.ends_with(".seer.test"), "{wildcard:?}");
3351 assert!(!wildcard.present && !wildcard.matches_answer);
3352 }
3353
3354 #[tokio::test]
3355 async fn mock_query_reports_idn_owners_as_a_labels() {
3356 let port = spawn_mock_dns_fn(|qname, qtype| match (qname, qtype) {
3360 ("xn--bcher-kva.seer.test", HickoryRecordType::A) => MockReply::Records(vec![
3361 record(
3362 "xn--bcher-kva.seer.test.",
3363 300,
3364 cname_rdata("edge.xn--caf-dma.test."),
3365 ),
3366 record("edge.xn--caf-dma.test.", 60, a_rdata([192, 0, 2, 7])),
3367 ]),
3368 _ => MockReply::NxDomain,
3369 })
3370 .await;
3371 let result = mock_query(port, "Bücher.seer.test", RecordType::A).await;
3372 assert_eq!(result.name, "xn--bcher-kva.seer.test");
3373 assert_eq!(
3374 shown(&result.answers.iter().collect::<Vec<_>>()),
3375 [
3376 row(
3377 "xn--bcher-kva.seer.test",
3378 RecordType::CNAME,
3379 "edge.xn--caf-dma.test."
3380 ),
3381 row("edge.xn--caf-dma.test", RecordType::A, "192.0.2.7"),
3382 ]
3383 );
3384 assert_eq!(result.cname_chain().count(), 1);
3385 }
3386
3387 #[tokio::test]
3388 async fn query_refuses_a_reserved_nameserver() {
3389 let r = DnsResolver::new();
3393 for reserved in ["127.0.0.1", "10.0.0.1", "::1", "tls://192.168.1.1"] {
3394 let err = r
3395 .query("www.example.com", RecordType::A, Some(reserved))
3396 .await
3397 .expect_err("a reserved nameserver must be refused");
3398 let msg = err.to_string().to_lowercase();
3399 assert!(
3400 msg.contains("blocked") || msg.contains("reserved"),
3401 "{reserved}: {msg}"
3402 );
3403 }
3404 }
3405
3406 #[tokio::test]
3407 async fn mock_query_reports_a_cname_the_server_did_not_follow_as_sent() {
3408 let (port, asked) = spawn_recording(|qname, qtype| match (qname, qtype) {
3413 ("www.seer.test", HickoryRecordType::A) => MockReply::Records(vec![record(
3414 "www.seer.test.",
3415 300,
3416 cname_rdata("edge.cdn.test."),
3417 )]),
3418 ("edge.cdn.test", HickoryRecordType::A) => {
3419 MockReply::Answer(vec![a_rdata([192, 0, 2, 7])])
3420 }
3421 _ => MockReply::NxDomain,
3422 })
3423 .await;
3424 let result = mock_query(port, "www.seer.test", RecordType::A).await;
3425 assert_eq!(result.status, DnsStatus::NoError);
3426 assert_eq!(result.flags, ["qr", "rd", "ra"]);
3427 assert_eq!(
3428 shown(&result.answers.iter().collect::<Vec<_>>()),
3429 [row("www.seer.test", RecordType::CNAME, "edge.cdn.test.")]
3430 );
3431 assert_eq!(result.records().count(), 0);
3432 assert!(
3433 !asked
3434 .lock()
3435 .unwrap()
3436 .iter()
3437 .any(|(n, _)| n == "edge.cdn.test"),
3438 "the target is not chased: {asked:?}"
3439 );
3440 }
3441
3442 #[tokio::test]
3443 async fn mock_query_reports_a_dangling_cname_with_its_chain() {
3444 let port = spawn_mock_dns_fn(|qname, _| match qname {
3448 "www.seer.test" => MockReply::Negative {
3449 code: ResponseCode::NXDomain,
3450 chain: vec![
3451 record("www.seer.test.", 300, cname_rdata("shop.seer.test.")),
3452 record("shop.seer.test.", 300, cname_rdata("gone.cdn.test.")),
3453 ],
3454 soa: "cdn.test",
3455 },
3456 _ => MockReply::NxDomainWithSoa("seer.test"),
3457 })
3458 .await;
3459 let result = mock_query(port, "www.seer.test", RecordType::A).await;
3460 assert_eq!(result.status, DnsStatus::NxDomain);
3461 assert_eq!(result.flags, ["qr", "rd", "ra"]);
3462 assert_eq!(
3463 shown(&result.cname_chain().collect::<Vec<_>>()),
3464 [
3465 row("www.seer.test", RecordType::CNAME, "shop.seer.test."),
3466 row("shop.seer.test", RecordType::CNAME, "gone.cdn.test."),
3467 ]
3468 );
3469 assert_eq!(result.records().count(), 0);
3470 assert_eq!(
3471 shown(&result.authority.iter().collect::<Vec<_>>()),
3472 [row(
3473 "cdn.test",
3474 RecordType::SOA,
3475 "ns1.cdn.test. hostmaster.cdn.test. 2026070101 7200 3600 1209600 300"
3476 )]
3477 );
3478 assert!(!result.is_nodata());
3479 assert_eq!(result.referral_zone(), None);
3480 assert_eq!(
3481 result.wildcard, None,
3482 "a negative answer has nothing to compare"
3483 );
3484 }
3485
3486 #[tokio::test]
3487 async fn mock_query_reports_nodata_behind_a_cname() {
3488 let port = spawn_mock_dns_fn(|qname, _| match qname {
3489 "www.seer.test" => MockReply::Negative {
3490 code: ResponseCode::NoError,
3491 chain: vec![record("www.seer.test.", 300, cname_rdata("edge.cdn.test."))],
3492 soa: "cdn.test",
3493 },
3494 _ => MockReply::NxDomain,
3495 })
3496 .await;
3497 let result = mock_query(port, "www.seer.test", RecordType::AAAA).await;
3498 assert_eq!(result.status, DnsStatus::NoError);
3499 assert_eq!(result.flags, ["qr", "rd", "ra"]);
3500 assert_eq!(result.cname_chain().count(), 1);
3501 assert_eq!(result.records().count(), 0);
3502 assert!(result.is_nodata());
3503 assert_eq!(result.referral_zone(), None);
3504 assert_eq!(result.authority.len(), 1);
3505 assert_eq!(result.authority[0].name, "cdn.test");
3506 }
3507
3508 #[tokio::test]
3509 async fn mock_query_reports_nxdomain_with_the_zone_soa() {
3510 let (port, asked) = spawn_recording(|_, _| MockReply::NxDomainWithSoa("seer.test")).await;
3511 let result = mock_query(port, "nx.seer.test", RecordType::A).await;
3512 assert_eq!(result.status, DnsStatus::NxDomain);
3513 assert!(result.answers.is_empty());
3514 assert!(!result.is_nodata(), "NXDOMAIN is not NODATA");
3515 assert_eq!(result.flags, ["qr", "rd", "ra"]);
3517 assert_eq!(
3518 shown(&result.authority.iter().collect::<Vec<_>>()),
3519 [row(
3520 "seer.test",
3521 RecordType::SOA,
3522 "ns1.seer.test. hostmaster.seer.test. 2026070101 7200 3600 1209600 300"
3523 )]
3524 );
3525 assert_eq!(result.wildcard, None);
3527 assert!(asked
3528 .lock()
3529 .unwrap()
3530 .iter()
3531 .any(|(n, _)| n == "nx.seer.test"));
3532 }
3533
3534 #[tokio::test]
3535 async fn mock_query_reports_nodata_with_the_zone_soa() {
3536 let port = spawn_mock_dns_fn(|_, _| MockReply::NoDataWithSoa("seer.test")).await;
3537 let result = mock_query(port, "www.seer.test", RecordType::MX).await;
3538 assert_eq!(result.status, DnsStatus::NoError);
3539 assert!(result.is_nodata());
3540 assert!(result.answers.is_empty());
3541 assert_eq!(result.authority.len(), 1);
3542 assert_eq!(result.authority[0].record_type, RecordType::SOA);
3543 assert_eq!(result.authority[0].name, "seer.test");
3544 assert_eq!(result.wildcard, None);
3545 }
3546
3547 #[tokio::test]
3548 async fn mock_query_reports_error_codes_as_results() {
3549 let port = spawn_mock_dns_fn(|qname, _| match qname {
3550 "servfail.seer.test" => MockReply::Rcode(ResponseCode::ServFail),
3551 "refused.seer.test" => MockReply::Rcode(ResponseCode::Refused),
3552 "notimp.seer.test" => MockReply::Rcode(ResponseCode::NotImp),
3553 _ => MockReply::NxDomain,
3554 })
3555 .await;
3556 for (name, status, shown) in [
3557 ("servfail.seer.test", DnsStatus::ServFail, "SERVFAIL"),
3558 ("refused.seer.test", DnsStatus::Refused, "REFUSED"),
3559 ("notimp.seer.test", DnsStatus::Other(4), "NOTIMP"),
3560 ] {
3561 let result = mock_query(port, name, RecordType::A).await;
3562 assert_eq!(result.status, status, "{name}");
3563 assert_eq!(result.status.to_string(), shown);
3564 assert!(result.answers.is_empty() && result.authority.is_empty());
3565 assert_eq!(result.flags, ["qr", "rd", "ra"], "{name}");
3566 assert!(!result.is_nodata());
3567 assert_eq!(result.wildcard, None);
3568 }
3569 }
3570
3571 #[tokio::test]
3572 async fn mock_query_reads_an_extended_rcode_from_edns() {
3573 let port = spawn_mock_dns_fn(|_, _| MockReply::Rcode(ResponseCode::BADVERS)).await;
3576 let result = mock_query(port, "www.seer.test", RecordType::A).await;
3577 assert_eq!(result.status, DnsStatus::Other(16));
3578 assert_eq!(result.status.to_string(), "BADVERS");
3579 }
3580
3581 #[tokio::test]
3582 async fn mock_query_repeats_a_truncated_reply_over_tcp() {
3583 let transports = Arc::new(std::sync::Mutex::new(0));
3586 let seen = Arc::clone(&transports);
3587 let port = spawn_mock_dns_fn_with_tcp(move |qname, _| {
3588 if qname != "big.seer.test" {
3589 return MockReply::NxDomain;
3590 }
3591 let mut n = seen.lock().unwrap();
3592 *n += 1;
3593 if *n == 1 {
3594 MockReply::Truncated
3595 } else {
3596 MockReply::Answer((1..=3).map(|i| a_rdata([192, 0, 2, i])).collect())
3597 }
3598 })
3599 .await;
3600 let result = mock_query(port, "big.seer.test", RecordType::A).await;
3601 assert_eq!(result.status, DnsStatus::NoError);
3602 assert_eq!(result.records().count(), 3);
3603 assert!(
3604 !result.flags.contains(&"tc".to_string()),
3605 "{:?}",
3606 result.flags
3607 );
3608 assert_eq!(*transports.lock().unwrap(), 2, "UDP, then TCP");
3609 }
3610
3611 async fn two_truncating_servers() -> ResolverConfig {
3615 let mut config = ResolverConfig::from_parts(None, vec![], vec![]);
3616 for _ in 0..2 {
3617 let port = spawn_mock_dns_split(
3618 |_, _| MockReply::Truncated,
3619 |qname, _| {
3620 if qname == "big.seer.test" {
3621 MockReply::Answer((1..=3).map(|i| a_rdata([192, 0, 2, i])).collect())
3622 } else {
3623 MockReply::NxDomain
3624 }
3625 },
3626 )
3627 .await;
3628 for server in
3629 single_server_config(IpAddr::from([127, 0, 0, 1]), port, true).name_servers()
3630 {
3631 config.add_name_server(server.clone());
3632 }
3633 }
3634 config
3635 }
3636
3637 #[tokio::test]
3640 async fn mock_resolve_asks_over_tcp_when_parallel_udp_replies_truncate() {
3641 let r = DnsResolver::new().with_timeout(Duration::from_secs(2));
3642 let config = two_truncating_servers().await;
3643 let target = ResolveTarget {
3644 resolver: Cow::Owned(build_resolver(config.clone(), r.timeout).unwrap()),
3645 config,
3646 };
3647 let records = r
3648 .resolve_records(&target, "big.seer.test", RecordType::A)
3649 .await
3650 .expect("a truncated answer is asked again over TCP");
3651 assert_eq!(records.len(), 3);
3652 }
3653
3654 #[tokio::test]
3658 async fn hickory_still_abandons_truncated_parallel_udp_replies() {
3659 let config = two_truncating_servers().await;
3660 let resolver = build_resolver(config, Duration::from_secs(2)).unwrap();
3661 let result = resolver.lookup("big.seer.test", HickoryRecordType::A).await;
3662 assert!(
3663 matches!(result, Err(NetError::Truncated)),
3664 "{:?}",
3665 result.map(|lookup| lookup.answers().len())
3666 );
3667 }
3668
3669 #[tokio::test]
3670 async fn mock_query_reports_a_referral_as_neither_nodata_nor_an_answer() {
3671 let port = spawn_mock_dns_fn(|_, _| MockReply::Delegation {
3675 zone: "child.seer.test".to_string(),
3676 servers: vec![
3677 (
3678 "ns1.child.seer.test".to_string(),
3679 vec![IpAddr::from([192, 0, 2, 1])],
3680 ),
3681 ("ns2.child.seer.test".to_string(), vec![]),
3682 ],
3683 })
3684 .await;
3685 let result = mock_query(port, "www.child.seer.test", RecordType::A).await;
3686 assert_eq!(result.status, DnsStatus::NoError);
3687 assert_eq!(result.flags, ["qr", "rd"]);
3689 assert!(result.answers.is_empty());
3690 assert_eq!(result.referral_zone(), Some("child.seer.test"));
3691 assert!(!result.is_nodata());
3692 assert_eq!(
3693 shown(&result.authority.iter().collect::<Vec<_>>()),
3694 [
3695 row("child.seer.test", RecordType::NS, "ns1.child.seer.test."),
3696 row("child.seer.test", RecordType::NS, "ns2.child.seer.test."),
3697 ]
3698 );
3699 assert_eq!(result.wildcard, None);
3700 }
3701
3702 #[tokio::test]
3703 async fn mock_query_reports_special_use_names_as_answered_locally() {
3704 let (port, asked) = spawn_recording(|_, _| MockReply::NxDomain).await;
3709 let ptr = mock_query(port, "127.0.0.1", RecordType::PTR).await;
3710 assert_eq!(ptr.name, "1.0.0.127.in-addr.arpa");
3711 assert!(ptr.answered_locally);
3712 assert_eq!(ptr.server, None, "no server was asked");
3713 assert!(ptr.flags.is_empty(), "the header is hickory's own");
3714 assert_eq!(ptr.status, DnsStatus::NoError);
3715 assert_eq!(
3716 shown(&ptr.answers.iter().collect::<Vec<_>>()),
3717 [row("1.0.0.127.in-addr.arpa", RecordType::PTR, "localhost.")]
3718 );
3719 assert_eq!(ptr.wildcard, None, "no wildcard probe for a local name");
3720
3721 let mx = mock_query(port, "foo.localhost", RecordType::MX).await;
3722 assert!(mx.answered_locally && mx.is_nodata());
3723 let onion = mock_query(port, "x.onion", RecordType::A).await;
3724 assert!(onion.answered_locally);
3725 assert_eq!(onion.status, DnsStatus::NxDomain);
3726
3727 assert!(asked.lock().unwrap().is_empty(), "{asked:?}");
3728 }
3729
3730 #[tokio::test]
3731 async fn mock_query_times_out_as_an_error() {
3732 let port = spawn_mock_dns(MockMode::Ignore).await;
3733 let resolver = DnsResolver::new()
3734 .with_timeout(Duration::from_millis(200))
3735 .allowing_private_hosts()
3736 .with_port(port);
3737 let result = resolver
3738 .query("www.seer.test", RecordType::A, Some("127.0.0.1"))
3739 .await;
3740 assert!(
3741 matches!(&result, Err(SeerError::DnsError(m)) if m.contains("A lookup failed")),
3742 "an unanswered query is a transport error, got: {result:?}"
3743 );
3744 }
3745
3746 #[tokio::test]
3747 async fn mock_query_reads_flags_from_the_response_header() {
3748 let port = spawn_mock_dns_fn(|qname, qtype| match (qname, qtype) {
3749 ("seer.test", HickoryRecordType::A) => {
3750 MockReply::AuthoritativeAnswer(vec![a_rdata([192, 0, 2, 1])])
3751 }
3752 _ => MockReply::NxDomain,
3753 })
3754 .await;
3755 let result = mock_query(port, "seer.test", RecordType::A).await;
3756 assert_eq!(result.flags, ["qr", "aa", "rd", "ra"]);
3757 assert_eq!(result.cname_chain().count(), 0);
3758 assert_eq!(
3759 result.records().next().map(|r| r.name.as_str()),
3760 Some("seer.test")
3761 );
3762 }
3763
3764 #[tokio::test]
3765 async fn mock_query_converts_the_new_record_types_under_their_owner() {
3766 let port = spawn_mock_dns(MockMode::Zone).await;
3767 let https = mock_query(port, "seer.test", RecordType::HTTPS).await;
3768 assert_eq!(https.answers.len(), 1, "{https:?}");
3769 assert_eq!(https.answers[0].name, "seer.test");
3770 assert!(https.answers[0]
3771 .data
3772 .to_string()
3773 .starts_with("1 . alpn=\"h3,h2\" port=8443"));
3774
3775 let alias = mock_query(port, "alias.seer.test", RecordType::HTTPS).await;
3776 assert_eq!(alias.answers[0].data.to_string(), "0 pool.seer.test.");
3777 assert_eq!(alias.answers[0].name, "alias.seer.test");
3778
3779 let cds = mock_query(port, "seer.test", RecordType::CDS).await;
3780 assert_eq!(cds.answers.len(), 2);
3781 let cdnskey = mock_query(port, "seer.test", RecordType::CDNSKEY).await;
3782 assert_eq!(cdnskey.answers[1].data.to_string(), "0 3 0 AA==");
3783 }
3784
3785 #[tokio::test]
3786 async fn mock_query_sorts_mx_like_resolve() {
3787 let port = spawn_mock_dns(MockMode::Zone).await;
3788 let result = mock_query(port, "seer.test", RecordType::MX).await;
3789 let prefs: Vec<String> = result.answers.iter().map(|r| r.data.to_string()).collect();
3790 assert_eq!(
3791 prefs,
3792 [
3793 "10 a.mail.seer.test.",
3794 "20 b.mail.seer.test.",
3795 "30 c.mail.seer.test."
3796 ]
3797 );
3798 }
3799
3800 #[tokio::test]
3801 async fn mock_query_resolves_srv_and_ptr_names_like_resolve() {
3802 let (port, asked) = spawn_recording(|qname, qtype| match (qname, qtype) {
3803 ("_sip._tcp.seer.test", HickoryRecordType::SRV) => {
3804 MockReply::Answer(vec![HickoryRData::SRV(
3805 hickory_resolver::proto::rr::rdata::SRV::new(
3806 10,
3807 5,
3808 5060,
3809 Name::from_ascii("sip.seer.test.").unwrap(),
3810 ),
3811 )])
3812 }
3813 ("1.2.0.192.in-addr.arpa", HickoryRecordType::PTR) => {
3814 MockReply::Answer(vec![HickoryRData::PTR(
3815 hickory_resolver::proto::rr::rdata::PTR(
3816 Name::from_ascii("ptr.seer.test.").unwrap(),
3817 ),
3818 )])
3819 }
3820 _ => MockReply::NxDomain,
3821 })
3822 .await;
3823
3824 let srv = mock_query(port, "_sip._tcp.seer.test", RecordType::SRV).await;
3825 assert_eq!(
3826 shown(&srv.answers.iter().collect::<Vec<_>>()),
3827 [row(
3828 "_sip._tcp.seer.test",
3829 RecordType::SRV,
3830 "10 5 5060 sip.seer.test."
3831 )]
3832 );
3833 let probe = srv.wildcard.expect("SRV name is below seer.test");
3835 assert!(probe.probe_name.ends_with("._tcp.seer.test"), "{probe:?}");
3836 assert!(asked.lock().unwrap().iter().any(|(n, t)| is_probe(n)
3837 && n.ends_with("._tcp.seer.test")
3838 && *t == HickoryRecordType::SRV));
3839
3840 let ptr = mock_query(port, "192.0.2.1", RecordType::PTR).await;
3841 assert_eq!(ptr.name, "1.2.0.192.in-addr.arpa");
3842 assert_eq!(ptr.answers[0].name, "1.2.0.192.in-addr.arpa");
3843
3844 let err = mock_dns_resolver(port)
3845 .query("seer.test", RecordType::SRV, Some("127.0.0.1"))
3846 .await
3847 .expect_err("a bare domain is not an SRV query");
3848 assert!(matches!(err, SeerError::InvalidInput(_)), "{err:?}");
3849 let err = mock_dns_resolver(port)
3850 .query(".bad.example", RecordType::A, Some("127.0.0.1"))
3851 .await;
3852 assert!(err.is_err(), "invalid names are rejected before any I/O");
3853 }
3854
3855 #[tokio::test]
3856 async fn mock_query_any_fans_out_to_the_new_types_and_dedupes() {
3857 let (port, asked) = spawn_recording(|qname, qtype| {
3858 let chain = || record("www.seer.test.", 300, cname_rdata("edge.seer.test."));
3859 match (qname, qtype) {
3860 ("www.seer.test", HickoryRecordType::CNAME) => MockReply::Records(vec![chain()]),
3861 ("www.seer.test", HickoryRecordType::A) => MockReply::Records(vec![
3862 chain(),
3863 record("edge.seer.test.", 60, a_rdata([192, 0, 2, 7])),
3864 ]),
3865 ("www.seer.test", HickoryRecordType::HTTPS) => {
3866 MockReply::Records(vec![
3867 chain(),
3868 record(
3869 "edge.seer.test.",
3870 60,
3871 HickoryRData::HTTPS(hickory_resolver::proto::rr::rdata::HTTPS(
3872 SVCB::new(1, Name::root(), vec![]),
3873 )),
3874 ),
3875 ])
3876 }
3877 ("www.seer.test", _) => MockReply::Records(vec![chain()]),
3880 _ => MockReply::NoDataWithSoa("seer.test"),
3881 }
3882 })
3883 .await;
3884 let result = mock_query(port, "www.seer.test", RecordType::ANY).await;
3885
3886 assert_eq!(result.record_type, RecordType::ANY);
3887 assert_eq!(result.status, DnsStatus::NoError);
3888 assert_eq!(result.flags, ["qr", "rd", "ra"]);
3889 assert_eq!(
3891 shown(&result.answers.iter().collect::<Vec<_>>()),
3892 [
3893 row("www.seer.test", RecordType::CNAME, "edge.seer.test."),
3894 row("edge.seer.test", RecordType::A, "192.0.2.7"),
3895 row("edge.seer.test", RecordType::HTTPS, "1 ."),
3896 ]
3897 );
3898 assert_eq!(result.cname_chain().count(), 1);
3899 assert_eq!(result.records().count(), 2);
3900 assert!(!result.is_nodata());
3901 assert_eq!(result.wildcard, None, "ANY is never probed");
3902
3903 let asked: Vec<HickoryRecordType> = asked
3904 .lock()
3905 .unwrap()
3906 .iter()
3907 .filter(|(n, _)| n == "www.seer.test")
3908 .map(|(_, t)| *t)
3909 .collect();
3910 for wire in ANY_TYPES.iter().filter_map(|t| wire_type(*t)) {
3911 assert!(asked.contains(&wire), "ANY must query {wire}: {asked:?}");
3912 }
3913 assert!(
3914 !asked.contains(&HickoryRecordType::ANY),
3915 "ANY is never sent"
3916 );
3917 }
3918
3919 #[tokio::test]
3920 async fn mock_query_any_errors_only_when_every_sub_query_fails() {
3921 let port = spawn_mock_dns(MockMode::Ignore).await;
3922 let resolver = DnsResolver::new()
3923 .with_timeout(Duration::from_millis(200))
3924 .allowing_private_hosts()
3925 .with_port(port);
3926 let result = resolver
3927 .query("www.seer.test", RecordType::ANY, Some("127.0.0.1"))
3928 .await;
3929 assert!(matches!(result, Err(SeerError::DnsError(_))), "{result:?}");
3930 }
3931
3932 async fn wildcard_zone(probe_reply: fn() -> MockReply) -> (u16, Asked) {
3935 spawn_recording(move |qname, qtype| match (qname, qtype) {
3936 ("www.seer.test", HickoryRecordType::A) => {
3937 MockReply::Answer(vec![a_rdata([192, 0, 2, 50])])
3938 }
3939 (name, HickoryRecordType::A) if is_probe(name) => probe_reply(),
3940 _ => MockReply::NxDomain,
3941 })
3942 .await
3943 }
3944
3945 #[tokio::test]
3946 async fn mock_wildcard_probe_matching_the_answer() {
3947 let (port, asked) =
3948 wildcard_zone(|| MockReply::Answer(vec![a_rdata([192, 0, 2, 50])])).await;
3949 let result = mock_query(port, "www.seer.test", RecordType::A).await;
3950 let probe = result.wildcard.expect("probe attached");
3951 assert!(probe.present && probe.matches_answer, "{probe:?}");
3952 assert_eq!(probes_sent(&asked), 1);
3953 }
3954
3955 #[tokio::test]
3956 async fn mock_wildcard_probe_with_different_data() {
3957 let (port, _) = wildcard_zone(|| MockReply::Answer(vec![a_rdata([192, 0, 2, 99])])).await;
3958 let result = mock_query(port, "www.seer.test", RecordType::A).await;
3959 let probe = result.wildcard.expect("probe attached");
3960 assert!(probe.present && !probe.matches_answer, "{probe:?}");
3961 }
3962
3963 #[tokio::test]
3964 async fn mock_wildcard_probe_absent() {
3965 let (port, _) = wildcard_zone(|| MockReply::NxDomainWithSoa("seer.test")).await;
3966 let result = mock_query(port, "www.seer.test", RecordType::A).await;
3967 let probe = result.wildcard.expect("probe attached");
3968 assert!(!probe.present && !probe.matches_answer, "{probe:?}");
3969 }
3970
3971 #[tokio::test]
3972 async fn mock_wildcard_probe_that_fails_is_dropped_not_fatal() {
3973 let (port, _) = wildcard_zone(|| MockReply::Rcode(ResponseCode::ServFail)).await;
3974 let result = mock_query(port, "www.seer.test", RecordType::A).await;
3975 assert_eq!(result.status, DnsStatus::NoError);
3976 assert_eq!(result.wildcard, None);
3977
3978 let (port, _) = wildcard_zone(|| MockReply::NoReply).await;
3980 let resolver = DnsResolver::new()
3981 .with_timeout(Duration::from_millis(200))
3982 .allowing_private_hosts()
3983 .with_port(port);
3984 let result = resolver
3985 .query("www.seer.test", RecordType::A, Some("127.0.0.1"))
3986 .await
3987 .expect("the probe never fails the query");
3988 assert_eq!(result.records().count(), 1);
3989 assert_eq!(result.wildcard, None);
3990 }
3991
3992 #[tokio::test]
3993 async fn mock_wildcard_probe_is_not_sent_for_ineligible_names() {
3994 let (port, asked) =
3996 spawn_recording(|_, _| MockReply::Answer(vec![a_rdata([192, 0, 2, 50])])).await;
3997 assert_eq!(
3999 mock_query(port, "seer.test", RecordType::A).await.wildcard,
4000 None
4001 );
4002 assert_eq!(
4004 mock_query(port, "*.seer.test", RecordType::A)
4005 .await
4006 .wildcard,
4007 None
4008 );
4009 assert_eq!(
4011 mock_query(port, "www.seer.test", RecordType::ANY)
4012 .await
4013 .wildcard,
4014 None
4015 );
4016 assert_eq!(probes_sent(&asked), 0, "{:?}", asked.lock().unwrap());
4017 }
4018
4019 #[tokio::test]
4020 async fn mock_nodata_folds_to_empty_and_classifies_absent() {
4021 let port = spawn_mock_dns(MockMode::NoData).await;
4022 let result = mock_dns_resolver(port)
4023 .resolve("seer.test", RecordType::NS, Some("127.0.0.1"))
4024 .await;
4025 assert!(
4026 matches!(&result, Ok(records) if records.is_empty()),
4027 "NODATA must fold to Ok(vec![]), got: {result:?}"
4028 );
4029 assert_eq!(classify_ns_presence(&result), DnsPresence::Absent);
4030 }
4031
4032 #[tokio::test]
4033 async fn mock_nxdomain_folds_to_empty_and_classifies_absent() {
4034 let port = spawn_mock_dns(MockMode::Nxdomain).await;
4035 let result = mock_dns_resolver(port)
4036 .resolve("seer.test", RecordType::NS, Some("127.0.0.1"))
4037 .await;
4038 assert!(
4039 matches!(&result, Ok(records) if records.is_empty()),
4040 "NXDOMAIN must fold to Ok(vec![]), got: {result:?}"
4041 );
4042 assert_eq!(classify_ns_presence(&result), DnsPresence::Absent);
4043 }
4044
4045 #[tokio::test]
4046 async fn mock_timeout_errors_and_classifies_unknown() {
4047 let port = spawn_mock_dns(MockMode::Ignore).await;
4048 let resolver = DnsResolver::new()
4050 .with_timeout(Duration::from_millis(200))
4051 .allowing_private_hosts()
4052 .with_port(port);
4053 let result = resolver
4054 .resolve("seer.test", RecordType::NS, Some("127.0.0.1"))
4055 .await;
4056 match &result {
4057 Err(SeerError::DnsError(_)) => {}
4058 other => panic!("unanswered query must surface a DnsError, got: {other:?}"),
4059 }
4060 assert_eq!(classify_ns_presence(&result), DnsPresence::Unknown);
4061 }
4062
4063 #[tokio::test]
4064 async fn resolve_type_rejects_composite_types_consistently() {
4065 let r = DnsResolver::new();
4070 for composite in [RecordType::SRV, RecordType::ANY] {
4071 let err = r
4072 .resolve_type(&r.default_target(), "seer.test", composite)
4073 .await
4074 .expect_err("composite types must be rejected by resolve_type");
4075 assert_eq!(
4076 err.to_string(),
4077 unsupported_record_type(composite).to_string()
4078 );
4079 }
4080 }
4081
4082 #[tokio::test]
4083 async fn mock_unanswered_probe_costs_a_positive_answer_only_the_grace() {
4084 let (port, asked) = wildcard_zone(|| MockReply::NoReply).await;
4088 let resolver = DnsResolver::new()
4089 .with_timeout(Duration::from_secs(2))
4090 .allowing_private_hosts()
4091 .with_port(port);
4092 let started = Instant::now();
4093 let result = resolver
4094 .query("www.seer.test", RecordType::A, Some("127.0.0.1"))
4095 .await
4096 .expect("the main query is answered");
4097 let elapsed = started.elapsed();
4098 assert_eq!(result.records().count(), 1);
4099 assert_eq!(result.wildcard, None, "an unfinished probe says nothing");
4100 assert!(probes_sent(&asked) >= 1, "the probe was sent");
4101 assert!(elapsed >= Duration::from_millis(400), "{elapsed:?}");
4103 assert!(elapsed < Duration::from_millis(1500), "{elapsed:?}");
4104 }
4105
4106 async fn partly_silent_zone() -> u16 {
4109 spawn_mock_dns_fn(|_, qtype| match qtype {
4110 HickoryRecordType::A => MockReply::Answer(vec![a_rdata([192, 0, 2, 7])]),
4111 HickoryRecordType::TXT | HickoryRecordType::DNSKEY => MockReply::NoReply,
4112 _ => MockReply::NoDataWithSoa("seer.test"),
4113 })
4114 .await
4115 }
4116
4117 fn short_resolver(port: u16) -> DnsResolver {
4118 DnsResolver::new()
4119 .with_timeout(Duration::from_millis(200))
4120 .allowing_private_hosts()
4121 .with_port(port)
4122 }
4123
4124 #[tokio::test]
4125 async fn mock_query_any_reports_the_types_that_failed() {
4126 let port = partly_silent_zone().await;
4129 let result = short_resolver(port)
4130 .query("www.seer.test", RecordType::ANY, Some("127.0.0.1"))
4131 .await
4132 .expect("some sub-queries were answered");
4133 assert_eq!(result.records().count(), 1);
4134 let failed: Vec<RecordType> = result.failed_types.iter().map(|f| f.record_type).collect();
4135 assert_eq!(failed, [RecordType::TXT, RecordType::DNSKEY]);
4136 assert!(
4137 result.failed_types[0].error.contains("timed out"),
4138 "{:?}",
4139 result.failed_types
4140 );
4141
4142 let a = short_resolver(port)
4144 .query("www.seer.test", RecordType::A, Some("127.0.0.1"))
4145 .await
4146 .unwrap();
4147 assert!(a.failed_types.is_empty());
4148 }
4149
4150 #[tokio::test]
4151 async fn mock_query_server_any_keeps_partial_failures_apart_from_silence() {
4152 let port = partly_silent_zone().await;
4153 let reply = short_resolver(port)
4154 .query_server("www.seer.test", RecordType::ANY, "127.0.0.1")
4155 .await
4156 .unwrap();
4157 let ServerReply::Response(result) = reply else {
4158 panic!("a partial answer is a response: {reply:?}");
4159 };
4160 assert_eq!(result.records().count(), 1);
4161 let failed: Vec<RecordType> = result.failed_types.iter().map(|f| f.record_type).collect();
4162 assert_eq!(failed, [RecordType::TXT, RecordType::DNSKEY]);
4163
4164 let silent = spawn_mock_dns(MockMode::Ignore).await;
4166 let reply = short_resolver(silent)
4167 .query_server("www.seer.test", RecordType::ANY, "127.0.0.1")
4168 .await
4169 .unwrap();
4170 assert!(
4171 matches!(&reply, ServerReply::Silent(reason) if reason == "timed out"),
4172 "{reply:?}"
4173 );
4174 }
4175
4176 #[tokio::test]
4177 async fn resolve_validates_the_name_before_resolving_the_nameserver() {
4178 let err = DnsResolver::new()
4182 .resolve(".bad..name", RecordType::A, Some("ns.seer.test"))
4183 .await
4184 .expect_err("an invalid name is rejected");
4185 assert!(
4186 matches!(
4187 err,
4188 SeerError::InvalidDomain(_) | SeerError::InvalidInput(_)
4189 ),
4190 "{err:?}"
4191 );
4192 }
4193
4194 #[tokio::test]
4195 async fn a_local_answer_never_resolves_the_nameserver_hostname() {
4196 let r = DnsResolver::new();
4201 let result = r
4202 .query(
4203 "127.0.0.1",
4204 RecordType::PTR,
4205 Some("ns.does-not-resolve.test"),
4206 )
4207 .await
4208 .expect("answered locally, the nameserver unused");
4209 assert!(result.answered_locally);
4210 assert_eq!(result.records().count(), 1);
4211
4212 let blocked = r
4213 .query("127.0.0.1", RecordType::PTR, Some("10.0.0.1"))
4214 .await
4215 .expect_err("a reserved nameserver is refused all the same");
4216 assert!(blocked.to_string().contains("blocked"), "{blocked}");
4217 assert!(r
4218 .query("127.0.0.1", RecordType::PTR, Some("ftp://ns.seer.test"))
4219 .await
4220 .is_err());
4221 }
4222
4223 #[tokio::test]
4228 async fn a_nameserver_hostname_lookup_is_held_to_one_timeout() {
4229 use crate::dns::test_support::{spawn_mock_dns_fn, MockReply};
4230
4231 let timeout = Duration::from_millis(500);
4232 let bootstrap = spawn_mock_dns_fn(|_, _| MockReply::NoReply).await;
4233 let resolver = DnsResolver::new()
4234 .with_timeout(timeout)
4235 .with_default_upstream_port(bootstrap)
4236 .allowing_private_hosts();
4237
4238 for use_query in [true, false] {
4239 let started = Instant::now();
4240 let err = if use_query {
4241 resolver
4242 .query("seer.test", RecordType::A, Some("ns.bootstrap.test"))
4243 .await
4244 .expect_err("the hostname never resolves")
4245 } else {
4246 resolver
4247 .resolve("seer.test", RecordType::A, Some("ns.bootstrap.test"))
4248 .await
4249 .expect_err("the hostname never resolves")
4250 };
4251 let elapsed = started.elapsed();
4252 assert!(err.to_string().contains("ns.bootstrap.test"), "{err}");
4253 assert!(elapsed < timeout * 2 - timeout / 5, "took {elapsed:?}");
4255 }
4256 }
4257
4258 #[tokio::test]
4264 async fn concurrent_lookups_through_a_failing_nameserver_fail_together() {
4265 use crate::dns::test_support::{spawn_mock_dns_fn, MockReply};
4266
4267 let timeout = Duration::from_millis(500);
4268 let bootstrap = spawn_mock_dns_fn(|_, _| MockReply::NoReply).await;
4269 let resolver = DnsResolver::new()
4270 .with_timeout(timeout)
4271 .with_default_upstream_port(bootstrap)
4272 .allowing_private_hosts();
4273 let lookup = || resolver.resolve("seer.test", RecordType::A, Some("ns.bootstrap.test"));
4274
4275 let started = Instant::now();
4276 let results = future::join_all((0..4).map(|_| lookup())).await;
4277 let elapsed = started.elapsed();
4278 for result in results {
4279 let err = result.expect_err("the hostname never resolves");
4280 assert!(err.to_string().contains("ns.bootstrap.test"), "{err}");
4281 }
4282 assert!(elapsed < timeout * 2, "took {elapsed:?}");
4283 }
4284
4285 #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
4290 async fn a_nameserver_hostname_lookup_spends_the_query_budget() {
4291 use crate::dns::test_support::{a_rdata, spawn_mock_dns_fn, MockReply};
4292
4293 let timeout = Duration::from_millis(800);
4294 let delay = timeout * 3 / 4;
4295 let bootstrap = spawn_mock_dns_fn(move |qname, qtype| match (qname, qtype) {
4296 ("ns.bootstrap.test", HickoryRecordType::A) => {
4299 std::thread::sleep(delay);
4300 MockReply::Answer(vec![
4301 a_rdata(Ipv4Addr::LOCALHOST),
4302 a_rdata(Ipv4Addr::LOCALHOST),
4303 ])
4304 }
4305 _ => MockReply::NoData,
4306 })
4307 .await;
4308 let target = spawn_mock_dns_fn(|_, _| MockReply::NoReply).await;
4310 let resolver = DnsResolver::new()
4311 .with_timeout(timeout)
4312 .with_default_upstream_port(bootstrap)
4313 .allowing_private_hosts()
4314 .with_port(target);
4315
4316 let started = Instant::now();
4317 let err = resolver
4318 .query("seer.test", RecordType::A, Some("ns.bootstrap.test"))
4319 .await
4320 .expect_err("the nameserver is silent");
4321 let elapsed = started.elapsed();
4322 assert!(err.to_string().contains("timed out"), "{err}");
4323 assert!(
4324 !err.to_string().contains("ns.bootstrap.test"),
4325 "the hostname resolved: {err}"
4326 );
4327 let budget = timeout * crate::dns::transport::QUERY_BUDGET_TIMEOUTS;
4329 assert!(elapsed < budget + delay / 2, "took {elapsed:?}");
4330 }
4331}