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: |
|