diff -r b2a26d05b063 -r 4243e22505f9 plugins/check_dns-delegation --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/check_dns-delegation Tue Jan 06 14:21:38 2015 +0100 @@ -0,0 +1,269 @@ +#! /usr/bin/perl +# source: https://ssl.schlittermann.de/hg/ius/nagios/nagios-plugin-dns-serial +# © 2014 Heiko Schlittermann + +=head1 NAME + + check_dns-serial - check the dns serial number from multiple sources + +=head1 SYNOPSIS + + check_dns-serial [options] DOMAINS + +=head1 DESCRIPTION + +B is designed as a Icinga/Nagios plugin to verify that +all responsible NS have the same serial number for their zones. + +Domains we are not responsible for are marked as B. + +The list of domains may consist of the following items: + +=over + +=item I + +A plain domain name. + +=item BI + +A file name containing the domains, line by line. + +=item B + +This item uses the output of C to get the list of +master/slave zones. The 127.in-addr.arpa, 168.192.in-addr.arpa, and +0.in-addr.arpa, and 127.in-addr.arpa zones are suppressed. + +=back + +=cut + +use 5.014; +use strict; +use warnings; +use Getopt::Long qw(GetOptionsFromArray); +use Net::DNS; +use Pod::Usage; +use if $ENV{DEBUG} => 'Smart::Comments'; + +sub uniq { my %h; @h{@_} = (); return keys %h; } +my @extns = qw(8.8.8.8 8.8.4.4); + +package Net::DNS::Resolver { + use Storable qw(freeze); + sub new { + my $class = shift; + state %cache; + return $cache{freeze \@_} //= $class->SUPER::new(@_); + } +} + +# return a list of the zones known to the local +# bind +sub get_local_zones { + my @conf; + open(my $z, '-|', 'named-checkconf -p'); + while (<$z>) { + state $line; + s/^\s*(.*?)\s*$/$1 /; + chomp($line .= $_); # continuation line + if (/\A\}/) { # config item done + $line =~ s/\s$//; + push @conf, $line; + $line = ''; + } + } + return grep { + # FIXME: 172.0 .. 172.31 is missing + not /\b(?:0|127|10|168\.192|255)\.in-addr\.arpa$/ and + not /^localhost$/; + } map { /zone\s"(\S+)"\s/ } grep { /type (?:master|slave)/ } @conf; +} + +sub get_domains { + my @sources = @_; + my @domains = (); + + foreach my $src (@sources) { + + if ($src =~ m{^(?:(/.*)|file://(/.*))}) { + open(my $f, '<', $1) or die "$0: Can't open $1 for reading: $!\n"; + push @domains, map { /^\s*(\S+)\s*/ } <$f>; + next; + } + + if ($src =~ m{^local:}) { + push @domains, get_local_zones; + next; + } + + push @domains, $src; + } + + return @domains; +} + +# return a list of "official" nameservers +sub ns { + my $domain = shift; + ### assert: @_ % 2 == 0 + my %resflags = (nameservers => \@extns, @_); + my $aa = delete $resflags{aa}; + my $nameservers = join ',' => @{$resflags{nameservers}}; + my @ns; + + my $r = Net::DNS::Resolver->new(%resflags); + my $q; + + for (my $i = 3; $i; --$i) { + $q = $r->query($domain, 'NS') and last; + } + die $r->errorstring . "\@$nameservers\n" if not $q; + + die "no aa \@$nameservers\n" if $aa and not $q->header->aa; + push @ns, map { $_->nsdname } grep { $_->type eq 'NS' } $q->answer; + + return sort @ns; +} + +sub serial { + my $domain = shift; + my %resflags = (nameservers => \@extns, @_); + my $nameservers = join ',' => @{$resflags{nameservers}}; + + my $r = Net::DNS::Resolver->new(%resflags); + my $q; + + for (my $i = 3; $i; --$i) { + $q = $r->query($domain, 'SOA') and last; + } + die $r->errorstring, "\@$nameservers\n" if not $q; + + return (map { $_->serial } grep { $_->type eq 'SOA' } $q->answer)[0]; +} + +# - the nameservers known from the ns records +# - from the primary master if this is not one of the +# NS for the zone +# - from a list of additional (hidden) servers +# +# OK - if the serial numbers are in sync +# WARNING - if there is some difference +# CRITICAL - if the serial cannot be found at one of the sources + +sub ns_ok { + my ($domain, $reference) = @_; + + my @errs; + my @our = eval { sort +ns($domain, nameservers => [$reference], aa => 1) }; + push @errs, $@ if $@; + my @their = eval { sort +ns($domain) }; + push @errs, $@ if $@; + + if (@errs) { + chomp @errs; + die join(', ' => @errs) . "\n"; + } + + if ("@our" ne "@their") { + local $" = ', '; + die "NS differ (our @our) vs (their @their)\n"; + } + + return uniq sort @our, @their; +} + +sub serial_ok { + my ($domain, @ns) = @_; + my @serials = map { my $s = serial $domain, nameservers => [$_]; "$s\@$_" } @ns; + + if (uniq(map { /(\d+)/ } @serials) != 1) { + die "serials do not match: @serials\n"; + } + + $serials[0] =~ /(\d+)/; + return $1; +} + +sub main { + my @argv = @_; + my $opt_reference = '127.0.0.1'; + my $opt_progress = -t; + + GetOptionsFromArray( + \@argv, + 'reference=s' => \$opt_reference, + 'progress!' => \$opt_progress, + 'h|help' => sub { pod2usage(-verbose => 1, -exit => 0) }, + 'm|man' => sub { + pod2usage( + -verbose => 2, + -exit => 0, + -noperldoc => system('perldoc -V 2>/dev/null 1>&2') + ); + } + ) + and @argv + or pod2usage; + my @domains = get_domains(@argv); + + my (@OK, %CRITICAL); + foreach my $domain (@domains) { + print STDERR "$domain " if $opt_progress; + + my @ns = eval { ns_ok($domain, $opt_reference) }; + if ($@) { + $CRITICAL{$domain} = $@; + say STDERR 'ns not ok' if $opt_progress; + next; + } + print STDERR 'ok(ns) ' if $opt_progress; + + my @serial = eval { serial_ok($domain, @ns) }; + if ($@) { + $CRITICAL{$domain} = $@; + say STDERR 'serial not ok' if $opt_progress; + next; + } + say STDERR 'ok(serial)' if $opt_progress; + push @OK, $domain; + + } + + # use DDP; + # p @OK; + # p %CRITICAL; + + if (my $n = keys %CRITICAL) { + print "CRITICAL: $n of " . @domains . " domains\n", + map { "$_: $CRITICAL{$_}" } sort keys %CRITICAL; + return 2; + } + + say 'OK: ' . @OK . ' domains checked'; + return 0; + +} + +exit main @ARGV unless caller; + +__END__ + +=head1 OPTIONS + +=over + +=item B<--reference>=I
+ +The address of the reference server for our own domains (default: 127.0.0.1) + +=item B<--progress> + +Tell about the progress. (default: on if input is connected to a terminal) + +=back + +=cut + +# vim:sts=4 ts=8 sw=4 et: