Skip to main content

mail_auth/dns/
hickory.rs

1/*
2 * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
3 *
4 * SPDX-License-Identifier: Apache-2.0 OR MIT
5 */
6
7//! Hickory DNS backend, enabled by the default `dns-hickory` feature.
8//!
9//! Queries go through a [`hickory_resolver::TokioResolver`] over UDP, TCP or
10//! DNS-over-TLS (RFC 7858).
11
12use super::{DnsEntry, QueryError, QueryResult};
13use crate::{Error, Instant, MessageAuthenticator, authenticator::DEFAULT_MAX_NEGATIVE_TTL};
14use hickory_resolver::{
15    TokioResolver,
16    config::{CLOUDFLARE, GOOGLE, QUAD9, ResolverConfig, ResolverOpts},
17    net::{DnsError, NetError, runtime::TokioRuntimeProvider},
18    proto::{
19        ProtoError,
20        rr::{Name, RData},
21    },
22    system_conf::read_system_conf,
23};
24use std::{
25    net::{IpAddr, Ipv4Addr, Ipv6Addr},
26    sync::Arc,
27};
28
29impl MessageAuthenticator {
30    /// Creates an authenticator that queries Cloudflare's public resolvers
31    /// over DNS-over-TLS (RFC 7858). Requires the `aws-lc-rs`
32    /// feature.
33    ///
34    /// # Errors
35    ///
36    /// Returns a [`NetError`] if the resolver cannot be built.
37    #[cfg(feature = "aws-lc-rs")]
38    pub fn new_cloudflare_tls() -> Result<Self, NetError> {
39        Self::new(ResolverConfig::tls(&CLOUDFLARE), ResolverOpts::default())
40    }
41
42    /// Creates an authenticator that queries Cloudflare's public resolvers
43    /// over UDP and TCP.
44    ///
45    /// # Errors
46    ///
47    /// Returns a [`NetError`] if the resolver cannot be built.
48    pub fn new_cloudflare() -> Result<Self, NetError> {
49        Self::new(
50            ResolverConfig::udp_and_tcp(&CLOUDFLARE),
51            ResolverOpts::default(),
52        )
53    }
54
55    /// Creates an authenticator that queries Google's public resolvers over
56    /// UDP and TCP.
57    ///
58    /// # Errors
59    ///
60    /// Returns a [`NetError`] if the resolver cannot be built.
61    pub fn new_google() -> Result<Self, NetError> {
62        Self::new(
63            ResolverConfig::udp_and_tcp(&GOOGLE),
64            ResolverOpts::default(),
65        )
66    }
67
68    /// Creates an authenticator that queries Quad9's public resolvers over
69    /// UDP and TCP.
70    ///
71    /// # Errors
72    ///
73    /// Returns a [`NetError`] if the resolver cannot be built.
74    pub fn new_quad9() -> Result<Self, NetError> {
75        Self::new(ResolverConfig::udp_and_tcp(&QUAD9), ResolverOpts::default())
76    }
77
78    /// Creates an authenticator that queries Quad9's public resolvers over
79    /// DNS-over-TLS (RFC 7858). Requires the `aws-lc-rs` feature.
80    ///
81    /// # Errors
82    ///
83    /// Returns a [`NetError`] if the resolver cannot be built.
84    #[cfg(feature = "aws-lc-rs")]
85    pub fn new_quad9_tls() -> Result<Self, NetError> {
86        Self::new(ResolverConfig::tls(&QUAD9), ResolverOpts::default())
87    }
88
89    /// Creates an authenticator that uses the system resolver configuration
90    /// (`/etc/resolv.conf` on Unix, the registry on Windows).
91    ///
92    /// # Errors
93    ///
94    /// Returns a [`NetError`] if the system configuration cannot be read or
95    /// the resolver cannot be built.
96    pub fn new_system_conf() -> Result<Self, NetError> {
97        let (config, options) = read_system_conf()?;
98        Self::new(config, options)
99    }
100
101    /// Creates an authenticator from an explicit hickory resolver
102    /// configuration and options.
103    ///
104    /// # Errors
105    ///
106    /// Returns a [`NetError`] if the resolver cannot be built.
107    pub fn new(config: ResolverConfig, options: ResolverOpts) -> Result<Self, NetError> {
108        Ok(MessageAuthenticator {
109            resolver: TokioResolver::builder_with_config(config, TokioRuntimeProvider::default())
110                .with_options(options)
111                .build()?,
112            max_negative_ttl: DEFAULT_MAX_NEGATIVE_TTL,
113        })
114    }
115
116    pub(crate) async fn query_txt(&self, key: &str) -> QueryResult<Vec<Vec<u8>>> {
117        let lookup = self
118            .resolver
119            .txt_lookup(Name::from_str_relaxed::<&str>(key)?)
120            .await?;
121        let expires = lookup.valid_until();
122        let mut entry: Vec<Vec<u8>> = Vec::new();
123        for record in lookup.answers() {
124            let RData::TXT(txt) = &record.data else {
125                continue;
126            };
127            match txt.txt_data.len() {
128                0 => {}
129                1 => entry.push(txt.txt_data[0].to_vec()),
130                _ => {
131                    let mut data = Vec::with_capacity(255 * txt.txt_data.len());
132                    for chunk in txt.txt_data.iter() {
133                        data.extend_from_slice(chunk);
134                    }
135                    entry.push(data);
136                }
137            }
138        }
139        Ok(DnsEntry { entry, expires })
140    }
141
142    pub(crate) async fn query_mx(&self, key: &str) -> QueryResult<Vec<(u16, Box<str>)>> {
143        let lookup = self
144            .resolver
145            .mx_lookup(Name::from_str_relaxed::<&str>(key)?)
146            .await?;
147        let expires = lookup.valid_until();
148        let entry = lookup
149            .answers()
150            .iter()
151            .filter_map(|r| {
152                let RData::MX(mx) = &r.data else {
153                    return None;
154                };
155                Some((
156                    mx.preference,
157                    mx.exchange.to_lowercase().to_ascii().into_boxed_str(),
158                ))
159            })
160            .collect();
161        Ok(DnsEntry { entry, expires })
162    }
163
164    pub(crate) async fn query_ipv4(&self, key: &str) -> QueryResult<Arc<[Ipv4Addr]>> {
165        let lookup = self
166            .resolver
167            .ipv4_lookup(Name::from_str_relaxed::<&str>(key)?)
168            .await?;
169        let expires = lookup.valid_until();
170        let entry: Arc<[Ipv4Addr]> = lookup
171            .answers()
172            .iter()
173            .filter_map(|r| {
174                if let RData::A(a) = &r.data {
175                    Some(a.0)
176                } else {
177                    None
178                }
179            })
180            .collect::<Vec<Ipv4Addr>>()
181            .into();
182        Ok(DnsEntry { entry, expires })
183    }
184
185    pub(crate) async fn query_ipv6(&self, key: &str) -> QueryResult<Arc<[Ipv6Addr]>> {
186        let lookup = self
187            .resolver
188            .ipv6_lookup(Name::from_str_relaxed::<&str>(key)?)
189            .await?;
190        let expires = lookup.valid_until();
191        let entry: Arc<[Ipv6Addr]> = lookup
192            .answers()
193            .iter()
194            .filter_map(|r| {
195                if let RData::AAAA(aaaa) = &r.data {
196                    Some(aaaa.0)
197                } else {
198                    None
199                }
200            })
201            .collect::<Vec<Ipv6Addr>>()
202            .into();
203        Ok(DnsEntry { entry, expires })
204    }
205
206    pub(crate) async fn query_ptr(&self, addr: IpAddr) -> QueryResult<Arc<[Box<str>]>> {
207        let lookup = self.resolver.reverse_lookup(addr).await?;
208        let expires: Instant = lookup.valid_until();
209        let entry = lookup
210            .answers()
211            .iter()
212            .filter_map(|r| {
213                let RData::PTR(ptr) = &r.data else {
214                    return None;
215                };
216                if !ptr.is_empty() {
217                    Some(ptr.to_lowercase().to_ascii().into_boxed_str())
218                } else {
219                    None
220                }
221            })
222            .collect::<Arc<[Box<str>]>>();
223        Ok(DnsEntry { entry, expires })
224    }
225
226    pub(crate) async fn query_exists(&self, key: &str) -> crate::Result<bool> {
227        match self
228            .resolver
229            .lookup_ip(Name::from_str_relaxed::<&str>(key)?)
230            .await
231        {
232            Ok(result) => Ok(result.as_lookup().answers().iter().any(|r| {
233                matches!(
234                    &r.data.record_type(),
235                    hickory_resolver::proto::rr::RecordType::A
236                        | hickory_resolver::proto::rr::RecordType::AAAA
237                )
238            })),
239            Err(err) if err.is_no_records_found() => Ok(false),
240            Err(err) => Err(err.into()),
241        }
242    }
243}
244
245impl From<ProtoError> for Error {
246    fn from(_: ProtoError) -> Self {
247        Error::Parse
248    }
249}
250
251impl From<ProtoError> for QueryError {
252    fn from(_: ProtoError) -> Self {
253        QueryError::Other(Error::Parse)
254    }
255}
256
257impl From<NetError> for QueryError {
258    fn from(err: NetError) -> Self {
259        match &err {
260            NetError::Dns(DnsError::NoRecordsFound(no_records)) => QueryError::NotFound {
261                code: no_records.response_code,
262                negative_ttl: no_records.negative_ttl,
263            },
264            _ => QueryError::Other(Error::Dns(crate::DnsError::Resolver(err.to_string()))),
265        }
266    }
267}
268
269impl From<NetError> for Error {
270    fn from(err: NetError) -> Self {
271        QueryError::from(err).into()
272    }
273}
274
275#[cfg(test)]
276mod test {
277    use super::Name;
278    use crate::Error;
279
280    #[test]
281    fn invalid_name_is_permanent_error() {
282        let err = Name::from_str_relaxed(format!("{}.example.org", "a".repeat(64))).unwrap_err();
283        assert_eq!(Error::from(err), Error::Parse);
284    }
285}