#! /bin/bash
#
# -- 
#
# -- released under the GPL
#
# -- Gigantic wrapper script for gratuitous ARP, 2005-11-17; -MAB
#    based idea from Mike Pomraning's perl grat_arp_eth0
#
#
# -- Environment variables supported by utility:
#
#      GRATARP_VERBOSE   whether to default to verbosity or not.
#                        -1, be less chatty
#                         0, usual behaviour
#                         1, be loud all the time
#      GRATARP_IFNAME    default interface on which to send frames
#                        system default is eth1, since that is "outside"
#                        on most of our boxen, and the most commonly
#                        problematic interface for us
#      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, can also
#                        be controlled on the command line
#
# -- See "usage" for tips on how to use this.  This should be a whole lot
#    easier than cracking open the "arping" manpage.
#
# -- Changelog
#
#    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>

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

VERSION=0.2

# -- my favorite shell functions (and their offspring)
#
gripe  () { printf "%s\n" "$@" >&2; }
abort  () { error "$@"; exit 1;     }
error  () { gripe "E: $@";          }
warn   () { gripe "E: $@";          }
inform () { test "$VERBOSE" -ge "$INFORM" && gripe "I: $@"; }
notice () { test "$VERBOSE" -ge "$NOTICE" && gripe "N: $@"; }
debug  () { test "$VERBOSE" -ge "$DEBUG"  && gripe "D: $@"; }


# - - - - - - - - - - -
  usage  () {
# - - - - - - - - - - -
#
  gripe  \
    "Usage: ${0##*/} [ options ] <ipaddr> [ <ipaddr> ... ]"                 \
    ""                                                                      \
    "Options (defaults marked, or in parentheses):"                         \
    "  -q, --quiet              Reset verbosity to none."                   \
    "  -v, --verbose            Be verbose.  (interactive default)"         \
    "  -a, --answer             Use ARP answer frames. (default)"           \
    "  -u, --unsolicited        Use unsolicited ARP frames."                \
    "  -c, --count <num>        Frame count. ($FRAME_COUNT)"                \
    "  -i, --interface <name>   Specify an interface. ($DEF_INTERFACE)"     \
    "  -d, --delay <seconds>    Inter-frame delay in seconds. ($DELAY)"     \
    ""                                                                      \
    "This utility accepts any number of <ipaddr> arguments and will emit a" \
    "gratuitous ARP for each specified IP address on <interface>, sleep "   \
    "<delay> time and repeat <count> times.  A <delay> of 0 will cause us"  \
    "to send ARP frames as fast as we can (which ain't that fast, since"    \
    "this whole thing is written in shell)."                                \
    ""                                                                      \
    "If you see an error message like below (one of the most common error"  \
    "messages with 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:"                                                             \
    ""                                                                      \
    "  ${0##*/} -i eth0 10.10.20.37"                                        \
    "    typical; advertise $FRAME_COUNT times for 10.10.20.37 on eth0"     \
    "  ${0##*/} --count 10 --quiet --interface eth0 10.10.20.33"            \
    "    send 10 frames advertising 10.10.20.37 on eth0"                    \
    "  ${0##*/} --delay 0 --quiet -i eth2 192.168.1.2 192.168.1.3"          \
    "    with no interframe delay on eth2, advertise for both .2 and .3"    \
    ""
  test "$#" -gt 0 && abort "$@"
  exit 0
}

# - - - - - - - - - - -
  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 $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 $perty - sent    $IPADDR on $INTERFACE"
         printf "%s " "$IPADDR"                                 ;;
      2) warn "$IPADDR on $INTERFACE: $ARPING_ERROR"            ;;

    esac

  done
}

# - - - - - - - - - - -
# main () {
# - - - - - - - - - - -
#
# -- normalize the command-line options
#
OPTIONS="vqhauc:i:d:"
LONGOPTIONS="verbose,quiet,help,answer,unsolicited,interface:,count:,delay:"

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


# -- 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...
#
tty --silent         && let VERBOSE=VERBOSE+1

# -- examples of options/arguments should look like this now:
#
#   -q -i eth0 --
#  --quiet --count 4 --
#  --interface eth2 --verbose --
#   -d 0 -v -i eth0 --
#   -a -d 0 -v -i eth0 --
#   -v -v -v -i eth0 --
# 
while test "$#" -gt "0" ; do
   case "$1" in

      -h | --h* ) usage                          ;;
      -u | --u* ) FRAME_TYPE="-U"                ;;  # -- type of frame can be
      -a | --a* ) FRAME_TYPE="-A"                ;;  #    answer / unsolicited
      -v | --v* ) let VERBOSE=VERBOSE+1          ;;  # -- increment verbosity
      -q | --q* ) VERBOSE=0                      ;;  # -- quell all verbosity
      -c | --c* ) FRAME_COUNT="$2"   && shift    ;;
      -i | --i* ) INTERFACE="$2"     && shift    ;;
      -d | --d* ) DELAY="$2"         && shift    ;;
             -- ) shift              && break    ;;  # -- option parsing done
             -* ) usage "Unknown option: $1"     ;;

   esac
   shift
done

# -- make sure the user specified at least one IP address
#
test $# -ge 1        || usage "Wrong number of arguments supplied."

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

INTERFACE=${INTERFACE:-$DEF_INTERFACE}

IPADDRS="$@"

# -- loop over the addresses, and send a frame at a time; this
#    is a little expensive, but allows more verbosity to user,
#    and allows us to remove addresses for which arping reports
#    errors
#
for (( round=1 ; "$round" <= "$FRAME_COUNT" ; round++ )) ; do

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

  # -- try to sleep, but don't worry too much, just keep going
  #
  sleep $DELAY || :

done

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

# -- end of file
