#!/usr/bin/perl -w
#
# Copyright (c) 2006, Jarrod Sayers.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in
# the documentation and/or other materials provided with the
# distribution.
#
# 3. Neither the name of the author nor the names of its contributors
# may be used to endorse or promote products derived from this
# software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
# $Id: dnscheck.pl,v 1.12 2006/02/24 08:49:43 jarrod Exp $
use strict;
use Getopt::Std;
# Defaults.
my $programname = (reverse split(/\//, $0))[0];
my $defaultd = "/usr/bin/dig:/usr/local/bin/dig";
my $defaulth = `/bin/hostname`; chomp($defaulth);
my $defaultn = "/etc/namedb/named.conf";
# Variables.
my %opts = ();
my %lookupcache = ();
my $errorflag = 0;
# Sort subroutine to group by parent.
sub byparent {
# Split, reverse, join and compare.
join(".", reverse split(/\./, $a)) cmp join(".", reverse split(/\./, $b));
}
# DNS lookup caching subroutine.
sub CacheDNSLookup() {
my $command = shift || "";
my $lookupkey = $opts{"c"} ? "" : $command;
# Check hash for existing lookup.
if ((!defined $lookupcache{$command}) && ($command =~ m/\S/)) {
# Perform lookup.
$lookupcache{$lookupkey} = `$command 2>&1`;
}
# Return cached version.
return(split(/\n/, $lookupcache{$lookupkey} || ""));
}
# DNS record fetcher subroutine.
sub GetDNSRecords() {
my $zone = lc(shift) || return;
my $nameserver = lc(shift) || "a.root-servers.net";
my $progress = lc(shift) || "";
my $skiprecursion = shift || 0;
my $parentzone = "";
my $digcommand = "";
my @diglookup = "";
my @digsoalookup = "";
my $digsoaheader = "";
my $digline = "";
my $dignameserver = "";
my %dignameservers = ();
my $digprogress = "";
my $digserial = 0;
my $bailout = "";
my $wascached = 0;
# Determine parent zone.
$parentzone = $zone;
$parentzone =~ s/^[^\.]+\.//i;
# Get NS records for zone.
$digcommand = $opts{"d"}.($skiprecursion ? "" : " +norecurse")." ns \"$zone\"".
((($skiprecursion) && ($opts{"u"})) ? "" : " \"\@$nameserver\"");
$wascached = defined $lookupcache{$digcommand} ? 1 : 0;
printf("[%*s] %s%s\n", length($parentzone), $progress, $digcommand, $wascached ? " (cached)" : "") if ($opts{"v"});
@diglookup = &CacheDNSLookup($digcommand);
# Set defaults.
$digsoaheader = "";
# Process each line.
foreach $digline (@diglookup) {
# Keep everything in lowercase.
$digline = lc($digline);
# Find the 'xx IN NS xx' lines.
if ($digline =~ m/^([^\;\s]\S*)\.\s+\d+\w?\s+IN\s+NS\s+(\S+)\.\S*$/i) {
# Keep the progress and name server.
$digprogress = $1;
$dignameserver = $2;
# Confirm that requested zone was found.
if ($digprogress ne $zone) {
# Still need to work forward.
$bailout = &GetDNSRecords($zone, $dignameserver, $digprogress);
} else {
# Set the default SOA serial number.
$digserial = 0;
# Get SOA from name server.
$digcommand = $opts{"d"}." +norecurse soa \"$zone\" \"\@$dignameserver\"";
$wascached = defined $lookupcache{$digcommand} ? 1 : 0;
@digsoalookup = &CacheDNSLookup($digcommand);
# Process each line.
foreach $digline (@digsoalookup) {
# Find the ->>HEADER<<- line.
if ($digline =~ m/^\;\;\s+\-\>\>HEADER\<\<\-\s+/) {
# Keep header.
$digsoaheader = $digline;
}
# Keep everything in lowercase.
$digline = lc($digline);
# Find the 'xx IN SOA xx' line.
if ($digline =~ m/^([^\;\s]\S*)\.\s+\d+\w?\s+IN\s+SOA\s+/i) {
# Compare zones for different zone names (bug reported by Rob Archer).
if ($1 ne $zone) {
# SOA record is for a different zone.
$digserial = -1;
}
}
# Look for SOA serial number.
if ($digline =~ m/^$zone\.\s+\d+\w?\s+IN\s+SOA\s+\S+\s+\S+\s+(\d+)\s+/i) {
# Keep the SOA serial number from DiG 9.
$digserial = $1 unless ($digserial < 0);
} elsif ($digline =~ m/^\s+(\d+)\s*\;\s*serial\s*$/i) {
# Keep the SOA serial number from earlier versions of DiG.
$digserial = $1 unless ($digserial < 0);
}
}
# Correct the SOA serial number.
$digserial = 0 unless ($digserial >= 1);
# Display name server and SOA serial number if verbose.
printf(" %*s + %s\. IN NS %s\. (serial %d)%s\n", length($parentzone), "", $zone, $dignameserver, $digserial,
$wascached ? " (cached)" : "") if ($opts{"v"});
# Look for failed requests.
if ($digsoaheader =~ m/\,\s+status\:\s+(\w+)\,/) {
# Override SOA serial number.
$digserial = lc($1) unless ($1 eq "NOERROR");
}
# Add to name server list.
$dignameservers{"$dignameserver [$digserial]"} = -1;
}
}
# Dont continue if zone was found.
last if ($bailout);
}
# Update name server list if found.
if (scalar keys %dignameservers > 0) {
# Join up record.
$bailout = join("|", sort keys %dignameservers);
$bailout =~ s/\|\|/\|/g;
$bailout =~ s/(^\||\|$)//g;
# Keep output neat if verbose.
printf("\n") if ($opts{"v"});
}
# Return name servers.
return($bailout);
}
# DNS record comparison subroutine.
sub CompareDNSRecords() {
my $zone = lc(shift) || return;
my $masterorslave = shift || "unknown";
my $nsloop = "";
my %nsregistry = ();
my %nslocal = ();
my %nshash = ();
my $nsentry = "";
my $longestns = 32;
my $currentserial = 0;
my $showzone = 0;
my $nsserver = "";
my $nsserial = 0;
# Hash all of the results.
foreach $nsentry ("+reg", split(/\|/, &GetDNSRecords($zone)), "+local", split(/\|/, &GetDNSRecords($zone, $opts{"h"}, "", 1))) {
# Only use valid records.
if ($nsentry =~ m/^\+(\w+)$/) {
# Update loop.
$nsloop = $1;
} elsif ($nsentry =~ m/^(\S+)\s+\[([\w\d]+)\]$/) {
# Keep server and integer version of SOA serial number.
$nsserver = $1;
$nsserial = $2;
# Update specific hash.
if ($nsloop eq "reg") {
# Update registry hash.
$nsregistry{$nsserver} = $nsserial;
} elsif ($nsloop eq "local") {
# Update local hash.
$nslocal{$nsserver} = $nsserial;
}
# Clean up SOA serial number for integer comparison.
$nsserial = 0 unless ($nsserial =~ m/^\d+$/);
# Detect changes in SOA serial number.
if ((($currentserial > 0) && ($currentserial != $nsserial)) || ($nsserial == 0)) {
# Force zone to be reported.
$showzone = 1;
}
# Update global hash.
$nshash{$nsserver} = -1;
# Update largest SOA serial number and length.
$currentserial = $nsserial > $currentserial ? $nsserial : $currentserial;
$longestns = length($nsentry) > $longestns ? length($nsentry) : $longestns;
}
}
# Dont continue if registry and local match.
return if ((join(":", sort keys %nsregistry) eq join(":", sort keys %nslocal)) && ($currentserial > 0) && (!$opts{"r"}) &&
(!$showzone));
# Display results.
printf("%s%s%s\n", $zone, $masterorslave ne "unknown" ? " [$masterorslave]" : "", " (serial $currentserial)");
$errorflag = 1;
# Dont display name servers if only zone with error requested.
return if ($opts{"p"});
# List each name server.
foreach $nsentry ($opts{"g"} ? sort keys %nshash : sort byparent keys %nshash) {
# Print entry.
printf(" %-*s %-3s %s\n", $longestns,
defined $nsregistry{$nsentry} ? $nsentry.((($nsregistry{$nsentry} ne $currentserial) || ($opts{"i"})) ?
" [".$nsregistry{$nsentry}."]" : "") : "",
(($currentserial > 0) && ((defined $nsregistry{$nsentry}) && (defined $nslocal{$nsentry})) &&
(($nsregistry{$nsentry} ne $currentserial) || ($nslocal{$nsentry} ne $currentserial))) ? "***" :
(((defined $nsregistry{$nsentry}) && (defined $nslocal{$nsentry})) ? " :" :
(((defined $nsregistry{$nsentry}) && (!defined $nslocal{$nsentry})) ? " -" : " +")),
defined $nslocal{$nsentry} ? $nsentry.((($nslocal{$nsentry} ne $currentserial) || ($opts{"i"})) ?
" [".$nslocal{$nsentry}."]" : "") : "");
}
# Keep output neat.
printf("\n");
}
# Variables.
my $namedfile = "";
my $namedline = "";
my $namedzone = "";
my $namedtype = "";
# Get command line options.
$opts{"d"} = $defaultd;
$opts{"h"} = $defaulth;
$opts{"n"} = $defaultn;
getopts("abcd:gh:imn:prsuvz:", \%opts) || (%opts = ());
# Find usable DiG.
foreach (split(/\:/, $opts{"d"})) {
# Test executable.
if ((-e $_) && (-x $_)) {
# Set and get out.
$opts{"d"} = $_;
last;
}
}
# Strip multiple locations of DiG.
$opts{"d"} = (split(/\:/, $opts{"d"} || ""))[0];
# Process options.
if (($opts{"d"}) && (!-x $opts{"d"})) {
# Cant find DiG (bug reported by Rob Archer).
printf("%s: can't locate %s\n", $programname, $opts{"d"});
exit 1;
} elsif (($opts{"p"}) && ($opts{"r"})) {
# Cant use both -p and -r.
printf("%s: options -p and -r are mutually exclusive\n", $programname);
exit 1;
} elsif (($opts{"a"}) && ($opts{"z"})) {
# Cant use both -a and -z.
printf("%s: options -a and -z are mutually exclusive\n", $programname);
exit 1;
} elsif (($opts{"m"}) && ($opts{"s"})) {
# Cant use both -m and -s.
printf("%s: options -m and -s are mutually exclusive\n", $programname);
exit 1;
} elsif ($opts{"a"}) {
# Read in named.conf file.
open(F, $opts{"n"});
while (<F>) {
# Keep line.
$namedline = $_;
$namedline =~ s/\s/ /g;
# Remove comments.
($namedline) = split(/\/\//, $namedline);
($namedline) = split(/\#/, $namedline);
# Add to file for processing.
$namedfile = join(" ", $namedfile, $namedline || "");
}
close(F);
# Process file.
while ($namedfile =~ m/^([^\{\}]+\{([^\{\}]*|\{([^\{\}]*|\{([^\{\}]*)\})*\})*\}\s*\;)(.*)$/) {
# Keep right part of line.
$namedfile = $5;
# Only process zones.
if ($1 =~ m/^\s*zone\s+\"([\d\w\.\-]+)\"\s*\{\s*type\s+(\w+)\s*\;/) {
# Keep zone information,
$namedzone = lc($1);
$namedtype = lc($2);
# Skip the hint type.
next unless (($namedtype eq "master") || ($namedtype eq "slave"));
# Skip master or slave processing if requested.
next if (($opts{"m"}) && ($namedtype ne "master"));
next if (($opts{"s"}) && ($namedtype ne "slave"));
# Process zone.
&CompareDNSRecords($namedzone, $namedtype);
}
}
} elsif ($opts{"z"}) {
# Specific zone specifed.
&CompareDNSRecords($opts{"z"});
} else {
# No options specified.
printf("usage: %s [-cgiuv] [-p | -r] [-a [-m | -s] | -z ...] [-d ...] [-h ...] [-n ...]\n", $programname);
printf(" -a scan named.conf file for authoritative zones\n");
printf(" -b bypass local authoritative resolver check [deprecated]\n");
printf(" -c disable dig result caching\n");
printf(" -d dig_utility override default colon separated locations of dig (default $defaultd)\n");
printf(" -g disable group-by-parent name server list\n");
printf(" -h resolver_host override default local resolver (default $defaulth)\n");
printf(" -i include serial numbers held at each name server in report\n");
printf(" -m only process master zones (requires -a)\n");
printf(" -n named_conf override default location of named.conf (default $defaultn)\n");
printf(" -p simply list domains with problems (alters exit code on errors)\n");
printf(" -r show report regardless of outcome\n");
printf(" -s only process slave zones (requires -a)\n");
printf(" -u use upstream name server specified in resolv.conf\n");
printf(" -v be verbose\n");
printf(" -z domain_name check single zone\n");
exit 1;
}
# Alter exit code if needed.
if (($opts{"p"}) && ($errorflag != 0)) {
# Exit.
exit($errorflag);
}
syntax highlighted by Code2HTML, v. 0.9.1