mirror of
https://github.com/mailscope/kumomta.git
synced 2026-09-08 19:48:57 +00:00
spf: add basic support for mx mechanism
This commit is contained in:
committed by
Wez Furlong
parent
e334084097
commit
a9e85d735f
@@ -1,11 +1,12 @@
|
||||
use crate::error::SpfError;
|
||||
use futures::future::BoxFuture;
|
||||
use hickory_resolver::TokioAsyncResolver;
|
||||
use hickory_resolver::{Name, TokioAsyncResolver};
|
||||
use std::net::IpAddr;
|
||||
|
||||
/// A trait for entities that perform DNS resolution.
|
||||
pub trait Lookup: Sync + Send {
|
||||
fn lookup_ip<'a>(&'a self, name: &'a str) -> BoxFuture<'a, Result<Vec<IpAddr>, SpfError>>;
|
||||
fn lookup_mx<'a>(&'a self, name: &'a str) -> BoxFuture<'a, Result<Vec<Name>, SpfError>>;
|
||||
fn lookup_txt<'a>(&'a self, name: &'a str) -> BoxFuture<'a, Result<Vec<String>, SpfError>>;
|
||||
}
|
||||
|
||||
@@ -21,6 +22,17 @@ impl Lookup for TokioAsyncResolver {
|
||||
})
|
||||
}
|
||||
|
||||
fn lookup_mx<'a>(&'a self, name: &'a str) -> BoxFuture<'a, Result<Vec<Name>, SpfError>> {
|
||||
Box::pin(async move {
|
||||
self.mx_lookup(name)
|
||||
.await
|
||||
.map_err(|err| SpfError::from_resolve(name, err))?
|
||||
.into_iter()
|
||||
.map(|mx| Ok(mx.exchange().clone()))
|
||||
.collect()
|
||||
})
|
||||
}
|
||||
|
||||
fn lookup_txt<'a>(&'a self, name: &'a str) -> BoxFuture<'a, Result<Vec<String>, SpfError>> {
|
||||
Box::pin(async move {
|
||||
self.txt_lookup(name)
|
||||
|
||||
@@ -106,6 +106,41 @@ impl Directive {
|
||||
.iter()
|
||||
.any(|&resolved_ip| cidr_len.matches(cx.client_ip, resolved_ip))
|
||||
}
|
||||
Mechanism::Mx { domain, cidr_len } => {
|
||||
let domain = cx.domain(domain)?;
|
||||
let exchanges = match resolver.lookup_mx(&domain).await {
|
||||
Ok(exchanges) => exchanges,
|
||||
Err(err) => {
|
||||
return Err(SpfResult {
|
||||
disposition: SpfDisposition::TempError,
|
||||
context: format!("error looking up IP for {domain}: {err}"),
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
let mut matched = false;
|
||||
for exchange in exchanges {
|
||||
let resolved = match resolver.lookup_ip(&exchange.to_string()).await {
|
||||
Ok(ips) => ips,
|
||||
Err(err) => {
|
||||
return Err(SpfResult {
|
||||
disposition: SpfDisposition::TempError,
|
||||
context: format!("error looking up IP for {exchange}: {err}"),
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
if resolved
|
||||
.iter()
|
||||
.any(|&resolved_ip| cidr_len.matches(cx.client_ip, resolved_ip))
|
||||
{
|
||||
matched = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
matched
|
||||
}
|
||||
_ => todo!("evaluate directive {self:?}"),
|
||||
};
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ use crate::dns::Lookup;
|
||||
use crate::error::SpfError;
|
||||
use crate::{CheckHostParams, SpfDisposition, SpfResult};
|
||||
use futures::future::BoxFuture;
|
||||
use hickory_proto::rr::rdata::{A, AAAA, TXT};
|
||||
use hickory_proto::rr::rdata::{A, AAAA, MX, TXT};
|
||||
use hickory_proto::rr::{LowerName, RData, RecordData, RecordSet, RecordType, RrKey};
|
||||
use hickory_proto::serialize::txt::Parser;
|
||||
use hickory_resolver::Name;
|
||||
@@ -99,6 +99,95 @@ async fn ip() {
|
||||
);
|
||||
}
|
||||
|
||||
/// https://www.rfc-editor.org/rfc/rfc7208#appendix-A.1
|
||||
#[tokio::test]
|
||||
async fn mx() {
|
||||
let resolver = TestResolver::default()
|
||||
.with_zone(EXAMPLE_COM)
|
||||
.with_spf("example.com", "v=spf1 mx -all".to_string());
|
||||
|
||||
let result = CheckHostParams {
|
||||
client_ip: IpAddr::V4(Ipv4Addr::from([192, 0, 2, 129])),
|
||||
domain: "example.com".to_string(),
|
||||
sender: "sender@example.com".to_string(),
|
||||
}
|
||||
.run(&resolver)
|
||||
.await;
|
||||
|
||||
k9::assert_equal!(
|
||||
&result,
|
||||
&SpfResult {
|
||||
disposition: SpfDisposition::Pass,
|
||||
context: "matched 'mx' directive".to_owned(),
|
||||
},
|
||||
"{result:?}"
|
||||
);
|
||||
|
||||
let resolver = TestResolver::default()
|
||||
.with_zone(EXAMPLE_COM)
|
||||
.with_zone(EXAMPLE_ORG)
|
||||
.with_spf("example.com", "v=spf1 mx:example.org -all".to_string());
|
||||
|
||||
let result = CheckHostParams {
|
||||
client_ip: IpAddr::V4(Ipv4Addr::from([192, 0, 2, 140])),
|
||||
domain: "example.com".to_string(),
|
||||
sender: "sender@example.com".to_string(),
|
||||
}
|
||||
.run(&resolver)
|
||||
.await;
|
||||
|
||||
k9::assert_equal!(
|
||||
&result,
|
||||
&SpfResult {
|
||||
disposition: SpfDisposition::Pass,
|
||||
context: "matched 'mx:example.org' directive".to_owned(),
|
||||
},
|
||||
"{result:?}"
|
||||
);
|
||||
|
||||
let resolver = TestResolver::default()
|
||||
.with_zone(EXAMPLE_COM)
|
||||
.with_zone(EXAMPLE_ORG)
|
||||
.with_spf(
|
||||
"example.com",
|
||||
"v=spf1 mx/30 mx:example.org/30 -all".to_string(),
|
||||
);
|
||||
|
||||
let result = CheckHostParams {
|
||||
client_ip: IpAddr::V4(Ipv4Addr::from([192, 0, 2, 131])),
|
||||
domain: "example.com".to_string(),
|
||||
sender: "sender@example.com".to_string(),
|
||||
}
|
||||
.run(&resolver)
|
||||
.await;
|
||||
|
||||
k9::assert_equal!(
|
||||
&result,
|
||||
&SpfResult {
|
||||
disposition: SpfDisposition::Pass,
|
||||
context: "matched 'mx/30' directive".to_owned(),
|
||||
},
|
||||
"{result:?}"
|
||||
);
|
||||
|
||||
let result = CheckHostParams {
|
||||
client_ip: IpAddr::V4(Ipv4Addr::from([192, 0, 2, 141])),
|
||||
domain: "example.com".to_string(),
|
||||
sender: "sender@example.com".to_string(),
|
||||
}
|
||||
.run(&resolver)
|
||||
.await;
|
||||
|
||||
k9::assert_equal!(
|
||||
&result,
|
||||
&SpfResult {
|
||||
disposition: SpfDisposition::Pass,
|
||||
context: "matched 'mx:example.org/30' directive".to_owned(),
|
||||
},
|
||||
"{result:?}"
|
||||
);
|
||||
}
|
||||
|
||||
/// https://www.rfc-editor.org/rfc/rfc7208#appendix-A
|
||||
const EXAMPLE_COM: &str = r#"; A domain with two mail servers, two hosts, and two servers
|
||||
; at the domain name
|
||||
@@ -206,6 +295,26 @@ impl Lookup for TestResolver {
|
||||
})
|
||||
}
|
||||
|
||||
fn lookup_mx<'a>(&'a self, full: &'a str) -> BoxFuture<'a, Result<Vec<Name>, SpfError>> {
|
||||
Box::pin(async move {
|
||||
let records = match self.get(full, RecordType::MX)? {
|
||||
Some(records) => records,
|
||||
None => {
|
||||
println!("key not found: {full}");
|
||||
return Err(SpfError::DnsRecordNotFound(full.to_string()));
|
||||
}
|
||||
};
|
||||
|
||||
let mut values = vec![];
|
||||
for record in records.records_without_rrsigs() {
|
||||
let mx = MX::try_borrow(record.data().unwrap()).unwrap();
|
||||
values.push(mx.exchange().clone());
|
||||
}
|
||||
|
||||
Ok(values)
|
||||
})
|
||||
}
|
||||
|
||||
fn lookup_txt<'a>(&'a self, full: &'a str) -> BoxFuture<'a, Result<Vec<String>, SpfError>> {
|
||||
Box::pin(async move {
|
||||
let records = match self.get(full, RecordType::TXT)? {
|
||||
|
||||
Reference in New Issue
Block a user