1use std::collections::BTreeMap;
79use std::net::IpAddr;
80use std::time::{Duration, Instant};
81
82use hickory_resolver::config::ConnectionConfig;
83use hickory_resolver::net::NetError;
84use hickory_resolver::proto::op::{Message, ResponseCode};
85use hickory_resolver::proto::rr::{Name, RData as HickoryRData, RecordType as HickoryRecordType};
86use hickory_resolver::TokioResolver;
87use serde::{Deserialize, Serialize};
88use tracing::{debug, instrument};
89
90use super::delegation::{build_recursive_resolver, is_local_no_route, partition_reserved};
91use super::query::{duration_ms, DnsStatus};
92use super::records::{DnsRecord, RecordType};
93use super::resolver::{fqdn, prepare_query, to_dns_record, wire_query_name, wire_type};
94use super::transport::{transport_reason, Transport};
95use super::DEFAULT_DNS_TIMEOUT;
96use crate::error::{Result, SeerError};
97
98const MAX_HOPS: usize = 16;
101
102const MAX_SERVERS_PER_LEVEL: usize = 3;
104
105const MAX_GLUELESS_PER_LEVEL: usize = 3;
108
109const TRACE_BUDGET_TIMEOUTS: u32 = (MAX_SERVERS_PER_LEVEL + MAX_GLUELESS_PER_LEVEL) as u32;
114
115const ROOT_SERVERS: [(&str, &str, &str); 13] = [
121 ("a.root-servers.net.", "198.41.0.4", "2001:503:ba3e::2:30"),
122 ("b.root-servers.net.", "170.247.170.2", "2801:1b8:10::b"),
123 ("c.root-servers.net.", "192.33.4.12", "2001:500:2::c"),
124 ("d.root-servers.net.", "199.7.91.13", "2001:500:2d::d"),
125 ("e.root-servers.net.", "192.203.230.10", "2001:500:a8::e"),
126 ("f.root-servers.net.", "192.5.5.241", "2001:500:2f::f"),
127 ("g.root-servers.net.", "192.112.36.4", "2001:500:12::d0d"),
128 ("h.root-servers.net.", "198.97.190.53", "2001:500:1::53"),
129 ("i.root-servers.net.", "192.36.148.17", "2001:7fe::53"),
130 ("j.root-servers.net.", "192.58.128.30", "2001:503:c27::2:30"),
131 ("k.root-servers.net.", "193.0.14.129", "2001:7fd::1"),
132 ("l.root-servers.net.", "199.7.83.42", "2001:500:9f::42"),
133 ("m.root-servers.net.", "202.12.27.33", "2001:dc3::35"),
134];
135
136#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
143pub struct TraceHop {
144 pub zone: String,
146 pub server: String,
148 pub address: String,
150 pub query_time_ms: u64,
152 pub status: DnsStatus,
154 pub authoritative: bool,
156 pub referral_zone: Option<String>,
160 pub referral: Vec<String>,
162 pub answers: Vec<DnsRecord>,
165 pub failed_servers: Vec<String>,
169}
170
171#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
196pub struct DnsTrace {
197 pub name: String,
202 pub record_type: RecordType,
204 pub hops: Vec<TraceHop>,
206 pub status: DnsStatus,
208 pub answers: Vec<DnsRecord>,
214 pub error: Option<String>,
218}
219
220pub struct DnsTracer {
227 timeout: Duration,
229 transport: Transport,
231 recursive: TokioResolver,
233 #[cfg(test)]
235 recursive_upstream: Option<(IpAddr, u16)>,
236 #[cfg(test)]
238 root_hints: Option<Vec<NsCandidate>>,
239 #[cfg(test)]
242 port_map: Option<std::collections::HashMap<String, u16>>,
243 #[cfg(test)]
245 allow_private_hosts: bool,
246 #[cfg(test)]
250 unroutable: Vec<(IpAddr, fn() -> std::io::Error)>,
251}
252
253impl std::fmt::Debug for DnsTracer {
254 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
255 f.debug_struct("DnsTracer")
256 .field("timeout", &self.timeout)
257 .finish()
258 }
259}
260
261impl Default for DnsTracer {
262 fn default() -> Self {
263 Self::new()
264 }
265}
266
267impl DnsTracer {
268 pub fn new() -> Self {
271 Self {
272 timeout: DEFAULT_DNS_TIMEOUT,
273 transport: Transport::new(DEFAULT_DNS_TIMEOUT),
274 recursive: build_recursive_resolver(DEFAULT_DNS_TIMEOUT, None),
275 #[cfg(test)]
276 recursive_upstream: None,
277 #[cfg(test)]
278 root_hints: None,
279 #[cfg(test)]
280 port_map: None,
281 #[cfg(test)]
282 allow_private_hosts: false,
283 #[cfg(test)]
284 unroutable: Vec::new(),
285 }
286 }
287
288 pub fn from_config(config: &crate::config::SeerConfig) -> Self {
294 Self::new().with_timeout(config.dns_timeout())
295 }
296
297 pub fn with_timeout(mut self, timeout: Duration) -> Self {
300 self.timeout = timeout;
301 self.transport = Transport::new(timeout);
302 self.recursive = build_recursive_resolver(timeout, self.recursive_upstream());
303 self
304 }
305
306 #[cfg(test)]
308 fn with_recursive_upstream(mut self, ip: IpAddr, port: u16) -> Self {
309 self.recursive_upstream = Some((ip, port));
310 self.recursive = build_recursive_resolver(self.timeout, self.recursive_upstream);
311 self
312 }
313
314 #[cfg(test)]
316 fn with_root_hints(mut self, hints: &[(&str, IpAddr)]) -> Self {
317 self.root_hints = Some(
318 hints
319 .iter()
320 .map(|(host, ip)| NsCandidate {
321 host: Name::from_ascii(fqdn(host)).expect("valid test root name"),
322 addrs: vec![*ip],
323 })
324 .collect(),
325 );
326 self
327 }
328
329 #[cfg(test)]
332 fn with_port_map(mut self, map: std::collections::HashMap<String, u16>) -> Self {
333 self.port_map = Some(
334 map.into_iter()
335 .map(|(host, port)| (host.trim_end_matches('.').to_ascii_lowercase(), port))
336 .collect(),
337 );
338 self
339 }
340
341 #[cfg(test)]
343 fn allowing_private_hosts(mut self) -> Self {
344 self.allow_private_hosts = true;
345 self
346 }
347
348 #[cfg(test)]
354 fn with_unroutable(mut self, addrs: &[IpAddr], error: fn() -> std::io::Error) -> Self {
355 self.unroutable.extend(addrs.iter().map(|&ip| (ip, error)));
356 self
357 }
358
359 #[cfg(test)]
360 fn recursive_upstream(&self) -> Option<(IpAddr, u16)> {
361 self.recursive_upstream
362 }
363
364 #[cfg(not(test))]
365 fn recursive_upstream(&self) -> Option<(IpAddr, u16)> {
366 None
367 }
368
369 #[cfg(test)]
370 fn allow_private(&self) -> bool {
371 self.allow_private_hosts
372 }
373
374 #[cfg(not(test))]
375 fn allow_private(&self) -> bool {
376 false
377 }
378
379 #[cfg(test)]
382 fn root_servers(&self) -> Vec<NsCandidate> {
383 self.root_hints.clone().unwrap_or_else(root_hints)
384 }
385
386 #[cfg(not(test))]
387 fn root_servers(&self) -> Vec<NsCandidate> {
388 root_hints()
389 }
390
391 #[cfg(test)]
395 fn direct_port(&self, host: &Name) -> u16 {
396 let key = name_text(host);
397 self.port_map
398 .as_ref()
399 .and_then(|map| map.get(key.trim_end_matches('.')).copied())
400 .unwrap_or(53)
401 }
402
403 #[cfg(not(test))]
404 fn direct_port(&self, _host: &Name) -> u16 {
405 53
406 }
407
408 #[cfg(test)]
411 fn simulated_no_route(&self, ip: IpAddr) -> Option<NetError> {
412 self.unroutable
413 .iter()
414 .find(|(addr, _)| *addr == ip)
415 .map(|(_, error)| NetError::from(error()))
416 }
417
418 #[cfg(not(test))]
419 fn simulated_no_route(&self, _ip: IpAddr) -> Option<NetError> {
420 None
421 }
422
423 #[instrument(skip(self), fields(name = %name, record_type = %record_type))]
452 pub async fn trace(&self, name: &str, record_type: RecordType) -> Result<DnsTrace> {
453 let qtype = trace_wire_type(record_type)?;
454 let (name, qname) = query_target(name, record_type)?;
455
456 let mut zone = Name::root();
457 let mut servers = self.root_servers();
458 let mut hops: Vec<TraceHop> = Vec::new();
459 let mut error = None;
460 let budget = self.timeout.saturating_mul(TRACE_BUDGET_TIMEOUTS);
463 let started = Instant::now();
464
465 loop {
466 let remaining = budget.saturating_sub(started.elapsed());
467 let level = self.ask_level(&zone, &servers, &qname, qtype);
468 let Ok(outcome) = tokio::time::timeout(remaining, level).await else {
469 let why = format!(
470 "gave up after {budget:?} without a final response, while asking the \
471 nameservers of {}",
472 name_text(&zone)
473 );
474 if hops.is_empty() {
475 return Err(SeerError::DnsError(format!(
476 "trace of {name} failed: {why}"
477 )));
478 }
479 error = Some(why);
480 break;
481 };
482 let (hop, step) = match outcome {
483 LevelOutcome::Answered { hop, step } => (hop, step),
484 LevelOutcome::Failed { final_hop, reason } => {
485 let why = format!(
486 "no nameserver for {} gave a usable response: {}",
487 name_text(&zone),
488 reason
489 );
490 match final_hop {
491 Some(hop) => hops.push(hop),
492 None if hops.is_empty() => {
494 return Err(SeerError::DnsError(format!(
495 "trace of {name} failed: {why}"
496 )))
497 }
498 None => {}
499 }
500 error = Some(why);
501 break;
502 }
503 };
504
505 let referral = match step {
507 Step::Final => {
508 hops.push(hop);
509 break;
510 }
511 Step::Referral(referral) => referral,
512 };
513 hops.push(hop);
514 if hops.len() >= MAX_HOPS {
515 error = Some(format!(
516 "gave up after {MAX_HOPS} delegation levels without a final response"
517 ));
518 break;
519 }
520 zone = referral.zone;
521 servers = referral.servers;
522 }
523
524 let (status, answers) = match hops.last() {
525 Some(last) => (last.status, last.answers.clone()),
526 None => {
528 return Err(SeerError::DnsError(format!(
529 "trace of {name} produced no response"
530 )))
531 }
532 };
533 Ok(DnsTrace {
534 name,
535 record_type,
536 hops,
537 status,
538 answers,
539 error,
540 })
541 }
542
543 async fn ask_level(
546 &self,
547 zone: &Name,
548 servers: &[NsCandidate],
549 qname: &Name,
550 qtype: HickoryRecordType,
551 ) -> LevelOutcome {
552 let (picks, mut failures) = plan_servers(servers, self.allow_private());
553 let mut queried = 0;
554 let mut resolved = 0;
555 let mut unusable_hop: Option<(TraceHop, usize)> = None;
559
560 for pick in picks {
561 if queried == MAX_SERVERS_PER_LEVEL {
562 break;
563 }
564 let (host, addrs) = match pick {
565 ServerPick::Addrs(host, addrs) => (host, addrs),
566 ServerPick::Glueless(host) => {
567 if resolved == MAX_GLUELESS_PER_LEVEL {
569 break;
570 }
571 resolved += 1;
572 let (addrs, notes) = self.resolve_glueless(&host).await;
573 failures.extend(notes);
574 (host, addrs)
575 }
576 };
577 let Some((ip, elapsed, result)) = self
578 .ask_server(&host, &addrs, qname, qtype, &mut failures)
579 .await
580 else {
581 continue;
582 };
583 queried += 1;
584 match result {
585 Ok(response) => {
586 let mut hop = hop_from_response(zone, &host, ip, elapsed, &response);
587 let reason = match classify_response(&response, zone) {
588 Ok(Step::Referral(referral)) => {
589 hop.referral_zone = Some(name_text(&referral.zone));
590 hop.referral = referral
591 .servers
592 .iter()
593 .map(|s| name_text(&s.host))
594 .collect();
595 match check_bailiwick(zone, &referral.zone, qname) {
600 Ok(()) => {
601 hop.failed_servers = failures;
602 let step = Step::Referral(referral);
603 return LevelOutcome::Answered { hop, step };
604 }
605 Err(fault) => format!(
606 "referred {fault} to {} (not toward the name)",
607 name_text(&referral.zone)
608 ),
609 }
610 }
611 Ok(step) => {
612 hop.failed_servers = failures;
613 return LevelOutcome::Answered { hop, step };
614 }
615 Err(reason) => reason,
616 };
617 unusable_hop = Some((hop, failures.len()));
618 failures.push(server_note(&host, ip, &reason));
619 }
620 Err(e) => failures.push(server_note(&host, ip, &transport_reason(&e))),
621 }
622 }
623
624 let reason = if failures.is_empty() {
625 "no nameserver address to query".to_string()
626 } else {
627 failures.join("; ")
628 };
629 let final_hop = unusable_hop.map(|(mut hop, own_note)| {
630 failures.remove(own_note);
631 hop.failed_servers = failures;
632 hop
633 });
634 LevelOutcome::Failed { final_hop, reason }
635 }
636
637 async fn ask_server(
643 &self,
644 host: &Name,
645 addrs: &[IpAddr],
646 qname: &Name,
647 qtype: HickoryRecordType,
648 failures: &mut Vec<String>,
649 ) -> Option<(IpAddr, Duration, std::result::Result<Message, NetError>)> {
650 for &ip in addrs {
651 debug!(server = %name_text(host), %ip, "trace: querying");
652 let started = Instant::now();
653 match self.exchange(host, ip, qname, qtype).await {
654 Err(e) if is_no_route(&e) => {
655 failures.push(server_note(host, ip, &transport_reason(&e)));
656 }
657 result => return Some((ip, started.elapsed(), result)),
658 }
659 }
660 None
661 }
662
663 async fn resolve_glueless(&self, host: &Name) -> (Vec<IpAddr>, Vec<String>) {
669 let text = name_text(host);
670 let lookup = tokio::time::timeout(self.timeout, self.recursive.lookup_ip(text.as_str()))
673 .await
674 .unwrap_or(Err(NetError::Timeout));
675 let ips: Vec<IpAddr> = match lookup {
676 Ok(lookup) => lookup.iter().collect(),
677 Err(e) if e.is_no_records_found() => Vec::new(),
678 Err(e) => {
679 return (
680 Vec::new(),
681 vec![format!(
682 "{text}: glueless nameserver lookup failed: {}",
683 transport_reason(&e)
684 )],
685 )
686 }
687 };
688 let (usable, refused) = partition_reserved(&ips, self.allow_private());
689 let mut notes: Vec<String> = refused
690 .iter()
691 .map(|(ip, reason)| server_note(host, *ip, &format!("refused, {reason}")))
692 .collect();
693 if usable.is_empty() && notes.is_empty() {
694 notes.push(format!("{text}: glueless nameserver has no address"));
695 }
696 let mut usable = usable;
697 crate::net::ipv4_first(&mut usable, |ip| *ip);
698 (usable, notes)
699 }
700
701 async fn exchange(
706 &self,
707 host: &Name,
708 ip: IpAddr,
709 qname: &Name,
710 qtype: HickoryRecordType,
711 ) -> std::result::Result<Message, NetError> {
712 if let Some(no_route) = self.simulated_no_route(ip) {
713 return Err(no_route);
714 }
715 let mut udp = ConnectionConfig::udp();
716 udp.port = self.direct_port(host);
717 let request = self.transport.request(qname.clone(), qtype, false);
720 self.transport.exchange(ip, &udp, &request).await
721 }
722}
723
724#[derive(Debug, Clone, PartialEq)]
727struct NsCandidate {
728 host: Name,
729 addrs: Vec<IpAddr>,
730}
731
732#[derive(Debug, PartialEq)]
734struct Referral {
735 zone: Name,
736 servers: Vec<NsCandidate>,
737}
738
739#[derive(Debug, PartialEq)]
741enum Step {
742 Final,
744 Referral(Referral),
746}
747
748enum LevelOutcome {
750 Answered { hop: TraceHop, step: Step },
752 Failed {
756 final_hop: Option<TraceHop>,
757 reason: String,
758 },
759}
760
761#[derive(Debug, PartialEq)]
763enum ServerPick {
764 Addrs(Name, Vec<IpAddr>),
766 Glueless(Name),
768}
769
770#[derive(Debug, Clone, Copy, PartialEq)]
772enum ReferralFault {
773 Upward,
775 Sideways,
778}
779
780impl std::fmt::Display for ReferralFault {
781 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
782 f.write_str(match self {
783 ReferralFault::Upward => "upward",
784 ReferralFault::Sideways => "sideways",
785 })
786 }
787}
788
789fn root_hints() -> Vec<NsCandidate> {
794 ROOT_SERVERS
795 .iter()
796 .map(|(host, v4, v6)| NsCandidate {
797 host: Name::from_ascii(host).expect("root hint names are valid"),
798 addrs: [v4, v6]
799 .iter()
800 .map(|addr| addr.parse().expect("root hint addresses are valid"))
801 .collect(),
802 })
803 .collect()
804}
805
806fn trace_wire_type(record_type: RecordType) -> Result<HickoryRecordType> {
810 wire_type(record_type).ok_or_else(|| {
811 SeerError::InvalidInput(
812 "trace needs a single record type; ANY is a fan-out over several queries".to_string(),
813 )
814 })
815}
816
817fn query_target(name: &str, record_type: RecordType) -> Result<(String, Name)> {
823 let reported = wire_query_name(&prepare_query(name, record_type)?, record_type)?;
824 let qname = Name::from_ascii(fqdn(&reported))
825 .map_err(|e| SeerError::InvalidDomain(format!("{reported}: {e}")))?;
826 Ok((reported, qname))
827}
828
829fn plan_servers(servers: &[NsCandidate], allow_private: bool) -> (Vec<ServerPick>, Vec<String>) {
836 let mut picks = Vec::new();
837 let mut glueless = Vec::new();
838 let mut notes = Vec::new();
839 for server in servers {
840 if server.addrs.is_empty() {
841 glueless.push(ServerPick::Glueless(server.host.clone()));
842 continue;
843 }
844 let (usable, refused) = partition_reserved(&server.addrs, allow_private);
845 notes.extend(
846 refused
847 .iter()
848 .map(|(ip, reason)| server_note(&server.host, *ip, &format!("refused, {reason}"))),
849 );
850 if !usable.is_empty() {
851 let mut usable = usable;
852 crate::net::ipv4_first(&mut usable, |ip| *ip);
853 picks.push(ServerPick::Addrs(server.host.clone(), usable));
854 }
855 }
856 picks.extend(glueless);
857 (picks, notes)
858}
859
860fn classify_response(response: &Message, zone: &Name) -> std::result::Result<Step, String> {
870 let code = response.metadata.response_code;
871 if !matches!(code, ResponseCode::NoError | ResponseCode::NXDomain) {
872 return Err(DnsStatus::from(code).to_string());
873 }
874 if !response.answers.is_empty()
875 || response.metadata.authoritative
876 || response
877 .authorities
878 .iter()
879 .any(|record| record.record_type() == HickoryRecordType::SOA)
880 {
881 return Ok(Step::Final);
882 }
883 if code == ResponseCode::NXDomain {
884 return Err("non-authoritative NXDOMAIN without an SOA".to_string());
885 }
886 extract_referral(response, zone)
887 .map(Step::Referral)
888 .ok_or_else(|| "empty non-authoritative response (no answer, referral or SOA)".to_string())
889}
890
891fn extract_referral(response: &Message, zone: &Name) -> Option<Referral> {
898 let child = response
899 .authorities
900 .iter()
901 .find(|record| matches!(record.data, HickoryRData::NS(_)))?
902 .name
903 .to_lowercase();
904 let targets: BTreeMap<String, Name> = response
905 .authorities
906 .iter()
907 .filter(|record| record.name == child)
908 .filter_map(|record| match &record.data {
909 HickoryRData::NS(ns) => {
910 let host = ns.0.to_lowercase();
911 Some((name_text(&host), host))
912 }
913 _ => None,
914 })
915 .collect();
916 let servers = targets
917 .into_values()
918 .map(|host| {
919 let mut addrs: Vec<IpAddr> = Vec::new();
920 if zone.zone_of(&host) {
921 for record in response.additionals.iter().filter(|r| r.name == host) {
922 let ip = match &record.data {
923 HickoryRData::A(a) => IpAddr::V4(a.0),
924 HickoryRData::AAAA(aaaa) => IpAddr::V6(aaaa.0),
925 _ => continue,
926 };
927 if !addrs.contains(&ip) {
928 addrs.push(ip);
929 }
930 }
931 }
932 NsCandidate { host, addrs }
933 })
934 .collect();
935 Some(Referral {
936 zone: child,
937 servers,
938 })
939}
940
941fn check_bailiwick(
945 zone: &Name,
946 child: &Name,
947 qname: &Name,
948) -> std::result::Result<(), ReferralFault> {
949 if child.zone_of(zone) {
950 return Err(ReferralFault::Upward);
951 }
952 if !zone.zone_of(child) || !child.zone_of(qname) {
953 return Err(ReferralFault::Sideways);
954 }
955 Ok(())
956}
957
958fn hop_from_response(
961 zone: &Name,
962 host: &Name,
963 ip: IpAddr,
964 elapsed: Duration,
965 response: &Message,
966) -> TraceHop {
967 TraceHop {
968 zone: name_text(zone),
969 server: name_text(host),
970 address: ip.to_string(),
971 query_time_ms: duration_ms(elapsed),
972 status: DnsStatus::from(response.metadata.response_code),
973 authoritative: response.metadata.authoritative,
974 referral_zone: None,
975 referral: Vec::new(),
976 answers: response.answers.iter().filter_map(to_dns_record).collect(),
977 failed_servers: Vec::new(),
978 }
979}
980
981fn server_note(host: &Name, ip: IpAddr, reason: &str) -> String {
983 format!("{} ({}): {}", name_text(host), ip, reason)
984}
985
986fn is_no_route(err: &NetError) -> bool {
990 matches!(err, NetError::Io(io) if is_local_no_route(io))
991}
992
993fn name_text(name: &Name) -> String {
996 name.to_ascii().to_ascii_lowercase()
997}
998
999#[cfg(test)]
1000mod tests {
1001 use std::collections::HashMap;
1008 use std::net::{Ipv4Addr, Ipv6Addr};
1009 use std::sync::{Arc, Mutex};
1010
1011 use hickory_resolver::proto::op::OpCode;
1012 use hickory_resolver::proto::rr::rdata as wire;
1013 use hickory_resolver::proto::rr::Record;
1014
1015 use super::*;
1016 use crate::dns::delegation::ADDRESS_FAMILY_UNSUPPORTED;
1017 use crate::dns::records::RecordData;
1018 use crate::dns::test_support::{
1019 a_rdata, cname_rdata, fq_name, record, soa_rdata, spawn_mock_dns, spawn_mock_dns_fn,
1020 spawn_mock_dns_fn_with_tcp, MockMode, MockReply,
1021 };
1022
1023 const LOOPBACK: IpAddr = IpAddr::V4(Ipv4Addr::LOCALHOST);
1024
1025 fn network_unreachable() -> std::io::Error {
1027 std::io::Error::new(
1028 std::io::ErrorKind::NetworkUnreachable,
1029 "Network is unreachable",
1030 )
1031 }
1032
1033 fn address_family_unsupported() -> std::io::Error {
1037 let code = ADDRESS_FAMILY_UNSUPPORTED.expect("a Unix or Windows target");
1038 std::io::Error::from_raw_os_error(code)
1039 }
1040
1041 #[test]
1044 fn root_hints_are_complete_and_public() {
1045 let hints = root_hints();
1046 assert_eq!(hints.len(), 13);
1047 for (letter, hint) in ('a'..='m').zip(&hints) {
1049 assert_eq!(name_text(&hint.host), format!("{letter}.root-servers.net."));
1050 assert_eq!(hint.addrs.len(), 2);
1051 assert!(hint.addrs[0].is_ipv4() && hint.addrs[1].is_ipv6());
1052 let (usable, refused) = partition_reserved(&hint.addrs, false);
1054 assert_eq!(usable, hint.addrs);
1055 assert!(refused.is_empty(), "{refused:?}");
1056 }
1057 }
1058
1059 #[test]
1060 fn name_text_is_fully_qualified_lowercase_ascii() {
1061 assert_eq!(name_text(&Name::root()), ".");
1062 assert_eq!(name_text(&fq_name("Example.COM")), "example.com.");
1063 assert_eq!(
1064 name_text(&Name::from_utf8("münchen.de.").unwrap()),
1065 "xn--mnchen-3ya.de."
1066 );
1067 }
1068
1069 #[test]
1070 fn bailiwick_accepts_only_strict_descendants_on_the_path() {
1071 let qname = fq_name("www.example.com");
1072 assert_eq!(
1074 check_bailiwick(&Name::root(), &fq_name("com"), &qname),
1075 Ok(())
1076 );
1077 assert_eq!(
1078 check_bailiwick(&fq_name("com"), &fq_name("example.com"), &qname),
1079 Ok(())
1080 );
1081 assert_eq!(
1083 check_bailiwick(&fq_name("example.com"), &fq_name("www.example.com"), &qname),
1084 Ok(())
1085 );
1086 assert_eq!(
1088 check_bailiwick(&Name::root(), &fq_name("example.com"), &qname),
1089 Ok(())
1090 );
1091 assert_eq!(
1093 check_bailiwick(&fq_name("com"), &fq_name("EXAMPLE.com"), &qname),
1094 Ok(())
1095 );
1096
1097 assert_eq!(
1099 check_bailiwick(&fq_name("com"), &Name::root(), &qname),
1100 Err(ReferralFault::Upward)
1101 );
1102 assert_eq!(
1103 check_bailiwick(&fq_name("com"), &fq_name("com"), &qname),
1104 Err(ReferralFault::Upward)
1105 );
1106 assert_eq!(
1107 check_bailiwick(&fq_name("example.com"), &fq_name("com"), &qname),
1108 Err(ReferralFault::Upward)
1109 );
1110 assert_eq!(
1112 check_bailiwick(&fq_name("com"), &fq_name("other.com"), &qname),
1113 Err(ReferralFault::Sideways)
1114 );
1115 assert_eq!(
1116 check_bailiwick(&fq_name("com"), &fq_name("example.net"), &qname),
1117 Err(ReferralFault::Sideways)
1118 );
1119 assert_eq!(
1121 check_bailiwick(
1122 &fq_name("example.com"),
1123 &fq_name("a.www.example.com"),
1124 &qname
1125 ),
1126 Err(ReferralFault::Sideways)
1127 );
1128 }
1129
1130 fn message() -> Message {
1131 Message::response(1, OpCode::Query)
1132 }
1133
1134 fn ns(owner: &str, target: &str) -> Record {
1135 record(owner, 300, HickoryRData::NS(wire::NS(fq_name(target))))
1136 }
1137
1138 fn a(owner: &str, ip: Ipv4Addr) -> Record {
1139 record(owner, 300, a_rdata(ip))
1140 }
1141
1142 fn aaaa(owner: &str, ip: Ipv6Addr) -> Record {
1143 record(owner, 300, HickoryRData::AAAA(wire::AAAA(ip)))
1144 }
1145
1146 #[test]
1147 fn referral_extraction_reads_ns_and_in_bailiwick_glue() {
1148 let mut response = message();
1149 response.add_authority(ns("Example.COM", "ns2.example.com"));
1151 response.add_authority(ns("example.com", "NS1.example.com"));
1152 response.add_authority(ns("example.com", "ns1.example.com"));
1153 response.add_authority(ns("example.com", "ns.dns-host.net"));
1154 response.add_authority(ns("other.com", "ns.other.com"));
1156 let v4 = Ipv4Addr::new(192, 0, 2, 1);
1157 let v6 = "2001:db8::1".parse().unwrap();
1158 response.add_additional(aaaa("ns1.example.com", v6));
1159 response.add_additional(a("ns1.example.com", v4));
1160 response.add_additional(a("ns1.example.com", v4));
1161 response.add_additional(a("ns.dns-host.net", Ipv4Addr::new(198, 51, 100, 7)));
1164 response.add_additional(a("unrelated.example.com", v4));
1166
1167 let referral = extract_referral(&response, &fq_name("com")).expect("a referral");
1168 assert_eq!(referral.zone, fq_name("example.com"));
1169 assert_eq!(name_text(&referral.zone), "example.com.");
1170 let servers: Vec<(String, Vec<IpAddr>)> = referral
1171 .servers
1172 .iter()
1173 .map(|s| (name_text(&s.host), s.addrs.clone()))
1174 .collect();
1175 assert_eq!(
1176 servers,
1177 vec![
1178 ("ns.dns-host.net.".to_string(), vec![]),
1179 (
1180 "ns1.example.com.".to_string(),
1181 vec![IpAddr::V6(v6), IpAddr::V4(v4)]
1182 ),
1183 ("ns2.example.com.".to_string(), vec![]),
1184 ]
1185 );
1186 let from_root = extract_referral(&response, &Name::root()).expect("a referral");
1188 assert_eq!(from_root.servers[0].addrs.len(), 1);
1189
1190 assert_eq!(extract_referral(&message(), &fq_name("com")), None);
1192 }
1193
1194 #[test]
1195 fn classification_separates_answers_negatives_referrals_and_lame_replies() {
1196 let zone = fq_name("com");
1197 let mut referral = message();
1198 referral.add_authority(ns("example.com", "ns1.example.com"));
1199 assert!(matches!(
1200 classify_response(&referral, &zone),
1201 Ok(Step::Referral(_))
1202 ));
1203
1204 let mut authoritative = referral.clone();
1206 authoritative.metadata.authoritative = true;
1207 assert_eq!(classify_response(&authoritative, &zone), Ok(Step::Final));
1208
1209 let mut nodata = referral.clone();
1211 nodata.add_authority(record("com", 300, soa_rdata("com")));
1212 assert_eq!(classify_response(&nodata, &zone), Ok(Step::Final));
1213 let mut nxdomain = nodata.clone();
1214 nxdomain.metadata.response_code = ResponseCode::NXDomain;
1215 assert_eq!(classify_response(&nxdomain, &zone), Ok(Step::Final));
1216 let mut aa_nxdomain = message();
1218 aa_nxdomain.metadata.authoritative = true;
1219 aa_nxdomain.metadata.response_code = ResponseCode::NXDomain;
1220 assert_eq!(classify_response(&aa_nxdomain, &zone), Ok(Step::Final));
1221
1222 let mut answer = referral.clone();
1224 answer.add_answer(record("www.example.com", 300, cname_rdata("edge.cdn.net")));
1225 assert_eq!(classify_response(&answer, &zone), Ok(Step::Final));
1226
1227 let mut servfail = answer;
1229 servfail.metadata.response_code = ResponseCode::ServFail;
1230 assert_eq!(
1231 classify_response(&servfail, &zone),
1232 Err("SERVFAIL".to_string())
1233 );
1234
1235 let lame = classify_response(&message(), &zone).expect_err("a lame reply");
1238 assert!(lame.contains("empty non-authoritative"), "{lame}");
1239 let mut bare_nxdomain = referral;
1242 bare_nxdomain.metadata.response_code = ResponseCode::NXDomain;
1243 let lame = classify_response(&bare_nxdomain, &zone).expect_err("a lame reply");
1244 assert!(lame.contains("NXDOMAIN without an SOA"), "{lame}");
1245 }
1246
1247 #[test]
1248 fn server_plan_orders_glued_first_prefers_ipv4_and_refuses_reserved() {
1249 let public_v4: IpAddr = "192.5.6.30".parse().unwrap();
1250 let public_v6: IpAddr = "2001:503:a83e::2:30".parse().unwrap();
1251 let private: IpAddr = "10.0.0.53".parse().unwrap();
1252 let metadata: IpAddr = "169.254.169.254".parse().unwrap();
1253 let servers = vec![
1254 NsCandidate {
1255 host: fq_name("glueless.example.net"),
1256 addrs: vec![],
1257 },
1258 NsCandidate {
1260 host: fq_name("ns1.example.com"),
1261 addrs: vec![public_v6, public_v4],
1262 },
1263 NsCandidate {
1266 host: fq_name("ns2.example.com"),
1267 addrs: vec![private, public_v6],
1268 },
1269 NsCandidate {
1271 host: fq_name("ns3.example.com"),
1272 addrs: vec![metadata],
1273 },
1274 ];
1275
1276 let (picks, notes) = plan_servers(&servers, false);
1277 assert_eq!(
1278 picks,
1279 vec![
1280 ServerPick::Addrs(fq_name("ns1.example.com"), vec![public_v4, public_v6]),
1283 ServerPick::Addrs(fq_name("ns2.example.com"), vec![public_v6]),
1284 ServerPick::Glueless(fq_name("glueless.example.net")),
1285 ]
1286 );
1287 assert_eq!(
1288 notes,
1289 vec![
1290 "ns2.example.com. (10.0.0.53): refused, private network (RFC 1918)",
1291 "ns3.example.com. (169.254.169.254): refused, cloud metadata endpoint (169.254.169.254)",
1292 ]
1293 );
1294
1295 let (picks, notes) = plan_servers(&servers, true);
1297 assert!(picks.contains(&ServerPick::Addrs(
1298 fq_name("ns3.example.com"),
1299 vec![metadata]
1300 )));
1301 assert!(notes.is_empty());
1302 }
1303
1304 #[test]
1305 fn query_target_follows_record_query_normalization() {
1306 let (name, qname) = query_target("WWW.Example.com", RecordType::A).unwrap();
1308 assert_eq!(name, "www.example.com");
1309 assert_eq!(qname, fq_name("www.example.com"));
1310 assert!(qname.is_fqdn());
1311 let (name, qname) = query_target("192.0.2.1", RecordType::PTR).unwrap();
1314 assert_eq!(name, "1.2.0.192.in-addr.arpa");
1315 assert_eq!(qname, fq_name("1.2.0.192.in-addr.arpa"));
1316 let (name, _) = query_target("2606:4700:4700::1111", RecordType::PTR).unwrap();
1317 assert_eq!(
1318 name,
1319 "1.1.1.1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.7.4.0.0.7.4.6.0.6.2.ip6.arpa"
1320 );
1321 assert!(query_target("_sip._tcp.example.com", RecordType::SRV).is_ok());
1323 assert!(matches!(
1324 query_target("example.com", RecordType::SRV),
1325 Err(SeerError::InvalidInput(_))
1326 ));
1327 assert!(query_target("not a name", RecordType::A).is_err());
1328 }
1329
1330 #[test]
1331 fn query_target_applies_the_resolvers_srv_label_rule() {
1332 let err = query_target("_sip_x._tcp.example.com", RecordType::SRV).unwrap_err();
1335 assert!(
1336 matches!(&err, SeerError::InvalidInput(m) if m.contains("invalid SRV service")),
1337 "{err:?}"
1338 );
1339 }
1340
1341 #[test]
1342 fn any_is_not_traceable() {
1343 assert!(matches!(
1344 trace_wire_type(RecordType::ANY),
1345 Err(SeerError::InvalidInput(_))
1346 ));
1347 assert_eq!(
1348 trace_wire_type(RecordType::SRV).unwrap(),
1349 HickoryRecordType::SRV
1350 );
1351 assert_eq!(
1352 trace_wire_type(RecordType::MX).unwrap(),
1353 HickoryRecordType::MX
1354 );
1355 }
1356
1357 #[test]
1358 fn trace_serializes_to_the_documented_shape() {
1359 let hop = TraceHop {
1360 zone: ".".to_string(),
1361 server: "a.root-servers.net.".to_string(),
1362 address: "198.41.0.4".to_string(),
1363 query_time_ms: 21,
1364 status: DnsStatus::NoError,
1365 authoritative: false,
1366 referral_zone: Some("com.".to_string()),
1367 referral: vec!["a.gtld-servers.net.".to_string()],
1368 answers: vec![],
1369 failed_servers: vec![],
1370 };
1371 let trace = DnsTrace {
1372 name: "www.example.com".to_string(),
1373 record_type: RecordType::A,
1374 hops: vec![hop],
1375 status: DnsStatus::NxDomain,
1376 answers: vec![],
1377 error: None,
1378 };
1379 let json = serde_json::to_value(&trace).unwrap();
1380 assert_eq!(
1381 json,
1382 serde_json::json!({
1383 "name": "www.example.com",
1384 "record_type": "A",
1385 "hops": [{
1386 "zone": ".",
1387 "server": "a.root-servers.net.",
1388 "address": "198.41.0.4",
1389 "query_time_ms": 21,
1390 "status": "NOERROR",
1391 "authoritative": false,
1392 "referral_zone": "com.",
1393 "referral": ["a.gtld-servers.net."],
1394 "answers": [],
1395 "failed_servers": []
1396 }],
1397 "status": "NXDOMAIN",
1398 "answers": [],
1399 "error": null
1400 })
1401 );
1402 let back: DnsTrace = serde_json::from_value(json).unwrap();
1404 assert_eq!(back.hops[0].referral_zone.as_deref(), Some("com."));
1405 assert_eq!(back.status, DnsStatus::NxDomain);
1406 }
1407
1408 #[test]
1409 fn transport_reasons_are_short_and_name_local_routing_failures() {
1410 assert_eq!(transport_reason(&NetError::Timeout), "timed out");
1411 let no_route = NetError::from(network_unreachable());
1414 assert!(is_no_route(&no_route));
1415 assert_eq!(
1416 transport_reason(&no_route),
1417 "no route from this host (Network is unreachable)"
1418 );
1419 let no_socket = NetError::from(address_family_unsupported());
1422 assert!(is_no_route(&no_socket));
1423 assert_eq!(
1424 transport_reason(&no_socket),
1425 format!("no route from this host ({})", address_family_unsupported())
1426 );
1427 let refused = NetError::from(std::io::Error::new(
1428 std::io::ErrorKind::ConnectionRefused,
1429 "refused",
1430 ));
1431 assert!(transport_reason(&refused).contains("refused"));
1432 }
1433
1434 #[test]
1435 fn from_config_applies_dns_timeout() {
1436 let mut config = crate::config::SeerConfig::default();
1437 config.timeouts.dns_secs = 9;
1438 let tracer = DnsTracer::from_config(&config);
1439 assert_eq!(tracer.timeout, Duration::from_secs(9));
1440 }
1441
1442 fn delegation(zone: &str, servers: &[&str]) -> MockReply {
1447 MockReply::Delegation {
1448 zone: zone.to_string(),
1449 servers: servers
1450 .iter()
1451 .map(|s| (s.to_string(), vec![LOOPBACK]))
1452 .collect(),
1453 }
1454 }
1455
1456 fn tracer(roots: &[&str], ports: &[(&str, u16)]) -> DnsTracer {
1459 let hints: Vec<(&str, IpAddr)> = roots.iter().map(|root| (*root, LOOPBACK)).collect();
1460 DnsTracer::new()
1461 .with_timeout(Duration::from_millis(500))
1462 .allowing_private_hosts()
1463 .with_root_hints(&hints)
1464 .with_port_map(
1465 ports
1466 .iter()
1467 .map(|(host, port)| (host.to_string(), *port))
1468 .collect(),
1469 )
1470 }
1471
1472 async fn three_level_tracer(answer: fn(&str) -> MockReply) -> DnsTracer {
1474 let root = spawn_mock_dns_fn(|_, _| delegation("test", &["ns1.nic.test"])).await;
1475 let tld = spawn_mock_dns_fn(|_, _| {
1476 delegation("example.test", &["ns2.example.test", "ns1.example.test"])
1477 })
1478 .await;
1479 let auth = spawn_mock_dns_fn(move |qname, _| answer(qname)).await;
1480 tracer(
1481 &["a.root.test"],
1482 &[
1483 ("a.root.test", root),
1484 ("ns1.nic.test", tld),
1485 ("ns1.example.test", auth),
1486 ("ns2.example.test", auth),
1487 ],
1488 )
1489 }
1490
1491 #[tokio::test]
1492 async fn walks_root_tld_and_authoritative_server() {
1493 let tracer = three_level_tracer(|_| {
1494 MockReply::AuthoritativeAnswer(vec![a_rdata(Ipv4Addr::new(192, 0, 2, 10))])
1495 })
1496 .await;
1497 let trace = tracer
1498 .trace("WWW.example.test", RecordType::A)
1499 .await
1500 .unwrap();
1501
1502 assert_eq!(trace.name, "www.example.test");
1503 assert_eq!(trace.record_type, RecordType::A);
1504 assert_eq!(trace.hops.len(), 3, "{trace:#?}");
1505 assert!(trace.error.is_none(), "{trace:#?}");
1506
1507 let root = &trace.hops[0];
1508 assert_eq!(root.zone, ".");
1509 assert_eq!(root.server, "a.root.test.");
1510 assert_eq!(root.address, "127.0.0.1");
1511 assert_eq!(root.status, DnsStatus::NoError);
1512 assert!(!root.authoritative);
1513 assert_eq!(root.referral_zone.as_deref(), Some("test."));
1514 assert_eq!(root.referral, vec!["ns1.nic.test."]);
1515 assert!(root.answers.is_empty());
1516
1517 let tld = &trace.hops[1];
1518 assert_eq!(tld.zone, "test.");
1519 assert_eq!(tld.server, "ns1.nic.test.");
1520 assert_eq!(tld.referral_zone.as_deref(), Some("example.test."));
1521 assert_eq!(tld.referral, vec!["ns1.example.test.", "ns2.example.test."]);
1522
1523 let auth = &trace.hops[2];
1524 assert_eq!(auth.zone, "example.test.");
1525 assert_eq!(auth.server, "ns1.example.test.");
1527 assert!(auth.authoritative);
1528 assert_eq!(auth.status, DnsStatus::NoError);
1529 assert_eq!(auth.referral_zone, None);
1530 assert!(auth.referral.is_empty());
1531 assert!(auth.failed_servers.is_empty());
1532
1533 assert_eq!(trace.status, DnsStatus::NoError);
1534 assert_eq!(trace.answers.len(), 1);
1535 assert_eq!(trace.answers[0].name, "www.example.test");
1536 assert_eq!(trace.answers[0].record_type, RecordType::A);
1537 assert!(matches!(
1538 &trace.answers[0].data,
1539 RecordData::A { address } if address == "192.0.2.10"
1540 ));
1541 }
1542
1543 #[tokio::test]
1544 async fn nxdomain_at_the_authoritative_level_ends_the_walk() {
1545 let tracer = three_level_tracer(|_| MockReply::AuthoritativeNxDomain("example.test")).await;
1546 let trace = tracer
1547 .trace("gone.example.test", RecordType::A)
1548 .await
1549 .unwrap();
1550
1551 assert_eq!(trace.hops.len(), 3);
1552 assert_eq!(trace.status, DnsStatus::NxDomain);
1553 assert!(trace.answers.is_empty());
1554 assert!(trace.error.is_none(), "a negative answer is not an error");
1555 let last = &trace.hops[2];
1556 assert_eq!(last.status, DnsStatus::NxDomain);
1557 assert!(last.authoritative, "the AA bit survives a negative answer");
1558 assert_eq!(last.referral_zone, None);
1559 }
1560
1561 #[tokio::test]
1562 async fn nodata_at_the_authoritative_level_ends_the_walk() {
1563 let tracer = three_level_tracer(|_| MockReply::AuthoritativeAnswer(vec![])).await;
1564 let trace = tracer
1565 .trace("www.example.test", RecordType::MX)
1566 .await
1567 .unwrap();
1568
1569 assert_eq!(trace.hops.len(), 3);
1570 assert_eq!(trace.status, DnsStatus::NoError);
1571 assert!(trace.answers.is_empty());
1572 assert!(trace.hops[2].authoritative);
1573 assert!(trace.error.is_none());
1574 }
1575
1576 #[tokio::test]
1577 async fn cname_answer_is_reported_not_chased() {
1578 let root = spawn_mock_dns_fn(|_, _| delegation("test", &["ns1.nic.test"])).await;
1579 let tld = spawn_mock_dns_fn(|_, _| delegation("example.test", &["ns1.example.test"])).await;
1580 let asked = Arc::new(Mutex::new(Vec::new()));
1581 let log = Arc::clone(&asked);
1582 let auth = spawn_mock_dns_fn(move |qname, qtype| {
1583 log.lock().unwrap().push((qname.to_string(), qtype));
1584 MockReply::AuthoritativeAnswer(vec![cname_rdata("edge.cdn.test")])
1585 })
1586 .await;
1587 let trace = tracer(
1588 &["a.root.test"],
1589 &[
1590 ("a.root.test", root),
1591 ("ns1.nic.test", tld),
1592 ("ns1.example.test", auth),
1593 ],
1594 )
1595 .trace("www.example.test", RecordType::A)
1596 .await
1597 .unwrap();
1598
1599 assert_eq!(trace.hops.len(), 3);
1600 assert!(trace.error.is_none());
1601 assert_eq!(trace.answers.len(), 1);
1602 let cname = &trace.answers[0];
1603 assert_eq!(cname.name, "www.example.test");
1604 assert_eq!(cname.record_type, RecordType::CNAME);
1605 assert!(matches!(
1606 &cname.data,
1607 RecordData::CNAME { target } if target == "edge.cdn.test."
1608 ));
1609 let asked = asked.lock().unwrap();
1612 assert!(!asked.is_empty());
1613 assert!(
1614 asked
1615 .iter()
1616 .all(|q| *q == ("www.example.test".to_string(), HickoryRecordType::A)),
1617 "{asked:?}"
1618 );
1619 }
1620
1621 #[tokio::test]
1622 async fn glueless_referral_is_resolved_through_the_recursive_resolver() {
1623 let root = spawn_mock_dns_fn(|_, _| delegation("test", &["ns1.nic.test"])).await;
1624 let tld = spawn_mock_dns_fn(|_, _| MockReply::Delegation {
1627 zone: "example.test".to_string(),
1628 servers: vec![("ns.dns-host.test".to_string(), vec![])],
1629 })
1630 .await;
1631 let auth = spawn_mock_dns_fn(|_, _| {
1632 MockReply::AuthoritativeAnswer(vec![a_rdata(Ipv4Addr::new(192, 0, 2, 20))])
1633 })
1634 .await;
1635 let recursive = spawn_mock_dns_fn(|qname, qtype| match (qname, qtype) {
1636 ("ns.dns-host.test", HickoryRecordType::A) => {
1637 MockReply::Answer(vec![a_rdata(Ipv4Addr::LOCALHOST)])
1638 }
1639 _ => MockReply::NoData,
1640 })
1641 .await;
1642 let trace = tracer(
1643 &["a.root.test"],
1644 &[
1645 ("a.root.test", root),
1646 ("ns1.nic.test", tld),
1647 ("ns.dns-host.test", auth),
1648 ],
1649 )
1650 .with_recursive_upstream(LOOPBACK, recursive)
1651 .trace("www.example.test", RecordType::A)
1652 .await
1653 .unwrap();
1654
1655 assert_eq!(trace.hops.len(), 3, "{trace:#?}");
1656 assert_eq!(trace.hops[1].referral, vec!["ns.dns-host.test."]);
1657 assert_eq!(trace.hops[2].server, "ns.dns-host.test.");
1658 assert_eq!(trace.hops[2].address, "127.0.0.1");
1659 assert_eq!(trace.answers.len(), 1);
1660 assert!(trace.error.is_none());
1661 }
1662
1663 #[tokio::test]
1664 async fn unresolvable_glueless_nameserver_ends_the_walk_with_an_error() {
1665 let root = spawn_mock_dns_fn(|_, _| MockReply::Delegation {
1666 zone: "test".to_string(),
1667 servers: vec![("ns.nowhere.test".to_string(), vec![])],
1668 })
1669 .await;
1670 let recursive = spawn_mock_dns(MockMode::Nxdomain).await;
1671 let trace = tracer(&["a.root.test"], &[("a.root.test", root)])
1672 .with_recursive_upstream(LOOPBACK, recursive)
1673 .trace("www.example.test", RecordType::A)
1674 .await
1675 .unwrap();
1676
1677 assert_eq!(trace.hops.len(), 1);
1678 let error = trace.error.expect("the walk must stop with an error");
1679 assert!(error.contains("test."), "{error}");
1680 assert!(error.contains("ns.nowhere.test."), "{error}");
1681 }
1682
1683 #[tokio::test]
1684 async fn reserved_root_hints_are_refused_without_the_test_seam() {
1685 let asked = Arc::new(Mutex::new(0usize));
1688 let count = Arc::clone(&asked);
1689 let root = spawn_mock_dns_fn(move |_, _| {
1690 *count.lock().unwrap() += 1;
1691 MockReply::AuthoritativeAnswer(vec![])
1692 })
1693 .await;
1694 let err = DnsTracer::new()
1695 .with_timeout(Duration::from_millis(500))
1696 .with_root_hints(&[("a.root.test", LOOPBACK)])
1697 .with_port_map(HashMap::from([("a.root.test".to_string(), root)]))
1698 .trace("www.example.test", RecordType::A)
1699 .await
1700 .expect_err("a loopback root must be refused");
1701 assert!(matches!(err, SeerError::DnsError(_)), "{err:?}");
1702 assert!(err.to_string().contains("loopback"), "{err}");
1703 assert_eq!(*asked.lock().unwrap(), 0, "no query may reach the server");
1704 }
1705
1706 #[tokio::test]
1707 async fn glueless_addresses_are_vetted_without_the_test_seam() {
1708 let public_v4 = Ipv4Addr::new(9, 9, 9, 9);
1712 let public_v6: Ipv6Addr = "2620:fe::fe".parse().unwrap();
1713 let private = Ipv4Addr::new(10, 0, 0, 53);
1714 let recursive = spawn_mock_dns_fn(move |qname, qtype| match (qname, qtype) {
1715 ("ns.evil.test", HickoryRecordType::A) => MockReply::Answer(vec![
1716 a_rdata(private),
1717 a_rdata(Ipv4Addr::new(169, 254, 169, 254)),
1718 ]),
1719 ("ns.evil.test", HickoryRecordType::AAAA) => {
1720 MockReply::Answer(vec![HickoryRData::AAAA(wire::AAAA(Ipv6Addr::LOCALHOST))])
1721 }
1722 ("ns.mixed.test", HickoryRecordType::A) => {
1723 MockReply::Answer(vec![a_rdata(private), a_rdata(public_v4)])
1724 }
1725 ("ns.mixed.test", HickoryRecordType::AAAA) => {
1726 MockReply::Answer(vec![HickoryRData::AAAA(wire::AAAA(public_v6))])
1727 }
1728 _ => MockReply::NoData,
1729 })
1730 .await;
1731 let tracer = DnsTracer::new()
1732 .with_timeout(Duration::from_millis(500))
1733 .with_recursive_upstream(LOOPBACK, recursive);
1734
1735 let (addrs, mut notes) = tracer.resolve_glueless(&fq_name("ns.evil.test")).await;
1737 assert!(addrs.is_empty(), "{addrs:?}");
1738 notes.sort();
1739 assert_eq!(
1740 notes,
1741 vec![
1742 "ns.evil.test. (10.0.0.53): refused, private network (RFC 1918)",
1743 "ns.evil.test. (169.254.169.254): refused, cloud metadata endpoint (169.254.169.254)",
1744 "ns.evil.test. (::1): refused, IPv6 loopback (::1)",
1745 ]
1746 );
1747
1748 let (addrs, notes) = tracer.resolve_glueless(&fq_name("ns.mixed.test")).await;
1750 assert_eq!(addrs, vec![IpAddr::V4(public_v4), IpAddr::V6(public_v6)]);
1751 assert_eq!(
1752 notes,
1753 vec!["ns.mixed.test. (10.0.0.53): refused, private network (RFC 1918)"]
1754 );
1755 }
1756
1757 #[tokio::test]
1758 async fn a_glueless_lookup_takes_at_most_one_timeout() {
1759 let recursive = spawn_mock_dns(MockMode::Ignore).await;
1762 let timeout = Duration::from_millis(300);
1763 let tracer = DnsTracer::new()
1764 .with_timeout(timeout)
1765 .with_recursive_upstream(LOOPBACK, recursive);
1766
1767 let started = Instant::now();
1768 let (addrs, notes) = tracer.resolve_glueless(&fq_name("ns.dark.test")).await;
1769 let elapsed = started.elapsed();
1770 assert!(addrs.is_empty(), "{addrs:?}");
1771 assert_eq!(
1772 notes,
1773 vec!["ns.dark.test.: glueless nameserver lookup failed: timed out"]
1774 );
1775 assert!(elapsed < timeout * 2, "took {elapsed:?}");
1776 }
1777
1778 #[tokio::test]
1779 async fn addresses_with_no_route_are_skipped_without_using_up_the_level() {
1780 let unroutable: Vec<IpAddr> = (1..=4)
1785 .map(|i| IpAddr::V4(Ipv4Addr::new(192, 0, 2, i)))
1786 .collect();
1787 let first_glue = unroutable[3];
1788 let root = spawn_mock_dns_fn(move |_, _| MockReply::Delegation {
1789 zone: "example.test".to_string(),
1790 servers: vec![("ns1.example.test".to_string(), vec![first_glue, LOOPBACK])],
1791 })
1792 .await;
1793 let auth = spawn_mock_dns_fn(|_, _| {
1794 MockReply::AuthoritativeAnswer(vec![a_rdata(Ipv4Addr::new(192, 0, 2, 50))])
1795 })
1796 .await;
1797 let trace = DnsTracer::new()
1798 .with_timeout(Duration::from_millis(500))
1799 .allowing_private_hosts()
1800 .with_root_hints(&[
1801 ("a.root.test", unroutable[0]),
1802 ("b.root.test", unroutable[1]),
1803 ("c.root.test", unroutable[2]),
1804 ("d.root.test", LOOPBACK),
1805 ])
1806 .with_port_map(HashMap::from([
1807 ("d.root.test".to_string(), root),
1808 ("ns1.example.test".to_string(), auth),
1809 ]))
1810 .with_unroutable(&unroutable, network_unreachable)
1811 .trace("www.example.test", RecordType::A)
1812 .await
1813 .unwrap();
1814
1815 assert!(trace.error.is_none(), "{trace:#?}");
1816 assert_eq!(trace.hops.len(), 2, "{trace:#?}");
1817 let no_route = "no route from this host (Network is unreachable)";
1818 let root = &trace.hops[0];
1819 assert_eq!(root.server, "d.root.test.");
1820 assert_eq!(
1821 root.failed_servers,
1822 vec![
1823 format!("a.root.test. (192.0.2.1): {no_route}"),
1824 format!("b.root.test. (192.0.2.2): {no_route}"),
1825 format!("c.root.test. (192.0.2.3): {no_route}"),
1826 ]
1827 );
1828 let auth = &trace.hops[1];
1829 assert_eq!(auth.server, "ns1.example.test.");
1830 assert_eq!(auth.address, "127.0.0.1", "the server's next address");
1831 assert_eq!(
1832 auth.failed_servers,
1833 vec![format!("ns1.example.test. (192.0.2.4): {no_route}")]
1834 );
1835 assert_eq!(trace.answers.len(), 1);
1836 }
1837
1838 #[tokio::test]
1839 async fn ipv6_only_servers_without_local_ipv6_do_not_use_up_the_level() {
1840 let v6_only: Vec<IpAddr> = (1..=3)
1845 .map(|i| IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, i)))
1846 .collect();
1847 let root =
1848 spawn_mock_dns_fn(|_, _| delegation("example.test", &["ns1.example.test"])).await;
1849 let auth = spawn_mock_dns_fn(|_, _| {
1850 MockReply::AuthoritativeAnswer(vec![a_rdata(Ipv4Addr::new(192, 0, 2, 50))])
1851 })
1852 .await;
1853 let trace = DnsTracer::new()
1854 .with_timeout(Duration::from_millis(500))
1855 .allowing_private_hosts()
1856 .with_root_hints(&[
1857 ("a.root.test", v6_only[0]),
1858 ("b.root.test", v6_only[1]),
1859 ("c.root.test", v6_only[2]),
1860 ("d.root.test", LOOPBACK),
1861 ])
1862 .with_port_map(HashMap::from([
1863 ("d.root.test".to_string(), root),
1864 ("ns1.example.test".to_string(), auth),
1865 ]))
1866 .with_unroutable(&v6_only, address_family_unsupported)
1867 .trace("www.example.test", RecordType::A)
1868 .await
1869 .unwrap();
1870
1871 assert!(trace.error.is_none(), "{trace:#?}");
1872 assert_eq!(trace.hops.len(), 2, "{trace:#?}");
1873 let no_socket = format!("no route from this host ({})", address_family_unsupported());
1874 let root = &trace.hops[0];
1875 assert_eq!(root.server, "d.root.test.");
1876 assert_eq!(
1877 root.failed_servers,
1878 vec![
1879 format!("a.root.test. (2001:db8::1): {no_socket}"),
1880 format!("b.root.test. (2001:db8::2): {no_socket}"),
1881 format!("c.root.test. (2001:db8::3): {no_socket}"),
1882 ]
1883 );
1884 assert_eq!(trace.answers.len(), 1);
1885 }
1886
1887 #[tokio::test]
1888 async fn a_lame_servers_empty_reply_falls_back_to_the_next_server() {
1889 let root = spawn_mock_dns_fn(|_, _| {
1890 delegation("example.test", &["ns1.example.test", "ns2.example.test"])
1891 })
1892 .await;
1893 let lame = spawn_mock_dns_fn(|_, _| MockReply::NoData).await;
1895 let auth = spawn_mock_dns_fn(|_, _| {
1896 MockReply::AuthoritativeAnswer(vec![a_rdata(Ipv4Addr::new(192, 0, 2, 60))])
1897 })
1898 .await;
1899 let trace = tracer(
1900 &["a.root.test"],
1901 &[
1902 ("a.root.test", root),
1903 ("ns1.example.test", lame),
1904 ("ns2.example.test", auth),
1905 ],
1906 )
1907 .trace("www.example.test", RecordType::A)
1908 .await
1909 .unwrap();
1910
1911 assert!(trace.error.is_none(), "{trace:#?}");
1912 assert_eq!(trace.hops.len(), 2);
1913 let last = &trace.hops[1];
1914 assert_eq!(last.server, "ns2.example.test.");
1915 assert!(last.authoritative);
1916 assert_eq!(
1917 last.failed_servers,
1918 vec![
1919 "ns1.example.test. (127.0.0.1): empty non-authoritative response (no answer, referral or SOA)"
1920 ]
1921 );
1922 assert_eq!(trace.status, DnsStatus::NoError);
1923 assert_eq!(trace.answers.len(), 1);
1924 }
1925
1926 #[tokio::test]
1927 async fn a_zone_of_lame_servers_is_an_error_not_a_negative_answer() {
1928 let root = spawn_mock_dns_fn(|_, _| {
1929 delegation("example.test", &["ns1.example.test", "ns2.example.test"])
1930 })
1931 .await;
1932 let empty = spawn_mock_dns_fn(|_, _| MockReply::NoData).await;
1933 let bare_nxdomain = spawn_mock_dns_fn(|_, _| MockReply::NxDomain).await;
1935 let trace = tracer(
1936 &["a.root.test"],
1937 &[
1938 ("a.root.test", root),
1939 ("ns1.example.test", empty),
1940 ("ns2.example.test", bare_nxdomain),
1941 ],
1942 )
1943 .trace("www.example.test", RecordType::A)
1944 .await
1945 .unwrap();
1946
1947 assert_eq!(trace.hops.len(), 2);
1948 let last = &trace.hops[1];
1949 assert_eq!(last.server, "ns2.example.test.");
1950 assert_eq!(last.status, DnsStatus::NxDomain);
1951 assert_eq!(
1952 last.failed_servers,
1953 vec![
1954 "ns1.example.test. (127.0.0.1): empty non-authoritative response (no answer, referral or SOA)"
1955 ]
1956 );
1957 let error = trace
1958 .error
1959 .expect("a lame zone proves nothing about the name");
1960 assert!(error.contains("example.test."), "{error}");
1961 assert!(error.contains("NXDOMAIN without an SOA"), "{error}");
1962 }
1963
1964 #[tokio::test]
1965 async fn upward_referral_from_every_server_stops_the_walk() {
1966 let root = spawn_mock_dns_fn(|_, _| delegation("test", &["ns1.nic.test"])).await;
1967 let tld = spawn_mock_dns_fn(|_, _| delegation(".", &["a.root.test"])).await;
1969 let trace = tracer(
1970 &["a.root.test"],
1971 &[("a.root.test", root), ("ns1.nic.test", tld)],
1972 )
1973 .trace("www.example.test", RecordType::A)
1974 .await
1975 .unwrap();
1976
1977 assert_eq!(trace.hops.len(), 2);
1978 assert_eq!(trace.hops[1].referral_zone.as_deref(), Some("."));
1979 assert_eq!(trace.status, DnsStatus::NoError);
1980 assert!(trace.answers.is_empty());
1981 let error = trace.error.expect("an upward referral is an error");
1982 assert!(error.contains("upward"), "{error}");
1983 assert!(error.contains("ns1.nic.test."), "{error}");
1984 }
1985
1986 #[tokio::test]
1990 async fn upward_referral_from_a_lame_server_asks_the_next() {
1991 let root =
1992 spawn_mock_dns_fn(|_, _| delegation("test", &["ns1.nic.test", "ns2.nic.test"])).await;
1993 let lame = spawn_mock_dns_fn(|_, _| delegation(".", &["a.root.test"])).await;
1994 let healthy =
1995 spawn_mock_dns_fn(|_, _| delegation("example.test", &["ns1.example.test"])).await;
1996 let auth =
1997 spawn_mock_dns_fn(|_, _| MockReply::AuthoritativeAnswer(vec![a_rdata([192, 0, 2, 1])]))
1998 .await;
1999 let trace = tracer(
2000 &["a.root.test"],
2001 &[
2002 ("a.root.test", root),
2003 ("ns1.nic.test", lame),
2004 ("ns2.nic.test", healthy),
2005 ("ns1.example.test", auth),
2006 ],
2007 )
2008 .trace("www.example.test", RecordType::A)
2009 .await
2010 .unwrap();
2011
2012 assert!(trace.error.is_none(), "{:?}", trace.error);
2013 assert_eq!(trace.hops.len(), 3);
2014 let tld_hop = &trace.hops[1];
2015 assert_eq!(tld_hop.server, "ns2.nic.test.");
2016 assert_eq!(tld_hop.referral_zone.as_deref(), Some("example.test."));
2017 assert!(
2018 tld_hop
2019 .failed_servers
2020 .iter()
2021 .any(|f| f.starts_with("ns1.nic.test.") && f.contains("referred upward")),
2022 "{:?}",
2023 tld_hop.failed_servers
2024 );
2025 assert_eq!(trace.answers.len(), 1);
2026 }
2027
2028 #[tokio::test]
2029 async fn sideways_referral_stops_the_walk() {
2030 let root = spawn_mock_dns_fn(|_, _| delegation("test", &["ns1.nic.test"])).await;
2031 let tld = spawn_mock_dns_fn(|_, _| delegation("other.test", &["ns1.other.test"])).await;
2032 let trace = tracer(
2033 &["a.root.test"],
2034 &[("a.root.test", root), ("ns1.nic.test", tld)],
2035 )
2036 .trace("www.example.test", RecordType::A)
2037 .await
2038 .unwrap();
2039
2040 assert_eq!(trace.hops.len(), 2);
2041 assert_eq!(trace.hops[1].referral_zone.as_deref(), Some("other.test."));
2042 let error = trace.error.expect("a sideways referral is an error");
2043 assert!(error.contains("sideways"), "{error}");
2044 }
2045
2046 #[tokio::test]
2047 async fn walk_gives_up_after_the_hop_limit() {
2048 let labels: Vec<String> = (1..=20)
2051 .map(|i| format!("l{i}"))
2052 .chain(["test".into()])
2053 .collect();
2054 let qname = labels.join(".");
2055 let zone_at = |depth: usize| labels[labels.len() - depth..].join(".");
2056 let zones: Vec<String> = (1..=labels.len()).map(zone_at).collect();
2057
2058 let mut depth = 0;
2059 let chain = zones.clone();
2060 let server = spawn_mock_dns_fn(move |_, _| {
2061 let zone = chain[depth.min(chain.len() - 1)].clone();
2062 depth += 1;
2063 MockReply::Delegation {
2064 servers: vec![(format!("ns.{zone}"), vec![LOOPBACK])],
2065 zone,
2066 }
2067 })
2068 .await;
2069 let ns_hosts: Vec<String> = zones.iter().map(|zone| format!("ns.{zone}")).collect();
2070 let mut ports: Vec<(&str, u16)> = ns_hosts.iter().map(|h| (h.as_str(), server)).collect();
2071 ports.push(("a.root.test", server));
2072
2073 let trace = tracer(&["a.root.test"], &ports)
2074 .trace(&qname, RecordType::A)
2075 .await
2076 .unwrap();
2077
2078 assert_eq!(trace.hops.len(), MAX_HOPS);
2079 let error = trace.error.expect("the walk must give up");
2080 assert!(error.contains(&MAX_HOPS.to_string()), "{error}");
2081 assert_eq!(trace.hops[0].zone, ".");
2083 assert_eq!(trace.hops[1].zone, "test.");
2084 assert_eq!(trace.hops[2].zone, "l20.test.");
2085 }
2086
2087 #[tokio::test]
2088 async fn the_whole_walk_is_held_to_one_deadline() {
2089 let timeout = Duration::from_millis(100);
2094 let depth = TRACE_BUDGET_TIMEOUTS as usize + 2;
2095 let labels: Vec<String> = (1..=depth)
2096 .map(|i| format!("l{i}"))
2097 .chain(["test".into()])
2098 .collect();
2099 let qname = labels.join(".");
2100 let zones: Vec<String> = (1..=labels.len())
2101 .map(|n| labels[labels.len() - n..].join("."))
2102 .collect();
2103
2104 let dark = spawn_mock_dns(MockMode::Ignore).await;
2105 let mut asked = 0;
2106 let chain = zones.clone();
2107 let live = spawn_mock_dns_fn(move |_, _| {
2108 let reply = match chain.get(asked) {
2109 Some(zone) => MockReply::Delegation {
2110 zone: zone.clone(),
2111 servers: vec![
2112 (format!("ns1.{zone}"), vec![LOOPBACK]),
2113 (format!("ns2.{zone}"), vec![LOOPBACK]),
2114 ],
2115 },
2116 None => MockReply::AuthoritativeAnswer(vec![a_rdata(Ipv4Addr::new(192, 0, 2, 40))]),
2117 };
2118 asked += 1;
2119 reply
2120 })
2121 .await;
2122 let ns_hosts: Vec<(String, u16)> = zones
2123 .iter()
2124 .flat_map(|zone| [(format!("ns1.{zone}"), dark), (format!("ns2.{zone}"), live)])
2125 .collect();
2126 let mut ports: Vec<(&str, u16)> = ns_hosts.iter().map(|(h, p)| (h.as_str(), *p)).collect();
2127 ports.extend([("a.root.test", dark), ("b.root.test", live)]);
2128
2129 let started = Instant::now();
2130 let trace = tracer(&["a.root.test", "b.root.test"], &ports)
2131 .with_timeout(timeout)
2132 .trace(&qname, RecordType::A)
2133 .await
2134 .unwrap();
2135 let elapsed = started.elapsed();
2136
2137 let budget = timeout * TRACE_BUDGET_TIMEOUTS;
2138 assert!(elapsed < budget + timeout, "took {elapsed:?}");
2139 let error = trace.error.expect("the walk must run out of time");
2140 assert!(error.starts_with("gave up after 600ms"), "{error}");
2141 assert!(!trace.hops.is_empty());
2144 assert!(trace.hops.iter().all(|hop| hop.referral_zone.is_some()));
2145 assert!(trace.answers.is_empty());
2146 let asking = zones[trace.hops.len() - 1].clone();
2147 assert!(
2148 error.ends_with(&format!("nameservers of {asking}.")),
2149 "{error}"
2150 );
2151 }
2152
2153 #[tokio::test]
2154 async fn unresponsive_server_falls_back_to_the_next_one() {
2155 let dark = spawn_mock_dns(MockMode::Ignore).await;
2156 let live = spawn_mock_dns_fn(|_, _| {
2157 MockReply::AuthoritativeAnswer(vec![a_rdata(Ipv4Addr::new(192, 0, 2, 30))])
2158 })
2159 .await;
2160 let trace = tracer(
2161 &["a.root.test", "b.root.test"],
2162 &[("a.root.test", dark), ("b.root.test", live)],
2163 )
2164 .trace("example.test", RecordType::A)
2165 .await
2166 .unwrap();
2167
2168 assert_eq!(trace.hops.len(), 1);
2169 let hop = &trace.hops[0];
2170 assert_eq!(hop.server, "b.root.test.");
2171 assert_eq!(
2172 hop.failed_servers,
2173 vec!["a.root.test. (127.0.0.1): timed out"]
2174 );
2175 assert_eq!(trace.answers.len(), 1);
2176 assert!(trace.error.is_none());
2177 }
2178
2179 #[tokio::test]
2180 async fn every_server_refusing_ends_with_the_refusal_as_the_final_hop() {
2181 let root = spawn_mock_dns_fn(|_, _| {
2182 delegation("example.test", &["ns1.example.test", "ns2.example.test"])
2183 })
2184 .await;
2185 let refused = spawn_mock_dns_fn(|_, _| MockReply::Rcode(ResponseCode::Refused)).await;
2186 let servfail = spawn_mock_dns_fn(|_, _| MockReply::Rcode(ResponseCode::ServFail)).await;
2187 let trace = tracer(
2188 &["a.root.test"],
2189 &[
2190 ("a.root.test", root),
2191 ("ns1.example.test", servfail),
2192 ("ns2.example.test", refused),
2193 ],
2194 )
2195 .trace("www.example.test", RecordType::A)
2196 .await
2197 .unwrap();
2198
2199 assert_eq!(trace.hops.len(), 2);
2200 let last = &trace.hops[1];
2201 assert_eq!(last.server, "ns2.example.test.");
2202 assert_eq!(last.status, DnsStatus::Refused);
2203 assert_eq!(
2204 last.failed_servers,
2205 vec!["ns1.example.test. (127.0.0.1): SERVFAIL"]
2206 );
2207 assert_eq!(trace.status, DnsStatus::Refused);
2208 let error = trace.error.expect("no usable response is an error");
2209 assert!(error.contains("example.test."), "{error}");
2210 assert!(
2211 error.contains("REFUSED") && error.contains("SERVFAIL"),
2212 "{error}"
2213 );
2214 }
2215
2216 #[tokio::test]
2217 async fn no_responsive_root_is_an_error() {
2218 let dark = spawn_mock_dns(MockMode::Ignore).await;
2219 let err = tracer(&["a.root.test"], &[("a.root.test", dark)])
2220 .trace("example.test", RecordType::A)
2221 .await
2222 .expect_err("nothing to trace without a root response");
2223 assert!(matches!(err, SeerError::DnsError(_)), "{err:?}");
2224 assert!(err.to_string().contains("timed out"), "{err}");
2225 }
2226
2227 #[tokio::test]
2228 async fn ptr_trace_of_an_ip_literal_uses_the_reverse_name() {
2229 let asked = Arc::new(Mutex::new(Vec::new()));
2230 let log = Arc::clone(&asked);
2231 let root = spawn_mock_dns_fn(move |qname, _| {
2232 log.lock().unwrap().push(qname.to_string());
2233 MockReply::AuthoritativeAnswer(vec![HickoryRData::PTR(wire::PTR(fq_name(
2234 "ptr.seer.test",
2235 )))])
2236 })
2237 .await;
2238 let trace = tracer(&["a.root.test"], &[("a.root.test", root)])
2239 .trace("192.0.2.1", RecordType::PTR)
2240 .await
2241 .unwrap();
2242
2243 assert_eq!(trace.name, "1.2.0.192.in-addr.arpa");
2244 assert_eq!(trace.answers.len(), 1);
2245 assert_eq!(trace.answers[0].name, "1.2.0.192.in-addr.arpa");
2246 assert_eq!(trace.answers[0].record_type, RecordType::PTR);
2247 assert!(asked
2248 .lock()
2249 .unwrap()
2250 .iter()
2251 .all(|q| q == "1.2.0.192.in-addr.arpa"));
2252 }
2253
2254 #[tokio::test]
2255 async fn truncated_reply_is_retried_over_tcp() {
2256 let mut served = 0;
2257 let root = spawn_mock_dns_fn_with_tcp(move |_, _| {
2258 served += 1;
2259 if served == 1 {
2260 MockReply::Truncated
2261 } else {
2262 MockReply::AuthoritativeAnswer(vec![a_rdata(Ipv4Addr::new(192, 0, 2, 40))])
2263 }
2264 })
2265 .await;
2266 let trace = tracer(&["a.root.test"], &[("a.root.test", root)])
2267 .trace("example.test", RecordType::A)
2268 .await
2269 .unwrap();
2270
2271 assert_eq!(trace.hops.len(), 1);
2272 assert!(trace.hops[0].authoritative);
2273 assert_eq!(trace.answers.len(), 1, "{trace:#?}");
2274 }
2275
2276 #[tokio::test]
2277 async fn answers_convert_the_service_binding_and_child_dnssec_types() {
2278 use hickory_resolver::proto::dnssec::rdata::{DNSSECRData, CDS};
2282 use hickory_resolver::proto::dnssec::{Algorithm, DigestType};
2283 use hickory_resolver::proto::rr::rdata::svcb::{Alpn, SvcParamKey, SvcParamValue, SVCB};
2284
2285 let tracer = three_level_tracer(|qname| match qname {
2286 "www.example.test" => {
2287 MockReply::AuthoritativeAnswer(vec![HickoryRData::HTTPS(wire::HTTPS(SVCB::new(
2288 1,
2289 Name::root(),
2290 vec![(
2291 SvcParamKey::Alpn,
2292 SvcParamValue::Alpn(Alpn(vec!["h2".to_string()])),
2293 )],
2294 )))])
2295 }
2296 _ => MockReply::AuthoritativeAnswer(vec![HickoryRData::DNSSEC(DNSSECRData::CDS(
2297 CDS::new(
2298 2371,
2299 Some(Algorithm::ED25519),
2300 DigestType::SHA256,
2301 vec![0xAB],
2302 ),
2303 ))]),
2304 })
2305 .await;
2306
2307 let https = tracer
2308 .trace("www.example.test", RecordType::HTTPS)
2309 .await
2310 .unwrap();
2311 assert_eq!(https.answers.len(), 1, "{https:#?}");
2312 assert_eq!(https.answers[0].name, "www.example.test");
2313 assert_eq!(https.answers[0].record_type, RecordType::HTTPS);
2314 assert_eq!(https.answers[0].data.to_string(), "1 . alpn=\"h2\"");
2315 assert_eq!(https.hops[2].answers, https.answers);
2316
2317 let cds = tracer.trace("example.test", RecordType::CDS).await.unwrap();
2318 assert_eq!(cds.answers.len(), 1, "{cds:#?}");
2319 assert_eq!(cds.answers[0].record_type, RecordType::CDS);
2320 assert_eq!(cds.answers[0].data.to_string(), "2371 15 2 AB");
2321 }
2322
2323 #[tokio::test]
2324 async fn at_most_three_servers_are_asked_per_level() {
2325 let roots = ["a.root.test", "b.root.test", "c.root.test", "d.root.test"];
2328 let mut ports = Vec::new();
2329 let mut hits = Vec::new();
2330 for _ in roots {
2331 let count = Arc::new(Mutex::new(0usize));
2332 let log = Arc::clone(&count);
2333 ports.push(
2334 spawn_mock_dns_fn(move |_, _| {
2335 *log.lock().unwrap() += 1;
2336 MockReply::NoReply
2337 })
2338 .await,
2339 );
2340 hits.push(count);
2341 }
2342 let map: Vec<(&str, u16)> = roots.iter().copied().zip(ports).collect();
2343 let err = tracer(&roots, &map)
2344 .with_timeout(Duration::from_millis(200))
2345 .trace("example.test", RecordType::A)
2346 .await
2347 .expect_err("no root answered");
2348
2349 let message = err.to_string();
2350 for root in &roots[..3] {
2351 assert!(message.contains(root), "{message}");
2352 }
2353 assert!(!message.contains("d.root.test"), "{message}");
2354 let hits: Vec<usize> = hits.iter().map(|count| *count.lock().unwrap()).collect();
2355 assert!(hits[..3].iter().all(|&n| n > 0), "{hits:?}");
2356 assert_eq!(hits[3], 0, "the fourth server must not be asked: {hits:?}");
2357 }
2358
2359 #[tokio::test]
2360 async fn at_most_three_glueless_nameservers_are_looked_up_per_level() {
2361 let root = spawn_mock_dns_fn(|_, _| MockReply::Delegation {
2362 zone: "test".to_string(),
2363 servers: (1..=4)
2364 .map(|i| (format!("ns{i}.nowhere.test"), vec![]))
2365 .collect(),
2366 })
2367 .await;
2368 let looked_up = Arc::new(Mutex::new(std::collections::BTreeSet::new()));
2369 let log = Arc::clone(&looked_up);
2370 let recursive = spawn_mock_dns_fn(move |qname, _| {
2371 log.lock().unwrap().insert(qname.to_string());
2372 MockReply::NxDomain
2373 })
2374 .await;
2375 let trace = tracer(&["a.root.test"], &[("a.root.test", root)])
2376 .with_recursive_upstream(LOOPBACK, recursive)
2377 .trace("www.example.test", RecordType::A)
2378 .await
2379 .unwrap();
2380
2381 assert_eq!(trace.hops.len(), 1);
2382 let error = trace
2383 .error
2384 .expect("no nameserver of `test.` has an address");
2385 assert!(!error.contains("ns4.nowhere.test"), "{error}");
2386 let looked_up: Vec<String> = looked_up.lock().unwrap().iter().cloned().collect();
2387 assert_eq!(
2388 looked_up,
2389 ["ns1.nowhere.test", "ns2.nowhere.test", "ns3.nowhere.test"],
2390 "sorted referral order, capped at three"
2391 );
2392 }
2393
2394 #[tokio::test]
2395 async fn trace_rejects_any_and_bad_names_before_sending() {
2396 let tracer = tracer(&["a.root.test"], &[]);
2397 assert!(matches!(
2398 tracer.trace("example.test", RecordType::ANY).await,
2399 Err(SeerError::InvalidInput(_))
2400 ));
2401 assert!(tracer.trace("not a name", RecordType::A).await.is_err());
2402 }
2403
2404 #[tokio::test]
2405 #[ignore = "live network; run with --ignored or SEER_LIVE_TESTS=1"]
2406 async fn test_live_trace_example_com() {
2407 let trace = DnsTracer::new()
2408 .trace("example.com", RecordType::A)
2409 .await
2410 .unwrap();
2411 assert!(trace.error.is_none(), "{trace:#?}");
2412 assert!(trace.hops.len() >= 3, "{trace:#?}");
2413 assert_eq!(trace.hops[0].zone, ".");
2414 assert_eq!(trace.hops[1].zone, "com.");
2415 let last = trace.hops.last().unwrap();
2416 assert_eq!(last.zone, "example.com.");
2417 assert!(last.authoritative);
2418 assert_eq!(trace.status, DnsStatus::NoError);
2419 assert!(!trace.answers.is_empty());
2420 }
2421}