#!/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 <pab@domtools.com>
#
# 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.<domain> 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 <pab@domtools.com>"
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
syntax highlighted by Code2HTML, v. 0.9.1