| author | Alberto Bertogli
<albertito@blitiri.com.ar> 2026-08-17 21:08:40 UTC |
| committer | Alberto Bertogli
<albertito@blitiri.com.ar> 2026-08-22 08:58:13 UTC |
| parent | 7a83916d6b64d8ef0baf3f2b12432b3eed594f49 |
| spf.go | +10 | -6 |
| spf_test.go | +5 | -1 |
| testdata/blitirispf-tests.yml | +51 | -0 |
diff --git a/spf.go b/spf.go index bc73e36..a8adf06 100644 --- a/spf.go +++ b/spf.go @@ -614,18 +614,22 @@ func (r *resolution) ptrField(res Result, field, domain string) (bool, Result, e } for _, n := range ns { - // Validate the record by doing a forward resolution: it has to - // have some A/AAAA. + // Validate the name by doing a forward resolution: it is only + // validated if it resolves back to the IP we are checking. + // https://tools.ietf.org/html/rfc7208#section-5.5 addrs, err := r.resolver.LookupIPAddr(r.ctx, n) if err != nil { // RFC explicitly says to skip domains which error here. continue } r.trace("ptr forward resolution %q -> %q", n, addrs) - if len(addrs) > 0 { - // Append the lower-case variants so we do a case-insensitive - // lookup below. - r.ipNames = append(r.ipNames, strings.ToLower(n)) + for _, addr := range addrs { + if addr.IP.Equal(r.ip) { + // Append the lower-case variants so we do a + // case-insensitive lookup below. + r.ipNames = append(r.ipNames, strings.ToLower(n)) + break + } } } } diff --git a/spf_test.go b/spf_test.go index 5ba1623..9cc523c 100644 --- a/spf_test.go +++ b/spf_test.go @@ -125,7 +125,11 @@ func TestIPv6(t *testing.T) { {"v=spf1 ip6:2001:db8::68 ~all", Pass, ErrMatchedIP}, {"v=spf1 ip6:2001:db8::1/24 ~all", Pass, ErrMatchedIP}, {"v=spf1 ip6:2001:db8::1/100 ~all", Pass, ErrMatchedIP}, - {"v=spf1 ptr -all", Pass, ErrMatchedPTR}, + // "domain" is one of the PTR names for our IP, but it resolves to + // ip1111 and not to the IP we are checking, so it is not validated + // and the implicit "ptr" does not match. + // https://tools.ietf.org/html/rfc7208#section-5.5 + {"v=spf1 ptr -all", Fail, ErrMatchedAll}, {"v=spf1 ptr:d6666 -all", Pass, ErrMatchedPTR}, {"v=spf1 ptr:sonlas6 -all", Pass, ErrMatchedPTR}, {"v=spf1 ptr:sonlas7 -all", Fail, ErrMatchedAll}, diff --git a/testdata/blitirispf-tests.yml b/testdata/blitirispf-tests.yml index 914e353..0e14493 100644 --- a/testdata/blitirispf-tests.yml +++ b/testdata/blitirispf-tests.yml @@ -513,3 +513,54 @@ zonedata: - SPF: v=spf1 redirect=doesnotexist sender: - A: 1.2.3.4 +--- +description: PTR forward confirmation +tests: + ptr-forward-confirmed: + description: | + A PTR name that resolves back to the IP being checked is validated, and + matches. + mailfrom: "foo@good.com" + host: 1.2.3.4 + result: pass + ptr-resolves-elsewhere: + description: | + A PTR name that resolves, but to a different address, must NOT be + validated: RFC 7208 section 5.5 requires <ip> to be among the addresses + returned for the name, not just that the name resolves to something. + mailfrom: "foo@bad.com" + host: 1.2.3.4 + result: fail + ptr-resolves-elsewhere-v6: + description: | + As above, but the name resolves to an address of a different family. + mailfrom: "foo@badv6.com" + host: 1.2.3.4 + result: fail + ptr-one-of-many: + description: | + A name with several addresses is validated if any one of them is the IP + being checked. + mailfrom: "foo@many.com" + host: 1.2.3.4 + result: pass +zonedata: + good.com: + - SPF: v=spf1 ptr:good.com -all + - A: 1.2.3.4 + bad.com: + - SPF: v=spf1 ptr:bad.com -all + - A: 9.9.9.9 + badv6.com: + - SPF: v=spf1 ptr:badv6.com -all + - AAAA: 2001:db8::1 + many.com: + - SPF: v=spf1 ptr:many.com -all + - A: 10.0.0.1 + - A: 1.2.3.4 + - A: 10.0.0.2 + 4.3.2.1.in-addr.arpa: + - PTR: good.com + - PTR: bad.com + - PTR: badv6.com + - PTR: many.com