1use std::borrow::Cow;
27use std::net::IpAddr;
28use std::pin::pin;
29use std::str::FromStr;
30use std::sync::Arc;
31use std::time::{Duration, Instant};
32
33use futures::future::{self, Either};
34use hickory_resolver::config::{
35 ConnectionConfig, NameServerConfig, ProtocolConfig, ResolveHosts, ResolverConfig, ResolverOpts,
36 ServerOrderingStrategy, GOOGLE,
37};
38use hickory_resolver::lookup::Lookup;
39use hickory_resolver::net::runtime::TokioRuntimeProvider;
40use hickory_resolver::net::NetError;
41use hickory_resolver::proto::dnssec::PublicKey;
42use hickory_resolver::proto::rr::rdata::svcb::{SvcParamKey, SvcParamValue, SVCB};
43use hickory_resolver::proto::rr::rdata::CAA;
44use hickory_resolver::proto::rr::{
45 Name, RData as HickoryRData, Record, RecordType as HickoryRecordType,
46};
47use hickory_resolver::proto::serialize::binary::BinEncodable;
48use hickory_resolver::TokioResolver;
49use tracing::{debug, instrument};
50
51use super::nameserver::{NameserverProtocol, NameserverSpec};
52use super::query::{
53 answered_locally, dedupe_records, fold_any, merge_any, random_probe_label, wildcard_outcome,
54 wildcard_probe_name, DnsQueryResult, Exchange, SubQuery, WildcardProbe,
55};
56use super::records::{DnsRecord, RecordData, RecordType, SvcParam};
57use super::transport::{NoResponse, Transport};
58use super::DEFAULT_DNS_TIMEOUT;
59use crate::error::{Result, SeerError};
60use crate::validation::{normalize_domain, normalize_query_name};
61
62pub(crate) const RESOLVER_ATTEMPTS: usize = 2;
66
67const PROBE_GRACE_DIVISOR: u32 = 5;
73
74const DEFAULT_UPSTREAM: &str = "default (Google Public DNS)";
76
77const ANY_TYPES: [RecordType; 11] = [
84 RecordType::A,
85 RecordType::AAAA,
86 RecordType::CNAME,
87 RecordType::MX,
88 RecordType::NS,
89 RecordType::TXT,
90 RecordType::SOA,
91 RecordType::CAA,
92 RecordType::HTTPS,
93 RecordType::DS,
94 RecordType::DNSKEY,
95];
96
97pub(crate) fn apply_standard_opts(opts: &mut ResolverOpts, timeout: Duration) {
105 opts.timeout = timeout;
106 opts.attempts = RESOLVER_ATTEMPTS;
107 opts.use_hosts_file = ResolveHosts::Never;
108 opts.server_ordering_strategy = ServerOrderingStrategy::UserProvidedOrder;
129}
130
131fn build_resolver(config: ResolverConfig, timeout: Duration) -> Result<TokioResolver> {
140 let mut builder = TokioResolver::builder_with_config(config, TokioRuntimeProvider::default());
141 apply_standard_opts(builder.options_mut(), timeout);
142 builder
143 .build()
144 .map_err(|e| SeerError::DnsError(format!("failed to construct DNS resolver: {}", e)))
145}
146
147struct ResolveTarget<'a> {
151 resolver: Cow<'a, TokioResolver>,
152 config: ResolverConfig,
153}
154
155fn tcp_only(config: &ResolverConfig) -> ResolverConfig {
158 let mut tcp =
159 ResolverConfig::from_parts(config.domain().cloned(), config.search().to_vec(), vec![]);
160 for server in config.name_servers() {
161 let mut server = server.clone();
162 let udp_ports: Vec<u16> = server
163 .connections
164 .iter()
165 .filter(|c| matches!(c.protocol, ProtocolConfig::Udp))
166 .map(|c| c.port)
167 .collect();
168 server
169 .connections
170 .retain(|c| !matches!(c.protocol, ProtocolConfig::Udp));
171 for port in udp_ports {
172 let has_tcp = server
173 .connections
174 .iter()
175 .any(|c| matches!(c.protocol, ProtocolConfig::Tcp) && c.port == port);
176 if !has_tcp {
177 let mut connection = ConnectionConfig::tcp();
178 connection.port = port;
179 server.connections.push(connection);
180 }
181 }
182 tcp.add_name_server(server);
183 }
184 tcp
185}
186
187fn build_default_resolver(timeout: Duration) -> TokioResolver {
194 build_resolver(default_upstream_config(), timeout)
195 .expect("default resolver build cannot fail with the bundled webpki root store")
196}
197
198fn default_upstream_config() -> ResolverConfig {
202 ResolverConfig::udp_and_tcp(&GOOGLE)
203}
204
205pub(crate) fn single_server_config(ip: IpAddr, port: u16, tcp_fallback: bool) -> ResolverConfig {
208 let mut ns = if tcp_fallback {
209 NameServerConfig::udp_and_tcp(ip)
210 } else {
211 NameServerConfig::udp(ip)
212 };
213 for connection in &mut ns.connections {
214 connection.port = port;
215 }
216 let mut config = ResolverConfig::from_parts(None, vec![], vec![]);
217 config.add_name_server(ns);
218 config
219}
220
221pub(crate) fn google_or_pinned(upstream: Option<(IpAddr, u16)>) -> ResolverConfig {
224 match upstream {
225 None => default_upstream_config(),
226 Some((ip, port)) => single_server_config(ip, port, false),
227 }
228}
229
230pub(crate) fn fqdn(name: &str) -> String {
233 if name.ends_with('.') {
234 name.to_string()
235 } else {
236 format!("{name}.")
237 }
238}
239
240fn build_upstream_config(
250 spec: &NameserverSpec,
251 ips: &[IpAddr],
252 port_override: Option<u16>,
253) -> ResolverConfig {
254 let mut config = ResolverConfig::from_parts(None, vec![], vec![]);
255 let port = port_override.unwrap_or(spec.port);
256 let mut ordered = ips.to_vec();
267 crate::net::ipv4_first(&mut ordered, |ip| *ip);
268 for ip in ordered {
269 let mut ns = match spec.protocol {
270 NameserverProtocol::Udp => NameServerConfig::udp(ip),
271 NameserverProtocol::Tls => NameServerConfig::tls(ip, Arc::from(spec.tls_name())),
272 NameserverProtocol::Https => NameServerConfig::https(
273 ip,
274 Arc::from(spec.tls_name()),
275 spec.path.as_deref().map(Arc::from),
276 ),
277 };
278 for connection in &mut ns.connections {
279 connection.port = port;
280 }
281 config.add_name_server(ns);
282 }
283 config
284}
285
286#[derive(Clone)]
294pub struct DnsResolver {
295 timeout: Duration,
296 default_resolver: TokioResolver,
299 transport: Transport,
302 port_override: Option<u16>,
307 allow_private_hosts: bool,
312 #[cfg(test)]
316 test_default_nameserver: Option<String>,
317}
318
319impl std::fmt::Debug for DnsResolver {
320 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
321 f.debug_struct("DnsResolver")
322 .field("timeout", &self.timeout)
323 .finish()
324 }
325}
326
327impl Default for DnsResolver {
328 fn default() -> Self {
329 Self::new()
330 }
331}
332
333impl DnsResolver {
334 pub fn new() -> Self {
336 Self::with_timeout_only(DEFAULT_DNS_TIMEOUT)
337 }
338
339 fn with_timeout_only(timeout: Duration) -> Self {
342 Self {
343 timeout,
344 default_resolver: build_default_resolver(timeout),
345 transport: Transport::new(timeout),
346 port_override: None,
347 allow_private_hosts: false,
348 #[cfg(test)]
349 test_default_nameserver: None,
350 }
351 }
352
353 pub fn from_config(config: &crate::config::SeerConfig) -> Self {
364 Self::with_timeout_only(config.dns_timeout())
365 }
366
367 #[cfg(test)]
369 pub(crate) fn timeout(&self) -> Duration {
370 self.timeout
371 }
372
373 #[cfg(test)]
375 pub(crate) fn allowing_private_hosts(mut self) -> Self {
376 self.allow_private_hosts = true;
377 self
378 }
379
380 #[cfg(test)]
383 pub(crate) fn with_port(mut self, port: u16) -> Self {
384 self.port_override = Some(port);
385 self
386 }
387
388 #[cfg(test)]
391 pub(crate) fn with_default_nameserver(mut self, nameserver: &str) -> Self {
392 self.test_default_nameserver = Some(nameserver.to_string());
393 self
394 }
395
396 #[cfg(test)]
400 pub(crate) fn with_default_upstream_port(mut self, port: u16) -> Self {
401 self.default_resolver = build_resolver(
402 single_server_config(std::net::Ipv4Addr::LOCALHOST.into(), port, false),
403 self.timeout,
404 )
405 .expect("a plain UDP resolver builds");
406 self
407 }
408
409 #[cfg(test)]
413 fn effective_nameserver<'a>(&'a self, nameserver: Option<&'a str>) -> Option<&'a str> {
414 nameserver.or(self.test_default_nameserver.as_deref())
415 }
416
417 #[cfg(not(test))]
418 fn effective_nameserver<'a>(&self, nameserver: Option<&'a str>) -> Option<&'a str> {
419 nameserver
420 }
421
422 pub fn with_timeout(mut self, timeout: Duration) -> Self {
426 self.timeout = timeout;
427 self.default_resolver = build_default_resolver(timeout);
428 self.transport = Transport::new(timeout);
429 self
430 }
431
432 #[cfg(test)]
435 async fn create_custom_resolver(&self, nameserver: &str) -> Result<TokioResolver> {
436 Ok(self.custom_target(nameserver).await?.resolver.into_owned())
437 }
438
439 async fn custom_target(&self, nameserver: &str) -> Result<ResolveTarget<'static>> {
442 let config = self.custom_upstream_config(nameserver).await?;
443 Ok(ResolveTarget {
444 resolver: Cow::Owned(build_resolver(config.clone(), self.timeout)?),
445 config,
446 })
447 }
448
449 fn default_target(&self) -> ResolveTarget<'_> {
451 ResolveTarget {
452 resolver: Cow::Borrowed(&self.default_resolver),
453 config: default_upstream_config(),
454 }
455 }
456
457 async fn custom_upstream_config(&self, nameserver: &str) -> Result<ResolverConfig> {
465 let spec = NameserverSpec::parse(nameserver)?;
470
471 let ips: Vec<IpAddr> = if let Ok(ip) = spec.host.parse::<IpAddr>() {
480 vec![ip]
481 } else {
482 let lookup = self.default_resolver.lookup_ip(spec.host.as_str());
485 let response = tokio::time::timeout(self.timeout, lookup)
486 .await
487 .unwrap_or(Err(NetError::Timeout))
488 .map_err(|e| {
489 SeerError::DnsError(format!(
490 "failed to resolve nameserver hostname {}: {}",
491 spec.host, e
492 ))
493 })?;
494 let resolved: Vec<IpAddr> = response.iter().collect();
495 if resolved.is_empty() {
496 return Err(SeerError::DnsError(format!(
497 "nameserver {} did not resolve to any addresses",
498 spec.host
499 )));
500 }
501 resolved
502 };
503
504 self.vet_nameserver_ips(nameserver, &ips)?;
505 Ok(build_upstream_config(&spec, &ips, self.port_override))
506 }
507
508 fn vet_nameserver_ips(&self, nameserver: &str, ips: &[IpAddr]) -> Result<()> {
515 if self.allow_private_hosts {
516 return Ok(());
517 }
518 for ip in ips {
519 if let Some(reason) = crate::validation::describe_reserved_ip(ip) {
520 return Err(SeerError::DnsError(format!(
521 "nameserver {} blocked: {}",
522 nameserver, reason
523 )));
524 }
525 }
526 Ok(())
527 }
528
529 fn vet_nameserver_offline(&self, nameserver: &str) -> Result<()> {
535 let spec = NameserverSpec::parse(nameserver)?;
536 match spec.host.parse::<IpAddr>() {
537 Ok(ip) => self.vet_nameserver_ips(nameserver, &[ip]),
538 Err(_) => Ok(()),
539 }
540 }
541
542 async fn upstream(&self, nameserver: Option<&str>) -> Result<ResolveTarget<'_>> {
545 Ok(match self.effective_nameserver(nameserver) {
546 Some(ns) => self.custom_target(ns).await?,
547 None => self.default_target(),
548 })
549 }
550
551 async fn query_upstream(&self, name: &str, nameserver: Option<&str>) -> Result<Upstream<'_>> {
559 if answered_locally(name) {
560 if let Some(ns) = self.effective_nameserver(nameserver) {
561 self.vet_nameserver_offline(ns)?;
562 }
563 return Ok(Upstream::Local(&self.default_resolver));
564 }
565 let deadline = self.transport.deadline();
566 let config = match self.effective_nameserver(nameserver) {
567 Some(ns) => self.custom_upstream_config(ns).await?,
568 None => default_upstream_config(),
569 };
570 Ok(Upstream::Direct {
571 config,
572 transport: &self.transport,
573 deadline,
574 })
575 }
576
577 #[instrument(skip(self), fields(domain = %domain, record_type = %record_type))]
626 pub async fn query(
627 &self,
628 domain: &str,
629 record_type: RecordType,
630 nameserver: Option<&str>,
631 ) -> Result<DnsQueryResult> {
632 let domain = prepare_query(domain, record_type)?;
634 let name = wire_query_name(&domain, record_type)?;
635 let upstream = self.query_upstream(&name, nameserver).await?;
636
637 debug!(
638 nameserver = nameserver.unwrap_or(DEFAULT_UPSTREAM),
639 "Querying DNS"
640 );
641
642 let (exchange, query_time, wildcard) = if record_type == RecordType::ANY {
643 let started = Instant::now();
644 let exchange = merge_any(exchange_any(&upstream, &name).await)?;
645 (exchange, started.elapsed(), None)
646 } else {
647 let grace = self.timeout / PROBE_GRACE_DIVISOR;
648 exchange_with_probe(&upstream, &name, record_type, grace).await?
649 };
650 Ok(exchange.into_result(name, record_type, nameserver, wildcard, query_time))
651 }
652
653 pub(crate) async fn query_server(
663 &self,
664 domain: &str,
665 record_type: RecordType,
666 nameserver: &str,
667 ) -> Result<ServerReply> {
668 let domain = prepare_query(domain, record_type)?;
669 let name = wire_query_name(&domain, record_type)?;
670 if answered_locally(&name) {
671 return self
673 .query(&domain, record_type, Some(nameserver))
674 .await
675 .map(ServerReply::Response);
676 }
677 let upstream = self.query_upstream(&name, Some(nameserver)).await?;
678
679 let started = Instant::now();
680 let exchange = if record_type == RecordType::ANY {
681 let replies = exchange_any(&upstream, &name).await;
682 if replies.iter().all(|(_, reply)| matches!(reply, Ok(Err(_)))) {
685 if let Some((_, Ok(Err(why)))) = replies.first() {
686 return Ok(ServerReply::Silent(why.reason()));
687 }
688 }
689 merge_any(replies)?
690 } else {
691 match upstream.try_exchange(&name, record_type).await? {
692 Ok(exchange) => exchange,
693 Err(why) => return Ok(ServerReply::Silent(why.reason())),
694 }
695 };
696 Ok(ServerReply::Response(exchange.into_result(
697 name,
698 record_type,
699 Some(nameserver),
700 None,
701 started.elapsed(),
702 )))
703 }
704
705 #[instrument(skip(self), fields(domain = %domain, record_type = %record_type))]
718 pub async fn resolve(
719 &self,
720 domain: &str,
721 record_type: RecordType,
722 nameserver: Option<&str>,
723 ) -> Result<Vec<DnsRecord>> {
724 let domain = prepare_query(domain, record_type)?;
726 let target = self.upstream(nameserver).await?;
728
729 debug!(
730 nameserver = nameserver.unwrap_or(DEFAULT_UPSTREAM),
731 "Resolving DNS"
732 );
733
734 match record_type {
735 RecordType::SRV => match parse_srv_query(&domain) {
736 Some((service, protocol, name)) => {
738 self.resolve_srv_core(&target, &service, &protocol, &name)
739 .await
740 }
741 None => Err(srv_format_error()),
744 },
745 RecordType::ANY => self.resolve_any(&target, &domain).await,
746 single => self.resolve_type(&target, &domain, single).await,
747 }
748 }
749
750 async fn resolve_srv_core(
757 &self,
758 target: &ResolveTarget<'_>,
759 service: &str,
760 protocol: &str,
761 domain: &str,
762 ) -> Result<Vec<DnsRecord>> {
763 let query_name = srv_query_name(service, protocol, domain)?;
764 self.resolve_records(target, &query_name, RecordType::SRV)
765 .await
766 }
767
768 async fn resolve_type(
776 &self,
777 target: &ResolveTarget<'_>,
778 domain: &str,
779 record_type: RecordType,
780 ) -> Result<Vec<DnsRecord>> {
781 match record_type {
782 RecordType::SRV | RecordType::ANY => Err(unsupported_record_type(record_type)),
783 RecordType::PTR => {
786 self.resolve_records(target, &ptr_query_name(domain), RecordType::PTR)
787 .await
788 }
789 single => self.resolve_records(target, domain, single).await,
790 }
791 }
792
793 async fn lookup_with_tcp_fallback(
805 &self,
806 target: &ResolveTarget<'_>,
807 name: &str,
808 wire_type: HickoryRecordType,
809 ) -> std::result::Result<Lookup, NetError> {
810 match target.resolver.lookup(name, wire_type).await {
811 Err(NetError::Truncated) => {
812 debug!(name, "truncated answer abandoned over UDP; asking over TCP");
813 match build_resolver(tcp_only(&target.config), self.timeout) {
814 Ok(tcp) => tcp.lookup(name, wire_type).await,
815 Err(e) => {
816 debug!(error = %e, "cannot build the TCP resolver");
817 Err(NetError::Truncated)
818 }
819 }
820 }
821 other => other,
822 }
823 }
824
825 async fn resolve_records(
834 &self,
835 target: &ResolveTarget<'_>,
836 domain: &str,
837 record_type: RecordType,
838 ) -> Result<Vec<DnsRecord>> {
839 let Some(wire_type) = wire_type(record_type) else {
840 return Err(unsupported_record_type(record_type));
841 };
842
843 let response = match self
844 .lookup_with_tcp_fallback(target, domain, wire_type)
845 .await
846 {
847 Ok(response) => response,
848 Err(e) if e.is_no_records_found() => return Ok(vec![]),
849 Err(e) => {
850 return Err(SeerError::DnsError(format!(
851 "{record_type} lookup failed: {e}"
852 )))
853 }
854 };
855
856 let mut records: Vec<DnsRecord> = response
857 .answers()
858 .iter()
859 .filter_map(|record| {
860 convert_rdata(record_type, &record.data, NameSpelling::Display).map(|data| {
861 DnsRecord {
862 name: domain.to_string(),
863 record_type,
864 ttl: record.ttl,
865 data,
866 }
867 })
868 })
869 .collect();
870
871 if record_type == RecordType::MX {
872 records.sort_by_key(|r| match &r.data {
873 RecordData::MX { preference, .. } => *preference,
874 _ => 0,
875 });
876 }
877
878 Ok(records)
879 }
880
881 async fn resolve_any(
882 &self,
883 target: &ResolveTarget<'_>,
884 domain: &str,
885 ) -> Result<Vec<DnsRecord>> {
886 let results = future::join_all(ANY_TYPES.into_iter().map(|record_type| async move {
891 (
892 record_type,
893 self.resolve_type(target, domain, record_type).await,
894 )
895 }))
896 .await;
897
898 let (record_sets, _failed) = fold_any(results)?;
902 Ok(dedupe_records(record_sets.into_iter().flatten()))
903 }
904}
905
906enum Upstream<'a> {
908 Local(&'a TokioResolver),
911 Direct {
915 config: ResolverConfig,
916 transport: &'a Transport,
917 deadline: tokio::time::Instant,
918 },
919}
920
921impl Upstream<'_> {
922 async fn exchange(&self, name: &str, record_type: RecordType) -> Result<Exchange> {
925 self.try_exchange(name, record_type)
926 .await?
927 .map_err(|why| SeerError::DnsError(format!("{record_type} lookup failed: {why}")))
928 }
929
930 async fn try_exchange(
933 &self,
934 name: &str,
935 record_type: RecordType,
936 ) -> Result<std::result::Result<Exchange, NoResponse>> {
937 let wire = wire_type(record_type).ok_or_else(|| unsupported_record_type(record_type))?;
938 let exchange = match self {
939 Upstream::Local(resolver) => {
940 Exchange::from_local(resolver.lookup(name, wire).await, record_type)?
941 }
942 Upstream::Direct {
943 config,
944 transport,
945 deadline,
946 } => {
947 let qname = Name::from_ascii(fqdn(name))
948 .map_err(|e| SeerError::InvalidDomain(format!("{name}: {e}")))?;
949 let request = transport.request(qname, wire, true);
950 match transport
951 .query(config.name_servers(), &request, *deadline)
952 .await
953 {
954 Ok(response) => Exchange::from_message(&response),
955 Err(why) => return Ok(Err(why)),
956 }
957 }
958 };
959 Ok(Ok(exchange.ordered(name, record_type)))
960 }
961}
962
963#[derive(Debug)]
965pub(crate) enum ServerReply {
966 Response(DnsQueryResult),
968 Silent(String),
971}
972
973async fn exchange_any(upstream: &Upstream<'_>, name: &str) -> Vec<(RecordType, SubQuery)> {
977 future::join_all(ANY_TYPES.into_iter().map(|record_type| async move {
978 (record_type, upstream.try_exchange(name, record_type).await)
979 }))
980 .await
981}
982
983async fn exchange_with_probe(
993 upstream: &Upstream<'_>,
994 name: &str,
995 record_type: RecordType,
996 grace: Duration,
997) -> Result<(Exchange, Duration, Option<WildcardProbe>)> {
998 let main = async {
999 let started = Instant::now();
1000 let result = upstream.exchange(name, record_type).await;
1001 (result, started.elapsed())
1002 };
1003 let Some(probe_name) = wildcard_probe_name(name, record_type, &random_probe_label()) else {
1004 let (result, elapsed) = main.await;
1005 return Ok((result?, elapsed, None));
1006 };
1007 let probe = upstream.exchange(&probe_name, record_type);
1008
1009 let main = pin!(main);
1010 let probe = pin!(probe);
1011 let ((result, elapsed), probe) = match future::select(main, probe).await {
1012 Either::Left(((result, elapsed), probe)) => {
1013 let attachable = matches!(&result, Ok(answer) if answer.has_records(record_type));
1014 let probe = if attachable {
1018 tokio::time::timeout(grace, probe).await.ok()
1019 } else {
1020 None
1021 };
1022 ((result, elapsed), probe)
1023 }
1024 Either::Right((probe, main)) => (main.await, Some(probe)),
1025 };
1026 let answer = result?;
1027 let wildcard =
1028 probe.and_then(|probe| wildcard_outcome(&probe_name, record_type, &answer, probe));
1029 Ok((answer, elapsed, wildcard))
1030}
1031
1032#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1037pub enum DnsPresence {
1038 Present,
1040 Absent,
1042 Unknown,
1044}
1045
1046fn classify_ns_presence(result: &Result<Vec<DnsRecord>>) -> DnsPresence {
1051 match result {
1052 Ok(records) if records.is_empty() => DnsPresence::Absent,
1053 Ok(_) => DnsPresence::Present,
1054 Err(_) => DnsPresence::Unknown,
1055 }
1056}
1057
1058impl DnsResolver {
1059 pub async fn presence(&self, domain: &str) -> DnsPresence {
1067 let Ok(apex) = normalize_domain(domain) else {
1071 return DnsPresence::Unknown;
1072 };
1073 classify_ns_presence(&self.resolve(&apex, RecordType::NS, None).await)
1074 }
1075}
1076
1077pub(crate) fn prepare_query(domain: &str, record_type: RecordType) -> Result<String> {
1100 prepare_query_with(domain, record_type, normalize_query_name)
1101}
1102
1103fn prepare_query_with(
1107 domain: &str,
1108 record_type: RecordType,
1109 normalize: impl Fn(&str) -> Result<String>,
1110) -> Result<String> {
1111 if record_type == RecordType::PTR {
1112 if let Ok(ip) = IpAddr::from_str(domain.trim()) {
1113 normalize(&reverse_dns_name(&ip))?;
1119 return Ok(ip.to_string());
1120 }
1121 }
1122 normalize(domain)
1123}
1124
1125pub(crate) fn parse_srv_query(name: &str) -> Option<(String, String, String)> {
1130 let mut parts = name.splitn(3, '.');
1131 let service = parts.next()?.strip_prefix('_')?;
1132 let protocol = parts.next()?.strip_prefix('_')?;
1133 let rest = parts.next()?;
1134 if service.is_empty() || protocol.is_empty() || rest.is_empty() {
1135 return None;
1136 }
1137 Some((service.to_string(), protocol.to_string(), rest.to_string()))
1138}
1139
1140fn srv_query_name(service: &str, protocol: &str, domain: &str) -> Result<String> {
1144 if !is_valid_srv_label(service) {
1145 return Err(SeerError::InvalidInput(format!(
1146 "invalid SRV service name: {}",
1147 service
1148 )));
1149 }
1150 if !is_valid_srv_label(protocol) {
1151 return Err(SeerError::InvalidInput(format!(
1152 "invalid SRV protocol name: {}",
1153 protocol
1154 )));
1155 }
1156 Ok(format!("_{}._{}.{}", service, protocol, domain))
1157}
1158
1159fn ptr_query_name(domain: &str) -> String {
1162 match IpAddr::from_str(domain) {
1163 Ok(ip) => reverse_dns_name(&ip),
1164 Err(_) => domain.to_string(),
1165 }
1166}
1167
1168pub(crate) fn wire_query_name(domain: &str, record_type: RecordType) -> Result<String> {
1174 match record_type {
1175 RecordType::SRV => {
1176 let (service, protocol, name) = parse_srv_query(domain).ok_or_else(srv_format_error)?;
1177 srv_query_name(&service, &protocol, &name)
1178 }
1179 RecordType::PTR => Ok(ptr_query_name(domain)),
1180 _ => Ok(domain.to_string()),
1181 }
1182}
1183
1184pub(crate) fn srv_format_error() -> SeerError {
1188 SeerError::InvalidInput(
1189 "SRV records require service name format: _service._proto.name".to_string(),
1190 )
1191}
1192
1193fn reverse_dns_name(ip: &IpAddr) -> String {
1194 match ip {
1195 IpAddr::V4(addr) => {
1196 let octets = addr.octets();
1197 format!(
1198 "{}.{}.{}.{}.in-addr.arpa",
1199 octets[3], octets[2], octets[1], octets[0]
1200 )
1201 }
1202 IpAddr::V6(addr) => {
1203 let segments = addr.segments();
1204 let mut result = String::with_capacity(72);
1206 let mut first = true;
1207 for segment in segments.iter().rev() {
1208 for shift in [0, 4, 8, 12] {
1209 if !first {
1210 result.push('.');
1211 }
1212 first = false;
1213 let nibble = (segment >> shift) & 0xF;
1214 result
1215 .push(char::from_digit(nibble as u32, 16).expect("nibble is always 0-15"));
1216 }
1217 }
1218 result.push_str(".ip6.arpa");
1219 result
1220 }
1221 }
1222}
1223
1224fn parse_caa(caa: &CAA) -> (u8, String, String) {
1225 let flags = caa.flags();
1235 let tag = caa.tag.clone();
1236 let value = String::from_utf8_lossy(&caa.value).to_string();
1237 (flags, tag, value)
1238}
1239
1240fn parse_svcb(svcb: &SVCB, names: NameSpelling) -> (u16, String, Vec<SvcParam>) {
1247 let params = svcb
1248 .svc_params
1249 .iter()
1250 .map(|(key, value)| SvcParam {
1251 key: svc_param_key_name(*key),
1252 value: svc_param_value(value),
1253 })
1254 .collect();
1255 (svcb.svc_priority, names.spell(&svcb.target_name), params)
1256}
1257
1258fn svc_param_key_name(key: SvcParamKey) -> String {
1261 match key {
1262 SvcParamKey::Mandatory => "mandatory".to_string(),
1263 SvcParamKey::Alpn => "alpn".to_string(),
1264 SvcParamKey::NoDefaultAlpn => "no-default-alpn".to_string(),
1265 SvcParamKey::Port => "port".to_string(),
1266 SvcParamKey::Ipv4Hint => "ipv4hint".to_string(),
1267 SvcParamKey::EchConfigList => "ech".to_string(),
1268 SvcParamKey::Ipv6Hint => "ipv6hint".to_string(),
1269 SvcParamKey::Key(_) | SvcParamKey::Key65535 | SvcParamKey::Unknown(_) => {
1270 format!("key{}", u16::from(key))
1271 }
1272 }
1273}
1274
1275fn svc_param_value(value: &SvcParamValue) -> String {
1279 use base64::{engine::general_purpose::STANDARD, Engine};
1280 fn join<T: std::fmt::Display>(items: impl Iterator<Item = T>) -> String {
1281 items
1282 .map(|item| item.to_string())
1283 .collect::<Vec<_>>()
1284 .join(",")
1285 }
1286 match value {
1287 SvcParamValue::Mandatory(keys) => join(keys.0.iter().map(|k| svc_param_key_name(*k))),
1288 SvcParamValue::Alpn(alpn) => join(alpn.0.iter().map(|id| {
1294 let listed = id.replace('\\', "\\\\").replace(',', "\\,");
1295 escape_char_string(listed.as_bytes())
1296 })),
1297 SvcParamValue::NoDefaultAlpn => String::new(),
1298 SvcParamValue::Port(port) => port.to_string(),
1299 SvcParamValue::Ipv4Hint(hint) => join(hint.0.iter().map(|a| a.0)),
1300 SvcParamValue::EchConfigList(ech) => STANDARD.encode(&ech.0),
1301 SvcParamValue::Ipv6Hint(hint) => join(hint.0.iter().map(|aaaa| aaaa.0)),
1302 SvcParamValue::Unknown(opaque) => escape_char_string(&opaque.0),
1303 }
1304}
1305
1306fn escape_char_string(bytes: &[u8]) -> String {
1311 let mut out = String::with_capacity(bytes.len());
1312 for &b in bytes {
1313 match b {
1314 b'"' | b'\\' => {
1315 out.push('\\');
1316 out.push(char::from(b));
1317 }
1318 0x21..=0x7E => out.push(char::from(b)),
1319 _ => out.push_str(&format!("\\{b:03}")),
1320 }
1321 }
1322 out
1323}
1324
1325pub(crate) fn wire_type(record_type: RecordType) -> Option<HickoryRecordType> {
1332 Some(match record_type {
1333 RecordType::A => HickoryRecordType::A,
1334 RecordType::AAAA => HickoryRecordType::AAAA,
1335 RecordType::CNAME => HickoryRecordType::CNAME,
1336 RecordType::MX => HickoryRecordType::MX,
1337 RecordType::NS => HickoryRecordType::NS,
1338 RecordType::TXT => HickoryRecordType::TXT,
1339 RecordType::SOA => HickoryRecordType::SOA,
1340 RecordType::PTR => HickoryRecordType::PTR,
1341 RecordType::SRV => HickoryRecordType::SRV,
1342 RecordType::CAA => HickoryRecordType::CAA,
1343 RecordType::DNSKEY => HickoryRecordType::DNSKEY,
1344 RecordType::DS => HickoryRecordType::DS,
1345 RecordType::CDS => HickoryRecordType::CDS,
1348 RecordType::CDNSKEY => HickoryRecordType::CDNSKEY,
1349 RecordType::HTTPS => HickoryRecordType::HTTPS,
1351 RecordType::SVCB => HickoryRecordType::SVCB,
1352 RecordType::TLSA => HickoryRecordType::TLSA,
1358 RecordType::SSHFP => HickoryRecordType::SSHFP,
1359 RecordType::NAPTR => HickoryRecordType::NAPTR,
1360 RecordType::ANY => return None,
1361 })
1362}
1363
1364fn unsupported_record_type(record_type: RecordType) -> SeerError {
1367 SeerError::DnsError(format!("unsupported record type: {}", record_type))
1368}
1369
1370fn hex_upper(bytes: &[u8]) -> String {
1373 bytes.iter().map(|b| format!("{:02X}", b)).collect()
1374}
1375
1376#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1380pub(crate) enum NameSpelling {
1381 Display,
1384 Ascii,
1389}
1390
1391impl NameSpelling {
1392 fn spell(self, name: &Name) -> String {
1393 match self {
1394 NameSpelling::Display => name.to_string(),
1395 NameSpelling::Ascii => name.to_ascii(),
1396 }
1397 }
1398}
1399
1400pub(crate) fn convert_rdata(
1408 record_type: RecordType,
1409 data: &HickoryRData,
1410 names: NameSpelling,
1411) -> Option<RecordData> {
1412 use hickory_resolver::proto::dnssec::rdata::DNSSECRData;
1413
1414 match (record_type, data) {
1415 (RecordType::A, HickoryRData::A(addr)) => Some(RecordData::A {
1416 address: addr.0.to_string(),
1417 }),
1418 (RecordType::AAAA, HickoryRData::AAAA(addr)) => Some(RecordData::AAAA {
1419 address: addr.0.to_string(),
1420 }),
1421 (RecordType::CNAME, HickoryRData::CNAME(cname)) => Some(RecordData::CNAME {
1422 target: names.spell(&cname.0),
1423 }),
1424 (RecordType::MX, HickoryRData::MX(mx)) => Some(RecordData::MX {
1425 preference: mx.preference,
1426 exchange: names.spell(&mx.exchange),
1427 }),
1428 (RecordType::NS, HickoryRData::NS(ns)) => Some(RecordData::NS {
1429 nameserver: names.spell(&ns.0),
1430 }),
1431 (RecordType::TXT, HickoryRData::TXT(txt)) => Some(RecordData::txt(
1432 txt.txt_data
1433 .iter()
1434 .map(|data| String::from_utf8_lossy(data).into_owned())
1435 .collect(),
1436 )),
1437 (RecordType::SOA, HickoryRData::SOA(soa)) => Some(RecordData::SOA {
1438 mname: names.spell(&soa.mname),
1439 rname: names.spell(&soa.rname),
1440 serial: soa.serial,
1441 refresh: soa.refresh as u32,
1447 retry: soa.retry as u32,
1448 expire: soa.expire as u32,
1449 minimum: soa.minimum,
1450 }),
1451 (RecordType::PTR, HickoryRData::PTR(ptr)) => Some(RecordData::PTR {
1452 target: names.spell(&ptr.0),
1453 }),
1454 (RecordType::SRV, HickoryRData::SRV(srv)) => Some(RecordData::SRV {
1455 priority: srv.priority,
1456 weight: srv.weight,
1457 port: srv.port,
1458 target: names.spell(&srv.target),
1459 }),
1460 (RecordType::CAA, HickoryRData::CAA(caa)) => {
1461 let (flags, tag, value) = parse_caa(caa);
1462 Some(RecordData::CAA { flags, tag, value })
1463 }
1464 (RecordType::DNSKEY, HickoryRData::DNSSEC(DNSSECRData::DNSKEY(dnskey))) => {
1465 use base64::{engine::general_purpose::STANDARD, Engine};
1466 let public_key_buf = dnskey.public_key();
1467 Some(RecordData::DNSKEY {
1468 flags: dnskey.flags(),
1469 protocol: 3,
1471 algorithm: u8::from(public_key_buf.algorithm()),
1472 public_key: STANDARD.encode(public_key_buf.public_bytes()),
1473 })
1474 }
1475 (RecordType::DS, HickoryRData::DNSSEC(DNSSECRData::DS(ds))) => Some(RecordData::DS {
1476 key_tag: ds.key_tag(),
1477 algorithm: u8::from(ds.algorithm()),
1478 digest_type: u8::from(ds.digest_type()),
1479 digest: hex_upper(ds.digest()),
1480 }),
1481 (RecordType::CDS, HickoryRData::DNSSEC(DNSSECRData::CDS(cds))) => Some(RecordData::CDS {
1483 key_tag: cds.key_tag(),
1484 algorithm: cds.algorithm().map_or(0, u8::from),
1485 digest_type: u8::from(cds.digest_type()),
1486 digest: hex_upper(cds.digest()),
1487 }),
1488 (RecordType::CDNSKEY, HickoryRData::DNSSEC(DNSSECRData::CDNSKEY(cdnskey))) => {
1489 use base64::{engine::general_purpose::STANDARD, Engine};
1490 let rdata = cdnskey.to_bytes().ok()?;
1497 Some(RecordData::CDNSKEY {
1498 flags: cdnskey.flags(),
1499 protocol: 3,
1501 algorithm: cdnskey.algorithm().map_or(0, u8::from),
1502 public_key: STANDARD.encode(rdata.get(4..)?),
1503 })
1504 }
1505 (RecordType::HTTPS, HickoryRData::HTTPS(https)) => {
1506 let (priority, target, params) = parse_svcb(&https.0, names);
1507 Some(RecordData::HTTPS {
1508 priority,
1509 target,
1510 params,
1511 })
1512 }
1513 (RecordType::SVCB, HickoryRData::SVCB(svcb)) => {
1514 let (priority, target, params) = parse_svcb(svcb, names);
1515 Some(RecordData::SVCB {
1516 priority,
1517 target,
1518 params,
1519 })
1520 }
1521 (RecordType::TLSA, HickoryRData::TLSA(tlsa)) => Some(RecordData::TLSA {
1522 cert_usage: u8::from(tlsa.cert_usage),
1523 selector: u8::from(tlsa.selector),
1524 matching: u8::from(tlsa.matching),
1525 cert_data: hex_upper(&tlsa.cert_data),
1526 }),
1527 (RecordType::SSHFP, HickoryRData::SSHFP(sshfp)) => Some(RecordData::SSHFP {
1528 algorithm: u8::from(sshfp.algorithm),
1529 fingerprint_type: u8::from(sshfp.fingerprint_type),
1530 fingerprint: hex_upper(&sshfp.fingerprint),
1531 }),
1532 (RecordType::NAPTR, HickoryRData::NAPTR(naptr)) => Some(RecordData::NAPTR {
1536 order: naptr.order,
1537 preference: naptr.preference,
1538 flags: String::from_utf8_lossy(&naptr.flags).into_owned(),
1539 services: String::from_utf8_lossy(&naptr.services).into_owned(),
1540 regexp: String::from_utf8_lossy(&naptr.regexp).into_owned(),
1541 replacement: names.spell(&naptr.replacement),
1542 }),
1543 _ => None,
1544 }
1545}
1546
1547pub(crate) fn from_wire_type(wire: HickoryRecordType) -> Option<RecordType> {
1552 RecordType::ALL
1553 .iter()
1554 .copied()
1555 .find(|t| wire_type(*t) == Some(wire))
1556}
1557
1558pub(crate) fn to_dns_record(record: &Record) -> Option<DnsRecord> {
1568 let record_type = from_wire_type(record.record_type())?;
1569 let data = convert_rdata(record_type, &record.data, NameSpelling::Ascii)?;
1570 Some(DnsRecord {
1571 name: owner_name(&record.name),
1572 record_type,
1573 ttl: record.ttl,
1574 data,
1575 })
1576}
1577
1578fn owner_name(name: &Name) -> String {
1583 let text = name.to_ascii();
1584 match text.strip_suffix('.') {
1585 Some(rest) if !rest.is_empty() => rest.to_string(),
1586 _ => text,
1587 }
1588}
1589
1590fn is_valid_srv_label(label: &str) -> bool {
1592 !label.is_empty()
1593 && label.len() <= 63
1594 && label.chars().all(|c| c.is_ascii_alphanumeric() || c == '-')
1595 && !label.starts_with('-')
1596 && !label.ends_with('-')
1597}
1598
1599#[cfg(test)]
1600mod tests {
1601 use super::*;
1609
1610 #[test]
1625 fn standard_opts_pin_server_order_so_ipv4_is_tried_first() {
1626 let mut opts = ResolverOpts::default();
1627 apply_standard_opts(&mut opts, Duration::from_secs(5));
1628 assert_eq!(
1629 opts.server_ordering_strategy,
1630 ServerOrderingStrategy::UserProvidedOrder,
1631 "QueryStatistics ordering is nondeterministic in a short-lived \
1632 process and can draw only black-holed IPv6 servers"
1633 );
1634 }
1635
1636 #[test]
1641 fn default_config_lists_enough_ipv4_servers_before_any_ipv6() {
1642 let config = ResolverConfig::udp_and_tcp(&GOOGLE);
1643 let ips: Vec<_> = config.name_servers().iter().map(|ns| ns.ip).collect();
1644 let concurrent = ResolverOpts::default().num_concurrent_reqs.max(1);
1645 assert!(
1646 ips.len() > concurrent,
1647 "expected more servers than the concurrency window: {ips:?}"
1648 );
1649 for (i, ip) in ips.iter().take(concurrent).enumerate() {
1650 assert!(
1651 ip.is_ipv4(),
1652 "server {i} in the concurrency window is not IPv4: {ips:?}"
1653 );
1654 }
1655 }
1656
1657 #[test]
1658 fn from_config_applies_dns_timeout() {
1659 let mut config = crate::config::SeerConfig::default();
1660 config.timeouts.dns_secs = 9;
1661 let resolver = DnsResolver::from_config(&config);
1662 assert_eq!(resolver.timeout, Duration::from_secs(9));
1663 assert_eq!(resolver.transport.timeout(), Duration::from_secs(9));
1664 let rebuilt = DnsResolver::new().with_timeout(Duration::from_secs(3));
1665 assert_eq!(rebuilt.transport.timeout(), Duration::from_secs(3));
1666 }
1667
1668 #[test]
1669 fn transport_budget_is_the_resolvers_attempts() {
1670 let mut opts = ResolverOpts::default();
1671 apply_standard_opts(&mut opts, Duration::from_secs(5));
1672 assert_eq!(
1673 opts.attempts as u32,
1674 crate::dns::transport::QUERY_BUDGET_TIMEOUTS
1675 );
1676 }
1677 use std::net::{Ipv4Addr, Ipv6Addr};
1678
1679 #[test]
1682 fn srv_label_accepts_alphanumeric_and_hyphen() {
1683 assert!(is_valid_srv_label("http"));
1684 assert!(is_valid_srv_label("ldap-tls"));
1685 assert!(is_valid_srv_label("a1"));
1686 assert!(is_valid_srv_label("tcp"));
1687 }
1688
1689 #[test]
1690 fn srv_label_rejects_empty() {
1691 assert!(!is_valid_srv_label(""));
1692 }
1693
1694 #[test]
1695 fn srv_label_rejects_leading_or_trailing_hyphen() {
1696 assert!(!is_valid_srv_label("-http"));
1697 assert!(!is_valid_srv_label("http-"));
1698 assert!(!is_valid_srv_label("-"));
1699 }
1700
1701 #[test]
1702 fn srv_label_rejects_dots() {
1703 assert!(!is_valid_srv_label("http.evil"));
1706 assert!(!is_valid_srv_label("a.b"));
1707 }
1708
1709 #[test]
1710 fn srv_label_rejects_special_chars() {
1711 assert!(!is_valid_srv_label("http evil"));
1712 assert!(!is_valid_srv_label("http/evil"));
1713 assert!(!is_valid_srv_label("http\0"));
1714 assert!(!is_valid_srv_label("http\n"));
1715 }
1716
1717 #[test]
1718 fn srv_label_rejects_over_63_chars() {
1719 let too_long = "a".repeat(64);
1720 assert!(!is_valid_srv_label(&too_long));
1721 let exactly_63 = "a".repeat(63);
1722 assert!(is_valid_srv_label(&exactly_63));
1723 }
1724
1725 #[test]
1728 fn classify_ns_presence_absent_on_empty_ok() {
1729 let r: Result<Vec<DnsRecord>> = Ok(vec![]);
1731 assert_eq!(classify_ns_presence(&r), DnsPresence::Absent);
1732 }
1733
1734 #[test]
1735 fn classify_ns_presence_present_on_records() {
1736 let rec = DnsRecord {
1737 name: "example.test.".to_string(),
1738 record_type: RecordType::NS,
1739 ttl: 3600,
1740 data: RecordData::NS {
1741 nameserver: "ns1.example.net.".to_string(),
1742 },
1743 };
1744 let r: Result<Vec<DnsRecord>> = Ok(vec![rec]);
1745 assert_eq!(classify_ns_presence(&r), DnsPresence::Present);
1746 }
1747
1748 #[test]
1749 fn classify_ns_presence_unknown_on_error() {
1750 let r: Result<Vec<DnsRecord>> = Err(SeerError::DnsError("servfail".to_string()));
1751 assert_eq!(classify_ns_presence(&r), DnsPresence::Unknown);
1752 }
1753
1754 #[test]
1757 fn reverse_dns_name_formats_ipv4_correctly() {
1758 let ip: IpAddr = Ipv4Addr::new(192, 0, 2, 1).into();
1759 assert_eq!(reverse_dns_name(&ip), "1.2.0.192.in-addr.arpa");
1760 }
1761
1762 #[test]
1763 fn reverse_dns_name_formats_ipv6_correctly() {
1764 let ip: IpAddr = Ipv6Addr::LOCALHOST.into();
1766 let name = reverse_dns_name(&ip);
1767 assert!(
1768 name.ends_with(".ip6.arpa"),
1769 "must end with .ip6.arpa; got: {}",
1770 name
1771 );
1772 assert!(
1774 name.starts_with("1."),
1775 "expected '1.' prefix, got: {}",
1776 name
1777 );
1778 assert_eq!(name.len(), 72);
1780 }
1781
1782 #[tokio::test]
1785 async fn custom_resolver_rejects_invalid_input() {
1786 let r = DnsResolver::new();
1791 let err = r.create_custom_resolver("..").await.unwrap_err();
1792 let msg = err.to_string().to_lowercase();
1793 assert!(
1794 msg.contains("dns resolution failed") || msg.contains("invalid"),
1795 "expected resolution failure, got: {}",
1796 msg
1797 );
1798 }
1799
1800 #[tokio::test]
1801 async fn custom_resolver_rejects_private_ipv4() {
1802 let r = DnsResolver::new();
1805 for reserved in ["127.0.0.1", "10.0.0.1", "192.168.1.1", "169.254.169.254"] {
1806 let err = r.create_custom_resolver(reserved).await.unwrap_err();
1807 let msg = err.to_string().to_lowercase();
1808 assert!(
1809 msg.contains("blocked") || msg.contains("reserved"),
1810 "reserved IP {} must be rejected, got error: {}",
1811 reserved,
1812 msg
1813 );
1814 }
1815 }
1816
1817 #[tokio::test]
1818 async fn custom_resolver_rejects_loopback_ipv6() {
1819 let r = DnsResolver::new();
1820 let err = r.create_custom_resolver("::1").await.unwrap_err();
1821 let msg = err.to_string().to_lowercase();
1822 assert!(
1823 msg.contains("blocked") || msg.contains("reserved"),
1824 "::1 must be rejected, got error: {}",
1825 msg
1826 );
1827 }
1828
1829 #[tokio::test]
1830 async fn custom_resolver_accepts_public_ipv4() {
1831 let r = DnsResolver::new();
1833 let result = r.create_custom_resolver("8.8.8.8").await;
1834 assert!(
1835 result.is_ok(),
1836 "8.8.8.8 must be accepted as a public nameserver, got: {:?}",
1837 result.err()
1838 );
1839 }
1840
1841 #[tokio::test]
1847 async fn custom_resolver_rejects_private_ip_for_dot_and_doh() {
1848 let r = DnsResolver::new();
1849 for reserved in [
1850 "tls://127.0.0.1",
1851 "tls://192.168.1.1:853",
1852 "tls://[::1]",
1853 "https://10.0.0.1/dns-query",
1854 "https://169.254.169.254",
1855 "tls://[fd00::1]:853",
1856 ] {
1857 let err = r.create_custom_resolver(reserved).await.unwrap_err();
1858 let msg = err.to_string().to_lowercase();
1859 assert!(
1860 msg.contains("blocked") || msg.contains("reserved"),
1861 "reserved spec {} must be rejected, got error: {}",
1862 reserved,
1863 msg
1864 );
1865 }
1866 let err = r
1869 .create_custom_resolver("https://[fd00::1]:443/dns-query")
1870 .await
1871 .unwrap_err();
1872 assert!(matches!(err, SeerError::InvalidInput(_)), "{err:?}");
1873 }
1874
1875 #[tokio::test]
1876 async fn custom_resolver_accepts_public_dot_and_doh_literals() {
1877 let r = DnsResolver::new();
1880 for spec in ["tls://1.1.1.1", "https://8.8.8.8/dns-query"] {
1881 let result = r.create_custom_resolver(spec).await;
1882 assert!(
1883 result.is_ok(),
1884 "{} must be accepted, got: {:?}",
1885 spec,
1886 result.err()
1887 );
1888 }
1889 }
1890
1891 #[tokio::test]
1892 async fn custom_resolver_rejects_unknown_scheme() {
1893 let r = DnsResolver::new();
1894 let err = r.create_custom_resolver("ftp://8.8.8.8").await.unwrap_err();
1895 assert!(
1896 matches!(err, SeerError::InvalidInput(_)),
1897 "unknown scheme must be an input error, got: {err:?}"
1898 );
1899 }
1900
1901 fn spec(s: &str) -> NameserverSpec {
1904 NameserverSpec::parse(s).unwrap_or_else(|e| panic!("{s:?} must parse: {e}"))
1905 }
1906
1907 #[test]
1908 fn tcp_only_swaps_udp_for_tcp_on_the_same_port() {
1909 let default = tcp_only(&default_upstream_config());
1910 assert!(!default.name_servers().is_empty());
1911 for server in default.name_servers() {
1912 assert_eq!(server.connections.len(), 1, "{server:?}");
1913 assert!(matches!(
1914 server.connections[0].protocol,
1915 ProtocolConfig::Tcp
1916 ));
1917 assert_eq!(server.connections[0].port, 53);
1918 }
1919
1920 let udp_only = tcp_only(&single_server_config(
1921 IpAddr::from([192, 0, 2, 1]),
1922 5353,
1923 false,
1924 ));
1925 let server = &udp_only.name_servers()[0];
1926 assert_eq!(server.connections.len(), 1);
1927 assert!(matches!(
1928 server.connections[0].protocol,
1929 ProtocolConfig::Tcp
1930 ));
1931 assert_eq!(server.connections[0].port, 5353);
1932 }
1933
1934 #[test]
1935 fn tcp_only_keeps_encrypted_transports() {
1936 let spec = NameserverSpec::parse("tls://1.1.1.1").unwrap();
1937 let config = build_upstream_config(&spec, &[IpAddr::from([1, 1, 1, 1])], None);
1938 let tcp = tcp_only(&config);
1939 assert_eq!(
1940 format!("{:?}", tcp.name_servers()),
1941 format!("{:?}", config.name_servers())
1942 );
1943 }
1944
1945 #[test]
1946 fn upstream_config_udp_defaults() {
1947 use hickory_resolver::config::ProtocolConfig;
1948
1949 let ip: IpAddr = "8.8.8.8".parse().unwrap();
1950 let config = build_upstream_config(&spec("8.8.8.8"), &[ip], None);
1951 let servers = config.name_servers();
1952 assert_eq!(servers.len(), 1);
1953 assert_eq!(servers[0].ip, ip);
1954 assert_eq!(servers[0].connections.len(), 1);
1955 assert_eq!(servers[0].connections[0].port, 53);
1956 assert!(matches!(
1957 servers[0].connections[0].protocol,
1958 ProtocolConfig::Udp
1959 ));
1960 }
1961
1962 #[test]
1963 fn upstream_config_udp_explicit_port() {
1964 let ip: IpAddr = "9.9.9.9".parse().unwrap();
1965 let config = build_upstream_config(&spec("9.9.9.9:5353"), &[ip], None);
1966 assert_eq!(config.name_servers()[0].connections[0].port, 5353);
1967 }
1968
1969 #[test]
1970 fn upstream_config_tls_sets_protocol_port_and_server_name() {
1971 use hickory_resolver::config::ProtocolConfig;
1972
1973 let ip: IpAddr = "9.9.9.9".parse().unwrap();
1974 let config = build_upstream_config(&spec("tls://dns.quad9.net"), &[ip], None);
1975 let ns = &config.name_servers()[0];
1976 assert_eq!(ns.ip, ip);
1977 assert_eq!(ns.connections.len(), 1);
1978 assert_eq!(ns.connections[0].port, 853);
1979 match &ns.connections[0].protocol {
1980 ProtocolConfig::Tls { server_name } => {
1981 assert_eq!(&**server_name, "dns.quad9.net");
1982 }
1983 other => panic!("expected Tls protocol, got {other:?}"),
1984 }
1985 }
1986
1987 #[test]
1988 fn upstream_config_tls_ip_literal_uses_ip_as_server_name() {
1989 use hickory_resolver::config::ProtocolConfig;
1990
1991 let ip: IpAddr = "1.1.1.1".parse().unwrap();
1992 let config = build_upstream_config(&spec("tls://1.1.1.1"), &[ip], None);
1993 match &config.name_servers()[0].connections[0].protocol {
1994 ProtocolConfig::Tls { server_name } => assert_eq!(&**server_name, "1.1.1.1"),
1995 other => panic!("expected Tls protocol, got {other:?}"),
1996 }
1997 }
1998
1999 #[test]
2000 fn upstream_config_https_sets_protocol_port_path_and_server_name() {
2001 use hickory_resolver::config::ProtocolConfig;
2002
2003 let ip: IpAddr = "104.16.248.249".parse().unwrap();
2004 let config = build_upstream_config(&spec("https://cloudflare-dns.com"), &[ip], None);
2005 let ns = &config.name_servers()[0];
2006 assert_eq!(ns.connections[0].port, 443);
2007 match &ns.connections[0].protocol {
2008 ProtocolConfig::Https { server_name, path } => {
2009 assert_eq!(&**server_name, "cloudflare-dns.com");
2010 assert_eq!(&**path, "/dns-query");
2011 }
2012 other => panic!("expected Https protocol, got {other:?}"),
2013 }
2014 }
2015
2016 #[test]
2017 fn upstream_config_https_custom_port_and_path() {
2018 use hickory_resolver::config::ProtocolConfig;
2019
2020 let ip: IpAddr = "8.8.8.8".parse().unwrap();
2021 let config = build_upstream_config(&spec("https://dns.google:8443/resolve"), &[ip], None);
2022 let ns = &config.name_servers()[0];
2023 assert_eq!(ns.connections[0].port, 8443);
2024 match &ns.connections[0].protocol {
2025 ProtocolConfig::Https { server_name, path } => {
2026 assert_eq!(&**server_name, "dns.google");
2027 assert_eq!(&**path, "/resolve");
2028 }
2029 other => panic!("expected Https protocol, got {other:?}"),
2030 }
2031 }
2032
2033 #[test]
2034 fn upstream_config_multiple_ips_share_spec() {
2035 use hickory_resolver::config::ProtocolConfig;
2038
2039 let ips: Vec<IpAddr> = vec![
2040 "9.9.9.9".parse().unwrap(),
2041 "149.112.112.112".parse().unwrap(),
2042 ];
2043 let config = build_upstream_config(&spec("tls://dns.quad9.net"), &ips, None);
2044 let servers = config.name_servers();
2045 assert_eq!(servers.len(), 2);
2046 for (ns, expected_ip) in servers.iter().zip(&ips) {
2047 assert_eq!(&ns.ip, expected_ip);
2048 assert_eq!(ns.connections[0].port, 853);
2049 assert!(matches!(
2050 &ns.connections[0].protocol,
2051 ProtocolConfig::Tls { server_name } if &**server_name == "dns.quad9.net"
2052 ));
2053 }
2054 }
2055
2056 #[test]
2064 fn upstream_config_orders_ipv4_before_ipv6() {
2065 let ips: Vec<IpAddr> = [
2067 "2606:4700::6810:f8f9",
2068 "2606:4700::6810:f9f9",
2069 "104.16.248.249",
2070 "104.16.249.249",
2071 ]
2072 .iter()
2073 .map(|s| s.parse().unwrap())
2074 .collect();
2075 let config =
2076 build_upstream_config(&spec("https://cloudflare-dns.com/dns-query"), &ips, None);
2077 let order: Vec<IpAddr> = config.name_servers().iter().map(|ns| ns.ip).collect();
2078 assert_eq!(
2079 order,
2080 [ips[2], ips[3], ips[0], ips[1]],
2081 "IPv4 first, each family in resolved order, nothing dropped"
2082 );
2083 let concurrent = ResolverOpts::default().num_concurrent_reqs.max(1);
2084 assert!(
2085 order.iter().take(concurrent).all(IpAddr::is_ipv4),
2086 "the parallel window must be all IPv4: {order:?}"
2087 );
2088 }
2089
2090 #[test]
2091 fn upstream_config_test_port_override_wins() {
2092 let ip: IpAddr = "127.0.0.1".parse().unwrap();
2094 let config = build_upstream_config(&spec("127.0.0.1"), &[ip], Some(9999));
2095 assert_eq!(config.name_servers()[0].connections[0].port, 9999);
2096 }
2097
2098 #[tokio::test]
2099 async fn query_asks_dot_and_doh_servers_through_the_vetted_config() {
2100 use hickory_resolver::config::ProtocolConfig;
2106
2107 let r = DnsResolver::new();
2108 let dot = r.custom_upstream_config("tls://1.1.1.1").await.unwrap();
2109 let [server] = dot.name_servers() else {
2110 panic!("one server per address: {dot:?}");
2111 };
2112 assert_eq!(server.ip, IpAddr::from([1, 1, 1, 1]));
2113 assert_eq!(server.connections.len(), 1);
2114 assert_eq!(server.connections[0].port, 853);
2115 assert!(matches!(
2116 &server.connections[0].protocol,
2117 ProtocolConfig::Tls { server_name } if &**server_name == "1.1.1.1"
2118 ));
2119
2120 let doh = r
2121 .custom_upstream_config("https://8.8.8.8/dns-query")
2122 .await
2123 .unwrap();
2124 let [server] = doh.name_servers() else {
2125 panic!("one server per address: {doh:?}");
2126 };
2127 assert_eq!(server.connections[0].port, 443);
2128 assert!(matches!(
2129 &server.connections[0].protocol,
2130 ProtocolConfig::Https { server_name, path }
2131 if &**server_name == "8.8.8.8" && &**path == "/dns-query"
2132 ));
2133
2134 let udp = r.custom_upstream_config("9.9.9.9").await.unwrap();
2137 assert_eq!(udp.name_servers()[0].connections[0].port, 53);
2138 assert!(matches!(
2139 udp.name_servers()[0].connections[0].protocol,
2140 ProtocolConfig::Udp
2141 ));
2142 let Upstream::Direct { config, .. } = r.query_upstream("seer.test", None).await.unwrap()
2144 else {
2145 panic!("a public name is sent to the default upstream");
2146 };
2147 assert!(config.name_servers()[0].ip.is_ipv4());
2148 }
2149
2150 #[tokio::test]
2153 #[ignore = "live network — DoT query against Cloudflare"]
2154 async fn live_query_over_dot() {
2155 let result = DnsResolver::new()
2156 .query("example.com", RecordType::A, Some("tls://1.1.1.1"))
2157 .await
2158 .expect("DoT query should get a response");
2159 assert_eq!(result.status, DnsStatus::NoError);
2160 assert!(result.flags.contains(&"qr".to_string()), "{result:?}");
2161 assert!(result.records().count() > 0, "expected A records over DoT");
2162 }
2163
2164 #[tokio::test]
2165 #[ignore = "live network — DoH query against Cloudflare"]
2166 async fn live_query_over_doh() {
2167 let result = DnsResolver::new()
2168 .query(
2169 "example.com",
2170 RecordType::A,
2171 Some("https://cloudflare-dns.com/dns-query"),
2172 )
2173 .await
2174 .expect("DoH query should get a response");
2175 assert_eq!(result.status, DnsStatus::NoError);
2176 assert!(result.records().count() > 0, "expected A records over DoH");
2177 }
2178
2179 #[tokio::test]
2180 #[ignore = "live network — NXDOMAIN keeps its header"]
2181 async fn live_query_nxdomain_has_flags_and_the_soa() {
2182 let result = DnsResolver::new()
2183 .query("seer-does-not-exist.example", RecordType::A, None)
2185 .await
2186 .expect("a negative answer is a result");
2187 assert_eq!(result.status, DnsStatus::NxDomain);
2188 assert!(result.flags.contains(&"qr".to_string()), "{result:?}");
2189 assert!(
2190 result
2191 .authority
2192 .iter()
2193 .any(|r| r.record_type == RecordType::SOA),
2194 "{result:?}"
2195 );
2196 }
2197
2198 #[tokio::test]
2199 #[ignore = "live network — DoT query against Cloudflare"]
2200 async fn live_resolve_over_dot() {
2201 let r = DnsResolver::new();
2202 let records = r
2203 .resolve("example.com", RecordType::A, Some("tls://1.1.1.1"))
2204 .await
2205 .expect("DoT lookup should succeed");
2206 assert!(!records.is_empty(), "expected A records over DoT");
2207 }
2208
2209 #[tokio::test]
2210 #[ignore = "live network — DoH query against Cloudflare"]
2211 async fn live_resolve_over_doh() {
2212 let r = DnsResolver::new();
2213 let records = r
2214 .resolve(
2215 "example.com",
2216 RecordType::A,
2217 Some("https://cloudflare-dns.com/dns-query"),
2218 )
2219 .await
2220 .expect("DoH lookup should succeed");
2221 assert!(!records.is_empty(), "expected A records over DoH");
2222 }
2223
2224 #[tokio::test]
2227 async fn resolve_srv_rejects_invalid_service_label() {
2228 let r = DnsResolver::new();
2229 let result = r
2231 .resolve_srv_core(&r.default_target(), "http.evil", "tcp", "example.com")
2232 .await;
2233 assert!(result.is_err());
2234 let msg = result.unwrap_err().to_string().to_lowercase();
2235 assert!(
2236 msg.contains("invalid srv service"),
2237 "expected SRV service validation error, got: {}",
2238 msg
2239 );
2240 }
2241
2242 #[tokio::test]
2243 async fn resolve_srv_rejects_invalid_protocol_label() {
2244 let r = DnsResolver::new();
2245 let result = r
2246 .resolve_srv_core(&r.default_target(), "http", "tcp.evil", "example.com")
2247 .await;
2248 assert!(result.is_err());
2249 let msg = result.unwrap_err().to_string().to_lowercase();
2250 assert!(
2251 msg.contains("invalid srv protocol"),
2252 "expected SRV protocol validation error, got: {}",
2253 msg
2254 );
2255 }
2256
2257 #[tokio::test]
2258 async fn resolve_srv_normalizes_and_validates_domain_input() {
2259 let r = DnsResolver::new();
2263 let result = r
2264 .resolve("_http._tcp.not a valid domain", RecordType::SRV, None)
2265 .await;
2266 assert!(
2267 matches!(result, Err(SeerError::InvalidDomain(_))),
2268 "expected InvalidDomain from domain validation, got: {result:?}"
2269 );
2270 }
2271
2272 #[tokio::test]
2275 async fn resolve_normalizes_uppercase_domain_input() {
2276 let r = DnsResolver::new();
2281 let result = r.resolve(".bad.example", RecordType::A, None).await;
2282 assert!(result.is_err(), "leading-dot domain must be rejected");
2283 }
2284
2285 #[test]
2290 fn parse_srv_query_extracts_service_proto_and_name() {
2291 assert_eq!(
2292 parse_srv_query("_sip._tcp.example.com"),
2293 Some((
2294 "sip".to_string(),
2295 "tcp".to_string(),
2296 "example.com".to_string()
2297 ))
2298 );
2299 }
2300
2301 #[test]
2302 fn parse_srv_query_keeps_multilabel_domain() {
2303 assert_eq!(
2304 parse_srv_query("_sip._tcp.sip.voice.google.com"),
2305 Some((
2306 "sip".to_string(),
2307 "tcp".to_string(),
2308 "sip.voice.google.com".to_string()
2309 ))
2310 );
2311 }
2312
2313 #[test]
2314 fn fqdn_appends_root_dot_once() {
2315 assert_eq!(fqdn("example.com"), "example.com.");
2316 assert_eq!(fqdn("example.com."), "example.com.");
2317 }
2318
2319 #[test]
2320 fn parse_srv_query_rejects_bare_domain() {
2321 assert_eq!(parse_srv_query("example.com"), None);
2322 }
2323
2324 #[test]
2325 fn parse_srv_query_rejects_missing_proto_label() {
2326 assert_eq!(parse_srv_query("_sip.example.com"), None);
2328 }
2329
2330 #[tokio::test]
2331 async fn resolve_rejects_bare_domain_for_srv_as_input_error() {
2332 let r = DnsResolver::new();
2337 let err = r
2338 .resolve("example.com", RecordType::SRV, None)
2339 .await
2340 .expect_err("bare-domain SRV must error");
2341 assert!(
2342 matches!(err, SeerError::InvalidInput(_)),
2343 "bare-domain SRV should be an input error, got: {err:?}"
2344 );
2345 assert!(err.to_string().contains("_service._proto"));
2346 }
2347
2348 #[tokio::test]
2349 #[ignore = "live network"]
2350 async fn resolve_srv_via_dig_style_name_returns_records() {
2351 let r = DnsResolver::new();
2354 let records = r
2355 .resolve("_caldavs._tcp.google.com", RecordType::SRV, None)
2356 .await
2357 .expect("dig-style SRV lookup should succeed");
2358 assert!(!records.is_empty(), "expected SRV records");
2359 assert!(records.iter().all(|r| r.record_type == RecordType::SRV));
2360 }
2361
2362 #[tokio::test]
2363 #[ignore = "live network"]
2364 async fn resolve_naptr_returns_records() {
2365 let r = DnsResolver::new();
2367 let records = r
2368 .resolve("sip2sip.info", RecordType::NAPTR, None)
2369 .await
2370 .expect("NAPTR lookup should succeed");
2371 assert!(!records.is_empty(), "expected NAPTR records");
2372 assert!(records.iter().all(|r| r.record_type == RecordType::NAPTR));
2373 }
2374
2375 #[test]
2378 fn prepare_query_passes_ipv6_literal_through_for_ptr() {
2379 let out = prepare_query("2606:4700:4700::1111", RecordType::PTR).unwrap();
2385 assert_eq!(out, "2606:4700:4700::1111");
2386 }
2387
2388 #[test]
2389 fn prepare_query_passes_ipv6_loopback_through_for_ptr() {
2390 let out = prepare_query("::1", RecordType::PTR).unwrap();
2391 assert_eq!(out, "::1");
2392 }
2393
2394 #[test]
2395 fn prepare_query_passes_ipv4_literal_through_for_ptr() {
2396 let out = prepare_query("8.8.8.8", RecordType::PTR).unwrap();
2397 assert_eq!(out, "8.8.8.8");
2398 }
2399
2400 #[test]
2401 fn prepare_query_normalizes_non_ip_ptr_names() {
2402 let out = prepare_query("1.1.1.1.in-addr.arpa", RecordType::PTR).unwrap();
2404 assert_eq!(out, "1.1.1.1.in-addr.arpa");
2405 }
2406
2407 #[test]
2408 fn prepare_query_keeps_www_for_record_queries() {
2409 let out = prepare_query("HTTPS://WWW.Example.com/path", RecordType::A).unwrap();
2413 assert_eq!(out, "www.example.com");
2414 let out = prepare_query("www.example.com", RecordType::CNAME).unwrap();
2415 assert_eq!(out, "www.example.com");
2416 }
2417
2418 #[test]
2419 fn prepare_query_still_normalizes_scheme_port_path_and_case() {
2420 let out = prepare_query("https://API.Example.COM:8443/v1?q=1#frag", RecordType::A).unwrap();
2421 assert_eq!(out, "api.example.com");
2422 let out = prepare_query("example.com.", RecordType::MX).unwrap();
2423 assert_eq!(out, "example.com");
2424 assert!(prepare_query("not a domain", RecordType::A).is_err());
2425 }
2426
2427 #[test]
2428 fn prepare_query_accepts_a_leading_wildcard_label() {
2429 let out = prepare_query("*.Example.com.", RecordType::A).unwrap();
2433 assert_eq!(out, "*.example.com");
2434 let out = prepare_query("*.1.168.192.in-addr.arpa", RecordType::PTR).unwrap();
2435 assert_eq!(out, "*.1.168.192.in-addr.arpa");
2436 assert!(prepare_query("a.*.example.com", RecordType::A).is_err());
2438 assert!(prepare_query("a*.example.com", RecordType::A).is_err());
2439 }
2440
2441 #[test]
2442 fn prepare_query_runs_ptr_ip_literal_through_the_normalizer() {
2443 let deny_arpa = |name: &str| -> Result<String> {
2449 if name.ends_with(".arpa") {
2450 Err(SeerError::DomainNotAllowed {
2451 domain: name.to_string(),
2452 tld: "arpa".to_string(),
2453 })
2454 } else {
2455 Ok(name.to_string())
2456 }
2457 };
2458 for ip in ["192.0.2.1", "2606:4700:4700::1111"] {
2459 let err = prepare_query_with(ip, RecordType::PTR, deny_arpa)
2460 .expect_err("PTR for an IP literal must obey the normalizer");
2461 assert!(matches!(err, SeerError::DomainNotAllowed { .. }), "{err:?}");
2462 }
2463 assert_eq!(
2465 prepare_query_with("192.0.2.1", RecordType::PTR, normalize_query_name).unwrap(),
2466 "192.0.2.1"
2467 );
2468 }
2469
2470 #[test]
2471 fn from_wire_type_inverts_wire_type_for_every_single_type() {
2472 for record_type in RecordType::ALL.iter().copied() {
2473 match wire_type(record_type) {
2474 Some(wire) => assert_eq!(from_wire_type(wire), Some(record_type)),
2475 None => assert_eq!(record_type, RecordType::ANY),
2476 }
2477 }
2478 assert_eq!(
2481 from_wire_type(HickoryRecordType::SRV),
2482 Some(RecordType::SRV)
2483 );
2484 assert_eq!(from_wire_type(HickoryRecordType::RRSIG), None);
2485 }
2486
2487 #[test]
2488 fn to_dns_record_keeps_the_real_owner_and_type() {
2489 let cname = Record::from_rdata(
2490 Name::from_ascii("www.seer.test.").unwrap(),
2491 300,
2492 HickoryRData::CNAME(hickory_resolver::proto::rr::rdata::CNAME(
2493 Name::from_ascii("edge.cdn.test.").unwrap(),
2494 )),
2495 );
2496 let converted = to_dns_record(&cname).expect("CNAME is modeled");
2497 assert_eq!(converted.name, "www.seer.test");
2498 assert_eq!(converted.record_type, RecordType::CNAME);
2499 assert_eq!(converted.ttl, 300);
2500 assert_eq!(converted.data.to_string(), "edge.cdn.test.");
2501
2502 let a = Record::from_rdata(
2503 Name::from_ascii("edge.cdn.test.").unwrap(),
2504 60,
2505 HickoryRData::A(hickory_resolver::proto::rr::rdata::A(Ipv4Addr::new(
2506 192, 0, 2, 7,
2507 ))),
2508 );
2509 let converted = to_dns_record(&a).expect("A is modeled");
2510 assert_eq!(converted.name, "edge.cdn.test");
2511 assert_eq!(converted.record_type, RecordType::A);
2512
2513 assert_eq!(owner_name(&Name::root()), ".");
2514 assert_eq!(
2516 owner_name(&Name::from_utf8("bücher.seer.test.").unwrap()),
2517 "xn--bcher-kva.seer.test"
2518 );
2519 }
2520
2521 #[test]
2522 fn to_dns_record_spells_rdata_names_as_a_labels_where_resolve_keeps_unicode() {
2523 use hickory_resolver::proto::rr::rdata::{CNAME, MX, SOA, SRV};
2524 let idn = |name: &str| Name::from_utf8(name).unwrap();
2525 let data = |rdata: HickoryRData| {
2526 let record = Record::from_rdata(idn("xn--bcher-kva.seer.test."), 300, rdata);
2527 to_dns_record(&record).expect("modeled").data.to_string()
2528 };
2529 assert_eq!(
2533 data(HickoryRData::CNAME(CNAME(idn("edge.café.test.")))),
2534 "edge.xn--caf-dma.test."
2535 );
2536 assert_eq!(
2537 data(HickoryRData::MX(MX::new(10, idn("mail.café.test.")))),
2538 "10 mail.xn--caf-dma.test."
2539 );
2540 assert_eq!(
2541 data(HickoryRData::SRV(SRV::new(
2542 1,
2543 5,
2544 443,
2545 idn("sip.café.test.")
2546 ))),
2547 "1 5 443 sip.xn--caf-dma.test."
2548 );
2549 assert_eq!(
2550 data(HickoryRData::SOA(SOA::new(
2551 idn("ns1.café.test."),
2552 idn("hostmaster.café.test."),
2553 1,
2554 7200,
2555 3600,
2556 1209600,
2557 300,
2558 ))),
2559 "ns1.xn--caf-dma.test. hostmaster.xn--caf-dma.test. 1 7200 3600 1209600 300"
2560 );
2561
2562 let cname = HickoryRData::CNAME(CNAME(idn("edge.café.test.")));
2564 assert_eq!(
2565 convert_rdata(RecordType::CNAME, &cname, NameSpelling::Display)
2566 .expect("modeled")
2567 .to_string(),
2568 "edge.café.test."
2569 );
2570 }
2571
2572 #[test]
2573 fn parse_caa_keeps_reserved_flag_bits() {
2574 let mut caa = CAA::new_issue(
2577 true,
2578 Some(Name::from_ascii("letsencrypt.org").unwrap()),
2579 vec![],
2580 );
2581 caa.reserved_flags = 0x01;
2582 let (flags, tag, value) = parse_caa(&caa);
2583 assert_eq!(flags, 0x81);
2584 assert_eq!(tag, "issue");
2585 assert_eq!(value, "letsencrypt.org");
2586 }
2587
2588 #[test]
2589 fn alpn_ids_are_escaped_at_both_levels() {
2590 use hickory_resolver::proto::rr::rdata::svcb::Alpn;
2591 let alpn = |ids: &[&str]| {
2592 svc_param_value(&SvcParamValue::Alpn(Alpn(
2593 ids.iter().map(|id| id.to_string()).collect(),
2594 )))
2595 };
2596 assert_eq!(alpn(&["h3", "h2"]), "h3,h2");
2597 assert_eq!(alpn(&["a,b"]), r"a\\,b");
2602 assert_eq!(alpn(&[r"x\y"]), r"x\\\\y");
2603 assert_eq!(alpn(&[r"f\oo,bar", "h2"]), r"f\\\\oo\\,bar,h2");
2605 }
2606
2607 use crate::dns::query::DnsStatus;
2618 use crate::dns::test_support::{
2619 a_rdata, cname_rdata, mock_dns_resolver, mock_dns_resolver_default, record, spawn_mock_dns,
2620 spawn_mock_dns_fn, spawn_mock_dns_fn_with_tcp, spawn_mock_dns_split, MockMode, MockReply,
2621 };
2622 use hickory_resolver::proto::op::ResponseCode;
2623
2624 async fn mock_zone_lookup(record_type: RecordType, domain: &str) -> Vec<DnsRecord> {
2625 let port = spawn_mock_dns(MockMode::Zone).await;
2626 mock_dns_resolver(port)
2627 .resolve(domain, record_type, Some("127.0.0.1"))
2628 .await
2629 .unwrap_or_else(|e| panic!("{record_type} lookup against mock must succeed: {e}"))
2630 }
2631
2632 #[tokio::test]
2633 async fn mock_resolve_a_returns_addresses() {
2634 let records = mock_zone_lookup(RecordType::A, "seer.test").await;
2635 assert_eq!(records.len(), 2);
2636 assert!(records.iter().all(|r| r.record_type == RecordType::A));
2637 assert_eq!(records[0].name, "seer.test");
2638 assert_eq!(records[0].ttl, 300);
2639 let addresses: Vec<String> = records
2640 .iter()
2641 .map(|r| match &r.data {
2642 RecordData::A { address } => address.clone(),
2643 other => panic!("expected A data, got {other:?}"),
2644 })
2645 .collect();
2646 assert!(addresses.contains(&"192.0.2.1".to_string()));
2647 assert!(addresses.contains(&"192.0.2.2".to_string()));
2648 }
2649
2650 #[tokio::test]
2651 async fn mock_resolve_mx_sorts_by_preference() {
2652 let records = mock_zone_lookup(RecordType::MX, "seer.test").await;
2653 let prefs: Vec<u16> = records
2654 .iter()
2655 .map(|r| match &r.data {
2656 RecordData::MX { preference, .. } => *preference,
2657 other => panic!("expected MX data, got {other:?}"),
2658 })
2659 .collect();
2660 assert_eq!(prefs, vec![10, 20, 30]);
2662 assert!(matches!(
2663 &records[0].data,
2664 RecordData::MX { exchange, .. } if exchange == "a.mail.seer.test."
2665 ));
2666 }
2667
2668 #[tokio::test]
2669 async fn mock_resolve_txt_joins_character_strings() {
2670 let records = mock_zone_lookup(RecordType::TXT, "seer.test").await;
2671 assert_eq!(records.len(), 1);
2672 match &records[0].data {
2673 RecordData::TXT { text, strings } => {
2674 assert_eq!(text, "v=spf1 -all");
2675 assert_eq!(strings, &["v=spf1 ", "-all"], "the strings as sent");
2676 }
2677 other => panic!("expected TXT data, got {other:?}"),
2678 }
2679 assert_eq!(records[0].data.to_string(), r#""v=spf1 " "-all""#);
2680 }
2681
2682 #[tokio::test]
2683 async fn mock_resolve_soa_maps_all_fields() {
2684 let records = mock_zone_lookup(RecordType::SOA, "seer.test").await;
2685 assert_eq!(records.len(), 1);
2686 match &records[0].data {
2687 RecordData::SOA {
2688 mname,
2689 rname,
2690 serial,
2691 refresh,
2692 retry,
2693 expire,
2694 minimum,
2695 } => {
2696 assert_eq!(mname, "ns1.seer.test.");
2697 assert_eq!(rname, "hostmaster.seer.test.");
2698 assert_eq!(*serial, 2026070101);
2699 assert_eq!(*refresh, 7200);
2700 assert_eq!(*retry, 3600);
2701 assert_eq!(*expire, 1209600);
2702 assert_eq!(*minimum, 300);
2703 }
2704 other => panic!("expected SOA data, got {other:?}"),
2705 }
2706 }
2707
2708 #[tokio::test]
2709 async fn mock_resolve_caa_maps_flags_tag_and_value() {
2710 let records = mock_zone_lookup(RecordType::CAA, "seer.test").await;
2711 assert_eq!(records.len(), 2);
2712 let by_tag = |wanted: &str| {
2713 records
2714 .iter()
2715 .find_map(|r| match &r.data {
2716 RecordData::CAA { flags, tag, value } if tag == wanted => {
2717 Some((*flags, value.clone()))
2718 }
2719 _ => None,
2720 })
2721 .unwrap_or_else(|| panic!("expected a CAA record with tag {wanted}"))
2722 };
2723 assert_eq!(by_tag("issue"), (0, "letsencrypt.org".to_string()));
2725 assert_eq!(
2726 by_tag("iodef"),
2727 (128, "mailto:security@seer.test".to_string())
2728 );
2729 }
2730
2731 #[tokio::test]
2732 async fn mock_resolve_tlsa_hex_encodes_cert_data() {
2733 let records = mock_zone_lookup(RecordType::TLSA, "_443._tcp.seer.test").await;
2734 assert_eq!(records.len(), 1);
2735 match &records[0].data {
2736 RecordData::TLSA {
2737 cert_usage,
2738 selector,
2739 matching,
2740 cert_data,
2741 } => {
2742 assert_eq!((*cert_usage, *selector, *matching), (3, 1, 1));
2743 assert_eq!(cert_data, "ABCD01");
2744 }
2745 other => panic!("expected TLSA data, got {other:?}"),
2746 }
2747 }
2748
2749 #[tokio::test]
2750 async fn mock_resolve_sshfp_hex_encodes_fingerprint() {
2751 let records = mock_zone_lookup(RecordType::SSHFP, "seer.test").await;
2752 assert_eq!(records.len(), 1);
2753 match &records[0].data {
2754 RecordData::SSHFP {
2755 algorithm,
2756 fingerprint_type,
2757 fingerprint,
2758 } => {
2759 assert_eq!((*algorithm, *fingerprint_type), (4, 2));
2760 assert_eq!(fingerprint, "DEADBEEF");
2761 }
2762 other => panic!("expected SSHFP data, got {other:?}"),
2763 }
2764 }
2765
2766 #[tokio::test]
2767 async fn mock_resolve_naptr_decodes_character_strings() {
2768 let records = mock_zone_lookup(RecordType::NAPTR, "seer.test").await;
2769 assert_eq!(records.len(), 1);
2770 match &records[0].data {
2771 RecordData::NAPTR {
2772 order,
2773 preference,
2774 flags,
2775 services,
2776 regexp,
2777 replacement,
2778 } => {
2779 assert_eq!((*order, *preference), (100, 50));
2780 assert_eq!(flags, "U");
2781 assert_eq!(services, "E2U+sip");
2782 assert_eq!(regexp, "!^.*$!sip:info@seer.test!");
2783 assert_eq!(replacement, ".");
2784 }
2785 other => panic!("expected NAPTR data, got {other:?}"),
2786 }
2787 }
2788
2789 #[tokio::test]
2790 async fn mock_resolve_srv_via_dig_style_name() {
2791 let records = mock_zone_lookup(RecordType::SRV, "_sip._tcp.seer.test").await;
2792 assert_eq!(records.len(), 1);
2793 assert_eq!(records[0].name, "_sip._tcp.seer.test");
2794 match &records[0].data {
2795 RecordData::SRV {
2796 priority,
2797 weight,
2798 port,
2799 target,
2800 } => {
2801 assert_eq!((*priority, *weight, *port), (10, 5, 5060));
2802 assert_eq!(target, "sipserver.seer.test.");
2803 }
2804 other => panic!("expected SRV data, got {other:?}"),
2805 }
2806 }
2807
2808 #[tokio::test]
2809 async fn mock_resolve_ptr_transforms_ip_literal() {
2810 let records = mock_zone_lookup(RecordType::PTR, "192.0.2.1").await;
2811 assert_eq!(records.len(), 1);
2812 assert_eq!(records[0].name, "1.2.0.192.in-addr.arpa");
2814 assert!(matches!(
2815 &records[0].data,
2816 RecordData::PTR { target } if target == "ptr.seer.test."
2817 ));
2818 }
2819
2820 #[tokio::test]
2821 async fn mock_resolve_keeps_www_label() {
2822 let records = mock_zone_lookup(RecordType::CNAME, "www.seer.test").await;
2825 assert_eq!(records.len(), 1, "{records:?}");
2826 assert_eq!(records[0].name, "www.seer.test");
2827 assert_eq!(records[0].data.to_string(), "edge.cdn.test.");
2828 }
2829
2830 #[tokio::test]
2831 async fn mock_resolve_srv_keeps_www_label() {
2832 let port = spawn_mock_dns_fn(|qname, qtype| match (qname, qtype) {
2833 ("_sip._tcp.www.seer.test", HickoryRecordType::SRV) => {
2834 MockReply::Answer(vec![HickoryRData::SRV(
2835 hickory_resolver::proto::rr::rdata::SRV::new(
2836 10,
2837 5,
2838 5060,
2839 Name::from_ascii("sip.seer.test.").unwrap(),
2840 ),
2841 )])
2842 }
2843 _ => MockReply::NoData,
2844 })
2845 .await;
2846 let records = mock_dns_resolver(port)
2847 .resolve(
2848 "_sip._tcp.www.seer.test",
2849 RecordType::SRV,
2850 Some("127.0.0.1"),
2851 )
2852 .await
2853 .expect("SRV against mock");
2854 assert_eq!(records.len(), 1, "{records:?}");
2855 assert_eq!(records[0].name, "_sip._tcp.www.seer.test");
2856 }
2857
2858 #[tokio::test]
2859 async fn mock_resolve_queries_a_wildcard_owner_name() {
2860 let port = spawn_mock_dns_fn(|qname, qtype| match (qname, qtype) {
2863 ("*.seer.test", HickoryRecordType::A) => MockReply::Answer(vec![HickoryRData::A(
2864 hickory_resolver::proto::rr::rdata::A(Ipv4Addr::new(192, 0, 2, 42)),
2865 )]),
2866 _ => MockReply::NoData,
2867 })
2868 .await;
2869 let records = mock_dns_resolver(port)
2870 .resolve("*.seer.test", RecordType::A, Some("127.0.0.1"))
2871 .await
2872 .expect("wildcard A against mock");
2873 assert_eq!(records.len(), 1, "{records:?}");
2874 assert_eq!(records[0].name, "*.seer.test");
2875 assert_eq!(records[0].data.to_string(), "192.0.2.42");
2876 }
2877
2878 #[tokio::test]
2879 async fn mock_presence_still_probes_the_apex_for_www_input() {
2880 let port = spawn_mock_dns_fn(|qname, qtype| match (qname, qtype) {
2883 ("seer.test", HickoryRecordType::NS) => MockReply::Answer(vec![HickoryRData::NS(
2884 hickory_resolver::proto::rr::rdata::NS(Name::from_ascii("ns1.seer.test.").unwrap()),
2885 )]),
2886 ("www.seer.test", HickoryRecordType::NS) => MockReply::NoDataWithSoa("seer.test"),
2889 _ => MockReply::NxDomain,
2890 })
2891 .await;
2892 let resolver = mock_dns_resolver_default(port);
2893 assert_eq!(
2894 resolver.presence("www.seer.test").await,
2895 DnsPresence::Present
2896 );
2897 }
2898
2899 #[tokio::test]
2900 async fn mock_resolve_ptr_transforms_ipv6_literal() {
2901 let records = mock_zone_lookup(RecordType::PTR, "2606:4700:4700::1111").await;
2902 assert_eq!(records.len(), 1, "{records:?}");
2903 assert_eq!(records[0].data.to_string(), "one.one.one.one.");
2904 }
2905
2906 #[tokio::test]
2907 async fn mock_resolve_any_aggregates_multiple_types() {
2908 let records = mock_zone_lookup(RecordType::ANY, "seer.test").await;
2909 for expected in [
2912 RecordType::A,
2913 RecordType::AAAA,
2914 RecordType::MX,
2915 RecordType::NS,
2916 RecordType::TXT,
2917 RecordType::SOA,
2918 RecordType::CAA,
2919 RecordType::HTTPS,
2920 RecordType::DNSKEY,
2921 ] {
2922 assert!(
2923 records.iter().any(|r| r.record_type == expected),
2924 "ANY must include {expected} records"
2925 );
2926 }
2927 assert_eq!(records.len(), 13);
2930 assert!(records.iter().all(|r| r.name == "seer.test"));
2931 assert!(!records.iter().any(|r| r.record_type == RecordType::CDS));
2933 }
2934
2935 #[tokio::test]
2936 async fn mock_resolve_converts_service_bindings() {
2937 let records = mock_zone_lookup(RecordType::HTTPS, "seer.test").await;
2938 assert_eq!(records.len(), 1, "{records:?}");
2939 let RecordData::HTTPS {
2940 priority,
2941 target,
2942 params,
2943 } = &records[0].data
2944 else {
2945 panic!("expected HTTPS data, got {:?}", records[0].data);
2946 };
2947 assert_eq!((*priority, target.as_str()), (1, "."));
2948 let pairs: Vec<(&str, &str)> = params
2949 .iter()
2950 .map(|p| (p.key.as_str(), p.value.as_str()))
2951 .collect();
2952 assert_eq!(
2953 pairs,
2954 [
2955 ("alpn", "h3,h2"),
2956 ("port", "8443"),
2957 ("ipv4hint", "192.0.2.1,192.0.2.2"),
2958 ("ech", "AAH+"),
2959 ("ipv6hint", "2001:db8::1"),
2960 ("key65333", "ex\\0321"),
2963 ]
2964 );
2965 assert_eq!(
2966 records[0].data.to_string(),
2967 "1 . alpn=\"h3,h2\" port=8443 ipv4hint=192.0.2.1,192.0.2.2 ech=AAH+ \
2968 ipv6hint=2001:db8::1 key65333=ex\\0321"
2969 );
2970
2971 let alias = mock_zone_lookup(RecordType::HTTPS, "alias.seer.test").await;
2972 assert_eq!(alias.len(), 1, "{alias:?}");
2973 assert_eq!(alias[0].data.to_string(), "0 pool.seer.test.");
2974
2975 let svcb = mock_zone_lookup(RecordType::SVCB, "_8443._foo.seer.test").await;
2976 assert_eq!(svcb.len(), 1, "{svcb:?}");
2977 assert_eq!(svcb[0].record_type, RecordType::SVCB);
2978 assert_eq!(
2979 svcb[0].data.to_string(),
2980 "2 svc.seer.test. mandatory=alpn,port alpn=\"foo\" no-default-alpn port=8443"
2981 );
2982 }
2983
2984 #[tokio::test]
2985 async fn mock_resolve_converts_child_dnssec_records() {
2986 let cds = mock_zone_lookup(RecordType::CDS, "seer.test").await;
2988 let shown: Vec<String> = cds.iter().map(|r| r.data.to_string()).collect();
2989 assert_eq!(shown, ["2371 15 2 ABCDEF", "0 0 0 00"]);
2990 assert!(cds.iter().all(|r| r.record_type == RecordType::CDS));
2991
2992 let cdnskey = mock_zone_lookup(RecordType::CDNSKEY, "seer.test").await;
2993 let shown: Vec<String> = cdnskey.iter().map(|r| r.data.to_string()).collect();
2994 assert_eq!(
2995 shown,
2996 [
2997 "257 3 15 BwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwc=",
2998 "0 3 0 AA==",
3000 ]
3001 );
3002 }
3003
3004 #[tokio::test]
3005 async fn mock_resolve_keeps_only_the_requested_type_behind_a_cname() {
3006 let port = spawn_mock_dns_fn(|qname, qtype| match (qname, qtype) {
3010 ("www.seer.test", HickoryRecordType::A) => MockReply::Records(vec![
3011 record("www.seer.test.", 300, cname_rdata("edge.cdn.test.")),
3012 record("edge.cdn.test.", 60, a_rdata([192, 0, 2, 7])),
3013 ]),
3014 _ => MockReply::NxDomain,
3015 })
3016 .await;
3017 let records = mock_dns_resolver(port)
3018 .resolve("www.seer.test", RecordType::A, Some("127.0.0.1"))
3019 .await
3020 .expect("A behind a CNAME");
3021 assert_eq!(
3022 records,
3023 [DnsRecord {
3024 name: "www.seer.test".to_string(),
3025 record_type: RecordType::A,
3026 ttl: 60,
3027 data: RecordData::A {
3028 address: "192.0.2.7".to_string(),
3029 },
3030 }]
3031 );
3032 }
3033
3034 fn is_probe(qname: &str) -> bool {
3037 qname.starts_with("seer-probe-")
3038 }
3039
3040 type Asked = Arc<std::sync::Mutex<Vec<(String, HickoryRecordType)>>>;
3042
3043 async fn spawn_recording<F>(mut handler: F) -> (u16, Asked)
3045 where
3046 F: FnMut(&str, HickoryRecordType) -> MockReply + Send + 'static,
3047 {
3048 let asked: Asked = Arc::default();
3049 let log = Arc::clone(&asked);
3050 let port = spawn_mock_dns_fn(move |qname, qtype| {
3051 log.lock().unwrap().push((qname.to_string(), qtype));
3052 handler(qname, qtype)
3053 })
3054 .await;
3055 (port, asked)
3056 }
3057
3058 fn probes_sent(asked: &Asked) -> usize {
3059 asked
3060 .lock()
3061 .unwrap()
3062 .iter()
3063 .filter(|(name, _)| is_probe(name))
3064 .count()
3065 }
3066
3067 async fn mock_query(port: u16, name: &str, record_type: RecordType) -> DnsQueryResult {
3068 mock_dns_resolver(port)
3069 .query(name, record_type, Some("127.0.0.1"))
3070 .await
3071 .unwrap_or_else(|e| panic!("{record_type} query against mock must succeed: {e}"))
3072 }
3073
3074 fn shown(records: &[&DnsRecord]) -> Vec<(String, RecordType, String)> {
3075 records
3076 .iter()
3077 .map(|r| (r.name.clone(), r.record_type, r.data.to_string()))
3078 .collect()
3079 }
3080
3081 fn row(name: &str, record_type: RecordType, data: &str) -> (String, RecordType, String) {
3082 (name.to_string(), record_type, data.to_string())
3083 }
3084
3085 #[tokio::test]
3086 async fn mock_query_reports_the_cname_chain_under_real_owners() {
3087 let port = spawn_mock_dns_fn(|qname, qtype| match (qname, qtype) {
3088 ("www.seer.test", HickoryRecordType::A) => MockReply::Records(vec![
3089 record("www.seer.test.", 300, cname_rdata("edge.cdn.test.")),
3090 record("edge.cdn.test.", 120, cname_rdata("origin.cdn.test.")),
3091 record("origin.cdn.test.", 60, a_rdata([192, 0, 2, 7])),
3092 record("origin.cdn.test.", 60, a_rdata([192, 0, 2, 8])),
3093 ]),
3094 _ => MockReply::NxDomain,
3095 })
3096 .await;
3097 let result = mock_query(port, "WWW.Seer.test", RecordType::A).await;
3098
3099 assert_eq!(result.name, "www.seer.test");
3100 assert_eq!(result.record_type, RecordType::A);
3101 assert_eq!(result.server.as_deref(), Some("127.0.0.1"));
3102 assert_eq!(result.status, DnsStatus::NoError);
3103 assert_eq!(result.flags, ["qr", "rd", "ra"]);
3104 assert_eq!(
3105 shown(&result.cname_chain().collect::<Vec<_>>()),
3106 [
3107 row("www.seer.test", RecordType::CNAME, "edge.cdn.test."),
3108 row("edge.cdn.test", RecordType::CNAME, "origin.cdn.test."),
3109 ]
3110 );
3111 assert_eq!(
3112 shown(&result.records().collect::<Vec<_>>()),
3113 [
3114 row("origin.cdn.test", RecordType::A, "192.0.2.7"),
3115 row("origin.cdn.test", RecordType::A, "192.0.2.8"),
3116 ]
3117 );
3118 assert_eq!(result.answers[2].ttl, 60);
3119 assert!(result.authority.is_empty());
3120 assert!(!result.is_nodata());
3121 let wildcard = result
3123 .wildcard
3124 .expect("probe ran for a name below seer.test");
3125 assert!(is_probe(&wildcard.probe_name), "{wildcard:?}");
3126 assert!(wildcard.probe_name.ends_with(".seer.test"), "{wildcard:?}");
3127 assert!(!wildcard.present && !wildcard.matches_answer);
3128 }
3129
3130 #[tokio::test]
3131 async fn mock_query_reports_idn_owners_as_a_labels() {
3132 let port = spawn_mock_dns_fn(|qname, qtype| match (qname, qtype) {
3136 ("xn--bcher-kva.seer.test", HickoryRecordType::A) => MockReply::Records(vec![
3137 record(
3138 "xn--bcher-kva.seer.test.",
3139 300,
3140 cname_rdata("edge.xn--caf-dma.test."),
3141 ),
3142 record("edge.xn--caf-dma.test.", 60, a_rdata([192, 0, 2, 7])),
3143 ]),
3144 _ => MockReply::NxDomain,
3145 })
3146 .await;
3147 let result = mock_query(port, "Bücher.seer.test", RecordType::A).await;
3148 assert_eq!(result.name, "xn--bcher-kva.seer.test");
3149 assert_eq!(
3150 shown(&result.answers.iter().collect::<Vec<_>>()),
3151 [
3152 row(
3153 "xn--bcher-kva.seer.test",
3154 RecordType::CNAME,
3155 "edge.xn--caf-dma.test."
3156 ),
3157 row("edge.xn--caf-dma.test", RecordType::A, "192.0.2.7"),
3158 ]
3159 );
3160 assert_eq!(result.cname_chain().count(), 1);
3161 }
3162
3163 #[tokio::test]
3164 async fn query_refuses_a_reserved_nameserver() {
3165 let r = DnsResolver::new();
3169 for reserved in ["127.0.0.1", "10.0.0.1", "::1", "tls://192.168.1.1"] {
3170 let err = r
3171 .query("www.example.com", RecordType::A, Some(reserved))
3172 .await
3173 .expect_err("a reserved nameserver must be refused");
3174 let msg = err.to_string().to_lowercase();
3175 assert!(
3176 msg.contains("blocked") || msg.contains("reserved"),
3177 "{reserved}: {msg}"
3178 );
3179 }
3180 }
3181
3182 #[tokio::test]
3183 async fn mock_query_reports_a_cname_the_server_did_not_follow_as_sent() {
3184 let (port, asked) = spawn_recording(|qname, qtype| match (qname, qtype) {
3189 ("www.seer.test", HickoryRecordType::A) => MockReply::Records(vec![record(
3190 "www.seer.test.",
3191 300,
3192 cname_rdata("edge.cdn.test."),
3193 )]),
3194 ("edge.cdn.test", HickoryRecordType::A) => {
3195 MockReply::Answer(vec![a_rdata([192, 0, 2, 7])])
3196 }
3197 _ => MockReply::NxDomain,
3198 })
3199 .await;
3200 let result = mock_query(port, "www.seer.test", RecordType::A).await;
3201 assert_eq!(result.status, DnsStatus::NoError);
3202 assert_eq!(result.flags, ["qr", "rd", "ra"]);
3203 assert_eq!(
3204 shown(&result.answers.iter().collect::<Vec<_>>()),
3205 [row("www.seer.test", RecordType::CNAME, "edge.cdn.test.")]
3206 );
3207 assert_eq!(result.records().count(), 0);
3208 assert!(
3209 !asked
3210 .lock()
3211 .unwrap()
3212 .iter()
3213 .any(|(n, _)| n == "edge.cdn.test"),
3214 "the target is not chased: {asked:?}"
3215 );
3216 }
3217
3218 #[tokio::test]
3219 async fn mock_query_reports_a_dangling_cname_with_its_chain() {
3220 let port = spawn_mock_dns_fn(|qname, _| match qname {
3224 "www.seer.test" => MockReply::Negative {
3225 code: ResponseCode::NXDomain,
3226 chain: vec![
3227 record("www.seer.test.", 300, cname_rdata("shop.seer.test.")),
3228 record("shop.seer.test.", 300, cname_rdata("gone.cdn.test.")),
3229 ],
3230 soa: "cdn.test",
3231 },
3232 _ => MockReply::NxDomainWithSoa("seer.test"),
3233 })
3234 .await;
3235 let result = mock_query(port, "www.seer.test", RecordType::A).await;
3236 assert_eq!(result.status, DnsStatus::NxDomain);
3237 assert_eq!(result.flags, ["qr", "rd", "ra"]);
3238 assert_eq!(
3239 shown(&result.cname_chain().collect::<Vec<_>>()),
3240 [
3241 row("www.seer.test", RecordType::CNAME, "shop.seer.test."),
3242 row("shop.seer.test", RecordType::CNAME, "gone.cdn.test."),
3243 ]
3244 );
3245 assert_eq!(result.records().count(), 0);
3246 assert_eq!(
3247 shown(&result.authority.iter().collect::<Vec<_>>()),
3248 [row(
3249 "cdn.test",
3250 RecordType::SOA,
3251 "ns1.cdn.test. hostmaster.cdn.test. 2026070101 7200 3600 1209600 300"
3252 )]
3253 );
3254 assert!(!result.is_nodata());
3255 assert_eq!(result.referral_zone(), None);
3256 assert_eq!(
3257 result.wildcard, None,
3258 "a negative answer has nothing to compare"
3259 );
3260 }
3261
3262 #[tokio::test]
3263 async fn mock_query_reports_nodata_behind_a_cname() {
3264 let port = spawn_mock_dns_fn(|qname, _| match qname {
3265 "www.seer.test" => MockReply::Negative {
3266 code: ResponseCode::NoError,
3267 chain: vec![record("www.seer.test.", 300, cname_rdata("edge.cdn.test."))],
3268 soa: "cdn.test",
3269 },
3270 _ => MockReply::NxDomain,
3271 })
3272 .await;
3273 let result = mock_query(port, "www.seer.test", RecordType::AAAA).await;
3274 assert_eq!(result.status, DnsStatus::NoError);
3275 assert_eq!(result.flags, ["qr", "rd", "ra"]);
3276 assert_eq!(result.cname_chain().count(), 1);
3277 assert_eq!(result.records().count(), 0);
3278 assert!(result.is_nodata());
3279 assert_eq!(result.referral_zone(), None);
3280 assert_eq!(result.authority.len(), 1);
3281 assert_eq!(result.authority[0].name, "cdn.test");
3282 }
3283
3284 #[tokio::test]
3285 async fn mock_query_reports_nxdomain_with_the_zone_soa() {
3286 let (port, asked) = spawn_recording(|_, _| MockReply::NxDomainWithSoa("seer.test")).await;
3287 let result = mock_query(port, "nx.seer.test", RecordType::A).await;
3288 assert_eq!(result.status, DnsStatus::NxDomain);
3289 assert!(result.answers.is_empty());
3290 assert!(!result.is_nodata(), "NXDOMAIN is not NODATA");
3291 assert_eq!(result.flags, ["qr", "rd", "ra"]);
3293 assert_eq!(
3294 shown(&result.authority.iter().collect::<Vec<_>>()),
3295 [row(
3296 "seer.test",
3297 RecordType::SOA,
3298 "ns1.seer.test. hostmaster.seer.test. 2026070101 7200 3600 1209600 300"
3299 )]
3300 );
3301 assert_eq!(result.wildcard, None);
3303 assert!(asked
3304 .lock()
3305 .unwrap()
3306 .iter()
3307 .any(|(n, _)| n == "nx.seer.test"));
3308 }
3309
3310 #[tokio::test]
3311 async fn mock_query_reports_nodata_with_the_zone_soa() {
3312 let port = spawn_mock_dns_fn(|_, _| MockReply::NoDataWithSoa("seer.test")).await;
3313 let result = mock_query(port, "www.seer.test", RecordType::MX).await;
3314 assert_eq!(result.status, DnsStatus::NoError);
3315 assert!(result.is_nodata());
3316 assert!(result.answers.is_empty());
3317 assert_eq!(result.authority.len(), 1);
3318 assert_eq!(result.authority[0].record_type, RecordType::SOA);
3319 assert_eq!(result.authority[0].name, "seer.test");
3320 assert_eq!(result.wildcard, None);
3321 }
3322
3323 #[tokio::test]
3324 async fn mock_query_reports_error_codes_as_results() {
3325 let port = spawn_mock_dns_fn(|qname, _| match qname {
3326 "servfail.seer.test" => MockReply::Rcode(ResponseCode::ServFail),
3327 "refused.seer.test" => MockReply::Rcode(ResponseCode::Refused),
3328 "notimp.seer.test" => MockReply::Rcode(ResponseCode::NotImp),
3329 _ => MockReply::NxDomain,
3330 })
3331 .await;
3332 for (name, status, shown) in [
3333 ("servfail.seer.test", DnsStatus::ServFail, "SERVFAIL"),
3334 ("refused.seer.test", DnsStatus::Refused, "REFUSED"),
3335 ("notimp.seer.test", DnsStatus::Other(4), "NOTIMP"),
3336 ] {
3337 let result = mock_query(port, name, RecordType::A).await;
3338 assert_eq!(result.status, status, "{name}");
3339 assert_eq!(result.status.to_string(), shown);
3340 assert!(result.answers.is_empty() && result.authority.is_empty());
3341 assert_eq!(result.flags, ["qr", "rd", "ra"], "{name}");
3342 assert!(!result.is_nodata());
3343 assert_eq!(result.wildcard, None);
3344 }
3345 }
3346
3347 #[tokio::test]
3348 async fn mock_query_reads_an_extended_rcode_from_edns() {
3349 let port = spawn_mock_dns_fn(|_, _| MockReply::Rcode(ResponseCode::BADVERS)).await;
3352 let result = mock_query(port, "www.seer.test", RecordType::A).await;
3353 assert_eq!(result.status, DnsStatus::Other(16));
3354 assert_eq!(result.status.to_string(), "BADVERS");
3355 }
3356
3357 #[tokio::test]
3358 async fn mock_query_repeats_a_truncated_reply_over_tcp() {
3359 let transports = Arc::new(std::sync::Mutex::new(0));
3362 let seen = Arc::clone(&transports);
3363 let port = spawn_mock_dns_fn_with_tcp(move |qname, _| {
3364 if qname != "big.seer.test" {
3365 return MockReply::NxDomain;
3366 }
3367 let mut n = seen.lock().unwrap();
3368 *n += 1;
3369 if *n == 1 {
3370 MockReply::Truncated
3371 } else {
3372 MockReply::Answer((1..=3).map(|i| a_rdata([192, 0, 2, i])).collect())
3373 }
3374 })
3375 .await;
3376 let result = mock_query(port, "big.seer.test", RecordType::A).await;
3377 assert_eq!(result.status, DnsStatus::NoError);
3378 assert_eq!(result.records().count(), 3);
3379 assert!(
3380 !result.flags.contains(&"tc".to_string()),
3381 "{:?}",
3382 result.flags
3383 );
3384 assert_eq!(*transports.lock().unwrap(), 2, "UDP, then TCP");
3385 }
3386
3387 async fn two_truncating_servers() -> ResolverConfig {
3391 let mut config = ResolverConfig::from_parts(None, vec![], vec![]);
3392 for _ in 0..2 {
3393 let port = spawn_mock_dns_split(
3394 |_, _| MockReply::Truncated,
3395 |qname, _| {
3396 if qname == "big.seer.test" {
3397 MockReply::Answer((1..=3).map(|i| a_rdata([192, 0, 2, i])).collect())
3398 } else {
3399 MockReply::NxDomain
3400 }
3401 },
3402 )
3403 .await;
3404 for server in
3405 single_server_config(IpAddr::from([127, 0, 0, 1]), port, true).name_servers()
3406 {
3407 config.add_name_server(server.clone());
3408 }
3409 }
3410 config
3411 }
3412
3413 #[tokio::test]
3416 async fn mock_resolve_asks_over_tcp_when_parallel_udp_replies_truncate() {
3417 let r = DnsResolver::new().with_timeout(Duration::from_secs(2));
3418 let config = two_truncating_servers().await;
3419 let target = ResolveTarget {
3420 resolver: Cow::Owned(build_resolver(config.clone(), r.timeout).unwrap()),
3421 config,
3422 };
3423 let records = r
3424 .resolve_records(&target, "big.seer.test", RecordType::A)
3425 .await
3426 .expect("a truncated answer is asked again over TCP");
3427 assert_eq!(records.len(), 3);
3428 }
3429
3430 #[tokio::test]
3434 async fn hickory_still_abandons_truncated_parallel_udp_replies() {
3435 let config = two_truncating_servers().await;
3436 let resolver = build_resolver(config, Duration::from_secs(2)).unwrap();
3437 let result = resolver.lookup("big.seer.test", HickoryRecordType::A).await;
3438 assert!(
3439 matches!(result, Err(NetError::Truncated)),
3440 "{:?}",
3441 result.map(|lookup| lookup.answers().len())
3442 );
3443 }
3444
3445 #[tokio::test]
3446 async fn mock_query_reports_a_referral_as_neither_nodata_nor_an_answer() {
3447 let port = spawn_mock_dns_fn(|_, _| MockReply::Delegation {
3451 zone: "child.seer.test".to_string(),
3452 servers: vec![
3453 (
3454 "ns1.child.seer.test".to_string(),
3455 vec![IpAddr::from([192, 0, 2, 1])],
3456 ),
3457 ("ns2.child.seer.test".to_string(), vec![]),
3458 ],
3459 })
3460 .await;
3461 let result = mock_query(port, "www.child.seer.test", RecordType::A).await;
3462 assert_eq!(result.status, DnsStatus::NoError);
3463 assert_eq!(result.flags, ["qr", "rd"]);
3465 assert!(result.answers.is_empty());
3466 assert_eq!(result.referral_zone(), Some("child.seer.test"));
3467 assert!(!result.is_nodata());
3468 assert_eq!(
3469 shown(&result.authority.iter().collect::<Vec<_>>()),
3470 [
3471 row("child.seer.test", RecordType::NS, "ns1.child.seer.test."),
3472 row("child.seer.test", RecordType::NS, "ns2.child.seer.test."),
3473 ]
3474 );
3475 assert_eq!(result.wildcard, None);
3476 }
3477
3478 #[tokio::test]
3479 async fn mock_query_reports_special_use_names_as_answered_locally() {
3480 let (port, asked) = spawn_recording(|_, _| MockReply::NxDomain).await;
3485 let ptr = mock_query(port, "127.0.0.1", RecordType::PTR).await;
3486 assert_eq!(ptr.name, "1.0.0.127.in-addr.arpa");
3487 assert!(ptr.answered_locally);
3488 assert_eq!(ptr.server, None, "no server was asked");
3489 assert!(ptr.flags.is_empty(), "the header is hickory's own");
3490 assert_eq!(ptr.status, DnsStatus::NoError);
3491 assert_eq!(
3492 shown(&ptr.answers.iter().collect::<Vec<_>>()),
3493 [row("1.0.0.127.in-addr.arpa", RecordType::PTR, "localhost.")]
3494 );
3495 assert_eq!(ptr.wildcard, None, "no wildcard probe for a local name");
3496
3497 let mx = mock_query(port, "foo.localhost", RecordType::MX).await;
3498 assert!(mx.answered_locally && mx.is_nodata());
3499 let onion = mock_query(port, "x.onion", RecordType::A).await;
3500 assert!(onion.answered_locally);
3501 assert_eq!(onion.status, DnsStatus::NxDomain);
3502
3503 assert!(asked.lock().unwrap().is_empty(), "{asked:?}");
3504 }
3505
3506 #[tokio::test]
3507 async fn mock_query_times_out_as_an_error() {
3508 let port = spawn_mock_dns(MockMode::Ignore).await;
3509 let resolver = DnsResolver::new()
3510 .with_timeout(Duration::from_millis(200))
3511 .allowing_private_hosts()
3512 .with_port(port);
3513 let result = resolver
3514 .query("www.seer.test", RecordType::A, Some("127.0.0.1"))
3515 .await;
3516 assert!(
3517 matches!(&result, Err(SeerError::DnsError(m)) if m.contains("A lookup failed")),
3518 "an unanswered query is a transport error, got: {result:?}"
3519 );
3520 }
3521
3522 #[tokio::test]
3523 async fn mock_query_reads_flags_from_the_response_header() {
3524 let port = spawn_mock_dns_fn(|qname, qtype| match (qname, qtype) {
3525 ("seer.test", HickoryRecordType::A) => {
3526 MockReply::AuthoritativeAnswer(vec![a_rdata([192, 0, 2, 1])])
3527 }
3528 _ => MockReply::NxDomain,
3529 })
3530 .await;
3531 let result = mock_query(port, "seer.test", RecordType::A).await;
3532 assert_eq!(result.flags, ["qr", "aa", "rd", "ra"]);
3533 assert_eq!(result.cname_chain().count(), 0);
3534 assert_eq!(
3535 result.records().next().map(|r| r.name.as_str()),
3536 Some("seer.test")
3537 );
3538 }
3539
3540 #[tokio::test]
3541 async fn mock_query_converts_the_new_record_types_under_their_owner() {
3542 let port = spawn_mock_dns(MockMode::Zone).await;
3543 let https = mock_query(port, "seer.test", RecordType::HTTPS).await;
3544 assert_eq!(https.answers.len(), 1, "{https:?}");
3545 assert_eq!(https.answers[0].name, "seer.test");
3546 assert!(https.answers[0]
3547 .data
3548 .to_string()
3549 .starts_with("1 . alpn=\"h3,h2\" port=8443"));
3550
3551 let alias = mock_query(port, "alias.seer.test", RecordType::HTTPS).await;
3552 assert_eq!(alias.answers[0].data.to_string(), "0 pool.seer.test.");
3553 assert_eq!(alias.answers[0].name, "alias.seer.test");
3554
3555 let cds = mock_query(port, "seer.test", RecordType::CDS).await;
3556 assert_eq!(cds.answers.len(), 2);
3557 let cdnskey = mock_query(port, "seer.test", RecordType::CDNSKEY).await;
3558 assert_eq!(cdnskey.answers[1].data.to_string(), "0 3 0 AA==");
3559 }
3560
3561 #[tokio::test]
3562 async fn mock_query_sorts_mx_like_resolve() {
3563 let port = spawn_mock_dns(MockMode::Zone).await;
3564 let result = mock_query(port, "seer.test", RecordType::MX).await;
3565 let prefs: Vec<String> = result.answers.iter().map(|r| r.data.to_string()).collect();
3566 assert_eq!(
3567 prefs,
3568 [
3569 "10 a.mail.seer.test.",
3570 "20 b.mail.seer.test.",
3571 "30 c.mail.seer.test."
3572 ]
3573 );
3574 }
3575
3576 #[tokio::test]
3577 async fn mock_query_resolves_srv_and_ptr_names_like_resolve() {
3578 let (port, asked) = spawn_recording(|qname, qtype| match (qname, qtype) {
3579 ("_sip._tcp.seer.test", HickoryRecordType::SRV) => {
3580 MockReply::Answer(vec![HickoryRData::SRV(
3581 hickory_resolver::proto::rr::rdata::SRV::new(
3582 10,
3583 5,
3584 5060,
3585 Name::from_ascii("sip.seer.test.").unwrap(),
3586 ),
3587 )])
3588 }
3589 ("1.2.0.192.in-addr.arpa", HickoryRecordType::PTR) => {
3590 MockReply::Answer(vec![HickoryRData::PTR(
3591 hickory_resolver::proto::rr::rdata::PTR(
3592 Name::from_ascii("ptr.seer.test.").unwrap(),
3593 ),
3594 )])
3595 }
3596 _ => MockReply::NxDomain,
3597 })
3598 .await;
3599
3600 let srv = mock_query(port, "_sip._tcp.seer.test", RecordType::SRV).await;
3601 assert_eq!(
3602 shown(&srv.answers.iter().collect::<Vec<_>>()),
3603 [row(
3604 "_sip._tcp.seer.test",
3605 RecordType::SRV,
3606 "10 5 5060 sip.seer.test."
3607 )]
3608 );
3609 let probe = srv.wildcard.expect("SRV name is below seer.test");
3611 assert!(probe.probe_name.ends_with("._tcp.seer.test"), "{probe:?}");
3612 assert!(asked.lock().unwrap().iter().any(|(n, t)| is_probe(n)
3613 && n.ends_with("._tcp.seer.test")
3614 && *t == HickoryRecordType::SRV));
3615
3616 let ptr = mock_query(port, "192.0.2.1", RecordType::PTR).await;
3617 assert_eq!(ptr.name, "1.2.0.192.in-addr.arpa");
3618 assert_eq!(ptr.answers[0].name, "1.2.0.192.in-addr.arpa");
3619
3620 let err = mock_dns_resolver(port)
3621 .query("seer.test", RecordType::SRV, Some("127.0.0.1"))
3622 .await
3623 .expect_err("a bare domain is not an SRV query");
3624 assert!(matches!(err, SeerError::InvalidInput(_)), "{err:?}");
3625 let err = mock_dns_resolver(port)
3626 .query(".bad.example", RecordType::A, Some("127.0.0.1"))
3627 .await;
3628 assert!(err.is_err(), "invalid names are rejected before any I/O");
3629 }
3630
3631 #[tokio::test]
3632 async fn mock_query_any_fans_out_to_the_new_types_and_dedupes() {
3633 let (port, asked) = spawn_recording(|qname, qtype| {
3634 let chain = || record("www.seer.test.", 300, cname_rdata("edge.seer.test."));
3635 match (qname, qtype) {
3636 ("www.seer.test", HickoryRecordType::CNAME) => MockReply::Records(vec![chain()]),
3637 ("www.seer.test", HickoryRecordType::A) => MockReply::Records(vec![
3638 chain(),
3639 record("edge.seer.test.", 60, a_rdata([192, 0, 2, 7])),
3640 ]),
3641 ("www.seer.test", HickoryRecordType::HTTPS) => {
3642 MockReply::Records(vec![
3643 chain(),
3644 record(
3645 "edge.seer.test.",
3646 60,
3647 HickoryRData::HTTPS(hickory_resolver::proto::rr::rdata::HTTPS(
3648 SVCB::new(1, Name::root(), vec![]),
3649 )),
3650 ),
3651 ])
3652 }
3653 ("www.seer.test", _) => MockReply::Records(vec![chain()]),
3656 _ => MockReply::NoDataWithSoa("seer.test"),
3657 }
3658 })
3659 .await;
3660 let result = mock_query(port, "www.seer.test", RecordType::ANY).await;
3661
3662 assert_eq!(result.record_type, RecordType::ANY);
3663 assert_eq!(result.status, DnsStatus::NoError);
3664 assert_eq!(result.flags, ["qr", "rd", "ra"]);
3665 assert_eq!(
3667 shown(&result.answers.iter().collect::<Vec<_>>()),
3668 [
3669 row("www.seer.test", RecordType::CNAME, "edge.seer.test."),
3670 row("edge.seer.test", RecordType::A, "192.0.2.7"),
3671 row("edge.seer.test", RecordType::HTTPS, "1 ."),
3672 ]
3673 );
3674 assert_eq!(result.cname_chain().count(), 1);
3675 assert_eq!(result.records().count(), 2);
3676 assert!(!result.is_nodata());
3677 assert_eq!(result.wildcard, None, "ANY is never probed");
3678
3679 let asked: Vec<HickoryRecordType> = asked
3680 .lock()
3681 .unwrap()
3682 .iter()
3683 .filter(|(n, _)| n == "www.seer.test")
3684 .map(|(_, t)| *t)
3685 .collect();
3686 for wire in ANY_TYPES.iter().filter_map(|t| wire_type(*t)) {
3687 assert!(asked.contains(&wire), "ANY must query {wire}: {asked:?}");
3688 }
3689 assert!(
3690 !asked.contains(&HickoryRecordType::ANY),
3691 "ANY is never sent"
3692 );
3693 }
3694
3695 #[tokio::test]
3696 async fn mock_query_any_errors_only_when_every_sub_query_fails() {
3697 let port = spawn_mock_dns(MockMode::Ignore).await;
3698 let resolver = DnsResolver::new()
3699 .with_timeout(Duration::from_millis(200))
3700 .allowing_private_hosts()
3701 .with_port(port);
3702 let result = resolver
3703 .query("www.seer.test", RecordType::ANY, Some("127.0.0.1"))
3704 .await;
3705 assert!(matches!(result, Err(SeerError::DnsError(_))), "{result:?}");
3706 }
3707
3708 async fn wildcard_zone(probe_reply: fn() -> MockReply) -> (u16, Asked) {
3711 spawn_recording(move |qname, qtype| match (qname, qtype) {
3712 ("www.seer.test", HickoryRecordType::A) => {
3713 MockReply::Answer(vec![a_rdata([192, 0, 2, 50])])
3714 }
3715 (name, HickoryRecordType::A) if is_probe(name) => probe_reply(),
3716 _ => MockReply::NxDomain,
3717 })
3718 .await
3719 }
3720
3721 #[tokio::test]
3722 async fn mock_wildcard_probe_matching_the_answer() {
3723 let (port, asked) =
3724 wildcard_zone(|| MockReply::Answer(vec![a_rdata([192, 0, 2, 50])])).await;
3725 let result = mock_query(port, "www.seer.test", RecordType::A).await;
3726 let probe = result.wildcard.expect("probe attached");
3727 assert!(probe.present && probe.matches_answer, "{probe:?}");
3728 assert_eq!(probes_sent(&asked), 1);
3729 }
3730
3731 #[tokio::test]
3732 async fn mock_wildcard_probe_with_different_data() {
3733 let (port, _) = wildcard_zone(|| MockReply::Answer(vec![a_rdata([192, 0, 2, 99])])).await;
3734 let result = mock_query(port, "www.seer.test", RecordType::A).await;
3735 let probe = result.wildcard.expect("probe attached");
3736 assert!(probe.present && !probe.matches_answer, "{probe:?}");
3737 }
3738
3739 #[tokio::test]
3740 async fn mock_wildcard_probe_absent() {
3741 let (port, _) = wildcard_zone(|| MockReply::NxDomainWithSoa("seer.test")).await;
3742 let result = mock_query(port, "www.seer.test", RecordType::A).await;
3743 let probe = result.wildcard.expect("probe attached");
3744 assert!(!probe.present && !probe.matches_answer, "{probe:?}");
3745 }
3746
3747 #[tokio::test]
3748 async fn mock_wildcard_probe_that_fails_is_dropped_not_fatal() {
3749 let (port, _) = wildcard_zone(|| MockReply::Rcode(ResponseCode::ServFail)).await;
3750 let result = mock_query(port, "www.seer.test", RecordType::A).await;
3751 assert_eq!(result.status, DnsStatus::NoError);
3752 assert_eq!(result.wildcard, None);
3753
3754 let (port, _) = wildcard_zone(|| MockReply::NoReply).await;
3756 let resolver = DnsResolver::new()
3757 .with_timeout(Duration::from_millis(200))
3758 .allowing_private_hosts()
3759 .with_port(port);
3760 let result = resolver
3761 .query("www.seer.test", RecordType::A, Some("127.0.0.1"))
3762 .await
3763 .expect("the probe never fails the query");
3764 assert_eq!(result.records().count(), 1);
3765 assert_eq!(result.wildcard, None);
3766 }
3767
3768 #[tokio::test]
3769 async fn mock_wildcard_probe_is_not_sent_for_ineligible_names() {
3770 let (port, asked) =
3772 spawn_recording(|_, _| MockReply::Answer(vec![a_rdata([192, 0, 2, 50])])).await;
3773 assert_eq!(
3775 mock_query(port, "seer.test", RecordType::A).await.wildcard,
3776 None
3777 );
3778 assert_eq!(
3780 mock_query(port, "*.seer.test", RecordType::A)
3781 .await
3782 .wildcard,
3783 None
3784 );
3785 assert_eq!(
3787 mock_query(port, "www.seer.test", RecordType::ANY)
3788 .await
3789 .wildcard,
3790 None
3791 );
3792 assert_eq!(probes_sent(&asked), 0, "{:?}", asked.lock().unwrap());
3793 }
3794
3795 #[tokio::test]
3796 async fn mock_nodata_folds_to_empty_and_classifies_absent() {
3797 let port = spawn_mock_dns(MockMode::NoData).await;
3798 let result = mock_dns_resolver(port)
3799 .resolve("seer.test", RecordType::NS, Some("127.0.0.1"))
3800 .await;
3801 assert!(
3802 matches!(&result, Ok(records) if records.is_empty()),
3803 "NODATA must fold to Ok(vec![]), got: {result:?}"
3804 );
3805 assert_eq!(classify_ns_presence(&result), DnsPresence::Absent);
3806 }
3807
3808 #[tokio::test]
3809 async fn mock_nxdomain_folds_to_empty_and_classifies_absent() {
3810 let port = spawn_mock_dns(MockMode::Nxdomain).await;
3811 let result = mock_dns_resolver(port)
3812 .resolve("seer.test", RecordType::NS, Some("127.0.0.1"))
3813 .await;
3814 assert!(
3815 matches!(&result, Ok(records) if records.is_empty()),
3816 "NXDOMAIN must fold to Ok(vec![]), got: {result:?}"
3817 );
3818 assert_eq!(classify_ns_presence(&result), DnsPresence::Absent);
3819 }
3820
3821 #[tokio::test]
3822 async fn mock_timeout_errors_and_classifies_unknown() {
3823 let port = spawn_mock_dns(MockMode::Ignore).await;
3824 let resolver = DnsResolver::new()
3826 .with_timeout(Duration::from_millis(200))
3827 .allowing_private_hosts()
3828 .with_port(port);
3829 let result = resolver
3830 .resolve("seer.test", RecordType::NS, Some("127.0.0.1"))
3831 .await;
3832 match &result {
3833 Err(SeerError::DnsError(_)) => {}
3834 other => panic!("unanswered query must surface a DnsError, got: {other:?}"),
3835 }
3836 assert_eq!(classify_ns_presence(&result), DnsPresence::Unknown);
3837 }
3838
3839 #[tokio::test]
3840 async fn resolve_type_rejects_composite_types_consistently() {
3841 let r = DnsResolver::new();
3846 for composite in [RecordType::SRV, RecordType::ANY] {
3847 let err = r
3848 .resolve_type(&r.default_target(), "seer.test", composite)
3849 .await
3850 .expect_err("composite types must be rejected by resolve_type");
3851 assert_eq!(
3852 err.to_string(),
3853 unsupported_record_type(composite).to_string()
3854 );
3855 }
3856 }
3857
3858 #[tokio::test]
3859 async fn mock_unanswered_probe_costs_a_positive_answer_only_the_grace() {
3860 let (port, asked) = wildcard_zone(|| MockReply::NoReply).await;
3864 let resolver = DnsResolver::new()
3865 .with_timeout(Duration::from_secs(2))
3866 .allowing_private_hosts()
3867 .with_port(port);
3868 let started = Instant::now();
3869 let result = resolver
3870 .query("www.seer.test", RecordType::A, Some("127.0.0.1"))
3871 .await
3872 .expect("the main query is answered");
3873 let elapsed = started.elapsed();
3874 assert_eq!(result.records().count(), 1);
3875 assert_eq!(result.wildcard, None, "an unfinished probe says nothing");
3876 assert!(probes_sent(&asked) >= 1, "the probe was sent");
3877 assert!(elapsed >= Duration::from_millis(400), "{elapsed:?}");
3879 assert!(elapsed < Duration::from_millis(1500), "{elapsed:?}");
3880 }
3881
3882 async fn partly_silent_zone() -> u16 {
3885 spawn_mock_dns_fn(|_, qtype| match qtype {
3886 HickoryRecordType::A => MockReply::Answer(vec![a_rdata([192, 0, 2, 7])]),
3887 HickoryRecordType::TXT | HickoryRecordType::DNSKEY => MockReply::NoReply,
3888 _ => MockReply::NoDataWithSoa("seer.test"),
3889 })
3890 .await
3891 }
3892
3893 fn short_resolver(port: u16) -> DnsResolver {
3894 DnsResolver::new()
3895 .with_timeout(Duration::from_millis(200))
3896 .allowing_private_hosts()
3897 .with_port(port)
3898 }
3899
3900 #[tokio::test]
3901 async fn mock_query_any_reports_the_types_that_failed() {
3902 let port = partly_silent_zone().await;
3905 let result = short_resolver(port)
3906 .query("www.seer.test", RecordType::ANY, Some("127.0.0.1"))
3907 .await
3908 .expect("some sub-queries were answered");
3909 assert_eq!(result.records().count(), 1);
3910 let failed: Vec<RecordType> = result.failed_types.iter().map(|f| f.record_type).collect();
3911 assert_eq!(failed, [RecordType::TXT, RecordType::DNSKEY]);
3912 assert!(
3913 result.failed_types[0].error.contains("timed out"),
3914 "{:?}",
3915 result.failed_types
3916 );
3917
3918 let a = short_resolver(port)
3920 .query("www.seer.test", RecordType::A, Some("127.0.0.1"))
3921 .await
3922 .unwrap();
3923 assert!(a.failed_types.is_empty());
3924 }
3925
3926 #[tokio::test]
3927 async fn mock_query_server_any_keeps_partial_failures_apart_from_silence() {
3928 let port = partly_silent_zone().await;
3929 let reply = short_resolver(port)
3930 .query_server("www.seer.test", RecordType::ANY, "127.0.0.1")
3931 .await
3932 .unwrap();
3933 let ServerReply::Response(result) = reply else {
3934 panic!("a partial answer is a response: {reply:?}");
3935 };
3936 assert_eq!(result.records().count(), 1);
3937 let failed: Vec<RecordType> = result.failed_types.iter().map(|f| f.record_type).collect();
3938 assert_eq!(failed, [RecordType::TXT, RecordType::DNSKEY]);
3939
3940 let silent = spawn_mock_dns(MockMode::Ignore).await;
3942 let reply = short_resolver(silent)
3943 .query_server("www.seer.test", RecordType::ANY, "127.0.0.1")
3944 .await
3945 .unwrap();
3946 assert!(
3947 matches!(&reply, ServerReply::Silent(reason) if reason == "timed out"),
3948 "{reply:?}"
3949 );
3950 }
3951
3952 #[tokio::test]
3953 async fn resolve_validates_the_name_before_resolving_the_nameserver() {
3954 let err = DnsResolver::new()
3958 .resolve(".bad..name", RecordType::A, Some("ns.seer.test"))
3959 .await
3960 .expect_err("an invalid name is rejected");
3961 assert!(
3962 matches!(
3963 err,
3964 SeerError::InvalidDomain(_) | SeerError::InvalidInput(_)
3965 ),
3966 "{err:?}"
3967 );
3968 }
3969
3970 #[tokio::test]
3971 async fn a_local_answer_never_resolves_the_nameserver_hostname() {
3972 let r = DnsResolver::new();
3977 let result = r
3978 .query(
3979 "127.0.0.1",
3980 RecordType::PTR,
3981 Some("ns.does-not-resolve.test"),
3982 )
3983 .await
3984 .expect("answered locally, the nameserver unused");
3985 assert!(result.answered_locally);
3986 assert_eq!(result.records().count(), 1);
3987
3988 let blocked = r
3989 .query("127.0.0.1", RecordType::PTR, Some("10.0.0.1"))
3990 .await
3991 .expect_err("a reserved nameserver is refused all the same");
3992 assert!(blocked.to_string().contains("blocked"), "{blocked}");
3993 assert!(r
3994 .query("127.0.0.1", RecordType::PTR, Some("ftp://ns.seer.test"))
3995 .await
3996 .is_err());
3997 }
3998
3999 #[tokio::test]
4004 async fn a_nameserver_hostname_lookup_is_held_to_one_timeout() {
4005 use crate::dns::test_support::{spawn_mock_dns_fn, MockReply};
4006
4007 let timeout = Duration::from_millis(500);
4008 let bootstrap = spawn_mock_dns_fn(|_, _| MockReply::NoReply).await;
4009 let resolver = DnsResolver::new()
4010 .with_timeout(timeout)
4011 .with_default_upstream_port(bootstrap)
4012 .allowing_private_hosts();
4013
4014 for use_query in [true, false] {
4015 let started = Instant::now();
4016 let err = if use_query {
4017 resolver
4018 .query("seer.test", RecordType::A, Some("ns.bootstrap.test"))
4019 .await
4020 .expect_err("the hostname never resolves")
4021 } else {
4022 resolver
4023 .resolve("seer.test", RecordType::A, Some("ns.bootstrap.test"))
4024 .await
4025 .expect_err("the hostname never resolves")
4026 };
4027 let elapsed = started.elapsed();
4028 assert!(err.to_string().contains("ns.bootstrap.test"), "{err}");
4029 assert!(elapsed < timeout * 2 - timeout / 5, "took {elapsed:?}");
4031 }
4032 }
4033
4034 #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
4039 async fn a_nameserver_hostname_lookup_spends_the_query_budget() {
4040 use crate::dns::test_support::{a_rdata, spawn_mock_dns_fn, MockReply};
4041
4042 let timeout = Duration::from_millis(800);
4043 let delay = timeout * 3 / 4;
4044 let bootstrap = spawn_mock_dns_fn(move |qname, qtype| match (qname, qtype) {
4045 ("ns.bootstrap.test", HickoryRecordType::A) => {
4048 std::thread::sleep(delay);
4049 MockReply::Answer(vec![
4050 a_rdata(Ipv4Addr::LOCALHOST),
4051 a_rdata(Ipv4Addr::LOCALHOST),
4052 ])
4053 }
4054 _ => MockReply::NoData,
4055 })
4056 .await;
4057 let target = spawn_mock_dns_fn(|_, _| MockReply::NoReply).await;
4059 let resolver = DnsResolver::new()
4060 .with_timeout(timeout)
4061 .with_default_upstream_port(bootstrap)
4062 .allowing_private_hosts()
4063 .with_port(target);
4064
4065 let started = Instant::now();
4066 let err = resolver
4067 .query("seer.test", RecordType::A, Some("ns.bootstrap.test"))
4068 .await
4069 .expect_err("the nameserver is silent");
4070 let elapsed = started.elapsed();
4071 assert!(err.to_string().contains("timed out"), "{err}");
4072 assert!(
4073 !err.to_string().contains("ns.bootstrap.test"),
4074 "the hostname resolved: {err}"
4075 );
4076 let budget = timeout * crate::dns::transport::QUERY_BUDGET_TIMEOUTS;
4078 assert!(elapsed < budget + delay / 2, "took {elapsed:?}");
4079 }
4080}