1use std::borrow::Cow;
19use std::net::IpAddr;
20use std::pin::pin;
21use std::str::FromStr;
22use std::sync::Arc;
23use std::time::{Duration, Instant};
24
25use futures::future::{self, Either};
26use hickory_resolver::config::{
27 NameServerConfig, ResolveHosts, ResolverConfig, ResolverOpts, ServerOrderingStrategy, GOOGLE,
28};
29use hickory_resolver::net::runtime::TokioRuntimeProvider;
30use hickory_resolver::net::NetError;
31use hickory_resolver::proto::dnssec::PublicKey;
32use hickory_resolver::proto::rr::rdata::svcb::{SvcParamKey, SvcParamValue, SVCB};
33use hickory_resolver::proto::rr::rdata::CAA;
34use hickory_resolver::proto::rr::{
35 Name, RData as HickoryRData, Record, RecordType as HickoryRecordType,
36};
37use hickory_resolver::proto::serialize::binary::BinEncodable;
38use hickory_resolver::TokioResolver;
39use tracing::{debug, instrument};
40
41use super::nameserver::{NameserverProtocol, NameserverSpec};
42use super::query::{
43 answered_locally, dedupe_records, merge_any, random_probe_label, wildcard_outcome,
44 wildcard_probe_name, DnsQueryResult, Exchange, WildcardProbe,
45};
46use super::records::{DnsRecord, RecordData, RecordType, SvcParam};
47use super::transport::Transport;
48use crate::error::{Result, SeerError};
49use crate::validation::{normalize_domain, normalize_query_name};
50
51fn dns_lookup_or_empty<T>(
55 result: std::result::Result<T, NetError>,
56 record_type: &str,
57) -> Result<Option<T>> {
58 match result {
59 Ok(response) => Ok(Some(response)),
60 Err(e) if e.is_no_records_found() => Ok(None),
61 Err(e) => Err(SeerError::DnsError(format!(
62 "{} lookup failed: {}",
63 record_type, e
64 ))),
65 }
66}
67
68const DEFAULT_TIMEOUT: Duration = Duration::from_secs(5);
71
72const ANY_TYPES: [RecordType; 11] = [
79 RecordType::A,
80 RecordType::AAAA,
81 RecordType::CNAME,
82 RecordType::MX,
83 RecordType::NS,
84 RecordType::TXT,
85 RecordType::SOA,
86 RecordType::CAA,
87 RecordType::HTTPS,
88 RecordType::DS,
89 RecordType::DNSKEY,
90];
91
92pub(crate) fn apply_standard_opts(opts: &mut ResolverOpts, timeout: Duration) {
100 opts.timeout = timeout;
101 opts.attempts = 2;
102 opts.use_hosts_file = ResolveHosts::Never;
103 opts.server_ordering_strategy = ServerOrderingStrategy::UserProvidedOrder;
124}
125
126fn build_resolver(config: ResolverConfig, timeout: Duration) -> Result<TokioResolver> {
135 let mut builder = TokioResolver::builder_with_config(config, TokioRuntimeProvider::default());
136 apply_standard_opts(builder.options_mut(), timeout);
137 builder
138 .build()
139 .map_err(|e| SeerError::DnsError(format!("failed to construct DNS resolver: {}", e)))
140}
141
142fn build_default_resolver(timeout: Duration) -> TokioResolver {
149 build_resolver(default_upstream_config(), timeout)
150 .expect("default resolver build cannot fail with the bundled webpki root store")
151}
152
153fn default_upstream_config() -> ResolverConfig {
157 ResolverConfig::udp_and_tcp(&GOOGLE)
158}
159
160pub(crate) fn single_server_config(ip: IpAddr, port: u16, tcp_fallback: bool) -> ResolverConfig {
163 let mut ns = if tcp_fallback {
164 NameServerConfig::udp_and_tcp(ip)
165 } else {
166 NameServerConfig::udp(ip)
167 };
168 for connection in &mut ns.connections {
169 connection.port = port;
170 }
171 let mut config = ResolverConfig::from_parts(None, vec![], vec![]);
172 config.add_name_server(ns);
173 config
174}
175
176pub(crate) fn google_or_pinned(upstream: Option<(IpAddr, u16)>) -> ResolverConfig {
179 match upstream {
180 None => default_upstream_config(),
181 Some((ip, port)) => single_server_config(ip, port, false),
182 }
183}
184
185pub(crate) fn fqdn(name: &str) -> String {
188 if name.ends_with('.') {
189 name.to_string()
190 } else {
191 format!("{name}.")
192 }
193}
194
195fn build_upstream_config(
205 spec: &NameserverSpec,
206 ips: &[IpAddr],
207 port_override: Option<u16>,
208) -> ResolverConfig {
209 let mut config = ResolverConfig::from_parts(None, vec![], vec![]);
210 let port = port_override.unwrap_or(spec.port);
211 let ordered = ips
222 .iter()
223 .filter(|ip| ip.is_ipv4())
224 .chain(ips.iter().filter(|ip| ip.is_ipv6()));
225 for ip in ordered {
226 let mut ns = match spec.protocol {
227 NameserverProtocol::Udp => NameServerConfig::udp(*ip),
228 NameserverProtocol::Tls => NameServerConfig::tls(*ip, Arc::from(spec.tls_name())),
229 NameserverProtocol::Https => NameServerConfig::https(
230 *ip,
231 Arc::from(spec.tls_name()),
232 spec.path.as_deref().map(Arc::from),
233 ),
234 };
235 for connection in &mut ns.connections {
236 connection.port = port;
237 }
238 config.add_name_server(ns);
239 }
240 config
241}
242
243#[derive(Clone)]
251pub struct DnsResolver {
252 timeout: Duration,
253 default_resolver: TokioResolver,
256 port_override: Option<u16>,
261 allow_private_hosts: bool,
266 #[cfg(test)]
270 test_default_nameserver: Option<String>,
271}
272
273impl std::fmt::Debug for DnsResolver {
274 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
275 f.debug_struct("DnsResolver")
276 .field("timeout", &self.timeout)
277 .finish()
278 }
279}
280
281impl Default for DnsResolver {
282 fn default() -> Self {
283 Self::new()
284 }
285}
286
287impl DnsResolver {
288 pub fn new() -> Self {
290 Self {
291 timeout: DEFAULT_TIMEOUT,
292 default_resolver: build_default_resolver(DEFAULT_TIMEOUT),
293 port_override: None,
294 allow_private_hosts: false,
295 #[cfg(test)]
296 test_default_nameserver: None,
297 }
298 }
299
300 pub fn from_config(config: &crate::config::SeerConfig) -> Self {
312 Self::new().with_timeout(config.dns_timeout())
313 }
314
315 #[cfg(test)]
317 pub(crate) fn timeout(&self) -> Duration {
318 self.timeout
319 }
320
321 #[cfg(test)]
323 pub(crate) fn allowing_private_hosts(mut self) -> Self {
324 self.allow_private_hosts = true;
325 self
326 }
327
328 #[cfg(test)]
331 pub(crate) fn with_port(mut self, port: u16) -> Self {
332 self.port_override = Some(port);
333 self
334 }
335
336 #[cfg(test)]
339 pub(crate) fn with_default_nameserver(mut self, nameserver: &str) -> Self {
340 self.test_default_nameserver = Some(nameserver.to_string());
341 self
342 }
343
344 #[cfg(test)]
348 fn effective_nameserver<'a>(&'a self, nameserver: Option<&'a str>) -> Option<&'a str> {
349 nameserver.or(self.test_default_nameserver.as_deref())
350 }
351
352 #[cfg(not(test))]
353 fn effective_nameserver<'a>(&self, nameserver: Option<&'a str>) -> Option<&'a str> {
354 nameserver
355 }
356
357 pub fn with_timeout(mut self, timeout: Duration) -> Self {
361 self.timeout = timeout;
362 self.default_resolver = build_default_resolver(timeout);
363 self
364 }
365
366 async fn create_custom_resolver(&self, nameserver: &str) -> Result<TokioResolver> {
367 build_resolver(self.custom_upstream_config(nameserver).await?, self.timeout)
368 }
369
370 async fn custom_upstream_config(&self, nameserver: &str) -> Result<ResolverConfig> {
377 let spec = NameserverSpec::parse(nameserver)?;
382
383 let ips: Vec<IpAddr> = if let Ok(ip) = spec.host.parse::<IpAddr>() {
392 vec![ip]
393 } else {
394 let response = self
395 .default_resolver
396 .lookup_ip(spec.host.as_str())
397 .await
398 .map_err(|e| {
399 SeerError::DnsError(format!(
400 "failed to resolve nameserver hostname {}: {}",
401 spec.host, e
402 ))
403 })?;
404 let resolved: Vec<IpAddr> = response.iter().collect();
405 if resolved.is_empty() {
406 return Err(SeerError::DnsError(format!(
407 "nameserver {} did not resolve to any addresses",
408 spec.host
409 )));
410 }
411 resolved
412 };
413
414 if !self.allow_private_hosts {
421 for ip in &ips {
422 if let Some(reason) = crate::validation::describe_reserved_ip(ip) {
423 return Err(SeerError::DnsError(format!(
424 "nameserver {} blocked: {}",
425 nameserver, reason
426 )));
427 }
428 }
429 }
430
431 Ok(build_upstream_config(&spec, &ips, self.port_override))
432 }
433
434 async fn upstream(&self, nameserver: Option<&str>) -> Result<Cow<'_, TokioResolver>> {
438 Ok(match self.effective_nameserver(nameserver) {
439 Some(ns) => Cow::Owned(self.create_custom_resolver(ns).await?),
440 None => Cow::Borrowed(&self.default_resolver),
441 })
442 }
443
444 async fn upstream_config(&self, nameserver: Option<&str>) -> Result<ResolverConfig> {
448 match self.effective_nameserver(nameserver) {
449 Some(ns) => self.custom_upstream_config(ns).await,
450 None => Ok(default_upstream_config()),
451 }
452 }
453
454 #[instrument(skip(self), fields(domain = %domain, record_type = %record_type))]
497 pub async fn query(
498 &self,
499 domain: &str,
500 record_type: RecordType,
501 nameserver: Option<&str>,
502 ) -> Result<DnsQueryResult> {
503 let domain = prepare_query(domain, record_type)?;
505 let name = wire_query_name(&domain, record_type)?;
506 let resolver;
507 let upstream = if answered_locally(&name) {
508 resolver = self.upstream(nameserver).await?;
509 Upstream::Local(&resolver)
510 } else {
511 Upstream::Direct {
512 config: self.upstream_config(nameserver).await?,
513 transport: Transport::new(self.timeout),
514 }
515 };
516
517 debug!(nameserver = nameserver.unwrap_or("system"), "Querying DNS");
518
519 let (exchange, query_time, wildcard) = if record_type == RecordType::ANY {
520 let started = Instant::now();
521 let exchange = exchange_any(&upstream, &name).await?;
522 (exchange, started.elapsed(), None)
523 } else {
524 exchange_with_probe(&upstream, &name, record_type).await?
525 };
526 Ok(exchange.into_result(name, record_type, nameserver, wildcard, query_time))
527 }
528
529 #[instrument(skip(self), fields(domain = %domain, record_type = %record_type))]
542 pub async fn resolve(
543 &self,
544 domain: &str,
545 record_type: RecordType,
546 nameserver: Option<&str>,
547 ) -> Result<Vec<DnsRecord>> {
548 let resolver = self.upstream(nameserver).await?;
550 let domain = prepare_query(domain, record_type)?;
551
552 debug!(nameserver = nameserver.unwrap_or("system"), "Resolving DNS");
553
554 match record_type {
555 RecordType::SRV => match parse_srv_query(&domain) {
556 Some((service, protocol, name)) => {
558 self.resolve_srv_core(&resolver, &service, &protocol, &name)
559 .await
560 }
561 None => Err(srv_format_error()),
564 },
565 RecordType::ANY => self.resolve_any(&resolver, &domain).await,
566 single => self.resolve_type(&resolver, &domain, single).await,
567 }
568 }
569
570 async fn resolve_srv_core(
577 &self,
578 resolver: &TokioResolver,
579 service: &str,
580 protocol: &str,
581 domain: &str,
582 ) -> Result<Vec<DnsRecord>> {
583 let query_name = srv_query_name(service, protocol, domain)?;
584 self.resolve_records(resolver, &query_name, RecordType::SRV)
585 .await
586 }
587
588 async fn resolve_type(
596 &self,
597 resolver: &TokioResolver,
598 domain: &str,
599 record_type: RecordType,
600 ) -> Result<Vec<DnsRecord>> {
601 match record_type {
602 RecordType::SRV | RecordType::ANY => Err(unsupported_record_type(record_type)),
603 RecordType::PTR => {
606 self.resolve_records(resolver, &ptr_query_name(domain), RecordType::PTR)
607 .await
608 }
609 single => self.resolve_records(resolver, domain, single).await,
610 }
611 }
612
613 async fn resolve_records(
621 &self,
622 resolver: &TokioResolver,
623 domain: &str,
624 record_type: RecordType,
625 ) -> Result<Vec<DnsRecord>> {
626 let Some(wire_type) = wire_type(record_type) else {
627 return Err(unsupported_record_type(record_type));
628 };
629
630 let Some(response) = dns_lookup_or_empty(
631 resolver.lookup(domain, wire_type).await,
632 &record_type.to_string(),
633 )?
634 else {
635 return Ok(vec![]);
636 };
637
638 let mut records: Vec<DnsRecord> = response
639 .answers()
640 .iter()
641 .filter_map(|record| {
642 convert_rdata(record_type, &record.data, NameSpelling::Display).map(|data| {
643 DnsRecord {
644 name: domain.to_string(),
645 record_type,
646 ttl: record.ttl,
647 data,
648 }
649 })
650 })
651 .collect();
652
653 if record_type == RecordType::MX {
654 records.sort_by_key(|r| match &r.data {
655 RecordData::MX { preference, .. } => *preference,
656 _ => 0,
657 });
658 }
659
660 Ok(records)
661 }
662
663 async fn resolve_any(&self, resolver: &TokioResolver, domain: &str) -> Result<Vec<DnsRecord>> {
664 let results = future::join_all(
669 ANY_TYPES
670 .into_iter()
671 .map(|record_type| self.resolve_type(resolver, domain, record_type)),
672 )
673 .await;
674
675 let mut all_records = Vec::new();
680 let mut any_ok = false;
681 let mut last_err = None;
682 for result in results {
683 match result {
684 Ok(records) => {
685 any_ok = true;
686 all_records.extend(records);
687 }
688 Err(e) => last_err = Some(e),
689 }
690 }
691
692 match last_err {
693 Some(e) if !any_ok => Err(e),
694 _ => Ok(dedupe_records(all_records)),
695 }
696 }
697}
698
699enum Upstream<'a> {
701 Local(&'a TokioResolver),
704 Direct {
707 config: ResolverConfig,
708 transport: Transport,
709 },
710}
711
712impl Upstream<'_> {
713 async fn exchange(&self, name: &str, record_type: RecordType) -> Result<Exchange> {
716 let wire = wire_type(record_type).ok_or_else(|| unsupported_record_type(record_type))?;
717 let exchange = match self {
718 Upstream::Local(resolver) => {
719 Exchange::from_local(resolver.lookup(name, wire).await, record_type)?
720 }
721 Upstream::Direct { config, transport } => {
722 let qname = Name::from_ascii(fqdn(name))
723 .map_err(|e| SeerError::InvalidDomain(format!("{name}: {e}")))?;
724 let request = transport.request(qname, wire, true);
725 let response = transport
726 .query(config.name_servers(), &request)
727 .await
728 .map_err(|why| {
729 SeerError::DnsError(format!("{record_type} lookup failed: {why}"))
730 })?;
731 Exchange::from_message(&response)
732 }
733 };
734 Ok(exchange.ordered(name, record_type))
735 }
736}
737
738async fn exchange_any(upstream: &Upstream<'_>, name: &str) -> Result<Exchange> {
741 let results = future::join_all(
742 ANY_TYPES
743 .into_iter()
744 .map(|record_type| upstream.exchange(name, record_type)),
745 )
746 .await;
747 merge_any(results)
748}
749
750async fn exchange_with_probe(
759 upstream: &Upstream<'_>,
760 name: &str,
761 record_type: RecordType,
762) -> Result<(Exchange, Duration, Option<WildcardProbe>)> {
763 let main = async {
764 let started = Instant::now();
765 let result = upstream.exchange(name, record_type).await;
766 (result, started.elapsed())
767 };
768 let Some(probe_name) = wildcard_probe_name(name, record_type, &random_probe_label()) else {
769 let (result, elapsed) = main.await;
770 return Ok((result?, elapsed, None));
771 };
772 let probe = upstream.exchange(&probe_name, record_type);
773
774 let main = pin!(main);
775 let probe = pin!(probe);
776 let ((result, elapsed), probe) = match future::select(main, probe).await {
777 Either::Left(((result, elapsed), probe)) => {
778 let attachable = matches!(&result, Ok(answer) if answer.has_records(record_type));
779 let probe = if attachable { Some(probe.await) } else { None };
780 ((result, elapsed), probe)
781 }
782 Either::Right((probe, main)) => (main.await, Some(probe)),
783 };
784 let answer = result?;
785 let wildcard =
786 probe.and_then(|probe| wildcard_outcome(&probe_name, record_type, &answer, probe));
787 Ok((answer, elapsed, wildcard))
788}
789
790#[derive(Debug, Clone, Copy, PartialEq, Eq)]
795pub enum DnsPresence {
796 Present,
798 Absent,
800 Unknown,
802}
803
804fn classify_ns_presence(result: &Result<Vec<DnsRecord>>) -> DnsPresence {
809 match result {
810 Ok(records) if records.is_empty() => DnsPresence::Absent,
811 Ok(_) => DnsPresence::Present,
812 Err(_) => DnsPresence::Unknown,
813 }
814}
815
816impl DnsResolver {
817 pub async fn presence(&self, domain: &str) -> DnsPresence {
825 let Ok(apex) = normalize_domain(domain) else {
829 return DnsPresence::Unknown;
830 };
831 classify_ns_presence(&self.resolve(&apex, RecordType::NS, None).await)
832 }
833}
834
835pub(crate) fn prepare_query(domain: &str, record_type: RecordType) -> Result<String> {
858 prepare_query_with(domain, record_type, normalize_query_name)
859}
860
861fn prepare_query_with(
865 domain: &str,
866 record_type: RecordType,
867 normalize: impl Fn(&str) -> Result<String>,
868) -> Result<String> {
869 if record_type == RecordType::PTR {
870 if let Ok(ip) = IpAddr::from_str(domain.trim()) {
871 normalize(&reverse_dns_name(&ip))?;
877 return Ok(ip.to_string());
878 }
879 }
880 normalize(domain)
881}
882
883pub(crate) fn parse_srv_query(name: &str) -> Option<(String, String, String)> {
888 let mut parts = name.splitn(3, '.');
889 let service = parts.next()?.strip_prefix('_')?;
890 let protocol = parts.next()?.strip_prefix('_')?;
891 let rest = parts.next()?;
892 if service.is_empty() || protocol.is_empty() || rest.is_empty() {
893 return None;
894 }
895 Some((service.to_string(), protocol.to_string(), rest.to_string()))
896}
897
898fn srv_query_name(service: &str, protocol: &str, domain: &str) -> Result<String> {
902 if !is_valid_srv_label(service) {
903 return Err(SeerError::InvalidInput(format!(
904 "invalid SRV service name: {}",
905 service
906 )));
907 }
908 if !is_valid_srv_label(protocol) {
909 return Err(SeerError::InvalidInput(format!(
910 "invalid SRV protocol name: {}",
911 protocol
912 )));
913 }
914 Ok(format!("_{}._{}.{}", service, protocol, domain))
915}
916
917fn ptr_query_name(domain: &str) -> String {
920 match IpAddr::from_str(domain) {
921 Ok(ip) => reverse_dns_name(&ip),
922 Err(_) => domain.to_string(),
923 }
924}
925
926pub(crate) fn wire_query_name(domain: &str, record_type: RecordType) -> Result<String> {
932 match record_type {
933 RecordType::SRV => {
934 let (service, protocol, name) = parse_srv_query(domain).ok_or_else(srv_format_error)?;
935 srv_query_name(&service, &protocol, &name)
936 }
937 RecordType::PTR => Ok(ptr_query_name(domain)),
938 _ => Ok(domain.to_string()),
939 }
940}
941
942pub(crate) fn srv_format_error() -> SeerError {
946 SeerError::InvalidInput(
947 "SRV records require service name format: _service._proto.name".to_string(),
948 )
949}
950
951fn reverse_dns_name(ip: &IpAddr) -> String {
952 match ip {
953 IpAddr::V4(addr) => {
954 let octets = addr.octets();
955 format!(
956 "{}.{}.{}.{}.in-addr.arpa",
957 octets[3], octets[2], octets[1], octets[0]
958 )
959 }
960 IpAddr::V6(addr) => {
961 let segments = addr.segments();
962 let mut result = String::with_capacity(72);
964 let mut first = true;
965 for segment in segments.iter().rev() {
966 for shift in [0, 4, 8, 12] {
967 if !first {
968 result.push('.');
969 }
970 first = false;
971 let nibble = (segment >> shift) & 0xF;
972 result
973 .push(char::from_digit(nibble as u32, 16).expect("nibble is always 0-15"));
974 }
975 }
976 result.push_str(".ip6.arpa");
977 result
978 }
979 }
980}
981
982fn parse_caa(caa: &CAA) -> (u8, String, String) {
983 let flags = caa.flags();
993 let tag = caa.tag.clone();
994 let value = String::from_utf8_lossy(&caa.value).to_string();
995 (flags, tag, value)
996}
997
998fn parse_svcb(svcb: &SVCB, names: NameSpelling) -> (u16, String, Vec<SvcParam>) {
1005 let params = svcb
1006 .svc_params
1007 .iter()
1008 .map(|(key, value)| SvcParam {
1009 key: svc_param_key_name(*key),
1010 value: svc_param_value(value),
1011 })
1012 .collect();
1013 (svcb.svc_priority, names.spell(&svcb.target_name), params)
1014}
1015
1016fn svc_param_key_name(key: SvcParamKey) -> String {
1019 match key {
1020 SvcParamKey::Mandatory => "mandatory".to_string(),
1021 SvcParamKey::Alpn => "alpn".to_string(),
1022 SvcParamKey::NoDefaultAlpn => "no-default-alpn".to_string(),
1023 SvcParamKey::Port => "port".to_string(),
1024 SvcParamKey::Ipv4Hint => "ipv4hint".to_string(),
1025 SvcParamKey::EchConfigList => "ech".to_string(),
1026 SvcParamKey::Ipv6Hint => "ipv6hint".to_string(),
1027 SvcParamKey::Key(_) | SvcParamKey::Key65535 | SvcParamKey::Unknown(_) => {
1028 format!("key{}", u16::from(key))
1029 }
1030 }
1031}
1032
1033fn svc_param_value(value: &SvcParamValue) -> String {
1037 use base64::{engine::general_purpose::STANDARD, Engine};
1038 fn join<T: std::fmt::Display>(items: impl Iterator<Item = T>) -> String {
1039 items
1040 .map(|item| item.to_string())
1041 .collect::<Vec<_>>()
1042 .join(",")
1043 }
1044 match value {
1045 SvcParamValue::Mandatory(keys) => join(keys.0.iter().map(|k| svc_param_key_name(*k))),
1046 SvcParamValue::Alpn(alpn) => join(alpn.0.iter().map(|id| {
1052 let listed = id.replace('\\', "\\\\").replace(',', "\\,");
1053 escape_char_string(listed.as_bytes())
1054 })),
1055 SvcParamValue::NoDefaultAlpn => String::new(),
1056 SvcParamValue::Port(port) => port.to_string(),
1057 SvcParamValue::Ipv4Hint(hint) => join(hint.0.iter().map(|a| a.0)),
1058 SvcParamValue::EchConfigList(ech) => STANDARD.encode(&ech.0),
1059 SvcParamValue::Ipv6Hint(hint) => join(hint.0.iter().map(|aaaa| aaaa.0)),
1060 SvcParamValue::Unknown(opaque) => escape_char_string(&opaque.0),
1061 }
1062}
1063
1064fn escape_char_string(bytes: &[u8]) -> String {
1069 let mut out = String::with_capacity(bytes.len());
1070 for &b in bytes {
1071 match b {
1072 b'"' | b'\\' => {
1073 out.push('\\');
1074 out.push(char::from(b));
1075 }
1076 0x21..=0x7E => out.push(char::from(b)),
1077 _ => out.push_str(&format!("\\{b:03}")),
1078 }
1079 }
1080 out
1081}
1082
1083pub(crate) fn wire_type(record_type: RecordType) -> Option<HickoryRecordType> {
1090 Some(match record_type {
1091 RecordType::A => HickoryRecordType::A,
1092 RecordType::AAAA => HickoryRecordType::AAAA,
1093 RecordType::CNAME => HickoryRecordType::CNAME,
1094 RecordType::MX => HickoryRecordType::MX,
1095 RecordType::NS => HickoryRecordType::NS,
1096 RecordType::TXT => HickoryRecordType::TXT,
1097 RecordType::SOA => HickoryRecordType::SOA,
1098 RecordType::PTR => HickoryRecordType::PTR,
1099 RecordType::SRV => HickoryRecordType::SRV,
1100 RecordType::CAA => HickoryRecordType::CAA,
1101 RecordType::DNSKEY => HickoryRecordType::DNSKEY,
1102 RecordType::DS => HickoryRecordType::DS,
1103 RecordType::CDS => HickoryRecordType::CDS,
1106 RecordType::CDNSKEY => HickoryRecordType::CDNSKEY,
1107 RecordType::HTTPS => HickoryRecordType::HTTPS,
1109 RecordType::SVCB => HickoryRecordType::SVCB,
1110 RecordType::TLSA => HickoryRecordType::TLSA,
1116 RecordType::SSHFP => HickoryRecordType::SSHFP,
1117 RecordType::NAPTR => HickoryRecordType::NAPTR,
1118 RecordType::ANY => return None,
1119 })
1120}
1121
1122fn unsupported_record_type(record_type: RecordType) -> SeerError {
1125 SeerError::DnsError(format!("unsupported record type: {}", record_type))
1126}
1127
1128fn hex_upper(bytes: &[u8]) -> String {
1131 bytes.iter().map(|b| format!("{:02X}", b)).collect()
1132}
1133
1134#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1138pub(crate) enum NameSpelling {
1139 Display,
1142 Ascii,
1147}
1148
1149impl NameSpelling {
1150 fn spell(self, name: &Name) -> String {
1151 match self {
1152 NameSpelling::Display => name.to_string(),
1153 NameSpelling::Ascii => name.to_ascii(),
1154 }
1155 }
1156}
1157
1158pub(crate) fn convert_rdata(
1166 record_type: RecordType,
1167 data: &HickoryRData,
1168 names: NameSpelling,
1169) -> Option<RecordData> {
1170 use hickory_resolver::proto::dnssec::rdata::DNSSECRData;
1171
1172 match (record_type, data) {
1173 (RecordType::A, HickoryRData::A(addr)) => Some(RecordData::A {
1174 address: addr.0.to_string(),
1175 }),
1176 (RecordType::AAAA, HickoryRData::AAAA(addr)) => Some(RecordData::AAAA {
1177 address: addr.0.to_string(),
1178 }),
1179 (RecordType::CNAME, HickoryRData::CNAME(cname)) => Some(RecordData::CNAME {
1180 target: names.spell(&cname.0),
1181 }),
1182 (RecordType::MX, HickoryRData::MX(mx)) => Some(RecordData::MX {
1183 preference: mx.preference,
1184 exchange: names.spell(&mx.exchange),
1185 }),
1186 (RecordType::NS, HickoryRData::NS(ns)) => Some(RecordData::NS {
1187 nameserver: names.spell(&ns.0),
1188 }),
1189 (RecordType::TXT, HickoryRData::TXT(txt)) => Some(RecordData::TXT {
1190 text: txt
1191 .txt_data
1192 .iter()
1193 .map(|data| String::from_utf8_lossy(data).to_string())
1194 .collect::<Vec<_>>()
1195 .join(""),
1196 }),
1197 (RecordType::SOA, HickoryRData::SOA(soa)) => Some(RecordData::SOA {
1198 mname: names.spell(&soa.mname),
1199 rname: names.spell(&soa.rname),
1200 serial: soa.serial,
1201 refresh: soa.refresh as u32,
1207 retry: soa.retry as u32,
1208 expire: soa.expire as u32,
1209 minimum: soa.minimum,
1210 }),
1211 (RecordType::PTR, HickoryRData::PTR(ptr)) => Some(RecordData::PTR {
1212 target: names.spell(&ptr.0),
1213 }),
1214 (RecordType::SRV, HickoryRData::SRV(srv)) => Some(RecordData::SRV {
1215 priority: srv.priority,
1216 weight: srv.weight,
1217 port: srv.port,
1218 target: names.spell(&srv.target),
1219 }),
1220 (RecordType::CAA, HickoryRData::CAA(caa)) => {
1221 let (flags, tag, value) = parse_caa(caa);
1222 Some(RecordData::CAA { flags, tag, value })
1223 }
1224 (RecordType::DNSKEY, HickoryRData::DNSSEC(DNSSECRData::DNSKEY(dnskey))) => {
1225 use base64::{engine::general_purpose::STANDARD, Engine};
1226 let public_key_buf = dnskey.public_key();
1227 Some(RecordData::DNSKEY {
1228 flags: dnskey.flags(),
1229 protocol: 3,
1231 algorithm: u8::from(public_key_buf.algorithm()),
1232 public_key: STANDARD.encode(public_key_buf.public_bytes()),
1233 })
1234 }
1235 (RecordType::DS, HickoryRData::DNSSEC(DNSSECRData::DS(ds))) => Some(RecordData::DS {
1236 key_tag: ds.key_tag(),
1237 algorithm: u8::from(ds.algorithm()),
1238 digest_type: u8::from(ds.digest_type()),
1239 digest: hex_upper(ds.digest()),
1240 }),
1241 (RecordType::CDS, HickoryRData::DNSSEC(DNSSECRData::CDS(cds))) => Some(RecordData::CDS {
1243 key_tag: cds.key_tag(),
1244 algorithm: cds.algorithm().map_or(0, u8::from),
1245 digest_type: u8::from(cds.digest_type()),
1246 digest: hex_upper(cds.digest()),
1247 }),
1248 (RecordType::CDNSKEY, HickoryRData::DNSSEC(DNSSECRData::CDNSKEY(cdnskey))) => {
1249 use base64::{engine::general_purpose::STANDARD, Engine};
1250 let rdata = cdnskey.to_bytes().ok()?;
1257 Some(RecordData::CDNSKEY {
1258 flags: cdnskey.flags(),
1259 protocol: 3,
1261 algorithm: cdnskey.algorithm().map_or(0, u8::from),
1262 public_key: STANDARD.encode(rdata.get(4..)?),
1263 })
1264 }
1265 (RecordType::HTTPS, HickoryRData::HTTPS(https)) => {
1266 let (priority, target, params) = parse_svcb(&https.0, names);
1267 Some(RecordData::HTTPS {
1268 priority,
1269 target,
1270 params,
1271 })
1272 }
1273 (RecordType::SVCB, HickoryRData::SVCB(svcb)) => {
1274 let (priority, target, params) = parse_svcb(svcb, names);
1275 Some(RecordData::SVCB {
1276 priority,
1277 target,
1278 params,
1279 })
1280 }
1281 (RecordType::TLSA, HickoryRData::TLSA(tlsa)) => Some(RecordData::TLSA {
1282 cert_usage: u8::from(tlsa.cert_usage),
1283 selector: u8::from(tlsa.selector),
1284 matching: u8::from(tlsa.matching),
1285 cert_data: hex_upper(&tlsa.cert_data),
1286 }),
1287 (RecordType::SSHFP, HickoryRData::SSHFP(sshfp)) => Some(RecordData::SSHFP {
1288 algorithm: u8::from(sshfp.algorithm),
1289 fingerprint_type: u8::from(sshfp.fingerprint_type),
1290 fingerprint: hex_upper(&sshfp.fingerprint),
1291 }),
1292 (RecordType::NAPTR, HickoryRData::NAPTR(naptr)) => Some(RecordData::NAPTR {
1296 order: naptr.order,
1297 preference: naptr.preference,
1298 flags: String::from_utf8_lossy(&naptr.flags).into_owned(),
1299 services: String::from_utf8_lossy(&naptr.services).into_owned(),
1300 regexp: String::from_utf8_lossy(&naptr.regexp).into_owned(),
1301 replacement: names.spell(&naptr.replacement),
1302 }),
1303 _ => None,
1304 }
1305}
1306
1307pub(crate) fn from_wire_type(wire: HickoryRecordType) -> Option<RecordType> {
1312 RecordType::ALL
1313 .iter()
1314 .copied()
1315 .find(|t| wire_type(*t) == Some(wire))
1316}
1317
1318pub(crate) fn to_dns_record(record: &Record) -> Option<DnsRecord> {
1328 let record_type = from_wire_type(record.record_type())?;
1329 let data = convert_rdata(record_type, &record.data, NameSpelling::Ascii)?;
1330 Some(DnsRecord {
1331 name: owner_name(&record.name),
1332 record_type,
1333 ttl: record.ttl,
1334 data,
1335 })
1336}
1337
1338fn owner_name(name: &Name) -> String {
1343 let text = name.to_ascii();
1344 match text.strip_suffix('.') {
1345 Some(rest) if !rest.is_empty() => rest.to_string(),
1346 _ => text,
1347 }
1348}
1349
1350fn is_valid_srv_label(label: &str) -> bool {
1352 !label.is_empty()
1353 && label.len() <= 63
1354 && label.chars().all(|c| c.is_ascii_alphanumeric() || c == '-')
1355 && !label.starts_with('-')
1356 && !label.ends_with('-')
1357}
1358
1359#[cfg(test)]
1360mod tests {
1361 use super::*;
1369
1370 #[test]
1385 fn standard_opts_pin_server_order_so_ipv4_is_tried_first() {
1386 let mut opts = ResolverOpts::default();
1387 apply_standard_opts(&mut opts, Duration::from_secs(5));
1388 assert_eq!(
1389 opts.server_ordering_strategy,
1390 ServerOrderingStrategy::UserProvidedOrder,
1391 "QueryStatistics ordering is nondeterministic in a short-lived \
1392 process and can draw only black-holed IPv6 servers"
1393 );
1394 }
1395
1396 #[test]
1401 fn default_config_lists_enough_ipv4_servers_before_any_ipv6() {
1402 let config = ResolverConfig::udp_and_tcp(&GOOGLE);
1403 let ips: Vec<_> = config.name_servers().iter().map(|ns| ns.ip).collect();
1404 let concurrent = ResolverOpts::default().num_concurrent_reqs.max(1);
1405 assert!(
1406 ips.len() > concurrent,
1407 "expected more servers than the concurrency window: {ips:?}"
1408 );
1409 for (i, ip) in ips.iter().take(concurrent).enumerate() {
1410 assert!(
1411 ip.is_ipv4(),
1412 "server {i} in the concurrency window is not IPv4: {ips:?}"
1413 );
1414 }
1415 }
1416
1417 #[test]
1418 fn from_config_applies_dns_timeout() {
1419 let mut config = crate::config::SeerConfig::default();
1420 config.timeouts.dns_secs = 9;
1421 let resolver = DnsResolver::from_config(&config);
1422 assert_eq!(resolver.timeout, Duration::from_secs(9));
1423 }
1424 use std::net::{Ipv4Addr, Ipv6Addr};
1425
1426 #[test]
1429 fn record_type_from_str_accepts_lowercase() {
1430 assert_eq!(RecordType::from_str("a").unwrap(), RecordType::A);
1431 assert_eq!(RecordType::from_str("mx").unwrap(), RecordType::MX);
1432 assert_eq!(RecordType::from_str("cname").unwrap(), RecordType::CNAME);
1433 assert_eq!(RecordType::from_str("dnskey").unwrap(), RecordType::DNSKEY);
1434 }
1435
1436 #[test]
1437 fn record_type_from_str_accepts_mixed_case() {
1438 assert_eq!(RecordType::from_str("Mx").unwrap(), RecordType::MX);
1439 assert_eq!(RecordType::from_str("cNaMe").unwrap(), RecordType::CNAME);
1440 }
1441
1442 #[test]
1443 fn record_type_from_str_rejects_whitespace_padded() {
1444 assert!(RecordType::from_str(" A").is_err());
1448 assert!(RecordType::from_str("A ").is_err());
1449 assert!(RecordType::from_str("\tA\n").is_err());
1450 }
1451
1452 #[test]
1453 fn record_type_from_str_rejects_unknown() {
1454 assert!(RecordType::from_str("NOTAREAL").is_err());
1455 assert!(RecordType::from_str("A1").is_err());
1456 assert!(RecordType::from_str("").is_err());
1457 }
1458
1459 #[test]
1460 fn record_type_from_str_accepts_star_as_any() {
1461 assert_eq!(RecordType::from_str("*").unwrap(), RecordType::ANY);
1462 assert_eq!(RecordType::from_str("ANY").unwrap(), RecordType::ANY);
1463 assert_eq!(RecordType::from_str("any").unwrap(), RecordType::ANY);
1464 }
1465
1466 #[test]
1469 fn srv_label_accepts_alphanumeric_and_hyphen() {
1470 assert!(is_valid_srv_label("http"));
1471 assert!(is_valid_srv_label("ldap-tls"));
1472 assert!(is_valid_srv_label("a1"));
1473 assert!(is_valid_srv_label("tcp"));
1474 }
1475
1476 #[test]
1477 fn srv_label_rejects_empty() {
1478 assert!(!is_valid_srv_label(""));
1479 }
1480
1481 #[test]
1482 fn srv_label_rejects_leading_or_trailing_hyphen() {
1483 assert!(!is_valid_srv_label("-http"));
1484 assert!(!is_valid_srv_label("http-"));
1485 assert!(!is_valid_srv_label("-"));
1486 }
1487
1488 #[test]
1489 fn srv_label_rejects_dots() {
1490 assert!(!is_valid_srv_label("http.evil"));
1493 assert!(!is_valid_srv_label("a.b"));
1494 }
1495
1496 #[test]
1497 fn srv_label_rejects_special_chars() {
1498 assert!(!is_valid_srv_label("http evil"));
1499 assert!(!is_valid_srv_label("http/evil"));
1500 assert!(!is_valid_srv_label("http\0"));
1501 assert!(!is_valid_srv_label("http\n"));
1502 }
1503
1504 #[test]
1505 fn srv_label_rejects_over_63_chars() {
1506 let too_long = "a".repeat(64);
1507 assert!(!is_valid_srv_label(&too_long));
1508 let exactly_63 = "a".repeat(63);
1509 assert!(is_valid_srv_label(&exactly_63));
1510 }
1511
1512 #[test]
1515 fn classify_ns_presence_absent_on_empty_ok() {
1516 let r: Result<Vec<DnsRecord>> = Ok(vec![]);
1518 assert_eq!(classify_ns_presence(&r), DnsPresence::Absent);
1519 }
1520
1521 #[test]
1522 fn classify_ns_presence_present_on_records() {
1523 let rec = DnsRecord {
1524 name: "example.test.".to_string(),
1525 record_type: RecordType::NS,
1526 ttl: 3600,
1527 data: RecordData::NS {
1528 nameserver: "ns1.example.net.".to_string(),
1529 },
1530 };
1531 let r: Result<Vec<DnsRecord>> = Ok(vec![rec]);
1532 assert_eq!(classify_ns_presence(&r), DnsPresence::Present);
1533 }
1534
1535 #[test]
1536 fn classify_ns_presence_unknown_on_error() {
1537 let r: Result<Vec<DnsRecord>> = Err(SeerError::DnsError("servfail".to_string()));
1538 assert_eq!(classify_ns_presence(&r), DnsPresence::Unknown);
1539 }
1540
1541 #[test]
1544 fn reverse_dns_name_formats_ipv4_correctly() {
1545 let ip: IpAddr = Ipv4Addr::new(192, 0, 2, 1).into();
1546 assert_eq!(reverse_dns_name(&ip), "1.2.0.192.in-addr.arpa");
1547 }
1548
1549 #[test]
1550 fn reverse_dns_name_formats_ipv6_correctly() {
1551 let ip: IpAddr = Ipv6Addr::LOCALHOST.into();
1553 let name = reverse_dns_name(&ip);
1554 assert!(
1555 name.ends_with(".ip6.arpa"),
1556 "must end with .ip6.arpa; got: {}",
1557 name
1558 );
1559 assert!(
1561 name.starts_with("1."),
1562 "expected '1.' prefix, got: {}",
1563 name
1564 );
1565 assert_eq!(name.len(), 72);
1567 }
1568
1569 #[test]
1572 fn resolver_new_has_default_timeout() {
1573 let r = DnsResolver::new();
1574 assert_eq!(r.timeout, DEFAULT_TIMEOUT);
1575 }
1576
1577 #[test]
1578 fn resolver_with_timeout_overrides_default() {
1579 let custom = Duration::from_secs(42);
1580 let r = DnsResolver::new().with_timeout(custom);
1581 assert_eq!(r.timeout, custom);
1582 }
1583
1584 #[test]
1585 fn resolver_default_matches_new() {
1586 let a = DnsResolver::default();
1587 let b = DnsResolver::new();
1588 assert_eq!(a.timeout, b.timeout);
1589 }
1590
1591 #[tokio::test]
1594 async fn custom_resolver_rejects_invalid_input() {
1595 let r = DnsResolver::new();
1600 let err = r.create_custom_resolver("..").await.unwrap_err();
1601 let msg = err.to_string().to_lowercase();
1602 assert!(
1603 msg.contains("dns resolution failed") || msg.contains("invalid"),
1604 "expected resolution failure, got: {}",
1605 msg
1606 );
1607 }
1608
1609 #[tokio::test]
1610 async fn custom_resolver_rejects_private_ipv4() {
1611 let r = DnsResolver::new();
1614 for reserved in ["127.0.0.1", "10.0.0.1", "192.168.1.1", "169.254.169.254"] {
1615 let err = r.create_custom_resolver(reserved).await.unwrap_err();
1616 let msg = err.to_string().to_lowercase();
1617 assert!(
1618 msg.contains("blocked") || msg.contains("reserved"),
1619 "reserved IP {} must be rejected, got error: {}",
1620 reserved,
1621 msg
1622 );
1623 }
1624 }
1625
1626 #[tokio::test]
1627 async fn custom_resolver_rejects_loopback_ipv6() {
1628 let r = DnsResolver::new();
1629 let err = r.create_custom_resolver("::1").await.unwrap_err();
1630 let msg = err.to_string().to_lowercase();
1631 assert!(
1632 msg.contains("blocked") || msg.contains("reserved"),
1633 "::1 must be rejected, got error: {}",
1634 msg
1635 );
1636 }
1637
1638 #[tokio::test]
1639 async fn custom_resolver_accepts_public_ipv4() {
1640 let r = DnsResolver::new();
1642 let result = r.create_custom_resolver("8.8.8.8").await;
1643 assert!(
1644 result.is_ok(),
1645 "8.8.8.8 must be accepted as a public nameserver, got: {:?}",
1646 result.err()
1647 );
1648 }
1649
1650 #[tokio::test]
1656 async fn custom_resolver_rejects_private_ip_for_dot_and_doh() {
1657 let r = DnsResolver::new();
1658 for reserved in [
1659 "tls://127.0.0.1",
1660 "tls://192.168.1.1:853",
1661 "tls://[::1]",
1662 "https://10.0.0.1/dns-query",
1663 "https://169.254.169.254",
1664 "tls://[fd00::1]:853",
1665 ] {
1666 let err = r.create_custom_resolver(reserved).await.unwrap_err();
1667 let msg = err.to_string().to_lowercase();
1668 assert!(
1669 msg.contains("blocked") || msg.contains("reserved"),
1670 "reserved spec {} must be rejected, got error: {}",
1671 reserved,
1672 msg
1673 );
1674 }
1675 let err = r
1678 .create_custom_resolver("https://[fd00::1]:443/dns-query")
1679 .await
1680 .unwrap_err();
1681 assert!(matches!(err, SeerError::InvalidInput(_)), "{err:?}");
1682 }
1683
1684 #[tokio::test]
1685 async fn custom_resolver_accepts_public_dot_and_doh_literals() {
1686 let r = DnsResolver::new();
1689 for spec in ["tls://1.1.1.1", "https://8.8.8.8/dns-query"] {
1690 let result = r.create_custom_resolver(spec).await;
1691 assert!(
1692 result.is_ok(),
1693 "{} must be accepted, got: {:?}",
1694 spec,
1695 result.err()
1696 );
1697 }
1698 }
1699
1700 #[tokio::test]
1701 async fn custom_resolver_rejects_unknown_scheme() {
1702 let r = DnsResolver::new();
1703 let err = r.create_custom_resolver("ftp://8.8.8.8").await.unwrap_err();
1704 assert!(
1705 matches!(err, SeerError::InvalidInput(_)),
1706 "unknown scheme must be an input error, got: {err:?}"
1707 );
1708 }
1709
1710 fn spec(s: &str) -> NameserverSpec {
1713 NameserverSpec::parse(s).unwrap_or_else(|e| panic!("{s:?} must parse: {e}"))
1714 }
1715
1716 #[test]
1717 fn upstream_config_udp_defaults() {
1718 use hickory_resolver::config::ProtocolConfig;
1719
1720 let ip: IpAddr = "8.8.8.8".parse().unwrap();
1721 let config = build_upstream_config(&spec("8.8.8.8"), &[ip], None);
1722 let servers = config.name_servers();
1723 assert_eq!(servers.len(), 1);
1724 assert_eq!(servers[0].ip, ip);
1725 assert_eq!(servers[0].connections.len(), 1);
1726 assert_eq!(servers[0].connections[0].port, 53);
1727 assert!(matches!(
1728 servers[0].connections[0].protocol,
1729 ProtocolConfig::Udp
1730 ));
1731 }
1732
1733 #[test]
1734 fn upstream_config_udp_explicit_port() {
1735 let ip: IpAddr = "9.9.9.9".parse().unwrap();
1736 let config = build_upstream_config(&spec("9.9.9.9:5353"), &[ip], None);
1737 assert_eq!(config.name_servers()[0].connections[0].port, 5353);
1738 }
1739
1740 #[test]
1741 fn upstream_config_tls_sets_protocol_port_and_server_name() {
1742 use hickory_resolver::config::ProtocolConfig;
1743
1744 let ip: IpAddr = "9.9.9.9".parse().unwrap();
1745 let config = build_upstream_config(&spec("tls://dns.quad9.net"), &[ip], None);
1746 let ns = &config.name_servers()[0];
1747 assert_eq!(ns.ip, ip);
1748 assert_eq!(ns.connections.len(), 1);
1749 assert_eq!(ns.connections[0].port, 853);
1750 match &ns.connections[0].protocol {
1751 ProtocolConfig::Tls { server_name } => {
1752 assert_eq!(&**server_name, "dns.quad9.net");
1753 }
1754 other => panic!("expected Tls protocol, got {other:?}"),
1755 }
1756 }
1757
1758 #[test]
1759 fn upstream_config_tls_ip_literal_uses_ip_as_server_name() {
1760 use hickory_resolver::config::ProtocolConfig;
1761
1762 let ip: IpAddr = "1.1.1.1".parse().unwrap();
1763 let config = build_upstream_config(&spec("tls://1.1.1.1"), &[ip], None);
1764 match &config.name_servers()[0].connections[0].protocol {
1765 ProtocolConfig::Tls { server_name } => assert_eq!(&**server_name, "1.1.1.1"),
1766 other => panic!("expected Tls protocol, got {other:?}"),
1767 }
1768 }
1769
1770 #[test]
1771 fn upstream_config_https_sets_protocol_port_path_and_server_name() {
1772 use hickory_resolver::config::ProtocolConfig;
1773
1774 let ip: IpAddr = "104.16.248.249".parse().unwrap();
1775 let config = build_upstream_config(&spec("https://cloudflare-dns.com"), &[ip], None);
1776 let ns = &config.name_servers()[0];
1777 assert_eq!(ns.connections[0].port, 443);
1778 match &ns.connections[0].protocol {
1779 ProtocolConfig::Https { server_name, path } => {
1780 assert_eq!(&**server_name, "cloudflare-dns.com");
1781 assert_eq!(&**path, "/dns-query");
1782 }
1783 other => panic!("expected Https protocol, got {other:?}"),
1784 }
1785 }
1786
1787 #[test]
1788 fn upstream_config_https_custom_port_and_path() {
1789 use hickory_resolver::config::ProtocolConfig;
1790
1791 let ip: IpAddr = "8.8.8.8".parse().unwrap();
1792 let config = build_upstream_config(&spec("https://dns.google:8443/resolve"), &[ip], None);
1793 let ns = &config.name_servers()[0];
1794 assert_eq!(ns.connections[0].port, 8443);
1795 match &ns.connections[0].protocol {
1796 ProtocolConfig::Https { server_name, path } => {
1797 assert_eq!(&**server_name, "dns.google");
1798 assert_eq!(&**path, "/resolve");
1799 }
1800 other => panic!("expected Https protocol, got {other:?}"),
1801 }
1802 }
1803
1804 #[test]
1805 fn upstream_config_multiple_ips_share_spec() {
1806 use hickory_resolver::config::ProtocolConfig;
1809
1810 let ips: Vec<IpAddr> = vec![
1811 "9.9.9.9".parse().unwrap(),
1812 "149.112.112.112".parse().unwrap(),
1813 ];
1814 let config = build_upstream_config(&spec("tls://dns.quad9.net"), &ips, None);
1815 let servers = config.name_servers();
1816 assert_eq!(servers.len(), 2);
1817 for (ns, expected_ip) in servers.iter().zip(&ips) {
1818 assert_eq!(&ns.ip, expected_ip);
1819 assert_eq!(ns.connections[0].port, 853);
1820 assert!(matches!(
1821 &ns.connections[0].protocol,
1822 ProtocolConfig::Tls { server_name } if &**server_name == "dns.quad9.net"
1823 ));
1824 }
1825 }
1826
1827 #[test]
1835 fn upstream_config_orders_ipv4_before_ipv6() {
1836 let ips: Vec<IpAddr> = [
1838 "2606:4700::6810:f8f9",
1839 "2606:4700::6810:f9f9",
1840 "104.16.248.249",
1841 "104.16.249.249",
1842 ]
1843 .iter()
1844 .map(|s| s.parse().unwrap())
1845 .collect();
1846 let config =
1847 build_upstream_config(&spec("https://cloudflare-dns.com/dns-query"), &ips, None);
1848 let order: Vec<IpAddr> = config.name_servers().iter().map(|ns| ns.ip).collect();
1849 assert_eq!(
1850 order,
1851 [ips[2], ips[3], ips[0], ips[1]],
1852 "IPv4 first, each family in resolved order, nothing dropped"
1853 );
1854 let concurrent = ResolverOpts::default().num_concurrent_reqs.max(1);
1855 assert!(
1856 order.iter().take(concurrent).all(IpAddr::is_ipv4),
1857 "the parallel window must be all IPv4: {order:?}"
1858 );
1859 }
1860
1861 #[test]
1862 fn upstream_config_test_port_override_wins() {
1863 let ip: IpAddr = "127.0.0.1".parse().unwrap();
1865 let config = build_upstream_config(&spec("127.0.0.1"), &[ip], Some(9999));
1866 assert_eq!(config.name_servers()[0].connections[0].port, 9999);
1867 }
1868
1869 #[tokio::test]
1870 async fn query_asks_dot_and_doh_servers_through_the_vetted_config() {
1871 use hickory_resolver::config::ProtocolConfig;
1877
1878 let r = DnsResolver::new();
1879 let dot = r.custom_upstream_config("tls://1.1.1.1").await.unwrap();
1880 let [server] = dot.name_servers() else {
1881 panic!("one server per address: {dot:?}");
1882 };
1883 assert_eq!(server.ip, IpAddr::from([1, 1, 1, 1]));
1884 assert_eq!(server.connections.len(), 1);
1885 assert_eq!(server.connections[0].port, 853);
1886 assert!(matches!(
1887 &server.connections[0].protocol,
1888 ProtocolConfig::Tls { server_name } if &**server_name == "1.1.1.1"
1889 ));
1890
1891 let doh = r
1892 .custom_upstream_config("https://8.8.8.8/dns-query")
1893 .await
1894 .unwrap();
1895 let [server] = doh.name_servers() else {
1896 panic!("one server per address: {doh:?}");
1897 };
1898 assert_eq!(server.connections[0].port, 443);
1899 assert!(matches!(
1900 &server.connections[0].protocol,
1901 ProtocolConfig::Https { server_name, path }
1902 if &**server_name == "8.8.8.8" && &**path == "/dns-query"
1903 ));
1904
1905 let udp = r.custom_upstream_config("9.9.9.9").await.unwrap();
1908 assert_eq!(udp.name_servers()[0].connections[0].port, 53);
1909 assert!(matches!(
1910 udp.name_servers()[0].connections[0].protocol,
1911 ProtocolConfig::Udp
1912 ));
1913 let default = r.upstream_config(None).await.unwrap();
1915 assert!(default.name_servers()[0].ip.is_ipv4());
1916 }
1917
1918 #[tokio::test]
1921 #[ignore = "live network — DoT query against Cloudflare"]
1922 async fn live_query_over_dot() {
1923 let result = DnsResolver::new()
1924 .query("example.com", RecordType::A, Some("tls://1.1.1.1"))
1925 .await
1926 .expect("DoT query should get a response");
1927 assert_eq!(result.status, DnsStatus::NoError);
1928 assert!(result.flags.contains(&"qr".to_string()), "{result:?}");
1929 assert!(result.records().count() > 0, "expected A records over DoT");
1930 }
1931
1932 #[tokio::test]
1933 #[ignore = "live network — DoH query against Cloudflare"]
1934 async fn live_query_over_doh() {
1935 let result = DnsResolver::new()
1936 .query(
1937 "example.com",
1938 RecordType::A,
1939 Some("https://cloudflare-dns.com/dns-query"),
1940 )
1941 .await
1942 .expect("DoH query should get a response");
1943 assert_eq!(result.status, DnsStatus::NoError);
1944 assert!(result.records().count() > 0, "expected A records over DoH");
1945 }
1946
1947 #[tokio::test]
1948 #[ignore = "live network — NXDOMAIN keeps its header"]
1949 async fn live_query_nxdomain_has_flags_and_the_soa() {
1950 let result = DnsResolver::new()
1951 .query("seer-does-not-exist.example", RecordType::A, None)
1953 .await
1954 .expect("a negative answer is a result");
1955 assert_eq!(result.status, DnsStatus::NxDomain);
1956 assert!(result.flags.contains(&"qr".to_string()), "{result:?}");
1957 assert!(
1958 result
1959 .authority
1960 .iter()
1961 .any(|r| r.record_type == RecordType::SOA),
1962 "{result:?}"
1963 );
1964 }
1965
1966 #[tokio::test]
1967 #[ignore = "live network — DoT query against Cloudflare"]
1968 async fn live_resolve_over_dot() {
1969 let r = DnsResolver::new();
1970 let records = r
1971 .resolve("example.com", RecordType::A, Some("tls://1.1.1.1"))
1972 .await
1973 .expect("DoT lookup should succeed");
1974 assert!(!records.is_empty(), "expected A records over DoT");
1975 }
1976
1977 #[tokio::test]
1978 #[ignore = "live network — DoH query against Cloudflare"]
1979 async fn live_resolve_over_doh() {
1980 let r = DnsResolver::new();
1981 let records = r
1982 .resolve(
1983 "example.com",
1984 RecordType::A,
1985 Some("https://cloudflare-dns.com/dns-query"),
1986 )
1987 .await
1988 .expect("DoH lookup should succeed");
1989 assert!(!records.is_empty(), "expected A records over DoH");
1990 }
1991
1992 #[tokio::test]
1995 async fn resolve_srv_rejects_invalid_service_label() {
1996 let r = DnsResolver::new();
1997 let result = r
1999 .resolve_srv_core(&r.default_resolver, "http.evil", "tcp", "example.com")
2000 .await;
2001 assert!(result.is_err());
2002 let msg = result.unwrap_err().to_string().to_lowercase();
2003 assert!(
2004 msg.contains("invalid srv service"),
2005 "expected SRV service validation error, got: {}",
2006 msg
2007 );
2008 }
2009
2010 #[tokio::test]
2011 async fn resolve_srv_rejects_invalid_protocol_label() {
2012 let r = DnsResolver::new();
2013 let result = r
2014 .resolve_srv_core(&r.default_resolver, "http", "tcp.evil", "example.com")
2015 .await;
2016 assert!(result.is_err());
2017 let msg = result.unwrap_err().to_string().to_lowercase();
2018 assert!(
2019 msg.contains("invalid srv protocol"),
2020 "expected SRV protocol validation error, got: {}",
2021 msg
2022 );
2023 }
2024
2025 #[tokio::test]
2026 async fn resolve_srv_normalizes_and_validates_domain_input() {
2027 let r = DnsResolver::new();
2031 let result = r
2032 .resolve("_http._tcp.not a valid domain", RecordType::SRV, None)
2033 .await;
2034 assert!(
2035 matches!(result, Err(SeerError::InvalidDomain(_))),
2036 "expected InvalidDomain from domain validation, got: {result:?}"
2037 );
2038 }
2039
2040 #[tokio::test]
2043 async fn resolve_normalizes_uppercase_domain_input() {
2044 let r = DnsResolver::new();
2049 let result = r.resolve(".bad.example", RecordType::A, None).await;
2050 assert!(result.is_err(), "leading-dot domain must be rejected");
2051 }
2052
2053 #[test]
2058 fn parse_srv_query_extracts_service_proto_and_name() {
2059 assert_eq!(
2060 parse_srv_query("_sip._tcp.example.com"),
2061 Some((
2062 "sip".to_string(),
2063 "tcp".to_string(),
2064 "example.com".to_string()
2065 ))
2066 );
2067 }
2068
2069 #[test]
2070 fn parse_srv_query_keeps_multilabel_domain() {
2071 assert_eq!(
2072 parse_srv_query("_sip._tcp.sip.voice.google.com"),
2073 Some((
2074 "sip".to_string(),
2075 "tcp".to_string(),
2076 "sip.voice.google.com".to_string()
2077 ))
2078 );
2079 }
2080
2081 #[test]
2082 fn fqdn_appends_root_dot_once() {
2083 assert_eq!(fqdn("example.com"), "example.com.");
2084 assert_eq!(fqdn("example.com."), "example.com.");
2085 }
2086
2087 #[test]
2088 fn parse_srv_query_rejects_bare_domain() {
2089 assert_eq!(parse_srv_query("example.com"), None);
2090 }
2091
2092 #[test]
2093 fn parse_srv_query_rejects_missing_proto_label() {
2094 assert_eq!(parse_srv_query("_sip.example.com"), None);
2096 }
2097
2098 #[tokio::test]
2099 async fn resolve_rejects_bare_domain_for_srv_as_input_error() {
2100 let r = DnsResolver::new();
2105 let err = r
2106 .resolve("example.com", RecordType::SRV, None)
2107 .await
2108 .expect_err("bare-domain SRV must error");
2109 assert!(
2110 matches!(err, SeerError::InvalidInput(_)),
2111 "bare-domain SRV should be an input error, got: {err:?}"
2112 );
2113 assert!(err.to_string().contains("_service._proto"));
2114 }
2115
2116 #[tokio::test]
2117 #[ignore = "live network"]
2118 async fn resolve_srv_via_dig_style_name_returns_records() {
2119 let r = DnsResolver::new();
2122 let records = r
2123 .resolve("_caldavs._tcp.google.com", RecordType::SRV, None)
2124 .await
2125 .expect("dig-style SRV lookup should succeed");
2126 assert!(!records.is_empty(), "expected SRV records");
2127 assert!(records.iter().all(|r| r.record_type == RecordType::SRV));
2128 }
2129
2130 #[tokio::test]
2131 #[ignore = "live network"]
2132 async fn resolve_naptr_returns_records() {
2133 let r = DnsResolver::new();
2135 let records = r
2136 .resolve("sip2sip.info", RecordType::NAPTR, None)
2137 .await
2138 .expect("NAPTR lookup should succeed");
2139 assert!(!records.is_empty(), "expected NAPTR records");
2140 assert!(records.iter().all(|r| r.record_type == RecordType::NAPTR));
2141 }
2142
2143 #[test]
2146 fn prepare_query_passes_ipv6_literal_through_for_ptr() {
2147 let out = prepare_query("2606:4700:4700::1111", RecordType::PTR).unwrap();
2153 assert_eq!(out, "2606:4700:4700::1111");
2154 }
2155
2156 #[test]
2157 fn prepare_query_passes_ipv6_loopback_through_for_ptr() {
2158 let out = prepare_query("::1", RecordType::PTR).unwrap();
2159 assert_eq!(out, "::1");
2160 }
2161
2162 #[test]
2163 fn prepare_query_passes_ipv4_literal_through_for_ptr() {
2164 let out = prepare_query("8.8.8.8", RecordType::PTR).unwrap();
2165 assert_eq!(out, "8.8.8.8");
2166 }
2167
2168 #[test]
2169 fn prepare_query_normalizes_non_ip_ptr_names() {
2170 let out = prepare_query("1.1.1.1.in-addr.arpa", RecordType::PTR).unwrap();
2172 assert_eq!(out, "1.1.1.1.in-addr.arpa");
2173 }
2174
2175 #[test]
2176 fn prepare_query_keeps_www_for_record_queries() {
2177 let out = prepare_query("HTTPS://WWW.Example.com/path", RecordType::A).unwrap();
2181 assert_eq!(out, "www.example.com");
2182 let out = prepare_query("www.example.com", RecordType::CNAME).unwrap();
2183 assert_eq!(out, "www.example.com");
2184 }
2185
2186 #[test]
2187 fn prepare_query_still_normalizes_scheme_port_path_and_case() {
2188 let out = prepare_query("https://API.Example.COM:8443/v1?q=1#frag", RecordType::A).unwrap();
2189 assert_eq!(out, "api.example.com");
2190 let out = prepare_query("example.com.", RecordType::MX).unwrap();
2191 assert_eq!(out, "example.com");
2192 assert!(prepare_query("not a domain", RecordType::A).is_err());
2193 }
2194
2195 #[test]
2196 fn prepare_query_accepts_a_leading_wildcard_label() {
2197 let out = prepare_query("*.Example.com.", RecordType::A).unwrap();
2201 assert_eq!(out, "*.example.com");
2202 let out = prepare_query("*.1.168.192.in-addr.arpa", RecordType::PTR).unwrap();
2203 assert_eq!(out, "*.1.168.192.in-addr.arpa");
2204 assert!(prepare_query("a.*.example.com", RecordType::A).is_err());
2206 assert!(prepare_query("a*.example.com", RecordType::A).is_err());
2207 }
2208
2209 #[test]
2210 fn prepare_query_runs_ptr_ip_literal_through_the_normalizer() {
2211 let deny_arpa = |name: &str| -> Result<String> {
2217 if name.ends_with(".arpa") {
2218 Err(SeerError::DomainNotAllowed {
2219 domain: name.to_string(),
2220 tld: "arpa".to_string(),
2221 })
2222 } else {
2223 Ok(name.to_string())
2224 }
2225 };
2226 for ip in ["192.0.2.1", "2606:4700:4700::1111"] {
2227 let err = prepare_query_with(ip, RecordType::PTR, deny_arpa)
2228 .expect_err("PTR for an IP literal must obey the normalizer");
2229 assert!(matches!(err, SeerError::DomainNotAllowed { .. }), "{err:?}");
2230 }
2231 assert_eq!(
2233 prepare_query_with("192.0.2.1", RecordType::PTR, normalize_query_name).unwrap(),
2234 "192.0.2.1"
2235 );
2236 }
2237
2238 #[test]
2239 fn from_wire_type_inverts_wire_type_for_every_single_type() {
2240 for record_type in RecordType::ALL.iter().copied() {
2241 match wire_type(record_type) {
2242 Some(wire) => assert_eq!(from_wire_type(wire), Some(record_type)),
2243 None => assert_eq!(record_type, RecordType::ANY),
2244 }
2245 }
2246 assert_eq!(
2249 from_wire_type(HickoryRecordType::SRV),
2250 Some(RecordType::SRV)
2251 );
2252 assert_eq!(from_wire_type(HickoryRecordType::RRSIG), None);
2253 }
2254
2255 #[test]
2256 fn to_dns_record_keeps_the_real_owner_and_type() {
2257 let cname = Record::from_rdata(
2258 Name::from_ascii("www.seer.test.").unwrap(),
2259 300,
2260 HickoryRData::CNAME(hickory_resolver::proto::rr::rdata::CNAME(
2261 Name::from_ascii("edge.cdn.test.").unwrap(),
2262 )),
2263 );
2264 let converted = to_dns_record(&cname).expect("CNAME is modeled");
2265 assert_eq!(converted.name, "www.seer.test");
2266 assert_eq!(converted.record_type, RecordType::CNAME);
2267 assert_eq!(converted.ttl, 300);
2268 assert_eq!(converted.data.to_string(), "edge.cdn.test.");
2269
2270 let a = Record::from_rdata(
2271 Name::from_ascii("edge.cdn.test.").unwrap(),
2272 60,
2273 HickoryRData::A(hickory_resolver::proto::rr::rdata::A(Ipv4Addr::new(
2274 192, 0, 2, 7,
2275 ))),
2276 );
2277 let converted = to_dns_record(&a).expect("A is modeled");
2278 assert_eq!(converted.name, "edge.cdn.test");
2279 assert_eq!(converted.record_type, RecordType::A);
2280
2281 assert_eq!(owner_name(&Name::root()), ".");
2282 assert_eq!(
2284 owner_name(&Name::from_utf8("bücher.seer.test.").unwrap()),
2285 "xn--bcher-kva.seer.test"
2286 );
2287 }
2288
2289 #[test]
2290 fn to_dns_record_spells_rdata_names_as_a_labels_where_resolve_keeps_unicode() {
2291 use hickory_resolver::proto::rr::rdata::{CNAME, MX, SOA, SRV};
2292 let idn = |name: &str| Name::from_utf8(name).unwrap();
2293 let data = |rdata: HickoryRData| {
2294 let record = Record::from_rdata(idn("xn--bcher-kva.seer.test."), 300, rdata);
2295 to_dns_record(&record).expect("modeled").data.to_string()
2296 };
2297 assert_eq!(
2301 data(HickoryRData::CNAME(CNAME(idn("edge.café.test.")))),
2302 "edge.xn--caf-dma.test."
2303 );
2304 assert_eq!(
2305 data(HickoryRData::MX(MX::new(10, idn("mail.café.test.")))),
2306 "10 mail.xn--caf-dma.test."
2307 );
2308 assert_eq!(
2309 data(HickoryRData::SRV(SRV::new(
2310 1,
2311 5,
2312 443,
2313 idn("sip.café.test.")
2314 ))),
2315 "1 5 443 sip.xn--caf-dma.test."
2316 );
2317 assert_eq!(
2318 data(HickoryRData::SOA(SOA::new(
2319 idn("ns1.café.test."),
2320 idn("hostmaster.café.test."),
2321 1,
2322 7200,
2323 3600,
2324 1209600,
2325 300,
2326 ))),
2327 "ns1.xn--caf-dma.test. hostmaster.xn--caf-dma.test. 1 7200 3600 1209600 300"
2328 );
2329
2330 let cname = HickoryRData::CNAME(CNAME(idn("edge.café.test.")));
2332 assert_eq!(
2333 convert_rdata(RecordType::CNAME, &cname, NameSpelling::Display)
2334 .expect("modeled")
2335 .to_string(),
2336 "edge.café.test."
2337 );
2338 }
2339
2340 #[test]
2341 fn parse_caa_keeps_reserved_flag_bits() {
2342 let mut caa = CAA::new_issue(
2345 true,
2346 Some(Name::from_ascii("letsencrypt.org").unwrap()),
2347 vec![],
2348 );
2349 caa.reserved_flags = 0x01;
2350 let (flags, tag, value) = parse_caa(&caa);
2351 assert_eq!(flags, 0x81);
2352 assert_eq!(tag, "issue");
2353 assert_eq!(value, "letsencrypt.org");
2354 }
2355
2356 #[test]
2357 fn alpn_ids_are_escaped_at_both_levels() {
2358 use hickory_resolver::proto::rr::rdata::svcb::Alpn;
2359 let alpn = |ids: &[&str]| {
2360 svc_param_value(&SvcParamValue::Alpn(Alpn(
2361 ids.iter().map(|id| id.to_string()).collect(),
2362 )))
2363 };
2364 assert_eq!(alpn(&["h3", "h2"]), "h3,h2");
2365 assert_eq!(alpn(&["a,b"]), r"a\\,b");
2370 assert_eq!(alpn(&[r"x\y"]), r"x\\\\y");
2371 assert_eq!(alpn(&[r"f\oo,bar", "h2"]), r"f\\\\oo\\,bar,h2");
2373 }
2374
2375 use crate::dns::query::DnsStatus;
2386 use crate::dns::test_support::{
2387 a_rdata, cname_rdata, mock_dns_resolver, mock_dns_resolver_default, record, spawn_mock_dns,
2388 spawn_mock_dns_fn, spawn_mock_dns_fn_with_tcp, MockMode, MockReply,
2389 };
2390 use hickory_resolver::proto::op::ResponseCode;
2391
2392 async fn mock_zone_lookup(record_type: RecordType, domain: &str) -> Vec<DnsRecord> {
2393 let port = spawn_mock_dns(MockMode::Zone).await;
2394 mock_dns_resolver(port)
2395 .resolve(domain, record_type, Some("127.0.0.1"))
2396 .await
2397 .unwrap_or_else(|e| panic!("{record_type} lookup against mock must succeed: {e}"))
2398 }
2399
2400 #[tokio::test]
2401 async fn mock_resolve_a_returns_addresses() {
2402 let records = mock_zone_lookup(RecordType::A, "seer.test").await;
2403 assert_eq!(records.len(), 2);
2404 assert!(records.iter().all(|r| r.record_type == RecordType::A));
2405 assert_eq!(records[0].name, "seer.test");
2406 assert_eq!(records[0].ttl, 300);
2407 let addresses: Vec<String> = records
2408 .iter()
2409 .map(|r| match &r.data {
2410 RecordData::A { address } => address.clone(),
2411 other => panic!("expected A data, got {other:?}"),
2412 })
2413 .collect();
2414 assert!(addresses.contains(&"192.0.2.1".to_string()));
2415 assert!(addresses.contains(&"192.0.2.2".to_string()));
2416 }
2417
2418 #[tokio::test]
2419 async fn mock_resolve_mx_sorts_by_preference() {
2420 let records = mock_zone_lookup(RecordType::MX, "seer.test").await;
2421 let prefs: Vec<u16> = records
2422 .iter()
2423 .map(|r| match &r.data {
2424 RecordData::MX { preference, .. } => *preference,
2425 other => panic!("expected MX data, got {other:?}"),
2426 })
2427 .collect();
2428 assert_eq!(prefs, vec![10, 20, 30]);
2430 assert!(matches!(
2431 &records[0].data,
2432 RecordData::MX { exchange, .. } if exchange == "a.mail.seer.test."
2433 ));
2434 }
2435
2436 #[tokio::test]
2437 async fn mock_resolve_txt_joins_character_strings() {
2438 let records = mock_zone_lookup(RecordType::TXT, "seer.test").await;
2439 assert_eq!(records.len(), 1);
2440 match &records[0].data {
2441 RecordData::TXT { text } => assert_eq!(text, "v=spf1 -all"),
2442 other => panic!("expected TXT data, got {other:?}"),
2443 }
2444 }
2445
2446 #[tokio::test]
2447 async fn mock_resolve_soa_maps_all_fields() {
2448 let records = mock_zone_lookup(RecordType::SOA, "seer.test").await;
2449 assert_eq!(records.len(), 1);
2450 match &records[0].data {
2451 RecordData::SOA {
2452 mname,
2453 rname,
2454 serial,
2455 refresh,
2456 retry,
2457 expire,
2458 minimum,
2459 } => {
2460 assert_eq!(mname, "ns1.seer.test.");
2461 assert_eq!(rname, "hostmaster.seer.test.");
2462 assert_eq!(*serial, 2026070101);
2463 assert_eq!(*refresh, 7200);
2464 assert_eq!(*retry, 3600);
2465 assert_eq!(*expire, 1209600);
2466 assert_eq!(*minimum, 300);
2467 }
2468 other => panic!("expected SOA data, got {other:?}"),
2469 }
2470 }
2471
2472 #[tokio::test]
2473 async fn mock_resolve_caa_maps_flags_tag_and_value() {
2474 let records = mock_zone_lookup(RecordType::CAA, "seer.test").await;
2475 assert_eq!(records.len(), 2);
2476 let by_tag = |wanted: &str| {
2477 records
2478 .iter()
2479 .find_map(|r| match &r.data {
2480 RecordData::CAA { flags, tag, value } if tag == wanted => {
2481 Some((*flags, value.clone()))
2482 }
2483 _ => None,
2484 })
2485 .unwrap_or_else(|| panic!("expected a CAA record with tag {wanted}"))
2486 };
2487 assert_eq!(by_tag("issue"), (0, "letsencrypt.org".to_string()));
2489 assert_eq!(
2490 by_tag("iodef"),
2491 (128, "mailto:security@seer.test".to_string())
2492 );
2493 }
2494
2495 #[tokio::test]
2496 async fn mock_resolve_tlsa_hex_encodes_cert_data() {
2497 let records = mock_zone_lookup(RecordType::TLSA, "_443._tcp.seer.test").await;
2498 assert_eq!(records.len(), 1);
2499 match &records[0].data {
2500 RecordData::TLSA {
2501 cert_usage,
2502 selector,
2503 matching,
2504 cert_data,
2505 } => {
2506 assert_eq!((*cert_usage, *selector, *matching), (3, 1, 1));
2507 assert_eq!(cert_data, "ABCD01");
2508 }
2509 other => panic!("expected TLSA data, got {other:?}"),
2510 }
2511 }
2512
2513 #[tokio::test]
2514 async fn mock_resolve_sshfp_hex_encodes_fingerprint() {
2515 let records = mock_zone_lookup(RecordType::SSHFP, "seer.test").await;
2516 assert_eq!(records.len(), 1);
2517 match &records[0].data {
2518 RecordData::SSHFP {
2519 algorithm,
2520 fingerprint_type,
2521 fingerprint,
2522 } => {
2523 assert_eq!((*algorithm, *fingerprint_type), (4, 2));
2524 assert_eq!(fingerprint, "DEADBEEF");
2525 }
2526 other => panic!("expected SSHFP data, got {other:?}"),
2527 }
2528 }
2529
2530 #[tokio::test]
2531 async fn mock_resolve_naptr_decodes_character_strings() {
2532 let records = mock_zone_lookup(RecordType::NAPTR, "seer.test").await;
2533 assert_eq!(records.len(), 1);
2534 match &records[0].data {
2535 RecordData::NAPTR {
2536 order,
2537 preference,
2538 flags,
2539 services,
2540 regexp,
2541 replacement,
2542 } => {
2543 assert_eq!((*order, *preference), (100, 50));
2544 assert_eq!(flags, "U");
2545 assert_eq!(services, "E2U+sip");
2546 assert_eq!(regexp, "!^.*$!sip:info@seer.test!");
2547 assert_eq!(replacement, ".");
2548 }
2549 other => panic!("expected NAPTR data, got {other:?}"),
2550 }
2551 }
2552
2553 #[tokio::test]
2554 async fn mock_resolve_srv_via_dig_style_name() {
2555 let records = mock_zone_lookup(RecordType::SRV, "_sip._tcp.seer.test").await;
2556 assert_eq!(records.len(), 1);
2557 assert_eq!(records[0].name, "_sip._tcp.seer.test");
2558 match &records[0].data {
2559 RecordData::SRV {
2560 priority,
2561 weight,
2562 port,
2563 target,
2564 } => {
2565 assert_eq!((*priority, *weight, *port), (10, 5, 5060));
2566 assert_eq!(target, "sipserver.seer.test.");
2567 }
2568 other => panic!("expected SRV data, got {other:?}"),
2569 }
2570 }
2571
2572 #[tokio::test]
2573 async fn mock_resolve_ptr_transforms_ip_literal() {
2574 let records = mock_zone_lookup(RecordType::PTR, "192.0.2.1").await;
2575 assert_eq!(records.len(), 1);
2576 assert_eq!(records[0].name, "1.2.0.192.in-addr.arpa");
2578 assert!(matches!(
2579 &records[0].data,
2580 RecordData::PTR { target } if target == "ptr.seer.test."
2581 ));
2582 }
2583
2584 #[tokio::test]
2585 async fn mock_resolve_keeps_www_label() {
2586 let records = mock_zone_lookup(RecordType::CNAME, "www.seer.test").await;
2589 assert_eq!(records.len(), 1, "{records:?}");
2590 assert_eq!(records[0].name, "www.seer.test");
2591 assert_eq!(records[0].data.to_string(), "edge.cdn.test.");
2592 }
2593
2594 #[tokio::test]
2595 async fn mock_resolve_srv_keeps_www_label() {
2596 let port = spawn_mock_dns_fn(|qname, qtype| match (qname, qtype) {
2597 ("_sip._tcp.www.seer.test", HickoryRecordType::SRV) => {
2598 MockReply::Answer(vec![HickoryRData::SRV(
2599 hickory_resolver::proto::rr::rdata::SRV::new(
2600 10,
2601 5,
2602 5060,
2603 Name::from_ascii("sip.seer.test.").unwrap(),
2604 ),
2605 )])
2606 }
2607 _ => MockReply::NoData,
2608 })
2609 .await;
2610 let records = mock_dns_resolver(port)
2611 .resolve(
2612 "_sip._tcp.www.seer.test",
2613 RecordType::SRV,
2614 Some("127.0.0.1"),
2615 )
2616 .await
2617 .expect("SRV against mock");
2618 assert_eq!(records.len(), 1, "{records:?}");
2619 assert_eq!(records[0].name, "_sip._tcp.www.seer.test");
2620 }
2621
2622 #[tokio::test]
2623 async fn mock_resolve_queries_a_wildcard_owner_name() {
2624 let port = spawn_mock_dns_fn(|qname, qtype| match (qname, qtype) {
2627 ("*.seer.test", HickoryRecordType::A) => MockReply::Answer(vec![HickoryRData::A(
2628 hickory_resolver::proto::rr::rdata::A(Ipv4Addr::new(192, 0, 2, 42)),
2629 )]),
2630 _ => MockReply::NoData,
2631 })
2632 .await;
2633 let records = mock_dns_resolver(port)
2634 .resolve("*.seer.test", RecordType::A, Some("127.0.0.1"))
2635 .await
2636 .expect("wildcard A against mock");
2637 assert_eq!(records.len(), 1, "{records:?}");
2638 assert_eq!(records[0].name, "*.seer.test");
2639 assert_eq!(records[0].data.to_string(), "192.0.2.42");
2640 }
2641
2642 #[tokio::test]
2643 async fn mock_presence_still_probes_the_apex_for_www_input() {
2644 let port = spawn_mock_dns_fn(|qname, qtype| match (qname, qtype) {
2647 ("seer.test", HickoryRecordType::NS) => MockReply::Answer(vec![HickoryRData::NS(
2648 hickory_resolver::proto::rr::rdata::NS(Name::from_ascii("ns1.seer.test.").unwrap()),
2649 )]),
2650 ("www.seer.test", HickoryRecordType::NS) => MockReply::NoDataWithSoa("seer.test"),
2653 _ => MockReply::NxDomain,
2654 })
2655 .await;
2656 let resolver = mock_dns_resolver_default(port);
2657 assert_eq!(
2658 resolver.presence("www.seer.test").await,
2659 DnsPresence::Present
2660 );
2661 }
2662
2663 #[tokio::test]
2664 async fn mock_resolve_ptr_transforms_ipv6_literal() {
2665 let records = mock_zone_lookup(RecordType::PTR, "2606:4700:4700::1111").await;
2666 assert_eq!(records.len(), 1, "{records:?}");
2667 assert_eq!(records[0].data.to_string(), "one.one.one.one.");
2668 }
2669
2670 #[tokio::test]
2671 async fn mock_resolve_any_aggregates_multiple_types() {
2672 let records = mock_zone_lookup(RecordType::ANY, "seer.test").await;
2673 for expected in [
2676 RecordType::A,
2677 RecordType::AAAA,
2678 RecordType::MX,
2679 RecordType::NS,
2680 RecordType::TXT,
2681 RecordType::SOA,
2682 RecordType::CAA,
2683 RecordType::HTTPS,
2684 RecordType::DNSKEY,
2685 ] {
2686 assert!(
2687 records.iter().any(|r| r.record_type == expected),
2688 "ANY must include {expected} records"
2689 );
2690 }
2691 assert_eq!(records.len(), 13);
2694 assert!(records.iter().all(|r| r.name == "seer.test"));
2695 assert!(!records.iter().any(|r| r.record_type == RecordType::CDS));
2697 }
2698
2699 #[tokio::test]
2700 async fn mock_resolve_converts_service_bindings() {
2701 let records = mock_zone_lookup(RecordType::HTTPS, "seer.test").await;
2702 assert_eq!(records.len(), 1, "{records:?}");
2703 let RecordData::HTTPS {
2704 priority,
2705 target,
2706 params,
2707 } = &records[0].data
2708 else {
2709 panic!("expected HTTPS data, got {:?}", records[0].data);
2710 };
2711 assert_eq!((*priority, target.as_str()), (1, "."));
2712 let pairs: Vec<(&str, &str)> = params
2713 .iter()
2714 .map(|p| (p.key.as_str(), p.value.as_str()))
2715 .collect();
2716 assert_eq!(
2717 pairs,
2718 [
2719 ("alpn", "h3,h2"),
2720 ("port", "8443"),
2721 ("ipv4hint", "192.0.2.1,192.0.2.2"),
2722 ("ech", "AAH+"),
2723 ("ipv6hint", "2001:db8::1"),
2724 ("key65333", "ex\\0321"),
2727 ]
2728 );
2729 assert_eq!(
2730 records[0].data.to_string(),
2731 "1 . alpn=\"h3,h2\" port=8443 ipv4hint=192.0.2.1,192.0.2.2 ech=AAH+ \
2732 ipv6hint=2001:db8::1 key65333=ex\\0321"
2733 );
2734
2735 let alias = mock_zone_lookup(RecordType::HTTPS, "alias.seer.test").await;
2736 assert_eq!(alias.len(), 1, "{alias:?}");
2737 assert_eq!(alias[0].data.to_string(), "0 pool.seer.test.");
2738
2739 let svcb = mock_zone_lookup(RecordType::SVCB, "_8443._foo.seer.test").await;
2740 assert_eq!(svcb.len(), 1, "{svcb:?}");
2741 assert_eq!(svcb[0].record_type, RecordType::SVCB);
2742 assert_eq!(
2743 svcb[0].data.to_string(),
2744 "2 svc.seer.test. mandatory=alpn,port alpn=\"foo\" no-default-alpn port=8443"
2745 );
2746 }
2747
2748 #[tokio::test]
2749 async fn mock_resolve_converts_child_dnssec_records() {
2750 let cds = mock_zone_lookup(RecordType::CDS, "seer.test").await;
2752 let shown: Vec<String> = cds.iter().map(|r| r.data.to_string()).collect();
2753 assert_eq!(shown, ["2371 15 2 ABCDEF", "0 0 0 00"]);
2754 assert!(cds.iter().all(|r| r.record_type == RecordType::CDS));
2755
2756 let cdnskey = mock_zone_lookup(RecordType::CDNSKEY, "seer.test").await;
2757 let shown: Vec<String> = cdnskey.iter().map(|r| r.data.to_string()).collect();
2758 assert_eq!(
2759 shown,
2760 [
2761 "257 3 15 BwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwc=",
2762 "0 3 0 AA==",
2764 ]
2765 );
2766 }
2767
2768 #[tokio::test]
2769 async fn mock_resolve_keeps_only_the_requested_type_behind_a_cname() {
2770 let port = spawn_mock_dns_fn(|qname, qtype| match (qname, qtype) {
2774 ("www.seer.test", HickoryRecordType::A) => MockReply::Records(vec![
2775 record("www.seer.test.", 300, cname_rdata("edge.cdn.test.")),
2776 record("edge.cdn.test.", 60, a_rdata([192, 0, 2, 7])),
2777 ]),
2778 _ => MockReply::NxDomain,
2779 })
2780 .await;
2781 let records = mock_dns_resolver(port)
2782 .resolve("www.seer.test", RecordType::A, Some("127.0.0.1"))
2783 .await
2784 .expect("A behind a CNAME");
2785 assert_eq!(
2786 records,
2787 [DnsRecord {
2788 name: "www.seer.test".to_string(),
2789 record_type: RecordType::A,
2790 ttl: 60,
2791 data: RecordData::A {
2792 address: "192.0.2.7".to_string(),
2793 },
2794 }]
2795 );
2796 }
2797
2798 fn is_probe(qname: &str) -> bool {
2801 qname.starts_with("seer-probe-")
2802 }
2803
2804 type Asked = Arc<std::sync::Mutex<Vec<(String, HickoryRecordType)>>>;
2806
2807 async fn spawn_recording<F>(mut handler: F) -> (u16, Asked)
2809 where
2810 F: FnMut(&str, HickoryRecordType) -> MockReply + Send + 'static,
2811 {
2812 let asked: Asked = Arc::default();
2813 let log = Arc::clone(&asked);
2814 let port = spawn_mock_dns_fn(move |qname, qtype| {
2815 log.lock().unwrap().push((qname.to_string(), qtype));
2816 handler(qname, qtype)
2817 })
2818 .await;
2819 (port, asked)
2820 }
2821
2822 fn probes_sent(asked: &Asked) -> usize {
2823 asked
2824 .lock()
2825 .unwrap()
2826 .iter()
2827 .filter(|(name, _)| is_probe(name))
2828 .count()
2829 }
2830
2831 async fn mock_query(port: u16, name: &str, record_type: RecordType) -> DnsQueryResult {
2832 mock_dns_resolver(port)
2833 .query(name, record_type, Some("127.0.0.1"))
2834 .await
2835 .unwrap_or_else(|e| panic!("{record_type} query against mock must succeed: {e}"))
2836 }
2837
2838 fn shown(records: &[&DnsRecord]) -> Vec<(String, RecordType, String)> {
2839 records
2840 .iter()
2841 .map(|r| (r.name.clone(), r.record_type, r.data.to_string()))
2842 .collect()
2843 }
2844
2845 fn row(name: &str, record_type: RecordType, data: &str) -> (String, RecordType, String) {
2846 (name.to_string(), record_type, data.to_string())
2847 }
2848
2849 #[tokio::test]
2850 async fn mock_query_reports_the_cname_chain_under_real_owners() {
2851 let port = spawn_mock_dns_fn(|qname, qtype| match (qname, qtype) {
2852 ("www.seer.test", HickoryRecordType::A) => MockReply::Records(vec![
2853 record("www.seer.test.", 300, cname_rdata("edge.cdn.test.")),
2854 record("edge.cdn.test.", 120, cname_rdata("origin.cdn.test.")),
2855 record("origin.cdn.test.", 60, a_rdata([192, 0, 2, 7])),
2856 record("origin.cdn.test.", 60, a_rdata([192, 0, 2, 8])),
2857 ]),
2858 _ => MockReply::NxDomain,
2859 })
2860 .await;
2861 let result = mock_query(port, "WWW.Seer.test", RecordType::A).await;
2862
2863 assert_eq!(result.name, "www.seer.test");
2864 assert_eq!(result.record_type, RecordType::A);
2865 assert_eq!(result.server.as_deref(), Some("127.0.0.1"));
2866 assert_eq!(result.status, DnsStatus::NoError);
2867 assert_eq!(result.flags, ["qr", "rd", "ra"]);
2868 assert_eq!(
2869 shown(&result.cname_chain().collect::<Vec<_>>()),
2870 [
2871 row("www.seer.test", RecordType::CNAME, "edge.cdn.test."),
2872 row("edge.cdn.test", RecordType::CNAME, "origin.cdn.test."),
2873 ]
2874 );
2875 assert_eq!(
2876 shown(&result.records().collect::<Vec<_>>()),
2877 [
2878 row("origin.cdn.test", RecordType::A, "192.0.2.7"),
2879 row("origin.cdn.test", RecordType::A, "192.0.2.8"),
2880 ]
2881 );
2882 assert_eq!(result.answers[2].ttl, 60);
2883 assert!(result.authority.is_empty());
2884 assert!(!result.is_nodata());
2885 let wildcard = result
2887 .wildcard
2888 .expect("probe ran for a name below seer.test");
2889 assert!(is_probe(&wildcard.probe_name), "{wildcard:?}");
2890 assert!(wildcard.probe_name.ends_with(".seer.test"), "{wildcard:?}");
2891 assert!(!wildcard.present && !wildcard.matches_answer);
2892 }
2893
2894 #[tokio::test]
2895 async fn mock_query_reports_idn_owners_as_a_labels() {
2896 let port = spawn_mock_dns_fn(|qname, qtype| match (qname, qtype) {
2900 ("xn--bcher-kva.seer.test", HickoryRecordType::A) => MockReply::Records(vec![
2901 record(
2902 "xn--bcher-kva.seer.test.",
2903 300,
2904 cname_rdata("edge.xn--caf-dma.test."),
2905 ),
2906 record("edge.xn--caf-dma.test.", 60, a_rdata([192, 0, 2, 7])),
2907 ]),
2908 _ => MockReply::NxDomain,
2909 })
2910 .await;
2911 let result = mock_query(port, "Bücher.seer.test", RecordType::A).await;
2912 assert_eq!(result.name, "xn--bcher-kva.seer.test");
2913 assert_eq!(
2914 shown(&result.answers.iter().collect::<Vec<_>>()),
2915 [
2916 row(
2917 "xn--bcher-kva.seer.test",
2918 RecordType::CNAME,
2919 "edge.xn--caf-dma.test."
2920 ),
2921 row("edge.xn--caf-dma.test", RecordType::A, "192.0.2.7"),
2922 ]
2923 );
2924 assert_eq!(result.cname_chain().count(), 1);
2925 }
2926
2927 #[tokio::test]
2928 async fn query_refuses_a_reserved_nameserver() {
2929 let r = DnsResolver::new();
2933 for reserved in ["127.0.0.1", "10.0.0.1", "::1", "tls://192.168.1.1"] {
2934 let err = r
2935 .query("www.example.com", RecordType::A, Some(reserved))
2936 .await
2937 .expect_err("a reserved nameserver must be refused");
2938 let msg = err.to_string().to_lowercase();
2939 assert!(
2940 msg.contains("blocked") || msg.contains("reserved"),
2941 "{reserved}: {msg}"
2942 );
2943 }
2944 }
2945
2946 #[tokio::test]
2947 async fn mock_query_reports_a_cname_the_server_did_not_follow_as_sent() {
2948 let (port, asked) = spawn_recording(|qname, qtype| match (qname, qtype) {
2953 ("www.seer.test", HickoryRecordType::A) => MockReply::Records(vec![record(
2954 "www.seer.test.",
2955 300,
2956 cname_rdata("edge.cdn.test."),
2957 )]),
2958 ("edge.cdn.test", HickoryRecordType::A) => {
2959 MockReply::Answer(vec![a_rdata([192, 0, 2, 7])])
2960 }
2961 _ => MockReply::NxDomain,
2962 })
2963 .await;
2964 let result = mock_query(port, "www.seer.test", RecordType::A).await;
2965 assert_eq!(result.status, DnsStatus::NoError);
2966 assert_eq!(result.flags, ["qr", "rd", "ra"]);
2967 assert_eq!(
2968 shown(&result.answers.iter().collect::<Vec<_>>()),
2969 [row("www.seer.test", RecordType::CNAME, "edge.cdn.test.")]
2970 );
2971 assert_eq!(result.records().count(), 0);
2972 assert!(
2973 !asked
2974 .lock()
2975 .unwrap()
2976 .iter()
2977 .any(|(n, _)| n == "edge.cdn.test"),
2978 "the target is not chased: {asked:?}"
2979 );
2980 }
2981
2982 #[tokio::test]
2983 async fn mock_query_reports_a_dangling_cname_with_its_chain() {
2984 let port = spawn_mock_dns_fn(|qname, _| match qname {
2988 "www.seer.test" => MockReply::Negative {
2989 code: ResponseCode::NXDomain,
2990 chain: vec![
2991 record("www.seer.test.", 300, cname_rdata("shop.seer.test.")),
2992 record("shop.seer.test.", 300, cname_rdata("gone.cdn.test.")),
2993 ],
2994 soa: "cdn.test",
2995 },
2996 _ => MockReply::NxDomainWithSoa("seer.test"),
2997 })
2998 .await;
2999 let result = mock_query(port, "www.seer.test", RecordType::A).await;
3000 assert_eq!(result.status, DnsStatus::NxDomain);
3001 assert_eq!(result.flags, ["qr", "rd", "ra"]);
3002 assert_eq!(
3003 shown(&result.cname_chain().collect::<Vec<_>>()),
3004 [
3005 row("www.seer.test", RecordType::CNAME, "shop.seer.test."),
3006 row("shop.seer.test", RecordType::CNAME, "gone.cdn.test."),
3007 ]
3008 );
3009 assert_eq!(result.records().count(), 0);
3010 assert_eq!(
3011 shown(&result.authority.iter().collect::<Vec<_>>()),
3012 [row(
3013 "cdn.test",
3014 RecordType::SOA,
3015 "ns1.cdn.test. hostmaster.cdn.test. 2026070101 7200 3600 1209600 300"
3016 )]
3017 );
3018 assert!(!result.is_nodata());
3019 assert_eq!(result.referral_zone(), None);
3020 assert_eq!(
3021 result.wildcard, None,
3022 "a negative answer has nothing to compare"
3023 );
3024 }
3025
3026 #[tokio::test]
3027 async fn mock_query_reports_nodata_behind_a_cname() {
3028 let port = spawn_mock_dns_fn(|qname, _| match qname {
3029 "www.seer.test" => MockReply::Negative {
3030 code: ResponseCode::NoError,
3031 chain: vec![record("www.seer.test.", 300, cname_rdata("edge.cdn.test."))],
3032 soa: "cdn.test",
3033 },
3034 _ => MockReply::NxDomain,
3035 })
3036 .await;
3037 let result = mock_query(port, "www.seer.test", RecordType::AAAA).await;
3038 assert_eq!(result.status, DnsStatus::NoError);
3039 assert_eq!(result.flags, ["qr", "rd", "ra"]);
3040 assert_eq!(result.cname_chain().count(), 1);
3041 assert_eq!(result.records().count(), 0);
3042 assert!(result.is_nodata());
3043 assert_eq!(result.referral_zone(), None);
3044 assert_eq!(result.authority.len(), 1);
3045 assert_eq!(result.authority[0].name, "cdn.test");
3046 }
3047
3048 #[tokio::test]
3049 async fn mock_query_reports_nxdomain_with_the_zone_soa() {
3050 let (port, asked) = spawn_recording(|_, _| MockReply::NxDomainWithSoa("seer.test")).await;
3051 let result = mock_query(port, "nx.seer.test", RecordType::A).await;
3052 assert_eq!(result.status, DnsStatus::NxDomain);
3053 assert!(result.answers.is_empty());
3054 assert!(!result.is_nodata(), "NXDOMAIN is not NODATA");
3055 assert_eq!(result.flags, ["qr", "rd", "ra"]);
3057 assert_eq!(
3058 shown(&result.authority.iter().collect::<Vec<_>>()),
3059 [row(
3060 "seer.test",
3061 RecordType::SOA,
3062 "ns1.seer.test. hostmaster.seer.test. 2026070101 7200 3600 1209600 300"
3063 )]
3064 );
3065 assert_eq!(result.wildcard, None);
3067 assert!(asked
3068 .lock()
3069 .unwrap()
3070 .iter()
3071 .any(|(n, _)| n == "nx.seer.test"));
3072 }
3073
3074 #[tokio::test]
3075 async fn mock_query_reports_nodata_with_the_zone_soa() {
3076 let port = spawn_mock_dns_fn(|_, _| MockReply::NoDataWithSoa("seer.test")).await;
3077 let result = mock_query(port, "www.seer.test", RecordType::MX).await;
3078 assert_eq!(result.status, DnsStatus::NoError);
3079 assert!(result.is_nodata());
3080 assert!(result.answers.is_empty());
3081 assert_eq!(result.authority.len(), 1);
3082 assert_eq!(result.authority[0].record_type, RecordType::SOA);
3083 assert_eq!(result.authority[0].name, "seer.test");
3084 assert_eq!(result.wildcard, None);
3085 }
3086
3087 #[tokio::test]
3088 async fn mock_query_reports_error_codes_as_results() {
3089 let port = spawn_mock_dns_fn(|qname, _| match qname {
3090 "servfail.seer.test" => MockReply::ServFail,
3091 "refused.seer.test" => MockReply::Refused,
3092 "notimp.seer.test" => MockReply::Rcode(ResponseCode::NotImp),
3093 _ => MockReply::NxDomain,
3094 })
3095 .await;
3096 for (name, status, shown) in [
3097 ("servfail.seer.test", DnsStatus::ServFail, "SERVFAIL"),
3098 ("refused.seer.test", DnsStatus::Refused, "REFUSED"),
3099 ("notimp.seer.test", DnsStatus::Other(4), "NOTIMP"),
3100 ] {
3101 let result = mock_query(port, name, RecordType::A).await;
3102 assert_eq!(result.status, status, "{name}");
3103 assert_eq!(result.status.to_string(), shown);
3104 assert!(result.answers.is_empty() && result.authority.is_empty());
3105 assert_eq!(result.flags, ["qr", "rd", "ra"], "{name}");
3106 assert!(!result.is_nodata());
3107 assert_eq!(result.wildcard, None);
3108 }
3109 }
3110
3111 #[tokio::test]
3112 async fn mock_query_reads_an_extended_rcode_from_edns() {
3113 let port = spawn_mock_dns_fn(|_, _| MockReply::Rcode(ResponseCode::BADVERS)).await;
3116 let result = mock_query(port, "www.seer.test", RecordType::A).await;
3117 assert_eq!(result.status, DnsStatus::Other(16));
3118 assert_eq!(result.status.to_string(), "BADVERS");
3119 }
3120
3121 #[tokio::test]
3122 async fn mock_query_repeats_a_truncated_reply_over_tcp() {
3123 let transports = Arc::new(std::sync::Mutex::new(0));
3126 let seen = Arc::clone(&transports);
3127 let port = spawn_mock_dns_fn_with_tcp(move |qname, _| {
3128 if qname != "big.seer.test" {
3129 return MockReply::NxDomain;
3130 }
3131 let mut n = seen.lock().unwrap();
3132 *n += 1;
3133 if *n == 1 {
3134 MockReply::Truncated
3135 } else {
3136 MockReply::Answer((1..=3).map(|i| a_rdata([192, 0, 2, i])).collect())
3137 }
3138 })
3139 .await;
3140 let result = mock_query(port, "big.seer.test", RecordType::A).await;
3141 assert_eq!(result.status, DnsStatus::NoError);
3142 assert_eq!(result.records().count(), 3);
3143 assert!(
3144 !result.flags.contains(&"tc".to_string()),
3145 "{:?}",
3146 result.flags
3147 );
3148 assert_eq!(*transports.lock().unwrap(), 2, "UDP, then TCP");
3149 }
3150
3151 #[tokio::test]
3152 async fn mock_query_reports_a_referral_as_neither_nodata_nor_an_answer() {
3153 let port = spawn_mock_dns_fn(|_, _| MockReply::Delegation {
3157 zone: "child.seer.test".to_string(),
3158 servers: vec![
3159 (
3160 "ns1.child.seer.test".to_string(),
3161 vec![IpAddr::from([192, 0, 2, 1])],
3162 ),
3163 ("ns2.child.seer.test".to_string(), vec![]),
3164 ],
3165 })
3166 .await;
3167 let result = mock_query(port, "www.child.seer.test", RecordType::A).await;
3168 assert_eq!(result.status, DnsStatus::NoError);
3169 assert_eq!(result.flags, ["qr", "rd"]);
3171 assert!(result.answers.is_empty());
3172 assert_eq!(result.referral_zone(), Some("child.seer.test"));
3173 assert!(!result.is_nodata());
3174 assert_eq!(
3175 shown(&result.authority.iter().collect::<Vec<_>>()),
3176 [
3177 row("child.seer.test", RecordType::NS, "ns1.child.seer.test."),
3178 row("child.seer.test", RecordType::NS, "ns2.child.seer.test."),
3179 ]
3180 );
3181 assert_eq!(result.wildcard, None);
3182 }
3183
3184 #[tokio::test]
3185 async fn mock_query_reports_special_use_names_as_answered_locally() {
3186 let (port, asked) = spawn_recording(|_, _| MockReply::NxDomain).await;
3191 let ptr = mock_query(port, "127.0.0.1", RecordType::PTR).await;
3192 assert_eq!(ptr.name, "1.0.0.127.in-addr.arpa");
3193 assert!(ptr.answered_locally);
3194 assert_eq!(ptr.server, None, "no server was asked");
3195 assert!(ptr.flags.is_empty(), "the header is hickory's own");
3196 assert_eq!(ptr.status, DnsStatus::NoError);
3197 assert_eq!(
3198 shown(&ptr.answers.iter().collect::<Vec<_>>()),
3199 [row("1.0.0.127.in-addr.arpa", RecordType::PTR, "localhost.")]
3200 );
3201 assert_eq!(ptr.wildcard, None, "no wildcard probe for a local name");
3202
3203 let mx = mock_query(port, "foo.localhost", RecordType::MX).await;
3204 assert!(mx.answered_locally && mx.is_nodata());
3205 let onion = mock_query(port, "x.onion", RecordType::A).await;
3206 assert!(onion.answered_locally);
3207 assert_eq!(onion.status, DnsStatus::NxDomain);
3208
3209 assert!(asked.lock().unwrap().is_empty(), "{asked:?}");
3210 }
3211
3212 #[tokio::test]
3213 async fn mock_query_times_out_as_an_error() {
3214 let port = spawn_mock_dns(MockMode::Ignore).await;
3215 let resolver = DnsResolver::new()
3216 .with_timeout(Duration::from_millis(200))
3217 .allowing_private_hosts()
3218 .with_port(port);
3219 let result = resolver
3220 .query("www.seer.test", RecordType::A, Some("127.0.0.1"))
3221 .await;
3222 assert!(
3223 matches!(&result, Err(SeerError::DnsError(m)) if m.contains("A lookup failed")),
3224 "an unanswered query is a transport error, got: {result:?}"
3225 );
3226 }
3227
3228 #[tokio::test]
3229 async fn mock_query_reads_flags_from_the_response_header() {
3230 let port = spawn_mock_dns_fn(|qname, qtype| match (qname, qtype) {
3231 ("seer.test", HickoryRecordType::A) => {
3232 MockReply::AuthoritativeAnswer(vec![a_rdata([192, 0, 2, 1])])
3233 }
3234 _ => MockReply::NxDomain,
3235 })
3236 .await;
3237 let result = mock_query(port, "seer.test", RecordType::A).await;
3238 assert_eq!(result.flags, ["qr", "aa", "rd", "ra"]);
3239 assert_eq!(result.cname_chain().count(), 0);
3240 assert_eq!(
3241 result.records().next().map(|r| r.name.as_str()),
3242 Some("seer.test")
3243 );
3244 }
3245
3246 #[tokio::test]
3247 async fn mock_query_converts_the_new_record_types_under_their_owner() {
3248 let port = spawn_mock_dns(MockMode::Zone).await;
3249 let https = mock_query(port, "seer.test", RecordType::HTTPS).await;
3250 assert_eq!(https.answers.len(), 1, "{https:?}");
3251 assert_eq!(https.answers[0].name, "seer.test");
3252 assert!(https.answers[0]
3253 .data
3254 .to_string()
3255 .starts_with("1 . alpn=\"h3,h2\" port=8443"));
3256
3257 let alias = mock_query(port, "alias.seer.test", RecordType::HTTPS).await;
3258 assert_eq!(alias.answers[0].data.to_string(), "0 pool.seer.test.");
3259 assert_eq!(alias.answers[0].name, "alias.seer.test");
3260
3261 let cds = mock_query(port, "seer.test", RecordType::CDS).await;
3262 assert_eq!(cds.answers.len(), 2);
3263 let cdnskey = mock_query(port, "seer.test", RecordType::CDNSKEY).await;
3264 assert_eq!(cdnskey.answers[1].data.to_string(), "0 3 0 AA==");
3265 }
3266
3267 #[tokio::test]
3268 async fn mock_query_sorts_mx_like_resolve() {
3269 let port = spawn_mock_dns(MockMode::Zone).await;
3270 let result = mock_query(port, "seer.test", RecordType::MX).await;
3271 let prefs: Vec<String> = result.answers.iter().map(|r| r.data.to_string()).collect();
3272 assert_eq!(
3273 prefs,
3274 [
3275 "10 a.mail.seer.test.",
3276 "20 b.mail.seer.test.",
3277 "30 c.mail.seer.test."
3278 ]
3279 );
3280 }
3281
3282 #[tokio::test]
3283 async fn mock_query_resolves_srv_and_ptr_names_like_resolve() {
3284 let (port, asked) = spawn_recording(|qname, qtype| match (qname, qtype) {
3285 ("_sip._tcp.seer.test", HickoryRecordType::SRV) => {
3286 MockReply::Answer(vec![HickoryRData::SRV(
3287 hickory_resolver::proto::rr::rdata::SRV::new(
3288 10,
3289 5,
3290 5060,
3291 Name::from_ascii("sip.seer.test.").unwrap(),
3292 ),
3293 )])
3294 }
3295 ("1.2.0.192.in-addr.arpa", HickoryRecordType::PTR) => {
3296 MockReply::Answer(vec![HickoryRData::PTR(
3297 hickory_resolver::proto::rr::rdata::PTR(
3298 Name::from_ascii("ptr.seer.test.").unwrap(),
3299 ),
3300 )])
3301 }
3302 _ => MockReply::NxDomain,
3303 })
3304 .await;
3305
3306 let srv = mock_query(port, "_sip._tcp.seer.test", RecordType::SRV).await;
3307 assert_eq!(
3308 shown(&srv.answers.iter().collect::<Vec<_>>()),
3309 [row(
3310 "_sip._tcp.seer.test",
3311 RecordType::SRV,
3312 "10 5 5060 sip.seer.test."
3313 )]
3314 );
3315 let probe = srv.wildcard.expect("SRV name is below seer.test");
3317 assert!(probe.probe_name.ends_with("._tcp.seer.test"), "{probe:?}");
3318 assert!(asked.lock().unwrap().iter().any(|(n, t)| is_probe(n)
3319 && n.ends_with("._tcp.seer.test")
3320 && *t == HickoryRecordType::SRV));
3321
3322 let ptr = mock_query(port, "192.0.2.1", RecordType::PTR).await;
3323 assert_eq!(ptr.name, "1.2.0.192.in-addr.arpa");
3324 assert_eq!(ptr.answers[0].name, "1.2.0.192.in-addr.arpa");
3325
3326 let err = mock_dns_resolver(port)
3327 .query("seer.test", RecordType::SRV, Some("127.0.0.1"))
3328 .await
3329 .expect_err("a bare domain is not an SRV query");
3330 assert!(matches!(err, SeerError::InvalidInput(_)), "{err:?}");
3331 let err = mock_dns_resolver(port)
3332 .query(".bad.example", RecordType::A, Some("127.0.0.1"))
3333 .await;
3334 assert!(err.is_err(), "invalid names are rejected before any I/O");
3335 }
3336
3337 #[tokio::test]
3338 async fn mock_query_any_fans_out_to_the_new_types_and_dedupes() {
3339 let (port, asked) = spawn_recording(|qname, qtype| {
3340 let chain = || record("www.seer.test.", 300, cname_rdata("edge.seer.test."));
3341 match (qname, qtype) {
3342 ("www.seer.test", HickoryRecordType::CNAME) => MockReply::Records(vec![chain()]),
3343 ("www.seer.test", HickoryRecordType::A) => MockReply::Records(vec![
3344 chain(),
3345 record("edge.seer.test.", 60, a_rdata([192, 0, 2, 7])),
3346 ]),
3347 ("www.seer.test", HickoryRecordType::HTTPS) => {
3348 MockReply::Records(vec![
3349 chain(),
3350 record(
3351 "edge.seer.test.",
3352 60,
3353 HickoryRData::HTTPS(hickory_resolver::proto::rr::rdata::HTTPS(
3354 SVCB::new(1, Name::root(), vec![]),
3355 )),
3356 ),
3357 ])
3358 }
3359 ("www.seer.test", _) => MockReply::Records(vec![chain()]),
3362 _ => MockReply::NoDataWithSoa("seer.test"),
3363 }
3364 })
3365 .await;
3366 let result = mock_query(port, "www.seer.test", RecordType::ANY).await;
3367
3368 assert_eq!(result.record_type, RecordType::ANY);
3369 assert_eq!(result.status, DnsStatus::NoError);
3370 assert_eq!(result.flags, ["qr", "rd", "ra"]);
3371 assert_eq!(
3373 shown(&result.answers.iter().collect::<Vec<_>>()),
3374 [
3375 row("www.seer.test", RecordType::CNAME, "edge.seer.test."),
3376 row("edge.seer.test", RecordType::A, "192.0.2.7"),
3377 row("edge.seer.test", RecordType::HTTPS, "1 ."),
3378 ]
3379 );
3380 assert_eq!(result.cname_chain().count(), 1);
3381 assert_eq!(result.records().count(), 2);
3382 assert!(!result.is_nodata());
3383 assert_eq!(result.wildcard, None, "ANY is never probed");
3384
3385 let asked: Vec<HickoryRecordType> = asked
3386 .lock()
3387 .unwrap()
3388 .iter()
3389 .filter(|(n, _)| n == "www.seer.test")
3390 .map(|(_, t)| *t)
3391 .collect();
3392 for wire in ANY_TYPES.iter().filter_map(|t| wire_type(*t)) {
3393 assert!(asked.contains(&wire), "ANY must query {wire}: {asked:?}");
3394 }
3395 assert!(
3396 !asked.contains(&HickoryRecordType::ANY),
3397 "ANY is never sent"
3398 );
3399 }
3400
3401 #[tokio::test]
3402 async fn mock_query_any_errors_only_when_every_sub_query_fails() {
3403 let port = spawn_mock_dns(MockMode::Ignore).await;
3404 let resolver = DnsResolver::new()
3405 .with_timeout(Duration::from_millis(200))
3406 .allowing_private_hosts()
3407 .with_port(port);
3408 let result = resolver
3409 .query("www.seer.test", RecordType::ANY, Some("127.0.0.1"))
3410 .await;
3411 assert!(matches!(result, Err(SeerError::DnsError(_))), "{result:?}");
3412 }
3413
3414 async fn wildcard_zone(probe_reply: fn() -> MockReply) -> (u16, Asked) {
3417 spawn_recording(move |qname, qtype| match (qname, qtype) {
3418 ("www.seer.test", HickoryRecordType::A) => {
3419 MockReply::Answer(vec![a_rdata([192, 0, 2, 50])])
3420 }
3421 (name, HickoryRecordType::A) if is_probe(name) => probe_reply(),
3422 _ => MockReply::NxDomain,
3423 })
3424 .await
3425 }
3426
3427 #[tokio::test]
3428 async fn mock_wildcard_probe_matching_the_answer() {
3429 let (port, asked) =
3430 wildcard_zone(|| MockReply::Answer(vec![a_rdata([192, 0, 2, 50])])).await;
3431 let result = mock_query(port, "www.seer.test", RecordType::A).await;
3432 let probe = result.wildcard.expect("probe attached");
3433 assert!(probe.present && probe.matches_answer, "{probe:?}");
3434 assert_eq!(probes_sent(&asked), 1);
3435 }
3436
3437 #[tokio::test]
3438 async fn mock_wildcard_probe_with_different_data() {
3439 let (port, _) = wildcard_zone(|| MockReply::Answer(vec![a_rdata([192, 0, 2, 99])])).await;
3440 let result = mock_query(port, "www.seer.test", RecordType::A).await;
3441 let probe = result.wildcard.expect("probe attached");
3442 assert!(probe.present && !probe.matches_answer, "{probe:?}");
3443 }
3444
3445 #[tokio::test]
3446 async fn mock_wildcard_probe_absent() {
3447 let (port, _) = wildcard_zone(|| MockReply::NxDomainWithSoa("seer.test")).await;
3448 let result = mock_query(port, "www.seer.test", RecordType::A).await;
3449 let probe = result.wildcard.expect("probe attached");
3450 assert!(!probe.present && !probe.matches_answer, "{probe:?}");
3451 }
3452
3453 #[tokio::test]
3454 async fn mock_wildcard_probe_that_fails_is_dropped_not_fatal() {
3455 let (port, _) = wildcard_zone(|| MockReply::ServFail).await;
3456 let result = mock_query(port, "www.seer.test", RecordType::A).await;
3457 assert_eq!(result.status, DnsStatus::NoError);
3458 assert_eq!(result.wildcard, None);
3459
3460 let (port, _) = wildcard_zone(|| MockReply::NoReply).await;
3462 let resolver = DnsResolver::new()
3463 .with_timeout(Duration::from_millis(200))
3464 .allowing_private_hosts()
3465 .with_port(port);
3466 let result = resolver
3467 .query("www.seer.test", RecordType::A, Some("127.0.0.1"))
3468 .await
3469 .expect("the probe never fails the query");
3470 assert_eq!(result.records().count(), 1);
3471 assert_eq!(result.wildcard, None);
3472 }
3473
3474 #[tokio::test]
3475 async fn mock_wildcard_probe_is_not_sent_for_ineligible_names() {
3476 let (port, asked) =
3478 spawn_recording(|_, _| MockReply::Answer(vec![a_rdata([192, 0, 2, 50])])).await;
3479 assert_eq!(
3481 mock_query(port, "seer.test", RecordType::A).await.wildcard,
3482 None
3483 );
3484 assert_eq!(
3486 mock_query(port, "*.seer.test", RecordType::A)
3487 .await
3488 .wildcard,
3489 None
3490 );
3491 assert_eq!(
3493 mock_query(port, "www.seer.test", RecordType::ANY)
3494 .await
3495 .wildcard,
3496 None
3497 );
3498 assert_eq!(probes_sent(&asked), 0, "{:?}", asked.lock().unwrap());
3499 }
3500
3501 #[tokio::test]
3502 async fn mock_nodata_folds_to_empty_and_classifies_absent() {
3503 let port = spawn_mock_dns(MockMode::NoData).await;
3504 let result = mock_dns_resolver(port)
3505 .resolve("seer.test", RecordType::NS, Some("127.0.0.1"))
3506 .await;
3507 assert!(
3508 matches!(&result, Ok(records) if records.is_empty()),
3509 "NODATA must fold to Ok(vec![]), got: {result:?}"
3510 );
3511 assert_eq!(classify_ns_presence(&result), DnsPresence::Absent);
3512 }
3513
3514 #[tokio::test]
3515 async fn mock_nxdomain_folds_to_empty_and_classifies_absent() {
3516 let port = spawn_mock_dns(MockMode::Nxdomain).await;
3517 let result = mock_dns_resolver(port)
3518 .resolve("seer.test", RecordType::NS, Some("127.0.0.1"))
3519 .await;
3520 assert!(
3521 matches!(&result, Ok(records) if records.is_empty()),
3522 "NXDOMAIN must fold to Ok(vec![]), got: {result:?}"
3523 );
3524 assert_eq!(classify_ns_presence(&result), DnsPresence::Absent);
3525 }
3526
3527 #[tokio::test]
3528 async fn mock_timeout_errors_and_classifies_unknown() {
3529 let port = spawn_mock_dns(MockMode::Ignore).await;
3530 let resolver = DnsResolver::new()
3532 .with_timeout(Duration::from_millis(200))
3533 .allowing_private_hosts()
3534 .with_port(port);
3535 let result = resolver
3536 .resolve("seer.test", RecordType::NS, Some("127.0.0.1"))
3537 .await;
3538 match &result {
3539 Err(SeerError::DnsError(_)) => {}
3540 other => panic!("unanswered query must surface a DnsError, got: {other:?}"),
3541 }
3542 assert_eq!(classify_ns_presence(&result), DnsPresence::Unknown);
3543 }
3544
3545 #[tokio::test]
3546 async fn resolve_type_rejects_composite_types_consistently() {
3547 let r = DnsResolver::new();
3552 for composite in [RecordType::SRV, RecordType::ANY] {
3553 let err = r
3554 .resolve_type(&r.default_resolver, "seer.test", composite)
3555 .await
3556 .expect_err("composite types must be rejected by resolve_type");
3557 assert_eq!(
3558 err.to_string(),
3559 unsupported_record_type(composite).to_string()
3560 );
3561 }
3562 }
3563}