#!/usr/bin/perl -w
use strict;
use warnings;
use Pod::Usage;
use Getopt::Long;
use POSIX qw( EXIT_SUCCESS EXIT_FAILURE );

# ------------------------------------------------------------------------
# This next POD section is used by pod2usage to generate --help output,
# and is used by the makefile to generate a man page.
#
=head1 NAME

ipsec_policy - show ipsec policy information

=head1 SYNOPSIS

    # detect what stack is used
    ipsec policy --detect-stack

    # display policy information
    ipsec policy [ --all | [ --inbound | --outbound | --forward ] ] \
                 [ --stack=name ] [ --read=file ] [ --debug ]

    # provide usage information
    ipsec policy --usage
    ipsec policy --help

=head1 DESCRIPTION

I<policy> displays the incoming, outgoing, and forwarding packet policies of
the system.  It is a wrapper around eixsting klips and netkey data, but
presented in a less terse form.

=head1 OPTIONS

=over 4

=item --detect-stack

Only display the stack that Openswan is using.  Possible results are.

=over 4

=item klips

KLIPS is the Openswan ipsec kernel module.  This stack type indicates that
KLIPS is not running in I<mast> mode (see next option), but rather in the 
default mode.  In this mode, KLIPS outgoing packet policy is dicated by
I<eroutes>.  See the B<ipsec_eroute> man page for further details.

=item mast

This is a mode of the Openswan ipsec kernel module, KLIPS.  In this mode
outgoing packet routing policies are dictated by iptalbles, and Linux kernel
policy routing.  This mode is selected by using C<protostack=mast> setting in
I<ipsec.conf>.

=item netkey

This stack indicates that Openswan is controlling the Linux kernel built-in
ipsec functionally.

=back


=item --all

Show inbound, outbound, and forward policites.  This is the default.

=item --inbound --in

Show only inbound policy.

=item --outbound --out

Show only outbound policy.

=item --forward --fwd

Show only forward policy.

=item --stack=<name>

Skip autodetection and force read policy from this stack.  See help on
B<--detect-stack> (above) for valid options and their descriptions.

=item --read=<file>

This option overrides what file would be read to gather the policy
information.  It could be used to read policy information from a snapshot
obtained from a running system.

In the case of the klips or mast stack, this file is the output of the
B</proc/net/ipsec/spi/all> file.

=item --help

Output help.

=item --debug

Output debug info.

=back

=head1 FILES

   /proc/net/ipsec/spi/all

=head1 SEE ALSO

B<ipsec>(8), B<ipsec_eroute>(8), B<ipsec_manual>(8)

=head1 HISTORY

Designed for the Openswan project <http://www.openswan.org> by Bart
Trojanowski.

=head1 BUGS

Does not support netkey yet.

=cut

# ------------------------------------------------------------------------
# general helper functions

my $debug = 0;
sub dbg
{
        my $txt = "@_";
        chomp $txt;
        warn "# $txt\n" if $debug;
}

sub assert_root
{
        my $txt = join(' ', @_);
        dbg "your uid is $>";
        if ($> ne 0) {
                die "You need to be root"
                    . (length($txt)==0 ? "." : " $txt")
                    . "\n";
        }
}

# ------------------------------------------------------------------------
# detect stack and print sadb
package Stack;
use vars qw($debug);
use subs qw(dbg assert_root);
*dbg = \&::main::dbg;
*assert_root = \&::main::assert_root;

sub detect_ipsec_stack
{
        my $class = shift;

        assert_root "to detect the ipsec stack; try --stack= and --read=.";

        my $os_mode = Stack::Klips->detect_openswan_stack_mode();
        return $os_mode if (defined $os_mode);

        die "We don't support netkey yet.\n";
}

sub new
{
        my $class = shift;
        my (%args) = @_;

        my $stack_name = $args{name};
        $stack_name = $class->detect_ipsec_stack() if not defined $stack_name;
        dbg "stack name is $stack_name";

        my $subclass = 'Stack::' . ucfirst($stack_name);
        dbg "subclass is $subclass";

        my $instance = eval { $subclass->new(%args) };
        if($@) { die "Unknown stack: $stack_name\nERROR: $@\n" };

        $instance->initialize_spdb();

        return $instance;
}

sub initialize_spdb
{
        my $this = shift;

        $this->{spdb} = {};
        $this->{spnames} = {
                'in'  => [],
                'out' => [],
                'fwd' => [],
        };
}

sub add_spdb_entry
{
        my $this = shift;
        my ($entry) = @_;

        my $name = $entry->{name};
        ${$this->{spdb}}{$name} = $entry;

        my $dir = $entry->{dir};
        if ( $dir ne 'in' and $dir ne 'out' ) {
                die "line $. has neither dir=in nor dir=out set\n";
        }

        push @{${$this->{spnames}}{$dir}}, $name;
}

sub print_spi
{
        my $this = shift;
        my ($prefix,$s) = (@_);

        return unless ($s->{'xform'} eq 'IPIP');
        return unless (defined $s->{'policy'});

        my @p = split("->", $s->{'policy'});

        printf "%-3s  %-10d   %-18s -> %-18s     %s     ref:%d him:%d\n",
                $prefix,
                $s->{'life_packets_c'} || 0,
                $p[0],
                $p[1],
                $s->{'name'},
                $s->{'ref'},
                $s->{'refhim'};
}

sub print_spdb_dir
{
        my $this = shift;
        my ($prefix, @names) = (@_);

        for my $name (sort @names) {
                my $s = ${$this->{spdb}}{$name};
                $this->print_spi($prefix, $s);
        }
}

sub print_spdb
{
        my $this = shift;

        print STDERR "stack: " . $this->name() . "\n";

        dbg "show: " . join (',',@{$this->{show}});
        for my $what (@{$this->{show}}) {
                $this->print_spdb_dir(uc $what, @{${$this->{spnames}}{$what}});
        }
}




# ------------------------------------------------------------------------
# klips specific code
package Stack::Klips;
our @ISA = qw( Stack );
use vars qw($debug);
use subs qw(dbg assert_root);
*dbg = \&::main::dbg;
*assert_root = \&::main::assert_root;

# some well known files
my $klips_proc_version='/proc/net/ipsec/version';
my $klips_proc_spi_all='/proc/net/ipsec/spi/all';

sub have_openswan
{
        my $class = shift;

        return 0 if system('which ipsec >/dev/null 2>&1');
        return 0 if system('ipsec version >/dev/null 2>&1');
        dbg "detected openswan binaries";
        return 1;
}

sub have_klips_module
{
        my $class = shift;

        return 0 if not -e $klips_proc_spi_all;
        return 0 if not -e $klips_proc_version;
        return 0 if system("cat $klips_proc_version 2>/dev/null | grep -q '^Openswan'");
        dbg "detected openswan module";
        return 1;
}

sub detect_openswan_stack_mode
{
        my $class = shift;

        return () if not have_openswan;
        return () if not have_klips_module;

        return () if not open(IN, 'ipsec auto --status |');
        my $ifline = <IN>;
        close(IN);
        dbg "read: $ifline";

        return () if not $ifline =~ m/using kernel interface: (\w+)$/;
        return () if $1 ne 'klips' and $1 ne 'mast';
        dbg "detected openswan in $1 mode";
        return $1;
}

sub new
{
        my $class = shift;
        my (%args) = @_;
        my %defaults = (
                read => $klips_proc_spi_all
        );
        my $this = { %defaults };
        bless $this, $class;
        # assign args
        while (my ($k,$v) = each(%args)) {
                next if not defined $v;
                dbg "arg $k is $v";
                $this->{$k} = $v 
        }
        return $this;
}

sub name
{
        return "klips"
}

sub read_spdb
{
        my $this = shift;

        my $input = $this->{read};
        dbg "reading from $input";
        assert_root "to read the policy database in $input." if not -r $input;

        open(SPI, "< $input") || die "failed to read $input\n";
        while (<SPI>) {
                m/^(\S+) (\S+): (.*)$/ || die "couldn't parse line $.: $_";
                my ($name, $xform, $rest) = ($1, $2, $3);

                my $tmp = { name  => $name,
                        xform => $xform };

                dbg '-' x 70;
                dbg "$name";
                dbg "$xform";
                dbg "$rest";

                foreach my $word (split /\s+/, $rest) {
                        $word =~ m/^(\S+)=(\S+)$/ || die "couldn't parse part of line $.: $word\n";
                        my ($var, $val) = ($1, $2);

                        dbg "  $var = $val";

                        $tmp->{$var} = $val;

                        if ($var eq 'life(c,s,h)') {
                                # life(c,s,h)=bytes(19240,0,0)addtime(811,0,0)usetime(784,0,0)packets(185,0,0)
                                for my $n (qw{bytes addtime usetime packets}) {
                                        if ($val =~ m/$n\((\d+),(\d+),(\d+)\)/) {
                                                dbg "    life_${n}_c = $1";
                                                dbg "    life_${n}_s = $2";
                                                dbg "    life_${n}_h = $3";

                                                $tmp->{"life_${n}_c"} = $1;
                                                $tmp->{"life_${n}_s"} = $2;
                                                $tmp->{"life_${n}_h"} = $3;
                                        }
                                }
                        }
                }

                $this->add_spdb_entry($tmp);
        }
        close(SPI);
}

# ------------------------------------------------------------------------
# mast specific code
package Stack::Mast;
our @ISA = qw( Stack::Klips );
use vars qw($debug);
use subs qw(dbg assert_root);
*dbg = \&::main::dbg;
*assert_root = \&::main::assert_root;

sub name
{
        return "mast"
}

# everything here is inhered from Klips (for now)

# ------------------------------------------------------------------------
# netkey specific code
package Stack::Netkey;
our @ISA = qw( Stack );
use vars qw($debug);
use subs qw(dbg assert_root);
*dbg = \&::main::dbg;
*assert_root = \&::main::assert_root;

sub new
{
        my $class = shift;
        my (%args) = @_;
        my %defaults = (
                # some defaults can go here
        );
        my $this = { %defaults };
        bless $this, $class;
        # assign args
        while (my ($k,$v) = each(%args)) {
                next if not defined $v;
                dbg "arg $k is $v";
                $this->{$k} = $v 
        }
        return $this;
}

sub name
{
        return "netkey"
}

sub read_spdb
{
        my $this = shift;
        print "Netkey read_spdb\n";
}

# ------------------------------------------------------------------------
# this is the main part of the program
package main;
my $show_all = 0;
my $show_inbound = 0;
my $show_outbound = 0;
my $show_forward = 0;
my $opt_stack;
my $opt_read;

my $rc = GetOptions(
        # detection only mode
        'detect-stack' => sub { print "stack=" . Stack->detect_ipsec_stack() . "\n"; exit 0 },
        # what to show
        'all'       => \$show_all,
        'inbound'   => \$show_inbound,
        'in'        => \$show_inbound,
        'outbound'  => \$show_outbound,
        'out'       => \$show_outbound,
        'forward'   => \$show_forward,
        'fwd'       => \$show_forward,
        # what to read
        'stack=s'   => \$opt_stack,
        'read=s'    => \$opt_read,
        # generic stuff
        'debug'     => \$debug,
        # help
        'usage'     => sub { pod2usage(-exitval => EXIT_SUCCESS, -verbose => 0) },
        'help'      => sub { pod2usage(-exitval => EXIT_SUCCESS, -verbose => 1) },
);
pod2usage('Invalid options specified.') if !$rc;

if ($show_all) {
        # show everything
        $show_inbound = $show_outbound = $show_forward = 1;

} elsif (not ($show_inbound | $show_outbound | $show_forward)) {
        # by default show only outbound policies
        $show_outbound = 1;
}

# collect names of policies to display
my @show = ();
push @show, 'in'  if $show_inbound;
push @show, 'out' if $show_outbound;
push @show, 'fwd' if $show_forward;

# get a new stack object
my $s = Stack->new(
        name => $opt_stack,
        read => $opt_read,
        show => \@show,
);

$s->read_spdb();
$s->print_spdb();

