Category: Networking Code
Author/Contact Info ybiC
Description: It's nearly impossible for me to keep server connections labeled correctly at my core switches.   This ditty uses nmap and Net::Ping to build a list of MAC addresses with associated hostnames/IP addresses for live connections on a local subnet/VLAN.   I can then compare it with the switch's CAM table to find what box is connected at which port.

I dabbled with Linux arping utility, but stuck with Net::Ping for one less external dependancy.

Comments or suggestions for improvement are both welcomed and appreciated.

Update 3: 2001-04-30
hashamafied passel o' scalars and minor format cleanup.

Update 2:
cleaned up a few minor Perlish faux pas' and added to-do of using snmpwalk syntax based from riffraff's post in this thread.

thanks to turnstep and to Ovid for feedback, and to ncw for his recent post Numeric list to optimised regexp , which made a no-brainer of regex's to match for nmap input.

#!/usr/bin/perl -w

# pod at tail

use strict;
use Net::Ping;

my $subnet = shift;                  # a.b.c.d/nn subnet/bitwise_netma
my %file = (
    nmapout   => 'panout',
    nmapclean => 'pamclean',
    pingout   => 'papout',
    arpout    => 'paaout',
    arpclean  => 'paaclean',
my %bin = (
    nmap => '/usr/bin/nmap',         # Debian 2.2r3 "Espy"
    arp  => '/usr/sbin/arp',         # Debian 2.2r3 "Espy"
my @nmapregex  = (                   # text to remove from nmap output
    '^.*Log of.*$',                  # header line
    '^.*\.0\).*$',                   # subnet lines
    '^.*\.255\).*$',                 # broadcast lines
    '^Host\s+.*\(',                  # text prior to IP address
    '\) appears to be up\.',         # text following IP address
    '^\s+',                          # whitespace-only lines
my @arpregex = (                     # text to remove from copied ARP 
    '^.*\(incomplete\).*$',          # incomplete MAC address
    '^Address\s+.*$',                # header line
    '\sether\s',                     # HW type
    '\s+C\s+eth0',                   # Flags, Mask, Iface
    '^\s+',                          # whitespace-only lines

# Make sure that *something* was entered by user for subnet/netmask
unless ($subnet) {
    print "\nUsage: pingarp a.b.c.d/nn<enter>\n",
        "where a.b.c.d is your local subnet ",
        "and nn is your bitwise netmask netmask.\n\n";
$bin{nmapsyntax} = "-sP $subnet -o $file{nmapout}",  # nmap v2.2

# Then check for valid subnet/mask from user to nmap
# ? how to prevent leading zeroes ? (nmap bombs on 'em)
# \. = octet boundry
# \/ = subnet-netmask separator
#    1st octet -  match 1-254
# [1-9]|(?:[1-9]|1\d|2[0-4])\d|25[0-4]
#    2nd, 3rd, 4th octets - match 0-254
# \d|(?:[1-9]|1\d|2[0-4])\d|25[0-4]
#    bitwise netmasks - 8..14, 16..22, 24..30
# [89]|1[012346789]|2[012456789]|30

print "\nChecking requirements.\n";
# if you got this far, Net::Ping must be installed,
# otherwise would have halted with compilation errors.
unless (-x $bin{nmap}) {
    print "nmap not accessible where script expects.\n\n";
unless (-x $bin{arp}) {
    print "arp not accessible where script expects.\n\n";

# Oddly enough, using nmap like this doesn't
# seem to properly populate system ARP table.
# System call uses "and" instead of "or" for some reason.
print "Building list of live hosts on local subnet.\n";
system ("$bin{nmap} $bin{nmapsyntax} > /dev/null") and (die "Error cal
+ling $bin{nmap}: $!");
open (NMAPOUT, "<$file{nmapout}")
    or die "Error opening $file{nmapout} RO: $!";
open (NMAPCLEAN, ">$file{nmapclean}")
    or die "Error opening $file{nmapclean} WO: $!";
while (<NMAPOUT>) {
    foreach my $regex(@nmapregex) {s/$regex//g;}
    print NMAPCLEAN $_;
close (NMAPOUT)
    or die "Error closing $file{nmapout}: $!";
    or die "Error closing $file{nmapclean}: $!";

# So we have to do this with real pings.
print "Populating local ARP table.\n";
open (NMAPCLEAN, "<$file{nmapclean}")
    or die "Error opening $file{nmapclean} RO: $!";
open (PINGOUT, ">$file{pingout}")
    or die "Error opening $file{pingout} WO: $!";
my @hosts = (<NMAPCLEAN>);
my $p = Net::Ping->new("icmp");
foreach my $host (@hosts) {
    print PINGOUT "$host is ";
    print PINGOUT 'NOT ' unless $p->ping($host, 2);
    print PINGOUT "reachable.\n";
    sleep (1);
# don't actually do anything with $file{pingout}, save it anyway.
    or die "Error closing $file{nmapclean}: $!";
close (PINGOUT)
    or die "Error closing $file{pingout}: $!";

print "Querying ARP table and cleaning results.\n";
system("$bin{arp} > $file{arpout}")
    and die "Error running $bin{arp}: $!";
open (ARPOUT, "<$file{arpout}")
    or die "Error opening $file{arpout} RO: $!";
open (ARPCLEAN, ">$file{arpclean}")
    or die "Error opening $file{arpclean} WO: $!";
while (<ARPOUT>) {
    foreach my $regex(@arpregex) {s/$regex//g;}
    print ARPCLEAN $_;
close (ARPOUT)
    or die "Error closing $file{arpout}: $!";
close (ARPCLEAN)
    or die "Error closing $file{arpclean}: $!";

    "\nOutput files:\n",
    "  finished results:      $file{arpclean}\n",
    "  raw arp table:         $file{arpout}\n",
    "  ping-responding hosts: $file{pingout}\n",
    "  nmap-responding hosts: $file{nmapclean}\n",
    "  raw nmap results:      $file{nmapout}\n\n"

=head1 Name

=head1 Summary

Generate list of MAC addrs for all live IP devices on your subnet.

=head1 Usage a.b.c.d/nn
   a.b.c.d = your local subnet address
   nn      = bitwise netmask


 Must run as root from host on same subnet as target hosts.
 Output files are placed in current directory.

=head1 Requires and related

 nmap v2.12
    included in standard Perl distribution


=head1 Tested

with Perl 5.00503 on Debian 2.2 "Espy"

=head1 Updated

 2001-04-30   14:00
   Hashamafy passel o' scalars.
   Format for 75 chars/line max.
   Eliminate multiple useless "my var" at start of script
     by calling with "my" when first used.
   Clean up inconsistent indenting.
   Replace doublequotes with singlequotes for strings.
   Initial working code.

=head1 ToDos

 Replace "Querying ARP table..." section with
   "snmpwalk <router> <community> IpnetToMediaPhysAddr"
   to allow script to work for remote segments.
   Will then need to prompt for (name|address) of remote router.
 Check for valid subnet/mask input.
   commented regex above or
 Check that script is being run by root (required for ICMP ping).
 Check into File::Temp for mess o' working files.
 Translate MACs for Token-Ring.

=head1 Author