#!/usr/bin/perl -w $VERSION = (qw$LastChangedRevision: 688 $)[1]; $VERSION ||= 0.23; =head1 NAME check_soa - Check nameservers for a domain =head1 SYNOPSIS B [B<-d>] [B<-t>] [B<-v>] I [I ...] =head1 DESCRIPTION B builds a list of nameservers for the zone which contains the specified domain name. The program queries each nameserver for the relevant SOA record and reports the zone serial number. =over 8 =item I Fully qualified domain name to be tested. Domains within ip6.arpa or in-addr.arpa namespaces may be specified using the appropriate IP address or prefix. =item I Optional name or list of IP addresses of specific nameserver to be tested. Addresses are used in the sequence they appear in the argument list. =back Error reports are generated for nameservers which reply with non-authoritative, outdated or incorrect information. SOA query packets are sent to the nameservers as rapidly as the underlying hardware will allow. The program waits for a response only when it is needed for analysis. Execution time is determined by the slowest nameserver. The perldoc(1) documentation page is displayed if the I argument is omitted. This program is based on the B idea described by Albitz and Liu. =head1 OPTIONS =over 8 =item B<-d> Turn on resolver diagnostics. =item B<-t> Ignore UDP datagram truncation. =item B<-v> Verbose output including address records for each server. =back =head1 EXAMPLES =over 8 =item check_soa example.com Query all nameservers for the specified domain. =item check_soa example.com ns.icann.org Query specific nameserver as above. =item check_soa 192.168.99.0 Query nameservers for specified in-addr.arpa subdomain. =item check_soa 2001:DB8::8:800:200C:417A Query nameservers for specified ip6.arpa subdomain. =item check_soa 2001:DB8:0:CD30::/60 As above, for IPv6 address prefix of specified length. =back =head1 BUGS The timeout code exploits the 4 argument form of select() function. This is not guaranteed to work in non-Unix environments. =head1 COPYRIGHT (c) 2003-2007 Dick Franks Erwfranks[...]acm.orgE This program is free software; you may use or redistribute it under the same terms as Perl itself. =head1 SEE ALSO Paul Albitz, Cricket Liu. DNS and BIND, 5th Edition. O'Reilly & Associates, 2006. M. Andrews. Negative Caching of DNS Queries. RFC2308, IETF Network Working Group, 1998. Tom Christiansen, Jon Orwant, Larry Wall. Programming Perl, 3rd Edition. O'Reilly & Associates, 2000. R. Elz, R. Bush. Clarifications to the DNS Specification. RFC2181, IETF Network Working Group, 1997. P. Mockapetris. Domain Names - Concepts and Facilities. RFC1034, IETF Network Working Group, 1987. =cut use strict; use Getopt::Std; my $self = $0; # script my %option; my $options = 'dtv'; # options getopts("$options", \%option); # also --help --version my ($domain, @server) = @ARGV; # arguments my $synopsis = "Synopsis:\t$self [-$options] domain [server]"; die eval{ system("perldoc -F $self"); "" }, "\n$synopsis\n\n" unless @ARGV; require Net::DNS; my @conf = ( debug => ($option{d} || 0), # -d enable diagnostics igntc => ($option{t} || 0), # -t ignore truncation recurse => 0 ); my $verbose = $option{v}; # -v verbose my $neg_ttl = 86400; # NCACHE TTL reporting threshold my $udp_timeout = 5; # timeout for parallel operations my $udp_wait = 0.010; # minimum polling interval my $name = Net::DNS::Question->new($domain)->qname; # invert IP address/prefix die "\tFeature not supported by Net::DNS ",&Net::DNS::version,"\n" if $name =~ m#:[A-Fa-f0-9:]+[0-9.]*$|\s\.ip6|/\d+$#; my $resolver = Net::DNS::Resolver->new(@conf, recurse => 1 ); # create resolver object $resolver->nameservers(@server) || die $resolver->string; my @ns = NS($name); # find NS serving name unless ( @ns ) { print $resolver->string; # show resolver state displayRR($name, 'NS'); # show response code displayRR($name, 'ANY'); # show any RR for name exit; # game over } my @nsdname = map{lc $_->nsdname} @ns unless @server; # extract server names from NS records my @nameserver = (@server, sort @nsdname); my $zone = $ns[0]->name; # find zone name for ( displayRR($zone, 'SOA') ) { # simple sanity check my $mname = lc $_->mname; # primary server my $rname = lc $_->rname; # mailbox of person responsible for zone my $n = int $_->expire/($_->retry || 1); # number of transfer attempts my $s = $n != 1 ? 's' : ''; report("data expires after $n zone transfer failure$s") unless $n > 3; report('zone data expires before scheduled refresh') unless $_->expire > $_->refresh; my @ncache = NCACHE($zone) if $_->minimum > $neg_ttl; for ( @ncache ) { my $ttl = $_->ttl; # flag large NCACHE TTL report('negative cache TTL', clock($ttl)) if $ttl > $neg_ttl; } next if $mname eq lc $zone; # local zone if ( "$rname." =~ /[^\\]\.(.+)$/i ) { # check mail domain for RNAME my $rnameok; foreach my $type (qw(MX A AAAA)) { my $packet = $resolver->send($1, $type); next unless $packet; $rnameok++ unless $packet->header->ra; $rnameok++ if $packet->answer; } report("unresolved RNAME field:\t$1") unless $rnameok; } next if $resolver->query("$mname.", 'A'); # skip if address record exists next if $resolver->query("$mname.", 'AAAA'); report("unresolved MNAME field:\t$mname."); # RFC2181, 7.3 next unless $mname =~ /((.+\.){2})$zone$/i; # missing final dot? report("absolute name expected:\t$1 <----- '.' absent") if $resolver->query($1, 'ANY'); } displayRR($zone, 'NS') if @server; # show NS if testing specified server displayRR($name, 'ANY'); # show RR for user-specified name print "----\n"; my ($bad) = checkNS($zone, @nameserver); # report status print "\n"; exit if @server; my $s = $bad != 1 ? 's' : ''; print "Unsatisfactory response from $bad nameserver$s\n\n" if $bad; exit; sub catnap { # short duration sleep my $duration = shift; # seconds sleep(1+$duration) unless eval { defined select(undef, undef, undef, $duration) }; } sub checkNS { # check nameservers (in parallel) and report status my $zone = shift; my $index = @_; # index last element my $element = pop @_ || return (0,0,{}); # pop element, terminate if undef my ($ns, $if) = split / /, $element; # name + optional interface IP my $res = Net::DNS::Resolver->new(@conf); # use clean resolver for each test my @xip = sort $res->nameservers($if || $ns); # point at nameserver @xip = $res->nameservers("$ns.") unless @xip; # retry as absolute name (eg. localhost.) my $ip = pop @xip; # last (or only) interface $res->nameservers($ip) if @xip; # send query packet to nameserver my ($socket, $sent) = ($res->bgsend($zone,'SOA'), time) if $ip; my ($fail, $latest, $uniq) = checkNS($zone,@_); # recurse to query others in parallel # pick up response as recursion unwinds my @pass = ($fail, $latest, $uniq); # use prebuilt return values my @fail = ($fail+1, $latest, $uniq); my %nsaddr; if ( @xip and $socket ) { # special handling for multihomed server $nsaddr{lc $ip}++; # silently ignore duplicate address record until ($res->bgisready($socket)) { # wait for outstanding query to complete last if time > ($sent + $udp_timeout); catnap($udp_wait); } } foreach my $xip (@xip) { # iterate over remaining interfaces my ($f) = checkNS($zone, (undef)x@_, "$ns $xip") unless $nsaddr{lc $xip}++; @pass = @fail if $f; # propagate failure to caller } my %nsname; # identify nameserver unless ( $ip ) { return @pass if lc $ns eq lc $zone; print "\n[$index]\t$ns\n"; report('unresolved server name'); return @fail; } elsif ( $ns =~ /:|^[0-9\.]+$/o ) { print "\n[$index]\t$ip\n"; } else { print "\n[$index]\t$ns ($ip)\n"; $nsname{lc $1}++ if $ns =~ /(.*[^\.])\.*$/o; } if ( $verbose ) { foreach ( grep{$_->type eq 'PTR'} listRR($ip) ) { $nsname{lc $_->ptrdname}++; } foreach my $ns ( sort keys %nsname ) { # show address records listRR($ns, 'A'); listRR($ns, 'AAAA'); } } my $packet; if ( $socket ) { until ( $res->bgisready($socket) ) { # timed wait on socket last if time > ($sent + $udp_timeout); catnap($udp_wait); # snatch a few milliseconds sleep } $packet = $res->bgread($socket) if $res->bgisready($socket); # get response } else { $packet = $res->send($zone, 'SOA'); # use sequential query model } unless ( $packet ) { # ... is no more! It has ceased to be! report('no response'); return @fail; } unless ( $packet->header->rcode eq 'NOERROR' ) { report($packet->header->rcode); # NXDOMAIN or fault at nameserver return @fail; # RFC2308, 2.1 } my $aa = $packet->header->aa; # authoritative answer my $tc = $packet->header->tc ? 'tc' : ''; # truncated response my @answer = $packet->answer; # answer section my @soa = grep{$_->type eq 'SOA'} @answer; # SOA records (plural!) my @result = @fail; # analyse response if ( @soa ) { @result = @pass if $aa and @soa == 1; # RFC2181, 6.1 report(scalar @soa, 'SOA records') unless @soa == 1; my $ttl = $soa[0]->ttl; # RFC1034, 6.2.1 (2) report("non-authoritative answer\tTTL", clock($ttl)) unless $aa; } elsif ( @soa = grep{$_->type eq 'SOA'} $packet->authority ) { my $ttl = $soa[0]->ttl; # RFC2308, 2.2 (1)(2) report("NODATA response\tTTL", clock($ttl)); return @fail unless grep{$_->name =~ /^$zone$/i} @soa; report('requested SOA in authority section; violates RFC2308'); } elsif ( my @ns = grep{$_->type eq 'NS'} $packet->authority ) { report('referral received from nameserver'); # RFC2308, 2.2 (4) my @n = grep{$_->nsdname =~ /$ns/i} @ns; # self referral? my @a = grep{$_->rdatastr =~ /$ip/i} $packet->additional; report('authoritative data expired') if @n or @a; return @fail; # RFC2181, 6.1 } else { report('NODATA response from nameserver'); # RFC2308, 2.2 (3) return @fail; # RFC2181, 6.1 } my $serial; # zone serial number for ( @soa ) { print "$tc\t\t\tzone serial\t", ($serial = $_->serial), "\n"; $_->serial(0); # key on static fields only next if $uniq->{lc $_->rdatastr}++; # skip repeated occurrences next unless keys %$uniq > 1; # zone should have unique SOA report('SOA record not unique'); # RFC2181, 6.1 @result = @fail; } return @result if $serial == $latest; # server has latest data unless ( $aa and ($serial > $latest) ) { # unexpected serial number report('serial number not current') if $latest; return @fail; } my $unrep = $latest ? (@_ - $fail) : 0; # all previous out of date my $s = $unrep > 1 ? 's' : ''; # pedants really are revolting! report("at least $unrep previously unreported stale serial number$s") if $unrep; return ($result[0]+$unrep, $serial, $uniq); # restate partial result } sub clock { # human-friendly TTL if ( (my $s = shift) < 180000 ) { my $h = int( ($s+180)/360 )/10; return "$s (${h}h)"; } else { my $d = int( ($s+43200)/86400 ); return "$s (${d}d)"; } } sub displayRR { # print specified RRs with flags or error code my $packet = $resolver->send(@_) || return (); # get specified RRs my $header = $packet->header; my $rcode = $header->rcode; # response code my $na = $header->tc ? 'tc' : ''; # non-auth response my $aa = $header->aa ? "aa $na" : $na; # authoritative answer my ($question) = $packet->question; my $qname = $question->qname; my $qtype = $question->qtype; my @answer = $packet->answer; my @rr = grep{$_->type !~ /^(NS|SOA)$/o} @answer; # almost ANY foreach ( ($qtype eq 'ANY') ? @rr : @answer) { # print RR with status flags my $string = $_->string; # display IPv6 compact form $string =~ s/(:[:0]*:)(?!.*::|.+\1)/::/o if $_->type eq 'AAAA'; my $l = 95; # abbreviate long RR substr($string,$l) = ' ...' if length $string > $l and $_->type !~ /SOA|PTR/o; print $_->name =~ /^$qname$/i ? $aa : $na, "\t$string\n"; } unless ( @answer or ($rcode ne 'NOERROR') ) { # NODATA pseudo-RCODE per RFC2308, 2.2 my @authority = $packet->authority; my @additional = $packet->additional; $rcode = 'NODATA' unless @authority + @additional; # type 3 $rcode = 'NODATA' if grep{$_->type eq 'SOA'} @authority; # type 1 or 2 } report("$rcode:\t", $question->string) unless $rcode eq 'NOERROR'; return @answer; } sub listRR { # print specified RRs without flags or error code my $packet = $resolver->send(@_) || return (); # get specified RRs my @answer = $packet->answer; foreach ( @answer ) { # print RR my $string = $_->string; # display IPv6 compact form $string =~ s/(:[:0]*:)(?!.*::|.+\1)/::/o if $_->type eq 'AAAA'; print "\t$string\n"; } return @answer; } sub NCACHE { # get NCACHE SOA for domain my $domain = shift || ''; my $seq = time; my $nxdomain = "_nxdn_$seq.$domain"; my $reply = $resolver->send($nxdomain) || return (); return grep{$_->type eq 'SOA'} $reply->authority; } sub NS { # find nameservers for domain my $domain = shift || '.'; my @ns = (); while ( $domain ) { my $packet = $resolver->send($domain, 'NS'); die $resolver->string unless $packet; # local resolver problem last if @ns = grep{$_->type eq 'NS'} $packet->answer; my ($ncache) = grep{$_->type eq 'SOA'} $packet->authority; my $apex = $ncache->name if $ncache; # zone cut return NS($apex) if $apex; # NODATA from zone server return () if defined $apex; # NXDOMAIN from root server # accept referral if any my @referral = grep{$_->type eq 'NS'} $packet->authority; last if @ns = grep{$_->name =~ /^$domain$/i} @referral; $resolver->recurse(0); # retry as non-recursive query $packet = $resolver->send($domain, 'NS'); $resolver->recurse(1); @referral = grep{$_->type eq 'NS'} $packet->authority; last if @ns = grep{$_->name =~ /^$domain$/i} @referral; # IP (pre 0.59 compatibility) my ($x) = grep{$_->qtype eq 'PTR'} $packet->question; return NS($x->qname) if $x; ($x, $domain) = split /\./, $domain, 2; # strip leftmost label } return @ns; } sub report { # concatenate strings into fault report print join(' ', '*'x4, @_, "\n"); } __END__