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 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            // sometimes these can be returned in a different order (hashmap
441            // shenanigans?) so explicitly sort in the test
442            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        // use TTL 300 for all records because that's what the other spec
759        // helpers have
760        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}