Commit
Author: Mauro D [mauro@stalw.art]
Hash: 2814d96381fcc6f86dd4c89cd6521fe1f36e138c
Timestamp: Sat, 31 Dec 2022 13:54:21 +0000 (1 year ago)

+57 -32 +/-8 browse
MX record grouping by preference.
1diff --git a/src/common/auth_results.rs b/src/common/auth_results.rs
2index dcfaa4d..711ad5d 100644
3--- a/src/common/auth_results.rs
4+++ b/src/common/auth_results.rs
5 @@ -280,7 +280,7 @@ impl AsAuthResult for Error {
6 Error::RevokedPublicKey => "revoked public key",
7 Error::IncompatibleAlgorithms => "incompatible record/signature algorithms",
8 Error::SignatureExpired => "signature error",
9- Error::DNSError => "dns error",
10+ Error::DNSError(_) => "dns error",
11 Error::DNSRecordNotFound(_) => "dns record not found",
12 Error::ARCInvalidInstance(i) => {
13 write!(header, "invalid ARC instance {})", i).ok();
14 @@ -347,7 +347,7 @@ mod test {
15 "header.s=otherselctor header.b=YWJjZGVm header.from=jdoe@example.org"
16 ),
17 DkimOutput {
18- result: DkimResult::TempError(Error::DNSError),
19+ result: DkimResult::TempError(Error::DNSError("".to_string())),
20 signature: (&Signature {
21 d: "atps.example.org".into(),
22 s: "otherselctor".into(),
23 diff --git a/src/common/resolver.rs b/src/common/resolver.rs
24index b9e407a..bdc6829 100644
25--- a/src/common/resolver.rs
26+++ b/src/common/resolver.rs
27 @@ -155,18 +155,24 @@ impl Resolver {
28 }
29
30 let mx_lookup = self.resolver.mx_lookup(key.as_ref()).await?;
31- let mut records = mx_lookup
32- .as_lookup()
33- .record_iter()
34- .filter_map(|r| {
35- let mx = r.data()?.as_mx()?;
36- MX {
37- exchange: mx.exchange().to_lowercase().to_string(),
38- preference: mx.preference(),
39+ let mx_records = mx_lookup.as_lookup().records();
40+ let mut records: Vec<MX> = Vec::with_capacity(mx_records.len());
41+ for mx_record in mx_records {
42+ if let Some(mx) = mx_record.data().and_then(|r| r.as_mx()) {
43+ let preference = mx.preference();
44+ let exchange = mx.exchange().to_lowercase().to_string();
45+
46+ if let Some(record) = records.iter_mut().find(|r| r.preference == preference) {
47+ record.exchanges.push(exchange);
48+ } else {
49+ records.push(MX {
50+ exchanges: vec![exchange],
51+ preference,
52+ });
53 }
54- .into()
55- })
56- .collect::<Vec<_>>();
57+ }
58+ }
59+
60 records.sort_unstable_by(|a, b| a.preference.cmp(&b.preference));
61
62 Ok(self
63 @@ -226,6 +232,17 @@ impl Resolver {
64 .insert(key.into_owned(), Arc::new(ips), ipv6_lookup.valid_until()))
65 }
66
67+ pub async fn ip_lookup(
68+ &self,
69+ key: impl IntoFqdn<'_>,
70+ ) -> crate::Result<impl Iterator<Item = IpAddr>> {
71+ self.resolver
72+ .lookup_ip(key.into_fqdn().as_ref())
73+ .await
74+ .map(|lookup| lookup.into_iter())
75+ .map_err(Error::from)
76+ }
77+
78 pub async fn ptr_lookup<'x>(&self, addr: IpAddr) -> crate::Result<Arc<Vec<String>>> {
79 if let Some(value) = self.cache_ptr.get(&addr) {
80 return Ok(value);
81 @@ -274,7 +291,7 @@ impl Resolver {
82 if matches!(err.kind(), ResolveErrorKind::NoRecordsFound { .. }) {
83 Ok(false)
84 } else {
85- Err(Error::DNSError)
86+ Err(err.into())
87 }
88 }
89 }
90 @@ -341,7 +358,7 @@ impl From<ResolveError> for Error {
91 ResolveErrorKind::NoRecordsFound { response_code, .. } => {
92 Error::DNSRecordNotFound(*response_code)
93 }
94- _ => Error::DNSError,
95+ _ => Error::DNSError(err.to_string()),
96 }
97 }
98 }
99 @@ -479,6 +496,16 @@ impl<'x> IntoFqdn<'x> for &'x str {
100 }
101 }
102
103+ impl<'x> IntoFqdn<'x> for &String {
104+ fn into_fqdn(self) -> Cow<'x, str> {
105+ if self.ends_with('.') {
106+ self.to_lowercase().into()
107+ } else {
108+ format!("{}.", self.to_lowercase()).into()
109+ }
110+ }
111+ }
112+
113 #[cfg(test)]
114 fn mock_resolve<T>(domain: &str) -> crate::Result<T> {
115 Err(if domain.contains("_parse_error.") {
116 @@ -486,7 +513,7 @@ fn mock_resolve<T>(domain: &str) -> crate::Result<T> {
117 } else if domain.contains("_invalid_record.") {
118 Error::InvalidRecordType
119 } else if domain.contains("_dns_error.") {
120- Error::DNSError
121+ Error::DNSError("".to_string())
122 } else {
123 Error::DNSRecordNotFound(trust_dns_resolver::proto::op::ResponseCode::NXDomain)
124 })
125 diff --git a/src/dkim/mod.rs b/src/dkim/mod.rs
126index 51cfd40..5708238 100644
127--- a/src/dkim/mod.rs
128+++ b/src/dkim/mod.rs
129 @@ -197,7 +197,7 @@ impl<'x> DkimOutput<'x> {
130 }
131
132 pub(crate) fn dns_error(err: Error) -> Self {
133- if matches!(&err, Error::DNSError) {
134+ if matches!(&err, Error::DNSError(_)) {
135 DkimOutput::temp_err(err)
136 } else {
137 DkimOutput::perm_err(err)
138 @@ -239,7 +239,7 @@ impl<'x> ArcOutput<'x> {
139
140 impl From<Error> for DkimResult {
141 fn from(err: Error) -> Self {
142- if matches!(&err, Error::DNSError) {
143+ if matches!(&err, Error::DNSError(_)) {
144 DkimResult::TempError(err)
145 } else {
146 DkimResult::PermError(err)
147 diff --git a/src/dkim/verify.rs b/src/dkim/verify.rs
148index 2cb335c..cb02c32 100644
149--- a/src/dkim/verify.rs
150+++ b/src/dkim/verify.rs
151 @@ -219,7 +219,7 @@ impl Resolver {
152 | Error::UnsupportedKeyType
153 | Error::IncompatibleAlgorithms => (record.rr & RR_SIGNATURE) != 0,
154 Error::SignatureExpired => (record.rr & RR_EXPIRATION) != 0,
155- Error::DNSError
156+ Error::DNSError(_)
157 | Error::DNSRecordNotFound(_)
158 | Error::InvalidRecordType
159 | Error::ParseError
160 diff --git a/src/dmarc/mod.rs b/src/dmarc/mod.rs
161index 733ccc7..3a80ce5 100644
162--- a/src/dmarc/mod.rs
163+++ b/src/dmarc/mod.rs
164 @@ -94,7 +94,7 @@ impl URI {
165
166 impl From<Error> for DmarcResult {
167 fn from(err: Error) -> Self {
168- if matches!(&err, Error::DNSError) {
169+ if matches!(&err, Error::DNSError(_)) {
170 DmarcResult::TempError(err)
171 } else {
172 DmarcResult::PermError(err)
173 diff --git a/src/dmarc/verify.rs b/src/dmarc/verify.rs
174index c29d34c..6d3b35b 100644
175--- a/src/dmarc/verify.rs
176+++ b/src/dmarc/verify.rs
177 @@ -137,7 +137,7 @@ impl Resolver {
178 .await
179 {
180 Ok(_) => true,
181- Err(Error::DNSError) => return None,
182+ Err(Error::DNSError(_)) => return None,
183 _ => false,
184 }
185 {
186 diff --git a/src/lib.rs b/src/lib.rs
187index 1025318..878e878 100644
188--- a/src/lib.rs
189+++ b/src/lib.rs
190 @@ -301,8 +301,8 @@ pub(crate) enum Txt {
191
192 #[derive(Debug, Clone, PartialEq, Eq)]
193 pub struct MX {
194- exchange: String,
195- preference: u16,
196+ pub exchanges: Vec<String>,
197+ pub preference: u16,
198 }
199
200 #[derive(Debug, Clone)]
201 @@ -415,7 +415,7 @@ pub enum Error {
202 IncompatibleAlgorithms,
203 SignatureExpired,
204
205- DNSError,
206+ DNSError(String),
207 DNSRecordNotFound(ResponseCode),
208
209 ARCChainTooLong,
210 @@ -467,7 +467,7 @@ impl Display for Error {
211 Error::ARCBrokenChain => write!(f, "Broken or missing ARC chain"),
212 Error::ARCChainTooLong => write!(f, "Too many ARC headers"),
213 Error::InvalidRecordType => write!(f, "Invalid record"),
214- Error::DNSError => write!(f, "DNS resolution error"),
215+ Error::DNSError(err) => write!(f, "DNS resolution error: {}", err),
216 Error::DNSRecordNotFound(code) => write!(f, "DNS record not found: {}", code),
217 Error::DMARCNotAligned => write!(f, "DMARC policy not aligned"),
218 }
219 diff --git a/src/spf/verify.rs b/src/spf/verify.rs
220index 6da76ed..a409a5a 100644
221--- a/src/spf/verify.rs
222+++ b/src/spf/verify.rs
223 @@ -181,16 +181,14 @@ impl Resolver {
224 .await
225 {
226 Ok(records) => {
227- for record in records.iter() {
228+ for exchange in records.iter().flat_map(|mx| mx.exchanges.iter()) {
229 if !lookup_limit.can_lookup() {
230 return output
231 .with_result(SpfResult::PermError)
232 .with_report(&spf_record);
233 }
234
235- match self
236- .ip_matches(&record.exchange, ip, *ip4_mask, *ip6_mask)
237- .await
238+ match self.ip_matches(exchange, ip, *ip4_mask, *ip6_mask).await
239 {
240 Ok(true) => {
241 matches = true;
242 @@ -385,12 +383,12 @@ impl Resolver {
243 ) -> crate::Result<bool> {
244 Ok(match ip {
245 IpAddr::V4(ip) => self
246- .ipv4_lookup(target_name.as_ref())
247+ .ipv4_lookup(target_name)
248 .await?
249 .iter()
250 .any(|addr| ip.matches_ipv4_mask(addr, ip4_mask)),
251 IpAddr::V6(ip) => self
252- .ipv6_lookup(target_name.as_ref())
253+ .ipv6_lookup(target_name)
254 .await?
255 .iter()
256 .any(|addr| ip.matches_ipv6_mask(addr, ip6_mask)),
257 @@ -620,7 +618,7 @@ mod test {
258 }
259 }
260 mxs.push(MX {
261- exchange: mx_name,
262+ exchanges: vec![mx_name],
263 preference: (pos + 1) as u16,
264 });
265 }