#!/bin/bash # Check if this host's MAC address is correct in /etc/ethers. # On each subnet, the Linux server with the lexically lowest IP address # will also ping every other host on the subnet and check if its MAC # address is correct. Command line args: # -s If given, hosts in /etc/hosts but not /etc/ethers are # complained about; if omitted, they are ignored silently. # -d Print irrelevant stuff useful for debugging. # -a Ping all hosts even though you aren't the leader, for debugging. # Timing (on Intel Core Duo E8400 at 3.0 GHz), load average was zilch. # Task was to check its own MAC address, plus ping all on a subnet with # 222 assigned IP addresses (most but not all were up). # User 1.008 sec, system 1.540 sec, elapsed 175 sec, load average 0.014 # Not counting load on YP server. # $Header: /src/math/lib/daily/RCS/D60ethers,v 1.1 2008/11/06 21:01:20 jimc Exp $ # $Log: D60ethers,v $ # Revision 1.1 2008/11/06 21:01:20 jimc # Initial revision # echo "== Check Ethernet addresses" opt_s=0 opt_d='' while [ $# -gt 0 ] ; do case X$1 in X-s ) opt_s=1 ; ;; X-d ) opt_d=1 ; ;; X-a ) opt_a=1 ; ;; * ) echo "Unrecognized command line argument '$1' ignored" ; ;; esac shift done if [ -z "$scrfil" ] ; then scrfil=/tmp/system/D60ethers.tmp ; fi # Prints an error message (all command line args) preceeded by the global # variables host, ifc, mac, macsun, inet. function barf() { echo -e "'$host' $ifc '$mac'/'$macsun'\n '$inet': $*" } # Converts an IP address as a dotted quad to a 32-bit integer, possibly signed. function ip2int () { local ip=$1 local IFSold="$IFS" IFS=' .' local int=0 local octet for octet in $ip ; do int=$(((int<<8)+octet)) done IFS="$IFSold" echo $int } # $1 = an IP address, $2 = CIDR number of bits, $3 = filename, $4 = nonnull or # null string. The file's column 1 = IP addresses, 2 = IP as integer. # This subroutine emits on stdout the IP addresses that are on the same # subnet as $1. If $4 is nonnull, only the first is emitted (else all of # them). function insubnet () { local mask=$((~((1<<(32-$2))-1))) local subnet=`ip2int $1` subnet=$((subnet&mask)) local fname=$3 local once='' if [ -n "$4" ] ; then once=";exit" ; fi awk -v subnet=$subnet -v mask=$mask '$1 ~ /[0-9][0-9]*\./ {if (and($2, mask) == subnet) {print $1'"$once"'}}' $fname } # Canonicalizes a MAC address to Sun format: lower case, no leading 0's. # Input on stdin, output on stdout. function canosun () { sed -e 'y/ABCDEF/abcdef/' -e 's/0\([0-9a-f]\)/\1/g' } # Does the research to check a MAC address, and prints error messages on stdout. # Arguments: $1 = MAC address, $2 = IP address, $3 = 1 to complain if a host # is not in /etc/ethers, 0 to skip it. # Returns 0 if OK, 1 if error. function checkmac () { mac=$1 inet=$2 strict=$3 sleep 0.1 # Canonicalize MAC address to Sun format. macsun=`echo $mac | canosun` # Reverse map to the hostname host=`dig +short -x $inet | sed -e 's/\.$//'` if [ -z "$host" ] ; then barf "IP has no hostname" return 1 fi # What NIS thinks the MAC address is. For Sun machines this # will be lower case without leading 0's, but this is not # necessarily true for Linux, so normalize it. local ethersrev=`ypmatch $host ethers.byname 2> /dev/null | awk '{print $1}' | canosun` if [ -n "$ethersrev" ] ; then : elif [ $strict -eq 0 ] ; then return 0 else barf "host is not in /etc/ethers" return 1 fi if [ "$macsun" != "$ethersrev" ] ; then barf "discrepancy: /etc/ethers says $ethersrev" return 1 fi ethersfwd=`ypmatch $macsun ethers.byaddr 2> /dev/null` if [ -z "$ethersfwd" ] ; then barf "actual MAC is not in /etc/ethers" return 1 fi return 0 } # Everyone checks his own MAC address(es). # It's in /sys/class/net/*/address (lower case with leading 0's) # NOTE, restricted to eth* ; if we ever need wlan* or the like, this must # be changed. nifcs=0 for idir in /sys/class/net/eth* ; do ifc=${idir##*/} if [ ! -r $idir/address ] ; then continue ; fi # Capture normal and Sun-style MAC addresses mac=`cat $idir/address` # Bypass loopback address if [ "$mac" = "00:00:00:00:00:00" ] ; then continue ; fi # Get the interface's IP address and corresponding hostname. inetm=`ip addr show dev $ifc | sed -e '/inet /!d' -e 's/^.*inet //' -e 's/ .*$//'` inet=${inetm%/*} if [ -z "$inet" -o "$inet" = "0.0.0.0" ] ; then continue ; fi inets[$nifcs]=$inet; masks[$nifcs]=${inetm#*/} ifcs[$nifcs]=$ifc; checkmac $mac $inet 1 nifcs=$((nifcs+1)) done # For each of my interfaces, am I the (lexically) lowest numbered server on # that subnet? # $scrfil = (IP, IP as int, 1-component hostname) sorted by hostname, excluding # zero and 255 broadcast addresses. Erratic whitespace messes up the sort, # and "awk" normalizes it. IFSold="$IFS" IFS=".$IFS" ypcat hosts | while read ip1 ip2 ip3 ip4 host1 junk ; do case "$ip1" in [0-9]* ) : ; ;; #has numeric IP address * ) continue ; ;; #lose comments esac case "$ip4" in 0 | 255 | '' ) continue ; ;; #lose broadcast addresses esac echo "$ip1.$ip2.$ip3.$ip4" $(($ip4+256*($ip3+256*($ip2+256*$ip1)))) $host1 done | sort -k 3,3 -u -o "$scrfil" IFS="$IFSold" # $scrfil.boxes = IP addresses of all Linux servers, lexically sorted hostgroup linux@server+amanda-rogue | \ join -2 3 -o 2.1 2.2 - $scrfil | \ sort -o $scrfil.boxes # leaders = list of subnets for which this host is the lowest numbered # Linux server. if [ -n "$opt_d" ] ; then printf "%-16s %-16s %s\n" Subnet Leader "" ; fi k=0 while [ $k -lt $nifcs ] ; do ifc=${ifcs[$k]} inet=${inets[$k]} leader=`insubnet $inet ${masks[$k]} $scrfil.boxes first` isleader='' kldr=0 if [ "$leader" = "$inet" -o -n "$opt_a" ] ; then leaders="$leaders $inet" isleader=' (leader)' if [ "$leader" != "$inet" ] ; then isleader=' (debug)' ; fi kldr=1 fi isldrs[$k]=$kldr if [ -n "$opt_d" ] ; then printf "%-16s %-16s %s\n" $inet $leader $isleader ; fi k=$((k+1)) done # For all subnets (if any) for which I am the lowest numbered host, # ping all hosts on that subnet # and verify the MAC address. "Down" hosts, and the local host, are not # detected and are skipped silently. (Hostgroup server includes no hosts # on the backdoor net, so someone has to do the check.) k=0 while [ $k -lt $nifcs ] ; do if [ ${isldrs[$k]} -eq 0 ] ; then k=$((k+1)) ; continue ; fi ifc=${ifcs[$k]} insubnet ${inets[$k]} ${masks[$k]} $scrfil > $scrfil.subnet cat $scrfil.subnet | while read inet junk ; do ping -q -c 1 -w 2 $inet > /dev/null 2>&1 mac=`arp -n -a $inet 2> /dev/null | awk '$4 ~ /^[0-9a-fA-F:]*$/ && $4 !~ /incomplete/ {print $4}'` if [ -z "$mac" ] ; then continue ; fi checkmac $mac $inet $opt_s done k=$((k+1)) done if [ -z "$opt_d" ] ; then rm -f $scrfil.boxes $scrfil.subnet ; fi