Skip to content
This repository was archived by the owner on Sep 2, 2025. It is now read-only.

Commit 3efbced

Browse files
authored
[BED-92] Add PTR support (#17)
Valid check host implementation should support PTR, even though it is not recommended to use.
2 parents eb77948 + 8804796 commit 3efbced

File tree

9 files changed

+165
-5
lines changed

9 files changed

+165
-5
lines changed

parser.go

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,7 @@ func (p *parser) check() (Result, string, unused, error) {
249249
case tExists:
250250
matches, result, ttl, err = p.parseExists(token)
251251
case tPTR:
252-
_, _, _ = p.parsePtr(token)
252+
matches, result, err = p.parsePtr(token)
253253
default:
254254
p.fireDirective(token, "")
255255
}
@@ -600,9 +600,56 @@ func (p *parser) parseExists(t *token) (bool, Result, time.Duration, error) {
600600
}
601601
}
602602

603+
// https://www.rfc-editor.org/rfc/rfc7208#section-5.5
603604
func (p *parser) parsePtr(t *token) (bool, Result, error) {
604-
p.fireDirective(t, domainSpec(t.value, p.domain))
605-
return false, internalError, nil
605+
fqdn := domainSpec(t.value, p.domain)
606+
fqdn, err := parseMacro(p, fqdn, false)
607+
if err == nil {
608+
fqdn, err = truncateFQDN(fqdn)
609+
}
610+
if err == nil && !isDomainName(fqdn) {
611+
err = newInvalidDomainError(fqdn)
612+
}
613+
fqdn = NormalizeFQDN(fqdn)
614+
p.fireDirective(t, fqdn)
615+
if err != nil {
616+
return true, Permerror, SyntaxError{t, err}
617+
}
618+
619+
ptrs, _, err := p.resolver.LookupPTR(p.ip.String())
620+
switch err {
621+
case nil:
622+
// continue
623+
case ErrDNSLimitExceeded:
624+
return false, Permerror, err
625+
case ErrDNSPermerror:
626+
return false, None, err
627+
default:
628+
return false, Temperror, err
629+
}
630+
631+
result, _ := matchingResult(t.qualifier)
632+
633+
for _, ptrDomain := range ptrs {
634+
found, _, err := p.resolver.MatchIP(ptrDomain, func(ip net.IP, host string) (bool, error) {
635+
if ip.Equal(p.ip) {
636+
// Check if the PTR domain matches the target name or is a subdomain of the target name
637+
if strings.HasSuffix(ptrDomain, fqdn) || fqdn == ptrDomain {
638+
return true, nil // Match found
639+
}
640+
}
641+
return false, nil
642+
})
643+
if err != nil {
644+
continue
645+
}
646+
647+
if found {
648+
return true, result, nil
649+
}
650+
}
651+
652+
return false, Fail, nil
606653
}
607654

608655
func (p *parser) handleRedirect(t *token) (Result, error) {

printer/printer.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,13 @@ func (p *Printer) LookupTXTStrict(name string) ([]string, time.Duration, error)
9393
return p.r.LookupTXTStrict(name)
9494
}
9595

96+
func (p *Printer) LookupPTR(name string) ([]string, time.Duration, error) {
97+
fmt.Fprintf(p.w, "%s lookup(PTR) %s\n", strings.Repeat(" ", p.c), name)
98+
atomic.AddInt64(&p.lc, 1)
99+
p.lc++
100+
return p.r.LookupPTR(name)
101+
}
102+
96103
func (p *Printer) Exists(name string) (bool, time.Duration, error) {
97104
fmt.Fprintf(p.w, "%s lookup(A)\n", strings.Repeat(" ", p.c))
98105
atomic.AddInt64(&p.lc, 1)

printer/printer_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,9 +139,10 @@ func ExamplePrinter() {
139139
// SPF: v=spf1 ptr ~all
140140
// v=spf1
141141
// ptr (ptr.test.redsift.io.)
142+
// lookup(PTR) 0.0.0.0
142143
// ~all
143144
// = softfail, "4m59s", , <nil>
144-
// ## of lookups: 13
145+
// ## of lookups: 15
145146
}
146147

147148
// b, _ := spf.CacheDump(c.GetALL(false)).MarshalJSON()

resolver_limited.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,3 +99,12 @@ func (r *LimitedResolver) MatchMX(name string, matcher IPMatcherFunc) (bool, tim
9999
return matcher(ip, name)
100100
})
101101
}
102+
103+
// LookupPTR returns the DNS PTR records for the given domain name
104+
// and the minimum TTL
105+
func (r *LimitedResolver) LookupPTR(name string) ([]string, time.Duration, error) {
106+
if !r.canLookup() {
107+
return nil, 0, ErrDNSLimitExceeded
108+
}
109+
return r.resolver.LookupPTR(name)
110+
}

resolver_miekg.go

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
package spf
22

33
import (
4-
"github.com/redsift/spf/v2/z"
54
"net"
5+
"strconv"
66
"strings"
77
"sync"
88
"time"
99

1010
"github.com/miekg/dns"
11+
"github.com/redsift/spf/v2/z"
1112
)
1213

1314
type MiekgDNSResolverOption func(r *miekgDNSResolver)
@@ -391,3 +392,66 @@ func (r *miekgDNSResolver) MatchMX(name string, matcher IPMatcherFunc) (bool, ti
391392

392393
return false, 0, nil
393394
}
395+
396+
// LookupPTR returns the DNS PTR records for the given IP and
397+
// the minimum TTL
398+
func (r *miekgDNSResolver) LookupPTR(name string) ([]string, time.Duration, error) {
399+
req := new(dns.Msg)
400+
req.SetQuestion(NewPTRAddress(name), dns.TypePTR)
401+
402+
res, err := r.exchange(req)
403+
if err != nil {
404+
return nil, 0, err
405+
}
406+
407+
var ttl uint32 = 1<<32 - 1
408+
409+
ptrs := make([]string, 0, len(res.Answer))
410+
for _, a := range res.Answer {
411+
if r, ok := a.(*dns.PTR); ok {
412+
ptrs = append(ptrs, r.Ptr)
413+
if d := a.Header().Ttl; d < ttl {
414+
ttl = d
415+
}
416+
}
417+
}
418+
419+
if len(ptrs) == 0 {
420+
ttl = 0
421+
}
422+
423+
return ptrs, time.Duration(ttl) * time.Second, nil
424+
}
425+
426+
func NewPTRAddress(address string) string {
427+
ip := net.ParseIP(address)
428+
if ip.To4() != nil {
429+
return ip4ToLookup(ip)
430+
} else {
431+
return ip6ToLookup(ip)
432+
}
433+
}
434+
435+
func ip4ToLookup(ip net.IP) string {
436+
return strconv.Itoa(int(ip[15])) +
437+
"." + strconv.Itoa(int(ip[14])) +
438+
"." + strconv.Itoa(int(ip[13])) +
439+
"." + strconv.Itoa(int(ip[12])) +
440+
"." + "in-addr.arpa."
441+
}
442+
443+
func ip6ToLookup(ip net.IP) string {
444+
// Must be IPv6
445+
buf := make([]byte, 0, len(ip)*4+9)
446+
// Add it, in reverse, to the buffer
447+
for i := len(ip) - 1; i >= 0; i-- {
448+
v := ip[i]
449+
buf = append(buf, hexDigit[v&0xF])
450+
buf = append(buf, '.')
451+
buf = append(buf, hexDigit[v>>4])
452+
buf = append(buf, '.')
453+
}
454+
// Append "ip6.arpa." and return (buf already has the final .)
455+
buf = append(buf, "ip6.arpa."...)
456+
return string(buf)
457+
}

resolver_retry.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,20 @@ func (r *retryResolver) LookupTXT(name string) ([]string, time.Duration, error)
9494
}
9595
}
9696

97+
// LookupPTR returns the DNS PTR records for the given address.
98+
func (r *retryResolver) LookupPTR(name string) ([]string, time.Duration, error) {
99+
expired := r.expiredFunc()
100+
for attempt := 0; ; attempt++ {
101+
for _, next := range r.rr {
102+
v, ttl, err := next.LookupPTR(name)
103+
if err != ErrDNSTemperror || expired() {
104+
return v, ttl, err
105+
}
106+
}
107+
time.Sleep(r.backoff(attempt))
108+
}
109+
}
110+
97111
// Exists is used for a DNS A RR lookup (even when the
98112
// connection type is IPv6). If any A record is returned, this
99113
// mechanism matches and returns the ttl.

resolver_retry_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@ func (r *brokenResolver) MatchMX(name string, matcher IPMatcherFunc) (bool, time
4343
return false, 0, r.error()
4444
}
4545

46+
func (r *brokenResolver) LookupPTR(name string) ([]string, time.Duration, error) {
47+
return nil, 0, r.error()
48+
}
49+
4650
func TestRetryResolver_Exists(t *testing.T) {
4751
lastErr := errors.New("last error")
4852

resolver_std.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,3 +143,14 @@ func (r *DNSResolver) MatchMX(name string, matcher IPMatcherFunc) (bool, time.Du
143143

144144
return false, 0, nil
145145
}
146+
147+
// LookupPTR returns the DNS PTR records for the given name and the TTL.
148+
func (r *DNSResolver) LookupPTR(name string) ([]string, time.Duration, error) {
149+
ptrs, err := net.LookupAddr(name)
150+
err = errDNS(err)
151+
if err != nil {
152+
return nil, 0, err
153+
}
154+
155+
return ptrs, 0, nil
156+
}

spf.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,9 @@ type Resolver interface {
8080
// Then IPMatcherFunc used to compare checked IP to the returned address(es).
8181
// If any address matches, the mechanism matches and returns the TTL.
8282
MatchMX(string, IPMatcherFunc) (bool, time.Duration, error)
83+
// LookupPTR returns the DNS PTR records for the given address and
84+
// the minimum TTL.
85+
LookupPTR(string) ([]string, time.Duration, error)
8386
}
8487

8588
// Option sets an optional parameter for the evaluating e-mail with regard to SPF

0 commit comments

Comments
 (0)