Skip to main content

dns_resolver/
local.rs

1use dns_types::protocol::types::*;
2use dns_types::zones::types::*;
3
4use crate::context::Context;
5use crate::util::types::*;
6
7/// Query type for CNAMEs - used for cache lookups.
8const CNAME_QTYPE: QueryType = QueryType::Record(RecordType::CNAME);
9
10/// Local DNS resolution.
11///
12/// This acts like a pseudo-nameserver, returning a `LocalResolutionResult`
13/// which is either consumed by another resolver, or converted directly into a
14/// `ResolvedRecord` to return to the client.
15///
16/// This corresponds to steps 2, 3, and 4 of the standard nameserver algorithm:
17///
18/// - check if there is a zone which matches the QNAME
19///
20/// - search through it for a match (either an answer, a CNAME, or a delegation)
21///
22/// - search through the cache if we didn't get an authoritative match
23///
24/// This function gives up if the CNAMEs form a cycle.
25///
26/// See section 4.3.2 of RFC 1034.
27///
28/// # Errors
29///
30/// See `ResolutionError`.
31pub 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    // `zones.resolve` implements the non-recursive part of step 3 of the
51    // standard resolver algorithm: matching down through the zone and returning
52    // what sort of end state is reached.
53    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            // If we get an answer:
58            //
59            // - if the zone is authoritative: we're done.
60            //
61            // - if the zone is not authoritative: check if this is a wildcard
62            // query or not:
63            //
64            //    - if it's not a wildcard query, return these results as a
65            //    non-authoritative answer (non-authoritative zone records
66            //    effectively override the wider domain name system).
67            //
68            //    - if it is a wildcard query, save these results and continue
69            //    to the cache (handled below), and use a prioritising merge to
70            //    combine the RR sets, preserving the override behaviour.
71            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            // If the name is a CNAME, try resolving it, then:
90            //
91            // - if resolving it only touches authoritative zones: return the
92            // response, which is authoritative if and only if this starting
93            // zone is authoritative, without consulting the cache for
94            // additional records.
95            //
96            // - if resolving it touches non-authoritative zones or the cache:
97            // return the response, which is not authoritative.
98            //
99            // - if resolving it fails: return the response, which is
100            // authoritative if and only if this starting zone is authoritative.
101            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            // If the name is delegated:
169            //
170            // - if this zone is authoritative, return the response with the NS
171            // RRs in the AUTHORITY section.
172            //
173            // - otherwise ignore and proceed to cache.
174            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            // If the name could not be resolved:
205            //
206            // - if this zone is authoritative, a NXDOMAIN response
207            // (todo)
208            //
209            // - otherwise ignore and proceed to cache.
210            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    // If we get here, either:
224    //
225    // - there is no zone for this question (in practice this will be unlikely,
226    // as the root hints get put into a non-authoritative root zone - and
227    // without root hints, we can't do much)
228    //
229    // - the query was answered by a non-authoritative zone, which means we may
230    // have other relevant RRs in the cache
231    //
232    // - the query could not be answered, because the non-authoritative zone
233    // responsible for the name either doesn't contain the name, or only has NS
234    // records (and the query is not for NS records - if it were, that would be
235    // a non-authoritative answer).
236    //
237    // In all cases, consult the cache for an answer to the question, and
238    // combine with the RRs we already have.
239
240    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/// Result of resolving a name using only zones and cache.
329#[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/// An authoritative name error response, returned by the
370/// non-recursive resolver.
371#[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            // sometimes these can be returned in a different order (hashmap
440            // shenanigans?) so explicitly sort in the test
441            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        // use TTL 300 for all records because that's what the other spec
758        // helpers have
759        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}