plugins/check_dns-delegation
changeset 10 4243e22505f9
parent 9 b2a26d05b063
child 11 cd4343d59850
equal deleted inserted replaced
9:b2a26d05b063 10:4243e22505f9
       
     1 #! /usr/bin/perl
       
     2 # source: https://ssl.schlittermann.de/hg/ius/nagios/nagios-plugin-dns-serial
       
     3 # © 2014 Heiko Schlittermann <hs@schlittermann.de>
       
     4 
       
     5 =head1 NAME
       
     6 
       
     7  check_dns-serial - check the dns serial number from multiple sources
       
     8 
       
     9 =head1 SYNOPSIS
       
    10 
       
    11  check_dns-serial [options] DOMAINS
       
    12 
       
    13 =head1 DESCRIPTION
       
    14 
       
    15 B<check_dns-serial> is designed as a Icinga/Nagios plugin to verify that
       
    16 all responsible NS have the same serial number for their zones.
       
    17 
       
    18 Domains we are not responsible for are marked as B<critical>.
       
    19 
       
    20 The list of domains may consist of the following items:
       
    21 
       
    22 =over
       
    23 
       
    24 =item I<domain>
       
    25 
       
    26 A plain domain name.
       
    27 
       
    28 =item B<file://>I<file>
       
    29 
       
    30 A file name containing the domains, line by line.
       
    31 
       
    32 =item B<local:>
       
    33 
       
    34 This item uses the output of C<named-checkconf -p> to get the list of
       
    35 master/slave zones. The 127.in-addr.arpa, 168.192.in-addr.arpa, and
       
    36 0.in-addr.arpa, and 127.in-addr.arpa zones are suppressed.
       
    37 
       
    38 =back
       
    39 
       
    40 =cut
       
    41 
       
    42 use 5.014;
       
    43 use strict;
       
    44 use warnings;
       
    45 use Getopt::Long qw(GetOptionsFromArray);
       
    46 use Net::DNS;
       
    47 use Pod::Usage;
       
    48 use if $ENV{DEBUG} => 'Smart::Comments';
       
    49 
       
    50 sub uniq { my %h; @h{@_} = (); return keys %h; }
       
    51 my @extns = qw(8.8.8.8 8.8.4.4);
       
    52 
       
    53 package Net::DNS::Resolver {
       
    54     use Storable qw(freeze);
       
    55     sub new {
       
    56         my $class = shift;
       
    57         state %cache;
       
    58         return $cache{freeze \@_} //= $class->SUPER::new(@_);
       
    59     }
       
    60 }
       
    61 
       
    62 # return a list of the zones known to the local
       
    63 # bind
       
    64 sub get_local_zones {
       
    65     my @conf;
       
    66     open(my $z, '-|', 'named-checkconf -p');
       
    67     while (<$z>) {
       
    68         state $line;
       
    69         s/^\s*(.*?)\s*$/$1 /;
       
    70         chomp($line .= $_);    # continuation line
       
    71         if (/\A\}/) {          # config item done
       
    72             $line =~ s/\s$//;
       
    73             push @conf, $line;
       
    74             $line = '';
       
    75         }
       
    76     }
       
    77     return grep { 
       
    78 	# FIXME: 172.0 .. 172.31 is missing
       
    79 	not /\b(?:0|127|10|168\.192|255)\.in-addr\.arpa$/ and
       
    80 	not /^localhost$/;
       
    81     } map { /zone\s"(\S+)"\s/ } grep { /type (?:master|slave)/ } @conf;
       
    82 }
       
    83 
       
    84 sub get_domains {
       
    85     my @sources = @_;
       
    86     my @domains = ();
       
    87 
       
    88     foreach my $src (@sources) {
       
    89 
       
    90         if ($src =~ m{^(?:(/.*)|file://(/.*))}) {
       
    91             open(my $f, '<', $1) or die "$0: Can't open $1 for reading: $!\n";
       
    92             push @domains, map { /^\s*(\S+)\s*/ } <$f>;
       
    93             next;
       
    94         }
       
    95 
       
    96         if ($src =~ m{^local:}) {
       
    97             push @domains, get_local_zones;
       
    98             next;
       
    99         }
       
   100 
       
   101         push @domains, $src;
       
   102     }
       
   103 
       
   104     return @domains;
       
   105 }
       
   106 
       
   107 # return a list of "official" nameservers
       
   108 sub ns {
       
   109     my $domain = shift;
       
   110     ### assert: @_ % 2 == 0
       
   111     my %resflags = (nameservers => \@extns, @_);
       
   112     my $aa = delete $resflags{aa};
       
   113     my $nameservers = join ',' => @{$resflags{nameservers}};
       
   114     my @ns;
       
   115 
       
   116     my $r = Net::DNS::Resolver->new(%resflags);
       
   117     my $q;
       
   118 
       
   119     for (my $i = 3; $i; --$i) {
       
   120         $q = $r->query($domain, 'NS') and last;
       
   121     }
       
   122     die $r->errorstring . "\@$nameservers\n" if not $q;
       
   123 
       
   124     die "no aa \@$nameservers\n" if $aa and not $q->header->aa;
       
   125     push @ns, map { $_->nsdname } grep { $_->type eq 'NS' } $q->answer;
       
   126 
       
   127     return sort @ns;
       
   128 }
       
   129 
       
   130 sub serial {
       
   131     my $domain = shift;
       
   132     my %resflags = (nameservers => \@extns, @_);
       
   133     my $nameservers = join ',' => @{$resflags{nameservers}};
       
   134 
       
   135     my $r = Net::DNS::Resolver->new(%resflags);
       
   136     my $q;
       
   137 
       
   138     for (my $i = 3; $i; --$i) {
       
   139         $q  = $r->query($domain, 'SOA') and last;
       
   140     }
       
   141     die $r->errorstring, "\@$nameservers\n" if not $q;
       
   142 
       
   143     return (map { $_->serial } grep { $_->type eq 'SOA' } $q->answer)[0];
       
   144 }
       
   145 
       
   146 # - the nameservers known from the ns records
       
   147 # - from the primary master if this is not one of the
       
   148 #   NS for the zone
       
   149 # - from a list of additional (hidden) servers
       
   150 #
       
   151 # OK - if the serial numbers are in sync
       
   152 # WARNING - if there is some difference
       
   153 # CRITICAL - if the serial cannot be found at one of the sources
       
   154 
       
   155 sub ns_ok {
       
   156     my ($domain, $reference) = @_;
       
   157 
       
   158     my @errs;
       
   159     my @our = eval { sort +ns($domain, nameservers => [$reference], aa => 1) };
       
   160     push @errs, $@ if $@;
       
   161     my @their = eval { sort +ns($domain) };
       
   162     push @errs, $@ if $@;
       
   163 
       
   164     if (@errs) {
       
   165         chomp @errs;
       
   166         die join(', ' => @errs) . "\n";
       
   167     }
       
   168     
       
   169     if ("@our" ne "@their") {
       
   170         local $" = ', ';
       
   171         die "NS differ (our @our) vs (their @their)\n";
       
   172     }
       
   173 
       
   174     return uniq sort @our, @their;
       
   175 }
       
   176 
       
   177 sub serial_ok {
       
   178     my ($domain, @ns) = @_;
       
   179     my @serials = map { my $s = serial $domain, nameservers => [$_]; "$s\@$_" } @ns;
       
   180 
       
   181     if (uniq(map { /(\d+)/ } @serials) != 1) {
       
   182         die "serials do not match: @serials\n";
       
   183     }
       
   184     
       
   185     $serials[0] =~ /(\d+)/;
       
   186     return $1;
       
   187 }
       
   188 
       
   189 sub main {
       
   190     my @argv          = @_;
       
   191     my $opt_reference = '127.0.0.1';
       
   192     my $opt_progress  = -t;
       
   193 
       
   194     GetOptionsFromArray(
       
   195         \@argv,
       
   196         'reference=s' => \$opt_reference,
       
   197         'progress!'   => \$opt_progress,
       
   198         'h|help'      => sub { pod2usage(-verbose => 1, -exit => 0) },
       
   199         'm|man'       => sub {
       
   200             pod2usage(
       
   201                 -verbose   => 2,
       
   202                 -exit      => 0,
       
   203                 -noperldoc => system('perldoc -V 2>/dev/null 1>&2')
       
   204             );
       
   205         }
       
   206       )
       
   207       and @argv
       
   208       or pod2usage;
       
   209     my @domains = get_domains(@argv);
       
   210 
       
   211     my (@OK, %CRITICAL);
       
   212     foreach my $domain (@domains) {
       
   213         print STDERR "$domain " if $opt_progress;
       
   214 
       
   215         my @ns = eval { ns_ok($domain, $opt_reference) };
       
   216 	if ($@) { 
       
   217             $CRITICAL{$domain} = $@;
       
   218             say STDERR 'ns not ok' if $opt_progress;
       
   219             next;
       
   220         }
       
   221         print STDERR 'ok(ns) ' if $opt_progress;
       
   222 
       
   223         my @serial = eval { serial_ok($domain, @ns) };
       
   224         if ($@) {
       
   225             $CRITICAL{$domain} = $@;
       
   226             say STDERR 'serial not ok' if $opt_progress;
       
   227             next;
       
   228         }
       
   229         say STDERR 'ok(serial)' if $opt_progress;
       
   230         push @OK, $domain;
       
   231 
       
   232     }
       
   233 
       
   234     #    use DDP;
       
   235     #    p @OK;
       
   236     #    p %CRITICAL;
       
   237 
       
   238     if (my $n = keys %CRITICAL) {
       
   239         print "CRITICAL: $n of " . @domains . " domains\n",
       
   240           map { "$_: $CRITICAL{$_}" } sort keys %CRITICAL;
       
   241         return 2;
       
   242     }
       
   243 
       
   244     say 'OK: ' . @OK . ' domains checked';
       
   245     return 0;
       
   246 
       
   247 }
       
   248 
       
   249 exit main @ARGV unless caller;
       
   250 
       
   251 __END__
       
   252 
       
   253 =head1 OPTIONS
       
   254 
       
   255 =over
       
   256 
       
   257 =item B<--reference>=I<address>
       
   258 
       
   259 The address of the reference server for our own domains (default: 127.0.0.1)
       
   260 
       
   261 =item B<--progress>
       
   262 
       
   263 Tell about the progress. (default: on if input is connected to a terminal)
       
   264 
       
   265 =back
       
   266 
       
   267 =cut
       
   268 
       
   269 # vim:sts=4 ts=8 sw=4 et: