2016-07-05 20:09:24 +02:00
|
|
|
#!/usr/bin/perl
|
|
|
|
use warnings;
|
2008-04-28 09:47:50 +02:00
|
|
|
use strict;
|
|
|
|
use Net::DNS;
|
|
|
|
|
|
|
|
sub usage {
|
|
|
|
print STDERR "Usage: $0 domainname-or-ip-address\n";
|
|
|
|
exit(1);
|
|
|
|
}
|
|
|
|
|
2016-07-05 20:09:24 +02:00
|
|
|
my $verbose;
|
|
|
|
|
|
|
|
if (($ARGV[0] || '') eq '-v') {
|
|
|
|
$verbose = 1;
|
|
|
|
shift @ARGV;
|
|
|
|
}
|
|
|
|
|
2008-04-28 09:47:50 +02:00
|
|
|
usage() unless (@ARGV == 1);
|
|
|
|
|
2008-05-05 16:17:39 +02:00
|
|
|
# generic resolver
|
|
|
|
my $res0 = new Net::DNS::Resolver;
|
|
|
|
|
|
|
|
# special resolver to query specific nameservers
|
|
|
|
my $res1 = new Net::DNS::Resolver;
|
|
|
|
my $zone;
|
2008-04-28 09:47:50 +02:00
|
|
|
if ($ARGV[0] =~ m/^(\d+)\.(\d+)\.(\d+)\.(\d+)$/) {
|
|
|
|
check_ptr($ARGV[0]);
|
|
|
|
} else {
|
|
|
|
check_a($ARGV[0]);
|
|
|
|
}
|
2008-05-05 16:17:39 +02:00
|
|
|
check_zone($ARGV[0]) if ($zone);
|
2008-04-28 09:47:50 +02:00
|
|
|
|
|
|
|
exit 0;
|
|
|
|
|
|
|
|
my %addr_to_name;
|
|
|
|
my %name_to_addr;
|
|
|
|
|
|
|
|
sub check_ptr {
|
|
|
|
my ($addr, $name) = @_;
|
|
|
|
|
|
|
|
my @names;
|
|
|
|
if (defined $addr_to_name{$addr}) {
|
|
|
|
@names = @{ $addr_to_name{$addr} };
|
|
|
|
} else {
|
|
|
|
# XXX - ipv6?
|
|
|
|
my $q = join('.', reverse (split(/\./, $addr)), "in-addr", "arpa");
|
2008-05-05 16:17:39 +02:00
|
|
|
my $reply = $res0->send($q, 'PTR');
|
2008-04-28 09:47:50 +02:00
|
|
|
for my $ans ($reply->answer) {
|
|
|
|
if ($ans->type eq 'PTR') {
|
|
|
|
push @names, $ans->ptrdname;
|
|
|
|
} else {
|
|
|
|
die "cannot happen";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (@names == 0) {
|
|
|
|
print "[$addr] $q has no PTR record\n";
|
|
|
|
}
|
|
|
|
$addr_to_name{$addr} = \@names;
|
2008-05-05 16:17:39 +02:00
|
|
|
get_zone($reply) unless $zone;
|
2008-04-28 09:47:50 +02:00
|
|
|
}
|
|
|
|
if ($name) {
|
|
|
|
unless (grep { $_ eq $name } @names) {
|
|
|
|
print "$name not found in PTR records of [$addr]\n";
|
|
|
|
}
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
for my $name (@names) {
|
2016-07-05 20:09:24 +02:00
|
|
|
print "I: [$addr] -> $name\n" if $verbose;
|
2008-04-28 09:47:50 +02:00
|
|
|
check_a($name, $addr);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
sub check_a {
|
|
|
|
my ($name, $addr) = @_;
|
|
|
|
|
|
|
|
my @addrs;
|
|
|
|
if (defined $name_to_addr{$name}) {
|
|
|
|
@addrs = @{ $name_to_addr{$name} };
|
|
|
|
} else {
|
2008-05-05 16:17:39 +02:00
|
|
|
my $reply = $res0->send($name, 'A');
|
2008-04-28 09:47:50 +02:00
|
|
|
for my $ans ($reply->answer) {
|
|
|
|
if ($ans->type eq 'A') {
|
|
|
|
push @addrs, $ans->address;
|
2008-05-05 16:17:39 +02:00
|
|
|
} elsif ($ans->type eq 'CNAME') {
|
|
|
|
die "cnames not yet supported";
|
2008-04-28 09:47:50 +02:00
|
|
|
} else {
|
|
|
|
print "unexpected response to A query for $name\n";
|
|
|
|
$ans->print;
|
|
|
|
}
|
|
|
|
}
|
2008-05-05 16:17:39 +02:00
|
|
|
if (@addrs == 0) {
|
2008-04-28 09:47:50 +02:00
|
|
|
print "$name has no A record\n";
|
|
|
|
}
|
|
|
|
$name_to_addr{$name} = \@addrs;
|
2008-05-05 16:17:39 +02:00
|
|
|
get_zone($reply) unless $zone;
|
2008-04-28 09:47:50 +02:00
|
|
|
}
|
|
|
|
if ($addr) {
|
|
|
|
unless (grep { $_ eq $addr } @addrs) {
|
|
|
|
print "[$addr] not found in A records of $name\n";
|
|
|
|
}
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
for my $addr (@addrs) {
|
2016-07-05 20:09:24 +02:00
|
|
|
print "I: $name -> [$addr]\n" if $verbose;
|
2008-04-28 09:47:50 +02:00
|
|
|
check_ptr($addr, $name);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2008-05-05 16:17:39 +02:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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);
|
2008-05-05 17:40:20 +02:00
|
|
|
} elsif ($rr->type eq 'CNAME') {
|
|
|
|
$key = $rr->cname;
|
|
|
|
} elsif ($rr->type eq 'TXT') {
|
|
|
|
$key = $rr->txtdata;
|
|
|
|
} elsif ($rr->type eq 'SRV') {
|
|
|
|
$key = join(' ', $rr->priority, $rr->weight, $rr->port, $rr->target);
|
|
|
|
} elsif ($rr->type eq 'PTR') {
|
|
|
|
$key = $rr->ptrdname;
|
|
|
|
} elsif ($rr->type eq 'HINFO') {
|
|
|
|
$key = join(' ', $rr->cpu, $rr->os);
|
|
|
|
} elsif ($rr->type eq 'LOC') {
|
|
|
|
# sloppy
|
|
|
|
my ($lat, $lon) = $rr->latlon;
|
|
|
|
$key = join(' ', $lat, $lon, $rr->altitude);
|
2008-05-05 16:17:39 +02:00
|
|
|
} else {
|
|
|
|
print STDERR "unhandled RR:\n";
|
|
|
|
print STDERR $rr->string, "\n";
|
|
|
|
exit(1);
|
|
|
|
}
|
|
|
|
return $key;
|
2008-04-28 09:47:50 +02:00
|
|
|
}
|
|
|
|
|
2008-05-05 16:17:39 +02:00
|
|
|
# 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
|