Skip to content

Instantly share code, notes, and snippets.

@stefansundin
Last active October 28, 2018 19:50
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save stefansundin/4387f14c19126e8dd534d70fa0a5dbd7 to your computer and use it in GitHub Desktop.
Save stefansundin/4387f14c19126e8dd534d70fa0a5dbd7 to your computer and use it in GitHub Desktop.
Count number of lookups in an SPF record.
#!/usr/bin/env ruby
# https://tools.ietf.org/html/rfc4408#section-10.1
# SPF implementations MUST limit the number of mechanisms and modifiers
# that do DNS lookups to at most 10 per SPF check, including any
# lookups caused by the use of the "include" mechanism or the
# "redirect" modifier. If this number is exceeded during a check, a
# PermError MUST be returned. The "include", "a", "mx", "ptr", and
# "exists" mechanisms as well as the "redirect" modifier do count
# against this limit. The "all", "ip4", and "ip6" mechanisms do not
# require DNS lookups and therefore do not count against this limit.
# The "exp" modifier does not count against this limit because the DNS
# lookup to fetch the explanation string occurs after the SPF record
# has been evaluated.
# When evaluating the "mx" and "ptr" mechanisms, or the %{p} macro,
# there MUST be a limit of no more than 10 MX or PTR RRs looked up and
# checked.
# https://en.wikipedia.org/wiki/Sender_Policy_Framework
# Another safeguard is the maximum of ten mechanisms querying DNS, i.e.
# any mechanism except from IP4, IP6, and ALL. Implementations can abort
# the evaluation with result SOFTERROR when it takes too long or a DNS
# query times out, but they must return PERMERROR if the policy directly
# or indirectly needs more than ten queries for mechanisms.
# Any redirect= also counts towards this processing limit.
# A typical SPF HELO policy v=spf1 a -all may execute up to three DNS
# queries: (1) TXT, (2) SPF (obsoleted by RFC 7208), and (3) A or AAAA.
# This last query counts as the first mechanism towards the limit (10).
# In this example it is also the last, because ALL needs no DNS lookup.
# https://serverfault.com/questions/584708/is-the-10-dns-lookup-limit-in-the-spf-spec-typically-enforced
require "resolv"
def lookup(domain, depth=0)
records = Resolv::DNS.open.getresources(domain, Resolv::DNS::Resource::IN::TXT)
record = records.find { |r|
r.data.start_with?("v=spf")
}
if !record
puts "Warning: Missing SPF record for #{domain}"
puts records.map(&:data)
return 0
end
record = record.data
puts "#{domain}: #{record}" if depth == 0 || $verbose
mechanisms = record.split(" ")[1..-1].map do |m|
# remove qualifiers
if %w[+ ? ~ -].include?(m[0])
m[1..-1]
else
m
end
end.select do |m|
# everything except "ip4:", "ip6:", and "all" count towards a limit
!m.start_with?("ip4:","ip6:") && m != "all"
end
# puts "mechanisms: #{mechanisms}"
lookups = 1 # count the lookup we just did
mechanisms.each do |m|
if m.start_with?("include:", "redirect=")
new_domain = m.match(/^(?:include:|redirect=)(.+)$/)[1]
domain_lookups = lookup(new_domain, depth+1)
puts "#{new_domain}: #{domain_lookups} lookup#{"s" if domain_lookups != 1}" if depth == 0 || $verbose
lookups += domain_lookups
elsif m == "a"
lookups += 1
else
abort("Unsupported: #{m}")
end
end
return lookups
end
$verbose = false
if ARGV.first == "-v"
$verbose = true
ARGV.shift
end
if !ARGV.first
puts "Please supply the domain name as the first argument."
exit(1)
end
puts "Total: #{lookup(ARGV.first)} lookups"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment