use Net::Netmask;
use Data::Dumper;
use Getopt::Long;
+use List::Util;
+
+no warnings 'uninitialized';
sub usage {
- die "Usage: $0 [--debug] [--mac] <ip-addr>\n";
+ die <<AMEN ;
+Usage: $0 [<options>] <switch>
+
+Options:
+--debug Show debugging outputs
+--mac Show one MAC address learned per port
+--allmac Show all MAC addresses learned per port
+--force-v1 Use SNMPv1 (work-around for buggy switches)
+AMEN
}
my $debug = 0;
my $mac = 0;
+my $all_mac = 0;
+my $force_v1 = 0;
GetOptions(
'debug' => \$debug,
'mac' => \$mac,
+ 'allmac' => \$all_mac,
+ 'force-v1' => \$force_v1,
) or usage;
@ARGV == 1 or usage;
my ($switch_ip) = @ARGV;
my $community = 'public';
+$mac ||= $all_mac;
my $is_tty = -t STDOUT;
sub attr {
my $t_red = attr("setaf 1");
my $t_green = attr("setaf 2");
my $t_yellow = attr("setaf 3");
+my $t_magenta = attr("setaf 5");
my $t_norm = attr("sgr0");
my ($snmp, $err) = Net::SNMP->session(
-hostname => $switch_ip,
- -version => '2c',
+ -version => ($force_v1 ? '1' : '2c'),
-community => $community,
);
$snmp or die "Cannot establish session: $err\n";
'name' => "$OID_basic.5",
'location' => "$OID_basic.6",
});
-print Dumper($basics) if $debug;
+print "# Basics:\n", Dumper($basics) if $debug;
my $bas = $basics->{0} or die "Cannot find basic info";
+for (values %$bas) {
+ s{\r}{}gs;
+ s{\n}{ | }gs;
+}
print "### Basics ###\n\n";
print "Device: ", $bas->{desc}, "\n";
my $if_table = my_get_table({
'desc' => "$OID_ifTable.1.2",
- 'name' => "$OID_ifTablev2.1.1",
- 'alias' => "$OID_ifTablev2.1.18",
'speed' => "$OID_ifTable.1.5",
- 'hispeed' => "$OID_ifTablev2.1.15",
+ 'mac' => "$OID_ifTable.1.6",
'admin' => "$OID_ifTable.1.7",
'oper' => "$OID_ifTable.1.8",
+ 'name' => "$OID_ifTablev2.1.1",
+ 'hispeed' => "$OID_ifTablev2.1.15",
+ 'alias' => "$OID_ifTablev2.1.18",
});
-print Dumper($if_table) if $debug;
+my %if_macs = ();
+for my $if (values %$if_table) {
+ $if->{mac} = join(':', map { sprintf "%02x", ord $_ } split(//, $if->{mac}));
+ $if_macs{$if->{mac}} = 1;
+ if ($if->{hispeed} == $if->{speed}) {
+ # Some buggy switches put hispeed == speed
+ delete $if->{hispeed};
+ }
+}
+print "# Interface table:\n", Dumper($if_table) if $debug;
my @ifaces = sort { $a <=> $b } keys %$if_table;
+print "MAC addr: ", join(" ", sort keys %if_macs), "\n";
my $OID_ipTable = '1.3.6.1.2.1.4.20';
my $ip_table = my_get_table({
'iface' => "$OID_ipTable.1.2",
'mask' => "$OID_ipTable.1.3",
});
-print Dumper($ip_table) if $debug;
+print "# IP table:\n", Dumper($ip_table) if $debug;
# XXX: IPv6 not supported yet
for my $ipa (keys %$ip_table) {
my $nm = Net::Netmask->new2($ipa, $ip->{mask}) or die "Cannot parse IP prefix";
push @{$if->{ip_addrs}}, $ipa . '/' . $nm->bits;
}
-print Dumper($if_table) if $debug;
+
+# Switch ports
+my $OID_1dBasePortTable = '1.3.6.1.2.1.17.1.4';
+my $port_table = my_get_table({
+ 'ifindex' => "$OID_1dBasePortTable.1.2",
+});
+print "# Switch ports:\n", Dumper($port_table) if $debug;
+
+for my $port (keys %$port_table) {
+ my $if = $if_table->{$port_table->{$port}->{ifindex}} or die "Port table refers to unknown iface";
+ $if->{switch_port} = $port;
+}
+
+print "# Extended interface table:\n", Dumper($if_table) if $debug;
if ($mac) {
my $OID_1qTpFdbTable = '1.3.6.1.2.1.17.7.1.2.2';
- my $OID_1qPort = "$OID_1qTpFdbTable.1.2";
- my $OID_1qStatus = "$OID_1qTpFdbTable.1.3";
my $vlan_fdb_table = my_get_table({
- 'port' => $OID_1qPort,
- 'status' => $OID_1qStatus,
+ 'port' => "$OID_1qTpFdbTable.1.2",
+ 'status' => "$OID_1qTpFdbTable.1.3",
});
- print Dumper($vlan_fdb_table) if $debug;
-
- for my $m (keys %$vlan_fdb_table) {
- my $fdb = $vlan_fdb_table->{$m};
- my $port = $if_table->{$fdb->{port}} or die "Forwarding DB refers to unknown iface";
- my @m = split /\./, $m;
- my $vlan = shift @m;
- my $mac = join(':', map { sprintf('%02x', $_) } @m);
- push @{$port->{macs}}, $mac;
+ print "# VLAN FDB:\n", Dumper($vlan_fdb_table) if $debug;
+
+ if (%$vlan_fdb_table) {
+ for my $m (keys %$vlan_fdb_table) {
+ my $fdb = $vlan_fdb_table->{$m};
+ $fdb->{status} == 3 or next; # Only learned MACs
+ my $port = $port_table->{$fdb->{port}} or die "Forwarding DB refers to unknown port";
+ my $if = $if_table->{$port->{ifindex}} or die "Forwarding DB refers to unknown iface";
+ my @m = split /\./, $m;
+ my $vlan = shift @m;
+ my $mac = join(':', map { sprintf('%02x', $_) } @m);
+ push @{$if->{macs}}, $mac;
+ }
+ } else {
+ print "# Trying fall-fack to .1d FDB\n" if $debug;
+ my $OID_1dTpFdbTable = '1.3.6.1.2.1.17.4.3';
+ my $fdb_table = my_get_table({
+ 'port' => "$OID_1dTpFdbTable.1.2",
+ 'status' => "$OID_1dTpFdbTable.1.3",
+ });
+ print "# Non-VLAN FDB:\n", Dumper($fdb_table) if $debug;
+ for my $m (keys %$fdb_table) {
+ my $fdb = $fdb_table->{$m};
+ $fdb->{status} == 3 or next; # Only learned MACs
+ my $port = $port_table->{$fdb->{port}} or die "Forwarding DB refers to unknown port";
+ my $if = $if_table->{$port->{ifindex}} or die "Forwarding DB refers to unknown iface";
+ my @m = split /\./, $m;
+ my $mac = join(':', map { sprintf('%02x', $_) } @m);
+ push @{$if->{macs}}, $mac;
+ }
+ }
+
+ for my $if (values %$if_table) {
+ $if->{macs} or next;
+ $if->{macs} = [ sort(List::Util::uniq(@{$if->{macs}})) ];
}
}
$vlan->{$k} = [ split //, unpack("B*", $vlan->{$k} // "") ];
}
}
-print Dumper($vlan_table) if $debug;
+print "# VLAN table\n", Dumper($vlan_table) if $debug;
my @vlans = sort { $a <=> $b } grep { $vlan_table->{$_}->{'row-status'} == 1 } keys %$vlan_table;
print "\n### VLANs ###\n\n";
}
print "\n### Ports ###\n\n";
-# XXX: We assume that 802.1d switch ports IDs are equal to interface IDs
-for my $port (@ifaces) {
- my $if = $if_table->{$port};
+for my $ifindex (@ifaces) {
+ my $if = $if_table->{$ifindex};
+ my $port = $if->{switch_port};
my $state;
my $scolor = $t_norm;
if ($if->{'admin'} != 1) {
$speed = int($speed/1000);
$speed = "${speed}G";
}
- printf "%-4d %-15.15s %s%-4s %-5s %s%-20s%s", $port, $if->{name}, $scolor, $state, $speed, $t_yellow, $if->{alias}, $t_norm;
+ printf "%-4d %-15.15s %s%-4s %-5s %s%-25.25s%s", $ifindex, $if->{name}, $scolor, $state, $speed, $t_yellow, $if->{alias}, $t_norm;
if ($mac) {
- my $macs = $if->{macs};
my $show_mac = "";
my $more_macs = " ";
- if ($macs && @$macs) {
- $show_mac = $macs->[0];
- $more_macs = "${t_yellow}+${t_norm}" if @$macs > 1;
+ my @macs = @{$if->{macs} // []};
+ if (@macs) {
+ $show_mac = $macs[0];
+ $more_macs = "${t_yellow}+${t_norm}" if @macs > 1;
}
- printf "%-17s%s ", $show_mac, $more_macs;
+ printf " %-17s%s ", $show_mac, $more_macs;
}
for my $vid (@vlans) {
if ($if->{ip_addrs}) {
print "${t_yellow} IP: ", join(" ", @{$if->{ip_addrs}}), "${t_norm}\n";
}
+ if ($all_mac) {
+ if ($if->{mac}) {
+ print "${t_yellow} Local MAC: ", $if->{mac}, "${t_norm}\n";
+ }
+ if ($if->{macs}) {
+ for my $m (@{$if->{macs}}) {
+ print "${t_magenta} MAC: $m${t_norm}\n";
+ }
+ }
+ }
}