Server IP : 85.214.239.14 / Your IP : 3.147.68.25 Web Server : Apache/2.4.62 (Debian) System : Linux h2886529.stratoserver.net 4.9.0 #1 SMP Tue Jan 9 19:45:01 MSK 2024 x86_64 User : www-data ( 33) PHP Version : 7.4.18 Disable Function : pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,pcntl_unshare, MySQL : OFF | cURL : OFF | WGET : ON | Perl : ON | Python : ON | Sudo : ON | Pkexec : OFF Directory : /usr/share/doc/libnet-dns-perl/examples/contrib/ |
Upload File : |
#!/usr/bin/perl -w # $Id: check_zone 1842 2021-07-08 14:25:00Z willem $ =head1 NAME check_zone - Check a DNS zone for errors =head1 SYNOPSIS C<check_zone> [ C<-r> ][ C<-v> ] I<domain> [ I<class> ] =head1 DESCRIPTION Checks a DNS zone for errors. Current checks are: =over 4 =item * Checks the domain's SOA from each of the domain's name servers. The SOA serial numbers should match. This program's output cannot be trusted if they do not. =item * Tries to perform an AXFR from each of the domain's name servers. This test helps to detect whether the name server is blocking AXFR. =item * Checks that all A records have corresponding PTR records. For each A record its PTR's name is match checked. =item * Checks that all PTR records match an A record (sometimes they match a CNAME). Check the PTR's name against the A record. =item * Checks that hosts listed in NS, MX, and CNAME records have A records. Checks for NS and CNAME records not pointing to another CNAME (i.e., they must directly resolve to an A record). That test may be somewhat controversial because, in many cases, a MX to a CNAME or a CNAME to another CNAME will resolve; however, in DNS circles it isn't a recommended practise. =item * Check each record processed for being with the class requested. This is an internal integrity check. =back =head1 OPTIONS =over 4 =item C<-r> Perform a recursive check on subdomains. =item C<-v> Verbose. =item C<-a alternate_domain> Treat <alternate_domain> as equal to <domain>. This is useful when supporting a change of domain names (eg from myolddomain.example.net to mynewdomain.example.net) where the PTR records can point to only one of the two supported domains (which are otherwise identical). =item C<-e exception_file> Ignore exceptions in file <exception_file>. File format can be space-separated domain pairs, one pair per line, or it can be straight output from this program itself (for simple cut-and-paste functionality). This allows for skipping entries that are odd or unusual, but not causing problems. Note: this only works with A - PTR checks. =back =head1 AUTHORS Originally developed by Michael Fuhr (mfuhr@dimensional.com) and hacked--with furor--by Dennis Glatting (dennis.glatting@software-munitions.com). "-a" and "-e" options added by Paul Archer =head1 SEE ALSO L<perl(1)>, L<axfr>, L<check_soa>, L<mx>, L<perldig>, L<Net::DNS> =head1 BUGS A query for an A RR against a name that is a CNAME may not follow the CNAME to an A RR. There isn't a mechanism to insure records are returned from an authoritative source. There appears to be a bug in the resolver AXFR routine where, if one server cannot be contacted, the routine doesn't try another in its list. =cut #require 'assert.pl'; use strict; use warnings; use Carp; use vars qw($opt_r); use vars qw($opt_v); use vars qw($opt_a); use vars qw($opt_e); use Getopt::Std; use File::Basename; use IO::Socket; use Net::DNS; getopts("rva:e:"); die "Usage: ", basename($0), " [ -r -v ] [ -a alternate_domain] [ -e eqivalent_domains_file ] domain [ class ]\n" unless (@ARGV >= 1) && (@ARGV <= 2); our $exit_status = 0; local $SIG{__WARN__} = sub {$exit_status=1 ; print STDERR @_ }; $opt_r = 1; our $main_domain=$ARGV[0]; our %exceptions = parse_exceptions_file(); foreach my $key (sort keys %exceptions) { print "$key:\t"; foreach my $val (@{$exceptions{$key}}) { print "$val "; } print "\n"; } check_domain(@ARGV); exit $exit_status; sub assert { croak 'violated assertion' unless shift; return } sub parse_exceptions_file { my %exceptions; my $file = $opt_e || ""; return %exceptions unless ( -r $file); open( my $fh, '<', $file ); die "Couldn't read $file: $!" unless $fh; while (<$fh>) { chomp; #print " raw line: $_\n"; next if /^$/; next if /^\s*#/; s/#.*$//; s/^\s*//; s/\s*$//; s/'//g; my ($left, $right) = (split /[\s:]+/, $_)[0, -1]; push @{$exceptions{$left}}, $right; #print "processed line: $line\n"; } close($fh); return %exceptions; } sub check_domain { my ( $domain, $class ) = @_; my $ns; my @zone; $class ||= "IN"; print "-" x 70, "\n"; print "$domain (class $class)\n"; print "\n"; my $res = Net::DNS::Resolver->new(); $res->defnames( 0 ); $res->retry( 2 ); my( $nspack, $ns_rr, @nsl ); # Get a list of name servers for the domain. # Error-out if the query isn't satisfied. # $nspack = $res->query( $domain, 'NS', $class ); unless( defined( $nspack )) { warn "Couldn't find nameservers for $domain: ", $res->errorstring, "\n"; return; } printf( "List of name servers returned from '%s'\n", $res->answerfrom ); foreach my $ns_rr ( $nspack->answer ) { $ns_rr->print if( $opt_v ); assert( $class eq $ns_rr->class ); assert( 'NS' eq $ns_rr->type ); if( $ns_rr->name eq $domain ) { print "\t", $ns_rr->rdatastr, "\n"; push @nsl, $ns_rr->rdatastr; } else { warn( "asked for '$domain', got '%s'\n", $ns_rr->rdatastr ); } } print "\n"; warn( "\tZone has no NS records\n" ) if( scalar( @nsl ) == 0 ); # Transfer the zone from each of the name servers. # The zone is transferred for several reasons. # First, so the check routines won't (an efficiency # issue) and second, to see if we can. # $res->nameservers( @nsl ); foreach my $ns ( @nsl ) { $res->nameservers( $ns ); my @local_zone = $res->axfr( $domain, $class ); unless( @local_zone ) { warn "Zone transfer from '", $ns, "' failed: ", $res->errorstring, "\n"; } @zone = @local_zone if( ! @zone ); } # Query each name server for the zone # and check the zone's SOA serial number. # print "checking SOA records\n"; check_soa( $domain, $class, \@nsl ); print "\n"; # Check specific record types. # print "checking NS records\n"; check_ns( $domain, $class, \@nsl, \@zone ); print "\n"; print "checking A records\n"; check_a( $domain, $class, \@nsl, \@zone ); print "\n"; print "checking PTR records\n"; check_ptr( $domain, $class, \@nsl, \@zone ); print "\n"; print "checking MX records\n"; check_mx( $domain, $class, \@nsl, \@zone ); print "\n"; print "checking CNAME records\n"; check_cname( $domain, $class, \@nsl, \@zone ); print "\n"; # Recurse? # if( $opt_r ) { my %subdomains; print "checking subdomains\n\n"; # Get a list of NS records from the zone that # are not for the zone (i.e., they're subdomains). # foreach ( grep { $_->type eq 'NS' and $_->name ne $domain } @zone ) { $subdomains{$_->name} = 1; } # For each subdomain, check it. # foreach ( sort keys %subdomains ) { check_domain($_, $class); } } return; } sub check_soa { my( $domain, $class, $nsl ) = @_; my( $soa_sn, $soa_diff ) = ( 0, 0 ); my( $ns, $soa_rr ); my $rr_count = 0; my $res = Net::DNS::Resolver->new(); $res->defnames( 0 ); $res->retry( 2 ); $res->recurse( 0 ); # Contact each name server and get the # SOA for the somain. # foreach my $ns ( @$nsl ) { my $soa = 0; my $nspack; # Query the name server and test # for a result. # $res->nameservers( $ns ); $nspack = $res->query( $domain, "SOA", $class ); unless( defined( $nspack )) { warn "Couldn't get SOA from '$ns'\n"; next; } # Look at each SOA for the domain from the # name server. Specifically, look to see if # its serial number is different across # the name servers. # foreach my $soa_rr ( $nspack->answer ) { $soa_rr->print if( $opt_v ); assert( $class eq $soa_rr->class ); assert( 'SOA' eq $soa_rr->type ); print "\t$ns:\t", $soa_rr->serial, "\n"; # If soa_sn is zero then an SOA serial number # hasn't been recorded. In that case record # the serial number. If the serial number # doesn't match a previously recorded one then # indicate they are different. # # If the serial numbers are different then you # cannot really trust the remainder of the test. # if( $soa_sn ) { $soa_diff = 1 if ( $soa_sn != $soa_rr->serial ); } else { $soa_sn = $soa_rr->serial; } } ++$rr_count; } print "\t*** SOAs are different!\n" if( $soa_diff ); print "$rr_count SOA RRs checked.\n"; return; } sub check_ptr { my( $domain, $class, $nsl, $zone ) = @_; my $res = Net::DNS::Resolver->new(); my $ptr_rr; my $rr_count = 0; $res->defnames( 0 ); $res->retry( 2 ); $res->nameservers( @$nsl ); foreach my $ptr_rr ( grep { $_->type eq 'PTR' } @$zone ) { my @types; $ptr_rr->print if( $opt_v ); assert( $class eq $ptr_rr->class ); assert( 'PTR' eq $ptr_rr->type ); print "\tchecking PTR rr '$ptr_rr' to PTR\n" if( $opt_v ); @types = types4name( $ptr_rr->ptrdname, $domain, $class, $nsl ); if( grep { $_ eq 'A' } @types ) { xcheck_ptr2a( $ptr_rr, $domain, $class, $nsl ); } else { warn "\t'", $ptr_rr->ptrdname, "' doesn't resolve to an A RR (RRs are '", join( ', ', @types ), "')\n"; } ++$rr_count; } print "$rr_count PTR RRs checked.\n"; return; } sub check_ns { my( $domain, $class, $nsl, $zone ) = @_; my $res = Net::DNS::Resolver->new(); my $ns_rr; my $rr_count = 0; $res->defnames( 0 ); $res->retry( 2 ); $res->nameservers( @$nsl ); # Go through the zone data and process # all NS RRs for the zone (delegation # NS RRs are ignored). Specifically, # check to see if the indicate name server # is a CNAME RR and the name resolves to an A # RR. Check to insure the address resolved # against the name has an associated PTR RR. # foreach my $ns_rr ( grep { $_->type eq 'NS' } @$zone ) { my @types; $ns_rr->print if( $opt_v ); assert( $class eq $ns_rr->class ); assert( 'NS' eq $ns_rr->type ); next if( $ns_rr->name ne $domain ); printf( "rr nsdname: %s\n", $ns_rr->nsdname ) if $opt_v; @types = types4name( $ns_rr->nsdname, $domain, $class, $nsl ); if( grep { $_ eq 'A' } @types ) { xcheck_name( $ns_rr->nsdname, $domain, $class, $nsl ); } else { warn "\t'", $ns_rr->nsdname, "' doesn't resolve to an A RR (RRs are '", join( ', ', @types ), "')\n"; } ++$rr_count; } print "$rr_count NS RRs checked.\n"; return; } sub check_a { my( $domain, $class, $nsl, $zone ) = @_; my $res = Net::DNS::Resolver->new(); my $a_rr; my $rr_count = 0; $res->defnames( 0 ); $res->retry( 2 ); $res->nameservers( @$nsl ); # Go through the zone data and process # all A RRs. Specifically, check to insure # each A RR matches a PTR RR and the PTR RR # matches the A RR. # foreach my $a_rr ( grep { $_->type eq 'A' } @$zone ) { $a_rr->print if( $opt_v ); assert( $class eq $a_rr->class ); assert( 'A' eq $a_rr->type ); print "\tchecking A RR '", $a_rr->address, "' to PTR\n" if( $opt_v ); xcheck_a2ptr( $a_rr, $domain, $class, $nsl ); ++$rr_count; } print "$rr_count A RRs checked.\n"; return; } sub check_mx { my( $domain, $class, $nsl, $zone ) = @_; my $res = Net::DNS::Resolver->new(); my $mx_rr; my $rr_count = 0; $res->defnames( 0 ); $res->retry( 2 ); $res->nameservers( @$nsl ); # Go through the zone data and process # all MX RRs. Specifically, check to insure # each MX RR resolves to an A RR and the # A RR has a matching PTR RR. # foreach my $mx_rr ( grep { $_->type eq 'MX' } @$zone ) { $mx_rr->print if( $opt_v ); assert( $class eq $mx_rr->class ); assert( 'MX' eq $mx_rr->type ); print "\tchecking MX RR '", $mx_rr->exchange, "' to A\n" if( $opt_v ); xcheck_name( $mx_rr->exchange, $domain, $class, $nsl ); ++$rr_count; } print "$rr_count MX RRs checked.\n"; return; } sub check_cname { my( $domain, $class, $nsl, $zone ) = @_; my $res = Net::DNS::Resolver->new(); my $cname_rr; my $rr_count = 0; $res->defnames( 0 ); $res->retry( 2 ); $res->nameservers( @$nsl ); # Go through the zone data and process # all CNAME RRs. Specifically, check to insure # each CNAME RR resolves to an A RR and the # A RR has a matching PTR RR. # foreach my $cname_rr ( grep { $_->type eq 'CNAME' } @$zone ) { my @types; $cname_rr->print if( $opt_v ); assert( $class eq $cname_rr->class ); assert( 'CNAME' eq $cname_rr->type ); print "\tchecking CNAME RR '", $cname_rr->cname, "' to A\n" if( $opt_v ); @types = types4name( $cname_rr->cname, $domain, $class, $nsl ); if( grep { $_ eq 'A' } @types ) { xcheck_name( $cname_rr->cname, $domain, $class, $nsl ); } else { warn "\t'", $cname_rr->cname, "' doesn't resolve to an A RR (RRs are '", join( ', ', @types ), "')\n"; } ++$rr_count; } print "$rr_count CNAME RRs checked.\n"; return; } sub check_w_equivs_and_exceptions { my ($left, $comp, $right) = @_; if (defined $exceptions{$left}) { foreach my $rval (@{$exceptions{$left}}) { $left = $right if ($rval eq $right); } } if ($opt_a){ $left =~ s/\.?$opt_a$//; $left =~ s/\.?$main_domain$//; $right =~ s/\.?$opt_a$//; $right =~ s/\.?$main_domain$//; } return (eval { "\"$left\" $comp \"$right\"" } ); } sub xcheck_a2ptr { my( $a_rr, $domain, $class, $nsl ) = @_; my $res = Net::DNS::Resolver->new(); $res->defnames( 0 ); $res->retry( 2 ); $res->nameservers( @$nsl ); assert( $class eq $a_rr->class ); assert( 'A' eq $a_rr->type ); # Request a PTR RR against the A RR. # A missing PTR RR is an error. # my $ans = $res->query( $a_rr->address, 'PTR', $class ); if( defined( $ans )) { my $ptr_rr; foreach my $ptr_rr ( $ans->answer ) { $ptr_rr->print if( $opt_v ); assert( $class eq $ptr_rr->class ); assert( 'PTR' eq $ptr_rr->type ); warn( "\t'", $a_rr->name, "' has address '", $a_rr->address, "' but PTR is '", $ptr_rr->ptrdname, "'\n" ) if( check_w_equivs_and_exceptions($a_rr->name, "ne", $ptr_rr->ptrdname) ); warn( "\t'", $a_rr->name, "' has address '", $a_rr->address, "' but PTR is '", ip_ptr2a_str( $ptr_rr->name ), "'\n" ) if( $a_rr->address ne ip_ptr2a_str( $ptr_rr->name )); } } else { warn( "\tNO PTR RR for '", $a_rr->name, "' at address '", $a_rr->address,"'\n" ); } return; } sub xcheck_ptr2a { my( $ptr_rr, $domain, $class, $nsl ) = @_; my $res = Net::DNS::Resolver->new(); $res->defnames( 0 ); $res->retry( 2 ); $res->nameservers( @$nsl ); assert( $class eq $ptr_rr->class ); assert( 'PTR' eq $ptr_rr->type ); # Request an A RR against the PTR RR. # A missing A RR is an error. # my $ans = $res->query( $ptr_rr->ptrdname, 'A', $class ); if( defined( $ans )) { my $a_rr; foreach my $a_rr ( $ans->answer ) { $a_rr->print if( $opt_v ); assert( $class eq $a_rr->class ); assert( 'A' eq $a_rr->type ); warn( "\tPTR RR '", $ptr_rr->name, "' has name '", $ptr_rr->ptrdname, "' but A query returned '", $a_rr->name, "'\n" ) if( check_w_equivs_and_exceptions($ptr_rr->ptrdname, "ne", $a_rr->name) ); warn( "\tPTR RR '", $ptr_rr->name, "' has address '", ip_ptr2a_str( $ptr_rr->name ), "' but A query returned '", $a_rr->address, "'\n" ) if( ip_ptr2a_str( $ptr_rr->name ) ne $a_rr->address ); } } else { warn( "\tNO A RR for '", $ptr_rr->ptrdname, "' at address '", ip_ptr2a_str( $ptr_rr->address ), "'\n" ); } return; } sub xcheck_name { my( $name, $domain, $class, $nsl ) = @_; my $res = Net::DNS::Resolver->new(); $res->defnames( 0 ); $res->retry( 2 ); $res->nameservers( @$nsl ); # Get the A RR for the name. # my $ans = $res->query( $name, 'A', $class ); if( defined( $ans )) { # There is one or more A RRs. # For each A RR do a reverse look-up # and verify the PTR matches the A. # my $a_rr; foreach my $a_rr ( $ans->answer ) { $a_rr->print if( $opt_v ); assert( $class eq $a_rr->class ); assert( 'A' eq $a_rr->type ); warn( "\tQuery for '$name' returned A RR name '", $a_rr->name, "'\n" ) if( check_w_equivs_and_exceptions($name, "ne", $a_rr->name) ); xcheck_a2ptr( $a_rr, $domain, $class, $nsl ); } } else { warn( "\t", $name, " has no A RR\n" ); } return; } sub types4name { my( $name, $domain, $class, $nsl ) = @_; my $res = Net::DNS::Resolver->new(); my @rr_types; $res->defnames( 0 ); $res->retry( 2 ); $res->nameservers( @$nsl ); # Get the RRs for the name. # my $ans = $res->query( $name, 'ANY', $class ); if( defined( $ans )) { my $any_rr; foreach my $any_rr ( $ans->answer ) { $any_rr->print if( $opt_v ); assert( $class eq $any_rr->class ); push @rr_types, ( $any_rr->type ); } } else { warn( "\t'", $name, "' doesn't resolve.\n" ); } # If there were no RRs for the name then # return the RR types of ??? # push @rr_types, ( '???' ) if( ! @rr_types ); return @rr_types; } sub ip_ptr2a_str { my( $d, $c, $b, $a ) = ip_parts( $_[0]); return "$a.$b.$c.$d"; } sub ip_parts { my $ip = $_[0]; assert( $ip ne '' ); if( $ip =~ /(\d+)\.(\d+)\.(\d+)\.(\d+)/oi ) { return ( $1, $2, $3, $4 ); } else { warn "Unable to parse '$ip'\n"; } assert( 0 ); return; }