spf

package module
v1.0.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 23, 2022 License: MIT Imports: 16 Imported by: 0

README

Sender Policy Framework

A comprehensive RFC7208 implementation

Build Status Go Report Card GoDoc

About

The SPF Library implements Sender Policy Framework described in RFC 7208. It aims to cover all rough edge cases from RFC 7208. Hence, the library does not operate on strings only, rather "understands" SPF records and reacts properly to valid and invalid input. Wherever I found it useful, I added comments with RFC sections and quotes directly in the source code, so the readers can follow implemented logic.

Current status

The library is still under development. API may change, including function/methods names and signatures. I will consider it correct and stable once it passess all tests described in the most popular SPF implementation - pyspf.

Testing

Testing is an important part of this implementation. There are unit tests that will run locally in your environment, however there are also configuration files for named DNS server that would be able to respond implemented testcases. (In fact, for the long time I used a real DNS server with such configuration as a testing infrastructure for my code). There is a plan to implement simple DNS server that would be able to read .yaml files with comprehensive testsuite defined in pyspf package. Code coverage is also important part of the development and the aim is to keep it as high as 9x %

Dependencies

SPF library depends on another DNS library. Sadly, Go's builtin DNS library is not elastic enough and does not allow for controlling underlying DNS queries/responses.

Pull requests & code review

If you have any comments about code structure feel free to reach out or simply make a Pull Request

Documentation

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrDNSTemperror      = errors.New("temporary DNS error")
	ErrDNSPermerror      = errors.New("permanent DNS error")
	ErrDNSLimitExceeded  = errors.New("limit exceeded")
	ErrSPFNotFound       = errors.New("SPF record not found")
	ErrInvalidCIDRLength = errors.New("invalid CIDR length")
	ErrTooManySPFRecords = errors.New("too many SPF records")
	ErrTooManyRedirects  = errors.New(`too many "redirect"`)
	ErrTooManyExps       = errors.New(`too many "exp"`)
	ErrSyntaxError       = errors.New(`wrong syntax`)
	ErrEmptyDomain       = errors.New("empty domain")
	ErrNotIPv4           = errors.New("address isn't ipv4")
	ErrNotIPv6           = errors.New("address isn't ipv6")
	ErrLoopDetected      = errors.New("infinite recursion detected")
	ErrUnreliableResult  = errors.New("result is unreliable with IgnoreMatches option enabled")
	ErrTooManyErrors     = errors.New("too many errors")
)

Errors could be used for root couse analysis

Functions

func Cause

func Cause(e error) (error, string)

func NewMiekgDNSResolver

func NewMiekgDNSResolver(addr string, opts ...MiekgDNSResolverOption) (*miekgDNSResolver, error)

NewMiekgDNSResolver returns new instance of Resolver with default dns.Client

func NormalizeFQDN

func NormalizeFQDN(name string) string

NormalizeFQDN appends a root domain (a dot) to the FQDN.

func Unwrap

func Unwrap(e error) (*token, error, bool)

Types

type CacheDump

type CacheDump map[interface{}]interface{}

func (CacheDump) ForEach

func (c CacheDump) ForEach(f func(*dns.Msg))

func (CacheDump) MarshalJSON

func (c CacheDump) MarshalJSON() ([]byte, error)

func (*CacheDump) UnmarshalJSON

func (c *CacheDump) UnmarshalJSON(b []byte) error

type DNSResolver

type DNSResolver struct{}

DNSResolver implements Resolver using local DNS

func (*DNSResolver) Exists

func (r *DNSResolver) Exists(name string) (bool, time.Duration, error)

Exists is used for a DNS A RR lookup (even when the connection type is IPv6). If any A record is returned, this mechanism matches and returns the ttl.

func (*DNSResolver) LookupTXT

func (r *DNSResolver) LookupTXT(name string) ([]string, time.Duration, error)

LookupTXT returns the DNS TXT records for the given domain name and the TTL.

func (*DNSResolver) LookupTXTStrict

func (r *DNSResolver) LookupTXTStrict(name string) ([]string, time.Duration, error)

LookupTXTStrict returns DNS TXT records for the given name and the TTL, however it will return ErrDNSPermerror upon NXDOMAIN (RCODE 3)

func (*DNSResolver) MatchIP

func (r *DNSResolver) MatchIP(name string, matcher IPMatcherFunc) (bool, time.Duration, error)

MatchIP provides an address lookup, which should be done on the name using the type of lookup (A or AAAA). Then IPMatcherFunc used to compare checked IP to the returned address(es). If any address matches, the mechanism matches and returns the TTL with it

func (*DNSResolver) MatchMX

func (r *DNSResolver) MatchMX(name string, matcher IPMatcherFunc) (bool, time.Duration, error)

MatchMX is similar to MatchIP but first performs an MX lookup on the name. Then it performs an address lookup on each MX name returned. Then IPMatcherFunc used to compare checked IP to the returned address(es). If any address matches, the mechanism matches and returns the TTL.

type DomainError

type DomainError struct {
	Err    string // description of the error
	Domain string // domain checked
}

DomainError represents a domain check error

func (*DomainError) Error

func (e *DomainError) Error() string

type IPMatcherFunc

type IPMatcherFunc func(ip net.IP, name string) (bool, error)

IPMatcherFunc returns true if ip matches to implemented rules. If IPMatcherFunc returns any non nil error, the Resolver must stop any further processing and use the error as resulting error. name is given for information purpose only and could be totally ignored by implementation.

type LimitedResolver

type LimitedResolver struct {
	// contains filtered or unexported fields
}

LimitedResolver wraps a Resolver and limits number of lookups possible to do with it. All overlimited calls return ErrDNSLimitExceeded.

func (*LimitedResolver) Exists

func (r *LimitedResolver) Exists(name string) (bool, time.Duration, error)

Exists is used for a DNS A RR lookup (even when the connection type is IPv6). If any A record is returned, this mechanism matches and returns the ttl. Returns false and ErrDNSLimitExceeded if total number of lookups made by underlying resolver exceed the limit.

func (*LimitedResolver) LookupTXT

func (r *LimitedResolver) LookupTXT(name string) ([]string, time.Duration, error)

LookupTXT returns the DNS TXT records for the given domain name and the minimum TTL. Used for "exp" modifier and do not cause DNS query.

func (*LimitedResolver) LookupTXTStrict

func (r *LimitedResolver) LookupTXTStrict(name string) ([]string, time.Duration, error)

LookupTXTStrict returns the DNS TXT records for the given domain name and the minimum TTL. Returns nil and ErrDNSLimitExceeded if total number of lookups made by underlying resolver exceed the limit. It will also return ErrDNSPermerror upon DNS call return error NXDOMAIN (RCODE 3)

func (*LimitedResolver) MatchIP

func (r *LimitedResolver) MatchIP(name string, matcher IPMatcherFunc) (bool, time.Duration, error)

MatchIP provides an address lookup, which should be done on the name using the type of lookup (A or AAAA). Then IPMatcherFunc used to compare checked IP to the returned address(es). If any address matches, the mechanism matches Returns false and ErrDNSLimitExceeded if total number of lookups made by underlying resolver exceed the limit. Also return the minimum TTL in true.

func (*LimitedResolver) MatchMX

func (r *LimitedResolver) MatchMX(name string, matcher IPMatcherFunc) (bool, time.Duration, error)

MatchMX is similar to MatchIP but first performs an MX lookup on the name. Then it performs an address lookup on each MX name returned. Then IPMatcherFunc used to compare checked IP to the returned address(es). If any address matches, the mechanism matches.

In addition to that limit, the evaluation of each "MX" record MUST NOT result in querying more than 10 address records -- either "A" or "AAAA" resource records. If this limit is exceeded, the "mx" mechanism MUST produce a "permerror" result.

Returns false and ErrDNSLimitExceeded if total number of lookups made by underlying resolver exceed the limit. Returns the minimum TTL in true.

type Listener

type Listener interface {
	CheckHost(ip net.IP, domain, sender string)
	CheckHostResult(r Result, explanation string, ttl time.Duration, err error)
	SPFRecord(s string)
	Directive(unused bool, qualifier, mechanism, value, effectiveValue string)
	NonMatch(qualifier, mechanism, value string, result Result, err error)
	Match(qualifier, mechanism, value string, result Result, explanation string, ttl time.Duration, err error)
	MatchingIP(qualifier, mechanism, value string, fqdn string, ipn net.IPNet, host string, ip net.IP)
}

type MiekgDNSResolverOption

type MiekgDNSResolverOption func(r *miekgDNSResolver)

func MiekgDNSCache

func MiekgDNSCache(c gcache.Cache) MiekgDNSResolverOption

func MiekgDNSClient

func MiekgDNSClient(c *dns.Client) MiekgDNSResolverOption

func MiekgDNSParallelism

func MiekgDNSParallelism(n int) MiekgDNSResolverOption

MiekgDNSParallelism change parallelism level of matching IP and MX Anything less than 1 means unlimited

type Option

type Option func(*parser)

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

func ErrorsThreshold

func ErrorsThreshold(n int) Option

func EvaluatedOn

func EvaluatedOn(t time.Time) Option

func HeloDomain

func HeloDomain(s string) Option

func IgnoreMatches

func IgnoreMatches() Option

func PartialMacros

func PartialMacros(v bool) Option

PartialMacros triggers partial macro expansion. Currently it expands only %{d} with provided domain, if not empty. Otherwise it keeps macro body. Escaped symbols like '%%,%-,%_' are not expanded.

func ReceivingFQDN

func ReceivingFQDN(s string) Option

func WithListener

func WithListener(l Listener) Option

func WithResolver

func WithResolver(r Resolver) Option

type Resolver

type Resolver interface {
	// LookupTXT returns the DNS TXT records for the given domain name and
	// the minimum TTL.
	LookupTXT(string) ([]string, time.Duration, error)
	// LookupTXTStrict returns DNS TXT records for the given name and the
	// minimum TTL, however it will return ErrDNSPermerror upon returned
	// NXDOMAIN (RCODE 3)
	LookupTXTStrict(string) ([]string, time.Duration, error)
	// Exists is used for a DNS A RR lookup (even when the
	// connection type is IPv6).  If any A record is returned, this
	// mechanism matches and returns the TTL with it.
	Exists(string) (bool, time.Duration, error)
	// MatchIP provides an address lookup, which should be done on the name
	// using the type of lookup (A or AAAA).
	// Then IPMatcherFunc used to compare checked IP to the returned address(es).
	// If any address matches, the mechanism matches and returns the TTL.
	MatchIP(string, IPMatcherFunc) (bool, time.Duration, error)
	// MatchMX is similar to MatchIP but first performs an MX lookup on the
	// name.  Then it performs an address lookup on each MX name returned.
	// Then IPMatcherFunc used to compare checked IP to the returned address(es).
	// If any address matches, the mechanism matches and returns the TTL.
	MatchMX(string, IPMatcherFunc) (bool, time.Duration, error)
}

Resolver provides abstraction for DNS layer

func NewLimitedResolver

func NewLimitedResolver(r Resolver, lookupLimit, mxQueriesLimit uint16) Resolver

NewLimitedResolver returns a resolver which will pass up to lookupLimit calls to r. In addition to that limit, the evaluation of each "MX" record will be limited to mxQueryLimit. All calls over the limit will return ErrDNSLimitExceeded. Make sure lookupLimit includes the initial SPF lookup

func NewRetryResolver

func NewRetryResolver(rr []Resolver, opts ...RetryResolverOption) Resolver

NewRetryResolver implements round-robin retry with backoff delay

type Result

type Result int

Result represents result of SPF evaluation as it defined by RFC7208 https://tools.ietf.org/html/rfc7208#section-2.6

const (

	// None means either (a) no syntactically valid DNS
	// domain name was extracted from the SMTP session that could be used
	// as the one to be authorized, or (b) no SPF records were retrieved
	// from the DNS.
	None Result
	// Neutral result means the ADMD has explicitly stated that it
	// is not asserting whether the IP address is authorized.
	Neutral
	// Pass result is an explicit statement that the client
	// is authorized to inject mail with the given identity.
	Pass
	// Fail result is an explicit statement that the client
	// is not authorized to use the domain in the given identity.
	Fail
	// Softfail result is a weak statement by the publishing ADMD
	// that the host is probably not authorized.  It has not published
	// a stronger, more definitive policy that results in a "fail".
	Softfail
	// Temperror result means the SPF verifier encountered a transient
	// (generally DNS) error while performing the check.
	// A later retry may succeed without further DNS operator action.
	Temperror
	// Permerror result means the domain's published records could
	// not be correctly interpreted.
	// This signals an error condition that definitely requires
	// DNS operator intervention to be resolved.
	Permerror
)

func CheckHost

func CheckHost(ip net.IP, domain, sender string, opts ...Option) (Result, string, string, error)

CheckHost is a main entrypoint function evaluating e-mail with regard to SPF and it utilizes DNSResolver as a resolver. As per RFC 7208 it will accept 3 parameters: <ip> - IP{4,6} address of the connected client <domain> - domain portion of the MAIL FROM or HELO identity <sender> - MAIL FROM or HELO identity All the parameters should be parsed and dereferenced from real email fields. This means domain should already be extracted from MAIL FROM field so this function can focus on the core part.

CheckHost returns result of verification, explanations as result of "exp=", raw discovered SPF policy and error as the reason for the encountered problem.

func (Result) MarshalText

func (r Result) MarshalText() ([]byte, error)

func (Result) String

func (r Result) String() string

String returns string form of the result as defined by RFC7208 https://tools.ietf.org/html/rfc7208#section-2.6

func (*Result) UnmarshalText

func (r *Result) UnmarshalText(text []byte) error

type RetryResolverOption

type RetryResolverOption func(r *retryResolver)

func BackoffDelayMin

func BackoffDelayMin(d time.Duration) RetryResolverOption

func BackoffFactor

func BackoffFactor(f float64) RetryResolverOption

func BackoffJitter

func BackoffJitter(b bool) RetryResolverOption

func BackoffTimeout

func BackoffTimeout(d time.Duration) RetryResolverOption

type SyntaxError

type SyntaxError struct {
	// contains filtered or unexported fields
}

SyntaxError represents parsing error, it holds reference to faulty token as well as error describing fault

func (SyntaxError) Cause

func (e SyntaxError) Cause() error

func (SyntaxError) Error

func (e SyntaxError) Error() string

func (SyntaxError) TokenString

func (e SyntaxError) TokenString() string

type Trace

type Trace struct {
	Result       Result `json:"result"`                 // the result
	Explanation  string `json:"exp,omitempty"`          // supporting information for the result
	ClientIP     net.IP `json:"clientIp,omitempty"`     // the IP address of the SMTP client
	Identity     string `json:"identity,omitempty"`     // the identity that was checked
	Helo         string `json:"helo,omitempty"`         // the host name given in the HELO or EHLO command
	EnvelopeFrom string `json:"envelopeFrom,omitempty"` // the envelope sender mailbox
	Problem      error  `json:"problem,omitempty"`      // if an error was returned, details about the error
	Receiver     string `json:"receiver,omitempty"`     // the host name of the SPF verifier
	Mechanism    string `json:"mechanism,omitempty"`    // the mechanism that matched
}

Trace holds data for "Received-SPF" header field https://tools.ietf.org/html/rfc7208#section-9.1

func (*Trace) ReceivedSPF

func (r *Trace) ReceivedSPF() string

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL