#!/bin/bash
# Network testing script v 1.9
# (c) 2005-2007 Javier Fernandez-Sanguino
#
# This script will test your system's network configuration using basic
# tests and providing both information (INFO messages), warnings (WARN)
# and possible errors (ERR messages) by checking:
# - Interface status
# - Availability of configured routers, including the default route
# - Proper host resolution, including DNS checks
# - Proper network connectivity, including ICMP and web connections to 
#   a remote web server (the web server used for the tests can be configured, 
#   see below)
#
# Some of the network tests are described in more detail at
# http://ubuntuforums.org/archive/index.php/t-25557.html
# 
# The script does not need special privileges to run as it does not 
# do any system change. It also will not fix the errors by itself.
# 
# Additional software requirements:
#    * ip from the iproute package. (could probably be rewrittent to
#    use ifconfig only or to parse /proc)
#    * ping from the iputils-ping package or the netkit-ping package.
#    * nc from the netcat package.
#
#   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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#  
# You can also find a copy of the GNU General Public License at
# http://www.gnu.org/licenses/licenses.html#TOCLGPL
#
# TODO
# - Works only on Linux, can this be generalised for other UNIX systems
#   (probably not unless rewritten in C)
# - Does not check for errors properly, use -e and test intensively
#   so that expected errors are trapped
#   (specially for tools that are not available, like netcat)
# - If the tools are localised to languages != english the script might 
#   break 
# - Ask 'host' maintainer to implement error codes as done with 
#   dlint
# - Should be able to check if DNS server is in the same network, if 
#   it doesn't answer to pings, check ARP in that case.
# - DHCP checks?
# - Other internal services tests? (LDAP if using pam...)
# - Generate summary of errors in the end (pretty report?)
# - Check if packets are being dropped by local firewall? (use dmesg
#   and look for our tests)
# - Support wireless interfaces? (use iwconfig)
# - Check for more than one web server (have CHECK_HOSTS be a number
#   of hosts and determine a metric to spout an error) ?
# - Use traceroute or tcptraceroute to see if there is network connectivity?
#   (traceroute is usually blocked by firewalls but tcptraceroute might
#   be an alternative to using nc)
# - Use mii-tool (requires root privileges)
# - Use ping -s XXXX to detect invalid MTUs
# - Use arpping to detect another host with our same IP address
# - Check other TODOs inline in the code

# Default values
VERB=3
LOG=0
while getopts ":hsv:" Option
do
    case $Option in
    h )	cat <<- EOF
	Usage: $0 [-s][-v <num>]

	 -s     Also log messages to local3 syslog facility
	 -v 0   Silent run
	 -v 1   Show only error messages
	 -v 2   Show error and warning messages
	 -v 3   Fully verbose (default)

EOF
	 exit 0;;
    v )	VERB=$OPTARG;;
    s )	LOG=1;;
    esac
done

# BEGIN configuration
# Configure to your needs, these values will be used when
# checking DNS and Internet connectivity
# DNS name to resolve.
# These are default values which can be overriden by the environment.
[ -z "$CHECK_HOST" ] && CHECK_HOST=www.debian.org
[ -z "$CHECK_IP_ADRESS" ] && CHECK_IP_ADRESS=194.109.137.218
# Web server to check for
[ -z "$CHECK_WEB_HOST" ] && CHECK_WEB_HOST=www.debian.org
[ -z "$CHECK_WEB_PORT" ] && CHECK_WEB_PORT=80
# END configuration
export CHECK_HOST CHECK_IP_ADRESS CHECK_WEB_HOST CHECK_WEB_PORT

PATH=/bin:/sbin:/usr/bin:/usr/sbin
LC_ALL=C
export PATH LC_ALL

# error reporting and logging functions
info () {
    [ "$VERB" -gt 2 ] && echo "INFO: $1"
    [ "$VERB" -gt 2 ] && [ "$LOG" -eq 1 ] && logger -p local3.info "$0 INFO: $1"
}

warn () {
    [ "$VERB" -gt 1 ] && echo "WARN: $1"
    [ "$VERB" -gt 1 ] && [ "$LOG" -eq 1 ] && logger -p local3.warn "$0 WARN: $1"
}

err () {
    [ "$VERB" -gt 0 ] && echo "ERR: $1" >&2
    [ "$VERB" -gt 0 ] && [ "$LOG" -eq 1 ] && logger -p local3.err "$0 ERR: $1"
}


# Check if all commands we need are available
# NOTE:  if using nslookup add "nslookup dnsutils"
( echo -e "netstat net-tools\nifconfig net-tools\n\
ping netkit-ping|inetutils-ping|iputils-ping\n\
arp net-tools\nip iproute\nhost host|bind9-host\nmktemp debianutils\n\
nc netcat" | 
while read cmd package; do
if ! `which $cmd 2>/dev/null >&2`; then
	err "$cmd is not available! (please install $package)" 
	exit 1
fi
done ) || exit 1
# Recommended programs
( echo -e "ethtool ethtool" |
while read cmd package; do
if ! `which $cmd 2>/dev/null >&2`; then
	warn "$cmd is not available (consider installing $package)" 
	exit 1
fi
done )

# Default route for programs
ETHTOOL=/usr/sbin/ethtool
MIITOOL=/sbin/mii-tool

# Other needs
# We need /proc/net
if [ ! -d /proc/net ] ; then
	err "/proc is not available! Please mount it ('mount -t /proc')"
	exit 1
fi


# Extract the interface of our default route

defaultif=`netstat -nr |grep ^0.0.0.0 | awk '{print $8}' | head -1`
defaultroutes=`netstat -nr |grep ^0.0.0.0 | wc -l`
if [ -z "$defaultif" ] ; then
	defaultif=none
	warn "This system does not have a default route"
elif [ "$defaultroutes" -gt 1 ] ; then
	warn "This system has more than one default route"
else 
	info "This system has exactly one default route"
fi



# Check loopback
check_local () {
# Is there a loopback interface? 
	if [ -n "`ip link show lo`" ] ; then
# OK, can we ping localhost
		if  ! check_host localhost 1; then
# Check 127.0.0.1  instead (not everybody uses this IP address however,
# although its the one commonly used)
			if  ! check_host 127.0.0.1 1; then
				err "Cannot ping localhost (127.0.0.1), loopback is broken in this system"
			else
				err "Localhost is not answering but 127.0.0.1, check /etc/hosts and verify localhost points to 127.0.0.1"
			fi
		else
		 info "Loopback interface is working properly"
		fi
			
	else
		err "There is no loopback interface in this system"
		status=1
	fi
	status=0
	return $status
}

check_if_link_miitool () {
	ifname=$1
	[ ! -x "$MIITOOL" ] && return 0
	status=0
	if $MIITOOL $ifname 2>&1| grep -q "no link"; then
		status=1
	fi
	return $status
}

check_if_link_ethtool () {
# Check if the interface has link
# Note: Unlike other sections of the script we need to be root
# to test this
	ifname=$1
	[ ! -x "$ETHTOOL" ] && return 0
	status=0
	LINK=`$ETHTOOL $ifname 2>&1| grep "Link detected"`
	# If ethtool fails to print out the link line we break off
	# notice that ethtool cannot get the link status out of all
	# possible network interfaces
	[ -z "$LINK" ] && return
	if ! echo $LINK | grep -q "Link detected: yes" ; then
		status=1
	fi
	return $status
}

check_if_link_iplink () {
	ifname=$1
	status=0
	[ ! -x /sbin/ip ] && return 0
	if /sbin/ip link show $ifname 2>&1 | grep -q "NO-CARRIER"; then
		status=1
	fi
	return $status
}



check_if_link() {
	status=-1
	iface=$1
	# Use ethtool if installed (preferable to mii-tool)
	# If none are installed we will test using 'ip link show'
	if [ "`id -u`" -eq 0 ] ; then
		if [ -x "$ETHTOOL" ] ; then
			check_if_link_ethtool $iface
			status=$?
		elif [ -x "$MIITOOL" ]; then
			check_if_link_miitool $iface
			status=$?
		fi
	fi
	# If no test has done use ip link
	if [ $status -eq -1 ]; then
		check_if_link_iplink $iface
		status=$?
	fi
	return $status
}

# Check network interfaces
check_if () {
	ifname=$1
	status=0
	[ -z "$ifname" ] && return 1
# Check if the interface has a link
	case "$ifname" in
	        eth*) check_if_link $ifname ; status=$?;;
	        *) ;;
	esac
# Print results
	if [ $status -ne 0 ] ; then
		if  [ "$ifname" = "$defaultif" ] ; then
			err "The $ifname interface that is associated with your default route has no link!"
		else 
	                warn "Interface $ifname does not have link"
		fi
	fi
# Find IP addresses for $ifname
	inetaddr=`ip addr show $ifname | grep "inet " | awk '{print $2}' | sed -e 's/\/.*//'`
	if [ -z "$inetaddr" ] ; then
		warn "The $ifname interface does not have an IP address assigned"
		status=1
	else
# TODO: WARN if more than 2 IP addresses?
		echo $inetaddr | while read ipaddr; do
			info "The $ifname interface has IP address $ipaddr assigned"
		done
	fi

# Lookup TX and RX statistics
# TODO: This is done using ifconfig but could use /proc/net/dev for
# more readibility or, better, 'netstat -i'
	txpkts=`ifconfig $ifname | awk '/RX packets/ { print $2 }' |sed 's/.*://'`
	rxpkts=`ifconfig $ifname | awk '/RX packets/ { print $2 }' |sed 's/.*://'`
	txerrors=`ifconfig $ifname | awk '/TX packets/ { print $3 }' |sed 's/.*://'`
	rxerrors=`ifconfig $ifname | awk '/RX packets/ { print $3 }' |sed 's/.*://'`
# TODO: Check also frames and collisions, to detect faulty cables
# or network devices (cheap hubs)
	if [ "$txpkts" -eq 0 ] && [ "$rxpkts" -eq 0 ] ; then
		err "The $ifname interface has not tx or rx any packets. Link down?"
		status=1
	elif  [ "$txpkts" -eq 0 ]; then
		warn "The $ifname interface has not transmitted any packets."
	elif [ "$rxpkts" -eq 0 ] ; then
		warn "The $ifname interface has not received any packets."
	else
		info "The $ifname interface has tx and rx packets."
	fi
# TODO: It should be best if there was a comparison with tx/rx packets.
# a few errors are not uncommon if the card has been running for a long
# time. It would be better if a relative comparison was done (i.e.
# less than 1% ok, more than 20% warning, over 80% major issue, etc.)
	if [ "$txerrors" -ne 0 ]; then
		warn "The $ifname interface has tx errors."
	fi
	if [ "$rxerrors" -ne 0 ]; then
                warn "The $ifname interface has rx errors."
	fi
	return $status
}

check_netif () {
	status=0
	ip link show | egrep '^[[:digit:]]' |
	while read ifnumber ifname status extra; do
		ifname=`echo $ifname |sed -e 's/:$//'`
                # TODO: this is redundant with the check if_link test
                # (although faster since using it would make us call 'ip'
                # twice.
		if [ -n "`echo $status | grep NO-CARRIER`" ] ; then
			if  [ "$ifname" = "$defaultif" ] ; then
				err "The $ifname interface that is associated with your default route is down!"
				status=1
			elif  [ "$ifname" = "lo"  ] ; then
				err "Your lo interface is down, this might cause issues with local applications (but not necessarily with network connectivity)"
			else
				warn "The $ifname interface is down"
			fi
		else
		# Check network routes associated with this interface
			info "The $ifname interface is up"
			check_if $ifname
			check_netroute $ifname
		fi
	done
	return $status
}

check_netroute () {
	ifname=$1
	[ -z "$ifname" ] && return 1
	netstat -nr  | grep "${ifname}$" |
	while read network gw netmask flags mss window irtt iface; do
	# For each gw that is not the default one, ping it
		if [ "$gw" != "0.0.0.0" ] ; then
			if ! check_router $gw  ; then
				err "The default route is not available since the default router is unreachable"
			fi
		fi
	done
}

check_router () {
# Checks if a router is up
	router=$1
	[ -z "$router" ] && return 1
	status=0
# First ping the router, if it does not answer then check arp tables and
# see if we have an arp. We use 5 packets since it is in our local network.
	ping -n -q -c 5 "$router" >/dev/null 2>&1 
	if [ "$?" -ne 0 ]; then
		warn "Router $router does not answer to ICMP pings"
# Router does not answer, check arp
		routerarp=`arp -n | grep "^$router" | grep -v incomplete`
		if [ -z "$routerarp" ] ; then
			err "We cannot retrieve a MAC address for router $router"
			status=1
		fi
	fi
	if [ "$status" -eq 0 ] ; then
		info "The router $router is reachable"
	fi
	return $status
}

check_host () {
# Check if a host is reachable
# TODO: 
# - if the host is in our local network (no route needs to be used) then
#   check ARP availability
# - if the host is not on our local network then check if we have a route
#   for it
	host=$1
	[ -z "$host" ] && return 1
# Use 10 packets as we expect this to be outside of our network
	COUNT=10
	[ -n "$2" ] && COUNT=$2
	status=0
	ping -n -q -c $COUNT "$host" >/dev/null 2>&1 
	if [ "$?" -ne 0 ]; then
		warn "Host $host does not answer to ICMP pings"
		status=1
	else
		info "Host $host answers to ICMP pings"
	fi
	return $status
}

check_dns () {
# Check the nameservers defined in /etc/resolv.conf
	status=1
	nsfound=0
	nsok=0
	tempfile=`mktemp tmptestnet.XXXXXX` || {  err "Cannot create temporary file! Aborting! " ; exit 1; }
	trap " [ -f \"$tempfile\" ] && /bin/rm -f -- \"$tempfile\"" 0 1 2 3 13 15
	cat /etc/resolv.conf | grep -v ^# | grep nameserver | 
	awk '/nameserver/ { for (i=2;i<=NF;i++) {  print $i ; } }'  >$tempfile
	for nameserver in `cat $tempfile`;  do
		nsfound=$(( $nsfound + 1 ))
		info "This system is configured to use nameserver $nameserver"
		check_host $nameserver 5
		if check_ns $nameserver ; then
			nsok=$(( $nsok +1 ))
		else
			status=$?
		fi
	done
	#Could also do:
	#nsfound=`wc -l $tempfile | awk '{print $1}'`
	/bin/rm -f -- "$tempfile"
	trap  0 1 2 3 13 15
	if [ "$nsfound" -eq 0 ] ; then
		err "The system does not have any nameserver configured"	
	else
		if [ "$status" -ne 0 ] ; then
			if [ "$nsfound" -eq 1 ] ; then
				err "There is one nameserver configured for this system but it does not work properly"
			else
				err "There are $nsfound nameservers configured for this system and none of them works properly"
			fi
		else
			if [ "$nsfound" -eq 1 ] ; then
				info "The nameserver configured for this system works properly"
			else
				info "There are $nsfound nameservers is configured for this system and $nsok are working properly"
			fi
		fi
	fi
	return $status
}

check_ns () {
# Check the nameserver using host
# TODO: use nslookup?
#	nslookup $CHECK_HOST -$nameserver 
	nameserver=$1
	[ -z "$nameserver" ] && return 1
	status=1
	CHECK_RESULT="$CHECK_HOST .* $CHECK_IP_ADDRESS"
# Using dnscheck:
	dnscheck=`host -t A $CHECK_HOST $nameserver 2>&1 | tail -1`
	if [ -n "`echo $dnscheck |grep NXDOMAIN`" ] ; then
		err "Dns server $nameserver does not resolv properly"
	elif [ -n "`echo $dnscheck | grep \"timed out\"`" ] ; then
		err "Dns server $nameserver is not available"
	elif [ -z "`echo $dnscheck | egrep \"$CHECK_RESULT\"`" ] ; then
		warn "Dns server $nameserver did not return the expected result for $CHECK_HOST"
	else
		info "Dns server $nameserver resolved correctly $CHECK_HOST"
		status=0
	fi

# Using dlint
#	dlint $CHECK_HOST @$nameserver >/dev/null 2>&1
#	if [ $? -eq 2 ] ; then
#		err "Dns server $nameserver does not resolv properly"
#	elif [ $? -ne 0 ]; then
#		err "Unexpected error when testing $nameserver"
#	else
#		info "Dns server $nameserver resolved correctly $CHECK_HOST"
#		status=0
#	fi

	return $status
}

check_conn () {
# Checks network connectivity
	if ! check_host $CHECK_WEB_HOST >/dev/null ; then
		warn "System does not seem to reach Internet host $CHECK_WEB_HOST through ICMP"
	else
		info "System can reach Internet host $CHECK_WEB_HOST"
	fi
	status=0
# Check web access, using nc
# TODO: 
# - this could also implement proxy checks (if the http_proxy environment is
#   defined?)
# - could also check against a valid content copy (otherwise it might be
#   fooled by transparent proxies)
	echo -e "HEAD / HTTP/1.0\n\n" |nc -w 20 $CHECK_WEB_HOST $CHECK_WEB_PORT >/dev/null 2>&1
	if [ $? -ne 0 ] ; then
		err "Cannot access web server at Internet host $CHECK_WEB_HOST"
		status=1
	else
		info "System can access web server at Internet host $CHECK_WEB_HOST"
	fi
	return $status
}

# TODO: checks could be conditioned, i.e. if there is no proper
# interface setup don't bother with DNS and don't do some Inet checks
# if DNS is not setup properly
check_local || exit 1
check_netif || exit 1
check_dns   || exit 1
check_conn  || exit 1

exit 0


# Set our locale environment, just in case ethtool gets translated
LC_ALL=C
export LC_ALL

