1use dns_types::protocol::types::*;
2use dns_types::zones::types::*;
3
4use crate::context::Context;
5use crate::util::types::*;
6
7const CNAME_QTYPE: QueryType = QueryType::Record(RecordType::CNAME);
9
10pub fn resolve_local<CT>(
32 context: &mut Context<'_, CT>,
33 question: &Question,
34) -> Result<LocalResolutionResult, ResolutionError> {
35 let _span = tracing::error_span!("resolve_local", %question).entered();
36
37 if context.at_recursion_limit() {
38 tracing::debug!("hit recursion limit");
39 return Err(ResolutionError::RecursionLimit);
40 }
41 if context.is_duplicate_question(question) {
42 tracing::debug!("hit duplicate question");
43 return Err(ResolutionError::DuplicateQuestion {
44 question: question.clone(),
45 });
46 }
47
48 let mut rrs_from_zone = Vec::new();
49
50 if let Some((zone, zone_result)) = context.zones.resolve(&question.name, question.qtype) {
54 let _zone_span = tracing::error_span!("zone", apex = %zone.get_apex().to_dotted_string(), is_authoritative = %zone.is_authoritative()).entered();
55
56 match zone_result {
57 ZoneResult::Answer { rrs } => {
72 context.metrics().zoneresult_answer(&rrs, zone, question);
73
74 if let Some(soa_rr) = zone.soa_rr() {
75 tracing::trace!("got authoritative answer");
76 return Ok(LocalResolutionResult::Done {
77 resolved: ResolvedRecord::Authoritative { rrs, soa_rr },
78 });
79 } else if question.qtype != QueryType::Wildcard && !rrs.is_empty() {
80 tracing::trace!("got non-authoritative answer");
81 return Ok(LocalResolutionResult::Done {
82 resolved: ResolvedRecord::NonAuthoritative { rrs, soa_rr: None },
83 });
84 } else {
85 tracing::trace!("got partial answer");
86 rrs_from_zone = rrs;
87 }
88 }
89 ZoneResult::CNAME { cname, rr } => {
102 context.metrics().zoneresult_cname(zone);
103
104 let mut rrs = vec![rr];
105 let cname_question = Question {
106 name: cname,
107 qtype: question.qtype,
108 qclass: question.qclass,
109 };
110
111 context.push_question(question);
112 let answer = match resolve_local(context, &cname_question) {
113 Ok(LocalResolutionResult::Done { resolved }) => match resolved {
114 ResolvedRecord::Authoritative {
115 rrs: mut cname_rrs,
116 soa_rr,
117 } => {
118 rrs.append(&mut cname_rrs);
119 tracing::trace!("got authoritative cname answer");
120 LocalResolutionResult::Done {
121 resolved: ResolvedRecord::Authoritative { rrs, soa_rr },
122 }
123 }
124 ResolvedRecord::AuthoritativeNameError { soa_rr } => {
125 tracing::trace!("got authoritative cname answer");
126 LocalResolutionResult::Done {
127 resolved: ResolvedRecord::Authoritative { rrs, soa_rr },
128 }
129 }
130 ResolvedRecord::NonAuthoritative {
131 rrs: mut cname_rrs,
132 soa_rr,
133 } => {
134 tracing::trace!("got non-authoritative cname answer");
135 rrs.append(&mut cname_rrs);
136 LocalResolutionResult::Done {
137 resolved: ResolvedRecord::NonAuthoritative { rrs, soa_rr },
138 }
139 }
140 },
141 Ok(LocalResolutionResult::Partial { rrs: mut cname_rrs }) => {
142 tracing::trace!("got partial cname answer");
143 rrs.append(&mut cname_rrs);
144 LocalResolutionResult::Partial { rrs }
145 }
146 Ok(LocalResolutionResult::CNAME {
147 rrs: mut cname_rrs,
148 cname_question,
149 }) => {
150 tracing::trace!("got incomplete cname answer");
151 rrs.append(&mut cname_rrs);
152 LocalResolutionResult::CNAME {
153 rrs,
154 cname_question,
155 }
156 }
157 _ => {
158 tracing::trace!("got incomplete cname answer");
159 LocalResolutionResult::CNAME {
160 rrs,
161 cname_question,
162 }
163 }
164 };
165 context.pop_question();
166 return Ok(answer);
167 }
168 ZoneResult::Delegation { ns_rrs } => {
175 tracing::trace!("got delegation");
176 context.metrics().zoneresult_delegation(zone);
177
178 if let Some(soa_rr) = zone.soa_rr() {
179 if ns_rrs.is_empty() {
180 tracing::warn!("got empty RRset from delegation");
181 return Err(ResolutionError::LocalDelegationMissingNS {
182 apex: zone.get_apex().clone(),
183 domain: question.name.clone(),
184 });
185 }
186
187 let name = ns_rrs[0].name.clone();
188 let mut hostnames = Vec::with_capacity(ns_rrs.len());
189 for rr in &ns_rrs {
190 if let RecordTypeWithData::NS { nsdname } = &rr.rtype_with_data {
191 hostnames.push(nsdname.clone());
192 } else {
193 tracing::warn!(rtype = %rr.rtype_with_data.rtype(), "got non-NS RR in a delegation");
194 }
195 }
196
197 return Ok(LocalResolutionResult::Delegation {
198 delegation: Nameservers { hostnames, name },
199 rrs: ns_rrs,
200 soa_rr: Some(soa_rr),
201 });
202 }
203 }
204 ZoneResult::NameError => {
211 tracing::trace!("got name error");
212 context.metrics().zoneresult_nameerror(zone);
213
214 if let Some(soa_rr) = zone.soa_rr() {
215 return Ok(LocalResolutionResult::Done {
216 resolved: ResolvedRecord::AuthoritativeNameError { soa_rr },
217 });
218 }
219 }
220 }
221 }
222
223 let mut rrs_from_cache = context.cache.get(&question.name, question.qtype);
241 if rrs_from_cache.is_empty() {
242 tracing::trace!(qtype = %question.qtype, "cache MISS");
243 context.metrics().cache_miss();
244 } else {
245 tracing::trace!(qtype = %question.qtype, "cache HIT");
246 context.metrics().cache_hit();
247 }
248
249 let mut final_cname = None;
250 if rrs_from_cache.is_empty() && question.qtype != CNAME_QTYPE {
251 let cache_cname_rrs = context.cache.get(&question.name, CNAME_QTYPE);
252 if cache_cname_rrs.is_empty() {
253 tracing::trace!(qtype = %CNAME_QTYPE, "cache MISS");
254 context.metrics().cache_miss();
255 } else {
256 tracing::trace!(qtype = %CNAME_QTYPE, "cache HIT");
257 context.metrics().cache_hit();
258 }
259
260 if !cache_cname_rrs.is_empty() {
261 let cname_rr = cache_cname_rrs[0].clone();
262 rrs_from_cache = vec![cname_rr.clone()];
263
264 if let RecordTypeWithData::CNAME { cname } = cname_rr.rtype_with_data {
265 context.push_question(question);
266 let resolved_cname = resolve_local(
267 context,
268 &Question {
269 name: cname.clone(),
270 qtype: question.qtype,
271 qclass: question.qclass,
272 },
273 );
274 context.pop_question();
275 match resolved_cname {
276 Ok(LocalResolutionResult::Done { resolved }) => {
277 rrs_from_cache.append(&mut resolved.rrs());
278 }
279 Ok(LocalResolutionResult::Partial { mut rrs }) => {
280 rrs_from_cache.append(&mut rrs);
281 }
282 Ok(LocalResolutionResult::CNAME {
283 mut rrs,
284 cname_question,
285 }) => {
286 rrs_from_cache.append(&mut rrs);
287 final_cname = Some(cname_question.name);
288 }
289 _ => {
290 final_cname = Some(cname);
291 }
292 }
293 } else {
294 tracing::warn!(rtype = %cname_rr.rtype_with_data.rtype(), "got non-CNAME RR from cache");
295 return Err(ResolutionError::CacheTypeMismatch {
296 query: CNAME_QTYPE,
297 result: cname_rr.rtype_with_data.rtype(),
298 });
299 }
300 }
301 }
302
303 let mut rrs = rrs_from_zone;
304 prioritising_merge(&mut rrs, rrs_from_cache);
305
306 if rrs.is_empty() {
307 Err(ResolutionError::DeadEnd {
308 question: question.clone(),
309 })
310 } else if let Some(cname) = final_cname {
311 Ok(LocalResolutionResult::CNAME {
312 rrs,
313 cname_question: Question {
314 name: cname,
315 qtype: question.qtype,
316 qclass: question.qclass,
317 },
318 })
319 } else if question.qtype == QueryType::Wildcard {
320 Ok(LocalResolutionResult::Partial { rrs })
321 } else {
322 Ok(LocalResolutionResult::Done {
323 resolved: ResolvedRecord::NonAuthoritative { rrs, soa_rr: None },
324 })
325 }
326}
327
328#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)]
330pub enum LocalResolutionResult {
331 Done {
332 resolved: ResolvedRecord,
333 },
334 Partial {
335 rrs: Vec<ResourceRecord>,
336 },
337 Delegation {
338 rrs: Vec<ResourceRecord>,
339 soa_rr: Option<ResourceRecord>,
340 delegation: Nameservers,
341 },
342 CNAME {
343 rrs: Vec<ResourceRecord>,
344 cname_question: Question,
345 },
346}
347
348impl From<LocalResolutionResult> for ResolvedRecord {
349 fn from(lsr: LocalResolutionResult) -> Self {
350 match lsr {
351 LocalResolutionResult::Done { resolved } => resolved,
352 LocalResolutionResult::Partial { rrs } => {
353 ResolvedRecord::NonAuthoritative { rrs, soa_rr: None }
354 }
355 LocalResolutionResult::Delegation { rrs, soa_rr, .. } => {
356 if let Some(soa_rr) = soa_rr {
357 ResolvedRecord::Authoritative { rrs, soa_rr }
358 } else {
359 ResolvedRecord::NonAuthoritative { rrs, soa_rr: None }
360 }
361 }
362 LocalResolutionResult::CNAME { rrs, .. } => {
363 ResolvedRecord::NonAuthoritative { rrs, soa_rr: None }
364 }
365 }
366 }
367}
368
369#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
372pub struct AuthoritativeNameError {
373 pub soa_rr: ResourceRecord,
374}
375
376#[cfg(test)]
377mod tests {
378 use dns_types::protocol::types::test_util::*;
379 use dns_types::zones::types::*;
380 use std::net::Ipv4Addr;
381
382 use super::*;
383 use crate::cache::test_util::*;
384 use crate::cache::SharedCache;
385
386 #[test]
387 fn resolve_local_is_authoritative_for_zones_with_soa() {
388 assert_eq!(
389 test_resolve_local("www.authoritative.example.com.", QueryType::Wildcard),
390 Ok(LocalResolutionResult::Done {
391 resolved: ResolvedRecord::Authoritative {
392 rrs: vec![a_record(
393 "www.authoritative.example.com.",
394 Ipv4Addr::new(1, 1, 1, 1)
395 )],
396 soa_rr: soa_rr(),
397 },
398 })
399 );
400 }
401
402 #[test]
403 fn resolve_local_is_partial_for_zones_without_soa() {
404 assert_eq!(
405 test_resolve_local("a.example.com.", QueryType::Wildcard),
406 Ok(LocalResolutionResult::Partial {
407 rrs: vec![a_record("a.example.com.", Ipv4Addr::new(1, 1, 1, 1))],
408 })
409 );
410 }
411
412 #[test]
413 fn resolve_local_is_partial_for_cache() {
414 let rr = a_record("cached.example.com.", Ipv4Addr::new(1, 1, 1, 1));
415
416 let cache = SharedCache::new();
417 cache.insert(&rr);
418
419 if let Ok(LocalResolutionResult::Partial { rrs }) =
420 test_resolve_local_with_cache("cached.example.com.", &cache, QueryType::Wildcard)
421 {
422 assert_cache_response(&rr, &rrs);
423 } else {
424 panic!("expected non-authoritative answer");
425 }
426 }
427
428 #[test]
429 fn resolve_local_returns_all_record_types() {
430 if let Ok(LocalResolutionResult::Done {
431 resolved:
432 ResolvedRecord::Authoritative {
433 rrs: mut actual_rrs,
434 soa_rr: actual_soa_rr,
435 },
436 }) = test_resolve_local(
437 "cname-and-a.authoritative.example.com.",
438 QueryType::Wildcard,
439 ) {
440 actual_rrs.sort();
443
444 assert_eq!(
445 actual_rrs,
446 vec![
447 a_record(
448 "cname-and-a.authoritative.example.com.",
449 Ipv4Addr::new(1, 1, 1, 1)
450 ),
451 cname_record(
452 "cname-and-a.authoritative.example.com.",
453 "www.authoritative.example.com."
454 ),
455 ]
456 );
457 assert_eq!(actual_soa_rr, soa_rr());
458 } else {
459 panic!("expected authoritative answer");
460 }
461 }
462
463 #[test]
464 fn resolve_local_prefers_authoritative_zones() {
465 let cache = SharedCache::new();
466 cache.insert(&a_record(
467 "www.authoritative.example.com.",
468 Ipv4Addr::new(8, 8, 8, 8),
469 ));
470
471 assert_eq!(
472 test_resolve_local_with_cache(
473 "www.authoritative.example.com.",
474 &cache,
475 QueryType::Wildcard
476 ),
477 Ok(LocalResolutionResult::Done {
478 resolved: ResolvedRecord::Authoritative {
479 rrs: vec![a_record(
480 "www.authoritative.example.com.",
481 Ipv4Addr::new(1, 1, 1, 1)
482 )],
483 soa_rr: soa_rr(),
484 },
485 })
486 );
487 }
488
489 #[test]
490 fn resolve_local_combines_nonauthoritative_zones_with_cache() {
491 let zone_rr = a_record("a.example.com.", Ipv4Addr::new(1, 1, 1, 1));
492 let cache_rr = cname_record("a.example.com.", "b.example.com.");
493
494 let cache = SharedCache::new();
495 cache.insert(&cache_rr);
496
497 if let Ok(LocalResolutionResult::Partial { rrs }) =
498 test_resolve_local_with_cache("a.example.com.", &cache, QueryType::Wildcard)
499 {
500 assert_eq!(2, rrs.len());
501 assert_eq!(zone_rr, rrs[0]);
502 assert_cache_response(&cache_rr, &[rrs[1].clone()]);
503 } else {
504 panic!("expected non-authoritative answer");
505 }
506 }
507
508 #[test]
509 fn resolve_local_overrides_cache_with_nonauthoritative_zones() {
510 let zone_rr = a_record("a.example.com.", Ipv4Addr::new(1, 1, 1, 1));
511 let cache_rr = a_record("a.example.com.", Ipv4Addr::new(8, 8, 8, 8));
512
513 let cache = SharedCache::new();
514 cache.insert(&cache_rr);
515
516 assert_eq!(
517 test_resolve_local("a.example.com.", QueryType::Wildcard),
518 Ok(LocalResolutionResult::Partial { rrs: vec![zone_rr] })
519 );
520 }
521
522 #[test]
523 fn resolve_local_expands_cnames_from_zone() {
524 assert_eq!(
525 test_resolve_local(
526 "cname-authoritative.authoritative.example.com.",
527 QueryType::Record(RecordType::A)
528 ),
529 Ok(LocalResolutionResult::Done {
530 resolved: ResolvedRecord::Authoritative {
531 rrs: vec![
532 cname_record(
533 "cname-authoritative.authoritative.example.com.",
534 "www.authoritative.example.com."
535 ),
536 a_record("www.authoritative.example.com.", Ipv4Addr::new(1, 1, 1, 1)),
537 ],
538 soa_rr: soa_rr(),
539 },
540 }),
541 );
542 }
543
544 #[test]
545 fn resolve_local_expands_cnames_from_cache() {
546 let cname_rr1 = cname_record("cname-1.example.com.", "cname-2.example.com.");
547 let cname_rr2 = cname_record("cname-2.example.com.", "a.example.com.");
548 let a_rr = a_record("a.example.com.", Ipv4Addr::new(1, 1, 1, 1));
549
550 let cache = SharedCache::new();
551 cache.insert(&cname_rr1);
552 cache.insert(&cname_rr2);
553
554 if let Ok(LocalResolutionResult::Done {
555 resolved: ResolvedRecord::NonAuthoritative { rrs, soa_rr: None },
556 }) = test_resolve_local_with_cache(
557 "cname-1.example.com.",
558 &cache,
559 QueryType::Record(RecordType::A),
560 ) {
561 assert_eq!(3, rrs.len());
562 assert_cache_response(&cname_rr1, &[rrs[0].clone()]);
563 assert_cache_response(&cname_rr2, &[rrs[1].clone()]);
564 assert_cache_response(&a_rr, &[rrs[2].clone()]);
565 } else {
566 panic!("expected non-authoritative answer");
567 }
568 }
569
570 #[test]
571 fn resolve_local_handles_cname_cycle() {
572 let qtype = QueryType::Record(RecordType::A);
573
574 assert_eq!(
575 test_resolve_local("cname-cycle-a.example.com.", qtype),
576 Ok(LocalResolutionResult::CNAME {
577 rrs: vec![
578 cname_record("cname-cycle-a.example.com.", "cname-cycle-b.example.com."),
579 cname_record("cname-cycle-b.example.com.", "cname-cycle-a.example.com."),
580 ],
581 cname_question: Question {
582 name: domain("cname-cycle-a.example.com."),
583 qclass: QueryClass::Wildcard,
584 qtype,
585 },
586 }),
587 );
588 }
589
590 #[test]
591 fn resolve_local_propagates_cname_nonauthority() {
592 assert_eq!(
593 test_resolve_local(
594 "cname-nonauthoritative.authoritative.example.com.",
595 QueryType::Record(RecordType::A)
596 ),
597 Ok(LocalResolutionResult::Done {
598 resolved: ResolvedRecord::NonAuthoritative {
599 rrs: vec![
600 cname_record(
601 "cname-nonauthoritative.authoritative.example.com.",
602 "a.example.com."
603 ),
604 a_record("a.example.com.", Ipv4Addr::new(1, 1, 1, 1)),
605 ],
606 soa_rr: None,
607 },
608 }),
609 );
610 }
611
612 #[test]
613 fn resolve_local_uses_most_specific_cname_authority() {
614 assert_eq!(
615 test_resolve_local(
616 "cname.authoritative-2.example.com.",
617 QueryType::Record(RecordType::A)
618 ),
619 Ok(LocalResolutionResult::Done {
620 resolved: ResolvedRecord::Authoritative {
621 rrs: vec![
622 cname_record(
623 "cname.authoritative-2.example.com.",
624 "www.authoritative.example.com."
625 ),
626 a_record("www.authoritative.example.com.", Ipv4Addr::new(1, 1, 1, 1)),
627 ],
628 soa_rr: soa_rr(),
629 },
630 }),
631 );
632 }
633
634 #[test]
635 fn resolve_local_returns_cname_response_if_unable_to_fully_resolve() {
636 let qtype = QueryType::Record(RecordType::A);
637
638 assert_eq!(
639 test_resolve_local("trailing-cname.example.com.", qtype),
640 Ok(LocalResolutionResult::CNAME {
641 rrs: vec![cname_record(
642 "trailing-cname.example.com.",
643 "somewhere-else.example.com."
644 )],
645 cname_question: Question {
646 name: domain("somewhere-else.example.com."),
647 qclass: QueryClass::Wildcard,
648 qtype,
649 },
650 })
651 );
652 }
653
654 #[test]
655 fn resolve_local_delegates_from_authoritative_zone() {
656 assert_eq!(
657 test_resolve_local(
658 "www.delegated.authoritative.example.com.",
659 QueryType::Wildcard
660 ),
661 Ok(LocalResolutionResult::Delegation {
662 rrs: vec![ns_record(
663 "delegated.authoritative.example.com.",
664 "ns.delegated.authoritative.example.com."
665 )],
666 soa_rr: Some(soa_rr()),
667 delegation: Nameservers {
668 name: domain("delegated.authoritative.example.com."),
669 hostnames: vec![domain("ns.delegated.authoritative.example.com.")],
670 }
671 })
672 );
673 }
674
675 #[test]
676 fn resolve_local_does_not_delegate_from_nonauthoritative_zone() {
677 let question = Question {
678 name: domain("www.delegated.example.com."),
679 qtype: QueryType::Wildcard,
680 qclass: QueryClass::Wildcard,
681 };
682
683 assert_eq!(
684 resolve_local(
685 &mut Context::new((), &zones(), &SharedCache::new(), 10),
686 &question
687 ),
688 Err(ResolutionError::DeadEnd {
689 question: question.clone()
690 })
691 );
692 }
693
694 #[test]
695 fn resolve_local_nameerrors_from_authoritative_zone() {
696 assert_eq!(
697 test_resolve_local(
698 "no.such.name.authoritative.example.com.",
699 QueryType::Wildcard
700 ),
701 Ok(LocalResolutionResult::Done {
702 resolved: ResolvedRecord::AuthoritativeNameError { soa_rr: soa_rr() },
703 }),
704 );
705 }
706
707 #[test]
708 fn resolve_local_does_not_nameerror_from_nonauthoritative_zone() {
709 let question = Question {
710 name: domain("no.such.name.example.com."),
711 qtype: QueryType::Wildcard,
712 qclass: QueryClass::Wildcard,
713 };
714
715 assert_eq!(
716 resolve_local(
717 &mut Context::new((), &zones(), &SharedCache::new(), 10),
718 &question,
719 ),
720 Err(ResolutionError::DeadEnd {
721 question: question.clone()
722 }),
723 );
724 }
725
726 fn test_resolve_local(
727 name: &str,
728 qtype: QueryType,
729 ) -> Result<LocalResolutionResult, ResolutionError> {
730 test_resolve_local_with_cache(name, &SharedCache::new(), qtype)
731 }
732
733 fn test_resolve_local_with_cache(
734 name: &str,
735 cache: &SharedCache,
736 qtype: QueryType,
737 ) -> Result<LocalResolutionResult, ResolutionError> {
738 resolve_local(
739 &mut Context::new((), &zones(), cache, 10),
740 &Question {
741 name: domain(name),
742 qclass: QueryClass::Wildcard,
743 qtype,
744 },
745 )
746 }
747
748 fn soa_rr() -> ResourceRecord {
749 zones()
750 .get(&domain("authoritative.example.com."))
751 .unwrap()
752 .soa_rr()
753 .unwrap()
754 }
755
756 #[allow(clippy::missing_panics_doc)]
757 fn zones() -> Zones {
758 let mut zones = Zones::new();
761
762 zones.insert(
763 Zone::deserialise(
764 r"
765$ORIGIN example.com.
766
767a 300 IN A 1.1.1.1
768blocked 300 IN A 0.0.0.0
769cname-cycle-a 300 IN CNAME cname-cycle-b
770cname-cycle-b 300 IN CNAME cname-cycle-a
771delegated 300 IN NS ns.delegated
772trailing-cname 300 IN CNAME somewhere-else
773",
774 )
775 .unwrap(),
776 );
777
778 zones.insert(
779 Zone::deserialise(
780 r"
781$ORIGIN authoritative.example.com.
782
783@ IN SOA mname rname 1 30 30 30 30
784
785www 300 IN A 1.1.1.1
786cname-and-a 300 IN A 1.1.1.1
787cname-and-a 300 IN CNAME www
788cname-authoritative 300 IN CNAME www
789cname-nonauthoritative 300 IN CNAME a.example.com.
790delegated 300 IN NS ns.delegated
791",
792 )
793 .unwrap(),
794 );
795
796 zones.insert(
797 Zone::deserialise(
798 r"
799$ORIGIN authoritative-2.example.com.
800
801@ IN SOA mname rname 1 30 30 30 30
802
803cname 300 IN CNAME www.authoritative.example.com.
804",
805 )
806 .unwrap(),
807 );
808
809 zones
810 }
811}