#!/usr/bin/perl
=head1 NAME
asa-vpn-sessions
=head1 SYNOPSIS
asa-vpn-sessions [-d] [-h] [-l] [-u user] [-i ip-addr]
=cut
use strict;
use warnings;
#################### ATTENTION #########################
# cfg data, must be changed
########################################################
#
our $ASA = 'my.asa.com';
our $OPER = 'operator';
our $PASS = '********';
#
########################################################
our $VERSION = 0.01;
use Pod::Usage qw(pod2usage);
use Getopt::Std qw(getopts);
use LWP::UserAgent qw();
our $IP_SEC_CLIENT_URL = "https://$OPER:$PASS\@$ASA/"
. "admin/exec/show%20vpn-sessiondb%20detail%20full%20remote";
our $IP_SSL_CLIENT_URL = "https://$OPER:$PASS\@$ASA/"
. "admin/exec/show%20vpn-sessiondb%20detail%20full%20svc";
our $DEBUG = 0;
our %opts;
pod2usage( -exitval => 1, -verbose => 1 )
unless getopts( 'dhlu:i:', \%opts );
pod2usage(
-exitval => 1,
-verbose => 1,
-message => 'options u and i are exclusive'
) if $opts{'u'} && $opts{'i'};
pod2usage( -exitval => 0, -verbose => 2 )
if $opts{'h'};
$DEBUG = 1 if $opts{'d'};
# fetch the vpn client infos unparsed from the ASA
our @ip_sec_clients = fetch_url($IP_SEC_CLIENT_URL);
our @ip_ssl_clients = fetch_url($IP_SSL_CLIENT_URL);
if ($DEBUG) {
warn join( "\n", @ip_sec_clients ), "\n\n";
warn join( "\n", @ip_ssl_clients ), "\n\n";
}
our $stash;
our $users;
our $addrs;
parse_all( @ip_sec_clients, @ip_ssl_clients );
if ( $opts{'i'} or $opts{'u'} ) {
list_session();
}
else {
list_all_sessions();
}
exit 0;
############################################################
# end of main
############################################################
sub fetch_url {
my $url = shift or die "Internal error, missing arg,";
my $ua = LWP::UserAgent->new( timeout => 5, requests_redirectable => [] )
or die "Can't create LWP UA object,";
my $response = $ua->get($url);
unless ( $response->is_success ) {
die "Error fetching URL from ASA: ", $response->status_line, "\n";
}
return split /\n/, $response->content;
}
sub parse_all {
my @lines = @_;
foreach my $line (@lines) {
# skip lines without usefull info
next if $line =~ m/^\s*$/;
next if $line =~ m/^\s*INFO:/;
next if $line =~ m/^\s*Type: NAC/;
next if $line =~ m/^\s*Session Type:/;
next if $line =~ m/^\s*IKE Tunnels:/;
next if $line =~ m/^\s*Clientless Tunnels:/;
parse_session($line) && next if $line =~ m/^\s*Session ID:/;
parse_ike($line) && next if $line =~ m/^\s*Type: IKE/;
parse_ipsecnatt($line) && next if $line =~ m/^\s*Type: IPsecOverNatT/;
parse_ipsectcp($line) && next if $line =~ m/^\s*Type: IPsecOverTCP/;
parse_ipsec($line) && next if $line =~ m/^\s*Type: IPsec/;
parse_webvpn($line) && next if $line =~ m/^\s*Type: Clientless/;
parse_ssl($line) && next if $line =~ m/^\s*Type: SSL-Tunnel/;
parse_dtls($line) && next if $line =~ m/^\s*Type: DTLS-Tunnel/;
warn "Can't parse this line:\n";
warn $line;
}
}
sub parse_session {
my $line = shift;
my $session_stash = split_line($line);
my $session_id = $session_stash->{'session id'}
or die "Can't parse unique Session ID!\n";
my $user = $session_stash->{'username'}
or die "Can't parse Username!\n";
my $ip = $session_stash->{'ip addr'}
or die "Can't parse IP Addr!\n";
$stash->{$session_id}{'session'} = $session_stash;
# cache user->sessid und ip->session for easy access
$users->{$user} = $session_id;
$addrs->{$ip} = $session_id;
return 1;
}
sub parse_ike {
my $line = shift;
my $ike_stash = split_line($line);
my $session_id = $ike_stash->{'tunnel id'}
or die "Can't parse unique Session ID!\n";
# cut off the sub-session id from session-id
$session_id =~ s/\.\d+//;
$stash->{$session_id}{'ike'} = $ike_stash;
return 1;
}
sub parse_ipsec {
my $line = shift;
my $ipsec_stash = split_line($line);
my $session_id = $ipsec_stash->{'tunnel id'}
or die "Can't parse unique Session ID!\n";
# cut off the sub-session id from session-id
$session_id =~ s/\.\d+//;
$stash->{$session_id}{'ipsec'} = $ipsec_stash;
return 1;
}
sub parse_ipsecnatt {
my $line = shift;
my $ipsecnatt_stash = split_line($line);
my $session_id = $ipsecnatt_stash->{'tunnel id'}
or die "Can't parse unique Session ID!\n";
# cut off the sub-session id from session-id
$session_id =~ s/\.\d+//;
$stash->{$session_id}{'ipsecnatt'} = $ipsecnatt_stash;
return 1;
}
sub parse_ipsectcp {
my $line = shift;
my $ipsectcp_stash = split_line($line);
my $session_id = $ipsectcp_stash->{'tunnel id'}
or die "Can't parse unique Session ID!\n";
# cut off the sub-session id from session-id
$session_id =~ s/\.\d+//;
$stash->{$session_id}{'ipsectcp'} = $ipsectcp_stash;
return 1;
}
sub parse_webvpn {
my $line = shift;
my $webvpn_stash = split_line($line);
my $session_id = $webvpn_stash->{'tunnel id'}
or die "Can't parse unique Session ID!\n";
# cut off the sub-session id from session-id
$session_id =~ s/\.\d+//;
$stash->{$session_id}{'webvpn'} = $webvpn_stash;
return 1;
}
sub parse_ssl {
my $line = shift;
my $ssl_stash = split_line($line);
my $session_id = $ssl_stash->{'tunnel id'}
or die "Can't parse unique Session ID!\n";
# cut off the sub-session id from session-id
$session_id =~ s/\.\d+//;
$stash->{$session_id}{'ssl'} = $ssl_stash;
return 1;
}
sub parse_dtls {
my $line = shift;
my $dtls_stash = split_line($line);
my $session_id = $dtls_stash->{'tunnel id'}
or die "Can't parse unique Session ID!\n";
# cut off the sub-session id from session-id
$session_id =~ s/\.\d+//;
$stash->{$session_id}{'dtls'} = $dtls_stash;
return 1;
}
sub split_line {
my $line = shift;
# fix bloody Cisco errors, missing the ':' between key and val
$line =~ s/Assigned IP /Assigned IP: /;
$line =~ s/UDP Source Port /UDP Source Port: /;
$line =~ s/UDP Destination Port /UDP Destination Port: /;
$line =~ s/TCP Src Port /TCP Src Port: /;
$line =~ s/TCP Dst Port /TCP Dst Port: /;
# remove trailing '||'
$line =~ s/\|\| \s* $//x;
# split the records into columns at the '|'
my @columns = split /\|\s+/, $line;
my $column_stash = {};
# split the columns into (k,v) tuples at the ':'
foreach my $column (@columns) {
my ( $key, $val ) = split /:\s+/, $column;
next unless defined $key;
warn "split error for key: '$key'\n" unless defined $val;
# trim
$key =~ s/^\s*|\s*$//g;
$val =~ s/^\s*|\s*$//g;
# normalize
$key = lc $key;
$column_stash->{$key} = $val;
}
return $column_stash;
}
sub list_all_sessions {
foreach my $session ( sort keys %$stash ) {
print_session($session);
}
}
sub list_session {
my $lookup_user = $opts{'u'};
my $lookup_ip = $opts{'i'};
if ($lookup_ip) {
foreach my $ip ( sort keys %$addrs ) {
print_session( $addrs->{$ip} ) if $ip =~ m/^\Q$lookup_ip\E$/i;
}
}
elsif ($lookup_user) {
foreach my $user ( sort keys %$users ) {
print_session( $users->{$user} ) if $user =~ m/\Q$lookup_user\E/i;
}
}
}
sub print_session {
my $id = shift;
printf "%-14s <- %-15s %-14s %s\n",
$stash->{$id}{'session'}{'ip addr'},
$stash->{$id}{'session'}{'public ip'},
$stash->{$id}{'ipsec'}{'type'}
|| $stash->{$id}{'ipsecnatt'}{'type'}
|| $stash->{$id}{'ipsectcp'}{'type'}
|| $stash->{$id}{'dtls'}{'type'}
|| $stash->{$id}{'ssl'}{'type'}
|| $stash->{$id}{'webvpn'}{'type'},
$stash->{$id}{'session'}{'username'};
if ( $opts{'l'} ) {
printf " SESSION: %-16s %-21s Bytes rx/tx: %d/%d\n",
$stash->{$id}{'session'}{'group'},
$stash->{$id}{'session'}{'duration'},
$stash->{$id}{'session'}{'bytes rx'},
$stash->{$id}{'session'}{'bytes tx'},
;
printf " IPSEC: %-16s %-21s %s\n", $stash->{$id}{'ipsec'}{'type'}
|| $stash->{$id}{'ipsecnatt'}{'type'}
|| $stash->{$id}{'ipsectcp'}{'type'},
$stash->{$id}{'ike'}{'client os type'},
$stash->{$id}{'ike'}{'client os ver'},
if $stash->{$id}{'ike'};
printf " DTLS: %-16s %-21s %s\n",
$stash->{$id}{'dtls'}{'encapsulation'},
$stash->{$id}{'dtls'}{'client type'},
$stash->{$id}{'dtls'}{'client ver'},
if $stash->{$id}{'dtls'};
print "\n";
}
}
=head1 OPTIONS
-h help
-d debug
-l long listing
-i ip-addr client ip address
-u username substring allowed
=head1 README
A script to list the Cisco-ASA vpn-sessions
At time of writing (8/2010), the ASA has a faulty SNMP implementation for the CISCO-REMOTE-ACCESS-MONITOR-MIB. This script fetches the session tables via https:
https://oper:
[email protected]/admin/exec/show%20vpn-sessiondb ...
=head1 PREREQUISITES
This script requires the C<LWP::UserAgent> module with SSL support.
=head1 SCRIPT CATEGORIES
Networking
=head1 SECURITY
This script doesn't verify the ASAs SSL certificate. Add the verification or use it with a low-privileged operator account only.
The following configuration enables a low-privileged operator account for http access:
=over
=item * add a low-privileged LOCAL operator account
asa(config)# username operator password ******** privilege 1
=item * enable LOCAL http authentication
asa(config)# aaa authentication http console LOCAL
=item * enable LOCAL command authorization
asa(config)# aaa authorization command LOCAL
=item * reduce the needed privilege for show vpn-sessiondb
asa(config)# privilege show level 0 mode exec command vpn-sessiondb
=item * enable the http server for ASDM management
asa(config)# http server enable
=item * allow your monitoring hosts
asa(config)# http your.ip.address your.netmask your-mgmt-if
=back
=head1 AUTHOR
Karl Gaissmaier, gaissmai (at) cpan.org
=head1 COPYRIGHT
Copyright (C) 8/2010 by Karl Gaissmaier
This program is free software; you can redistribute it and/or modify it
under the same terms as Perl itself.
=cut
# vim: sw=2