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 ¶ms.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}