Skip to main content

mail_auth/common/
resolver.rs

1/*
2 * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
3 *
4 * SPDX-License-Identifier: Apache-2.0 OR MIT
5 */
6
7use super::{parse::TxtRecordParser, verify::DomainKey};
8use crate::Instant;
9use crate::{
10    DnssecStatus, Error, IpLookupStrategy, MX, MessageAuthenticator, RecordSet, ResolverCache, Txt,
11    dkim::{Atps, DomainKeyReport},
12    dmarc::Dmarc,
13    mta_sts::{MtaSts, TlsRpt},
14    spf::{Macro, Spf},
15};
16#[cfg(not(feature = "dns-doh"))]
17use hickory_resolver::{
18    TokioResolver,
19    config::{CLOUDFLARE, GOOGLE, QUAD9, ResolverConfig, ResolverOpts},
20    net::{DnsError, NetError, runtime::TokioRuntimeProvider},
21    proto::{
22        ProtoError,
23        rr::{Name, RData},
24    },
25    system_conf::read_system_conf,
26};
27use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
28use std::sync::Arc;
29
30pub struct DnsEntry<T> {
31    pub entry: T,
32    pub expires: Instant,
33}
34
35#[cfg(not(feature = "dns-doh"))]
36impl MessageAuthenticator {
37    #[cfg(any(feature = "ring", feature = "aws-lc-rs"))]
38    pub fn new_cloudflare_tls() -> Result<Self, NetError> {
39        Self::new(ResolverConfig::tls(&CLOUDFLARE), ResolverOpts::default())
40    }
41
42    pub fn new_cloudflare() -> Result<Self, NetError> {
43        Self::new(
44            ResolverConfig::udp_and_tcp(&CLOUDFLARE),
45            ResolverOpts::default(),
46        )
47    }
48
49    pub fn new_google() -> Result<Self, NetError> {
50        Self::new(
51            ResolverConfig::udp_and_tcp(&GOOGLE),
52            ResolverOpts::default(),
53        )
54    }
55
56    pub fn new_quad9() -> Result<Self, NetError> {
57        Self::new(ResolverConfig::udp_and_tcp(&QUAD9), ResolverOpts::default())
58    }
59
60    #[cfg(any(feature = "ring", feature = "aws-lc-rs"))]
61    pub fn new_quad9_tls() -> Result<Self, NetError> {
62        Self::new(ResolverConfig::tls(&QUAD9), ResolverOpts::default())
63    }
64
65    pub fn new_system_conf() -> Result<Self, NetError> {
66        let (config, options) = read_system_conf()?;
67        Self::new(config, options)
68    }
69
70    pub fn new(config: ResolverConfig, options: ResolverOpts) -> Result<Self, NetError> {
71        Ok(MessageAuthenticator(
72            TokioResolver::builder_with_config(config, TokioRuntimeProvider::default())
73                .with_options(options)
74                .build()?,
75        ))
76    }
77
78    pub fn resolver(&self) -> &TokioResolver {
79        &self.0
80    }
81}
82
83impl MessageAuthenticator {
84    pub async fn txt_raw_lookup(&self, key: impl ToFqdn) -> crate::Result<Vec<u8>> {
85        let key = key.to_fqdn();
86
87        #[cfg(not(feature = "dns-doh"))]
88        let records = {
89            let lookup = self
90                .0
91                .txt_lookup(Name::from_str_relaxed::<&str>(key.as_ref())?)
92                .await?;
93            let mut records: Vec<Vec<u8>> = Vec::new();
94            for record in lookup.answers() {
95                if let RData::TXT(txt) = &record.data {
96                    let mut entry = Vec::new();
97                    for item in &txt.txt_data {
98                        entry.extend_from_slice(item);
99                    }
100                    records.push(entry);
101                }
102            }
103            records
104        };
105
106        #[cfg(feature = "dns-doh")]
107        let records = self.doh_txt(key.as_ref()).await?.entry;
108
109        Ok(records.into_iter().flatten().collect())
110    }
111
112    pub async fn txt_lookup<T: TxtRecordParser + Into<Txt> + UnwrapTxtRecord>(
113        &self,
114        key: impl ToFqdn,
115        cache: Option<&impl ResolverCache<Box<str>, Txt>>,
116    ) -> crate::Result<Arc<T>> {
117        let key = key.to_fqdn();
118        if let Some(value) = cache.as_ref().and_then(|c| c.get::<str>(key.as_ref())) {
119            return T::unwrap_txt(value);
120        }
121
122        #[cfg(any(test, feature = "test"))]
123        if true {
124            return mock_resolve(key.as_ref());
125        }
126
127        #[cfg(not(feature = "dns-doh"))]
128        let (records, expires) = {
129            let lookup = self
130                .0
131                .txt_lookup(Name::from_str_relaxed::<&str>(key.as_ref())?)
132                .await?;
133            let expires = lookup.valid_until();
134            let mut records: Vec<Vec<u8>> = Vec::new();
135            for record in lookup.answers() {
136                let RData::TXT(txt) = &record.data else {
137                    continue;
138                };
139                match txt.txt_data.len() {
140                    0 => {}
141                    1 => records.push(txt.txt_data[0].to_vec()),
142                    _ => {
143                        let mut entry = Vec::with_capacity(255 * txt.txt_data.len());
144                        for data in txt.txt_data.iter() {
145                            entry.extend_from_slice(data);
146                        }
147                        records.push(entry);
148                    }
149                }
150            }
151            (records, expires)
152        };
153
154        #[cfg(feature = "dns-doh")]
155        let (records, expires) = {
156            let raw = self.doh_txt(key.as_ref()).await?;
157            (raw.entry, raw.expires)
158        };
159
160        let mut result = Err(Error::Dns(crate::DnsError::InvalidRecordType));
161        for record in &records {
162            result = T::parse(record);
163            if result.is_ok() {
164                break;
165            }
166        }
167
168        let result: Txt = result.into();
169
170        if let Some(cache) = cache {
171            cache.insert(key, result.clone(), expires);
172        }
173
174        T::unwrap_txt(result)
175    }
176
177    pub async fn mx_lookup(
178        &self,
179        key: impl ToFqdn,
180        cache: Option<&impl ResolverCache<Box<str>, RecordSet<MX>>>,
181    ) -> crate::Result<RecordSet<MX>> {
182        let key = key.to_fqdn();
183        if let Some(value) = cache.as_ref().and_then(|c| c.get::<str>(key.as_ref())) {
184            return Ok(value);
185        }
186
187        #[cfg(any(test, feature = "test"))]
188        if true {
189            return mock_resolve(key.as_ref());
190        }
191
192        #[cfg(not(feature = "dns-doh"))]
193        let (mx_records, expires): (Vec<(u16, Box<str>)>, Instant) = {
194            let lookup = self
195                .0
196                .mx_lookup(Name::from_str_relaxed::<&str>(key.as_ref())?)
197                .await?;
198            let expires = lookup.valid_until();
199            let mx_records = lookup
200                .answers()
201                .iter()
202                .filter_map(|r| {
203                    let RData::MX(mx) = &r.data else {
204                        return None;
205                    };
206                    Some((
207                        mx.preference,
208                        mx.exchange.to_lowercase().to_string().into_boxed_str(),
209                    ))
210                })
211                .collect();
212            (mx_records, expires)
213        };
214
215        #[cfg(feature = "dns-doh")]
216        let (mx_records, expires): (Vec<(u16, Box<str>)>, Instant) = {
217            let raw = self.doh_mx(key.as_ref()).await?;
218            (raw.entry, raw.expires)
219        };
220
221        let mut records: Vec<(u16, Vec<Box<str>>)> = Vec::with_capacity(mx_records.len());
222        for (preference, exchange) in mx_records {
223            if let Some(record) = records.iter_mut().find(|r| r.0 == preference) {
224                record.1.push(exchange);
225            } else {
226                records.push((preference, vec![exchange]));
227            }
228        }
229
230        records.sort_unstable_by_key(|a| a.0);
231        let records: Arc<[MX]> = records
232            .into_iter()
233            .map(|(preference, exchanges)| MX {
234                preference,
235                exchanges: exchanges.into_boxed_slice(),
236            })
237            .collect::<Arc<[MX]>>();
238        let records = RecordSet {
239            rrset: records,
240            dnssec_status: DnssecStatus::Indeterminate,
241        };
242
243        if let Some(cache) = cache {
244            cache.insert(key, records.clone(), expires);
245        }
246
247        Ok(records)
248    }
249
250    pub async fn ipv4_lookup(
251        &self,
252        key: impl ToFqdn,
253        cache: Option<&impl ResolverCache<Box<str>, RecordSet<Ipv4Addr>>>,
254    ) -> crate::Result<RecordSet<Ipv4Addr>> {
255        let key = key.to_fqdn();
256        if let Some(value) = cache.as_ref().and_then(|c| c.get::<str>(key.as_ref())) {
257            return Ok(value);
258        }
259
260        let ipv4_lookup = self.ipv4_lookup_raw(key.as_ref()).await?;
261        let records = RecordSet {
262            rrset: ipv4_lookup.entry,
263            dnssec_status: DnssecStatus::Indeterminate,
264        };
265
266        if let Some(cache) = cache {
267            cache.insert(key, records.clone(), ipv4_lookup.expires);
268        }
269
270        Ok(records)
271    }
272
273    pub async fn ipv4_lookup_raw(&self, key: &str) -> crate::Result<DnsEntry<Arc<[Ipv4Addr]>>> {
274        #[cfg(any(test, feature = "test"))]
275        if true {
276            return mock_resolve(key);
277        }
278
279        #[cfg(not(feature = "dns-doh"))]
280        {
281            let lookup = self
282                .0
283                .ipv4_lookup(Name::from_str_relaxed::<&str>(key)?)
284                .await?;
285            let expires = lookup.valid_until();
286            let entry: Arc<[Ipv4Addr]> = lookup
287                .answers()
288                .iter()
289                .filter_map(|r| {
290                    if let RData::A(a) = &r.data {
291                        Some(a.0)
292                    } else {
293                        None
294                    }
295                })
296                .collect::<Vec<Ipv4Addr>>()
297                .into();
298            Ok(DnsEntry { entry, expires })
299        }
300
301        #[cfg(feature = "dns-doh")]
302        self.doh_ipv4(key).await
303    }
304
305    pub async fn ipv6_lookup(
306        &self,
307        key: impl ToFqdn,
308        cache: Option<&impl ResolverCache<Box<str>, RecordSet<Ipv6Addr>>>,
309    ) -> crate::Result<RecordSet<Ipv6Addr>> {
310        let key = key.to_fqdn();
311        if let Some(value) = cache.as_ref().and_then(|c| c.get::<str>(key.as_ref())) {
312            return Ok(value);
313        }
314
315        let ipv6_lookup = self.ipv6_lookup_raw(key.as_ref()).await?;
316        let records = RecordSet {
317            rrset: ipv6_lookup.entry,
318            dnssec_status: DnssecStatus::Indeterminate,
319        };
320
321        if let Some(cache) = cache {
322            cache.insert(key, records.clone(), ipv6_lookup.expires);
323        }
324
325        Ok(records)
326    }
327
328    pub async fn ipv6_lookup_raw(&self, key: &str) -> crate::Result<DnsEntry<Arc<[Ipv6Addr]>>> {
329        #[cfg(any(test, feature = "test"))]
330        if true {
331            return mock_resolve(key);
332        }
333
334        #[cfg(not(feature = "dns-doh"))]
335        {
336            let lookup = self
337                .0
338                .ipv6_lookup(Name::from_str_relaxed::<&str>(key)?)
339                .await?;
340            let expires = lookup.valid_until();
341            let entry: Arc<[Ipv6Addr]> = lookup
342                .answers()
343                .iter()
344                .filter_map(|r| {
345                    if let RData::AAAA(aaaa) = &r.data {
346                        Some(aaaa.0)
347                    } else {
348                        None
349                    }
350                })
351                .collect::<Vec<Ipv6Addr>>()
352                .into();
353            Ok(DnsEntry { entry, expires })
354        }
355
356        #[cfg(feature = "dns-doh")]
357        self.doh_ipv6(key).await
358    }
359
360    pub async fn ip_lookup(
361        &self,
362        key: &str,
363        mut strategy: IpLookupStrategy,
364        max_results: usize,
365        cache_ipv4: Option<&impl ResolverCache<Box<str>, RecordSet<Ipv4Addr>>>,
366        cache_ipv6: Option<&impl ResolverCache<Box<str>, RecordSet<Ipv6Addr>>>,
367    ) -> crate::Result<Vec<IpAddr>> {
368        loop {
369            match strategy {
370                IpLookupStrategy::Ipv4Only | IpLookupStrategy::Ipv4thenIpv6 => {
371                    match (self.ipv4_lookup(key, cache_ipv4).await, strategy) {
372                        (Ok(result), _) => {
373                            return Ok(result
374                                .rrset
375                                .iter()
376                                .take(max_results)
377                                .copied()
378                                .map(IpAddr::from)
379                                .collect());
380                        }
381                        (Err(err), IpLookupStrategy::Ipv4Only) => return Err(err),
382                        _ => {
383                            strategy = IpLookupStrategy::Ipv6Only;
384                        }
385                    }
386                }
387                IpLookupStrategy::Ipv6Only | IpLookupStrategy::Ipv6thenIpv4 => {
388                    match (self.ipv6_lookup(key, cache_ipv6).await, strategy) {
389                        (Ok(result), _) => {
390                            return Ok(result
391                                .rrset
392                                .iter()
393                                .take(max_results)
394                                .copied()
395                                .map(IpAddr::from)
396                                .collect());
397                        }
398                        (Err(err), IpLookupStrategy::Ipv6Only) => return Err(err),
399                        _ => {
400                            strategy = IpLookupStrategy::Ipv4Only;
401                        }
402                    }
403                }
404            }
405        }
406    }
407
408    pub async fn ptr_lookup(
409        &self,
410        addr: IpAddr,
411        cache: Option<&impl ResolverCache<IpAddr, RecordSet<Box<str>>>>,
412    ) -> crate::Result<RecordSet<Box<str>>> {
413        if let Some(value) = cache.as_ref().and_then(|c| c.get(&addr)) {
414            return Ok(value);
415        }
416
417        #[cfg(any(test, feature = "test"))]
418        if true {
419            return mock_resolve(&addr.to_string());
420        }
421
422        #[cfg(not(feature = "dns-doh"))]
423        let (entry, expires): (Arc<[Box<str>]>, Instant) = {
424            let lookup = self.0.reverse_lookup(addr).await?;
425            let expires = lookup.valid_until();
426            let entry = lookup
427                .answers()
428                .iter()
429                .filter_map(|r| {
430                    let RData::PTR(ptr) = &r.data else {
431                        return None;
432                    };
433                    if !ptr.is_empty() {
434                        Some(ptr.to_lowercase().to_string().into_boxed_str())
435                    } else {
436                        None
437                    }
438                })
439                .collect::<Arc<[Box<str>]>>();
440            (entry, expires)
441        };
442
443        #[cfg(feature = "dns-doh")]
444        let (entry, expires): (Arc<[Box<str>]>, Instant) = {
445            let raw = self.doh_ptr(addr).await?;
446            (raw.entry, raw.expires)
447        };
448
449        let ptr = RecordSet {
450            rrset: entry,
451            dnssec_status: DnssecStatus::Indeterminate,
452        };
453
454        if let Some(cache) = cache {
455            cache.insert(addr, ptr.clone(), expires);
456        }
457
458        Ok(ptr)
459    }
460
461    #[cfg(any(test, feature = "test"))]
462    pub async fn exists(
463        &self,
464        key: impl ToFqdn,
465        cache_ipv4: Option<&impl ResolverCache<Box<str>, RecordSet<Ipv4Addr>>>,
466        cache_ipv6: Option<&impl ResolverCache<Box<str>, RecordSet<Ipv6Addr>>>,
467    ) -> crate::Result<bool> {
468        let key = key.to_fqdn();
469        match self.ipv4_lookup(key.as_ref(), cache_ipv4).await {
470            Ok(_) => Ok(true),
471            Err(Error::Dns(crate::DnsError::RecordNotFound(_))) => {
472                match self.ipv6_lookup(key.as_ref(), cache_ipv6).await {
473                    Ok(_) => Ok(true),
474                    Err(Error::Dns(crate::DnsError::RecordNotFound(_))) => Ok(false),
475                    Err(err) => Err(err),
476                }
477            }
478            Err(err) => Err(err),
479        }
480    }
481
482    #[cfg(not(any(test, feature = "test")))]
483    pub async fn exists(
484        &self,
485        key: impl ToFqdn,
486        cache_ipv4: Option<&impl ResolverCache<Box<str>, RecordSet<Ipv4Addr>>>,
487        cache_ipv6: Option<&impl ResolverCache<Box<str>, RecordSet<Ipv6Addr>>>,
488    ) -> crate::Result<bool> {
489        let key = key.to_fqdn();
490
491        if cache_ipv4.is_some_and(|c| c.get::<str>(key.as_ref()).is_some())
492            || cache_ipv6.is_some_and(|c| c.get::<str>(key.as_ref()).is_some())
493        {
494            return Ok(true);
495        }
496
497        #[cfg(not(feature = "dns-doh"))]
498        {
499            match self
500                .0
501                .lookup_ip(Name::from_str_relaxed::<&str>(key.as_ref())?)
502                .await
503            {
504                Ok(result) => Ok(result.as_lookup().answers().iter().any(|r| {
505                    matches!(
506                        &r.data.record_type(),
507                        hickory_resolver::proto::rr::RecordType::A
508                            | hickory_resolver::proto::rr::RecordType::AAAA
509                    )
510                })),
511                Err(err) if err.is_no_records_found() => Ok(false),
512                Err(err) => Err(err.into()),
513            }
514        }
515
516        #[cfg(feature = "dns-doh")]
517        self.doh_exists(key.as_ref()).await
518    }
519}
520
521#[cfg(not(feature = "dns-doh"))]
522impl From<ProtoError> for Error {
523    fn from(err: ProtoError) -> Self {
524        Error::Dns(crate::DnsError::Resolver(err.to_string()))
525    }
526}
527
528#[cfg(not(feature = "dns-doh"))]
529impl From<NetError> for Error {
530    fn from(err: NetError) -> Self {
531        match &err {
532            NetError::Dns(DnsError::NoRecordsFound(no_records)) => {
533                Error::Dns(crate::DnsError::RecordNotFound(no_records.response_code))
534            }
535            _ => Error::Dns(crate::DnsError::Resolver(err.to_string())),
536        }
537    }
538}
539
540impl From<DomainKey> for Txt {
541    fn from(v: DomainKey) -> Self {
542        Txt::DomainKey(v.into())
543    }
544}
545
546impl From<DomainKeyReport> for Txt {
547    fn from(v: DomainKeyReport) -> Self {
548        Txt::DomainKeyReport(v.into())
549    }
550}
551
552impl From<Atps> for Txt {
553    fn from(v: Atps) -> Self {
554        Txt::Atps(v.into())
555    }
556}
557
558impl From<Spf> for Txt {
559    fn from(v: Spf) -> Self {
560        Txt::Spf(v.into())
561    }
562}
563
564impl From<Macro> for Txt {
565    fn from(v: Macro) -> Self {
566        Txt::SpfMacro(v.into())
567    }
568}
569
570impl From<Dmarc> for Txt {
571    fn from(v: Dmarc) -> Self {
572        Txt::Dmarc(v.into())
573    }
574}
575
576impl From<MtaSts> for Txt {
577    fn from(v: MtaSts) -> Self {
578        Txt::MtaSts(v.into())
579    }
580}
581
582impl From<TlsRpt> for Txt {
583    fn from(v: TlsRpt) -> Self {
584        Txt::TlsRpt(v.into())
585    }
586}
587
588impl<T: Into<Txt>> From<crate::Result<T>> for Txt {
589    fn from(v: crate::Result<T>) -> Self {
590        match v {
591            Ok(v) => v.into(),
592            Err(err) => Txt::Error(err),
593        }
594    }
595}
596
597pub trait UnwrapTxtRecord: Sized {
598    fn unwrap_txt(txt: Txt) -> crate::Result<Arc<Self>>;
599}
600
601impl UnwrapTxtRecord for DomainKey {
602    fn unwrap_txt(txt: Txt) -> crate::Result<Arc<Self>> {
603        match txt {
604            Txt::DomainKey(a) => Ok(a),
605            Txt::Error(err) => Err(err),
606            _ => Err(Error::Io("Invalid record type".to_string())),
607        }
608    }
609}
610
611impl UnwrapTxtRecord for DomainKeyReport {
612    fn unwrap_txt(txt: Txt) -> crate::Result<Arc<Self>> {
613        match txt {
614            Txt::DomainKeyReport(a) => Ok(a),
615            Txt::Error(err) => Err(err),
616            _ => Err(Error::Io("Invalid record type".to_string())),
617        }
618    }
619}
620
621impl UnwrapTxtRecord for Atps {
622    fn unwrap_txt(txt: Txt) -> crate::Result<Arc<Self>> {
623        match txt {
624            Txt::Atps(a) => Ok(a),
625            Txt::Error(err) => Err(err),
626            _ => Err(Error::Io("Invalid record type".to_string())),
627        }
628    }
629}
630
631impl UnwrapTxtRecord for Spf {
632    fn unwrap_txt(txt: Txt) -> crate::Result<Arc<Self>> {
633        match txt {
634            Txt::Spf(a) => Ok(a),
635            Txt::Error(err) => Err(err),
636            _ => Err(Error::Io("Invalid record type".to_string())),
637        }
638    }
639}
640
641impl UnwrapTxtRecord for Macro {
642    fn unwrap_txt(txt: Txt) -> crate::Result<Arc<Self>> {
643        match txt {
644            Txt::SpfMacro(a) => Ok(a),
645            Txt::Error(err) => Err(err),
646            _ => Err(Error::Io("Invalid record type".to_string())),
647        }
648    }
649}
650
651impl UnwrapTxtRecord for Dmarc {
652    fn unwrap_txt(txt: Txt) -> crate::Result<Arc<Self>> {
653        match txt {
654            Txt::Dmarc(a) => Ok(a),
655            Txt::Error(err) => Err(err),
656            _ => Err(Error::Io("Invalid record type".to_string())),
657        }
658    }
659}
660
661impl UnwrapTxtRecord for MtaSts {
662    fn unwrap_txt(txt: Txt) -> crate::Result<Arc<Self>> {
663        match txt {
664            Txt::MtaSts(a) => Ok(a),
665            Txt::Error(err) => Err(err),
666            _ => Err(Error::Io("Invalid record type".to_string())),
667        }
668    }
669}
670
671impl UnwrapTxtRecord for TlsRpt {
672    fn unwrap_txt(txt: Txt) -> crate::Result<Arc<Self>> {
673        match txt {
674            Txt::TlsRpt(a) => Ok(a),
675            Txt::Error(err) => Err(err),
676            _ => Err(Error::Io("Invalid record type".to_string())),
677        }
678    }
679}
680
681pub trait ToFqdn {
682    fn to_fqdn(&self) -> Box<str>;
683}
684
685impl<T: AsRef<str>> ToFqdn for T {
686    fn to_fqdn(&self) -> Box<str> {
687        let value = self.as_ref();
688        if value.ends_with('.') {
689            value.to_lowercase().into()
690        } else {
691            format!("{}.", value.to_lowercase()).into()
692        }
693    }
694}
695
696pub trait ToReverseName {
697    fn to_reverse_name(&self) -> String;
698}
699
700impl ToReverseName for IpAddr {
701    fn to_reverse_name(&self) -> String {
702        use std::fmt::Write;
703
704        match self {
705            IpAddr::V4(ip) => {
706                let mut segments = String::with_capacity(15);
707                for octet in ip.octets().iter().rev() {
708                    if !segments.is_empty() {
709                        segments.push('.');
710                    }
711                    let _ = write!(&mut segments, "{}", octet);
712                }
713                segments
714            }
715            IpAddr::V6(ip) => {
716                let mut segments = String::with_capacity(63);
717                for segment in ip.segments().iter().rev() {
718                    for &p in format!("{segment:04x}").as_bytes().iter().rev() {
719                        if !segments.is_empty() {
720                            segments.push('.');
721                        }
722                        segments.push(char::from(p));
723                    }
724                }
725                segments
726            }
727        }
728    }
729}
730
731#[cfg(any(test, feature = "test"))]
732pub fn mock_resolve<T>(domain: &str) -> crate::Result<T> {
733    Err(if domain.contains("_parse_error.") {
734        Error::ParseError
735    } else if domain.contains("_invalid_record.") {
736        Error::Dns(crate::DnsError::InvalidRecordType)
737    } else if domain.contains("_dns_error.") {
738        Error::Dns(crate::DnsError::Resolver("".to_string()))
739    } else {
740        Error::Dns(crate::DnsError::RecordNotFound(crate::DNS_RCODE_NXDOMAIN))
741    })
742}
743
744#[cfg(test)]
745mod test {
746    use std::net::IpAddr;
747
748    use crate::common::resolver::ToReverseName;
749
750    #[test]
751    fn reverse_lookup_addr() {
752        for (addr, expected) in [
753            ("1.2.3.4", "4.3.2.1"),
754            (
755                "2001:db8::cb01",
756                "1.0.b.c.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2",
757            ),
758            (
759                "2a01:4f9:c011:b43c::1",
760                "1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.c.3.4.b.1.1.0.c.9.f.4.0.1.0.a.2",
761            ),
762        ] {
763            assert_eq!(addr.parse::<IpAddr>().unwrap().to_reverse_name(), expected);
764        }
765    }
766}