git » spf » commit e17a976

ptr: Make reverse DNS lookup errors fail to match

author Alberto Bertogli
2026-08-23 12:02:26 UTC
committer Alberto Bertogli
2026-08-23 12:02:26 UTC
parent d2b814bc9cdd8cb89726a72e8fc3ee8fff7124be

ptr: Make reverse DNS lookup errors fail to match

Currently, when processing a ptr mechanism, if the reverse DNS lookup
fails (with other than NXDOMAIN), then it returns temperror.

However, the standard is clear that we should just fail to match
instead:

> Check all validated domain names to see if they either match the
> <target-name> domain or are a subdomain of the <target-name> domain.
> If any do, this mechanism matches.  If no validated domain name can
> be found, or if none of the validated domain names match or are a
> subdomain of the <target-name>, this mechanism fails to match.  If a
> DNS error occurs while doing the PTR RR lookup, then this mechanism
> fails to match.  If a DNS error occurs while doing an A RR lookup,
> then that domain name is skipped and the search continues.

https://tools.ietf.org/html/rfc7208#section-5.5

This patch fixes this by making those lookup errors fail to match as
described by the standard.

spf.go +8 -5
spf_test.go +13 -3
testdata/blitirispf-tests.yml +0 -1

diff --git a/spf.go b/spf.go
index 9c302f8..ec83ea4 100644
--- a/spf.go
+++ b/spf.go
@@ -639,11 +639,14 @@ func (r *resolution) ptrField(res Result, field, domain string) (bool, Result, e
 			return true, PermError, verr
 		}
 		if err != nil {
-			// https://tools.ietf.org/html/rfc7208#section-5
-			if isNotFound(err) {
-				return false, "", err
-			}
-			return true, TempError, err
+			// Any DNS error here makes the mechanism fail to match, instead
+			// of the temperror that section 5 prescribes for the other
+			// mechanisms. The reverse zone is controlled by the operator of
+			// the connecting IP, not by the domain publishing the record, so
+			// errors in it must not affect the result.
+			// https://tools.ietf.org/html/rfc7208#section-5.5
+			r.trace("ptr reverse lookup error, no match: %v", err)
+			return false, "", err
 		}
 
 		// Only take the first 10 names, ignore the rest.
diff --git a/spf_test.go b/spf_test.go
index 552110c..bc8a21d 100644
--- a/spf_test.go
+++ b/spf_test.go
@@ -424,8 +424,13 @@ func TestDNSTemporaryErrors(t *testing.T) {
 		{"v=spf1 include:tmperr", TempError},
 		{"v=spf1 a:tmperr", TempError},
 		{"v=spf1 mx:tmperr", TempError},
-		{"v=spf1 ptr:tmperr", TempError},
 		{"v=spf1 mx:tmpmx", TempError},
+
+		// "ptr" is the exception: a DNS error on the reverse lookup makes
+		// the mechanism fail to match, so we fall through to the end of the
+		// record and get neutral.
+		// https://tools.ietf.org/html/rfc7208#section-5.5
+		{"v=spf1 ptr:tmperr", Neutral},
 	}
 
 	for _, c := range cases {
@@ -460,11 +465,16 @@ func TestDNSPermanentErrors(t *testing.T) {
 
 		// RFC specifies that on any DNS error (other than NXDOMAIN),
 		// we must return TempError.
-		// https://www.rfc-editor.org/rfc/rfc7208#section-5
+		// https://tools.ietf.org/html/rfc7208#section-5
 		{"v=spf1 a:permerr", TempError},
 		{"v=spf1 mx:permerr", TempError},
-		{"v=spf1 ptr:permerr", TempError},
 		{"v=spf1 mx:permmx", TempError},
+
+		// "ptr" is the exception: a DNS error on the reverse lookup makes
+		// the mechanism fail to match, so we fall through to the end of the
+		// record and get neutral.
+		// https://tools.ietf.org/html/rfc7208#section-5.5
+		{"v=spf1 ptr:permerr", Neutral},
 	}
 
 	for _, c := range cases {
diff --git a/testdata/blitirispf-tests.yml b/testdata/blitirispf-tests.yml
index e40196f..f68e7c3 100644
--- a/testdata/blitirispf-tests.yml
+++ b/testdata/blitirispf-tests.yml
@@ -1079,7 +1079,6 @@ tests:
     mailfrom: "foo@ptrerr.net"
     host: 1.2.3.4
     result: softfail
-    skip: We return temperror instead of treating it as a no-match.
 zonedata:
   ptrerr.net:
     - SPF: v=spf1 ptr ~all