#!/usr/bin/perl -w
#
#############################################################################
#
# File: fwknop_test.pl
#
# Purpose: This program provides a testing infrastructure for the fwknop
#          Single Packet Authorization client and server.
#
# Author: Michael Rash (mbr@cipherdyne.org)
#
# Version: 1.9.0
#
# Copyright (C) 2007 Michael Rash (mbr@cipherdyne.org)
#
# License (GNU Public License):
#
#    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
#
#############################################################################
#
# $Id: fwknop_test.pl 903 2007-12-15 13:36:58Z mbr $
#

use IO::Socket;
use Getopt::Long;
use strict;

#=================== config defaults ==============
my $fwknopCmd  = '../fwknop';
my $fwknopdCmd = '../fwknopd';
my $knoptmCmd  = '../knoptm';
my $tcpdumpCmd = '/usr/sbin/tcpdump';

my $conf_dir    = 'conf';
my $output_dir  = 'output';

my $default_access_conf   = "$conf_dir/default_access.conf";
my $default_fwknop_conf   = "$conf_dir/default_fwknop.conf";
my $forward_access_conf   = "$conf_dir/forward_access.conf";
my $forward_fwknop_conf   = "$conf_dir/forward_chain_fwknop.conf";
my $pcap_file_fwknop_conf = "$conf_dir/pcap_file_fwknop.conf";

my $local_key_file = 'local_spa.key';

my $loopback_intf = 'lo'; ### default on linux

my $spa_dst = '127.0.0.1';
my $spa_src = '127.0.0.2';

my $cmd_stdout = "$output_dir/cmd.stdout";
my $cmd_stderr = "$output_dir/cmd.stderr";

my $test_cmd_file = '/tmp/fwknop_test.txt';
my $test_cmd = "echo fwknoptest > $test_cmd_file";
#==================== end config ==================

my $fw_access_timeout = 20;  ### default
my $require_user      = '';
my $require_source_addr = 0;
my $fwknop_conf_file    = '/etc/fwknop/fwknop.conf';
my $cache_encrypted_Rijndael_spa_packet = '';
my $cache_Rijndael_key = '';
my $successful_tests   = 0;
my $sniff_file = '';
my $PRINT_LEN = 65;
my $cmd_regex = '';
my $verbose = 0;
my $help    = 0;
my %config  = ();
my %cmds    = ();

### make Getopts case sensitive
Getopt::Long::Configure('no_ignore_case');

exit 1 unless GetOptions(
    'fwknop-command=s'  => \$fwknopCmd,
    'fwknopd-command=s' => \$fwknopdCmd,
    'knoptm-command=s'  => \$knoptmCmd,
    'access-conf=s'     => \$default_access_conf,
    'loopback-intf=s'   => \$loopback_intf,
    'cmd-stdout=s'      => \$cmd_stdout,
    'cmd-stderr=s'      => \$cmd_stderr,
    'verbose'           => \$verbose,
    'help'              => \$help
);

&usage() if $help;

&setup();
&parse_access_conf();

print "\n[+] ==> Running fwknop test suite; ",
    "firewall: $config{'FIREWALL_TYPE'} <==\n\n";

### main tests
&test_driver('perl program compilation', \&perl_compilation);
&test_driver('C program compilation', \&C_compilation);
&test_driver('Stopping any running fwknopd processes', \&stop_fwknopd);
if ($config{'FIREWALL_TYPE'} eq 'iptables') {
    &test_driver('Flushing all fwknop iptables rules', \&flush_iptables);
}
&test_driver('Testing Rijndael key validity', \&short_key);
&test_driver('Generating SPA access packet with fwknop client',
    \&SPA_access_packet);
&test_driver('Sniffing SPA access packet to acquire access',
    \&sniff_decrypt);
&test_driver('Firewall access rules exist', \&fw_rules_exist);
&fw_sleep();  ### give knoptm a chance to remove the rules
&test_driver('Firewall access rules removed', \&fw_rules_removed);
&test_driver('Stopping all running fwknopd processes', \&stop_fwknopd);
&test_driver('Replay attack detection', \&replay_attack);
&test_driver('SPA packet randomness', \&packet_randomness);

if ($require_source_addr) {
    &test_driver('Generating SPA packet with 0.0.0.0 src addr',
        \&source_addr);
    &test_driver('Sniffing packet source address with 0.0.0.0 src addr',
        \&sniff_source_addr);
}

if ($require_user) {
    &test_driver('Generating SPA packet with invalid user', \&invalid_user);
    &test_driver('Invalid user detection', \&sniff_invalid_user);
}

&test_driver('Generating SPA command packet', \&SPA_command_packet);
&test_driver('Sniffing SPA command packet and executing', \&fwknopd_command);
&test_driver('Making sure firewall rules have been removed',
    \&fw_rules_removed);

if ($cmd_regex) {
    $test_cmd = "touch $test_cmd_file";
    if ($test_cmd !~ /$cmd_regex/) {
        &test_driver('Generating SPA command packet with non-matching regex',
            \&SPA_non_matching_re_command_packet);
        &test_driver('SPA command packet filtered', \&fwknopd_no_re_command);
    }
}
&test_driver('Making sure firewall rules do not exist',
    \&fw_rules_removed);

### test FORWARD chain support and DNAT connections
if ($config{'FIREWALL_TYPE'} eq 'iptables') {
    &test_driver('Stopping all running fwknopd processes', \&stop_fwknopd);
    &test_driver('Generating FORWARD chain access packet',
        \&SPA_forward_access_packet);
    &test_driver('FORWARD request detection', \&forward_request);
    &test_driver('FORWARD and DNAT access', \&forward_access);
    &fw_sleep();  ### give knoptm a chance to remove the rules
    &test_driver('Making sure firewall rules have been removed',
        \&fw_rules_removed);
}

### this test should also work on FreeBSD, but there seem to be issues
### acquiring packet data from /var/log/sniff.pcap
if ($config{'FIREWALL_TYPE'} eq 'iptables'
        and -e $tcpdumpCmd and -x $tcpdumpCmd) {
    &test_driver('Stopping all running fwknopd processes', \&stop_fwknopd);
    &get_sniff_file();
    &start_tcpdump();

    sleep 1;
    &test_driver('Generating SPA access packet with fwknop client',
        \&SPA_access_packet);

    ### send the SPA packet once to make sure the $sniff_file is
    ### not zero-size (tcpdump will write it to $sniff_file)
    &send_packet('somedatastring');

    sleep 1;

    ### start fwknopd and acquire packet data from $sniff_file
    &test_driver('SPA communications via tcpdump capture file',
        \&filesystem_capture);

    &test_driver('Firewall access rules exist', \&fw_rules_exist);
    &fw_sleep();  ### give knoptm a chance to remove the rules
    &test_driver('Firewall access rules removed', \&fw_rules_removed);
}

&test_driver('Stopping all running fwknopd processes', \&stop_fwknopd);

if ($config{'FIREWALL_TYPE'} eq 'iptables') {
    &test_driver('Deleting all fwknopd iptables chains', \&del_ipt_chains);
}

print "\n[+] ==> Passed all $successful_tests tests against fwknop. <==\n\n";
exit 0;
#======================== end main =========================

sub test_driver() {
    my ($msg, $func_ref) = @_;
    if (-e "${cmd_stderr}.$successful_tests") {
        unlink "${cmd_stderr}.$successful_tests"
            or die "[*] Could not unlink(${cmd_stderr}.$successful_tests)";
    }
    if (-e "${cmd_stdout}.$successful_tests") {
        unlink "${cmd_stdout}.$successful_tests"
            or die "[*] Could not unlink(${cmd_stdout}.$successful_tests)";
    }
    &test_print($msg);
    &{$func_ref};
    &pass();
    return;
}

sub run_fwknopd() {
    my ($cached_spa_packet, $fwknop_conf, $access_conf) = @_;

    ### start fwknopd to monitor for the Rijndael SPA packet over
    ### the loopback interface
    my $fwknopd_pid = &start_fwknopd($fwknop_conf, $access_conf);

    ### give fwknopd a chance to parse its config and start sniffing
    ### on the loopback interface
    sleep 2;

    ### send the SPA packet over UDP port 62201 to the fwknopd server
    &send_packet($cache_encrypted_Rijndael_spa_packet);

    ### fwknopd will exit after receiving the cached packet (--Count 1)
    waitpid($fwknopd_pid, 0);

    return;
}

sub filesystem_capture() {

    &run_fwknopd($cache_encrypted_Rijndael_spa_packet, $pcap_file_fwknop_conf,
        $default_access_conf);

    return;
}

sub replay_attack() {

    &run_fwknopd($cache_encrypted_Rijndael_spa_packet, $default_fwknop_conf,
        $default_access_conf);

    ### now that fwknopd has exited, see if the replay was detected
    my $found_replay = 0;
    open SE, "< ${cmd_stderr}.$successful_tests"
        or die "[*] Could not open ${cmd_stderr}.$successful_tests: $!";
    while (<SE>) {
        if (/\[\-\]\s+attempted\s+SPA\s+packet\s+replay/i) {
            ### [-] attempted SPA packet replay from: 127.0.0.1 (original \
            ### SPA src: 127.0.0.1, MD5: 40qPGOpvRBX54E4CmVsxSA)
            $found_replay = 1;
            last;
        }
    }
    close SE;

    unless ($found_replay) {
        &print_errors("fail ($successful_tests)\n[*] Replay " .
                "attack not detected:");
    }
    return;
}

sub fw_rules_removed() {
    if (&fw_rules()) {
        print "fail ($successful_tests)\n[*] SPA access " .
                "rules for $spa_src not removed.\n";
        exit 1;
    }
    return;
}

sub fw_rules_exist() {
    unless (&fw_rules()) {
        print "fail ($successful_tests)\n[*] SPA access " .
            "rules for $spa_src do not exist.\n";
        exit 1;
    }
    return;
}

sub flush_iptables() {
    unless (&run_cmd("$fwknopdCmd --fw-flush")) {
        &print_errors("fail ($successful_tests)\n[*] Could " .
            "not execute $fwknopdCmd --fw-flush");
    }
    return;
}

sub stop_fwknopd() {
    unless (&run_cmd("$fwknopdCmd --Kill")) {
        &print_errors("fail ($successful_tests)\n[*] Could " .
            "not execute $fwknopdCmd --Kill");
    }
    unless (&run_cmd("$fwknopdCmd --Status")) {
        &print_errors("fail ($successful_tests)\n[*] Could " .
                "not execute $fwknopdCmd --Status");
    }

    my $pid_file_msg_ctr = 0;
    open F, "< ${cmd_stdout}.$successful_tests"
        or die "[*] Could not open ${cmd_stdout}.$successful_tests: $!";
    while (<F>) {
        ### [+] knopwatchd pidfile does not exist.
        if (/^\s*\[\+\].*pidfile\s+does\s+not\s+exist/) {
            $pid_file_msg_ctr++;
        }
    }
    close F;
    unless ($pid_file_msg_ctr == 3) {
        &print_errors("fail ($successful_tests)\n[*] All " .
            "three fwknopd processes are not stopped");
    }
    return;
}

sub fw_sleep() {
    print "    (Sleeping for $fw_access_timeout (+5) seconds for firewall " .
        "rule timeout)\n    ";
    for (my $i=$fw_access_timeout+5; $i > 0; $i--) {
        if (($i % 5) == 0) {
            print "$i ";
        }
        sleep 1;
    }
    print "0\n";
    return;
}

sub fw_rules() {
    unless (&run_cmd("$fwknopdCmd --fw-list")) {
        &print_errors("fail ($successful_tests)\n[*] Could not " .
            "execute fwknopd --fw-list");
    }

    my $found_access_rules = 0;
    open F, "< ${cmd_stdout}.$successful_tests" or
        die "[*] Could not open ${cmd_stdout}.$successful_tests: $!";
    while (<F>) {
        if (/\b$spa_src\b/) {
            $found_access_rules = 1;
            last;
        }
    }
    close F;
    if ($found_access_rules) {
        return 1;
    }
    return 0;
}

sub sniff_decrypt() {

    &run_fwknopd($cache_encrypted_Rijndael_spa_packet, $default_fwknop_conf,
        $default_access_conf);

    ### now that fwknopd has exited, see if the SPA packet was valid
    open SE, "< ${cmd_stderr}.$successful_tests"
        or die "[*] Could not open ${cmd_stderr}.$successful_tests: $!";
    while (<SE>) {
        if (/\[\-\]\s+Key\s+mis\-?match/i) {
            ### [-] Key mis-match or broken message checksum for SOURCE \
            ### ANY (# 1 in access.conf)
            &print_errors("fail ($successful_tests)\n[*] Invalid SPA packet, ");
        } elsif (/\[\-\]\s+Decrypted.*not\s+conform/i) {
            ### [-] Decrypted message does not conform to a valid SPA packet
            &print_errors("fail ($successful_tests)\n[*] Invalid SPA packet, ");
        }
    }
    close SE;
    return;
}

sub forward_request() {

    &run_fwknopd($cache_encrypted_Rijndael_spa_packet, $default_fwknop_conf,
        $default_access_conf);

    ### now that fwknopd has exited, make sure the FORWARD access request
    ### packet was detected and did not allow access
    my $forward_access_detected = 0;
    open SE, "< ${cmd_stderr}.$successful_tests"
        or die "[*] Could not open ${cmd_stderr}.$successful_tests: $!";
    while (<SE>) {
        ### [-] FORWARD access requested through non-forward access
        ### SOURCE block (SOURCE line num: 152)
        if (/\[\-\]\s+FORWARD\s+access\s+requested\s+through\s+non-forward/i) {
            $forward_access_detected = 1;
            last;
        }
    }
    close SE;
    unless ($forward_access_detected) {
        &print_errors("fail ($successful_tests)\n[*] FORWARD access request packet " .
            "should have been detected and not allowed");
    }
    return;
}

sub forward_access() {

    &run_fwknopd($cache_encrypted_Rijndael_spa_packet, $forward_fwknop_conf,
        $forward_access_conf);

    ### now that fwknopd has exited, make sure the FORWARD access request
    ### packet was detected and did not allow access
    my $forward_access_stage1 = 0;
    my $forward_access_stage2 = 0;
    open SE, "< ${cmd_stderr}.$successful_tests"
        or die "[*] Could not open ${cmd_stderr}.$successful_tests: $!";
    while (<SE>) {
        ### [+] add FWKNOP_FORWARD 127.0.0.2 -> 0.0.0.0/0(tcp/22) ACCEPT rule 10 sec
        ### [+] add FWKNOP_PREROUTING 127.0.0.2 -> 192.168.10.3(tcp/22) DNAT rule 10 sec
        if (/\[\+\]\s+add\s+FWKNOP_FORWARD\s+/i) {
            $forward_access_stage1 = 1;
            next;
        }
        if ($forward_access_stage1 and /\[\+\]\s+add\s+FWKNOP_PREROUTING\s+.*DNAT/i) {
            $forward_access_stage2 = 1;
            last;
        }
    }
    close SE;
    unless ($forward_access_stage2) {
        &print_errors("fail ($successful_tests)\n[*] FORWARD and DNAT " .
            "access not granted");
    }
    return;
}

sub fwknopd_command() {

    unlink $test_cmd_file if -e $test_cmd_file;
    &run_fwknopd($cache_encrypted_Rijndael_spa_packet, $default_fwknop_conf,
        $default_access_conf);

    ### now that fwknopd has exited, see if the SPA packet was valid
    my $found_command = 0;
    open SE, "< ${cmd_stderr}.$successful_tests"
        or die "[*] Could not open ${cmd_stderr}.$successful_tests: $!";
    while (<SE>) {
        ### executing command "echo test > /tmp/fwknop_test.txt" for 127.0.0.1
        if (/\[\+\]\s+executing\s+command.*$test_cmd_file/i) {
            $found_command = 1;
            last;
        }
    }
    close SE;
    unless ($found_command and -e $test_cmd_file) {
        &print_errors("fail ($successful_tests)\n[*] Command not executed by fwknopd");
    }
    return;
}

sub fwknopd_no_re_command() {

    unlink $test_cmd_file if -e $test_cmd_file;
    &run_fwknopd($cache_encrypted_Rijndael_spa_packet, $default_fwknop_conf,
        $default_access_conf);

    ### now that fwknopd has exited, make sure the command was not
    ### executed
    my $command_filtered = 0;
    open SE, "< ${cmd_stderr}.$successful_tests"
        or die "[*] Could not open ${cmd_stderr}.$successful_tests: $!";
    while (<SE>) {
        ### [-] received command "127.0.0.2,touch /tmp/fwknop_test.txt"
        ### from 127.0.0.1 but CMD_REGEX did not match 127.0.0.1
        if (/\[\-\]\s+.*but\s+CMD_REGEX\s+did\s+not\s+match/i) {
            $command_filtered = 1;
            last;
        }
    }
    close SE;
    unless ($command_filtered and not -e $test_cmd_file) {
        &print_errors("fail ($successful_tests)\n[*] Command executed by fwknopd but " .
            "should have been filtered");
    }
    return;
}

sub del_ipt_chains() {
    unless (&run_cmd("$fwknopdCmd --fw-flush --fw-del-chains")) {
        &print_errors("fail ($successful_tests)\n[*] Could " .
            "not execute $fwknopdCmd --fw-flush --fw-del-chains");
    }
    unless (&run_cmd("$fwknopdCmd --fw-list")) {
        &print_errors("fail ($successful_tests)\n[*] Could " .
            "not execute $fwknopdCmd --fw-list");
    }
    my $chain_exists = 0;
    open F, "< ${cmd_stdout}.$successful_tests"
        or die "[*] Could not open ${cmd_stdout}.$successful_tests: $!";
    while (<F>) {
        if (/FWKNOP_/) {
            $chain_exists = 1;
            last;
        }
    }
    close F;
    if ($chain_exists) {
        &print_errors("fail ($successful_tests)\n[*] Not " .
            "all fwknop chains were removed");
    }
    return;
}

sub send_packet() {
    my $spa_packet = shift;

    my $socket = IO::Socket::INET->new(
        PeerAddr => $spa_dst,
        PeerPort => 62201,
        Proto    => 'udp',
        Timeout  => 1
    ) or die "[*] Could not acquire UDP/62201 socket to $spa_dst: $!";

    $socket->send($spa_packet);

    undef $socket;
    return;
}

sub start_fwknopd() {
    my ($conf_file, $access_file) = @_;

    my $pid = fork();
    die "[*] Could not fork: $!" unless defined $pid;

    if ($pid == 0) {

        ### we are the child, so start fwknopd
        exit &run_cmd("$fwknopdCmd --debug --Count 1 -i $loopback_intf " .
            "-c $conf_file -a $access_file --fw-type " .
            "$config{'FIREWALL_TYPE'} --Test-mode --knoptmCmd $knoptmCmd");

    }
    return $pid;
}

sub get_sniff_file() {
    open F, "< $pcap_file_fwknop_conf" or die "[*] Could not open ",
        "$pcap_file_fwknop_conf: $!";
    while (<F>) {
        if (/^\s*PCAP_PKT_FILE\s+(\S+)\s*;/) {
            $sniff_file = $1;
        }
    }
    close F;
    die "[*] Could not get PCAP_PKT_FILE file." unless $sniff_file;
    if (-e $sniff_file) {
        unlink $sniff_file or die "[*] Could not unlink($sniff_file)";
    }
    return;
}

sub start_tcpdump() {
    my $pid = fork();
    die "[*] Could not fork: $!" unless defined $pid;

    if ($pid == 0) {

        ### collect SPA packet data into a file with tcpdump where
        ### fwknopd can then acquire it.
        exit &run_cmd("$tcpdumpCmd -i $loopback_intf -c 2 -U -nn " .
            "-s 0 -w $sniff_file udp port 62201");

    }
    return $pid;
}

sub SPA_access_packet() {
    &get_access_packet();
    return;
}

sub SPA_command_packet() {
    &get_command_packet();
    return;
}

sub SPA_forward_access_packet() {
    &get_forward_access_packet();
    return;
}

sub SPA_non_matching_re_command_packet() {
    &get_command_packet();
    return;
}

sub short_key() {
    &get_access_packet_short_key();
    return;
}

sub invalid_user() {
    my $require_user_copy = $require_user;
    $require_user = 'mbr' . $require_user;
    &get_access_packet();
    $require_user = $require_user_copy;
    return;
}

sub sniff_invalid_user() {

    &run_fwknopd($cache_encrypted_Rijndael_spa_packet, $default_fwknop_conf,
        $default_access_conf);

    ### now that fwknopd has exited, see if the SPA packet was valid
    my $found_user_err = 0;
    open SE, "< ${cmd_stderr}.$successful_tests"
        or die "[*] Could not open ${cmd_stderr}.$successful_tests: $!";
    while (<SE>) {
        if (/\[\-\]\s+username\s+mismatch/) {
            ### [-] username mismatch from 127.0.0.1, expecting root, got mbr
            $found_user_err = 1;
            last;
        }
    }
    close SE;
    unless ($found_user_err) {
        &print_errors("fail ($successful_tests)\n[*] fwknopd accepted SPA packet with " .
            "invalid user");
    }
    return;
}

sub source_addr() {
    my $spa_src_copy = $spa_src;
    $spa_src = '0.0.0.0';
    &get_access_packet();
    $spa_src = $spa_src_copy;
    return;
}

sub sniff_source_addr() {

    &run_fwknopd($cache_encrypted_Rijndael_spa_packet, $default_fwknop_conf,
        $default_access_conf);

    ### now that fwknopd has exited, see if the SPA packet was valid
    my $found_src_err = 0;
    open SE, "< ${cmd_stderr}.$successful_tests"
        or die "[*] Could not open ${cmd_stderr}.$successful_tests: $!";
    while (<SE>) {
        if (/\[\-\]\s+IP.*sent\s+SPA\s+packet.*but\s+
                REQUIRE_SOURCE_ADDRESS\s+is\s+enabled/ix) {
            ### [-] IP: 127.0.0.1 sent SPA packet that contained 0.0.0.0 (-s
            ### on the client side) but REQUIRE_SOURCE_ADDRESS is enabled
            ### (SOURCE line num: 15)
            $found_src_err = 1;
            last;
        }
    }
    close SE;
    unless ($found_src_err) {
        &print_errors("fail ($successful_tests)\n[*] fwknopd accepted " .
            "SPA packet with 0.0.0.0 source address");
    }
    return;
}

sub packet_randomness() {

    my $orig_spa_packet = $cache_encrypted_Rijndael_spa_packet;

    &get_access_packet();

    if ($orig_spa_packet eq $cache_encrypted_Rijndael_spa_packet) {
        &print_errors("fail ($successful_tests)\n[*] New packet is " .
            "identical to cached copy; rand() not working?");
    }
    return;
}

sub parse_access_conf() {
    ### parse the Rijndael key from the test_access.conf file (this same
    ### will be used by fwknopd to decrypt the SPA packet)
    open F, "< $default_access_conf" or
        die "[*] Could not open $default_access_conf: $!";
    while (<F>) {
        if (/^\s*KEY:\s*(.*)\s*;/) {
            $cache_Rijndael_key = $1;
        } elsif (/^\s*FW_ACCESS_TIMEOUT:\s*(\d+)\s*;/) {
            $fw_access_timeout = $1;
        } elsif (/^\s*REQUIRE_USERNAME:\s*(.*)\s*;/) {
            $require_user = $1;
        } elsif (/^\s*REQUIRE_SOURCE_ADDRESS:\s*Y\s*;/) {
            $require_source_addr = 1;
        } elsif (/^\s*CMD_REGEX:\s*(.*)\s*;/) {
            $cmd_regex = qr|$1|;
        }
    }
    close F;
    return;
}

sub get_access_packet() {

    ### --Test so that SPA packet is not sent
    ### -a so that REQUIRE_SOURCE_ADDRESS checks out
    my $found_packet_data = 0;
    my $spa_packet = '';

    open K, "> $local_key_file"
        or die "[*] Could not open $local_key_file: $!";
    print K "$spa_dst: $cache_Rijndael_key\n";
    close K;

    unless (&run_cmd("$fwknopCmd --no-save --get-key $local_key_file -D " .
            "$spa_dst -a $spa_src --Test -v --Spoof-user $require_user")) {
        &print_errors("fail ($successful_tests)\n[*] Could not " .
                "generate encrypted SPA packet");
    }
    open F, "< ${cmd_stdout}.$successful_tests"
        or die "[*] Could not open ${cmd_stdout}.$successful_tests: $!";
    while (<F>) {
        ### [+] Packet data:
        if (/^\s*\[\+\]\s+Packet\s+data:/) {
            $found_packet_data = 1;
            next;
        }
        if ($found_packet_data) {
            if (/(\S+)/) {
                $cache_encrypted_Rijndael_spa_packet = $1;
                last;
            }
        }
    }
    close F;
    unless ($cache_encrypted_Rijndael_spa_packet) {
        &print_errors("fail ($successful_tests)\n[*] Could not generate " .
            "encrypted SPA packet");
    }
    return;
}

sub get_forward_access_packet() {

    ### --Test so that SPA packet is not sent
    ### -a so that REQUIRE_SOURCE_ADDRESS checks out
    my $found_packet_data = 0;
    my $spa_packet = '';

    open K, "> $local_key_file"
        or die "[*] Could not open $local_key_file: $!";
    print K "$spa_dst: $cache_Rijndael_key\n";
    close K;

    unless (&run_cmd("$fwknopCmd --no-save --get-key $local_key_file -D " .
            "$spa_dst -a $spa_src --Test -v --Spoof-user $require_user " .
            "--Forward-access 192.168.10.3,55000")) {
        &print_errors("fail ($successful_tests)\n[*] Could not generate " .
                "encrypted SPA packet");
    }
    open F, "< ${cmd_stdout}.$successful_tests"
        or die "[*] Could not open ${cmd_stdout}.$successful_tests: $!";
    while (<F>) {
        ### [+] Packet data:
        if (/^\s*\[\+\]\s+Packet\s+data:/) {
            $found_packet_data = 1;
            next;
        }
        if ($found_packet_data) {
            if (/(\S+)/) {
                $cache_encrypted_Rijndael_spa_packet = $1;
                last;
            }
        }
    }
    close F;
    unless ($cache_encrypted_Rijndael_spa_packet) {
        &print_errors("fail ($successful_tests)\n[*] Could not generate " .
                "encrypted SPA packet");
    }
    return;
}

sub get_access_packet_short_key() {

    ### --Test so that SPA packet is not sent
    ### -a so that REQUIRE_SOURCE_ADDRESS checks out
    my $found_packet_data = 0;
    my $spa_packet = '';

    open K, "> $local_key_file"
        or die "[*] Could not open $local_key_file: $!";
    print K "$spa_dst: short\n";
    close K;

    ### this is supposed to fail
    unless (not &run_cmd("$fwknopCmd --no-save --get-key $local_key_file -D " .
            "$spa_dst -a $spa_src --Test -v --Spoof-user $require_user")) {
        &print_errors("fail ($successful_tests)\n[*] Generated " .
                "SPA packet with invalid key");
    }

    my $packet_test = '';
    open F, "< ${cmd_stdout}.$successful_tests"
        or die "[*] Could not open ${cmd_stdout}.$successful_tests: $!";
    while (<F>) {
        ### [+] Packet data:
        if (/^\s*\[\+\]\s+Packet\s+data:/) {
            $found_packet_data = 1;
            next;
        }
        if ($found_packet_data) {
            if (/(\S+)/) {
                $packet_test = $1;
                last;
            }
        }
    }
    close F;
    if ($packet_test) {
        &print_errors("fail ($successful_tests)\n[*] fwknop " .
            "generated SPA packet even though the Rijndael key is too short");
    }
    return;
}

sub get_command_packet() {

    ### --Test so that SPA packet is not sent
    ### -a so that REQUIRE_SOURCE_ADDRESS checks out
    my $found_packet_data = 0;
    my $spa_packet = '';

    open K, "> $local_key_file"
        or die "[*] Could not open $local_key_file: $!";
    print K "$spa_dst: $cache_Rijndael_key\n";
    close K;

    $cache_encrypted_Rijndael_spa_packet = '';

    unless (&run_cmd(qq|$fwknopCmd --no-save --get-key $local_key_file -D | .
            qq|$spa_dst -a $spa_src --Test -v --Spoof-user $require_user | .
            qq|--Server-cmd "$test_cmd"|)) {
        &print_errors("fail ($successful_tests)\n[*] Could not " .
            "generate encrypted SPA command packet");
    }
    open F, "< ${cmd_stdout}.$successful_tests"
        or die "[*] Could not open ${cmd_stdout}.$successful_tests: $!";
    while (<F>) {
        ### [+] Packet data:
        if (/^\s*\[\+\]\s+Packet\s+data:/) {
            $found_packet_data = 1;
            next;
        }
        if ($found_packet_data) {
            if (/(\S+)/) {
                $cache_encrypted_Rijndael_spa_packet = $1;
                last;
            }
        }
    }
    close F;
    unless ($cache_encrypted_Rijndael_spa_packet) {
        &print_errors("fail ($successful_tests)\n[*] Could not " .
                "generate encrypted SPA packet");
    }
    return;
}

sub perl_compilation() {
    for my $prog ($fwknopCmd, $fwknopdCmd, $knoptmCmd) {
        unless (&run_cmd("perl -c $prog")) {
            &print_errors("fail ($successful_tests)\n[*] " .
                "$prog does not compile");
        }
    }
    return;
}

sub C_compilation() {
    unless (&run_cmd('make -C ..')) {
        &print_errors("fail ($successful_tests)\n[*] fwknop " .
                "C programs would not compile");
    }
    return;
}

sub print_errors() {
    my $msg = shift;
    print $msg, "\n";
    if (-e "${cmd_stderr}.$successful_tests"
            and -s "${cmd_stderr}.$successful_tests") {
        if ($verbose) {
            open E, "< ${cmd_stderr}.$successful_tests"
                or die "[*] Could not open ",
                    "${cmd_stderr}.$successful_tests: $!";
            print while <E>;
            close E;
        } else {
            print "    Errors available in: ",
                "${cmd_stderr}.$successful_tests file.\n";
        }
    }
    exit 1;
}

sub run_cmd() {
    my $cmd = shift;
    my $rv = ((system "$cmd > ${cmd_stdout}.$successful_tests " .
            "2> ${cmd_stderr}.$successful_tests") >> 8);
    if ($rv == 0) {
        return 1;
    }
    return 0;
}

sub test_print() {
    my $msg = shift;
    print "[+] $msg";
    my $dots = '';
    for (my $i=length($msg); $i < $PRINT_LEN; $i++) {
        $dots .= '.';
    }
    print $dots;
    return;
}

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

    $|++; ### turn off buffering

    die "[*] $conf_dir directory does not exist." unless -d $conf_dir;
    unless (-d $output_dir) {
        mkdir $output_dir or die "[*] Could not mkdir $output_dir: $!";
    }

    my $i = 0;
    while (-e "${cmd_stdout}.$i") {
        unlink "${cmd_stdout}.$i"
            or die "[*] Could not unlink(${cmd_stdout}.$i)";
        $i++;
    }
    $i = 0;
    while (-e "${cmd_stderr}.$i") {
        unlink "${cmd_stderr}.$i"
            or die "[*] Could not unlink(${cmd_stderr}.$i)";
        $i++;
    }

    for my $prog ($fwknopCmd, $fwknopdCmd, $knoptmCmd) {
        die "[*] $prog does not exist" unless -e $prog;
        die "[*] $prog not executable" unless -x $prog;
    }

    &import_config();

    ### make sure fwknopd is not already running
    if (-e "$config{'FWKNOP_PID_FILE'}") {
        open P, "< $config{'FWKNOP_PID_FILE'}" or die "[*] Could not open ",
            "$config{'FWKNOP_PID_FILE'}: $!";
        my $pid = <P>;
        close P;
        chomp $pid;
        if (kill 0, $pid) {
            die "[*] Please stop the running fwknop instance.";
        }
    }

    ### set the loopback interface to lo0 for FreeBSD
    if ($config{'FIREWALL_TYPE'} eq 'ipfw' and $loopback_intf eq 'lo') {
        $loopback_intf = 'lo0';
    }
    return;
}

sub import_config() {
    open C, "< $fwknop_conf_file"
        or die "[*] Could not open $fwknop_conf_file: $!";
    while (<C>) {
        next if /^\s*#/;
        if (/^\s*(\S+)\s+(.*?)\;/) {
            my $varname = $1;
            my $val     = $2;
            if ($val =~ m|/.+| and $varname =~ /^\s*(\S+)Cmd$/) {
                ### found a command
                $cmds{$1} = $val;
            } else {
                $config{$varname} = $val;
            }
        }
    }
    close C;

    ### resolve internal vars within variable values
    &expand_vars();
}

sub expand_vars() {

    my $has_sub_var = 1;
    my $resolve_ctr = 0;

    while ($has_sub_var) {
        $resolve_ctr++;
        $has_sub_var = 0;
        if ($resolve_ctr >= 20) {
            die "[*] Exceeded maximum variable resolution counter.";
        }
        for my $hr (\%config, \%cmds) {
            for my $var (keys %$hr) {
                my $val = $hr->{$var};
                if ($val =~ m|\$(\w+)|) {
                    my $sub_var = $1;
                    die "[*] sub-ver $sub_var not allowed within same ",
                        "variable $var" if $sub_var eq $var;
                    if (defined $config{$sub_var}) {
                        $val =~ s|\$$sub_var|$config{$sub_var}|;
                        $hr->{$var} = $val;
                    } else {
                        die "[*] sub-var \"$sub_var\" not defined in ",
                            "config for var: $var."
                    }
                    $has_sub_var = 1;
                }
            }
        }
    }
    return;
}

sub pass() {
    print "pass ($successful_tests)\n";
    if ($verbose) {
        for my $file ("${cmd_stdout}.$successful_tests",
                "${cmd_stderr}.$successful_tests") {

            open E, "< $file"
                or die "[*] Could not open $file: $!";
            print while <E>;
            close E;
        }
    }
    $successful_tests++;
    return;
}

sub usage() {
    print <<_HELP_;

Usage: fwknop_test.pl [options]

Options:
    -v, --verbose           - Generate verbose output.

_HELP_
    exit 0;
}
