#!/usr/bin/perl
$VERSION
= (
qw$LastChangedRevision: 1194 $
)[1];
my
$self
= $0;
my
$options
=
'dnstv'
;
my
%option
;
eval
{
require
Getopt::Std; Getopt::Std::getopts(
$options
, \
%option
) };
warn
"Can't locate Getopt::Std\n"
if
$@;
my
@arg
=
qw( domain [nameserver] )
;
my
@flag
=
map
{
"[-$_]"
}
split
( //,
$options
);
die
eval
{
system
(
"perldoc -F $self"
);
""
},
<<END unless scalar @ARGV;
Synopsis: $self @flag @arg
END
my
(
$domain
,
@nameserver
) =
@ARGV
;
my
@conf
= (
debug
=> (
$option
{d} || 0 ),
igntc
=> (
$option
{t} || 0 )
);
my
$negtest
=
$option
{n};
my
$dnssec
=
$option
{s};
my
$verbose
=
$option
{v};
my
$neg_min
= 300;
my
$neg_max
= 86400;
my
$udp_timeout
= 5;
my
$udp_wait
= 0.020;
my
$resolver
= new Net::DNS::Resolver(
@conf
);
$resolver
->nameservers(
@nameserver
) or
die
$resolver
->string;
$resolver
->dnssec(1)
if
$dnssec
;
my
(
$question
) = new Net::DNS::Packet(
$domain
)->question;
my
$name
=
lc
$question
->qname;
my
$NetDNSrev
=
&Net::DNS::version
;
die
"\tFeature not supported by Net::DNS $NetDNSrev\n"
if
$name
=~ m
my
@ns
= NS(
$name
);
unless
(
@ns
) {
displayRR(
$name
,
'ANY'
);
displayRR(
$name
,
'NS'
);
die
$resolver
->string;
}
my
(
$zone
) =
map
{
$_
->name }
@ns
;
my
@nsname
=
grep
{
$_
ne
$zone
}
map
{
$_
->nsdname }
@ns
;
my
@server
=
@nameserver
? (
@nameserver
) : (
sort
@nsname
);
my
@soa
=
grep
{
$_
->type eq
'SOA'
} displayRR(
$zone
,
'SOA'
);
foreach
my
$soa
(
@soa
) {
my
$owner
=
lc
$soa
->name;
my
$mname
=
lc
$soa
->mname;
my
$rname
=
lc
$soa
->rname;
my
$resolved
;
foreach
my
$rrtype
(
qw( A AAAA )
) {
my
$probe
=
$resolver
->
send
(
$mname
,
$rrtype
);
last
if
(
$resolved
=
scalar
$probe
->answer );
}
for
(
$mname
) {
last
unless
$_
eq
$owner
;
displayRR(
$zone
,
'NS'
)
unless
@nameserver
;
displayRR(
$zone
,
'ANY'
)
unless
$_
eq
$name
;
last
unless
/(in-addr|ip6)\.arpa/i;
report(
'unexpected address record in locally served zone [RFC6303]'
)
if
$resolved
;
}
last
unless
@nsname
;
report(
'unresolved MNAME'
,
$mname
)
unless
$resolved
;
unless
(
$rname
=~ /(@|[^\\]\.)([^@]+)$/ ) {
report(
'incomplete RNAME'
,
$rname
)
unless
$rname
eq
'<>'
;
}
elsif
( $2 ne
$mname
) {
my
$resolved
;
foreach
my
$rrtype
(
qw( MX A AAAA CNAME )
) {
my
$probe
=
$resolver
->
send
( $2,
$rrtype
);
last
if
(
$resolved
=
scalar
$probe
->answer );
}
report(
'unresolved RNAME'
,
$rname
)
unless
$resolved
;
}
unless
(
$soa
->expire >
$soa
->refresh ) {
report(
'slave expires zone data before scheduled refresh'
);
}
else
{
my
$window
=
$soa
->expire -
$soa
->refresh - 1;
my
$retry
=
$soa
->retry || 1;
my
$n
= 1 +
int
(
$window
/
$retry
);
my
$s
=
$n
> 1 ?
's'
:
''
;
report(
"slave expires zone data after $n transfer failure$s"
)
unless
$n
> 3;
}
my
(
$min
) =
sort
{
$a
<=>
$b
} (
$soa
->minimum,
$soa
->ttl );
$negtest
++
if
$min
<
$neg_min
or
$soa
->minimum >
$neg_max
;
}
my
@ncache
= NCACHE(
$zone
)
if
$negtest
;
displayRR(
$name
,
'ANY'
)
if
@soa
;
displayRR(
$zone
,
'NS'
)
if
@nameserver
;
print
"----\n"
;
my
(
$bad
,
$seq
,
$iphash
) = checkNS(
$zone
,
@server
);
$iphash
->{
$seq
} ||=
'<unidentified>'
;
print
"\n"
;
my
$s
=
$bad
!= 1 ?
's'
:
''
;
print
"Unsatisfactory response from $bad nameserver$s\n\n"
if
$bad
and
@server
> 1;
my
%mname
=
reverse
%$iphash
;
my
$mcount
=
keys
%mname
;
if
(
$mcount
> 1 ) {
report(
'SOAs do not identify unique primary server'
);
foreach
my
$mname
(
sort
keys
%mname
) {
foreach
(
$mname
,
$resolver
->nameservers(
$mname
) ) {
delete
$iphash
->{
$_
} }
}
my
%serial
=
map
{ (
$iphash
->{
$_
} =>
$_
) }
sort
{
$a
<=>
$b
}
keys
%$iphash
;
foreach
(
sort
keys
%mname
) { report(
sprintf
'%10s %s'
,
$serial
{
$_
},
$_
) }
}
exit
;
sub
checkNS0 {
my
$serial
=
undef
;
my
$hash
= {};
my
$res
= new Net::DNS::Resolver(
@conf
);
foreach
my
$soa
(
grep
{
$_
->type eq
'SOA'
}
@ncache
,
@soa
) {
my
$mname
=
lc
$soa
->mname;
foreach
(
$mname
,
$res
->nameservers(
$mname
) ) {
$hash
->{
$_
} =
$mname
}
my
$s
=
$soa
->serial;
$hash
->{
$s
} =
$mname
;
$serial
=
$s
if
ordered(
$serial
,
$s
);
}
return
( 0,
$serial
,
$hash
);
}
sub
checkNS {
my
$zone
=
shift
;
my
$index
=
scalar
@_
;
my
$element
=
pop
@_
or
return
checkNS0;
my
(
$ns
,
$if
) =
split
/ /,
lc
$element
;
my
$res
= new Net::DNS::Resolver(
@conf
);
my
@xip
=
sort
$res
->nameservers(
$if
||
$ns
);
@xip
=
$res
->nameservers(
"$ns."
)
unless
@xip
;
my
$ip
=
pop
@xip
;
$res
->nameservers(
$ip
)
if
@xip
;
$res
->recurse(0);
my
(
$socket
,
$sent
) = (
$res
->bgsend(
$zone
,
'SOA'
),
time
)
if
$ip
;
my
(
$fail
,
$latest
,
$hash
) = checkNS(
$zone
,
@_
);
my
$packet
;
if
(
$socket
) {
until
(
$res
->bgisready(
$socket
) ) {
last
if
time
> (
$sent
+
$udp_timeout
);
delay(
$udp_wait
);
}
$packet
=
$res
->bgread(
$socket
)
if
$res
->bgisready(
$socket
);
}
elsif
(
$ip
) {
$packet
=
$res
->
send
(
$zone
,
'SOA'
);
}
my
@pass
= (
$fail
,
$latest
,
$hash
);
my
@fail
= (
$fail
+ 1,
$latest
,
$hash
);
my
%nsaddr
= (
$ip
=> 1 )
if
$ip
;
foreach
my
$xip
(
@xip
) {
next
if
$nsaddr
{
$xip
}++;
my
(
$f
,
$x
,
$h
) = checkNS(
$zone
, (
undef
) x
scalar
(
@_
),
"$ns $xip"
);
%$hash
= (
%$hash
,
%$h
);
@pass
=
@fail
if
$f
;
}
my
$rcode
;
my
@soa
;
unless
(
$packet
) {
$rcode
=
'no response'
;
}
elsif
(
$packet
->header->rcode ne
'NOERROR'
) {
$rcode
=
$packet
->header->rcode;
}
else
{
@soa
=
grep
{
$_
->type eq
'SOA'
}
$packet
->answer;
foreach
my
$soa
(
@soa
) {
my
$mname
=
lc
$soa
->mname;
my
@ip
=
$res
->nameservers(
$mname
)
unless
$hash
->{
$mname
};
foreach
(
$mname
,
@ip
) {
$hash
->{
$_
} =
$mname
}
}
}
my
$primary
=
$hash
->{
$ip
||
$ns
} ?
'*'
:
''
;
unless
(
$ip
) {
print
"\n[$index]$primary\t$ns\n"
;
$rcode
=
'unresolved server name'
;
}
elsif
(
$ns
eq
$ip
) {
print
"\n[$index]$primary\t$ip\n"
;
}
else
{
print
"\n[$index]$primary\t$ns ($ip)\n"
;
}
if
(
$verbose
) {
my
@ptr
=
grep
{
$_
->type eq
'PTR'
} displayRR(
$ip
)
if
$ip
;
my
@fwd
=
sort
map
{
lc
$_
->ptrdname }
@ptr
;
foreach
my
$name
(
@fwd
?
@fwd
: (
$ns
) ) {
displayRR(
$name
,
'A'
);
displayRR(
$name
,
'AAAA'
);
}
}
if
(
$rcode
) {
return
@pass
if
$ns
eq
lc
$zone
;
report(
$rcode
);
return
@fail
;
}
my
@result
=
@fail
;
my
@auth
=
$packet
->authority
unless
@soa
;
my
@ncache
=
grep
{
$_
->type eq
'SOA'
}
@auth
;
my
@refer
=
grep
{
$_
->type eq
'NS'
}
@auth
;
if
(
@soa
) {
if
(
@soa
> 1 ) {
report(
'multiple SOA records'
);
}
elsif
(
$packet
->header->aa ) {
@result
=
@pass
;
}
else
{
my
$ttl
=
$soa
[0]->ttl;
report(
'non-authoritative answer'
, ttl(
$ttl
) );
}
}
elsif
(
@ncache
) {
my
(
$ttl
) =
map
{
$_
->ttl }
@soa
=
@ncache
;
report(
'NODATA response'
, ttl(
$ttl
) );
return
@fail
unless
grep
{
$_
->name =~ /^
$zone
$/i }
@ncache
;
report(
'requested SOA in authority section; violates RFC2308'
);
}
elsif
(
@refer
) {
my
@n
=
grep
{
$_
->nsdname =~ /
$ns
/i }
@refer
;
report(
'authoritative data expired'
)
if
@n
;
report(
'not configured for zone'
)
unless
@n
;
return
@fail
;
}
else
{
report(
'NODATA response from nameserver'
);
return
@fail
;
}
report(
'truncated response from nameserver'
)
if
$packet
->header->tc;
my
(
$serial
) =
map
{
$_
->serial }
@soa
;
if
(
$primary
&& ordered(
$serial
,
$latest
) ) {
my
$response
=
$res
->
send
(
$zone
,
'SOA'
);
my
(
$retest
) =
grep
{
$_
->type eq
'SOA'
}
$response
->answer
if
$response
;
$serial
=
$retest
->serial
if
ordered(
$serial
,
$retest
->serial );
}
print
"\t\t\tzone serial\t"
,
$serial
,
"\n"
;
$hash
->{
$serial
} =
$hash
->{
$ip
}
if
$primary
;
if
( ordered(
$serial
,
$latest
) ) {
report(
'serial number not current'
);
return
@fail
unless
$primary
;
report(
'discredited as unique primary nameserver'
);
return
@fail
;
}
return
@result
if
$serial
==
$latest
;
my
$x
=
$if
? 0 : (
$index
- 1 ) -
$fail
;
my
$s
=
$x
> 1 ?
's'
:
''
;
report(
"at least $x previously unreported stale serial number$s"
)
if
$x
;
return
(
$result
[0] +
$x
,
$serial
,
$hash
);
}
sub
delay {
my
$duration
=
shift
;
sleep
( 1 +
$duration
)
unless
eval
{
defined
select
(
undef
,
undef
,
undef
,
$duration
) };
}
sub
displayRR {
my
$packet
=
$resolver
->
send
(
@_
) or
return
();
my
$header
=
$packet
->header;
my
$rcode
=
$header
->rcode;
my
(
$question
) =
$packet
->question;
my
$qtype
=
$question
->qtype;
my
$qname
=
$question
->qname;
my
$name
=
eval
{
$question
->name }
if
$qname
=~ /^xn--/;
my
@annotation
= (
";\t$name\n"
)
if
$name
;
my
@answer
=
$packet
->answer;
my
@authority
=
$packet
->authority;
my
@ncache
=
grep
{
$_
->type eq
'SOA'
}
@authority
;
my
@workaround
=
@ncache
if
$qtype
eq
'SOA'
;
my
@remark
=
qw(unexpected)
if
@workaround
;
$rcode
=
'NODATA'
if
@ncache
&&
$rcode
eq
'NOERROR'
;
foreach
my
$rr
(
@answer
,
@workaround
) {
next
if
$qtype
eq
'ANY'
&&
$rr
->type =~ /^(SOA|NS)$/;
local
$SIG
{__WARN__} =
sub
{ };
print
@annotation
if
$rr
->name eq
$qname
;
for
(
$rr
->string ) {
my
$l
=
$verbose
?
length
(
$_
) : 108;
substr
(
$_
,
$l
) =
' ...'
if
length
(
$_
) >
$l
&&
$rr
->type ne
'SOA'
;
print
"$_\n"
;
}
}
report(
@remark
,
"$rcode:"
,
$question
->string,
@annotation
)
if
$rcode
ne
'NOERROR'
;
return
@answer
;
}
sub
NCACHE {
my
$domain
=
shift
||
''
;
my
$seq
=
time
;
my
$nxdomain
=
"_nx_$seq.$domain"
;
my
$reply
=
$resolver
->
send
(
$nxdomain
,
'PTR'
) or
return
();
for
(
$reply
->answer ) {
report(
'wildcard invalidates NCACHE test:'
,
$_
->string );
return
();
}
my
@ncache
=
grep
{
$_
->type eq
'SOA'
}
$reply
->authority;
for
(
@ncache
) {
report(
'negative cache data'
, ttl(
$_
->ttl ),
'( SOA:'
,
$_
->serial .
' )'
);
}
return
@ncache
;
}
sub
NS {
my
$name
=
shift
;
my
$packet
=
$resolver
->
send
(
$name
,
'NS'
) or
die
$resolver
->string;
my
@answer
=
grep
{
$_
->type eq
'NS'
}
$packet
->answer;
return
@answer
if
@answer
;
my
$domain
=
lc
$name
;
if
(
my
(
$ncache
) =
grep
{
$_
->type eq
'SOA'
}
$packet
->authority ) {
my
$apex
=
lc
$ncache
->name;
return
()
unless
$apex
=~ /[^.]/;
return
NS(
$apex
)
if
$apex
ne
$domain
;
}
my
@referral
=
grep
{
$_
->type eq
'NS'
}
$packet
->authority;
return
@referral
if
grep
{
$domain
eq
lc
$_
->name }
@referral
;
my
(
$x
,
$parent
) =
split
/\./,
$domain
, 2;
return
NS(
$parent
||
return
() );
}
sub
ordered {
my
(
$a
,
$b
) =
@_
;
return
defined
$b
unless
defined
$a
;
return
0
unless
defined
$b
;
if
(
$a
< 0 ) {
$a
= (
$a
^ 0x80000000 ) & 0xFFFFFFFF;
$b
= (
$b
^ 0x80000000 ) & 0xFFFFFFFF;
}
return
$a
<
$b
? (
$a
> (
$b
- 0x80000000 ) ) : (
$b
< (
$a
- 0x80000000 ) );
}
sub
report {
print
'### '
,
join
(
"\t"
,
@_
),
"\n"
;
}
sub
ttl {
my
$t
=
shift
;
my
(
$s
,
$m
,
$h
,
$y
,
$d
) = (
gmtime
(
$t
) )[0 .. 2, 5, 7];
unless
(
$y
== 70 ) {
return
sprintf
'TTL %u (%uy%ud)'
,
$t
,
$y
- 70,
$d
;
}
elsif
(
$h
) {
return
sprintf
'TTL %u (%ud%0.2uh)'
,
$t
,
$d
,
$h
if
$d
;
return
sprintf
'TTL %u (%uh%0.2um)'
,
$t
,
$h
,
$m
if
$m
;
return
sprintf
'TTL %u (%uh)'
,
$t
,
$h
;
}
else
{
return
sprintf
'TTL %u (%ud)'
,
$t
,
$d
if
$d
;
return
sprintf
'TTL %u (%um%0.2us)'
,
$t
,
$m
,
$s
if
$s
;
return
sprintf
'TTL %u (%um)'
,
$t
,
$m
;
}
}