#!/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 (<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