# # Extract ARP table from 3Com SuperStack II switches (3900, 9300) # and CoreBuilder 3500. # # $Id: ARP.pm,v 1.25 2002/04/11 13:45:00 trockij Exp $ # # Copyright (C) 1998 Jim Trocki # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # package A3Com::ARP; require 5.004; use strict; use vars qw($VERSION); use A3Com::Base; use SNMP; $VERSION = "0.05"; sub normalize_mac; sub lookup_mac_history; sub new { my $proto = shift; my $class = ref($proto) || $proto; my %vars = @_; my $self = {}; $self->{"IPMAC"} = {}; $self->{"MACIP"} = {}; $self->{"ERROR"} = ""; $self->{"SWITCH"} = ""; $self->{"READ_METHOD"} = \&snmp_read; $self->{"LOGIN"} = "read"; $self->{"PASSWORD"} = ""; $self->{"READ_COMMUNITY"} = "public"; $self->{"WRITE_COMMUNITY"} = "public"; $self->{"LOG"} = 0; $self->{"CACHE"} = 1; $self->{"GLOBALCACHE"} = 1; $self->{"CACHETIME"} = $A3Com::Base::DEFAULT_CACHETIME; $self->{"GLOBALCACHEDIR"} = "/usr/local/share/a3com"; if (defined $ENV{"HOME"}) { $self->{"CACHEDIR"} = $ENV{"HOME"} . "/.a3com"; } else { $self->{"CACHEDIR"} = ".a3com"; } my %c = &A3Com::Base::_read_conf ($self, $ENV{"A3COM_CONF"}); for (keys %c) { $self->{$_} = $c{$_}; } if (defined $ENV{"A3COM_GLOBALCACHEDIR"}) { $self->{"GLOBALCACHEDIR"} = $ENV{"A3COM_GLOBALCACHEDIR"}; } if (defined $ENV{"A3COM_CACHEDIR"}) { $self->{"CACHEDIR"} = $ENV{"A3COM_CACHEDIR"}; } $self->{"_LOADED"} = 0; for my $k (keys %vars) { $self->{$k} = $vars{$k} if ($vars{$k} ne ""); } bless ($self, $class); return $self; } sub cachedir { my $self = shift; if (@_) { $self->{"CACHEDIR"} = shift } return $self->{"CACHEDIR"}; } sub globalcachedir { my $self = shift; if (@_) { $self->{"GLOBALCACHEDIR"} = shift } return $self->{"GLOBALCACHEDIR"}; } sub cachetime { my $self = shift; if (@_) { $self->{"CACHETIME"} = shift } return $self->{"CACHETIME"}; } sub cache { my $self = shift; if (@_) { $self->{"CACHE"} = shift } return $self->{"CACHE"}; } sub globalcache { my $self = shift; if (@_) { $self->{"GLOBALCACHE"} = shift } return $self->{"GLOBALCACHE"}; } sub switch { my $self = shift; if (@_) { $self->{"SWITCH"} = shift } return $self->{"SWITCH"}; } sub error { my $self = shift; if (@_) { $self->{"ERROR"} = shift } return $self->{"ERROR"}; } sub log { my $self = shift; if (@_) { $self->{"LOG"} = shift } return $self->{"LOG"}; } # # given a MAC address, return the list of IP addresses # which it resolves to # sub ip { my $self = shift; my $mac = A3Com::Base::_mac_normalize (shift); $self->{"ERROR"} = ""; return undef if (!defined $self->_load_cache ($self)); if (!defined $self->{"MACIP"}->{$mac}) { $self->{"ERROR"} = "$mac not found"; return (undef); } return @{$self->{"MACIP"}->{$mac}}; } # # given an IP address, return the MAC address which it resolves to # sub mac { my ($self, $ip) = @_; $self->{"ERROR"} = ""; return undef if (!defined $self->_load_cache ($self)); return $self->{"IPMAC"}->{$ip}; } sub arp_table { my $self = shift; $self->{"ERROR"} = ""; return undef if (!defined $self->_load_cache ($self)); return %{$self->{"IPMAC"}}; } sub ip_addrs { my $self = shift; $self->{"ERROR"} = ""; return undef if (!defined $self->_load_cache ($self)); return keys %{$self->{"IPMAC"}}; } sub mac_addrs { my $self = shift; $self->{"ERROR"} = ""; return undef if (!defined $self->_load_cache ($self)); return keys %{$self->{"MACIP"}}; } sub file_read { my ($self, $file) = @_; my ($mac, @ip); $self->{"ERROR"} = ""; if (!defined $file) { if (! -d $self->{"CACHEDIR"}) { $self->{"ERROR"} = "CACHEDIR is not a directory"; return undef; } $file = "$self->{CACHEDIR}/$self->{SWITCH}.arp"; } if (!open (I, $file)) { $self->{"ERROR"} = "$!"; return undef; } while () { next if (/^\s*#/ || /^\s*$/); chomp; ($mac, @ip) = split; @{$self->{"MACIP"}->{$mac}} = @ip; for (@ip) { $self->{"IPMAC"}->{$_} = $mac; } } close (I); $self->{"LOADED"} = 1; } sub file_write { my ($self, $file) = @_; my ($ip, $mac); $self->{"ERROR"} = ""; if (!defined $file) { my $cd; if ($self->{"GLOBALCACHE"}) { $cd = "GLOBALCACHEDIR"; } else { $cd = "CACHEDIR"; } if (! -d $self->{$cd}) { $self->{"ERROR"} = "$cd is not a directory"; return undef; } $file = "$self->{$cd}/$self->{SWITCH}.arp"; } if (!open (O, ">$file")) { $self->{"ERROR"} = "$!"; return undef; } print O "#\n# ARP table for $self->{SWITCH} as of " . time . "\n#\n"; foreach $mac (keys %{$self->{"MACIP"}}) { print O "$mac @{$self->{MACIP}->{$mac}}\n"; } close (O); } # # Read the ARP table from a host using MIB-2 # sub snmp_read { my $self = shift; my ($s, $mac, $ip); $self->{"ERROR"} = ""; if ($self->{"SWITCH"} eq "") { $self->{"ERROR"} = "no switch defined"; return undef; } if (!defined ($s = new SNMP::Session ( DestHost => $self->{"SWITCH"}, Community => ($self->{"READ_COMMUNITY"} or "public"), Timeout => 30_000_000))) { $self->{"ERROR"} = "could not create SNMP session: " . $s->{"ErrorStr"}; return undef; } my $row = new SNMP::VarList ( ['ipNetToMediaIfIndex'], ['ipNetToMediaPhysAddress'], ['ipNetToMediaNetAddress'], ['ipNetToMediaType'], ); while ($s->getnext ($row)) { last if ($row->[0]->[0] ne "ipNetToMediaIfIndex"); last if ($s->{"ErrorStr"} ne ""); $mac = sprintf ("%02x:%02x:%02x:%02x:%02x:%02x", unpack ("C6", $row->[1]->[2])); $ip = $row->[2]->[2]; $self->{"IPMAC"}->{$ip} = $mac; push @{$self->{"MACIP"}->{$mac}}, $row->[2]->[2]; } return 1; } sub _load_cache { my $self = shift; if (!$self->{"_LOADED"}) { if ($self->{"GLOBALCACHE"}) { return undef if (!defined (A3Com::Base::_load_global ($self, ".arp"))); } else { return undef if (!defined (A3Com::Base::_load ($self, ".arp"))); } $self->{"_LOADED"} = 1; } } # # load an ARP history structure from a file # sub load_history { my $self = shift; my $file = shift; my $a; $self->{"ERROR"} = ""; if (!defined $file) { if (! -d $self->{"GLOBALCACHEDIR"}) { $self->{"ERROR"} = "GLOBALCACHEDIR is not a directory"; return undef; } $file = "$self->{GLOBALCACHEDIR}/global.arphist"; } if (!open (I, $file)) { $self->{"ERROR"} = "$!"; return undef; } my $ip = ""; my $first_mac_in_seq = 0; my $l; while (defined ($l = )) { next if ($l =~ /^\s*#/ || $l =~ /^\s*$/); if ($l =~ /^(\d+\.\d+\.\d+\.\d+)/) { $ip = $1; $first_mac_in_seq = 1; next; } if ($ip eq "") { # error next; } my ($t, $m) = ($l =~ / (\d+) \s+ (([0-9a-f]{1,2}:){5}[0-9a-f]{1,2}) \s* $ /ix); if ($t eq "") { # error next; } $m = normalize_mac ($m); # first_noticed if ($first_mac_in_seq) { $a->{$ip}->{"current"} = $m; $a->{$ip}->{"current_time"} = $t; $first_mac_in_seq = 0; } $a->{$ip}->{"history"}->{$t} = $m; } close (I); return $a; } # # save an ARP history structure to a file # sub save_history { my $self = shift; my $h = shift; my $file = shift; $self->{"ERROR"} = ""; if (!defined $file) { if (! -d $self->{"GLOBALCACHEDIR"}) { $self->{"ERROR"} = "GLOBALCACHEDIR is not a directory"; return undef; } $file = "$self->{GLOBALCACHEDIR}/global.arphist"; } if (!open (O, ">$file")) { $self->{"ERROR"} = "$!"; return undef; } my $t = time; my $lt = localtime ($t); print O < $b} keys %{$h->{$ip}->{"history"}}) { print O "\t$t $h->{$ip}->{history}->{$t}\n"; } } close (O); } # # Merge a current ARP table into a historic ARP table # # %a = $self->arp_table; # $c = \%a; # $h = history # $t = time # sub merge_current_with_history { my $self = shift; my $c = shift; my $h = shift; my $t = shift; my $n = $h; foreach my $ip (keys %{$c}) { # # IP address does not already exist in history # if (!defined $h->{$ip}) { $n->{$ip}->{"current"} = $c->{$ip}; # current MAC addr $n->{$ip}->{"current_time"} = $t; $n->{$ip}->{"history"}->{$t} = $c->{$ip}; # current MAC addr next; } # # IP address already exists in history, MAC same # if ($h->{$ip}->{"current"} eq $c->{$ip}) { $n->{$ip}->{"current"} = $c->{$ip}; $n->{$ip}->{"current_time"} = $t; delete $n->{$ip}->{"history"}->{ (reverse sort {$a <=> $b} keys %{$h->{$ip}->{"history"}})[0] }; $n->{$ip}->{"history"}->{$t} = $c->{$ip}; # # IP address already exists in history, MAC differs # } else { $n->{$ip}->{"current"} = $c->{$ip}; $n->{$ip}->{"current_time"} = $t; $n->{$ip}->{"history"}->{$t} = $c->{$ip}; } } return $n; } # # given a MAC addr and a ARP history table, # return the IPs which resolve to that MAC addr # sub lookup_mac_history { my $self = shift; my $h = shift; my $mac = shift; $mac = normalize_mac ($mac); my @ips = (); foreach my $ip (keys %{$h}) { push (@ips, $ip) if ($h->{$ip}->{"current"} eq $mac); } @ips; } # # convert a MAC addr in an arbitrary format to something # which resembles 01:02:03:0a:0b:0c # sub normalize_mac { my $mac = shift; my @a = split (/[-:]/, $mac, 6); for (@a) { s/^.$/0$&/; } join (":", @a); } =head1 NAME A3Com::ARP - Methods for collecting ARP tables from 3Com or other network gear which provides such information =head1 SYNOPSIS use A3Com::ARP; my $at = new A3Com::ARP ( "SWITCH" => "name", "READ_COMMUNITY" => "public", "CACHE" => 0, ); my %atable = $at->arp_table; if ($at->error ne "") { die "error: " . $at->error . "\n"; } foreachy my $ip (keys %atable) { print "$ip\n"; foreach my $mac (@{$atable{$ip}}) { print " $mac\n"; } } =head1 DESCRIPTION This code used to support only 3com gear, but it has been extended to provide functionality for devices which support standards-based SNMP agents. The methods provided by this module operate on ARP tables from network devices. The tables may be extracted from the devices via SNMP and the IP-MIB's ipNetToMediaTable, or by telnetting to the device and reading the tables from the interactive interface. The telnet method is supported for 3com CoreBuilder 3500 (software <2.0) because the agent responds to queries too slowly via SNMP, yet yields acceptable performance via the interactive interface. CoreBuilder Software 2.* does not have such performance problems. The type of device and firmware is detected by first sampling the value of sysObjectID.0, then determining the vendor and further way of obtaining the firmware version. The default is to get the ARP table via SNMP and ipNetToMediaTable. A global and local cacheing mechanism is supported so that each read of the arp table does not need to talk to a host. The idea is to have a set of tools which run periodically (via cron or some other mechanism) which keep the caches populated and fresh, while other tools use the caches. =head1 METHODS =over 4 =item new my $bt = new A3Com::ARP ( "SWITCH" => "switch hostname or ip address", "LOGIN" => "login name (used for telnet interface only)", "PASSWORD" => "password (used for telnet interface only)", "READ_COMMUNITY" => "snmp read community", "WRITE_COMMUNITY" => "snmp write community", "GLOBALCACHEDIR" => "path to global cache", "CACHEDIR" => "path to personal cache", "LOG" => 0 or 1, "CACHE" => 0 or 1, "GLOBALCACHE" => 0 or 1, ); The default for GLOBALCACHEDIR is "/usr/local/share/a3com". This may be overridden by the environment variable "A3COM_GLOBALCACHEDIR" or by assigning a value to GLOBALCACHEDIR when the object is created. CACHEDIR defaults to $HOME/.a3com if HOME is defined, or to the environment variable A3COM_CACHEDIR if it is defined, otherwise it is ".a3com" in the working directory. When an object is created, global configuration file is read. This file is the A3COM_CONF environment variable, /etc/a3com.conf, or /usr/local/etc/a3com.conf, in that order. The format of this file is "name = value". Blank lines and lines beginning with # are skipped. These assignments are made in the same way as passing the arguments to the new method. For example, the config file may look like this: LOGIN=read READ_COMMUNITY=public GLOBALCACHEDIR=/var/lib/a3comcache =item cachedir ($path) Sets cache dir if $path is given, otherwise returns it. =item globalcachedir ($pat) Sets global cache dir if $path is given, otherwise returns it. =item cachetime ($secs) Sets cache time if $path is given, otherwise returns it. =item cache (0 or 1) Enables or disables local cacheing. =item globalcache (0 or 1) Enables or disables global cacheing. =item switch Sets switch name if $path is given, otherwise returns it. =item error Returns the error string of the last method call, or "" if there was no error. =item log (0 or 1) Sets or unsets logging. =item ip ($macaddr) Returns a list of IP addresses which resolve to MAC address $macaddr, or undef on error. =item mac ($ip) Returns the MAC address which resolves to IP address $ip, or undef on error. =item arp_table Returns the ARP table as a hash, indexed by IP address. The structure resembles this: %at = ( "10.1.1.1" => "00:11:22:33:44:55", "10.2.2.2" => "0a:0b:0c:0d:0e:0f", ); =item ip_addrs Returns list of IP addresses in the ARP table. =item mac_addrs Returns list of MAC addresses in the ARP table. =item file_read ($file) Reads an ARP table from $file and stores it in the object. If the filename is not supplied, it attempts to read a file named "$SWITCH.arp" in the $CACHEDIR directory. Returns undef on error. =item file_write ($file) Write a ARP table to a file. If the filename is not supplied, then it attempts to write a file named "$SWITCH.arp" in the $CACHEDIR directory. Returns undef on error. =item load_history ($filename) Loads ARP table history from $filename and returns a ref to the hash on success, or undef on error. The hash looks like this: { "10.1.1.1" => { "history" => { "time1" => "01:02:03:0a:0b:0c", "time2" => "0a:00:80:00:01:02", ... }, "current" => "0a:00:80:00:01:02", "current_time" => time, }, ... } =item save_history ($histref, $filename) Saves ARP table history $histref to $filename and return undef on error. Writes a file which looks like this: 10.1.1.1 time mac time mac ... =item merge_current_with_history ($current, $history, $time) =item lookup_mac_history ($history, $mac) Given a MAC addr and a ARP history table, return the list of IPs which resolve to that MAC addr. =item normalize_mac ($mac) Convert a MAC addr in an arbitrary format to something which resembles "01:02:03:0a:0b:0c". =back =head1 SEE ALSO =head1 HISTORY =cut