trippy_dns/
lazy_resolver.rs

1use crate::config::Config;
2use crate::resolver::{DnsEntry, ResolvedIpAddrs, Resolver, Result};
3use std::fmt::{Display, Formatter};
4use std::net::IpAddr;
5use std::rc::Rc;
6
7/// How DNS queries will be resolved.
8#[derive(Debug, Copy, Clone, Eq, PartialEq)]
9pub enum ResolveMethod {
10    /// Resolve using the OS resolver.
11    System,
12    /// Resolve using the `/etc/resolv.conf` DNS configuration.
13    Resolv,
14    /// Resolve using the Google `8.8.8.8` DNS service.
15    Google,
16    /// Resolve using the Cloudflare `1.1.1.1` DNS service.
17    Cloudflare,
18}
19
20/// How to resolve IP addresses.
21#[derive(Debug, Copy, Clone, Eq, PartialEq)]
22pub enum IpAddrFamily {
23    /// Lookup IPv4 only.
24    Ipv4Only,
25    /// Lookup IPv6 only.
26    Ipv6Only,
27    /// Lookup IPv6 with a fallback to IPv4
28    Ipv6thenIpv4,
29    /// Lookup IPv4 with a fallback to IPv6
30    Ipv4thenIpv6,
31}
32
33impl Display for IpAddrFamily {
34    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
35        match self {
36            Self::Ipv4Only => write!(f, "Ipv4Only"),
37            Self::Ipv6Only => write!(f, "Ipv6Only"),
38            Self::Ipv6thenIpv4 => write!(f, "Ipv6thenIpv4"),
39            Self::Ipv4thenIpv6 => write!(f, "Ipv4thenIpv6"),
40        }
41    }
42}
43
44/// A cheaply cloneable, non-blocking, caching, forward and reverse DNS resolver.
45#[derive(Clone)]
46pub struct DnsResolver {
47    inner: Rc<inner::DnsResolver>,
48}
49
50impl DnsResolver {
51    /// Create and start a new `DnsResolver`.
52    pub fn start(config: Config) -> std::io::Result<Self> {
53        Ok(Self {
54            inner: Rc::new(inner::DnsResolver::start(config)?),
55        })
56    }
57
58    /// Get the `Config`.
59    #[must_use]
60    pub fn config(&self) -> &Config {
61        self.inner.config()
62    }
63
64    /// Flush the cache of responses.
65    pub fn flush(&self) {
66        self.inner.flush();
67    }
68}
69
70impl Resolver for DnsResolver {
71    fn lookup(&self, hostname: impl AsRef<str>) -> Result<ResolvedIpAddrs> {
72        self.inner.lookup(hostname.as_ref())
73    }
74    #[must_use]
75    fn reverse_lookup(&self, addr: impl Into<IpAddr>) -> DnsEntry {
76        self.inner.reverse_lookup(addr.into(), false, false)
77    }
78    #[must_use]
79    fn reverse_lookup_with_asinfo(&self, addr: impl Into<IpAddr>) -> DnsEntry {
80        self.inner.reverse_lookup(addr.into(), true, false)
81    }
82    #[must_use]
83    fn lazy_reverse_lookup(&self, addr: impl Into<IpAddr>) -> DnsEntry {
84        self.inner.reverse_lookup(addr.into(), false, true)
85    }
86    #[must_use]
87    fn lazy_reverse_lookup_with_asinfo(&self, addr: impl Into<IpAddr>) -> DnsEntry {
88        self.inner.reverse_lookup(addr.into(), true, true)
89    }
90}
91
92/// Private impl of resolver.
93mod inner {
94    use super::{Config, IpAddrFamily, ResolveMethod};
95    use crate::resolver::{AsInfo, DnsEntry, Error, Resolved, ResolvedIpAddrs, Result, Unresolved};
96    use crossbeam::channel::{bounded, Receiver, Sender};
97    use hickory_resolver::config::{LookupIpStrategy, ResolverConfig, ResolverOpts};
98    use hickory_resolver::error::{ResolveError, ResolveErrorKind};
99    use hickory_resolver::proto::error::ProtoError;
100    use hickory_resolver::proto::rr::RecordType;
101    use hickory_resolver::system_conf::read_system_conf;
102    use hickory_resolver::{Name, Resolver};
103    use itertools::{Either, Itertools};
104    use parking_lot::RwLock;
105    use std::collections::HashMap;
106    use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
107    use std::str::FromStr;
108    use std::sync::Arc;
109    use std::thread;
110    use std::time::{Duration, SystemTime};
111
112    /// The maximum number of in-flight reverse DNS resolutions that may be
113    const RESOLVER_MAX_QUEUE_SIZE: usize = 100;
114
115    /// The duration wait to enqueue a `DnsEntry::Pending` to the resolver before returning
116    /// `DnsEntry::Timeout`.
117    const RESOLVER_QUEUE_TIMEOUT: Duration = Duration::from_millis(10);
118
119    /// Alias for a cache of reverse DNS lookup entries.
120    type Cache = Arc<RwLock<HashMap<IpAddr, CacheEntry>>>;
121
122    /// A cache entry for a reverse DNS lookup.
123    #[derive(Debug, Clone)]
124    struct CacheEntry {
125        /// The DNS entry to cache.
126        entry: DnsEntry,
127        /// The timestamp of the entry.
128        timestamp: SystemTime,
129    }
130
131    impl CacheEntry {
132        const fn new(entry: DnsEntry, timestamp: SystemTime) -> Self {
133            Self { entry, timestamp }
134        }
135
136        fn set_timestamp(&mut self, timestamp: SystemTime) {
137            self.timestamp = timestamp;
138        }
139    }
140
141    #[derive(Clone)]
142    enum DnsProvider {
143        TrustDns(Arc<Resolver>),
144        DnsLookup,
145    }
146
147    #[derive(Debug, Clone)]
148    struct DnsResolveRequest {
149        addr: IpAddr,
150        with_asinfo: bool,
151    }
152
153    /// Resolver implementation.
154    pub(super) struct DnsResolver {
155        config: Config,
156        provider: DnsProvider,
157        tx: Sender<DnsResolveRequest>,
158        addr_cache: Cache,
159    }
160
161    impl DnsResolver {
162        pub(super) fn start(config: Config) -> std::io::Result<Self> {
163            let (tx, rx) = bounded(RESOLVER_MAX_QUEUE_SIZE);
164            let addr_cache = Arc::new(RwLock::new(HashMap::new()));
165
166            let provider = if matches!(config.resolve_method, ResolveMethod::System) {
167                DnsProvider::DnsLookup
168            } else {
169                let mut options = ResolverOpts::default();
170                let ip_strategy = match config.addr_family {
171                    IpAddrFamily::Ipv4Only => LookupIpStrategy::Ipv4Only,
172                    IpAddrFamily::Ipv6Only => LookupIpStrategy::Ipv6Only,
173                    IpAddrFamily::Ipv6thenIpv4 => LookupIpStrategy::Ipv6thenIpv4,
174                    IpAddrFamily::Ipv4thenIpv6 => LookupIpStrategy::Ipv4thenIpv6,
175                };
176                options.timeout = config.timeout;
177                options.ip_strategy = ip_strategy;
178                let res = match config.resolve_method {
179                    ResolveMethod::Resolv => {
180                        let (resolver_cfg, mut options) = read_system_conf()?;
181                        options.timeout = config.timeout;
182                        options.ip_strategy = ip_strategy;
183                        Resolver::new(resolver_cfg, options)
184                    }
185                    ResolveMethod::Google => Resolver::new(ResolverConfig::google(), options),
186                    ResolveMethod::Cloudflare => {
187                        Resolver::new(ResolverConfig::cloudflare(), options)
188                    }
189                    ResolveMethod::System => unreachable!(),
190                }?;
191                let resolver = Arc::new(res);
192                DnsProvider::TrustDns(resolver)
193            };
194
195            // spawn a thread to process the resolve queue
196            {
197                let cache = addr_cache.clone();
198                let provider = provider.clone();
199                thread::spawn(move || resolver_queue_processor(rx, &provider, &cache));
200            }
201            Ok(Self {
202                config,
203                provider,
204                tx,
205                addr_cache,
206            })
207        }
208
209        pub(super) const fn config(&self) -> &Config {
210            &self.config
211        }
212
213        pub(super) fn lookup(&self, hostname: &str) -> Result<ResolvedIpAddrs> {
214            match &self.provider {
215                DnsProvider::TrustDns(resolver) => Ok(resolver
216                    .lookup_ip(hostname)
217                    .map_err(|err| Error::LookupFailed(Box::new(err)))?
218                    .iter()
219                    .collect::<Vec<_>>()),
220                DnsProvider::DnsLookup => {
221                    let (ipv4, ipv6): (Vec<_>, Vec<_>) = dns_lookup::lookup_host(hostname)
222                        .map_err(|err| Error::LookupFailed(Box::new(err)))?
223                        .into_iter()
224                        .partition_map(|ip| match ip {
225                            IpAddr::V4(_) => Either::Left(ip),
226                            IpAddr::V6(_) => Either::Right(ip),
227                        });
228                    Ok(match self.config.addr_family {
229                        IpAddrFamily::Ipv4Only => {
230                            if ipv4.is_empty() {
231                                vec![]
232                            } else {
233                                ipv4
234                            }
235                        }
236                        IpAddrFamily::Ipv6Only => {
237                            if ipv6.is_empty() {
238                                vec![]
239                            } else {
240                                ipv6
241                            }
242                        }
243                        IpAddrFamily::Ipv6thenIpv4 => {
244                            if ipv6.is_empty() {
245                                ipv4
246                            } else {
247                                ipv6
248                            }
249                        }
250                        IpAddrFamily::Ipv4thenIpv6 => {
251                            if ipv4.is_empty() {
252                                ipv6
253                            } else {
254                                ipv4
255                            }
256                        }
257                    })
258                }
259            }
260            .map(ResolvedIpAddrs)
261        }
262
263        pub(super) fn reverse_lookup(
264            &self,
265            addr: IpAddr,
266            with_asinfo: bool,
267            lazy: bool,
268        ) -> DnsEntry {
269            if lazy {
270                self.lazy_reverse_lookup(addr, with_asinfo).entry
271            } else {
272                reverse_lookup(&self.provider, addr, with_asinfo).entry
273            }
274        }
275
276        fn lazy_reverse_lookup(&self, addr: IpAddr, with_asinfo: bool) -> CacheEntry {
277            let mut enqueue = false;
278            let now = SystemTime::now();
279
280            // Check if we have already attempted to resolve this `IpAddr` and return the current
281            // `DnsEntry` if so, otherwise add it in a state of `DnsEntry::Pending`.
282            let mut dns_entry = self
283                .addr_cache
284                .write()
285                .entry(addr)
286                .or_insert_with(|| {
287                    enqueue = true;
288                    CacheEntry::new(DnsEntry::Pending(addr), now)
289                })
290                .clone();
291
292            // If the entry exists but is stale then enqueue it again.  The existing entry will
293            // be returned until it is refreshed but with an updated timestamp to prevent it from
294            // being enqueued multiple times.
295            match &dns_entry.entry {
296                DnsEntry::Resolved(_) | DnsEntry::NotFound(_) | DnsEntry::Failed(_) => {
297                    if now.duration_since(dns_entry.timestamp).unwrap_or_default() > self.config.ttl
298                    {
299                        self.addr_cache
300                            .write()
301                            .get_mut(&addr)
302                            .expect("addr must be in cache")
303                            .set_timestamp(now);
304                        enqueue = true;
305                    }
306                }
307                _ => {}
308            }
309
310            // If the entry exists but has timed out, then set it as `DnsEntry::Pending` and enqueue
311            // it again.
312            if let DnsEntry::Timeout(addr) = dns_entry.entry {
313                *self
314                    .addr_cache
315                    .write()
316                    .get_mut(&addr)
317                    .expect("addr must be in cache") =
318                    CacheEntry::new(DnsEntry::Pending(addr), now);
319                dns_entry = CacheEntry::new(DnsEntry::Pending(addr), now);
320                enqueue = true;
321            }
322
323            // If this is a newly added `DnsEntry` then send it to the channel to be resolved in the
324            // background.  We do this after the above to ensure we aren't holding the
325            // lock on the cache, which is used by the resolver and so would deadlock.
326            if enqueue {
327                if self
328                    .tx
329                    .send_timeout(
330                        DnsResolveRequest { addr, with_asinfo },
331                        RESOLVER_QUEUE_TIMEOUT,
332                    )
333                    .is_ok()
334                {
335                    dns_entry
336                } else {
337                    *self
338                        .addr_cache
339                        .write()
340                        .get_mut(&addr)
341                        .expect("addr must be in cache") =
342                        CacheEntry::new(DnsEntry::Timeout(addr), now);
343                    CacheEntry::new(DnsEntry::Timeout(addr), now)
344                }
345            } else {
346                dns_entry
347            }
348        }
349
350        pub fn flush(&self) {
351            self.addr_cache.write().clear();
352        }
353    }
354
355    /// Process each `IpAddr` from the resolver queue and perform the reverse DNS lookup.
356    ///
357    /// For each `IpAddr`, perform the reverse DNS lookup and update the cache with the result
358    /// (`Resolved`, `NotFound`, `Timeout` or `Failed`) for that addr.
359    fn resolver_queue_processor(
360        rx: Receiver<DnsResolveRequest>,
361        provider: &DnsProvider,
362        cache: &Cache,
363    ) {
364        for DnsResolveRequest { addr, with_asinfo } in rx {
365            let dns_entry = reverse_lookup(provider, addr, with_asinfo);
366            cache.write().insert(addr, dns_entry);
367        }
368    }
369
370    fn reverse_lookup(provider: &DnsProvider, addr: IpAddr, with_asinfo: bool) -> CacheEntry {
371        let now = SystemTime::now();
372        match &provider {
373            DnsProvider::DnsLookup => {
374                // we can't distinguish between a failed lookup or a genuine error, and so we just
375                // assume all failures are `DnsEntry::NotFound`.
376                match dns_lookup::lookup_addr(&addr) {
377                    Ok(dns) => {
378                        CacheEntry::new(DnsEntry::Resolved(Resolved::Normal(addr, vec![dns])), now)
379                    }
380                    Err(_) => CacheEntry::new(DnsEntry::NotFound(Unresolved::Normal(addr)), now),
381                }
382            }
383            DnsProvider::TrustDns(resolver) => match resolver.reverse_lookup(addr) {
384                Ok(name) => {
385                    let hostnames = name
386                        .into_iter()
387                        .map(|mut s| {
388                            s.0.set_fqdn(false);
389                            s
390                        })
391                        .map(|s| s.to_string())
392                        .collect();
393                    if with_asinfo {
394                        let as_info = lookup_asinfo(resolver, addr).unwrap_or_default();
395                        CacheEntry::new(
396                            DnsEntry::Resolved(Resolved::WithAsInfo(addr, hostnames, as_info)),
397                            now,
398                        )
399                    } else {
400                        CacheEntry::new(DnsEntry::Resolved(Resolved::Normal(addr, hostnames)), now)
401                    }
402                }
403                Err(err) => match err.kind() {
404                    ResolveErrorKind::NoRecordsFound { .. } => {
405                        if with_asinfo {
406                            let as_info = lookup_asinfo(resolver, addr).unwrap_or_default();
407                            CacheEntry::new(
408                                DnsEntry::NotFound(Unresolved::WithAsInfo(addr, as_info)),
409                                now,
410                            )
411                        } else {
412                            CacheEntry::new(DnsEntry::NotFound(Unresolved::Normal(addr)), now)
413                        }
414                    }
415                    ResolveErrorKind::Timeout => CacheEntry::new(DnsEntry::Timeout(addr), now),
416                    _ => CacheEntry::new(DnsEntry::Failed(addr), now),
417                },
418            },
419        }
420    }
421
422    /// Lookup up `AsInfo` for an `IpAddr` address.
423    fn lookup_asinfo(resolver: &Arc<Resolver>, addr: IpAddr) -> Result<AsInfo> {
424        let origin_query_txt = match addr {
425            IpAddr::V4(addr) => query_asn_ipv4(resolver, addr)?,
426            IpAddr::V6(addr) => query_asn_ipv6(resolver, addr)?,
427        };
428        let asinfo = parse_origin_query_txt(&origin_query_txt)?;
429        let asn_query_txt = query_asn_name(resolver, &asinfo.asn)?;
430        let as_name = parse_asn_query_txt(&asn_query_txt)?;
431        Ok(AsInfo {
432            asn: asinfo.asn,
433            prefix: asinfo.prefix,
434            cc: asinfo.cc,
435            registry: asinfo.registry,
436            allocated: asinfo.allocated,
437            name: as_name,
438        })
439    }
440
441    /// Perform the `origin` query.
442    fn query_asn_ipv4(resolver: &Arc<Resolver>, addr: Ipv4Addr) -> Result<String> {
443        let query = format!(
444            "{}.origin.asn.cymru.com.",
445            addr.octets().iter().rev().join(".")
446        );
447        let name = Name::from_str(query.as_str()).map_err(proto_error)?;
448        let response = resolver
449            .lookup(name, RecordType::TXT)
450            .map_err(resolve_error)?;
451        let data = response
452            .iter()
453            .next()
454            .ok_or_else(|| Error::QueryAsnOriginFailed)?;
455        let bytes = data.as_txt().ok_or_else(|| Error::QueryAsnOriginFailed)?;
456        Ok(bytes.to_string())
457    }
458
459    /// Perform the `origin` query.
460    fn query_asn_ipv6(resolver: &Arc<Resolver>, addr: Ipv6Addr) -> Result<String> {
461        let query = format!(
462            "{:x}.origin6.asn.cymru.com.",
463            addr.octets()
464                .iter()
465                .rev()
466                .flat_map(|o| [o & 0x0F, (o & 0xF0) >> 4])
467                .format(".")
468        );
469        let name = Name::from_str(query.as_str()).map_err(proto_error)?;
470        let response = resolver
471            .lookup(name, RecordType::TXT)
472            .map_err(resolve_error)?;
473        let data = response
474            .iter()
475            .next()
476            .ok_or_else(|| Error::QueryAsnOriginFailed)?;
477        let bytes = data.as_txt().ok_or_else(|| Error::QueryAsnOriginFailed)?;
478        Ok(bytes.to_string())
479    }
480
481    /// Perform the `asn` query.
482    fn query_asn_name(resolver: &Arc<Resolver>, asn: &str) -> Result<String> {
483        let query = format!("AS{asn}.asn.cymru.com.");
484        let name = Name::from_str(query.as_str()).map_err(proto_error)?;
485        let response = resolver
486            .lookup(name, RecordType::TXT)
487            .map_err(resolve_error)?;
488        let data = response
489            .iter()
490            .next()
491            .ok_or_else(|| Error::QueryAsnFailed)?;
492        let bytes = data.as_txt().ok_or_else(|| Error::QueryAsnFailed)?;
493        Ok(bytes.to_string())
494    }
495
496    /// The `origin` DNS query returns a TXT record in the formal:
497    ///      `asn | prefix | cc | registry | allocated`
498    ///
499    /// For example:
500    ///      `12301 | 81.0.100.0/22 | HU | ripencc | 2001-12-06`
501    ///
502    /// From this we extract all fields.
503    fn parse_origin_query_txt(origin_query_txt: &str) -> Result<AsInfo> {
504        if origin_query_txt.chars().filter(|c| *c == '|').count() != 4 {
505            return Err(Error::ParseOriginQueryFailed(String::from(
506                origin_query_txt,
507            )));
508        }
509        let mut split = origin_query_txt.split('|');
510        let asn = split.next().unwrap_or_default().trim().to_string();
511        let prefix = split.next().unwrap_or_default().trim().to_string();
512        let cc = split.next().unwrap_or_default().trim().to_string();
513        let registry = split.next().unwrap_or_default().trim().to_string();
514        let allocated = split.next().unwrap_or_default().trim().to_string();
515        Ok(AsInfo {
516            asn,
517            prefix,
518            cc,
519            registry,
520            allocated,
521            name: String::default(),
522        })
523    }
524
525    /// The `asn` DNS query returns a TXT record in the formal:
526    ///      `asn | cc | registry | allocated | name`
527    ///
528    /// For example:
529    ///      `12301 | HU | ripencc | 1999-02-25 | INVITECH, HU`
530    ///
531    /// From this we extract the 4th field (name, `INVITECH, HU` in this example)
532    fn parse_asn_query_txt(asn_query_txt: &str) -> Result<String> {
533        if asn_query_txt.chars().filter(|c| *c == '|').count() != 4 {
534            return Err(Error::ParseAsnQueryFailed(String::from(asn_query_txt)));
535        }
536        let mut split = asn_query_txt.split('|');
537        Ok(split.nth(4).unwrap_or_default().trim().to_string())
538    }
539
540    /// Convert a `ResolveError` to an `Error::LookupFailed`.
541    fn resolve_error(err: ResolveError) -> Error {
542        Error::LookupFailed(Box::new(err))
543    }
544
545    /// Convert a `ProtoError` to an `Error::LookupFailed`.
546    fn proto_error(err: ProtoError) -> Error {
547        Error::LookupFailed(Box::new(err))
548    }
549}