From 8dfd591dd443ca3e0cfeb5e531a6f74b5a0dae82 Mon Sep 17 00:00:00 2001 From: Wez Furlong Date: Thu, 9 Oct 2025 12:22:59 +0100 Subject: [PATCH] spf: ensure that all names are fqdn when resolving No spf record should use the set of search domains in a local resolver; all names are implicitly FQDN. Let's explicitly mark up the names that way so that we can avoid both potentially matching a surprising local record and also excess DNS lookups for records that might not exist. --- crates/kumo-spf/src/lib.rs | 14 ++++++++++++-- crates/kumo-spf/src/record.rs | 15 +++++++++------ 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/crates/kumo-spf/src/lib.rs b/crates/kumo-spf/src/lib.rs index 029f83bf..21fab1a2 100644 --- a/crates/kumo-spf/src/lib.rs +++ b/crates/kumo-spf/src/lib.rs @@ -239,7 +239,10 @@ impl<'a> SpfContext<'a> { } let name = match Name::from_str_relaxed(self.domain) { - Ok(name) => name, + Ok(mut name) => { + name.set_fqdn(true); + name + } Err(_) => { // Per , invalid // domain names yield a "none" result during initial processing. @@ -362,7 +365,7 @@ impl<'a> SpfContext<'a> { context: format!("too many PTR records for {}", self.client_ip), }); } - match resolver.resolve_ip(&ptr.to_string()).await { + match resolver.resolve_ip(&fully_qualify(&ptr.to_string())).await { Ok(ips) => { if ips.iter().any(|&ip| ip == self.client_ip) { let mut ptr = ptr.clone(); @@ -383,3 +386,10 @@ impl<'a> SpfContext<'a> { Ok(None) } } + +pub(crate) fn fully_qualify(domain_name: &str) -> String { + match dns_resolver::fully_qualify(domain_name) { + Ok(name) => name.to_string(), + Err(_) => domain_name.to_string(), + } +} diff --git a/crates/kumo-spf/src/record.rs b/crates/kumo-spf/src/record.rs index 4f43b335..5ded0650 100644 --- a/crates/kumo-spf/src/record.rs +++ b/crates/kumo-spf/src/record.rs @@ -1,5 +1,5 @@ use crate::spec::MacroSpec; -use crate::{SpfContext, SpfDisposition, SpfResult}; +use crate::{fully_qualify, SpfContext, SpfDisposition, SpfResult}; use dns_resolver::Resolver; use std::fmt; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; @@ -112,7 +112,7 @@ impl Record { // if no records are returned, or if more than one record is returned, // or if there are syntax errors in the explanation string, then proceed // as if no "exp" modifier was given." - let explanation = match resolver.resolve_txt(&domain).await { + let explanation = match resolver.resolve_txt(&fully_qualify(&domain)).await { Ok(answers) if answers.records.len() == 1 => answers.as_txt().pop().unwrap(), Ok(_) | Err(_) => return SpfResult::fail(failed), }; @@ -162,7 +162,7 @@ impl Directive { 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 { + let resolved = match resolver.resolve_ip(&fully_qualify(&domain)).await { Ok(ips) => ips, Err(err) => { return Err(SpfResult { @@ -179,7 +179,7 @@ impl Directive { 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 { + let exchanges = match resolver.resolve_mx(&fully_qualify(&domain)).await { Ok(exchanges) => exchanges, Err(err) => { return Err(SpfResult { @@ -199,7 +199,10 @@ impl Directive { }); } - let resolved = match resolver.resolve_ip(&exchange.to_string()).await { + let resolved = match resolver + .resolve_ip(&fully_qualify(&exchange.to_string())) + .await + { Ok(ips) => ips, Err(err) => { return Err(SpfResult { @@ -279,7 +282,7 @@ impl Directive { Mechanism::Exists { domain } => { cx.check_lookup_limit()?; let domain = cx.domain(Some(domain), resolver).await?; - match resolver.resolve_ip(&domain).await { + match resolver.resolve_ip(&fully_qualify(&domain)).await { Ok(ips) => ips.iter().any(|ip| ip.is_ipv4()), Err(err) => { return Err(SpfResult {