#!/usr/bin/perl # # dnswalk Walk through a DNS tree, pulling out zone data and # dumping it in a directory tree # # $Id: dnswalk,v 1.18 1997/10/06 13:23:58 barr Exp barr $ # # check data collected for legality using standard resolver # # invoke as dnswalk domain > logfile # Options: # -r Recursively descend subdomains of domain # -i Suppress check for invalid characters in a domain name. # -a turn on warning of duplicate A records. # -d Debugging # -m Check only if the domain has been modified. (Useful only if # dnswalk has been run previously.) # -F Enable "facist" checking. (See man page) # -l Check lame delegations # # modern perl needs this explicitly globalized: # # File-scope "globals" my %errlist; my %num_error; use Getopt::Std; use IO::Socket; use Net::DNS; getopts("D:rfiadmFl"); $num_error{'FAIL'}=0; # failures to access data $num_error{'WARN'}=0; # questionable data $num_error{'BAD'}=0; # bad data # Where all zone transfer information is saved. You can change this to # something like /tmp/dnswalk if you don't want to clutter up the current # directory if ($opt_D) { $basedir = $opt_D; } else { $basedir = "."; } ($domain = $ARGV[0]) =~ tr/A-Z/a-z/; if ($domain !~ /\.$/) { die "Usage: dnswalk domain\ndomain MUST end with a '.'\n \tOptions:\n \t\t-r\tRecursively descend subdomains of domain\n \t\t-i\tSuppress check for invalid characters in a domain name.\n \t\t-a\tturn on warning of duplicate A records.\n \t\t-d\tDebugging\n \t\t-m\tCheck only if the domain has been modified. (Useful only if\n \t\t\tdnswalk has been run previously.)\n \t\t-F\tEnable 'facist' checking. (See man page)\n \t\t-l\tCheck lame delegations\n\n"; } if (! -d $basedir) { mkdir($basedir,0777) || die "FAIL: Cannot create $basedir: $!\n"; } &dowalk($domain); print STDERR "$num_error{'FAIL'} failures, $num_error{'WARN'} warnings, $num_error{'BAD'} errors.\n"; exit $num_error{'BAD'}; sub dowalk { my (@subdoms); my (@sortdoms); my ($domain)=$_[0]; $modified=0; return unless $domain; print "Checking $domain\n"; @subdoms=&doaxfr($domain); &check_zone($domain) if (@zone); undef @zone; return if (!@subdoms); @sortdoms = sort byhostname @subdoms; local ($subdom); if ($opt_r) { foreach $subdom (@sortdoms) { &dowalk($subdom); } } } # try to get a zone transfer, trying each listed authoritative server if # if fails. sub doaxfr { my ($domain) = @_; my %subdoms; my @servers = getauthservers($domain); printerr("BAD", "$domain has only one authoritative nameserver\n") if scalar(@servers) == 1; printerr("BAD", "$domain has NO authoritative nameservers!\n") if scalar(@servers) == 0; my @zone; SERVER: foreach my $server (@servers) { print STDERR "Getting zone transfer of $domain from $server..."; my $res = Net::DNS::Resolver->new; $res->nameservers($server); @zone = $res->axfr($domain); unless (@zone) { print STDERR "failed\n"; printerr("FAIL", "Zone transfer of $domain from $server failed: " . $res->errorstring . "\n"); next SERVER; } foreach my $rr (@zone) { if ($rr->type eq "NS") { my $subdom = lc($rr->name); # lowercase for comparison if (!equal($subdom, $domain) && !$subdoms{$subdom}) { $subdoms{$subdom} = 1; } } } print STDERR "done.\n"; last SERVER; # Exit loop after successful transfer } unless (@zone) { printerr("BAD", "All zone transfer attempts of $domain failed!\n"); return (); } return keys %subdoms; } sub getauthservers { my ($domain) = @_; return unless $domain; my $master = getmaster($domain); return unless $master; # No SOA/master found, abort. my $res = Net::DNS::Resolver->new; my $ns_req = $res->query($domain, "NS"); unless (defined $ns_req && $ns_req->header->ancount > 0) { printerr("FAIL", "No nameservers found for $domain: " . $res->errorstring . "\n"); return; } my @servers; my %servhash; my $foundmaster = 0; foreach my $ns_rr ($ns_req->answer) { my $ns_name = lc($ns_rr->nsdname); if (equal($ns_name, $master)) { $foundmaster = 1; # Put master at top } else { unless ($servhash{$ns_name}) { push @servers, $ns_name; $servhash{$ns_name} = 1; } } } # Put master at the top if found unshift @servers, $master if $foundmaster; return @servers; } # return 'master' server for zone sub getmaster { my ($zone) = @_; return "" unless $zone; my $res = Net::DNS::Resolver->new; my $soa_req = $res->query($zone, "SOA"); unless (defined $soa_req) { printerr("FAIL", "Cannot get SOA record for $zone: " . $res->errorstring . "\n"); return ""; } foreach my $rr ($soa_req->answer) { if ($rr->type eq "SOA") { return $rr->mname; } } printerr("BAD", "SOA record not found for $zone\n"); return ""; } # open result of zone tranfer and check lots of nasty things # here's where the fun begins use Net::DNS; use Socket; sub check_zone { my ($domain) = @_; my %glues; my ($name, $aliases, $addrtype, $length, @addrs); my $lastns; foreach my $rr (@zone) { # Warn about invalid characters in A or MX names if (($rr->type eq "A" || $rr->type eq "MX") && !$opt_i && $rr->name =~ /[^\*][^-A-Za-z0-9.]/) { printerr("WARN", $rr->name . ": invalid character(s) in name\n"); } if ($rr->type eq "SOA") { print STDERR 's' if $opt_d; my @soa = split(/\s+/, $rr->rdstring); my $rname = $soa[1]; print "SOA=" . $rr->mname . " contact=" . $rname . "\n"; # Basic contact format check if ($rname =~ /@/ || $rname !~ /[^.]+(\.[^.]+){2,}/) { printerr("WARN", "SOA contact name (" . $rname . ") is invalid\n"); } } elsif ($rr->type eq "PTR") { print STDERR 'p' if $opt_d; my @keys = split(/\./, $rr->name); if (scalar(@keys) == 6 && $keys[0] ne "0") { ($name, $aliases, $addrtype, $length, @addrs) = gethostbyname($rr->ptrdname); if (!$name) { printerr("WARN", $rr->name . " PTR " . $rr->ptrdname . ": unknown host\n"); } elsif (!equal($name, $rr->ptrdname)) { printerr("WARN", $rr->name . " PTR " . $rr->ptrdname . ": CNAME (to $name)\n"); } elsif (!matchaddrlist($rr->name)) { printerr("WARN", $rr->name . " PTR " . $rr->ptrdname . ": A record not found\n"); } } } elsif ($rr->type eq "A") { print STDERR 'a' if $opt_d; my $packed_ip = pack('C4', split(/\./, $rr->address)); ($name, $aliases, $addrtype, $length, @addrs) = gethostbyaddr($packed_ip, AF_INET); if (!$name && $rr->address !~ /^255/) { printerr("WARN", $rr->name . " A " . $rr->address . ": no PTR record\n"); } elsif ($opt_F && $name && !equal($name, $rr->name)) { # Ignore localhost and similar special cases if (index((split(/\./, $rr->name, 2))[0] . "-", (split(/\./, $name, 2))[0]) == -1) { printerr("WARN", $rr->name . " A " . $rr->address . ": points to $name\n") if (split(/\./, $name))[0] ne "localhost"; } } if ($main::opt_a) { # Duplicate glue record detection if (!$glues{$rr->address}) { $glues{$rr->address} = $rr->name; } elsif ($glues{$rr->address} eq $rr->name && !equal($lastns, $domain)) { printerr("WARN", $rr->name . ": possible duplicate A record (glue of $lastns?)\n"); } } } elsif ($rr->type eq "NS") { $lastns = $rr->name; print STDERR 'n' if $opt_d; checklamer($rr->name, $rr->nsdname) if $main::opt_l; if (isipv4addr($rr->nsdname)) { printerr("BAD", $rr->name . " NS " . $rr->nsdname . ": Nameserver must be a hostname\n"); } ($name, $aliases, $addrtype, $length, @addrs) = gethostbyname($rr->nsdname); if (!$name) { printerr("BAD", $rr->name . " NS " . $rr->nsdname . ": unknown host\n"); } elsif (!equal($name, $rr->nsdname)) { printerr("BAD", $rr->name . " NS " . $rr->nsdname . ": CNAME (to $name)\n"); } } elsif ($rr->type eq "MX") { print STDERR 'm' if $opt_d; if (isipv4addr($rr->exchange)) { printerr("BAD", $rr->name . " MX " . $rr->exchange . ": Mail exchange must be a hostname\n"); } ($name, $aliases, $addrtype, $length, @addrs) = gethostbyname($rr->exchange); if (!$name) { printerr("WARN", $rr->name . " MX " . $rr->exchange . ": unknown host\n"); } elsif (!equal($name, $rr->exchange)) { printerr("WARN", $rr->name . " MX " . $rr->exchange . ": CNAME (to $name)\n"); } } elsif ($rr->type eq "CNAME") { print STDERR 'c' if $opt_d; ($name, $aliases, $addrtype, $length, @addrs) = gethostbyname($rr->cname); if (isipv4addr($rr->cname)) { printerr("BAD", $rr->name . " CNAME " . $rr->cname . ": alias must be a hostname\n"); } if (!$name) { printerr("WARN", $rr->name . " CNAME " . $rr->cname . ": unknown host\n"); } elsif (!equal($name, $rr->cname)) { printerr("WARN", $rr->name . " CNAME " . $rr->cname . ": CNAME (to $name)\n"); } } } print STDERR "\n" if $opt_d; close(FILE); } # prints an error message, suppressing duplicates sub printerr { my ($type, $err) = @_; unless (exists $errlist{$err}) { print "$type: $err"; $num_error{$type}++; print STDERR "!" if $opt_d; $errlist{$err} = 1; } else { print STDERR "." if $opt_d; } } sub equal { # Do case-insensitive string comparisons local ($one)= $_[0]; local ($two)= $_[1]; $stripone=$one; if (chop($stripone) eq '.') { $one=$stripone; } $striptwo=$two; if (chop($striptwo) eq '.') { $two=$striptwo; } $one =~ tr/A-Z/a-z/; $two =~ tr/A-Z/a-z/; return ($one eq $two); } # check if argument looks like an IPv4 address sub isipv4addr { my ($host)=$_[0]; my ($one,$two,$three,$four); ($one,$two,$three,$four)=split(/\./,$host); my $whole="$one$two$three$four"; # strings evaluated as numbers are zero return (($whole+0) eq $whole); } sub matchaddrlist { local($match)=pack('C4', reverse(split(/\./,$_[0],4))); local($found)=0; foreach $i (@addrs) { $found=1 if ($i eq $match); } return $found; } # there's a better way to do this, it just hasn't evolved from # my brain to this program yet. sub byhostname { my @c = reverse split(/\./, $a); my @d = reverse split(/\./, $b); my $max = @c > @d ? $#c : $#d; for my $i (0 .. $max) { next if $c[$i] eq $d[$i]; return -1 if !defined($c[$i]) || $c[$i] eq ""; return 1 if !defined($d[$i]) || $d[$i] eq ""; if ($c[$i] =~ /^\d+$/ && $d[$i] =~ /^\d+$/) { return $c[$i] <=> $d[$i]; } else { return $c[$i] cmp $d[$i]; } } return 0; } sub checklamer { my ($zone, $nameserver) = @_; my $res = Net::DNS::Resolver->new; unless ($res->nameservers($nameserver)) { printerr("FAIL", "Cannot find address for nameserver: " . $res->errorstring . "\n"); return; } my $packet = Net::DNS::Packet->new($zone, "SOA", "IN"); my $soa_req = $res->send($packet); unless (defined $soa_req) { printerr("FAIL", "Cannot get SOA record for $zone from $nameserver (lame?): " . $res->errorstring . "\n"); return; } unless ($soa_req->header->aa) { printerr("BAD", "$zone NS $nameserver: lame NS delegation\n"); } }