author | Evaldas Auryla
<ea@uoga.net> 2020-09-09 23:07:11 UTC |
committer | Alberto Bertogli
<albertito@blitiri.com.ar> 2020-09-20 14:50:43 UTC |
parent | 340e6a4bd850249fc155479c904d79c8b422dcfe |
spf.go | +46 | -7 |
spf_test.go | +31 | -0 |
diff --git a/spf.go b/spf.go index ef4a27c..8a9f108 100644 --- a/spf.go +++ b/spf.go @@ -105,6 +105,15 @@ var ( errMatchedExists = fmt.Errorf("matched 'exists'") ) +// Default value for the maximum number of DNS lookups while resolving SPF. +// RFC is quite clear 10 must be the maximum allowed. +// https://tools.ietf.org/html/rfc7208#section-4.6.4 +const defaultMaxLookups = 10 + +// Option type, for setting options. Users are expected to treat this as an +// opaque type and not rely on the implementation, which is subject to change. +type Option func(*resolution) + // CheckHost fetches SPF records for `domain`, parses them, and evaluates them // to determine if `ip` is permitted to send mail for it. // Because it doesn't receive enough information to handle macros well, its @@ -116,25 +125,54 @@ var ( // Deprecated: use CheckHostWithSender instead. func CheckHost(ip net.IP, domain string) (Result, error) { trace("check host %q %q", ip, domain) - r := &resolution{ip, 0, "@" + domain, nil} + r := &resolution{ + ip: ip, + maxcount: defaultMaxLookups, + sender: "@" + domain, + } return r.Check(domain) } // CheckHostWithSender fetches SPF records for `sender`'s domain, parses them, // and evaluates them to determine if `ip` is permitted to send mail for it. // The `helo` domain is used if the sender has no domain part. +// +// The `opts` optional parameter can be used to adjust some specific +// behaviours, such as the maximum number of DNS lookups allowed. +// // Reference: https://tools.ietf.org/html/rfc7208#section-4 -func CheckHostWithSender(ip net.IP, helo, sender string) (Result, error) { +func CheckHostWithSender(ip net.IP, helo, sender string, opts ...Option) (Result, error) { _, domain := split(sender) if domain == "" { domain = helo } trace("check host with sender %q %q %q (%q)", ip, helo, sender, domain) - r := &resolution{ip, 0, sender, nil} + r := &resolution{ + ip: ip, + maxcount: defaultMaxLookups, + sender: sender, + } + + for _, opt := range opts { + opt(r) + } + return r.Check(domain) } +// OverrideLookupLimit overrides the maximum number of DNS lookups allowed +// during SPF evaluation. Note that using this violates the RFC, which is +// quite explicit that the maximum allowed MUST be 10 (the default). Please +// use with care. +// +// This is EXPERIMENTAL for now, and the API is subject to change. +func OverrideLookupLimit(limit uint) Option { + return func(r *resolution) { + r.maxcount = limit + } +} + // split an user@domain address into user and domain. func split(addr string) (string, string) { ps := strings.SplitN(addr, "@", 2) @@ -146,8 +184,9 @@ func split(addr string) (string, string) { } type resolution struct { - ip net.IP - count uint + ip net.IP + count uint + maxcount uint sender string @@ -216,9 +255,9 @@ func (r *resolution) Check(domain string) (Result, error) { continue } - // Limit the number of resolutions to 10 + // Limit the number of resolutions. // https://tools.ietf.org/html/rfc7208#section-4.6.4 - if r.count > 10 { + if r.count > r.maxcount { trace("lookup limit reached") return PermError, errLookupLimitReached } diff --git a/spf_test.go b/spf_test.go index 69d4a8b..efd5e5b 100644 --- a/spf_test.go +++ b/spf_test.go @@ -450,3 +450,34 @@ func TestInvalidMacro(t *testing.T) { } } } + +func TestOverrideLookupLimit(t *testing.T) { + dns = NewDNS() + trace = t.Logf + + dns.txt["domain1"] = []string{"v=spf1 include:domain2"} + dns.txt["domain2"] = []string{"v=spf1 include:domain3"} + dns.txt["domain3"] = []string{"v=spf1 include:domain4"} + dns.txt["domain4"] = []string{"v=spf1 +all"} + + // The default of 10 should be enough. + res, err := CheckHostWithSender(ip1111, "helo", "user@domain1") + if res != Pass { + t.Errorf("expected pass, got %q / %q", res, err) + } + + // Set the limit to 4, which is enough. + res, err = CheckHostWithSender(ip1111, "helo", "user@domain1", + OverrideLookupLimit(4)) + if res != Pass { + t.Errorf("expected pass, got %q / %q", res, err) + } + + // Set the limit to 3, which is not enough. + res, err = CheckHostWithSender(ip1111, "helo", "user@domain1", + OverrideLookupLimit(3)) + if res != PermError || err != errLookupLimitReached { + t.Errorf("expected permerror/lookup limit reached, got %q / %q", + res, err) + } +}