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