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