#!/bin/sh
#
#	program:	dlint
#	usage:		dlint [-n] domain
#	purpose:	To scan through a 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)
#	author:		Paul Balyoz <pbalyoz@netzone.com>
#			Copyright (c) 1993-1997 by Paul A. Balyoz

# be sure this path includes the directory that holds your dig executable:
PATH=/usr/ucb:/bin:/usr/bin:/usr/local/bin:/usr/share/bin:/usr/com/bin
export PATH

# Please don't change this yourself:
VERSION=1.1.2

TMPNS=/tmp/dlintns.$$
TMPZONE=/tmp/dlintzone.$$
TMPPTR=/tmp/dlintptr.$$
TMPA=/tmp/dlinta.$$
TMPSUBDOMS=/tmp/dlintsubdoms.$$
TMPERR=/tmp/dlinterr.$$

usage() {
	echo 'usage: dlint [-n] domain' 2>&1
	echo '       (domain can be in-addr format, too)' 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 (code stolen from DOC script)
#
ans=`dig | awk '/DiG/ && / 2./ {print "ok"; exit}'`
if test x"$ans" != x"ok"; then
	echo ';; This program requires DiG version 2.x, which I cannot find.'
	exit 3
fi

#
# Options (change these if you need to)
#
digopts='+ret=2 +pfset=0x2024'

#
# 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

#
# 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 (dig)"
	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 ";; run date: `date`"
fi

#
# Transfer this whole zone to a temporary file
#
echo ";; ============================================================"
echo ";; Now linting $domain"
dig NS $domain $digopts | awk '$3=="NS" {print $4}' > $TMPNS
if test ! -s $TMPNS; then
	echo "ERROR: no name servers found for domain $domain"
	echo ";; ============================================================"
	echo ";; dlint of $domain run ending with errors."
	rm -f $TMPNS $TMPZONE $TMPPTR $TMPA $TMPSUBDOMS $TMPERR
	exit 2
fi
i=1
badns=true
while test $i -le `wc -l < $TMPNS`; do
	badns=false
	ns=`tail +$i $TMPNS | head -1`
	echo ";; trying nameserver $ns"
	dig @$ns AXFR $domain $digopts > $TMPZONE 2> $TMPERR
	if test `wc -l < $TMPERR` -eq 0; then
		break
	fi
	echo "WARNING: nameserver $ns is not responding properly to queries."
	badns=true
	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."
	rm -f $TMPNS $TMPZONE $TMPPTR $TMPA $TMPSUBDOMS $TMPERR
	exitcode=2
	exit $exitcode
fi


###############################
# Lint this domain thoroughly #
###############################

#
# TEST 1 (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.
#
# 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.
#      This can't be done right without strict RFC1101 conformance.
#
if $inaddrdomain; then
	awk '!/^;/ && $3=="PTR"' < $TMPZONE | sort -u > $TMPPTR
	i=0
	while test $i -lt `wc -l < $TMPPTR`; do
		i=`expr $i + 1`
		set `tail +$i $TMPPTR | head -1`
		inaddr=$1 host=$4
		# 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 a gateway:
		addr=`dig @$ns A $host $digopts | awk '$3=="A" {print $4}'`
		if test x"$addr" = x""; then
			echo "WARNING: $host has no A record, but that's OK only if it's a network or other special name instead of a host."
			test $exitcode -lt 1 && exitcode=1
			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
	done

#
# TEST 1 (for regular domains)
# All hosts with A records must have reverse in-addr.arpa records
# and they should point back to the same host name.
#
# 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.  (Future:  a local exception list?)
# BUG: Localhost (127.0.0.1) is a mess, how should it really be done?  If you
#      define localhost.<domain> for every domain, you are screwed when you
#      look up 1.0.0.127.in-addr.arpa because it can only point to one of them.
#      But will all software on all computers really query "localhost." (root
#      domain), or will some of them do the usual "localhost" (current domain)?
#      If the latter, then we probably should just flag down "localhost" and
#      "127.0.0.1" as an exception to the rule, and ignore it.
#
else
	awk '!/^;/ && $3=="A"' < $TMPZONE | sort -u > $TMPA
	i=0
	while test $i -lt `wc -l < $TMPA`; do
		i=`expr $i + 1`
		set `tail +$i $TMPA | head -1`
		host=$1 addr=$4
		inaddr=`echo $addr | awk -F. '{print $4 "." $3 "." $2 "." $1 ".in-addr.arpa."}'`
		inhost=`dig @$ns PTR $inaddr $digopts | awk '$3=="PTR" {print $4}'`
		if test x"$inhost" = x""; then
			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 record should be added to DNS:"
			echo "	$inaddr	IN	PTR	$host"
			test $exitcode -lt 2 && exitcode=2
			continue
		fi
		# numptrs ends up with lots of spaces in it, don't print it inside quotes.
		numptrs=`echo "$inhost" | wc -l`
		if test $numptrs -gt 1; then
			echo "ERROR: $inaddr has" $numptrs "PTR records, but there should be only 1."
		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 $foundit -eq 0; then
			soa=`dig @$ns SOA $host $digopts | awk '$3=="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 x"$addr" = x"127.0.0.1"; then
					message="WARNING"
					test $exitcode -lt 1 && exitcode=1
				else
					message="ERROR"
					test $exitcode -lt 2 && exitcode=2
				fi
				if test $numptrs -eq 1; then
					echo "$message: \"$host A $addr\", but the PTR record for $inaddr is \"$inhost\""
				else
					# NOTE: don't remove 2nd "echo", it's necessary:
					echo "$message: \"$host A $addr\", but the PTR records for $inaddr are \"`echo $multipleinhosts`\""
				fi
				if test x"$addr" = x"127.0.0.1"; then
					echo "	One of the above two records might be wrong."
				else
					echo "	One of the above two records must be wrong; to have 2 names for 1 address, replace the A record with"
					if test $numptrs -eq 1; then
						echo "	a CNAME record:"
					else
						echo "	a CNAME record referring to the proper host, for example:"
					fi
					echo "	$host	IN	CNAME	$inhost"
				fi
				continue
			fi
		else
#			if test $numptrs -eq 1; then
#				echo ";; $host and $inhost match."
#			else
#				echo ";; $host matches one of $multipleinhosts"
#			fi
			:
		fi
	done
fi


#
# Recursively traverse all sub-domains beneath this domain
#

if $recurse; then
	dig @$ns $domain AXFR $digopts | awk '$3=="NS" {print $1}' | grep -v "^$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=$?
			if test $status -gt $exitcode; then
				exitcode=$status
			fi
			i=`expr $i + 1`
		done
	else
		echo ";; no subdomains 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." ;;
esac
rm -f $TMPNS $TMPZONE $TMPPTR $TMPA $TMPSUBDOMS $TMPERR
exit $exitcode
