Skip to content

Commit

Permalink
BED-591/expose-all-found-SPF-policies (#26)
Browse files Browse the repository at this point in the history
  • Loading branch information
dmotylev authored Mar 20, 2024
2 parents 26265dd + ecf8aaf commit 18a817f
Show file tree
Hide file tree
Showing 4 changed files with 59 additions and 31 deletions.
6 changes: 4 additions & 2 deletions depaware.txt
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ github.com/redsift/spf/v2 dependencies: (generated by github.com/tailscale/depaw
golang.org/x/crypto/chacha20poly1305 from crypto/tls
golang.org/x/crypto/cryptobyte from crypto/ecdsa+
golang.org/x/crypto/cryptobyte/asn1 from crypto/ecdsa+
golang.org/x/crypto/curve25519 from crypto/tls
golang.org/x/crypto/hkdf from crypto/tls
LD golang.org/x/net/bpf from golang.org/x/net/ipv4+
golang.org/x/net/dns/dnsmessage from net
Expand All @@ -25,13 +24,15 @@ github.com/redsift/spf/v2 dependencies: (generated by github.com/tailscale/depaw
LD golang.org/x/sys/unix from github.com/miekg/dns+
bufio from github.com/miekg/dns
bytes from bufio+
cmp from net/netip+
container/list from crypto/tls
context from crypto/tls+
crypto from crypto/ecdsa+
crypto/aes from crypto/ecdsa+
crypto/cipher from crypto/aes+
crypto/des from crypto/tls+
crypto/dsa from crypto/x509
crypto/ecdh from crypto/ecdsa+
crypto/ecdsa from crypto/tls+
crypto/ed25519 from crypto/tls+
crypto/elliptic from crypto/ecdsa+
Expand Down Expand Up @@ -61,7 +62,7 @@ github.com/redsift/spf/v2 dependencies: (generated by github.com/tailscale/depaw
hash from crypto+
io from bufio+
io/fs from crypto/x509+
io/ioutil from golang.org/x/sys/cpu+
io/ioutil from github.com/outcaste-io/ristretto/z
math from crypto/rsa+
math/big from crypto/dsa+
math/bits from crypto/internal/edwards25519/field+
Expand All @@ -76,6 +77,7 @@ github.com/redsift/spf/v2 dependencies: (generated by github.com/tailscale/depaw
reflect from crypto/x509+
regexp from github.com/dustin/go-humanize+
regexp/syntax from regexp
slices from encoding/base32+
sort from encoding/asn1+
strconv from crypto+
strings from bufio+
Expand Down
20 changes: 15 additions & 5 deletions parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,10 @@ func Cause(e error) (string, error) {
return lastToken.String(), e
}

func (e SpfError) Unwrap() error {
return e.err
}

func (e SpfError) Cause() error {
return e.err
}
Expand Down Expand Up @@ -204,14 +208,20 @@ func (p *parser) checkHost(ip net.IP, domain, sender string) (r Result, expl str
// If the resultant record set includes no records, check_host()
// produces the "none" result. If the resultant record set includes
// more than one record, check_host() produces the "permerror" result.
spf, err = filterSPF(txts)
if err != nil {
return Permerror, "", "", NewSpfError(spferr.KindValidation, err, nil)
policies := filterSPF(txts)

if len(policies) == 0 {
return None, "", "", NewSpfError(spferr.KindValidation,
&PolicyDeploymentError{Err: ErrSPFNotFound, Domain: domain}, nil)
}
if spf == "" {
return None, "", "", NewSpfError(spferr.KindValidation, ErrSPFNotFound, nil)

if len(policies) > 1 {
return Permerror, "", "", NewSpfError(spferr.KindValidation,
&PolicyDeploymentError{Err: ErrTooManySPFRecords, Domain: domain, Policies: policies}, nil)
}

spf = policies[0]

r, expl, u, err = newParserWithVisited(p.visited, p.fireFirstMatchOnce, p.options...).with(spf, sender, domain, ip).check()
return
}
Expand Down
25 changes: 14 additions & 11 deletions parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1329,25 +1329,28 @@ func TestSelectingRecord(t *testing.T) {
}))
defer dns.HandleRemove("many-records.")

samples := []struct {
tests := []struct {
d string
r Result
e error
}{
{"notexists", None, SpfError{kind: spferr.KindDNS, err: ErrDNSPermerror}},
{"v-spf2", None, SpfError{kind: spferr.KindValidation, err: ErrSPFNotFound}},
{"v-spf10", None, SpfError{kind: spferr.KindValidation, err: ErrSPFNotFound}},
{"no-record", None, SpfError{kind: spferr.KindValidation, err: ErrSPFNotFound}},
{"many-records", Permerror, SpfError{kind: spferr.KindValidation, err: ErrTooManySPFRecords}},
{"notexists", None, ErrDNSPermerror},
{"v-spf2", None, ErrSPFNotFound},
{"v-spf10", None, ErrSPFNotFound},
{"no-record", None, ErrSPFNotFound},
{"many-records", Permerror, ErrTooManySPFRecords},
{"mixed-records", Pass, nil},
}

ip := net.ParseIP("10.0.0.1")
for i, s := range samples {
r, _, _, e := CheckHost(ip, s.d, s.d, WithResolver(testResolver))
if r != s.r || e != s.e {
t.Errorf("#%d `%s` want [`%v` `%v`], got [`%v` `%v`]", i, s.d, s.r, s.e, r, e)
}
for _, test := range tests {
tt := test
t.Run(tt.d, func(t *testing.T) {
r, _, _, e := CheckHost(ip, tt.d, tt.d, WithResolver(testResolver))
if r != test.r || !errors.Is(e, tt.e) {
t.Errorf("CheckHost(...)\n\twant [`%#v` `%#v`]\n\tgot [`%#v` `%#v`]", tt.r, tt.e, r, e)
}
})
}
}

Expand Down
39 changes: 26 additions & 13 deletions spf.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,26 @@ var (
ErrTooManyErrors = errors.New("too many errors")
)

type PolicyDeploymentError struct {
// Err is the error that occurred during the policy lookup.
Err error
// Domain is the domain that was being looked up.
Domain string
// Policies is the list of policies that were found as part of the lookup.
Policies []string
}

func (e *PolicyDeploymentError) Error() string {
if e == nil {
return "<nil>"
}
return e.Err.Error()
}

func (e *PolicyDeploymentError) Unwrap() error {
return e.Err
}

// DomainError represents a domain check error
type DomainError struct {
Err string // description of the error
Expand Down Expand Up @@ -337,24 +357,20 @@ func CheckHost(ip net.IP, domain, sender string, opts ...Option) (Result, string
// "v=spf1". Note that the version section is terminated by either an
// SP character or the end of the record. As an example, a record with
// a version section of "v=spf10" does not match and is discarded.
func filterSPF(txt []string) (string, error) {
func filterSPF(txt []string) []string {
const (
v = "v=spf1"
vLen = 6
)
var (
spf string
n int
)
var spf []string

for _, s := range txt {
if len(s) < vLen {
continue
}
if len(s) == vLen {
if s == v {
spf = s
n++
spf = append(spf, s)
}
continue
}
Expand All @@ -364,13 +380,10 @@ func filterSPF(txt []string) (string, error) {
if !strings.HasPrefix(s, v) {
continue
}
spf = s
n++
}
if n > 1 {
return "", ErrTooManySPFRecords
spf = append(spf, s)
}
return spf, nil

return spf
}

// isDomainName checks if a string is a presentation-format domain name
Expand Down

0 comments on commit 18a817f

Please sign in to comment.