I did not commit the retry logic after all, adapt debug message
[mirror/dsa-nagios.git] / dsa-nagios-checks / checks / dsa-check-zone-rrsig-expiration
1 #!/usr/bin/perl
2
3 # downloaded from http://dns.measurement-factory.com/tools/nagios-plugins/check_zone_rrsig_expiration.html
4 # on 2010-02-07 by Peter Palfrader
5
6 # $Id: check_zone_rrsig_expiration,v 1.7 2008/11/25 01:36:36 wessels Exp $
7 #
8 # check_zone_rrsig_expiration
9 #
10 # nagios plugin to check expiration times of RRSIG records.  Reminds
11 # you if its time to re-sign your zone.
12
13 # Copyright (c) 2008, The Measurement Factory, Inc. All rights reserved.
14
15 # Redistribution and use in source and binary forms, with or without
16 # modification, are permitted provided that the following conditions
17 # are met:
18
19 # Redistributions of source code must retain the above copyright
20 # notice, this list of conditions and the following disclaimer.
21 # Redistributions in binary form must reproduce the above copyright
22 # notice, this list of conditions and the following disclaimer in the
23 # documentation and/or other materials provided with the distribution.
24 # Neither the name of The Measurement Factory nor the names of its
25 # contributors may be used to endorse or promote products derived
26 # from this software without specific prior written permission.
27
28 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
29 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
30 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
31 # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
32 # COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
33 # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
34 # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
35 # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
36 # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
37 # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
38 # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
39 # POSSIBILITY OF SUCH DAMAGE.
40
41 # Copyright (c) 2010 Peter Palfrader <peter@palfrader.org>
42 # - various fixes and cleanups
43 # - do more than one zone
44 # Copyright (c) 2012 Peter Palfrader <peter@palfrader.org>
45 #  - add -s option to configure udp packet size.  default changed from 4k to 1k
46 # Copyright (c) 2013 Peter Palfrader <peter@palfrader.org>
47 #  - add -r option to override initial refs.
48
49
50 # usage
51 #
52 # define command {
53 #   command_name    check-zone-rrsig
54 #   command_line    /usr/local/libexec/nagios-local/check_zone_rrsig -Z $HOSTADDRESS$
55 # }
56
57 # define service {
58 #   name                dns-rrsig-service
59 #   check_command       check-zone-rrsig
60 #   ...
61 # }
62
63 # define host {
64 #   use dns-zone
65 #   host_name zone.example.com
66 #   alias ZONE example.com
67 # }
68
69 # define service {
70 #   use dns-rrsig-service
71 #   host_name zone.example.com
72 # }
73
74 use warnings;
75 use strict;
76
77 use Getopt::Std;
78 use Net::DNS::Resolver;
79 use Time::HiRes qw ( gettimeofday tv_interval);
80 use Time::Local;
81 use List::Util qw ( shuffle );
82
83 sub convert_time {
84         my $in = shift;
85         my ($ticks, $unit) = ($in =~ /^(\d+)([smhdw]?)$/);
86
87         if ($unit eq 's' || $unit eq '') { }
88         elsif ($unit eq 'm') { $ticks *= 60; }
89         elsif ($unit eq 'h') { $ticks *= 60*60; }
90         elsif ($unit eq 'd') { $ticks *= 60*60*24; }
91         elsif ($unit eq 'w') { $ticks *= 60*60*24*7; }
92         else { die "Invalid unit '$unit' in '$in'\n" }
93         return $ticks;
94 }
95
96 my %opts = (t=>30, s=>1024);
97 getopts('hdt:c:w:s:r:', \%opts);
98 usage() unless scalar @ARGV == 1;
99 usage() if $opts{h};
100 my $zone = $ARGV[0];
101
102 my $data;
103 my $start;
104 my $stop;
105 my $CRIT = 3 * 3600*24;
106 my $WARN = 7 * 3600*24;
107
108 $CRIT = convert_time($opts{c}) if defined $opts{c};
109 $WARN = convert_time($opts{w}) if defined $opts{w};
110
111 my @refs = qw (
112 a.root-servers.net
113 b.root-servers.net
114 c.root-servers.net
115 d.root-servers.net
116 e.root-servers.net
117 f.root-servers.net
118 g.root-servers.net
119 h.root-servers.net
120 i.root-servers.net
121 j.root-servers.net
122 k.root-servers.net
123 l.root-servers.net
124 m.root-servers.net
125 );
126 @refs = split(/\s*,\s*/, $opts{r}) if (defined $opts{r});
127
128 $start = [gettimeofday()];
129 do_recursion();
130 do_queries();
131 $stop = [gettimeofday()];
132 do_analyze();
133
134 sub do_recursion {
135         my $done = 0;
136         my $res = Net::DNS::Resolver->new;
137         do {
138                 print STDERR "\nRECURSE\n" if $opts{d};
139                 my $pkt;
140                 my $prettyrefs = (scalar @refs) ? join(", ", @refs) : "empty set(!?)";
141                 foreach my $ns (shuffle @refs) {
142                         print STDERR "sending query for $zone SOA to $ns\n" if $opts{d};
143                         $res->nameserver($ns);
144                         $res->udp_timeout($opts{t});
145                         $res->udppacketsize($opts{s});
146                         $pkt = $res->send($zone, 'SOA');
147                         last if $pkt;
148                 }
149                 print STDERR "No response to seed query for $zone SOA from $prettyrefs.\n" if $opts{d};
150                 critical("No response to seed query for $zone from $prettyrefs.") unless $pkt;
151                 critical($pkt->header->rcode . " from " . $pkt->answerfrom)
152                         unless ($pkt->header->rcode eq 'NOERROR');
153                 @refs = ();
154                 foreach my $rr ($pkt->authority) {
155                         print STDERR $rr->string, "\n" if $opts{d};
156                         push (@refs, $rr->nsdname) if $rr->type eq 'NS';
157                         next unless lc($rr->name) eq lc($zone);
158                         add_nslist_to_data($pkt);
159                         $done = 1;
160                 }
161                 critical("No new references after querying for $zone SOA from $prettyrefs.  Packet was ".$pkt->string) unless (scalar @refs);
162         } while (! $done);
163 }
164
165
166 sub do_queries {
167         my $n;
168         do {
169                 $n = 0;
170                 foreach my $ns (keys %$data) {
171                         next if $data->{$ns}->{done};
172                         print STDERR "\nQUERY \@$ns SOA $zone\n" if $opts{d};
173
174                         my $pkt = send_query($zone, 'SOA', $ns);
175                         add_nslist_to_data($pkt);
176                         $data->{$ns}->{queries}->{SOA} = $pkt;
177
178                         print STDERR "done with $ns\n" if $opts{d};
179                         $data->{$ns}->{done} = 1;
180                         $n++;
181                 }
182         } while ($n);
183 }
184
185 sub do_analyze {
186         my $nscount = 0;
187         my $NOW = time;
188         my %MAX_EXP_BY_TYPE;
189         foreach my $ns (keys %$data) {
190                 print STDERR "\nANALYZE $ns\n" if $opts{d};
191                 my $pkt = $data->{$ns}->{queries}->{SOA};
192                 critical("No response from $ns") unless $pkt;
193                 print STDERR $pkt->string if $opts{d};
194                 critical($pkt->header->rcode . " from $ns")
195                         unless ($pkt->header->rcode eq 'NOERROR');
196                 critical("$ns is lame") unless $pkt->header->ancount;
197                 foreach my $rr ($pkt->answer) {
198                         next unless $rr->type eq 'RRSIG';
199                         my $exp = sigrr_exp_epoch($rr);
200                         my $T = $rr->typecovered;
201                         if (!defined($MAX_EXP_BY_TYPE{$T}->{exp}) || $exp > $MAX_EXP_BY_TYPE{$T}->{exp}) {
202                                 $MAX_EXP_BY_TYPE{$T}->{exp} = $exp;
203                                 $MAX_EXP_BY_TYPE{$T}->{ns} = $ns;
204                         }
205                 }
206                 $nscount++;
207         }
208         warning("No nameservers found.  Is '$zone' a zone?") if ($nscount < 1);
209         warning("No RRSIGs found") unless %MAX_EXP_BY_TYPE;
210         my $min_exp = undef;
211         my $min_ns = undef;
212         my $min_type = undef;
213         foreach my $T (keys %MAX_EXP_BY_TYPE) {
214                 printf STDERR ("%s RRSIG expires in %.1f days\n", $T, ($MAX_EXP_BY_TYPE{$T}->{exp}-$NOW)/86400) if $opts{d};
215                 if (!defined($min_exp) || $MAX_EXP_BY_TYPE{$T}->{exp} < $min_exp) {
216                         $min_exp = $MAX_EXP_BY_TYPE{$T}->{exp};
217                         $min_ns = $MAX_EXP_BY_TYPE{$T}->{ns};
218                         $min_type = $T;
219                 }
220         }
221         critical("$min_ns has expired RRSIGs") if ($min_exp < $NOW);
222         if ($min_exp - $NOW < ($CRIT)) {
223                 my $ND = sprintf "%3.1f days", ($min_exp-$NOW)/86400;
224                 critical("$min_type RRSIG expires in $ND at $min_ns")
225         }
226         if ($min_exp - $NOW < ($WARN)) {
227                 my $ND = sprintf "%3.1f days", ($min_exp-$NOW)/86400;
228                 warning("$min_type RRSIG expires in $ND at $min_ns")
229         }
230         success(sprintf("No RRSIGs at zone apex expiring in the next %3.1f days", $WARN/86400));
231 }
232
233 sub sigrr_exp_epoch {
234         my $rr = shift;
235         die unless $rr->type eq 'RRSIG';
236         my $exp = $rr->sigexpiration;
237         die "bad exp time '$exp'"
238                 unless $exp =~ /^(\d\d\d\d)(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)$/;
239         my $exp_epoch = timegm($6,$5,$4,$3,$2-1,$1);
240         return $exp_epoch;
241 }
242
243 sub add_nslist_to_data {
244         my $pkt = shift;
245         foreach my $ns (get_nslist($pkt)) {
246                 next if defined $data->{$ns}->{done};
247                 print STDERR "adding NS $ns\n" if $opts{d};
248                 $data->{$ns}->{done} |= 0;
249         }
250 }
251
252 sub success {
253         output('OK', shift);
254         exit(0);
255 }
256
257 sub warning {
258         output('WARNING', shift);
259         exit(1);
260 }
261
262 sub critical {
263         output('CRITICAL', shift);
264         exit(2);
265 }
266
267 sub output {
268         my $state = shift;
269         my $msg = shift;
270         $stop = [gettimeofday()] unless $stop;
271         my $latency = tv_interval($start, $stop);
272         printf "ZONE %s: %s; (%.2fs) |time=%.6fs;;;0.000000\n",
273                 $state,
274                 $msg,
275                 $latency,
276                 $latency;
277 }
278
279 sub usage {
280         print STDERR "usage: $0 [-d] [-w=<warn>] [-c=<crit>] [-t=<timeout>] <zone>\n";
281         exit 3;
282 }
283
284 sub send_query {
285         my $qname = shift;
286         my $qtype = shift;
287         my $server = shift;
288         my $res = Net::DNS::Resolver->new;
289         $res->nameserver($server) if $server;
290         $res->udp_timeout($opts{t});
291         $res->udp_timeout($opts{t});
292         $res->dnssec(1);
293         $res->retry(2);
294         $res->udppacketsize($opts{s});
295         my $pkt = $res->send($qname, $qtype);
296         unless ($pkt) {
297                 $res->usevc(1);
298                 $res->tcp_timeout($opts{t});
299                 $pkt = $res->send($qname, $qtype);
300         }
301         return $pkt;
302 }
303
304 sub get_nslist {
305         my $pkt = shift;
306         return () unless $pkt;
307         return () unless $pkt->authority;
308         my @nslist;
309         foreach my $rr ($pkt->authority) {
310                 next unless ($rr->type eq 'NS');
311                 next unless ($rr->name eq $zone);
312                 push(@nslist, lc($rr->nsdname));
313         }
314         return @nslist;
315 }