#!/usr/bin/perl
#
# "mon" monitor to detect dropped tunnels
# on a Xedia (Lucent) AccessPoint gateway
#
# arguments are "[-c community] [-h] [-t] host [host...]"
# 
# -c community   SNMP community
# -r             display PTR records for RemoteAddr
# -t             display 
#
# Jim Trocki
#
# $Id: xedia-ipsec-tunnel.monitor,v 1.1.1.1 2004/06/09 05:18:05 trockij Exp $
#
#    Copyright (C) 2000, 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
#
use SNMP;
use Getopt::Std;
use Socket;
use strict;

sub get_status;
sub get_table;
sub get_vars;
sub fancy_psu_table;
sub bitmask_len;
sub rev_lookup;

$ENV{"MIBS"} = "RFC1213-MIB:XEDIA-IPSEC-MIB";

my %opt;
getopts ('c:rt', \%opt);

my $COMM = $opt{"c"} || "public";

my @failures = ();
my $detail;
my %tunnels;
my @HOSTS;
my @TUNNELS;
my %RESOLVE_CACHE;

@HOSTS = @ARGV;

if ($opt{"t"})
{
    @TUNNELS = @ARGV;
}

foreach my $host (@HOSTS)
{
    my %status = get_status ($host, $COMM);

    if ($status{"error"} ne "")
    {
    	push (@failures, $host);
	$detail .= "could not retrieve status from $host: $status{error}\n\n";
	next;
    }

    elsif ($status{"failure"})
    {
	if ($opt{"t"})
	{
	    foreach my $addr (@{$status{"failed_tunnels"}})
	    {
		if ($opt{"r"})
		{
		    my $r = rev_lookup ($addr->[0]);

		    push (@failures, $r eq "unknown" ? "$addr->[0]/$addr->[1]" : $r)
		}

		else
		{
		    push (@failures, "$addr->[0]/$addr->[1]");
		}
	    }
	}

	else
	{
	    push (@failures, $host);
	}

	$detail .= $status{"failure_summary"};
    }

    $tunnels{$host} = $status{"tunnel_table"};
}


#
# output returned to mon
#
if (@failures != 0)
{
    print join (" ", @failures), "\n";
    print "$detail\n";
}

else
{
    print "no problems\n";
}

print <<EOF;
Tunnel Table
-------------------------------------------------------------------------------
Host                 AS   OS           curSAs RemGW           RemoteAddr
EOF

if ($opt{"r"})
{
    print <<EOF;
                                                              Hostname  
EOF
}

print <<EOF;
-------------------------------------------------------------------------------
EOF

foreach my $host (keys %tunnels)
{
    foreach my $r (@{$tunnels{$host}})
    {
    	printf ("%-20s %-4s %-12s %-6d %-15s %-15s\n",
		$host,
		$r->{"ipsecTunnelAdminStatus"},
		$r->{"ipsecTunnelOperStatus"},
		$r->{"ipsecTunnelCurSAs"},
		$r->{"ipsecTunnelRemoteGateway"},
		$r->{"ipsecTunnelRemoteAddress"} . "/" .
			bitmask_len ($r->{"ipsecTunnelRemoteAddressMask"}),
	);

	if ($opt{"r"})
	{
	    print " " x 62 . rev_lookup ($r->{"ipsecTunnelRemoteAddress"}) . "\n";
	}
    }

    print "\n";
}

exit 1 if (@failures != 0);

exit 0;



#
# params: (hostname, community)
#
# returns:
# (
#  "error" => "error name, empty string means no error",
# )
#
sub get_status
{
    my ($host, $comm) = @_;

    my $s;
    if (!defined ($s = new SNMP::Session (
    	"DestHost" => $host,
	"Community" => $comm,
	"Version" => 2,
	"UseEnums" => 1,
	)))
    {
    	return ("error" => "cannot create session");
    }

    my $error;
    my $failure_detected = 0;
    my $failure_summary = "";
    my $tunnel_table;
    my $failed_tunnels;

    #
    # is this really a xedia router?
    #
    my $sys_oid = $s->get (["sysObjectID", 0]);

    return ("error" => $s->{"ErrorStr"}) if ($s->{"ErrorStr"} ne "");

    return ("error" => "not Xedia AP")
    	if ($sys_oid !~ /^\.1\.3\.6\.1\.4\.1\.838\.5.(1|450|1000)/);

    #
    # tunnel table
    #
    ($error, $tunnel_table) = get_table ($s,
	["ipsecTunnelType"],
    	["ipsecTunnelAdminStatus"],
	["ipsecTunnelOperStatus"],
	["ipsecTunnelCurSAs"],
	["ipsecTunnelRemoteGateway"],
	["ipsecTunnelRemoteAddress"],
	["ipsecTunnelRemoteAddressMask"],
    );

    return ("error" => $error) if ($error ne "");

    my $pruned_tunnel_table;
    foreach my $r (@{$tunnel_table})
    {
	next if ($r->{"ipsecTunnelType"} ne "siteToSiteDynamic");

	push (@{$pruned_tunnel_table}, $r);

    	next if ($r->{"ipsecTunnelAdminStatus"} eq "up" &&
		$r->{"ipsecTunnelOperStatus"} eq "up");
	
	next if ($r->{"ipsecTunnelAdminStatus"} eq "down");

	push @{$failed_tunnels}, [$r->{"ipsecTunnelRemoteAddress"},
		bitmask_len ($r->{"ipsecTunnelRemoteAddressMask"})];

	$failure_summary .= "$host tunnel failure for $r->{ipsecTunnelRemoteAddress}/" .
		bitmask_len ($r->{"ipsecTunnelRemoteAddressMask"}) .
		" gw $r->{ipsecTunnelRemoteGateway}\n";
	$failure_detected++;
    }

    (
    	"error"			=> "",
	"failure"		=> $failure_detected,
	"failure_summary"	=> $failure_summary,
	"failed_tunnels"	=> $failed_tunnels,
	"tunnel_table"		=> $pruned_tunnel_table,
    );
}


sub get_table
{
    my ($s, @tbl) = @_;

    my $table = [];
    my $tblid = $tbl[0]->[0];
    my $i = 0;

    my $row = new SNMP::VarList (@tbl);

    return ("MIB problem") if (!defined $row);

    while (defined ($s->getnext ($row)))
    {
    	last if ($s->{"ErrorStr"} ne "");

	my $r = $row->[0]->[0];

 	last if ($r ne $tblid);

	foreach my $col (@{$row})
	{
	    $table->[$i]->{"iid"} = $col->[1];
	    $table->[$i]->{$col->[0]} = $col->[2];
	}

	$i++;
    }

    return ($s->{"ErrorStr"}) if ($s->{"ErrorStr"} ne "");

    (
    	"",
	$table,
    );
}


sub get_vars
{
    my ($s, @vars) = @_;

    my $r = new SNMP::VarList ( @vars );

    return ("MIB problem") if (!defined $r);

    return ($s->ErrorStr) if (!defined ($s->get ($r)));

    my $v;

    foreach my $element (@{$r})
    {
    	$v->{$element->[0]} = $element->[2];
    }

    ("", $v);
}


sub bitmask_len
{
    my $bitmask = shift;

    my $len = 0;
    foreach my $octet (split /\./, $bitmask)
    {
    	for (my $i = 0; $i < 8; $i++)
	{
	    $len += $octet & 1;
	    $octet >>= 1;
	}
    }

    $len;
}


sub rev_lookup
{
    my $addr = shift;

    return $RESOLVE_CACHE{$addr} if ($RESOLVE_CACHE{$addr});

    my $h = gethostbyaddr (inet_aton ($addr), AF_INET);

    if (!defined $h)
    {
	$h = "unknown";
    }

    else
    {
	$h =~ s/\..*$//;
    }

    $RESOLVE_CACHE{$addr} = $h;
    $h;
}

