#!/usr/local/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 () { # 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); }