1use std::borrow::Cow;
22use std::net::IpAddr;
23use std::pin::pin;
24use std::str::FromStr;
25use std::sync::Arc;
26use std::time::{Duration, Instant};
27
28use futures::future::{self, Either};
29use hickory_resolver::config::{
30 NameServerConfig, ResolveHosts, ResolverConfig, ResolverOpts, ServerOrderingStrategy, GOOGLE,
31};
32use hickory_resolver::net::runtime::TokioRuntimeProvider;
33use hickory_resolver::proto::dnssec::PublicKey;
34use hickory_resolver::proto::rr::rdata::svcb::{SvcParamKey, SvcParamValue, SVCB};
35use hickory_resolver::proto::rr::rdata::CAA;
36use hickory_resolver::proto::rr::{
37 Name, RData as HickoryRData, Record, RecordType as HickoryRecordType,
38};
39use hickory_resolver::proto::serialize::binary::BinEncodable;
40use hickory_resolver::TokioResolver;
41use tracing::{debug, instrument};
42
43use super::nameserver::{NameserverProtocol, NameserverSpec};
44use super::query::{
45 answered_locally, dedupe_records, fold_any, merge_any, random_probe_label, wildcard_outcome,
46 wildcard_probe_name, DnsQueryResult, Exchange, SubQuery, WildcardProbe,
47};
48use super::records::{DnsRecord, RecordData, RecordType, SvcParam};
49use super::transport::{NoResponse, Transport};
50use super::DEFAULT_DNS_TIMEOUT;
51use crate::error::{Result, SeerError};
52use crate::validation::{normalize_domain, normalize_query_name};
53
54pub(crate) const RESOLVER_ATTEMPTS: usize = 2;
58
59const PROBE_GRACE_DIVISOR: u32 = 5;
65
66const DEFAULT_UPSTREAM: &str = "default (Google Public DNS)";
68
69const ANY_TYPES: [RecordType; 11] = [
76 RecordType::A,
77 RecordType::AAAA,
78 RecordType::CNAME,
79 RecordType::MX,
80 RecordType::NS,
81 RecordType::TXT,
82 RecordType::SOA,
83 RecordType::CAA,
84 RecordType::HTTPS,
85 RecordType::DS,
86 RecordType::DNSKEY,
87];
88
89pub(crate) fn apply_standard_opts(opts: &mut ResolverOpts, timeout: Duration) {
97 opts.timeout = timeout;
98 opts.attempts = RESOLVER_ATTEMPTS;
99 opts.use_hosts_file = ResolveHosts::Never;
100 opts.server_ordering_strategy = ServerOrderingStrategy::UserProvidedOrder;
121}
122
123fn build_resolver(config: ResolverConfig, timeout: Duration) -> Result<TokioResolver> {
132 let mut builder = TokioResolver::builder_with_config(config, TokioRuntimeProvider::default());
133 apply_standard_opts(builder.options_mut(), timeout);
134 builder
135 .build()
136 .map_err(|e| SeerError::DnsError(format!("failed to construct DNS resolver: {}", e)))
137}
138
139fn build_default_resolver(timeout: Duration) -> TokioResolver {
146 build_resolver(default_upstream_config(), timeout)
147 .expect("default resolver build cannot fail with the bundled webpki root store")
148}
149
150fn default_upstream_config() -> ResolverConfig {
154 ResolverConfig::udp_and_tcp(&GOOGLE)
155}
156
157pub(crate) fn single_server_config(ip: IpAddr, port: u16, tcp_fallback: bool) -> ResolverConfig {
160 let mut ns = if tcp_fallback {
161 NameServerConfig::udp_and_tcp(ip)
162 } else {
163 NameServerConfig::udp(ip)
164 };
165 for connection in &mut ns.connections {
166 connection.port = port;
167 }
168 let mut config = ResolverConfig::from_parts(None, vec![], vec![]);
169 config.add_name_server(ns);
170 config
171}
172
173pub(crate) fn google_or_pinned(upstream: Option<(IpAddr, u16)>) -> ResolverConfig {
176 match upstream {
177 None => default_upstream_config(),
178 Some((ip, port)) => single_server_config(ip, port, false),
179 }
180}
181
182pub(crate) fn fqdn(name: &str) -> String {
185 if name.ends_with('.') {
186 name.to_string()
187 } else {
188 format!("{name}.")
189 }
190}
191
192fn build_upstream_config(
202 spec: &NameserverSpec,
203 ips: &[IpAddr],
204 port_override: Option<u16>,
205) -> ResolverConfig {
206 let mut config = ResolverConfig::from_parts(None, vec![], vec![]);
207 let port = port_override.unwrap_or(spec.port);
208 let mut ordered = ips.to_vec();
219 crate::net::ipv4_first(&mut ordered, |ip| *ip);
220 for ip in ordered {
221 let mut ns = match spec.protocol {
222 NameserverProtocol::Udp => NameServerConfig::udp(ip),
223 NameserverProtocol::Tls => NameServerConfig::tls(ip, Arc::from(spec.tls_name())),
224 NameserverProtocol::Https => NameServerConfig::https(
225 ip,
226 Arc::from(spec.tls_name()),
227 spec.path.as_deref().map(Arc::from),
228 ),
229 };
230 for connection in &mut ns.connections {
231 connection.port = port;
232 }
233 config.add_name_server(ns);
234 }
235 config
236}
237
238#[derive(Clone)]
246pub struct DnsResolver {
247 timeout: Duration,
248 default_resolver: TokioResolver,
251 transport: Transport,
254 port_override: Option<u16>,
259 allow_private_hosts: bool,
264 #[cfg(test)]
268 test_default_nameserver: Option<String>,
269}
270
271impl std::fmt::Debug for DnsResolver {
272 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
273 f.debug_struct("DnsResolver")
274 .field("timeout", &self.timeout)
275 .finish()
276 }
277}
278
279impl Default for DnsResolver {
280 fn default() -> Self {
281 Self::new()
282 }
283}
284
285impl DnsResolver {
286 pub fn new() -> Self {
288 Self::with_timeout_only(DEFAULT_DNS_TIMEOUT)
289 }
290
291 fn with_timeout_only(timeout: Duration) -> Self {
294 Self {
295 timeout,
296 default_resolver: build_default_resolver(timeout),
297 transport: Transport::new(timeout),
298 port_override: None,
299 allow_private_hosts: false,
300 #[cfg(test)]
301 test_default_nameserver: None,
302 }
303 }
304
305 pub fn from_config(config: &crate::config::SeerConfig) -> Self {
316 Self::with_timeout_only(config.dns_timeout())
317 }
318
319 #[cfg(test)]
321 pub(crate) fn timeout(&self) -> Duration {
322 self.timeout
323 }
324
325 #[cfg(test)]
327 pub(crate) fn allowing_private_hosts(mut self) -> Self {
328 self.allow_private_hosts = true;
329 self
330 }
331
332 #[cfg(test)]
335 pub(crate) fn with_port(mut self, port: u16) -> Self {
336 self.port_override = Some(port);
337 self
338 }
339
340 #[cfg(test)]
343 pub(crate) fn with_default_nameserver(mut self, nameserver: &str) -> Self {
344 self.test_default_nameserver = Some(nameserver.to_string());
345 self
346 }
347
348 #[cfg(test)]
352 fn effective_nameserver<'a>(&'a self, nameserver: Option<&'a str>) -> Option<&'a str> {
353 nameserver.or(self.test_default_nameserver.as_deref())
354 }
355
356 #[cfg(not(test))]
357 fn effective_nameserver<'a>(&self, nameserver: Option<&'a str>) -> Option<&'a str> {
358 nameserver
359 }
360
361 pub fn with_timeout(mut self, timeout: Duration) -> Self {
365 self.timeout = timeout;
366 self.default_resolver = build_default_resolver(timeout);
367 self.transport = Transport::new(timeout);
368 self
369 }
370
371 async fn create_custom_resolver(&self, nameserver: &str) -> Result<TokioResolver> {
372 build_resolver(self.custom_upstream_config(nameserver).await?, self.timeout)
373 }
374
375 async fn custom_upstream_config(&self, nameserver: &str) -> Result<ResolverConfig> {
382 let spec = NameserverSpec::parse(nameserver)?;
387
388 let ips: Vec<IpAddr> = if let Ok(ip) = spec.host.parse::<IpAddr>() {
397 vec![ip]
398 } else {
399 let response = self
400 .default_resolver
401 .lookup_ip(spec.host.as_str())
402 .await
403 .map_err(|e| {
404 SeerError::DnsError(format!(
405 "failed to resolve nameserver hostname {}: {}",
406 spec.host, e
407 ))
408 })?;
409 let resolved: Vec<IpAddr> = response.iter().collect();
410 if resolved.is_empty() {
411 return Err(SeerError::DnsError(format!(
412 "nameserver {} did not resolve to any addresses",
413 spec.host
414 )));
415 }
416 resolved
417 };
418
419 self.vet_nameserver_ips(nameserver, &ips)?;
420 Ok(build_upstream_config(&spec, &ips, self.port_override))
421 }
422
423 fn vet_nameserver_ips(&self, nameserver: &str, ips: &[IpAddr]) -> Result<()> {
430 if self.allow_private_hosts {
431 return Ok(());
432 }
433 for ip in ips {
434 if let Some(reason) = crate::validation::describe_reserved_ip(ip) {
435 return Err(SeerError::DnsError(format!(
436 "nameserver {} blocked: {}",
437 nameserver, reason
438 )));
439 }
440 }
441 Ok(())
442 }
443
444 fn vet_nameserver_offline(&self, nameserver: &str) -> Result<()> {
450 let spec = NameserverSpec::parse(nameserver)?;
451 match spec.host.parse::<IpAddr>() {
452 Ok(ip) => self.vet_nameserver_ips(nameserver, &[ip]),
453 Err(_) => Ok(()),
454 }
455 }
456
457 async fn upstream(&self, nameserver: Option<&str>) -> Result<Cow<'_, TokioResolver>> {
461 Ok(match self.effective_nameserver(nameserver) {
462 Some(ns) => Cow::Owned(self.create_custom_resolver(ns).await?),
463 None => Cow::Borrowed(&self.default_resolver),
464 })
465 }
466
467 async fn query_upstream(&self, name: &str, nameserver: Option<&str>) -> Result<Upstream<'_>> {
473 if answered_locally(name) {
474 if let Some(ns) = self.effective_nameserver(nameserver) {
475 self.vet_nameserver_offline(ns)?;
476 }
477 return Ok(Upstream::Local(&self.default_resolver));
478 }
479 let config = match self.effective_nameserver(nameserver) {
480 Some(ns) => self.custom_upstream_config(ns).await?,
481 None => default_upstream_config(),
482 };
483 Ok(Upstream::Direct {
484 config,
485 transport: &self.transport,
486 })
487 }
488
489 #[instrument(skip(self), fields(domain = %domain, record_type = %record_type))]
536 pub async fn query(
537 &self,
538 domain: &str,
539 record_type: RecordType,
540 nameserver: Option<&str>,
541 ) -> Result<DnsQueryResult> {
542 let domain = prepare_query(domain, record_type)?;
544 let name = wire_query_name(&domain, record_type)?;
545 let upstream = self.query_upstream(&name, nameserver).await?;
546
547 debug!(
548 nameserver = nameserver.unwrap_or(DEFAULT_UPSTREAM),
549 "Querying DNS"
550 );
551
552 let (exchange, query_time, wildcard) = if record_type == RecordType::ANY {
553 let started = Instant::now();
554 let exchange = merge_any(exchange_any(&upstream, &name).await)?;
555 (exchange, started.elapsed(), None)
556 } else {
557 let grace = self.timeout / PROBE_GRACE_DIVISOR;
558 exchange_with_probe(&upstream, &name, record_type, grace).await?
559 };
560 Ok(exchange.into_result(name, record_type, nameserver, wildcard, query_time))
561 }
562
563 pub(crate) async fn query_server(
573 &self,
574 domain: &str,
575 record_type: RecordType,
576 nameserver: &str,
577 ) -> Result<ServerReply> {
578 let domain = prepare_query(domain, record_type)?;
579 let name = wire_query_name(&domain, record_type)?;
580 if answered_locally(&name) {
581 return self
583 .query(&domain, record_type, Some(nameserver))
584 .await
585 .map(ServerReply::Response);
586 }
587 let upstream = self.query_upstream(&name, Some(nameserver)).await?;
588
589 let started = Instant::now();
590 let exchange = if record_type == RecordType::ANY {
591 let replies = exchange_any(&upstream, &name).await;
592 if replies.iter().all(|(_, reply)| matches!(reply, Ok(Err(_)))) {
595 if let Some((_, Ok(Err(why)))) = replies.first() {
596 return Ok(ServerReply::Silent(why.reason()));
597 }
598 }
599 merge_any(replies)?
600 } else {
601 match upstream.try_exchange(&name, record_type).await? {
602 Ok(exchange) => exchange,
603 Err(why) => return Ok(ServerReply::Silent(why.reason())),
604 }
605 };
606 Ok(ServerReply::Response(exchange.into_result(
607 name,
608 record_type,
609 Some(nameserver),
610 None,
611 started.elapsed(),
612 )))
613 }
614
615 #[instrument(skip(self), fields(domain = %domain, record_type = %record_type))]
628 pub async fn resolve(
629 &self,
630 domain: &str,
631 record_type: RecordType,
632 nameserver: Option<&str>,
633 ) -> Result<Vec<DnsRecord>> {
634 let domain = prepare_query(domain, record_type)?;
636 let resolver = self.upstream(nameserver).await?;
638
639 debug!(
640 nameserver = nameserver.unwrap_or(DEFAULT_UPSTREAM),
641 "Resolving DNS"
642 );
643
644 match record_type {
645 RecordType::SRV => match parse_srv_query(&domain) {
646 Some((service, protocol, name)) => {
648 self.resolve_srv_core(&resolver, &service, &protocol, &name)
649 .await
650 }
651 None => Err(srv_format_error()),
654 },
655 RecordType::ANY => self.resolve_any(&resolver, &domain).await,
656 single => self.resolve_type(&resolver, &domain, single).await,
657 }
658 }
659
660 async fn resolve_srv_core(
667 &self,
668 resolver: &TokioResolver,
669 service: &str,
670 protocol: &str,
671 domain: &str,
672 ) -> Result<Vec<DnsRecord>> {
673 let query_name = srv_query_name(service, protocol, domain)?;
674 self.resolve_records(resolver, &query_name, RecordType::SRV)
675 .await
676 }
677
678 async fn resolve_type(
686 &self,
687 resolver: &TokioResolver,
688 domain: &str,
689 record_type: RecordType,
690 ) -> Result<Vec<DnsRecord>> {
691 match record_type {
692 RecordType::SRV | RecordType::ANY => Err(unsupported_record_type(record_type)),
693 RecordType::PTR => {
696 self.resolve_records(resolver, &ptr_query_name(domain), RecordType::PTR)
697 .await
698 }
699 single => self.resolve_records(resolver, domain, single).await,
700 }
701 }
702
703 async fn resolve_records(
712 &self,
713 resolver: &TokioResolver,
714 domain: &str,
715 record_type: RecordType,
716 ) -> Result<Vec<DnsRecord>> {
717 let Some(wire_type) = wire_type(record_type) else {
718 return Err(unsupported_record_type(record_type));
719 };
720
721 let response = match resolver.lookup(domain, wire_type).await {
722 Ok(response) => response,
723 Err(e) if e.is_no_records_found() => return Ok(vec![]),
724 Err(e) => {
725 return Err(SeerError::DnsError(format!(
726 "{record_type} lookup failed: {e}"
727 )))
728 }
729 };
730
731 let mut records: Vec<DnsRecord> = response
732 .answers()
733 .iter()
734 .filter_map(|record| {
735 convert_rdata(record_type, &record.data, NameSpelling::Display).map(|data| {
736 DnsRecord {
737 name: domain.to_string(),
738 record_type,
739 ttl: record.ttl,
740 data,
741 }
742 })
743 })
744 .collect();
745
746 if record_type == RecordType::MX {
747 records.sort_by_key(|r| match &r.data {
748 RecordData::MX { preference, .. } => *preference,
749 _ => 0,
750 });
751 }
752
753 Ok(records)
754 }
755
756 async fn resolve_any(&self, resolver: &TokioResolver, domain: &str) -> Result<Vec<DnsRecord>> {
757 let results = future::join_all(ANY_TYPES.into_iter().map(|record_type| async move {
762 (
763 record_type,
764 self.resolve_type(resolver, domain, record_type).await,
765 )
766 }))
767 .await;
768
769 let (record_sets, _failed) = fold_any(results)?;
773 Ok(dedupe_records(record_sets.into_iter().flatten()))
774 }
775}
776
777enum Upstream<'a> {
779 Local(&'a TokioResolver),
782 Direct {
785 config: ResolverConfig,
786 transport: &'a Transport,
787 },
788}
789
790impl Upstream<'_> {
791 async fn exchange(&self, name: &str, record_type: RecordType) -> Result<Exchange> {
794 self.try_exchange(name, record_type)
795 .await?
796 .map_err(|why| SeerError::DnsError(format!("{record_type} lookup failed: {why}")))
797 }
798
799 async fn try_exchange(
802 &self,
803 name: &str,
804 record_type: RecordType,
805 ) -> Result<std::result::Result<Exchange, NoResponse>> {
806 let wire = wire_type(record_type).ok_or_else(|| unsupported_record_type(record_type))?;
807 let exchange = match self {
808 Upstream::Local(resolver) => {
809 Exchange::from_local(resolver.lookup(name, wire).await, record_type)?
810 }
811 Upstream::Direct { config, transport } => {
812 let qname = Name::from_ascii(fqdn(name))
813 .map_err(|e| SeerError::InvalidDomain(format!("{name}: {e}")))?;
814 let request = transport.request(qname, wire, true);
815 match transport.query(config.name_servers(), &request).await {
816 Ok(response) => Exchange::from_message(&response),
817 Err(why) => return Ok(Err(why)),
818 }
819 }
820 };
821 Ok(Ok(exchange.ordered(name, record_type)))
822 }
823}
824
825#[derive(Debug)]
827pub(crate) enum ServerReply {
828 Response(DnsQueryResult),
830 Silent(String),
833}
834
835async fn exchange_any(upstream: &Upstream<'_>, name: &str) -> Vec<(RecordType, SubQuery)> {
839 future::join_all(ANY_TYPES.into_iter().map(|record_type| async move {
840 (record_type, upstream.try_exchange(name, record_type).await)
841 }))
842 .await
843}
844
845async fn exchange_with_probe(
855 upstream: &Upstream<'_>,
856 name: &str,
857 record_type: RecordType,
858 grace: Duration,
859) -> Result<(Exchange, Duration, Option<WildcardProbe>)> {
860 let main = async {
861 let started = Instant::now();
862 let result = upstream.exchange(name, record_type).await;
863 (result, started.elapsed())
864 };
865 let Some(probe_name) = wildcard_probe_name(name, record_type, &random_probe_label()) else {
866 let (result, elapsed) = main.await;
867 return Ok((result?, elapsed, None));
868 };
869 let probe = upstream.exchange(&probe_name, record_type);
870
871 let main = pin!(main);
872 let probe = pin!(probe);
873 let ((result, elapsed), probe) = match future::select(main, probe).await {
874 Either::Left(((result, elapsed), probe)) => {
875 let attachable = matches!(&result, Ok(answer) if answer.has_records(record_type));
876 let probe = if attachable {
880 tokio::time::timeout(grace, probe).await.ok()
881 } else {
882 None
883 };
884 ((result, elapsed), probe)
885 }
886 Either::Right((probe, main)) => (main.await, Some(probe)),
887 };
888 let answer = result?;
889 let wildcard =
890 probe.and_then(|probe| wildcard_outcome(&probe_name, record_type, &answer, probe));
891 Ok((answer, elapsed, wildcard))
892}
893
894#[derive(Debug, Clone, Copy, PartialEq, Eq)]
899pub enum DnsPresence {
900 Present,
902 Absent,
904 Unknown,
906}
907
908fn classify_ns_presence(result: &Result<Vec<DnsRecord>>) -> DnsPresence {
913 match result {
914 Ok(records) if records.is_empty() => DnsPresence::Absent,
915 Ok(_) => DnsPresence::Present,
916 Err(_) => DnsPresence::Unknown,
917 }
918}
919
920impl DnsResolver {
921 pub async fn presence(&self, domain: &str) -> DnsPresence {
929 let Ok(apex) = normalize_domain(domain) else {
933 return DnsPresence::Unknown;
934 };
935 classify_ns_presence(&self.resolve(&apex, RecordType::NS, None).await)
936 }
937}
938
939pub(crate) fn prepare_query(domain: &str, record_type: RecordType) -> Result<String> {
962 prepare_query_with(domain, record_type, normalize_query_name)
963}
964
965fn prepare_query_with(
969 domain: &str,
970 record_type: RecordType,
971 normalize: impl Fn(&str) -> Result<String>,
972) -> Result<String> {
973 if record_type == RecordType::PTR {
974 if let Ok(ip) = IpAddr::from_str(domain.trim()) {
975 normalize(&reverse_dns_name(&ip))?;
981 return Ok(ip.to_string());
982 }
983 }
984 normalize(domain)
985}
986
987pub(crate) fn parse_srv_query(name: &str) -> Option<(String, String, String)> {
992 let mut parts = name.splitn(3, '.');
993 let service = parts.next()?.strip_prefix('_')?;
994 let protocol = parts.next()?.strip_prefix('_')?;
995 let rest = parts.next()?;
996 if service.is_empty() || protocol.is_empty() || rest.is_empty() {
997 return None;
998 }
999 Some((service.to_string(), protocol.to_string(), rest.to_string()))
1000}
1001
1002fn srv_query_name(service: &str, protocol: &str, domain: &str) -> Result<String> {
1006 if !is_valid_srv_label(service) {
1007 return Err(SeerError::InvalidInput(format!(
1008 "invalid SRV service name: {}",
1009 service
1010 )));
1011 }
1012 if !is_valid_srv_label(protocol) {
1013 return Err(SeerError::InvalidInput(format!(
1014 "invalid SRV protocol name: {}",
1015 protocol
1016 )));
1017 }
1018 Ok(format!("_{}._{}.{}", service, protocol, domain))
1019}
1020
1021fn ptr_query_name(domain: &str) -> String {
1024 match IpAddr::from_str(domain) {
1025 Ok(ip) => reverse_dns_name(&ip),
1026 Err(_) => domain.to_string(),
1027 }
1028}
1029
1030pub(crate) fn wire_query_name(domain: &str, record_type: RecordType) -> Result<String> {
1036 match record_type {
1037 RecordType::SRV => {
1038 let (service, protocol, name) = parse_srv_query(domain).ok_or_else(srv_format_error)?;
1039 srv_query_name(&service, &protocol, &name)
1040 }
1041 RecordType::PTR => Ok(ptr_query_name(domain)),
1042 _ => Ok(domain.to_string()),
1043 }
1044}
1045
1046pub(crate) fn srv_format_error() -> SeerError {
1050 SeerError::InvalidInput(
1051 "SRV records require service name format: _service._proto.name".to_string(),
1052 )
1053}
1054
1055fn reverse_dns_name(ip: &IpAddr) -> String {
1056 match ip {
1057 IpAddr::V4(addr) => {
1058 let octets = addr.octets();
1059 format!(
1060 "{}.{}.{}.{}.in-addr.arpa",
1061 octets[3], octets[2], octets[1], octets[0]
1062 )
1063 }
1064 IpAddr::V6(addr) => {
1065 let segments = addr.segments();
1066 let mut result = String::with_capacity(72);
1068 let mut first = true;
1069 for segment in segments.iter().rev() {
1070 for shift in [0, 4, 8, 12] {
1071 if !first {
1072 result.push('.');
1073 }
1074 first = false;
1075 let nibble = (segment >> shift) & 0xF;
1076 result
1077 .push(char::from_digit(nibble as u32, 16).expect("nibble is always 0-15"));
1078 }
1079 }
1080 result.push_str(".ip6.arpa");
1081 result
1082 }
1083 }
1084}
1085
1086fn parse_caa(caa: &CAA) -> (u8, String, String) {
1087 let flags = caa.flags();
1097 let tag = caa.tag.clone();
1098 let value = String::from_utf8_lossy(&caa.value).to_string();
1099 (flags, tag, value)
1100}
1101
1102fn parse_svcb(svcb: &SVCB, names: NameSpelling) -> (u16, String, Vec<SvcParam>) {
1109 let params = svcb
1110 .svc_params
1111 .iter()
1112 .map(|(key, value)| SvcParam {
1113 key: svc_param_key_name(*key),
1114 value: svc_param_value(value),
1115 })
1116 .collect();
1117 (svcb.svc_priority, names.spell(&svcb.target_name), params)
1118}
1119
1120fn svc_param_key_name(key: SvcParamKey) -> String {
1123 match key {
1124 SvcParamKey::Mandatory => "mandatory".to_string(),
1125 SvcParamKey::Alpn => "alpn".to_string(),
1126 SvcParamKey::NoDefaultAlpn => "no-default-alpn".to_string(),
1127 SvcParamKey::Port => "port".to_string(),
1128 SvcParamKey::Ipv4Hint => "ipv4hint".to_string(),
1129 SvcParamKey::EchConfigList => "ech".to_string(),
1130 SvcParamKey::Ipv6Hint => "ipv6hint".to_string(),
1131 SvcParamKey::Key(_) | SvcParamKey::Key65535 | SvcParamKey::Unknown(_) => {
1132 format!("key{}", u16::from(key))
1133 }
1134 }
1135}
1136
1137fn svc_param_value(value: &SvcParamValue) -> String {
1141 use base64::{engine::general_purpose::STANDARD, Engine};
1142 fn join<T: std::fmt::Display>(items: impl Iterator<Item = T>) -> String {
1143 items
1144 .map(|item| item.to_string())
1145 .collect::<Vec<_>>()
1146 .join(",")
1147 }
1148 match value {
1149 SvcParamValue::Mandatory(keys) => join(keys.0.iter().map(|k| svc_param_key_name(*k))),
1150 SvcParamValue::Alpn(alpn) => join(alpn.0.iter().map(|id| {
1156 let listed = id.replace('\\', "\\\\").replace(',', "\\,");
1157 escape_char_string(listed.as_bytes())
1158 })),
1159 SvcParamValue::NoDefaultAlpn => String::new(),
1160 SvcParamValue::Port(port) => port.to_string(),
1161 SvcParamValue::Ipv4Hint(hint) => join(hint.0.iter().map(|a| a.0)),
1162 SvcParamValue::EchConfigList(ech) => STANDARD.encode(&ech.0),
1163 SvcParamValue::Ipv6Hint(hint) => join(hint.0.iter().map(|aaaa| aaaa.0)),
1164 SvcParamValue::Unknown(opaque) => escape_char_string(&opaque.0),
1165 }
1166}
1167
1168fn escape_char_string(bytes: &[u8]) -> String {
1173 let mut out = String::with_capacity(bytes.len());
1174 for &b in bytes {
1175 match b {
1176 b'"' | b'\\' => {
1177 out.push('\\');
1178 out.push(char::from(b));
1179 }
1180 0x21..=0x7E => out.push(char::from(b)),
1181 _ => out.push_str(&format!("\\{b:03}")),
1182 }
1183 }
1184 out
1185}
1186
1187pub(crate) fn wire_type(record_type: RecordType) -> Option<HickoryRecordType> {
1194 Some(match record_type {
1195 RecordType::A => HickoryRecordType::A,
1196 RecordType::AAAA => HickoryRecordType::AAAA,
1197 RecordType::CNAME => HickoryRecordType::CNAME,
1198 RecordType::MX => HickoryRecordType::MX,
1199 RecordType::NS => HickoryRecordType::NS,
1200 RecordType::TXT => HickoryRecordType::TXT,
1201 RecordType::SOA => HickoryRecordType::SOA,
1202 RecordType::PTR => HickoryRecordType::PTR,
1203 RecordType::SRV => HickoryRecordType::SRV,
1204 RecordType::CAA => HickoryRecordType::CAA,
1205 RecordType::DNSKEY => HickoryRecordType::DNSKEY,
1206 RecordType::DS => HickoryRecordType::DS,
1207 RecordType::CDS => HickoryRecordType::CDS,
1210 RecordType::CDNSKEY => HickoryRecordType::CDNSKEY,
1211 RecordType::HTTPS => HickoryRecordType::HTTPS,
1213 RecordType::SVCB => HickoryRecordType::SVCB,
1214 RecordType::TLSA => HickoryRecordType::TLSA,
1220 RecordType::SSHFP => HickoryRecordType::SSHFP,
1221 RecordType::NAPTR => HickoryRecordType::NAPTR,
1222 RecordType::ANY => return None,
1223 })
1224}
1225
1226fn unsupported_record_type(record_type: RecordType) -> SeerError {
1229 SeerError::DnsError(format!("unsupported record type: {}", record_type))
1230}
1231
1232fn hex_upper(bytes: &[u8]) -> String {
1235 bytes.iter().map(|b| format!("{:02X}", b)).collect()
1236}
1237
1238#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1242pub(crate) enum NameSpelling {
1243 Display,
1246 Ascii,
1251}
1252
1253impl NameSpelling {
1254 fn spell(self, name: &Name) -> String {
1255 match self {
1256 NameSpelling::Display => name.to_string(),
1257 NameSpelling::Ascii => name.to_ascii(),
1258 }
1259 }
1260}
1261
1262pub(crate) fn convert_rdata(
1270 record_type: RecordType,
1271 data: &HickoryRData,
1272 names: NameSpelling,
1273) -> Option<RecordData> {
1274 use hickory_resolver::proto::dnssec::rdata::DNSSECRData;
1275
1276 match (record_type, data) {
1277 (RecordType::A, HickoryRData::A(addr)) => Some(RecordData::A {
1278 address: addr.0.to_string(),
1279 }),
1280 (RecordType::AAAA, HickoryRData::AAAA(addr)) => Some(RecordData::AAAA {
1281 address: addr.0.to_string(),
1282 }),
1283 (RecordType::CNAME, HickoryRData::CNAME(cname)) => Some(RecordData::CNAME {
1284 target: names.spell(&cname.0),
1285 }),
1286 (RecordType::MX, HickoryRData::MX(mx)) => Some(RecordData::MX {
1287 preference: mx.preference,
1288 exchange: names.spell(&mx.exchange),
1289 }),
1290 (RecordType::NS, HickoryRData::NS(ns)) => Some(RecordData::NS {
1291 nameserver: names.spell(&ns.0),
1292 }),
1293 (RecordType::TXT, HickoryRData::TXT(txt)) => Some(RecordData::txt(
1294 txt.txt_data
1295 .iter()
1296 .map(|data| String::from_utf8_lossy(data).into_owned())
1297 .collect(),
1298 )),
1299 (RecordType::SOA, HickoryRData::SOA(soa)) => Some(RecordData::SOA {
1300 mname: names.spell(&soa.mname),
1301 rname: names.spell(&soa.rname),
1302 serial: soa.serial,
1303 refresh: soa.refresh as u32,
1309 retry: soa.retry as u32,
1310 expire: soa.expire as u32,
1311 minimum: soa.minimum,
1312 }),
1313 (RecordType::PTR, HickoryRData::PTR(ptr)) => Some(RecordData::PTR {
1314 target: names.spell(&ptr.0),
1315 }),
1316 (RecordType::SRV, HickoryRData::SRV(srv)) => Some(RecordData::SRV {
1317 priority: srv.priority,
1318 weight: srv.weight,
1319 port: srv.port,
1320 target: names.spell(&srv.target),
1321 }),
1322 (RecordType::CAA, HickoryRData::CAA(caa)) => {
1323 let (flags, tag, value) = parse_caa(caa);
1324 Some(RecordData::CAA { flags, tag, value })
1325 }
1326 (RecordType::DNSKEY, HickoryRData::DNSSEC(DNSSECRData::DNSKEY(dnskey))) => {
1327 use base64::{engine::general_purpose::STANDARD, Engine};
1328 let public_key_buf = dnskey.public_key();
1329 Some(RecordData::DNSKEY {
1330 flags: dnskey.flags(),
1331 protocol: 3,
1333 algorithm: u8::from(public_key_buf.algorithm()),
1334 public_key: STANDARD.encode(public_key_buf.public_bytes()),
1335 })
1336 }
1337 (RecordType::DS, HickoryRData::DNSSEC(DNSSECRData::DS(ds))) => Some(RecordData::DS {
1338 key_tag: ds.key_tag(),
1339 algorithm: u8::from(ds.algorithm()),
1340 digest_type: u8::from(ds.digest_type()),
1341 digest: hex_upper(ds.digest()),
1342 }),
1343 (RecordType::CDS, HickoryRData::DNSSEC(DNSSECRData::CDS(cds))) => Some(RecordData::CDS {
1345 key_tag: cds.key_tag(),
1346 algorithm: cds.algorithm().map_or(0, u8::from),
1347 digest_type: u8::from(cds.digest_type()),
1348 digest: hex_upper(cds.digest()),
1349 }),
1350 (RecordType::CDNSKEY, HickoryRData::DNSSEC(DNSSECRData::CDNSKEY(cdnskey))) => {
1351 use base64::{engine::general_purpose::STANDARD, Engine};
1352 let rdata = cdnskey.to_bytes().ok()?;
1359 Some(RecordData::CDNSKEY {
1360 flags: cdnskey.flags(),
1361 protocol: 3,
1363 algorithm: cdnskey.algorithm().map_or(0, u8::from),
1364 public_key: STANDARD.encode(rdata.get(4..)?),
1365 })
1366 }
1367 (RecordType::HTTPS, HickoryRData::HTTPS(https)) => {
1368 let (priority, target, params) = parse_svcb(&https.0, names);
1369 Some(RecordData::HTTPS {
1370 priority,
1371 target,
1372 params,
1373 })
1374 }
1375 (RecordType::SVCB, HickoryRData::SVCB(svcb)) => {
1376 let (priority, target, params) = parse_svcb(svcb, names);
1377 Some(RecordData::SVCB {
1378 priority,
1379 target,
1380 params,
1381 })
1382 }
1383 (RecordType::TLSA, HickoryRData::TLSA(tlsa)) => Some(RecordData::TLSA {
1384 cert_usage: u8::from(tlsa.cert_usage),
1385 selector: u8::from(tlsa.selector),
1386 matching: u8::from(tlsa.matching),
1387 cert_data: hex_upper(&tlsa.cert_data),
1388 }),
1389 (RecordType::SSHFP, HickoryRData::SSHFP(sshfp)) => Some(RecordData::SSHFP {
1390 algorithm: u8::from(sshfp.algorithm),
1391 fingerprint_type: u8::from(sshfp.fingerprint_type),
1392 fingerprint: hex_upper(&sshfp.fingerprint),
1393 }),
1394 (RecordType::NAPTR, HickoryRData::NAPTR(naptr)) => Some(RecordData::NAPTR {
1398 order: naptr.order,
1399 preference: naptr.preference,
1400 flags: String::from_utf8_lossy(&naptr.flags).into_owned(),
1401 services: String::from_utf8_lossy(&naptr.services).into_owned(),
1402 regexp: String::from_utf8_lossy(&naptr.regexp).into_owned(),
1403 replacement: names.spell(&naptr.replacement),
1404 }),
1405 _ => None,
1406 }
1407}
1408
1409pub(crate) fn from_wire_type(wire: HickoryRecordType) -> Option<RecordType> {
1414 RecordType::ALL
1415 .iter()
1416 .copied()
1417 .find(|t| wire_type(*t) == Some(wire))
1418}
1419
1420pub(crate) fn to_dns_record(record: &Record) -> Option<DnsRecord> {
1430 let record_type = from_wire_type(record.record_type())?;
1431 let data = convert_rdata(record_type, &record.data, NameSpelling::Ascii)?;
1432 Some(DnsRecord {
1433 name: owner_name(&record.name),
1434 record_type,
1435 ttl: record.ttl,
1436 data,
1437 })
1438}
1439
1440fn owner_name(name: &Name) -> String {
1445 let text = name.to_ascii();
1446 match text.strip_suffix('.') {
1447 Some(rest) if !rest.is_empty() => rest.to_string(),
1448 _ => text,
1449 }
1450}
1451
1452fn is_valid_srv_label(label: &str) -> bool {
1454 !label.is_empty()
1455 && label.len() <= 63
1456 && label.chars().all(|c| c.is_ascii_alphanumeric() || c == '-')
1457 && !label.starts_with('-')
1458 && !label.ends_with('-')
1459}
1460
1461#[cfg(test)]
1462mod tests {
1463 use super::*;
1471
1472 #[test]
1487 fn standard_opts_pin_server_order_so_ipv4_is_tried_first() {
1488 let mut opts = ResolverOpts::default();
1489 apply_standard_opts(&mut opts, Duration::from_secs(5));
1490 assert_eq!(
1491 opts.server_ordering_strategy,
1492 ServerOrderingStrategy::UserProvidedOrder,
1493 "QueryStatistics ordering is nondeterministic in a short-lived \
1494 process and can draw only black-holed IPv6 servers"
1495 );
1496 }
1497
1498 #[test]
1503 fn default_config_lists_enough_ipv4_servers_before_any_ipv6() {
1504 let config = ResolverConfig::udp_and_tcp(&GOOGLE);
1505 let ips: Vec<_> = config.name_servers().iter().map(|ns| ns.ip).collect();
1506 let concurrent = ResolverOpts::default().num_concurrent_reqs.max(1);
1507 assert!(
1508 ips.len() > concurrent,
1509 "expected more servers than the concurrency window: {ips:?}"
1510 );
1511 for (i, ip) in ips.iter().take(concurrent).enumerate() {
1512 assert!(
1513 ip.is_ipv4(),
1514 "server {i} in the concurrency window is not IPv4: {ips:?}"
1515 );
1516 }
1517 }
1518
1519 #[test]
1520 fn from_config_applies_dns_timeout() {
1521 let mut config = crate::config::SeerConfig::default();
1522 config.timeouts.dns_secs = 9;
1523 let resolver = DnsResolver::from_config(&config);
1524 assert_eq!(resolver.timeout, Duration::from_secs(9));
1525 assert_eq!(resolver.transport.timeout(), Duration::from_secs(9));
1526 let rebuilt = DnsResolver::new().with_timeout(Duration::from_secs(3));
1527 assert_eq!(rebuilt.transport.timeout(), Duration::from_secs(3));
1528 }
1529
1530 #[test]
1531 fn transport_budget_is_the_resolvers_attempts() {
1532 let mut opts = ResolverOpts::default();
1533 apply_standard_opts(&mut opts, Duration::from_secs(5));
1534 assert_eq!(
1535 opts.attempts as u32,
1536 crate::dns::transport::QUERY_BUDGET_TIMEOUTS
1537 );
1538 }
1539 use std::net::{Ipv4Addr, Ipv6Addr};
1540
1541 #[test]
1544 fn srv_label_accepts_alphanumeric_and_hyphen() {
1545 assert!(is_valid_srv_label("http"));
1546 assert!(is_valid_srv_label("ldap-tls"));
1547 assert!(is_valid_srv_label("a1"));
1548 assert!(is_valid_srv_label("tcp"));
1549 }
1550
1551 #[test]
1552 fn srv_label_rejects_empty() {
1553 assert!(!is_valid_srv_label(""));
1554 }
1555
1556 #[test]
1557 fn srv_label_rejects_leading_or_trailing_hyphen() {
1558 assert!(!is_valid_srv_label("-http"));
1559 assert!(!is_valid_srv_label("http-"));
1560 assert!(!is_valid_srv_label("-"));
1561 }
1562
1563 #[test]
1564 fn srv_label_rejects_dots() {
1565 assert!(!is_valid_srv_label("http.evil"));
1568 assert!(!is_valid_srv_label("a.b"));
1569 }
1570
1571 #[test]
1572 fn srv_label_rejects_special_chars() {
1573 assert!(!is_valid_srv_label("http evil"));
1574 assert!(!is_valid_srv_label("http/evil"));
1575 assert!(!is_valid_srv_label("http\0"));
1576 assert!(!is_valid_srv_label("http\n"));
1577 }
1578
1579 #[test]
1580 fn srv_label_rejects_over_63_chars() {
1581 let too_long = "a".repeat(64);
1582 assert!(!is_valid_srv_label(&too_long));
1583 let exactly_63 = "a".repeat(63);
1584 assert!(is_valid_srv_label(&exactly_63));
1585 }
1586
1587 #[test]
1590 fn classify_ns_presence_absent_on_empty_ok() {
1591 let r: Result<Vec<DnsRecord>> = Ok(vec![]);
1593 assert_eq!(classify_ns_presence(&r), DnsPresence::Absent);
1594 }
1595
1596 #[test]
1597 fn classify_ns_presence_present_on_records() {
1598 let rec = DnsRecord {
1599 name: "example.test.".to_string(),
1600 record_type: RecordType::NS,
1601 ttl: 3600,
1602 data: RecordData::NS {
1603 nameserver: "ns1.example.net.".to_string(),
1604 },
1605 };
1606 let r: Result<Vec<DnsRecord>> = Ok(vec![rec]);
1607 assert_eq!(classify_ns_presence(&r), DnsPresence::Present);
1608 }
1609
1610 #[test]
1611 fn classify_ns_presence_unknown_on_error() {
1612 let r: Result<Vec<DnsRecord>> = Err(SeerError::DnsError("servfail".to_string()));
1613 assert_eq!(classify_ns_presence(&r), DnsPresence::Unknown);
1614 }
1615
1616 #[test]
1619 fn reverse_dns_name_formats_ipv4_correctly() {
1620 let ip: IpAddr = Ipv4Addr::new(192, 0, 2, 1).into();
1621 assert_eq!(reverse_dns_name(&ip), "1.2.0.192.in-addr.arpa");
1622 }
1623
1624 #[test]
1625 fn reverse_dns_name_formats_ipv6_correctly() {
1626 let ip: IpAddr = Ipv6Addr::LOCALHOST.into();
1628 let name = reverse_dns_name(&ip);
1629 assert!(
1630 name.ends_with(".ip6.arpa"),
1631 "must end with .ip6.arpa; got: {}",
1632 name
1633 );
1634 assert!(
1636 name.starts_with("1."),
1637 "expected '1.' prefix, got: {}",
1638 name
1639 );
1640 assert_eq!(name.len(), 72);
1642 }
1643
1644 #[tokio::test]
1647 async fn custom_resolver_rejects_invalid_input() {
1648 let r = DnsResolver::new();
1653 let err = r.create_custom_resolver("..").await.unwrap_err();
1654 let msg = err.to_string().to_lowercase();
1655 assert!(
1656 msg.contains("dns resolution failed") || msg.contains("invalid"),
1657 "expected resolution failure, got: {}",
1658 msg
1659 );
1660 }
1661
1662 #[tokio::test]
1663 async fn custom_resolver_rejects_private_ipv4() {
1664 let r = DnsResolver::new();
1667 for reserved in ["127.0.0.1", "10.0.0.1", "192.168.1.1", "169.254.169.254"] {
1668 let err = r.create_custom_resolver(reserved).await.unwrap_err();
1669 let msg = err.to_string().to_lowercase();
1670 assert!(
1671 msg.contains("blocked") || msg.contains("reserved"),
1672 "reserved IP {} must be rejected, got error: {}",
1673 reserved,
1674 msg
1675 );
1676 }
1677 }
1678
1679 #[tokio::test]
1680 async fn custom_resolver_rejects_loopback_ipv6() {
1681 let r = DnsResolver::new();
1682 let err = r.create_custom_resolver("::1").await.unwrap_err();
1683 let msg = err.to_string().to_lowercase();
1684 assert!(
1685 msg.contains("blocked") || msg.contains("reserved"),
1686 "::1 must be rejected, got error: {}",
1687 msg
1688 );
1689 }
1690
1691 #[tokio::test]
1692 async fn custom_resolver_accepts_public_ipv4() {
1693 let r = DnsResolver::new();
1695 let result = r.create_custom_resolver("8.8.8.8").await;
1696 assert!(
1697 result.is_ok(),
1698 "8.8.8.8 must be accepted as a public nameserver, got: {:?}",
1699 result.err()
1700 );
1701 }
1702
1703 #[tokio::test]
1709 async fn custom_resolver_rejects_private_ip_for_dot_and_doh() {
1710 let r = DnsResolver::new();
1711 for reserved in [
1712 "tls://127.0.0.1",
1713 "tls://192.168.1.1:853",
1714 "tls://[::1]",
1715 "https://10.0.0.1/dns-query",
1716 "https://169.254.169.254",
1717 "tls://[fd00::1]:853",
1718 ] {
1719 let err = r.create_custom_resolver(reserved).await.unwrap_err();
1720 let msg = err.to_string().to_lowercase();
1721 assert!(
1722 msg.contains("blocked") || msg.contains("reserved"),
1723 "reserved spec {} must be rejected, got error: {}",
1724 reserved,
1725 msg
1726 );
1727 }
1728 let err = r
1731 .create_custom_resolver("https://[fd00::1]:443/dns-query")
1732 .await
1733 .unwrap_err();
1734 assert!(matches!(err, SeerError::InvalidInput(_)), "{err:?}");
1735 }
1736
1737 #[tokio::test]
1738 async fn custom_resolver_accepts_public_dot_and_doh_literals() {
1739 let r = DnsResolver::new();
1742 for spec in ["tls://1.1.1.1", "https://8.8.8.8/dns-query"] {
1743 let result = r.create_custom_resolver(spec).await;
1744 assert!(
1745 result.is_ok(),
1746 "{} must be accepted, got: {:?}",
1747 spec,
1748 result.err()
1749 );
1750 }
1751 }
1752
1753 #[tokio::test]
1754 async fn custom_resolver_rejects_unknown_scheme() {
1755 let r = DnsResolver::new();
1756 let err = r.create_custom_resolver("ftp://8.8.8.8").await.unwrap_err();
1757 assert!(
1758 matches!(err, SeerError::InvalidInput(_)),
1759 "unknown scheme must be an input error, got: {err:?}"
1760 );
1761 }
1762
1763 fn spec(s: &str) -> NameserverSpec {
1766 NameserverSpec::parse(s).unwrap_or_else(|e| panic!("{s:?} must parse: {e}"))
1767 }
1768
1769 #[test]
1770 fn upstream_config_udp_defaults() {
1771 use hickory_resolver::config::ProtocolConfig;
1772
1773 let ip: IpAddr = "8.8.8.8".parse().unwrap();
1774 let config = build_upstream_config(&spec("8.8.8.8"), &[ip], None);
1775 let servers = config.name_servers();
1776 assert_eq!(servers.len(), 1);
1777 assert_eq!(servers[0].ip, ip);
1778 assert_eq!(servers[0].connections.len(), 1);
1779 assert_eq!(servers[0].connections[0].port, 53);
1780 assert!(matches!(
1781 servers[0].connections[0].protocol,
1782 ProtocolConfig::Udp
1783 ));
1784 }
1785
1786 #[test]
1787 fn upstream_config_udp_explicit_port() {
1788 let ip: IpAddr = "9.9.9.9".parse().unwrap();
1789 let config = build_upstream_config(&spec("9.9.9.9:5353"), &[ip], None);
1790 assert_eq!(config.name_servers()[0].connections[0].port, 5353);
1791 }
1792
1793 #[test]
1794 fn upstream_config_tls_sets_protocol_port_and_server_name() {
1795 use hickory_resolver::config::ProtocolConfig;
1796
1797 let ip: IpAddr = "9.9.9.9".parse().unwrap();
1798 let config = build_upstream_config(&spec("tls://dns.quad9.net"), &[ip], None);
1799 let ns = &config.name_servers()[0];
1800 assert_eq!(ns.ip, ip);
1801 assert_eq!(ns.connections.len(), 1);
1802 assert_eq!(ns.connections[0].port, 853);
1803 match &ns.connections[0].protocol {
1804 ProtocolConfig::Tls { server_name } => {
1805 assert_eq!(&**server_name, "dns.quad9.net");
1806 }
1807 other => panic!("expected Tls protocol, got {other:?}"),
1808 }
1809 }
1810
1811 #[test]
1812 fn upstream_config_tls_ip_literal_uses_ip_as_server_name() {
1813 use hickory_resolver::config::ProtocolConfig;
1814
1815 let ip: IpAddr = "1.1.1.1".parse().unwrap();
1816 let config = build_upstream_config(&spec("tls://1.1.1.1"), &[ip], None);
1817 match &config.name_servers()[0].connections[0].protocol {
1818 ProtocolConfig::Tls { server_name } => assert_eq!(&**server_name, "1.1.1.1"),
1819 other => panic!("expected Tls protocol, got {other:?}"),
1820 }
1821 }
1822
1823 #[test]
1824 fn upstream_config_https_sets_protocol_port_path_and_server_name() {
1825 use hickory_resolver::config::ProtocolConfig;
1826
1827 let ip: IpAddr = "104.16.248.249".parse().unwrap();
1828 let config = build_upstream_config(&spec("https://cloudflare-dns.com"), &[ip], None);
1829 let ns = &config.name_servers()[0];
1830 assert_eq!(ns.connections[0].port, 443);
1831 match &ns.connections[0].protocol {
1832 ProtocolConfig::Https { server_name, path } => {
1833 assert_eq!(&**server_name, "cloudflare-dns.com");
1834 assert_eq!(&**path, "/dns-query");
1835 }
1836 other => panic!("expected Https protocol, got {other:?}"),
1837 }
1838 }
1839
1840 #[test]
1841 fn upstream_config_https_custom_port_and_path() {
1842 use hickory_resolver::config::ProtocolConfig;
1843
1844 let ip: IpAddr = "8.8.8.8".parse().unwrap();
1845 let config = build_upstream_config(&spec("https://dns.google:8443/resolve"), &[ip], None);
1846 let ns = &config.name_servers()[0];
1847 assert_eq!(ns.connections[0].port, 8443);
1848 match &ns.connections[0].protocol {
1849 ProtocolConfig::Https { server_name, path } => {
1850 assert_eq!(&**server_name, "dns.google");
1851 assert_eq!(&**path, "/resolve");
1852 }
1853 other => panic!("expected Https protocol, got {other:?}"),
1854 }
1855 }
1856
1857 #[test]
1858 fn upstream_config_multiple_ips_share_spec() {
1859 use hickory_resolver::config::ProtocolConfig;
1862
1863 let ips: Vec<IpAddr> = vec![
1864 "9.9.9.9".parse().unwrap(),
1865 "149.112.112.112".parse().unwrap(),
1866 ];
1867 let config = build_upstream_config(&spec("tls://dns.quad9.net"), &ips, None);
1868 let servers = config.name_servers();
1869 assert_eq!(servers.len(), 2);
1870 for (ns, expected_ip) in servers.iter().zip(&ips) {
1871 assert_eq!(&ns.ip, expected_ip);
1872 assert_eq!(ns.connections[0].port, 853);
1873 assert!(matches!(
1874 &ns.connections[0].protocol,
1875 ProtocolConfig::Tls { server_name } if &**server_name == "dns.quad9.net"
1876 ));
1877 }
1878 }
1879
1880 #[test]
1888 fn upstream_config_orders_ipv4_before_ipv6() {
1889 let ips: Vec<IpAddr> = [
1891 "2606:4700::6810:f8f9",
1892 "2606:4700::6810:f9f9",
1893 "104.16.248.249",
1894 "104.16.249.249",
1895 ]
1896 .iter()
1897 .map(|s| s.parse().unwrap())
1898 .collect();
1899 let config =
1900 build_upstream_config(&spec("https://cloudflare-dns.com/dns-query"), &ips, None);
1901 let order: Vec<IpAddr> = config.name_servers().iter().map(|ns| ns.ip).collect();
1902 assert_eq!(
1903 order,
1904 [ips[2], ips[3], ips[0], ips[1]],
1905 "IPv4 first, each family in resolved order, nothing dropped"
1906 );
1907 let concurrent = ResolverOpts::default().num_concurrent_reqs.max(1);
1908 assert!(
1909 order.iter().take(concurrent).all(IpAddr::is_ipv4),
1910 "the parallel window must be all IPv4: {order:?}"
1911 );
1912 }
1913
1914 #[test]
1915 fn upstream_config_test_port_override_wins() {
1916 let ip: IpAddr = "127.0.0.1".parse().unwrap();
1918 let config = build_upstream_config(&spec("127.0.0.1"), &[ip], Some(9999));
1919 assert_eq!(config.name_servers()[0].connections[0].port, 9999);
1920 }
1921
1922 #[tokio::test]
1923 async fn query_asks_dot_and_doh_servers_through_the_vetted_config() {
1924 use hickory_resolver::config::ProtocolConfig;
1930
1931 let r = DnsResolver::new();
1932 let dot = r.custom_upstream_config("tls://1.1.1.1").await.unwrap();
1933 let [server] = dot.name_servers() else {
1934 panic!("one server per address: {dot:?}");
1935 };
1936 assert_eq!(server.ip, IpAddr::from([1, 1, 1, 1]));
1937 assert_eq!(server.connections.len(), 1);
1938 assert_eq!(server.connections[0].port, 853);
1939 assert!(matches!(
1940 &server.connections[0].protocol,
1941 ProtocolConfig::Tls { server_name } if &**server_name == "1.1.1.1"
1942 ));
1943
1944 let doh = r
1945 .custom_upstream_config("https://8.8.8.8/dns-query")
1946 .await
1947 .unwrap();
1948 let [server] = doh.name_servers() else {
1949 panic!("one server per address: {doh:?}");
1950 };
1951 assert_eq!(server.connections[0].port, 443);
1952 assert!(matches!(
1953 &server.connections[0].protocol,
1954 ProtocolConfig::Https { server_name, path }
1955 if &**server_name == "8.8.8.8" && &**path == "/dns-query"
1956 ));
1957
1958 let udp = r.custom_upstream_config("9.9.9.9").await.unwrap();
1961 assert_eq!(udp.name_servers()[0].connections[0].port, 53);
1962 assert!(matches!(
1963 udp.name_servers()[0].connections[0].protocol,
1964 ProtocolConfig::Udp
1965 ));
1966 let Upstream::Direct { config, .. } = r.query_upstream("seer.test", None).await.unwrap()
1968 else {
1969 panic!("a public name is sent to the default upstream");
1970 };
1971 assert!(config.name_servers()[0].ip.is_ipv4());
1972 }
1973
1974 #[tokio::test]
1977 #[ignore = "live network — DoT query against Cloudflare"]
1978 async fn live_query_over_dot() {
1979 let result = DnsResolver::new()
1980 .query("example.com", RecordType::A, Some("tls://1.1.1.1"))
1981 .await
1982 .expect("DoT query should get a response");
1983 assert_eq!(result.status, DnsStatus::NoError);
1984 assert!(result.flags.contains(&"qr".to_string()), "{result:?}");
1985 assert!(result.records().count() > 0, "expected A records over DoT");
1986 }
1987
1988 #[tokio::test]
1989 #[ignore = "live network — DoH query against Cloudflare"]
1990 async fn live_query_over_doh() {
1991 let result = DnsResolver::new()
1992 .query(
1993 "example.com",
1994 RecordType::A,
1995 Some("https://cloudflare-dns.com/dns-query"),
1996 )
1997 .await
1998 .expect("DoH query should get a response");
1999 assert_eq!(result.status, DnsStatus::NoError);
2000 assert!(result.records().count() > 0, "expected A records over DoH");
2001 }
2002
2003 #[tokio::test]
2004 #[ignore = "live network — NXDOMAIN keeps its header"]
2005 async fn live_query_nxdomain_has_flags_and_the_soa() {
2006 let result = DnsResolver::new()
2007 .query("seer-does-not-exist.example", RecordType::A, None)
2009 .await
2010 .expect("a negative answer is a result");
2011 assert_eq!(result.status, DnsStatus::NxDomain);
2012 assert!(result.flags.contains(&"qr".to_string()), "{result:?}");
2013 assert!(
2014 result
2015 .authority
2016 .iter()
2017 .any(|r| r.record_type == RecordType::SOA),
2018 "{result:?}"
2019 );
2020 }
2021
2022 #[tokio::test]
2023 #[ignore = "live network — DoT query against Cloudflare"]
2024 async fn live_resolve_over_dot() {
2025 let r = DnsResolver::new();
2026 let records = r
2027 .resolve("example.com", RecordType::A, Some("tls://1.1.1.1"))
2028 .await
2029 .expect("DoT lookup should succeed");
2030 assert!(!records.is_empty(), "expected A records over DoT");
2031 }
2032
2033 #[tokio::test]
2034 #[ignore = "live network — DoH query against Cloudflare"]
2035 async fn live_resolve_over_doh() {
2036 let r = DnsResolver::new();
2037 let records = r
2038 .resolve(
2039 "example.com",
2040 RecordType::A,
2041 Some("https://cloudflare-dns.com/dns-query"),
2042 )
2043 .await
2044 .expect("DoH lookup should succeed");
2045 assert!(!records.is_empty(), "expected A records over DoH");
2046 }
2047
2048 #[tokio::test]
2051 async fn resolve_srv_rejects_invalid_service_label() {
2052 let r = DnsResolver::new();
2053 let result = r
2055 .resolve_srv_core(&r.default_resolver, "http.evil", "tcp", "example.com")
2056 .await;
2057 assert!(result.is_err());
2058 let msg = result.unwrap_err().to_string().to_lowercase();
2059 assert!(
2060 msg.contains("invalid srv service"),
2061 "expected SRV service validation error, got: {}",
2062 msg
2063 );
2064 }
2065
2066 #[tokio::test]
2067 async fn resolve_srv_rejects_invalid_protocol_label() {
2068 let r = DnsResolver::new();
2069 let result = r
2070 .resolve_srv_core(&r.default_resolver, "http", "tcp.evil", "example.com")
2071 .await;
2072 assert!(result.is_err());
2073 let msg = result.unwrap_err().to_string().to_lowercase();
2074 assert!(
2075 msg.contains("invalid srv protocol"),
2076 "expected SRV protocol validation error, got: {}",
2077 msg
2078 );
2079 }
2080
2081 #[tokio::test]
2082 async fn resolve_srv_normalizes_and_validates_domain_input() {
2083 let r = DnsResolver::new();
2087 let result = r
2088 .resolve("_http._tcp.not a valid domain", RecordType::SRV, None)
2089 .await;
2090 assert!(
2091 matches!(result, Err(SeerError::InvalidDomain(_))),
2092 "expected InvalidDomain from domain validation, got: {result:?}"
2093 );
2094 }
2095
2096 #[tokio::test]
2099 async fn resolve_normalizes_uppercase_domain_input() {
2100 let r = DnsResolver::new();
2105 let result = r.resolve(".bad.example", RecordType::A, None).await;
2106 assert!(result.is_err(), "leading-dot domain must be rejected");
2107 }
2108
2109 #[test]
2114 fn parse_srv_query_extracts_service_proto_and_name() {
2115 assert_eq!(
2116 parse_srv_query("_sip._tcp.example.com"),
2117 Some((
2118 "sip".to_string(),
2119 "tcp".to_string(),
2120 "example.com".to_string()
2121 ))
2122 );
2123 }
2124
2125 #[test]
2126 fn parse_srv_query_keeps_multilabel_domain() {
2127 assert_eq!(
2128 parse_srv_query("_sip._tcp.sip.voice.google.com"),
2129 Some((
2130 "sip".to_string(),
2131 "tcp".to_string(),
2132 "sip.voice.google.com".to_string()
2133 ))
2134 );
2135 }
2136
2137 #[test]
2138 fn fqdn_appends_root_dot_once() {
2139 assert_eq!(fqdn("example.com"), "example.com.");
2140 assert_eq!(fqdn("example.com."), "example.com.");
2141 }
2142
2143 #[test]
2144 fn parse_srv_query_rejects_bare_domain() {
2145 assert_eq!(parse_srv_query("example.com"), None);
2146 }
2147
2148 #[test]
2149 fn parse_srv_query_rejects_missing_proto_label() {
2150 assert_eq!(parse_srv_query("_sip.example.com"), None);
2152 }
2153
2154 #[tokio::test]
2155 async fn resolve_rejects_bare_domain_for_srv_as_input_error() {
2156 let r = DnsResolver::new();
2161 let err = r
2162 .resolve("example.com", RecordType::SRV, None)
2163 .await
2164 .expect_err("bare-domain SRV must error");
2165 assert!(
2166 matches!(err, SeerError::InvalidInput(_)),
2167 "bare-domain SRV should be an input error, got: {err:?}"
2168 );
2169 assert!(err.to_string().contains("_service._proto"));
2170 }
2171
2172 #[tokio::test]
2173 #[ignore = "live network"]
2174 async fn resolve_srv_via_dig_style_name_returns_records() {
2175 let r = DnsResolver::new();
2178 let records = r
2179 .resolve("_caldavs._tcp.google.com", RecordType::SRV, None)
2180 .await
2181 .expect("dig-style SRV lookup should succeed");
2182 assert!(!records.is_empty(), "expected SRV records");
2183 assert!(records.iter().all(|r| r.record_type == RecordType::SRV));
2184 }
2185
2186 #[tokio::test]
2187 #[ignore = "live network"]
2188 async fn resolve_naptr_returns_records() {
2189 let r = DnsResolver::new();
2191 let records = r
2192 .resolve("sip2sip.info", RecordType::NAPTR, None)
2193 .await
2194 .expect("NAPTR lookup should succeed");
2195 assert!(!records.is_empty(), "expected NAPTR records");
2196 assert!(records.iter().all(|r| r.record_type == RecordType::NAPTR));
2197 }
2198
2199 #[test]
2202 fn prepare_query_passes_ipv6_literal_through_for_ptr() {
2203 let out = prepare_query("2606:4700:4700::1111", RecordType::PTR).unwrap();
2209 assert_eq!(out, "2606:4700:4700::1111");
2210 }
2211
2212 #[test]
2213 fn prepare_query_passes_ipv6_loopback_through_for_ptr() {
2214 let out = prepare_query("::1", RecordType::PTR).unwrap();
2215 assert_eq!(out, "::1");
2216 }
2217
2218 #[test]
2219 fn prepare_query_passes_ipv4_literal_through_for_ptr() {
2220 let out = prepare_query("8.8.8.8", RecordType::PTR).unwrap();
2221 assert_eq!(out, "8.8.8.8");
2222 }
2223
2224 #[test]
2225 fn prepare_query_normalizes_non_ip_ptr_names() {
2226 let out = prepare_query("1.1.1.1.in-addr.arpa", RecordType::PTR).unwrap();
2228 assert_eq!(out, "1.1.1.1.in-addr.arpa");
2229 }
2230
2231 #[test]
2232 fn prepare_query_keeps_www_for_record_queries() {
2233 let out = prepare_query("HTTPS://WWW.Example.com/path", RecordType::A).unwrap();
2237 assert_eq!(out, "www.example.com");
2238 let out = prepare_query("www.example.com", RecordType::CNAME).unwrap();
2239 assert_eq!(out, "www.example.com");
2240 }
2241
2242 #[test]
2243 fn prepare_query_still_normalizes_scheme_port_path_and_case() {
2244 let out = prepare_query("https://API.Example.COM:8443/v1?q=1#frag", RecordType::A).unwrap();
2245 assert_eq!(out, "api.example.com");
2246 let out = prepare_query("example.com.", RecordType::MX).unwrap();
2247 assert_eq!(out, "example.com");
2248 assert!(prepare_query("not a domain", RecordType::A).is_err());
2249 }
2250
2251 #[test]
2252 fn prepare_query_accepts_a_leading_wildcard_label() {
2253 let out = prepare_query("*.Example.com.", RecordType::A).unwrap();
2257 assert_eq!(out, "*.example.com");
2258 let out = prepare_query("*.1.168.192.in-addr.arpa", RecordType::PTR).unwrap();
2259 assert_eq!(out, "*.1.168.192.in-addr.arpa");
2260 assert!(prepare_query("a.*.example.com", RecordType::A).is_err());
2262 assert!(prepare_query("a*.example.com", RecordType::A).is_err());
2263 }
2264
2265 #[test]
2266 fn prepare_query_runs_ptr_ip_literal_through_the_normalizer() {
2267 let deny_arpa = |name: &str| -> Result<String> {
2273 if name.ends_with(".arpa") {
2274 Err(SeerError::DomainNotAllowed {
2275 domain: name.to_string(),
2276 tld: "arpa".to_string(),
2277 })
2278 } else {
2279 Ok(name.to_string())
2280 }
2281 };
2282 for ip in ["192.0.2.1", "2606:4700:4700::1111"] {
2283 let err = prepare_query_with(ip, RecordType::PTR, deny_arpa)
2284 .expect_err("PTR for an IP literal must obey the normalizer");
2285 assert!(matches!(err, SeerError::DomainNotAllowed { .. }), "{err:?}");
2286 }
2287 assert_eq!(
2289 prepare_query_with("192.0.2.1", RecordType::PTR, normalize_query_name).unwrap(),
2290 "192.0.2.1"
2291 );
2292 }
2293
2294 #[test]
2295 fn from_wire_type_inverts_wire_type_for_every_single_type() {
2296 for record_type in RecordType::ALL.iter().copied() {
2297 match wire_type(record_type) {
2298 Some(wire) => assert_eq!(from_wire_type(wire), Some(record_type)),
2299 None => assert_eq!(record_type, RecordType::ANY),
2300 }
2301 }
2302 assert_eq!(
2305 from_wire_type(HickoryRecordType::SRV),
2306 Some(RecordType::SRV)
2307 );
2308 assert_eq!(from_wire_type(HickoryRecordType::RRSIG), None);
2309 }
2310
2311 #[test]
2312 fn to_dns_record_keeps_the_real_owner_and_type() {
2313 let cname = Record::from_rdata(
2314 Name::from_ascii("www.seer.test.").unwrap(),
2315 300,
2316 HickoryRData::CNAME(hickory_resolver::proto::rr::rdata::CNAME(
2317 Name::from_ascii("edge.cdn.test.").unwrap(),
2318 )),
2319 );
2320 let converted = to_dns_record(&cname).expect("CNAME is modeled");
2321 assert_eq!(converted.name, "www.seer.test");
2322 assert_eq!(converted.record_type, RecordType::CNAME);
2323 assert_eq!(converted.ttl, 300);
2324 assert_eq!(converted.data.to_string(), "edge.cdn.test.");
2325
2326 let a = Record::from_rdata(
2327 Name::from_ascii("edge.cdn.test.").unwrap(),
2328 60,
2329 HickoryRData::A(hickory_resolver::proto::rr::rdata::A(Ipv4Addr::new(
2330 192, 0, 2, 7,
2331 ))),
2332 );
2333 let converted = to_dns_record(&a).expect("A is modeled");
2334 assert_eq!(converted.name, "edge.cdn.test");
2335 assert_eq!(converted.record_type, RecordType::A);
2336
2337 assert_eq!(owner_name(&Name::root()), ".");
2338 assert_eq!(
2340 owner_name(&Name::from_utf8("bücher.seer.test.").unwrap()),
2341 "xn--bcher-kva.seer.test"
2342 );
2343 }
2344
2345 #[test]
2346 fn to_dns_record_spells_rdata_names_as_a_labels_where_resolve_keeps_unicode() {
2347 use hickory_resolver::proto::rr::rdata::{CNAME, MX, SOA, SRV};
2348 let idn = |name: &str| Name::from_utf8(name).unwrap();
2349 let data = |rdata: HickoryRData| {
2350 let record = Record::from_rdata(idn("xn--bcher-kva.seer.test."), 300, rdata);
2351 to_dns_record(&record).expect("modeled").data.to_string()
2352 };
2353 assert_eq!(
2357 data(HickoryRData::CNAME(CNAME(idn("edge.café.test.")))),
2358 "edge.xn--caf-dma.test."
2359 );
2360 assert_eq!(
2361 data(HickoryRData::MX(MX::new(10, idn("mail.café.test.")))),
2362 "10 mail.xn--caf-dma.test."
2363 );
2364 assert_eq!(
2365 data(HickoryRData::SRV(SRV::new(
2366 1,
2367 5,
2368 443,
2369 idn("sip.café.test.")
2370 ))),
2371 "1 5 443 sip.xn--caf-dma.test."
2372 );
2373 assert_eq!(
2374 data(HickoryRData::SOA(SOA::new(
2375 idn("ns1.café.test."),
2376 idn("hostmaster.café.test."),
2377 1,
2378 7200,
2379 3600,
2380 1209600,
2381 300,
2382 ))),
2383 "ns1.xn--caf-dma.test. hostmaster.xn--caf-dma.test. 1 7200 3600 1209600 300"
2384 );
2385
2386 let cname = HickoryRData::CNAME(CNAME(idn("edge.café.test.")));
2388 assert_eq!(
2389 convert_rdata(RecordType::CNAME, &cname, NameSpelling::Display)
2390 .expect("modeled")
2391 .to_string(),
2392 "edge.café.test."
2393 );
2394 }
2395
2396 #[test]
2397 fn parse_caa_keeps_reserved_flag_bits() {
2398 let mut caa = CAA::new_issue(
2401 true,
2402 Some(Name::from_ascii("letsencrypt.org").unwrap()),
2403 vec![],
2404 );
2405 caa.reserved_flags = 0x01;
2406 let (flags, tag, value) = parse_caa(&caa);
2407 assert_eq!(flags, 0x81);
2408 assert_eq!(tag, "issue");
2409 assert_eq!(value, "letsencrypt.org");
2410 }
2411
2412 #[test]
2413 fn alpn_ids_are_escaped_at_both_levels() {
2414 use hickory_resolver::proto::rr::rdata::svcb::Alpn;
2415 let alpn = |ids: &[&str]| {
2416 svc_param_value(&SvcParamValue::Alpn(Alpn(
2417 ids.iter().map(|id| id.to_string()).collect(),
2418 )))
2419 };
2420 assert_eq!(alpn(&["h3", "h2"]), "h3,h2");
2421 assert_eq!(alpn(&["a,b"]), r"a\\,b");
2426 assert_eq!(alpn(&[r"x\y"]), r"x\\\\y");
2427 assert_eq!(alpn(&[r"f\oo,bar", "h2"]), r"f\\\\oo\\,bar,h2");
2429 }
2430
2431 use crate::dns::query::DnsStatus;
2442 use crate::dns::test_support::{
2443 a_rdata, cname_rdata, mock_dns_resolver, mock_dns_resolver_default, record, spawn_mock_dns,
2444 spawn_mock_dns_fn, spawn_mock_dns_fn_with_tcp, MockMode, MockReply,
2445 };
2446 use hickory_resolver::proto::op::ResponseCode;
2447
2448 async fn mock_zone_lookup(record_type: RecordType, domain: &str) -> Vec<DnsRecord> {
2449 let port = spawn_mock_dns(MockMode::Zone).await;
2450 mock_dns_resolver(port)
2451 .resolve(domain, record_type, Some("127.0.0.1"))
2452 .await
2453 .unwrap_or_else(|e| panic!("{record_type} lookup against mock must succeed: {e}"))
2454 }
2455
2456 #[tokio::test]
2457 async fn mock_resolve_a_returns_addresses() {
2458 let records = mock_zone_lookup(RecordType::A, "seer.test").await;
2459 assert_eq!(records.len(), 2);
2460 assert!(records.iter().all(|r| r.record_type == RecordType::A));
2461 assert_eq!(records[0].name, "seer.test");
2462 assert_eq!(records[0].ttl, 300);
2463 let addresses: Vec<String> = records
2464 .iter()
2465 .map(|r| match &r.data {
2466 RecordData::A { address } => address.clone(),
2467 other => panic!("expected A data, got {other:?}"),
2468 })
2469 .collect();
2470 assert!(addresses.contains(&"192.0.2.1".to_string()));
2471 assert!(addresses.contains(&"192.0.2.2".to_string()));
2472 }
2473
2474 #[tokio::test]
2475 async fn mock_resolve_mx_sorts_by_preference() {
2476 let records = mock_zone_lookup(RecordType::MX, "seer.test").await;
2477 let prefs: Vec<u16> = records
2478 .iter()
2479 .map(|r| match &r.data {
2480 RecordData::MX { preference, .. } => *preference,
2481 other => panic!("expected MX data, got {other:?}"),
2482 })
2483 .collect();
2484 assert_eq!(prefs, vec![10, 20, 30]);
2486 assert!(matches!(
2487 &records[0].data,
2488 RecordData::MX { exchange, .. } if exchange == "a.mail.seer.test."
2489 ));
2490 }
2491
2492 #[tokio::test]
2493 async fn mock_resolve_txt_joins_character_strings() {
2494 let records = mock_zone_lookup(RecordType::TXT, "seer.test").await;
2495 assert_eq!(records.len(), 1);
2496 match &records[0].data {
2497 RecordData::TXT { text, strings } => {
2498 assert_eq!(text, "v=spf1 -all");
2499 assert_eq!(strings, &["v=spf1 ", "-all"], "the strings as sent");
2500 }
2501 other => panic!("expected TXT data, got {other:?}"),
2502 }
2503 assert_eq!(records[0].data.to_string(), r#""v=spf1 " "-all""#);
2504 }
2505
2506 #[tokio::test]
2507 async fn mock_resolve_soa_maps_all_fields() {
2508 let records = mock_zone_lookup(RecordType::SOA, "seer.test").await;
2509 assert_eq!(records.len(), 1);
2510 match &records[0].data {
2511 RecordData::SOA {
2512 mname,
2513 rname,
2514 serial,
2515 refresh,
2516 retry,
2517 expire,
2518 minimum,
2519 } => {
2520 assert_eq!(mname, "ns1.seer.test.");
2521 assert_eq!(rname, "hostmaster.seer.test.");
2522 assert_eq!(*serial, 2026070101);
2523 assert_eq!(*refresh, 7200);
2524 assert_eq!(*retry, 3600);
2525 assert_eq!(*expire, 1209600);
2526 assert_eq!(*minimum, 300);
2527 }
2528 other => panic!("expected SOA data, got {other:?}"),
2529 }
2530 }
2531
2532 #[tokio::test]
2533 async fn mock_resolve_caa_maps_flags_tag_and_value() {
2534 let records = mock_zone_lookup(RecordType::CAA, "seer.test").await;
2535 assert_eq!(records.len(), 2);
2536 let by_tag = |wanted: &str| {
2537 records
2538 .iter()
2539 .find_map(|r| match &r.data {
2540 RecordData::CAA { flags, tag, value } if tag == wanted => {
2541 Some((*flags, value.clone()))
2542 }
2543 _ => None,
2544 })
2545 .unwrap_or_else(|| panic!("expected a CAA record with tag {wanted}"))
2546 };
2547 assert_eq!(by_tag("issue"), (0, "letsencrypt.org".to_string()));
2549 assert_eq!(
2550 by_tag("iodef"),
2551 (128, "mailto:security@seer.test".to_string())
2552 );
2553 }
2554
2555 #[tokio::test]
2556 async fn mock_resolve_tlsa_hex_encodes_cert_data() {
2557 let records = mock_zone_lookup(RecordType::TLSA, "_443._tcp.seer.test").await;
2558 assert_eq!(records.len(), 1);
2559 match &records[0].data {
2560 RecordData::TLSA {
2561 cert_usage,
2562 selector,
2563 matching,
2564 cert_data,
2565 } => {
2566 assert_eq!((*cert_usage, *selector, *matching), (3, 1, 1));
2567 assert_eq!(cert_data, "ABCD01");
2568 }
2569 other => panic!("expected TLSA data, got {other:?}"),
2570 }
2571 }
2572
2573 #[tokio::test]
2574 async fn mock_resolve_sshfp_hex_encodes_fingerprint() {
2575 let records = mock_zone_lookup(RecordType::SSHFP, "seer.test").await;
2576 assert_eq!(records.len(), 1);
2577 match &records[0].data {
2578 RecordData::SSHFP {
2579 algorithm,
2580 fingerprint_type,
2581 fingerprint,
2582 } => {
2583 assert_eq!((*algorithm, *fingerprint_type), (4, 2));
2584 assert_eq!(fingerprint, "DEADBEEF");
2585 }
2586 other => panic!("expected SSHFP data, got {other:?}"),
2587 }
2588 }
2589
2590 #[tokio::test]
2591 async fn mock_resolve_naptr_decodes_character_strings() {
2592 let records = mock_zone_lookup(RecordType::NAPTR, "seer.test").await;
2593 assert_eq!(records.len(), 1);
2594 match &records[0].data {
2595 RecordData::NAPTR {
2596 order,
2597 preference,
2598 flags,
2599 services,
2600 regexp,
2601 replacement,
2602 } => {
2603 assert_eq!((*order, *preference), (100, 50));
2604 assert_eq!(flags, "U");
2605 assert_eq!(services, "E2U+sip");
2606 assert_eq!(regexp, "!^.*$!sip:info@seer.test!");
2607 assert_eq!(replacement, ".");
2608 }
2609 other => panic!("expected NAPTR data, got {other:?}"),
2610 }
2611 }
2612
2613 #[tokio::test]
2614 async fn mock_resolve_srv_via_dig_style_name() {
2615 let records = mock_zone_lookup(RecordType::SRV, "_sip._tcp.seer.test").await;
2616 assert_eq!(records.len(), 1);
2617 assert_eq!(records[0].name, "_sip._tcp.seer.test");
2618 match &records[0].data {
2619 RecordData::SRV {
2620 priority,
2621 weight,
2622 port,
2623 target,
2624 } => {
2625 assert_eq!((*priority, *weight, *port), (10, 5, 5060));
2626 assert_eq!(target, "sipserver.seer.test.");
2627 }
2628 other => panic!("expected SRV data, got {other:?}"),
2629 }
2630 }
2631
2632 #[tokio::test]
2633 async fn mock_resolve_ptr_transforms_ip_literal() {
2634 let records = mock_zone_lookup(RecordType::PTR, "192.0.2.1").await;
2635 assert_eq!(records.len(), 1);
2636 assert_eq!(records[0].name, "1.2.0.192.in-addr.arpa");
2638 assert!(matches!(
2639 &records[0].data,
2640 RecordData::PTR { target } if target == "ptr.seer.test."
2641 ));
2642 }
2643
2644 #[tokio::test]
2645 async fn mock_resolve_keeps_www_label() {
2646 let records = mock_zone_lookup(RecordType::CNAME, "www.seer.test").await;
2649 assert_eq!(records.len(), 1, "{records:?}");
2650 assert_eq!(records[0].name, "www.seer.test");
2651 assert_eq!(records[0].data.to_string(), "edge.cdn.test.");
2652 }
2653
2654 #[tokio::test]
2655 async fn mock_resolve_srv_keeps_www_label() {
2656 let port = spawn_mock_dns_fn(|qname, qtype| match (qname, qtype) {
2657 ("_sip._tcp.www.seer.test", HickoryRecordType::SRV) => {
2658 MockReply::Answer(vec![HickoryRData::SRV(
2659 hickory_resolver::proto::rr::rdata::SRV::new(
2660 10,
2661 5,
2662 5060,
2663 Name::from_ascii("sip.seer.test.").unwrap(),
2664 ),
2665 )])
2666 }
2667 _ => MockReply::NoData,
2668 })
2669 .await;
2670 let records = mock_dns_resolver(port)
2671 .resolve(
2672 "_sip._tcp.www.seer.test",
2673 RecordType::SRV,
2674 Some("127.0.0.1"),
2675 )
2676 .await
2677 .expect("SRV against mock");
2678 assert_eq!(records.len(), 1, "{records:?}");
2679 assert_eq!(records[0].name, "_sip._tcp.www.seer.test");
2680 }
2681
2682 #[tokio::test]
2683 async fn mock_resolve_queries_a_wildcard_owner_name() {
2684 let port = spawn_mock_dns_fn(|qname, qtype| match (qname, qtype) {
2687 ("*.seer.test", HickoryRecordType::A) => MockReply::Answer(vec![HickoryRData::A(
2688 hickory_resolver::proto::rr::rdata::A(Ipv4Addr::new(192, 0, 2, 42)),
2689 )]),
2690 _ => MockReply::NoData,
2691 })
2692 .await;
2693 let records = mock_dns_resolver(port)
2694 .resolve("*.seer.test", RecordType::A, Some("127.0.0.1"))
2695 .await
2696 .expect("wildcard A against mock");
2697 assert_eq!(records.len(), 1, "{records:?}");
2698 assert_eq!(records[0].name, "*.seer.test");
2699 assert_eq!(records[0].data.to_string(), "192.0.2.42");
2700 }
2701
2702 #[tokio::test]
2703 async fn mock_presence_still_probes_the_apex_for_www_input() {
2704 let port = spawn_mock_dns_fn(|qname, qtype| match (qname, qtype) {
2707 ("seer.test", HickoryRecordType::NS) => MockReply::Answer(vec![HickoryRData::NS(
2708 hickory_resolver::proto::rr::rdata::NS(Name::from_ascii("ns1.seer.test.").unwrap()),
2709 )]),
2710 ("www.seer.test", HickoryRecordType::NS) => MockReply::NoDataWithSoa("seer.test"),
2713 _ => MockReply::NxDomain,
2714 })
2715 .await;
2716 let resolver = mock_dns_resolver_default(port);
2717 assert_eq!(
2718 resolver.presence("www.seer.test").await,
2719 DnsPresence::Present
2720 );
2721 }
2722
2723 #[tokio::test]
2724 async fn mock_resolve_ptr_transforms_ipv6_literal() {
2725 let records = mock_zone_lookup(RecordType::PTR, "2606:4700:4700::1111").await;
2726 assert_eq!(records.len(), 1, "{records:?}");
2727 assert_eq!(records[0].data.to_string(), "one.one.one.one.");
2728 }
2729
2730 #[tokio::test]
2731 async fn mock_resolve_any_aggregates_multiple_types() {
2732 let records = mock_zone_lookup(RecordType::ANY, "seer.test").await;
2733 for expected in [
2736 RecordType::A,
2737 RecordType::AAAA,
2738 RecordType::MX,
2739 RecordType::NS,
2740 RecordType::TXT,
2741 RecordType::SOA,
2742 RecordType::CAA,
2743 RecordType::HTTPS,
2744 RecordType::DNSKEY,
2745 ] {
2746 assert!(
2747 records.iter().any(|r| r.record_type == expected),
2748 "ANY must include {expected} records"
2749 );
2750 }
2751 assert_eq!(records.len(), 13);
2754 assert!(records.iter().all(|r| r.name == "seer.test"));
2755 assert!(!records.iter().any(|r| r.record_type == RecordType::CDS));
2757 }
2758
2759 #[tokio::test]
2760 async fn mock_resolve_converts_service_bindings() {
2761 let records = mock_zone_lookup(RecordType::HTTPS, "seer.test").await;
2762 assert_eq!(records.len(), 1, "{records:?}");
2763 let RecordData::HTTPS {
2764 priority,
2765 target,
2766 params,
2767 } = &records[0].data
2768 else {
2769 panic!("expected HTTPS data, got {:?}", records[0].data);
2770 };
2771 assert_eq!((*priority, target.as_str()), (1, "."));
2772 let pairs: Vec<(&str, &str)> = params
2773 .iter()
2774 .map(|p| (p.key.as_str(), p.value.as_str()))
2775 .collect();
2776 assert_eq!(
2777 pairs,
2778 [
2779 ("alpn", "h3,h2"),
2780 ("port", "8443"),
2781 ("ipv4hint", "192.0.2.1,192.0.2.2"),
2782 ("ech", "AAH+"),
2783 ("ipv6hint", "2001:db8::1"),
2784 ("key65333", "ex\\0321"),
2787 ]
2788 );
2789 assert_eq!(
2790 records[0].data.to_string(),
2791 "1 . alpn=\"h3,h2\" port=8443 ipv4hint=192.0.2.1,192.0.2.2 ech=AAH+ \
2792 ipv6hint=2001:db8::1 key65333=ex\\0321"
2793 );
2794
2795 let alias = mock_zone_lookup(RecordType::HTTPS, "alias.seer.test").await;
2796 assert_eq!(alias.len(), 1, "{alias:?}");
2797 assert_eq!(alias[0].data.to_string(), "0 pool.seer.test.");
2798
2799 let svcb = mock_zone_lookup(RecordType::SVCB, "_8443._foo.seer.test").await;
2800 assert_eq!(svcb.len(), 1, "{svcb:?}");
2801 assert_eq!(svcb[0].record_type, RecordType::SVCB);
2802 assert_eq!(
2803 svcb[0].data.to_string(),
2804 "2 svc.seer.test. mandatory=alpn,port alpn=\"foo\" no-default-alpn port=8443"
2805 );
2806 }
2807
2808 #[tokio::test]
2809 async fn mock_resolve_converts_child_dnssec_records() {
2810 let cds = mock_zone_lookup(RecordType::CDS, "seer.test").await;
2812 let shown: Vec<String> = cds.iter().map(|r| r.data.to_string()).collect();
2813 assert_eq!(shown, ["2371 15 2 ABCDEF", "0 0 0 00"]);
2814 assert!(cds.iter().all(|r| r.record_type == RecordType::CDS));
2815
2816 let cdnskey = mock_zone_lookup(RecordType::CDNSKEY, "seer.test").await;
2817 let shown: Vec<String> = cdnskey.iter().map(|r| r.data.to_string()).collect();
2818 assert_eq!(
2819 shown,
2820 [
2821 "257 3 15 BwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwc=",
2822 "0 3 0 AA==",
2824 ]
2825 );
2826 }
2827
2828 #[tokio::test]
2829 async fn mock_resolve_keeps_only_the_requested_type_behind_a_cname() {
2830 let port = spawn_mock_dns_fn(|qname, qtype| match (qname, qtype) {
2834 ("www.seer.test", HickoryRecordType::A) => MockReply::Records(vec![
2835 record("www.seer.test.", 300, cname_rdata("edge.cdn.test.")),
2836 record("edge.cdn.test.", 60, a_rdata([192, 0, 2, 7])),
2837 ]),
2838 _ => MockReply::NxDomain,
2839 })
2840 .await;
2841 let records = mock_dns_resolver(port)
2842 .resolve("www.seer.test", RecordType::A, Some("127.0.0.1"))
2843 .await
2844 .expect("A behind a CNAME");
2845 assert_eq!(
2846 records,
2847 [DnsRecord {
2848 name: "www.seer.test".to_string(),
2849 record_type: RecordType::A,
2850 ttl: 60,
2851 data: RecordData::A {
2852 address: "192.0.2.7".to_string(),
2853 },
2854 }]
2855 );
2856 }
2857
2858 fn is_probe(qname: &str) -> bool {
2861 qname.starts_with("seer-probe-")
2862 }
2863
2864 type Asked = Arc<std::sync::Mutex<Vec<(String, HickoryRecordType)>>>;
2866
2867 async fn spawn_recording<F>(mut handler: F) -> (u16, Asked)
2869 where
2870 F: FnMut(&str, HickoryRecordType) -> MockReply + Send + 'static,
2871 {
2872 let asked: Asked = Arc::default();
2873 let log = Arc::clone(&asked);
2874 let port = spawn_mock_dns_fn(move |qname, qtype| {
2875 log.lock().unwrap().push((qname.to_string(), qtype));
2876 handler(qname, qtype)
2877 })
2878 .await;
2879 (port, asked)
2880 }
2881
2882 fn probes_sent(asked: &Asked) -> usize {
2883 asked
2884 .lock()
2885 .unwrap()
2886 .iter()
2887 .filter(|(name, _)| is_probe(name))
2888 .count()
2889 }
2890
2891 async fn mock_query(port: u16, name: &str, record_type: RecordType) -> DnsQueryResult {
2892 mock_dns_resolver(port)
2893 .query(name, record_type, Some("127.0.0.1"))
2894 .await
2895 .unwrap_or_else(|e| panic!("{record_type} query against mock must succeed: {e}"))
2896 }
2897
2898 fn shown(records: &[&DnsRecord]) -> Vec<(String, RecordType, String)> {
2899 records
2900 .iter()
2901 .map(|r| (r.name.clone(), r.record_type, r.data.to_string()))
2902 .collect()
2903 }
2904
2905 fn row(name: &str, record_type: RecordType, data: &str) -> (String, RecordType, String) {
2906 (name.to_string(), record_type, data.to_string())
2907 }
2908
2909 #[tokio::test]
2910 async fn mock_query_reports_the_cname_chain_under_real_owners() {
2911 let port = spawn_mock_dns_fn(|qname, qtype| match (qname, qtype) {
2912 ("www.seer.test", HickoryRecordType::A) => MockReply::Records(vec![
2913 record("www.seer.test.", 300, cname_rdata("edge.cdn.test.")),
2914 record("edge.cdn.test.", 120, cname_rdata("origin.cdn.test.")),
2915 record("origin.cdn.test.", 60, a_rdata([192, 0, 2, 7])),
2916 record("origin.cdn.test.", 60, a_rdata([192, 0, 2, 8])),
2917 ]),
2918 _ => MockReply::NxDomain,
2919 })
2920 .await;
2921 let result = mock_query(port, "WWW.Seer.test", RecordType::A).await;
2922
2923 assert_eq!(result.name, "www.seer.test");
2924 assert_eq!(result.record_type, RecordType::A);
2925 assert_eq!(result.server.as_deref(), Some("127.0.0.1"));
2926 assert_eq!(result.status, DnsStatus::NoError);
2927 assert_eq!(result.flags, ["qr", "rd", "ra"]);
2928 assert_eq!(
2929 shown(&result.cname_chain().collect::<Vec<_>>()),
2930 [
2931 row("www.seer.test", RecordType::CNAME, "edge.cdn.test."),
2932 row("edge.cdn.test", RecordType::CNAME, "origin.cdn.test."),
2933 ]
2934 );
2935 assert_eq!(
2936 shown(&result.records().collect::<Vec<_>>()),
2937 [
2938 row("origin.cdn.test", RecordType::A, "192.0.2.7"),
2939 row("origin.cdn.test", RecordType::A, "192.0.2.8"),
2940 ]
2941 );
2942 assert_eq!(result.answers[2].ttl, 60);
2943 assert!(result.authority.is_empty());
2944 assert!(!result.is_nodata());
2945 let wildcard = result
2947 .wildcard
2948 .expect("probe ran for a name below seer.test");
2949 assert!(is_probe(&wildcard.probe_name), "{wildcard:?}");
2950 assert!(wildcard.probe_name.ends_with(".seer.test"), "{wildcard:?}");
2951 assert!(!wildcard.present && !wildcard.matches_answer);
2952 }
2953
2954 #[tokio::test]
2955 async fn mock_query_reports_idn_owners_as_a_labels() {
2956 let port = spawn_mock_dns_fn(|qname, qtype| match (qname, qtype) {
2960 ("xn--bcher-kva.seer.test", HickoryRecordType::A) => MockReply::Records(vec![
2961 record(
2962 "xn--bcher-kva.seer.test.",
2963 300,
2964 cname_rdata("edge.xn--caf-dma.test."),
2965 ),
2966 record("edge.xn--caf-dma.test.", 60, a_rdata([192, 0, 2, 7])),
2967 ]),
2968 _ => MockReply::NxDomain,
2969 })
2970 .await;
2971 let result = mock_query(port, "Bücher.seer.test", RecordType::A).await;
2972 assert_eq!(result.name, "xn--bcher-kva.seer.test");
2973 assert_eq!(
2974 shown(&result.answers.iter().collect::<Vec<_>>()),
2975 [
2976 row(
2977 "xn--bcher-kva.seer.test",
2978 RecordType::CNAME,
2979 "edge.xn--caf-dma.test."
2980 ),
2981 row("edge.xn--caf-dma.test", RecordType::A, "192.0.2.7"),
2982 ]
2983 );
2984 assert_eq!(result.cname_chain().count(), 1);
2985 }
2986
2987 #[tokio::test]
2988 async fn query_refuses_a_reserved_nameserver() {
2989 let r = DnsResolver::new();
2993 for reserved in ["127.0.0.1", "10.0.0.1", "::1", "tls://192.168.1.1"] {
2994 let err = r
2995 .query("www.example.com", RecordType::A, Some(reserved))
2996 .await
2997 .expect_err("a reserved nameserver must be refused");
2998 let msg = err.to_string().to_lowercase();
2999 assert!(
3000 msg.contains("blocked") || msg.contains("reserved"),
3001 "{reserved}: {msg}"
3002 );
3003 }
3004 }
3005
3006 #[tokio::test]
3007 async fn mock_query_reports_a_cname_the_server_did_not_follow_as_sent() {
3008 let (port, asked) = spawn_recording(|qname, qtype| match (qname, qtype) {
3013 ("www.seer.test", HickoryRecordType::A) => MockReply::Records(vec![record(
3014 "www.seer.test.",
3015 300,
3016 cname_rdata("edge.cdn.test."),
3017 )]),
3018 ("edge.cdn.test", HickoryRecordType::A) => {
3019 MockReply::Answer(vec![a_rdata([192, 0, 2, 7])])
3020 }
3021 _ => MockReply::NxDomain,
3022 })
3023 .await;
3024 let result = mock_query(port, "www.seer.test", RecordType::A).await;
3025 assert_eq!(result.status, DnsStatus::NoError);
3026 assert_eq!(result.flags, ["qr", "rd", "ra"]);
3027 assert_eq!(
3028 shown(&result.answers.iter().collect::<Vec<_>>()),
3029 [row("www.seer.test", RecordType::CNAME, "edge.cdn.test.")]
3030 );
3031 assert_eq!(result.records().count(), 0);
3032 assert!(
3033 !asked
3034 .lock()
3035 .unwrap()
3036 .iter()
3037 .any(|(n, _)| n == "edge.cdn.test"),
3038 "the target is not chased: {asked:?}"
3039 );
3040 }
3041
3042 #[tokio::test]
3043 async fn mock_query_reports_a_dangling_cname_with_its_chain() {
3044 let port = spawn_mock_dns_fn(|qname, _| match qname {
3048 "www.seer.test" => MockReply::Negative {
3049 code: ResponseCode::NXDomain,
3050 chain: vec![
3051 record("www.seer.test.", 300, cname_rdata("shop.seer.test.")),
3052 record("shop.seer.test.", 300, cname_rdata("gone.cdn.test.")),
3053 ],
3054 soa: "cdn.test",
3055 },
3056 _ => MockReply::NxDomainWithSoa("seer.test"),
3057 })
3058 .await;
3059 let result = mock_query(port, "www.seer.test", RecordType::A).await;
3060 assert_eq!(result.status, DnsStatus::NxDomain);
3061 assert_eq!(result.flags, ["qr", "rd", "ra"]);
3062 assert_eq!(
3063 shown(&result.cname_chain().collect::<Vec<_>>()),
3064 [
3065 row("www.seer.test", RecordType::CNAME, "shop.seer.test."),
3066 row("shop.seer.test", RecordType::CNAME, "gone.cdn.test."),
3067 ]
3068 );
3069 assert_eq!(result.records().count(), 0);
3070 assert_eq!(
3071 shown(&result.authority.iter().collect::<Vec<_>>()),
3072 [row(
3073 "cdn.test",
3074 RecordType::SOA,
3075 "ns1.cdn.test. hostmaster.cdn.test. 2026070101 7200 3600 1209600 300"
3076 )]
3077 );
3078 assert!(!result.is_nodata());
3079 assert_eq!(result.referral_zone(), None);
3080 assert_eq!(
3081 result.wildcard, None,
3082 "a negative answer has nothing to compare"
3083 );
3084 }
3085
3086 #[tokio::test]
3087 async fn mock_query_reports_nodata_behind_a_cname() {
3088 let port = spawn_mock_dns_fn(|qname, _| match qname {
3089 "www.seer.test" => MockReply::Negative {
3090 code: ResponseCode::NoError,
3091 chain: vec![record("www.seer.test.", 300, cname_rdata("edge.cdn.test."))],
3092 soa: "cdn.test",
3093 },
3094 _ => MockReply::NxDomain,
3095 })
3096 .await;
3097 let result = mock_query(port, "www.seer.test", RecordType::AAAA).await;
3098 assert_eq!(result.status, DnsStatus::NoError);
3099 assert_eq!(result.flags, ["qr", "rd", "ra"]);
3100 assert_eq!(result.cname_chain().count(), 1);
3101 assert_eq!(result.records().count(), 0);
3102 assert!(result.is_nodata());
3103 assert_eq!(result.referral_zone(), None);
3104 assert_eq!(result.authority.len(), 1);
3105 assert_eq!(result.authority[0].name, "cdn.test");
3106 }
3107
3108 #[tokio::test]
3109 async fn mock_query_reports_nxdomain_with_the_zone_soa() {
3110 let (port, asked) = spawn_recording(|_, _| MockReply::NxDomainWithSoa("seer.test")).await;
3111 let result = mock_query(port, "nx.seer.test", RecordType::A).await;
3112 assert_eq!(result.status, DnsStatus::NxDomain);
3113 assert!(result.answers.is_empty());
3114 assert!(!result.is_nodata(), "NXDOMAIN is not NODATA");
3115 assert_eq!(result.flags, ["qr", "rd", "ra"]);
3117 assert_eq!(
3118 shown(&result.authority.iter().collect::<Vec<_>>()),
3119 [row(
3120 "seer.test",
3121 RecordType::SOA,
3122 "ns1.seer.test. hostmaster.seer.test. 2026070101 7200 3600 1209600 300"
3123 )]
3124 );
3125 assert_eq!(result.wildcard, None);
3127 assert!(asked
3128 .lock()
3129 .unwrap()
3130 .iter()
3131 .any(|(n, _)| n == "nx.seer.test"));
3132 }
3133
3134 #[tokio::test]
3135 async fn mock_query_reports_nodata_with_the_zone_soa() {
3136 let port = spawn_mock_dns_fn(|_, _| MockReply::NoDataWithSoa("seer.test")).await;
3137 let result = mock_query(port, "www.seer.test", RecordType::MX).await;
3138 assert_eq!(result.status, DnsStatus::NoError);
3139 assert!(result.is_nodata());
3140 assert!(result.answers.is_empty());
3141 assert_eq!(result.authority.len(), 1);
3142 assert_eq!(result.authority[0].record_type, RecordType::SOA);
3143 assert_eq!(result.authority[0].name, "seer.test");
3144 assert_eq!(result.wildcard, None);
3145 }
3146
3147 #[tokio::test]
3148 async fn mock_query_reports_error_codes_as_results() {
3149 let port = spawn_mock_dns_fn(|qname, _| match qname {
3150 "servfail.seer.test" => MockReply::Rcode(ResponseCode::ServFail),
3151 "refused.seer.test" => MockReply::Rcode(ResponseCode::Refused),
3152 "notimp.seer.test" => MockReply::Rcode(ResponseCode::NotImp),
3153 _ => MockReply::NxDomain,
3154 })
3155 .await;
3156 for (name, status, shown) in [
3157 ("servfail.seer.test", DnsStatus::ServFail, "SERVFAIL"),
3158 ("refused.seer.test", DnsStatus::Refused, "REFUSED"),
3159 ("notimp.seer.test", DnsStatus::Other(4), "NOTIMP"),
3160 ] {
3161 let result = mock_query(port, name, RecordType::A).await;
3162 assert_eq!(result.status, status, "{name}");
3163 assert_eq!(result.status.to_string(), shown);
3164 assert!(result.answers.is_empty() && result.authority.is_empty());
3165 assert_eq!(result.flags, ["qr", "rd", "ra"], "{name}");
3166 assert!(!result.is_nodata());
3167 assert_eq!(result.wildcard, None);
3168 }
3169 }
3170
3171 #[tokio::test]
3172 async fn mock_query_reads_an_extended_rcode_from_edns() {
3173 let port = spawn_mock_dns_fn(|_, _| MockReply::Rcode(ResponseCode::BADVERS)).await;
3176 let result = mock_query(port, "www.seer.test", RecordType::A).await;
3177 assert_eq!(result.status, DnsStatus::Other(16));
3178 assert_eq!(result.status.to_string(), "BADVERS");
3179 }
3180
3181 #[tokio::test]
3182 async fn mock_query_repeats_a_truncated_reply_over_tcp() {
3183 let transports = Arc::new(std::sync::Mutex::new(0));
3186 let seen = Arc::clone(&transports);
3187 let port = spawn_mock_dns_fn_with_tcp(move |qname, _| {
3188 if qname != "big.seer.test" {
3189 return MockReply::NxDomain;
3190 }
3191 let mut n = seen.lock().unwrap();
3192 *n += 1;
3193 if *n == 1 {
3194 MockReply::Truncated
3195 } else {
3196 MockReply::Answer((1..=3).map(|i| a_rdata([192, 0, 2, i])).collect())
3197 }
3198 })
3199 .await;
3200 let result = mock_query(port, "big.seer.test", RecordType::A).await;
3201 assert_eq!(result.status, DnsStatus::NoError);
3202 assert_eq!(result.records().count(), 3);
3203 assert!(
3204 !result.flags.contains(&"tc".to_string()),
3205 "{:?}",
3206 result.flags
3207 );
3208 assert_eq!(*transports.lock().unwrap(), 2, "UDP, then TCP");
3209 }
3210
3211 #[tokio::test]
3212 async fn mock_query_reports_a_referral_as_neither_nodata_nor_an_answer() {
3213 let port = spawn_mock_dns_fn(|_, _| MockReply::Delegation {
3217 zone: "child.seer.test".to_string(),
3218 servers: vec![
3219 (
3220 "ns1.child.seer.test".to_string(),
3221 vec![IpAddr::from([192, 0, 2, 1])],
3222 ),
3223 ("ns2.child.seer.test".to_string(), vec![]),
3224 ],
3225 })
3226 .await;
3227 let result = mock_query(port, "www.child.seer.test", RecordType::A).await;
3228 assert_eq!(result.status, DnsStatus::NoError);
3229 assert_eq!(result.flags, ["qr", "rd"]);
3231 assert!(result.answers.is_empty());
3232 assert_eq!(result.referral_zone(), Some("child.seer.test"));
3233 assert!(!result.is_nodata());
3234 assert_eq!(
3235 shown(&result.authority.iter().collect::<Vec<_>>()),
3236 [
3237 row("child.seer.test", RecordType::NS, "ns1.child.seer.test."),
3238 row("child.seer.test", RecordType::NS, "ns2.child.seer.test."),
3239 ]
3240 );
3241 assert_eq!(result.wildcard, None);
3242 }
3243
3244 #[tokio::test]
3245 async fn mock_query_reports_special_use_names_as_answered_locally() {
3246 let (port, asked) = spawn_recording(|_, _| MockReply::NxDomain).await;
3251 let ptr = mock_query(port, "127.0.0.1", RecordType::PTR).await;
3252 assert_eq!(ptr.name, "1.0.0.127.in-addr.arpa");
3253 assert!(ptr.answered_locally);
3254 assert_eq!(ptr.server, None, "no server was asked");
3255 assert!(ptr.flags.is_empty(), "the header is hickory's own");
3256 assert_eq!(ptr.status, DnsStatus::NoError);
3257 assert_eq!(
3258 shown(&ptr.answers.iter().collect::<Vec<_>>()),
3259 [row("1.0.0.127.in-addr.arpa", RecordType::PTR, "localhost.")]
3260 );
3261 assert_eq!(ptr.wildcard, None, "no wildcard probe for a local name");
3262
3263 let mx = mock_query(port, "foo.localhost", RecordType::MX).await;
3264 assert!(mx.answered_locally && mx.is_nodata());
3265 let onion = mock_query(port, "x.onion", RecordType::A).await;
3266 assert!(onion.answered_locally);
3267 assert_eq!(onion.status, DnsStatus::NxDomain);
3268
3269 assert!(asked.lock().unwrap().is_empty(), "{asked:?}");
3270 }
3271
3272 #[tokio::test]
3273 async fn mock_query_times_out_as_an_error() {
3274 let port = spawn_mock_dns(MockMode::Ignore).await;
3275 let resolver = DnsResolver::new()
3276 .with_timeout(Duration::from_millis(200))
3277 .allowing_private_hosts()
3278 .with_port(port);
3279 let result = resolver
3280 .query("www.seer.test", RecordType::A, Some("127.0.0.1"))
3281 .await;
3282 assert!(
3283 matches!(&result, Err(SeerError::DnsError(m)) if m.contains("A lookup failed")),
3284 "an unanswered query is a transport error, got: {result:?}"
3285 );
3286 }
3287
3288 #[tokio::test]
3289 async fn mock_query_reads_flags_from_the_response_header() {
3290 let port = spawn_mock_dns_fn(|qname, qtype| match (qname, qtype) {
3291 ("seer.test", HickoryRecordType::A) => {
3292 MockReply::AuthoritativeAnswer(vec![a_rdata([192, 0, 2, 1])])
3293 }
3294 _ => MockReply::NxDomain,
3295 })
3296 .await;
3297 let result = mock_query(port, "seer.test", RecordType::A).await;
3298 assert_eq!(result.flags, ["qr", "aa", "rd", "ra"]);
3299 assert_eq!(result.cname_chain().count(), 0);
3300 assert_eq!(
3301 result.records().next().map(|r| r.name.as_str()),
3302 Some("seer.test")
3303 );
3304 }
3305
3306 #[tokio::test]
3307 async fn mock_query_converts_the_new_record_types_under_their_owner() {
3308 let port = spawn_mock_dns(MockMode::Zone).await;
3309 let https = mock_query(port, "seer.test", RecordType::HTTPS).await;
3310 assert_eq!(https.answers.len(), 1, "{https:?}");
3311 assert_eq!(https.answers[0].name, "seer.test");
3312 assert!(https.answers[0]
3313 .data
3314 .to_string()
3315 .starts_with("1 . alpn=\"h3,h2\" port=8443"));
3316
3317 let alias = mock_query(port, "alias.seer.test", RecordType::HTTPS).await;
3318 assert_eq!(alias.answers[0].data.to_string(), "0 pool.seer.test.");
3319 assert_eq!(alias.answers[0].name, "alias.seer.test");
3320
3321 let cds = mock_query(port, "seer.test", RecordType::CDS).await;
3322 assert_eq!(cds.answers.len(), 2);
3323 let cdnskey = mock_query(port, "seer.test", RecordType::CDNSKEY).await;
3324 assert_eq!(cdnskey.answers[1].data.to_string(), "0 3 0 AA==");
3325 }
3326
3327 #[tokio::test]
3328 async fn mock_query_sorts_mx_like_resolve() {
3329 let port = spawn_mock_dns(MockMode::Zone).await;
3330 let result = mock_query(port, "seer.test", RecordType::MX).await;
3331 let prefs: Vec<String> = result.answers.iter().map(|r| r.data.to_string()).collect();
3332 assert_eq!(
3333 prefs,
3334 [
3335 "10 a.mail.seer.test.",
3336 "20 b.mail.seer.test.",
3337 "30 c.mail.seer.test."
3338 ]
3339 );
3340 }
3341
3342 #[tokio::test]
3343 async fn mock_query_resolves_srv_and_ptr_names_like_resolve() {
3344 let (port, asked) = spawn_recording(|qname, qtype| match (qname, qtype) {
3345 ("_sip._tcp.seer.test", HickoryRecordType::SRV) => {
3346 MockReply::Answer(vec![HickoryRData::SRV(
3347 hickory_resolver::proto::rr::rdata::SRV::new(
3348 10,
3349 5,
3350 5060,
3351 Name::from_ascii("sip.seer.test.").unwrap(),
3352 ),
3353 )])
3354 }
3355 ("1.2.0.192.in-addr.arpa", HickoryRecordType::PTR) => {
3356 MockReply::Answer(vec![HickoryRData::PTR(
3357 hickory_resolver::proto::rr::rdata::PTR(
3358 Name::from_ascii("ptr.seer.test.").unwrap(),
3359 ),
3360 )])
3361 }
3362 _ => MockReply::NxDomain,
3363 })
3364 .await;
3365
3366 let srv = mock_query(port, "_sip._tcp.seer.test", RecordType::SRV).await;
3367 assert_eq!(
3368 shown(&srv.answers.iter().collect::<Vec<_>>()),
3369 [row(
3370 "_sip._tcp.seer.test",
3371 RecordType::SRV,
3372 "10 5 5060 sip.seer.test."
3373 )]
3374 );
3375 let probe = srv.wildcard.expect("SRV name is below seer.test");
3377 assert!(probe.probe_name.ends_with("._tcp.seer.test"), "{probe:?}");
3378 assert!(asked.lock().unwrap().iter().any(|(n, t)| is_probe(n)
3379 && n.ends_with("._tcp.seer.test")
3380 && *t == HickoryRecordType::SRV));
3381
3382 let ptr = mock_query(port, "192.0.2.1", RecordType::PTR).await;
3383 assert_eq!(ptr.name, "1.2.0.192.in-addr.arpa");
3384 assert_eq!(ptr.answers[0].name, "1.2.0.192.in-addr.arpa");
3385
3386 let err = mock_dns_resolver(port)
3387 .query("seer.test", RecordType::SRV, Some("127.0.0.1"))
3388 .await
3389 .expect_err("a bare domain is not an SRV query");
3390 assert!(matches!(err, SeerError::InvalidInput(_)), "{err:?}");
3391 let err = mock_dns_resolver(port)
3392 .query(".bad.example", RecordType::A, Some("127.0.0.1"))
3393 .await;
3394 assert!(err.is_err(), "invalid names are rejected before any I/O");
3395 }
3396
3397 #[tokio::test]
3398 async fn mock_query_any_fans_out_to_the_new_types_and_dedupes() {
3399 let (port, asked) = spawn_recording(|qname, qtype| {
3400 let chain = || record("www.seer.test.", 300, cname_rdata("edge.seer.test."));
3401 match (qname, qtype) {
3402 ("www.seer.test", HickoryRecordType::CNAME) => MockReply::Records(vec![chain()]),
3403 ("www.seer.test", HickoryRecordType::A) => MockReply::Records(vec![
3404 chain(),
3405 record("edge.seer.test.", 60, a_rdata([192, 0, 2, 7])),
3406 ]),
3407 ("www.seer.test", HickoryRecordType::HTTPS) => {
3408 MockReply::Records(vec![
3409 chain(),
3410 record(
3411 "edge.seer.test.",
3412 60,
3413 HickoryRData::HTTPS(hickory_resolver::proto::rr::rdata::HTTPS(
3414 SVCB::new(1, Name::root(), vec![]),
3415 )),
3416 ),
3417 ])
3418 }
3419 ("www.seer.test", _) => MockReply::Records(vec![chain()]),
3422 _ => MockReply::NoDataWithSoa("seer.test"),
3423 }
3424 })
3425 .await;
3426 let result = mock_query(port, "www.seer.test", RecordType::ANY).await;
3427
3428 assert_eq!(result.record_type, RecordType::ANY);
3429 assert_eq!(result.status, DnsStatus::NoError);
3430 assert_eq!(result.flags, ["qr", "rd", "ra"]);
3431 assert_eq!(
3433 shown(&result.answers.iter().collect::<Vec<_>>()),
3434 [
3435 row("www.seer.test", RecordType::CNAME, "edge.seer.test."),
3436 row("edge.seer.test", RecordType::A, "192.0.2.7"),
3437 row("edge.seer.test", RecordType::HTTPS, "1 ."),
3438 ]
3439 );
3440 assert_eq!(result.cname_chain().count(), 1);
3441 assert_eq!(result.records().count(), 2);
3442 assert!(!result.is_nodata());
3443 assert_eq!(result.wildcard, None, "ANY is never probed");
3444
3445 let asked: Vec<HickoryRecordType> = asked
3446 .lock()
3447 .unwrap()
3448 .iter()
3449 .filter(|(n, _)| n == "www.seer.test")
3450 .map(|(_, t)| *t)
3451 .collect();
3452 for wire in ANY_TYPES.iter().filter_map(|t| wire_type(*t)) {
3453 assert!(asked.contains(&wire), "ANY must query {wire}: {asked:?}");
3454 }
3455 assert!(
3456 !asked.contains(&HickoryRecordType::ANY),
3457 "ANY is never sent"
3458 );
3459 }
3460
3461 #[tokio::test]
3462 async fn mock_query_any_errors_only_when_every_sub_query_fails() {
3463 let port = spawn_mock_dns(MockMode::Ignore).await;
3464 let resolver = DnsResolver::new()
3465 .with_timeout(Duration::from_millis(200))
3466 .allowing_private_hosts()
3467 .with_port(port);
3468 let result = resolver
3469 .query("www.seer.test", RecordType::ANY, Some("127.0.0.1"))
3470 .await;
3471 assert!(matches!(result, Err(SeerError::DnsError(_))), "{result:?}");
3472 }
3473
3474 async fn wildcard_zone(probe_reply: fn() -> MockReply) -> (u16, Asked) {
3477 spawn_recording(move |qname, qtype| match (qname, qtype) {
3478 ("www.seer.test", HickoryRecordType::A) => {
3479 MockReply::Answer(vec![a_rdata([192, 0, 2, 50])])
3480 }
3481 (name, HickoryRecordType::A) if is_probe(name) => probe_reply(),
3482 _ => MockReply::NxDomain,
3483 })
3484 .await
3485 }
3486
3487 #[tokio::test]
3488 async fn mock_wildcard_probe_matching_the_answer() {
3489 let (port, asked) =
3490 wildcard_zone(|| MockReply::Answer(vec![a_rdata([192, 0, 2, 50])])).await;
3491 let result = mock_query(port, "www.seer.test", RecordType::A).await;
3492 let probe = result.wildcard.expect("probe attached");
3493 assert!(probe.present && probe.matches_answer, "{probe:?}");
3494 assert_eq!(probes_sent(&asked), 1);
3495 }
3496
3497 #[tokio::test]
3498 async fn mock_wildcard_probe_with_different_data() {
3499 let (port, _) = wildcard_zone(|| MockReply::Answer(vec![a_rdata([192, 0, 2, 99])])).await;
3500 let result = mock_query(port, "www.seer.test", RecordType::A).await;
3501 let probe = result.wildcard.expect("probe attached");
3502 assert!(probe.present && !probe.matches_answer, "{probe:?}");
3503 }
3504
3505 #[tokio::test]
3506 async fn mock_wildcard_probe_absent() {
3507 let (port, _) = wildcard_zone(|| MockReply::NxDomainWithSoa("seer.test")).await;
3508 let result = mock_query(port, "www.seer.test", RecordType::A).await;
3509 let probe = result.wildcard.expect("probe attached");
3510 assert!(!probe.present && !probe.matches_answer, "{probe:?}");
3511 }
3512
3513 #[tokio::test]
3514 async fn mock_wildcard_probe_that_fails_is_dropped_not_fatal() {
3515 let (port, _) = wildcard_zone(|| MockReply::Rcode(ResponseCode::ServFail)).await;
3516 let result = mock_query(port, "www.seer.test", RecordType::A).await;
3517 assert_eq!(result.status, DnsStatus::NoError);
3518 assert_eq!(result.wildcard, None);
3519
3520 let (port, _) = wildcard_zone(|| MockReply::NoReply).await;
3522 let resolver = DnsResolver::new()
3523 .with_timeout(Duration::from_millis(200))
3524 .allowing_private_hosts()
3525 .with_port(port);
3526 let result = resolver
3527 .query("www.seer.test", RecordType::A, Some("127.0.0.1"))
3528 .await
3529 .expect("the probe never fails the query");
3530 assert_eq!(result.records().count(), 1);
3531 assert_eq!(result.wildcard, None);
3532 }
3533
3534 #[tokio::test]
3535 async fn mock_wildcard_probe_is_not_sent_for_ineligible_names() {
3536 let (port, asked) =
3538 spawn_recording(|_, _| MockReply::Answer(vec![a_rdata([192, 0, 2, 50])])).await;
3539 assert_eq!(
3541 mock_query(port, "seer.test", RecordType::A).await.wildcard,
3542 None
3543 );
3544 assert_eq!(
3546 mock_query(port, "*.seer.test", RecordType::A)
3547 .await
3548 .wildcard,
3549 None
3550 );
3551 assert_eq!(
3553 mock_query(port, "www.seer.test", RecordType::ANY)
3554 .await
3555 .wildcard,
3556 None
3557 );
3558 assert_eq!(probes_sent(&asked), 0, "{:?}", asked.lock().unwrap());
3559 }
3560
3561 #[tokio::test]
3562 async fn mock_nodata_folds_to_empty_and_classifies_absent() {
3563 let port = spawn_mock_dns(MockMode::NoData).await;
3564 let result = mock_dns_resolver(port)
3565 .resolve("seer.test", RecordType::NS, Some("127.0.0.1"))
3566 .await;
3567 assert!(
3568 matches!(&result, Ok(records) if records.is_empty()),
3569 "NODATA must fold to Ok(vec![]), got: {result:?}"
3570 );
3571 assert_eq!(classify_ns_presence(&result), DnsPresence::Absent);
3572 }
3573
3574 #[tokio::test]
3575 async fn mock_nxdomain_folds_to_empty_and_classifies_absent() {
3576 let port = spawn_mock_dns(MockMode::Nxdomain).await;
3577 let result = mock_dns_resolver(port)
3578 .resolve("seer.test", RecordType::NS, Some("127.0.0.1"))
3579 .await;
3580 assert!(
3581 matches!(&result, Ok(records) if records.is_empty()),
3582 "NXDOMAIN must fold to Ok(vec![]), got: {result:?}"
3583 );
3584 assert_eq!(classify_ns_presence(&result), DnsPresence::Absent);
3585 }
3586
3587 #[tokio::test]
3588 async fn mock_timeout_errors_and_classifies_unknown() {
3589 let port = spawn_mock_dns(MockMode::Ignore).await;
3590 let resolver = DnsResolver::new()
3592 .with_timeout(Duration::from_millis(200))
3593 .allowing_private_hosts()
3594 .with_port(port);
3595 let result = resolver
3596 .resolve("seer.test", RecordType::NS, Some("127.0.0.1"))
3597 .await;
3598 match &result {
3599 Err(SeerError::DnsError(_)) => {}
3600 other => panic!("unanswered query must surface a DnsError, got: {other:?}"),
3601 }
3602 assert_eq!(classify_ns_presence(&result), DnsPresence::Unknown);
3603 }
3604
3605 #[tokio::test]
3606 async fn resolve_type_rejects_composite_types_consistently() {
3607 let r = DnsResolver::new();
3612 for composite in [RecordType::SRV, RecordType::ANY] {
3613 let err = r
3614 .resolve_type(&r.default_resolver, "seer.test", composite)
3615 .await
3616 .expect_err("composite types must be rejected by resolve_type");
3617 assert_eq!(
3618 err.to_string(),
3619 unsupported_record_type(composite).to_string()
3620 );
3621 }
3622 }
3623
3624 #[tokio::test]
3625 async fn mock_unanswered_probe_costs_a_positive_answer_only_the_grace() {
3626 let (port, asked) = wildcard_zone(|| MockReply::NoReply).await;
3630 let resolver = DnsResolver::new()
3631 .with_timeout(Duration::from_secs(2))
3632 .allowing_private_hosts()
3633 .with_port(port);
3634 let started = Instant::now();
3635 let result = resolver
3636 .query("www.seer.test", RecordType::A, Some("127.0.0.1"))
3637 .await
3638 .expect("the main query is answered");
3639 let elapsed = started.elapsed();
3640 assert_eq!(result.records().count(), 1);
3641 assert_eq!(result.wildcard, None, "an unfinished probe says nothing");
3642 assert!(probes_sent(&asked) >= 1, "the probe was sent");
3643 assert!(elapsed >= Duration::from_millis(400), "{elapsed:?}");
3645 assert!(elapsed < Duration::from_millis(1500), "{elapsed:?}");
3646 }
3647
3648 async fn partly_silent_zone() -> u16 {
3651 spawn_mock_dns_fn(|_, qtype| match qtype {
3652 HickoryRecordType::A => MockReply::Answer(vec![a_rdata([192, 0, 2, 7])]),
3653 HickoryRecordType::TXT | HickoryRecordType::DNSKEY => MockReply::NoReply,
3654 _ => MockReply::NoDataWithSoa("seer.test"),
3655 })
3656 .await
3657 }
3658
3659 fn short_resolver(port: u16) -> DnsResolver {
3660 DnsResolver::new()
3661 .with_timeout(Duration::from_millis(200))
3662 .allowing_private_hosts()
3663 .with_port(port)
3664 }
3665
3666 #[tokio::test]
3667 async fn mock_query_any_reports_the_types_that_failed() {
3668 let port = partly_silent_zone().await;
3671 let result = short_resolver(port)
3672 .query("www.seer.test", RecordType::ANY, Some("127.0.0.1"))
3673 .await
3674 .expect("some sub-queries were answered");
3675 assert_eq!(result.records().count(), 1);
3676 let failed: Vec<RecordType> = result.failed_types.iter().map(|f| f.record_type).collect();
3677 assert_eq!(failed, [RecordType::TXT, RecordType::DNSKEY]);
3678 assert!(
3679 result.failed_types[0].error.contains("timed out"),
3680 "{:?}",
3681 result.failed_types
3682 );
3683
3684 let a = short_resolver(port)
3686 .query("www.seer.test", RecordType::A, Some("127.0.0.1"))
3687 .await
3688 .unwrap();
3689 assert!(a.failed_types.is_empty());
3690 }
3691
3692 #[tokio::test]
3693 async fn mock_query_server_any_keeps_partial_failures_apart_from_silence() {
3694 let port = partly_silent_zone().await;
3695 let reply = short_resolver(port)
3696 .query_server("www.seer.test", RecordType::ANY, "127.0.0.1")
3697 .await
3698 .unwrap();
3699 let ServerReply::Response(result) = reply else {
3700 panic!("a partial answer is a response: {reply:?}");
3701 };
3702 assert_eq!(result.records().count(), 1);
3703 let failed: Vec<RecordType> = result.failed_types.iter().map(|f| f.record_type).collect();
3704 assert_eq!(failed, [RecordType::TXT, RecordType::DNSKEY]);
3705
3706 let silent = spawn_mock_dns(MockMode::Ignore).await;
3708 let reply = short_resolver(silent)
3709 .query_server("www.seer.test", RecordType::ANY, "127.0.0.1")
3710 .await
3711 .unwrap();
3712 assert!(
3713 matches!(&reply, ServerReply::Silent(reason) if reason == "timed out"),
3714 "{reply:?}"
3715 );
3716 }
3717
3718 #[tokio::test]
3719 async fn resolve_validates_the_name_before_resolving_the_nameserver() {
3720 let err = DnsResolver::new()
3724 .resolve(".bad..name", RecordType::A, Some("ns.seer.test"))
3725 .await
3726 .expect_err("an invalid name is rejected");
3727 assert!(
3728 matches!(
3729 err,
3730 SeerError::InvalidDomain(_) | SeerError::InvalidInput(_)
3731 ),
3732 "{err:?}"
3733 );
3734 }
3735
3736 #[tokio::test]
3737 async fn a_local_answer_never_resolves_the_nameserver_hostname() {
3738 let r = DnsResolver::new();
3743 let result = r
3744 .query(
3745 "127.0.0.1",
3746 RecordType::PTR,
3747 Some("ns.does-not-resolve.test"),
3748 )
3749 .await
3750 .expect("answered locally, the nameserver unused");
3751 assert!(result.answered_locally);
3752 assert_eq!(result.records().count(), 1);
3753
3754 let blocked = r
3755 .query("127.0.0.1", RecordType::PTR, Some("10.0.0.1"))
3756 .await
3757 .expect_err("a reserved nameserver is refused all the same");
3758 assert!(blocked.to_string().contains("blocked"), "{blocked}");
3759 assert!(r
3760 .query("127.0.0.1", RecordType::PTR, Some("ftp://ns.seer.test"))
3761 .await
3762 .is_err());
3763 }
3764}