From 888f0a58eb8125229d28b2d194c0208ae4f340e4 Mon Sep 17 00:00:00 2001 From: hjp Date: Mon, 5 May 2008 14:17:39 +0000 Subject: [PATCH] Added some zone checks: * check if NS records are consistent * compare zones from all authoritative name servers --- dns/check.pl | 218 ++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 207 insertions(+), 11 deletions(-) diff --git a/dns/check.pl b/dns/check.pl index 72dfd41..8c8486f 100644 --- a/dns/check.pl +++ b/dns/check.pl @@ -9,13 +9,18 @@ sub usage { usage() unless (@ARGV == 1); -my $res = new Net::DNS::Resolver; +# generic resolver +my $res0 = new Net::DNS::Resolver; + +# special resolver to query specific nameservers +my $res1 = new Net::DNS::Resolver; +my $zone; if ($ARGV[0] =~ m/^(\d+)\.(\d+)\.(\d+)\.(\d+)$/) { check_ptr($ARGV[0]); } else { check_a($ARGV[0]); - # check_ns($ARGV[0]); } +check_zone($ARGV[0]) if ($zone); exit 0; @@ -31,7 +36,7 @@ sub check_ptr { } else { # XXX - ipv6? my $q = join('.', reverse (split(/\./, $addr)), "in-addr", "arpa"); - my $reply = $res->send($q, 'PTR'); + my $reply = $res0->send($q, 'PTR'); for my $ans ($reply->answer) { if ($ans->type eq 'PTR') { push @names, $ans->ptrdname; @@ -43,6 +48,7 @@ sub check_ptr { print "[$addr] $q has no PTR record\n"; } $addr_to_name{$addr} = \@names; + get_zone($reply) unless $zone; } if ($name) { unless (grep { $_ eq $name } @names) { @@ -62,19 +68,22 @@ sub check_a { if (defined $name_to_addr{$name}) { @addrs = @{ $name_to_addr{$name} }; } else { - my $reply = $res->send($name, 'A'); + my $reply = $res0->send($name, 'A'); for my $ans ($reply->answer) { if ($ans->type eq 'A') { push @addrs, $ans->address; + } elsif ($ans->type eq 'CNAME') { + die "cnames not yet supported"; } else { print "unexpected response to A query for $name\n"; $ans->print; } } - if (@addrs == 0) { + if (@addrs == 0) { print "$name has no A record\n"; } $name_to_addr{$name} = \@addrs; + get_zone($reply) unless $zone; } if ($addr) { unless (grep { $_ eq $addr } @addrs) { @@ -87,11 +96,198 @@ sub check_a { } } -my $answer = $res->query($ARGV[0], $ARGV[1]); -if ($answer) { - $answer->print; -} else { - print STDERR "query failed: ", $res->errorstring, "\n"; +sub get_zone { + my ($reply) = @_; + + for my $auth ($reply->authority) { + if (defined $zone) { + if ($zone ne $auth->name) { + print "inconsistent authority RRs:\n"; + print $auth->print; + print "doesn't match previously found zone $zone\n"; + } + } else { + $zone = $auth->name; + } + } } -# $res->nameservers($ARGV[2]); +sub check_zone { + my ($name) = @_; + + my $rootns = chr(rand(13) + 65) . ".root-servers.net."; + my $reply = $res0->send($rootns, 'A'); + my @ns = (($reply->answer)[0]->address); + my %authns; + my %seen; + while (@ns) { + my $ns = shift (@ns); + $res1->nameservers($ns); + my $reply = $res1->send($zone, 'NS'); + + # if the reply contains a non-empty answer section, use it as + # a list of authoritative name servers. + if ($reply->answer) { + for my $rr ($reply->answer) { + if ($rr->type eq 'NS') { + print STDERR "$ns reported NS ", $rr->nsdname, "\n";; + + $authns{$ns}{$rr->nsdname} = 1; + $authns{ALL}{$rr->nsdname} = 1; + for (get_addresses($rr->nsdname)) { + unless ($seen{$_}) { + push @ns, $_; + $seen{$_} = 1; + } + } + } + } + next; + } + # + if ($reply->authority) { + for my $rr ($reply->authority) { + if ($rr->type eq 'NS') { + if ($rr->name eq $name) { + # if the reply contains an authority section with the right + # domain, use it as a list of authoritative name servers. + print STDERR "$ns reported NS ", $rr->nsdname, "\n";; + + $authns{$ns}{$rr->nsdname} = 1; + $authns{ALL}{$rr->nsdname} = 1; + for (get_addresses($rr->nsdname)) { + unless ($seen{$_}) { + push @ns, $_; + $seen{$_} = 1; + } + } + } else { + # Otherwise, just add the nameservers from the authority section + # to the list of nameservers still to query. + for (get_addresses($rr->nsdname)) { + unless ($seen{$_}) { + push @ns, $_; + $seen{$_} = 1; + } + } + } + } + } + next; + } + + + } + + # We must make sure that we get a result from all authoritative + # name servers + # + # XXX + # + # Isn't that included in the next test? If an authoritative + # nameserver doesn't answer, it will be reported as not reporting + # all nameservers. + + # All lists of nameservers must be identical. + # + for my $authns (sort keys %{ $authns{ALL} }) { + for my $origns (sort keys %authns) { + print "$origns doesn't report $authns\n" unless $authns{$origns}{$authns}; + } + } + + my @noaxfr; + my %zone; + for my $authns (sort keys %{ $authns{ALL} }) { + $res1->nameservers($authns); + my @zone = $res1->axfr($zone); + push @noaxfr, $authns unless @zone; + for my $rr (@zone) { + my $key = rr2key($rr); + + $zone{$authns}{$rr->name}{$rr->type}{$key} = 1; + $zone{ALL}{$rr->name}{$rr->type}{$key} = 1; + } + } + for my $authns (@noaxfr) { + $res1->nameservers($authns); + for my $name (sort keys %{ $zone{ALL} }) { + for my $type (sort keys %{ $zone{ALL}{$name} }) { + my $reply = $res1->send($name, $type); + for my $rr ($reply->answer) { + if ($rr->type eq $type) { + my $key = rr2key($rr); + $zone{$authns}{$rr->name}{$rr->type}{$key} = 1; + $zone{ALL}{$rr->name}{$rr->type}{$key} = 1; + } + } + } + } + } + for my $authns (sort keys %zone) { + # next if $authns eq 'ALL'; + for my $name (sort keys %{ $zone{ALL} }) { + for my $type (sort keys %{ $zone{ALL}{$name} }) { + for my $key (sort keys %{ $zone{ALL}{$name}{$type} }) { + unless ($zone{$authns}{$name}{$type}{$key}) { + print STDERR "$authns is missing $name $type $key\n"; + } + } + } + } + } + + +} + +sub get_addresses { + my ($name) = @_; + my @addrs; + my $reply = $res0->send($name, 'A'); + for my $rr ($reply->answer) { + if ($rr->type eq 'A') { + push @addrs, $rr->address; + } + # XXX - resolve CNAMEs? + } + return @addrs; +} + +sub rr2key { + my ($rr) = @_; + + my $key; + + if ($rr->type eq 'A') { + $key = $rr->address; + } elsif ($rr->type eq 'SOA') { + $key = join(' ', $rr->mname, $rr->rname, $rr->serial, $rr->refresh, $rr->retry, $rr->expire, $rr->minimum); + } elsif ($rr->type eq 'NS') { + $key = $rr->nsdname; + } elsif ($rr->type eq 'MX') { + $key = join(' ', $rr->preference, $rr->exchange); + } else { + print STDERR "unhandled RR:\n"; + print STDERR $rr->string, "\n"; + exit(1); + } + return $key; +} + +# Notes: +# +# for every a record, check ptr. +# +# for every ptr, check a +# +# find the zone (authority section). +# +# Check name servers (starting at random root). +# +# Try axfr. +# check each record: +# the same on all nameservers? +# A to PTR and vice versa +# MX +# +# vim: tw=0 sw=4 expandtab