spf: add basic support for mx mechanism

This commit is contained in:
Dirkjan Ochtman
2024-10-12 08:00:15 -07:00
committed by Wez Furlong
parent e334084097
commit a9e85d735f
3 changed files with 158 additions and 2 deletions
+13 -1
View File
@@ -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)
+35
View File
@@ -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:?}"),
};
+110 -1
View File
@@ -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)? {