#!/usr/local/bin/perl

#
# NAME
#    mhsearch -- search for multihomed hosts
#
# AUTHOR
#    Jeremy Elson, jelson@circlemud.org
#    16 December 1997
#    Version 0.2
#
# SYNOPSIS
#    mhsearch network.number/mask-length
#
# EXAMPLES
#    mhsearch 128.231.128.0/23
#       (searches 128.231.128.1 through 128.231.129.255)
#    mhsearch 128.220.13.0/24
#       (searches 128.220.13.1 through 128.220.13.255)
#
# DESCRIPTION
#   This program scans a datalink network to see if there are any
#   devices that are simultaneously using more than one IP address
#   (i.e., a multihomed host).  It does so by systematically pinging
#   every address on a network specified on the command-line,
#   including the broadcast address.  This causes an ARP request to be
#   generated for each host, which should respond with an ARP reply.
#   mhsearch then uses the kernel's ARP cache to check for multiple IP
#   addresses that map to the same MAC address.
#
#   Note, this program does *not* scan for IP address collisions
#   (i.e., a single IP address being used by more than one host).  It
#   does the opposite: scan for a single host using more than one IP
#   address.
#
# KNOWN BUGS
#   Multihomed hosts may be reported that are outside of the network
#   specified on the command line.  This is because any multihomed
#   host found in the kernel's ARP cache will be reported, even if
#   those entries were not added to the ARP cache as a result of
#   mhsearch's operation.
#
#   mhsearch does not verify that the given network number and subnet
#   mask length are valid.
#
#   There is no guarantee that hosts will remain in the ARP cache
#   in between the time when they are pinged and when the ARP cache
#   is checked.  If there is a lot of network activity on the machine
#   during mhsearch's operation, some ARP mappings may be lost.
#
# NOTES
#   This program has strong dependencies on the arguments and location
#   of the "ping" program, and on the exact form of the output of the
#   "arp -a" command.  mhsearch was developed under Solaris 2.5.1 and
#   will not run under OS's whose ping arguments or "arp -a" output
#   differs from Solaris'.
#
#   Of course, mhsearch can only find multihomed hosts on a locally
#   connected datalink network.  It cannot scan networks through a
#   router.

############################################################################

$safe_arp_table_size = 20;

# parse_arp_table only works for parsing the output of "arp -a" under
# Solaris, or another OS whose "arp -a" output looks exactly
# identical.
sub parse_arp_table {
    # Dump the arp table using "arp -a"
    open(ARPA, "arp -a|");

    while ($line = <ARPA>) {
	($interface, $hostname, $mask, $flags, $hw) = split(/\s+/, $line);

	# To shut up the debugger...
	undef $interface;

	# Ignore ARP requests that haven't completed yet
	next if ($flags =~ /U/ && $hw eq "");

	# If this arp entry has no flags, the "flags" field actually
	# has the hardware address.
	if ($hw eq "") {
	    $hw = $flags;
	}

	# Make sure the mask is well-formed; if not, discard it
	# (probably, this is a line from the heading of the arp table)
	next if ($mask !~ /(\d+)\.(\d+)\.(\d+)\.(\d+)/);

	# Make sure that we haven't added this host to the map before
	next if (exists $hosts{$hostname});

	# Record the fact that we've seen this host before
	$hosts{$hostname} = 1;

	# Record the mapping
	if (exists $map{$hw}) {
	    $map{$hw} = $map{$hw} . ", $hostname";
	} else {
	    $map{$hw} = $hostname;
	}
    }

    close(ARPA);
}


sub normalize {
    if ($a4 >= 256) {
	$a3++;
	$a4 = 0;
    }

    if ($a3 >= 256) {
	$a2++;
	$a3 = 0;
    }
}


# "Wake up" hosts (i.e., add them to our arp cache) by pinging them.
# Pings all hosts on the network sequentially.  Checks the ARP table
# after every $safe_arp_table_size hosts.
sub wake_up {
    $a4++;
    &normalize;

    $num_launched = 0;

    for ($i = 0; $i < $num_hosts; $i++) {
	$ip = "$a1.$a2.$a3.$a4";
	system "/usr/sbin/ping $ip 1 >/dev/null &";
	$a4++;
	&normalize;

	if (++$num_launched == $safe_arp_table_size) {
	    print "pinging up through $ip...\n";
	    sleep 3;
	    $num_launched = 0;
	    &parse_arp_table;
	}
    }

    # The last address we pinged should have been the broadcast address.
    # Sleep for several seconds to make sure that all of the ARP requests
    # complete, then check the ARP cache one last time.
    print "pinging broadcast address...\n";
    sleep 8;
    &parse_arp_table;
}


sub report {
    $duplicates = 0;

    foreach $i (sort keys %map) {
	if ($map{$i} =~ /, /) {
	    if ($duplicates == 0) {
		print "Duplicates found:\n";
	    }
	    print "$i is using $map{$i}\n";
	    $duplicates++;
	}
    }

    if ($duplicates == 0) {
	print "No duplicates found.\n";
    } else {
	if ($duplicates > 1) {
	    $h = "hosts";
	} else {
	    $h = "host";
	}

	printf "$duplicates $h found to be using multiple addresses.\n",
    }
}


##########################################################################

undef %map;
undef %hosts;

# Make sure the argument is (superficially) in the correct form
if ($#ARGV == 0 && $ARGV[0] =~ /(\d+)\.(\d+)\.(\d+)\.(\d+)\/(\d+)/) {
    $a1 = $1;
    $a2 = $2;
    $a3 = $3;
    $a4 = $4;
    $masklen = $5;

    $num_hosts = (2**(32-$masklen)) - 1;

    if ($num_hosts > 1024) {
	die "can't ping so many hosts!\n";
    }

    $n = $num_hosts-1;
    print "Scanning IP network $a1.$a2.$a3.$a4, mask len $masklen";
    print " ($n hosts + 1 broadcast address)\n";
} else {
    die "usage: $0 ip.ip.ip.ip/masklen\n";
}

&wake_up;
&report;
