mirror of
https://github.com/mailscope/kumomta.git
synced 2026-10-26 08:16:13 +00:00
spf: apply dns lookup limits
Noticed that we weren't apply the limits specified by https://datatracker.ietf.org/doc/html/rfc7208#section-4.6.4
This commit is contained in:
@@ -7,6 +7,8 @@ use instant_xml::{FromXml, ToXml};
|
||||
use serde::{Serialize, Serializer};
|
||||
use std::fmt;
|
||||
use std::net::IpAddr;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::time::SystemTime;
|
||||
|
||||
pub mod record;
|
||||
@@ -160,6 +162,7 @@ struct SpfContext<'a> {
|
||||
pub(crate) now: SystemTime,
|
||||
pub(crate) ehlo_domain: Option<&'a str>,
|
||||
pub(crate) relaying_host_name: &'a str,
|
||||
lookups_remaining: Arc<AtomicUsize>,
|
||||
}
|
||||
|
||||
impl<'a> SpfContext<'a> {
|
||||
@@ -187,12 +190,14 @@ impl<'a> SpfContext<'a> {
|
||||
now: SystemTime::now(),
|
||||
ehlo_domain: None,
|
||||
relaying_host_name: "localhost",
|
||||
lookups_remaining: Arc::new(AtomicUsize::new(10)),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn with_ehlo_domain(&self, ehlo_domain: Option<&'a str>) -> Self {
|
||||
Self {
|
||||
ehlo_domain,
|
||||
lookups_remaining: self.lookups_remaining.clone(),
|
||||
..*self
|
||||
}
|
||||
}
|
||||
@@ -200,15 +205,39 @@ impl<'a> SpfContext<'a> {
|
||||
pub fn with_relaying_host_name(&self, relaying_host_name: Option<&'a str>) -> Self {
|
||||
Self {
|
||||
relaying_host_name: relaying_host_name.unwrap_or(self.relaying_host_name),
|
||||
lookups_remaining: self.lookups_remaining.clone(),
|
||||
..*self
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn with_domain(&self, domain: &'a str) -> Self {
|
||||
Self { domain, ..*self }
|
||||
Self {
|
||||
domain,
|
||||
lookups_remaining: self.lookups_remaining.clone(),
|
||||
..*self
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn check_lookup_limit(&self) -> Result<(), SpfResult> {
|
||||
let remain = self.lookups_remaining.load(Ordering::Relaxed);
|
||||
if remain > 0 {
|
||||
self.lookups_remaining.store(remain - 1, Ordering::Relaxed);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
Err(SpfResult {
|
||||
disposition: SpfDisposition::PermError,
|
||||
context: "DNS lookup limits exceeded".to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn check(&self, resolver: &dyn Resolver, initial: bool) -> SpfResult {
|
||||
if !initial {
|
||||
if let Err(err) = self.check_lookup_limit() {
|
||||
return err;
|
||||
}
|
||||
}
|
||||
|
||||
let name = match Name::from_str_relaxed(self.domain) {
|
||||
Ok(name) => name,
|
||||
Err(_) => {
|
||||
@@ -300,6 +329,9 @@ impl<'a> SpfContext<'a> {
|
||||
spec: Option<&MacroSpec>,
|
||||
resolver: &dyn Resolver,
|
||||
) -> Result<Option<String>, SpfResult> {
|
||||
// https://datatracker.ietf.org/doc/html/rfc7208#section-4.6.4
|
||||
self.check_lookup_limit()?;
|
||||
|
||||
let domain = self.domain(spec, resolver).await?;
|
||||
|
||||
let domain = match Name::from_str_relaxed(&domain) {
|
||||
@@ -322,7 +354,14 @@ impl<'a> SpfContext<'a> {
|
||||
}
|
||||
};
|
||||
|
||||
for ptr in ptrs.iter().filter(|ptr| domain.zone_of(ptr)) {
|
||||
for (idx, ptr) in ptrs.iter().filter(|ptr| domain.zone_of(ptr)).enumerate() {
|
||||
if idx >= 10 {
|
||||
// https://datatracker.ietf.org/doc/html/rfc7208#section-4.6.4
|
||||
return Err(SpfResult {
|
||||
disposition: SpfDisposition::PermError,
|
||||
context: format!("too many PTR records for {}", self.client_ip),
|
||||
});
|
||||
}
|
||||
match resolver.resolve_ip(&ptr.to_string()).await {
|
||||
Ok(ips) => {
|
||||
if ips.iter().any(|&ip| ip == self.client_ip) {
|
||||
|
||||
@@ -160,6 +160,7 @@ impl Directive {
|
||||
let matched = match &self.mechanism {
|
||||
Mechanism::All => true,
|
||||
Mechanism::A { domain, cidr_len } => {
|
||||
cx.check_lookup_limit()?;
|
||||
let domain = cx.domain(domain.as_ref(), resolver).await?;
|
||||
let resolved = match resolver.resolve_ip(&domain).await {
|
||||
Ok(ips) => ips,
|
||||
@@ -176,6 +177,7 @@ impl Directive {
|
||||
.any(|&resolved_ip| cidr_len.matches(cx.client_ip, resolved_ip))
|
||||
}
|
||||
Mechanism::Mx { domain, cidr_len } => {
|
||||
cx.check_lookup_limit()?;
|
||||
let domain = cx.domain(domain.as_ref(), resolver).await?;
|
||||
let exchanges = match resolver.resolve_mx(&domain).await {
|
||||
Ok(exchanges) => exchanges,
|
||||
@@ -188,7 +190,15 @@ impl Directive {
|
||||
};
|
||||
|
||||
let mut matched = false;
|
||||
for exchange in exchanges {
|
||||
for (idx, exchange) in exchanges.into_iter().enumerate() {
|
||||
if idx >= 10 {
|
||||
// https://datatracker.ietf.org/doc/html/rfc7208#section-4.6.4
|
||||
return Err(SpfResult {
|
||||
disposition: SpfDisposition::PermError,
|
||||
context: format!("too many MX records for {domain}"),
|
||||
});
|
||||
}
|
||||
|
||||
let resolved = match resolver.resolve_ip(&exchange.to_string()).await {
|
||||
Ok(ips) => ips,
|
||||
Err(err) => {
|
||||
@@ -227,6 +237,7 @@ impl Directive {
|
||||
}
|
||||
.matches(cx.client_ip, IpAddr::V6(*ip6_network)),
|
||||
Mechanism::Ptr { domain } => {
|
||||
// Note that validated_domain applies lookup limits
|
||||
let validated_domain = cx.validated_domain(domain.as_ref(), resolver).await?;
|
||||
|
||||
validated_domain.is_some()
|
||||
@@ -266,6 +277,7 @@ impl Directive {
|
||||
}
|
||||
}
|
||||
Mechanism::Exists { domain } => {
|
||||
cx.check_lookup_limit()?;
|
||||
let domain = cx.domain(Some(domain), resolver).await?;
|
||||
match resolver.resolve_ip(&domain).await {
|
||||
Ok(ips) => ips.iter().any(|ip| ip.is_ipv4()),
|
||||
|
||||
@@ -395,6 +395,29 @@ async fn ptr() {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn lookup_limits() {
|
||||
let mut resolver = TestResolver::default().with_zone(EXAMPLE_COM);
|
||||
for i in 1..=15 {
|
||||
resolver = resolver.with_txt(
|
||||
&format!("inc{i}.com"),
|
||||
format!("v=spf1 redirect=inc{}.com", i + 1),
|
||||
);
|
||||
}
|
||||
|
||||
let resolver = resolver.with_txt("example.com", "v=spf1 redirect=inc1.com".to_string());
|
||||
|
||||
let result = evaluate_ip(Ipv4Addr::from([192, 0, 2, 65]), &resolver).await;
|
||||
k9::assert_equal!(
|
||||
&result,
|
||||
&SpfResult {
|
||||
disposition: SpfDisposition::PermError,
|
||||
context: "DNS lookup limits exceeded".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
|
||||
|
||||
Reference in New Issue
Block a user