#!/usr/bin/perl -w

use lib (qw|../lib ../../IPTables-Parse/lib ../../IPTables-Parse.git/lib|);
use strict;

eval {
    require IPTables::ChainMgr;
};
die "[*] Adjust 'use lib' statement to include ",
    "directory where IPTables::Parse lives" if $@;

#==================== config =====================
my $iptables_bin  = '/sbin/iptables';
my $ip6tables_bin = '/sbin/ip6tables';

my $test_table = 'filter';
my $test_chain = 'CHAINMGR';
my $test_jump_from_chain = 'INPUT';

### normalization will produce the correct network addresses ("10.1.2.3/24" is
### deliberate)
my $ipv4_src = '10.1.2.3/24';
my $ipv4_dst = '192.168.1.2';
my $ipv6_src = 'fe80::200:f8ff:fe21:67cf';
my $ipv6_dst = '0000:0000:00AA:0000:0000:AA00:0000:0001/64';

my $logfile   = 'test.log';
my $PRINT_LEN = 68;
my $chain_past_end = 1000;
#================== end config ===================

my %targets = (
    'ACCEPT' => '',
    'DROP'   => '',
    'QUEUE'  => '',
    'RETURN' => '',
);

my %iptables_chains = (
    'mangle' => [qw/PREROUTING INPUT OUTPUT FORWARD POSTROUTING/],
    'raw'    => [qw/PREROUTING OUTPUT/],
    'filter' => [qw/INPUT OUTPUT FORWARD/],
    'nat'    => [qw/PREROUTING OUTPUT POSTROUTING/]
);

my %ip6tables_chains = (
    'mangle' => [qw/PREROUTING INPUT OUTPUT FORWARD POSTROUTING/],
    'raw'    => [qw/PREROUTING OUTPUT/],
    'filter' => [qw/INPUT OUTPUT FORWARD/],
);

my $passed = 0;
my $failed = 0;
my $executed = 0;

&init();

&iptables_tests();
&ip6tables_tests();

&logr("\n[+] passed/failed/executed: $passed/$failed/$executed tests\n\n");

exit 0;

sub iptables_tests() {

    &logr("\n[+] Running $iptables_bin tests...\n");
    my %opts = (
        'iptables' => $iptables_bin,
        'iptout'   => '/tmp/iptables.out',
        'ipterr'   => '/tmp/iptables.err',
        'debug'    => 0,
        'verbose'  => 0
    );

    my $ipt_obj = new IPTables::ChainMgr(%opts)
        or die "[*] Could not acquire IPTables::ChainMgr object";

    ### built-in chains
    &chain_exists_tests($ipt_obj, \%iptables_chains);

    &test_cycle($ipt_obj);

    return;
}

sub ip6tables_tests() {

    &logr("\n[+] Running $ip6tables_bin tests...\n");
    my %opts = (
        'ip6tables' => $ip6tables_bin,
        'iptout'   => '/tmp/ip6tables.out',
        'ipterr'   => '/tmp/ip6tables.err',
        'debug'    => 0,
        'verbose'  => 0
    );

    my $ipt_obj = new IPTables::ChainMgr(%opts)
        or die "[*] Could not acquire IPTables::ChainMgr object";

    ### built-in chains
    &chain_exists_tests($ipt_obj, \%ip6tables_chains);

    &test_cycle($ipt_obj);

    return;
}

sub test_cycle() {
    my $ipt_obj = shift;

    &custom_chain_init($ipt_obj, $test_table,
        $test_jump_from_chain, $test_chain);

    ### create/delete chain cycle
    &chain_does_not_exist_test($ipt_obj, $test_table, $test_chain);
    &create_chain_test($ipt_obj, $test_table, $test_chain);
    &flush_chain_test($ipt_obj, $test_table, $test_chain);
    &delete_chain_test($ipt_obj, $test_table, $test_jump_from_chain, $test_chain);

    ### create chain, add rules, delete chain cycle
    &chain_does_not_exist_test($ipt_obj, $test_table, $test_chain);
    &create_chain_test($ipt_obj, $test_table, $test_chain);
    &add_rules_tests($ipt_obj, $test_table, $test_chain);
    &flush_chain_test($ipt_obj, $test_table, $test_chain);
    &delete_chain_test($ipt_obj, $test_table, $test_jump_from_chain, $test_chain);

    ### create chain add rules, add jump rule, delete chain cycle
    &chain_does_not_exist_test($ipt_obj, $test_table, $test_chain);
    &create_chain_test($ipt_obj, $test_table, $test_chain);
    &add_rules_tests($ipt_obj, $test_table, $test_chain);
    &find_rules_tests($ipt_obj, $test_table, $test_chain);
    &add_extended_rules_tests($ipt_obj, $test_table, $test_chain);
    &find_extended_rules_tests($ipt_obj, $test_table, $test_chain);
    &add_jump_rule_test($ipt_obj, $test_table, $test_chain);
    &find_jump_rule_test($ipt_obj, $test_table, $test_chain);
    &flush_chain_test($ipt_obj, $test_table, $test_chain);
    &delete_chain_test($ipt_obj, $test_table, $test_jump_from_chain, $test_chain);

    return;
}

sub chain_exists_tests() {
    my ($ipt_obj, $tables_chains_hr) = @_;

    for my $table (keys %$tables_chains_hr) {
        for my $chain (@{$tables_chains_hr->{$table}}) {
            &dots_print("chain_exists(): $table $chain");

            my ($rv, $out_ar, $err_ar) = $ipt_obj->chain_exists($table, $chain);

            &pass_fail($rv, "   $table chain $chain does not exist.");
        }
    }

    return;
}

sub flush_chain_test() {
    my ($ipt_obj, $test_table, $test_chain) = @_;

    &dots_print("flush_chain(): $test_table $test_chain");

    my ($rv, $out_ar, $err_ar) = $ipt_obj->flush_chain($test_table, $test_chain);
    &pass_fail($rv, "   Could not flush $test_table $test_chain chain.");

    return;
}

sub add_jump_rule_test() {
    my ($ipt_obj, $test_table, $test_chain) = @_;

    &dots_print("add_jump_rule(): $test_table $test_jump_from_chain -> $test_chain ");
    my ($rv, $out_ar, $err_ar) = $ipt_obj->add_jump_rule($test_table,
        $test_jump_from_chain, 1, $test_chain);

    &pass_fail($rv, "   Could not add jump rule.");

    return;
}

sub find_jump_rule_test() {
    my ($ipt_obj, $test_table, $test_chain) = @_;

    my $ip_any_net = '0.0.0.0/0';
    $ip_any_net = '::/0' if $ipt_obj->{'_ipt_bin_name'} eq 'ip6tables';

    &dots_print("find jump rule: $test_table $test_jump_from_chain -> $test_chain ");

    my ($rule_position, $num_chain_rules) = $ipt_obj->find_ip_rule($ip_any_net,
            $ip_any_net, $test_table, $test_jump_from_chain, $test_chain, {});

    &pass_fail($rule_position, "   Could not find jump rule.");

    return;
}


sub add_rules_tests() {
    my ($ipt_obj, $test_table, $test_chain) = @_;

    my $src_ip = $ipt_obj->normalize_net($ipv4_src);
    my $dst_ip = $ipt_obj->normalize_net($ipv4_dst);

    if ($ipt_obj->{'_ipt_bin_name'} eq 'ip6tables') {
        $src_ip = $ipt_obj->normalize_net($ipv6_src);
        $dst_ip = $ipt_obj->normalize_net($ipv6_dst);
    }

    for my $target (qw/LOG ACCEPT RETURN/) {
        &dots_print("add_ip_rules(): $test_table $test_chain $src_ip -> $dst_ip $target ");
        my ($rv, $out_ar, $err_ar) = $ipt_obj->add_ip_rule($src_ip,
                $dst_ip, $chain_past_end, $test_table, $test_chain, $target, {});

        &pass_fail($rv, "   Could not add $src_ip -> $dst_ip $target rule.");
    }

    return;
}

sub find_rules_tests() {
    my ($ipt_obj, $test_table, $test_chain) = @_;

    my $src_ip = $ipt_obj->normalize_net($ipv4_src);
    my $dst_ip = $ipt_obj->normalize_net($ipv4_dst);

    if ($ipt_obj->{'_ipt_bin_name'} eq 'ip6tables') {
        $src_ip = $ipt_obj->normalize_net($ipv6_src);
        $dst_ip = $ipt_obj->normalize_net($ipv6_dst);
    }

    for my $target (qw/LOG ACCEPT RETURN/) {
        &dots_print("find rule: $test_table $test_chain $src_ip -> $dst_ip $target ");
        my ($rule_position, $num_chain_rules) = $ipt_obj->find_ip_rule($src_ip,
                $dst_ip, $test_table, $test_chain, $target, {'normalize' => 1});

        &pass_fail($rule_position, "   Could not find $src_ip -> $dst_ip $target rule.");
    }

    return;
}

sub add_extended_rules_tests() {
    my ($ipt_obj, $test_table, $test_chain) = @_;

    my $src_ip = $ipt_obj->normalize_net($ipv4_src);
    my $dst_ip = $ipt_obj->normalize_net($ipv4_dst);

    if ($ipt_obj->{'_ipt_bin_name'} eq 'ip6tables') {
        $src_ip = $ipt_obj->normalize_net($ipv6_src);
        $dst_ip = $ipt_obj->normalize_net($ipv6_dst);
    }

    for my $target (qw/LOG ACCEPT RETURN/) {

        ### TCP
        &dots_print("add_ip_rules(): $test_table $test_chain TCP $src_ip(0) -> $dst_ip(80) $target ");
        my ($rv, $out_ar, $err_ar) = $ipt_obj->add_ip_rule($src_ip,
                $dst_ip, $chain_past_end, $test_table, $test_chain, $target,
                {'protocol' => 'tcp', 's_port' => 0, 'd_port' => 80});
        &pass_fail($rv, "   Could not add TCP $src_ip(0) -> $dst_ip(80) $target rule");

        ### TCP + state tracking
        &dots_print("add_ip_rules(): $test_table $test_chain TCP $src_ip(0) -> $dst_ip(80) state ESTABLISHED,RELATED $target ");
        ($rv, $out_ar, $err_ar) = $ipt_obj->add_ip_rule($src_ip,
                $dst_ip, $chain_past_end, $test_table, $test_chain, $target,
                {'protocol' => 'tcp', 's_port' => 0, 'd_port' => 80, 'state' => 'ESTABLISHED,RELATED'});
        &pass_fail($rv, "   Could not add TCP $src_ip(0) -> $dst_ip(80) state ESTABLISHED,RELATED $target rule");

        ### TCP + ctstate tracking
        &dots_print("add_ip_rules(): $test_table $test_chain TCP $src_ip(0) -> $dst_ip(80) ctstate ESTABLISHED,RELATED $target ");
        ($rv, $out_ar, $err_ar) = $ipt_obj->add_ip_rule($src_ip,
                $dst_ip, $chain_past_end, $test_table, $test_chain, $target,
                {'protocol' => 'tcp', 's_port' => 0, 'd_port' => 80, 'ctstate' => 'ESTABLISHED,RELATED'});
        &pass_fail($rv, "   Could not add TCP $src_ip(0) -> $dst_ip(80) ctstate ESTABLISHED,RELATED $target rule");

        ### UDP
        &dots_print("add_ip_rules(): $test_table $test_chain UDP $src_ip(0) -> $dst_ip(53) $target ");
        ($rv, $out_ar, $err_ar) = $ipt_obj->add_ip_rule($src_ip,
                $dst_ip, $chain_past_end, $test_table, $test_chain, $target,
                {'protocol' => 'udp', 's_port' => 0, 'd_port' => 53});
        &pass_fail($rv, "   Could not add UDP $src_ip(0) -> $dst_ip(53) $target rule");

    }

    return;
}

sub find_extended_rules_tests() {
    my ($ipt_obj, $test_table, $test_chain) = @_;

    my $src_ip = $ipt_obj->normalize_net($ipv4_src);
    my $dst_ip = $ipt_obj->normalize_net($ipv4_dst);

    if ($ipt_obj->{'_ipt_bin_name'} eq 'ip6tables') {
        $src_ip = $ipt_obj->normalize_net($ipv6_src);
        $dst_ip = $ipt_obj->normalize_net($ipv6_dst);
    }

    for my $target (qw/LOG ACCEPT RETURN/) {
        &dots_print("find rule: $test_table $test_chain TCP $src_ip(0) -> $dst_ip(80) $target ");
        my ($rule_position, $num_chain_rules) = $ipt_obj->find_ip_rule($src_ip,
                $dst_ip, $test_table, $test_chain, $target,
                {'normalize' => 1, 'protocol' => 'tcp', 's_port' => 0, 'd_port' => 80});
        &pass_fail($rule_position, "   Could not find TCP $src_ip(0) -> $dst_ip(80) $target rule");

        &dots_print("find rule: $test_table $test_chain TCP $src_ip(0) -> $dst_ip(80) state ESTABLISHED,RELATED $target ");
        ($rule_position, $num_chain_rules) = $ipt_obj->find_ip_rule($src_ip,
                $dst_ip, $test_table, $test_chain, $target,
                {'normalize' => 1, 'protocol' => 'tcp', 's_port' => 0,
                'd_port' => 80, 'state' => 'ESTABLISHED,RELATED'});
        &pass_fail($rule_position, "   Could not find TCP $src_ip(0) -> $dst_ip(80) state ESTABLISHED,RELATED $target rule");

        &dots_print("find rule: $test_table $test_chain TCP $src_ip(0) -> $dst_ip(80) ctstate ESTABLISHED,RELATED $target ");
        ($rule_position, $num_chain_rules) = $ipt_obj->find_ip_rule($src_ip,
                $dst_ip, $test_table, $test_chain, $target,
                {'normalize' => 1, 'protocol' => 'tcp', 's_port' => 0,
                'd_port' => 80, 'ctstate' => 'ESTABLISHED,RELATED'});
        &pass_fail($rule_position, "   Could not find TCP $src_ip(0) -> $dst_ip(80) ctstate ESTABLISHED,RELATED $target rule");

        &dots_print("find rule: $test_table $test_chain UDP $src_ip(0) -> $dst_ip(53) $target ");
        ($rule_position, $num_chain_rules) = $ipt_obj->find_ip_rule($src_ip,
                $dst_ip, $test_table, $test_chain, $target,
                {'normalize' => 1, 'protocol' => 'udp', 's_port' => 0, 'd_port' => 53});
        &pass_fail($rule_position, "   Could not find UDP $src_ip(0) -> $dst_ip(53) $target rule");
    }

    return;
}


sub create_chain_test() {
    my ($ipt_obj, $test_table, $test_chain) = @_;

    &dots_print("create_chain(): $test_table $test_chain");

    my ($rv, $out_ar, $err_ar) = $ipt_obj->create_chain($test_table, $test_chain);

    &pass_fail($rv, "   Could not create $test_table $test_chain chain");
    die "[*] FATAL" unless $rv;

    return;
}

sub chain_does_not_exist_test() {
    my ($ipt_obj, $test_table, $test_chain) = @_;

    &dots_print("!chain_exists(): $test_table $test_chain");

    my ($rv, $out_ar, $err_ar) = $ipt_obj->chain_exists($test_table, $test_chain);

    if ($rv) {
        $rv = 0;
    } else {
        $rv = 1;
    }
    &pass_fail(++$rv, "   Chain exists.");
    die "[*] FATAL" unless $rv;

    return;
}

sub custom_chain_init() {
    my ($ipt_obj, $test_table, $test_jump_from_chain, $test_chain) = @_;

    my ($rv, $out_ar, $err_ar) = $ipt_obj->chain_exists($test_table,
            $test_chain);
    if ($rv) {
        $ipt_obj->delete_chain($test_table, $test_jump_from_chain, $test_chain);
    }
    return;
}

sub delete_chain_test() {
    my ($ipt_obj, $test_table, $test_jump_from_chain, $test_chain) = @_;

    &dots_print("delete_chain(): $test_table $test_chain");

    my ($rv, $out_ar, $err_ar) = $ipt_obj->delete_chain($test_table,
        $test_jump_from_chain, $test_chain);

    &pass_fail($rv, "   Could not delete chain.");
    die "[*] FATAL" unless $rv;

    return;
}

sub dots_print() {
    my $msg = shift;
    &logr($msg);
    my $dots = '';
    for (my $i=length($msg); $i < $PRINT_LEN; $i++) {
        $dots .= '.';
    }
    &logr($dots);
    return;
}

sub pass_fail() {
    my ($rv, $fail_msg) = @_;

    $executed++;

    if ($rv) {
        &logr("pass ($executed)\n");
        $passed++;
    } else {
        &logr("fail ($executed)\n");
        &logr("$fail_msg\n");
        $failed++;
    }

    return;
}

sub logr() {
    my $msg = shift;
    print STDOUT $msg;
    open F, ">> $logfile" or die $!;
    print F $msg;
    close F;
    return;
}

sub init() {

    $|++; ### turn off buffering

    $< == 0 && $> == 0 or
        die "[*] $0: You must be root (or equivalent ",
            "UID 0 account) to effectively test fwknop";

    unlink $logfile if -e $logfile;
    for my $bin ($iptables_bin, $ip6tables_bin) {
        die "[*] $bin does not exist" unless -e $bin;
        die "[*] $bin not executable" unless -x $bin;
    }

    return;
}
