I've been lurking on this list for a long time but I've never posted.

I've attached a perl program I threw together a couple years ago which does recursive SPF resolution. This might help your debugging.

For this type of testing, pass it a domain on STDIN:
echo 'gmail.com' | spf2ip.pl

I use it to create a daily whitelist of frequent domain IP's. I use it by calling it with a filename argument. It reads the file which has a domain on each line, iterates through all the domains, recursively resolving each, and creating a long list of all the SPF IP's of all the domains in the file.

In default mode, it adds enough comments to explain how it generates the results. For example, here is the output of the above command:

# echo 'gmail.com' | spf2ip.pl

#########################################################################################
# gmail.com
#########################################################################################
# -------- [REDIRECT=] _spf.google.com (depth=1)
# ---------------- [INCLUDE:] _netblocks.google.com (depth=2)
35.190.247.0/24
64.233.160.0/19
66.102.0.0/20
66.249.80.0/20
72.14.192.0/18
74.125.0.0/16
108.177.8.0/21
173.194.0.0/16
209.85.128.0/17
216.58.192.0/19
216.239.32.0/19
# ---------------- [INCLUDE:] _netblocks2.google.com (depth=2)
# ---------------- [INCLUDE:] _netblocks3.google.com (depth=2)
172.217.0.0/19
172.217.32.0/20
172.217.128.0/19
172.217.160.0/20
172.217.192.0/19
108.177.96.0/19
35.191.0.0/16
130.211.0.0/22




SPF resolution of "microsoft.com" returns 76 IP's.

Adding a "-d" argument will output a lot more debug info, whereas "-q' suppresses any debug info and only outputs IP's. The header of the perl file does some explanation, but it was only intended for me to read, so it is not a super thorough explanation.

And yes, it looks like a stereotypical perl program (ugly, quick, and functional).

Hope this helps,
-Andy







On 9/30/2019 7:21 AM, gil...@poolp.org wrote:
September 30, 2019 4:25 PM, "Denis Fondras" <open...@ledeuns.net> wrote:

On Mon, Sep 30, 2019 at 01:55:28PM +0000, gil...@poolp.org wrote:

Hello,

I'd like to bring native support for SPF in OpenSMTPD in a future release,
but for this I need a bit of help to make sure my SPF resolver works fine.

I have created a repository with a standalone executable that performs the
SPF lookup and checks if an IP address is allowed to send on behalf of the
sending domain:

https://github.com/poolpOrg/spf

https://github.com/poolpOrg/spf/blob/master/README.md

If you could test and report issues, it would be nice,

It seems IPv6 check is broken :

$ dig ledeuns.net TXT +short
"v=spf1 ip4:185.22.129.11 ip6:2a00:6060:1::1 ip6:2a00:6060:ffff::1005:ff02 -all"

$ ./spf ledeuns.net 185.22.129.1
checking if 185.22.129.1 can send for ledeuns.net: fail
$ ./spf ledeuns.net 185.22.129.11
checking if 185.22.129.11 can send for ledeuns.net: pass
$ ./spf ledeuns.net 2a00:6060:1::1
checking if 2a00:6060:1::1 can send for ledeuns.net: fail


will fix that, thanks



#!/usr/bin/perl

# PURPOSE/FUNCTION
# This program performs recursive DNS lookups of spf records (which are in TXT 
records)
# and keeps recursing until numeric addresses are reached.  Tested with IP4, 
but the regex's
# ~should~ function with IP6 addresses.

# INPUT
# filename: text file, one domain per line, # and blank lines ignored.
# STDIN: list of one or more domans to lookup

#OUTPUT
# single numeric address per line
# recursive comments will be included if ($DomainNameComments == 1)

# DEPENDENCIES:
# 1. PERL installed at the above target.
# 2. 'dig' command.

# ASSUMPTIONS:
# 1. This program DOES do recursive spf resolution (i.e. a reverse lookup of 
ALL spf-authorized sending IP's.)
# 2. This program does NOT do forward MX resolutions (i.e. lookup of where to 
send mail).

use Switch;

# Global variable which is the final product.
my @IPlist;
my $DomainNameComments = 1;
my @DomainNames;

my $ARGERROR=0;
# The HASH for the command line settings (i.e. arguments).
my %settings =  (       -q      =>      0,              # "quiet" which 
suppresses comments in output.
                                        -d      =>      0               # 
"debug" which adds debug info to the output.
                                        );
# Iterate through the args, confirming accuracy and making settings.
foreach my $arg (@ARGV) {
        if (exists $settings{$arg}) { 
                        $settings{$arg} =  1;
                }
        else {
                print "\'$arg\' is invalid argument!\n";
                $ARGERROR++;    
                }
}
# Exit if any invalid arguments.
if ($ARGERROR) { exit; }                                        
                                        
                                        
# Read domain names from STDIN.
foreach my $line ( <STDIN> ) {
                chomp( $line );
                foreach my $arg (split(' ',$line)) {
                                push(@DomainNames,$arg);
                        }
        }


                
foreach $name (@DomainNames) {
        # Do this for each domain in the [above] list.
        if(! $settings{'-q'}) {
                push 
(@IPlist,"\n#########################################################################################\n#
 
$name\n#########################################################################################");
        }
        resolve_rec($name,1);
}

foreach $ip (@IPlist) {
        print($ip,"\n");
}



#################################################################################
#################################################################################
#################################################################################

# PROCEDURE     to read the input-file specified on the command line.
sub load_file {
        my $depth = $_[1];
        my $DomainFileName = $ARGV[0];
        open(FILE, $DomainFileName) or die "Can't read file $DomainFileName 
[$!]\n";  
        my $DomainsString = do { local $/; <FILE> }; # slurp!
        close (FILE); 

        # remove comment lines
        $DomainsString =~ s/^#.*\n*//gm;
        # remove empty lines
        $DomainsString =~ s/^\s*$//gm; 
        
        return split("\n",$DomainsString);
}



# PROCEDURE to recursively resolve the SPF records down to numeric addresses.
sub resolve_rec {
        my $depth = $_[1];
        
        # Resolve the domain-name.
        if ($settings{'-d'}) { print "Processing: $_[0]\n"; }
        my $spf = `dig $_[0] TXT +noall +answer | grep "v=spf1" | sed 's/" 
"//'`;
        
        # RETURN if no spf record present.
        if ($spf eq "") {
                return;
                }
        
        # Set the result equal to the pertinent part.
        $spf =~ /.*v=spf1 (.*)"/;
        $spf = $1;
        
        # Split the result into a list (may have length of only one).
        my @spflist = split / /, $spf;
        
        # Process each item of the result.
        foreach my $rec (@spflist) {
                # IF item is a numeric address, then add it onto the 
output-list.
                if ($rec =~ /^ip4:(.*)/) {
                        push (@IPlist,$1);
                        next;
                }
                # IF item is a another SPF domain-name, then recurse.
                if ($rec =~ /(include:|redirect=)(.*)/) {
                        if(! $settings{'-q'}) {
                                # Create a label which expresses domain-name 
and depth of recursion.
                                my $labelline = "# "."--------" x $depth." 
[".uc($1)."] ".$2." (depth=".$depth.")";
                                # Add the label on to the output-list (as a 
comment line).
                                push (@IPlist,$labelline);
                        }
                        resolve_rec($2,$depth+1);
                        next;
                }
                # IF this item is an A or MX record, then resolve it and then 
push address onto the output-list.
                if ($rec =~ /^(ptr|a|mx):(.*)/) {
                        if(! $settings{'-q'}) {
                                # Create a label which expresses domain-name 
and depth of recursion.
                                my $labelline = "# "."--------" x $depth." 
[".uc($1).":] ".$2." (depth=".$depth.")";
                                # Add the label to the output-list (as a 
comment line).
                                push (@IPlist,$labelline);
                        }
                        resolve_nonrec($2, uc($1));
                        next;
                }
        }
}

# PROCEDURE to [non-recursively] resolve non-SPF records to numeric addresses.
#  - only A and MX records are likely to fall into this category.
#  - these should resolve to single numeric addresses, so no recursion 
necessary.
sub resolve_nonrec {
        my $domainname = $_[0];
        my $type = $_[1];
        $type = uc($type);
        # A "PTR" indicates an "A" (or "AAAA") lookup, so substitute "A" for 
"PTR".
        $type =~ s/PTR/A/;
        my $rec = `dig $domainname $type +noall +answer | tail -n+4 | sed 's/" 
"//'`;
        
        # Split the result into individual lines.
        my @list = split /\n/, $rec;
        # For each line, match the IP (which is at the end).
        foreach $line (@list) {
                $line =~ /\s([0-9,\.]+)$/s;
                my $ip = $1;
                # Add the IP to the output-list.
                push (@IPlist,$ip);
        }
        
}

Reply via email to