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