home *** CD-ROM | disk | FTP | other *** search
- #!/usr/local/bin/perl
- # dnswalk Walk through a DNS tree, pulling out zone data and
- # dumping it in a directory tree
- #
- # $Id: dnswalk,v 8.1 1996/10/25 04:57:41 vixie Exp $
- #
- # check data collected for legality using standard resolver
- #
- # invoke as dnswalk domain > logfile
- # Options:
- # -r Recursively descend subdomains of domain
- # -f Force a zone transfer, ignore existing axfr file
- # -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
- # -D dir Use 'dir' as base directory for saved axfr files
-
- require "getopts.pl";
-
- do Getopts("D:rfiadmFl");
-
- # 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";
- }
- if (! -d $basedir) {
- mkdir($basedir,0777) || die "Cannot create $basedir: $!\n";
- }
-
- &dowalk($domain);
-
- exit;
-
- sub dowalk {
- local (@subdoms);
- local (@sortdoms);
- local ($domain)=$_[0];
- $modified=0;
- # ($file,@subdoms)=&doaxfr($domain); /* perl bug */
- @subdoms=&doaxfr($domain);
- $file=shift(@subdoms);
- if ($file && !($opt_m && !$modified)) {
- &checkfile($file,$domain);
- }
- else {
- print STDERR "skipping...\n";
- }
- @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 {
- local ($domain)=@_[0];
- local (%subdoms)=();
- local ($subdom);
- local ($serial);
- local ($foundsoa)=0; # attempt to make up for dig's poor
- # error handling
- ($path=&host2path($domain)) =~ tr/A-Z/a-z/;
- local(@servers) = &getauthservers($domain);
- &printerr("warning: $domain has only one authoritative nameserver\n") if (scalar(@servers) == 1);
- &printerr("warning: $domain has NO authoritative nameservers!\n") if (scalar(@servers) == 0);
- if ((-f "$basedir/$path/axfr") && (!$main'opt_f)) {
- open(DIG,"<$basedir/$path/axfr") || die "cannot open $basedir/$path/axfr: $!\n";
- while (<DIG>) {
- chop;
- if (/(\d+)\s*; ?serial/) {
- $serial=$1;
- }
- if (/(\S+)\s*\d+\s+NS/) {
- $subdom = $1;
- $subdom =~ tr/A-Z/a-z/;
- if ((!&equal($subdom,$domain)) && ( !$subdoms{$subdom})) {
- $subdoms{$subdom}=1;
- }
- }
- }
- # if there's no serial number in file, assume it is corrupt
- if ($serial) {
- foreach $server (@servers) {
- $authserno=&getserno($domain,$server);
- last if ($authserno);
- }
- if ($authserno <= $serial) {
- print STDERR "Using existing zone transfer info for $domain\n";
- return ("$basedir/$path/axfr", keys %subdoms);
- }
- }
- }
- &mkdirpath($path);
- SERVER:
- foreach $server (@servers) {
- $foundsoa=0;
- $SIG{'INT'}="nop;";
- print STDERR "Getting zone transfer of $domain from $server...";
- open(DIG,"dig axfr $domain \@$server 2>/dev/null |");
- open(DIGOUT,">$basedir/$path/axfr") || die "cannot open $basedir/$path/axfr: $!\n";
- @subdoms=undef;
- while (<DIG>) {
- if (/(\S+)\s*\d+\s+NS/) {
- $subdom = $1;
- $subdom =~ tr/A-Z/a-z/;
- if ((!&equal($subdom,$domain)) && ( !$subdoms{$subdom})) {
- $subdoms{$subdom}=1;
- }
- }
- elsif (/\S+\s*\d+\s+SOA/) {
- $foundsoa=1;
- }
- print DIGOUT $_;
- }
- if ($? || !$foundsoa) {
- print STDERR "failed.\n";
- close(DIGOUT);
- next SERVER;
- }
- print STDERR "done.\n";
- close(DIGOUT);
- close(DIG);
- last SERVER;
- } # foreach #
- $SIG{'INT'}=undef;
- if ($? || !$foundsoa) {
- print STDERR "All zone transfer attempts of $domain failed\n";
- print "Cannot check $domain: no available nameservers!\n";
- unlink("$basedir/$path/axfr");
- &rmdirpath($path);
- return undef;
- }
- $modified=1;
- return ("$basedir/$path/axfr", keys %subdoms);
- }
-
- # returns "edu/psu/pop" given "pop.psu.edu"
- sub host2path {
- join('/',reverse(split(/\./,$_[0])));
- }
-
- # makes sure all directories exist in "foo/bar/baz"
- sub mkdirpath {
- local (@path)=split(/\//,$_[0]);
- local ($dir)=$basedir;
- foreach $p (@path) {
- $dir .= "/".$p;
- if (! -d $dir) {
- mkdir($dir, 0777) || die "Cannot mkdir $dir: $!\n";
- }
- }
- }
-
- # remove empty directories in path
- sub rmdirpath {
- local (@path)=split(/\//,$_[0]);
- local (@dirs);
- local ($dir)=$basedir;
- foreach $p (@path) {
- push(@dirs, ($dir .= "/".$p));
- }
- foreach $p (reverse(@dirs)) {
- last if !rmdir($p);
- }
- }
-
- sub getserno {
- local ($serno)="";
- $SIG{'INT'}="nop;";
- open(DIG,"dig soa $_[0] \@$_[1] 2>/dev/null|");
- while (<DIG>) {
- if (/(\d+)\s*; ?serial/) {
- $serno=$1;
- }
- }
- close(DIG);
- $SIG{'INT'}=undef;
- return $serno;
- }
-
-
- sub getauthservers {
- local ($domain)=$_[0];
- local ($master)=&getmaster($domain);
- local ($foundmaster)=0;
- local ($s);
- open(DIG,"dig +noau ns $_[0] 2>/dev/null|");
- local(@servers)=();
- local(%servhash)=();
- while (<DIG>) {
- chop;
- tr/A-Z/a-z/;
- if (/\S+\s+\d+\s+ns\s+(\S+)/) {
- $s=$1;
- if (&equal($s,$master)) {
- $foundmaster=1; # make sure the master is at the top
- } else {
- push(@servers,$s) if ($servhash{$s}++<1);
- }
- }
- }
- close(DIG);
- if ($foundmaster) {
- unshift(@servers,$master);
- }
- return @servers;
- }
-
-
- sub getmaster { # return 'master' server for zone
- local ($master)="";
- $SIG{'INT'}="nop;";
- open(DIG,"dig soa $_[0] 2>/dev/null|");
- while (<DIG>) {
- tr/A-Z/a-z/;
- if (/.*\t\d+\tsoa\t(\S+) /) {
- $master=$1;
- }
- }
- close(DIG);
- $SIG{'INT'}=undef;
- return $master;
- }
-
- # open result of zone tranfer and check lots of nasty things
- # here's where the fun begins
- sub checkfile {
- open(FILE,"<$_[0]") || die "Cannot open $_[0]: $!\n";
- undef $errlist;
- print "Checking $domain\n";
- local (%glues)=(); # look for duplicate glue (A) records
- local ($name, $aliases, $addrtype, $length, @addrs);
- local ($prio,$mx);
- local ($soa,$contact);
- local ($lastns); # last NS record we saw
- local (@keys); # temp variable
- $soa=undef;
- $doubledom = $domain . $domain;
- $doubledom =~ s/(\W)/\\\1/g; # quote string so it's a regexp
- while (<FILE>) {
- chop;
- if (/^;/) {
- if (/(.*[Ee][Rr][Rr][Oo][Rr].*)/) {
- # print any dig errors
- print $1 ."\n";
- next;
- }
- }
- next if /^$/; # skip blanks
- # check to see if there is a "foo.bar.baz.bar.baz."
- # probably a trailing-dot death.
- if (/$doubledom/) {
- &printerr(" $_: domain occurred twice, forgot trailing '.'?\n");
- }
- split(/\t/);
- # 0=key 2=rrtype 3=value (4=value if 2=MX)
- next if ($_[0] =~ /;/);
- # complain only for mail names
- if (($_[0] =~ /[^\*][^-A-Za-z0-9.]/) && (!$opt_i) && (($_[2] eq "A") || ($_[2] eq "MX"))) {
- &printerr(" $_[0]: invalid character(s) in name\n");
- }
- if ($_[2] eq "SOA") {
- print STDERR 's' if $opt_d;
- if (! $soa) { # avoid duplicate SOA's. Argh.
- ($soa,$contact) = $_[3] =~ /(\S+)\s+(\S+)/;
- print "SOA=$soa contact=$contact\n";
- }
- } elsif ($_[2] eq "PTR") {
- print STDERR 'p' if $opt_d;
- if (scalar((@keys=split(/\./,$_[0]))) == 6 ) {
- # check if forward name exists, but only if reverse is
- # a full IP addr
- # skip ".0" networks
- if ($keys[0] ne "0") {
- if (!(($name, $aliases, $addrtype, $length, @addrs)=gethostbyname($_[3])) && !$?) {
- &printerr(" gethostbyname($_[3]): $!\n");
- }
- else {
- if (!$name) {
- &printerr(" $_[0] PTR $_[3]: unknown host\n");
- }
- elsif (!&equal($name,$_[3])) {
- &printerr(" $_[0] PTR $_[3]: CNAME (to $name)\n");
- }
- elsif (!&matchaddrlist($_[0])) {
- &printerr(" $_[0] PTR $_[3]: A record not found\n");
- }
- }
- }
- }
- } elsif (($_[2] eq "A") ) {
- print STDERR 'a' if $opt_d;
- # check to see that a reverse PTR record exists
- if (!(($name,$aliases,$addrtype,$length,@addrs)=gethostbyaddr(pack('C4', split(/\./,$_[3])),2)) && !$?) {
- &printerr(" gethostbyaddr($_[3]): $!\n");
- }
- else {
- if (!$name) {
- # hack - allow RFC 1101 netmasks encoding
- if ( $_[3] !=~ /^255/) {
- &printerr(" $_[0] A $_[3]: no PTR record\n");
- }
- }
- elsif ($opt_F && !&equal($name,$_[0])) {
- &printerr(" $_[0] A $_[3]: points to $name\n") if ((split(/\./,$name))[0] ne "localhost");
- }
- if ($main'opt_a) {
- # keep list in %glues, report any duplicates
- if ($glues{$_[3]} eq "") {
- $glues{$_[3]}=$_[0];
- }
- elsif (($glues{$_[3]} eq $_[0]) && (!&equal($lastns,$domain))) {
- &printerr(" $_[0]: possible duplicate A record (glue of $lastns?)\n");
- }
- }
- }
- } elsif ($_[2] eq "NS") {
- $lastns=$_[0];
- print STDERR 'n' if $opt_d;
- # check to see if object of NS is real
- &checklamer($_[0],$_[3]) if ($main'opt_l);
- if (!(($name, $aliases, $addrtype, $length, @addrs)=gethostbyname($_[3])) && !$?) {
- &printerr(" gethostbyname($_[3]): $!\n");
- }
- else {
- if (!$name) {
- &printerr(" $_[0] NS $_[3]: unknown host\n");
- } elsif (!&equal($name,$_[3])) {
- &printerr(" $_[0] NS $_[3]: CNAME (to $name)\n");
- }
- }
- } elsif ($_[2] eq "MX") {
- print STDERR 'm' if $opt_d;
- # check to see if object of mx is real
- ($prio,$mx)=split(/ /,$_[3]);
- if (!(($name, $aliases, $addrtype, $length, @addrs)=gethostbyname($mx)) && !$?) {
- &printerr(" gethostbyname($mx): $!\n");
- }
- else {
- if (!$name) {
- &printerr(" $_[0] MX $_[3]: unknown host\n");
- }
- elsif (!&equal($name,$mx)) {
- &printerr(" $_[0] MX $_[3]: CNAME (to $name)\n");
- }
- }
- } elsif ($_[2] eq "CNAME") {
- print STDERR 'c' if $opt_d;
- if (!(($name, $aliases, $addrtype, $length, @addrs)=gethostbyname($_[3])) && !$?) {
- &printerr(" gethostbyname($_[3]): $!\n");
- }
- else {
- if (!$name) {
- &printerr(" $_[0] CNAME $_[3]: unknown host\n");
- } elsif (!&equal($name,$_[3])) {
- &printerr(" $_[0] CNAME $_[3]: CNAME (to $name)\n");
- }
- }
- }
- }
- print STDERR "\n" if $opt_d;
- close(FILE);
- }
-
- # prints an error message, suppressing duplicates
- sub printerr {
- local ($err)=$_[0];
- if ($errlist{$err}==undef) {
- print $err;
- 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);
- }
-
- 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 {
- @c = reverse(split(/\./,$a));
- @d = reverse(split(/\./,$b));
- for ($i=0;$i<=(($#c > $#d) ? $#c : $#d) ;$i++) {
- next if $c[$i] eq $d[$i];
- return -1 if $c[$i] eq "";
- return 1 if $d[$i] eq "";
- if ($c[$i] eq int($c[$i])) {
- # numeric
- return $c[$i] <=> $d[$i];
- }
- else {
- # string
- return $c[$i] cmp $d[$i];
- }
- }
- return 0;
- }
-
- sub checklamer {
- local ($isauth)=0;
- local ($error)=0;
- # must check twice, since first query may be authoritative
- # trap stderr here and print if non-empty
- open(DIG,"dig soa +noaa $_[0] \@$_[1] 2>&1 1>/dev/null |");
- while (<DIG>) {
- print " $_[0] NS $_[1]: nameserver error (lame?):\n" if !$error;
- print;
- $error=1;
- }
- close(DIG);
- return if $error;
- open(DIG,"dig soa +noaa $_[0] \@$_[1] 2>/dev/null|");
- while (<DIG>) {
- if (/status: NXDOMAIN/) {
- $isauth=0;
- last;
- }
- if (/status: SERVFAIL/) {
- $isauth=0;
- last;
- }
- if (/;; flags.*aa.*;/) {
- $isauth=1;
- }
- }
- if (!$isauth) {
- print " $_[0] NS $_[1]: lame NS delegation\n";
- }
- close(DIG);
- return;
- }
-