#!/bin/sh # # program: dlint # usage: dlint [-n] zone # options: -n no recursion # purpose: To scan through a DNS zone domain hierarchy and report certain # possible configuration problems found therein. # output: A verbose description of what was found in comments, # with warnings and error messages of any problems. # Output is intended to be computer-parsable. # Usage message gets printed on stderr. # exit value: 0 if everything looks right # 1 if nothing worse than a warning was found # 2 if any errors were found # 3 for usage error (i.e., incorrect command line options) # # Copyright (C) 1993-1998 Paul A. Balyoz # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. # # # NOTES # # * Handling localhost (127.0.0.1) is hard, how should it really be done? # If you define localhost. in many domains, you are screwed when you # look up 1.0.0.127.in-addr.arpa because it can only point to one of them. # Now maybe you think that 1.0.0.127.in-addr.arpa should point to "localhost." # But will all software on all computers really query "localhost." (in the root # domain), or will they actually be querying "localhost" (no dot, so resolver # considers it in the current domain)? # Current Solution: special-case "localhost" 127.0.0.1. # The only localhost-related things we check now are: # * 1.0.0.127.in-addr.arpa. points to some host that doesn't point back to # 127.0.0.1 (normal TEST 3a checking), # * if hostname "localhost" in any domain maps to an IP address other than # 127.0.0.1 (or host has address 127.0.0.1 but isn't named localhost), # * 1.0.0.127.in-addr.arpa. doesn't point to hostname "localhost" in any # domain (or it has host "localhost" but wrong in-addr.arpa address). # # Path to standard bin dirs on many platforms. # Be sure this path includes the directory that holds your dig executable: if test x"$PATH" = x""; then # for security purposes PATH="/usr/ucb:/usr/bsd:/bin:/usr/bin:/usr/local/bin:/usr/share/bin:/usr/com/bin" else PATH="${PATH}:/usr/ucb:/usr/bsd:/bin:/usr/bin:/usr/local/bin:/usr/share/bin:/usr/com/bin" fi export PATH VERSION=1.4.0 # ----------- BEGIN CONFIGURATIONS ------------------------- # RR filter from DiG output format to all FQDN on every line format. # Change this path for your site! See Makefile. rrfilt="/usr/local/bin/digparse" # ------------- END CONFIGURATIONS ------------------------- TMPNS=/var/tmp/dlintns.$$ TMPZONE=/var/tmp/dlintzone.$$ TMPPTR=/var/tmp/dlintptr.$$ TMPA=/var/tmp/dlinta.$$ TMPSUBDOMS=/var/tmp/dlintsubdoms.$$ TMPERR=/var/tmp/dlinterr.$$ TMPERR2=/var/tmp/dlinterr2.$$ TMPSERIALS=/var/tmp/dlintserials.$$ trap "rm -f $TMPNS $TMPZONE $TMPPTR $TMPA $TMPSUBDOMS $TMPERR $TMPERR2; exit 4" 1 2 3 15 usage() { echo 'usage: dlint [-n] zone' 2>&1 echo ' example zones: yoursite.com. 3.2.1.in-addr.arpa.' 2>&1 exit 3 } if test $# -lt 1 -o $# -gt 2; then usage fi # # Configure for System V echo or BSD echo, whichever we have. # if test `echo -n hello|wc -l` -eq 0; then echoc='' echon='-n' else echoc='\c' echon='' fi # # Check if dig is installed and get the version number. # If version < 2.1, fail. If version 9 or greater, set special settings. # ver=`dig localhost any | grep DiG | head -1 | sed -e 's/.*DiG \([0-9.]*\).*/\1/'` ans=`echo $ver | awk '$1 >= 2.1 {print "ok"; exit}'` # floating point math if test x"$ans" != x"ok"; then echo ';; This program requires DiG version 2.1 or newer, which I cannot find.' exit 3 fi dig9=`echo $ver | awk '$1 >= 9.0 {print "yes"; exit}'` # floating point math # # Options - why'd they change so many of these in BIND 9? # The +nostats option is not documented in BIND 9.0.1 but works. # if test x"$dig9" = x"yes"; then digopts='+ret=2 +noauthority +noadditional +noquestion +nostats +nocmd' else digopts='+ret=2 +noauthor +noaddit +noques +noHeader +noheader +cl +noqr +nostats +nocmd' fi # # Other things you might need to change # # Filter that converts input to lowercase tolower='tr A-Z a-z' # # Initialize flags (leave these alone) # exitcode=0 norecurse=false silent=false domain='' inaddrdomain=false # # Determine type of domain (forward or inverse) # arg 1 = domain name with ending period. # returns: "inverse" or "forward" on stdout # domaintype () { lcdom=`echo "$1" | $tolower` case $lcdom in *in-addr.arpa.) echo "inverse" ;; *) echo "forward" ;; esac } # # Parse command-line arguments # for i do case "$i" in -n) norecurse=true ;; -silent) silent=true ;; *) if test x"$domain" = x""; then domain="$i" else usage fi ;; esac done # Reverse-sense flags if $silent; then notsilent=false else notsilent=true fi if $norecurse; then recurse=false else recurse=true fi # No domain or empty domain specified if test x"$domain" = x""; then usage fi # Determine if domain is inverse-address or not ans=`echo $domain | $tolower | awk '/.in-addr.arpa/ {print "ok"; exit}'` if test x"$ans" = x"ok"; then inaddrdomain=true fi # # Print welcome message if not calling self recursively # if $notsilent; then echo ";; dlint version $VERSION, Copyright (C) 1998 Paul A. Balyoz " echo ";; Dlint comes with ABSOLUTELY NO WARRANTY." echo ";; This is free software, and you are welcome to redistribute it" echo ";; under certain conditions. Type 'man dlint' for details." echo ";; command line: $0 $*" echo $echon ";; flags:$echoc" if $inaddrdomain; then echo $echon " inaddr-domain$echoc" else echo $echon " normal-domain$echoc" fi if $norecurse; then echo $echon " not-recursive$echoc" else echo $echon " recursive$echoc" fi echo "." echo ";; using dig version $ver" echo ";; run starting: `date`" fi echo ";; ============================================================" echo ";; Now linting $domain" # # Identify all nameservers for this zone # #echo "XX $domain NS" dig $domain NS $digopts | $rrfilt | awk '$2=="NS" {print $3}' > $TMPNS if test ! -s $TMPNS; then echo "ERROR: no name servers found for domain $domain" echo " That domain is probably not a zone. Remove the leftmost portion of the name and try again." echo ";; ============================================================" echo ";; dlint of $domain run ending with errors." echo ";; run ending: `date`" rm -f $TMPNS $TMPZONE $TMPPTR $TMPA $TMPSUBDOMS $TMPERR $TMPERR2 exit 2 fi # # TEST 1 # Check all zone nameservers' SOA RRs for serial number similarity. # If they have < 2 nameservers, complain. # responding= if test `wc -l < $TMPNS` -eq 1; then echo "WARNING: only 1 nameserver found for zone $domain" echo " Every zone should have 2 or more nameservers at all times." test $exitcode -lt 1 && exitcode=1 else echo ";; Checking serial numbers per nameserver" rm -f $TMPSERIALS for ns in `cat $TMPNS`; do # Sanity check nameserver's name if test x`domaintype $ns` = x"inverse"; then echo "WARNING: nameserver $ns has in-addr.arpa. in its name which is bad; skipping." echo " I'll bet you left off its full domain name on the NS record, as in:" echo " $domain IN NS someserver" echo " You should append the fully qualified domain name with ending period, as in:" echo " $domain IN NS someserver.your.domain.com." test $exitcode -lt 1 && exitcode=1 continue fi # Ask this nameserver for domain's SOA record #echo "XX @$ns $domain SOA" serial=`dig @$ns $domain SOA $digopts 2> $TMPERR | $rrfilt | \ awk '$2=="SOA" {print $5; exit}'` # Ignore run-time errors that aren't real errors: # (BIND 9.0.1 default build in RedHat Linux 7.0) grep -v setsockopt $TMPERR > $TMPERR2 mv $TMPERR2 $TMPERR # Eliminate nameservers that couldn't return an SOA for zone $ns if test ! -s $TMPERR; then echo ";; $serial $ns"; echo "$serial $ns" >> $TMPSERIALS else echo "WARNING: nameserver $ns returned an error when asked for SOA of $domain; skipping." test $exitcode -lt 1 && exitcode=1 responding=" responding" fi done if test ! -s $TMPSERIALS; then echo "ERROR: no good name servers found for domain $domain" echo " Aborting run." echo ";; ============================================================" echo ";; dlint of $domain run ending with errors." echo ";; run ending: `date`" rm -f $TMPNS $TMPZONE $TMPPTR $TMPA $TMPSUBDOMS $TMPERR $TMPERR2 $TMPSERIALS exit 2 fi if test `awk '{print $1}' < $TMPSERIALS | sort -u | wc -l` -gt 1; then echo "WARNING: nameservers don't seem to agree on the zone's serial number." echo " Dlint will query nameserver with largest serial number first." test $exitcode -lt 1 && exitcode=1 else echo ";; All$responding nameservers agree on the serial number." fi # Re-order nameservers from highest SOA serial number to lowest. # This also removes bogus nameservers from $TMPNS. sort +0nr $TMPSERIALS | awk '{print $2}' > $TMPNS rm -f $TMPSERIALS fi # # SETUP FOR TESTS 2 AND 3 # Transfer this whole zone to a temporary file # echo ";; Now caching whole zone (this could take a minute)" i=1 badns=true while test $i -le `wc -l < $TMPNS`; do badns=false ns=`tail +$i $TMPNS | head -1` echo ";; trying nameserver $ns" #echo "XX @$ns $domain AXFR" dig @$ns $domain AXFR $digopts 2> $TMPERR | $rrfilt > $TMPZONE # Ignore run-time errors that aren't real errors: # (BIND 9.0.1 default build in RedHat Linux 7.0) grep -v setsockopt $TMPERR > $TMPERR2 mv $TMPERR2 $TMPERR if test `wc -l < $TMPERR` -eq 0; then break fi echo "WARNING: nameserver $ns is not responding properly to queries; skipping." badns=true test $exitcode -lt 1 && exitcode=1 i=`expr $i + 1` done if $badns; then echo "ERROR: could not find any working nameservers for $domain" echo ";; ============================================================" echo ";; dlint of $domain run ending with errors." echo ";; run ending: `date`" rm -f $TMPNS $TMPZONE $TMPPTR $TMPA $TMPSUBDOMS $TMPERR $TMPERR2 test $exitcode -lt 2 && exitcode=2 exit $exitcode fi # # TEST 2 # Look for all zone records with "#" as first character (illegal) -- # they probably thought they were commenting out a line! # grep '^#' $TMPZONE > $TMPA if test $? -eq 0; then echo "ERROR: some zone records begin with '#' character which is illegal." test $exitcode -lt 2 && exitcode=2 len=`wc -l < $TMPA` if test $len -lt 5; then echo " Use ';' for comment symbol, not '#'! Offending records:" sed -e 's/^/ /' < $TMPA else echo " Use ';' for comment symbol, not '#'! First 5 offending records:" head -5 $TMPA | sed -e 's/^/ /' fi fi # # TEST 3a (for in-addr.arpa domains) # All PTR records' hosts must have an A record with the same address, # unless that PTR rec is a network name instead of a host [RFC1101] # (see later tests). But we don't know if it's really a network or # just a host with a missing A record, so we report it. # Any PTR record for 1.0.0.127.in-addr.arpa should point to a host named # "localhost" (in any domain), and vice-versa. # # BUG: We assume all X.X.X.X.in-addr.arpa format names are those of hosts, # and all others (less than 4 X's) are networks. But if you happen to # be doing subnetting such that the number of host bits < 8, then your # subnets will have 4 octets too, which we don't handle properly. # Before CIDR, this couldn't be done right without strict RFC1101 # adherance, which nobody really cared about except myself. # With CIDR it may be possible, I need to sit down and think about it. # if $inaddrdomain; then awk '!/^;/ && $2=="PTR"' < $TMPZONE | sort -u > $TMPPTR i=0 len=`wc -l < $TMPPTR` if test $len -gt 0; then echo ";;" $len "PTR records found." else echo "ERROR: no PTR records found." test $exitcode -lt 2 && exitcode=2 fi while test $i -lt $len; do i=`expr $i + 1` set `tail +$i $TMPPTR | head -1` inaddr=$1 host=$3 # if not 4 numeric octets, assume it's a network address. num=`echo $inaddr | tr . '\012' | awk '{r++} /^in-addr$/ {print r - 1}'` if test 0"$num" -ne 4; then continue fi # this may hold more than one address if host is multihomed or a gateway: #echo "XX $host A" addr=`dig $host A $digopts | $rrfilt | awk '$2=="A" {print $3}'` if test x"$addr" = x""; then case $inaddr in '#'*) echo "ERROR: illegal domain name $inaddr has a PTR record." echo " Use ';' for zone file comments, not '#'!" test $exitcode -lt 2 && exitcode=2 ;; *) echo "WARNING: \"$inaddr PTR $host\", but $host has no A record." echo " But that's OK only if it's a network or other special name instead of a host." test $exitcode -lt 1 && exitcode=1 ;; esac continue fi ina=`echo $inaddr | awk -F. '{print $4 "." $3 "." $2 "." $1}'` a=`echo "$addr" | awk "/^$ina\$/ {print}"` if test x"$a" != x""; then # echo ";; $inaddr and $addr match." : else echo "ERROR: \"$inaddr PTR $host\", but the address of $host is really $addr" test $exitcode -lt 2 && exitcode=2 fi # If record is 1.0.0.127.in-addr.arpa., make sure hostname is localhost.* if test x"$ina" = x"127.0.0.1"; then hostname=`echo $host | awk -F. '{print $1}' | $tolower` if test x"$hostname" != x"localhost"; then echo "WARNING: \"$inaddr PTR $host\", but it should point to localhost instead." echo " This could confuse some computers (particularly Unix) in that domain." test $exitcode -lt 1 && exitcode=1 fi fi # If record has host named localhost.*, make sure PTR rec is 1.0.0.127.in-addr.arpa. hostname=`echo $host | awk -F. '{print $1}' | $tolower` if test x"$hostname" = x"localhost"; then if test x"$ina" != x"127.0.0.1"; then echo "WARNING: \"$inaddr PTR $host\", but only 1.0.0.127.in-addr.arpa. should point to localhost." echo " This could confuse some computers (particularly Unix) in that domain." test $exitcode -lt 1 && exitcode=1 fi fi done # # TEST 3b (for regular domains) # All hosts with A records must have reverse in-addr.arpa PTR records # and they should point back to the same host name. # Any host named "localhost" in any domain should have IP address 127.0.0.1, # and vice-versa. # # BUG: Sometimes there will be a special host in a domain that has an A record # pointing to some host which has a different name in _another_ zone. # Example: info.nau.edu is really pumpkin.ucc.nau.edu in disguise. # This is currently reported as an error, there's no way to tell it is # intentional. (not sure how to deal with this) # else awk '!/^;/ && $2=="A"' < $TMPZONE | sort -u > $TMPA i=0 len=`wc -l < $TMPA` if test $len -gt 0; then echo ";;" $len "A records found." else echo "ERROR: no A records found." test $exitcode -lt 2 && exitcode=2 fi while test $i -lt $len; do i=`expr $i + 1` set `tail +$i $TMPA | head -1` host=$1 addr=$3 inaddr=`echo $addr | awk -F. '{print $4 "." $3 "." $2 "." $1 ".in-addr.arpa."}'` #echo "XX $inaddr PTR" inhost=`dig $inaddr PTR $digopts | $rrfilt | awk '$2=="PTR" {print $3}'` if test x"$inhost" = x""; then case $host in '#'*) echo "ERROR: illegal domain name $host has an A record." echo " Use ';' for zone file comments, not '#'!" ;; *) echo "ERROR: $host has an A record of $addr, but no reverse PTR record for $inaddr can be found on nameserver $ns" echo " The following resource record should be added:" echo " $inaddr IN PTR $host" ;; esac test $exitcode -lt 2 && exitcode=2 continue fi numptrs=`echo "$inhost" | wc -l` # numptrs ends up with lots of spaces in it, so don't put it inside quotes... if test $numptrs -gt 1; then echo "ERROR: $inaddr has" $numptrs "PTR records, but there should be only 1." test $exitcode -lt 2 && exitcode=2 fi lhost=`echo $host | $tolower` multipleinhosts="$inhost" foundit=0 for inhost in $multipleinhosts; do linhost=`echo $inhost | $tolower` if test x"$linhost" = x"$lhost"; then foundit=1 fi done if test x"$addr" != x"127.0.0.1"; then if test $foundit -eq 0; then #echo "XX @$ns $host SOA" soa=`dig @$ns $host SOA $digopts | $rrfilt | awk '$2=="SOA" {print "ok";exit}'` if test x"$soa" = x"ok"; then echo "WARNING: the zone $host has an A record but no reverse PTR record. This is probably OK." test $exitcode -lt 1 && exitcode=1 else if test $numptrs -eq 1; then echo "ERROR: \"$host A $addr\", but the PTR record for $inaddr is \"$inhost\"" else # NOTE: don't remove 2nd "echo", it's necessary: echo "ERROR: \"$host A $addr\", but the PTR records for $inaddr are \"`echo $multipleinhosts`\"" fi test $exitcode -lt 2 && exitcode=2 echo " One of the above two records are wrong unless the host is a name server or mail server." echo " To have 2 names for 1 address on any other hosts, replace the A record" if test $numptrs -eq 1; then echo " with a CNAME record:" else echo " with a CNAME record referring to the proper host, for example:" fi echo " $host IN CNAME $inhost" continue fi fi else # IP address is 127.0.0.1 -- make sure hostname is localhost.* hostname=`echo $lhost | awk -F. '{print $1}'` if test x"$hostname" != x"localhost"; then echo "WARNING: \"$host A $addr\", but only localhost should be 127.0.0.1." echo " This could confuse some computers (particularly Unix) in that domain." test $exitcode -lt 1 && exitcode=1 fi fi # if hostname is localhost.*, make sure IP address is 127.0.0.1 hostname=`echo $lhost | awk -F. '{print $1}'` if test x"$hostname" = x"localhost" -a x"$addr" != x"127.0.0.1"; then echo "WARNING: \"$host A $addr\", but localhost should always be 127.0.0.1." echo " This could confuse some computers (particularly Unix) in that domain." test $exitcode -lt 1 && exitcode=1 fi done fi ############################## # OTHER TESTS GO HERE ############################## # # Recursively traverse all sub-domains beneath this domain # if $recurse; then #echo "XX @$ns $domain AXFR" dig @$ns $domain AXFR $digopts | $rrfilt | awk '$2=="NS" {print $1}' | grep -iv "^$domain\$" | sort -u > $TMPSUBDOMS if test -s $TMPSUBDOMS; then i=1 len=`wc -l < $TMPSUBDOMS` while test $i -le $len; do line=`sed -e "$i!d" < $TMPSUBDOMS` # run ourself to analyze the subdomain $0 -silent $line status=$? case $status in 3) exitcode=$status break ;; 4) exitcode=$status break ;; *) if test $status -gt $exitcode; then exitcode=$status fi ;; esac i=`expr $i + 1` done else echo ";; no subzones found below $domain, so no recursion will take place." fi fi # # Quit with proper error code # echo ";; ============================================================" echo $echon ";; dlint of $domain run ending $echoc" case $exitcode in 0) echo "normally." ;; 1) echo "with warnings." ;; 2) echo "with errors." ;; 3) echo "due to usage error." ;; 4) echo "due to signal interruption." ;; esac echo ";; run ending: `date`" rm -f $TMPNS $TMPZONE $TMPPTR $TMPA $TMPSUBDOMS $TMPERR $TMPERR2 exit $exitcode