#!/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.1
#
# 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 979 2008-01-26 19:43:56Z 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 $gpgCmd     = '/usr/bin/gpg';

my $conf_dir   = 'conf';
my $output_dir = 'output';
my $logfile    = 'test.log';
my $tarfile    = 'fwknop_test.tar.gz';

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 $output_access_conf    = "$conf_dir/output_access.conf";
my $output_fwknop_conf    = "$conf_dir/output_chain_fwknop.conf";
my $spa_aging_fwknop_conf = "$conf_dir/spa_aging_fwknop.conf";
my $pcap_file_fwknop_conf = "$conf_dir/pcap_file_fwknop.conf";
my $gpg_access_conf       = "$conf_dir/gpg_access.conf";
my $no_promisc_fwknop_conf = "$conf_dir/no_promisc_fwknop.conf";
my $multi_source_access_conf = "$conf_dir/multi_source_access.conf";
my $no_loopback_ip_match_access_conf = "$conf_dir/no_loopback_ip_match_access.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 $sniff_alarm = 20;

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 $fwknopd_output_file = '';
my $require_source_addr = 0;
my $permit_client_ports = 0;
my $fwknop_conf_file    = '/etc/fwknop/fwknop.conf';
my $cache_encrypted_Rijndael_spa_packet = '';
my $cache_Rijndael_key = '';
my $successful_tests   = 0;
my $failed_tests       = 0;
my $test_num    = 0;
my $unauth_port = 0;
my $sniff_file = '';
my $open_ports = '';
my $PRINT_LEN = 68;
my $NUM_RAND  = 100;
my $cmd_regex = '';
my $prepare_results = 0;
my $help    = 0;
my %config  = ();
my %cmds    = ();
my $ip_re   = qr|(?:[0-2]?\d{1,2}\.){3}[0-2]?\d{1,2}|;

### SPA message types from fwknop clients
my $SPA_COMMAND_MODE = 0;
my $SPA_ACCESS_MODE  = 1;  ### default
my $SPA_FORWARD_ACCESS_MODE = 2;

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

exit 1 unless GetOptions(
    'Prepare-results'   => \$prepare_results,
    '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,
    'help'              => \$help
);

&usage() if $help;

exit &prepare_results() if $prepare_results;

&setup();
&parse_access_conf($default_access_conf);

&logr("\n[+] ==> Running fwknop test suite; " .
    "firewall: $config{'FIREWALL_TYPE'} <==\n\n");

### main tests
&test_driver('(Setup) perl program compilation', \&perl_compilation);
&test_driver('(Setup) C program compilation', \&C_compilation);
&test_driver('(Setup) Command line argument processing', \&getopt_test);
&test_driver("(Setup) List $config{'FIREWALL_TYPE'} rules", \&fw_list);
&test_driver("(Setup) System information and fwknop installation specifics",
    \&specs);
&test_driver('(Setup) Stopping any running fwknopd processes',
    \&stop_fwknopd);

if ($config{'FIREWALL_TYPE'} eq 'iptables') {
    &test_driver('(Setup) Flushing all fwknopd iptables rules',
        \&flush_iptables);
    &test_driver('(Setup) Deleting all fwknopd iptables chains',
        \&del_ipt_chains);
    ### add an ACCEPT rule for udp 62201 communications over the
    ### loopback interface
    my $del_ctr = 0;
    while (((system('iptables -D INPUT -i lo -p udp --dport 62201 ' .
            '-j ACCEPT > /dev/null 2>&1')) >> 8) == 0) {
        $del_ctr++;
        last if $del_ctr >= 5;
    }
    system 'iptables -I INPUT 1 -i lo -p udp --dport 62201 ' .
        '-j ACCEPT > /dev/null 2>&1';
}
&test_driver('(Basic communications) Generating SPA access packet',
    \&SPA_access_packet);
&test_driver('(Basic communications) Sniffing SPA access packet',
    \&sniff_decrypt);
&test_driver('(Basic communications) Verifying SPA access packet format',
    \&spa_access_format);
&test_driver('(Basic communications) Firewall access rules exist',
    \&fw_rules_exist);
&fw_sleep();  ### give knoptm a chance to remove the rules
&test_driver('(Basic communications) Firewall access rules removed',
    \&fw_rules_removed);
&test_driver('(Basic communications) Stopping all running fwknopd processes',
    \&stop_fwknopd);

&test_driver('(Replay, randomness, broken data) Rijndael key validity',
    \&short_key);
&test_driver('(Replay, randomness, broken data) Replay attack detection',
    \&replay_attack);
&stop_fwknopd_quiet();
&test_driver("(Replay, randomness, broken data) $NUM_RAND random packets",
    \&packet_randomness);
&test_driver('(Replay, randomness, broken data) Truncated SPA packet',
    \&truncated_SPA_packet);
&test_driver('(Replay, randomness, broken data) Sniffing truncated SPA packet',
    \&truncated_SPA_sniff_decrypt);
&test_driver('(Replay, randomness, broken data) Firewall rules do not exist',
    \&fw_rules_removed);
&stop_fwknopd_quiet();
&test_driver('(Replay, randomness, broken data) SPA packet with bogus key',
    \&bogus_key_SPA_packet);
&test_driver('(Replay, randomness, broken data) Sniffing broken SPA packet',
    \&bogus_SPA_sniff_decrypt);
&test_driver('(Replay, randomness, broken data) Firewall rules do not exist',
    \&fw_rules_removed);
&stop_fwknopd_quiet();

### It is ok to append data in the current code since the Rijndael decrypt
### only returns the actual SPA payload (may need to revist this)
&test_driver('(Append data) Data appended to SPA packet', \&append_SPA_packet);
&test_driver('(Append data) Sniffing appended SPA packet',
    \&append_SPA_sniff_decrypt);
&test_driver('(Append data) Firewall rules exist', \&fw_rules_exist);
&stop_fwknopd_quiet();

### non-promiscuous capture
&test_driver('(Non-promisc capture) Generating SPA access packet',
    \&SPA_access_packet);
&test_driver('(Non-promisc capture) Sniffing SPA access packet',
    \&no_promisc_sniff_decrypt);
&test_driver('(Non-promisc capture) Verifying sniffed SPA access packet',
    \&spa_access_format);
&test_driver('(Non-promisc capture) Firewall access rules exist',
    \&fw_rules_exist);
&fw_sleep();  ### give knoptm a chance to remove the rules
&test_driver('(Non-promisc capture) Firewall access rules removed',
    \&fw_rules_removed);
&test_driver('(Non-promisc capture) Stopping all fwknopd processes',
    \&stop_fwknopd);

### SPA packet aging
&test_driver('(SPA aging) Generating SPA access packet',
    \&SPA_access_packet);
&test_driver('(SPA aging) Expired SPA packet detection',
    \&sniff_old_packet);
&test_driver('(SPA aging) Making sure firewall rules do not exist',
    \&fw_rules_removed);
&stop_fwknopd_quiet();

if ($require_source_addr) {
    &test_driver('(Require SRC) Generating SPA packet with 0.0.0.0 src addr',
        \&source_addr);
    &test_driver('(Require SRC) Sniffing packet with 0.0.0.0 src addr',
        \&sniff_source_addr);
    &test_driver('(Require SRC) Making sure firewall rules do not exist',
        \&fw_rules_removed);
    &stop_fwknopd_quiet();
}

if ($require_user) {
    &test_driver('(Require user) Generating SPA packet with unauthorized user',
        \&unauthorized_user);
    &test_driver('(Require user) Unauthorized user detection',
        \&sniff_unauthorized_user);
    &test_driver('(Require user) Making sure firewall rules do not exist',
        \&fw_rules_removed);
    &stop_fwknopd_quiet();
}

unless ($permit_client_ports) {
    ### the default access.conf does not allow client-requested ports
    &test_driver('(Permit ports) Generating unauthorized port access request',
        \&unauthorized_port_request);
    &test_driver('(Permit ports) Unauthorized port access detection',
        \&sniff_unauthorized_port_request);
    &test_driver('(Permit ports) Making sure firewall rules do not exist',
        \&fw_rules_removed);
    &stop_fwknopd_quiet();
}

### non-matching SOURCE test
&test_driver('(Bogus src) Generating SPA packet from non-matching src',
    \&non_matching_source_generation);
&test_driver('(Bogus src) Verifying SPA access packet format',
    \&spa_access_format);
&test_driver('(Bogus src) Non-matching SOURCE block',
    \&non_matching_source_block);
&test_driver('(Bogus src) Making sure firewall rules do not exist',
    \&fw_rules_removed);
&stop_fwknopd_quiet();

### test multi-SOURCE access.conf configuration
&test_driver('(Multi-SOURCE) Generating SPA access packet',
    \&SPA_access_packet);
&test_driver('(Multi-SOURCE) Sniffing SPA access packet',
    \&sniff_decrypt_multi_source);
&test_driver('(Multi-SOURCE) Verifying SPA access packet format',
    \&spa_access_format);
&test_driver('(Multi-SOURCE) Firewall access rules exist',
    \&fw_rules_exist);
&fw_sleep();  ### give knoptm a chance to remove the rules
&test_driver('(Multi-SOURCE) Firewall access rules removed',
    \&fw_rules_removed);
&test_driver('(Multi-SOURCE) Stopping running fwknopd processes',
    \&stop_fwknopd);

if (-e $gpgCmd and -x $gpgCmd) {
    &test_driver('(GnuPG) Generating SPA access packet',
        \&get_gpg_access_packet);
    &test_driver('(GnuPG) Sniffing SPA access packet to acquire access',
        \&gpg_sniff_decrypt);
    &test_driver('(GnuPG) Verifying sniffed SPA access packet format',
        \&spa_access_format);
    &test_driver('(GnuPG) Firewall access rules exist', \&fw_rules_exist);
    &fw_sleep();  ### give knoptm a chance to remove the rules
    &test_driver('(GnuPG) Firewall access rules removed', \&fw_rules_removed);
    &test_driver('(GnuPG) Stopping all running fwknopd processes', \&stop_fwknopd);
}

### test SPA command execution instead of access requests
&test_driver('(Command execution) Generating SPA command packet',
    \&SPA_command_packet);
&test_driver('(Command execution) Sniffing SPA command packet and executing',
    \&fwknopd_command);
&test_driver('(Command execution) Verifying SPA command packet format',
    \&spa_command_format);
&test_driver('(Command execution) Making sure firewall rules do not exist',
    \&fw_rules_removed);

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

if ($config{'FIREWALL_TYPE'} eq 'iptables') {

    ### test FORWARD chain support and DNAT connections
    &test_driver('(FORWARD chain) Stopping all running fwknopd processes',
        \&stop_fwknopd);
    &test_driver('(FORWARD chain) Generating FORWARD chain access packet',
        \&SPA_forward_access_packet);
    &test_driver('(FORWARD chain) FORWARD request detection',
        \&forward_request);
    &test_driver('(FORWARD chain) FORWARD and DNAT access rules',
        \&forward_access);
    &test_driver('(FORWARD chain) Verifying SPA FORWARD access packet format',
        \&spa_forward_access_format);
    &fw_sleep();  ### give knoptm a chance to remove the rules
    &test_driver('(FORWARD chain) Making sure firewall rules are removed',
        \&fw_rules_removed);

    ### test OUTPUT chain support
    &test_driver('(OUTPUT chain) Stopping all running fwknopd processes',
        \&stop_fwknopd);
    &test_driver('(OUTPUT chain) Generating OUTPUT chain access packet',
        \&SPA_output_access_packet);
    &test_driver('(OUTPUT chain) OUTPUT access rules', \&output_access);
    &test_driver('(OUTPUT chain) Verifying OUTPUT access packet format',
        \&spa_output_access_format);
    &fw_sleep();  ### give knoptm a chance to remove the rules
    &test_driver('(OUTPUT chain) Making sure firewall rules are 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 with Net::Pcap 0.05
if (-e $tcpdumpCmd and -x $tcpdumpCmd) {

    ### see if tcpdump can sniff over the loopback interface
    &test_driver("(Filesystem tcpdump capture) Sniffing over $loopback_intf",
        \&tcpdump_over_loopback);

    if ($config{'FIREWALL_TYPE'} eq 'iptables') {

        &test_driver('(Filesystem tcpdump capture) Stopping fwknopd processes',
            \&stop_fwknopd);
        &get_sniff_file();
        &start_tcpdump();
        sleep 1;

        &test_driver('(Filesystem tcpdump capture) Generating SPA packet',
            \&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('(Filesystem tcpdump capture) SPA communications via file',
            \&filesystem_capture);

        &test_driver('(Filesystem tcpdump capture) Firewall access rules exist',
            \&fw_rules_exist);
        &fw_sleep();  ### give knoptm a chance to remove the rules
        &test_driver('(Filesystem tcpdump capture) 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);
    my $del_ctr = 0;
    while (((system('iptables -D INPUT -i lo -p udp --dport 62201 ' .
            '-j ACCEPT > /dev/null 2>&1')) >> 8) == 0) {
        $del_ctr++;
        last if $del_ctr >= 5;
    }
}
&test_driver('Verifying SPA digest file format', \&digest_format);
&test_driver('Collecting fwknop syslog messages', \&fwknop_syslog);

&logr("\n");
if ($successful_tests) {
    &logr("[+] ==> Passed $successful_tests/$test_num tests " .
        "against fwknop. <==\n");
}
if ($failed_tests) {
    &logr("[+] ==> Failed $failed_tests/$test_num tests " .
        "against fwknop. <==\n");
}
&logr("[+] This console output has been stored in: $logfile\n\n");
exit 0;
#======================== end main =========================

sub test_driver() {
    my ($msg, $func_ref) = @_;

    &dots_print($msg);
    if (&{$func_ref}) {
        &pass();
    } else {
        $failed_tests++;
    }
    $test_num++;
    return;
}

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

    my $rv = 1;

    &parse_access_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);

    local $SIG{'ALRM'} = sub {die "[*] Sniff packet alarm.\n"};
    ### on some systems and libpcap combinations, it is possible for fwknopd
    ### to not receive packet data, so setting an alarm allows us to recover
    alarm $sniff_alarm;
    eval {
        ### fwknopd will exit after receiving the cached packet (--Count 1)
        waitpid($fwknopd_pid, 0);
    };
    alarm 0;
    if ($@) {
        kill 9, $fwknopd_pid unless kill 15, $fwknopd_pid;
        $rv = 0;
    }

    return $rv;
}

sub fw_list() {
    if (&run_cmd("$fwknopdCmd --fw-list -v")) {
        return 1;
    }
    return &print_errors("fail ($test_num)\n[*] Could " .
        "not execute $fwknopdCmd --fw-list -v");
}

sub specs() {
    open F, "> ${cmd_stdout}.$test_num" or die $!;
    close F;
    open F, "> ${cmd_stderr}.$test_num" or die $!;
    close F;
    if ($config{'FIREWALL_TYPE'} eq 'iptables') {
        my $cmd = "iptables -v -n -L";
        system qq{echo "# $cmd" >> } .
            "${cmd_stdout}.$test_num 2>> ${cmd_stderr}.$test_num";
        system "$cmd >> ${cmd_stdout}.$test_num 2>> ${cmd_stderr}.$test_num";
    } else {
        my $cmd = "ipfw list";
        system qq{echo "# $cmd" >> } .
            "${cmd_stdout}.$test_num 2>> ${cmd_stderr}.$test_num";
        system "$cmd >> ${cmd_stdout}.$test_num 2>> ${cmd_stderr}.$test_num";
    }
    for my $cmd ('uname -a', 'perl -V', 'ifconfig -a',
            'ls -l /etc', 'if [ -e /etc/issue ]; then cat /etc/issue; fi',
            'if [ -e /proc/cpuinfo ]; then cat /proc/cpuinfo; fi',
            "$fwknopdCmd -S", "$fwknopdCmd -V",
            "$fwknopCmd -V", "ldd $tcpdumpCmd",
            'find /usr/lib/fwknop -type f') {
        system qq{echo "# $cmd" >> } .
            "${cmd_stdout}.$test_num 2>> ${cmd_stderr}.$test_num";
        system "$cmd >> ${cmd_stdout}.$test_num 2>> ${cmd_stderr}.$test_num";
    }
    return 1;
}

sub fwknop_syslog() {
    my $cmd = 'grep fwknop /var/log/* |tail -n 5000';
    if (&run_cmd($cmd)) {
        return 1;
    }
    return &print_errors("fail ($test_num)\n[*] Could not execute $cmd");
}

sub filesystem_capture() {

    if (&run_fwknopd($cache_encrypted_Rijndael_spa_packet,
            $pcap_file_fwknop_conf, $default_access_conf)) {
        return 1;
    }
    return &print_errors("fail ($test_num)\n[*] Sniff alarm " .
            "($sniff_alarm seconds) expired");
}

sub sniff_old_packet() {

    sleep 5;  ### FIXME, should acquire this from $spa_aging_fwknop_conf

    if (&run_fwknopd($cache_encrypted_Rijndael_spa_packet,
            $spa_aging_fwknop_conf, $default_access_conf)) {

        ### now that fwknopd has exited, see if the replay was detected
        my $found_old_packet = 0;
        open SE, "< ${cmd_stderr}.$test_num"
            or die "[*] Could not open ${cmd_stderr}.$test_num: $!";
        while (<SE>) {
            if (/remote\s+time\s+stamp\s+is\s+older/i) {
                $found_old_packet = 1;
                last;
            }
        }
        close SE;

        if ($found_old_packet) {
            return 1;
        } else {
            return &print_errors("fail ($test_num)\n[*] fwknopd " .
                    "accepted old SPA packet");
        }
    }
    return &print_errors("fail ($test_num)\n[*] Sniff alarm " .
            "($sniff_alarm seconds) expired");
}

sub replay_attack() {

    if (&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}.$test_num"
            or die "[*] Could not open ${cmd_stderr}.$test_num: $!";
        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;

        if ($found_replay) {
            return 1;
        } else {
            return &print_errors("fail ($test_num)\n[*] Replay " .
                    "attack not detected");
        }
    }
    return &print_errors("fail ($test_num)\n[*] Sniff alarm " .
            "($sniff_alarm seconds) expired");
}

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

sub non_matching_source_block() {

    if (&run_fwknopd($cache_encrypted_Rijndael_spa_packet,
            $default_fwknop_conf, $no_loopback_ip_match_access_conf)) {

        ### now that fwknopd has exited, see if the SPA packet was valid
        my $found_no_ip_match = 0;
        open SE, "< ${cmd_stderr}.$test_num"
            or die "[*] Could not open ${cmd_stderr}.$test_num: $!";
        while (<SE>) {
            if (/did\s+not\s+match\s+any\s+SOURCE/) {
                $found_no_ip_match = 1;
                last;
            }
        }
        close SE;
        unless ($found_no_ip_match) {
            return &print_errors("fail ($test_num)\n[*] fwknopd " .
                "accepted SPA packet with no matching SOURCE block");
        }
        return 1;
    }
    return &print_errors("fail ($test_num)\n[*] Sniff alarm " .
            "($sniff_alarm seconds) expired");
}

sub fw_rules_removed() {
    if (&fw_rules()) {
        ### forcibly delete the rules so that the next test
        ### will not also fail if the rules are not supposed to
        ### be there.
        &flush_quiet();
        return &print_errors("fail ($test_num)\n[*] SPA access " .
                "rules for $spa_src not removed.");
    }
    return 1;
}

sub flush_quiet() {
    if ($config{'FIREWALL_TYPE'} eq 'iptables') {
        system "$fwknopdCmd --fw-flush > /dev/null 2>&1";
    } else {
        system "$fwknopdCmd --fw-del-ip $spa_src > /dev/null 2>&1";
    }
    return;
}

sub tcpdump_over_loopback() {
    &get_sniff_file();
    &start_tcpdump();
    sleep 1;
    &send_packet('somedatastring');
    sleep 1;
    &send_packet('somedatastring');
    sleep 1;

    if (not -e $sniff_file or -s $sniff_file == 0) {
        return &print_errors("fail ($test_num)\n[*] tcpdump " .
            "could not acquire packet data over $loopback_intf interface");
    }
    return 1;
}

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

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

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

    my $pid_file_msg_ctr = 0;
    open F, "< ${cmd_stdout}.$test_num"
        or die "[*] Could not open ${cmd_stdout}.$test_num: $!";
    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) {
        return &print_errors("fail ($test_num)\n[*] All " .
            "three fwknopd processes are not stopped");
    }
    return 1;
}

sub stop_fwknopd_quiet() {
    &flush_quiet();
    system "$fwknopdCmd --Kill > /dev/null 2>&1";
    return;
}

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

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

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

sub gpg_sniff_decrypt() {

    $fwknopd_output_file = "${cmd_stderr}.$test_num";

    if (&run_fwknopd($cache_encrypted_Rijndael_spa_packet,
            $default_fwknop_conf, $gpg_access_conf)) {

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

sub sniff_decrypt() {

    $fwknopd_output_file = "${cmd_stderr}.$test_num";

    if (&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, "< $fwknopd_output_file"
            or die "[*] Could not open $fwknopd_output_file: $!";
        while (<SE>) {
            if (/\[\-\]\s+Key\s+mis\-?match/i) {
                ### [-] Key mis-match or broken message checksum for SOURCE \
                ### ANY (# 1 in access.conf)
                return &print_errors("fail ($test_num)\n[*] " .
                    "Key mis-match");
            } elsif (/\[\-\]\s+Decrypted.*not\s+conform/i) {
                ### [-] Decrypted message does not conform to a valid SPA packet
                return &print_errors("fail ($test_num)\n[*] " .
                        "Invalid SPA packet");
            }
        }
        close SE;
        return 1;
    }
    return &print_errors("fail ($test_num)\n[*] Sniff alarm " .
            "($sniff_alarm seconds) expired");
}

sub no_promisc_sniff_decrypt() {

    $fwknopd_output_file = "${cmd_stderr}.$test_num";

    if (&run_fwknopd($cache_encrypted_Rijndael_spa_packet,
            $no_promisc_fwknop_conf, $default_access_conf)) {

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

        if ($found_no_promisc) {
            return 1;
        } else {
            return &print_errors("fail ($test_num)\n[*] Could not " .
                    "sniff in non-promisc mode");
        }
    }
    return &print_errors("fail ($test_num)\n[*] Sniff alarm " .
            "($sniff_alarm seconds) expired");
}

sub sniff_decrypt_multi_source() {

    $fwknopd_output_file = "${cmd_stderr}.$test_num";

    if (&run_fwknopd($cache_encrypted_Rijndael_spa_packet,
            $default_fwknop_conf, $multi_source_access_conf)) {

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

sub spa_output_access_format() {
    ### Random data:   1946117908964953
    ### Username:      root
    ### Remote time:   1197922462
    ### Remote ver:    1.9.0
    ### Action type:   1 (SPA_ACCESS_MODE)
    ### Action:        127.0.0.2,none,0
    ### MD5 sum:       DLnVQDc5XTkazUbeJX14Og
    my $valid_lines = 0;
    open F, "< $fwknopd_output_file" or die "[*] Could not open ",
        "$fwknopd_output_file: $!";
    while (<F>) {
        if (/^\s+Random\s+data:\s+\d+/i) {
            $valid_lines++;
        } elsif (/^\s+Username:\s+\w+$/i) {
            $valid_lines++;
        } elsif (/^\s+Remote\s+time:\s+\d+$/i) {
            $valid_lines++;
        } elsif (/^\s+Remote\s+ver:\s+(\S+)$/i) {
            $valid_lines++;
        } elsif (/^\s+Action\s+type:\s+$SPA_ACCESS_MODE\s+/i) {
            $valid_lines++;
        } elsif (/^\s+Action:\s+$ip_re/i) {
            $valid_lines++;
        } elsif (/^\s+MD5\s+sum:\s+\S+$/i) {
            $valid_lines++;
        }
    }
    close F;
    unless ($valid_lines == 7) {
        return &print_errors("fail ($test_num)\n[*] Dubious " .
            "sniffed packet format");
    }
    return 1;
}

sub spa_access_format() {
    ### Random data:   1946117908964953
    ### Username:      root
    ### Remote time:   1197922462
    ### Remote ver:    1.9.0
    ### Action type:   1 (SPA_ACCESS_MODE)
    ### Action:        127.0.0.2,none,0
    ### MD5 sum:       DLnVQDc5XTkazUbeJX14Og
    my $valid_lines = 0;
    open F, "< $fwknopd_output_file" or die "[*] Could not open ",
        "$fwknopd_output_file: $!";
    while (<F>) {
        if (/^\s+Random\s+data:\s+\d+/i) {
            $valid_lines++;
        } elsif (/^\s+Username:\s+\w+$/i) {
            $valid_lines++;
        } elsif (/^\s+Remote\s+time:\s+\d+$/i) {
            $valid_lines++;
        } elsif (/^\s+Remote\s+ver:\s+(\S+)$/i) {
            $valid_lines++;
        } elsif (/^\s+Action\s+type:\s+$SPA_ACCESS_MODE\s+/i) {
            $valid_lines++;
        } elsif (/^\s+Action:\s+$ip_re/i) {
            $valid_lines++;
        } elsif (/^\s+MD5\s+sum:\s+\S+$/i) {
            $valid_lines++;
        }
    }
    close F;
    unless ($valid_lines == 7) {
        return &print_errors("fail ($test_num)\n[*] Dubious " .
            "sniffed packet format");
    }
    return 1;
}

sub spa_command_format() {
    ### Random data:   6248973224583174
    ### Username:      root
    ### Remote time:   1197928728
    ### Remote ver:    1.9.0
    ### Action type:   0 (SPA_COMMAND_MODE)
    ### Action:        127.0.0.2,echo fwknoptest > /tmp/fwknop_test.txt
    ### MD5 sum:       LVZ44rzSbyy28TidYx1C1Q
    my $valid_lines = 0;
    open F, "< $fwknopd_output_file" or die "[*] Could not open ",
        "$fwknopd_output_file: $!";
    while (<F>) {
        if (/^\s+Random\s+data:\s+\d+/i) {
            $valid_lines++;
        } elsif (/^\s+Username:\s+\w+$/i) {
            $valid_lines++;
        } elsif (/^\s+Remote\s+time:\s+\d+$/i) {
            $valid_lines++;
        } elsif (/^\s+Remote\s+ver:\s+(\S+)$/i) {
            $valid_lines++;
        } elsif (/^\s+Action\s+type:\s+$SPA_COMMAND_MODE\s+/i) {
            $valid_lines++;
        } elsif (/^\s+Action:\s+$ip_re/i) {
            $valid_lines++;
        } elsif (/^\s+MD5\s+sum:\s+\S+$/i) {
            $valid_lines++;
        }
    }
    close F;
    unless ($valid_lines == 7) {
        return &print_errors("fail ($test_num)\n[*] Dubious " .
            "sniffed packet format");
    }
    return 1;
}

sub spa_forward_access_format() {

    ### Random data:   1005424690238209
    ### Username:      root
    ### Remote time:   1197927488
    ### Remote ver:    1.9.0
    ### Action type:   2 (SPA_FORWARD_ACCESS_MODE)
    ### Action:        127.0.0.2,none,0
    ### Forward info:  192.168.10.3,55000
    ### MD5 sum:       uA9Z75Xj2G0QW+iBPfNffA
    my $valid_lines = 0;
    open F, "< $fwknopd_output_file" or die "[*] Could not open ",
        "$fwknopd_output_file: $!";
    while (<F>) {
        if (/^\s+Random\s+data:\s+\d+/i) {
            $valid_lines++;
        } elsif (/^\s+Username:\s+\w+$/i) {
            $valid_lines++;
        } elsif (/^\s+Remote\s+time:\s+\d+$/i) {
            $valid_lines++;
        } elsif (/^\s+Remote\s+ver:\s+(\S+)$/i) {
            $valid_lines++;
        } elsif (/^\s+Action\s+type:\s+$SPA_FORWARD_ACCESS_MODE\s+/i) {
            $valid_lines++;
        } elsif (/^\s+Action:\s+$ip_re/i) {
            $valid_lines++;
        } elsif (/^\s+Forward\s+info:\s+$ip_re,\d+$/i) {
            $valid_lines++;
        } elsif (/^\s+MD5\s+sum:\s+\S+$/i) {
            $valid_lines++;
        }
    }
    close F;
    unless ($valid_lines == 8) {
        return &print_errors("fail ($test_num)\n[*] Dubious " .
            "sniffed packet format");
    }
    return 1;
}

sub forward_request() {

    if (&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}.$test_num"
            or die "[*] Could not open ${cmd_stderr}.$test_num: $!";
        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) {
            return &print_errors("fail ($test_num)\n[*] FORWARD access " .
                "request packet should have been detected and not allowed");
        }
        return 1;
    }
    return &print_errors("fail ($test_num)\n[*] Sniff alarm " .
            "($sniff_alarm seconds) expired");
}

sub output_access() {

    $fwknopd_output_file = "${cmd_stderr}.$test_num";

    if (&run_fwknopd($cache_encrypted_Rijndael_spa_packet,
            $output_fwknop_conf, $output_access_conf)) {

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

sub forward_access() {

    $fwknopd_output_file = "${cmd_stderr}.$test_num";

    if (&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, "< $fwknopd_output_file"
            or die "[*] Could not open $fwknopd_output_file: $!";
        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) {
            return &print_errors("fail ($test_num)\n[*] FORWARD and DNAT " .
                "access not granted");
        }
        return 1;
    }
    return &print_errors("fail ($test_num)\n[*] Sniff alarm " .
            "($sniff_alarm seconds) expired");
}

sub fwknopd_command() {

    unlink $test_cmd_file if -e $test_cmd_file;
    $fwknopd_output_file = "${cmd_stderr}.$test_num";

    if (&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, "< $fwknopd_output_file"
            or die "[*] Could not open $fwknopd_output_file: $!";
        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) {
            return &print_errors("fail ($test_num)\n[*] " .
                "Command not executed by fwknopd");
        }
        return 1;
    }
    return &print_errors("fail ($test_num)\n[*] Sniff alarm " .
            "($sniff_alarm seconds) expired");
}

sub fwknopd_no_re_command() {

    unlink $test_cmd_file if -e $test_cmd_file;

    if (&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}.$test_num"
            or die "[*] Could not open ${cmd_stderr}.$test_num: $!";
        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) {
            return &print_errors("fail ($test_num)\n[*] Command " .
                "executed by fwknopd but should have been filtered");
        }
        return 1;
    }
    return &print_errors("fail ($test_num)\n[*] Sniff alarm " .
            "($sniff_alarm seconds) expired");
}

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

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 --verbose --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() {
    return &get_access_packet();
}

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

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

sub SPA_output_access_packet() {
    return &get_output_access_packet();
}

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

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

sub unauthorized_port_request() {
    my $port_copy = $open_ports;
    if ($open_ports =~ /(\d+)/) {
        my $port_num = $1;
        $unauth_port = $port_num + 1;
        $open_ports =~ s|$port_num|$unauth_port|;
    }
    die "[*] Could not assign valid unauthorized port" unless $unauth_port;
    my $rv = &get_access_packet();
    $open_ports = $port_copy;
    return $rv;
}

sub sniff_unauthorized_port_request() {

    if (&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_non_permit = 0;
        open SE, "< ${cmd_stderr}.$test_num"
            or die "[*] Could not open ${cmd_stderr}.$test_num: $!";
        while (<SE>) {
            if (/IP.*not\s+permitted\s+to\s+open\s/) {
                $found_non_permit = 1;
                last;
            }
        }
        close SE;
        unless ($found_non_permit) {
            return &print_errors("fail ($test_num)\n[*] fwknopd " .
                "accepted SPA packet with unauthorized port request");
        }
        return 1;
    }
    return &print_errors("fail ($test_num)\n[*] Sniff alarm " .
            "($sniff_alarm seconds) expired");
}

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

sub sniff_unauthorized_user() {

    if (&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}.$test_num"
            or die "[*] Could not open ${cmd_stderr}.$test_num: $!";
        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) {
            return &print_errors("fail ($test_num)\n[*] fwknopd " .
                "accepted SPA packet with unauthorized user");
        }
        return 1;
    }
    return &print_errors("fail ($test_num)\n[*] Sniff alarm " .
            "($sniff_alarm seconds) expired");
}

sub truncated_SPA_packet() {
    my $rv = &get_access_packet();
    ### chop off the last 10 chars
    $cache_encrypted_Rijndael_spa_packet =~ s|.{10}$||;
    return $rv;
}

sub append_SPA_packet() {
    my $rv = &get_access_packet();
    ### append 10 garbage chars
    $cache_encrypted_Rijndael_spa_packet .= '1234567890';
    return $rv;
}

sub truncated_SPA_sniff_decrypt() {

    $fwknopd_output_file = "${cmd_stderr}.$test_num";

    if (&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_err = 0;
        open SE, "< $fwknopd_output_file"
            or die "[*] Could not open $fwknopd_output_file: $!";
        while (<SE>) {
            if (/\[\-\]\s+Key\s+mis\-?match/i) {
                ### [-] Key mis-match or broken message checksum for SOURCE \
                ### ANY (# 1 in access.conf)
                $found_err = 1;
                last; 
                return &print_errors("fail ($test_num)\n[*] " .
                    "Key mis-match");
            } elsif (/\[\-\]\s+Decrypted.*not\s+conform/i) {
                ### [-] Decrypted message does not conform to a valid SPA packet
                return &print_errors("fail ($test_num)\n[*] " .
                        "Invalid SPA packet");
                $found_err = 1;
                last; 
            }
        }
        close SE;
        if ($found_err) {
            return 1;
        } else {
            return &print_errors("fail ($test_num)\n[*] " .
                    "fwknopd accepted truncated SPA packet");
        }
    }
    return &print_errors("fail ($test_num)\n[*] Sniff alarm " .
            "($sniff_alarm seconds) expired");
}

sub append_SPA_sniff_decrypt() {

    $fwknopd_output_file = "${cmd_stderr}.$test_num";

    if (&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, "< $fwknopd_output_file"
            or die "[*] Could not open $fwknopd_output_file: $!";
        while (<SE>) {
            if (/\[\-\]\s+Key\s+mis\-?match/i) {
                ### [-] Key mis-match or broken message checksum for SOURCE \
                ### ANY (# 1 in access.conf)
                return &print_errors("fail ($test_num)\n[*] " .
                    "Key mis-match");
            } elsif (/\[\-\]\s+Decrypted.*not\s+conform/i) {
                ### [-] Decrypted message does not conform to a valid SPA packet
                return &print_errors("fail ($test_num)\n[*] " .
                        "Invalid SPA packet");
            }
        }
        close SE;
        return 1;
    }
    return &print_errors("fail ($test_num)\n[*] Sniff alarm " .
            "($sniff_alarm seconds) expired");
}

sub bogus_SPA_sniff_decrypt() {

    $fwknopd_output_file = "${cmd_stderr}.$test_num";

    if (&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_err = 0;
        open SE, "< $fwknopd_output_file"
            or die "[*] Could not open $fwknopd_output_file: $!";
        while (<SE>) {
            if (/\[\-\]\s+Key\s+mis\-?match/i) {
                ### [-] Key mis-match or broken message checksum for SOURCE \
                ### ANY (# 1 in access.conf)
                $found_err = 1;
                last; 
                return &print_errors("fail ($test_num)\n[*] " .
                    "Key mis-match");
            } elsif (/\[\-\]\s+Decrypted.*not\s+conform/i) {
                ### [-] Decrypted message does not conform to a valid SPA packet
                return &print_errors("fail ($test_num)\n[*] " .
                        "Invalid SPA packet");
                $found_err = 1;
                last; 
            }
        }
        close SE;
        if ($found_err) {
            return 1;
        } else {
            return &print_errors("fail ($test_num)\n[*] " .
                    "fwknopd accepted SPA packet with bogus key");
        }
    }
    return &print_errors("fail ($test_num)\n[*] Sniff alarm " .
            "($sniff_alarm seconds) expired");
}

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

sub sniff_source_addr() {

    if (&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}.$test_num"
            or die "[*] Could not open ${cmd_stderr}.$test_num: $!";
        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) {
            return &print_errors("fail ($test_num)\n[*] fwknopd " .
                "accepted SPA packet with 0.0.0.0 source address");
        }
        return 1;
    }
    return &print_errors("fail ($test_num)\n[*] Sniff alarm " .
            "($sniff_alarm seconds) expired");
}

sub packet_randomness() {

    my %packet_cache = ();

    for (my $i=0; $i < $NUM_RAND; $i++) {

        &get_access_packet();

        if (defined $packet_cache{$cache_encrypted_Rijndael_spa_packet}) {
            return &print_errors("fail ($test_num)\n[*] Packet $i is " .
                "identical to a previously generated SPA packet; " .
                "rand() not working?");
        } else {
            $packet_cache{$cache_encrypted_Rijndael_spa_packet} = '';
        }
    }
    return 1;
}

sub parse_access_conf() {
    my $file = shift;
    ### parse the Rijndael key from the test_access.conf file (this same
    ### will be used by fwknopd to decrypt the SPA packet)
    open F, "< $file" or
        die "[*] Could not open $file: $!";
    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*PERMIT_CLIENT_PORTS:\s*Y\s*;/) {
            $permit_client_ports = 1;
        } elsif (/^\s*OPEN_PORTS:\s*(\S+)\s*;/) {
            $open_ports = $1;
        } elsif (/^\s*CMD_REGEX:\s*(.*)\s*;/) {
            $cmd_regex = qr|$1|;
        }
    }
    close F;
    return;
}

sub get_gpg_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 -A $open_ports --no-save --get-key " .
            "$local_key_file --gpg-home conf/client-gpg " .
            "--gpg-recip 361BBAD4 --gpg-sign 6A3FAD56 -D $spa_dst " .
            "-a $spa_src --Test -v --Spoof-user $require_user")) {
        return &print_errors("fail ($test_num)\n[*] Could not " .
                "generate encrypted SPA packet");
    }
    open F, "< ${cmd_stdout}.$test_num"
        or die "[*] Could not open ${cmd_stdout}.$test_num: $!";
    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) {
        return &print_errors("fail ($test_num)\n[*] Could not generate " .
            "encrypted SPA packet");
    }
    return 1;
}

sub bogus_key_SPA_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: someboguskey\n";
    close K;

    unless (&run_cmd("$fwknopCmd -A $open_ports --no-save --get-key " .
            "$local_key_file -D $spa_dst -a $spa_src --Test -v " .
            "--Spoof-user $require_user")) {
        return &print_errors("fail ($test_num)\n[*] Could not " .
                "generate encrypted SPA packet");
    }
    open F, "< ${cmd_stdout}.$test_num"
        or die "[*] Could not open ${cmd_stdout}.$test_num: $!";
    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) {
        return &print_errors("fail ($test_num)\n[*] Could not generate " .
            "encrypted SPA packet");
    }
    return 1;
}

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 -A $open_ports --no-save --get-key " .
            "$local_key_file -D $spa_dst -a $spa_src --Test -v " .
            "--Spoof-user $require_user")) {
        return &print_errors("fail ($test_num)\n[*] Could not " .
                "generate encrypted SPA packet");
    }
    open F, "< ${cmd_stdout}.$test_num"
        or die "[*] Could not open ${cmd_stdout}.$test_num: $!";
    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) {
        return &print_errors("fail ($test_num)\n[*] Could not generate " .
            "encrypted SPA packet");
    }
    return 1;
}

sub get_output_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 -A $open_ports --no-save --get-key " .
            "$local_key_file -D $spa_dst -a $spa_src --Test -v " .
            "--Spoof-user $require_user ")) {
        return &print_errors("fail ($test_num)\n[*] Could not generate " .
                "encrypted SPA packet");
    }
    open F, "< ${cmd_stdout}.$test_num"
        or die "[*] Could not open ${cmd_stdout}.$test_num: $!";
    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) {
        return &print_errors("fail ($test_num)\n[*] Could not generate " .
                "encrypted SPA packet");
    }
    return 1;
}

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 -A $open_ports --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")) {
        return &print_errors("fail ($test_num)\n[*] Could not generate " .
                "encrypted SPA packet");
    }
    open F, "< ${cmd_stdout}.$test_num"
        or die "[*] Could not open ${cmd_stdout}.$test_num: $!";
    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) {
        return &print_errors("fail ($test_num)\n[*] Could not generate " .
                "encrypted SPA packet");
    }
    return 1;
}

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 -A $open_ports --no-save --get-key " .
            "$local_key_file -D $spa_dst -a $spa_src --Test -v " .
            "--Spoof-user $require_user")) {
        return &print_errors("fail ($test_num)\n[*] Generated " .
                "SPA packet with invalid key");
    }

    my $packet_test = '';
    open F, "< ${cmd_stdout}.$test_num"
        or die "[*] Could not open ${cmd_stdout}.$test_num: $!";
    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) {
        return &print_errors("fail ($test_num)\n[*] fwknop " .
            "generated SPA packet even though the Rijndael key is too short");
    }
    return 1;
}

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"|)) {
        return &print_errors("fail ($test_num)\n[*] Could not " .
            "generate encrypted SPA command packet");
    }
    open F, "< ${cmd_stdout}.$test_num"
        or die "[*] Could not open ${cmd_stdout}.$test_num: $!";
    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) {
        return &print_errors("fail ($test_num)\n[*] Could not " .
                "generate encrypted SPA packet");
    }
    return 1;
}

sub digest_format() {
    return &print_errors("fail ($test_num)\n[*] Digest file " .
            "$output_dir/md5sums does not exist")
            unless -e "$output_dir/md5sums";
    open F, "$output_dir/md5sums" or die "[*] Could not open " .
        "$output_dir/md5sums";
    my $error = 0;
    while (<F>) {
        ### 127.0.0.1 xFTZnnstv1GPHD7bZDVJ2A [Sat Jan 12 20:55:24 2008]
        unless (/^\s*$ip_re\s+\S+\s+\[.*\]/) {
            $error = 1;
            last;
        }
    }
    close F;
    if ($error) {
        return &print_errors("fail ($test_num)\n[*] Digest file " .
                "$output_dir/md5sums has invalid formatted line")
    }
    return 1;
}

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

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

sub getopt_test() {
    for my $cmd ($fwknopCmd, $fwknopdCmd, $knoptmCmd) {
        if (&run_cmd("$fwknopCmd --no-such-argument")) {
            return &print_errors("fail ($test_num)\n[*] $cmd " .
                    "allowed --no-such-argument on the command line");
        }
    }
    return 1;
}

sub print_errors() {
    my $msg = shift;
    &logr("$msg\n");
    if (-e "${cmd_stderr}.$test_num") {
        &logr("    STDOUT available in: " .
            "${cmd_stdout}.$test_num file.\n");
    }
    if (-e "${cmd_stderr}.$test_num") {
        &logr("    STDERR available in: " .
            "${cmd_stderr}.$test_num file.\n");
    }
    return 0;
}

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

sub dots_print() {
    my $msg = shift;
    &logr($msg);
    my $dots = '';
    for (my $i=length($msg); $i < $PRINT_LEN; $i++) {
        $dots .= '.';
    }
    &logr($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: $!";
    }

    for my $file (glob("$output_dir/cmd*")) {
        unlink $file or die "[*] Could not unlink($file)";
    }

    for my $file (glob("$output_dir/*.warn")) {
        unlink $file or die "[*] Could not unlink($file)";
    }

    for my $file (glob("$output_dir/*.die")) {
        unlink $file or die "[*] Could not unlink($file)";
    }

    if (-e "$output_dir/md5sums") {
        unlink "$output_dir/md5sums" or
            die "[*] Could not unlink($output_dir/md5sums)";
    }

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

    unless (-x $gpgCmd) {
        my $found = 0;
        for my $path qw|/bin /sbin /usr/bin /usr/sbin
                /usr/local/bin /usr/local/sbin| {
            if (-x "$path/gpg") {
                $gpgCmd = "$path/gpg";
                $found = 1;
            }
        }
        unless ($found) {
            &logr("[-] Warning: could not find gpg command, " .
                "disabling GnuPG tests\n");
        }
    }

    unless (-x $tcpdumpCmd) {
        my $found = 0;
        for my $path qw|/bin /sbin /usr/bin /usr/sbin
                /usr/local/bin /usr/local/sbin| {
            if (-x "$path/tcpdump") {
                $tcpdumpCmd = "$path/tcpdump";
                $found = 1;
            }
        }
        unless ($found) {
            &logr("[-] Warning: could not find tcpdump command, " .
                "disabling filesystem sniffing tests\n");
        }
    }

    if (-e $logfile) {
        unlink $logfile or die $!;
    }

    &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;

    ### this is a hack to take ad
    $cmds{'tcpdump'} = $tcpdumpCmd;

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

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 prepare_results() {
    my $rv = 0;
    die "[*] $output_dir does not exist" unless -d $output_dir;
    die "[*] $logfile does not exist, has fwknop_test.pl been executed?"
        unless -e $logfile;
    if (-e $tarfile) {
        unlink $tarfile or die "[*] Could not unlink $tarfile: $!";
    }

    ### remove non-loopback IP addresses
    my $search_re = qr/\b127\.0\.0\.1\b/;
    system "perl -p -i -e 's|$search_re|00MY1271STR00|g' $output_dir/cmd*";
    $search_re = qr/\b127\.0\.0\.2\b/;
    system "perl -p -i -e 's|$search_re|00MY1272STR00|g' $output_dir/cmd*";
    $search_re = qr/\b0\.0\.0\.0\b/;
    system "perl -p -i -e 's|$search_re|00MY0000STR00|g' $output_dir/cmd*";
    $search_re = qr/\b(?:[0-2]?\d{1,2}\.){3}[0-2]?\d{1,2}\b/;
    system "perl -p -i -e 's|$search_re|N.N.N.N|g' $output_dir/cmd*";
    system "perl -p -i -e 's|00MY1271STR00|127.0.0.1|g' $output_dir/cmd*";
    system "perl -p -i -e 's|00MY1272STR00|127.0.0.2|g' $output_dir/cmd*";
    system "perl -p -i -e 's|00MY0000STR00|0.0.0.0|g' $output_dir/cmd*";

    ### remove hostname from any uname output
    $search_re = qr/\suname\s+\-a\s*\n\s*(\S+)\s+\S+/;
    system "perl -p -i -e 'undef \$/; s|$search_re" .
        "| uname -a\n\$1 (removed)|s' $output_dir/cmd*";

    $search_re = qr/uname=\x27(\S+)\s+\S+/;
    system "perl -p -i -e 's|$search_re|uname= \$1 (removed)|' $output_dir/cmd*";

    ### create tarball
    system "tar cvfz $tarfile $logfile $output_dir";
    print "[+] Anonymized test results file: $tarfile\n";
    if (-e $tarfile) {
        $rv = 1;
    }
    return $rv;
}

sub pass() {
    &logr("pass ($test_num)\n");
    $successful_tests++;
    return;
}

sub logr() {
    my $msg = shift;

    print STDOUT $msg;
    open F, ">> $logfile" or die $!;
    print F $msg;
    close F;
    return;
}

sub usage() {
    print <<_HELP_;

Usage: fwknop_test.pl [options]

Options:

    -P, --Prepare-results    - Prepare test suite results for communication
                               to a third party.
    -h, --help               - Display usage information.

_HELP_
    exit 0;
}
