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 std::net::Ipv4Addr;
380
381 use super::*;
382 use crate::cache::test_util::*;
383 use crate::cache::SharedCache;
384
385 #[test]
386 fn resolve_local_is_authoritative_for_zones_with_soa() {
387 assert_eq!(
388 test_resolve_local("www.authoritative.example.com.", QueryType::Wildcard),
389 Ok(LocalResolutionResult::Done {
390 resolved: ResolvedRecord::Authoritative {
391 rrs: vec![a_record(
392 "www.authoritative.example.com.",
393 Ipv4Addr::new(1, 1, 1, 1)
394 )],
395 soa_rr: soa_rr(),
396 },
397 })
398 );
399 }
400
401 #[test]
402 fn resolve_local_is_partial_for_zones_without_soa() {
403 assert_eq!(
404 test_resolve_local("a.example.com.", QueryType::Wildcard),
405 Ok(LocalResolutionResult::Partial {
406 rrs: vec![a_record("a.example.com.", Ipv4Addr::new(1, 1, 1, 1))],
407 })
408 );
409 }
410
411 #[test]
412 fn resolve_local_is_partial_for_cache() {
413 let rr = a_record("cached.example.com.", Ipv4Addr::new(1, 1, 1, 1));
414
415 let cache = SharedCache::new();
416 cache.insert(rr.clone());
417
418 if let Ok(LocalResolutionResult::Partial { rrs }) =
419 test_resolve_local_with_cache("cached.example.com.", &cache, QueryType::Wildcard)
420 {
421 assert_cache_response(&rr, &rrs);
422 } else {
423 panic!("expected non-authoritative answer");
424 }
425 }
426
427 #[test]
428 fn resolve_local_returns_all_record_types() {
429 if let Ok(LocalResolutionResult::Done {
430 resolved:
431 ResolvedRecord::Authoritative {
432 rrs: mut actual_rrs,
433 soa_rr: actual_soa_rr,
434 },
435 }) = test_resolve_local(
436 "cname-and-a.authoritative.example.com.",
437 QueryType::Wildcard,
438 ) {
439 actual_rrs.sort();
442
443 assert_eq!(
444 actual_rrs,
445 vec![
446 a_record(
447 "cname-and-a.authoritative.example.com.",
448 Ipv4Addr::new(1, 1, 1, 1)
449 ),
450 cname_record(
451 "cname-and-a.authoritative.example.com.",
452 "www.authoritative.example.com."
453 ),
454 ]
455 );
456 assert_eq!(actual_soa_rr, soa_rr());
457 } else {
458 panic!("expected authoritative answer");
459 }
460 }
461
462 #[test]
463 fn resolve_local_prefers_authoritative_zones() {
464 let cache = SharedCache::new();
465 cache.insert(a_record(
466 "www.authoritative.example.com.",
467 Ipv4Addr::new(8, 8, 8, 8),
468 ));
469
470 assert_eq!(
471 test_resolve_local_with_cache(
472 "www.authoritative.example.com.",
473 &cache,
474 QueryType::Wildcard
475 ),
476 Ok(LocalResolutionResult::Done {
477 resolved: ResolvedRecord::Authoritative {
478 rrs: vec![a_record(
479 "www.authoritative.example.com.",
480 Ipv4Addr::new(1, 1, 1, 1)
481 )],
482 soa_rr: soa_rr(),
483 },
484 })
485 );
486 }
487
488 #[test]
489 fn resolve_local_combines_nonauthoritative_zones_with_cache() {
490 let zone_rr = a_record("a.example.com.", Ipv4Addr::new(1, 1, 1, 1));
491 let cache_rr = cname_record("a.example.com.", "b.example.com.");
492
493 let cache = SharedCache::new();
494 cache.insert(cache_rr.clone());
495
496 if let Ok(LocalResolutionResult::Partial { rrs }) =
497 test_resolve_local_with_cache("a.example.com.", &cache, QueryType::Wildcard)
498 {
499 assert_eq!(2, rrs.len());
500 assert_eq!(zone_rr, rrs[0]);
501 assert_cache_response(&cache_rr, &[rrs[1].clone()]);
502 } else {
503 panic!("expected non-authoritative answer");
504 }
505 }
506
507 #[test]
508 fn resolve_local_overrides_cache_with_nonauthoritative_zones() {
509 let zone_rr = a_record("a.example.com.", Ipv4Addr::new(1, 1, 1, 1));
510 let cache_rr = a_record("a.example.com.", Ipv4Addr::new(8, 8, 8, 8));
511
512 let cache = SharedCache::new();
513 cache.insert(cache_rr);
514
515 assert_eq!(
516 test_resolve_local("a.example.com.", QueryType::Wildcard),
517 Ok(LocalResolutionResult::Partial { rrs: vec![zone_rr] })
518 );
519 }
520
521 #[test]
522 fn resolve_local_expands_cnames_from_zone() {
523 assert_eq!(
524 test_resolve_local(
525 "cname-authoritative.authoritative.example.com.",
526 QueryType::Record(RecordType::A)
527 ),
528 Ok(LocalResolutionResult::Done {
529 resolved: ResolvedRecord::Authoritative {
530 rrs: vec![
531 cname_record(
532 "cname-authoritative.authoritative.example.com.",
533 "www.authoritative.example.com."
534 ),
535 a_record("www.authoritative.example.com.", Ipv4Addr::new(1, 1, 1, 1)),
536 ],
537 soa_rr: soa_rr(),
538 },
539 }),
540 );
541 }
542
543 #[test]
544 fn resolve_local_expands_cnames_from_cache() {
545 let cname_rr1 = cname_record("cname-1.example.com.", "cname-2.example.com.");
546 let cname_rr2 = cname_record("cname-2.example.com.", "a.example.com.");
547 let a_rr = a_record("a.example.com.", Ipv4Addr::new(1, 1, 1, 1));
548
549 let cache = SharedCache::new();
550 cache.insert(cname_rr1.clone());
551 cache.insert(cname_rr2.clone());
552
553 if let Ok(LocalResolutionResult::Done {
554 resolved: ResolvedRecord::NonAuthoritative { rrs, soa_rr: None },
555 }) = test_resolve_local_with_cache(
556 "cname-1.example.com.",
557 &cache,
558 QueryType::Record(RecordType::A),
559 ) {
560 assert_eq!(3, rrs.len());
561 assert_cache_response(&cname_rr1, &[rrs[0].clone()]);
562 assert_cache_response(&cname_rr2, &[rrs[1].clone()]);
563 assert_cache_response(&a_rr, &[rrs[2].clone()]);
564 } else {
565 panic!("expected non-authoritative answer");
566 }
567 }
568
569 #[test]
570 fn resolve_local_handles_cname_cycle() {
571 let qtype = QueryType::Record(RecordType::A);
572
573 assert_eq!(
574 test_resolve_local("cname-cycle-a.example.com.", qtype),
575 Ok(LocalResolutionResult::CNAME {
576 rrs: vec![
577 cname_record("cname-cycle-a.example.com.", "cname-cycle-b.example.com."),
578 cname_record("cname-cycle-b.example.com.", "cname-cycle-a.example.com."),
579 ],
580 cname_question: Question {
581 name: domain("cname-cycle-a.example.com."),
582 qclass: QueryClass::Wildcard,
583 qtype,
584 },
585 }),
586 );
587 }
588
589 #[test]
590 fn resolve_local_propagates_cname_nonauthority() {
591 assert_eq!(
592 test_resolve_local(
593 "cname-nonauthoritative.authoritative.example.com.",
594 QueryType::Record(RecordType::A)
595 ),
596 Ok(LocalResolutionResult::Done {
597 resolved: ResolvedRecord::NonAuthoritative {
598 rrs: vec![
599 cname_record(
600 "cname-nonauthoritative.authoritative.example.com.",
601 "a.example.com."
602 ),
603 a_record("a.example.com.", Ipv4Addr::new(1, 1, 1, 1)),
604 ],
605 soa_rr: None,
606 },
607 }),
608 );
609 }
610
611 #[test]
612 fn resolve_local_uses_most_specific_cname_authority() {
613 assert_eq!(
614 test_resolve_local(
615 "cname.authoritative-2.example.com.",
616 QueryType::Record(RecordType::A)
617 ),
618 Ok(LocalResolutionResult::Done {
619 resolved: ResolvedRecord::Authoritative {
620 rrs: vec![
621 cname_record(
622 "cname.authoritative-2.example.com.",
623 "www.authoritative.example.com."
624 ),
625 a_record("www.authoritative.example.com.", Ipv4Addr::new(1, 1, 1, 1)),
626 ],
627 soa_rr: soa_rr(),
628 },
629 }),
630 );
631 }
632
633 #[test]
634 fn resolve_local_returns_cname_response_if_unable_to_fully_resolve() {
635 let qtype = QueryType::Record(RecordType::A);
636
637 assert_eq!(
638 test_resolve_local("trailing-cname.example.com.", qtype),
639 Ok(LocalResolutionResult::CNAME {
640 rrs: vec![cname_record(
641 "trailing-cname.example.com.",
642 "somewhere-else.example.com."
643 )],
644 cname_question: Question {
645 name: domain("somewhere-else.example.com."),
646 qclass: QueryClass::Wildcard,
647 qtype,
648 },
649 })
650 );
651 }
652
653 #[test]
654 fn resolve_local_delegates_from_authoritative_zone() {
655 assert_eq!(
656 test_resolve_local(
657 "www.delegated.authoritative.example.com.",
658 QueryType::Wildcard
659 ),
660 Ok(LocalResolutionResult::Delegation {
661 rrs: vec![ns_record(
662 "delegated.authoritative.example.com.",
663 "ns.delegated.authoritative.example.com."
664 )],
665 soa_rr: Some(soa_rr()),
666 delegation: Nameservers {
667 name: domain("delegated.authoritative.example.com."),
668 hostnames: vec![domain("ns.delegated.authoritative.example.com.")],
669 }
670 })
671 );
672 }
673
674 #[test]
675 fn resolve_local_does_not_delegate_from_nonauthoritative_zone() {
676 let question = Question {
677 name: domain("www.delegated.example.com."),
678 qtype: QueryType::Wildcard,
679 qclass: QueryClass::Wildcard,
680 };
681
682 assert_eq!(
683 resolve_local(
684 &mut Context::new((), &zones(), &SharedCache::new(), 10),
685 &question
686 ),
687 Err(ResolutionError::DeadEnd {
688 question: question.clone()
689 })
690 );
691 }
692
693 #[test]
694 fn resolve_local_nameerrors_from_authoritative_zone() {
695 assert_eq!(
696 test_resolve_local(
697 "no.such.name.authoritative.example.com.",
698 QueryType::Wildcard
699 ),
700 Ok(LocalResolutionResult::Done {
701 resolved: ResolvedRecord::AuthoritativeNameError { soa_rr: soa_rr() },
702 }),
703 );
704 }
705
706 #[test]
707 fn resolve_local_does_not_nameerror_from_nonauthoritative_zone() {
708 let question = Question {
709 name: domain("no.such.name.example.com."),
710 qtype: QueryType::Wildcard,
711 qclass: QueryClass::Wildcard,
712 };
713
714 assert_eq!(
715 resolve_local(
716 &mut Context::new((), &zones(), &SharedCache::new(), 10),
717 &question,
718 ),
719 Err(ResolutionError::DeadEnd {
720 question: question.clone()
721 }),
722 );
723 }
724
725 fn test_resolve_local(
726 name: &str,
727 qtype: QueryType,
728 ) -> Result<LocalResolutionResult, ResolutionError> {
729 test_resolve_local_with_cache(name, &SharedCache::new(), qtype)
730 }
731
732 fn test_resolve_local_with_cache(
733 name: &str,
734 cache: &SharedCache,
735 qtype: QueryType,
736 ) -> Result<LocalResolutionResult, ResolutionError> {
737 resolve_local(
738 &mut Context::new((), &zones(), cache, 10),
739 &Question {
740 name: domain(name),
741 qclass: QueryClass::Wildcard,
742 qtype,
743 },
744 )
745 }
746
747 fn soa_rr() -> ResourceRecord {
748 zones()
749 .get(&domain("authoritative.example.com."))
750 .unwrap()
751 .soa_rr()
752 .unwrap()
753 }
754
755 #[allow(clippy::missing_panics_doc)]
756 fn zones() -> Zones {
757 let mut zones = Zones::new();
760
761 zones.insert(
762 Zone::deserialise(
763 r"
764$ORIGIN example.com.
765
766a 300 IN A 1.1.1.1
767blocked 300 IN A 0.0.0.0
768cname-cycle-a 300 IN CNAME cname-cycle-b
769cname-cycle-b 300 IN CNAME cname-cycle-a
770delegated 300 IN NS ns.delegated
771trailing-cname 300 IN CNAME somewhere-else
772",
773 )
774 .unwrap(),
775 );
776
777 zones.insert(
778 Zone::deserialise(
779 r"
780$ORIGIN authoritative.example.com.
781
782@ IN SOA mname rname 1 30 30 30 30
783
784www 300 IN A 1.1.1.1
785cname-and-a 300 IN A 1.1.1.1
786cname-and-a 300 IN CNAME www
787cname-authoritative 300 IN CNAME www
788cname-nonauthoritative 300 IN CNAME a.example.com.
789delegated 300 IN NS ns.delegated
790",
791 )
792 .unwrap(),
793 );
794
795 zones.insert(
796 Zone::deserialise(
797 r"
798$ORIGIN authoritative-2.example.com.
799
800@ IN SOA mname rname 1 30 30 30 30
801
802cname 300 IN CNAME www.authoritative.example.com.
803",
804 )
805 .unwrap(),
806 );
807
808 zones
809 }
810}