From 22f2e30657d227ac6f678b6caf4980df37e6db1b Mon Sep 17 00:00:00 2001 From: James Lamb Date: Wed, 29 Jun 2022 10:02:01 +1000 Subject: [PATCH 1/2] The RFC suggests SPF records have a record size. The default suggested limit is 450 octets. > Similarly, the sizes for replies to all queries related > to SPF have to be evaluated to fit in a single 512-octet UDP > packet (i.e., DNS message size limited to 450 octets). This proposed change introduces a 450 octet limit check with an optional override. Reference RFC : https://datatracker.ietf.org/doc/html/rfc7208#section-3.4 --- spf.go | 73 ++++++++++++++++++++++++++++++++++++----------------- spf_test.go | 22 ++++++++++++++++ 2 files changed, 72 insertions(+), 23 deletions(-) diff --git a/spf.go b/spf.go index be9d585..aa468be 100644 --- a/spf.go +++ b/spf.go @@ -95,6 +95,7 @@ var ( ErrNoResult = errors.New("no DNS record found") ErrLookupLimitReached = errors.New("lookup limit reached") ErrVoidLookupLimitReached = errors.New("void lookup limit reached") + ErrOctetLimitReached = errors.New("response size exceeds 450 octets") ErrTooManyMXRecords = errors.New("too many MX records") ErrMultipleRecords = errors.New("multiple matching DNS records") @@ -118,6 +119,11 @@ const ( // with a configurable default of 2. // https://tools.ietf.org/html/rfc7208#section-4.6.4 defaultMaxVoidLookups = 2 + + // Default value for maximum octet size of the SPF response. The + // RFC suggests the limit should be 450. + // https://tools.ietf.org/html/rfc7208#section-3.4 + defaultMaxResponseOctets = 450 ) // TraceFunc is the type of tracing functions. @@ -147,14 +153,15 @@ type Option func(*resolution) // Deprecated: use CheckHostWithSender instead. func CheckHost(ip net.IP, domain string) (Result, error) { r := &resolution{ - ip: ip, - maxcount: defaultMaxLookups, - maxvoidcount: defaultMaxVoidLookups, - helo: domain, - sender: "@" + domain, - ctx: context.TODO(), - resolver: defaultResolver, - trace: defaultTrace, + ip: ip, + maxcount: defaultMaxLookups, + maxvoidcount: defaultMaxVoidLookups, + maxresponseoctets: defaultMaxResponseOctets, + helo: domain, + sender: "@" + domain, + ctx: context.TODO(), + resolver: defaultResolver, + trace: defaultTrace, } return r.Check(domain) } @@ -178,14 +185,15 @@ func CheckHostWithSender(ip net.IP, helo, sender string, opts ...Option) (Result } r := &resolution{ - ip: ip, - maxcount: defaultMaxLookups, - maxvoidcount: defaultMaxVoidLookups, - helo: helo, - sender: sender, - ctx: context.TODO(), - resolver: defaultResolver, - trace: defaultTrace, + ip: ip, + maxcount: defaultMaxLookups, + maxvoidcount: defaultMaxVoidLookups, + maxresponseoctets: defaultMaxResponseOctets, + helo: helo, + sender: sender, + ctx: context.TODO(), + resolver: defaultResolver, + trace: defaultTrace, } for _, opt := range opts { @@ -219,6 +227,18 @@ func OverrideVoidLookupLimit(limit uint) Option { } } +// OverrideOctetLimit overrides the maximum octet size allowed +// during SPF evaluation. +// Note that as per RFC, the default value of 450 SHOULD +// be used. Please use with care. +// +// This is EXPERIMENTAL for now, and the API is subject to change. +func OverrideOctetLimit(limit int) Option { + return func(r *resolution) { + r.maxresponseoctets = limit + } +} + // WithContext is an option to set the context for this operation, which will // be passed along to the resolver functions and other external calls if // needed. @@ -246,8 +266,7 @@ var defaultResolver DNSResolver = net.DefaultResolver // // The default is to use net.DefaultResolver, which should be appropriate for // most users. -// -// This is EXPERIMENTAL for now, and the API is subject to change. +//https://tools.ietf.org/html/rfc7208#section-3.4 func WithResolver(resolver DNSResolver) Option { return func(r *resolution) { r.resolver = resolver @@ -278,11 +297,12 @@ func split(addr string) (string, string) { } type resolution struct { - ip net.IP - count uint - maxcount uint - voidcount uint - maxvoidcount uint + ip net.IP + count uint + maxcount uint + voidcount uint + maxvoidcount uint + maxresponseoctets int helo string sender string @@ -329,6 +349,13 @@ func (r *resolution) Check(domain string) (Result, error) { return None, ErrNoResult } + if len(txt) > r.maxresponseoctets { + // SPF response greater than 450 octets + // https://tools.ietf.org/html/rfc7208#section-3.4 + r.trace("dns perm error: %s response size %d", ErrOctetLimitReached, len(txt)) + return PermError, ErrOctetLimitReached + } + fields := strings.Split(txt, " ") // redirects must be handled after the rest; instead of having two loops, diff --git a/spf_test.go b/spf_test.go index bae1098..e3f3053 100644 --- a/spf_test.go +++ b/spf_test.go @@ -671,3 +671,25 @@ func TestWithTraceFunc(t *testing.T) { t.Errorf("expected >0 trace function calls, got 0") } } + +func TestOctetSizeLimit(t *testing.T) { + dns := NewDefaultResolver() + dns.Txt["domain"] = []string{"v=spf1 ip4:192.168.168.0/24 ip4:192.168.169.0/24 ip4:192.168.170.0/24 ip4:192.168.171.0/24 ip4:192.168.172.0/24 ip4:192.168.173.0/24 ip4:192.168.174.0/24 ip4:192.168.175.0/24 ip4:192.168.176.0/24 ip4:192.168.177.0/24 ip4:192.168.178.0/24 ip4:192.168.179.0/24 ip4:192.168.180.0/24 ip4:192.168.181.0/24 ip4:192.168.182.0/24 ip4:192.168.183.0/24 ip4:192.168.184.0/24 ip4:192.168.185.0/24 ip4:192.168.186.0/24 ip4:192.168.187.0/24 ip4:192.168.188.0/24 ip4:192.168.189.0/24 ip4:192.168.190.0/24 ~all"} + defaultTrace = t.Logf + + res, err := CheckHost(ip1111, "domain") + if res != PermError && err != ErrOctetLimitReached { + t.Errorf("expected permerror, got %v (%v)", res, err) + } +} + +func TestOverrideOctetSizeLimit(t *testing.T) { + dns := NewDefaultResolver() + dns.Txt["domain"] = []string{"v=spf1 ip4:1.1.1.1 ip4:192.168.169.0/24 ip4:192.168.170.0/24 ip4:192.168.171.0/24 ip4:192.168.172.0/24 ip4:192.168.173.0/24 ip4:192.168.174.0/24 ip4:192.168.175.0/24 ip4:192.168.176.0/24 ip4:192.168.177.0/24 ip4:192.168.178.0/24 ip4:192.168.179.0/24 ip4:192.168.180.0/24 ip4:192.168.181.0/24 ip4:192.168.182.0/24 ip4:192.168.183.0/24 ip4:192.168.184.0/24 ip4:192.168.185.0/24 ip4:192.168.186.0/24 ip4:192.168.187.0/24 ip4:192.168.188.0/24 ip4:192.168.189.0/24 ip4:192.168.190.0/24 ~all"} + defaultTrace = t.Logf + + res, err := CheckHostWithSender(ip1111, "helo", "user@domain", OverrideOctetLimit(500)) + if res != Pass { + t.Errorf("expected permerror, got %v (%v)", res, err) + } +} From c417c78ac3ecd925ad7f510f49737d5aeeeae8ef Mon Sep 17 00:00:00 2001 From: James Lamb Date: Wed, 13 Jul 2022 09:12:38 +1000 Subject: [PATCH 2/2] tidy up response length variable names to be more descriptive --- spf.go | 60 ++++++++++++++++++++++++++--------------------------- spf_test.go | 2 +- 2 files changed, 31 insertions(+), 31 deletions(-) diff --git a/spf.go b/spf.go index aa468be..cb38ffe 100644 --- a/spf.go +++ b/spf.go @@ -95,7 +95,7 @@ var ( ErrNoResult = errors.New("no DNS record found") ErrLookupLimitReached = errors.New("lookup limit reached") ErrVoidLookupLimitReached = errors.New("void lookup limit reached") - ErrOctetLimitReached = errors.New("response size exceeds 450 octets") + ErrRecordTooLong = errors.New("DNS record is too long") ErrTooManyMXRecords = errors.New("too many MX records") ErrMultipleRecords = errors.New("multiple matching DNS records") @@ -123,7 +123,7 @@ const ( // Default value for maximum octet size of the SPF response. The // RFC suggests the limit should be 450. // https://tools.ietf.org/html/rfc7208#section-3.4 - defaultMaxResponseOctets = 450 + defaultMaxRecordLength = 450 ) // TraceFunc is the type of tracing functions. @@ -153,15 +153,15 @@ type Option func(*resolution) // Deprecated: use CheckHostWithSender instead. func CheckHost(ip net.IP, domain string) (Result, error) { r := &resolution{ - ip: ip, - maxcount: defaultMaxLookups, - maxvoidcount: defaultMaxVoidLookups, - maxresponseoctets: defaultMaxResponseOctets, - helo: domain, - sender: "@" + domain, - ctx: context.TODO(), - resolver: defaultResolver, - trace: defaultTrace, + ip: ip, + maxcount: defaultMaxLookups, + maxvoidcount: defaultMaxVoidLookups, + maxresponsesize: defaultMaxRecordLength, + helo: domain, + sender: "@" + domain, + ctx: context.TODO(), + resolver: defaultResolver, + trace: defaultTrace, } return r.Check(domain) } @@ -185,15 +185,15 @@ func CheckHostWithSender(ip net.IP, helo, sender string, opts ...Option) (Result } r := &resolution{ - ip: ip, - maxcount: defaultMaxLookups, - maxvoidcount: defaultMaxVoidLookups, - maxresponseoctets: defaultMaxResponseOctets, - helo: helo, - sender: sender, - ctx: context.TODO(), - resolver: defaultResolver, - trace: defaultTrace, + ip: ip, + maxcount: defaultMaxLookups, + maxvoidcount: defaultMaxVoidLookups, + maxresponsesize: defaultMaxRecordLength, + helo: helo, + sender: sender, + ctx: context.TODO(), + resolver: defaultResolver, + trace: defaultTrace, } for _, opt := range opts { @@ -235,7 +235,7 @@ func OverrideVoidLookupLimit(limit uint) Option { // This is EXPERIMENTAL for now, and the API is subject to change. func OverrideOctetLimit(limit int) Option { return func(r *resolution) { - r.maxresponseoctets = limit + r.maxresponsesize = limit } } @@ -297,12 +297,12 @@ func split(addr string) (string, string) { } type resolution struct { - ip net.IP - count uint - maxcount uint - voidcount uint - maxvoidcount uint - maxresponseoctets int + ip net.IP + count uint + maxcount uint + voidcount uint + maxvoidcount uint + maxresponsesize int helo string sender string @@ -349,11 +349,11 @@ func (r *resolution) Check(domain string) (Result, error) { return None, ErrNoResult } - if len(txt) > r.maxresponseoctets { + if len(txt) > r.maxresponsesize { // SPF response greater than 450 octets // https://tools.ietf.org/html/rfc7208#section-3.4 - r.trace("dns perm error: %s response size %d", ErrOctetLimitReached, len(txt)) - return PermError, ErrOctetLimitReached + r.trace("dns perm error: %s response size %d", ErrRecordTooLong, len(txt)) + return PermError, ErrRecordTooLong } fields := strings.Split(txt, " ") diff --git a/spf_test.go b/spf_test.go index e3f3053..f9b3d94 100644 --- a/spf_test.go +++ b/spf_test.go @@ -678,7 +678,7 @@ func TestOctetSizeLimit(t *testing.T) { defaultTrace = t.Logf res, err := CheckHost(ip1111, "domain") - if res != PermError && err != ErrOctetLimitReached { + if res != PermError && err != ErrRecordTooLong { t.Errorf("expected permerror, got %v (%v)", res, err) } }