# HG changeset patch # User Heiko Schlittermann (JUMPER) # Date 1420671451 -3600 # Node ID 112e7c316db9c3172ef6ab4c51210fbdbe75165a # Parent 3ea8010e4fbc15ccdb4fb4988dc1cd8bc7bf7c00 moved the code to a perl module Now the working parts are in Nagios::Check::DNS::delegation.pm. It's not a package. Just a detached module. diff -r 3ea8010e4fbc -r 112e7c316db9 Build.PL --- a/Build.PL Wed Jan 07 17:15:54 2015 +0100 +++ b/Build.PL Wed Jan 07 23:57:31 2015 +0100 @@ -5,13 +5,19 @@ my $builder = Module::Build->new( dist_name => 'nagios-plugin-dns-delegation', - dist_version_from => 'plugins/check_dns-delegation', + dist_version_from => 'lib/Nagios/Check/DNS/delegation.pm', dist_abstract => 'nagios check for dns serial numbers', +# PL_files => { +# 'plugins/check_dns-delegation.PL' => +# 'nagios/plugins/ius/check_dns-delegation' +# }, checks_files => { - 'plugins/check_dns-delegation' => 'nagios/plugins/ius/check_dns-delegation', + 'bin/check_dns-delegation' => + 'nagios/plugins/ius/check_dns-delegation', }, - license => 'perl', - requires => { + bin_scripts => [glob 'bin/*'], + license => 'perl', + requires => { perl => '5.14.2', 'Net::DNS' => '0.66', }, diff -r 3ea8010e4fbc -r 112e7c316db9 MANIFEST --- a/MANIFEST Wed Jan 07 17:15:54 2015 +0100 +++ b/MANIFEST Wed Jan 07 23:57:31 2015 +0100 @@ -1,5 +1,6 @@ .hgignore +bin/check_dns-delegation Build.PL +lib/Nagios/Check/DNS/delegation.pm MANIFEST This list of files -plugins/check_dns-delegation t/10-minimal.t diff -r 3ea8010e4fbc -r 112e7c316db9 MANIFEST.SKIP --- a/MANIFEST.SKIP Wed Jan 07 17:15:54 2015 +0100 +++ b/MANIFEST.SKIP Wed Jan 07 23:57:31 2015 +0100 @@ -72,3 +72,4 @@ # Avoid archives of this distribution \bnagios-plugin-dns-serial-[\d\.\_]+ +\bscratch\b diff -r 3ea8010e4fbc -r 112e7c316db9 bin/check_dns-delegation --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/bin/check_dns-delegation Wed Jan 07 23:57:31 2015 +0100 @@ -0,0 +1,99 @@ +#! /usr/bin/perl +# source: https://ssl.schlittermann.de/hg/ius/nagios/nagios-plugin-dns-serial +# © 2014 Heiko Schlittermann +use 5.014; +use strict; +use warnings; +use Nagios::Check::DNS::delegation qw(main); + +exit main @ARGV unless caller; + +__END__ + +=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 know about the delegation. + +Each domain has to pass the following tests: + +=over + +=item The I server needs to be authoritive. + +=item The NS records known outside (checked with some public DNS service) +need to match the NS records obtained from the reference server. + +=item The serial numbers obtained from the NS servers B the +reference server need to match. All servers need to be authoritive! + +=back + +The I are passed a a list in one of the following forms: + +=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. + +The B domains are added automatically (See opt B). + +=back + +=cut + + +=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) + +=item B<--override>=I + +This file lists NS names for domains. Instead of trusting our own server +we use the NS listed as the authoritive ones. This is primarly useful for +some of these domains that are held on the "pending" servers of joker. + +=back + +=head2 Format + + # comment + ... # comment + + +=head1 PERMISSIONS + +No special permissions are necessary, except for the domain-list URL F, since +the output of C is read. This may fail, depending on the configuration of +your bind. + +=cut + +# vim:sts=4 ts=8 sw=4 et: diff -r 3ea8010e4fbc -r 112e7c316db9 lib/Nagios/Check/DNS/delegation.pm --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/lib/Nagios/Check/DNS/delegation.pm Wed Jan 07 23:57:31 2015 +0100 @@ -0,0 +1,229 @@ +use 5.014; +use strict; +use warnings; +use Getopt::Long qw(GetOptionsFromArray); +use Net::DNS; +use Pod::Usage; +use if $ENV{DEBUG} => 'Smart::Comments'; +use List::Util qw(shuffle); + +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(@_); + } +} + +sub read_override { # YEAH! :) black magic + local @ARGV = shift; + return map { (shift @$_, $_) } grep { @$_ > 1 } map { [split] } map { s/#.*//r } <>; +} + +# 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 %arg = @_; + my @sources = @{ $arg{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*/ } grep { !/^\s*#/ } <$f>; + next; + } + + if ($src =~ m{^local:}) { + push @domains, get_local_zones; + push @domains, @{$arg{local}} if $arg{local}; + 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 $override = delete $resflags{override}; + my $nameservers = join ',' => @{$resflags{nameservers}}; + my @ns; + + return sort @{$override->{$domain}} if exists $override->{$domain}; + + 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, $override) = @_; + + my (@errs, @ns); + my @our = eval { sort +ns($domain, nameservers => [$reference], aa => 1, override => $override) }; + 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 sprintf "NS differ (%s @our) vs (public @their)\n", + $override->{$domain} ? 'override' : 'our'; + } + + @ns = uniq sort @our, @their; + ### @ns + return @ns; +} + +sub serial_ok { + my ($domain, @ns) = @_; + my @serials = map { my $s = serial $domain, nameservers => [$_], aa => 1; "$s\@$_" } @ns; + ### @serials + + 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; + my ($opt_override)= grep { -f } '/etc/bind/zones.override'; + + + GetOptionsFromArray( + \@argv, + 'reference=s' => \$opt_reference, + 'progress!' => \$opt_progress, + 'override=s' => \$opt_override, + '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 %override = read_override($opt_override) if defined $opt_override; + my @domains = get_domains(sources => \@argv, local => [keys %override]); + + my (@OK, %CRITICAL); + foreach my $domain (shuffle @domains) { + print STDERR "$domain " if $opt_progress; + + my @ns = eval { ns_ok($domain, $opt_reference, \%override) }; + if ($@) { + $CRITICAL{$domain} = $@; + say STDERR 'fail(ns)' if $opt_progress; + next; + } + print STDERR 'ok(ns) ' if $opt_progress; + + my @serial = eval { serial_ok($domain, @ns, $opt_reference) }; + if ($@) { + $CRITICAL{$domain} = $@; + say STDERR 'fail(serial)' 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; + +} + +1; +# vim:sts=4 ts=8 sw=4 et: diff -r 3ea8010e4fbc -r 112e7c316db9 plugins/check_dns-delegation --- a/plugins/check_dns-delegation Wed Jan 07 17:15:54 2015 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,321 +0,0 @@ -#! /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 know about the delegation. - -Each domain has to pass the following tests: - -=over - -=item The I server needs to be authoritive. - -=item The NS records known outside (checked with some public DNS service) -need to match the NS records obtained from the reference server. - -=item The serial numbers obtained from the NS servers B the -reference server need to match. All servers need to be authoritive! - -=back - -The I are passed a a list in one of the following forms: - -=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. - -The B domains are added automatically (See opt B). - -=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'; -use List::Util qw(shuffle); - -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(@_); - } -} - -sub read_override { # YEAH! :) black magic - local @ARGV = shift; - return map { (shift $_, $_) } grep { @$_ > 1 } map { [split] } map { s/#.*//r } <>; -} - -# 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 %arg = @_; - my @sources = @{ $arg{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*/ } grep { !/^\s*#/ } <$f>; - next; - } - - if ($src =~ m{^local:}) { - push @domains, get_local_zones; - push @domains, @{$arg{local}} if $arg{local}; - 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 $override = delete $resflags{override}; - my $nameservers = join ',' => @{$resflags{nameservers}}; - my @ns; - - return sort @{$override->{$domain}} if exists $override->{$domain}; - - 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, $override) = @_; - - my (@errs, @ns); - my @our = eval { sort +ns($domain, nameservers => [$reference], aa => 1, override => $override) }; - 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 sprintf "NS differ (%s @our) vs (public @their)\n", - $override->{$domain} ? 'override' : 'our'; - } - - @ns = uniq sort @our, @their; - ### @ns - return @ns; -} - -sub serial_ok { - my ($domain, @ns) = @_; - my @serials = map { my $s = serial $domain, nameservers => [$_], aa => 1; "$s\@$_" } @ns; - ### @serials - - 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; - my ($opt_override)= grep { -f } '/etc/bind/zones.override'; - - - GetOptionsFromArray( - \@argv, - 'reference=s' => \$opt_reference, - 'progress!' => \$opt_progress, - 'override=s' => \$opt_override, - '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 %override = read_override($opt_override) if defined $opt_override; - my @domains = get_domains(sources => \@argv, local => [keys %override]); - - my (@OK, %CRITICAL); - foreach my $domain (shuffle @domains) { - print STDERR "$domain " if $opt_progress; - - my @ns = eval { ns_ok($domain, $opt_reference, \%override) }; - if ($@) { - $CRITICAL{$domain} = $@; - say STDERR 'fail(ns)' if $opt_progress; - next; - } - print STDERR 'ok(ns) ' if $opt_progress; - - my @serial = eval { serial_ok($domain, @ns, $opt_reference) }; - if ($@) { - $CRITICAL{$domain} = $@; - say STDERR 'fail(serial)' 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) - -=item B<--override>=I - -This file lists NS names for domains. Instead of trusting our own server -we use the NS listed as the authoritive ones. This is primarly useful for -some of these domains that are held on the "pending" servers of joker. - -=back - -=head2 Format - - # comment - ... # comment - - -=head1 PERMISSIONS - -No special permissions are necessary, except for the domain-list URL F, since -the output of C is read. This may fail, depending on the configuration of -your bind. - -=cut - -# vim:sts=4 ts=8 sw=4 et: diff -r 3ea8010e4fbc -r 112e7c316db9 t/10-minimal.t --- a/t/10-minimal.t Wed Jan 07 17:15:54 2015 +0100 +++ b/t/10-minimal.t Wed Jan 07 23:57:31 2015 +0100 @@ -19,13 +19,11 @@ sub dig_serial { (split " ", `dig +short SOA @_`)[2] } - -sub dig_ns { - sort map { /(\S+?)\.?$/ } `dig +short NS @_`; -} +sub dig_ns { sort map { /(\S+?)\.?$/ } `dig +short NS @_` } # we require it, it's not a normal module -require_ok 'blib/nagios/plugins/ius/check_dns-delegation' +use blib; +use_ok('Nagios::Check::DNS::delegation') or BAIL_OUT q{can't require the module}; @@ -91,7 +89,7 @@ } # ns for some domain we're not the master for, should be refused -throws_ok { ns('example.org', nameservers => [qw/f.nic.de a.nic.de b.nic.de/]) } +throws_ok { ns('example.org', nameservers => [qw/f.nic.de a.nic.de/]) } qr/^REFUSED/ => 'throws on refused query'; throws_ok { ns('safasdfasdfrandomadsfefvddeas') } qr/^NXDOMAIN/ => 'throws on nx domain';