Skip to main content

mail_auth/
iprev.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//! Reverse IP address verification (`iprev`, RFC 8601 Section 3).
8//!
9//! The `iprev` check confirms that the SMTP client's IP address has a PTR
10//! record whose name resolves back to the same address. Run it with
11//! [`MessageAuthenticator::verify_iprev`] and report the outcome with
12//! [`AuthenticationResults::with_iprev_result`](crate::AuthenticationResults::with_iprev_result).
13
14use crate::dns::DnsCache;
15use crate::{
16    DnsError, Error, MessageAuthenticator,
17    dns::{NoCache, Parameters},
18};
19use std::{fmt::Display, net::IpAddr, sync::Arc};
20
21/// The outcome of an `iprev` check (RFC 8601 Section 3), produced by
22/// [`MessageAuthenticator::verify_iprev`].
23#[derive(Debug, PartialEq, Eq, Clone)]
24pub struct IprevOutput {
25    /// The `iprev` result.
26    pub result: IprevResult,
27    /// The host names returned by the PTR lookup, lowercased. `None` when the
28    /// PTR lookup itself failed.
29    pub ptr: Option<Arc<[Box<str>]>>,
30}
31
32/// An `iprev` result code, as defined in RFC 8601 Section 3.
33///
34/// The error carried by the failure variants explains the result and is
35/// written as a comment in the `Authentication-Results` header.
36#[derive(Debug, PartialEq, Eq, Clone)]
37pub enum IprevResult {
38    /// A PTR name resolved back to the client IP address.
39    Pass,
40    /// The PTR names were found, but none of them resolved back to the client
41    /// IP address. Carries [`Error::NotAligned`].
42    Fail(crate::Error),
43    /// A DNS lookup failed with a transient error ([`DnsError::Resolver`]).
44    /// The check may succeed if retried later.
45    TempError(crate::Error),
46    /// A DNS lookup failed permanently: no PTR record exists, a name was
47    /// malformed or a forward lookup returned no records.
48    PermError(crate::Error),
49    /// No `iprev` check was performed. [`MessageAuthenticator::verify_iprev`]
50    /// never returns this variant.
51    None,
52}
53
54impl MessageAuthenticator {
55    /// Verifies the reverse DNS of an SMTP client IP address (RFC 8601
56    /// Section 3).
57    ///
58    /// `params` is the client IP address, either alone or wrapped in
59    /// [`Parameters`] together with a DNS cache.
60    ///
61    /// The check performs these DNS lookups:
62    ///
63    /// 1. A PTR query for the IP address.
64    /// 2. For each of the first two names returned, an A query (IPv4 client)
65    ///    or AAAA query (IPv6 client).
66    ///
67    /// The result is:
68    ///
69    /// - [`IprevResult::Pass`] as soon as a forward lookup returns the client
70    ///   address.
71    /// - [`IprevResult::Fail`] with [`Error::NotAligned`] when every forward
72    ///   lookup succeeded but none returned the client address, or when the PTR
73    ///   answer was empty.
74    /// - [`IprevResult::TempError`] or [`IprevResult::PermError`] when the PTR
75    ///   lookup failed, or when no forward lookup matched and at least one of
76    ///   them failed. Resolver errors ([`DnsError::Resolver`]) are temporary;
77    ///   all other errors are permanent. The error of the last failed lookup is
78    ///   reported.
79    ///
80    /// [`IprevOutput::ptr`] holds the PTR names whenever the PTR lookup
81    /// succeeded.
82    ///
83    /// # Example
84    ///
85    /// ```rust,no_run
86    /// use mail_auth::{AuthenticationResults, IprevResult, MessageAuthenticator};
87    /// use std::net::IpAddr;
88    ///
89    /// # async fn run() {
90    /// let authenticator = MessageAuthenticator::new_cloudflare_tls().unwrap();
91    /// let remote_ip: IpAddr = "192.0.2.10".parse().unwrap();
92    ///
93    /// let iprev = authenticator.verify_iprev(remote_ip).await;
94    /// if iprev.result() == &IprevResult::Pass {
95    ///     println!("PTR names: {:?}", iprev.ptr);
96    /// }
97    ///
98    /// let header = AuthenticationResults::new("mx.example.org")
99    ///     .with_iprev_result(&iprev, remote_ip)
100    ///     .to_string();
101    /// # }
102    /// ```
103    pub async fn verify_iprev<'x, C>(
104        &self,
105        params: impl Into<Parameters<'x, IpAddr, C>>,
106    ) -> IprevOutput
107    where
108        C: DnsCache + 'x,
109    {
110        let params = params.into();
111        match self.ptr_lookup(params.input, params.ptr_cache()).await {
112            Ok(ptr) => {
113                let mut last_err = None;
114                for host in ptr.records.iter().take(2) {
115                    match &params.input {
116                        IpAddr::V4(ip) => match self.ipv4_lookup(host, params.ipv4_cache()).await {
117                            Ok(ips) => {
118                                if ips.records.iter().any(|cip| cip == ip) {
119                                    return IprevOutput {
120                                        result: IprevResult::Pass,
121                                        ptr: Some(ptr.records.clone()),
122                                    };
123                                }
124                            }
125                            Err(err) => {
126                                last_err = err.into();
127                            }
128                        },
129                        IpAddr::V6(ip) => match self.ipv6_lookup(host, params.ipv6_cache()).await {
130                            Ok(ips) => {
131                                if ips.records.iter().any(|cip| cip == ip) {
132                                    return IprevOutput {
133                                        result: IprevResult::Pass,
134                                        ptr: Some(ptr.records.clone()),
135                                    };
136                                }
137                            }
138                            Err(err) => {
139                                last_err = err.into();
140                            }
141                        },
142                    }
143                }
144
145                IprevOutput {
146                    result: if let Some(err) = last_err {
147                        err.into()
148                    } else {
149                        IprevResult::Fail(Error::NotAligned)
150                    },
151                    ptr: Some(ptr.records.clone()),
152                }
153            }
154            Err(err) => IprevOutput {
155                result: err.into(),
156                ptr: None,
157            },
158        }
159    }
160}
161
162impl From<IpAddr> for Parameters<'_, IpAddr, NoCache> {
163    fn from(params: IpAddr) -> Self {
164        Parameters::new(params)
165    }
166}
167
168impl IprevOutput {
169    /// Returns the `iprev` result.
170    pub fn result(&self) -> &IprevResult {
171        &self.result
172    }
173}
174
175impl From<Error> for IprevResult {
176    fn from(err: Error) -> Self {
177        if matches!(&err, Error::Dns(DnsError::Resolver(_))) {
178            IprevResult::TempError(err)
179        } else {
180            IprevResult::PermError(err)
181        }
182    }
183}
184
185impl Display for IprevResult {
186    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
187        match self {
188            IprevResult::Pass => f.write_str("pass"),
189            IprevResult::Fail(err) => write!(f, "fail; {err}"),
190            IprevResult::TempError(err) => write!(f, "temp error; {err}"),
191            IprevResult::PermError(err) => write!(f, "perm error; {err}"),
192            IprevResult::None => f.write_str("none"),
193        }
194    }
195}