Last active
October 28, 2018 19:50
-
-
Save stefansundin/4387f14c19126e8dd534d70fa0a5dbd7 to your computer and use it in GitHub Desktop.
Count number of lookups in an SPF record.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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