#! /bin/bash
#
# -- gratuitous-arp-0.4, 2006-05-19; Martin A. Brown <martin@linux-ip.net>
#
# -- released under the GPL
#
# -- This is, essentially, a large wrapper around the arping utility,
#    which makes it easier to just type "gratuitous-arp -i eth0 10.0.5.4"
#
# -- Environment variables supported by the utility, all of which can be
#    overridden by command line options:
#
#      GRATARP_VERBOSE   whether to default to verbosity or not.
#                        negative, be less chatty
#                        zero, usual behaviour
#                        positive, be loud all the time
#      GRATARP_IFNAME    default interface on which to send frames
#                        system default is eth0
#      GRATARP_TYPE      which type of frame to use
#                         -A will specify "answer frames" (default)
#                         -U will specify "unsolicited frames"
#      GRATARP_COUNT     number of frames to send by default

# -- ChangeLog
#
#   0.42 2006-09-09; -MAB
#        final release of packaged gratuitous-arp
#   0.41 2006-09-09; -MAB
#        internal version bump, while preparing package and Makefile
#    0.4 2006-05-19; -MAB
#        documentation and usage; README
#        clearer logic for loop termination
#        skip final (unnecessary) sleep in main loop
#        minor debug output fixes
#    0.3 2006-05-18; -MAB
#        improve sanity checking against arping (ensure we aren't trying
#          to use the Thomas Habets version.
#    0.3 2006-05-18; -MAB
#        improve usage output, other minor cleanups
#    0.2 2005-11-17; -MAB
#        first cleaned-up and GPL'd version based on older work

# -- Contributors
#
#    Mike Pomraning <mjp@pilcrow.madison.wi.us>

# -- BUGS/TODO
#
#    - Figure out a way to support both the Thomas Habets and the
#      Kuznetsov arping utilities.
#    - usleep binary is available, allow delay as microseconds
#

# -- script global variables
#
VERBOSE="${GRATARP_VERBOSE:-0}"
DEF_INTERFACE="${GRATARP_IFNAME:-eth0}"
FRAME_COUNT="${GRATARP_COUNT:-3}"
FRAME_TYPE="${GRATARP_TYPE:--A}"
DELAY="${GRATARP_DELAY:-1}"

WARN=1
INFORM=2
NOTICE=3
DEBUG=4

STDOUT=1
STDERR=2

VERSION=0.42
NAME=gratuitous-arp

REQUIRED_UTILITIES="sleep cat"

# -- my favorite shell functions (and their offspring)
#
# - - - - - - - - - 
  gripe () {
# - - - - - - - - - 
#
  local descriptor=$1   && shift
  printf -- "%s\n" "$@" >&${!descriptor}
}

abort  () { gripe STDERR "$@"; exit 1;     }

warn   () { test "$VERBOSE" -ge "$WARN"   && gripe STDERR "W: $@"; }
inform () { test "$VERBOSE" -ge "$INFORM" && gripe STDERR "I: $@"; }
notice () { test "$VERBOSE" -ge "$NOTICE" && gripe STDERR "N: $@"; }
debug  () { test "$VERBOSE" -ge "$DEBUG"  && gripe STDERR "D: $@"; }

interrupt () { exit 0 ; }

trap interrupt INT

# - - - - - - - - - - -
  version  () {
# - - - - - - - - - - -
#
  gripe STDOUT "${0##*/}-${VERSION}"
  exit 0
}

# - - - - - - - - - - -
  usage  () {
# - - - - - - - - - - -
#
  local descriptor=$1   && shift
  cat >&${!descriptor} <<-EOUSAGE
Usage: ${0##*/} [ options ] <ipaddr> [ <ipaddr> ... ]

Options (defaults marked, or in parentheses):
  -h, --help               Print out this handy help screen.
  -L, --long-usage         Provide help and some more hints.
  -q, --quiet              Reset verbosity to none.
  -v, --verbose            Increase verbosity, can be used multiple times.
  -U, --unsolicited        Use unsolicited ARP frames.
  -A, --answer             Use ARP answer frames. (default)
  -c, --count <number>     Packet count. ($FRAME_COUNT)
  -i, --interface <name>   Specify an interface. ($DEF_INTERFACE)
  -d, --delay <seconds>    Inter-frame delay in seconds. ($DELAY)
  -a, --arping <path>      Full path to iputils arping. (arping in \$PATH)

EOUSAGE
}

# - - - - - - - - - - -
  short_usage  () {
# - - - - - - - - - - -
#
  local descriptor="${1:-STDOUT}"  && shift
  usage $descriptor
  test "$#" -gt 0 && abort "$@"
  exit 0
}

# - - - - - - - - - - -
  long_usage  () {
# - - - - - - - - - - -
#
  local descriptor="${1:-STDOUT}"  && shift
  cat >&${!descriptor} <<-EOUSAGE
${0##*/}-${VERSION} README

Description
===========
When an IP address (or a set of addresses) moves from one device to
another, it is often necessary to communicate this IP migration to other
devices on the network.  Many IP stacks accept these so-called
gratuitous ARP messages and refresh their ARP cache accordingly.  Though
arguably a security problem, the behaviour is very convenient in the
following situations:

  - failover where the machines are NOT sharing a MAC address
  - swapping machines, moving an IP (or a set of IPs) from one active
    device to another machine
  - stealing multiple IPs from your friends during LAN parties

There are two major arping utilities.  Despite the rich set of options
and features offered by the newer arping by Thomas Habets, it doesn't
easily support gratuitous ARP.  Thus, this utility relies on the arping
supplied with iputils, written by Alexey Kuznetsov.  If you have both
installed, simply set the \$PATH appropriately before calling this
script, or try the --arping (-a) option to specify the path to your
iputils arping.


Arguments
=========
This utility accepts an arbitrary number of <ipaddr> arguments and will
transmit a gratuitous ARP for each specified IP address.  It will loop
serially through the entire list of IP addresses specified as arguments.


Options
=======
The default delay time between ARP transmissions is $DELAY seconds, but
can be controlled with the --delay (-d) option.  Specifying a delay of
zero (0) causes us to send ARP frames as fast as we can, which ain't
that fast, since this whole thing is written in shell.

${0##*/} will loop over the arguments by default $FRAME_COUNT times.
This can overriden by using the --count (-c) option.  Specifying a count
of zero (0) will cause the utility to loop forever.

There are two common ARP packet types used for gratuitous ARP functions.
The options --unsolicited (-U) and --answer (-A) select the desired type.

The --arping (-a) option allows the user to specify the path to the
desired arping binary, instead of relying on \$PATH.


Common error
============
If you see an error message like below (one of the most common error
messages with gratuitous ARP using arping), check your <interface> to
see that it has the <ipaddr> listed.  You will not be able to issue a
gratuitous ARP unless the address is available on the specified
interface.

   E: 10.10.20.37 on eth0: bind: Cannot assign requested address


Examples
========
typical usage; advertise $FRAME_COUNT times for 10.10.20.37 on eth0
  ${0##*/} -i eth0 10.10.20.37

send 10 frames advertising 10.10.20.37 on eth0, and shut up!
  ${0##*/} --count 10 --quiet --interface eth0 10.10.20.33

with no interframe delay on eth2, advertise for both .2 and .3
  ${0##*/} --delay 0 --quiet -i eth2 192.168.1.2 192.168.1.3

EOUSAGE

  usage $descriptor

exit 0

}

# - - - - - - - - - - -
  parse_options () {
# - - - - - - - - - - -
#
  command getopt                    \
    --unquoted                      \
    --name "${0##*/}"               \
    --options "${OPTIONS}"          \
    --longoptions "${LONGOPTIONS}"  \
    -- "$@"
}

# - - - - - - - - - - -
  verify_support  () {
# - - - - - - - - - - -
#
# -- all we want to do is make sure that all of our support
# programs exist.
#
  local program
  local programs="$@"
  local N
  local ok=0           # -- return 0, if everything is OK!
  
  for program in $programs; do
    N=$( which "$program" 2>/dev/null )
    if test -z "$N"; then
      ok=1
      gripe STDERR "Could not locate necessary support program:  $program"
    fi
  done
  return $ok
}

# - - - - - - - - - - -
  arping_sanity_check () {
# - - - - - - - - - - -
#
  local arping="$1"

  inform "Running short sanity checks on $arping."

  # -- first, let's see if we have the "right" arping for this wrapper
  #
  $arping -V >/dev/null 2>&1 \
    || abort "Thomas Habets arping.  Try \"${0##*/} --long-usage\" for details."

  # -- now, let's see if the current user has permission to run arping
  #
  ARPING_ERROR=$( $arping -I $INTERFACE -c 1 0.0.0.0 2>&1 )

  case "$?" in

    0)   return 0                                                  ;;
    1)   return 0                                                  ;;
    2)   abort "Error trying to run ${arping}: ${ARPING_ERROR}."   ;;

  esac
}

# - - - - - - - - - - -
  send_frame () {
# - - - - - - - - - - -
#
# -- do the dirty work here!
#    we need to get the frame_type, the interface and the IP to hit
# -- we'll spit any error messages from arping to STDOUT for the
#    caller to deal with
#
  local frame_type=$1      && shift
  local interface=$1       && shift
  local ipaddr=$1          && shift

  #debug "arping $frame_type -c 1 -I $interface $ipaddr"

  # -- this funny nonsense below is so that we can throw away
  #    the STDOUT, but keep the error message if there is any
  #
  {
    $ARPING_BIN $frame_type -c 1 -I $interface $ipaddr >/dev/null
  } 2>&1
}

# - - - - - - - - - - -
  hit_targets () {
# - - - - - - - - - - -
#
# -- use lots of globals (eych), take as an argument, the count
#    of the current loop
#
# -- STDOUT from this function includes any IPADDRs for which the
#    arping call did not fail.  We can catch our errors, continue
#    our gratuitous ARP, or halt as needed.
#
  local round=$1           && shift

  unset GOOD_IPS

  purty=$( printf "%05d" "$round" )

  for IPADDR in $IPADDRS ; do

    debug "frame $purty - sending $IPADDR on $INTERFACE"

    ARPING_ERROR="$( send_frame $FRAME_TYPE $INTERFACE $IPADDR )"

    case $? in

      0) notice "frame $purty - sent    $IPADDR on $INTERFACE"
         printf "%s " "$IPADDR"                                 ;;
      2) warn "$IPADDR on $INTERFACE: $ARPING_ERROR"            ;;

    esac

  done
}

# - - - - - - - - - - -
  work_remains () {
# - - - - - - - - - - -
#
  local cont=0
  local halt=1
  
  test -n "$IPADDRS"                 || return $halt
  test "$FRAME_COUNT" -ne 0          || return $cont
  test "$round" -ge "$FRAME_COUNT"   || return $cont

  # -- otherwise

                                        return $halt

}




# -- check to make sure that we have a few key utilities before
#    launching off the command line processing
#
verify_support which \
  || abort "${0##*/}: Cannot proceed without \"which\" utility, quitting."

verify_support getopt \
  || abort "${0##*/}: Cannot process command line options, quitting."

# - - - - - - - - - - -
# main () {
# - - - - - - - - - - -
#
# -- normalize the command-line options
#
OPTIONS="vqhLVAUc:i:d:a:"
LONGOPTIONS="verbose,quiet,help,long-usage,version"
LONGOPTIONS="$LONGOPTIONS,answer,unsolicited,interface:,count:,delay:,arping:"

# -- calling parse_options twice seems silly.  It is.  It allows, us
#    however, to control the error reporting to the user a bit more
#    carefully.  All the OCD kids are doing it.
#
parse_options "$@" >/dev/null \
  || abort "Try \"${0##*/} --help\" for more information."

set -- $( parse_options "$@" )

# -- Bump up verbosity a notch if we are connected to a terminal;
#    generally, users like to know a bit more of what's going on...
# -- if 'tty' ain't there, we are just going to happily continue.
#
tty 2>/dev/null --silent  && let VERBOSE=VERBOSE+1

# -- examples of options/arguments should look like this now:
# 
while test "$#" -gt "0" ; do
   case "$1" in

      -h | --h*    ) short_usage                              ;;
      -L | --l*    ) long_usage                               ;;
      -V | --vers* ) version                                  ;;
      -v | --verb* ) let VERBOSE=VERBOSE+1                    ;;
      -q | --q*    ) VERBOSE=0                                ;;

      -u | --u*    ) FRAME_TYPE="-U"                          ;;
      -a | --answ* ) FRAME_TYPE="-A"                          ;;
      -A | --arpi* ) ARPING_BIN="$2"    && shift              ;;
      -c | --c*    ) FRAME_COUNT="$2"   && shift              ;;
      -i | --i*    ) INTERFACE="$2"     && shift              ;;
      -d | --d*    ) DELAY="$2"         && shift              ;;

             --    ) shift              && break              ;;
             -*    ) short_usage "Unknown option: $1"         ;;

   esac
   shift
done

verify_support $REQUIRED_UTILITIES \
  || abort "${0##*/}: Missing utilities, cannot continue, quitting."

# -- special handling, since there are two arping utilities and this
#    wrapper does not work with the Thomas Habets version, only with
#    the Alexey Kuznetsov, iputils version.
#
ARPING_BIN=${ARPING_BIN:-$( which arping 2>/dev/null )}

test -x "$ARPING_BIN" \
  || abort "Could not find an arping utility."

arping_sanity_check "$ARPING_BIN"

# -- make sure the user specified at least one IP address
#
test $# -ge 1 \
  || short_usage STDERR "${0##*/}: No addresses specified."

test -z "$INTERFACE" \
  && inform "Interface not specified, using $DEF_INTERFACE."

INTERFACE=${INTERFACE:-$DEF_INTERFACE}

IPADDRS="$@"

debug "About to begin main loop."

# -- loop over the addresses, and send a frame at a time; this
#    is much more expensive, but allows more verbosity to user,
#    and allows us to remove addresses for which arping reports
#    errors
#
round=0
while work_remains ; do

  # -- hit_targets will report the IP addresses for which it was
  #    able to send ARP frames
  #
  IPADDRS=$( hit_targets $round )

  let round=round+1

  # -- don't sleep, unless we'll loop again
  #
  if work_remains ; then 
    sleep $DELAY || :
  fi

done

inform "Sent $round ARP frames on $INTERFACE from $IPADDRS"

# -- end of file
