plugins/check_dns-delegation
changeset 20 112e7c316db9
parent 19 3ea8010e4fbc
child 21 a4e11829f96e
equal deleted inserted replaced
19:3ea8010e4fbc 20:112e7c316db9
     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-delegation> is designed as a Icinga/Nagios plugin to verify that
       
    16 all responsible NS know about the delegation.
       
    17 
       
    18 Each domain has to pass the following tests:
       
    19 
       
    20 =over
       
    21 
       
    22 =item The I<reference> server needs to be authoritive.
       
    23 
       
    24 =item The NS records known outside (checked with some public DNS service)
       
    25 need to match the NS records obtained from the reference server.
       
    26 
       
    27 =item The serial numbers obtained from the NS servers B<and> the
       
    28 reference server need to match. All servers need to be authoritive!
       
    29 
       
    30 =back
       
    31 
       
    32 The I<DOMAINS> are passed a a list in one of the following forms:
       
    33 
       
    34 =over
       
    35 
       
    36 =item I<domain>
       
    37 
       
    38 A plain domain name.
       
    39 
       
    40 =item B<file://>I<file>
       
    41 
       
    42 A file name containing the domains, line by line.
       
    43 
       
    44 =item B<local:>
       
    45 
       
    46 This item uses the output of C<named-checkconf -p> to get the list of
       
    47 master/slave zones. The 127.in-addr.arpa, 168.192.in-addr.arpa, and
       
    48 0.in-addr.arpa, and 127.in-addr.arpa zones are suppressed.
       
    49 
       
    50 The B<override> domains are added automatically (See opt B<override>).
       
    51 
       
    52 =back
       
    53 
       
    54 =cut
       
    55 
       
    56 use 5.014;
       
    57 use strict;
       
    58 use warnings;
       
    59 use Getopt::Long qw(GetOptionsFromArray);
       
    60 use Net::DNS;
       
    61 use Pod::Usage;
       
    62 use if $ENV{DEBUG} => 'Smart::Comments';
       
    63 use List::Util qw(shuffle);
       
    64 
       
    65 sub uniq { my %h; @h{@_} = (); return keys %h; }
       
    66 my @extns = qw(8.8.8.8 8.8.4.4);
       
    67 
       
    68 package Net::DNS::Resolver {
       
    69     use Storable qw(freeze);
       
    70     sub new {
       
    71         my $class = shift;
       
    72         state %cache;
       
    73         return $cache{freeze \@_} //= $class->SUPER::new(@_);
       
    74     }
       
    75 }
       
    76 
       
    77 sub read_override {    # YEAH! :) black magic
       
    78     local @ARGV = shift;
       
    79     return map { (shift $_, $_) } grep { @$_ > 1 } map { [split] } map { s/#.*//r } <>;
       
    80 }
       
    81 
       
    82 # return a list of the zones known to the local
       
    83 # bind
       
    84 sub get_local_zones {
       
    85     my @conf;
       
    86     open(my $z, '-|', 'named-checkconf -p');
       
    87     while (<$z>) {
       
    88         state $line;
       
    89         s/^\s*(.*?)\s*$/$1 /;
       
    90         chomp($line .= $_);    # continuation line
       
    91         if (/\A\}/) {          # config item done
       
    92             $line =~ s/\s$//;
       
    93             push @conf, $line;
       
    94             $line = '';
       
    95         }
       
    96     }
       
    97     return grep { 
       
    98 	# FIXME: 172.0 .. 172.31 is missing
       
    99 	not /\b(?:0|127|10|168\.192|255)\.in-addr\.arpa$/ and
       
   100 	not /^localhost$/;
       
   101     } map { /zone\s"(\S+)"\s/ } grep { /type (?:master|slave)/ } @conf;
       
   102 }
       
   103 
       
   104 sub get_domains {
       
   105     my %arg = @_;
       
   106     my @sources = @{ $arg{sources} };
       
   107     my @domains = ();
       
   108 
       
   109     foreach my $src (@sources) {
       
   110 
       
   111         if ($src =~ m{^(?:(/.*)|file://(/.*))}) {
       
   112             open(my $f, '<', $1) or die "$0: Can't open $1 for reading: $!\n";
       
   113             push @domains, map { /^\s*(\S+)\s*/ } grep { !/^\s*#/ } <$f>;
       
   114             next;
       
   115         }
       
   116 
       
   117         if ($src =~ m{^local:}) {
       
   118             push @domains, get_local_zones;
       
   119             push @domains, @{$arg{local}} if $arg{local};
       
   120             next;
       
   121         }
       
   122 
       
   123         push @domains, $src;
       
   124     }
       
   125 
       
   126     return @domains;
       
   127 }
       
   128 
       
   129 # return a list of "official" nameservers
       
   130 sub ns {
       
   131     my $domain = shift;
       
   132     ### assert: @_ % 2 == 0
       
   133     my %resflags = (nameservers => \@extns, @_);
       
   134     my $aa = delete $resflags{aa};
       
   135     my $override = delete $resflags{override};
       
   136     my $nameservers = join ',' => @{$resflags{nameservers}};
       
   137     my @ns;
       
   138 
       
   139     return sort @{$override->{$domain}} if exists $override->{$domain};
       
   140 
       
   141     my $r = Net::DNS::Resolver->new(%resflags);
       
   142     my $q;
       
   143 
       
   144     for (my $i = 3; $i; --$i) {
       
   145         $q = $r->query($domain, 'NS') and last;
       
   146     }
       
   147     die $r->errorstring . "\@$nameservers\n" if not $q;
       
   148 
       
   149     die "no aa \@$nameservers\n" if $aa and not $q->header->aa;
       
   150     push @ns, map { $_->nsdname } grep { $_->type eq 'NS' } $q->answer;
       
   151 
       
   152     return sort @ns;
       
   153 }
       
   154 
       
   155 sub serial {
       
   156     my $domain = shift;
       
   157     my %resflags = (nameservers => \@extns, @_);
       
   158     my $nameservers = join ',' => @{$resflags{nameservers}};
       
   159 
       
   160     my $r = Net::DNS::Resolver->new(%resflags);
       
   161     my $q;
       
   162 
       
   163     for (my $i = 3; $i; --$i) {
       
   164         $q  = $r->query($domain, 'SOA') and last;
       
   165     }
       
   166     die $r->errorstring, "\@$nameservers\n" if not $q;
       
   167 
       
   168     return (map { $_->serial } grep { $_->type eq 'SOA' } $q->answer)[0];
       
   169 }
       
   170 
       
   171 # - the nameservers known from the ns records
       
   172 # - from the primary master if this is not one of the
       
   173 #   NS for the zone
       
   174 # - from a list of additional (hidden) servers
       
   175 #
       
   176 # OK - if the serial numbers are in sync
       
   177 # WARNING - if there is some difference
       
   178 # CRITICAL - if the serial cannot be found at one of the sources
       
   179 
       
   180 sub ns_ok {
       
   181     my ($domain, $reference, $override) = @_;
       
   182 
       
   183     my (@errs, @ns);
       
   184     my @our = eval { sort +ns($domain, nameservers => [$reference], aa => 1, override => $override) };
       
   185     push @errs, $@ if $@;
       
   186 
       
   187     my @their = eval { sort +ns($domain) };
       
   188     push @errs, $@ if $@;
       
   189 
       
   190     if (@errs) {
       
   191         chomp @errs;
       
   192         die join(', ' => @errs) . "\n";
       
   193     }
       
   194     
       
   195     if ("@our" ne "@their") {
       
   196         local $" = ', ';
       
   197         die sprintf "NS differ (%s @our) vs (public @their)\n",
       
   198             $override->{$domain} ? 'override' : 'our';
       
   199     }
       
   200 
       
   201     @ns = uniq sort @our, @their;
       
   202     ### @ns
       
   203     return @ns;
       
   204 }
       
   205 
       
   206 sub serial_ok {
       
   207     my ($domain, @ns) = @_;
       
   208     my @serials = map { my $s = serial $domain, nameservers => [$_], aa => 1; "$s\@$_" } @ns;
       
   209     ### @serials
       
   210 
       
   211     if (uniq(map { /(\d+)/ } @serials) != 1) {
       
   212         die "serials do not match: @serials\n";
       
   213     }
       
   214     
       
   215     $serials[0] =~ /(\d+)/;
       
   216     return $1;
       
   217 }
       
   218 
       
   219 sub main {
       
   220     my @argv          = @_;
       
   221     my $opt_reference = '127.0.0.1';
       
   222     my $opt_progress  = -t;
       
   223     my ($opt_override)= grep { -f } '/etc/bind/zones.override';
       
   224                         
       
   225 
       
   226     GetOptionsFromArray(
       
   227         \@argv,
       
   228         'reference=s' => \$opt_reference,
       
   229         'progress!'   => \$opt_progress,
       
   230         'override=s'  => \$opt_override,
       
   231         'h|help'      => sub { pod2usage(-verbose => 1, -exit => 0) },
       
   232         'm|man'       => sub {
       
   233             pod2usage(
       
   234                 -verbose   => 2,
       
   235                 -exit      => 0,
       
   236                 -noperldoc => system('perldoc -V 2>/dev/null 1>&2')
       
   237             );
       
   238         }
       
   239       )
       
   240       and @argv
       
   241       or pod2usage;
       
   242     my %override = read_override($opt_override) if defined $opt_override;
       
   243     my @domains = get_domains(sources => \@argv, local => [keys %override]);
       
   244 
       
   245     my (@OK, %CRITICAL);
       
   246     foreach my $domain (shuffle @domains) {
       
   247         print STDERR "$domain " if $opt_progress;
       
   248 
       
   249         my @ns = eval { ns_ok($domain, $opt_reference, \%override) };
       
   250 	if ($@) { 
       
   251             $CRITICAL{$domain} = $@;
       
   252             say STDERR 'fail(ns)' if $opt_progress;
       
   253             next;
       
   254         }
       
   255         print STDERR 'ok(ns) ' if $opt_progress;
       
   256 
       
   257         my @serial = eval { serial_ok($domain, @ns, $opt_reference) };
       
   258         if ($@) {
       
   259             $CRITICAL{$domain} = $@;
       
   260             say STDERR 'fail(serial)' if $opt_progress;
       
   261             next;
       
   262         }
       
   263         say STDERR 'ok(serial)' if $opt_progress;
       
   264         push @OK, $domain;
       
   265 
       
   266     }
       
   267 
       
   268     #    use DDP;
       
   269     #    p @OK;
       
   270     #    p %CRITICAL;
       
   271 
       
   272     if (my $n = keys %CRITICAL) {
       
   273         print "CRITICAL: $n of " . @domains . " domains\n",
       
   274           map { "$_: $CRITICAL{$_}" } sort keys %CRITICAL;
       
   275         return 2;
       
   276     }
       
   277 
       
   278     say 'OK: ' . @OK . ' domains checked';
       
   279     return 0;
       
   280 
       
   281 }
       
   282 
       
   283 exit main @ARGV unless caller;
       
   284 
       
   285 __END__
       
   286 
       
   287 =head1 OPTIONS
       
   288 
       
   289 =over
       
   290 
       
   291 =item B<--reference>=I<address>
       
   292 
       
   293 The address of the reference server for our own domains (default: 127.0.0.1)
       
   294 
       
   295 =item B<--progress>
       
   296 
       
   297 Tell about the progress. (default: on if input is connected to a terminal)
       
   298 
       
   299 =item B<--override>=I<override file>
       
   300 
       
   301 This file lists NS names for domains. Instead of trusting our own server
       
   302 we use the NS listed as the authoritive ones. This is primarly useful for
       
   303 some of these domains that are held on the "pending" servers of joker.
       
   304 
       
   305 =back
       
   306 
       
   307 =head2 Format
       
   308 
       
   309  # comment
       
   310  <domain> <ns> ... # comment
       
   311 
       
   312 
       
   313 =head1 PERMISSIONS
       
   314 
       
   315 No special permissions are necessary, except for the domain-list URL F<local:>, since
       
   316 the output of C<named-checkconf -p> is read. This may fail, depending on the configuration of 
       
   317 your bind.
       
   318 
       
   319 =cut
       
   320 
       
   321 # vim:sts=4 ts=8 sw=4 et: