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.
This commit is contained in:
Wez Furlong
2025-10-09 12:22:59 +01:00
parent 24602cff44
commit 8dfd591dd4
2 changed files with 21 additions and 8 deletions
+12 -2
View File
@@ -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 <https://www.rfc-editor.org/rfc/rfc7208#section-4.3>, 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(),
}
}
+9 -6
View File
@@ -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 {