forked from HariSekhon/Nagios-Plugins
-
Notifications
You must be signed in to change notification settings - Fork 0
/
check_dns.pl
executable file
·219 lines (191 loc) · 8.56 KB
/
check_dns.pl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
#!/usr/bin/perl -T
# nagios: -epn
#
# Author: Hari Sekhon
# Date: 2012-05-11 15:49:14 +0100 (Fri, 11 May 2012)
#
# https://github.com/harisekhon/nagios-plugins
#
# License: see accompanying LICENSE file
#
my @valid_types = qw/A MX NS PTR SOA SRV TXT/;
$DESCRIPTION = "Nagios Plugin to test a DNS record
Primarily written to check things like NS and MX records for domains which the standard check_dns Nagios plugin can't do.
Full list of supported record types: " . join(", ", @valid_types) . "
The regex if supplied is validated against each record returned and it is anchored (^regex\$) as that's normally what you want to make it easier to strictly validate IP / name results, but if testing partial TXT records you may need to use .* before and after the regex, eg. '.*spf.*'. If differing TXT records are returned then use alternation '|' as per regex standard to be able to match both types of records, eg. 'regex1|regex2', see tests/test_dns.sh for an example. Requiring validating every record is much safer to ensure there is no rogue DNS server injected in to your domain and anchoring prevents a regex of 10\.10\.10\.10 from matching an unexpected service on 10.10.10.100";
# TODO: root name servers switch, determine root name servers for the specific TLD and go straight to them to bypass intermediate caching
$VERSION = "0.8.2";
use strict;
use warnings;
use Net::DNS;
use Time::HiRes 'time';
BEGIN {
use File::Basename;
use lib dirname(__FILE__) . "/lib";
}
use HariSekhonUtils qw/:DEFAULT :regex/;
use Data::Dumper;
$status_prefix = "DNS";
my $default_type = "A";
my $type = $default_type;
my $record;
my $server;
my @servers;
my $expected_result;
my $expected_regex;
my $expected_regex2;
my $no_uniq_results;
%options = (
"s|server=s" => [ \$server, "DNS server(s) to query, can be a comma separated list of servers" ],
"r|record=s" => [ \$record, "DNS record to query" ],
"q|type=s" => [ \$type, "DNS query type (defaults to '$default_type' record)" ],
"e|expected-result=s" => [ \$expected_result, "Expected results, comma separated" ],
"R|expected-regex=s" => [ \$expected_regex, "Expected regex to validate against each returned result (anchored, so if testing partial TXT records you may need to use .* before and after the regex, and if differing TXT records are returned then use alternation '|' to support the different regex, see tests/test_dns.sh for an example)" ],
"no-uniq-results" => [ \$no_uniq_results, "Test and display all results, not only unique results" ]
);
@usage_order = qw/server record type expected-result expected-regex/;
get_options();
$server or usage "server(s) not specified";
@servers = split(/\s*[,\s]\s*/, $server);
for(my $i=0; $i < scalar @servers; $i++){
$servers[$i] = validate_host($servers[$i]);
}
grep($type, @valid_types) or usage "unsupported type '$type' given, must be one of: " . join(",", @valid_types);
if($type eq "PTR"){
$record = isIP($record) or usage "invalid record given for type PTR, should be an IP";
} elsif($type eq "SRV"){
$record =~ /^[A-Za-z_\.-]+\.$domain_regex/ or usage "invalid record given for type SRV, must contain only alphanumeric, underscores, dashes followed by a valid domain name format";
} else {
$record = isDomain($record) or isFqdn($record) or usage "invalid record given, should be a domain or fully qualified host name";
}
vlog_option "server", join(",", @servers);
vlog_option "record", $record;
vlog_option "type", $type;
my @expected_results;
if($expected_result){
@expected_results = sort split(/\s*,\s*/, $expected_result);
if($type eq "A"){
foreach(@expected_results){
isIP($_) or usage "invalid expected result '$_' for A record, should be an IP address";
}
} elsif(grep($type, qw/CNAME MX NS PTR/)){
foreach(@expected_results){
isFqdn($_) or usage "invalid expected result '$_' for CNAME/MX/NS/PTR record, should be an fqdn";
}
}
vlog_option "expected results", $expected_result;
}
$expected_regex2 = validate_regex($expected_regex) if defined($expected_regex);
vlog2;
set_timeout();
$status = "OK";
my @resolved_dns_servers;
for(my $i=0; $i < scalar @servers; $i++){
$servers[$i] = resolve_ip($servers[$i]) || next;
push(@resolved_dns_servers, $servers[$i]);
}
@resolved_dns_servers || quit "CRITICAL", "no given DNS servers resolved to IPs, cannot query them";
my $res = Net::DNS::Resolver->new(
nameservers => [@resolved_dns_servers],
recurse => 1,
debug => $debug,
);
vlog2 "created resolver pointing to " . join(",", @servers);
$res->tcp_timeout(2);
$res->udp_timeout(2);
vlog2 "set resolver timeout to 2 secs per server";
my @results;
my @rogue_results;
my @missing_results;
vlog2 "sending query for $record $type record";
my $start = time;
my $query = $res->query($record, $type);
my $stop = time;
my $total_time = sprintf("%.4f", $stop - $start);
vlog2;
plural @servers;
$query or quit "CRITICAL", "query returned with no answer from server$plural " . join(",", @servers) . " in $total_time secs" . ( $verbose ? " for record '$record' type '$type'" : "");
vlog2 "query returned in $total_time secs";
my $perfdata = " | dns_query_time='${total_time}s'";
vlog3 "returned records:\n";
foreach my $rr ($query->answer){
vlog3 Dumper($rr);
my $result;
if($rr->type eq "A"){
$result = $rr->address;
} elsif($rr->type eq "CNAME"){
$result = $rr->cname;
$result =~ s/\.$//;
} elsif($rr->type eq "MX"){
$result = $rr->exchange;
} elsif($rr->type eq "NS"){
$result = $rr->nsdname;
} elsif($rr->type eq "PTR"){
$result = $rr->ptrdname;
} elsif($rr->type eq "SOA"){
$result = $rr->serial;
} elsif($rr->type eq "SRV"){
$result = $rr->target;
} elsif($rr->type eq "TXT"){
$result = $rr->txtdata;
} else {
quit "UNKNOWN", "unknown/unsupported record type '$rr->type' returned for record '$record'";
}
vlog2 "got result: $result\n";
if($type eq "A"){
isIP($result) or quit "CRITICAL", "invalid result '$result' returned for A record by DNS server, expected IP address for A record$perfdata";
} elsif(grep { $type eq $_ } qw/CNAME MX NS PTR SRV/){
isFqdn($result) or quit "CRITICAL", "invalid result '$result' returned " . ($verbose ? "for record '$record' type '$type' ": "") . "by DNS server, expected FQDN for this record type$perfdata";
} elsif($type eq "SOA"){
isInt($result) or quit "CRITICAL", "invalid serial result '$result' returned for SOA record " . ($verbose ? "'$record' ": "") . "by DNS server, expected an unsigned integer$perfdata";
}
push(@results, $result);
if(@expected_results){
unless(grep(lc $_ eq lc $result, @expected_results)){
vlog3 "result '$result' wasn't found in expected results, added to rogue list";
push(@rogue_results, $result);
}
}
}
@results or quit "CRITICAL", "no result received for '$record' $type record from servers " . join(",", @servers) . " in $total_time secs";
my @results_uniq = sort(uniq_array(@results));
foreach my $expected_result2 (@expected_results){
unless(grep(lc $_ eq lc $expected_result2, @results_uniq)){
vlog3 "$expected_result2 wasn't found in results, adding to missing list";
push(@missing_results, $expected_result2);
}
}
my @regex_mismatches;
if($expected_regex2){
foreach my $result (@results){
$result =~ /^$expected_regex2$/ or push(@regex_mismatches, $result);
}
}
@regex_mismatches = sort(uniq_array(@regex_mismatches)) if(@regex_mismatches);
$msg .= "$record $type record ";
if(scalar @rogue_results or scalar @missing_results){
critical;
$msg .= "mismatch, expected '" . join(",", @expected_results) . "', got '";
} elsif(scalar @regex_mismatches){
critical;
$msg .= "regex validation failed on '" . join("','", @regex_mismatches) . "' against regex '$expected_regex', returns '";
} elsif($type eq "SOA"){
$msg .= "return serial '";
} else {
$msg .= "returns '";
}
if($no_uniq_results){
$msg .= join("','", @results);
} else {
$msg .= join("','", @results_uniq);
}
$msg .= "'";
$msg .= " in $total_time secs" if $verbose;
$msg .= $perfdata;
my $extended_command = dirname $progname;
$extended_command .= "/$progname -s $server -r $record -q $type";
$extended_command .= " -e $expected_result" if $expected_result;
$extended_command .= " -R '$expected_regex'" if $expected_regex;
$extended_command .= " -t $timeout" if($timeout ne $timeout_default);
vlog3 "\nextended command: $extended_command\n\n";
quit $status, $msg;