Spfcheck

TestModule VersionHex DocsLast UpdatedLicenseTotal Download

spfcheck is a command line tool to examine and debug SPF records.

Maintaining SPF records as part of your email security can quickly become cumbersome, especially when there is a need to include SPF records across administrative boundaries.

Use spfcheck to:

spfcheck passes the rfc7208 test suite and should be reasonably rfc7208 compliant.

Usage

Usage: spfcheck [options] [sender ...]

where sender = [localpart@]domain and localpart defaults to 'postmaster'

Options:
  -H, --help              print this message and exit
  -a, --author=string     sets author in the markdown metadata (default spfcheck)
  -c, --color             use color for log messages on stderr (default is true)
  -d, --dns=filepath      file with DNS RR records to prepopulate the DNS cache
  -h, --helo=string       sending MTA helo/ehlo identity (defaults to nil)
  -i, --ip=string         sending MTA IPv4/IPv6 address (defaults to 127.0.0.1)
  -m, --markdown          use markdown format for output (default depends, see Report flag)
  -n, --nameserver=string an IPv4/IPv6 address of a nameserver to use
  -r, --report=string     either "all" or one of more letters of "vgsewpdat" (see below)
  -t, --title=string      sets title in the markdown metadata (default "SPF report")
  -v, --verbosity=number  set logging noise level (0..5), default is 4 (informational)
  -w, --width=NUM         limits line length to increase readability (defaults to 60)
  and
  --no-color              turn off colors for log messages
  --no-markdown           turn off markdown formatting for reports

The default is to simply print the verdict and some stats to stdout and print notification messages to stderr.

% spfcheck example.com --no-color

example.com %spf[0]-ctx-info:   > sender is 'example.com'
example.com %spf[0]-ctx-info:   > local part set to 'postmaster'
example.com %spf[0]-ctx-info:   > domain part set to 'example.com'
example.com %spf[0]-ctx-info:   > helo set to 'example.com'
example.com %spf[0]-ctx-info:   > ip set to '127.0.0.1'
example.com %spf[0]-ctx-info:   > DNS cache preloaded with 0 entrie(s)
example.com %spf[0]-ctx-info:   > verbosity level 4
example.com %spf[0]-ctx-info:   > created context for 'example.com'
example.com %spf[0]-spf-note:   > spfcheck(example.com, 127.0.0.1, example.com)
example.com %spf[0]-dns-info:   > DNS QUERY (1) txt example.com - ["v=spf1 -all", "yxvy9m4blrswgrsz8ndjh467n2y7mgl2"]
example.com %spf[0]-eval-note:  > spf[0] -all - matches
example.com %spf[0]-dns-info:   > DNS QUERY (2) soa example.com - [{"ns.icann.org", "noc.dns.icann.org", 2021120707, 7200, 3600, 1209600, 3600}]

domain     : example.com
ip         : 127.0.0.1
sender     : example.com
verdict    : fail
reason     : spf[0] -all
owner      : example.com
contact    : noc@dns.icann.org
num_spf    : 1
num_dnsm   : 0
num_dnsq   : 1
num_dnsv   : 0
num_checks : 1
num_warn   : 0
num_error  : 0
duration   : 0
explanation: 

Batchmode

If no sender is given on the command line, spfcheck will read stdin for the domains (and options) to check. In this case, the verdict(s) are output on stdout in csv-format as each domain is (sequentially) evaluated.

% cat assets/domains.txt
example.com
me@example.net -i 1.2.3.4

% cat assets/domains.txt | spfcheck -v 0
domain,ip,sender,verdict,reason,owner,contact,num_spf,num_dnsm,num_dnsq,num_dnsv,num_checks,num_warn,num_error,duration,explanation
"example.com","127.0.0.1","example.com",:fail,"spf[0] -all","example.com","noc@dns.icann.org",1,0,1,0,1,0,0,1,""
"example.net","1.2.3.4","me@example.net",:fail,"spf[0] -all","example.net","noc@dns.icann.org",1,0,1,0,1,0,0,0,""

DNS flag

The -d flag can be used to either point to local file with RR-records or specify DNS data on the command line. If the file exists, it is read and used to prepopulate the cache. Otherwise, the text will be read as DNS data. This makes it possible to try out records before publishing them in DNS. That file should contain 1 RR record per line using:

Where:

The DNS type and error are both are case-insensitive and all domains are taken relative to root ('.') which is always stripped if present. Using the second format of domain error will set the error on all known RR type's.

For the curious, the RR-type SPF is a relic from the past and only used when running the rfc7208 testsuite.

% spfcheck example.com -v 0 -d "example.com TXT v=spf1 +all"

domain     : example.com
ip         : 127.0.0.1
sender     : example.com
verdict    : pass
reason     : spf[0] +all
owner      : example.com
contact    : noc@dns.icann.org
num_spf    : 1
num_dnsm   : 0
num_dnsq   : 1
num_dnsv   : 0
num_checks : 1
num_warn   : 1
num_error  : 0
duration   : 0
explanation:


# Or using a file

% cat assets/zonedata.txt
# comments are ignored as are empty lines

example.com TXT v=spf1 -all exp=why.%{d}
example.com TXT just another txt record
why.example.com TXT %{d}: %{i} is not one of our MTA's

% spfcheck example.com -v 0 -d assets/zonedata.txt

domain     : example.com
ip         : 127.0.0.1
sender     : example.com
verdict    : fail
reason     : spf[0] -all
owner      : example.com
contact    : noc@dns.icann.org
num_spf    : 1
num_dnsm   : 0
num_dnsq   : 2
num_dnsv   : 0
num_checks : 1
num_warn   : 0
num_error  : 0
duration   : 1
explanation: example.com: 127.0.0.1 is not one of our MTA's

spfcheck counts the number of dns mechanisms seen (dnsm), the number of queries performed (dnsq) and the number of void dns queries seen (dnsv). If the evaluation took more than 10 dns mechanisms or saw more than 2 void DNS lookups, the verdict is modified accordingly. The soa queries used to retrieve/find the owner and contact information are not included in the dns counters.

Helo flag

The -h allows for setting the EHLO domain name and defaults to given sender. Note that spfcheck only checks SPF for sender, so this is only useful when checking the expansion of the %{h}-macro in a policy.

Ip flag

The -i flag is used to set sender's IP to either an IPv4 or an IPv6 address, it defaults to 127.0.0.1 as an unlikely address to be authorized by anyone. The goal is to go down the rabbit hole as far as possible and check the entire nested SPF policy for given sender. Notes:

% spfcheck example.com --no-color -i "::ffff:1.2.3.4"
example.com %spf[0]-ctx-info:   > sender is 'example.com'
example.com %spf[0]-ctx-info:   > local part set to 'postmaster'
example.com %spf[0]-ctx-info:   > domain part set to 'example.com'
example.com %spf[0]-ctx-info:   > ip is '1.2.3.4'
example.com %spf[0]-ctx-note:   > '1.2.3.4' was extracted from IPv4-mapped IPv6 address '::ffff:1.2.3.4'
example.com %spf[0]-ctx-info:   > helo set to 'example.com'
example.com %spf[0]-ctx-info:   > DNS cache preloaded with 0 entrie(s)
example.com %spf[0]-ctx-info:   > verbosity level 4
example.com %spf[0]-ctx-info:   > created context for 'example.com'
example.com %spf[0]-spf-note:   > spfcheck(example.com, 1.2.3.4, example.com)
example.com %spf[0]-dns-info:   > DNS QUERY (1) txt example.com - ["8j5nfqld20zpcyr8xjw0ydcfq9rk8hgm", "v=spf1 -all"]
example.com %spf[0]-eval-note:  > spf[0] -all - matches
example.com %spf[0]-dns-info:   > DNS QUERY (2) soa example.com - [{"ns.icann.org", "noc.dns.icann.org", 2021111701, 7200, 3600, 1209600, 3600}]

domain     : example.com
ip         : 1.2.3.4
sender     : example.com
verdict    : fail
reason     : spf[0] -all
owner      : example.com
contact    : noc@dns.icann.org
num_spf    : 1
num_dnsm   : 0
num_dnsq   : 1
num_dnsv   : 0
num_checks : 1
num_warn   : 0
num_error  : 0
duration   : 0
explanation:

# or check for some prefix

% spfcheck example.com -i 1.1.255.0/24 -d "example.com txt v=spf1 ip4:1.1.0.0/16 -all" --no-color
example.com %spf[0]-ctx-info:   > sender is 'example.com'
example.com %spf[0]-ctx-info:   > local part set to 'postmaster'
example.com %spf[0]-ctx-info:   > domain part set to 'example.com'
example.com %spf[0]-ctx-info:   > ip is '1.1.255.0/24'
example.com %spf[0]-ctx-info:   > helo set to 'example.com'
example.com %spf[0]-ctx-info:   > DNS cache preloaded with 1 entrie(s)
example.com %spf[0]-ctx-info:   > verbosity level 4
example.com %spf[0]-ctx-info:   > created context for 'example.com'
example.com %spf[0]-spf-note:   > spfcheck(example.com, 1.1.255.0/24, example.com)
example.com %spf[0]-dns-info:   > DNS QUERY (1) [cache] txt example.com - ["v=spf1 ip4:1.1.0.0/16 -all"]
example.com %spf[0]-eval-note:  > spf[0] ip4:1.1.0.0/16 - matches 1.1.255.0/24
example.com %spf[0]-dns-info:   > DNS QUERY (2) soa example.com - [{"ns.icann.org", "noc.dns.icann.org", 2021111701, 7200, 3600, 1209600, 3600}]

domain     : example.com
ip         : 1.1.255.0/24
sender     : example.com
verdict    : pass
reason     : spf[0] ip4:1.1.0.0/16
owner      : example.com
contact    : noc@dns.icann.org
num_spf    : 1
num_dnsm   : 0
num_dnsq   : 1
num_dnsv   : 0
num_checks : 1
num_warn   : 0
num_error  : 0
duration   : 0
explanation: 

Nameserver flag

Use -n ip or --nameserver ip to specify an IPv4 or IPv6 address of a, possibly external, recursive nameserver to use for an SPF policy evaluation instead of using the system default settings. Specify multiple nameservers by repeating the option with different IP addresses, in which case they will be tried in the order listed.

% spfcheck example.com -n 2001:4860:4860::8888 -v 5 --no-color

example.com %spf[0]-ctx-info:   > sender is 'example.com'
example.com %spf[0]-ctx-info:   > local part set to 'postmaster'
example.com %spf[0]-ctx-info:   > domain part set to 'example.com'
example.com %spf[0]-ctx-info:   > ip set to '127.0.0.1'
example.com %spf[0]-ctx-debug:  > atype set to 'a'
example.com %spf[0]-ctx-info:   > helo set to 'example.com'
example.com %spf[0]-ctx-debug:  > helo defaults to sender value
example.com %spf[0]-ctx-info:   > DNS cache preloaded with 0 entrie(s)
example.com %spf[0]-ctx-info:   > verbosity level 5
example.com %spf[0]-ctx-debug:  > DNS timeout set to 2000
example.com %spf[0]-ctx-debug:  > max DNS mechanisms set to 10
example.com %spf[0]-ctx-debug:  > max void DNS lookups set to 2
example.com %spf[0]-ctx-debug:  > verdict defaults to 'neutral'
example.com %spf[0]-ctx-debug:  > nameservers set to [{{8193, 18528, 18528, 0, 0, 0, 0, 34952}, 53}]
example.com %spf[0]-ctx-info:   > created context for 'example.com'
example.com %spf[0]-spf-note:   > spfcheck(example.com, 127.0.0.1, example.com)
example.com %spf[0]-dns-debug:  > added {example.com, txt} -> "v=spf1 -all"
example.com %spf[0]-dns-debug:  > added {example.com, txt} -> "8j5nfqld20zpcyr8xjw0ydcfq9rk8hgm"
example.com %spf[0]-dns-info:   > DNS QUERY (1) txt example.com - ["8j5nfqld20zpcyr8xjw0ydcfq9rk8hgm", "v=spf1 -all"]
example.com %spf[0]-eval-note:  > spf[0] -all - matches
example.com %spf[0]-dns-debug:  > added {example.com, soa} -> {"ns.icann.org", "noc.dns.icann.org", 2021111701, 7200, 3600, 1209600, 3600}
example.com %spf[0]-dns-info:   > DNS QUERY (2) soa example.com - [{"ns.icann.org", "noc.dns.icann.org", 2021111701, 7200, 3600, 1209600, 3600}]

domain     : example.com
ip         : 127.0.0.1
sender     : example.com
verdict    : fail
reason     : spf[0] -all
owner      : example.com
contact    : noc@dns.icann.org
num_spf    : 1
num_dnsm   : 0
num_dnsq   : 1
num_dnsv   : 0
num_checks : 1
num_warn   : 0
num_error  : 0
duration   : 0
explanation:

No color flag

The --no-color flag disables the use of colors in log messages, which is better when redirecting logging to a file.

For example:

%cat assets/domains.txt
example.com
me@example.net -i 1.2.3.4

% cat assets/domains.txt | spfcheck -v 5 --no-color 2>assets/log.txt > assets/checked.csv
% cat assets/log.txt
example.com %spf[0]-ctx-info:   > sender is 'example.com'
example.com %spf[0]-ctx-info:   > local part set to 'postmaster'
example.com %spf[0]-ctx-info:   > domain part set to 'example.com'
example.com %spf[0]-ctx-info:   > ip set to '127.0.0.1'
example.com %spf[0]-ctx-debug:  > atype set to 'a'
example.com %spf[0]-ctx-info:   > helo set to 'example.com'
example.com %spf[0]-ctx-debug:  > helo defaults to sender value
example.com %spf[0]-ctx-info:   > DNS cache preloaded with 0 entrie(s)
example.com %spf[0]-ctx-info:   > verbosity level 5
example.com %spf[0]-ctx-debug:  > DNS timeout set to 2000
example.com %spf[0]-ctx-debug:  > max DNS mechanisms set to 10
example.com %spf[0]-ctx-debug:  > max void DNS lookups set to 2
example.com %spf[0]-ctx-debug:  > verdict defaults to 'neutral'
example.com %spf[0]-ctx-debug:  > nameservers set to default
example.com %spf[0]-ctx-info:   > created context for 'example.com'
example.com %spf[0]-spf-note:   > spfcheck(example.com, 127.0.0.1, example.com)
example.com %spf[0]-dns-debug:  > added {example.com, txt} -> "v=spf1 -all"
example.com %spf[0]-dns-debug:  > added {example.com, txt} -> "8j5nfqld20zpcyr8xjw0ydcfq9rk8hgm"
example.com %spf[0]-dns-info:   > DNS QUERY (1) txt example.com - ["8j5nfqld20zpcyr8xjw0ydcfq9rk8hgm", "v=spf1 -all"]
example.com %spf[0]-eval-note:  > spf[0] -all - matches
example.com %spf[0]-dns-debug:  > added {example.com, soa} -> {"ns.icann.org", "noc.dns.icann.org", 2021111701, 7200, 3600, 1209600, 3600}
example.com %spf[0]-dns-info:   > DNS QUERY (2) soa example.com - [{"ns.icann.org", "noc.dns.icann.org", 2021111701, 7200, 3600, 1209600, 3600}]
example.net %spf[0]-ctx-info:   > sender is 'me@example.net'
example.net %spf[0]-ctx-info:   > local part set to 'me'
example.net %spf[0]-ctx-info:   > domain part set to 'example.net'
example.net %spf[0]-ctx-info:   > ip set to '1.2.3.4'
example.net %spf[0]-ctx-debug:  > atype set to 'a'
example.net %spf[0]-ctx-info:   > helo set to 'me@example.net'
example.net %spf[0]-ctx-debug:  > helo defaults to sender value
example.net %spf[0]-ctx-info:   > DNS cache preloaded with 0 entrie(s)
example.net %spf[0]-ctx-info:   > verbosity level 5
example.net %spf[0]-ctx-debug:  > DNS timeout set to 2000
example.net %spf[0]-ctx-debug:  > max DNS mechanisms set to 10
example.net %spf[0]-ctx-debug:  > max void DNS lookups set to 2
example.net %spf[0]-ctx-debug:  > verdict defaults to 'neutral'
example.net %spf[0]-ctx-debug:  > nameservers set to default
example.net %spf[0]-ctx-info:   > created context for 'example.net'
example.net %spf[0]-spf-note:   > spfcheck(example.net, 1.2.3.4, me@example.net)
example.net %spf[0]-dns-debug:  > added {example.net, txt} -> "v=spf1 -all"
example.net %spf[0]-dns-debug:  > added {example.net, txt} -> "5fpl1ghm7scnth0907z0pft8c79lvc8t"
example.net %spf[0]-dns-info:   > DNS QUERY (1) txt example.net - ["5fpl1ghm7scnth0907z0pft8c79lvc8t", "v=spf1 -all"]
example.net %spf[0]-eval-note:  > spf[0] -all - matches
example.net %spf[0]-dns-debug:  > added {example.net, soa} -> {"ns.icann.org", "noc.dns.icann.org", 2021111701, 7200, 3600, 1209600, 3600}
example.net %spf[0]-dns-info:   > DNS QUERY (2) soa example.net - [{"ns.icann.org", "noc.dns.icann.org", 2021111701, 7200, 3600, 1209600, 3600}]

Report flag

The -r flag can be used to print out some information, topics include:

In case no -r flag is used, spfcheck will simply print out the verdict without any markdown formatting. If only one topic is to be reported, the default is to omit markdown formatting as well unless requested explicitly with the -m flag.

% spfcheck example.com example.net example.org -v 0 -r s

[0] example.com -- (example.com, noc@dns.icann.org)
    v=spf1 -all

[0] example.net -- (example.net, noc@dns.icann.org)
    v=spf1 -all

[0] example.org -- (example.org, noc@dns.icann.org)
    v=spf1 -all

Alternatively, a simple markdown report can be generated. Use the -t and -a flags to customize the title and author information respectively. The report below shows the SPF records used by several example domains. If they had included other SPF records, those would show as well.

% spfcheck example.com example.net example.org -v 0 -r s -m \
  -t "Spf records used by Example domains" -a mail@example.com

    ---
    title: Spf records used by Example domains
    author: mail@example.com
    date: 2021-11-28 12:44:30
    ...


    # example.com

    ## SPF

    ```
    [0] example.com -- (example.com, noc@dns.icann.org)
        v=spf1 -all

    ```

    # example.net

    ## SPF

    ```
    [0] example.net -- (example.net, noc@dns.icann.org)
        v=spf1 -all

    ```

    # example.org

    ## SPF

    ```
    [0] example.org -- (example.org, noc@dns.icann.org)
        v=spf1 -all

    ```

Use the g report topic to visualize a domain's SPF policy as a graphviz directed graph. If markdown is active, the fenced codeblock will have graphviz as its class, allowing the conversion of the markdown to e.g. a pdf with a picture of the SPF policy using pandoc and a graphviz filter.

# example of an SPF policy with some problems
% cat assets/example.db
example.com txt v=spf1 a:%{d1}.org include:spf-a.example.com include:spf-b.example.com redirect=spf-c.example.com
example.org a 1.2.3.4
spf-a.example.com txt v=spf1 a mx include:spf-b.example.com ~all
spf-b.example.com txt v=spf1 include:netblocks4.example.com include:netblocks6.example.com
netblocks4.example.com txt v=spf1 ip4:10.10.10.0/24 ip4:192.168.0.0/16 ip4:172.16.0.0/12 -all
netblocks6.example.com txt v=spf1 ip6:2001:db8:2001::/64 ip6:2001:db8:2002::/64 ip6:2001:db8:2003::/64 -all
spf-c.example.com txt v=spf1 a:bad.%{d2} ip4:1.1.1.1 -all
bad.example.com a TIMEOUT

% spfcheck example.com --no-color -v 2 -d assets/example.db -r g 2> assets/example.com.log> assets/example.com.dot
% dot -Tpng -O assets/example.com.dot

% cat assets/example.com.log
example.com %spf[1]-dns-warn:   | > DNS QUERY (4) a spf-a.example.com - NXDOMAIN
example.com %spf[1]-dns-warn:   | > DNS QUERY (5) mx spf-a.example.com - NXDOMAIN
example.com %spf[2]-parse-warn: | | > SPF record has implicit end (?all)
example.com %spf[5]-parse-warn: | > SPF record has implicit end (?all)
example.com %spf[6]-ipt-warn:   | | > spf[6] ip4:10.10.10.0/24 - redundant entry, already have: spf[3] ip4:10.10.10.0/24
example.com %spf[6]-ipt-warn:   | | > spf[6] ip4:192.168.0.0/16 - redundant entry, already have: spf[3] ip4:192.168.0.0/16
example.com %spf[6]-ipt-warn:   | | > spf[6] ip4:172.16.0.0/12 - redundant entry, already have: spf[3] ip4:172.16.0.0/12
example.com %spf[7]-ipt-warn:   | | > spf[7] ip6:2001:db8:2001::/64 - redundant entry, already have: spf[4] ip6:2001:db8:2001::/64
example.com %spf[7]-ipt-warn:   | | > spf[7] ip6:2001:db8:2002::/64 - redundant entry, already have: spf[4] ip6:2001:db8:2002::/64
example.com %spf[7]-ipt-warn:   | | > spf[7] ip6:2001:db8:2003::/64 - redundant entry, already have: spf[4] ip6:2001:db8:2003::/64
example.com %spf[8]-dns-warn:   > DNS QUERY (13) [cache] a bad.example.com - :TIMEOUT
example.com %spf[8]-eval-error: > spf[8] a:bad.%{d2} - timeout
example.com %spf[8]-dns-warn:   > DNS QUERY (14) soa spf-c.example.com - NXDOMAIN

SPF policy

A few notes on interpretation:

Verbosity flag

The -v flag controls the verbosity level of logging on stderr:

% spfcheck example.com -v 2 --no-color -d "example.com txt v=spf1 a -a/24 mx +all"  

example.com %spf[0]-parse-warn: > spf[0] +all - usage not advisable
example.com %spf[0]-parse-warn: > spf[0] +all - default '+' can be omitted
example.com %spf[0]-ipt-warn:   > spf[0] -a/24 - overlaps with more specific spf[0] a
example.com %spf[0]-ipt-warn:   > spf[0] -a/24 - inconsistent with more specific spf[0] a
example.com %spf[0]-eval-warn:  > spf[0] mx - unusable due to null MX for example.com

domain     : example.com
ip         : 127.0.0.1
sender     : example.com
verdict    : pass
reason     : spf[0] +all
owner      : example.com
contact    : noc@dns.icann.org
num_spf    : 1
num_dnsm   : 3
num_dnsq   : 4
num_dnsv   : 0
num_checks : 4
num_warn   : 5
num_error  : 0
duration   : 0
explanation: 

Width flag

Finally, the -w flag can be used to control the width used when printing information of certain topics. Primarily meant so a markdown formatted report can be easily converted to pdf. Default value is 60, but you can make it as wide as necessary.

Installation

spfcheck requires Elixir 1.12.0 or later and can be installed as escript:

mix escript.install hex spfcheck

After installation, ~/.mix/escripts/spfcheck invokes the escript.

Use the underlying Spf modules in a project by adding spfcheck to the list of dependencies in mix.exs:

def deps do
  [
    {:spfcheck, "~> 0.7.0"}
  ]
end