The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
#!/usr/local/bin/perl

# Do some sanity checks on the DNS forward and reverse files.

# This checks for duplicate host and ip entries in DNS forward,
# duplicate ip in DNS reverse, DNS forward entries with no reverse, DNS
# forward entries that don't match their reverses, and DNS reverse
# entries with no forward entry.

#
# This file is copyright (C)  Philip Guenther <guenther@gac.edu>, 1999
# 
# This program is free software; redistribution and modification
# in any form is explicitly permitted provided that all source
# code versions retain this copyright notice and the following
# disclaimer.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
#
# Questions, ideas and suggestions are enthusiasticly welcomed
# by the author.
#

use strict;

# Where is the named.conf file?
my($named_conf) = "/etc/named.conf";

if (@ARGV) {
    $named_conf = shift;
}

sub regularize;

my(%ip, %files, $file, $named_dir, %name, $domain, $name, $ip, %nomatch);

$| = 1;
$\ = "\n";

print "Checking DNS forward files...";

# Parse the named.conf file to find the primary domains
Parser->parse_file($named_conf);

chdir($named_dir);

while(($domain, $file) = each(%files)) {
    next if $domain =~ /in-addr\.arpa/i;
    $domain =~ tr[A-Z][a-z];
    print " - Scanning $file...";
    open(FORW, $file) || die "unable to open $file: $!";
    while(<FORW>) {
	my($nowarn, $nomatch);
	$nowarn = 1 if /;NOWARN/;
	$nomatch = 1 if /;NOMATCH/;
	next if /;SKIP/;
	s:;.*::;
	next if /^\s*$/;
	if (/\sA\s/) {
	    if (/^[\w@]/) {
		if (/^([@\w-.]+)\s+(?:\d+\s+)?(?:IN\s+)?A\s+([\d.]+)/) {
		    $ip = $2;
		    $name = regularize($1, $domain);
		    if ($name{$name}) {
			print "$name occurs multiple times" unless $nowarn;
		    } else {
			$name{$name} = $ip;
			$nomatch{$name} = 1 if $nomatch;
		    }
		} else {
		    chop;
		    print "Invalid line?\n$_";
		}
	    } elsif (/^\s+(?:\d+\s+)?(?:IN\s+)?A\s+([\d.]+)/) {
		$ip = $1;
		$name{$name} .= " $ip";
	    } else {
		print "Syntax error on line $.: $_";
		next;
	    }
	    if ($ip{$ip}) {
		print "Duplicate ip $ip with hosts $ip{$ip} and $name"
						unless $nowarn;
		next;
	    }
	    if ($name !~ /\S/ || ! $ip) {
		print "Empty name, say wha?  \$. = $., file = $file";
		next;
	    }
	    $ip{$ip} = $name;
	} elsif (/^([@\w-]+)\s+(?:\d+\s+)?(?:IN\s+)?PTR\s+(\d+\.){4}in-addr\.arpa/i) {
	    # A network tag
	    chop($ip = $2);		# rip off the trailing dot
	    $name = regularize($1, $domain);
	    $ip = join(".", reverse split(/\./, $ip));	# reverse the addr
	    if ($name{$name}) {
		print "$name occurs multiple times" unless $nowarn;
	    } else {
		$name{$name} = $ip;
		$nomatch{$name} = 1 if $nomatch;
	    }
	    if ($ip{$ip}) {
		print "Duplicate ip $ip with hosts $ip{$ip} and $name"
						unless $nowarn;
		next;
	    }
	    if ($name !~ /\S/ || $ip) {
		print "Empty name, say wha?  \$. = $., file = $file";
		next;
	    }
	    $ip{$ip} = $name;
	} elsif (/^([@\w-.]+)\s+(?:\d+\s+)?(?:IN\s+)?CNAME\s+([\w-.]+)/) {
	    $name = regularize($1, $domain);
	    if ($name{$name}) {
		print "$name occurs multiple times" unless $nowarn;
	    } else {
		$name{$name} = regularize($2, $domain);
		$nomatch{$name} = 1 if $nomatch;
	    }
	} elsif (/^\$ORIGIN\s+([\w-.]+)/i) {
	    $domain = regularize($1, $domain);
	} else {
	    next if /\sNS\s/;
	    next if /\sTXT\s/;
	    next if /\sHINFO\s/;
	    next if /\sMX\s/;
	    if (/\sSOA\s/) {
		if (/\(/) {
		    while (<FORW>) {
			s/;.*//;
			last if /\)/;
		    }
		}
		next;
	    }
	    print "Syntax error on line $.: unknown format: $_";
	    next;
	}

    }
    close(FORW);
}

my(%revname, %revip);
print "Checking DNS reverse files...";

while(($domain, $file) = each(%files)) {
    next unless $domain =~ s/\.in-addr\.arpa\.?//i;
    # flip the domain around
    $domain = join(".", reverse split(/\./, $domain));
    print " - Scanning $file...";
    open(REV, $file) || die "unable to open $file: $!";
    while(<REV>) {
	my($nowarn, $nomatch);
	$nowarn = 1 if /;NOWARN/;
	$nomatch = 1 if /;NOMATCH/;
	next if /;SKIP/;
	s:;.*::;
	next if /^\s*$/;
	if (/\sPTR\s/) {
	    if (/^(\d+)(\.\d+)?\s+(?:IN\s+)?PTR\s+([\w-.]+)\.\s*$/) {
		my($host, $subnet, $name) = ($1, $2, $3);
		$ip = "$domain$subnet.$host";
		if ($revname{$name}) {
		    $revname{$name} .= " $ip";
		} else {
		    $revname{$name} = $ip;
		}
		if ($revip{$ip}) {
		    print "$ip occurs multiple times";
		} else {
		    $revip{$ip} = $name;
		    $nomatch{$ip} = 1 if $nomatch;
		}
	    } else {
		print "Unknown PTR format on line $.: $_" unless $nowarn;
		next;
	    }
	} elsif (/^\$ORIGIN\s+([\w-.]+)/i) {
	    if (substr($1, -1, 1) eq '.') {
		$domain = $1;
		chop($domain);
		if ($domain =~ s/\.in-addr\.arpa//i) {
		    $domain = join(".", reverse split(/\./, $domain));
		} else {
		    print "\$ORIGIN set to out of zone: $file";
		}
	    } else {
		$domain .= join(".", '', reverse split(/\./, $1));
	    }
	    next;
	} else {
	    next if /\sNS\s/;
	    next if /\sA\s/;		# netmask declaration
	    next if /\sTXT\s/;
	    next if /\sHINFO\s/;
	    if (/\sSOA\s/) {
		if (/\(/) {
		    while (<REV>) {
			s/;.*//;
			last if /\)/;
		    }
		}
		next;
	    }
	    print "Syntax error on line $.: unknown format: $_";
	    next;
	}
    }
    close(REV);
}

print "Checking DNS reverse against DNS forward...";

while(($ip, $name) = each %revip) {
    next if $nomatch{$ip};
    if (defined $ip{$ip}) {
	if (!defined($name{$name})) {
	    print <<EOM
 * Name for ip $ip in reverse ($name) not in forward.
   Forward file thinks it should be $ip{$ip}
EOM
	} else {
	    if ($name{$name} !~ /\b$ip\b/) {
		print " * $ip isn't an address for $name in the DNS forward";
	    }
	    if ($name ne $ip{$ip}) {
		print <<EOM
 * Name for ip $ip in reverse ($name) is in the forward,
   but the forward think its address(es) is/are $name{$name}.
EOM
	    }
	}
	delete $ip{$ip};
    } else {
	print " * No forward entry for $ip (name = $name)";
    }
}

print "Checking for DNS forward entries with no reverse...";
foreach (sort keys %ip) {
    if (/^(?:127\..*|\d+)$/) {
	# These should be fine
    } else {
	print " * No reverse entry for $ip{$_}, ip = $_"
		    unless $nomatch{$ip{$_}};
    }
}

sub regularize {
    my($name, $domain) = @_;
    if ($name eq "@") {
	$name = $domain;
    } elsif (substr($name, -1, 1) ne '.') {
	$name .= ".$domain";
    } else {
	chop($name);
    }
    $name
}

package Parser;

use BIND::Conf_Parser;
use vars qw(@ISA);
BEGIN{ @ISA = qw(BIND::Conf_Parser); }

sub handle_option {
    my($self, $option, $argument) = @_;
    return unless $option eq "directory";
    $named_dir = $argument;
}

sub handle_zone {
    my($self, $name, $class, $type, $options) = @_;
    return unless $type eq "master" && $class eq "in";
    $files{$name} = $options->{file};
}