1use std::collections::BTreeSet;
59use std::net::IpAddr;
60use std::time::Duration;
61
62use hickory_resolver::config::ResolverOpts;
63use hickory_resolver::net::runtime::TokioRuntimeProvider;
64use hickory_resolver::net::{DnsError, NetError};
65use hickory_resolver::proto::op::ResponseCode;
66use hickory_resolver::proto::rr::{RData as HickoryRData, Record, RecordType as HickoryRecordType};
67use hickory_resolver::TokioResolver;
68use serde::{Deserialize, Serialize};
69use tracing::{debug, instrument};
70
71use super::resolver::{
72 apply_standard_opts, build_plain_resolver, fqdn, google_or_pinned, single_server_config,
73};
74use super::DEFAULT_DNS_TIMEOUT;
75use crate::error::{Result, SeerError};
76use crate::validation::normalize_domain;
77
78const CHECK_BUDGET_TIMEOUTS: u32 = 2 + 3 * PARENT_SERVERS_WANTED as u32 + 3;
87
88const DELEGATED_NS_BUDGET_TIMEOUTS: u32 = 2;
93
94const MAX_NS_PROBED: usize = 6;
98
99const MAX_PARENT_SERVERS_TRIED: usize = 4;
101
102const PARENT_SERVERS_WANTED: usize = 2;
104
105#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
109#[serde(rename_all = "kebab-case")]
110pub enum LameKind {
111 Nxdomain,
113 NoNsRecords,
115 NotAuthoritative,
117 Referral,
119 EmptyAnswer,
121 ErrorRcode,
123 Unresponsive,
126}
127
128#[derive(Debug, Clone, Serialize, Deserialize)]
131pub struct LameNs {
132 pub host: String,
134 pub kind: LameKind,
136 pub reason: String,
138}
139
140#[derive(Debug, Clone, Serialize, Deserialize)]
145pub struct DelegationReport {
146 pub domain: String,
148 pub parent_zone: String,
151 pub parent_server_queried: Vec<String>,
153 pub delegated_ns: Vec<String>,
155 pub zone_ns: Vec<String>,
158 pub in_sync: bool,
161 pub missing_from_zone: Vec<String>,
163 pub missing_from_parent: Vec<String>,
165 pub lame: Vec<LameNs>,
167 pub warnings: Vec<String>,
170}
171
172#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
181pub struct DnsNameservers {
182 pub delegated: Vec<String>,
184 pub in_sync: bool,
186 pub missing_from_whois: Vec<String>,
188 pub missing_from_dns: Vec<String>,
190}
191
192impl DnsNameservers {
193 pub fn compare(whois: &[String], delegated: Vec<String>) -> Self {
196 let norm = |n: &String| n.trim_end_matches('.').to_ascii_lowercase();
197 let whois: BTreeSet<String> = whois.iter().map(norm).collect();
198 let delegated: BTreeSet<String> = delegated.iter().map(norm).collect();
199 let missing_from_whois: Vec<String> = delegated.difference(&whois).cloned().collect();
200 let missing_from_dns: Vec<String> = whois.difference(&delegated).cloned().collect();
201 Self {
202 in_sync: missing_from_whois.is_empty() && missing_from_dns.is_empty(),
203 delegated: delegated.into_iter().collect(),
204 missing_from_whois,
205 missing_from_dns,
206 }
207 }
208
209 pub fn whois_lists_none(&self) -> bool {
213 !self.delegated.is_empty()
216 && self.missing_from_dns.is_empty()
217 && self.missing_from_whois.len() == self.delegated.len()
218 }
219}
220
221struct ParentDelegation {
224 servers_queried: Vec<String>,
226 delegated: BTreeSet<String>,
228 warnings: Vec<String>,
229}
230
231#[derive(Debug)]
239enum DirectNs {
240 Response {
242 authoritative: bool,
247 response_code: ResponseCode,
248 answer_ns: Vec<String>,
250 authority_ns: Vec<String>,
253 soa_zone: Option<String>,
257 },
258 Rcode(ResponseCode),
260 Unreachable(String),
262 LocalNoRoute(String),
268}
269
270#[derive(Debug)]
272enum ProbeOutcome {
273 Healthy(Vec<String>),
275 Lame(LameKind, String),
277 Skipped(String),
280}
281
282#[derive(Clone)]
288pub struct DelegationChecker {
289 timeout: Duration,
291 recursive: TokioResolver,
295 #[cfg(test)]
297 recursive_upstream: Option<(IpAddr, u16)>,
298 #[cfg(test)]
301 port_map: Option<std::collections::HashMap<String, u16>>,
302 #[cfg(test)]
304 allow_private_hosts: bool,
305 #[cfg(test)]
308 unroutable: Vec<IpAddr>,
309 #[cfg(test)]
312 budget: Option<Duration>,
313}
314
315impl std::fmt::Debug for DelegationChecker {
316 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
317 f.debug_struct("DelegationChecker")
318 .field("timeout", &self.timeout)
319 .finish()
320 }
321}
322
323impl Default for DelegationChecker {
324 fn default() -> Self {
325 Self::new()
326 }
327}
328
329impl DelegationChecker {
330 pub fn new() -> Self {
333 Self {
334 timeout: DEFAULT_DNS_TIMEOUT,
335 recursive: build_recursive_resolver(DEFAULT_DNS_TIMEOUT, None),
336 #[cfg(test)]
337 recursive_upstream: None,
338 #[cfg(test)]
339 port_map: None,
340 #[cfg(test)]
341 allow_private_hosts: false,
342 #[cfg(test)]
343 unroutable: Vec::new(),
344 #[cfg(test)]
345 budget: None,
346 }
347 }
348
349 pub fn from_config(config: &crate::config::SeerConfig) -> Self {
355 Self::new().with_timeout(config.dns_timeout())
356 }
357
358 pub fn with_timeout(mut self, timeout: Duration) -> Self {
361 self.timeout = timeout;
362 self.recursive = build_recursive_resolver(timeout, self.recursive_upstream());
363 self
364 }
365
366 #[cfg(test)]
368 fn with_recursive_upstream(mut self, ip: IpAddr, port: u16) -> Self {
369 self.recursive_upstream = Some((ip, port));
370 self.recursive = build_recursive_resolver(self.timeout, self.recursive_upstream);
371 self
372 }
373
374 #[cfg(test)]
377 fn with_port_map(mut self, map: std::collections::HashMap<String, u16>) -> Self {
378 self.port_map = Some(map);
379 self
380 }
381
382 #[cfg(test)]
384 fn allowing_private_hosts(mut self) -> Self {
385 self.allow_private_hosts = true;
386 self
387 }
388
389 #[cfg(test)]
392 fn with_unroutable(mut self, addrs: &[IpAddr]) -> Self {
393 self.unroutable.extend_from_slice(addrs);
394 self
395 }
396
397 #[cfg(test)]
399 fn with_budget(mut self, budget: Duration) -> Self {
400 self.budget = Some(budget);
401 self
402 }
403
404 async fn parent_delegation(
412 &self,
413 domain: &str,
414 parent_zone: &str,
415 deadline: tokio::time::Instant,
416 budget: Duration,
417 ) -> Result<ParentDelegation> {
418 let mut warnings = Vec::new();
419 let out_of_time = || {
420 SeerError::DnsError(format!(
421 "delegation check for {domain} gave up after {budget:?} without a parent-zone answer"
422 ))
423 };
424
425 let parent_hosts = tokio::time::timeout_at(deadline, self.recursive_ns_set(parent_zone))
427 .await
428 .map_err(|_| out_of_time())??;
429 if parent_hosts.is_empty() {
430 return Err(SeerError::DnsError(format!(
431 "parent zone {} has no NS records — cannot locate the delegation for {}",
432 parent_zone, domain
433 )));
434 }
435
436 let mut parent_server_queried = Vec::new();
440 let mut delegated: BTreeSet<String> = BTreeSet::new();
441 for host in parent_hosts.iter().take(MAX_PARENT_SERVERS_TRIED) {
442 if parent_server_queried.len() >= PARENT_SERVERS_WANTED {
443 break;
444 }
445 let Ok(asked) = tokio::time::timeout_at(deadline, self.ask_host(host, domain)).await
446 else {
447 warnings.push(format!(
448 "stopped asking parent servers: the check's {budget:?} budget ran out"
449 ));
450 break;
451 };
452 let direct = match asked {
453 Ok(direct) => direct,
454 Err(reason) => {
455 warnings.push(format!("skipped parent server {}: {}", host, reason));
456 continue;
457 }
458 };
459 match parent_answer(direct, domain) {
463 ParentAnswer::Delegation(ns) => {
464 delegated.extend(ns);
465 parent_server_queried.push(host.clone());
466 }
467 ParentAnswer::Negative(code) => {
468 if code == ResponseCode::NXDomain {
469 warnings.push(format!(
470 "parent server {} returned NXDOMAIN for {}",
471 host, domain
472 ));
473 }
474 parent_server_queried.push(host.clone());
475 }
476 ParentAnswer::Lame(reason) => {
477 warnings.push(format!("parent server {host} {reason}"));
478 }
479 }
480 }
481
482 if parent_server_queried.is_empty() {
483 return Err(SeerError::DnsError(format!(
484 "no parent-zone ({}) nameserver answered the delegation query for {}",
485 parent_zone, domain
486 )));
487 }
488 if delegated.is_empty() {
489 warnings.push(format!(
490 "parent zone {} returned no NS delegation for {} — the domain may not be delegated",
491 parent_zone, domain
492 ));
493 }
494
495 Ok(ParentDelegation {
496 servers_queried: parent_server_queried,
497 delegated,
498 warnings,
499 })
500 }
501
502 #[instrument(skip(self), fields(domain = %domain))]
508 pub async fn delegated_ns(&self, domain: &str) -> Result<Vec<String>> {
509 let domain = normalize_domain(domain)?;
510 let parent_zone = parent_zone_of(&domain);
511 let budget = self.timeout.saturating_mul(DELEGATED_NS_BUDGET_TIMEOUTS);
512 let deadline = tokio::time::Instant::now() + budget;
513 let delegation = self
514 .parent_delegation(&domain, &parent_zone, deadline, budget)
515 .await?;
516 Ok(delegation.delegated.into_iter().collect())
517 }
518
519 #[cfg(not(test))]
520 fn budget(&self) -> Duration {
521 self.timeout.saturating_mul(CHECK_BUDGET_TIMEOUTS)
522 }
523
524 #[cfg(test)]
525 fn budget(&self) -> Duration {
526 self.budget
527 .unwrap_or_else(|| self.timeout.saturating_mul(CHECK_BUDGET_TIMEOUTS))
528 }
529
530 #[cfg(test)]
531 fn simulated_no_route(&self, ip: IpAddr) -> bool {
532 self.unroutable.contains(&ip)
533 }
534
535 #[cfg(not(test))]
536 fn simulated_no_route(&self, _ip: IpAddr) -> bool {
537 false
538 }
539
540 #[cfg(test)]
541 fn recursive_upstream(&self) -> Option<(IpAddr, u16)> {
542 self.recursive_upstream
543 }
544
545 #[cfg(not(test))]
546 fn recursive_upstream(&self) -> Option<(IpAddr, u16)> {
547 None
548 }
549
550 #[cfg(test)]
551 fn allow_private(&self) -> bool {
552 self.allow_private_hosts
553 }
554
555 #[cfg(not(test))]
556 fn allow_private(&self) -> bool {
557 false
558 }
559
560 #[cfg(test)]
564 fn direct_port(&self, host: &str) -> u16 {
565 self.port_map
566 .as_ref()
567 .and_then(|map| map.get(&normalize_ns_name(host)).copied())
568 .unwrap_or(53)
569 }
570
571 #[cfg(not(test))]
572 fn direct_port(&self, _host: &str) -> u16 {
573 53
574 }
575
576 #[instrument(skip(self), fields(domain = %domain))]
587 pub async fn check(&self, domain: &str) -> Result<DelegationReport> {
588 let domain = normalize_domain(domain)?;
589 let parent_zone = parent_zone_of(&domain);
590 let budget = self.budget();
591 let deadline = tokio::time::Instant::now() + budget;
592
593 let ParentDelegation {
595 servers_queried: parent_server_queried,
596 delegated,
597 mut warnings,
598 } = self
599 .parent_delegation(&domain, &parent_zone, deadline, budget)
600 .await?;
601
602 let probe_hosts: Vec<String> = delegated.iter().take(MAX_NS_PROBED).cloned().collect();
604 if delegated.len() > MAX_NS_PROBED {
605 warnings.push(format!(
606 "probing only {} of {} delegated nameservers",
607 MAX_NS_PROBED,
608 delegated.len()
609 ));
610 }
611 let outcomes = futures::future::join_all(probe_hosts.iter().map(|host| async {
614 tokio::time::timeout_at(deadline, self.probe_delegated_ns(host, &domain))
615 .await
616 .unwrap_or_else(|_| {
617 ProbeOutcome::Skipped(format!(
618 "not probed within the check's {budget:?} budget"
619 ))
620 })
621 }))
622 .await;
623
624 let mut zone_ns: BTreeSet<String> = BTreeSet::new();
625 let mut lame = Vec::new();
626 for (host, outcome) in probe_hosts.iter().zip(outcomes) {
627 match outcome {
628 ProbeOutcome::Healthy(ns) => zone_ns.extend(ns),
629 ProbeOutcome::Lame(kind, reason) => lame.push(LameNs {
630 host: host.clone(),
631 kind,
632 reason,
633 }),
634 ProbeOutcome::Skipped(reason) => {
635 warnings.push(format!("could not probe {}: {}", host, reason));
636 }
637 }
638 }
639
640 let (missing_from_zone, missing_from_parent) = if zone_ns.is_empty() {
644 if !delegated.is_empty() {
645 warnings.push(
646 "no delegated nameserver returned an authoritative NS answer — \
647 parent/zone set comparison skipped"
648 .to_string(),
649 );
650 }
651 (Vec::new(), Vec::new())
652 } else {
653 (
654 delegated.difference(&zone_ns).cloned().collect(),
655 zone_ns.difference(&delegated).cloned().collect(),
656 )
657 };
658
659 let in_sync = !delegated.is_empty()
660 && !zone_ns.is_empty()
661 && missing_from_zone.is_empty()
662 && missing_from_parent.is_empty()
663 && lame.is_empty();
664
665 lame.sort_by(|a, b| a.host.cmp(&b.host));
666
667 Ok(DelegationReport {
668 domain,
669 parent_zone,
670 parent_server_queried,
671 delegated_ns: delegated.into_iter().collect(),
672 zone_ns: zone_ns.into_iter().collect(),
673 in_sync,
674 missing_from_zone,
675 missing_from_parent,
676 lame,
677 warnings,
678 })
679 }
680
681 async fn recursive_ns_set(&self, zone: &str) -> Result<Vec<String>> {
685 match self
686 .recursive
687 .lookup(fqdn(zone), HickoryRecordType::NS)
688 .await
689 {
690 Ok(lookup) => {
691 let set: BTreeSet<String> =
692 ns_targets_for(lookup.answers(), zone).into_iter().collect();
693 Ok(set.into_iter().collect())
694 }
695 Err(e) if e.is_no_records_found() => Ok(Vec::new()),
696 Err(e) => Err(SeerError::DnsError(format!(
697 "NS lookup for parent zone {} failed: {}",
698 zone, e
699 ))),
700 }
701 }
702
703 async fn resolve_host_ips(&self, host: &str) -> std::result::Result<Vec<IpAddr>, String> {
707 let response = tokio::time::timeout(self.timeout, self.recursive.lookup_ip(fqdn(host)))
710 .await
711 .unwrap_or(Err(NetError::Timeout))
712 .map_err(|e| format!("could not resolve {}: {}", host, e))?;
713 let ips: Vec<IpAddr> = response.iter().collect();
714 if ips.is_empty() {
715 return Err(format!("{} did not resolve to any address", host));
716 }
717 let (vetted, refused) = partition_reserved(&ips, self.allow_private());
721 if vetted.is_empty() {
722 return Err(match refused.first() {
723 Some((_, reason)) => format!("{} resolves to a blocked address ({})", host, reason),
724 None => format!("{} did not resolve to a usable address", host),
725 });
726 }
727 let mut vetted = vetted;
728 crate::net::ipv4_first(&mut vetted, |ip| *ip);
729 Ok(vetted)
730 }
731
732 async fn ask_host(&self, host: &str, domain: &str) -> std::result::Result<DirectNs, String> {
739 let mut last = None;
740 for ip in self.resolve_host_ips(host).await? {
741 debug!(server = %host, %ip, "direct NS query");
742 match self.direct_ns_query(host, ip, domain).await {
743 DirectNs::LocalNoRoute(reason) => {
744 last = Some(DirectNs::LocalNoRoute(format!("{ip}: {reason}")));
745 }
746 direct => return Ok(direct),
747 }
748 }
749 Ok(last.unwrap_or_else(|| DirectNs::Unreachable("no address to query".to_string())))
751 }
752
753 async fn direct_ns_query(&self, host: &str, ip: IpAddr, domain: &str) -> DirectNs {
758 if self.simulated_no_route(ip) {
759 return DirectNs::LocalNoRoute("network is unreachable (simulated)".to_string());
760 }
761 let resolver = match self.build_direct_resolver(host, ip) {
762 Ok(resolver) => resolver,
763 Err(e) => return DirectNs::Unreachable(format!("resolver construction failed: {}", e)),
764 };
765 match resolver.lookup(fqdn(domain), HickoryRecordType::NS).await {
766 Ok(lookup) => {
770 let message = lookup.message();
771 DirectNs::Response {
772 authoritative: message.metadata.authoritative,
773 response_code: message.metadata.response_code,
774 answer_ns: ns_targets_for(&message.answers, domain),
775 authority_ns: ns_targets_for(&message.authorities, domain),
776 soa_zone: soa_zone_in(&message.authorities),
777 }
778 }
779 Err(e) => classify_net_error(e, domain),
780 }
781 }
782
783 fn build_direct_resolver(&self, host: &str, ip: IpAddr) -> Result<TokioResolver> {
788 let config = single_server_config(ip, self.direct_port(host), true);
789 let mut builder =
790 TokioResolver::builder_with_config(config, TokioRuntimeProvider::default());
791 apply_direct_opts(builder.options_mut(), self.timeout);
792 builder
793 .build()
794 .map_err(|e| SeerError::DnsError(format!("failed to construct DNS resolver: {}", e)))
795 }
796
797 async fn probe_delegated_ns(&self, host: &str, domain: &str) -> ProbeOutcome {
800 match self.ask_host(host, domain).await {
801 Ok(direct) => outcome_for_direct(direct),
802 Err(reason) => ProbeOutcome::Skipped(reason),
805 }
806 }
807}
808
809fn outcome_for_direct(direct: DirectNs) -> ProbeOutcome {
812 match direct {
813 DirectNs::Response {
814 response_code: ResponseCode::NXDomain,
815 ..
816 } => ProbeOutcome::Lame(
817 LameKind::Nxdomain,
818 "returned NXDOMAIN for its own zone".to_string(),
819 ),
820 DirectNs::Response {
821 authoritative: true,
822 answer_ns,
823 ..
824 } if !answer_ns.is_empty() => ProbeOutcome::Healthy(answer_ns),
825 DirectNs::Response {
826 authoritative: true,
827 ..
828 } => ProbeOutcome::Lame(
829 LameKind::NoNsRecords,
830 "authoritative answer contained no NS records".to_string(),
831 ),
832 DirectNs::Response { answer_ns, .. } if !answer_ns.is_empty() => ProbeOutcome::Lame(
833 LameKind::NotAuthoritative,
834 "answered non-authoritatively (AA bit not set)".to_string(),
835 ),
836 DirectNs::Response { authority_ns, .. } if !authority_ns.is_empty() => ProbeOutcome::Lame(
837 LameKind::Referral,
838 "not authoritative — referred the query elsewhere".to_string(),
839 ),
840 DirectNs::Response { .. } => ProbeOutcome::Lame(
841 LameKind::EmptyAnswer,
842 "returned an empty answer (no NS records)".to_string(),
843 ),
844 DirectNs::Rcode(code) => ProbeOutcome::Lame(
845 LameKind::ErrorRcode,
846 format!("refused the query ({})", code),
847 ),
848 DirectNs::Unreachable(reason) => ProbeOutcome::Lame(
849 LameKind::Unresponsive,
850 format!("no usable response: {}", reason),
851 ),
852 DirectNs::LocalNoRoute(reason) => ProbeOutcome::Skipped(format!(
853 "no route from this host ({}) — server not probed (e.g. an IPv6-only nameserver on an IPv4-only host)",
854 reason
855 )),
856 }
857}
858
859fn apply_direct_opts(opts: &mut ResolverOpts, timeout: Duration) {
865 apply_standard_opts(opts, timeout);
866 opts.recursion_desired = false;
867}
868
869pub(crate) fn build_recursive_resolver(
876 timeout: Duration,
877 upstream: Option<(IpAddr, u16)>,
878) -> TokioResolver {
879 build_plain_resolver(google_or_pinned(upstream), timeout)
880}
881
882fn classify_net_error(err: NetError, domain: &str) -> DirectNs {
887 match err {
888 NetError::Dns(DnsError::NoRecordsFound(no_records)) => {
889 let authorities = no_records.authorities.as_deref().unwrap_or_default();
890 DirectNs::Response {
891 authoritative: false,
892 response_code: no_records.response_code,
893 answer_ns: Vec::new(),
894 authority_ns: ns_targets_for(authorities, domain),
895 soa_zone: no_records
896 .soa
897 .as_ref()
898 .map(|soa| normalize_ns_name(&soa.name.to_ascii()))
899 .or_else(|| soa_zone_in(authorities)),
900 }
901 }
902 NetError::Dns(DnsError::ResponseCode(code)) => DirectNs::Rcode(code),
903 NetError::Timeout => DirectNs::Unreachable("timed out".to_string()),
904 NetError::NoConnections => DirectNs::LocalNoRoute(
916 "every connection attempt failed at the local socket layer".to_string(),
917 ),
918 NetError::Io(io_err) if is_local_no_route(&io_err) => {
921 DirectNs::LocalNoRoute(io_err.to_string())
922 }
923 other => DirectNs::Unreachable(other.to_string()),
924 }
925}
926
927pub(crate) fn is_local_no_route(err: &std::io::Error) -> bool {
934 use std::io::ErrorKind::{HostUnreachable, NetworkUnreachable};
935 matches!(err.kind(), NetworkUnreachable | HostUnreachable)
936 || err
937 .raw_os_error()
938 .is_some_and(|code| Some(code) == ADDRESS_FAMILY_UNSUPPORTED)
939}
940
941#[cfg(unix)]
949pub(crate) const ADDRESS_FAMILY_UNSUPPORTED: Option<i32> = Some(libc::EAFNOSUPPORT);
950#[cfg(windows)]
951pub(crate) const ADDRESS_FAMILY_UNSUPPORTED: Option<i32> =
952 Some(windows_sys::Win32::Networking::WinSock::WSAEAFNOSUPPORT);
953#[cfg(not(any(unix, windows)))]
954pub(crate) const ADDRESS_FAMILY_UNSUPPORTED: Option<i32> = None;
955
956fn parent_zone_of(domain: &str) -> String {
960 domain
961 .split_once('.')
962 .map(|(_, rest)| rest.to_string())
963 .unwrap_or_else(|| domain.to_string())
964}
965
966pub(crate) fn partition_reserved(
972 ips: &[IpAddr],
973 allow_private: bool,
974) -> (Vec<IpAddr>, Vec<(IpAddr, &'static str)>) {
975 let mut usable = Vec::new();
976 let mut refused = Vec::new();
977 for &ip in ips {
978 match crate::validation::describe_reserved_ip(&ip) {
979 Some(reason) if !allow_private => refused.push((ip, reason)),
980 _ => usable.push(ip),
981 }
982 }
983 (usable, refused)
984}
985
986fn normalize_ns_name(name: &str) -> String {
989 name.trim_end_matches('.').to_ascii_lowercase()
990}
991
992fn ns_targets_for(records: &[Record], owner: &str) -> Vec<String> {
1003 let owner = normalize_ns_name(owner);
1004 records
1005 .iter()
1006 .filter_map(|record| {
1007 if normalize_ns_name(&record.name.to_ascii()) != owner {
1008 return None;
1009 }
1010 match &record.data {
1011 HickoryRData::NS(ns) => Some(normalize_ns_name(&ns.0.to_ascii())),
1012 _ => None,
1013 }
1014 })
1015 .collect()
1016}
1017
1018fn soa_zone_in(records: &[Record]) -> Option<String> {
1021 records
1022 .iter()
1023 .find(|record| matches!(record.data, HickoryRData::SOA(_)))
1024 .map(|record| normalize_ns_name(&record.name.to_ascii()))
1025}
1026
1027#[derive(Debug, PartialEq)]
1029enum ParentAnswer {
1030 Delegation(Vec<String>),
1032 Negative(ResponseCode),
1035 Lame(String),
1039}
1040
1041fn parent_answer(direct: DirectNs, domain: &str) -> ParentAnswer {
1048 match direct {
1049 DirectNs::Response {
1050 answer_ns,
1051 authority_ns,
1052 ..
1053 } if !answer_ns.is_empty() || !authority_ns.is_empty() => {
1054 ParentAnswer::Delegation(answer_ns.into_iter().chain(authority_ns).collect())
1055 }
1056 DirectNs::Response {
1057 response_code,
1058 soa_zone: Some(zone),
1059 ..
1060 } if zone_contains(&zone, domain) => ParentAnswer::Negative(response_code),
1061 DirectNs::Response { response_code, .. } => ParentAnswer::Lame(format!(
1062 "gave no delegation for {domain} ({response_code} without the zone's SOA)"
1063 )),
1064 DirectNs::Rcode(code) => ParentAnswer::Lame(format!("answered with {code}")),
1065 DirectNs::Unreachable(reason) => ParentAnswer::Lame(format!("did not respond: {reason}")),
1066 DirectNs::LocalNoRoute(reason) => {
1067 ParentAnswer::Lame(format!("not probed — no route from this host ({reason})"))
1068 }
1069 }
1070}
1071
1072fn zone_contains(zone: &str, name: &str) -> bool {
1074 zone.is_empty()
1075 || name == zone
1076 || name
1077 .strip_suffix(zone)
1078 .is_some_and(|rest| rest.ends_with('.'))
1079}
1080
1081#[cfg(test)]
1082mod tests {
1083 use std::collections::HashMap;
1093 use std::net::Ipv4Addr;
1094
1095 use hickory_resolver::proto::rr::rdata as wire;
1096 use hickory_resolver::proto::rr::Name;
1097
1098 use super::*;
1099 use crate::dns::test_support::{spawn_mock_dns, spawn_mock_dns_fn, MockMode, MockReply};
1100
1101 #[test]
1104 fn parent_zone_strips_leftmost_label() {
1105 assert_eq!(parent_zone_of("example.com"), "com");
1106 assert_eq!(parent_zone_of("sub.example.co.uk"), "example.co.uk");
1107 assert_eq!(parent_zone_of("example.co.uk"), "co.uk");
1108 }
1109
1110 #[test]
1111 fn direct_opts_share_the_standard_set_and_disable_recursion() {
1112 let mut opts = ResolverOpts::default();
1115 apply_direct_opts(&mut opts, Duration::from_secs(7));
1116 assert_eq!(
1117 opts.server_ordering_strategy,
1118 hickory_resolver::config::ServerOrderingStrategy::UserProvidedOrder
1119 );
1120 assert_eq!(opts.timeout, Duration::from_secs(7));
1121 assert!(!opts.recursion_desired);
1122 }
1123
1124 #[test]
1125 fn ns_targets_match_idn_owners_in_punycode() {
1126 let owner = Name::from_ascii("xn--mnchen-3ya.de.").unwrap();
1130 let records = vec![
1131 Record::from_rdata(
1132 owner.clone(),
1133 300,
1134 HickoryRData::NS(wire::NS(
1135 Name::from_ascii("ns1.xn--mnchen-3ya.de.").unwrap(),
1136 )),
1137 ),
1138 Record::from_rdata(
1139 owner,
1140 300,
1141 HickoryRData::NS(wire::NS(Name::from_ascii("NS2.Example.NET.").unwrap())),
1142 ),
1143 ];
1144 let mut targets = ns_targets_for(&records, "xn--mnchen-3ya.de");
1145 targets.sort();
1146 assert_eq!(
1147 targets,
1148 vec![
1149 "ns1.xn--mnchen-3ya.de".to_string(),
1150 "ns2.example.net".to_string()
1151 ]
1152 );
1153 let tld = vec![Record::from_rdata(
1156 Name::from_ascii("xn--p1ai.").unwrap(),
1157 300,
1158 HickoryRData::NS(wire::NS(Name::from_ascii("a.dns.ripn.net.").unwrap())),
1159 )];
1160 assert_eq!(
1161 ns_targets_for(&tld, "xn--p1ai"),
1162 vec!["a.dns.ripn.net".to_string()]
1163 );
1164 }
1165
1166 #[test]
1167 fn ns_name_normalization_is_case_and_dot_insensitive() {
1168 assert_eq!(normalize_ns_name("NS1.Example.COM."), "ns1.example.com");
1169 assert_eq!(normalize_ns_name("ns1.example.com"), "ns1.example.com");
1170 }
1171
1172 #[test]
1173 fn partition_reserved_refuses_reserved_addresses_with_their_reason() {
1174 let public: IpAddr = "192.5.6.30".parse().unwrap();
1175 let private: IpAddr = "10.0.0.53".parse().unwrap();
1176 let loopback: IpAddr = "::1".parse().unwrap();
1177 let (usable, refused) = partition_reserved(&[private, public, loopback], false);
1178 assert_eq!(usable, vec![public]);
1179 assert_eq!(
1180 refused,
1181 vec![
1182 (private, "private network (RFC 1918)"),
1183 (loopback, "IPv6 loopback (::1)")
1184 ]
1185 );
1186 let (usable, refused) = partition_reserved(&[private, public], true);
1188 assert_eq!(usable, vec![private, public]);
1189 assert!(refused.is_empty());
1190 }
1191
1192 #[test]
1193 fn local_no_route_transport_errors_classify_as_local() {
1194 use std::io;
1195 for kind in [
1200 io::ErrorKind::NetworkUnreachable,
1201 io::ErrorKind::HostUnreachable,
1202 ] {
1203 let direct = classify_net_error(io::Error::new(kind, "no route").into(), "example.com");
1204 assert!(
1205 matches!(direct, DirectNs::LocalNoRoute(_)),
1206 "{kind:?} classified as {direct:?}"
1207 );
1208 }
1209 let pooled = classify_net_error(NetError::NoConnections, "example.com");
1215 assert!(
1216 matches!(pooled, DirectNs::LocalNoRoute(_)),
1217 "NoConnections classified as {pooled:?}"
1218 );
1219 let refused = classify_net_error(
1222 io::Error::new(io::ErrorKind::ConnectionRefused, "refused").into(),
1223 "example.com",
1224 );
1225 assert!(
1226 matches!(refused, DirectNs::Unreachable(_)),
1227 "refused classified as {refused:?}"
1228 );
1229 }
1230
1231 #[test]
1232 fn address_family_unsupported_classifies_as_local() {
1233 use std::io;
1234 let code = ADDRESS_FAMILY_UNSUPPORTED.expect("a Unix or Windows target");
1240 let err = io::Error::from_raw_os_error(code);
1241 assert!(is_local_no_route(&err), "{err:?} ({:?})", err.kind());
1242 let direct = classify_net_error(err.into(), "example.com");
1243 assert!(
1244 matches!(direct, DirectNs::LocalNoRoute(_)),
1245 "EAFNOSUPPORT classified as {direct:?}"
1246 );
1247 for other in [
1250 io::Error::other("Address family not supported by protocol"),
1251 io::Error::from(io::ErrorKind::Unsupported),
1252 ] {
1253 assert!(!is_local_no_route(&other), "{other:?}");
1254 }
1255 }
1256
1257 #[test]
1260 fn each_lame_outcome_has_its_kind() {
1261 let response =
1262 |authoritative, code, answer: &[&str], authority: &[&str]| DirectNs::Response {
1263 authoritative,
1264 response_code: code,
1265 answer_ns: answer.iter().map(|s| s.to_string()).collect(),
1266 authority_ns: authority.iter().map(|s| s.to_string()).collect(),
1267 soa_zone: None,
1268 };
1269 let kind = |direct| match outcome_for_direct(direct) {
1270 ProbeOutcome::Lame(kind, _) => kind,
1271 other => panic!("expected lame, got {other:?}"),
1272 };
1273 assert_eq!(
1274 kind(response(true, ResponseCode::NXDomain, &[], &[])),
1275 LameKind::Nxdomain
1276 );
1277 assert_eq!(
1278 kind(response(true, ResponseCode::NoError, &[], &[])),
1279 LameKind::NoNsRecords
1280 );
1281 assert_eq!(
1282 kind(response(false, ResponseCode::NoError, &["ns1.test"], &[])),
1283 LameKind::NotAuthoritative
1284 );
1285 assert_eq!(
1286 kind(response(
1287 false,
1288 ResponseCode::NoError,
1289 &[],
1290 &["ns.parent.test"]
1291 )),
1292 LameKind::Referral
1293 );
1294 assert_eq!(
1295 kind(response(false, ResponseCode::NoError, &[], &[])),
1296 LameKind::EmptyAnswer
1297 );
1298 assert_eq!(
1299 kind(DirectNs::Rcode(ResponseCode::Refused)),
1300 LameKind::ErrorRcode
1301 );
1302 assert_eq!(
1303 kind(DirectNs::Unreachable("timed out".to_string())),
1304 LameKind::Unresponsive
1305 );
1306 }
1307
1308 #[test]
1309 fn local_no_route_probe_outcome_is_skipped_not_lame() {
1310 let outcome =
1311 outcome_for_direct(DirectNs::LocalNoRoute("Network is unreachable".to_string()));
1312 assert!(
1313 matches!(outcome, ProbeOutcome::Skipped(_)),
1314 "got {outcome:?}"
1315 );
1316 }
1317
1318 #[test]
1319 fn from_config_applies_dns_timeout() {
1320 let mut config = crate::config::SeerConfig::default();
1321 config.timeouts.dns_secs = 9;
1322 let checker = DelegationChecker::from_config(&config);
1323 assert_eq!(checker.timeout, Duration::from_secs(9));
1324 }
1325
1326 fn ns_rdata(target: &str) -> HickoryRData {
1327 HickoryRData::NS(wire::NS(
1328 Name::from_ascii(target).expect("valid test NS name"),
1329 ))
1330 }
1331
1332 fn loopback_a() -> HickoryRData {
1333 HickoryRData::A(wire::A(Ipv4Addr::LOCALHOST))
1334 }
1335
1336 async fn spawn_recursive_mock(resolvable: &[&str]) -> u16 {
1340 let resolvable: Vec<String> = resolvable.iter().map(|s| s.to_string()).collect();
1341 spawn_mock_dns_fn(move |qname, qtype| {
1342 match (qname, qtype) {
1343 ("test", HickoryRecordType::NS) => {
1344 MockReply::Answer(vec![ns_rdata("a.parent.test.")])
1345 }
1346 (host, HickoryRecordType::A)
1347 if host == "a.parent.test" || resolvable.iter().any(|r| r == host) =>
1348 {
1349 MockReply::Answer(vec![loopback_a()])
1350 }
1351 _ => MockReply::NoData,
1353 }
1354 })
1355 .await
1356 }
1357
1358 async fn spawn_zone_server(rrset: &[&str]) -> u16 {
1360 let records: Vec<HickoryRData> = rrset.iter().map(|ns| ns_rdata(ns)).collect();
1361 spawn_mock_dns_fn(move |_qname, qtype| {
1362 if qtype == HickoryRecordType::NS {
1363 MockReply::AuthoritativeAnswer(records.clone())
1364 } else {
1365 MockReply::NoData
1366 }
1367 })
1368 .await
1369 }
1370
1371 async fn spawn_parent_server(referral: &[&str]) -> u16 {
1373 let servers: Vec<(String, Vec<IpAddr>)> = referral
1374 .iter()
1375 .map(|ns| (ns.to_string(), Vec::new()))
1376 .collect();
1377 spawn_mock_dns_fn(move |qname, qtype| {
1378 if qname == "seer.test" && qtype == HickoryRecordType::NS {
1379 MockReply::Delegation {
1380 zone: "seer.test".to_string(),
1381 servers: servers.clone(),
1382 }
1383 } else {
1384 MockReply::NoData
1385 }
1386 })
1387 .await
1388 }
1389
1390 fn checker(recursive_port: u16, port_map: HashMap<String, u16>) -> DelegationChecker {
1391 DelegationChecker::new()
1392 .with_timeout(Duration::from_millis(500))
1393 .allowing_private_hosts()
1394 .with_recursive_upstream(Ipv4Addr::LOCALHOST.into(), recursive_port)
1395 .with_port_map(port_map)
1396 }
1397
1398 #[tokio::test]
1401 async fn healthy_in_sync_delegation() {
1402 let recursive = spawn_recursive_mock(&["ns1.seer.test", "ns2.seer.test"]).await;
1403 let parent = spawn_parent_server(&["ns1.seer.test.", "ns2.seer.test."]).await;
1404 let ns1 = spawn_zone_server(&["ns1.seer.test.", "ns2.seer.test."]).await;
1405 let ns2 = spawn_zone_server(&["ns1.seer.test.", "ns2.seer.test."]).await;
1406
1407 let report = checker(
1408 recursive,
1409 HashMap::from([
1410 ("a.parent.test".to_string(), parent),
1411 ("ns1.seer.test".to_string(), ns1),
1412 ("ns2.seer.test".to_string(), ns2),
1413 ]),
1414 )
1415 .check("seer.test")
1416 .await
1417 .expect("healthy delegation check must succeed");
1418
1419 assert_eq!(report.domain, "seer.test");
1420 assert_eq!(report.parent_zone, "test");
1421 assert_eq!(report.parent_server_queried, vec!["a.parent.test"]);
1422 assert_eq!(report.delegated_ns, vec!["ns1.seer.test", "ns2.seer.test"]);
1423 assert_eq!(report.zone_ns, vec!["ns1.seer.test", "ns2.seer.test"]);
1424 assert!(report.in_sync, "report should be in sync: {report:?}");
1425 assert!(report.missing_from_zone.is_empty());
1426 assert!(report.missing_from_parent.is_empty());
1427 assert!(report.lame.is_empty());
1428 }
1429
1430 #[tokio::test]
1431 async fn parent_lists_ns_the_zone_omits() {
1432 let recursive = spawn_recursive_mock(&["ns1.seer.test", "ns3.seer.test"]).await;
1435 let parent = spawn_parent_server(&["ns1.seer.test.", "ns3.seer.test."]).await;
1436 let zone = spawn_zone_server(&["ns1.seer.test."]).await;
1437 let zone2 = spawn_zone_server(&["ns1.seer.test."]).await;
1438
1439 let report = checker(
1440 recursive,
1441 HashMap::from([
1442 ("a.parent.test".to_string(), parent),
1443 ("ns1.seer.test".to_string(), zone),
1444 ("ns3.seer.test".to_string(), zone2),
1445 ]),
1446 )
1447 .check("seer.test")
1448 .await
1449 .expect("check must succeed");
1450
1451 assert_eq!(report.delegated_ns, vec!["ns1.seer.test", "ns3.seer.test"]);
1452 assert_eq!(report.zone_ns, vec!["ns1.seer.test"]);
1453 assert_eq!(report.missing_from_zone, vec!["ns3.seer.test"]);
1454 assert!(report.missing_from_parent.is_empty());
1455 assert!(!report.in_sync);
1456 assert!(report.lame.is_empty(), "set mismatch is not lameness");
1457 }
1458
1459 #[tokio::test]
1460 async fn zone_lists_ns_the_parent_omits() {
1461 let recursive = spawn_recursive_mock(&["ns1.seer.test"]).await;
1464 let parent = spawn_parent_server(&["ns1.seer.test."]).await;
1465 let zone = spawn_zone_server(&["ns1.seer.test.", "ns2.seer.test."]).await;
1466
1467 let report = checker(
1468 recursive,
1469 HashMap::from([
1470 ("a.parent.test".to_string(), parent),
1471 ("ns1.seer.test".to_string(), zone),
1472 ]),
1473 )
1474 .check("seer.test")
1475 .await
1476 .expect("check must succeed");
1477
1478 assert_eq!(report.delegated_ns, vec!["ns1.seer.test"]);
1479 assert_eq!(report.zone_ns, vec!["ns1.seer.test", "ns2.seer.test"]);
1480 assert_eq!(report.missing_from_parent, vec!["ns2.seer.test"]);
1481 assert!(report.missing_from_zone.is_empty());
1482 assert!(!report.in_sync);
1483 }
1484
1485 #[tokio::test]
1486 async fn refusing_server_is_lame() {
1487 let recursive = spawn_recursive_mock(&["ns1.seer.test", "ns2.seer.test"]).await;
1488 let parent = spawn_parent_server(&["ns1.seer.test.", "ns2.seer.test."]).await;
1489 let ns1 = spawn_zone_server(&["ns1.seer.test.", "ns2.seer.test."]).await;
1490 let ns2 = spawn_mock_dns_fn(|_, _| MockReply::Rcode(ResponseCode::Refused)).await;
1491
1492 let report = checker(
1493 recursive,
1494 HashMap::from([
1495 ("a.parent.test".to_string(), parent),
1496 ("ns1.seer.test".to_string(), ns1),
1497 ("ns2.seer.test".to_string(), ns2),
1498 ]),
1499 )
1500 .check("seer.test")
1501 .await
1502 .expect("check must succeed");
1503
1504 assert!(report.missing_from_zone.is_empty());
1507 assert!(report.missing_from_parent.is_empty());
1508 assert_eq!(report.lame.len(), 1, "lame: {:?}", report.lame);
1509 assert_eq!(report.lame[0].host, "ns2.seer.test");
1510 assert!(
1511 report.lame[0].reason.to_lowercase().contains("refused"),
1512 "reason should mention the refusal: {}",
1513 report.lame[0].reason
1514 );
1515 assert_eq!(report.lame[0].kind, LameKind::ErrorRcode);
1516 assert!(!report.in_sync);
1517 }
1518
1519 #[tokio::test]
1520 async fn non_authoritative_answer_is_lame() {
1521 let recursive = spawn_recursive_mock(&["ns1.seer.test", "ns2.seer.test"]).await;
1522 let parent = spawn_parent_server(&["ns1.seer.test.", "ns2.seer.test."]).await;
1523 let ns1 = spawn_zone_server(&["ns1.seer.test.", "ns2.seer.test."]).await;
1524 let ns2 = spawn_mock_dns_fn(|_, qtype| {
1527 if qtype == HickoryRecordType::NS {
1528 MockReply::Answer(vec![ns_rdata("ns1.seer.test."), ns_rdata("ns2.seer.test.")])
1529 } else {
1530 MockReply::NoData
1531 }
1532 })
1533 .await;
1534
1535 let report = checker(
1536 recursive,
1537 HashMap::from([
1538 ("a.parent.test".to_string(), parent),
1539 ("ns1.seer.test".to_string(), ns1),
1540 ("ns2.seer.test".to_string(), ns2),
1541 ]),
1542 )
1543 .check("seer.test")
1544 .await
1545 .expect("check must succeed");
1546
1547 assert_eq!(report.lame.len(), 1, "lame: {:?}", report.lame);
1548 assert_eq!(report.lame[0].host, "ns2.seer.test");
1549 assert!(
1550 report.lame[0].reason.contains("non-authoritatively"),
1551 "reason: {}",
1552 report.lame[0].reason
1553 );
1554 assert_eq!(report.lame[0].kind, LameKind::NotAuthoritative);
1555 assert!(!report.in_sync);
1556 assert_eq!(report.zone_ns, vec!["ns1.seer.test", "ns2.seer.test"]);
1558 }
1559
1560 #[tokio::test]
1561 async fn unresponsive_server_is_lame_with_timeout_reason() {
1562 let recursive = spawn_recursive_mock(&["ns1.seer.test", "ns2.seer.test"]).await;
1563 let parent = spawn_parent_server(&["ns1.seer.test.", "ns2.seer.test."]).await;
1564 let ns1 = spawn_zone_server(&["ns1.seer.test.", "ns2.seer.test."]).await;
1565 let ns2 = spawn_mock_dns(MockMode::Ignore).await;
1567
1568 let report = checker(
1569 recursive,
1570 HashMap::from([
1571 ("a.parent.test".to_string(), parent),
1572 ("ns1.seer.test".to_string(), ns1),
1573 ("ns2.seer.test".to_string(), ns2),
1574 ]),
1575 )
1576 .check("seer.test")
1577 .await
1578 .expect("check must succeed");
1579
1580 assert_eq!(report.lame.len(), 1, "lame: {:?}", report.lame);
1581 assert_eq!(report.lame[0].host, "ns2.seer.test");
1582 assert!(
1583 report.lame[0].reason.contains("no usable response"),
1584 "reason: {}",
1585 report.lame[0].reason
1586 );
1587 assert_eq!(report.lame[0].kind, LameKind::Unresponsive);
1588 assert!(!report.in_sync);
1589 }
1590
1591 #[tokio::test]
1592 async fn unresolvable_glue_is_warned_and_skipped_not_lame() {
1593 let recursive = spawn_recursive_mock(&["ns1.seer.test"]).await;
1596 let parent = spawn_parent_server(&["ns1.seer.test.", "ns2.seer.test."]).await;
1597 let ns1 = spawn_zone_server(&["ns1.seer.test.", "ns2.seer.test."]).await;
1598
1599 let report = checker(
1600 recursive,
1601 HashMap::from([
1602 ("a.parent.test".to_string(), parent),
1603 ("ns1.seer.test".to_string(), ns1),
1604 ]),
1605 )
1606 .check("seer.test")
1607 .await
1608 .expect("check must succeed");
1609
1610 assert!(report.lame.is_empty(), "skip must not be lame: {report:?}");
1611 assert!(
1612 report.warnings.iter().any(|w| w.contains("ns2.seer.test")),
1613 "warnings should mention the skipped host: {:?}",
1614 report.warnings
1615 );
1616 assert!(report.in_sync, "sets match and nothing is lame: {report:?}");
1617 }
1618
1619 #[tokio::test]
1620 async fn no_responsive_parent_server_is_an_error() {
1621 let recursive = spawn_recursive_mock(&[]).await;
1623 let parent = spawn_mock_dns(MockMode::Ignore).await;
1624
1625 let err = checker(
1626 recursive,
1627 HashMap::from([("a.parent.test".to_string(), parent)]),
1628 )
1629 .check("seer.test")
1630 .await
1631 .expect_err("no parent answer must be an error");
1632 assert!(
1633 matches!(err, SeerError::DnsError(_)),
1634 "expected DnsError, got: {err:?}"
1635 );
1636 assert!(err.to_string().contains("parent"), "got: {err}");
1637 }
1638
1639 #[tokio::test]
1643 async fn unroutable_address_falls_back_to_the_next() {
1644 let unroutable: IpAddr = Ipv4Addr::new(127, 0, 0, 2).into();
1645 let recursive = spawn_mock_dns_fn(move |qname, qtype| match (qname, qtype) {
1646 ("test", HickoryRecordType::NS) => MockReply::Answer(vec![ns_rdata("a.parent.test.")]),
1647 (_, HickoryRecordType::A) => MockReply::Answer(vec![
1648 HickoryRData::A(wire::A(Ipv4Addr::new(127, 0, 0, 2))),
1649 loopback_a(),
1650 ]),
1651 _ => MockReply::NoData,
1652 })
1653 .await;
1654 let parent = spawn_parent_server(&["ns1.seer.test."]).await;
1655 let ns1 = spawn_zone_server(&["ns1.seer.test."]).await;
1656 let ports = HashMap::from([
1657 ("a.parent.test".to_string(), parent),
1658 ("ns1.seer.test".to_string(), ns1),
1659 ]);
1660
1661 let report = checker(recursive, ports.clone())
1662 .with_unroutable(&[unroutable])
1663 .check("seer.test")
1664 .await
1665 .expect("check must succeed");
1666 assert_eq!(report.parent_server_queried, vec!["a.parent.test"]);
1667 assert_eq!(report.zone_ns, vec!["ns1.seer.test"]);
1668 assert!(report.in_sync, "{report:?}");
1669
1670 let err = checker(recursive, ports)
1672 .with_unroutable(&[unroutable, Ipv4Addr::LOCALHOST.into()])
1673 .check("seer.test")
1674 .await
1675 .expect_err("no parent server reachable");
1676 assert!(err.to_string().contains("parent"), "{err}");
1677 }
1678
1679 #[tokio::test]
1684 async fn the_check_is_held_to_one_deadline() {
1685 assert_eq!(CHECK_BUDGET_TIMEOUTS, 11);
1686 let recursive = spawn_recursive_mock(&["ns1.seer.test", "ns2.seer.test"]).await;
1687 let parent = spawn_parent_server(&["ns1.seer.test.", "ns2.seer.test."]).await;
1688 let ns1 = spawn_zone_server(&["ns1.seer.test.", "ns2.seer.test."]).await;
1689 let ns2 = spawn_mock_dns(MockMode::Ignore).await;
1690
1691 let started = std::time::Instant::now();
1692 let report = checker(
1693 recursive,
1694 HashMap::from([
1695 ("a.parent.test".to_string(), parent),
1696 ("ns1.seer.test".to_string(), ns1),
1697 ("ns2.seer.test".to_string(), ns2),
1698 ]),
1699 )
1700 .with_budget(Duration::from_millis(300))
1701 .check("seer.test")
1702 .await
1703 .expect("the parent answered in time");
1704
1705 assert!(started.elapsed() < Duration::from_millis(500));
1707 assert!(report.lame.is_empty(), "{report:?}");
1708 assert!(
1709 report
1710 .warnings
1711 .iter()
1712 .any(|w| w.contains("ns2.seer.test") && w.contains("budget")),
1713 "{:?}",
1714 report.warnings
1715 );
1716 assert_eq!(report.zone_ns, vec!["ns1.seer.test", "ns2.seer.test"]);
1717 }
1718
1719 #[tokio::test]
1722 async fn delegated_ns_reads_the_parent_referral_without_probing() {
1723 let recursive = spawn_recursive_mock(&[]).await;
1725 let parent = spawn_parent_server(&["NS2.seer.test.", "ns1.seer.test."]).await;
1726 let delegated = checker(
1727 recursive,
1728 HashMap::from([("a.parent.test".to_string(), parent)]),
1729 )
1730 .delegated_ns("seer.test")
1731 .await
1732 .expect("parent answers the delegation");
1733 assert_eq!(delegated, vec!["ns1.seer.test", "ns2.seer.test"]);
1734 }
1735
1736 #[tokio::test]
1737 async fn delegated_ns_errors_when_no_parent_server_answers() {
1738 let recursive = spawn_recursive_mock(&[]).await;
1739 let dead = std::net::UdpSocket::bind("127.0.0.1:0").unwrap();
1741 let port = dead.local_addr().unwrap().port();
1742 drop(dead);
1743 let result = checker(
1744 recursive,
1745 HashMap::from([("a.parent.test".to_string(), port)]),
1746 )
1747 .delegated_ns("seer.test")
1748 .await;
1749 assert!(result.is_err(), "{result:?}");
1750 }
1751
1752 #[test]
1753 fn dns_nameservers_compare_normalizes_and_diffs() {
1754 let whois = vec!["NS1.nic.tg.".to_string(), "ns2.nic.tg".to_string()];
1756 let delegated = ["ns1.nic.tg", "ns2.nic.tg", "ns4.nic.tg", "ns5.netmaster.tg"]
1757 .map(String::from)
1758 .to_vec();
1759 let ns = DnsNameservers::compare(&whois, delegated);
1760 assert!(!ns.in_sync);
1761 assert_eq!(
1762 ns.missing_from_whois,
1763 vec!["ns4.nic.tg", "ns5.netmaster.tg"]
1764 );
1765 assert!(ns.missing_from_dns.is_empty());
1766
1767 let same = DnsNameservers::compare(&whois, vec!["ns2.nic.tg".into(), "ns1.nic.tg".into()]);
1768 assert!(same.in_sync);
1769 assert_eq!(same.delegated, vec!["ns1.nic.tg", "ns2.nic.tg"]);
1770 }
1771
1772 async fn spawn_recursive_with_parents(parents: &'static [&'static str]) -> u16 {
1777 spawn_mock_dns_fn(move |qname, qtype| match (qname, qtype) {
1778 ("test", HickoryRecordType::NS) => {
1779 MockReply::Answer(parents.iter().map(|p| ns_rdata(&format!("{p}."))).collect())
1780 }
1781 (_, HickoryRecordType::A) => MockReply::Answer(vec![loopback_a()]),
1782 _ => MockReply::NoData,
1783 })
1784 .await
1785 }
1786
1787 #[tokio::test]
1791 async fn lame_parents_do_not_count_toward_wanted() {
1792 use std::sync::atomic::{AtomicUsize, Ordering};
1793 use std::sync::Arc;
1794
1795 let recursive =
1796 spawn_recursive_with_parents(&["a.parent.test", "b.parent.test", "c.parent.test"])
1797 .await;
1798 let lame_a = spawn_mock_dns_fn(|_, _| MockReply::NoData).await;
1799 let lame_b = spawn_mock_dns_fn(|_, _| MockReply::Delegation {
1801 zone: "elsewhere.test".to_string(),
1802 servers: vec![("ns.elsewhere.test".to_string(), Vec::new())],
1803 })
1804 .await;
1805 let asked = Arc::new(AtomicUsize::new(0));
1806 let seen = asked.clone();
1807 let healthy_c = spawn_mock_dns_fn(move |qname, qtype| {
1808 seen.fetch_add(1, Ordering::SeqCst);
1809 if qname == "seer.test" && qtype == HickoryRecordType::NS {
1810 MockReply::Delegation {
1811 zone: "seer.test".to_string(),
1812 servers: vec![("ns1.seer.test".to_string(), Vec::new())],
1813 }
1814 } else {
1815 MockReply::NoData
1816 }
1817 })
1818 .await;
1819 let checker = checker(
1820 recursive,
1821 HashMap::from([
1822 ("a.parent.test".to_string(), lame_a),
1823 ("b.parent.test".to_string(), lame_b),
1824 ("c.parent.test".to_string(), healthy_c),
1825 ]),
1826 );
1827
1828 let delegated = checker.delegated_ns("seer.test").await.expect("c answers");
1829 assert_eq!(delegated, vec!["ns1.seer.test"]);
1830 assert!(asked.load(Ordering::SeqCst) >= 1);
1831
1832 let ParentDelegation {
1833 servers_queried,
1834 warnings,
1835 ..
1836 } = checker
1837 .parent_delegation(
1838 "seer.test",
1839 "test",
1840 tokio::time::Instant::now() + Duration::from_secs(10),
1841 Duration::from_secs(10),
1842 )
1843 .await
1844 .expect("c answers");
1845 assert_eq!(servers_queried, vec!["c.parent.test"]);
1846 for lame in ["a.parent.test", "b.parent.test"] {
1847 assert!(
1848 warnings
1849 .iter()
1850 .any(|w| w.contains(lame) && w.contains("no delegation")),
1851 "{warnings:?}"
1852 );
1853 }
1854 }
1855
1856 #[tokio::test]
1859 async fn parent_nxdomain_with_its_soa_counts_as_an_answer() {
1860 let recursive = spawn_recursive_mock(&[]).await;
1861 let parent = spawn_mock_dns_fn(|_, _| MockReply::AuthoritativeNxDomain("test")).await;
1862 let report = checker(
1863 recursive,
1864 HashMap::from([("a.parent.test".to_string(), parent)]),
1865 )
1866 .check("seer.test")
1867 .await
1868 .expect("the parent answered");
1869 assert_eq!(report.parent_server_queried, vec!["a.parent.test"]);
1870 assert!(report.delegated_ns.is_empty());
1871 assert!(
1872 report.warnings.iter().any(|w| w.contains("NXDOMAIN")),
1873 "{:?}",
1874 report.warnings
1875 );
1876 }
1877
1878 #[test]
1879 fn parent_answer_needs_a_delegation_or_the_zones_soa() {
1880 let response = |code, authority: &[&str], soa: Option<&str>| DirectNs::Response {
1881 authoritative: false,
1882 response_code: code,
1883 answer_ns: Vec::new(),
1884 authority_ns: authority.iter().map(|s| s.to_string()).collect(),
1885 soa_zone: soa.map(str::to_string),
1886 };
1887 let nx = ResponseCode::NXDomain;
1888 let ok = ResponseCode::NoError;
1889 assert_eq!(
1890 parent_answer(response(ok, &["ns1.seer.test"], None), "seer.test"),
1891 ParentAnswer::Delegation(vec!["ns1.seer.test".to_string()])
1892 );
1893 for soa in ["test", "", "seer.test"] {
1894 assert_eq!(
1895 parent_answer(response(nx, &[], Some(soa)), "seer.test"),
1896 ParentAnswer::Negative(nx),
1897 "{soa:?}"
1898 );
1899 }
1900 assert_eq!(
1901 parent_answer(response(ok, &[], Some("test")), "seer.test"),
1902 ParentAnswer::Negative(ok)
1903 );
1904 for soa in [None, Some("other.test"), Some("eer.test")] {
1906 assert!(
1907 matches!(
1908 parent_answer(response(ok, &[], soa), "seer.test"),
1909 ParentAnswer::Lame(_)
1910 ),
1911 "{soa:?}"
1912 );
1913 assert!(matches!(
1914 parent_answer(response(nx, &[], soa), "seer.test"),
1915 ParentAnswer::Lame(_)
1916 ));
1917 }
1918 assert!(matches!(
1919 parent_answer(DirectNs::Rcode(ResponseCode::Refused), "seer.test"),
1920 ParentAnswer::Lame(_)
1921 ));
1922 }
1923}