Skip to main content

hickory_resolver/
name_server_pool.rs

1// Copyright 2015-2019 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
8use std::collections::{HashMap, VecDeque};
9use std::net::IpAddr;
10use std::pin::Pin;
11use std::sync::atomic::AtomicU8;
12use std::sync::{
13    Arc,
14    atomic::{AtomicUsize, Ordering as AtomicOrdering},
15};
16use std::task::{Context, Poll};
17use std::time::{Duration, Instant, SystemTime};
18
19use futures_util::lock::{Mutex as AsyncMutex, MutexGuard};
20use futures_util::stream::{FuturesUnordered, Stream, StreamExt, once};
21use futures_util::{
22    Future, FutureExt,
23    future::{BoxFuture, Shared},
24};
25use parking_lot::Mutex;
26#[cfg(feature = "serde")]
27use serde::{Deserialize, Serialize};
28use smallvec::SmallVec;
29#[cfg(all(feature = "toml", any(feature = "__tls", feature = "__quic")))]
30use tracing::info;
31use tracing::{debug, error};
32
33#[cfg(any(feature = "__tls", feature = "__quic"))]
34use crate::config::OpportunisticEncryptionConfig;
35use crate::{
36    config::{NameServerConfig, OpportunisticEncryption, ResolverOpts, ServerOrderingStrategy},
37    connection_provider::{ConnectionProvider, TlsConfig},
38    name_server::{ConnectionPolicy, NameServer},
39    net::{
40        DnsError, NetError, NoRecords,
41        runtime::{RuntimeProvider, Time},
42        xfer::{DnsHandle, Protocol},
43    },
44    proto::{
45        access_control::AccessControlSet,
46        op::{DnsRequest, DnsRequestOptions, DnsResponse, OpCode, Query, ResponseCode},
47        rr::{
48            Name, RData, Record,
49            rdata::{
50                A, AAAA,
51                opt::{ClientSubnet, EdnsCode, EdnsOption},
52            },
53        },
54    },
55};
56
57/// Abstract interface for mocking purpose
58#[derive(Clone)]
59pub struct NameServerPool<P: ConnectionProvider> {
60    state: Arc<PoolState<P>>,
61    active_requests: Arc<Mutex<HashMap<Arc<CacheKey>, SharedLookup>>>,
62    ttl: Option<TtlInstant>,
63    zone: Option<Name>,
64}
65
66impl<P: ConnectionProvider> NameServerPool<P> {
67    /// Construct a NameServerPool from a set of name server configs
68    pub fn from_config(
69        servers: impl IntoIterator<Item = NameServerConfig>,
70        cx: Arc<PoolContext>,
71        conn_provider: P,
72    ) -> Self {
73        Self::from_nameservers(
74            servers
75                .into_iter()
76                .map(|server| {
77                    Arc::new(NameServer::new(
78                        [],
79                        server,
80                        &cx.options,
81                        conn_provider.clone(),
82                    ))
83                })
84                .collect(),
85            cx,
86        )
87    }
88
89    #[doc(hidden)]
90    pub fn from_nameservers(servers: Vec<Arc<NameServer<P>>>, cx: Arc<PoolContext>) -> Self {
91        Self {
92            state: Arc::new(PoolState {
93                servers,
94                cx,
95                next: AtomicUsize::new(0),
96            }),
97            active_requests: Arc::new(Mutex::new(HashMap::new())),
98            ttl: None,
99            zone: None,
100        }
101    }
102
103    /// Set a TTL on the NameServerPool
104    pub fn with_ttl(mut self, ttl: Duration) -> Self {
105        self.ttl = Some(TtlInstant::now() + ttl);
106        self
107    }
108
109    /// Set the zone on the NameServerPool
110    pub fn with_zone(mut self, zone: Name) -> Self {
111        self.zone = Some(zone);
112        self
113    }
114
115    /// Check if the TTL on the NameServerPool (if set) has expired
116    pub fn ttl_expired(&self) -> bool {
117        match self.ttl {
118            Some(ttl) => TtlInstant::now() > ttl,
119            None => false,
120        }
121    }
122
123    /// Returns the pool's options.
124    pub fn context(&self) -> &Arc<PoolContext> {
125        &self.state.cx
126    }
127
128    /// Return the zone associated with the pool
129    pub fn zone(&self) -> Option<&Name> {
130        self.zone.as_ref()
131    }
132}
133
134// Type alias for TTL unit tests to use tokio's time pause/advance
135#[cfg(not(feature = "tokio"))]
136type TtlInstant = std::time::Instant;
137#[cfg(feature = "tokio")]
138type TtlInstant = tokio::time::Instant;
139
140impl<P: ConnectionProvider> DnsHandle for NameServerPool<P> {
141    type Response = Pin<Box<dyn Stream<Item = Result<DnsResponse, NetError>> + Send>>;
142    type Runtime = P::RuntimeProvider;
143
144    fn lookup(&self, query: Query, mut options: DnsRequestOptions) -> Self::Response {
145        debug!("querying: {} {:?}", query.name(), query.query_type());
146        options.case_randomization = self.state.cx.options.case_randomization;
147        self.send(DnsRequest::from_query(query, options))
148    }
149
150    fn send(&self, request: DnsRequest) -> Self::Response {
151        let state = self.state.clone();
152        let acs = self.state.cx.answer_address_filter.clone();
153        let active_requests = self.active_requests.clone();
154
155        Box::pin(once(async move {
156            debug!("sending request: {:?}", request.queries);
157            let query = match request.queries.first() {
158                Some(q) => q.clone(),
159                None => return Err("no query in request".into()),
160            };
161
162            let key = Arc::new(CacheKey::from_request(&request));
163
164            let (lookup, is_creator) = {
165                let mut active = active_requests.lock();
166                if let Some(existing) = active.get(&key) {
167                    debug!(%query, "query currently in progress - returning shared lookup");
168                    (existing.clone(), false)
169                } else {
170                    debug!(%query, "creating new shared lookup");
171
172                    let lookup = async move {
173                        match state.try_send(request).await {
174                            Ok(response) => Some(Ok(response)),
175                            Err(e) => Some(Err(e)),
176                        }
177                    }
178                    .boxed()
179                    .shared();
180
181                    let shared_lookup = SharedLookup(lookup);
182                    active.insert(key.clone(), shared_lookup.clone());
183                    (shared_lookup, true)
184                }
185            };
186
187            // Only the creator removes the key so that the entry is not
188            // removed prematurely by a waiter task.  Using a guard ensures
189            // the entry is removed even if `lookup.await` panics, which
190            // would otherwise leave a poisoned `SharedLookup` in the map and
191            // cause every subsequent request for the same key to also panic.
192            let _cleanup = is_creator.then(|| ActiveRequestCleanup {
193                active_requests: active_requests.clone(),
194                key: key.clone(),
195            });
196
197            let response = lookup.await;
198            let mut response = response?;
199
200            for record in response
201                .answers
202                .iter()
203                .chain(response.authorities.iter())
204                .chain(response.additionals.iter())
205            {
206                if record.dns_class == query.query_class {
207                    continue;
208                }
209                error!(
210                    %query,
211                    record_name = %record.name,
212                    record_class = %record.dns_class,
213                    record_type = %record.record_type(),
214                    "rejecting response: record class does not match query class",
215                );
216                return Err(NetError::ForeignClassRecord {
217                    record_name: record.name.clone(),
218                    record_class: record.dns_class,
219                    record_type: record.record_type(),
220                });
221            }
222
223            if acs.allows_all() {
224                return Ok(response);
225            }
226
227            let answer_filter = |record: &Record| {
228                let ip = match &record.data {
229                    RData::A(A(ipv4)) => (*ipv4).into(),
230                    RData::AAAA(AAAA(ipv6)) => (*ipv6).into(),
231                    _ => return true,
232                };
233
234                if acs.denied(ip) {
235                    error!(
236                        %query,
237                        %ip,
238                        "removing ip from response: answer filter matched"
239                    );
240
241                    false
242                } else {
243                    true
244                }
245            };
246
247            let answers_len = response.answers.len();
248            let authorities_len = response.authorities.len();
249
250            response.additionals.retain(answer_filter);
251            response.answers.retain(answer_filter);
252            response.authorities.retain(answer_filter);
253
254            if response.answers.is_empty() && answers_len != 0
255                || (response.answers.is_empty()
256                    && response.authorities.is_empty()
257                    && authorities_len != 0)
258            {
259                return Err(NoRecords::new(Box::new(query.clone()), ResponseCode::NXDomain).into());
260            }
261
262            // Since the message might have changed, create a new response from
263            // the message to update the buffer.
264            DnsResponse::from_message(response.into_message()).map_err(NetError::from)
265        }))
266    }
267}
268
269struct PoolState<P: ConnectionProvider> {
270    servers: Vec<Arc<NameServer<P>>>,
271    cx: Arc<PoolContext>,
272    next: AtomicUsize,
273}
274
275impl<P: ConnectionProvider> PoolState<P> {
276    async fn try_send(&self, request: DnsRequest) -> Result<DnsResponse, NetError> {
277        let mut servers = self.servers.clone();
278        match self.cx.options.server_ordering_strategy {
279            // select the highest priority connection
280            //   reorder the connections based on current view...
281            //   this reorders the inner set
282            ServerOrderingStrategy::QueryStatistics => {
283                sort_servers_by_query_statistics(&mut servers);
284            }
285            ServerOrderingStrategy::UserProvidedOrder => {}
286            ServerOrderingStrategy::RoundRobin => {
287                let num_concurrent_reqs = if self.cx.options.num_concurrent_reqs > 1 {
288                    self.cx.options.num_concurrent_reqs
289                } else {
290                    1
291                };
292                if num_concurrent_reqs < servers.len() {
293                    let index = self
294                        .next
295                        .fetch_add(num_concurrent_reqs, AtomicOrdering::SeqCst)
296                        % servers.len();
297                    servers.rotate_left(index);
298                }
299            }
300        }
301
302        // If the name server we're trying is giving us backpressure by returning NetErrorKind::Busy,
303        // we will first try the other name servers (as for other error types). However, if the other
304        // servers are also busy, we're going to wait for a little while and then retry each server that
305        // returned Busy in the previous round. If the server is still Busy, this continues, while
306        // the backoff increases exponentially (by a factor of 2), until it hits 300ms, in which case we
307        // give up. The request might still be retried by the caller (likely the DnsRetryHandle).
308        //
309        // Enforce an end-to-end deadline so the total time spent in this loop never exceeds the
310        // configured timeout.  Without this, the pool can spend up to N × timeout (where N is the
311        // number of servers) before returning an error — well past the point where clients have
312        // given up and retransmitted the query.
313        let deadline = Instant::now() + self.cx.options.timeout;
314
315        let mut servers = VecDeque::from(servers);
316        let mut backoff = Duration::from_millis(20);
317        let mut busy = SmallVec::<[Arc<NameServer<P>>; 2]>::new();
318        let mut err = NetError::NoConnections;
319        let mut policy = ConnectionPolicy::default();
320
321        loop {
322            // Check the deadline before starting a new round of server attempts.
323            if Instant::now() >= deadline {
324                return Err(NetError::Timeout);
325            }
326
327            // construct the parallel requests, 2 is the default
328            let mut par_servers = SmallVec::<[_; 2]>::new();
329            while !servers.is_empty()
330                && par_servers.len() < Ord::max(self.cx.options.num_concurrent_reqs, 1)
331            {
332                if let Some(server) = servers.pop_front() {
333                    if policy.allows_server(&server) {
334                        par_servers.push(server);
335                    }
336                }
337            }
338
339            if par_servers.is_empty() {
340                if !busy.is_empty() && backoff < Duration::from_millis(300) {
341                    // Cap the backoff sleep so we don't sleep past the deadline.
342                    let remaining = deadline.saturating_duration_since(Instant::now());
343                    if remaining.is_zero() {
344                        return Err(NetError::Timeout);
345                    }
346                    <<P as ConnectionProvider>::RuntimeProvider as RuntimeProvider>::Timer::delay_for(
347                        backoff.min(remaining),
348                    ).await;
349                    servers.extend(busy.drain(..).filter(|ns| policy.allows_server(ns)));
350                    backoff *= 2;
351                    continue;
352                }
353                return Err(err);
354            }
355
356            // Track all servers in the parallel batch so we can penalize any
357            // that are still in-flight when a winner is found.
358            let in_flight = par_servers.iter().cloned().collect::<SmallVec<[_; 2]>>();
359
360            let batch_start = Instant::now();
361            let mut requests = par_servers
362                .into_iter()
363                .map(|server| {
364                    let mut request = request.clone();
365
366                    // Set the retry interval to 1.2 times the current decayed SRTT
367                    let retry_interval =
368                        Duration::from_micros((server.decayed_srtt() * 1.2) as u64);
369                    request.options_mut().retry_interval = retry_interval;
370                    debug!(?retry_interval, ip = ?server.ip(), "setting retry_interval");
371
372                    let future = server.clone().send(request, policy, &self.cx);
373                    async { (server, future.await) }
374                })
375                .collect::<FuturesUnordered<_>>();
376
377            // Servers that have already completed (successfully or with an
378            // error) — used to avoid double-penalizing them.
379            let mut completed = SmallVec::<[IpAddr; 2]>::new();
380
381            while let Some((server, result)) = requests.next().await {
382                completed.push(server.ip());
383                let e = match result {
384                    Ok(response) if response.truncation && policy.disable_udp => {
385                        debug!("truncated response received and UDP already disabled, giving up");
386                        NetError::Truncated
387                    }
388                    Ok(response) if response.truncation => {
389                        debug!("truncated response received, retrying over TCP");
390                        policy.disable_udp = true;
391                        err = NetError::Truncated;
392                        servers.push_front(server);
393                        continue;
394                    }
395                    Ok(response) => {
396                        // Penalize servers still in-flight (see `record_cancelled`).
397                        let winner_rtt = batch_start.elapsed();
398                        for abandoned in &in_flight {
399                            if !completed.contains(&abandoned.ip()) {
400                                debug!(ip = ?abandoned.ip(), ?winner_rtt, "recording cancelled parallel server");
401                                abandoned.record_cancelled(winner_rtt);
402                            }
403                        }
404                        return Ok(response);
405                    }
406                    Err(e) => e,
407                };
408
409                match &e {
410                    // We assume the response is spoofed, so ignore it and avoid UDP server for this
411                    // request to try and avoid further spoofing.
412                    NetError::QueryCaseMismatch => {
413                        servers.push_front(server);
414                        policy.disable_udp = true;
415                        continue;
416                    }
417                    // If the server is busy, try it again later if necessary.
418                    NetError::Busy => busy.push(server),
419                    // If the connection failed or timed out, try another one.
420                    NetError::Io(_) | NetError::NoConnections | NetError::Timeout => {}
421                    // If we got an `NXDomain` response from a server whose negative responses we
422                    // don't trust, we should try another server.
423                    NetError::Dns(DnsError::NoRecordsFound(NoRecords {
424                        response_code: ResponseCode::NXDomain,
425                        ..
426                    })) if !server.trust_negative_responses() => {}
427                    _ => return Err(e),
428                }
429
430                err = most_specific(err, e);
431            }
432        }
433    }
434}
435
436/// Compare two errors to see if one contains a server response.
437fn most_specific(previous: NetError, current: NetError) -> NetError {
438    match (&previous, &current) {
439        (
440            NetError::Dns(DnsError::NoRecordsFound { .. }),
441            NetError::Dns(DnsError::NoRecordsFound { .. }),
442        ) => return previous,
443        (NetError::Dns(DnsError::NoRecordsFound { .. }), _) => return previous,
444        (_, NetError::Dns(DnsError::NoRecordsFound { .. })) => return current,
445        _ => (),
446    }
447
448    match (&previous, &current) {
449        (NetError::Io { .. }, NetError::Io { .. }) => return previous,
450        (NetError::Io { .. }, _) => return current,
451        (_, NetError::Io { .. }) => return previous,
452        _ => (),
453    }
454
455    match (&previous, &current) {
456        (NetError::Timeout, NetError::Timeout) => return previous,
457        (NetError::Timeout, _) => return previous,
458        (_, NetError::Timeout) => return current,
459        _ => (),
460    }
461
462    previous
463}
464
465/// Sorts servers by their decayed SRTT for query-statistics-based ordering.
466///
467/// Uses `sort_by_cached_key` to evaluate each server's decayed SRTT exactly
468/// once. This is critical because `decayed_srtt()` reads shared mutable state
469/// that can change between calls due to concurrent query completions, which
470/// would violate the total-order invariant required by `sort_by`.
471pub(crate) fn sort_servers_by_query_statistics<P: ConnectionProvider>(
472    servers: &mut [Arc<NameServer<P>>],
473) {
474    // Positive f64 bit patterns sort in the same order as their float values,
475    // so to_bits() is a valid u64 ordering key for non-negative SRTT values.
476    servers.sort_by_cached_key(|s| s.decayed_srtt().to_bits());
477}
478
479/// Context for a [`NameServerPool`]
480#[non_exhaustive]
481pub struct PoolContext {
482    /// Resolver options
483    pub options: ResolverOpts,
484    /// TLS configuration
485    #[cfg(feature = "__tls")]
486    pub tls: rustls::ClientConfig,
487    /// Opportunistic probe budget
488    pub opportunistic_probe_budget: AtomicU8,
489    /// Opportunistic encryption configuration
490    pub opportunistic_encryption: OpportunisticEncryption,
491    /// Opportunistic encryption name server transport state
492    pub transport_state: AsyncMutex<NameServerTransportState>,
493    /// Answer address filter
494    pub answer_address_filter: AccessControlSet,
495}
496
497impl PoolContext {
498    /// Creates a new PoolContext
499    #[cfg_attr(not(feature = "__tls"), expect(unused_variables))]
500    pub fn new(options: ResolverOpts, tls: TlsConfig) -> Self {
501        Self {
502            answer_address_filter: options.answer_address_filter(),
503            options,
504            #[cfg(feature = "__tls")]
505            tls: tls.config,
506            opportunistic_probe_budget: AtomicU8::default(),
507            opportunistic_encryption: OpportunisticEncryption::default(),
508            transport_state: AsyncMutex::new(NameServerTransportState::default()),
509        }
510    }
511
512    /// Set the opportunistic probe budget
513    pub fn with_probe_budget(self, budget: u8) -> Self {
514        self.opportunistic_probe_budget
515            .store(budget, AtomicOrdering::SeqCst);
516        self
517    }
518
519    /// Add an answer address filter
520    pub fn with_answer_filter(mut self, answer_filter: AccessControlSet) -> Self {
521        self.answer_address_filter = answer_filter;
522        self
523    }
524
525    /// Enables opportunistic encryption with default configuration
526    #[cfg(any(feature = "__tls", feature = "__quic"))]
527    pub fn with_opportunistic_encryption(mut self) -> Self {
528        self.opportunistic_encryption = OpportunisticEncryption::Enabled {
529            config: OpportunisticEncryptionConfig::default(),
530        };
531        self
532    }
533
534    /// Sets the transport state
535    pub fn with_transport_state(mut self, transport_state: NameServerTransportState) -> Self {
536        self.transport_state = AsyncMutex::new(transport_state);
537        self
538    }
539
540    pub(crate) async fn transport_state(&self) -> MutexGuard<'_, NameServerTransportState> {
541        self.transport_state.lock().await
542    }
543}
544
545/// A mapping from nameserver IP address and protocol to encrypted transport state.
546#[derive(Debug, Default, Clone)]
547#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
548#[repr(transparent)]
549pub struct NameServerTransportState(HashMap<IpAddr, ProtocolTransportState>);
550
551impl NameServerTransportState {
552    /// Return the count of nameservers with protocol transport state.
553    pub fn nameserver_count(&self) -> usize {
554        self.0.len()
555    }
556
557    /// Update the transport state for the given IP and protocol to record a connection initiation.
558    pub(crate) fn initiate_connection(&mut self, ip: IpAddr, protocol: Protocol) {
559        let protocol_state = self.0.entry(ip).or_default();
560        *protocol_state.get_mut(protocol) = TransportState::default();
561    }
562
563    /// Update the transport state for the given IP and protocol to record a connection completion.
564    pub(crate) fn complete_connection(&mut self, ip: IpAddr, protocol: Protocol) {
565        let protocol_state = self.0.entry(ip).or_default();
566        *protocol_state.get_mut(protocol) = TransportState::Success {
567            last_response: None,
568        };
569    }
570
571    /// Update the successful transport state for the given IP and protocol to record a response received.
572    pub(crate) fn response_received(&mut self, ip: IpAddr, protocol: Protocol) {
573        let Some(protocol_state) = self.0.get_mut(&ip) else {
574            return;
575        };
576        let TransportState::Success { last_response, .. } = protocol_state.get_mut(protocol) else {
577            return;
578        };
579        *last_response = Some(SystemTime::now());
580    }
581
582    /// Update the transport state for the given IP and protocol to record a received error.
583    pub(crate) fn error_received(&mut self, ip: IpAddr, protocol: Protocol, error: &NetError) {
584        let protocol_state = self.0.entry(ip).or_default();
585        *protocol_state.get_mut(protocol) = match &error {
586            NetError::Timeout => TransportState::TimedOut {
587                #[cfg(any(feature = "__tls", feature = "__quic"))]
588                completed_at: SystemTime::now(),
589            },
590            _ => TransportState::Failed {
591                #[cfg(any(feature = "__tls", feature = "__quic"))]
592                completed_at: SystemTime::now(),
593            },
594        };
595    }
596
597    /// Returns true if any supported encrypted protocol had a recent success for the given IP
598    /// within the damping period.
599    #[cfg(any(feature = "__tls", feature = "__quic"))]
600    pub(crate) fn any_recent_success(&self, ip: IpAddr, config: &OpportunisticEncryption) -> bool {
601        #[allow(unused_assignments, unused_mut)]
602        let mut tls_success = false;
603        #[allow(unused_assignments, unused_mut)]
604        let mut quic_success = false;
605
606        #[cfg(feature = "__tls")]
607        {
608            tls_success = self.recent_success(ip, Protocol::Tls, config);
609        }
610
611        #[cfg(feature = "__quic")]
612        {
613            quic_success = self.recent_success(ip, Protocol::Quic, config);
614        }
615
616        tls_success || quic_success
617    }
618
619    /// Returns true if any encrypted protocol had a recent success for the given IP within the damping period.
620    #[cfg(not(any(feature = "__tls", feature = "__quic")))]
621    pub(crate) fn any_recent_success(
622        &self,
623        _ip: IpAddr,
624        _config: &OpportunisticEncryption,
625    ) -> bool {
626        false
627    }
628
629    /// Returns true if there has been a successful response within the persistence period for the
630    /// IP/protocol.
631    ///
632    /// Returns false if opportunistic encryption is disabled, or if there has not been a successful
633    /// response read.
634    #[cfg(any(feature = "__tls", feature = "__quic"))]
635    pub(crate) fn recent_success(
636        &self,
637        ip: IpAddr,
638        protocol: Protocol,
639        config: &OpportunisticEncryption,
640    ) -> bool {
641        let OpportunisticEncryption::Enabled { config } = config else {
642            return false;
643        };
644
645        let Some(protocol_state) = self.0.get(&ip) else {
646            return false;
647        };
648
649        let TransportState::Success { last_response, .. } = protocol_state.get(protocol) else {
650            return false;
651        };
652
653        let Some(last_response) = last_response else {
654            return false;
655        };
656
657        last_response.elapsed().unwrap_or(Duration::MAX) <= config.persistence_period
658    }
659
660    /// Returns true if there has been a successful response within the persistence period.
661    ///
662    /// Returns false if opportunistic encryption is disabled, or if there has not been a successful
663    /// response read.
664    #[cfg(not(any(feature = "__tls", feature = "__quic")))]
665    pub(crate) fn recent_success(
666        &self,
667        _ip: IpAddr,
668        _protocol: Protocol,
669        _config: &OpportunisticEncryption,
670    ) -> bool {
671        false
672    }
673
674    /// Returns true if we should probe encrypted transport based on RFC 9539 damping logic.
675    #[cfg(any(feature = "__tls", feature = "__quic"))]
676    pub(crate) fn should_probe_encrypted(
677        &self,
678        ip: IpAddr,
679        protocol: Protocol,
680        config: &OpportunisticEncryption,
681    ) -> bool {
682        debug_assert!(protocol.is_encrypted());
683
684        let OpportunisticEncryption::Enabled { config, .. } = config else {
685            return false;
686        };
687
688        let Some(protocol_state) = self.0.get(&ip) else {
689            return true;
690        };
691
692        match protocol_state.get(protocol) {
693            TransportState::Initiated => false,
694            TransportState::Success { .. } => true,
695            TransportState::Failed { completed_at } | TransportState::TimedOut { completed_at } => {
696                completed_at.elapsed().unwrap_or(Duration::MAX) > config.damping_period
697            }
698        }
699    }
700
701    /// Returns true if we should probe encrypted transport based on RFC 9539 damping logic.
702    #[cfg(not(any(feature = "__tls", feature = "__quic")))]
703    pub(crate) fn should_probe_encrypted(
704        &self,
705        _ip: IpAddr,
706        _protocol: Protocol,
707        _config: &OpportunisticEncryption,
708    ) -> bool {
709        false
710    }
711
712    /// For testing, set the last response time for successful connections to the ip/protocol.
713    #[cfg(all(test, feature = "__tls"))]
714    pub(crate) fn set_last_response(&mut self, ip: IpAddr, protocol: Protocol, when: SystemTime) {
715        let Some(protocol_state) = self.0.get_mut(&ip) else {
716            return;
717        };
718
719        let TransportState::Success { last_response, .. } = protocol_state.get_mut(protocol) else {
720            return;
721        };
722
723        *last_response = Some(when);
724    }
725
726    /// For testing, set the completion time for failed connections to the ip/protocol.
727    #[cfg(all(test, feature = "__tls"))]
728    pub(crate) fn set_failure_time(&mut self, ip: IpAddr, protocol: Protocol, when: SystemTime) {
729        let protocol_state = self.0.entry(ip).or_default();
730        *protocol_state.get_mut(protocol) = TransportState::Failed { completed_at: when };
731    }
732}
733
734#[derive(Debug, Clone, Copy, Default)]
735#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
736struct ProtocolTransportState {
737    #[cfg(feature = "__tls")]
738    tls: TransportState,
739    #[cfg(feature = "__quic")]
740    quic: TransportState,
741}
742
743impl ProtocolTransportState {
744    #[cfg_attr(not(any(feature = "__tls", feature = "__quic")), allow(dead_code))]
745    fn get_mut(&mut self, protocol: Protocol) -> &mut TransportState {
746        match protocol {
747            #[cfg(feature = "__tls")]
748            Protocol::Tls => &mut self.tls,
749            #[cfg(feature = "__quic")]
750            Protocol::Quic => &mut self.quic,
751            _ => unreachable!("unsupported opportunistic encryption protocol: {protocol:?}"),
752        }
753    }
754
755    #[cfg_attr(not(any(feature = "__tls", feature = "__quic")), allow(dead_code))]
756    fn get(&self, protocol: Protocol) -> &TransportState {
757        match protocol {
758            #[cfg(feature = "__tls")]
759            Protocol::Tls => &self.tls,
760            #[cfg(feature = "__quic")]
761            Protocol::Quic => &self.quic,
762            _ => unreachable!("unsupported opportunistic encryption protocol: {protocol:?}"),
763        }
764    }
765}
766
767/// State tracked per nameserver IP/protocol to inform opportunistic encryption.
768#[derive(Debug, Clone, Copy, Default)]
769#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
770enum TransportState {
771    /// Connection attempt has been initiated.
772    #[default]
773    Initiated,
774    /// Connection completed successfully.
775    Success {
776        /// The last instant at which a response was read on the connection (if any).
777        last_response: Option<SystemTime>,
778    },
779    /// Connection failed with an error.
780    Failed {
781        /// The instant the connection attempt was completed at.
782        #[cfg(any(feature = "__tls", feature = "__quic"))]
783        completed_at: SystemTime,
784    },
785    /// Connection timed out.
786    TimedOut {
787        /// The instant the connection attempt was completed at.
788        #[cfg(any(feature = "__tls", feature = "__quic"))]
789        completed_at: SystemTime,
790    },
791}
792
793#[cfg(all(feature = "toml", any(feature = "__tls", feature = "__quic")))]
794pub use opportunistic_encryption_persistence::OpportunisticEncryptionStatePersistTask;
795
796#[cfg(all(feature = "toml", any(feature = "__tls", feature = "__quic")))]
797mod opportunistic_encryption_persistence {
798    #[cfg(unix)]
799    use std::fs::File;
800    use std::{
801        fs::{self, OpenOptions},
802        io::{self, Write},
803        marker::PhantomData,
804        path::{Path, PathBuf},
805    };
806
807    use tracing::trace;
808
809    use super::*;
810    use crate::config::OpportunisticEncryptionPersistence;
811    use crate::net::runtime::Spawn;
812
813    /// A background task for periodically saving opportunistic encryption state.
814    pub struct OpportunisticEncryptionStatePersistTask<T> {
815        cx: Arc<PoolContext>,
816        path: PathBuf,
817        save_interval: Duration,
818        _time: PhantomData<T>,
819    }
820
821    impl<T: Time> OpportunisticEncryptionStatePersistTask<T> {
822        /// Starts the persistence task based on the given configuration.
823        pub async fn start<P: RuntimeProvider>(
824            config: OpportunisticEncryptionPersistence,
825            pool_context: &Arc<PoolContext>,
826            conn_provider: P,
827        ) -> Result<Option<P::Handle>, String> {
828            info!(
829                path = %config.path.display(),
830                save_interval = ?config.save_interval,
831                "spawning encrypted transport state persistence task"
832            );
833
834            let new =
835                OpportunisticEncryptionStatePersistTask::<P::Timer>::new(config, pool_context);
836
837            // Try to save the state back immediately so we can surface write errors early
838            // instead of when the background task runs later on.
839            new.save(&*new.cx.transport_state.lock().await)
840                .map_err(|err| {
841                    format!(
842                        "failed to save opportunistic encryption state: {path}: {err}",
843                        path = new.path.display()
844                    )
845                })?;
846
847            let mut handle = conn_provider.create_handle();
848            handle.spawn_bg(new.run());
849            Ok(Some(handle))
850        }
851
852        fn new(config: OpportunisticEncryptionPersistence, cx: &Arc<PoolContext>) -> Self {
853            Self {
854                cx: cx.clone(),
855                path: config.path,
856                save_interval: config.save_interval,
857                _time: PhantomData,
858            }
859        }
860
861        async fn run(self) {
862            let Self {
863                save_interval,
864                path,
865                cx,
866                ..
867            } = &self;
868
869            loop {
870                T::delay_for(*save_interval).await;
871                trace!(path = %path.display(), ?save_interval, "persisting opportunistic encryption state");
872                if let Err(e) = self.save(&*cx.transport_state.lock().await) {
873                    error!("failed to save opportunistic encryption state: {e}");
874                }
875            }
876        }
877
878        fn save(&self, state: &NameServerTransportState) -> Result<(), io::Error> {
879            let toml_content = toml::to_string_pretty(state).map_err(|e| {
880                io::Error::new(
881                    io::ErrorKind::InvalidData,
882                    format!("failed to serialize state to TOML: {e}"),
883                )
884            })?;
885
886            if let Some(parent) = parent_directory(&self.path) {
887                fs::create_dir_all(parent)?;
888            }
889
890            let temp_path = {
891                let mut temp = self.path.as_os_str().to_os_string();
892                temp.push(".tmp");
893                PathBuf::from(temp)
894            };
895
896            {
897                let mut temp_file = OpenOptions::new()
898                    .write(true)
899                    .create(true)
900                    .truncate(true)
901                    .open(&temp_path)?;
902
903                temp_file.write_all(toml_content.as_bytes())?;
904                temp_file.sync_all()?;
905            }
906
907            #[cfg(unix)]
908            if let Some(parent) = parent_directory(&self.path) {
909                File::open(parent)?.sync_all()?;
910            }
911
912            fs::rename(&temp_path, &self.path)?;
913            debug!(state_file = %self.path.display(), "saved opportunistic encryption state");
914            Ok(())
915        }
916    }
917
918    /// Gets the parent directory of an absolute or relative path.
919    fn parent_directory(path: &Path) -> Option<&Path> {
920        let parent = path.parent()?;
921        // Special case: if the path has only one component, `parent()` will return an empty string. We
922        // should return "." instead, a relative path pointing at the current directory.
923        Some(match parent == Path::new("") {
924            true => Path::new("."),
925            false => parent,
926        })
927    }
928}
929
930/// RAII guard that removes a deduplication key from `active_requests` when dropped.
931///
932/// This is created only by the "creator" task (the one that inserted the key).
933/// Using `Drop` guarantees the entry is removed even if the inner future panics,
934/// preventing a poisoned [`SharedLookup`] from remaining in the map and causing
935/// every subsequent request for the same key to also panic.
936struct ActiveRequestCleanup {
937    active_requests: Arc<Mutex<HashMap<Arc<CacheKey>, SharedLookup>>>,
938    key: Arc<CacheKey>,
939}
940
941impl Drop for ActiveRequestCleanup {
942    fn drop(&mut self) {
943        self.active_requests.lock().remove(&self.key);
944    }
945}
946
947/// Fields of a [`DnsRequest`] that are used as a key when memoizing queries.
948#[derive(PartialEq, Eq, Hash)]
949struct CacheKey {
950    op_code: OpCode,
951    recursion_desired: bool,
952    checking_disabled: bool,
953    queries: Vec<Query>,
954    dnssec_ok: bool,
955    client_subnet: Option<ClientSubnet>,
956}
957
958impl CacheKey {
959    fn from_request(request: &DnsRequest) -> Self {
960        let dnssec_ok;
961        let client_subnet;
962        if let Some(edns) = &request.edns {
963            dnssec_ok = edns.flags().dnssec_ok;
964            if let Some(EdnsOption::Subnet(subnet)) = edns.option(EdnsCode::Subnet) {
965                client_subnet = Some(*subnet);
966            } else {
967                client_subnet = None;
968            }
969        } else {
970            dnssec_ok = false;
971            client_subnet = None;
972        }
973        Self {
974            op_code: request.op_code,
975            recursion_desired: request.recursion_desired,
976            checking_disabled: request.checking_disabled,
977            queries: request.queries.clone(),
978            dnssec_ok,
979            client_subnet,
980        }
981    }
982}
983
984#[derive(Clone)]
985pub(crate) struct SharedLookup(Shared<BoxFuture<'static, Option<Result<DnsResponse, NetError>>>>);
986
987impl Future for SharedLookup {
988    type Output = Result<DnsResponse, NetError>;
989
990    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
991        self.0.poll_unpin(cx).map(|o| match o {
992            Some(r) => r,
993            None => Err("no response from nameserver".into()),
994        })
995    }
996}
997
998#[cfg(test)]
999#[cfg(feature = "tokio")]
1000mod tests {
1001    use std::collections::HashSet;
1002    use std::future::Future;
1003    use std::io;
1004    use std::net::{IpAddr, SocketAddr};
1005    use std::pin::Pin;
1006    use std::str::FromStr;
1007    use std::sync::atomic::{AtomicBool, Ordering};
1008    use std::thread;
1009    use std::time::Duration;
1010
1011    use futures_util::future;
1012    use test_support::{
1013        MockNetworkHandler, MockProvider, MockRecord, MockTcpStream, MockUdpSocket, subscribe,
1014    };
1015    use tokio::runtime::Runtime;
1016
1017    use super::*;
1018    use crate::config::{NameServerConfig, ResolverConfig, ServerOrderingStrategy};
1019    use crate::net::runtime::{RuntimeProvider, TokioHandle, TokioRuntimeProvider, TokioTime};
1020    use crate::net::xfer::{DnsHandle, FirstAnswer, Protocol};
1021    use crate::proto::op::{DnsRequestOptions, Message, Query};
1022    use crate::proto::rr::{DNSClass, Name, RecordType};
1023
1024    #[ignore]
1025    // because of there is a real connection that needs a reasonable timeout
1026    #[test]
1027    #[allow(clippy::uninlined_format_args)]
1028    fn test_failed_then_success_pool() {
1029        subscribe();
1030
1031        let mut config1 = NameServerConfig::udp(IpAddr::from([127, 0, 0, 252]));
1032        config1.trust_negative_responses = false;
1033        let config2 = NameServerConfig::udp(IpAddr::from([8, 8, 8, 8]));
1034
1035        let resolver_config = ResolverConfig::from_name_servers(vec![config1, config2]);
1036
1037        let io_loop = Runtime::new().unwrap();
1038        let pool = NameServerPool::from_config(
1039            resolver_config.name_servers,
1040            Arc::new(PoolContext::new(
1041                ResolverOpts::default(),
1042                TlsConfig::new().unwrap(),
1043            )),
1044            TokioRuntimeProvider::new(),
1045        );
1046
1047        let name = Name::parse("www.example.com.", None).unwrap();
1048
1049        // TODO: it's not clear why there are two failures before the success
1050        for i in 0..2 {
1051            assert!(
1052                io_loop
1053                    .block_on(
1054                        pool.lookup(
1055                            Query::query(name.clone(), RecordType::A),
1056                            DnsRequestOptions::default()
1057                        )
1058                        .first_answer()
1059                    )
1060                    .is_err(),
1061                "iter: {}",
1062                i
1063            );
1064        }
1065
1066        for i in 0..10 {
1067            assert!(
1068                io_loop
1069                    .block_on(
1070                        pool.lookup(
1071                            Query::query(name.clone(), RecordType::A),
1072                            DnsRequestOptions::default()
1073                        )
1074                        .first_answer()
1075                    )
1076                    .is_ok(),
1077                "iter: {}",
1078                i
1079            );
1080        }
1081    }
1082
1083    #[tokio::test]
1084    async fn test_multi_use_conns() {
1085        subscribe();
1086
1087        let conn_provider = TokioRuntimeProvider::default();
1088        let opts = ResolverOpts {
1089            try_tcp_on_error: true,
1090            ..ResolverOpts::default()
1091        };
1092
1093        let tcp = NameServerConfig::tcp(IpAddr::from([8, 8, 8, 8]));
1094        let name_server = Arc::new(NameServer::new([], tcp, &opts, conn_provider));
1095        let name_servers = vec![name_server];
1096        let pool = NameServerPool::from_nameservers(
1097            name_servers.clone(),
1098            Arc::new(PoolContext::new(opts, TlsConfig::new().unwrap())),
1099        );
1100
1101        let name = Name::from_str("www.example.com.").unwrap();
1102
1103        // first lookup
1104        let response = pool
1105            .lookup(
1106                Query::query(name.clone(), RecordType::A),
1107                DnsRequestOptions::default(),
1108            )
1109            .first_answer()
1110            .await
1111            .expect("lookup failed");
1112
1113        assert!(!response.answers.is_empty());
1114
1115        assert!(
1116            name_servers[0].is_connected(),
1117            "if this is failing then the NameServers aren't being properly shared."
1118        );
1119
1120        // first lookup
1121        let response = pool
1122            .lookup(
1123                Query::query(name, RecordType::AAAA),
1124                DnsRequestOptions::default(),
1125            )
1126            .first_answer()
1127            .await
1128            .expect("lookup failed");
1129
1130        assert!(!response.answers.is_empty());
1131
1132        assert!(
1133            name_servers[0].is_connected(),
1134            "if this is failing then the NameServers aren't being properly shared."
1135        );
1136    }
1137
1138    /// Regression test: when the first name server in the pool times out, the pool should
1139    /// try the remaining servers rather than returning the timeout error immediately.
1140    ///
1141    /// Before the fix (adding `NetError::Timeout` to the retry match arm in `try_send`),
1142    /// a timeout from one server would cause the entire lookup to fail even when other
1143    /// servers in the pool could have answered successfully.
1144    #[tokio::test]
1145    async fn test_pool_retries_on_timeout() {
1146        subscribe();
1147
1148        let timeout_ip = IpAddr::from([10, 0, 0, 1]);
1149        let good_ip = IpAddr::from([10, 0, 0, 2]);
1150        let query_name = Name::from_str("example.com.").unwrap();
1151
1152        // Set up a mock handler where the good server returns a valid A record.
1153        let responses = vec![MockRecord::a(good_ip, &query_name, good_ip)];
1154        let handler = MockNetworkHandler::new(responses);
1155        let mock_provider = MockProvider::new(handler);
1156
1157        // Wrap in TimeoutProvider so that the timeout_ip always fails with TimedOut.
1158        let provider = TimeoutProvider::new(mock_provider, vec![timeout_ip]);
1159
1160        let opts = ResolverOpts {
1161            num_concurrent_reqs: 1,
1162            server_ordering_strategy: ServerOrderingStrategy::UserProvidedOrder,
1163            ..ResolverOpts::default()
1164        };
1165
1166        let pool = NameServerPool::from_nameservers(
1167            vec![
1168                Arc::new(NameServer::new(
1169                    [].into_iter(),
1170                    NameServerConfig::udp(timeout_ip),
1171                    &opts,
1172                    provider.clone(),
1173                )),
1174                Arc::new(NameServer::new(
1175                    [].into_iter(),
1176                    NameServerConfig::udp(good_ip),
1177                    &opts,
1178                    provider.clone(),
1179                )),
1180            ],
1181            Arc::new(PoolContext::new(opts, TlsConfig::new().unwrap())),
1182        );
1183
1184        // This should succeed: the pool should fall through the timeout from the first
1185        // server and get the answer from the second server.
1186        let response = pool
1187            .lookup(
1188                Query::query(query_name.clone(), RecordType::A),
1189                DnsRequestOptions::default(),
1190            )
1191            .first_answer()
1192            .await
1193            .expect("pool should retry on timeout and succeed with the second server");
1194
1195        assert!(
1196            !response.answers.is_empty(),
1197            "expected A record in response"
1198        );
1199    }
1200
1201    /// Regression test: when a server times out, its server-level SRTT should be penalized
1202    /// so that it gets deprioritized in future pool ordering.
1203    #[tokio::test]
1204    async fn test_timeout_penalizes_server_srtt() {
1205        subscribe();
1206
1207        let timeout_ip = IpAddr::from([10, 0, 0, 1]);
1208        let good_ip = IpAddr::from([10, 0, 0, 2]);
1209        let query_name = Name::from_str("example.com.").unwrap();
1210
1211        let responses = vec![MockRecord::a(good_ip, &query_name, good_ip)];
1212        let handler = MockNetworkHandler::new(responses);
1213        let mock_provider = MockProvider::new(handler);
1214        let provider = TimeoutProvider::new(mock_provider, vec![timeout_ip]);
1215
1216        let opts = ResolverOpts {
1217            num_concurrent_reqs: 1,
1218            server_ordering_strategy: ServerOrderingStrategy::UserProvidedOrder,
1219            ..ResolverOpts::default()
1220        };
1221
1222        let ns_timeout = Arc::new(NameServer::new(
1223            [].into_iter(),
1224            NameServerConfig::udp(timeout_ip),
1225            &opts,
1226            provider.clone(),
1227        ));
1228        let ns_good = Arc::new(NameServer::new(
1229            [].into_iter(),
1230            NameServerConfig::udp(good_ip),
1231            &opts,
1232            provider.clone(),
1233        ));
1234
1235        let initial_srtt_timeout = ns_timeout.decayed_srtt();
1236
1237        let pool = NameServerPool::from_nameservers(
1238            vec![ns_timeout.clone(), ns_good.clone()],
1239            Arc::new(PoolContext::new(opts, TlsConfig::new().unwrap())),
1240        );
1241
1242        // Perform a lookup - the first server will timeout, second will succeed.
1243        let _response = pool
1244            .lookup(
1245                Query::query(query_name.clone(), RecordType::A),
1246                DnsRequestOptions::default(),
1247            )
1248            .first_answer()
1249            .await
1250            .expect("lookup should succeed via second server");
1251
1252        // The timeout server's SRTT should have been penalized (increased).
1253        assert!(
1254            ns_timeout.decayed_srtt() > initial_srtt_timeout,
1255            "timeout server SRTT should increase after failure: {} should be > {}",
1256            ns_timeout.decayed_srtt(),
1257            initial_srtt_timeout,
1258        );
1259
1260        // The good server's SRTT should not have been penalized.
1261        // It may have changed slightly due to recording a successful RTT, but should
1262        // not have jumped to the failure penalty value.
1263        let failure_penalty = 5_000_000.0_f64; // SRTT failure penalty
1264        assert!(
1265            ns_good.decayed_srtt() < failure_penalty,
1266            "good server SRTT should not be penalized: {}",
1267            ns_good.decayed_srtt(),
1268        );
1269    }
1270
1271    /// A [`RuntimeProvider`] wrapper that returns `io::ErrorKind::TimedOut` from `bind_udp`
1272    /// for a specified set of server IPs, simulating a connection-level timeout. All other
1273    /// IPs are delegated to the inner provider.
1274    #[derive(Clone)]
1275    struct TimeoutProvider {
1276        inner: MockProvider,
1277        timeout_ips: Arc<HashSet<IpAddr>>,
1278    }
1279
1280    impl TimeoutProvider {
1281        fn new(inner: MockProvider, timeout_ips: Vec<IpAddr>) -> Self {
1282            Self {
1283                inner,
1284                timeout_ips: Arc::new(timeout_ips.into_iter().collect()),
1285            }
1286        }
1287    }
1288
1289    impl RuntimeProvider for TimeoutProvider {
1290        type Handle = TokioHandle;
1291        type Timer = TokioTime;
1292        type Udp = MockUdpSocket;
1293        type Tcp = MockTcpStream;
1294
1295        fn create_handle(&self) -> Self::Handle {
1296            self.inner.create_handle()
1297        }
1298
1299        fn connect_tcp(
1300            &self,
1301            server_addr: SocketAddr,
1302            bind_addr: Option<SocketAddr>,
1303            timeout: Option<Duration>,
1304        ) -> Pin<Box<dyn Future<Output = Result<Self::Tcp, io::Error>> + Send>> {
1305            if self.timeout_ips.contains(&server_addr.ip()) {
1306                Box::pin(future::ready(Err(io::Error::from(io::ErrorKind::TimedOut))))
1307            } else {
1308                self.inner.connect_tcp(server_addr, bind_addr, timeout)
1309            }
1310        }
1311
1312        fn bind_udp(
1313            &self,
1314            local_addr: SocketAddr,
1315            server_addr: SocketAddr,
1316        ) -> Pin<Box<dyn Future<Output = Result<Self::Udp, io::Error>> + Send>> {
1317            if self.timeout_ips.contains(&server_addr.ip()) {
1318                Box::pin(future::ready(Err(io::Error::from(io::ErrorKind::TimedOut))))
1319            } else {
1320                self.inner.bind_udp(local_addr, server_addr)
1321            }
1322        }
1323    }
1324
1325    /// Regression test: an unreachable server racing in parallel must be penalized.
1326    ///
1327    /// When `num_concurrent_reqs >= 2`, multiple servers are queried in parallel
1328    /// via `FuturesUnordered`. If a reachable server responds first, the
1329    /// unreachable server's future is dropped (cancelled). Before the fix, this
1330    /// meant `record_failure()` was never called for the unreachable server,
1331    /// leaving its SRTT unchanged so it would be retried on every subsequent
1332    /// query.
1333    #[tokio::test]
1334    async fn test_cancelled_parallel_server_is_penalized() {
1335        subscribe();
1336
1337        let unreachable_ip = IpAddr::from([10, 0, 0, 1]);
1338        let good_ip = IpAddr::from([10, 0, 0, 2]);
1339        let query_name = Name::from_str("example.com.").unwrap();
1340
1341        let responses = vec![MockRecord::a(good_ip, &query_name, good_ip)];
1342        let handler = MockNetworkHandler::new(responses);
1343        let mock_provider = MockProvider::new(handler);
1344        let provider = PendingProvider::new(mock_provider, vec![unreachable_ip]);
1345
1346        let opts = ResolverOpts {
1347            // Both servers are queried in parallel — the key condition for this bug.
1348            num_concurrent_reqs: 2,
1349            server_ordering_strategy: ServerOrderingStrategy::UserProvidedOrder,
1350            ..ResolverOpts::default()
1351        };
1352
1353        let ns_unreachable = Arc::new(NameServer::new(
1354            [].into_iter(),
1355            NameServerConfig::udp(unreachable_ip),
1356            &opts,
1357            provider.clone(),
1358        ));
1359        let ns_good = Arc::new(NameServer::new(
1360            [].into_iter(),
1361            NameServerConfig::udp(good_ip),
1362            &opts,
1363            provider.clone(),
1364        ));
1365
1366        let initial_srtt = ns_unreachable.decayed_srtt();
1367
1368        let pool = NameServerPool::from_nameservers(
1369            vec![ns_unreachable.clone(), ns_good.clone()],
1370            Arc::new(PoolContext::new(opts, TlsConfig::new().unwrap())),
1371        );
1372
1373        // The good server wins the race; the unreachable server's future is cancelled.
1374        let _response = pool
1375            .lookup(
1376                Query::query(query_name.clone(), RecordType::A),
1377                DnsRequestOptions::default(),
1378            )
1379            .first_answer()
1380            .await
1381            .expect("lookup should succeed via good server");
1382
1383        // The unreachable server's SRTT must have increased despite its future
1384        // being cancelled (not completing with an error).
1385        assert!(
1386            ns_unreachable.decayed_srtt() > initial_srtt,
1387            "unreachable server SRTT should increase after being cancelled: {} should be > {}",
1388            ns_unreachable.decayed_srtt(),
1389            initial_srtt,
1390        );
1391
1392        // The good server should not have been penalized.
1393        let failure_penalty = 5_000_000.0_f64;
1394        assert!(
1395            ns_good.decayed_srtt() < failure_penalty,
1396            "good server SRTT should not be penalized: {}",
1397            ns_good.decayed_srtt(),
1398        );
1399    }
1400
1401    /// A [`RuntimeProvider`] wrapper where specified IPs never complete their
1402    /// connection — the future stays pending forever. This simulates an
1403    /// unreachable server (SYN sent, no SYN-ACK) where the OS TCP handshake
1404    /// hasn't timed out yet.
1405    #[derive(Clone)]
1406    struct PendingProvider {
1407        inner: MockProvider,
1408        pending_ips: Arc<HashSet<IpAddr>>,
1409    }
1410
1411    impl PendingProvider {
1412        fn new(inner: MockProvider, pending_ips: Vec<IpAddr>) -> Self {
1413            Self {
1414                inner,
1415                pending_ips: Arc::new(pending_ips.into_iter().collect()),
1416            }
1417        }
1418    }
1419
1420    impl RuntimeProvider for PendingProvider {
1421        type Handle = TokioHandle;
1422        type Timer = TokioTime;
1423        type Udp = MockUdpSocket;
1424        type Tcp = MockTcpStream;
1425
1426        fn create_handle(&self) -> Self::Handle {
1427            self.inner.create_handle()
1428        }
1429
1430        fn connect_tcp(
1431            &self,
1432            server_addr: SocketAddr,
1433            bind_addr: Option<SocketAddr>,
1434            timeout: Option<Duration>,
1435        ) -> Pin<Box<dyn Future<Output = Result<Self::Tcp, io::Error>> + Send>> {
1436            if self.pending_ips.contains(&server_addr.ip()) {
1437                Box::pin(future::pending())
1438            } else {
1439                self.inner.connect_tcp(server_addr, bind_addr, timeout)
1440            }
1441        }
1442
1443        fn bind_udp(
1444            &self,
1445            local_addr: SocketAddr,
1446            server_addr: SocketAddr,
1447        ) -> Pin<Box<dyn Future<Output = Result<Self::Udp, io::Error>> + Send>> {
1448            if self.pending_ips.contains(&server_addr.ip()) {
1449                Box::pin(future::pending())
1450            } else {
1451                self.inner.bind_udp(local_addr, server_addr)
1452            }
1453        }
1454    }
1455
1456    /// Regression test: `sort_servers_by_query_statistics` must not panic when
1457    /// SRTT values are concurrently modified.
1458    ///
1459    /// `record()` and `record_failure()` can modify a server's SRTT while
1460    /// another thread sorts the server list. With `sort_by`, the comparator
1461    /// re-evaluates `decayed_srtt()` on every comparison, observing values
1462    /// that change between calls and violating the total-order invariant.
1463    /// The fix uses `sort_by_cached_key`, which evaluates each key exactly
1464    /// once before sorting.
1465    #[test]
1466    fn test_sort_by_decayed_srtt_does_not_panic() {
1467        let opts = ResolverOpts::default();
1468        let mock_provider = MockProvider::new(MockNetworkHandler::new(vec![]));
1469
1470        let mut servers = (1..=50)
1471            .map(|i| {
1472                let ns = Arc::new(NameServer::new(
1473                    [],
1474                    NameServerConfig::udp(IpAddr::from([10, 0, 0, i])),
1475                    &opts,
1476                    mock_provider.clone(),
1477                ));
1478                // Activate the time-based decay path by recording a failure,
1479                // which sets `last_update` to `Some(now)`.
1480                ns.test_record_failure();
1481                ns
1482            })
1483            .collect::<Vec<_>>();
1484
1485        // Spawn a thread that continuously modifies SRTT values, simulating
1486        // concurrent queries completing on other threads.
1487        let servers_writer = servers.clone();
1488        let stop = Arc::new(AtomicBool::new(false));
1489        let stop_writer = stop.clone();
1490        let writer = thread::spawn(move || {
1491            while !stop_writer.load(Ordering::Relaxed) {
1492                for s in &servers_writer {
1493                    s.test_record_failure();
1494                }
1495            }
1496        });
1497
1498        // Ensure the writer thread stops even if the test panics.
1499        struct StopGuard(Arc<AtomicBool>);
1500        impl Drop for StopGuard {
1501            fn drop(&mut self) {
1502                self.0.store(true, Ordering::Relaxed);
1503            }
1504        }
1505        let _guard = StopGuard(stop.clone());
1506
1507        // Call the production sort function many times while the writer
1508        // thread concurrently modifies SRTT values. With sort_by_cached_key
1509        // this is safe. With sort_by, the concurrent modifications cause
1510        // inconsistent comparisons that panic the sort.
1511        for _ in 0..100_000 {
1512            sort_servers_by_query_statistics(&mut servers);
1513        }
1514
1515        stop.store(true, Ordering::Relaxed);
1516        writer.join().unwrap();
1517    }
1518
1519    #[tokio::test]
1520    async fn test_pool_foreign_class_records() {
1521        subscribe();
1522
1523        struct TestCase {
1524            name: &'static str,
1525            section: ForeignSection,
1526            class: Option<DNSClass>,
1527            expect_err: bool,
1528        }
1529
1530        let cases = [
1531            TestCase {
1532                name: "foreign class CH in answers is rejected",
1533                section: ForeignSection::Answer,
1534                class: Some(DNSClass::CH),
1535                expect_err: true,
1536            },
1537            TestCase {
1538                name: "foreign class CH in authorities is rejected",
1539                section: ForeignSection::Authority,
1540                class: Some(DNSClass::CH),
1541                expect_err: true,
1542            },
1543            TestCase {
1544                name: "foreign class HS in additionals is rejected",
1545                section: ForeignSection::Additional,
1546                class: Some(DNSClass::HS),
1547                expect_err: true,
1548            },
1549            TestCase {
1550                name: "clean IN-only response passes through",
1551                section: ForeignSection::None,
1552                class: None,
1553                expect_err: false,
1554            },
1555        ];
1556
1557        for case in cases {
1558            let result = run_foreign_class_lookup(case.section, case.class).await;
1559            match (case.expect_err, result) {
1560                (true, Err(NetError::ForeignClassRecord { record_class, .. })) => {
1561                    assert_eq!(
1562                        Some(record_class),
1563                        case.class,
1564                        "{}: unexpected record_class",
1565                        case.name
1566                    );
1567                }
1568                (true, other) => {
1569                    panic!(
1570                        "{}: expected ForeignClassRecord error, got {other:?}",
1571                        case.name
1572                    )
1573                }
1574                (false, Ok(response)) => assert!(
1575                    !response.answers.is_empty(),
1576                    "{}: expected non-empty answer",
1577                    case.name
1578                ),
1579                (false, Err(e)) => panic!("{}: expected success, got error {e:?}", case.name),
1580            }
1581        }
1582    }
1583
1584    async fn run_foreign_class_lookup(
1585        section: ForeignSection,
1586        class: Option<DNSClass>,
1587    ) -> Result<DnsResponse, NetError> {
1588        let server_ip = IpAddr::from([10, 0, 0, 1]);
1589        let query_name = Name::from_str("example.com.")?;
1590        let target_name = query_name.clone();
1591
1592        let handler =
1593            MockNetworkHandler::new(vec![MockRecord::a(server_ip, &query_name, server_ip)])
1594                .with_mutation(Box::new(
1595                    move |_destination: IpAddr, _protocol: Protocol, msg: &mut Message| {
1596                        let Some(class) = class else { return };
1597                        if msg.queries.first().map(|q| &q.name) != Some(&target_name) {
1598                            return;
1599                        }
1600                        let mut record = Record::from_rdata(
1601                            target_name.clone(),
1602                            300,
1603                            RData::A(A([6, 6, 6, 6].into())),
1604                        );
1605                        record.dns_class = class;
1606                        match section {
1607                            ForeignSection::Answer => {
1608                                msg.add_answer(record);
1609                            }
1610                            ForeignSection::Authority => {
1611                                msg.add_authority(record);
1612                            }
1613                            ForeignSection::Additional => {
1614                                msg.add_additional(record);
1615                            }
1616                            ForeignSection::None => {}
1617                        }
1618                    },
1619                ));
1620
1621        let pool = NameServerPool::from_nameservers(
1622            vec![Arc::new(NameServer::new(
1623                [].into_iter(),
1624                NameServerConfig::udp(server_ip),
1625                &ResolverOpts::default(),
1626                MockProvider::new(handler),
1627            ))],
1628            Arc::new(PoolContext::new(ResolverOpts::default(), TlsConfig::new()?)),
1629        );
1630
1631        pool.lookup(
1632            Query::query(query_name, RecordType::A),
1633            DnsRequestOptions::default(),
1634        )
1635        .first_answer()
1636        .await
1637    }
1638
1639    #[derive(Clone, Copy)]
1640    enum ForeignSection {
1641        Answer,
1642        Authority,
1643        Additional,
1644        None,
1645    }
1646
1647    /// When a TCP-only server returns a truncated response, the pool should
1648    /// return `NetError::Truncated` rather than retrying indefinitely.
1649    ///
1650    /// The first attempt sees truncation with `disable_udp = false` and sets
1651    /// `disable_udp = true` for a retry. The second attempt sees truncation
1652    /// with `disable_udp = true` and returns the error immediately.
1653    #[tokio::test]
1654    async fn test_truncated_tcp_only_no_infinite_retry() {
1655        subscribe();
1656
1657        let server_ip = IpAddr::from([10, 0, 0, 1]);
1658        let query_name = Name::from_str("example.com.").unwrap();
1659
1660        let responses = vec![MockRecord::a(server_ip, &query_name, server_ip)];
1661        let handler = MockNetworkHandler::new(responses).with_mutation(Box::new(
1662            |_destination: IpAddr, _protocol: Protocol, msg: &mut Message| {
1663                msg.metadata.truncation = true;
1664            },
1665        ));
1666        let provider = MockProvider::new(handler);
1667
1668        let opts = ResolverOpts {
1669            num_concurrent_reqs: 1,
1670            server_ordering_strategy: ServerOrderingStrategy::UserProvidedOrder,
1671            ..ResolverOpts::default()
1672        };
1673
1674        let pool = NameServerPool::from_nameservers(
1675            vec![Arc::new(NameServer::new(
1676                [].into_iter(),
1677                NameServerConfig::tcp(server_ip),
1678                &opts,
1679                provider.clone(),
1680            ))],
1681            Arc::new(PoolContext::new(opts, TlsConfig::new().unwrap())),
1682        );
1683
1684        let result = pool
1685            .lookup(
1686                Query::query(query_name.clone(), RecordType::A),
1687                DnsRequestOptions::default(),
1688            )
1689            .first_answer()
1690            .await;
1691
1692        assert!(
1693            matches!(result, Err(NetError::Truncated)),
1694            "expected Truncated error, got: {result:?}"
1695        );
1696
1697        // The server should have been queried at most twice: once to discover
1698        // truncation and once more after disabling UDP (which is a no-op for
1699        // a TCP-only server, but triggers the truncation-after-retry path).
1700        let queries = provider.queries(&server_ip);
1701        assert!(
1702            queries.len() <= 2,
1703            "TCP-only server should not be retried more than once, got {} queries",
1704            queries.len()
1705        );
1706    }
1707
1708    /// When a UDP+TCP server returns truncated on UDP, the pool retries over
1709    /// TCP. If TCP also returns truncated, the pool should return
1710    /// `NetError::Truncated` instead of retrying again.
1711    #[tokio::test]
1712    async fn test_truncated_udp_then_tcp_no_infinite_retry() {
1713        subscribe();
1714
1715        let server_ip = IpAddr::from([10, 0, 0, 1]);
1716        let query_name = Name::from_str("example.com.").unwrap();
1717
1718        let responses = vec![MockRecord::a(server_ip, &query_name, server_ip)];
1719        // Always set truncation regardless of protocol.
1720        let handler = MockNetworkHandler::new(responses).with_mutation(Box::new(
1721            |_destination: IpAddr, _protocol: Protocol, msg: &mut Message| {
1722                msg.metadata.truncation = true;
1723            },
1724        ));
1725        let provider = MockProvider::new(handler);
1726
1727        let opts = ResolverOpts {
1728            num_concurrent_reqs: 1,
1729            server_ordering_strategy: ServerOrderingStrategy::UserProvidedOrder,
1730            ..ResolverOpts::default()
1731        };
1732
1733        let pool = NameServerPool::from_nameservers(
1734            vec![Arc::new(NameServer::new(
1735                [].into_iter(),
1736                NameServerConfig::udp_and_tcp(server_ip),
1737                &opts,
1738                provider.clone(),
1739            ))],
1740            Arc::new(PoolContext::new(opts, TlsConfig::new().unwrap())),
1741        );
1742
1743        let result = pool
1744            .lookup(
1745                Query::query(query_name.clone(), RecordType::A),
1746                DnsRequestOptions::default(),
1747            )
1748            .first_answer()
1749            .await;
1750
1751        assert!(
1752            matches!(result, Err(NetError::Truncated)),
1753            "expected Truncated error, got: {result:?}"
1754        );
1755
1756        // Exactly 2 queries: one over UDP (truncated), one over TCP (truncated).
1757        let queries = provider.queries(&server_ip);
1758        assert!(
1759            queries.len() <= 2,
1760            "server should not be retried more than once after TCP truncation, got {} queries",
1761            queries.len()
1762        );
1763    }
1764
1765    /// With multiple servers all returning truncated, the pool should give up
1766    /// after the first server's TCP retry returns truncated, not cycle through
1767    /// all servers repeatedly.
1768    #[tokio::test]
1769    async fn test_truncated_multiple_servers_no_retry_storm() {
1770        subscribe();
1771
1772        let server1_ip = IpAddr::from([10, 0, 0, 1]);
1773        let server2_ip = IpAddr::from([10, 0, 0, 2]);
1774        let query_name = Name::from_str("example.com.").unwrap();
1775
1776        let responses = vec![
1777            MockRecord::a(server1_ip, &query_name, server1_ip),
1778            MockRecord::a(server2_ip, &query_name, server2_ip),
1779        ];
1780        let handler = MockNetworkHandler::new(responses).with_mutation(Box::new(
1781            |_destination: IpAddr, _protocol: Protocol, msg: &mut Message| {
1782                msg.metadata.truncation = true;
1783            },
1784        ));
1785        let provider = MockProvider::new(handler);
1786
1787        let opts = ResolverOpts {
1788            num_concurrent_reqs: 1,
1789            server_ordering_strategy: ServerOrderingStrategy::UserProvidedOrder,
1790            ..ResolverOpts::default()
1791        };
1792
1793        let pool = NameServerPool::from_nameservers(
1794            vec![
1795                Arc::new(NameServer::new(
1796                    [].into_iter(),
1797                    NameServerConfig::udp_and_tcp(server1_ip),
1798                    &opts,
1799                    provider.clone(),
1800                )),
1801                Arc::new(NameServer::new(
1802                    [].into_iter(),
1803                    NameServerConfig::udp_and_tcp(server2_ip),
1804                    &opts,
1805                    provider.clone(),
1806                )),
1807            ],
1808            Arc::new(PoolContext::new(opts, TlsConfig::new().unwrap())),
1809        );
1810
1811        let result = pool
1812            .lookup(
1813                Query::query(query_name.clone(), RecordType::A),
1814                DnsRequestOptions::default(),
1815            )
1816            .first_answer()
1817            .await;
1818
1819        assert!(
1820            matches!(result, Err(NetError::Truncated)),
1821            "expected Truncated error, got: {result:?}"
1822        );
1823
1824        let total_queries =
1825            provider.queries(&server1_ip).len() + provider.queries(&server2_ip).len();
1826        assert!(
1827            total_queries <= 3,
1828            "total queries across all servers should be bounded, got {total_queries}"
1829        );
1830    }
1831}