Skip to main content

hickory_resolver/
caching_client.rs

1// Copyright 2015-2023 Benjamin Fry <benjaminfry@me.com>
2//
3// Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
4// https://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
5// https://opensource.org/licenses/MIT>, at your option. This file may not be
6// copied, modified, or distributed except according to those terms.
7
8//! Caching related functionality for the Resolver.
9
10use std::{
11    borrow::Cow,
12    future::Future,
13    time::{Duration, Instant},
14};
15
16use once_cell::sync::Lazy;
17
18use crate::{
19    cache::{MAX_TTL, ResponseCache, TtlConfig},
20    lookup::Lookup,
21    net::{
22        DnsError, NetError, NoRecords,
23        xfer::{DnsHandle, FirstAnswer},
24    },
25    proto::{
26        op::{DnsRequestOptions, DnsResponse, Message, OpCode, Query, ResponseCode},
27        rr::{
28            DNSClass, Name, RData, Record, RecordRef, RecordType,
29            domain::usage::{
30                DEFAULT, IN_ADDR_ARPA_127, INVALID, IP6_ARPA_1, LOCAL,
31                LOCALHOST as LOCALHOST_usage, ONION, ResolverUsage,
32            },
33            rdata::{A, AAAA, CNAME, PTR},
34        },
35    },
36};
37
38static LOCALHOST: Lazy<RData> =
39    Lazy::new(|| RData::PTR(PTR(Name::from_ascii("localhost.").unwrap())));
40static LOCALHOST_V4: Lazy<RData> = Lazy::new(|| RData::A(A::new(127, 0, 0, 1)));
41static LOCALHOST_V6: Lazy<RData> = Lazy::new(|| RData::AAAA(AAAA::new(0, 0, 0, 0, 0, 0, 0, 1)));
42
43/// Counts the depth of CNAME query resolutions.
44#[derive(Default, Clone, Copy)]
45struct DepthTracker {
46    query_depth: u8,
47}
48
49impl DepthTracker {
50    fn nest(self) -> Self {
51        Self {
52            query_depth: self.query_depth + 1,
53        }
54    }
55
56    fn is_exhausted(self) -> bool {
57        self.query_depth + 1 >= Self::MAX_QUERY_DEPTH
58    }
59
60    const MAX_QUERY_DEPTH: u8 = 8; // arbitrarily chosen number...
61}
62
63#[derive(Clone, Debug)]
64#[doc(hidden)]
65pub struct CachingClient<C>
66where
67    C: DnsHandle,
68{
69    cache: ResponseCache,
70    client: C,
71    preserve_intermediates: bool,
72    #[cfg(feature = "metrics")]
73    cache_metrics: crate::metrics::CacheMetrics,
74}
75
76impl<C> CachingClient<C>
77where
78    C: DnsHandle + Send + 'static,
79{
80    #[doc(hidden)]
81    pub fn new(max_size: u64, client: C, preserve_intermediates: bool) -> Self {
82        Self::with_cache(
83            ResponseCache::new(max_size, TtlConfig::default()),
84            client,
85            preserve_intermediates,
86        )
87    }
88
89    pub(crate) fn with_cache(
90        cache: ResponseCache,
91        client: C,
92        preserve_intermediates: bool,
93    ) -> Self {
94        Self {
95            cache,
96            client,
97            preserve_intermediates,
98            #[cfg(feature = "metrics")]
99            cache_metrics: crate::metrics::CacheMetrics::default(),
100        }
101    }
102
103    /// Perform a lookup against this caching client, looking first in the cache for a result
104    pub fn lookup(
105        &self,
106        query: Query,
107        options: DnsRequestOptions,
108    ) -> impl Future<Output = Result<Lookup, NetError>> {
109        Self::inner_lookup(
110            query,
111            options,
112            self.clone(),
113            vec![],
114            DepthTracker::default(),
115        )
116    }
117
118    async fn inner_lookup(
119        query: Query,
120        options: DnsRequestOptions,
121        mut client: Self,
122        preserved_records: Vec<Record>,
123        depth: DepthTracker,
124    ) -> Result<Lookup, NetError> {
125        // see https://tools.ietf.org/html/rfc6761
126        //
127        // ```text
128        // Name resolution APIs and libraries SHOULD recognize localhost
129        // names as special and SHOULD always return the IP loopback address
130        // for address queries and negative responses for all other query
131        // types.  Name resolution APIs SHOULD NOT send queries for
132        // localhost names to their configured caching DNS server(s).
133        // ```
134        // special use rules only apply to the IN Class
135        if query.query_class() == DNSClass::IN {
136            let usage = match query.name() {
137                n if LOCALHOST_usage.zone_of(n) => &*LOCALHOST_usage,
138                n if IN_ADDR_ARPA_127.zone_of(n) => &*LOCALHOST_usage,
139                n if IP6_ARPA_1.zone_of(n) => &*LOCALHOST_usage,
140                n if INVALID.zone_of(n) => &*INVALID,
141                n if LOCAL.zone_of(n) => &*LOCAL,
142                n if ONION.zone_of(n) => &*ONION,
143                _ => &*DEFAULT,
144            };
145
146            match usage.resolver() {
147                ResolverUsage::Loopback => match query.query_type() {
148                    // TODO: look in hosts for these ips/names first...
149                    RecordType::A => return Ok(Lookup::from_rdata(query, LOCALHOST_V4.clone())),
150                    RecordType::AAAA => return Ok(Lookup::from_rdata(query, LOCALHOST_V6.clone())),
151                    RecordType::PTR => return Ok(Lookup::from_rdata(query, LOCALHOST.clone())),
152                    // Are there any other types we can use?
153                    _ => return Err(NoRecords::new(query, ResponseCode::NoError).into()),
154                },
155                // TODO: this requires additional config, as Kubernetes and other systems misuse the .local. zone.
156                // when mdns is not enabled we will return errors on LinkLocal ("*.local.") names
157                ResolverUsage::LinkLocal => (),
158                ResolverUsage::NxDomain => {
159                    return Err(NoRecords::new(query, ResponseCode::NXDomain).into());
160                }
161                ResolverUsage::Normal => (),
162            }
163        }
164
165        let is_dnssec = client.client.is_verifying_dnssec();
166
167        #[cfg(feature = "metrics")]
168        let request_start = Instant::now();
169
170        if let Some(cached_lookup) = client.lookup_from_cache(&query) {
171            #[cfg(feature = "metrics")]
172            {
173                client.cache_metrics.cache_hit.increment(1);
174                client
175                    .cache_metrics
176                    .cache_hit_duration
177                    .record(request_start.elapsed());
178                client
179                    .cache_metrics
180                    .cache_size
181                    .set(client.cache.entry_count() as f64);
182            }
183            return cached_lookup;
184        };
185
186        #[cfg(feature = "metrics")]
187        client.cache_metrics.cache_miss.increment(1);
188
189        let response_message = client
190            .client
191            .lookup(query.clone(), options)
192            .first_answer()
193            .await;
194
195        // TODO: technically this might be duplicating work, as name_server already performs this evaluation.
196        //  we may want to create a new type, if evaluated... but this is most generic to support any impl in LookupState...
197        let response_message = if let Ok(response) = response_message {
198            DnsError::from_response(response).map_err(NetError::from)
199        } else {
200            response_message
201        };
202
203        // TODO: take all records and cache them?
204        //  if it's DNSSEC they must be signed, otherwise?
205        let records = match response_message {
206            Ok(response_message) => {
207                // allow the handle_noerror function to deal with any error codes
208                let records = match Self::handle_noerror(
209                    &mut client,
210                    options,
211                    &query,
212                    response_message,
213                    preserved_records,
214                    depth,
215                ) {
216                    Ok(records) => records,
217                    Err(err) => {
218                        #[cfg(feature = "metrics")]
219                        client
220                            .cache_metrics
221                            .cache_miss_duration
222                            .record(request_start.elapsed());
223                        return Err(err);
224                    }
225                };
226
227                Ok(records)
228            }
229            // this is the only cacheable form
230            Err(NetError::Dns(DnsError::NoRecordsFound(mut no_records))) => {
231                if is_dnssec {
232                    no_records.negative_ttl = None;
233                }
234                Err(no_records.into())
235            }
236            Err(err) => {
237                #[cfg(feature = "metrics")]
238                client
239                    .cache_metrics
240                    .cache_miss_duration
241                    .record(request_start.elapsed());
242                return Err(err);
243            }
244        };
245
246        // after the request, evaluate if we have additional queries to perform
247        let result = match records {
248            Ok(Records::CnameChain { next: future, .. }) => match future.await {
249                Ok(lookup) => client.cname(lookup, query),
250                Err(e) => client.cache(query, Err(e)),
251            },
252            Ok(Records::Exists { message }) => client.cache(query, Ok(message)),
253            Err(e) => client.cache(query, Err(e)),
254        };
255
256        #[cfg(feature = "metrics")]
257        client
258            .cache_metrics
259            .cache_miss_duration
260            .record(request_start.elapsed());
261
262        result
263    }
264
265    /// Check if this query is already cached
266    fn lookup_from_cache(&self, query: &Query) -> Option<Result<Lookup, NetError>> {
267        let now = Instant::now();
268        let message_res = self.cache.get(query, now)?;
269        let message = match message_res {
270            Ok(message) => message,
271            Err(err) => return Some(Err(err)),
272        };
273
274        let valid_until = now
275            + Duration::from_secs(
276                message
277                    .answers
278                    .iter()
279                    .map(|r| r.ttl)
280                    .min()
281                    .unwrap_or(MAX_TTL)
282                    .into(),
283            );
284
285        Some(Ok(Lookup::new(message, valid_until)))
286    }
287
288    /// Handle the case where there is no error returned
289    fn handle_noerror(
290        client: &mut Self,
291        options: DnsRequestOptions,
292        query: &Query,
293        response: DnsResponse,
294        mut preserved_records: Vec<Record>,
295        depth: DepthTracker,
296    ) -> Result<Records<impl Future<Output = Result<Lookup, NetError>>>, NetError> {
297        // TODO: there should be a ResolverOpts config to disable the
298        // name validation in this function to more closely match the
299        // behaviour of glibc if that's what the user expects.
300
301        // initial ttl is what CNAMES use for min usage
302        const INITIAL_TTL: u32 = MAX_TTL;
303
304        // need to capture these before the subsequent and destructive record processing
305        let soa = response.soa().as_ref().map(RecordRef::to_owned);
306        let negative_ttl = response.negative_ttl();
307        let response_code = response.response_code;
308
309        // seek out CNAMES, this is only performed if the query is not a CNAME, ANY, or SRV
310        // FIXME: for SRV this evaluation is inadequate. CNAME is a single chain to a single record
311        //   for SRV, there could be many different targets. The search_name needs to be enhanced to
312        //   be a list of names found for SRV records.
313        let (search_name, was_cname, preserved_records) = {
314            // this will only search for CNAMEs if the request was not meant to be for one of the triggers for recursion
315            let (search_name, cname_ttl, was_cname) =
316                if query.query_type().is_any() || query.query_type().is_cname() {
317                    (Cow::Borrowed(query.name()), INITIAL_TTL, false)
318                } else {
319                    // Folds any cnames from the answers section, into the final cname in the answers section
320                    //   this works by folding the last CNAME found into the final folded result.
321                    //   it assumes that the CNAMEs are in chained order in the DnsResponse Message...
322                    // For SRV, the name added for the search becomes the target name.
323                    //
324                    // TODO: should this include the additionals?
325                    response.answers.iter().fold(
326                        (Cow::Borrowed(query.name()), INITIAL_TTL, false),
327                        |(search_name, cname_ttl, was_cname), r| {
328                            match &r.data {
329                                RData::CNAME(CNAME(cname)) => {
330                                    // take the minimum TTL of the cname_ttl and the next record in the chain
331                                    let ttl = cname_ttl.min(r.ttl);
332                                    debug_assert_eq!(r.record_type(), RecordType::CNAME);
333                                    if search_name.as_ref() == &r.name {
334                                        return (Cow::Owned(cname.clone()), ttl, true);
335                                    }
336                                }
337                                RData::SRV(srv) => {
338                                    // take the minimum TTL of the cname_ttl and the next record in the chain
339                                    let ttl = cname_ttl.min(r.ttl);
340                                    debug_assert_eq!(r.record_type(), RecordType::SRV);
341
342                                    // the search name becomes the srv.target
343                                    return (Cow::Owned(srv.target.clone()), ttl, true);
344                                }
345                                _ => (),
346                            }
347
348                            (search_name, cname_ttl, was_cname)
349                        },
350                    )
351                };
352
353            // take all answers. // TODO: following CNAMES?
354            let mut message = response.into_message();
355
356            // set of names that still require resolution
357            // TODO: this needs to be enhanced for SRV
358            let mut found_name = false;
359            let mut found_cname_target = false;
360            // Scan through all sections to determine what we found.
361            // We need this first pass to decide our strategy: return complete message vs filter
362            for r in message.all_sections() {
363                // restrict to the RData type requested
364                if query.query_class() != r.dns_class {
365                    continue;
366                }
367
368                // standard evaluation, it's an any type, or it's the requested type and the
369                // search_name matches
370                let type_matches =
371                    query.query_type().is_any() || query.query_type() == r.record_type();
372                let name_matches = search_name.as_ref() == &r.name || query.name() == &r.name;
373                if type_matches && name_matches {
374                    found_name = true;
375                    // Track if we found the CNAME target (not just the original name)
376                    if was_cname && search_name.as_ref() == &r.name {
377                        found_cname_target = true;
378                    }
379                }
380            }
381
382            // After following all the CNAMES to the last one, try and lookup the final name
383            if found_name && (!was_cname || preserved_records.is_empty()) {
384                // Decide strategy: do we need to filter, or return the message as-is?
385                // - If we have accumulated records from previous CNAME hops → must filter and merge
386                // - If we found the CNAME target in this response → filter out intermediate CNAMEs
387                //   (unless preserve_intermediates)
388                // - Otherwise → return complete message to preserve all sections exactly as DNS server
389                //   sent them
390                let needs_filtering = !preserved_records.is_empty()
391                    || (found_cname_target && !client.preserve_intermediates);
392
393                if needs_filtering {
394                    // Filter records that belong in ANSWER section only
395                    // Don't include records from ADDITIONAL/AUTHORITY here - they're preserved as-is below
396                    preserved_records.extend(message.all_sections().filter_map(|r| {
397                        // because this resolved potentially recursively, we want the min TTL from the chain
398                        let ttl = cname_ttl.min(r.ttl);
399                        let mut r = r.clone();
400                        r.ttl = ttl;
401
402                        // restrict to the RData type requested
403                        if query.query_class() != r.dns_class {
404                            return None;
405                        }
406
407                        // standard evaluation, it's an any type, or it's the requested type
408                        // and the search_name matches
409                        let query_type = query.query_type();
410                        let record_type = r.record_type();
411                        let type_matches = query_type.is_any() || query_type == record_type;
412                        let name_matches =
413                            search_name.as_ref() == &r.name || query.name() == &r.name;
414                        if type_matches && name_matches {
415                            return Some(r);
416                        }
417
418                        // CNAME evaluation, the record is from the CNAME lookup chain.
419                        if client.preserve_intermediates && record_type == RecordType::CNAME {
420                            return Some(r);
421                        }
422
423                        // Note: NS glue and SRV target IPs are NOT included here
424                        // They belong in ADDITIONAL section and are preserved below via insert_additionals
425                        None
426                    }));
427
428                    // Replace ANSWER section with filtered records, preserve AUTHORITY and ADDITIONAL sections
429                    message.answers = preserved_records;
430                }
431
432                // Strip DNSSEC records if DO bit is not set.
433                message = message.maybe_strip_dnssec_records(options.edns_set_dnssec_ok);
434
435                return Ok(Records::Exists { message });
436            }
437
438            // We didn't find the answer - need to continue following CNAME chain
439            // Only accumulate ANSWER-section records (CNAMEs) for next hop
440            // AUTHORITY and ADDITIONAL records stay with their original message and are not carried forward
441            preserved_records.extend(message.take_all_sections().filter_map(|mut r| {
442                // because this resolved potentially recursively, we want the min TTL from the chain
443                r.ttl = cname_ttl.min(r.ttl);
444
445                // restrict to the RData type requested
446                if query.query_class() != r.dns_class {
447                    return None;
448                }
449
450                // CNAME evaluation, the record is from the CNAME lookup chain.
451                if client.preserve_intermediates && r.record_type() == RecordType::CNAME {
452                    return Some(r);
453                }
454
455                // Note: NS glue and SRV target IPs are NOT accumulated across hops
456                // They belong in ADDITIONAL section of their original response, not in ANSWER
457                None
458            }));
459
460            (search_name.into_owned(), was_cname, preserved_records)
461        };
462
463        // TODO: for SRV records we *could* do an implicit lookup, but, this requires knowing the type of IP desired
464        //    for now, we'll make the API require the user to perform a follow up to the lookups.
465        // It was a CNAME, but not included in the request...
466        if was_cname && !depth.is_exhausted() {
467            let next_query = Query::query(search_name, query.query_type());
468            Ok(Records::CnameChain {
469                next: Box::pin(Self::inner_lookup(
470                    next_query,
471                    options,
472                    client.clone(),
473                    #[cfg(test)]
474                    preserved_records.clone(),
475                    #[cfg(not(test))]
476                    preserved_records,
477                    depth.nest(),
478                )),
479                #[cfg(test)]
480                preserved_records,
481            })
482        } else {
483            // TODO: review See https://tools.ietf.org/html/rfc2308 for NoData section
484            // Note on DNSSEC, in secure_client_handle, if verify_nsec fails then the request fails.
485            //   this will mean that no unverified negative caches will make it to this point and be stored
486            let mut new = NoRecords::new(query.clone(), response_code);
487            new.soa = soa.map(Box::new);
488            new.negative_ttl = negative_ttl;
489            Err(new.into())
490        }
491    }
492
493    #[allow(clippy::unnecessary_wraps)]
494    fn cname(&self, lookup: Lookup, query: Query) -> Result<Lookup, NetError> {
495        let mut message = Message::response(0, OpCode::Query);
496        message.add_query(query.clone());
497        message.add_answers(lookup.answers().iter().cloned());
498        message.add_authorities(lookup.authorities().iter().cloned());
499        message.add_additionals(lookup.additionals().iter().cloned());
500        self.cache.insert(query, Ok(message), Instant::now());
501        Ok(lookup)
502    }
503
504    fn cache(&self, query: Query, result: Result<Message, NetError>) -> Result<Lookup, NetError> {
505        let now = Instant::now();
506        let result = match result {
507            Ok(mut message) => {
508                // Clamp record TTLs before building the Lookup so that the first
509                // response to the client reflects positive_min/max_ttl, not the
510                // raw upstream TTL.
511                let ttl = self
512                    .cache
513                    .clamp_positive_ttls(query.query_type(), &mut message);
514                let valid_until = now + ttl;
515                let lookup = Lookup::new(message.clone(), valid_until);
516                self.cache.insert(query, Ok(message), now);
517                Ok(lookup)
518            }
519            Err(err) => {
520                self.cache.insert(query, Err(err.clone()), now);
521                Err(err)
522            }
523        };
524        #[cfg(feature = "metrics")]
525        self.cache_metrics
526            .cache_size
527            .set(self.cache.entry_count() as f64);
528        result
529    }
530
531    /// Flushes/Removes all entries from the cache
532    pub fn clear_cache(&self) {
533        self.cache.clear();
534    }
535
536    /// Flushes/Removes the entry from the cache that is associated with this query
537    pub fn clear_cache_query(&self, query: &Query) {
538        self.cache.clear_query(query);
539    }
540}
541
542enum Records<F> {
543    /// The records exist, stored as a complete DNS Message
544    Exists { message: Message },
545    /// Future lookup for recursive cname records
546    CnameChain {
547        next: F,
548        #[cfg(test)]
549        preserved_records: Vec<Record>,
550    },
551}
552
553// see also the lookup_tests.rs in integration-tests crate
554#[cfg(test)]
555mod tests {
556    use std::net::*;
557    use std::str::FromStr;
558    use std::time::*;
559
560    use futures_executor::block_on;
561    use test_support::subscribe;
562
563    use super::*;
564    use crate::cache::TtlConfig;
565    use crate::lookup_ip::tests::*;
566    use crate::proto::op::{Message, Query};
567    use crate::proto::rr::rdata::{NS, SRV};
568    use crate::proto::rr::{Name, Record};
569
570    #[test]
571    fn test_empty_cache() {
572        subscribe();
573        let cache = ResponseCache::new(1, TtlConfig::default());
574        let client = mock(vec![empty()]);
575        let client = CachingClient::with_cache(cache, client, false);
576
577        let error = block_on(CachingClient::inner_lookup(
578            Query::new(),
579            DnsRequestOptions::default(),
580            client,
581            vec![],
582            DepthTracker::default(),
583        ))
584        .unwrap_err();
585
586        let NetError::Dns(DnsError::NoRecordsFound(no_records)) = error else {
587            panic!("wrong error received")
588        };
589
590        assert_eq!(no_records.query, Box::new(Query::new()));
591        assert_eq!(no_records.negative_ttl, None);
592    }
593
594    #[test]
595    fn test_from_cache() {
596        subscribe();
597        let cache = ResponseCache::new(1, TtlConfig::default());
598        let query = Query::new();
599        let mut message = Message::response(0, OpCode::Query);
600        message.add_query(query.clone());
601        message.add_answer(Record::from_rdata(
602            query.name().clone(),
603            u32::MAX,
604            RData::A(A::new(127, 0, 0, 1)),
605        ));
606        cache.insert(query.clone(), Ok(message), Instant::now());
607
608        let client = mock(vec![empty()]);
609        let client = CachingClient::with_cache(cache, client, false);
610
611        let ips = block_on(CachingClient::inner_lookup(
612            Query::new(),
613            DnsRequestOptions::default(),
614            client,
615            vec![],
616            DepthTracker::default(),
617        ))
618        .unwrap();
619
620        assert_eq!(
621            ips.answers(),
622            &[Record::from_rdata(
623                query.name().clone(),
624                u32::MAX,
625                RData::A(A::new(127, 0, 0, 1))
626            )]
627        );
628    }
629
630    #[test]
631    fn test_no_cache_insert() {
632        subscribe();
633        let cache = ResponseCache::new(1, TtlConfig::default());
634        // first should come from client...
635        let client = mock(vec![v4_message()]);
636        let client = CachingClient::with_cache(cache.clone(), client, false);
637
638        let ips = block_on(CachingClient::inner_lookup(
639            Query::query(Name::root(), RecordType::A),
640            DnsRequestOptions::default(),
641            client,
642            vec![],
643            DepthTracker::default(),
644        ))
645        .unwrap();
646
647        assert_eq!(
648            ips.answers(),
649            &[Record::from_rdata(
650                Name::root(),
651                86400,
652                RData::A(A::new(127, 0, 0, 1))
653            )]
654        );
655
656        // next should come from cache...
657        let client = mock(vec![empty()]);
658        let client = CachingClient::with_cache(cache, client, false);
659
660        let ips = block_on(CachingClient::inner_lookup(
661            Query::query(Name::root(), RecordType::A),
662            DnsRequestOptions::default(),
663            client,
664            vec![],
665            DepthTracker::default(),
666        ))
667        .unwrap();
668
669        assert_eq!(
670            ips.answers(),
671            &[Record::from_rdata(
672                Name::root(),
673                86400,
674                RData::A(A::new(127, 0, 0, 1))
675            )]
676        );
677    }
678
679    #[allow(clippy::unnecessary_wraps)]
680    pub(crate) fn cname_message() -> Result<DnsResponse, NetError> {
681        let mut message = Message::query();
682        message.add_query(Query::query(
683            Name::from_str("www.example.com.").unwrap(),
684            RecordType::A,
685        ));
686        message.insert_answers(vec![Record::from_rdata(
687            Name::from_str("www.example.com.").unwrap(),
688            86400,
689            RData::CNAME(CNAME(Name::from_str("actual.example.com.").unwrap())),
690        )]);
691        Ok(DnsResponse::from_message(message.into_response()).unwrap())
692    }
693
694    #[allow(clippy::unnecessary_wraps)]
695    pub(crate) fn srv_message() -> Result<DnsResponse, NetError> {
696        let mut message = Message::query();
697        message.add_query(Query::query(
698            Name::from_str("_443._tcp.www.example.com.").unwrap(),
699            RecordType::SRV,
700        ));
701        message.insert_answers(vec![Record::from_rdata(
702            Name::from_str("_443._tcp.www.example.com.").unwrap(),
703            86400,
704            RData::SRV(SRV::new(
705                1,
706                2,
707                443,
708                Name::from_str("www.example.com.").unwrap(),
709            )),
710        )]);
711        Ok(DnsResponse::from_message(message.into_response()).unwrap())
712    }
713
714    #[allow(clippy::unnecessary_wraps)]
715    pub(crate) fn ns_message() -> Result<DnsResponse, NetError> {
716        let mut message = Message::query();
717        message.add_query(Query::query(
718            Name::from_str("www.example.com.").unwrap(),
719            RecordType::NS,
720        ));
721        message.insert_answers(vec![Record::from_rdata(
722            Name::from_str("www.example.com.").unwrap(),
723            86400,
724            RData::NS(NS(Name::from_str("www.example.com.").unwrap())),
725        )]);
726        Ok(DnsResponse::from_message(message.into_response()).unwrap())
727    }
728
729    fn no_recursion_on_query_test(query_type: RecordType) {
730        let cache = ResponseCache::new(1, TtlConfig::default());
731
732        // the cname should succeed, we shouldn't query again after that, which would cause an error...
733        let client = mock(vec![error(), cname_message()]);
734        let client = CachingClient::with_cache(cache, client, false);
735
736        let ips = block_on(CachingClient::inner_lookup(
737            Query::query(Name::from_str("www.example.com.").unwrap(), query_type),
738            DnsRequestOptions::default(),
739            client,
740            vec![],
741            DepthTracker::default(),
742        ))
743        .expect("lookup failed");
744
745        assert_eq!(
746            ips.answers(),
747            &[Record::from_rdata(
748                Name::from_str("www.example.com.").unwrap(),
749                86400,
750                RData::CNAME(CNAME(Name::from_str("actual.example.com.").unwrap()))
751            )]
752        );
753    }
754
755    #[test]
756    fn test_no_recursion_on_cname_query() {
757        subscribe();
758        no_recursion_on_query_test(RecordType::CNAME);
759    }
760
761    #[test]
762    fn test_no_recursion_on_all_query() {
763        subscribe();
764        no_recursion_on_query_test(RecordType::ANY);
765    }
766
767    #[test]
768    fn test_non_recursive_srv_query() {
769        subscribe();
770
771        let cache = ResponseCache::new(1, TtlConfig::default());
772
773        // the cname should succeed, we shouldn't query again after that, which would cause an error...
774        let client = mock(vec![error(), srv_message()]);
775        let client = CachingClient::with_cache(cache, client, false);
776
777        let ips = block_on(CachingClient::inner_lookup(
778            Query::query(
779                Name::from_str("_443._tcp.www.example.com.").unwrap(),
780                RecordType::SRV,
781            ),
782            DnsRequestOptions::default(),
783            client,
784            vec![],
785            DepthTracker::default(),
786        ))
787        .expect("lookup failed");
788
789        assert_eq!(
790            ips.answers(),
791            &[Record::from_rdata(
792                Name::from_str("_443._tcp.www.example.com.").unwrap(),
793                86400,
794                RData::SRV(SRV::new(
795                    1,
796                    2,
797                    443,
798                    Name::from_str("www.example.com.").unwrap(),
799                ))
800            )]
801        );
802    }
803
804    #[test]
805    fn test_single_srv_query_response() {
806        subscribe();
807
808        let cache = ResponseCache::new(1, TtlConfig::default());
809
810        let mut message = srv_message().unwrap().into_message();
811        message.add_answer(Record::from_rdata(
812            Name::from_str("www.example.com.").unwrap(),
813            86400,
814            RData::CNAME(CNAME(Name::from_str("actual.example.com.").unwrap())),
815        ));
816        message.insert_additionals(vec![
817            Record::from_rdata(
818                Name::from_str("actual.example.com.").unwrap(),
819                86400,
820                RData::A(A::new(127, 0, 0, 1)),
821            ),
822            Record::from_rdata(
823                Name::from_str("actual.example.com.").unwrap(),
824                86400,
825                RData::AAAA(AAAA::new(0, 0, 0, 0, 0, 0, 0, 1)),
826            ),
827        ]);
828
829        let client = mock(vec![
830            error(),
831            Ok(DnsResponse::from_message(message).unwrap()),
832        ]);
833        let client = CachingClient::with_cache(cache, client, false);
834
835        let ips = block_on(CachingClient::inner_lookup(
836            Query::query(
837                Name::from_str("_443._tcp.www.example.com.").unwrap(),
838                RecordType::SRV,
839            ),
840            DnsRequestOptions::default(),
841            client,
842            vec![],
843            DepthTracker::default(),
844        ))
845        .expect("lookup failed");
846
847        // Answers section should have SRV + CNAME
848        let answers = ips
849            .answers()
850            .iter()
851            .map(|r| r.data.clone())
852            .collect::<Vec<_>>();
853        assert!(answers.contains(&RData::SRV(SRV::new(
854            1,
855            2,
856            443,
857            Name::from_str("www.example.com.").unwrap(),
858        ))));
859        assert!(answers.contains(&RData::CNAME(CNAME(
860            Name::from_str("actual.example.com.").unwrap()
861        ))));
862
863        // Additionals section should have A + AAAA records
864        let additionals = ips
865            .additionals()
866            .iter()
867            .map(|r| r.data.clone())
868            .collect::<Vec<_>>();
869        assert!(additionals.contains(&RData::A(A::new(127, 0, 0, 1))));
870        assert!(additionals.contains(&RData::AAAA(AAAA::new(0, 0, 0, 0, 0, 0, 0, 1))));
871    }
872
873    // TODO: if we ever enable recursive lookups for SRV, here are the tests...
874    // #[test]
875    // fn test_recursive_srv_query() {
876    //     let cache = Arc::new(Mutex::new(DnsLru::new(1)));
877
878    //     let mut message = Message::new();
879    //     message.add_answer(Record::from_rdata(
880    //         Name::from_str("www.example.com.").unwrap(),
881    //         86400,
882    //         RecordType::CNAME,
883    //         RData::CNAME(Name::from_str("actual.example.com.").unwrap()),
884    //     ));
885    //     message.insert_additionals(vec![
886    //         Record::from_rdata(
887    //             Name::from_str("actual.example.com.").unwrap(),
888    //             86400,
889    //             RecordType::A,
890    //             RData::A(Ipv4Addr::LOCALHOST),
891    //         ),
892    //     ]);
893
894    //     let mut client = mock(vec![error(), Ok(DnsResponse::from_message(message).unwrap()), srv_message()]);
895
896    //     let ips = QueryState::lookup(
897    //         Query::query(
898    //             Name::from_str("_443._tcp.www.example.com.").unwrap(),
899    //             RecordType::SRV,
900    //         ),
901    //         Default::default(),
902    //         &mut client,
903    //         cache.clone(),
904    //     ).wait()
905    //         .expect("lookup failed");
906
907    //     assert_eq!(
908    //         ips.iter().cloned().collect::<Vec<_>>(),
909    //         vec![
910    //             RData::SRV(SRV::new(
911    //                 1,
912    //                 2,
913    //                 443,
914    //                 Name::from_str("www.example.com.").unwrap(),
915    //             )),
916    //             RData::A(Ipv4Addr::LOCALHOST),
917    //             //RData::AAAA(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)),
918    //         ]
919    //     );
920    // }
921
922    #[test]
923    fn test_single_ns_query_response() {
924        subscribe();
925
926        let cache = ResponseCache::new(1, TtlConfig::default());
927
928        let mut message = ns_message().unwrap().into_message();
929        message.add_answer(Record::from_rdata(
930            Name::from_str("www.example.com.").unwrap(),
931            86400,
932            RData::CNAME(CNAME(Name::from_str("actual.example.com.").unwrap())),
933        ));
934        message.insert_additionals(vec![
935            Record::from_rdata(
936                Name::from_str("actual.example.com.").unwrap(),
937                86400,
938                RData::A(A::new(127, 0, 0, 1)),
939            ),
940            Record::from_rdata(
941                Name::from_str("actual.example.com.").unwrap(),
942                86400,
943                RData::AAAA(AAAA::new(0, 0, 0, 0, 0, 0, 0, 1)),
944            ),
945        ]);
946
947        let client = mock(vec![
948            error(),
949            Ok(DnsResponse::from_message(message).unwrap()),
950        ]);
951        let client = CachingClient::with_cache(cache, client, false);
952
953        let ips = block_on(CachingClient::inner_lookup(
954            Query::query(Name::from_str("www.example.com.").unwrap(), RecordType::NS),
955            DnsRequestOptions::default(),
956            client,
957            vec![],
958            DepthTracker::default(),
959        ))
960        .expect("lookup failed");
961
962        // Answers section should have NS + CNAME
963        let answers = ips
964            .answers()
965            .iter()
966            .map(|r| r.data.clone())
967            .collect::<Vec<_>>();
968        assert!(answers.contains(&RData::NS(NS(Name::from_str("www.example.com.").unwrap()))));
969        assert!(answers.contains(&RData::CNAME(CNAME(
970            Name::from_str("actual.example.com.").unwrap()
971        ))));
972
973        // Additionals section should have A + AAAA records
974        let additionals = ips
975            .additionals()
976            .iter()
977            .map(|r| r.data.clone())
978            .collect::<Vec<_>>();
979        assert!(additionals.contains(&RData::A(A::new(127, 0, 0, 1))));
980        assert!(additionals.contains(&RData::AAAA(AAAA::new(0, 0, 0, 0, 0, 0, 0, 1))));
981    }
982
983    /// Purpose: Verify glue records stay in ADDITIONAL section
984    ///
985    /// This test ensures that when querying for NS records, the glue A records for those
986    /// nameservers stay in the ADDITIONAL section and do NOT leak into the ANSWER section.
987    #[test]
988    fn test_ns_query_glue_in_additional_section() {
989        subscribe();
990
991        let cache = ResponseCache::new(1, TtlConfig::default());
992
993        // Create NS query response for example.com with glue in ADDITIONAL section
994        let mut message = Message::response(0, OpCode::Query);
995        message.add_query(Query::query(
996            Name::from_str("example.com.").unwrap(),
997            RecordType::NS,
998        ));
999
1000        // ANSWER section: NS records
1001        message.insert_answers(vec![
1002            Record::from_rdata(
1003                Name::from_str("example.com.").unwrap(),
1004                3600,
1005                RData::NS(NS(Name::from_str("ns1.example.com.").unwrap())),
1006            ),
1007            Record::from_rdata(
1008                Name::from_str("example.com.").unwrap(),
1009                3600,
1010                RData::NS(NS(Name::from_str("ns2.example.com.").unwrap())),
1011            ),
1012        ]);
1013
1014        // ADDITIONAL section: Glue A records for the nameservers
1015        message.insert_additionals(vec![
1016            Record::from_rdata(
1017                Name::from_str("ns1.example.com.").unwrap(),
1018                3600,
1019                RData::A(A::new(192, 0, 2, 1)),
1020            ),
1021            Record::from_rdata(
1022                Name::from_str("ns2.example.com.").unwrap(),
1023                3600,
1024                RData::A(A::new(192, 0, 2, 2)),
1025            ),
1026        ]);
1027
1028        let client = mock(vec![
1029            error(),
1030            Ok(DnsResponse::from_message(message).unwrap()),
1031        ]);
1032        let client = CachingClient::with_cache(cache, client, false);
1033
1034        let lookup = block_on(CachingClient::inner_lookup(
1035            Query::query(Name::from_str("example.com.").unwrap(), RecordType::NS),
1036            DnsRequestOptions::default(),
1037            client,
1038            vec![],
1039            DepthTracker::default(),
1040        ))
1041        .expect("lookup failed");
1042
1043        // Verify: NS records in ANSWER section only
1044        let answers = lookup.answers().iter().collect::<Vec<_>>();
1045        assert_eq!(
1046            answers.len(),
1047            2,
1048            "Should have exactly 2 NS records in ANSWER"
1049        );
1050
1051        // Verify all answer records are NS type
1052        for answer in &answers {
1053            assert_eq!(
1054                answer.record_type(),
1055                RecordType::NS,
1056                "All ANSWER section records should be NS type"
1057            );
1058        }
1059
1060        // Verify: Glue A records in ADDITIONAL section only
1061        let additionals = lookup.additionals().iter().collect::<Vec<_>>();
1062        assert_eq!(
1063            additionals.len(),
1064            2,
1065            "Should have exactly 2 glue A records in ADDITIONAL"
1066        );
1067
1068        // Verify all additional records are A type
1069        for additional in &additionals {
1070            assert_eq!(
1071                additional.record_type(),
1072                RecordType::A,
1073                "All ADDITIONAL section records should be A type (glue records)"
1074            );
1075        }
1076
1077        // Verify glue records do NOT appear in ANSWER section
1078        for answer in &answers {
1079            assert_ne!(
1080                answer.record_type(),
1081                RecordType::A,
1082                "A records (glue) should NEVER appear in ANSWER for NS query - this was the original bug!"
1083            );
1084        }
1085
1086        // Verify AUTHORITY section is empty
1087        assert_eq!(
1088            lookup.authorities().len(),
1089            0,
1090            "AUTHORITY section should be empty"
1091        );
1092    }
1093
1094    /// Purpose: Verify sections preserved when CNAME and target in same response
1095    ///
1096    /// This test verifies that when a CNAME and its target appear in the same DNS response,
1097    /// the AUTHORITY and ADDITIONAL sections are preserved correctly, and filtering only
1098    /// affects the ANSWER section when preserve_intermediates=false.
1099    #[test]
1100    fn test_single_hop_cname_preserves_sections() {
1101        subscribe();
1102
1103        let cache = ResponseCache::new(1, TtlConfig::default());
1104
1105        // Create a response with CNAME + A in ANSWER, plus AUTHORITY and ADDITIONAL sections
1106        let mut message = Message::response(0, OpCode::Query);
1107        message.add_query(Query::query(
1108            Name::from_str("www.example.com.").unwrap(),
1109            RecordType::A,
1110        ));
1111
1112        // ANSWER section: CNAME + A record
1113        message.insert_answers(vec![
1114            Record::from_rdata(
1115                Name::from_str("www.example.com.").unwrap(),
1116                300,
1117                RData::CNAME(CNAME(Name::from_str("v4.example.com.").unwrap())),
1118            ),
1119            Record::from_rdata(
1120                Name::from_str("v4.example.com.").unwrap(),
1121                300,
1122                RData::A(A::new(192, 0, 2, 1)),
1123            ),
1124        ]);
1125
1126        // AUTHORITY section: NS record
1127        message.insert_authorities(vec![Record::from_rdata(
1128            Name::from_str("example.com.").unwrap(),
1129            3600,
1130            RData::NS(NS(Name::from_str("ns1.example.com.").unwrap())),
1131        )]);
1132
1133        // ADDITIONAL section: Glue for NS
1134        message.insert_additionals(vec![Record::from_rdata(
1135            Name::from_str("ns1.example.com.").unwrap(),
1136            3600,
1137            RData::A(A::new(192, 0, 2, 10)),
1138        )]);
1139
1140        let client = mock(vec![
1141            error(),
1142            Ok(DnsResponse::from_message(message).unwrap()),
1143        ]);
1144        let client = CachingClient::with_cache(cache, client, false); // preserve_intermediates=false
1145
1146        let lookup = block_on(CachingClient::inner_lookup(
1147            Query::query(Name::from_str("www.example.com.").unwrap(), RecordType::A),
1148            DnsRequestOptions::default(),
1149            client,
1150            vec![],
1151            DepthTracker::default(),
1152        ))
1153        .expect("lookup failed");
1154
1155        // Verify ANSWER: Only A record (CNAME filtered out because target was found)
1156        let answers = lookup.answers().iter().collect::<Vec<_>>();
1157        assert_eq!(
1158            answers.len(),
1159            1,
1160            "ANSWER should have 1 record (CNAME filtered)"
1161        );
1162        assert_eq!(
1163            answers[0].record_type(),
1164            RecordType::A,
1165            "ANSWER should contain only the A record"
1166        );
1167        match answers[0].data {
1168            RData::A(a) => assert_eq!(a, A::new(192, 0, 2, 1), "A record should have correct IP"),
1169            _ => panic!("wrong rdata type"),
1170        }
1171
1172        // Verify AUTHORITY: NS record preserved
1173        let authorities = lookup.authorities().iter().collect::<Vec<_>>();
1174        assert_eq!(
1175            authorities.len(),
1176            1,
1177            "AUTHORITY section should be preserved"
1178        );
1179        assert_eq!(
1180            authorities[0].record_type(),
1181            RecordType::NS,
1182            "AUTHORITY should contain NS record"
1183        );
1184
1185        // Verify ADDITIONAL: Glue preserved
1186        let additionals = lookup.additionals().iter().collect::<Vec<_>>();
1187        assert_eq!(
1188            additionals.len(),
1189            1,
1190            "ADDITIONAL section should be preserved"
1191        );
1192        assert_eq!(
1193            additionals[0].record_type(),
1194            RecordType::A,
1195            "ADDITIONAL should contain glue A record"
1196        );
1197        match additionals[0].data {
1198            RData::A(a) => assert_eq!(
1199                a,
1200                A::new(192, 0, 2, 10),
1201                "Glue record should have correct IP"
1202            ),
1203            _ => panic!("wrong rdata type"),
1204        }
1205    }
1206
1207    /// test_single_hop_cname_with_preserve_intermediates
1208    ///
1209    /// Purpose: Verify CNAME is kept when preserve_intermediates=true
1210    ///
1211    /// Same setup as Test 2.1 but with preserve_intermediates=true, so the CNAME
1212    /// should be kept in the ANSWER section along with the A record.
1213    #[test]
1214    fn test_single_hop_cname_with_preserve_intermediates() {
1215        subscribe();
1216
1217        let cache = ResponseCache::new(1, TtlConfig::default());
1218
1219        // Same response as Test 2.1
1220        let mut message = Message::response(0, OpCode::Query);
1221        message.add_query(Query::query(
1222            Name::from_str("www.example.com.").unwrap(),
1223            RecordType::A,
1224        ));
1225
1226        message.insert_answers(vec![
1227            Record::from_rdata(
1228                Name::from_str("www.example.com.").unwrap(),
1229                300,
1230                RData::CNAME(CNAME(Name::from_str("v4.example.com.").unwrap())),
1231            ),
1232            Record::from_rdata(
1233                Name::from_str("v4.example.com.").unwrap(),
1234                300,
1235                RData::A(A::new(192, 0, 2, 1)),
1236            ),
1237        ]);
1238
1239        message.insert_authorities(vec![Record::from_rdata(
1240            Name::from_str("example.com.").unwrap(),
1241            3600,
1242            RData::NS(NS(Name::from_str("ns1.example.com.").unwrap())),
1243        )]);
1244
1245        message.insert_additionals(vec![Record::from_rdata(
1246            Name::from_str("ns1.example.com.").unwrap(),
1247            3600,
1248            RData::A(A::new(192, 0, 2, 10)),
1249        )]);
1250
1251        let client = mock(vec![
1252            error(),
1253            Ok(DnsResponse::from_message(message).unwrap()),
1254        ]);
1255        let client = CachingClient::with_cache(cache, client, true); // preserve_intermediates=true
1256
1257        let lookup = block_on(CachingClient::inner_lookup(
1258            Query::query(Name::from_str("www.example.com.").unwrap(), RecordType::A),
1259            DnsRequestOptions::default(),
1260            client,
1261            vec![],
1262            DepthTracker::default(),
1263        ))
1264        .expect("lookup failed");
1265
1266        // Verify ANSWER: Both CNAME and A record
1267        let answers = lookup.answers().iter().collect::<Vec<_>>();
1268        assert_eq!(answers.len(), 2, "ANSWER should have 2 records (CNAME + A)");
1269
1270        // Check for CNAME record
1271        let cname_records = answers
1272            .iter()
1273            .filter(|r| r.record_type() == RecordType::CNAME)
1274            .collect::<Vec<_>>();
1275        assert_eq!(cname_records.len(), 1, "Should have 1 CNAME record");
1276
1277        // Check for A record
1278        let a_records = answers
1279            .iter()
1280            .filter(|r| r.record_type() == RecordType::A)
1281            .collect::<Vec<_>>();
1282        assert_eq!(a_records.len(), 1, "Should have 1 A record");
1283
1284        // Verify AUTHORITY: NS records preserved (1 record)
1285        assert_eq!(
1286            lookup.authorities().len(),
1287            1,
1288            "AUTHORITY section should be preserved"
1289        );
1290
1291        // Verify ADDITIONAL: Glue preserved (1 record)
1292        assert_eq!(
1293            lookup.additionals().len(),
1294            1,
1295            "ADDITIONAL section should be preserved"
1296        );
1297    }
1298
1299    /// Purpose: Verify only final response sections are preserved in multi-hop CNAME chains
1300    ///
1301    /// This test verifies that in a multi-hop CNAME chain, only the AUTHORITY and ADDITIONAL
1302    /// sections from the FINAL response are preserved, not merged from intermediate responses.
1303    #[test]
1304    fn test_multi_hop_cname_preserves_final_sections() {
1305        subscribe();
1306
1307        let cache = ResponseCache::new(1, TtlConfig::default());
1308
1309        // Response 1 (first hop): CNAME only
1310        let mut message1 = Message::response(0, OpCode::Query);
1311        message1.add_query(Query::query(
1312            Name::from_str("www.example.com.").unwrap(),
1313            RecordType::A,
1314        ));
1315
1316        message1.insert_answers(vec![Record::from_rdata(
1317            Name::from_str("www.example.com.").unwrap(),
1318            300,
1319            RData::CNAME(CNAME(Name::from_str("v4.example.com.").unwrap())),
1320        )]);
1321
1322        // AUTHORITY from first response (should NOT be in final result)
1323        message1.insert_authorities(vec![Record::from_rdata(
1324            Name::from_str("www-zone.example.com.").unwrap(),
1325            3600,
1326            RData::NS(NS(Name::from_str("ns-www.example.com.").unwrap())),
1327        )]);
1328
1329        // ADDITIONAL from first response (should NOT be in final result)
1330        message1.insert_additionals(vec![Record::from_rdata(
1331            Name::from_str("ns-www.example.com.").unwrap(),
1332            3600,
1333            RData::A(A::new(192, 0, 2, 20)),
1334        )]);
1335
1336        // Response 2 (second hop): Final A record
1337        let mut message2 = Message::response(0, OpCode::Query);
1338        message2.add_query(Query::query(
1339            Name::from_str("v4.example.com.").unwrap(),
1340            RecordType::A,
1341        ));
1342
1343        message2.insert_answers(vec![Record::from_rdata(
1344            Name::from_str("v4.example.com.").unwrap(),
1345            300,
1346            RData::A(A::new(192, 0, 2, 1)),
1347        )]);
1348
1349        // AUTHORITY from second response (SHOULD be in final result)
1350        message2.insert_authorities(vec![Record::from_rdata(
1351            Name::from_str("v4-zone.example.com.").unwrap(),
1352            3600,
1353            RData::NS(NS(Name::from_str("ns-v4.example.com.").unwrap())),
1354        )]);
1355
1356        // ADDITIONAL from second response (SHOULD be in final result)
1357        message2.insert_additionals(vec![Record::from_rdata(
1358            Name::from_str("ns-v4.example.com.").unwrap(),
1359            3600,
1360            RData::A(A::new(192, 0, 2, 30)),
1361        )]);
1362
1363        let mut client = CachingClient::with_cache(cache, mock(vec![]), false); // preserve_intermediates=false
1364
1365        // First hop: Process CNAME response
1366        let result1 = CachingClient::handle_noerror(
1367            &mut client,
1368            DnsRequestOptions::default(),
1369            &Query::query(Name::from_str("www.example.com.").unwrap(), RecordType::A),
1370            DnsResponse::from_message(message1).unwrap(),
1371            vec![],
1372            DepthTracker::default(),
1373        );
1374
1375        // Should return Records::CnameChain with empty preserved_records (preserve_intermediates=false)
1376        let preserved_records = match result1 {
1377            Ok(Records::CnameChain {
1378                preserved_records, ..
1379            }) => {
1380                // Verify preserved_records is empty when preserve_intermediates=false
1381                assert_eq!(
1382                    preserved_records.len(),
1383                    0,
1384                    "With preserve_intermediates=false, preserved_records should be empty"
1385                );
1386                preserved_records
1387            }
1388            Ok(Records::Exists { .. }) => {
1389                panic!("Expected Records::CnameChain from first hop, got Records::Exists")
1390            }
1391            Err(e) => panic!(
1392                "Expected Records::CnameChain from first hop, got error: {}",
1393                e
1394            ),
1395        };
1396
1397        // Second hop: Process final A record response
1398        let result2 = CachingClient::handle_noerror(
1399            &mut client,
1400            DnsRequestOptions::default(),
1401            &Query::query(Name::from_str("v4.example.com.").unwrap(), RecordType::A),
1402            DnsResponse::from_message(message2).unwrap(),
1403            preserved_records,
1404            DepthTracker::default().nest(),
1405        );
1406
1407        // Should return Records::Exists
1408        let lookup_message = match result2 {
1409            Ok(Records::Exists { message, .. }) => message,
1410            Ok(Records::CnameChain { .. }) => {
1411                panic!("Expected Records::Exists from second hop, got Records::CnameChain")
1412            }
1413            Err(e) => panic!("Expected Records::Exists from second hop, got error: {}", e),
1414        };
1415
1416        // Create a Lookup from the final message
1417        let lookup = Lookup::new(lookup_message, Instant::now() + Duration::from_secs(300));
1418
1419        // Verify ANSWER: Only final A record (CNAME from Response 1 filtered)
1420        let answers = lookup.answers().iter().collect::<Vec<_>>();
1421        assert_eq!(
1422            answers.len(),
1423            1,
1424            "ANSWER should have only the final A record"
1425        );
1426        assert_eq!(answers[0].record_type(), RecordType::A);
1427        match answers[0].data {
1428            RData::A(a) => assert_eq!(a, A::new(192, 0, 2, 1), "Should have IP from Response 2"),
1429            _ => panic!("wrong rdata type"),
1430        }
1431        match answers[0].data {
1432            RData::A(a) => assert_eq!(a, A::new(192, 0, 2, 1), "Should have IP from Response 2"),
1433            _ => panic!("wrong rdata type"),
1434        }
1435
1436        // Verify AUTHORITY: From Response 2 only (not merged with Response 1)
1437        let authorities = lookup.authorities().iter().collect::<Vec<_>>();
1438        assert_eq!(
1439            authorities.len(),
1440            1,
1441            "AUTHORITY should have 1 record from final response only"
1442        );
1443
1444        // Check it's the NS from Response 2, not Response 1
1445        match &authorities[0].data {
1446            RData::NS(ns_name) => assert_eq!(
1447                ns_name.0,
1448                Name::from_str("ns-v4.example.com.").unwrap(),
1449                "AUTHORITY should be from Response 2 (ns-v4), NOT Response 1 (ns-www)"
1450            ),
1451            _ => panic!("wrong rdata type"),
1452        }
1453
1454        // Verify ADDITIONAL: From Response 2 only
1455        let additionals = lookup.additionals().iter().collect::<Vec<_>>();
1456        assert_eq!(
1457            additionals.len(),
1458            1,
1459            "ADDITIONAL should have 1 record from final response only"
1460        );
1461
1462        // Check it's the IP from Response 2, not Response 1
1463        match additionals[0].data {
1464            RData::A(a) => assert_eq!(
1465                a,
1466                A::new(192, 0, 2, 30),
1467                "ADDITIONAL should have IP 192.0.2.30 from Response 2, NOT 192.0.2.20 from Response 1"
1468            ),
1469            _ => panic!("wrong rdata type"),
1470        }
1471    }
1472
1473    /// test_multi_hop_cname_with_preserve_accumulates_cnames
1474    ///
1475    /// Purpose: Verify CNAMEs from multiple hops are accumulated when
1476    /// preserve_intermediates=true
1477    ///
1478    /// Same setup as test_multi_hop_cname_preserves_final_sections
1479    /// but with preserve_intermediates=true, so the CNAME from the
1480    /// first hop should be included in the final ANSWER section.
1481    ///
1482    /// Uses handle_noerror directly to test the two-hop CNAME chain
1483    /// with CNAME preservation.
1484    #[test]
1485    fn test_multi_hop_cname_with_preserve_accumulates_cnames() {
1486        subscribe();
1487
1488        let cache = ResponseCache::new(1, TtlConfig::default());
1489
1490        // Response 1 (first hop): CNAME only
1491        let mut message1 = Message::response(0, OpCode::Query);
1492        message1.add_query(Query::query(
1493            Name::from_str("www.example.com.").unwrap(),
1494            RecordType::A,
1495        ));
1496
1497        message1.insert_answers(vec![Record::from_rdata(
1498            Name::from_str("www.example.com.").unwrap(),
1499            300,
1500            RData::CNAME(CNAME(Name::from_str("v4.example.com.").unwrap())),
1501        )]);
1502
1503        message1.insert_authorities(vec![Record::from_rdata(
1504            Name::from_str("www-zone.example.com.").unwrap(),
1505            3600,
1506            RData::NS(NS(Name::from_str("ns-www.example.com.").unwrap())),
1507        )]);
1508
1509        message1.insert_additionals(vec![Record::from_rdata(
1510            Name::from_str("ns-www.example.com.").unwrap(),
1511            3600,
1512            RData::A(A::new(192, 0, 2, 20)),
1513        )]);
1514
1515        // Response 2 (second hop): Final A record
1516        let mut message2 = Message::response(0, OpCode::Query);
1517        message2.add_query(Query::query(
1518            Name::from_str("v4.example.com.").unwrap(),
1519            RecordType::A,
1520        ));
1521
1522        message2.insert_answers(vec![Record::from_rdata(
1523            Name::from_str("v4.example.com.").unwrap(),
1524            300,
1525            RData::A(A::new(192, 0, 2, 1)),
1526        )]);
1527
1528        message2.insert_authorities(vec![Record::from_rdata(
1529            Name::from_str("v4-zone.example.com.").unwrap(),
1530            3600,
1531            RData::NS(NS(Name::from_str("ns-v4.example.com.").unwrap())),
1532        )]);
1533
1534        message2.insert_additionals(vec![Record::from_rdata(
1535            Name::from_str("ns-v4.example.com.").unwrap(),
1536            3600,
1537            RData::A(A::new(192, 0, 2, 30)),
1538        )]);
1539
1540        let client = mock(vec![]);
1541        let mut client = CachingClient::with_cache(cache, client, true); // preserve_intermediates=true
1542
1543        // First hop: Process CNAME response
1544        let result1 = CachingClient::handle_noerror(
1545            &mut client,
1546            DnsRequestOptions::default(),
1547            &Query::query(Name::from_str("www.example.com.").unwrap(), RecordType::A),
1548            DnsResponse::from_message(message1.clone()).unwrap(),
1549            vec![],
1550            DepthTracker::default(),
1551        );
1552
1553        // With preserve_intermediates=true, verify CNAME is preserved
1554        let preserved_records = match result1 {
1555            Ok(Records::CnameChain {
1556                preserved_records, ..
1557            }) => {
1558                // Verify preserved_records contains the CNAME when preserve_intermediates=true
1559                assert_eq!(
1560                    preserved_records.len(),
1561                    1,
1562                    "With preserve_intermediates=true, preserved_records should contain the CNAME"
1563                );
1564                assert_eq!(
1565                    preserved_records[0].record_type(),
1566                    RecordType::CNAME,
1567                    "Preserved record should be a CNAME"
1568                );
1569                preserved_records
1570            }
1571            _ => panic!("Expected CnameChain from first hop"),
1572        };
1573
1574        // Second hop: Process final A record with preserved CNAME
1575        let result2 = CachingClient::handle_noerror(
1576            &mut client,
1577            DnsRequestOptions::default(),
1578            &Query::query(Name::from_str("v4.example.com.").unwrap(), RecordType::A),
1579            DnsResponse::from_message(message2).unwrap(),
1580            preserved_records,
1581            DepthTracker::default().nest(),
1582        );
1583
1584        let lookup_message = match result2 {
1585            Ok(Records::Exists { message, .. }) => message,
1586            Ok(Records::CnameChain { .. }) => {
1587                panic!("Expected Records::Exists from second hop, got Records::CnameChain")
1588            }
1589            Err(e) => panic!("Expected Records::Exists from second hop, got error: {}", e),
1590        };
1591
1592        // Create a Lookup from the final message
1593        let lookup = Lookup::new(lookup_message, Instant::now() + Duration::from_secs(300));
1594
1595        // Verify ANSWER: CNAME from Response 1 + A from Response 2
1596        let answers = lookup.answers().iter().collect::<Vec<_>>();
1597        assert_eq!(
1598            answers.len(),
1599            2,
1600            "ANSWER should have CNAME + A (both preserved)"
1601        );
1602
1603        // Check for CNAME record (from Response 1)
1604        let cname_records = answers
1605            .iter()
1606            .filter(|r| r.record_type() == RecordType::CNAME)
1607            .collect::<Vec<_>>();
1608        assert_eq!(
1609            cname_records.len(),
1610            1,
1611            "Should have 1 CNAME from Response 1"
1612        );
1613
1614        match &cname_records[0].data {
1615            RData::CNAME(cname_target) => assert_eq!(
1616                cname_target.0,
1617                Name::from_str("v4.example.com.").unwrap(),
1618                "CNAME should point to v4.example.com"
1619            ),
1620            _ => panic!("wrong rdata type"),
1621        }
1622
1623        // Check for A record (from Response 2)
1624        let a_records = answers
1625            .iter()
1626            .filter(|r| r.record_type() == RecordType::A)
1627            .collect::<Vec<_>>();
1628        assert_eq!(a_records.len(), 1, "Should have 1 A record");
1629        match a_records[0].data {
1630            RData::A(a) => assert_eq!(
1631                a,
1632                A::new(192, 0, 2, 1),
1633                "A record should have IP from Response 2"
1634            ),
1635            _ => panic!("wrong rdata type"),
1636        };
1637
1638        // Verify AUTHORITY: From Response 2 only (1 record)
1639        assert_eq!(
1640            lookup.authorities().len(),
1641            1,
1642            "AUTHORITY should be from final response only"
1643        );
1644
1645        // Verify ADDITIONAL: From Response 2 only (1 record)
1646        assert_eq!(
1647            lookup.additionals().len(),
1648            1,
1649            "ADDITIONAL should be from final response only"
1650        );
1651    }
1652
1653    fn cname_ttl_test(first: u32, second: u32) {
1654        let lru = ResponseCache::new(1, TtlConfig::default());
1655        // expecting no queries to be performed
1656        let mut client = CachingClient::with_cache(lru, mock(vec![error()]), false);
1657
1658        let mut message = Message::query();
1659        message.insert_answers(vec![Record::from_rdata(
1660            Name::from_str("ttl.example.com.").unwrap(),
1661            first,
1662            RData::CNAME(CNAME(Name::from_str("actual.example.com.").unwrap())),
1663        )]);
1664        message.insert_additionals(vec![Record::from_rdata(
1665            Name::from_str("actual.example.com.").unwrap(),
1666            second,
1667            RData::A(A::new(127, 0, 0, 1)),
1668        )]);
1669
1670        let records = CachingClient::handle_noerror(
1671            &mut client,
1672            DnsRequestOptions::default(),
1673            &Query::query(Name::from_str("ttl.example.com.").unwrap(), RecordType::A),
1674            DnsResponse::from_message(message.into_response()).unwrap(),
1675            vec![],
1676            DepthTracker::default(),
1677        );
1678
1679        if let Ok(Records::Exists { message }) = records {
1680            assert!(!message.answers.is_empty());
1681        } else {
1682            panic!("expected Records::Exists");
1683        }
1684    }
1685
1686    #[test]
1687    fn test_cname_ttl() {
1688        subscribe();
1689        cname_ttl_test(1, 2);
1690        cname_ttl_test(2, 1);
1691    }
1692
1693    #[test]
1694    fn test_early_return_localhost() {
1695        subscribe();
1696        let cache = ResponseCache::new(0, TtlConfig::default());
1697        let client = mock(vec![empty()]);
1698        let client = CachingClient::with_cache(cache, client, false);
1699
1700        {
1701            let query = Query::query(Name::from_ascii("localhost.").unwrap(), RecordType::A);
1702            let lookup = block_on(client.lookup(query.clone(), DnsRequestOptions::default()))
1703                .expect("should have returned localhost");
1704            assert_eq!(lookup.query(), &query);
1705            assert_eq!(
1706                lookup.answers(),
1707                &[Record::from_rdata(
1708                    query.name().clone(),
1709                    MAX_TTL,
1710                    LOCALHOST_V4.clone()
1711                )]
1712            );
1713        }
1714
1715        {
1716            let query = Query::query(Name::from_ascii("localhost.").unwrap(), RecordType::AAAA);
1717            let lookup = block_on(client.lookup(query.clone(), DnsRequestOptions::default()))
1718                .expect("should have returned localhost");
1719            assert_eq!(lookup.query(), &query);
1720            assert_eq!(
1721                lookup.answers(),
1722                &[Record::from_rdata(
1723                    query.name().clone(),
1724                    MAX_TTL,
1725                    LOCALHOST_V6.clone()
1726                )]
1727            );
1728        }
1729
1730        {
1731            let query = Query::query(Name::from(Ipv4Addr::LOCALHOST), RecordType::PTR);
1732            let lookup = block_on(client.lookup(query.clone(), DnsRequestOptions::default()))
1733                .expect("should have returned localhost");
1734            assert_eq!(lookup.query(), &query);
1735            assert_eq!(
1736                lookup.answers(),
1737                &[Record::from_rdata(
1738                    query.name().clone(),
1739                    MAX_TTL,
1740                    LOCALHOST.clone()
1741                )]
1742            );
1743        }
1744
1745        {
1746            let query = Query::query(
1747                Name::from(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)),
1748                RecordType::PTR,
1749            );
1750            let lookup = block_on(client.lookup(query.clone(), DnsRequestOptions::default()))
1751                .expect("should have returned localhost");
1752            assert_eq!(lookup.query(), &query);
1753            assert_eq!(
1754                lookup.answers(),
1755                &[Record::from_rdata(
1756                    query.name().clone(),
1757                    MAX_TTL,
1758                    LOCALHOST.clone()
1759                )]
1760            );
1761        }
1762
1763        assert!(
1764            block_on(client.lookup(
1765                Query::query(Name::from_ascii("localhost.").unwrap(), RecordType::MX),
1766                DnsRequestOptions::default()
1767            ))
1768            .is_err()
1769        );
1770
1771        assert!(
1772            block_on(client.lookup(
1773                Query::query(Name::from(Ipv4Addr::LOCALHOST), RecordType::MX),
1774                DnsRequestOptions::default()
1775            ))
1776            .is_err()
1777        );
1778
1779        assert!(
1780            block_on(client.lookup(
1781                Query::query(
1782                    Name::from(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)),
1783                    RecordType::MX
1784                ),
1785                DnsRequestOptions::default()
1786            ))
1787            .is_err()
1788        );
1789    }
1790
1791    #[test]
1792    fn test_early_return_invalid() {
1793        subscribe();
1794        let cache = ResponseCache::new(0, TtlConfig::default());
1795        let client = mock(vec![empty()]);
1796        let client = CachingClient::with_cache(cache, client, false);
1797
1798        assert!(
1799            block_on(client.lookup(
1800                Query::query(
1801                    Name::from_ascii("horrible.invalid.").unwrap(),
1802                    RecordType::A,
1803                ),
1804                DnsRequestOptions::default()
1805            ))
1806            .is_err()
1807        );
1808    }
1809
1810    #[test]
1811    fn test_no_error_on_dot_local_no_mdns() {
1812        subscribe();
1813
1814        let cache = ResponseCache::new(1, TtlConfig::default());
1815
1816        let mut message = srv_message().unwrap().into_message();
1817        message.add_query(Query::query(
1818            Name::from_ascii("www.example.local.").unwrap(),
1819            RecordType::A,
1820        ));
1821        message.add_answer(Record::from_rdata(
1822            Name::from_str("www.example.local.").unwrap(),
1823            86400,
1824            RData::A(A::new(127, 0, 0, 1)),
1825        ));
1826
1827        let client = mock(vec![
1828            error(),
1829            Ok(DnsResponse::from_message(message).unwrap()),
1830        ]);
1831        let client = CachingClient::with_cache(cache, client, false);
1832
1833        assert!(
1834            block_on(client.lookup(
1835                Query::query(
1836                    Name::from_ascii("www.example.local.").unwrap(),
1837                    RecordType::A,
1838                ),
1839                DnsRequestOptions::default()
1840            ))
1841            .is_ok()
1842        );
1843    }
1844}