#!/usr/bin/perl -w
#
#############################################################################
#
# File: fwknopd (/usr/sbin/fwknopd)
#
# URL: http://www.cipherdyne.org/fwknop/
#
# Purpose: fwknopd implements the server portion of an authorization scheme
#          known as Single Packet Authorization (SPA) that requires only a
#          single encrypted packet to communicate various pieces of
#          information including desired access through an iptables policy
#          and/or specific commands to execute on the target system.  The
#          main application of this program is to protect services such as
#          SSH with an additional layer of security in order to make the
#          exploitation of vulnerabilities (both 0-day and unpatched code)
#          much more difficult.  For more information, see the fwknop(8) man
#          page.
#
#          More information can be found in the fwknop(8) and fwknopd(8) man
#          pages, and also online here:
#
#          http://www.cipherdyne.org/fwknop/docs/
#
# Author: Michael Rash (mbr@cipherdyne.org)
#
# Version: 1.9.12
#
# Copyright (C) 2004-2009 Michael Rash (mbr@cipherdyne.org)
#
# License - GNU General Public License version 2 (GPLv2):
#
#    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: fwknopd 1533 2009-09-08 02:44:02Z mbr $
#

use IO::Socket;
use IO::Handle;
use MIME::Base64;
use Data::Dumper;
use POSIX ':sys_wait_h';
use Getopt::Long;
use strict;

my $config_file = '/etc/fwknop/fwknop.conf';
my $access_conf_file = '';

my $version = '1.9.12';
my $revision_svn = '$Revision: 1533 $';
my $rev_num = '1';
($rev_num) = $revision_svn =~ m|\$Rev.*:\s+(\S+)|;

my %config = ();
my $override_config_str = '';
my %cmds   = ();
my %p0f    = ();
my @access = ();
my $blacklist_ar = [];
my $blacklist_exclude_ar = [];
my %p0f_sigs  = ();
my %pid_files = ();
my %ip_sequences = ();
my %digest_store = ();
my %ipt_input    = ();
my %ipt_forward  = ();
my %ipt_prerouting  = ();
my %ipt_postrouting = ();
my %ipt_output = ();  ### optional
my @ipt_config = ();

my $ipfw_is_dynamic = 0;

my $os_fprint_only = 0;
my $print_version  = 0;
my $print_help     = 0;
my $stop_daemons   = 0;
my $restart        = 0;
my $status         = 0;
my $debug          = 0;
my $packet_ctr     = 0;
my $packet_limit   = 0;
my $lib_dir        = '';
my $fw_list        = 0;
my $fw_type        = '';
my $fw_flush       = 0;
my $ipt_del_chains = 0;
my $fw_del_ip      = '';
my $test_mode      = 0;
my $verbose        = 0;
my $imported_gpg   = 0;
my $os_ipt_log     = '';
my $use_sendmail   = 0;
my $cmdline_intf   = '';
my $warn_msg       = '';
my $die_msg        = '';
my $cmdline_knoptm = '';
my $skip_fko_module = 0;
my $use_fko_module  = 0;
my $test_fko_exists = 0;
my $fko_incoming_digest_type = 0;
my $fko_obj = ();
my $cmdl_disable_gpg = 0;
my $cmdline_fwknop_serv = '';
my $knoptm_debug_file = '';
my $knoptm_include_pidname = 0;
my $fwkserv_debug_file = '';
my $fwkserv_include_pidname = 0;
my $err_wait_timer = 30;  ### seconds
my $gpg_agent_info = '';
my $gpg_no_options = 0;
my $gpg_use_options = 0;
my $gpg_default_prefix = 'hQ';  ### base64 encoded 0x8502
my $build_ipt_config = 0;
my $skipped_first_loop = 0;
my $imported_crypt_cbc = 0;
my $pcap_sleep_interval = 1;  ### seconds
my $imported_iptables_modules = 0;
my $include_all_config_data   = 0;
my $voluntary_exit_timestamp  = 0;
my $fw_data_file = '';  ### legacy port knocking mode
my $dump_config = 0;
my $spa_dump_packets = '';

my $cmdline_locale = '';
my $no_locale = 0;

### SPA message types from fwknop clients

### COMMAND message:
###     random data : user : client_timestamp : client_version : \
###     type (0) : command : digest
my $SPA_COMMAND_MODE = 0;

### ACCESS message (this type is used most often):
###     random data : user : client_timestamp : client_version : \
###     type (1) : access_request : digest
my $SPA_ACCESS_MODE  = 1;  ### default

### NAT ACCESS message:
###     random data : user : client_timestamp : client_version : \
###     type (2) : access_request : NAT_info : digest
my $SPA_NAT_ACCESS_MODE = 2;

### ACCESS message with client-defined firewall timeout:
###     random data : user : client_timestamp : client_version : \
###     type (3) : access_request : timeout : digest
my $SPA_CLIENT_TIMEOUT_ACCESS_MODE = 3;

### NAT ACCESS message with client-defined firewall timeout:
###     random data : user : client_timestamp : client_version : \
###     type (4) : access_request : NAT_info : timeout : digest
my $SPA_CLIENT_TIMEOUT_NAT_ACCESS_MODE = 4;

### local NAT ACCESS message:
###     random data : user : client_timestamp : client_version : \
###     type (5) : access_request : NAT_info : message digest
my $SPA_LOCAL_NAT_ACCESS_MODE = 5;

### local NAT ACCESS message with client-defined firewall timeout:
###     random data : user : client_timestamp : client_version : \
###     type (6) : access_request : NAT_info : timeout : message digest
my $SPA_CLIENT_TIMEOUT_LOCAL_NAT_ACCESS_MODE = 6;

my %spa_mode_strings = (
    $SPA_COMMAND_MODE    => 'SPA_COMMAND_MODE',
    $SPA_ACCESS_MODE     => 'SPA_ACCESS_MODE',
    $SPA_NAT_ACCESS_MODE => 'SPA_NAT_ACCESS_MODE',
    $SPA_CLIENT_TIMEOUT_ACCESS_MODE     => 'SPA_CLIENT_TIMEOUT_ACCESS_MODE',
    $SPA_CLIENT_TIMEOUT_NAT_ACCESS_MODE => 'SPA_CLIENT_TIMEOUT_NAT_ACCESS_MODE',
    $SPA_LOCAL_NAT_ACCESS_MODE          => 'SPA_LOCAL_NAT_ACCESS_MODE',
    $SPA_CLIENT_TIMEOUT_LOCAL_NAT_ACCESS_MODE => 'SPA_CLIENT_TIMEOUT_LOCAL_NAT_ACCESS_MODE'
);

### limits on nummber of fields within a decrypted SPA packet
my $SPA_MIN_PACKET_FIELDS = 7;
my $SPA_MAX_PACKET_FIELDS = 9;

### default time values
my $knock_interval = 60;
my $default_access_timeout = 300;

my $enc_port_offset   = 61000;  ### default offset
my $enc_key           = '';
my $enc_alg           = 'Rijndael';
my $enc_blocksize     = 32;

### there is a constant "RIJNDAEL_KEYSIZE" in the Crypt::Rijndael sources, but
### it is not used; a 16 byte key size is fine.
my $enc_keysize = 16;

my $ALG_RIJNDAEL = 1;
my $ALG_GNUPG    = 2;

my $PCAP      = 0;
my $FILE_PCAP = 1;
my $ULOG_PCAP = 2;
my $SHARED_SEQUENCE  = 3;
my $ENCRYPT_SEQUENCE = 4;

### Bool to detect Linux "Cooked" datalink layers
my $PCAP_COOKED_INTF = 0;

### digest constants
my $SHA256_DIGEST_LEN = 43;
my $SHA1_DIGEST_LEN   = 27;
my $MD5_DIGEST_LEN    = 22;
my $FKO_RECOMPUTE     = 1;

### logr constants
my $SEND_MAIL = 1;
my $NO_MAIL   = 0;
my $LOG_VERBOSE = 1;
my $LOG_QUIET   = 2;

my $ZERO_SLEEP = 0;
my $STDOUT = 1;
my $STDERR = 2;

### packet counters
my $tcp_ctr  = 0;
my $udp_ctr  = 0;
my $icmp_ctr = 0;

### protocol values
my $IPPROTO_ICMP = 1;
my $IPPROTO_TCP  = 6;
my $IPPROTO_UDP  = 17;

### tcp option types
my $tcp_nop_type       = 1;
my $tcp_mss_type       = 2;
my $tcp_win_scale_type = 3;
my $tcp_sack_type      = 4;
my $tcp_timestamp_type = 8;

my %tcp_p0f_opt_types = (
    'N' => $tcp_nop_type,
    'M' => $tcp_mss_type,
    'W' => $tcp_win_scale_type,
    'S' => $tcp_sack_type,
    'T' => $tcp_timestamp_type
);

my $ETH_HDR_LEN      = 14;
my $MIN_IP_HDR_LEN   = 20;
my $MIN_ICMP_HDR_LEN = 8;   ### most practical for SPA packets over ICMP
my $UDP_HDR_LEN      = 8;
my $MIN_TCP_HDR_LEN  = 20;

my $EXTERNAL_CMD_ALARM = 30;  ### default for external commands

my %access_keys = (
    'SOURCE' => [],
    'KEY'    => '',
    'OPEN_PORTS'     => '',
    'GPG_REMOTE_ID'  => '',
    'GPG_DECRYPT_ID' => '',
    'GPG_DECRYPT_PW' => '',
    'GPG_HOME_DIR'   => '',
    'GPG_NO_OPTIONS' => 0,
    'GPG_USE_OPTIONS' => 0,
    'GPG_NO_REQUIRE_PREFIX' => 0,
    'GPG_PREFIX' => '',
    'GPG_PATH'   => '',
    'ULOG_PCAP'  => '',
    'FILE_PCAP'  => '',
    'DATA_COLLECT_MODE' => '',
    'ENCRYPT_SEQUENCE'  => '',
    'SHARED_SEQUENCE'   => '',
    'PORT_OFFSET'       => '',
    'REQUIRE_AUTH_METHOD' => '',
    'SHADOW_FILE'    => '',
    'KNOCK_INTERVAL' => '',
    'KNOCK_LIMIT'    => '',
    'PERMIT_CLIENT_PORTS' => '',
    'PERMIT_CLIENT_TIMEOUT' => '',
    'ENABLE_FORWARD_ACCESS' => 0,
    'ENABLE_CMD_EXEC'     => '',
    'DISABLE_FW_ACCESS'   => '',
    'REQUIRE_SOURCE_ADDRESS' => [],
    'require_src_addr_exceptions' => [],
    'INTERNAL_NET_ACCESS' => [],  ### for --Forward-access IP restrictions
    'internal_net_exceptions' => [],
    'CMD_REGEX'         => '',
    'FW_ACCESS_TIMEOUT' => '',
    'REQUIRE_USERNAME'  => '',
    'MIN_TIME_DIFF' => '',
    'MAX_TIME_DIFF' => '',
    'RESTRICT_INTF' => '',
    'ENABLE_EXTERNAL_CMDS' => 0,
    'EXTERNAL_CMD_OPEN'  => '',
    'EXTERNAL_CMD_CLOSE' => '',
    'EXTERNAL_CMD_ALARM' => '',
);

my $ip_re = qr|(?:[0-2]?\d{1,2}\.){3}[0-2]?\d{1,2}|;

my @args_cp = @ARGV;

### run GetOpt() to get comand line args
&handle_command_line();

&usage(0) if $print_help;

if ($print_version) {
    print "[+] fwknopd v$version (file revision: $rev_num)\n",
        "      by Michael Rash <mbr\@cipherdyne.org>\n";
    exit 0;
}

if ($os_fprint_only) {
    print "[+] Entering OS fingerprinting mode.\n";
}

print STDERR localtime() . " [+] ** Starting fwknopd (debug mode) **\n",
    "    fwknopd Command line: @args_cp\n" if $debug;

### setup to run
&fwknop_init();

if ($config{'AUTH_MODE'} eq 'KNOCK' or $os_fprint_only) {

    ### we are running in traditional port knocking mode
    &knock_loop();

} elsif ($config{'AUTH_MODE'} eq 'FILE_PCAP'
        or $config{'AUTH_MODE'} eq 'ULOG_PCAP'
        or $config{'AUTH_MODE'} eq 'PCAP') {

    ### we are parsing the pcap file created by the ulogd pcap
    ### writer, or in sniffing mode against an interface

    require Net::Pcap;

    if ($debug ) {
        print STDERR "[+] Net::Pcap::VERSION $Net::Pcap::VERSION\n";
    }

    &pcap_loop();

} elsif ($config{'AUTH_MODE'} eq 'SOCKET') {

    ### we are going to acquire SPA packet data from the fwknop_serv
    ### process via a domain socket.  fwknop_serv itself will listen
    ### on a tcp or udp port for an incoming SPA packet, so libpcap
    ### does not become involved in this mode.

    &socket_loop();
}

exit 0;
#============================ end main ==============================

sub pcap_loop() {

    ### we use both a size and an inode check in the FILE_PCAP and
    ### ULOG_PCAP modes to check if the file has been rotated
    my $pcap_file_size  = 0;
    my $pcap_file_inode = 0;

    ### get pcap opject
    my $pcap_t = &get_pcap_obj();

    ### Check for "cooked" Linux datalink layers (i.e. rp-pppoe)
    eval {
        if (not $PCAP_COOKED_INTF and $Net::Pcap::VERSION > 0.05) {
            if (Net::Pcap::datalink_val_to_name(
                    Net::Pcap::datalink($pcap_t)) eq 'LINUX_SLL') {
                print STDERR "[+] Detected Linux Cooked Interface.\n" if $debug;
                $PCAP_COOKED_INTF = 1;
            }
        }
    };
    &collect_warn_die_msgs() if $@;

    if ($config{'AUTH_MODE'} eq 'FILE_PCAP'
            or $config{'AUTH_MODE'} eq 'ULOG_PCAP') {
        ### get file size (we don't need a -e check here because
        ### this is handled in get_pcap_obj()).
        $pcap_file_size = -s $config{'PCAP_PKT_FILE'};

        ### get inode associated with the sniffing file
        $pcap_file_inode = (stat($config{'PCAP_PKT_FILE'}))[1];
    }
    print STDERR localtime() . " [+] pcap_loop()\n" if $debug;

    my $check_file_ctr = 0;

    &collect_warn_die_msgs();

    for (;;) {

        my @tmpcbargs; my $cbcalled = 0;
        my $tmpcb = sub {
            my $tag = $_[0];
            my %hdr = %{$_[1]};
            my $pkt = $_[2];
            @tmpcbargs = ($tag, \%hdr, $pkt);
            $cbcalled = 1;
        };

        Net::Pcap::loop($pcap_t, 1, $tmpcb, 'fwknop_tag');
        pcap_process_pkt(@tmpcbargs) if($cbcalled);

        if ($config{'AUTH_MODE'} eq 'FILE_PCAP'
                or $config{'AUTH_MODE'} eq 'ULOG_PCAP') {

            ### check to see if the pcap file has been rotated (we need to
            ### close and re-open)
            if ($check_file_ctr >= 10) {
                if (-e $config{'PCAP_PKT_FILE'}) {
                    my $size_tmp  = -s $config{'PCAP_PKT_FILE'};
                    my $inode_tmp = (stat($config{'PCAP_PKT_FILE'}))[1];
                    if ($inode_tmp != $pcap_file_inode
                            or $size_tmp < $pcap_file_size) {

                        ### the file was rotated or shrank, so get new
                        ### pcap_t object
                        Net::Pcap::close($pcap_t);

                        &logr('[+]', "pcap file $config{'PCAP_PKT_FILE'} " .
                            "shrank or was rotated, so re-opening", $NO_MAIL);
                        $pcap_t = &get_pcap_obj();

                        ### set file size and inode
                        $pcap_file_size  = $size_tmp;
                        $pcap_file_inode = $inode_tmp;
                    }
                } else {
                    Net::Pcap::close($pcap_t);
                    &logr('[+]', "pcap file $config{'PCAP_PKT_FILE'} " .
                        "was rotated, so re-opening", $NO_MAIL);
                    $pcap_t = &get_pcap_obj();

                    ### set file size and inode
                    $pcap_file_size  = -s $config{'PCAP_PKT_FILE'};
                    $pcap_file_inode = (stat($config{'PCAP_PKT_FILE'}))[1];
                }
                $check_file_ctr = 0;
            }
            $check_file_ctr++;
        }

        &collect_warn_die_msgs();
        sleep $pcap_sleep_interval;
    }

    Net::Pcap::close($pcap_t);

    return;
}

sub pcap_process_pkt() {
    my ($tag, $hdr, $pkt) = @_;

    &collect_warn_die_msgs();

    return unless $tag eq 'fwknop_tag';
    return unless defined $hdr;
    return unless defined $pkt;

    my $ether_data = '';
    my $ip         = '';
    my $src_ip     = '';
    my $proto      = '';
    my $transport_data = '';

    if ($debug) {
        print STDERR localtime() . " [+] Received packet ***[" .
            localtime() . "]***\n";
        if ($verbose) {
            print STDERR localtime() .
                "     Complete raw packet data (hex dump, including ",
                    "packet headers):\n";
            &hex_dump($pkt);
        }
    }

    ### check the length of the packet; if it is not at least
    ### 160 bytes long (this is the default MIN_SPA_PKT_LEN value, and
    ### this is conservative) then it cannot be an SPA packet
    my $pkt_len = length($pkt);
    if (length($pkt) < $config{'MIN_SPA_PKT_LEN'}) {
        if ($debug and $verbose) {
            print
"[-] Packet length ($pkt_len bytes) less than $config{'MIN_SPA_PKT_LEN'}\n",
"    minimum, so is not an SPA packet; skipping.\n";
        }
        return;
    }

    if ($config{'AUTH_MODE'} eq 'ULOG_PCAP') {
        ### The ulogd pcap writer does not include link layer information
        $ip = &ip_decode($pkt) or return;
    } else {
        if ($config{'FIREWALL_TYPE'} eq 'ipfw'
                and $config{'PCAP_INTF'} eq 'lo0') {

            ### it seems that FreeBSD does not include an Ethernet header
            ### over loopback but puts a different set of four bytes
            $pkt =~ s/^.{4}// if $pkt =~ /^[^\x45].{3}\x45/;

            $ip = &ip_decode($pkt) or return;
        } else {
            if ($PCAP_COOKED_INTF) {
                $ether_data = unpack("x16a*", $pkt);
            } else {
                $ether_data = &ethernet_strip($pkt) or return;
            }
            $ip = &ip_decode($ether_data) or return;
        }
    }

    ### get the source IP address from the IP header
    $src_ip = $ip->{'src_ip'} or return;

    ### get the protocol
    $proto = $ip->{'proto'} or return;

    if ($proto == $IPPROTO_ICMP) {
        $transport_data = &icmp_decode_data($ip->{'data'});
    } elsif ($proto == $IPPROTO_TCP) {
        $transport_data = &tcp_decode_data($ip->{'data'});
    } elsif ($proto == $IPPROTO_UDP) {
        $transport_data = &udp_decode_data($ip->{'data'});
    } else {
        return;
    }

    &decode_SPA_data($transport_data, $src_ip, $proto);

    return;
}

sub decode_SPA_data() {
    my ($transport_data, $src_ip, $proto) = @_;

    ### make sure we have _some_ data in the packet; in practice
    ### any valid SPA message will be longer than 10 bytes, but this
    ### check is better than nothing
    return if $transport_data eq '';

    my $enc_msg_len = 0;
    $enc_msg_len = length($transport_data);
    if (10 < $enc_msg_len and $enc_msg_len < $config{'MAX_SNIFF_BYTES'}) {
        print STDERR localtime() . " [+] Data len: $enc_msg_len bytes\n"
            if $debug;
    } else {
        print STDERR localtime() . " [-] $enc_msg_len bytes, not ",
            "attempting decrypt)\n" if $debug;
        return;
    }

    if ($debug) {
        ### make sure not to print non-printable stuff
        my $data_tmp = $transport_data;
        $data_tmp =~ s/[^\x20-\x7e]/NA/g;
        print STDERR localtime() .
            " [+] Raw packet data (single line): $data_tmp\n";

        ### print packet data out in tcpdump -X format
        if ($verbose) {
            print STDERR localtime() .
                "     Raw packet data (hex dump, minus packet headers):\n";
            &hex_dump($transport_data);
        }
    }

    my $candidate_spa_data = '';

    if ($proto == $IPPROTO_TCP and $config{'ENABLE_SPA_OVER_HTTP'} eq 'Y') {
        if ($transport_data =~ m|GET\s+(\S+)\s+HTTP/\d|) {
            $candidate_spa_data = $1;

            $candidate_spa_data =~ s/\.html// if $candidate_spa_data =~ /\.html/;
            $candidate_spa_data =~ s|^/|| if $candidate_spa_data =~ m|^/|;
            $candidate_spa_data =~ s|^http://\S+/||
                if $candidate_spa_data =~ m|^http://\S+/|;

            unless (&is_url_base64($candidate_spa_data)) {
                if ($debug) {
                    print STDERR localtime() . " [+] Packet contains non-base64 ",
                        "(with URL mods) encoded characters, skipping.\n";
                    &check_packet_limit();
                }
                return;
            }
        }
    }

    unless ($candidate_spa_data) {
        $candidate_spa_data = $transport_data;

        ### check to make sure the packet data only contains base64 encoded
        ### characters per RFC 3548:   0-9, A-Z, a-z, +, /, =
        unless (&is_base64($candidate_spa_data)) {
            if ($debug) {
                print STDERR localtime() . " [+] Packet contains non-base64 ",
                    "encoded characters, skipping.\n";
                &check_packet_limit();
            }
            return;
        }
    }

    ### see if this packet is worthy of being granted access through
    ### the firewall
    &SPA_check_grant_access($src_ip, $enc_msg_len, $candidate_spa_data);

    &collect_warn_die_msgs();

    return;
}

sub ethernet_strip() {
    my $pkt = shift;

    my $eth_data = '';

    if (length($pkt) >= $ETH_HDR_LEN) {
        $eth_data = substr($pkt, $ETH_HDR_LEN);
    }

    if (not $eth_data and ($debug and $verbose)) {
        print "[-] Could not properly decode Ethernet header.\n";
    }
    ### Silently return '' for short frames
    return $eth_data;
}

sub ip_addr_bytes_to_string() {
    my $bytes = shift;

    my ($a, $b, $c, $d) = unpack('C4', $bytes);
    return "$a.$b.$c.$d";
}

sub ip_decode() {
    my $pkt = shift;

    my $ip = {};
    if (length($pkt) >= $MIN_IP_HDR_LEN and $pkt =~ /^\x45/) {
        (my $ver_ihl, $ip->{'tos'}, $ip->{'len'}, $ip->{'id'}, my $flags_frag,
         $ip->{'ttl'}, $ip->{'proto'}, $ip->{'cksum'}, my $src_ip, my $dest_ip)
            = unpack("CCnnnCCna4a4", $pkt);
        $ip->{'ver'} = $ver_ihl >> 4;
        $ip->{'hlen'} = $ver_ihl & 0x0F;
        $ip->{'flags'} = $flags_frag >> 13;
        $ip->{'foffset'} = ($flags_frag & 0x1FFF) * 8;
        $ip->{'src_ip'} = &ip_addr_bytes_to_string($src_ip);
        $ip->{'dest_ip'} = &ip_addr_bytes_to_string($dest_ip);
        my $data_start = $ip->{'hlen'} * 4;
        if ($data_start >= $MIN_IP_HDR_LEN) {
            $ip->{'data'} = substr($pkt, $data_start);
        }
    }
    if (not keys %$ip and ($debug and $verbose)) {
        print "[-] Could not properly decode IP header.\n";
    }
    return $ip;
}

sub icmp_decode_data() {
    my $icmp = shift;

    my $icmp_data = '';
    if (length($icmp) >= $MIN_ICMP_HDR_LEN) {
        $icmp_data = substr($icmp, $MIN_ICMP_HDR_LEN);
    }
    ### Silently return '' for short packets
    if (not $icmp_data and ($debug and $verbose)) {
        print "[-] Could not properly decode ICMP header.\n";
    }
    return $icmp_data;
}

sub tcp_decode_data() {
    my $tcp = shift;

    my $tcp_data = '';

    if (length($tcp) >= $MIN_TCP_HDR_LEN) {

        my $data_start = 4 * (ord(substr($tcp, 12, 1)) >> 4);
        if ($data_start >= $MIN_TCP_HDR_LEN) {
            $tcp_data = substr($tcp, $data_start);
        }
    }
    ### Silently return '' for short packets
    if (not $tcp_data and ($debug and $verbose)) {
        print "[-] Could not properly decode TCP header.\n";
    }
    return $tcp_data;
}

sub udp_decode_data() {
    my $udp = shift;

    my $udp_data = '';
    if (length($udp) >= $UDP_HDR_LEN) {
        $udp_data = substr($udp, $UDP_HDR_LEN);
    }
    ### Silently return '' for short packets
    if (not $udp_data and ($debug and $verbose)) {
        print "[-] Could not properly decode UDP header.\n";
    }
    return $udp_data;
}

sub socket_loop() {

    print STDERR localtime() . " [+] socket_loop() acquiring SPA ",
        "packet data from: $cmds{'fwknop_serv'} via domain socket: ",
        "$config{'FWKNOP_SERV_SOCK'}\n";

    my $fwknop_serv_sock = IO::Socket::UNIX->new(
        Type    => SOCK_STREAM,
        Local   => $config{'FWKNOP_SERV_SOCK'},
        Listen  => SOMAXCONN,
        Timeout => .1
    ) or die "[*] Could not acquire fwknopd communications domain socket: $!";

    for (;;) {

        my $fwknop_serv_connection = $fwknop_serv_sock->accept();
        if ($fwknop_serv_connection) {

            my @fwknop_serv_msgs = <$fwknop_serv_connection>;
            for my $msg (@fwknop_serv_msgs) {
                if ($msg =~ /^($ip_re):(\d{1,2}):(\S+)/) {
                    my $src_ip  = $1;
                    my $proto   = $2;
                    my $spa_msg = $3;

                    &decode_SPA_data($spa_msg, $src_ip, $proto);
                }
            }
            @fwknop_serv_msgs = ();
        }
    }
    return;
}

sub SPA_check_grant_access() {
    my ($src_ip, $enc_msg_len, $pkt_data) = @_;

    if ($spa_dump_packets) {
        if (&is_base64($pkt_data)) {
            print "\nLen: $enc_msg_len, pkt: $pkt_data\n";
        } else {
            print "\nLine contains non base64 chars, skipping.\n";
            return;
        }
    }

    ### first check to see if we have any matching access directives
    ### (in access.conf) for $src_ip, and if not we will do _nothing_
    ### with this packet.
    my $access_nums_aref = &check_src($src_ip);

    if ($#$access_nums_aref > -1) {

        ### See if the packet qualifies for any access
        SOURCE: for my $num (@$access_nums_aref) {
            my $access_hr = $access[$num];

            next SOURCE unless $access_hr->{'DATA_COLLECT_MODE'} == $PCAP
                or $access_hr->{'DATA_COLLECT_MODE'} == $FILE_PCAP
                or $access_hr->{'DATA_COLLECT_MODE'} == $ULOG_PCAP;

            &dump_access($access_hr, $num) if $debug and $verbose;

            ### keep track of which source block we are dealing with from
            ### access.conf
            my $source_block_num = $access_hr->{'block_num'};

            ### see if we can decrypt and base64-decode
            &fko_acquire_object() if $use_fko_module;
            my ($decrypt_rv, $decrypted_msg, $gpg_sign_id, $decrypt_algo)
                = &SPA_decrypt($pkt_data, $enc_msg_len, $access_hr);

            unless ($decrypt_rv) {
                &fko_destroy_object() if $use_fko_module;
                next SOURCE;
            }

            ### check for replay attacks
            my ($digest_rv, $digest)
                = &is_replay_attack($decrypted_msg, $src_ip);
            if ($digest_rv) {
                &fko_destroy_object() if $use_fko_module;
                return;
            }

            ### see if we have a syntactically valid message - this
            ### also runs the check_digest() function to validate the
            ### internal digest against the decrypted data.
            my ($validate_rv, $msg_hr) = &pcap_validate_msg(
                $decrypted_msg, $source_block_num, $access_hr);
            if ($debug and not $validate_rv) {
                print STDERR localtime() . " [-] Decrypted message does not ",
                    "conform to a valid SPA packet.\n";
            }
            unless ($validate_rv) {
                &fko_destroy_object() if $use_fko_module;
                next SOURCE;
            }

            if ($spa_dump_packets) {
                print "    Disk write digest: $digest\n";
                for my $key (keys %$msg_hr) {
                    printf "    %20s -> %s\n", $key, $msg_hr->{$key};
                }
                return;
            }

            ### check to see if client side time stamp is too old
            my $time_check_rv = &SPA_check_packet_age($msg_hr->{'remote_time'});
            unless ($validate_rv) {
                &fko_destroy_object() if $use_fko_module;
                next SOURCE;
            }
            next SOURCE unless $time_check_rv;

            ### dump packet to stderr for debugging purposes
            &SPA_dump_packet($msg_hr) if $debug;

            ### check username
            next SOURCE unless &SPA_check_user($access_hr, $src_ip, $msg_hr);
            unless ($validate_rv) {
                &fko_destroy_object() if $use_fko_module;
                next SOURCE;
            }

            ### check authentication method
            unless (&SPA_check_auth_method($access_hr, $src_ip, $msg_hr)) {
                &fko_destroy_object() if $use_fko_module;
                next SOURCE;
            }

            if ($msg_hr->{'action_type'} == $SPA_ACCESS_MODE
                    or $msg_hr->{'action_type'} == $SPA_NAT_ACCESS_MODE
                    or $msg_hr->{'action_type'} == $SPA_LOCAL_NAT_ACCESS_MODE
                    or $msg_hr->{'action_type'} == $SPA_CLIENT_TIMEOUT_ACCESS_MODE
                    or $msg_hr->{'action_type'} == $SPA_CLIENT_TIMEOUT_NAT_ACCESS_MODE
                    or $msg_hr->{'action_type'} == $SPA_CLIENT_TIMEOUT_LOCAL_NAT_ACCESS_MODE) {

                if (&SPA_access($msg_hr, $src_ip, $decrypt_algo,
                        $gpg_sign_id, $digest, $access_hr)) {
                    &fko_destroy_object() if $use_fko_module;
                    last SOURCE;
                } else {
                    &fko_destroy_object() if $use_fko_module;
                    next SOURCE;
                }
            } elsif ($msg_hr->{'action_type'} == $SPA_COMMAND_MODE) {
                if (&SPA_cmd($msg_hr, $src_ip, $decrypt_algo,
                        $gpg_sign_id, $digest, $access_hr)) {
                    &fko_destroy_object() if $use_fko_module;
                    last SOURCE;
                } else {
                    &fko_destroy_object() if $use_fko_module;
                    next SOURCE;
                }
            }
        }
    } else {
        print STDERR localtime() . " [-] Packet from $src_ip did not ",
            "match any SOURCE blocks in $config{'ACCESS_CONF'}\n" if $debug;
    }

    &check_packet_limit();
    return;
}

sub check_packet_limit() {
    ### see if we need to exit if the packet limit (set with -C on the
    ### command line) has been reached
    return unless $packet_limit;

    $packet_ctr++;
    if ($packet_ctr >= $packet_limit) {
        &logr('[+]', "packet limit ($packet_limit) reached, exiting.",
            $NO_MAIL);
        if ($knoptm_debug_file and -e $knoptm_debug_file) {
            &logr('[+]', "collecting knoptm debug messages " .
                "from $knoptm_debug_file", $NO_MAIL);
            open F, "< $knoptm_debug_file" or die $!;
            while (<F>) {
                chomp;
                &logr("KNOPTM:", $_, $NO_MAIL);
            }
            close F;
        }
        if ($fwkserv_debug_file and -e $fwkserv_debug_file) {
            &logr('[+]', "collecting fwknop_serv debug messages " .
                "from $fwkserv_debug_file", $NO_MAIL);
            open F, "< $fwkserv_debug_file" or die $!;
            while (<F>) {
                chomp;
                &logr("FWKNOP_SERV:", $_, $NO_MAIL);
            }
            close F;
        }
        exit 0;
    }
    return;
}

sub SPA_decrypt() {
    my ($pkt_data, $enc_msg_len, $access_hr) = @_;

    my $decrypted_msg = '';
    my $decrypt_algo  = $ALG_RIJNDAEL;
    my $gpg_sign_id   = '';
    my $decrypt_rv    = 0;

    if ($debug) {
        print STDERR localtime() . " [+] Attempting to ",
            "decrypt the following data ($enc_msg_len bytes):\n";
        &hex_dump($pkt_data);
    }

    if (not $cmdl_disable_gpg
            and $enc_msg_len > $config{'MIN_GNUPG_MSG_SIZE'}
            and defined $access_hr->{'GPG_REMOTE_ID'}) {
        ### attempt GPG decrypt (only if the length of the encrypted
        ### payload is greater than the minimum size for an SPA message
        ### encrypted with GnuPG; even encrypting a single byte of data
        ### with a 1024 bit GnuPG key results in 340 bytes of encrypted
        ### payload in my testing).
        ($decrypt_rv, $decrypted_msg, $gpg_sign_id) =
                &pcap_GPG_decrypt_msg($pkt_data, $access_hr);

        $decrypt_algo = $ALG_GNUPG if $decrypt_rv;
    }

    ### fall back to Rijndael if the GnuPG decrypt was not successful
    ### (and note that the GnuPG decryption is only attempted if the
    ### packet size is large enough).
    if (defined $access_hr->{'KEY'} and not $decrypt_rv) {

        ($decrypt_rv, $decrypted_msg) = &pcap_Rijndael_decrypt_msg(
                            $pkt_data, $access_hr->{'KEY'});
    }

    if ($decrypt_rv) {
        if ($debug and not $use_fko_module) {
            ### make sure not to print non-printable stuff
            my $dec_tmp_msg = $decrypted_msg;
            $dec_tmp_msg =~ s/[^\x20-\x7e]/NA/g;
            print STDERR localtime() . " [+] Decrypted ",
                "message: $dec_tmp_msg\n";
            if ($verbose) {
                print STDERR localtime() . "     Decrypted message (hex dump):\n";
                &hex_dump($decrypted_msg);
            }
        }
    } else {
        print STDERR localtime() . " [-] Failed decrypt for SOURCE block ",
            "$access_hr->{'src_str'}\n" if $debug;
    }

    return $decrypt_rv, $decrypted_msg, $gpg_sign_id, $decrypt_algo;
}

sub SPA_check_packet_age() {
    my $remote_time = shift;

    if ($config{'ENABLE_SPA_PACKET_AGING'} eq 'Y') {
        my $time_diff = time() - $remote_time;
        if (abs($time_diff) > $config{'MAX_SPA_PACKET_AGE'}) {
            &logr('[-]', "remote time stamp age difference is larger than " .
                "$config{'MAX_SPA_PACKET_AGE'} second max.", $SEND_MAIL);
            print STDERR localtime() . " [-] Time difference: $time_diff " .
                "(seconds), " . ($time_diff / 3600) . " (hours)\n";
            return 0;
        }
    }
    return 1;
}

sub SPA_dump_packet() {
    my $msg_hr = shift;

    print STDERR localtime() . " [+] Packet fields:\n";
    printf STDERR "    %-16s %s\n    %-16s %s\n    %-16s %s\n" .
                  "    %-16s %s\n    %-16s %s",
            'Random data:', $msg_hr->{'random_number'},
            'Username:',    $msg_hr->{'username'},
            'Remote time:', $msg_hr->{'remote_time'},
            'Remote ver:',  $msg_hr->{'remote_version'},
            'Action type:', $msg_hr->{'action_type'};

    for my $action_type (keys %spa_mode_strings) {
        if ($msg_hr->{'action_type'} == $action_type) {
            print STDERR " ($spa_mode_strings{$action_type})\n";
            last;
        }
    }

    printf STDERR "    %-16s %s\n",
            'Action:', $msg_hr->{'action'};

    if ($msg_hr->{'action_type'} == $SPA_CLIENT_TIMEOUT_ACCESS_MODE
            or $msg_hr->{'action_type'} == $SPA_CLIENT_TIMEOUT_NAT_ACCESS_MODE
            or $msg_hr->{'action_type'} == $SPA_CLIENT_TIMEOUT_LOCAL_NAT_ACCESS_MODE) {
        printf STDERR "    %-16s %s\n",
                'Client timeout:', $msg_hr->{'client_timeout'};
    }

    if ($msg_hr->{'server_auth'}) {
        if ($msg_hr->{'server_auth'} =~ /^\s*(\w+),(.*)/) {
            my $server_auth_type = lc($1);
            my $server_auth_crypt_pw = $2;
            if ($debug) {
                printf STDERR "    %-16s %s", 'Server auth:', $server_auth_type;
                for (my $i=0; $i < length($server_auth_crypt_pw); $i++) {
                    print STDERR '*';
                }
                print STDERR "\n";
            }
        }
    }
    if ($msg_hr->{'nat_info'}) {
        printf STDERR "    %-16s %s\n", 'NAT info:',
            $msg_hr->{'nat_info'};
    }
    printf STDERR "    %-16s %s\n", "$msg_hr->{'digest_str'} digest:",
        $msg_hr->{'digest'};
    return;
}

sub SPA_check_user() {
    my ($access_hr, $src_ip, $msg_hr) = @_;

    if (defined $access_hr->{'REQUIRE_USERNAME'}) {
        my $found = 0;
        my $user  = '';
        for my $valid_user (@{$access_hr->{'VALID_USERS'}}) {
            if ($valid_user eq $msg_hr->{'username'}) {
                $found = 1;
                $user  = $valid_user;
            }
        }
        unless ($found) {
            &logr('[-]', "username mismatch from $src_ip, expecting " .
                "$access_hr->{'REQUIRE_USERNAME'}, got " .
                "$msg_hr->{'username'}", $SEND_MAIL);
            return 0;
        }
    }
    return 1;
}

sub SPA_check_auth_method() {
    my ($access_hr, $src_ip, $msg_hr) = @_;

    my $server_auth_type     = '';
    my $server_auth_crypt_pw = '';
    if ($msg_hr->{'server_auth'}) {
        if ($msg_hr->{'server_auth'} =~ /^\s*(\w+),(.*)/) {
            $server_auth_type = lc($1);
            $server_auth_crypt_pw = $2;
        }
    }

    if (defined $access_hr->{'REQUIRE_AUTH_METHOD'}) {
        if ($server_auth_type
                eq $access_hr->{'REQUIRE_AUTH_METHOD'}) {
            if ($server_auth_type eq 'crypt') {
                ### check the local UNIX crypt() password associated
                ### with the user
                unless (&server_auth_verify_crypt_pw(
                            $msg_hr->{'username'},
                            $server_auth_crypt_pw,
                            $access_hr->{'SHADOW_FILE'})) {
                    &logr('[-]', "IP: $src_ip failed server-auth UNIX " .
                        "crypt() password test", $NO_MAIL);
                    return 0;
                }
            }
        } else {
            &logr('[-]', "required server-auth method " .
                "\"$access_hr->{'REQUIRE_AUTH_METHOD'}\" " .
                "not supplied by $src_ip", $NO_MAIL);
            return 0;
        }
    }
    return 1;
}

sub SPA_access() {
    my ($msg_hr, $src_ip, $decrypt_algo, $gpg_sign_id,
        $digest, $access_hr) = @_;

    my $allow_src    = '';
    my %open_ports   = ();
    my %grant_ports  = ();
    my %nat_info     = ();
    my $grant_access = 0;

    if ($access_hr->{'DISABLE_FW_ACCESS'}) {
        &logr('[-]', "received fw access request from $src_ip, " .
            "but DISABLE_FW_ACCESS is set to a true value " .
            "(SOURCE line num: $access_hr->{'src_line_num'})", $NO_MAIL);
        return 0;
    }

    if ($msg_hr->{'action_type'} == $SPA_CLIENT_TIMEOUT_ACCESS_MODE
            or $msg_hr->{'action_type'} == $SPA_CLIENT_TIMEOUT_NAT_ACCESS_MODE
            or $msg_hr->{'action_type'} == $SPA_CLIENT_TIMEOUT_LOCAL_NAT_ACCESS_MODE) {

        if ($access_hr->{'PERMIT_CLIENT_TIMEOUT'}) {
            $access_hr->{'FW_ACCESS_TIMEOUT'} = $msg_hr->{'client_timeout'};
        } else {
            &logr('[-]', "received fw access request from $src_ip, " .
                "with client-defined timeout, but PERMIT_CLIENT_TIMEOUT is not " .
                "set (SOURCE line num: $access_hr->{'src_line_num'})", $NO_MAIL);
            return 0;
        }
    }

    $allow_src = $1 if $msg_hr->{'action'} =~ /($ip_re)/;

    unless ($allow_src) {
        &logr('[-]', "no valid IP address within action portion of SPA " .
            "packet from $src_ip (SOURCE line num: " .
            "$access_hr->{'src_line_num'})", $SEND_MAIL);
        return 0;
    }

    if ($allow_src eq '0.0.0.0') {
        if ($config{'REQUIRE_SOURCE_ADDRESS'} eq 'Y' or not
                &is_ip_included($src_ip,
                    $access_hr->{'REQUIRE_SOURCE_ADDRESS'},
                    $access_hr->{'require_src_addr_exceptions'})) {
            &logr('[-]', "IP: $src_ip sent SPA packet that " .
                "contained 0.0.0.0 (-s on the client side) " .
                "but REQUIRE_SOURCE_ADDRESS is enabled " .
                "(SOURCE line num: $access_hr->{'src_line_num'})",
                $SEND_MAIL);
            return 0;
        } else {
            $allow_src = $src_ip;
        }
    }

    if (&is_ip_included($allow_src, $blacklist_ar, $blacklist_exclude_ar)) {
        print STDERR localtime() . " [+] SPA_access() ",
        "$allow_src in BLACKLIST" if $debug;
        &logr('[-]', "allow IP: $allow_src SPA packet from $src_ip is " .
            "blacklisted (SOURCE line num: " .
            "$access_hr->{'src_line_num'})", $SEND_MAIL);
        return 0;
    }

    ### initialize to the OPEN_PORTS directives (if defined; we know that
    ### either OPEN_PORTS or PERMIT_CLIENT_PORTS was specified in the
    ### access.conf file)
    %open_ports = %{$access_hr->{'OPEN_PORTS'}}
        if defined $access_hr->{'OPEN_PORTS'};

    if ($access_hr->{'ENABLE_EXTERNAL_CMDS'}
            or ($config{'FIREWALL_TYPE'} eq 'external_cmd'
            and $config{'ENABLE_EXTERNAL_CMDS'} eq 'Y')) {

        $grant_access = 1;
    }

    if ($msg_hr->{'action'} =~ /$ip_re,(tcp|udp|icmp),(\d+)/i) {

        ### single port access format (e.g. tcp,22)
        my $allow_port  = $1;
        my $allow_proto = $2;

        if ($access_hr->{'PERMIT_CLIENT_PORTS'}) {
            $grant_ports{$allow_proto}{$allow_port} = '';
            $grant_access = 1;
        } else {
            if (defined $open_ports{$allow_proto} and
                    defined $open_ports{$allow_proto}{$allow_port}) {
                $grant_ports{$allow_proto}{$allow_port} = '';
                $grant_access = 1;
            } else {
                unless ($grant_access) {
                    &logr('[-]', "IP $allow_src not permitted to open " .
                        "$allow_proto/$allow_port (SOURCE line num: " .
                        "$access_hr->{'src_line_num'})", $NO_MAIL);
                    return 0;
                }
            }
        }

    } elsif ($msg_hr->{'action'} =~ /$ip_re,(\S+)/) {

        ### multi-port access format (-A was specified by
        ### the client)
        my $access_str = $1;

        my @dec_allow_ports = split /,/, $access_str;

        for my $port_str (@dec_allow_ports) {
            if ($port_str =~ m|(\D+)/(\d+)|) {
                my $proto = lc($1);
                my $port  = $2;

                next unless ($proto eq 'tcp'
                    or $proto eq 'udp'
                    or $proto eq 'icmp');
                $port = 0 if $proto eq 'icmp';

                if ($access_hr->{'PERMIT_CLIENT_PORTS'}) {
                    $grant_ports{$proto}{$port} = '';
                    $grant_access = 1;
                } else {
                    if (defined $open_ports{$proto} and
                            defined $open_ports{$proto}{$port}) {
                        $grant_ports{$proto}{$port} = '';
                        $grant_access = 1;
                    } else {
                        unless ($grant_access) {
                            &logr('[-]', "IP $allow_src not permitted to " .
                                "open $proto/$port (SOURCE line num: " .
                                "$access_hr->{'src_line_num'})", $NO_MAIL);
                            return 0;
                        }
                    }
                }
            }
        }
    }

    ### handle SPA access through iptables FORWARD chain for
    ### SPA_NAT_ACCESS_MODE messages (or through the INPUT chain for
    ### SPA_LOCAL_NAT_ACCESS_MODE messages)
    ### iptables -t nat -A PREROUTING -p tcp -s <SPA_src> --dport 55000 \
    ### -i eth0 -j DNAT --to 192.168.10.3:80
    if ($msg_hr->{'nat_info'}
                and $msg_hr->{'nat_info'} =~ /($ip_re),(\d+)/) {

        %nat_info = (
            'internal_ip'   => $1,
            'external_port' => $2,
        );

        unless ($config{'FIREWALL_TYPE'} eq 'iptables') {
            &logr('[-]', "NAT access requested through non-iptables " .
                "firewall (SOURCE line num: ".
                "$access_hr->{'src_line_num'})", $NO_MAIL);
            return 0;
        }

        if ($msg_hr->{'action_type'} == $SPA_NAT_ACCESS_MODE
                or $msg_hr->{'action_type'} == $SPA_CLIENT_TIMEOUT_NAT_ACCESS_MODE) {
            unless ($access_hr->{'ENABLE_FORWARD_ACCESS'}) {
                &logr('[-]', "FORWARD access requested through non-forward " .
                    "access SOURCE block (SOURCE line num: ".
                    "$access_hr->{'src_line_num'})", $NO_MAIL);
                return 0;
            }
        }

        if ($msg_hr->{'action_type'} == $SPA_LOCAL_NAT_ACCESS_MODE
                or $msg_hr->{'action_type'} == $SPA_CLIENT_TIMEOUT_LOCAL_NAT_ACCESS_MODE) {
            unless ($config{'ENABLE_IPT_LOCAL_NAT'} eq 'Y') {
                &logr('[-]', "Local NAT access requested without " .
                    "ENABLE_IPT_LOCAL_NAT enabled", $NO_MAIL);
                return 0;
            }
        }

        ### check to see if access is allowed to internal IP (or a local IP
        ### for NAT'd local connections)
        unless (&is_ip_included($nat_info{'internal_ip'},
                $access_hr->{'INTERNAL_NET_ACCESS'},
                $access_hr->{'internal_net_exceptions'})) {
            &logr('[-]', "NAT access to $nat_info{'internal_ip'} " .
                "restricted (SOURCE line num: ".
                "$access_hr->{'src_line_num'})", $NO_MAIL);
            return 0;
        }
        my $port_ctr = 0;
        for my $proto (keys %grant_ports) {
            for my $port (keys %{$grant_ports{$proto}}) {
                $port_ctr++;
            }
        }
        ### we can only map one forwarding port on the external interface
        ### to be forwarded to one internal service
        if ($port_ctr > 1) {
            &logr('[-]', "cannot forward more than one port " .
                "(SOURCE line num: $access_hr->{'src_line_num'})", $NO_MAIL);
            return 0;
        }
    } else {
        if ($access_hr->{'ENABLE_FORWARD_ACCESS'}) {
            &logr('[-]', "non-forward access requested through FORWARD " .
                "access SOURCE block (SOURCE line num: " .
                "$access_hr->{'src_line_num'})", $NO_MAIL);
            return 0;
        }
    }

    if ($decrypt_algo == $ALG_GNUPG) {
        if ($access_hr->{'GPG_REMOTE_ID'} ne 'ANY') {
            &logr('[+]', "received valid GnuPG encrypted packet " .
                qq|(signed with required key ID: "$gpg_sign_id") from: | .
                "$src_ip, remote user: $msg_hr->{'username'}, " .
                "client version: $msg_hr->{'remote_version'} " .
                "(SOURCE line num: $access_hr->{'src_line_num'})", $NO_MAIL);
        } else {
            &logr('[+]', "received valid GnuPG encrypted packet " .
                "from: $src_ip, remote user: $msg_hr->{'username'}, " .
                "client version: $msg_hr->{'remote_version'} " .
                "(SOURCE line num: $access_hr->{'src_line_num'})",
                $NO_MAIL);
        }
    } else {
        &logr('[+]', "received valid Rijndael encrypted " .
            "packet from: $src_ip, remote user: $msg_hr->{'username'}, " .
            "client version: $msg_hr->{'remote_version'} " .
            "(SOURCE line num: $access_hr->{'src_line_num'})",
            $NO_MAIL);
    }

    unless ($grant_access) {
        &logr('[-]', "Could not work out access to ports from SPA packet " .
            "originating from: $src_ip", $NO_MAIL);
        return 0;
    }

    ### cache the digest
    $digest_store{$digest} = $src_ip;

    ### write digest to disk
    &diskwrite_digest($digest, $src_ip)
        if $config{'ENABLE_DIGEST_PERSISTENCE'} eq 'Y';

    ### grant access through the firewall
    &grant_access($allow_src, $msg_hr, \%nat_info,
        {}, \%grant_ports, $access_hr);

    return 1;
}

sub SPA_cmd() {
    my ($msg_hr, $src_ip, $decrypt_algo, $gpg_sign_id,
        $digest, $access_hr) = @_;

    unless ($access_hr->{'ENABLE_CMD_EXEC'}) {
        &logr('[-]', qq|received command "$msg_hr->{'action'}" | .
                "but command mode not enabled for $src_ip", $SEND_MAIL);
        return 0;
    }

    if (defined $access_hr->{'CMD_REGEX'}) {
        unless ($msg_hr->{'action'} =~ m|$access_hr->{'CMD_REGEX'}|) {
            &logr('[-]', qq|received command "$msg_hr->{'action'}" | .
                    "from $src_ip but CMD_REGEX did not match $src_ip",
                    $SEND_MAIL);
            return 0;
        }
    }

    my $cmd = $msg_hr->{'action'};
    my $run_cmd = '';
    my $cmd_ip  = '';

    if ($cmd =~ m|^\s*($ip_re),(.*)|) {
        $cmd_ip  = $1;
        $run_cmd = $2;
    } else {
        $run_cmd = $cmd;
    }

    ### pre-1.0 versions did not prepend command string with "<ip>,"
    if ($cmd_ip eq '0.0.0.0') {
        if ($config{'REQUIRE_SOURCE_ADDRESS'} eq 'Y' or not
                &is_ip_included($cmd_ip,
                    $access_hr->{'REQUIRE_SOURCE_ADDRESS'},
                    $access_hr->{'require_src_addr_exceptions'})) {
            &logr('[-]', "IP: $src_ip sent SPA packet that " .
                "contained 0.0.0.0 (-s on the client side) " .
                "but REQUIRE_SOURCE_ADDRESS is enabled " .
                "(SOURCE line num: $access_hr->{'src_line_num'})",
                $SEND_MAIL);
            return 0;
        }
    }

    if (&is_ip_included($cmd_ip, $blacklist_ar, $blacklist_exclude_ar)) {
        print STDERR localtime() . " [+] SPA_cmd() ",
        "$cmd_ip in BLACKLIST" if $debug;
        &logr('[-]', "cmd IP: $cmd_ip SPA packet from $src_ip is " .
            "blacklisted (SOURCE line num: " .
            "$access_hr->{'src_line_num'})", $SEND_MAIL);
        return 0;
    }

    if ($decrypt_algo == $ALG_GNUPG) {
        if ($access_hr->{'GPG_REMOTE_ID'} ne 'ANY') {
            &logr('[+]', "received valid GnuPG encrypted packet " .
                qq|(signed with required key ID: "$gpg_sign_id") from: | .
                "$src_ip, remote user: $msg_hr->{'username'}",
                $NO_MAIL);
        } else {
            &logr('[+]', "received valid GnuPG encrypted packet " .
                "from: $src_ip, remote user: $msg_hr->{'username'}",
                $NO_MAIL);
        }
    } else {
        &logr('[+]', "received valid Rijndael encrypted " .
            "packet from: $src_ip, remote user: $msg_hr->{'username'}",
            $NO_MAIL);
    }

    &logr('[+]', qq|executing command "$run_cmd" for $src_ip|, $SEND_MAIL);

    ### cache the digest
    $digest_store{$digest} = $src_ip;

    ### write the digest to disk
    &diskwrite_digest($digest, $src_ip)
        if $config{'ENABLE_DIGEST_PERSISTENCE'} eq 'Y';

    ### execute the command
    &exec_command($run_cmd, $config{'PCAP_CMD_TIMEOUT'});

    return 1;
}

sub external_cmd_open() {
    my ($src, $msg_hr, $open_ports_hr, $access_hr) = @_;

    my $open_cmd  = '';
    my $close_cmd = '';
    my $cmd_port  = 0;
    my $cmd_proto = 'NA';
    my $found_port_proto = 0;
    my $cmd_alarm = $EXTERNAL_CMD_ALARM;

    if ($access_hr->{'EXTERNAL_CMD_OPEN'}) {
        $open_cmd  = $access_hr->{'EXTERNAL_CMD_OPEN'};
        $close_cmd = $access_hr->{'EXTERNAL_CMD_CLOSE'};
        $cmd_alarm = $access_hr->{'EXTERNAL_CMD_ALARM'};
    } elsif ($config{'EXTERNAL_CMD_OPEN'} and $config{'EXTERNAL_CMD_CLOSE'}) {
        $open_cmd  = $config{'EXTERNAL_CMD_OPEN'};
        $close_cmd = $config{'EXTERNAL_CMD_CLOSE'};
        $cmd_alarm = $config{'EXTERNAL_CMD_ALARM'};
    } else {
        return;
    }

    PROTO: for my $proto (keys %{$open_ports_hr}) {
        for my $port (keys %{$open_ports_hr->{$proto}}) {
            ### only allow one port/proto substitution for now - this can be
            ### worked around by passing OPEN_PORTS directly (via key
            ### substitution below) on the external command line.
            $cmd_port  = $port;
            $cmd_proto = $proto;
            $found_port_proto = 1;
            last PROTO;
        }
    }

    ### perform variable substitutions on the external command to run
    $open_cmd = &external_cmd_str_expand($open_cmd, $src, $cmd_port,
            $cmd_proto, $access_hr);
    $close_cmd = &external_cmd_str_expand($close_cmd, $src, $cmd_port,
            $cmd_proto, $access_hr);

    &logr('[+]', qq|executing external open command "$open_cmd" for $src|,
        $SEND_MAIL);

    ### execute the "open" command
    &exec_command($open_cmd, $cmd_alarm);

    ### let knoptm run the "close" command
    &write_knoptm_fw_cache_entry(
        time(),
        $access_hr->{'FW_ACCESS_TIMEOUT'},
        $src,
        0,
        '0.0.0.0',
        $cmd_port,
        $cmd_proto,
        'NA',
        'NA',
        'NA',
        'NA',
        '0.0.0.0/0',
        0,
        encode_base64($close_cmd, ''),
        $cmd_alarm
    );

    return;
}

sub external_cmd_str_expand() {
    my ($cmd_str, $src, $cmd_port, $cmd_proto, $access_hr) = @_;

    print STDERR localtime() . " [+] External command ",
        "(before var expansion): $cmd_str\n" if $debug;

    ### expand SPA source IP, port, and protocol
    if ($config{'ENABLE_EXT_CMD_PREFIX'} eq 'Y') {
        $cmd_str =~ s|\$$config{'EXT_CMD_PREFIX'}SRC|$src|;
        $cmd_str =~ s|\$$config{'EXT_CMD_PREFIX'}PORT|$cmd_port|;
        $cmd_str =~ s|\$$config{'EXT_CMD_PREFIX'}PROTO|$cmd_proto|;
    } else {
        $cmd_str =~ s|\$SRC|$src|;
        $cmd_str =~ s|\$PORT|$cmd_port|;
        $cmd_str =~ s|\$PROTO|$cmd_proto|;
    }

    ### expand any hash keys from access.conf
    for my $key (keys %access_keys) {
        next unless defined $access_hr->{$key};
        if ($config{'ENABLE_EXT_CMD_PREFIX'} eq 'Y') {
            $cmd_str =~ s|\$$config{'EXT_CMD_PREFIX'}$key|$access_hr->{$key}|;
        } else {
            $cmd_str =~ s|\$$key|$access_hr->{$key}|;
        }
    }

    print STDERR localtime() . "     External command ",
        "(after var expansion): $cmd_str\n" if $debug;

    return $cmd_str;
}

sub is_replay_attack() {
    my ($decrypted_data, $src_ip) = @_;

    my $rv = 0;
    my @digests = ();
    my $disk_write_digest = '';

    if ($use_fko_module) {
        ### store off the original digest type associated with this incoming
        ### SPA packet
        $fko_incoming_digest_type = 0;
        $fko_incoming_digest_type = $fko_obj->digest_type() or return 1, '';
    }

    if ($config{'DIGEST_TYPE'} eq 'ALL') {
        if ($use_fko_module) {
            for my $digest_type (FKO->FKO_DIGEST_SHA256,
                    FKO->FKO_DIGEST_SHA1,
                    FKO->FKO_DIGEST_MD5) {
                my $digest = &fko_compute_digest($digest_type);
                if ($digest) {
                    push @digests, $digest;
                } else {
                    return 1, '';
                }
            }
        } else {
            push @digests, sha256_base64($decrypted_data);
            push @digests, sha1_base64($decrypted_data);
            push @digests, md5_base64($decrypted_data);
        }
    } else {
        if ($config{'DIGEST_TYPE'} =~ /SHA256/) {
            if ($use_fko_module) {
                my $digest = &fko_compute_digest(FKO->FKO_DIGEST_SHA256);
                if ($digest) {
                    push @digests, $digest;
                } else {
                    return 1, '';
                }
            } else {
                push @digests, sha256_base64($decrypted_data);
            }
        }
        if ($config{'DIGEST_TYPE'} =~ /SHA1/) {
            if ($use_fko_module) {
                my $digest = &fko_compute_digest(FKO->FKO_DIGEST_SHA1);
                if ($digest) {
                    push @digests, $digest;
                } else {
                    return 1, '';
                }
            } else {
                push @digests, sha1_base64($decrypted_data);
            }
        }
        if ($config{'DIGEST_TYPE'} =~ /MD5/) {
            if ($use_fko_module) {
                my $digest = &fko_compute_digest(FKO->FKO_DIGEST_MD5);
                if ($digest) {
                    push @digests, $digest;
                } else {
                    return 1, '';
                }
            } else {
                push @digests, md5_base64($decrypted_data);
            }
        }
    }

    if (@digests) {

        ### this prefers SHA256 because of the ordering above.
        $disk_write_digest = $digests[0];

        if ($debug) {
            print STDERR localtime() . ' [+] Final @digests array: ', "\n",
                Dumper(@digests);
        }
        for my $digest (@digests) {
            ### note that the %digest_store may contain non-SHA256 digests from
            ### a previous instance of fwknop - this check ensures that we
            ### consider all previous digests
            if (defined $digest_store{$digest}) {
                ### Replay attack!  Send warning email and return.
                if ($digest_store{$digest}) {
                    &logr('[-]', "attempted SPA packet replay from: $src_ip " .
                        "(original SPA src: $digest_store{$digest}, " .
                        "digest: $digest)",
                        $SEND_MAIL);
                } else {
                    &logr('[-]', "attempted SPA packet replay from: $src_ip " .
                        "($digest: $digest)", $SEND_MAIL);
                }

                ### see if we need to exit if the packet limit (set with -C on the
                ### command line) has been reached
                &check_packet_limit();

                $rv = 1;
                last;
            }
        }
    } else {
        ### could not calculate the digest for some reason; don't
        ### trust the packet
        &logr('[-]', "could not calculate digest " .
            "for SPA packet from: $src_ip", $SEND_MAIL);
        $rv = 1;
    }
    return $rv, $disk_write_digest;
}

sub fko_acquire_object() {

    &fko_destroy_object() if $fko_obj;

    ### initialize the FKO object
    $fko_obj = FKO->new()
        or die "[*] Could not acquire FKO object: ", FKO->error_str;

    if ($debug) {
        print STDERR localtime() . " [+] Using libfko ",
            "functions via the FKO module.\n";
    }
    return;
}

sub fko_destroy_object() {
    $fko_obj->destroy();
    $fko_obj = ();
    return;
}

sub fko_compute_digest() {
    my $digest_type = shift;

    my $fko_err = $fko_obj->digest_type($digest_type);
    if ($fko_err) {
        &logr('[-]', "FKO error setting digest type " .
            "$digest_type: " . $fko_obj->errstr($fko_err),
            $NO_MAIL);
    }
    $fko_err = $fko_obj->spa_digest($FKO_RECOMPUTE);
    if ($fko_err) {
        &logr('[-]', "FKO error recomputing computing digest: " .
            $fko_obj->errstr($fko_err),
            $NO_MAIL);
        return 0;
    }

    my $digest = $fko_obj->spa_digest();
    unless ($digest) {
        &logr('[-]', "FKO error computing digest: " .
            $fko_obj->errstr($fko_err),
            $NO_MAIL);
        return 0;
    }

    print STDERR localtime() . " [+] FKO calculated digest ",
        "(type: $digest_type): $digest\n" if $debug;
    return $digest;
}

sub server_auth_verify_crypt_pw() {
    my ($username, $pw, $shadow_file) = @_;

    unless (-e $shadow_file) {
        &logr('[-]', "shadow file $shadow_file does not exist", $NO_MAIL);
        return 0;
    }

    my $shadow_hash = '';
    open S, "< $shadow_file" or die "[*] Could not open $shadow_file: $!";
    while (<S>) {
        my $line = $_;
        if ($line =~ /^\s*$username:(\S+?):/) {
            $shadow_hash = $1;
        }
    }
    close S;

    ### mbr:$1$nrU****************************:13108:0:99999:7:::
    unless ($shadow_hash) {
        &logr('[-]', "could not get password entry for $username " .
            "from /etc/shadow", $NO_MAIL);
        return 0;
    }

    return 1 if (crypt($pw, $shadow_hash) eq $shadow_hash);
    return 0;
}

sub knock_loop() {

    print STDERR localtime() . " [+] Opening $fw_data_file, and ",
        "entering main loop.\n" if $debug;

    ### track file size so we can re-open if the logfile is rotated
    my $fw_data_file_size  = -s $fw_data_file;
    my $fw_data_file_inode = (stat($fw_data_file))[1];
    my $fw_data_file_check_ctr = 0;

    my $skip_first_loop = 1;

    open FWLOG, $fw_data_file or die "[*] Could not open $fw_data_file: $!";

    ### main server loop to parse iptables log messages
    MAIN: for (;;) {

        my @fw_pkts = ();

        ### allow the contents of the fwdata file to be processed only after
        ### the first loop has been executed.
        if ($skip_first_loop) {

            $skip_first_loop = 0;
            seek FWLOG,0,2;  ### seek to the end of the file
            next MAIN;

        } else {

            @fw_pkts = <FWLOG>;
        }

        if ($fw_data_file_check_ctr == 10) {
            if (-e $fw_data_file) {
                my $size_tmp  = -s $fw_data_file;
                my $inode_tmp = (stat($fw_data_file))[1];
                if ($inode_tmp != $fw_data_file_inode
                        or $size_tmp < $fw_data_file_size) {

                    close FWDATA;

                    &sys_log('[+]', "iptables syslog file $fw_data_file " .
                        "shrank or was rotated, so re-opening");

                    ### re-open the fwdata file
                    open FWDATA, $fw_data_file or die
                        "[*] Could not open $fw_data_file: $!";

                    $skip_first_loop = 1;

                    ### set file size and inode
                    $fw_data_file_size  = $size_tmp;
                    $fw_data_file_inode = $inode_tmp;
                }
            }
            $fw_data_file_check_ctr = 0;
        }

        &process_pkts(\@fw_pkts) if @fw_pkts;

        ### always check to see if we need to timeout knock sequences
        ### that exceed the KNOCK_INTERVAL
        &timeout_invalid_sequences();

        &collect_warn_die_msgs();

        ### clearerr() on the FWLOG filehandle to be ready for new packets
        FWLOG->clearerr();

        sleep $config{'SLEEP_INTERVAL'};
    }
    close FWLOG;
    return;
}

sub pcap_validate_msg() {
    my ($msg, $source_block_num, $access_hr) = @_;

    my %msg_hsh = (
        'random_number'   => 0,
        'username'        => '',
        'remote_time'     => 0,
        'remote_version'  => '',
        'numeric_version' => 0,   ### calculated locally by fwknopd
        'action_type'     => -1,
        'action'          => '',
        'server_auth'     => '',  ### optional
        'nat_info'        => '',  ### optional
        'client_timeout'  => -1,  ### optional
        'digest'          => ''
    );

    my @fields = ();
    my $fko_err = 0;

    ### the last field in the SPA packet is the digest, so see if it
    ### checks out first (this is the internal digest, not the digest that
    ### guards against replay attacks).
    unless (&check_digest($msg, \%msg_hsh)) {
        print STDERR localtime() . " [-] Key mis-match or broken message ",
            "checksum for SOURCE $access_hr->{'src_str'} ",
            "(# $source_block_num in access.conf)\n"
            if $debug;
        return 0, {};
    }

    unless ($use_fko_module) {
        @fields = split /:/, $msg;

        unless (@fields) {
            print STDERR localtime() . " [-] Could not split decrypted ",
                "message into an array.\n" if $debug;
            return 0, {};
        }

        if ($debug and $verbose) {
            print STDERR localtime() . " [+] Packet array:\n", Dumper @fields;
        }

        unless ($#fields+1 >= $SPA_MIN_PACKET_FIELDS
                and $#fields+1 <= $SPA_MAX_PACKET_FIELDS) {
            print STDERR localtime() . " [-] Invalid number of fields in ",
                "SPA packet, expected $SPA_MIN_PACKET_FIELDS-",
                "$SPA_MAX_PACKET_FIELDS, got " . ($#fields+1) . ".\n" if $debug;
            return 0, {};
        }
    }

    ### random number
    #
    if ($use_fko_module) {
        $msg_hsh{'random_number'} = $fko_obj->rand_value();
    } else {
        $msg_hsh{'random_number'} = $fields[0];
    }
    unless (&is_digit($msg_hsh{'random_number'})) {
        &logr('[-]', "non-digit random number in decrypted SPA " .
            "packet: $msg_hsh{'random_number'}", $SEND_MAIL);
        return 0, {};
    }

    ### username
    #
    if ($use_fko_module) {
        $msg_hsh{'username'} = $fko_obj->username();
    } else {
        $msg_hsh{'username'} = decode_base64($fields[1]);
    }

    ### timestamp
    #
    if ($use_fko_module) {
        $msg_hsh{'remote_time'} = $fko_obj->timestamp();
    } else {
        $msg_hsh{'remote_time'} = $fields[2];
    }
    unless (&is_digit($msg_hsh{'remote_time'})) {
        &logr('[-]', "non-digit timestamp in decrypted SPA packet",
            $SEND_MAIL);
        return 0, {};
    }

    ### remote client version
    #
    if ($use_fko_module) {
        $msg_hsh{'remote_version'} = $fko_obj->version();
    } else {
        $msg_hsh{'remote_version'} = $fields[3];
    }

    unless (&SPA_parse_client_version(\%msg_hsh)) {
        &logr('[-]', "invalid client string in decrypted SPA packet",
            $SEND_MAIL);
        return 0, {};
    }

    ### message type
    #
    if ($use_fko_module) {
        $msg_hsh{'action_type'} = $fko_obj->spa_message_type();
    } else {
        $msg_hsh{'action_type'} = $fields[4];
    }

    if (&is_digit($msg_hsh{'action_type'})) {
        return 0, {} unless $msg_hsh{'action_type'} == $SPA_COMMAND_MODE
                or $msg_hsh{'action_type'} == $SPA_ACCESS_MODE
                or $msg_hsh{'action_type'} == $SPA_NAT_ACCESS_MODE
                or $msg_hsh{'action_type'} == $SPA_CLIENT_TIMEOUT_ACCESS_MODE
                or $msg_hsh{'action_type'} == $SPA_CLIENT_TIMEOUT_NAT_ACCESS_MODE
                or $msg_hsh{'action_type'} == $SPA_LOCAL_NAT_ACCESS_MODE
                or $msg_hsh{'action_type'} == $SPA_CLIENT_TIMEOUT_LOCAL_NAT_ACCESS_MODE;
        $msg_hsh{'action_type'} = $msg_hsh{'action_type'};
    } else {
        &logr('[-]', "non-digit action type in decrypted SPA packet",
            $SEND_MAIL);
        return 0, {};
    }
    if ($debug) {
        print STDERR localtime() .
            " [+] SPA action type: $msg_hsh{'action_type'}\n";
    }

    ### action
    #
    if ($use_fko_module) {
        $msg_hsh{'action'} = $fko_obj->spa_message();
    } else {
        $msg_hsh{'action'} = decode_base64($fields[5]);
    }

    ### server_auth was introduced in 0.9.3
    #
    if ($msg_hsh{'numeric_version'} >= 93) {

        ### iptables FORWARD/DNAT access was introduced in 1.9.0
        if ($msg_hsh{'numeric_version'} >= 190) {
            my $found = 0;
            if ($msg_hsh{'action_type'} == $SPA_NAT_ACCESS_MODE
                        or $msg_hsh{'action_type'} == $SPA_LOCAL_NAT_ACCESS_MODE) {
                if ($use_fko_module) {
                    $msg_hsh{'nat_info'} = $fko_obj->spa_nat_access();
                } else {
                    if ($#fields == $SPA_MIN_PACKET_FIELDS) {
                        $msg_hsh{'nat_info'} = decode_base64($fields[6]);
                    }
                }
                $found = 1;
            } elsif ($msg_hsh{'numeric_version'} >= 192) {
                ### client timeouts were introduced in 1.9.2
                if ($msg_hsh{'action_type'} == $SPA_CLIENT_TIMEOUT_ACCESS_MODE) {
                    if ($use_fko_module) {
                        $msg_hsh{'client_timeout'} = $fko_obj->spa_client_timeout();
                    } else {
                        $msg_hsh{'client_timeout'} = $fields[6];
                    }
                    $found = 1;
                } elsif ($msg_hsh{'action_type'}
                            == $SPA_CLIENT_TIMEOUT_NAT_ACCESS_MODE) {
                    if ($use_fko_module) {
                        $msg_hsh{'nat_info'} = $fko_obj->spa_nat_access();
                        $msg_hsh{'client_timeout'} = $fko_obj->spa_client_timeout();
                    } else {
                        $msg_hsh{'nat_info'} = decode_base64($fields[6]);
                        $msg_hsh{'client_timeout'} = $fields[7];
                    }
                    $found = 1;
                } elsif ($msg_hsh{'action_type'}
                            == $SPA_CLIENT_TIMEOUT_LOCAL_NAT_ACCESS_MODE) {
                    if ($use_fko_module) {
                        $msg_hsh{'nat_info'} = $fko_obj->spa_nat_access();
                        $msg_hsh{'client_timeout'} = $fko_obj->spa_client_timeout();
                    } else {
                        $msg_hsh{'nat_info'} = decode_base64($fields[6]);
                        $msg_hsh{'client_timeout'} = $fields[7];
                    }
                    $found = 1;
                }
                if ($found) {
                    unless (&is_digit($msg_hsh{'client_timeout'})) {
                        &logr('[-]', "non-digit client timeout in decrypted " .
                            "SPA packet", $SEND_MAIL);
                        return 0, {};
                    }
                }
            }
            unless ($found) {
                if (not $use_fko_module and $#fields+1 > $SPA_MIN_PACKET_FIELDS) {
                    $msg_hsh{'server_auth'} = decode_base64($fields[6]);
                }
            }
        } else {
            if ($use_fko_module) {
                &logr('[-]', "remote libfko version less than minimum ",
                    "required by FKO module", $SEND_MAIL);
                return 0, {};
            } else {
                if ($#fields+1 > $SPA_MIN_PACKET_FIELDS) {
                    $msg_hsh{'server_auth'} = decode_base64($fields[6]);
                }
            }
        }
    } else {
        unless ($use_fko_module and $#fields+1 == $SPA_MIN_PACKET_FIELDS) {
            print STDERR localtime() . " [-] SPA packet from version: ",
                "$msg_hsh{'remote_version'} ",
                "does not have $SPA_MIN_PACKET_FIELDS fields"
                if $debug;
            return 0, {};
        }
    }

    print STDERR Dumper \%msg_hsh if $debug and $verbose;

    if ($debug) {
        print STDERR localtime() .
            " [+] Decoded message: $msg_hsh{'random_number'}:",
            "$msg_hsh{'username'}:$msg_hsh{'remote_time'}:",
            "$msg_hsh{'remote_version'}:$msg_hsh{'action_type'}:",
            "$msg_hsh{'action'}";

        if ($msg_hsh{'nat_info'}) {
            print STDERR ":$msg_hsh{'nat_info'}";
        }

        if ($msg_hsh{'client_timeout'}) {
            print STDERR ":$msg_hsh{'client_timeout'}";
        }

        ### careful not to display password information
        if ($msg_hsh{'server_auth'}
                and $msg_hsh{'server_auth'} =~ /^\s*(\w+),(.*)/) {

            print STDERR ":$1,";
            for (my $i=0; $i < length($2); $i++) {
                print STDERR "*";
            }
        }

        print STDERR ":$msg_hsh{'digest'}\n";
    }
    return 1, \%msg_hsh;
}

sub SPA_parse_client_version() {
    my $msg_hr = shift;

    my $ver = '';
    if ($msg_hr->{'remote_version'} =~ /^(\d+\.\d+\.\d+)-pre\d+$/) {
        ### remote client is a -pre release
        $ver = $1;
    } elsif ($msg_hr->{'remote_version'} =~ /^(\d+\.\d+\.\d+)$/) {
        $ver = $1;
    } elsif ($msg_hr->{'remote_version'} =~ /^(\d+\.\d+)-pre\d+$/) {
        ### remote client is a -pre release
        $ver = $1;
    } elsif ($msg_hr->{'remote_version'} =~ /^(\d+\.\d+)$/) {
        $ver = $1;
    } else {
        print STDERR localtime() . " [-] Could not determine remote ",
            "client numeric version." if $debug;
        return 0;
    }

    $ver =~ s|\.||g;
    $ver =~ s|^0||;
    $msg_hr->{'numeric_version'} = $ver;

    print STDERR localtime() . " [+] Remote client numeric version: $ver\n"
        if $debug;
    return 1;
}

sub check_digest() {
    my ($msg_str, $hr) = @_;

    ### give priority to FKO module
    return &fko_check_digest($hr) if $use_fko_module;

    my $rv = 0;
    if ($msg_str =~ /(.*):(\S+)/) {
        my $msg = $1;
        my $sum = $2;
        if (length($sum) == $SHA256_DIGEST_LEN) {
            if ($config{'DIGEST_TYPE'} eq 'ALL'
                    or $config{'DIGEST_TYPE'} =~ /SHA256/) {
                if ($sum eq sha256_base64($msg)) {
                    $hr->{'digest_str'} = 'SHA256';
                    $hr->{'digest'} = $sum;
                    $rv = 1;
                }
            }
        } elsif (length($sum) == $SHA1_DIGEST_LEN) {
            if ($config{'DIGEST_TYPE'} eq 'ALL'
                    or $config{'DIGEST_TYPE'} =~ /SHA1/) {
                if ($sum eq sha1_base64($msg)) {
                    $hr->{'digest_str'} = 'SHA1';
                    $hr->{'digest'} = $sum;
                    $rv = 1;
                }
            }
        } elsif (length($sum) == $MD5_DIGEST_LEN) {
            if ($config{'DIGEST_TYPE'} eq 'ALL'
                    or $config{'DIGEST_TYPE'} =~ /MD5/) {
                if ($sum eq md5_base64($msg)) {
                    $hr->{'digest_str'} = 'MD5';
                    $hr->{'digest'} = $sum;
                    $rv = 1;
                }
            }
        }
    }

    unless ($rv) {
        print STDERR localtime() . " [-] Digest alg mis-match.\n" if $debug;
    }

    return $rv;
}

sub fko_check_digest() {
    my $hr = shift;

    my $rv = 0;

    my $digest_type = $fko_incoming_digest_type;

    print localtime() . " [+] FKO digest type: $digest_type, ",
        "DIGEST_TYPE var: $config{'DIGEST_TYPE'}\n" if $debug;

    if ($digest_type == FKO->FKO_DIGEST_SHA256) {
        if ($config{'DIGEST_TYPE'} eq 'ALL'
                or $config{'DIGEST_TYPE'} =~ /SHA256/) {
            $hr->{'digest_str'} = 'SHA256';
            $rv = 1;
        }
    } elsif ($digest_type == FKO->FKO_DIGEST_SHA1) {
        if ($config{'DIGEST_TYPE'} eq 'ALL'
                or $config{'DIGEST_TYPE'} =~ /SHA1/) {
            $hr->{'digest_str'} = 'SHA1';
            $rv = 1;
        }
    } elsif ($digest_type == FKO->FKO_DIGEST_MD5) {
        if ($config{'DIGEST_TYPE'} eq 'ALL'
                or $config{'DIGEST_TYPE'} =~ /MD5/) {
            $hr->{'digest_str'} = 'MD5';
            $rv = 1;
        }
    } else {
        print STDERR localtime() . " [-] FKO invalid digest type: $digest_type\n"
            if $debug;
    }

    if ($rv) {
        $hr->{'digest'} = $fko_obj->spa_digest();
    } else {
        print STDERR localtime() . " [-] Digest alg mis-match.\n" if $debug;
    }

    return $rv;
}

sub get_pcap_obj() {

    my $pcap_t  = '';
    my $filter  = '';
    my $err     = '';
    my $netmask = 0;
    my $address = 0;

    if ($config{'AUTH_MODE'} eq 'FILE_PCAP'
            or $config{'AUTH_MODE'} eq 'ULOG_PCAP') {

        unless (-e $config{'PCAP_PKT_FILE'}) {
            &pcap_file_exists_loop();
        }

        unless (-s $config{'PCAP_PKT_FILE'} > 0) {
            ### required since we cannot use Net::Pcap::open_offline()
            ### to open a zero-size pcap file.
            &pcap_nonzero_size_loop();
        }

        print STDERR localtime() . " [+] Acquiring packet data from file: ",
            "$config{'PCAP_PKT_FILE'}\n" if $debug;

        $pcap_t = Net::Pcap::open_offline($config{'PCAP_PKT_FILE'}, \$err)
            or die "[*] Could not open $config{'PCAP_PKT_FILE'}: $err";

        ### get past any packets that were from a previous fwknopd
        ### execution.
        Net::Pcap::loop($pcap_t, -1, \&null_func, 'fwknop_tag');

    } else {
        if ($config{'ENABLE_PCAP_PROMISC'} eq 'Y') {
            print STDERR localtime() . " [+] Sniffing (promisc) packet data ",
                "from interface: $config{'PCAP_INTF'}\n" if $debug;
            $pcap_t = Net::Pcap::open_live($config{'PCAP_INTF'},
                $config{'MAX_SNIFF_BYTES'}, 1, 100, \$err)
                    or die "[*] Could not open $config{'PCAP_INTF'}: $err";
        } else {
            print STDERR localtime() . " [+] Sniffing (non-promisc) packet ",
                "data from interface: $config{'PCAP_INTF'}\n" if $debug;
            $pcap_t = Net::Pcap::open_live($config{'PCAP_INTF'},
                $config{'MAX_SNIFF_BYTES'}, 0, 100, \$err)
                    or die "[*] Could not open $config{'PCAP_INTF'}: $err";
        }
    }

    ### apply pcap filter if necessary
    if ($config{'PCAP_FILTER'} ne 'NONE') {
        if ($config{'AUTH_MODE'} eq 'PCAP') {
            if (Net::Pcap::lookupnet($config{'PCAP_INTF'}, \$address,
                    \$netmask, \$err) != 0) {
                if ($config{'ENABLE_PCAP_PROMISC'} eq 'N') {
                    &logr('[-]', "warning: ENABLE_PCAP_PROMISC is disabled and " .
                        "could not get net information for " .
                        "$config{'PCAP_INTF'}: $err, continuing anyway",
                        $NO_MAIL);
                }
            }
        }
        ### set the filter on the traffic
        Net::Pcap::compile($pcap_t, \$filter, $config{'PCAP_FILTER'},
                0, $netmask)
            && die '[*] Unable to compile packet capture filter';
        Net::Pcap::setfilter($pcap_t, $filter)
            && die '[*] Unable to set packet capture filter';
    }

    return $pcap_t;
}

sub pcap_file_exists_loop() {
    while (not -e $config{'PCAP_PKT_FILE'}) {
        &logr('[-]', "pcap file $config{'PCAP_PKT_FILE'} does not " .
            "exist, waiting $err_wait_timer seconds for sniffer to " .
            "create file", $NO_MAIL);
        sleep $err_wait_timer;
    }
    return;
}

sub pcap_nonzero_size_loop() {
    while (-s $config{'PCAP_PKT_FILE'} == 0) {
        &logr('[-]', "zero size pcap file $config{'PCAP_PKT_FILE'}, " .
            "waiting $err_wait_timer seconds for packet data", $NO_MAIL);
        sleep $err_wait_timer;
    }
    return;
}

sub exec_command() {
    my ($cmd, $cmd_alarm) = @_;
    my $pid;
    if ($pid = fork()) {
        local $SIG{'ALRM'} = sub {die "[*] External script timeout.\n"};
        ### the external script should be finished within this timeout
        alarm $cmd_alarm;
        eval {
            waitpid($pid, 0);
        };
        alarm 0;
        if ($@) {
            kill 9, $pid unless kill 15, $pid;
        }
    } else {
        die "[*] Could not fork for external script: $!" unless defined $pid;
        ### if we are already redirecting output within the command itself
        ### then don't redirect again
        if ($cmd =~ /\s*>\s*/) {
            exec qq{$cmd};
        } else {
            exec qq{$cmd > /dev/null 2>&1};
        }
    }
    return;
}

### knock server processsing
sub process_pkts() {
    my $fw_pkts_aref = shift;
    PKT: for my $pkt (@$fw_pkts_aref) {
        my $src = '';
        my $dst = '';
        my $len = -1;
        my $tos = '';
        my $ttl = -1;
        my $id  = -1;
        my $proto = '';
        my $sp    = -1;
        my $dp    = -1;
        my $win   = -1;
        my $type  = -1;
        my $code  = -1;
        my $seq   = -1;
        my $flags = '';
        my $frag_bit = 0;
        my $tcp_options = '';
        next unless $pkt =~ /kernel.*IN=.*OUT=/;
        ### May 18 22:21:26 orthanc kernel: DROP IN=eth2 OUT=
        ### MAC=00:60:1d:23:d0:01:00:60:1d:23:d3:0e:08:00 SRC=192.168.20.25
        ### DST=192.168.20.1 LEN=60 TOS=0x10 PREC=0x00 TTL=64 ID=47300 DF
        ### PROTO=TCP SPT=34111 DPT=6345 WINDOW=5840 RES=0x00 SYN URGP=0
        if ($pkt =~ /SRC=(\S+)\s+DST=(\S+)\s+LEN=(\d+)\s+TOS=(\S+)
                    \s*.*\s+TTL=(\d+)\s+ID=(\d+)\s*.*\s+PROTO=TCP\s+
                    SPT=(\d+)\s+DPT=(\d+)\s+WINDOW=(\d+)\s+
                    RES=\S+\s*(.*)\s+URGP=/x) {
            ($src, $dst, $len, $tos, $ttl, $id, $sp, $dp, $win, $flags) =
                ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10);
            if ($pkt =~ /\sRES=\S+\s*(.*)\s+URGP=/) {
                    $flags = $1;
            }
            $proto = 'tcp';
            unless ($flags !~ /WIN/ &&
                    $flags =~ /ACK/ ||
                    $flags =~ /SYN/ ||
                    $flags =~ /RST/ ||
                    $flags =~ /URG/ ||
                    $flags =~ /PSH/ ||
                    $flags =~ /FIN/ ||
                    $flags eq 'NULL') {
                print STDERR localtime() . " [*] err packet: bad tcp flags.\n"
                    if $debug;
                next PKT;
            }
            $frag_bit = 1 if $pkt =~ /\sDF\s+PROTO/;
            ### don't pickup IP options if --log-ip-options is used
            ### (they appear before the PROTO= field).
            if ($pkt =~ /URGP=\S+\s+OPT\s+\((\S+)\)/) {
                $tcp_options = $1;
            }
            $tcp_ctr++;

            ### Jul 15 23:32:53 orthanc kernel: DROP IN=eth1 OUT=
            ### MAC=00:0c:41:24:68:ef:00:0c:41:24:56:37:08:00 SRC=192.168.10.3
            ### DST=192.168.10.1 LEN=29 TOS=0x00 PREC=0x00 TTL=64 ID=48500 DF
            ### PROTO=UDP SPT=32768 DPT=65533 LEN=9
        } elsif ($pkt =~ /SRC=(\S+)\s+DST=(\S+)\s+LEN=(\d+)\s+TOS=(\S+)\s+
                          .*?\sTTL=(\d+)\s+ID=(\d+)\s*.*\sPROTO=UDP\s+
                          SPT=(\d+)\s+DPT=(\d+)/x) {
            ($src, $dst, $len, $tos, $ttl, $id, $sp, $dp) =
                ($1,$2,$3,$4,$5,$6,$7,$8);
            $proto = 'udp';
            ### make sure we have a "reasonable" packet (note that nmap
            ### can scan port 0 and iptables can report this fact)
            unless ($src and $dst and $len >= 0 and $tos and $ttl >= 0
                    and $id >= 0 and $sp >= 0 and $dp >= 0) {
                next PKT;
            }
            $udp_ctr++;
        } elsif ($pkt =~ /SRC=(\S+)\s+DST=(\S+)\s+LEN=(\d+).*
                          TTL=(\d+).*PROTO=ICMP\s+TYPE=(\d+)\s+
                          CODE=(\d+)\s+ID=(\d+)\s+SEQ=(\d+)/x) {
            ($src, $dst, $len, $ttl, $type, $code, $id, $seq) =
                ($1,$2,$3,$4,$5,$6,$7,$8);
            $proto = 'icmp';
            unless ($src and $dst and $len >= 0 and $ttl >= 0 and $proto
                    and $type >= 0 and $code >= 0 and $id >= 0
                    and $seq >= 0) {
                next PKT;
            }
            $proto = 'icmp';
            $icmp_ctr++;
        } else {
            print STDERR localtime() . " [-] no regex match for pkt: $pkt\n"
                if $debug;
        }

        ### check to see if there are any access directives for $src, and
        ### if not we will do _nothing_ with this IP (unless we are just
        ### trying to fingerprint it).
        my $access_nums_aref = &check_src($src) unless $os_fprint_only;

        unless ($os_fprint_only) {
            unless ($access_nums_aref) {
                print STDERR localtime() . " [-] Packet from $src did not ",
                    "match any SOURCE in $config{'ACCESS_CONF'}\n" if $debug;
                next PKT;
            }
        }

        if ($proto eq 'tcp') {
            print STDERR localtime() . " [+] $proto $src $sp -> $dst $dp, ",
                "$flags\n" if $debug;
        } elsif ($proto eq 'udp') {
            print STDERR localtime() . " [+] $proto $src $sp -> $dst ",
                "$dp\n" if $debug;
        } elsif ($proto eq 'icmp') {
            print STDERR localtime() . " [+] $proto $src -> $dst\n" if $debug;
        }

        ### try to fingerprint the remote OS even though the knock
        ### sequence is not validated yet.
        if ($proto eq 'tcp' and $flags =~ /SYN/) {  ### must have a SYN pkt
            if ($tcp_options) {  ### hopefully --log-tcp-options is being used

                ### p0f based fingerprinting
                &p0f($src, $len, $frag_bit, $ttl, $win, $tcp_options);
            }
        }

        next PKT if $os_fprint_only;

        my $expecting_decrypt = 0;
        my $decrypted = 0;

        NUM: for my $num (@$access_nums_aref) {
            my $access_hr = $access[$num];

            $ip_sequences{$src}{$num} = {}
                unless defined $ip_sequences{$src}{$num};

            my $seq_hr = $ip_sequences{$src}{$num};

            ### keep track of which source block we are dealing with from
            ### access.conf
            my $source_block_num = $access_hr->{'block_num'};

            $seq_hr->{'grant_ctr'} = 0
                if not defined $seq_hr->{'grant_ctr'};

            ### see if the destination port is part of the correct knock sequence
            ### for this source
            my $matched_sequence = 0;

            if ($access_hr->{'DATA_COLLECT_MODE'} == $ENCRYPT_SEQUENCE) {
                if ($dp >= $access_hr->{'PORT_OFFSET'} and
                        $dp < $access_hr->{'PORT_OFFSET'} + 256) {

                    ### keep timestamp for when we started tracking the
                    ### encrypted sequence
                    $seq_hr->{'enc_stime'} = time()
                        unless defined $seq_hr->{'enc_stime'};

                    ### add the destination port to the encrypted sequence
                    push @{$seq_hr->{'enc_ports'}}, $dp;

                    print STDERR localtime() . " [+] Added $dp to encrypted ",
                        "sequence for $src ",
                        "(packet: $#{$seq_hr->{'enc_ports'}})\n"
                        if $debug;
                }

                ### see if the encrypted sequence checks out
                if ($#{$seq_hr->{'enc_ports'}}
                        == $enc_blocksize - 1) {

                    $expecting_decrypt = 1;

                    ### attempt to decrypt the sequence
                    my ($rv, $allow_src, $dec_allow_port,
                        $dec_allow_proto, $username) =
                            &decrypt_sequence($src, $seq_hr,
                                $access_hr);

                    if ($rv) {
                        $decrypted = 1;

                        &logr('[+]', "successful knock decrypt for $src " .
                            "(SOURCE block: $source_block_num)", $SEND_MAIL);

                        ### see if we need to match the OS
                        unless (&matched_os($src, $access_hr)) {
                            delete $ip_sequences{$src}{$num};
                            next NUM;
                        }

                        ### see if we need to match the username
                        unless (&matched_username($username,
                                $access_hr)) {
                            delete $ip_sequences{$src}{$num};
                            next NUM;
                        }

                        ### check to see if we have already exceeded the
                        ### maximum number of allowed sequences (this helps
                        ### to prevent replay attacks).
                        if (defined $access_hr->{'KNOCK_LIMIT'}) {
                            if ($seq_hr->{'grant_ctr'}
                                    > $access_hr->{'KNOCK_LIMIT'}) {
                                &logr('[-]', "$src exceeded knock limit (set to " .
                                    "$access_hr->{'KNOCK_LIMIT'} accesses)",
                                    $SEND_MAIL);
                                &logr('[-]', "access controls for $src will " .
                                    "not be modified", $SEND_MAIL);
                                delete $ip_sequences{$src}{$num};
                                next NUM;
                            }
                        }

                        ### all criteria met for encrypted sequence;
                        ### grant access
                        my %open_ports = %{$access_hr->{'OPEN_PORTS'}};
                        $open_ports{$dec_allow_proto}{$dec_allow_port} = '';

                        &grant_access($allow_src, {}, {}, $seq_hr,
                            \%open_ports, $access_hr);

                    }
                    delete $ip_sequences{$src}{$num};
                    next NUM;
                }
            } elsif (defined $access_hr->{'SHARED_SEQUENCE'}) {
                $seq_hr->{'port_seq'} = 0
                    unless defined $seq_hr->{'port_seq'};
                if ($dp == $access_hr->{'SHARED_SEQUENCE'}->
                            [$seq_hr->{'port_seq'}]->{'port'}
                        and $proto eq $access_hr->{'SHARED_SEQUENCE'}->
                            [$seq_hr->{'port_seq'}]->{'proto'}) {

                    push @{$seq_hr->{'port_times'}}, time();

                    ### increment sequence counter (takes into account timing
                    ### requirements).
                    next NUM unless &incr_seq($src, $seq_hr, $access_hr);

                    ### if we made it to the end of the sequence then we have
                    ### a correct knock sequence
                    if ($seq_hr->{'port_seq'}
                            == $#{$access_hr->{'SHARED_SEQUENCE'}}+1) {
                        print STDERR localtime() . " [+] Matched knock ",
                            "sequence for $src\n" if $debug;
                        $matched_sequence = 1;
                    }
                } else {
                    print STDERR localtime() . " [-] Could not match dst ",
                        "port: $dp at sequence ",
                        "number: $seq_hr->{'port_seq'}\n"
                        if $debug;
                    delete $ip_sequences{$src}{$num};
                    next NUM;
                }
            }

            ### we matched the knock sequence, so reset for new
            ### sequence (note we may have other criteria to meet
            ### before actually granting access).
            if ($matched_sequence) {
                delete $seq_hr->{'port_times'};
                $seq_hr->{'port_seq'} = 0;

                &logr('[+]', "port knock access sequence matched for $src " .
                    "(SOURCE block: $source_block_num)", $SEND_MAIL);

                next NUM unless &matched_os($src, $seq_hr);

                ### check to see if we have already exceeded the maximum number
                ### of allowed sequences (this helps to prevent replay attacks).
                if (defined $access_hr->{'KNOCK_LIMIT'}) {
                    if ($seq_hr->{'grant_ctr'}
                            > $access_hr->{'KNOCK_LIMIT'}) {
                        &logr('[-]', "$src exceeded knock limit (set to " .
                            "$access_hr->{'KNOCK_LIMIT'} accesses)",
                            $SEND_MAIL);
                        &logr('[-]', "access controls for $src will not be " .
                            "modified", $SEND_MAIL);
                        next NUM;
                    }
                }

                ### if we made it here then the shared sequence checked out and
                ### we need to grant access by modifying the iptables ruleset
                ### (if the ruleset does not already allow $src of course).
                &grant_access($src, {}, {}, $seq_hr,
                    $access_hr->{'OPEN_PORTS'}, $access_hr);
            }
        }
        if ($expecting_decrypt and not $decrypted) {
            &logr('[-]', "sequence decrypt failed for $src", $SEND_MAIL);
        }
    }

    ### see if we need to exit if the packet limit (set with -C on the
    ### command line) has been reached
    &check_packet_limit();

    if ($os_fprint_only) {
        &print_p0f();
    }
    return;
}

sub matched_os() {
    my ($src, $href) = @_;

    ### see if we require any OS match at all
    return 1 unless (defined $href->{'REQUIRE_OS'} or
            defined $href->{'REQUIRE_OS_REGEX'});

    unless (defined $p0f{$src}) {
        ### could not guess the OS
        if (defined $href->{'REQUIRE_OS'}) {
            &logr('[-]', "could not fingerprint OS for $src, expecting OS: " .
                $href->{'REQUIRE_OS'}, $SEND_MAIL);
        } elsif (defined $href->{'REQUIRE_OS_REGEX'}) {
            &logr('[-]', "could not fingerprint OS for $src, expecting OS " .
                "regex: $href->{'REQUIRE_OS_REGEX'}", $SEND_MAIL);
        }
        return 0;
    }

    if (defined $href->{'REQUIRE_OS'}) {
        if (defined $p0f{$src}) {
            my $first_os_key = '';
            for my $os (keys %{$p0f{$src}}) {
                $first_os_key = $os unless $first_os_key;
                if ($os eq $href->{'REQUIRE_OS'}) {
                    &logr('[+]', "OS guess: $os " .
                        "matched for $src", $SEND_MAIL);
                    return 1;
                }
            }
            ### there may be more than one OS fingerprint, but
            ### just print one (if we make it here there was no
            ### match).
            &logr('[-]', "OS fingerprint mismatch for $src: " .
                "expected: $href->{'REQUIRE_OS'}, " .
                "received: $first_os_key", $SEND_MAIL);
            return 0;

        }
    } elsif (defined $href->{'REQUIRE_OS_REGEX'}) {
        if (defined $p0f{$src}) {
            my $first_os_key = '';
            for my $os (keys %{$p0f{$src}}) {
                $first_os_key = $os unless $first_os_key;
                if ($os =~ m|$href->{'REQUIRE_OS_REGEX'}|i) {
                    &logr('[+]', "OS guess: $os " .
                        "regex matched for $src", $SEND_MAIL);
                    return 1;
                }
            }

            ### there may be more than one OS fingerprint, but
            ### just print one.
            &logr('[-]', "OS fingerprint regex mismatch for $src: " .
                "expected: $href->{'REQUIRE_OS_REGEX'}, " .
                "received: $first_os_key", $SEND_MAIL);
            return 0;
        }
    }
    return 0;
}

sub matched_username() {
    my ($username, $href) = @_;

    return 1 unless defined $href->{'REQUIRE_USERNAME'};

    if ($username) {
        if ($username eq $href->{'REQUIRE_USERNAME'}) {
            &logr('[+]', "username $username match", $NO_MAIL);
            return 1;
        } else {
            &logr('[-]', "username mismatch, expected: " .
                "$href->{'REQUIRE_USERNAME'}, got: $username", $SEND_MAIL);
            return 0;
        }
    } else {
        &logr('[-]', "missing username in encrypted " .
            "sequence, expected: $href->{'REQUIRE_USERNAME'}", $SEND_MAIL);
        return 0;
    }
    return 0;
}

sub check_src() {
    my $src = shift;

    my @access_nums = ();

    if (&is_ip_included($src, $blacklist_ar, $blacklist_exclude_ar)) {
        print STDERR localtime() . " [+] check_src() ",
            "$src in BLACKLIST" if $debug;
        return \@access_nums;
    }

    ### now process the SOURCE stanzas
    for (my $i=0; $i<=$#access; $i++) {
        my $access_hr = $access[$i];
        my $matched_src = 0;
        if (&is_ip_included($src, $access_hr->{'SOURCE'},
                $access_hr->{'exclude_nets'})) {
            print STDERR localtime() . " [+] Packet from $src matched ",
                "$access_hr->{'src_str'} (line: ",
                "$access_hr->{'src_line_num'})\n"
                if $debug;
            push @access_nums, $i;
        }
    }
    return \@access_nums;
}

sub is_base64() {
    my $data = shift;

    ### check to make sure the packet data only contains base64 encoded
    ### characters per RFC 3548:   0-9, A-Z, a-z, +, /, =
    if ($data =~ /[^\x30-\x39\x41-\x5a\x61-\x7a\x2b\x2f\x3d]/) {
        return 0;
    }
    if ($data =~ /=[^=]/) {
        return 0;
    }
    return 1;
}

sub is_url_base64() {
    my $data = shift;

    ### check to make sure the packet data only contains base64 encoded
    ### characters per RFC 3548, except that "-" replaces "+", and "_"
    ### replaces "/":
    if ($data =~ /[^\x30-\x39\x41-\x5a\x61-\x7a\x2d\x5f\x3d]/) {
        return 0;
    }
    if ($data =~ /=[^=]/) {
        return 0;
    }
    return 1;
}

sub is_ip_included() {
    my ($ip, $include_ar, $exclude_ar) = @_;

    my $is_included = 0;

    ### check the include criteria
    for my $net (@$include_ar) {
        if (ipv4_in_network($net, $ip)) {
            print STDERR localtime() . " [+] $ip included by $net\n"
                if $debug;
            $is_included = 1;
            last;
        }
    }

    if ($is_included) {
        ### check the exclude criteria
        for my $net (@$exclude_ar) {
            if (ipv4_in_network($net, $ip)) {
                print STDERR localtime() . " [-] $ip excluded by ! $net\n"
                    if $debug;
                $is_included = 0;
                last;
            }
        }
    }
    return $is_included;
}

sub incr_seq() {
    my ($src, $seq_hr, $access_hr) = @_;
    if (defined $access_hr->{'MIN_TIME_DIFF'}) {
        ### can check relative timings only after we have more than
        ### one matching sequence packet
        if ($seq_hr->{'port_seq'} > 0) {
            if (defined $access_hr->{'MAX_TIME_DIFF'}) {
                my $time = time();
                if (($time - $seq_hr->{'port_times'}[$seq_hr->{'port_seq'}-1])
                            > $access_hr->{'MIN_TIME_DIFF'} and
                        ($time - $seq_hr->{'port_times'}[$seq_hr->{'port_seq'}-1])
                            < $access_hr->{'MAX_TIME_DIFF'}) {
                    print STDERR localtime() . " [+] Sequence min/max time match: ",
                        "($seq_hr->{'port_seq'}) ",
                        "$access_hr->{'SHARED_SEQUENCE'}->[$seq_hr->{'port_seq'}]->{'proto'}/",
                        "$access_hr->{'SHARED_SEQUENCE'}->[$seq_hr->{'port_seq'}]->{'port'}\n"
                        if $debug;
                } else {
                    &logr('[-]', 'Sequence min/max_time exceeded: ' .
                        "$access_hr->{'SHARED_SEQUENCE'}->[$seq_hr->{'port_seq'}]->{'proto'}/" .
                        "$access_hr->{'SHARED_SEQUENCE'}->[$seq_hr->{'port_seq'}]->{'port'} " .
                        "(port sequence num: $seq_hr->{'port_seq'}) ", $SEND_MAIL);
                    $seq_hr->{'port_seq'} = 0;
                    delete $seq_hr->{'port_times'};
                    return 0;
                }
            } else {
                if ((time()
                        - $seq_hr->{'port_times'}[$seq_hr->{'port_seq'}-1])
                        > $access_hr->{'MIN_TIME_DIFF'}) {
                    print STDERR localtime() . " [+] Sequence min_time match: ",
                        "($seq_hr->{'port_seq'}) ",
                        "$access_hr->{'SHARED_SEQUENCE'}->[$seq_hr->{'port_seq'}]->{'proto'}/",
                        "$access_hr->{'SHARED_SEQUENCE'}->[$seq_hr->{'port_seq'}]->{'port'}\n"
                        if $debug;
                } else {
                    &logr('[-]', "Sequence min_time (" .
                        "$access_hr->{'MIN_TIME_DIFF'} seconds) not met: " .
                        "$access_hr->{'SHARED_SEQUENCE'}->[$seq_hr->{'port_seq'}]->{'proto'}/" .
                        "$access_hr->{'SHARED_SEQUENCE'}->[$seq_hr->{'port_seq'}]->{'port'} " .
                        "(port sequence num: $seq_hr->{'port_seq'}) ", $SEND_MAIL);
                    delete $seq_hr->{'port_times'};
                    $seq_hr->{'port_seq'} = 0;
                    return 0;
                }
            }
        } else {
            print STDERR localtime() . " [+] 1 Sequence match: ",
                "($seq_hr->{'port_seq'}) ",
                "$access_hr->{'SHARED_SEQUENCE'}->[$seq_hr->{'port_seq'}]->{'proto'}/",
                "$access_hr->{'SHARED_SEQUENCE'}->[$seq_hr->{'port_seq'}]->{'port'}\n"
                if $debug;
        }
    } elsif (defined $access_hr->{'MAX_TIME_DIFF'}) {
        if ($seq_hr->{'port_seq'} > 0) {
            if ((time()
                    - $seq_hr->{'port_times'}[$seq_hr->{'port_seq'}-1])
                    < $access_hr->{'MAX_TIME_DIFF'}) {
                print STDERR localtime() . " [+] Sequence max_time match: ",
                    "($seq_hr->{'port_seq'}) ",
                    "$access_hr->{'SHARED_SEQUENCE'}->[$seq_hr->{'port_seq'}]->{'proto'}/",
                    "$access_hr->{'SHARED_SEQUENCE'}->[$seq_hr->{'port_seq'}]->{'port'}\n"
                    if $debug;
            } else {
                &logr('[-]', "Sequence max_time ($access_hr->{'MAX_TIME_DIFF'} seconds) exceeded: " .
                    "$access_hr->{'SHARED_SEQUENCE'}->[$seq_hr->{'port_seq'}]->{'proto'}/" .
                    "$access_hr->{'SHARED_SEQUENCE'}->[$seq_hr->{'port_seq'}]->{'port'}" .
                    "(port sequence num: $seq_hr->{'port_seq'}) ", $SEND_MAIL);
                delete $seq_hr->{'port_times'};
                $seq_hr->{'port_seq'} = 0;
                return 0;
            }
        } else {
            print STDERR localtime() . " [+] Sequence match: ($seq_hr->{'port_seq'}) ",
                "$access_hr->{'SHARED_SEQUENCE'}->[$seq_hr->{'port_seq'}]->{'proto'}/",
                "$access_hr->{'SHARED_SEQUENCE'}->[$seq_hr->{'port_seq'}]->{'port'}\n"
                if $debug;
        }
    } else {
        print STDERR localtime() . " [+] Sequence match: ($seq_hr->{'port_seq'}) ",
            "$access_hr->{'SHARED_SEQUENCE'}->[$seq_hr->{'port_seq'}]->{'proto'}/",
            "$access_hr->{'SHARED_SEQUENCE'}->[$seq_hr->{'port_seq'}]->{'port'}\n"
            if $debug;
    }

    ### if we made it here, then we met the timing requirements (if required)
    $seq_hr->{'port_seq'}++;
    return 1;
}

sub pcap_GPG_decrypt_msg() {
    my ($msg, $access_hr) = @_;

    my @plaintext = ();
    my $decrypt_rv = 0;
    my $pid;
    my $decrypted_msg = '';
    my $base64_decoded_msg = '';
    my $found_sig     = 0;
    my $gpg_sign_id   = '';

    unless ($msg =~ /^$access_hr->{'GPG_PREFIX'}/) {
        if ($access_hr->{'GPG_NO_REQUIRE_PREFIX'}) {
            print STDERR localtime() . qq| [-] Incoming base64-encoded |,
                qq|SPA packet is not prefixed with: |,
                qq|"$access_hr->{'GPG_PREFIX'}"\n| if $debug;
        } else {
            print STDERR localtime() . qq| [+] Adding |,
                qq|"$access_hr->{'GPG_PREFIX'}" prefix to |,
                "base64-encoded message.\n" if $debug;
            $msg = $access_hr->{'GPG_PREFIX'} . $msg;
        }
    }

    my ($equals_rv, $equals_padding) = &base64_equals_padding($msg);

    unless ($equals_rv) {
        return $decrypt_rv, $decrypted_msg, $gpg_sign_id;
    }

    if ($equals_rv and $equals_padding) {
        print STDERR localtime() . " [+] Padding base64-encoded message ",
            "with '$equals_padding'.\n" if $debug;
        $msg .= $equals_padding;
    }

    if ($config{'ENABLE_SPA_OVER_HTTP'} eq 'Y' and &is_url_base64($msg)) {
        $msg =~ s|\-|+|g;
        $msg =~ s|\_|/|g;
    }

    if ($use_fko_module) {

        my $fko_err = $fko_obj->spa_data($msg);
        if ($fko_err) {
            if ($debug) {
                &logr('[-]', "FKO error setting spa_data(): " .
                    $fko_obj->errstr($fko_err), $NO_MAIL);
            }
            return $decrypt_rv, $decrypted_msg, $gpg_sign_id;
        }

        ### set the decryption type to use gpg
        $fko_err = $fko_obj->encryption_type(FKO->FKO_ENCRYPTION_GPG);
        if ($fko_err) {
            if ($debug) {
                &logr('[-]', "FKO error setting decryption type to gpg: " .
                    $fko_obj->errstr($fko_err), $NO_MAIL);
                return $decrypt_rv, $decrypted_msg, $gpg_sign_id;
            }
        }

        $fko_err = $fko_obj->gpg_home_dir($access_hr->{'GPG_HOME_DIR'});
        if ($fko_err) {
            if ($debug) {
                &logr('[-]', "FKO error setting gpg home dir: " .
                    $fko_obj->errstr($fko_err), $NO_MAIL);
                return $decrypt_rv, $decrypted_msg, $gpg_sign_id;
            }
        }

        $fko_err = $fko_obj->gpg_recipient($access_hr->{'GPG_DECRYPT_ID'});
        if ($fko_err) {
            if ($debug) {
                &logr('[-]', "FKO error setting signing key " .
                    "gpg_signer(): " . $fko_obj->errstr($fko_err),
                    $NO_MAIL);
            }
            return $decrypt_rv, $decrypted_msg, $gpg_sign_id;
        }

        $fko_err = $fko_obj->decrypt_spa_data($access_hr->{'GPG_DECRYPT_PW'});
        if ($fko_err) {
            if ($debug) {
                &logr('[-]', "FKO error decrypting data via " .
                    "GnuPG decrypt_spa_data(): " .
                    $fko_obj->errstr($fko_err),
                    $NO_MAIL);
            }
            return $decrypt_rv, $decrypted_msg, $gpg_sign_id;
        }

        return 1, $decrypted_msg, $gpg_sign_id;
    }

    if ($debug) {
        print STDERR localtime() . " [+] decode_base64() against the ",
            "following data: $msg\n";
    }

    ### base64 decode the packet
    $base64_decoded_msg = decode_base64($msg);

    ### continue only if decode_base64() had no "Premature end of base64 data"
    ### errors - we want to minimize code that executes against suspicious
    ### packet data
    if ($warn_msg =~ /Premature\s+end/i
            or $warn_msg =~ /Premature\s+padding/i) {
        if ($debug) {
            print STDERR localtime() . " [-] $warn_msg";
        }
        return $decrypt_rv, $decrypted_msg, $gpg_sign_id;
    }

    print STDERR localtime() . " [+] Attempting GnuPG decrypt...\n" if $debug;
    if ($debug and $verbose) {
        print STDERR localtime() . "     Decrypting raw data (hex dump):\n";
        &hex_dump($base64_decoded_msg);
    }

    my $gnupg = GnuPG::Interface->new();

    my %gnupg_options = (
        'batch'   => 1,
        'homedir' => $access_hr->{'GPG_HOME_DIR'},
        'no_options' => 1
    );

    delete $gnupg_options{'batch'} if ($debug and $verbose and not $test_mode);
    delete $gnupg_options{'batch'} if $access_hr->{'GPG_USE_OPTIONS'};

    $gnupg->options->hash_init(%gnupg_options);

    if ($access_hr->{'GPG_PATH'}) {
        $gnupg->call($access_hr->{'GPG_PATH'});
    } elsif (defined $cmds{'gpg'}) {
        $gnupg->call($cmds{'gpg'});
    }

    my $input_fh  = IO::Handle->new() or die $!;
    my $output_fh = IO::Handle->new() or die $!;
    my $error_fh  = IO::Handle->new() or die $!;
    my $pw_fh     = IO::Handle->new() or die $!;
    my $status_fh = IO::Handle->new() or die $!;

    my $handles = GnuPG::Handles->new(
        stdin      => $input_fh,
        stdout     => $output_fh,
        stderr     => $error_fh,
        passphrase => $pw_fh,
        status     => $status_fh,
    );

    $gnupg->options->default_key($access_hr->{'GPG_DECRYPT_ID'});

    if (defined $access_hr->{'GPG_AGENT_INFO'}) {

        $ENV{'GPG_AGENT_INFO'} = $access_hr->{'GPG_AGENT_INFO'};

        $pid = $gnupg->decrypt('handles' => $handles,
            'command_args' => [ qw( --use-agent ) ]);

    } elsif ($gpg_agent_info) {

        ### global definition for gpg-agent connection information
        ### from the command line
        $ENV{'GPG_AGENT_INFO'} = $gpg_agent_info;

        $pid = $gnupg->decrypt('handles' => $handles,
            'command_args' => [ qw( --use-agent ) ]);

    } else {

        $pid = $gnupg->decrypt('handles' => $handles);
    }

    print $pw_fh $access_hr->{'GPG_DECRYPT_PW'};

    close $pw_fh;

    print $input_fh $base64_decoded_msg;
    close $input_fh;

    @plaintext = <$output_fh>;
    close $output_fh;

    my @errors = <$error_fh>;
    close $error_fh;

    my @status = <$status_fh>;
    close $status_fh;

    waitpid $pid, 0;

    if ($debug) {
        print STDERR localtime() . " [+] GnuPG status messages:\n";
        print STDERR for @status;
    }

    ### we require the message to be signed; make sure
    ### the signature is good
    KEY: for my $key_id (@{$access_hr->{'GPG_REMOTE_ID'}}) {
        $key_id = $1 if $key_id =~ /^0x(\w+)/;
        my $found_candidate_sig = 0;
        if ($debug) {
            print STDERR localtime() . " [+] gpg key ID: $key_id\n",
                    localtime() . "     GnuPG error messages:\n";
        }
        LINE: for my $err (@errors) {
            print STDERR localtime() . "     $err" if $debug;
            if ($key_id eq 'ANY') {
                if ($err =~ /Good\s+signature/i) {
                    $found_sig = 1;
                    $gpg_sign_id = $key_id;
                    last KEY;
                }
            } else {
                if ($err =~ /Signature\s+made.*ID\s+$key_id$/) {
                    $found_candidate_sig = 1;
                    next LINE;
                }
                if ($found_candidate_sig and $err =~ /Good\s+signature/i) {
                    $found_sig = 1;
                    $gpg_sign_id = $key_id;
                    last KEY;
                }
            }
        }
    }

    if ($found_sig and @plaintext) {
        $decrypt_rv = 1;
        $decrypted_msg .= $_ for @plaintext;
    } else {
        print STDERR localtime() . " [-] GnuPG message not signed by any ",
            "required key ID.\n" if $debug;
    }

    return $decrypt_rv, $decrypted_msg, $gpg_sign_id;
}

sub pcap_Rijndael_decrypt_msg() {
    my ($msg, $enc_key) = @_;

    my $decrypted_msg = '';
    my $decrypt_rv    = 0;
    my $base64_decoded_msg = '';

    unless ($msg =~ /^U2FsdGVkX1/) {
        if ($debug) {
            print STDERR localtime() . " [+] Adding encoded 'Salted__' ",
                "prefix (U2FsdGVkX1) to incoming encoded SPA packet.\n";
        }
        $msg = 'U2FsdGVkX1' . $msg;
    }

    my ($equals_rv, $equals_padding) = &base64_equals_padding($msg);

    unless ($equals_rv) {
        return $decrypt_rv, $decrypted_msg;
    }

    if ($use_fko_module) {

        if ($config{'ENABLE_SPA_OVER_HTTP'} eq 'Y' and &is_url_base64($msg)) {
            $msg =~ s|\-|+|g;
            $msg =~ s|\_|/|g;
        }

        my $fko_err = $fko_obj->spa_data($msg);
        if ($fko_err) {
            if ($debug) {
                &logr('[-]', "FKO error setting spa_data(): " .
                    $fko_obj->errstr($fko_err), $NO_MAIL);
            }
            return $decrypt_rv, $decrypted_msg;
        }

        $fko_err = $fko_obj->decrypt_spa_data($enc_key);
        if ($fko_err) {
            if ($debug) {
                &logr('[-]', "FKO error decrypting data via " .
                    "Rijndael decrypt_spa_data(): " .
                    $fko_obj->errstr($fko_err),
                    $NO_MAIL);
            }
            return $decrypt_rv, $decrypted_msg;
        }

        return 1, $decrypted_msg;

    }

    if ($equals_padding) {
        print STDERR localtime() . " [+] Padding base64-encoded message ",
            "with '$equals_padding'.\n" if $debug;
        $msg .= $equals_padding;
    }

    if ($config{'ENABLE_SPA_OVER_HTTP'} eq 'Y' and &is_url_base64($msg)) {
        $msg =~ s|\-|+|g;
        $msg =~ s|\_|/|g;
    }

    if ($debug) {
        print STDERR localtime() . " [+] decode_base64() against the ",
            "following data: $msg\n";
    }

    ### base64 decode the packet
    $base64_decoded_msg = decode_base64($msg);

    ### continue only if decode_base64() had no "Premature end of base64 data"
    ### errors - we want to minimize code that executes against suspicious
    ### packet data
    if ($warn_msg =~ /Premature\s+end/i
            or $warn_msg =~ /Premature\s+padding/i) {
        if ($debug) {
            print STDERR localtime() . " [-] $warn_msg";
        }
        return $decrypt_rv, $decrypted_msg;
    }

    ### look for the Salted__ prefix
    unless ($base64_decoded_msg =~ /^Salted__/) {
        if ($debug) {
            print STDERR localtime() . " [-] base64-decoded data does ",
                "not begin with 'Salted__'\n";
        }
        return $decrypt_rv, $decrypted_msg;
    }

    print STDERR localtime() . " [+] Attempting Rijndael decrypt...\n"
        if $debug;

    if ($debug and $verbose) {
        print STDERR localtime() . "     Decrypting raw data (hex dump):\n";
        &hex_dump($base64_decoded_msg);
    }

    my $cipher = Crypt::CBC->new({
        'key'    => $enc_key,
        'cipher' => $enc_alg,
    });
    eval {
        $decrypted_msg = $cipher->decrypt($base64_decoded_msg);
    };
    if ($debug and $verbose) {
        print STDERR "    Salt:\n";
        &hex_dump($cipher->salt());
        print STDERR "    Key:\n";
        &hex_dump($cipher->key());
        print STDERR "    IV:\n";
        &hex_dump($cipher->iv());
        print STDERR "    PassPhrase:\n";
        &hex_dump($cipher->passphrase());
        print STDERR "    Block Size: " . $cipher->blocksize() ."\n",
            "    Key Size:   " . $cipher->keysize(). "\n\n";
    }

    if ($@) {
        $decrypted_msg = '';
    } else {
        $decrypt_rv = 1;
    }
    return $decrypt_rv, $decrypted_msg;
}

sub decrypt_sequence() {
    my ($src, $seq_hr, $access_hr) = @_;

    my $cipher_txt = '';
    my $allow_src  = '';

    $cipher_txt .= chr($_ - $access_hr->{'PORT_OFFSET'})
        for @{$seq_hr->{'enc_ports'}};

    return 0 unless $cipher_txt;

    if ($debug) {
        my @tmp_chars = split //, $cipher_txt;
        print STDERR localtime() . ' [+] Cipher text (' .
            length($cipher_txt) . ' bytes): ';
        print STDERR ord($_) . ' ' for @tmp_chars;
        print STDERR "\n";
    }

    my $cipher = Crypt::CBC->new({
        'key'    => $access_hr->{'KEY'},
        'cipher' => $enc_alg,
    });

    ### we now have our encrypted string, so try to decrypt it
    my $plain_txt = '';
    eval {
        $plain_txt = $cipher->decrypt($cipher_txt);
    };
    undef $cipher;

    return 0,0,0,0 if ($@ or not $plain_txt);

    if ($debug) {
        my @tmp_chars = split //, $plain_txt;
        print STDERR localtime() . " [+] Plain text: ";
        print STDERR ord($_) . ' ' for @tmp_chars;
        print STDERR "\n";
    }

    my @chars = split //, $plain_txt;

    ### the first four characters in the @chars array represent the
    ### four octets of the IP we are going to modify access for
    for my $octet ($chars[0], $chars[1], $chars[2], $chars[3]) {
        unless (0 <= ord($octet) and ord($octet) < 256) {
            &logr('[-]', "invalid IP octet: " . ord($octet), $SEND_MAIL);
            return 0,0,0,0;
        }
        $allow_src .= ord($octet) . '.';
    }
    $allow_src =~ s/\.$//;

    if ($allow_src eq '0.0.0.0') {
        ### the client sent 0.0.0.0 across, so it may be behind a
        ### NAT device (or the person just doesn't know their source
        ### address) so open the firewall for the source of the
        ### encrypted sequence.
        if ($config{'REQUIRE_SOURCE_ADDRESS'} eq 'Y' or not
                &is_ip_included($allow_src,
                    $access_hr->{'REQUIRE_SOURCE_ADDRESS'},
                    $access_hr->{'require_src_addr_exceptions'})) {
            ### we require the source address to be contained within
            ### the encrypted packet.
            return 0,0,0,0;
        }
        $allow_src = $src;
    }

    my $port_upper_bits = ord($chars[4]) << 8;
    my $port_lower_bits = ord($chars[5]);
    my $allow_port = $port_upper_bits | $port_lower_bits;

    unless (0 <= $allow_port and $allow_port < 65536) {
        &logr('[-]', "bad port number: $allow_port", $SEND_MAIL);
        return 0,0,0,0;
    }

    my $allow_proto = '';
    my $proto = ord($chars[6]);
    if ($proto == 6) {
        $allow_proto = 'tcp';
    } elsif ($proto == 17) {
        $allow_proto = 'udp';
    } elsif ($proto == 1) {
        $allow_proto = 'icmp';
    } else {
        &logr('[-]', "bad protocol number: $proto", $SEND_MAIL);
        return 0,0,0,0;
    }

    my $checksum_data = ord($chars[7]);

    my $checksum = 0;
    for (my $i=0; $i < 7; $i++) {
        $checksum += ord($chars[$i]);
    }
    $checksum = $checksum % 256;

    unless ($checksum_data == $checksum) {
        &logr('[-]', "invalid checksum for $src", $SEND_MAIL);
        return 0,0,0,0;
    }

    my $username = '';
    my $i=8;
    while ($i <= $#chars and ord($chars[$i]) != 0) {
        $username .= $chars[$i];
        $i++;
    }

    return 1, $allow_src, $allow_port, $allow_proto, $username;
}

sub grant_access() {
    my ($src, $msg_hr, $nat_info_hr, $seq_hr,
            $open_ports_hr, $access_hr) = @_;

    if ($access_hr->{'EXTERNAL_CMD_OPEN'}
            or ($config{'FIREWALL_TYPE'} eq 'external_cmd'
            and $config{'EXTERNAL_CMD_OPEN'})) {

        ### run EXTERNAL_CMD_OPEN and let knoptm run EXTERNAL_CMD_CLOSE
        &external_cmd_open($src, $msg_hr, $open_ports_hr, $access_hr);

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

            ### iptables access; the destination IP is only used if access is
            ### forwarded through the iptables policy
            &grant_ipt_access($src, $msg_hr, $nat_info_hr,
                    $seq_hr, $open_ports_hr, $access_hr);

        } elsif ($config{'FIREWALL_TYPE'} eq 'ipfw') {

            ### ipfw access
            &grant_ipfw_access($src, $open_ports_hr, $access_hr);
        }
    }
    return;
}

sub grant_ipt_access() {
    my ($src, $msg_hr, $nat_info_hr, $seq_hr,
            $open_ports_hr, $access_hr) = @_;

    my @ipt_hrefs = ();
    my $ipt = &get_iptables_chainmgr_obj($config{'IPT_EXEC_SLEEP'});

    my $local_nat = 0;

    if (keys %$msg_hr) {  ### For PK mode, this hash ref is empty
        if ($msg_hr->{'action_type'} == $SPA_LOCAL_NAT_ACCESS_MODE
                or $msg_hr->{'action_type'}
                    == $SPA_CLIENT_TIMEOUT_LOCAL_NAT_ACCESS_MODE) {
            $local_nat = 1;
        }
    }

    if ($access_hr->{'ENABLE_FORWARD_ACCESS'} or $local_nat) {

        unless (defined $nat_info_hr->{'internal_ip'}) {
            print STDERR localtime() . " [-] Internal IP not ",
                "defined for NAT\n" if $debug;
            undef $ipt;
            return;
        }
        push @ipt_hrefs, \%ipt_prerouting if %ipt_prerouting;
        push @ipt_hrefs, \%ipt_postrouting if %ipt_postrouting;

        if ($local_nat) {
            push @ipt_hrefs, \%ipt_input;
            print STDERR localtime() . " [+] INPUT NAT access for $src ",
                "to local IP: $nat_info_hr->{'internal_ip'}\n"
                if $debug;
        } else {
            push @ipt_hrefs, \%ipt_forward;
            print STDERR localtime() . " [+] FORWARD access for $src ",
                "to internal IP: $nat_info_hr->{'internal_ip'}\n"
                if $debug;
        }

    } else {
        if (defined $nat_info_hr->{'internal_ip'}) {
            undef $ipt;
            return;
        }
        push @ipt_hrefs, \%ipt_input;
        if ($access_hr->{'ENABLE_OUTPUT_ACCESS'}) {
            push @ipt_hrefs, \%ipt_output;
        }
    }

    my $ipt_hr_num = 0;
    for my $hr (@ipt_hrefs) {

        if ($debug) {
            $ipt_hr_num++;
            print STDERR localtime() . " [+] ipt_href: $ipt_hr_num\n",
                Dumper($hr);
        }
        my $nat_ip   = '0.0.0.0/0';
        my $nat_port = 0;

        ### add rule for $ip unless it already exists
        my $target     = $hr->{'target'};
        my $direction  = $hr->{'direction'};
        my $table      = $hr->{'table'};
        my $from_chain = $hr->{'from_chain'};
        my $to_chain   = $hr->{'to_chain'};
        my $jump_rule_position = $hr->{'jump_rule_position'};
        my $auto_rule_position = $hr->{'auto_rule_position'};

        my $grant_src = $src;
        my $grant_dst = '0.0.0.0/0';

        if ($direction eq 'dst') {
            ### OUTPUT chain
            $grant_dst = $src;
            $grant_src = '0.0.0.0/0';
        }

        my $rv = 0;
        my $out_ar = [];
        my $err_ar = [];

        ### make sure "to_chain" exists
        for (my $try=0; $try < $config{'IPT_EXEC_TRIES'}; $try++) {
            ($rv, $out_ar, $err_ar)
                = $ipt->create_chain($table, $to_chain);
            last if $rv;
        }

        if ($rv) {
            print STDERR localtime() . "     create_chain() returned: $rv\n"
                if $debug;
        } else {
            print STDERR localtime() . " [-] create_chain() ",
                "returned: $rv, errors:\n" if $debug;
            &psyslog_errs($err_ar);
            undef $ipt;
            return;
        }

        ### add jump rule to the "to_chain" from the "from_chain"
        for (my $try=0; $try < $config{'IPT_EXEC_TRIES'}; $try++) {
            ($rv, $out_ar, $err_ar) = $ipt->add_jump_rule($table,
                $from_chain, $jump_rule_position, $to_chain);
            last if $rv;
        }

        if ($rv) {
            print STDERR localtime() . "     add_jump_rule() ",
                "returned: $rv\n" if $debug;
        } else {
            print STDERR localtime() . " [-] add_jump_rule() ",
                "returned: $rv, errors:\n" if $debug;
            &psyslog_errs($err_ar);
            undef $ipt;
            return;
        }

        for my $proto (keys %{$open_ports_hr}) {
            for my $port (keys %{$open_ports_hr->{$proto}}) {

                my $num_chain_rules = 0;
                my $dport = $port;
                my $sport = 0;

                my %extended_info = ('protocol' => $proto);
                if ($direction eq 'dst') {
                    ### OUTPUT chain
                    $extended_info{'s_port'} = $port;
                    $sport = $port;
                    $dport = 0;
                } else {
                    $extended_info{'d_port'} = $port;
                }

                ### deal with DNAT and SNAT (normally MASQUERADE unless
                ### ENABLE_IPT_SNAT is set)
                if ($table eq 'nat' and ($target eq 'DNAT'
                            or $target eq 'SNAT')) {
                    if ($target eq 'DNAT') {
                        $extended_info{'to_ip'}
                            = $nat_info_hr->{'internal_ip'};
                        $extended_info{'to_port'} = $dport;
                        $extended_info{'d_port'}
                            = $nat_info_hr->{'external_port'};
                        $nat_ip   = $nat_info_hr->{'internal_ip'};
                        $nat_port = $dport;
                        $dport    = $nat_info_hr->{'external_port'};
                    } elsif ($target eq 'SNAT') {
                        $extended_info{'to_ip'}
                            = $config{'SNAT_TRANSLATE_IP'};
                        $extended_info{'to_port'} = $dport;
                        $extended_info{'d_port'}  = $dport;
                        $nat_ip   = $config{'SNAT_TRANSLATE_IP'};
                        $nat_port = $dport;
                    }
                }

                ($rv, $num_chain_rules) = $ipt->find_ip_rule($grant_src,
                    $grant_dst, $table, $to_chain, $target, \%extended_info);

                if ($rv) {
                    print STDERR localtime() . "     find_ip_rule() ",
                        "returned $rv\n" if $debug;
                    my $str = "$grant_src -> $grant_dst($proto/$port)";
                    if ($direction eq 'dst') {
                        $str = "$grant_src($proto/$port) -> $grant_dst";
                    }
                    if (defined $extended_info{'to_ip'}) {
                        $str = "$grant_src -> $extended_info{'to_ip'}" .
                            "($proto/$extended_info{'d_port'} to " .
                            "$extended_info{'to_port'})";
                    }
                    &logr('[-]', "source: $str already allowed to connect " .
                        "in chain: $to_chain", $SEND_MAIL);
                } else {
                    print STDERR localtime() . "     find_ip_rule() ",
                        "returned $rv\n" if $debug;
                    my $str = "add $to_chain $grant_src -> " .
                        "$grant_dst($proto/$port) $target rule ";
                    if ($direction eq 'dst') {
                        $str = "add $to_chain $grant_src($proto/$port) -> " .
                            "$grant_dst $target rule ";
                    }
                    if (defined $extended_info{'to_ip'}) {
                        $str = "add $to_chain $grant_src -> " .
                            "$extended_info{'to_ip'}" .
                            "($proto/$extended_info{'d_port'} to " .
                            "$extended_info{'to_port'}) " .
                            "$target rule ";
                    }
                    $str .= "$access_hr->{'FW_ACCESS_TIMEOUT'} sec";

                    &logr('[+]', $str, $SEND_MAIL);

                    for (my $try=0; $try < $config{'IPT_EXEC_TRIES'}; $try++) {
                        ($rv, $out_ar, $err_ar) = $ipt->add_ip_rule($grant_src,
                            $grant_dst, $auto_rule_position, $table, $to_chain,
                            $target, \%extended_info);
                        last if $rv;
                    }

                    if ($rv) {

                        if ($debug) {
                            print STDERR localtime() . " [+] add_ip_rule() ",
                                "returned $rv\n",
                                 " [+] Dumping $to_chain to ",
                                "see newly added rule:\n";
                            $ipt->run_ipt_cmd("$cmds{'iptables'} -t " .
                                "$table -v -n -L $to_chain");
                        }

                        ### keep track of how many times we have granted access
                        $seq_hr->{'grant_ctr'}++ unless
                            $access_hr->{'DATA_COLLECT_MODE'} == $PCAP
                            or $access_hr->{'DATA_COLLECT_MODE'} == $FILE_PCAP
                            or $access_hr->{'DATA_COLLECT_MODE'} == $ULOG_PCAP;

                        ### Communicate the new firewall rule to knoptm so
                        ### that it can be removed.
                        &write_knoptm_fw_cache_entry(
                            time(),
                            $access_hr->{'FW_ACCESS_TIMEOUT'},
                            $grant_src,
                            $sport,
                            $grant_dst,
                            $dport,
                            $proto,
                            $table,
                            $to_chain,
                            $target,
                            $direction,
                            $nat_ip,
                            $nat_port,
                            encode_base64('NA', ''),
                            0
                        );
                    } else {
                        print STDERR localtime() . " [-] add_ip_rule() ",
                            "returned $rv\n" if $debug;
                        &psyslog_errs($err_ar);
                    }
                }
            }
        }
    }
    $seq_hr->{'port_seq'} = 0
        unless $access_hr->{'DATA_COLLECT_MODE'} == $PCAP
            or $access_hr->{'DATA_COLLECT_MODE'} == $FILE_PCAP
            or $access_hr->{'DATA_COLLECT_MODE'} == $ULOG_PCAP;

    undef $ipt;
    return;
}

sub grant_ipfw_access() {
    my ($src, $open_ports_hr, $access_hr) = @_;

    my $dst = '0.0.0.0/0';

    for my $proto (keys %{$open_ports_hr}) {
        for my $port (keys %{$open_ports_hr->{$proto}}) {

            my ($active_rulenum, $set_num, $new_rulenum)
                    = &ipfw_find_ip_rule($src, 'any', $proto, $port);

            if ($active_rulenum and $set_num == 0) {
                &logr('[-]', "source: $src already allowed " .
                    "to connect to $proto/$port", $SEND_MAIL);
            } else {

                my $msg = '';
                if ($active_rulenum and $ipfw_is_dynamic
                        and $set_num == $config{'IPFW_SET_NUM'}) {
                    $msg = 'reactivating ipfw allow rule for '
                } else {
                    $msg = 'adding ipfw allow rule for '
                }
                $msg .= "$src -> $proto";

                $msg .= "/$port" if $proto ne 'icmp';
                $msg .= " ($access_hr->{'FW_ACCESS_TIMEOUT'} " .
                    "seconds)";

                &logr('[+]', $msg, $SEND_MAIL);

                my $res = 0;
                if ($active_rulenum and $ipfw_is_dynamic
                        and $set_num == $config{'IPFW_SET_NUM'}) {
                    $res = &ipfw_move_rule($active_rulenum, 0);
                } else {
                    $res = &ipfw_add_ip_rule($new_rulenum,
                            $src, 'any', $proto, $port);
                }
                if ($res) {

                    ### communicate the new rule to knoptm so that it can
                    ### be removed.
                    &write_knoptm_fw_cache_entry(
                        time(),
                        $access_hr->{'FW_ACCESS_TIMEOUT'},
                        $src,
                        0,
                        $dst,
                        $port,
                        $proto,
                        'NA',
                        'NA',
                        'NA',
                        'NA',
                        '0.0.0.0/0',
                        0,
                        encode_base64('NA', ''),
                        0
                    );
                }
            }
        }
    }

    return;
}

sub ipt_check_stateful_rule() {

    my $ipt = &get_iptables_chainmgr_obj($ZERO_SLEEP);

    print STDERR localtime() . " [+] Checking for iptables state ",
        "tracking rule...\n" if $debug;
    ### check for at least one state tracking rule in _some_ chain
    my ($rv, $out_ar, $err_ar) = $ipt->run_ipt_cmd(
        "$cmds{'iptables'} -v -n -L");
    my $found_state_rule = 0;
    for my $rule (@$out_ar) {
        ### ACCEPT all -- 0.0.0.0/0 0.0.0.0/0 state RELATED,ESTABLISHED
        if ($rule =~ /\sACCEPT\s+.*ESTABLISHED/) {
            $found_state_rule = 1;
            last;
        }
    }
    unless ($found_state_rule) {
        &logr('[-]', "warning, could not find any iptables state tracking " .
            "rules", $SEND_MAIL);
    }
    undef $ipt;
    return;
}

sub ipfw_check_stateful_rule() {

    my $cmd = "$cmds{'ipfw'} -dS list";

    open LIST, "$cmd |" or die "[*] Could not execute $cmd: $!";
    my $found_state_rule = 0;
    while (<LIST>) {
        if (/check-state/) {
            ### from the ipfw man page:
            # check-state
            #    Checks the packet against the dynamic ruleset.  If a match is
            #    found, execute the action associated with the rule which gener-
            #    ated this dynamic rule, otherwise move to the next rule.
            #    Check-state rules do not have a body.  If no check-state rule is
            #    found, the dynamic ruleset is checked at the first keep-state or
            #    limit rule.
            $found_state_rule = 1;
            $ipfw_is_dynamic  = 1;
            last;
        } elsif (/keep-state/) {
            ### from the ipfw man page:
            # keep-state
            #    Upon a match, the firewall will create a dynamic rule, whose
            #    default behaviour is to match bidirectional traffic between
            #    source and destination IP/port using the same protocol.  The rule
            #    has a limited lifetime (controlled by a set of sysctl(8) vari-
            #    ables), and the lifetime is refreshed every time a matching
            #    packet is found.
            $found_state_rule = 1;
            $ipfw_is_dynamic  = 1;
            last;
        } elsif (/allow.*to\s+any\s+established/) {
            $found_state_rule = 1;
            last;
        }
    }
    close LIST;
    unless ($found_state_rule) {
        &logr('[-]', "warning, could not find ipfw state tracking rules",
            $SEND_MAIL);
    }

    return;
}

sub ipfw_del_ip() {
    my @del_rule_nums = ();
    print "[+] Deleting allow rules for src $fw_del_ip...\n";

    my $cmd = "$cmds{'ipfw'} -S list";
    open LIST, "$cmd |" or die "[*] Could not execute $cmd: $!";
    while (<LIST>) {
        ### 00003 set 0 allow tcp from 127.0.0.2 to any dst-port 22 keep-state
        ### 00002 allow tcp from 1.1.1.1 to any dst-port 22 keep-state
        if (/^\s*(\d+)\s+set\s+\d+\s+allow\s+\S+\s+from\s+$fw_del_ip\s+to\s+
                any\s+/x) {
            push @del_rule_nums, $1;
        }
    }
    close LIST;

    ### delete all rules that have the IP as a source to any destination
    for my $rulenum (@del_rule_nums) {
        my $cmd = "$cmds{'ipfw'} delete $rulenum";
        print "    $cmd\n";
        open IPFW, "| $cmd" or die "[*] Could not execute $cmd";
        close IPFW;
    }
    return 0;
}

sub ipfw_find_ip_rule() {
    my ($src, $dst, $proto, $port) = @_;

    my $active_rulenum = 0;
    my $set_num = -1;
    my $new_rulenum = $config{'IPFW_RULE_NUM'};  ### sets a minimum

    my %rule_nums = ();

    my $cmd = "$cmds{'ipfw'} -S list";
    open LIST, "$cmd |" or die "[*] Could not execute $cmd: $!";
    while (<LIST>) {
        if ($proto eq 'tcp') {
            ### 00002 set 2 allow tcp from 1.1.1.1 to any dst-port 22 keep-state
            if (/^\s*(\#\s+DISABLED\s+)?(\d+)\s+set\s+(\d+)\s+allow\s+$proto\s+
                        from\s+$src\s+to\s+$dst\s+dst-port\s+$port\s+
                        keep-state/x) {
                $active_rulenum = $2;
                $set_num = $3;
            }
        } elsif ($proto eq 'udp') {
            ### 00002 set 2 allow udp from 1.1.1.1 to any dst-port 53
            if (/^\s*(\#\s+DISABLED\s+)?(\d+)\s+set\s+(\d+)\s+allow\s+$proto\s+
                        from\s+$src\s+to\s+$dst\s+dst-port\s+$port\s+keep-state/x) {
                $active_rulenum = $2;
                $set_num = $3;
            }
        } else {  ### icmp
            if (/^\s*(\#\s+DISABLED\s+)?(\d+)\s+set\s+(\d+)\s+allow\s+$proto\s+
                        from\s+$src\s+to\s+$dst/x) {
                $active_rulenum = $2;
                $set_num = $3;
            }
        }
        if (/^\s*(\d+)/) {
            my $rulenum = $1;
            ### remove any leading zeros from the rule number
            $rulenum =~ s/^0{1,4}//g;
            $rule_nums{$rulenum} = '';
        }
    }
    close LIST;

    if ($active_rulenum) {
        ### remove any leading zeros from the rule number
        $active_rulenum =~ s/^0{1,4}//g;
    }

    ### find the next unused rule number
    $new_rulenum++ while (defined $rule_nums{$new_rulenum});

    return $active_rulenum, $set_num, $new_rulenum;
}

sub ipfw_add_ip_rule() {
    my ($new_rulenum, $src, $dst, $proto, $port) = @_;

    my $cmd = "$cmds{'ipfw'} add $new_rulenum " .
        "allow $proto from $src to $dst";

    if ($proto eq 'tcp' or $proto eq 'udp') {
        $cmd .= " $port";

        ### only add keep-state if ipfw already uses dynamic rules
        $cmd .= ' keep-state' if $ipfw_is_dynamic;
    }

    if ($debug) {
        print STDERR "[+] ipfw_add_ip_rule() $new_rulenum ",
            "$src $dst $proto $port\n    BEFORE:\n";
        &ipfw_policy_print($STDERR);
    }

    open IPFW, "| $cmd" or die "[*] Could not execute $cmd: $!";
    close IPFW;

    if ($debug) {
        print STDERR "    AFTER:\n";
        &ipfw_policy_print($STDERR);
    }

    return 1;
}

sub ipfw_delete_ip_rule() {
    my $rulenum = shift;

    if ($debug) {
        print STDERR "[+] ipfw_delete_ip_rule() rulenum: $rulenum\n",
            "    BEFORE:\n";
        &ipfw_policy_print($STDERR);
    }

    open IPFW, "| $cmds{'ipfw'} delete $rulenum" or die "[*] Could not ",
        "execute $cmds{'ipfw'} delete $rulenum";
    close IPFW;

    if ($debug) {
        print STDERR "    AFTER:\n";
        &ipfw_policy_print($STDERR);
    }

    return 1;
}

sub ipfw_disable_set() {
    my $setnum = shift;

    if ($debug) {
        print STDERR "[+] ipfw_disable_set() set num: $setnum\n",
            "    BEFORE:\n";
        &ipfw_policy_print($STDERR);
    }

    open IPFW, "| $cmds{'ipfw'} set disable $setnum" or die "[*] Could ",
        "not execute $cmds{'ipfw'} set disable $setnum";
    close IPFW;

    if ($debug) {
        print STDERR "    AFTER:\n";
        &ipfw_policy_print($STDERR);
    }

    return 1;
}

sub ipfw_move_rule() {
    my ($rulenum, $setnum) = @_;

    if ($debug) {
        print STDERR "[+] ipfw_move_rule() rulenum: $rulenum, set: $setnum\n",
            "    BEFORE:\n";
        &ipfw_policy_print($STDERR);
    }

    my $cmd = "$cmds{'ipfw'} set move rule $rulenum to $setnum";

    open IPFW, "| $cmd" or die "[*] Could not execute $cmd: $!";
    close IPFW;

    if ($debug) {
        print STDERR "    AFTER:\n";
        &ipfw_policy_print($STDERR);
    }

    return 1;
}

sub write_knoptm_fw_cache_entry() {
    my ($rule_timestamp, $timeout, $src, $sport, $dst, $dport,
        $proto, $table, $chain, $target, $direction, $nat_ip,
        $nat_port, $external_cmd_close, $external_cmd_alarm) = @_;

    ### the rule is permanent per the zero value for FW_ACCESS_TIMEOUT in
    ### this source block
    return if $timeout == 0;

    my $knoptm_cache_entry_line = "$rule_timestamp $timeout $src $sport " .
        "$dst $dport $proto $table $chain $target $direction $nat_ip " .
        "$nat_port $external_cmd_close $external_cmd_alarm";

    print STDERR localtime() . " [+] Writing fw time cache entry to: ",
        "$config{'KNOPTM_IP_TIMEOUT_SOCK'} $knoptm_cache_entry_line\n"
        if $debug;

    ### open domain socket with running knoptm process
    my $sock = IO::Socket::UNIX->new($config{'KNOPTM_IP_TIMEOUT_SOCK'})
        or die "[*] Could not acquire $config{'KNOPTM_IP_TIMEOUT_SOCK'} ",
        "socket: $!";
    print $sock "$knoptm_cache_entry_line\n";
    close $sock;

    return;
}

sub timeout_invalid_sequences() {
    for my $src (keys %ip_sequences) {
        for my $seq_num (keys %{$ip_sequences{$src}}) {
            my $knock_interval
                = $access[$seq_num]{'KNOCK_INTERVAL'};

            if (defined $access[$seq_num]{'KNOCK_LIMIT'}) {
                if (defined $ip_sequences{$src}{$seq_num}{'grant_ctr'}
                        and $ip_sequences{$src}{$seq_num}{'grant_ctr'} >
                        $access[$seq_num]{'KNOCK_LIMIT'}) {
                    ### don't timeout knock sequence if the knock limit
                    ### has been exceeded
                    next;
                }
            }

            ### encrypted sequences
            if (defined $ip_sequences{$src}{$seq_num}{'enc_stime'}) {
                if (time() - $ip_sequences{$src}{$seq_num}{'enc_stime'}
                        > $knock_interval) {
                    &logr('[+]', "invalid encrypted sequence $src timeout",
                        $NO_MAIL);
                    delete $ip_sequences{$src}{$seq_num};
                    next;
                }
            }

            ### shared sequences
            if (defined $ip_sequences{$src}{$seq_num}{'port_stime'}) {
                if (time() - $ip_sequences{$src}
                        {$seq_num}{'port_stime'}->[0]
                        > $knock_interval) {
                    &logr('[+]', "invalid shared sequence $src timeout",
                        $NO_MAIL);
                    delete $ip_sequences{$src}{$seq_num};
                    next;
                }
            }
        }
    }
    return;
}

sub p0f() {
    my ($src, $len, $frag_bit, $ttl, $win, $tcp_options) = @_;

    print STDERR localtime() . " [+] p0f(): $src len: $len, frag_bit: ",
        "$frag_bit, ttl: $ttl, win: $win\n" if $debug;

    my ($options_aref) = &parse_tcp_options($src, $tcp_options);

    return unless $options_aref;

    ### try to match SYN packet length
    LEN: for my $sig_len (keys %p0f_sigs) {
        my $matched_len = 0;
        if ($sig_len eq '*') {  ### len can be wildcarded in pf.os
            $matched_len = 1;
        } elsif ($sig_len =~ /^\%(\d+)/) {
            if (($len % $1) == 0) {
                $matched_len = 1;
            }
        } elsif ($len == $sig_len) {
            $matched_len = 1;
        }
        next LEN unless $matched_len;

        ### try to match fragmentation bit
        FRAG: for my $test_frag_bit ($frag_bit, '*') {  ### don't need "%nnn" check
            next FRAG unless defined $p0f_sigs{$sig_len}{$test_frag_bit};

            ### find out for which p0f sigs the TTL is within range
            TTL: for my $sig_ttl (keys %{$p0f_sigs{$sig_len}{$test_frag_bit}}) {
                unless ($ttl > $sig_ttl - $config{'MAX_HOPS'}
                        and $ttl <= $sig_ttl) {
                    next TTL;
                }

                ### match tcp window size
                WIN: for my $sig_win_size (keys
                        %{$p0f_sigs{$sig_len}{$test_frag_bit}{$sig_ttl}}) {
                    my $matched_win_size = 0;
                    if ($sig_win_size eq '*') {
                        $matched_win_size = 1;
                    } elsif ($sig_win_size =~ /^\%(\d+)/) {
                        if (($win % $1) == 0) {
                            $matched_win_size = 1;
                        }
                    } elsif ($sig_win_size =~ /^S(\d+)/) {
                        ### window size must be a multiple of maximum
                        ### seqment size
                        my $multiple = $1;
                        for my $opt_hr (@$options_aref) {
                            if (defined $opt_hr->{$tcp_p0f_opt_types{'M'}}) {
                                my $mss_val = $opt_hr->{$tcp_p0f_opt_types{'M'}};
                                if ($win == $mss_val * $multiple) {
                                    $matched_win_size = 1;
                                }
                            }
                            last;
                        }
                    } elsif ($sig_win_size == $win) {
                        $matched_win_size = 1;
                    }

                    next WIN unless $matched_win_size;

                    TCPOPTS: for my $sig_opts (keys %{$p0f_sigs{$sig_len}
                            {$test_frag_bit}{$sig_ttl}{$sig_win_size}}) {
                        my @sig_opts = split /\,/, $sig_opts;
                        for (my $i=0; $i<=$#sig_opts; $i++) {
                            ### tcp option order is important.  Check to see if
                            ### the option order in the packet matches the order we
                            ### expect to see in the signature
                            if ($sig_opts[$i] =~ /^([NMWST])/) {
                                my $sig_letter = $1;

                                unless (defined $options_aref->[$i]->
                                        {$tcp_p0f_opt_types{$sig_letter}}) {
                                    next TCPOPTS;  ### could not match tcp option order
                                }

                                ### MSS, window scale, and timestamp have
                                ### specific signatures requirements on values
                                if ($sig_letter eq 'M') {
                                    if ($sig_opts[$i] =~ /M(\d+)/) {
                                        my $sig_mss_val = $1;
                                        next TCPOPTS unless $options_aref->[$i]->
                                            {$tcp_p0f_opt_types{$sig_letter}}
                                                == $sig_mss_val;
                                    } elsif ($sig_opts[$i] =~ /M\%(\d+)/) {
                                        my $sig_mss_mod_val = $1;
                                        next TCPOPTS unless (($options_aref->[$i]->
                                            {$tcp_p0f_opt_types{$sig_letter}}
                                                % $sig_mss_mod_val) == 0);
                                    } ### else it is "M*" which always matches
                                } elsif ($sig_letter eq 'W') {
                                    if ($sig_opts[$i] =~ /W(\d+)/) {
                                        my $sig_win_val = $1;
                                        next TCPOPTS unless $options_aref->[$i]->
                                            {$tcp_p0f_opt_types{$sig_letter}}
                                                == $sig_win_val;
                                    } elsif ($sig_opts[$i] =~ /W\%(\d+)/) {
                                        my $sig_win_mod_val = $1;
                                        next TCPOPTS unless (($options_aref->[$i]->
                                            {$tcp_p0f_opt_types{$sig_letter}}
                                                % $sig_win_mod_val) == 0);
                                    } ### else it is "W*" which always matches
                                } elsif ($sig_letter eq 'T') {
                                    if ($sig_opts[$i] =~ /T0/) {
                                        next TCPOPTS unless $options_aref->[$i]->
                                            {$tcp_p0f_opt_types{$sig_letter}}
                                                == 0;
                                    }  ### else it is just "T" which matches
                                }

                            }
                        }
                        OS: for my $os (keys %{$p0f_sigs{$sig_len}
                                {$test_frag_bit}{$sig_ttl}{$sig_win_size}
                                {$sig_opts}}) {
                            my $sig = $p0f_sigs{$sig_len}
                                {$test_frag_bit}{$sig_ttl}{$sig_win_size}
                                {$sig_opts}{$os};
                            print STDERR localtime() . " [+] os: $os, $sig\n"
                                if $debug;
                            $p0f{$src}{$os} = $sig;
                        }
                    }
                }
            }
        }
    }
    return;
}

sub parse_tcp_options() {
    my ($src, $tcp_options) = @_;
    my @opts = ();
    my @hex_nums = ();
    my $debug_str = '';

    if (length($tcp_options) % 2 != 0) {  ### make sure length a multiple of two
        &logr('[-]', 'tcp options length not a multiple of two.', $NO_MAIL);
        return '';
    }
    ### $tcp_options is a hex string like "020405B401010402" from the iptables
    ### log message
    my @chars = split //, $tcp_options;
    for (my $i=0; $i <= $#chars; $i += 2) {
        my $str = $chars[$i] . $chars[$i+1];
        push @hex_nums, $str;
    }

    my $max_parse_attempts = $#chars;
    my $parse_ctr = 0;

    OPT: for (my $opt_kind=0; $opt_kind <= $#hex_nums;) {

        $parse_ctr++;
        return [] if $parse_ctr > $max_parse_attempts;

        last OPT unless defined $hex_nums[$opt_kind+1];

        my $is_nop = 0;
        my $len = hex($hex_nums[$opt_kind+1]);
        if (hex($hex_nums[$opt_kind]) == $tcp_nop_type) {
            $debug_str .= 'NOP, ' if $debug;
            push @opts, {$tcp_nop_type => ''};
            $is_nop = 1;
        } elsif (hex($hex_nums[$opt_kind]) == $tcp_mss_type) {  ### MSS
            my $mss_hex = '';
            for (my $i=$opt_kind+2; $i < ($opt_kind+$len); $i++) {
                $mss_hex .= $hex_nums[$i];
            }
            my $mss = hex($mss_hex);
            push @opts, {$tcp_mss_type => $mss};
            $debug_str .= 'MSS: ' . hex($mss_hex) . ', ' if $debug;
        } elsif (hex($hex_nums[$opt_kind]) == $tcp_win_scale_type) {
            my $window_scale_hex = '';
            for (my $i=$opt_kind+2; $i < ($opt_kind+$len); $i++) {
                $window_scale_hex .= $hex_nums[$i];
            }
            my $win_scale = hex($window_scale_hex);
            push @opts, {$tcp_win_scale_type => $win_scale};
            $debug_str .= 'Win Scale: ' . hex($window_scale_hex) . ', ' if $debug;
        } elsif (hex($hex_nums[$opt_kind]) == $tcp_sack_type) {
            push @opts, {$tcp_sack_type => ''};
            $debug_str .= 'SACK, ' if $debug;
        } elsif (hex($hex_nums[$opt_kind]) == $tcp_timestamp_type) {
            my $timestamp_hex = '';
            for (my $i=$opt_kind+2; $i < ($opt_kind+$len) - 4; $i++) {
                $timestamp_hex .= $hex_nums[$i];
            }
            my $timestamp = hex($timestamp_hex);
            push @opts, {$tcp_timestamp_type => $timestamp};
            $debug_str .= 'Timestamp: ' . hex($timestamp_hex) . ', ' if $debug;
        } elsif (hex($hex_nums[$opt_kind]) == 0) {  ### End of option list
            last OPT;
        }
        if ($is_nop) {
            $opt_kind += 1;
        } else {
            if ($len == 0 or $len == 1) {
                ### this should never happen; it indicates a broken TCP stack
                ### or maliciously constructed options since the len field is
                ### large enough to accommodate the TLV encoding
                my $msg = "broken $len-byte len field within TCP options " .
                    "string: $tcp_options from source IP: $src";
                print STDERR "    $msg\n" if $debug;
                &logr('[-]', $msg, $NO_MAIL);
                return [];
            }
            ### get to the next option-kind field
            $opt_kind += $len;
        }
    }
    if ($debug) {
        $debug_str =~ s/\,$//;
        print STDERR localtime() . " [+] $debug_str\n" if $debug;
    }
    return \@opts;
}

sub print_p0f() {
    for my $src (keys %p0f) {
        print "[+] $src\n";
        for my $os (keys %{$p0f{$src}}) {
            printf "      %-33s%s\n", $p0f{$src}{$os}, $os;
        }
    }
    exit 0;
}

sub import_p0f_sigs() {
    my $p0f_file = $config{'P0F_FILE'};
    open P, "< $p0f_file" or die '[*] Could not open ',
        "$p0f_file: $!";
    my @lines = <P>;
    close P;
    my $os = '';
    for my $line (@lines) {
        chomp $line;
        next if $line =~ /^\s*#/;
        next unless $line =~ /\S/;

        ### S3:64:1:60:M*,S,T,N,W1:        Linux:2.5::Linux 2.5 (sometimes 2.4)
        ### 16384:64:1:60:M*,N,W0,N,N,T:   FreeBSD:4.4::FreeBSD 4.4
        ### 16384:64:1:44:M*:              FreeBSD:2.0-2.2::FreeBSD 2.0-4.1

        if ($line =~ /^(\S+?):(\S+?):(\S+?):(\S+?):(\S+?):\s+(.*)\s*/) {
            my $win_size = $1;
            my $ttl      = $2;
            my $frag_bit = $3;
            my $len      = $4;
            my $options  = $5;
            my $os       = $6;

            my $sig_str = "$win_size:$ttl:$frag_bit:$len:$options";
            ### don't know how to handle MTU-based window size yet
            unless ($win_size =~ /T/) {
                $p0f_sigs{$len}{$frag_bit}{$ttl}{$win_size}{$options}{$os}
                    = $sig_str;
            }
        }
    }

    print STDERR Dumper %p0f_sigs if $debug and $verbose;
    &logr('[+]', 'imported p0f-based passive OS fingerprinting signatures',
        $NO_MAIL);
    return;
}

sub import_access() {
    open A, "< $config{'ACCESS_CONF'}" or die "[*] Could not open ",
        "$config{'ACCESS_CONF'}: $!";
    my @lines = <A>;
    close A;
    my $src  = '';
    my $type = '';
    my $valid_ctr = 0;
    my $source_block_num = 0;

    for (my $i=0; $i<=$#lines; $i++) {
        my $line = $lines[$i];
        chomp $line;
        next if $line =~ /^\s*#/;
        next unless $line =~ /\S/;

        die "[*] No semicolon ending found for line: ",
            "$line in $config{'ACCESS_CONF'}"
            unless $line =~ /^.+;/;
    }

    for (my $i=0; $i<=$#lines; $i++) {
        my $line = $lines[$i];
        chomp $line;
        next if $line =~ /^\s*#/;
        next unless $line =~ /\S/;

        die "[*] No semicolon ending found for line: ",
            "$line in $config{'ACCESS_CONF'}"
            unless $line =~ /^.+;/;

        my $type = '';
        my %access_hsh = ();

        if ($line =~ /^\s*SOURCE:/) {
            ### keep track of SOURCE block number; note that this value
            ### increments whether or not we actually have a valid block
            ### (so we can keep track of exactly which block within the
            ### access.conf file).
            $source_block_num++;
            $access_hsh{'block_num'} = $source_block_num;

            my $src_str = '';
            if ($line =~ m|^\s*SOURCE:?\s*(.*)\s*;|) {
                $src_str = $1;
                ($access_hsh{'SOURCE'}, $access_hsh{'exclude_nets'})
                        = &parse_nets($src_str);
            }
            $i++;
            $access_hsh{'src_line_num'} = $i;
            $access_hsh{'src_str'}      = $src_str;
            while (defined $lines[$i] and $lines[$i] !~ /^\s*SOURCE:?/) {
                my $line = $lines[$i];
                $i++;
                chomp $line;
                next if $line =~ /^\s*#/;
                next unless $line =~ /\S/;
                if ($line =~ /^\s*DATA_COLLECT_MODE:?\s+(\S+);/) {
                    my $mode = $1;
                    if (uc($mode) eq 'PCAP') {
                        $access_hsh{'DATA_COLLECT_MODE'} = $PCAP;
                    } elsif (uc($mode) eq 'FILE_PCAP') {
                        $access_hsh{'DATA_COLLECT_MODE'} = $FILE_PCAP;
                    } elsif (uc($mode) eq 'ULOG_PCAP') {
                        $access_hsh{'DATA_COLLECT_MODE'} = $ULOG_PCAP;
                    } elsif (uc($mode) eq 'ENCRYPT_SEQUENCE') {
                        $access_hsh{'DATA_COLLECT_MODE'} = $ENCRYPT_SEQUENCE;
                    }
                } elsif ($line =~ /^\s*ENCRYPT_SEQUENCE\s*;/) {
                    $access_hsh{'DATA_COLLECT_MODE'} = $ENCRYPT_SEQUENCE;
                } elsif ($line =~ /^\s*KEY:?\s*(.*)\s*;/) {
                    unless ($imported_crypt_cbc) {
                        require Crypt::CBC;
                        print STDERR "[+] Crypt::CBC::VERSION ",
                            "$Crypt::CBC::VERSION\n" if $debug;
                    }
                    $imported_crypt_cbc = 1;
                    $access_hsh{'KEY'} = $1;
                    ### pad with zeros to the key size
                    while (length($access_hsh{'KEY'}) < $enc_keysize) {
                        $access_hsh{'KEY'} .= '0';
                    }
                } elsif ($line =~ /^\s*GPG_REMOTE_ID:?\s*(.*)\s*;/) {
                    unless ($imported_gpg) {
                        require GnuPG::Interface;
                        print STDERR "[+] GnuPG::Interface::VERSION ",
                            "$GnuPG::Interface::VERSION\n" if $debug;
                    }
                    $imported_gpg = 1;
                    my @arr = split /\s*\,\s*/, $1;
                    for my $gpg_key_id (@arr) {
                        push @{$access_hsh{'GPG_REMOTE_ID'}}, $gpg_key_id;
                    }
                } elsif ($line =~ /^\s*GPG_DECRYPT_ID:?\s*(.*)\s*;/) {
                    unless ($imported_gpg) {
                        require GnuPG::Interface;
                        print STDERR "[+] GnuPG::Interface::VERSION ",
                            "$GnuPG::Interface::VERSION\n" if $debug;
                    }
                    $imported_gpg = 1;
                    $access_hsh{'GPG_DECRYPT_ID'} = $1;
                } elsif ($line =~ /^\s*GPG_DECRYPT_PW:?\s*(.*)\s*;/) {
                    unless ($imported_gpg) {
                        require GnuPG::Interface;
                        print STDERR "[+] GnuPG::Interface::VERSION ",
                            "$GnuPG::Interface::VERSION\n" if $debug;
                    }
                    $imported_gpg = 1;
                    $access_hsh{'GPG_DECRYPT_PW'} = $1;
                } elsif ($line =~ /^\s*GPG_HOME_DIR:?\s*(\S+)\s*;/) {
                    unless ($imported_gpg) {
                        require GnuPG::Interface;
                        print STDERR "[+] GnuPG::Interface::VERSION ",
                            "$GnuPG::Interface::VERSION\n" if $debug;
                    }
                    $imported_gpg = 1;
                    $access_hsh{'GPG_HOME_DIR'} = $1;
                } elsif ($line =~ /^\s*GPG_NO_OPTIONS:?\s*(\S+);/) {
                    my $val = $1;
                    if ($val =~ /y/i) {
                        $access_hsh{'GPG_NO_OPTIONS'} = 1;
                    } else {
                        $access_hsh{'GPG_NO_OPTIONS'} = 0;
                    }
                } elsif ($line =~ /^\s*GPG_USE_OPTIONS:?\s*(\S+);/) {
                    my $val = $1;
                    if ($val =~ /y/i) {
                        $access_hsh{'GPG_USE_OPTIONS'} = 1;
                    } else {
                        $access_hsh{'GPG_USE_OPTIONS'} = 0;
                    }
                } elsif ($line =~ /^\s*GPG_NO_REQUIRE_PREFIX:?\s*(\S+);/) {
                    my $val = $1;
                    if ($val =~ /y/i) {
                        $access_hsh{'GPG_NO_REQUIRE_PREFIX'} = 1;
                    } else {
                        $access_hsh{'GPG_NO_REQUIRE_PREFIX'} = 0;
                    }
                } elsif ($line =~ /^\s*GPG_PREFIX:?\s*(\S+);/) {
                    $access_hsh{'GPG_PREFIX'} = $1;
                } elsif ($line =~ /^\s*GPG_PATH:?\s*(\S+);/) {
                    $access_hsh{'GPG_PATH'} = $1;
                    unless (-e $access_hsh{'GPG_PATH'}
                            and -x $access_hsh{'GPG_PATH'}) {
                        die "[*] $access_hsh{'GPG_PATH'} does not exist ",
                            "or could not execute.";
                    }
                } elsif ($line =~ /^\s*FILE_PCAP\s*;/) {
                    ### used in file pcap mode
                    $access_hsh{'DATA_COLLECT_MODE'} = $FILE_PCAP;
                } elsif ($line =~ /^\s*ULOG_PCAP\s*;/) {
                    ### used in ulog pcap mode
                    $access_hsh{'DATA_COLLECT_MODE'} = $ULOG_PCAP;
                } elsif ($line =~ /^\s*PCAP\s*;/) {
                    ### used in pcap mode
                    $access_hsh{'DATA_COLLECT_MODE'} = $PCAP;
                } elsif ($line =~ /^\s*SHARED_SEQUENCE:?\s*(.*)\s*;/) {
                    $access_hsh{'DATA_COLLECT_MODE'} = $SHARED_SEQUENCE;
                    my $sequence = $1;
                    my @arr = split /\s*\,\s*/, $sequence;
                    for my $port (@arr) {
                        my %hsh = ();
                        if ($port =~ m|tcp/(\d+)|) {
                            %hsh = ('port' => $1, 'proto' => 'tcp');
                        } elsif ($port =~ m|udp/(\d+)|) {
                            %hsh = ('port' => $1, 'proto' => 'udp');
                        } elsif ($port =~ m|icmp|) {
                            %hsh = ('port' => -1, 'proto' => 'icmp');
                        }
                        next unless %hsh;
                        push @{$access_hsh{'SHARED_SEQUENCE'}}, \%hsh;
                    }
                } elsif ($line =~ /^\s*PORT_OFFSET:?\s*(\d+)\s*;/) {
                    $access_hsh{'PORT_OFFSET'} = $1;
                } elsif ($line =~ /^\s*OPEN_PORTS:?\s*(.*)\s*;/) {
                    my $open_ports = $1;
                    my @arr = split /\s*\,\s*/, $open_ports;
                    for my $port (@arr) {
                        if ($port =~ m|tcp/(\d+)|i) {
                            $access_hsh{'OPEN_PORTS'}{'tcp'}{$1} = '';
                        } elsif ($port =~ m|udp/(\d+)|i) {
                            $access_hsh{'OPEN_PORTS'}{'udp'}{$1} = '';
                        } elsif ($port =~ m|icmp|i) {
                            $access_hsh{'OPEN_PORTS'}{'icmp'}{0} = '';
                        }
                    }
                } elsif ($line =~ /^\s*ENABLE_FORWARD_ACCESS\s*;/) {
                    $access_hsh{'ENABLE_FORWARD_ACCESS'} = 1;
                } elsif ($line =~ /^\s*ENABLE_FORWARD_ACCESS:?\s*(\S+);/) {
                    my $val = $1;
                    if ($val =~ /y/i) {
                        $access_hsh{'ENABLE_FORWARD_ACCESS'} = 1;
                    } else {
                        $access_hsh{'ENABLE_FORWARD_ACCESS'} = 0;
                    }
                } elsif ($line =~ /^\s*ENABLE_OUTPUT_ACCESS\s*;/) {
                    $access_hsh{'ENABLE_OUTPUT_ACCESS'} = 1;
                } elsif ($line =~ /^\s*ENABLE_OUTPUT_ACCESS:?\s*(\S+);/) {
                    my $val = $1;
                    if ($val =~ /y/i) {
                        $access_hsh{'ENABLE_OUTPUT_ACCESS'} = 1;
                    } else {
                        $access_hsh{'ENABLE_OUTPUT_ACCESS'} = 0;
                    }
                } elsif ($line =~ /^\s*ENABLE_EXTERNAL_CMDS:?\s*(\S+);/) {
                    my $val = $1;
                    if ($val =~ /y/i) {
                        $access_hsh{'ENABLE_EXTERNAL_CMDS'} = 1;
                    } else {
                        $access_hsh{'ENABLE_EXTERNAL_CMDS'} = 0;
                    }
                } elsif ($line =~ /^\s*EXTERNAL_CMD_OPEN:?\s*(.*);/) {
                    $access_hsh{'EXTERNAL_CMD_OPEN'} = $1;
                    $access_hsh{'ENABLE_EXTERNAL_CMDS'} = 1;
                } elsif ($line =~ /^\s*EXTERNAL_CMD_CLOSE:?\s*(.*);/) {
                    $access_hsh{'EXTERNAL_CMD_CLOSE'} = $1;
                    $access_hsh{'ENABLE_EXTERNAL_CMDS'} = 1;
                } elsif ($line =~ /^\s*EXTERNAL_CMD_ALARM:?\s*(\d+);/) {
                    $access_hsh{'EXTERNAL_CMD_ALARM'} = $1;
                } elsif ($line =~ /^\s*REQUIRE_AUTH_METHOD:?\s*(\S+)\s*;/) {
                    $access_hsh{'REQUIRE_AUTH_METHOD'} = lc($1);
                } elsif ($line =~ /^\s*SHADOW_FILE:?\s*(\S+)\s*;/) {
                    $access_hsh{'SHADOW_FILE'} = $1;
                } elsif ($line =~ /^\s*KNOCK_INTERVAL:?\s*(\d+)\s*;/) {
                    $access_hsh{'KNOCK_INTERVAL'} = $1;
                } elsif ($line =~ /^\s*KNOCK_LIMIT:?\s*(\d+)\s*;/) {
                    $access_hsh{'KNOCK_LIMIT'} = $1;
                } elsif ($line =~ /^\s*PERMIT_CLIENT_PORTS\s*;/) {
                    $access_hsh{'PERMIT_CLIENT_PORTS'} = 1;
                } elsif ($line =~ /^\s*PERMIT_CLIENT_PORTS:?\s*(\S+);/) {
                    my $val = $1;
                    if ($val =~ /y/i) {
                        $access_hsh{'PERMIT_CLIENT_PORTS'} = 1;
                    } else {
                        $access_hsh{'PERMIT_CLIENT_PORTS'} = 0;
                    }
                } elsif ($line =~ /^\s*PERMIT_CLIENT_TIMEOUT\s*;/) {
                    $access_hsh{'PERMIT_CLIENT_TIMEOUT'} = 1;
                } elsif ($line =~ /^\s*PERMIT_CLIENT_TIMEOUT:?\s*(\S+);/) {
                    my $val = $1;
                    if ($val =~ /y/i) {
                        $access_hsh{'PERMIT_CLIENT_TIMEOUT'} = 1;
                    } else {
                        $access_hsh{'PERMIT_CLIENT_TIMEOUT'} = 0;
                    }
                } elsif ($line =~ /^\s*ENABLE_CMD_EXEC\s*;/) {
                    $access_hsh{'ENABLE_CMD_EXEC'} = 1;
                } elsif ($line =~ /^\s*ENABLE_CMD_EXEC:?\s*(\S+);/) {
                    my $val = $1;
                    if ($val =~ /y/i) {
                        $access_hsh{'ENABLE_CMD_EXEC'} = 1;
                    } else {
                        $access_hsh{'ENABLE_CMD_EXEC'} = 0;
                    }
                } elsif ($line =~ /^\s*DISABLE_FW_ACCESS\s*;/) {
                    $access_hsh{'DISABLE_FW_ACCESS'} = 1;
                } elsif ($line =~ /^\s*DISABLE_FW_ACCESS:?\s*(\S+);/) {
                    my $val = $1;
                    if ($val =~ /y/i) {
                        $access_hsh{'DISABLE_FW_ACCESS'} = 1;
                    } else {
                        $access_hsh{'DISABLE_FW_ACCESS'} = 0;
                    }
                } elsif ($line =~ /^\s*REQUIRE_SOURCE_ADDRESS:?\s*(.*)\s*;/) {
                    my $str = $1;
                    if ($str =~ /y/i) {
                        $str = '';  ### don't allow the client to set 0.0.0.0
                    } elsif ($str =~ /n/i) {
                        $str = '0.0.0.0';  ### allow the client to set 0.0.0.0
                    }
                    ### we are setting specific allowed networks for the internal
                    ### allow IP's (i.e. with -a or -R on the client side)
                    ($access_hsh{'REQUIRE_SOURCE_ADDRESS'},
                    $access_hsh{'require_src_addr_exceptions'})
                            = &parse_nets($str);
                } elsif ($line =~ /^\s*INTERNAL_NET_ACCESS:?\s*(.*)\s*;/) {
                    ### for --Forward-access restrictions to internal IP addresses
                    ($access_hsh{'INTERNAL_NET_ACCESS'},
                    $access_hsh{'internal_net_exceptions'})
                            = &parse_nets($1);
                } elsif ($line =~ /^\s*CMD_REGEX:?\s*(.*)\s*;/) {
                    $access_hsh{'CMD_REGEX'} = qr|$1|;
                } elsif ($line =~ /^\s*FW_ACCESS_TIMEOUT:?\s*(\d+)\s*;/) {
                    $access_hsh{'FW_ACCESS_TIMEOUT'} = $1;
                } elsif ($line =~ /^\s*MAX_ACCESS_TIMEOUT:?\s*(\d+)\s*;/) {
                    $access_hsh{'FW_ACCESS_TIMEOUT'} = $1;
                } elsif ($line =~ /^\s*REQUIRE_OS:?\s*(.*)\s*;/) {
                    $access_hsh{'REQUIRE_OS'} = $1;
                } elsif ($line =~ /^\s*REQUIRE_OS_REGEX:?\s*(.*)\s*;/) {
                    $access_hsh{'REQUIRE_OS_REGEX'} = $1;
                } elsif ($line =~ /^\s*REQUIRE_USERNAME:?\s*(.*)\s*;/) {
                    $access_hsh{'REQUIRE_USERNAME'} = $1;
                } elsif ($line =~ /^\s*MIN_TIME_DIFF:?\s*(\d+)\s*;/) {
                    $access_hsh{'MIN_TIME_DIFF'} = $1;
                } elsif ($line =~ /^\s*MAX_TIME_DIFF:?\s*(\d+)\s*;/) {
                    $access_hsh{'MAX_TIME_DIFF'} = $1;
                } elsif ($line =~ /^\s*RESTRICT_INTF:?\s*(\w+)\s*;/) {
                    $access_hsh{'RESTRICT_INTF'} = $1;
                }
            }
            $i--;
        }
        unless (defined $access_hsh{'PERMIT_CLIENT_PORTS'}) {
            $access_hsh{'PERMIT_CLIENT_PORTS'} = 0;
        }
        unless (defined $access_hsh{'PERMIT_CLIENT_TIMEOUT'}) {
            $access_hsh{'PERMIT_CLIENT_TIMEOUT'} = 0;
        }
        if (&validate_src_access_hsh(\%access_hsh)) {
            push @access, \%access_hsh;
            $valid_ctr++;
        }

        if (defined $access_hsh{'REQUIRE_USERNAME'}) {
            my @users = split /\s*,\s*/, $access_hsh{'REQUIRE_USERNAME'};
            for my $user (@users) {
                push @{$access_hsh{'VALID_USERS'}}, $user;
            }
        }
    }

    if ($valid_ctr == 0) {
        die "[*] No valid SOURCE blocks defined in $config{'ACCESS_CONF'} ",
            "(review syslog for more info). Exiting.";
    }
    &dump_config() if $debug;
    if ($debug and $verbose) {
        for (my $i=0; $i<=$#access; $i++) {
            &dump_access($access[$i], $i);
        }
    }
    &logr('[+]', 'imported access directives ' .
        "($valid_ctr SOURCE definitions).", $NO_MAIL);
    return;
}

sub parse_nets() {
    my $net_str = shift;
    my @include_nets = ();
    my @exclude_nets = ();

    $net_str =~ s|\!\s+|!|g;

    for my $str (split /\s*,\s*/, $net_str) {

        if ($str =~ m|$ip_re/$ip_re|
                or $str =~ m|$ip_re/\d{1,2}|
                or $str =~ m|$ip_re|
                or $str =~ m|any|i) {

            if ($str =~ /any/i) {
                if ($str =~ m|!|) {
                    ### ipv4_in_network('0.0.0.0', $someip) always matches
                    push @exclude_nets, '0.0.0.0';
                } else {
                    push @include_nets, '0.0.0.0';
                }
            } else {
                if ($str =~ m|!|) {
                    push @exclude_nets, &ip_info_only($str);
                } else {
                    push @include_nets, &ip_info_only($str);
                }
            }
        } else {
            ### allow the string "NONE"
            unless ($net_str =~ m|none|i) {
                die qq|[*] Improper "$str" in SOURCE line |,
                    qq|in $config{'ACCESS_CONF'}|;
            }
        }
    }
    return \@include_nets, \@exclude_nets;
}

sub ip_info_only() {
    my $str = shift;
    my $ip_info = '';
    if ($str =~ m|($ip_re/$ip_re)|) {
        $ip_info = $1;
    } elsif ($str =~ m|($ip_re/\d{1,2})|) {
        $ip_info = $1;
    } elsif ($str =~ m|($ip_re)|) {
        $ip_info = $1;
    }
    die "[*] Could not parse IP information from: $str"
        unless $ip_info;
    return $ip_info;
}

sub dump_config() {
    my $rv = 0;
    print STDERR "\n", localtime() . " [+] Dumping config from: $config_file\n";
    for my $var (sort keys %config) {
        my $str = $config{$var};
        ### sanitize sensitive information
        $str = '(removed)' if $var eq 'EMAIL_ADDRESSES';
        $str = '(removed)' if $var eq 'HOSTNAME';
        $str = '(removed)' if $var eq 'BLACKLIST';
        $str = '(removed)' if $var eq 'GPG_DEFAULT_HOME_DIR';
        printf STDERR "%-30s %s\n", $var, $str;
    }
    print STDERR "\n", localtime() . " [+] Command paths:\n\n";
    for my $var (sort keys %cmds) {
        printf STDERR "%-30s %s\n", $var, $cmds{$var};
    }
    return $rv;
}

sub dump_access() {
    my ($access_hr, $num) = @_;
    print STDERR localtime() . " SOURCE block: $num\n";
    for my $key (keys %access_keys) {
        next unless defined $access_hr->{$key};
        if ($key eq 'KEY') {
            ### never print out symmetric keys
            print STDERR "$key: (removed)\n";
        } elsif ($key eq 'GPG_DECRYPT_PW') {
            ### never print out gpg passwords
            print STDERR "$key: (removed)\n";
        } elsif ($key eq 'GPG_DECRYPT_ID') {
            if ($include_all_config_data) {
                print STDERR "$key: $access_hr->{$key}\n";
            } else {
                print STDERR "$key: (removed)\n";
            }
        } elsif ($key eq 'GPG_REMOTE_ID') {
            if ($include_all_config_data) {
                print STDERR "$key: $access_hr->{$key}\n";
            } else {
                print STDERR "$key: (removed)\n";
            }
        } elsif ($key eq 'GPG_HOME_DIR') {
            if ($include_all_config_data) {
                print STDERR "$key: $access_hr->{$key}\n";
            } else {
                print STDERR "$key: (removed)\n";
            }
        } elsif ($key eq 'OPEN_PORTS'
                or $key eq 'SOURCE'
                or $key eq 'REQUIRE_SOURCE_ADDRESS'
                or $key eq 'require_src_addr_exceptions'
                or $key eq 'INTERNAL_NET_ACCESS'
                or $key eq 'internal_net_exceptions'
                or $key eq 'REQUIRE_SOURCE_ADDRESS') {
            print STDERR "$key: ", Dumper $access_hr->{$key};
        } else {
            print STDERR "$key: $access_hr->{$key}\n";
        }
    }
    return;
}

sub import_override_configs() {
    my @override_configs = split /,/, $override_config_str;
    for my $file (@override_configs) {
        die "[*] Override config file $file does not exist"
            unless -e $file;
        &import_config($file);
    }
    return;
}

sub import_config() {
    my $config_file = shift;
    open C, "< $config_file" or die "[*] Could not open ",
        "config file $config_file: $!";
    my @lines = <C>;
    close C;
    for my $line (@lines) {
        chomp $line;
        next if $line =~ /^\s*#/;
        next unless $line =~ /\S/;

        die "[*] No semicolon ending found for line: ",
            "$line in $config_file"
            unless $line =~ /^.+;/;

        if ($line =~ /^\s*(\S+)\s+(.*?)\;/) {
            my $varname = $1;
            my $val     = $2;
            if ($varname =~ /^(\w+)Cmd$/) {
                ### found a command
                $cmds{$1} = $val unless defined $cmds{$1};
            } else {
                $config{$varname} = $val unless defined $config{$varname};
            }
        }
    }
    return;
}

sub validate_src_access_hsh() {
    my $access_hr = shift;
    my $src_line = 0;
    my $gpg_mode = 0;
    if (defined $access_hr->{'SOURCE'}) {
        $src_line = $access_hr->{'src_line_num'};
    } else {
        die "[*] $config{'ACCESS_CONF'}: missing SOURCE variable.";
    }

    if (not defined $access_hr->{'OPEN_PORTS'} and
            not $access_hr->{'PERMIT_CLIENT_PORTS'}) {

        unless ($access_hr->{'EXTERNAL_CMD_OPEN'}
                or ($config{'FIREWALL_TYPE'} eq 'external_cmd'
                and $config{'EXTERNAL_CMD_OPEN'})) {

            die "[*] $config{'ACCESS_CONF'}: SOURCE (line: $src_line) ",
                "missing\n    OPEN_PORTS and PERMIT_CLIENT_PORTS is disabled.";
        }
    }

    if (not defined $access_hr->{'REQUIRE_SOURCE_ADDRESS'}) {
        ### allow 0.0.0.0
        ($access_hr->{'REQUIRE_SOURCE_ADDRESS'},
        $access_hr->{'require_src_addr_exceptions'})
                = &parse_nets('0.0.0.0');
    }

    if (not defined $access_hr->{'INTERNAL_NET_ACCESS'}) {
        ### allow 0.0.0.0
        ($access_hr->{'INTERNAL_NET_ACCESS'},
        $access_hr->{'internal_net_exceptions'})
                = &parse_nets('0.0.0.0');
    }

    ### default to SPA mode via standard pcap
    $access_hr->{'DATA_COLLECT_MODE'} = $PCAP
        unless defined $access_hr->{'DATA_COLLECT_MODE'};

    ### only allow forwarding access if ENABLE_IPT_FORWARDING is enabled
    if ($access_hr->{'ENABLE_FORWARD_ACCESS'}
            and $config{'ENABLE_IPT_FORWARDING'} eq 'N') {
        die "[*] $config{'ACCESS_CONF'}: SOURCE: (line: $src_line) ",
                "ENABLE_FORWARD_ACCESS\n    enabled, but ",
                "ENABLE_IPT_FORWARDING disabled in fwknop.conf.";
    }

    if ($access_hr->{'DATA_COLLECT_MODE'} == $ENCRYPT_SEQUENCE) {
        unless (defined $access_hr->{'KEY'}) {
            die "[*] $config{'ACCESS_CONF'}: SOURCE: (line: $src_line) ",
                "missing KEY\n    variable for encrypt_seq collection mode.";
        }
        unless (defined $access_hr->{'PORT_OFFSET'}) {
            &logr('[-]', "$config{'ACCESS_CONF'}: SOURCE: (line: $src_line) " .
                "missing PORT_OFFSET, defaulting to $enc_port_offset.",
                $NO_MAIL);
            $access_hr->{'PORT_OFFSET'} = $enc_port_offset;
        }
    } elsif ($access_hr->{'DATA_COLLECT_MODE'} == $SHARED_SEQUENCE) {
        unless (defined $access_hr->{'OPEN_PORTS'}) {
            die "[*] $config{'ACCESS_CONF'}: SOURCE (line: $src_line) ",
                "missing\n    OPEN_PORTS variable.";
        }
    } elsif ($access_hr->{'DATA_COLLECT_MODE'} == $PCAP
            or $access_hr->{'DATA_COLLECT_MODE'} == $FILE_PCAP
            or $access_hr->{'DATA_COLLECT_MODE'} == $ULOG_PCAP) {

        if (defined $access_hr->{'GPG_AGENT_INFO'}
                and not defined $access_hr->{'GPG_DECRYPT_PW'}) {
            $access_hr->{'GPG_DECRYPT_PW'} = '';
        }
        unless (defined $access_hr->{'KEY'} or
                (defined $access_hr->{'GPG_REMOTE_ID'}
                and defined $access_hr->{'GPG_DECRYPT_ID'}
                and defined $access_hr->{'GPG_DECRYPT_PW'})) {
            die "[*] $config{'ACCESS_CONF'}: SOURCE (line: $src_line) ",
                "missing KEY or\n    (GPG_DECRYPT_ID, GPG_DECRYPT_PW, or ",
                "GPG_REMOTE_ID) variable for pcap collection mode.";
        }
        if (defined $access_hr->{'GPG_DECRYPT_ID'}
                or defined $access_hr->{'GPG_DECRYPT_PW'}
                or defined $access_hr->{'GPG_HOME_DIR'}) {
            unless (defined $access_hr->{'GPG_REMOTE_ID'}) {
                die "[*] $config{'ACCESS_CONF'}: SOURCE (line: $src_line) ",
                    "missing\n    GPG_REMOTE_ID variable.";
            }
        }
        $gpg_mode = 1 if defined $access_hr->{'GPG_REMOTE_ID'}
                and not $cmdl_disable_gpg;
        if ($gpg_mode) {
            $access_hr->{'GPG_PREFIX'} = $gpg_default_prefix
                unless defined $access_hr->{'GPG_PREFIX'};
            $access_hr->{'GPG_PATH'} = ''
                unless defined $access_hr->{'GPG_PATH'};
            $access_hr->{'GPG_NO_OPTIONS'} = 0
                unless defined $access_hr->{'GPG_NO_OPTIONS'};
            $access_hr->{'GPG_USE_OPTIONS'} = 0
                unless defined $access_hr->{'GPG_USE_OPTIONS'};
            unless ($access_hr->{'GPG_PATH'}) {
                &check_commands({'gpg' => ''}, {});
            }
        }
        if (defined ($access_hr->{'REQUIRE_AUTH_METHOD'})) {
            unless (lc($access_hr->{'REQUIRE_AUTH_METHOD'}) eq 'crypt') {
                die "[*] $config{'ACCESS_CONF'}: SOURCE (line: $src_line) ",
                    "invalid\n    REQUIRE_AUTH_METHOD, must be set to 'crypt'.";
            }
            unless (defined $access_hr->{'SHADOW_FILE'}) {
                $access_hr->{'SHADOW_FILE'} = '/etc/shadow';
            }
        }
        if (defined $access_hr->{'EXTERNAL_CMD_OPEN'}
                and $access_hr->{'EXTERNAL_CMD_OPEN'}) {
            unless (defined $access_hr->{'EXTERNAL_CMD_CLOSE'}
                    and $access_hr->{'EXTERNAL_CMD_CLOSE'}) {
                die "[*] $config{'ACCESS_CONF'}: SOURCE (line: $src_line) ",
                    "invalid\n    Cannot define EXTERNAL_CMD_OPEN without also ",
                    "defining EXTERNAL_CMD_CLOSE";
            }
        } else {
            $access_hr->{'EXTERNAL_CMD_OPEN'} = '';
        }
        if (defined $access_hr->{'EXTERNAL_CMD_CLOSE'}
                and $access_hr->{'EXTERNAL_CMD_CLOSE'}) {
            unless (defined $access_hr->{'EXTERNAL_CMD_OPEN'}
                    and $access_hr->{'EXTERNAL_CMD_OPEN'}) {
                die "[*] $config{'ACCESS_CONF'}: SOURCE (line: $src_line) ",
                    "invalid\n    Cannot define EXTERNAL_CMD_CLOSE without also ",
                    "defining EXTERNAL_CMD_OPEN";
            }
        } else {
            $access_hr->{'EXTERNAL_CMD_CLOSE'} = '';
        }
        if ($access_hr->{'EXTERNAL_CMD_OPEN'}) {
            $access_hr->{'EXTERNAL_CMD_ALARM'} = $EXTERNAL_CMD_ALARM
                unless defined $access_hr->{'EXTERNAL_CMD_ALARM'};
        }
    } else {
        die "[*] $config{'ACCESS_CONF'}: SOURCE (line: $src_line) ",
            "missing valid DATA_COLLECT_MODE\n    key (must be one of ",
            "ENCRYPT_SEQUENCE, SHARED_SEQUENCE, FILE_PCAP, ULOG_PCAP, or PCAP).";
    }
    if (defined $access_hr->{'MIN_TIME_DIFF'} and
            defined $access_hr->{'MAX_TIME_DIFF'}) {
        if ($access_hr->{'MAX_TIME_DIFF'} <
                $access_hr->{'MIN_TIME_DIFF'}) {
            die "[*] $config{'ACCESS_CONF'}: SOURCE (line: $src_line) ",
                "MAX_TIME_DIFF\n    cannot be less than MIN_TIME_DIFF.";
        }
    }
    if (defined $access_hr->{'KNOCK_INTERVAL'}) {
        if ($access_hr->{'KNOCK_INTERVAL'} < 0) {
            die "[*] $config{'ACCESS_CONF'}: SOURCE (line: $src_line) ",
                "KNOCK_INTERVAL\n    must be greater than or equal to zero.";
        }
    } else {
        unless ($access_hr->{'DATA_COLLECT_MODE'} == $PCAP
                or $access_hr->{'DATA_COLLECT_MODE'} == $FILE_PCAP
                or $access_hr->{'DATA_COLLECT_MODE'} == $ULOG_PCAP) {
            &logr('[-]', "$config{'ACCESS_CONF'}: SOURCE (line: $src_line) " .
                "missing KNOCK_INTERVAL, defaulting to $knock_interval.",
                $NO_MAIL);
            $access_hr->{'KNOCK_INTERVAL'} = $knock_interval;
        }
    }
    if (defined $access_hr->{'FW_ACCESS_TIMEOUT'}) {
        if ($access_hr->{'FW_ACCESS_TIMEOUT'} < 0) {
            die "[*] $config{'ACCESS_CONF'}: SOURCE (line: $src_line) ",
                "FW_ACCESS_TIMEOUT\n    must be greater than or equal to zero.";
        }
    } else {
        if (defined $access_hr->{'PERMIT_CLIENT_TIMEOUT'}) {
            ### in this case we will derive the timeout from the SPA
            ### packet.
            $access_hr->{'FW_ACCESS_TIMEOUT'} = 0;
        } else {
            &logr('[-]', "$config{'ACCESS_CONF'}: SOURCE (line: $src_line) " .
                "missing FW_ACCESS_TIMEOUT, defaulting to $default_access_timeout",
                $NO_MAIL);
            $access_hr->{'FW_ACCESS_TIMEOUT'} = $default_access_timeout;
        }
    }
    if (defined $access_hr->{'KNOCK_LIMIT'}) {
        if ($access_hr->{'KNOCK_LIMIT'} < 0) {
            die "[*] $config{'ACCESS_CONF'}: SOURCE (line: $src_line) ",
                "KNOCK_LIMIT\n    must be greater than or equal to zero.";
        }
    }
    if ($gpg_mode and not $cmdl_disable_gpg) {
        unless (defined $access_hr->{'GPG_HOME_DIR'}) {
            $access_hr->{'GPG_HOME_DIR'} = $config{'GPG_DEFAULT_HOME_DIR'};
        }
        unless (-d $access_hr->{'GPG_HOME_DIR'}) {
            die "[*] $config{'ACCESS_CONF'}: GnuPG directory " .
                "$access_hr->{'GPG_HOME_DIR'} does not exist.";
        }
    }
    if (defined $access_hr->{'KEY'}
                and $access_hr->{'KEY'} =~ /_?_CHANGEME_?_/) {
        die "[*] $config{'ACCESS_CONF'}: Update the KEY variable ".
            "from the default of __CHANGEME__";
    }
    return 1;
}

sub handle_command_line() {
    ### make Getopts case sensitive
    Getopt::Long::Configure('no_ignore_case');
    die "[*] Use --help for usage information.\n" unless (GetOptions(
        'config=s'       => \$config_file,
        'access-conf=s'  => \$access_conf_file,
        'os'             => \$os_fprint_only,
        'intf=s'         => \$cmdline_intf,
        'Count=i'        => \$packet_limit,
        'fw-log=s'       => \$os_ipt_log,
        'fw-list'        => \$fw_list,
        'fw-flush'       => \$fw_flush,
        'fw-del-chains'  => \$ipt_del_chains,
        'fw-del-ip=s'    => \$fw_del_ip,
        'fw-type=s'      => \$fw_type,
        'gpg-agent-info=s' => \$gpg_agent_info,
        'gpg-no-options'   => \$gpg_no_options,
        'gpg-use-options'  => \$gpg_use_options,
        'no-gpg'         => \$cmdl_disable_gpg,
        'debug'          => \$debug,
        'Kill'           => \$stop_daemons,
        'Restart'        => \$restart,
        'Status'         => \$status,
        'Override-config=s'  => \$override_config_str,
        'Linux-cooked-intf'  => \$PCAP_COOKED_INTF,
        'Include-all-config' => \$include_all_config_data,
        'Test-mode'      => \$test_mode,
        'knoptmCmd=s'    => \$cmdline_knoptm,
        'fwknop_servCmd=s' => \$cmdline_fwknop_serv,
        'fwkserv-debug-file=s'  => \$fwkserv_debug_file,
        'fwkserv-debug-pidname' => \$fwkserv_include_pidname,
        'knoptm-debug-file=s'   => \$knoptm_debug_file,
        'knoptm-debug-pidname'  => \$knoptm_include_pidname,
        'spa-dump-packets=s' => \$spa_dump_packets,
        'LC_ALL=s'       => \$cmdline_locale,
        'locale=s'       => \$cmdline_locale,
        'Lib-dir=s'      => \$lib_dir,
        'no-LC_ALL'      => \$no_locale,
        'no-locale'      => \$no_locale,
        'no-FKO-module'  => \$skip_fko_module,
        'test-FKO-exists' => \$test_fko_exists,
        'Dump-config'    => \$dump_config,
        'verbose'        => \$verbose,
        'Version'        => \$print_version,
        'help'           => \$print_help
    ));
    return;
}

### check paths to commands and attempt to correct if any are wrong.
sub check_commands() {
    my ($include_hr, $exclude_hr) = @_;

    if ($debug and $verbose) {
        print STDERR "[+] check_commands() include/exclude hrefs:\n",
            Dumper($include_hr), Dumper $exclude_hr;
    }

    my @path = qw(
        /bin
        /sbin
        /usr/bin
        /usr/sbin
        /usr/local/bin
        /usr/local/sbin
    );

    for my $cmd (keys %cmds) {
        if (keys %$include_hr) {
            next unless defined $include_hr->{$cmd};
        }
        if (keys %$exclude_hr) {
            next if defined $exclude_hr->{$cmd};
        }

        if ($cmd eq 'iptables') {
            next unless $config{'FIREWALL_TYPE'} eq 'iptables';
        } elsif ($cmd eq 'ipfw') {
            next unless $config{'FIREWALL_TYPE'} eq 'ipfw';
        }

        if ($cmd eq 'mknod') {
            next unless $config{'AUTH_MODE'} eq 'KNOCK';
        }

        if ($cmd eq 'mail' or $cmd eq 'sendmail') {
            next if $config{'ALERTING_METHODS'} =~ /noe?mail/i;
        }

        unless (-x $cmds{$cmd}) {
            my $found = 0;
            if ($debug) {
                print STDERR "[-] $cmd not located/executable at $cmds{$cmd}\n";
            }
            PATH: for my $dir (@path) {
                if (-x "${dir}/${cmd}") {
                    $cmds{$cmd} = "${dir}/${cmd}";
                    $found = 1;
                    last PATH;
                }
            }
            if ($found) {
                if ($debug) {
                    print STDERR "    Found $cmd at $cmds{$cmd}\n";
                }
            } else {
                die "[*] Could not find $cmd anywhere. Please edit the\n",
                    "config section in $config_file to include the path to\n",
                    "$cmd." unless $cmd eq 'sendmail';
            }
        }
        if (-x $cmds{$cmd}) {
            if ($cmd eq 'sendmail') {
                $use_sendmail = 1;
            }
        } else {
            die "[*] Command $cmd is located at $cmds{$cmd}, but ",
                "is not executable by uid: $<" unless $cmd eq 'sendmail';
        }
    }
    return;
}

sub sendmail() {
    my $subject = shift;
    $subject =~ s/\"//g;

    if ($use_sendmail) {
        open SMAIL, "| $cmds{'sendmail'} -t" or
            die "[*] Could not execute $cmds{'sendmail'}: $!";
        print SMAIL "From: $config{'EMAIL_ADDRESSES'}\n",
            "To: $config{'EMAIL_ADDRESSES'}\n",
            "Subject: $subject\n\n";
        close SMAIL;
    } else {
        open MAIL, qq{| $cmds{'mail'} -s "$subject" $config{'EMAIL_ADDRESSES'} } .
            "> /dev/null" or die "[*] Could not send mail: $cmds{'mail'} -s " .
            "$subject\" $config{'EMAIL_ADDRESSES'}: $!";
        close MAIL;
    }
    return;
}

sub spa_dump_packets() {
    my $rv = 0;

    &import_access();

    print "[+] Reading in encoded/encrypted SPA packets ",
        "from file: $spa_dump_packets\n";
    open F, "< $spa_dump_packets" or
        die "[*] Could not open $spa_dump_packets: $!";
    while (<F>) {
        next unless /\S/;
        chomp;
        &SPA_check_grant_access('127.0.0.1', length($_), $_);
    }
    close F;
    return $rv;
}

sub uniquepid() {
    if (-e $config{'FWKNOP_PID_FILE'}) {
        my $caller = $0;
        open PIDFILE, "< $config{'FWKNOP_PID_FILE'}";
        my $pid = <PIDFILE>;
        close PIDFILE;
        chomp $pid;
        if (kill 0, $pid) {  # fwknopd is already running
            die "[*] fwknopd (pid: $pid) is already running!  Exiting.\n";
        }
    }
    return;
}

sub writepid() {
    open P, "> $config{'FWKNOP_PID_FILE'}" or die "[*] Could not open ",
        "$config{'FWKNOP_PID_FILE'}: $!";
    print P $$, "\n";
    close P;
    chmod 0600, $config{'FWKNOP_PID_FILE'};
    return;
}

sub writecmdline() {
    my $args_cp_aref = shift;
    open C, "> $config{'FWKNOP_CMDLINE_FILE'}" or die "[*] Could not open ",
        "$config{'FWKNOP_CMDLINE_FILE'}: $!";
    print C "@$args_cp_aref\n";
    close C;
    chmod 0600, $config{'FWKNOP_CMDLINE_FILE'};
    return;
}

sub stop_fwknop() {

    &logr('[+]', 'shutting down fwknop daemons', $NO_MAIL);

    ### must kill knopwatchd first since if not, it might try to restart
    ### any of the other two daemons.
    for my $name qw(knopwatchd knopmd knoptm fwknop_serv fwknopd) {
        &stop_daemon($name, $LOG_VERBOSE);
    }

    return 0;
}

sub restart() {
    my $cmdline = '';
    if (-e $config{'FWKNOP_CMDLINE_FILE'}) {
        open CMD, "< $config{'FWKNOP_CMDLINE_FILE'}" or
            die "[*] Could not open $config{'FWKNOP_CMDLINE_FILE'}: $!";
        $cmdline = <CMD>;
        close CMD;
        chomp $cmdline;
    }

    ### stop any running fwknop daemons.
    &stop_fwknop();

    print "[+] Restarting fwknop daemons.\n";
    if ($cmdline) {
        open FWKNOPD, "| $cmds{'fwknopd'} $cmdline" or die "[*] Could not ",
            "execute $cmds{'fwknopd'} $cmdline";
        close FWKNOPD;
    } else {
        open FWKNOPD, "| $cmds{'fwknopd'}" or die "[*] Could not ",
            "execute $cmds{'fwknopd'}";
        close FWKNOPD;
    }
    return 0;
}

sub status() {

    for my $pidname qw(knopwatchd knopmd knoptm fwknopd) {
        my $pidfile = $pid_files{$pidname};
        if (-e $pidfile) {
            open PIDFILE, "< $pidfile" or die '[*] Could not open ',
                "$pidfile: $!";
            my $pid = <PIDFILE>;
            close PIDFILE;
            chomp $pid;
            if (kill 0, $pid) {
                print "[+] $pidname is running as pid: $pid\n";
            } else {
                my $print = 1;
                if (($config{'AUTH_MODE'} =~ /PCAP/
                        or $config{'AUTH_MODE'} eq 'SOCKET')
                        and $pidname eq 'knopmd') {
                    $print = 0;
                }
                print "[+] $pidname is not currently running.\n" if $print;
            }
        } else {
            my $print = 1;
            if (($config{'AUTH_MODE'} =~ /PCAP/
                    or $config{'AUTH_MODE'} eq 'SOCKET')
                    and $pidname eq 'knopmd') {
                $print = 0;
            }
            print "[+] $pidname pidfile does not exist.\n" if $print;
        }
    }
    return 0;
}

sub fwknop_init() {

    ### import any override config files first
    &import_override_configs() if $override_config_str;

    ### import config
    &import_config($config_file);

    ### expand any embedded vars within config values
    &expand_vars({'EXTERNAL_CMD_OPEN' => '', 'EXTERNAL_CMD_CLOSE' => ''});

    ### set __NONE__ values to ''
    &set_null_vars();

    $config{'PCAP_INTF'} = $cmdline_intf if $cmdline_intf;
    $PCAP_COOKED_INTF = 1 if $config{'PCAP_INTF'} eq 'any';

    ### make sure all the vars we need are actually in the config file.
    &required_vars();

    ### import fwknop perl modules
    &import_perl_modules();

    ### validate config
    &validate_config();

    ### make sure command paths are correct
    unless ($os_fprint_only) {
        &check_commands({}, {'gpg' => '', 'gpg2' => '', 'mail' => ''});

        unless ($use_sendmail) {
            &check_commands({'mail' => ''}, {});
        }
    }

    if ($fw_del_ip) {
        die "[*] $fw_del_ip does not look like an IP address"
            unless $fw_del_ip =~ /$ip_re/;
    }

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

        ### --fw-list, lists rules in FWKNOP iptables chains
        exit &ipt_list() if $fw_list;

        ### --fw-flush, flush rules in FWKNOP iptables chains
        exit &ipt_flush() if $fw_flush;

        ### --fw-del-ip <IP>
        exit &ipt_del_ip() if $fw_del_ip;

        ### build iptables config from IPT_INPUT_ACCESS, IPT_OUTPUT_ACCESS,
        ### IPT_FORWARD_ACCESS, IPT_DNAT_ACCESS, and IPT_MASQ_ACCESS keywords
        &build_ipt_config();

    } elsif ($config{'FIREWALL_TYPE'} eq 'ipfw') {

        ### --fw-list, lists all ipfw rules
        exit &ipfw_list() if $fw_list;

        ### --fw-flush (not actually supported yet for ipfw firewalls)
        exit &ipfw_flush() if $fw_flush;

        ### --fw-del-ip <IP>
        exit &ipfw_del_ip() if $fw_del_ip;

    } elsif ($config{'FIREWALL_TYPE'} eq 'external_cmd') {

        if ($fw_list) {
            if (-x $cmds{'iptables'}) {
                exit &ipt_list();
            } elsif (-x $cmds{'ipfw'}) {
                exit &ipfw_list();
            }
        }
    }

    %pid_files = (
        'knopwatchd'  => $config{'KNOPWATCHD_PID_FILE'},
        'knoptm'      => $config{'KNOPTM_PID_FILE'},
        'knopmd'      => $config{'KNOPMD_PID_FILE'},
        'fwknop_serv' => $config{'TCPSERV_PID_FILE'},
        'fwknopd'     => $config{'FWKNOP_PID_FILE'}
    );

    ### --Kill
    exit &stop_fwknop() if $stop_daemons;

    ### --Restart
    exit &restart() if $restart;

    ### --Status
    exit &status() if $status;

    ### --spa-dump-packets (dumps decrypted SPA packets out on stdout)
    exit &spa_dump_packets() if $spa_dump_packets;

    ### make sure there is not another fwknopd process already running.
    &uniquepid() unless $os_fprint_only;

    &logr('[+]', "starting fwknopd v$version (file revision: $rev_num)",
        $NO_MAIL);

    print STDERR "[+] Start time: [" .
        localtime() . "]\n" if $debug;

    if ($config{'FIREWALL_TYPE'} eq 'iptables') {
        ### always remove any existing rules
        &ipt_flush() if $config{'FLUSH_IPT_AT_INIT'} eq 'Y';

        ### alert the user if there are no state tracking rules loaded
        ### in the iptables policy to keep connections open
        &ipt_check_stateful_rule();

    } elsif ($config{'FIREWALL_TYPE'} eq 'ipfw') {

        ### alert the user if there are no state tracking rules loaded
        ### in the ipfw policy to keep connections open
        &ipfw_check_stateful_rule();
        &ipfw_disable_set($config{'IPFW_SET_NUM'}) if $ipfw_is_dynamic;
    }

    if ($packet_limit) {
        die "[*] -C must be greater than zero" unless $packet_limit > 0;
    }

    if ($config{'AUTH_MODE'} eq 'KNOCK') {  ### legacy port knocking mode

        if ($config{'ENABLE_SYSLOG_FILE'} eq 'Y') {
            $fw_data_file = $config{'IPT_SYSLOG_FILE'};
            die "[*] IPT_SYSLOG_FILE $config{'IPT_SYSLOG_FILE'} does not exist"
                unless -e $fw_data_file;
        } else {
            $fw_data_file = $config{'FW_DATA_FILE'};
            unless (-e "$fw_data_file") {
                print "[+] Creating $fw_data_file file\n";
                open F, "> $fw_data_file" or die "[*] Could not open ",
                    "$fw_data_file: $!";
                close F;
                chmod 0600, $fw_data_file or die "[*] Could ",
                    "not chmod(0600, $fw_data_file): $!";
                chown 0, 0, $fw_data_file or die "[*] Could not ",
                    "chown 0,0,$fw_data_file: $!";
            }
            unless (-e $config{'KNOPMD_FIFO'}) {
                system "$cmds{'mknod'} -m 600 $config{'KNOPMD_FIFO'} p";
            }
        }

        ### import passive OS fingerprints (based on p0f)
        &import_p0f_sigs();
    }

    if ($os_fprint_only) {
        if ($os_ipt_log) {
            $fw_data_file = $os_ipt_log;
        }
        print "[+] Parsing iptables log: $fw_data_file\n";
    } else {

        ### import access directives
        &import_access();

        if ($dump_config) {
            &dump_config();
            print STDERR "\n", localtime() . " [+] Dumping access ",
                "config: $config{'ACCESS_CONF'}\n\n";
            for (my $i=0; $i<=$#access; $i++) {
                &dump_access($access[$i], $i);
            }
            exit 0;
        }

        unless ($debug) {
            my $pid = fork();
            exit 0 if $pid;
            die "[*] $0: Couldn't fork: $!" unless defined $pid;
            POSIX::setsid() or die "[*] $0: Can't start a new session: $!";
        }

        for my $dir qw(/var/lib /var/run) {
            next if -d $dir;
            mkdir $dir, 0755 or die "[*] Could not mkdir $dir: $!";
        }

        for my $dir qw(
            FWKNOP_DIR
            FWKNOP_ERR_DIR
            FWKNOP_RUN_DIR
            FWKNOP_LIB_DIR
        ) {
            next if -d $config{$dir};
            mkdir $config{$dir}, 0500 or
                die "[*] Could not mkdir $config{$dir}: $!";
        }

        ### write our pid out to disk
        &writepid();

        ### write a copy of our command line out to disk
        &writecmdline(\@args_cp) unless $debug;

        ### start knopmd and knopwatchd here (if they are already running
        ### it is ok, another instance will not be started).
        if ($config{'AUTH_MODE'} ne 'KNOCK') {

            ### make sure knopmd is not running
            &stop_daemon('knopmd', $LOG_QUIET);

            ### see if we need to start the fwknop_serv TCP server.  The only
            ### real application of this is when running SPA packets over the
            ### Tor network.
            if ($config{'ENABLE_TCP_SERVER'} eq 'Y'
                    or $config{'ENABLE_UDP_SERVER'} eq 'Y') {
                if ($config{'AUTH_MODE'} =~ /PCAP/
                        and $config{'PCAP_FILTER'} ne 'NONE'
                        and $config{'PCAP_FILTER'} !~ /^\s*port\s+62201/
                        and $config{'PCAP_FILTER'} !~ /tcp\s+port\s+62201/) {
                    &logr('[-]', "ENABLE_TCP_SERVER is enabled, but " .
                        "PCAP_FILTER may not accept TCP/62201", $SEND_MAIL);
                }

                ### restart fwknop_serv
                &stop_daemon('fwknop_serv', $LOG_QUIET);

                my $fws_cmd = "$cmds{'fwknop_serv'} -c $config_file";
                $fws_cmd .= " --locale $cmdline_locale" if $cmdline_locale;
                $fws_cmd .= " --Override-config $override_config_str"
                    if $override_config_str;
                $fws_cmd .= " --Debug-include-pidname"
                    if $fwkserv_include_pidname;

                print STDERR localtime() . " [+] Executing: $fws_cmd\n"
                    if $debug;
                open FWSERV, "| $fws_cmd" or die "[*] Could not ",
                    "execute $fws_cmd";
                close FWSERV;
            }
        } else {
            unless ($config{'ENABLE_SYSLOG_FILE'} eq 'Y') {
                print STDERR " [+] Executing: $cmds{'knopmd'}\n" if $debug;
                open KNOPMD, "| $cmds{'knopmd'}"
                    or die "[*] Could not execute $cmds{'knopmd'}";
                close KNOPMD;
            }
        }

        ### knoptm removes firewall rules if we are running in PCAP mode
        ### so tell it not to exit
        my $cmd = $cmds{'knoptm'};
        if ($debug and $config{'ENABLE_VOLUNTARY_EXITS'} eq 'Y') {
            $cmd .= ' --no-voluntary-exits';
        }
        ### append interface for running interface existence checks
        ### (ppp interfaces might go away and reappear)
        $cmd .= " -i $config{'PCAP_INTF'}";
        $cmd .= " --fw-type $config{'FIREWALL_TYPE'}" if $fw_type;
        $cmd .= " --locale $cmdline_locale" if $cmdline_locale;
        $cmd .= " --Override-config $override_config_str"
            if $override_config_str;
        $cmd .= " --Debug-include-pidname" if $knoptm_include_pidname;

        print STDERR localtime() . " [+] Executing: $cmd -c $config_file\n"
            if $debug;

        ### always start knoptm
        open KNOPTM, "| $cmd -c $config_file" or die "[*] Could not execute ",
            "$cmd -c $config_file";
        close KNOPTM;

        ### always start knopwatchd except for debugging mode
        unless ($debug) {
            $cmd  = $cmds{'knopwatchd'};
            $cmd .= " -c $config_file";
            $cmd .= " -O $override_config_str"
                if $override_config_str;
            open KNOPWATCHD, "| $cmd" or die "[*] Could not ",
                "execute $cmds{'knopwatchd'}";
            close KNOPWATCHD;
        }
    }

    if ($config{'AUTH_MODE'} ne 'KNOCK') {
        &import_digests() if $config{'ENABLE_DIGEST_PERSISTENCE'} eq 'Y';
    }

    if ($config{'AUTH_MODE'} eq 'SOCKET') {
        unlink $config{'FWKNOP_SERV_SOCK'}
            if -e $config{'FWKNOP_SERV_SOCK'};
    }

    ### Install signal handlers for debugging and for reaping zombie
    ### processes.
    $SIG{'__WARN__'} = \&warn_handler;
    $SIG{'__DIE__'}  = \&die_handler;
    $SIG{'CHLD'}     = \&REAPER;

    if ($debug) {
        print STDERR localtime() .
            " [+] Set SIGCHLD handler to: " . \&REAPER . "\n";
        print STDERR localtime() .
            " [+] Set __WARN__ handler to: " . \&warn_handler . "\n";
        print STDERR localtime() .
            " [+] Set __DIE__ handler to: " . \&die_handler . "\n";
    }

    &handle_locale();

    return;
}

sub handle_locale() {
    $config{'LOCALE'} = $cmdline_locale if $cmdline_locale;

    if ($config{'LOCALE'} ne 'NONE' and not $no_locale) {
        ### set LC_ALL env variable
        $ENV{'LC_ALL'} = $config{'LOCALE'};
    }
    return;
}

sub ipfw_list() {

    print "[+] Listing fwknop 'allow' or 'DISABLED' rules ",
        "from ipfw policy...\n\n";
    my $ipfw_policy_ar = &ipfw_dump_policy();
    for (@$ipfw_policy_ar) {
        print if /\spass\s/ or /\sallow\s/ or /DISABLED/;
    }
    return 0;
}

sub ipfw_dump_policy() {
    my $cmd = "$cmds{'ipfw'} -dS list";
    my @ipfw_policy = ();
    open IPFW, "$cmd |" or die "[*] Could not execute $cmd: $!";
    @ipfw_policy = <IPFW>;
    close IPFW;
    return \@ipfw_policy;
}

sub ipfw_policy_print() {
    my $print_dst = shift;
    my $ipfw_policy_ar = &ipfw_dump_policy();
    if ($print_dst == $STDOUT) {
        print for @$ipfw_policy_ar;
    } elsif ($print_dst == $STDERR) {
        print STDERR for @$ipfw_policy_ar;
    } else {
        die "[*] Invalid print dst: $print_dst";
    }
    return;
}

sub ipt_list() {

    if ($verbose) {
        print "[+] Dumping iptables policy...\n";
        for my $table qw/filter nat/ {
            system "$cmds{'iptables'} -t $table -v -n -L";
        }
        return 0;
    } else {

        ### build iptables config from IPT_INPUT_ACCESS, IPT_OUTPUT_ACCESS,
        ### IPT_FORWARD_ACCESS, IPT_DNAT_ACCESS, IPT_MASQUERADE_ACCESS keywords
        &build_ipt_config();

        &dump_ipt_policy();
    }
    return 0;
}

sub dump_ipt_policy() {

    print STDERR localtime() . " [+] dump_ipt_policy()\n" if $debug;

    my $ipt = &get_iptables_chainmgr_obj($ZERO_SLEEP);
    print "[+] Listing rules in fwknop chains...\n";
    for my $hr (@ipt_config) {
        my $table    = $hr->{'table'};
        my $to_chain = $hr->{'to_chain'};

        if ($ipt->chain_exists($table, $to_chain)) {
            my ($rv, $out_ar, $err_ar) =
                $ipt->run_ipt_cmd("$cmds{'iptables'} -t " .
                    "$table -v -n -L $to_chain");

            if ($rv and $out_ar) {
                print for @$out_ar;
                print "\n";
            }
        } else {
            print "[-] Table: $table, chain: $to_chain, does not exist\n";
        }
    }
    undef $ipt;
    return 0;
}

sub ipt_del_ip() {

    &build_ipt_config();

    my $ipt = &get_iptables_chainmgr_obj($ZERO_SLEEP);

    print "[+] Deleting ACCEPT src $fw_del_ip rules from fwknop chains...\n";

    if (@ipt_config) {
        for my $hr (@ipt_config) {
            next unless $hr;
            my $table    = $hr->{'table'};
            my $to_chain = $hr->{'to_chain'};

            if ($ipt->chain_exists($table, $to_chain)) {
                for (;;) {
                    my ($rulenum, $chain_rules) = $ipt->find_ip_rule(
                            $fw_del_ip, '0.0.0.0/0', $table, $to_chain,
                            'ACCEPT', {});
                    last unless $rulenum;
                    print "    $cmds{'iptables'} -D $to_chain $rulenum\n";
                    $ipt->run_ipt_cmd("$cmds{'iptables'} -D $to_chain $rulenum");
                }
            }
        }
    }
    undef $ipt;
    return 0;
}

sub ipfw_flush() {
    print "[+] Flushing ipfw policies not supported yet.\n";
    return 0;
}

sub ipt_flush() {

    &build_ipt_config();

    my $ipt = &get_iptables_chainmgr_obj($ZERO_SLEEP);

    if ($fw_flush) {
        print "[+] Flushing iptables fwknop chains...\n";
    } else {
        &logr('[+]', 'flushing existing iptables fwknop chains',
            $NO_MAIL);
    }
    if (@ipt_config) {
        for my $hr (@ipt_config) {
            next unless $hr;
            my $table      = $hr->{'table'};
            my $from_chain = $hr->{'from_chain'};
            my $to_chain   = $hr->{'to_chain'};

            if ($ipt->chain_exists($table, $to_chain)) {
                my ($rv, $out_ar, $err_ar)
                        = $ipt->flush_chain($table, $to_chain);
                if ($rv) {
                    $ipt->delete_chain($table, $from_chain, $to_chain)
                        if $ipt_del_chains;
                    print "[+] Flushed: $to_chain\n" if $fw_flush;
                } else {
                    if ($fw_flush) {
                        print "[-] Could not flush: $to_chain\n";
                        print for @$err_ar;
                    }
                }
            } else {
                print "[-] Chain: $to_chain does not exist.\n" if $fw_flush;
            }
        }
    } else {
        print "[-] No valid IPT_AUTO_CHAIN keywords.\n" if $fw_flush;
    }
    undef $ipt;
    return 0;
}

sub stop_daemon() {
    my ($name, $log_flag) = @_;

    print STDERR localtime() . " [+] Stopping $name daemon...\n"
        if $debug or $verbose;

    unless (-e $pid_files{$name}) {
        if ($log_flag == $LOG_VERBOSE) {
            my $print = 1;
            if ($config{'AUTH_MODE'} =~ /PCAP/ and $name eq 'knopmd') {
                $print = 0;
            }
            if ($name eq 'fwknop_serv'
                    and $config{'ENABLE_TCP_SERVER'} eq 'N'
                    and $config{'ENABLE_UDP_SERVER'} eq 'N') {
                $print = 0;
            }
            print "[-] pid file $pid_files{$name} does not " .
                "exist for $name\n" if $print;
        }
        return;
    }

    open PID, "< $pid_files{$name}"
        or die "[*] Could not open $pid_files{$name}: $!";
    my $pid = <PID>;
    close PID;
    chomp $pid;

    if (kill 0, $pid) {
        if ($log_flag == $LOG_VERBOSE) {
            print "[+] $name is running (pid: $pid), stopping daemon\n";
        }
        kill 9, $pid unless kill 15, $pid;
        sleep 1;
        if (kill 0, $pid) {
            die "[*] Could not kill $name process (pid: $pid)";
        }
    } else {
        if ($log_flag == $LOG_VERBOSE) {
            my $print = 1;
            if ($config{'AUTH_MODE'} =~ /PCAP/ and $name eq 'knopmd') {
                $print = 0;
            }
            print "[-] $name is not running\n" if $print;
        }
    }

    unlink $pid_files{$name};

    return;
}

### write a message to syslog (leaves off $prefix, which assigns a
### "type" to the message, when writing syslog; might add it later
sub logr() {
    my ($prefix, $msg, $send_email) = @_;

    if ($debug) {
        print STDERR localtime() . " $prefix $msg\n";
        return;
    }

    ### see if we need to send an email
    if ($config{'ALERTING_METHODS'} =~ /verbose/ or
            ($send_email == $SEND_MAIL
            and $config{'ALERTING_METHODS'} !~ /noe?mail/i)) {

        &sendmail("$prefix $config{'HOSTNAME'} fwknopd: $msg");
    }

    return if $config{'ALERTING_METHODS'} =~ /no.?syslog/i;

    ### this is an ugly hack to avoid the 'can't use string as subroutine'
    ### error because of 'use strict'
    if ($config{'SYSLOG_FACILITY'} =~ /LOG_LOCAL7/i) {
        openlog($config{'SYSLOG_IDENTITY'}, &LOG_DAEMON(), &LOG_LOCAL7());
    } elsif ($config{'SYSLOG_FACILITY'} =~ /LOG_LOCAL6/i) {
        openlog($config{'SYSLOG_IDENTITY'}, &LOG_DAEMON(), &LOG_LOCAL6());
    } elsif ($config{'SYSLOG_FACILITY'} =~ /LOG_LOCAL5/i) {
        openlog($config{'SYSLOG_IDENTITY'}, &LOG_DAEMON(), &LOG_LOCAL5());
    } elsif ($config{'SYSLOG_FACILITY'} =~ /LOG_LOCAL4/i) {
        openlog($config{'SYSLOG_IDENTITY'}, &LOG_DAEMON(), &LOG_LOCAL4());
    } elsif ($config{'SYSLOG_FACILITY'} =~ /LOG_LOCAL3/i) {
        openlog($config{'SYSLOG_IDENTITY'}, &LOG_DAEMON(), &LOG_LOCAL3());
    } elsif ($config{'SYSLOG_FACILITY'} =~ /LOG_LOCAL2/i) {
        openlog($config{'SYSLOG_IDENTITY'}, &LOG_DAEMON(), &LOG_LOCAL2());
    } elsif ($config{'SYSLOG_FACILITY'} =~ /LOG_LOCAL1/i) {
        openlog($config{'SYSLOG_IDENTITY'}, &LOG_DAEMON(), &LOG_LOCAL1());
    } elsif ($config{'SYSLOG_FACILITY'} =~ /LOG_LOCAL0/i) {
        openlog($config{'SYSLOG_IDENTITY'}, &LOG_DAEMON(), &LOG_LOCAL0());
    }

    if ($config{'SYSLOG_PRIORITY'} =~ /LOG_INFO/i) {
        syslog(&LOG_INFO(), $msg);
    } elsif ($config{'SYSLOG_PRIORITY'} =~ /LOG_DEBUG/i) {
        syslog(&LOG_DEBUG(), $msg);
    } elsif ($config{'SYSLOG_PRIORITY'} =~ /LOG_NOTICE/i) {
        syslog(&LOG_NOTICE(), $msg);
    } elsif ($config{'SYSLOG_PRIORITY'} =~ /LOG_WARNING/i) {
        syslog(&LOG_WARNING(), $msg);
    } elsif ($config{'SYSLOG_PRIORITY'} =~ /LOG_ERR/i) {
        syslog(&LOG_ERR(), $msg);
    } elsif ($config{'SYSLOG_PRIORITY'} =~ /LOG_CRIT/i) {
        syslog(&LOG_CRIT(), $msg);
    } elsif ($config{'SYSLOG_PRIORITY'} =~ /LOG_ALERT/i) {
        syslog(&LOG_ALERT(), $msg);
    } elsif ($config{'SYSLOG_PRIORITY'} =~ /LOG_EMERG/i) {
        syslog(&LOG_EMERG(), $msg);
    }

    closelog();

    return;
}

sub psyslog_errs() {
    my $aref = shift;

    if ($debug) {
        for my $msg (@$aref) {
            print STDERR localtime() . " $msg\n";
        }
    }

    return if $config{'ALERTING_METHODS'} =~ /no.?syslog/i;

    ### this is an ugly hack to avoid the 'can't use string as subroutine'
    ### error because of 'use strict'
    if ($config{'SYSLOG_FACILITY'} =~ /LOG_LOCAL7/i) {
        openlog($config{'SYSLOG_IDENTITY'},&LOG_DAEMON(), &LOG_LOCAL7());
    } elsif ($config{'SYSLOG_FACILITY'} =~ /LOG_LOCAL6/i) {
        openlog($config{'SYSLOG_IDENTITY'}, &LOG_DAEMON(), &LOG_LOCAL6());
    } elsif ($config{'SYSLOG_FACILITY'} =~ /LOG_LOCAL5/i) {
        openlog($config{'SYSLOG_IDENTITY'}, &LOG_DAEMON(), &LOG_LOCAL5());
    } elsif ($config{'SYSLOG_FACILITY'} =~ /LOG_LOCAL4/i) {
        openlog($config{'SYSLOG_IDENTITY'}, &LOG_DAEMON(), &LOG_LOCAL4());
    } elsif ($config{'SYSLOG_FACILITY'} =~ /LOG_LOCAL3/i) {
        openlog($config{'SYSLOG_IDENTITY'}, &LOG_DAEMON(), &LOG_LOCAL3());
    } elsif ($config{'SYSLOG_FACILITY'} =~ /LOG_LOCAL2/i) {
        openlog($config{'SYSLOG_IDENTITY'}, &LOG_DAEMON(), &LOG_LOCAL2());
    } elsif ($config{'SYSLOG_FACILITY'} =~ /LOG_LOCAL1/i) {
        openlog($config{'SYSLOG_IDENTITY'}, &LOG_DAEMON(), &LOG_LOCAL1());
    } elsif ($config{'SYSLOG_FACILITY'} =~ /LOG_LOCAL0/i) {
        openlog($config{'SYSLOG_IDENTITY'}, &LOG_DAEMON(), &LOG_LOCAL0());
    }

    if ($config{'SYSLOG_PRIORITY'} =~ /LOG_INFO/i) {
        for (my $i=0; $i<5 && $i<=$#$aref; $i++) {
            syslog(&LOG_INFO(), $aref->[$i]);
        }
    } elsif ($config{'SYSLOG_PRIORITY'} =~ /LOG_DEBUG/i) {
        for (my $i=0; $i<5 && $i<=$#$aref; $i++) {
            syslog(&LOG_DEBUG(), $aref->[$i]);
        }
    } elsif ($config{'SYSLOG_PRIORITY'} =~ /LOG_NOTICE/i) {
        for (my $i=0; $i<5 && $i<=$#$aref; $i++) {
            syslog(&LOG_NOTICE(), $aref->[$i]);
        }
    } elsif ($config{'SYSLOG_PRIORITY'} =~ /LOG_WARNING/i) {
        for (my $i=0; $i<5 && $i<=$#$aref; $i++) {
            syslog(&LOG_WARNING(), $aref->[$i]);
        }
    } elsif ($config{'SYSLOG_PRIORITY'} =~ /LOG_ERR/i) {
        for (my $i=0; $i<5 && $i<=$#$aref; $i++) {
            syslog(&LOG_ERR(), $aref->[$i]);
        }
    } elsif ($config{'SYSLOG_PRIORITY'} =~ /LOG_CRIT/i) {
        for (my $i=0; $i<5 && $i<=$#$aref; $i++) {
            syslog(&LOG_CRIT(), $aref->[$i]);
        }
    } elsif ($config{'SYSLOG_PRIORITY'} =~ /LOG_ALERT/i) {
        for (my $i=0; $i<5 && $i<=$#$aref; $i++) {
            syslog(&LOG_ALERT(), $aref->[$i]);
        }
    } elsif ($config{'SYSLOG_PRIORITY'} =~ /LOG_EMERG/i) {
        for (my $i=0; $i<5 && $i<=$#$aref; $i++) {
            syslog(&LOG_EMERG(), $aref->[$i]);
        }
    }

    closelog();
    return;
}

sub set_null_vars() {
    for my $var (keys %config) {
        $config{$var} = '' if $config{$var} eq '__NONE__';
    }
    return;
}

sub expand_vars() {
    my $exclude_hr = shift;

    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) {
                next if defined $exclude_hr->{$var};
                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 build_ipt_config() {

    return if $build_ipt_config;
    $build_ipt_config = 1;

    print STDERR localtime() . " [+] Building iptables config info.\n"
        if $debug;

    ### for the FWKNOP_INPUT chain
    if (&parse_ipt_var(\%ipt_input, $config{'IPT_INPUT_ACCESS'})) {
        push @ipt_config, \%ipt_input;
    }

    if ($config{'ENABLE_IPT_OUTPUT'} eq 'Y' or $fw_list or $fw_flush) {
        ### for the optional FWKNOP_OUTPUT chain
        if (&parse_ipt_var(\%ipt_output, $config{'IPT_OUTPUT_ACCESS'})) {
            push @ipt_config, \%ipt_output;
        }
    }

    if ($config{'ENABLE_IPT_FORWARDING'} eq 'Y'
            or $config{'ENABLE_IPT_LOCAL_NAT'} eq 'Y'
            or $fw_list or $fw_flush) {

        ### for the FWKNOP_FORWARD chain
        if (&parse_ipt_var(\%ipt_forward, $config{'IPT_FORWARD_ACCESS'})) {
            push @ipt_config, \%ipt_forward;
        }
        ### for the FWKNOP_PREROUTING chain
        if (&parse_ipt_var(\%ipt_prerouting, $config{'IPT_DNAT_ACCESS'})) {
            push @ipt_config, \%ipt_prerouting;
        }
        ### for the FWKNOP_POSTROUTING chain
        if ($config{'ENABLE_IPT_SNAT'} eq 'Y') {
            if ($config{'SNAT_TRANSLATE_IP'} =~ /^$ip_re$/) {
                if (&parse_ipt_var(\%ipt_postrouting,
                        $config{'IPT_SNAT_ACCESS'})) {
                    push @ipt_config, \%ipt_postrouting;
                }
            } else {
                if (&parse_ipt_var(\%ipt_postrouting,
                        $config{'IPT_MASQUERADE_ACCESS'})) {
                    push @ipt_config, \%ipt_postrouting;
                }
            }
        }
    }
    return;
}

sub parse_ipt_var() {
    my ($hr, $ipt_line) = @_;

    unless ($imported_iptables_modules) {

        require IPTables::ChainMgr;

        if ($debug) {
            print STDERR "[+] IPTables::ChainMgr::VERSION ",
                "$IPTables::ChainMgr::VERSION\n";
        }

        $imported_iptables_modules = 1;
    }

    my $ipt = &get_iptables_chainmgr_obj($ZERO_SLEEP);

    my @block = split /\s*,\s*/, $ipt_line;
    if ($#block == 4 or $#block == 6) {
        if ($#block == 4) {
            ### ACCEPT, src, filter, INPUT, FWKNOP_INPUT;
            $hr->{'target'}     = $block[0];
            $hr->{'direction'}  = $block[1];
            $hr->{'table'}      = $block[2];
            $hr->{'from_chain'} = $block[3];
            $hr->{'to_chain'}   = $block[4];
            $hr->{'jump_rule_position'} = 1;
            $hr->{'auto_rule_position'} = 1;
            ### this is the old format; generate a warning
            my $msg = "the IPT_AUTO_CHAIN variable in fwknop.conf " .
                "needs to be updated to set the jump rule position and " .
                "the auto rule position; defaulting both to 1.";
                &logr('[-]', $msg, $NO_MAIL);
                print STDERR localtime() . " [-] build_ipt_config(): $msg\n"
                    if $debug;
        } else {
            ### ACCEPT, src, filter, INPUT, 1, FWKNOP_INPUT, 1;
            $hr->{'target'} = $block[0];
            $hr->{'direction'}  = $block[1];
            $hr->{'table'}      = $block[2];
            $hr->{'from_chain'} = $block[3];
            $hr->{'jump_rule_position'} = $block[4];
            $hr->{'to_chain'} = $block[5];
            $hr->{'auto_rule_position'} = $block[6];
        }
        unless ($hr->{'direction'} eq 'src' or
                    $hr->{'direction'} eq 'dst' or
                    $hr->{'direction'} eq 'both') {
            my $msg = "invalid direction $hr->{'direction'} " .
                "in IPT_AUTO_CHAIN keyword";
            &logr('[-]', $msg, $NO_MAIL);
            die "[-] build_ipt_config(): $msg\n";
        }
        if ($hr->{'from_chain'} eq $hr->{'to_chain'}) {
            my $msg = "cannot have identical from_chain and to_chain " .
                "in IPT_AUTO_CHAIN keyword";
            &logr('[-]', $msg, $NO_MAIL);
            die "[-] build_ipt_config(): $msg\n";
        }
        my ($rv, $out_ar, $err_ar)
            = $ipt->chain_exists($hr->{'table'}, $hr->{'from_chain'});

        unless ($rv) {
            my $msg = "invalid IPT_AUTO_CHAIN keyword, " .
                "$hr->{'table'} $hr->{'from_chain'} chain does not exist.";
            &logr('[-]', $msg, $NO_MAIL);
            if ($hr->{'from_chain'} eq 'FORWARD'
                    or $hr->{'from_chain'} eq 'PREROUTING') {
                ### usually fwknop is used against the INPUT chain, so
                ### don't die in this case
                undef $ipt;
                return 0;
            }
            die "[-] build_ipt_config(): $msg\n";
        }
    } else {
        my $msg = "invalid IPT_AUTO_CHAIN variable: $ipt_line";
        &logr('[-]', $msg, $NO_MAIL);
        die "[-] build_ipt_config(): $msg\n";
    }
    undef $ipt;
    return 1;
}

sub diskwrite_digest() {
    my ($digest, $src_ip) = @_;

    print STDERR localtime() . " [+] Calculated digest: $digest for SPA ",
        "packet from: $src_ip\n" if $debug;
    open F, ">> $config{'DIGEST_FILE'}" or die "[*] Could not open ",
        "$config{'DIGEST_FILE'}: $!";
    if ($config{'ENABLE_DIGEST_INCLUDE_SRC'} eq 'Y') {
        print F "$src_ip $digest [" . localtime() . "]\n";
    } else {
        print F $digest, "\n";
    }
    close F;
    return;
}

sub import_perl_modules() {

    my $mod_paths_ar = &get_mod_paths();

    if ($#$mod_paths_ar > -1) {  ### /usr/lib/fwknop/ exists
        push @$mod_paths_ar, @INC;
        splice @INC, 0, $#$mod_paths_ar+1, @$mod_paths_ar;
    }

    if ($debug) {
        print STDERR "[+] import_perl_modules(): The \@INC array:\n";
        print STDERR "$_\n" for @INC;
    }

    ### see if the FKO module is installed
    unless ($skip_fko_module or $config{'ENABLE_FKO_MODULE'} eq 'N') {
        eval { require FKO };
        unless ($@) {
            $use_fko_module = 1;
            if ($debug or $test_fko_exists) {
                print STDERR localtime() . " [+] Using FKO module.\n";
            }
            exit 0 if $test_fko_exists;
        }
    }

    unless ($config{'ALERTING_METHODS'} =~ /no.?syslog/i) {
        require Unix::Syslog;
        Unix::Syslog->import(qw(:subs :macros));

        if ($debug) {
            print STDERR "[+] Unix::Syslog::VERSION $Unix::Syslog::VERSION\n";
        }
    }

    require Net::IPv4Addr;
    Net::IPv4Addr->import(qw/ipv4_in_network/);

    if ($debug) {
        print STDERR "[+] Net::IPv4Addr::VERSION $Net::IPv4Addr::VERSION\n";
    }

    return;
}

sub get_mod_paths() {

    my @paths = ();

    $config{'FWKNOP_MOD_DIR'} = $lib_dir if $lib_dir;

    unless (-d $config{'FWKNOP_MOD_DIR'}) {
        my $dir_tmp = $config{'FWKNOP_MOD_DIR'};
        $dir_tmp =~ s|lib/|lib64/|;
        if (-d $dir_tmp) {
            $config{'FWKNOP_MOD_DIR'} = $dir_tmp;
        } else {
            return [];
        }
    }

    opendir D, $config{'FWKNOP_MOD_DIR'}
        or die "[*] Could not open $config{'FWKNOP_MOD_DIR'}: $!";
    my @dirs = readdir D;
    closedir D;

    push @paths, $config{'FWKNOP_MOD_DIR'};

    for my $dir (@dirs) {
        ### get directories like "/usr/lib/fwknop/x86_64-linux"
        next unless -d "$config{'FWKNOP_MOD_DIR'}/$dir";
        push @paths, "$config{'FWKNOP_MOD_DIR'}/$dir"
            if $dir =~ m|linux| or $dir =~ m|thread|
                or (-d "$config{'FWKNOP_MOD_DIR'}/$dir/auto");
    }
    return \@paths;
}

sub import_digests() {

    %digest_store = ();

    for my $digest_file ($config{'DIGEST_FILE'},
            "$config{'FWKNOP_DIR'}/md5sums") {
        next unless -e $digest_file;

        open F, "< $digest_file" or die "[*] Could not open ",
            "$digest_file: $!";
        while (<F>) {
            if (/^\s*($ip_re)\s+(\S+)\s+\[.{15,25}\s\d{4}\]/) {
                ### 127.0.0.1 36wD3+KXHLuqqp18D0qODA [Wed Nov 28 09:13:31 2007]
                ### Date tracking was added in 1.8.4
                $digest_store{$2} = $1;
            } elsif (/^\s*($ip_re)\s+(\S+)$/) {  ### 127.0.0.1 36wD3+KXHLuqqp18D0qODA
                ### version 1.8.3 includes the source IP address for each
                ### SPA packet (unless ENABLE_DIGEST_INCLUDE_SRC is disabled)
                $digest_store{$2} = $1;
            } elsif (/^\s*(\S+)$/) {
                $digest_store{$1} = '';
            }
        }
        close F;
    }

    if ($debug) {
        print STDERR localtime() . " [+] digest_store hash: \n",
            Dumper(\%digest_store);
    }

    &logr('[+]', "imported previous tracking digests from disk " .
        "cache: $config{'DIGEST_FILE'}", $NO_MAIL);
    return;
}

sub hex_dump() {
    my $data = shift;

    my @chars = split //, $data;
    my $ctr = 0;
    my $ascii_str = '';
    for my $char (@chars) {
        if ($ctr % 16 == 0) {
            print STDERR " $ascii_str\n" if $ascii_str;
            printf STDERR "        0x%.4x:  ", $ctr;
            $ascii_str = '';
        }
        printf STDERR "%.2x", ord($char);

        if ((($ctr+1) % 2 == 0) and ($ctr % 16 != 0)) {
            print STDERR ' ';
        }

        if ($char =~ /[^\x20-\x7e]/) {
            $ascii_str .= '.';
        } else {
            $ascii_str .= $char;
        }
        $ctr++;
    }
    if ($ascii_str) {
        my $remainder = 1;
        if ($ctr % 16 != 0) {
            $remainder = 16 - $ctr % 16;
            if ($remainder % 2 == 0) {
                $remainder = 2*$remainder + int($remainder/2) + 1;
            } else {
                $remainder = 2*$remainder + int($remainder/2) + 2;
            }
        }
        print STDERR ' 'x$remainder, $ascii_str;
    }
    print STDERR "\n";
    return;
}

sub validate_config() {

    die qq([*] Invalid EMAIL_ADDRESSES value: "$config{'EMAIL_ADDRESSES'}")
        unless $config{'EMAIL_ADDRESSES'} =~ /\S+\@\S+/;

    ### translate commas into spaces
    $config{'EMAIL_ADDRESSES'} =~ s/\s*\,\s/ /g;

    unless ($config{'FIREWALL_TYPE'} eq 'iptables'
            or $config{'FIREWALL_TYPE'} eq 'ipfw'
            or $config{'FIREWALL_TYPE'} eq 'external_cmd') {
        die "[*] FIREWALL_TYPE must be either 'iptables', 'ipfw', ",
            "or 'external_cmd'";
    }

    unless ($config{'AUTH_MODE'} eq 'KNOCK'
            or $config{'AUTH_MODE'} eq 'FILE_PCAP'
            or $config{'AUTH_MODE'} eq 'ULOG_PCAP'
            or $config{'AUTH_MODE'} eq 'PCAP'
            or $config{'AUTH_MODE'} eq 'SOCKET') {
        die "[*] AUTH_MODE must be either KNOCK, FILE_PCAP, ",
            "ULOG_PCAP, PCAP, or SOCKET";
    }

    if ($config{'AUTH_MODE'} eq 'KNOCK'
            and $config{'FIREWALL_TYPE'} eq 'ipfw') {
        die "[*] Port knocking mode (see AUTH_MODE var) not yet supported on ",
            "ipfw firewalls.";
    }

    unless ($config{'MIN_GNUPG_MSG_SIZE'} =~ /^\d+$/) {
        die "[*] Variable MIN_GNUPG_MSG_SIZE must be a ",
            "positive integer value.";
    }

    &check_ip_forward_value() if $config{'ENABLE_IPT_FORWARDING'} eq 'Y';

    if ($fw_type) {
        die "[*] --fw-type must be 'iptables', 'ipfw', or 'external_cmd'"
            unless $fw_type eq 'iptables'
                or $fw_type eq 'ipfw'
                or $fw_type eq 'external_cmd';
        $config{'FIREWALL_TYPE'} = $fw_type if $fw_type;
    }

    $config{'ACCESS_CONF'} = $access_conf_file if $access_conf_file;

    ### handle BLACKLIST variable
    ($blacklist_ar, $blacklist_exclude_ar)
            = &parse_nets($config{'BLACKLIST'});

    if ($cmdline_knoptm) {
        ### used by the test suite
        $cmds{'knoptm'} = $cmdline_knoptm;
    }
    if ($cmdline_fwknop_serv) {
        ### used by the test suite
        $cmds{'fwknop_serv'} = $cmdline_fwknop_serv;
    }

    my $found_digest = 0;
    my $use_md5    = 0;
    my $use_sha1   = 0;
    my $use_sha256 = 0;
    unless ($use_fko_module) {
        if ($config{'DIGEST_TYPE'} eq 'ALL') {
            $found_digest = 1;
            $use_md5      = 1;
            $use_sha1     = 1;
            $use_sha256   = 1;
        } else {
            if ($config{'DIGEST_TYPE'} =~ /SHA256/) {
                $found_digest = 1;
                $use_sha256 = 1;
            }
            if ($config{'DIGEST_TYPE'} =~ /SHA1/) {
                $found_digest = 1;
                $use_sha1 = 1;
            }
            if ($config{'DIGEST_TYPE'} =~ /MD5/) {
                $found_digest = 1;
                $use_md5 = 1;
            }
        }
        unless ($found_digest) {
            die "[*] DIGEST_TYPE must be one of ALL, SHA256, SHA1, or MD5";
        }
    }

    ### an old fwknop client can send an SPA packet with an
    ### MD5 sum
    if ($use_md5) {
        require Digest::MD5;
        Digest::MD5->import(qw(md5_base64));
        print STDERR "[+] Digest::MD5::VERSION $Digest::MD5::VERSION\n"
            if $debug;
    }

    if ($use_sha1 or $use_sha256) {
        require Digest::SHA;
        if ($use_sha1 and $use_sha256) {
            Digest::SHA->import(qw(sha1_base64 sha256_base64));
        } elsif ($use_sha1) {
            Digest::SHA->import(qw(sha1_base64));
        } elsif ($use_sha256) {
            Digest::SHA->import(qw(sha256_base64));
        }
        print STDERR "[+] Digest::SHA::VERSION $Digest::SHA::VERSION\n"
            if $debug;
    }

    for my $var qw/IPT_EXEC_SLEEP/ {
        die "[*] var $var must contain a digit."
            unless &is_digit($config{$var});
    }

    $PCAP_COOKED_INTF = 1 if $config{'ENABLE_COOKED_INTF'} eq 'Y';

    die "[*] IPFW_SET_NUM must be a digit between 0 and 31"
        unless &is_digit($config{'IPFW_SET_NUM'});

    if ($config{'IPFW_SET_NUM'} < 0 or $config{'IPFW_SET_NUM'} > 31) {
        die "[*] IPFW_SET_NUM must be a digit between 0 and 31";
    }

    die "[*] IPFW_DYNAMIC_INTERVAL must be a digit greater than zero"
        unless &is_digit($config{'IPFW_DYNAMIC_INTERVAL'});

    if ($config{'IPFW_DYNAMIC_INTERVAL'} <= 0) {
        die "[*] IPFW_DYNAMIC_INTERVAL must be a digit greater than zero";
    }

    return;
}

sub check_ip_forward_value() {
    if (-e $config{'PROC_IP_FORWARD_FILE'}) {
        open F, "< $config{'PROC_IP_FORWARD_FILE'}" or die "[*] Could not ",
            "open $config{'PROC_IP_FORWARD_FILE'}: $!";
        my $forward_val = <F>;
        close F;
        chomp $forward_val;
        unless ($forward_val == 1) {
            my $msg = "ENABLE_IPT_FORWARDING is enabled, but IP forwarding " .
                "is disabled in $config{'PROC_IP_FORWARD_FILE'}";
            if ($config{'ENABLE_PROC_IP_FORWARD'} eq 'Y') {
                $msg .= ', enabling';
                open F, "> $config{'PROC_IP_FORWARD_FILE'}" or die "[*] Could not ",
                    "open $config{'PROC_IP_FORWARD_FILE'}: $!";
                print F "1\n";
                close F;
            }
            &logr('[-]', $msg, $SEND_MAIL);
        }
    } else {
        &logr('[-]', "$config{'PROC_IP_FORWARD_FILE'} does not exist, " .
            "but ENABLE_IPT_FORWARDING is enabled", $SEND_MAIL);
        $config{'ENABLE_IPT_FORWARDING'} = 'N';
    }
    return;
}

sub get_iptables_chainmgr_obj() {
    my $ipt_sleep = shift;

    my %ipt_opts = (
        'iptables'  => $cmds{'iptables'},
        'iptout'    => $config{'IPT_OUTPUT_FILE'},
        'ipterr'    => $config{'IPT_ERROR_FILE'},
        'ipt_alarm' => $config{'IPT_CMD_ALARM'},
        'ipt_exec_style'  => $config{'IPT_EXEC_STYLE'},
        'sigchld_handler' => \&REAPER
    );
    $ipt_opts{'debug'}   = 1 if $debug;
    $ipt_opts{'verbose'} = 1 if $verbose and not $test_mode;
    $ipt_opts{'ipt_exec_sleep'} = $ipt_sleep if $ipt_sleep > 0;

    my $ipt = new IPTables::ChainMgr(%ipt_opts)
        or die '[*] Could not acquire IPTables::ChainMgr object.';

    return $ipt;
}

sub die_handler() {
    $die_msg = shift;
    return;
}

### write all warnings to a logfile
sub warn_handler() {
    $warn_msg = shift;
    return;
}

sub write_die_msg() {
    open D, ">> $config{'FWKNOP_ERR_DIR'}/fwknopd.die" or
        die "[*] Could not open $config{'FWKNOP_ERR_DIR'}/fwknopd.die: $!";
    print D scalar localtime(), " fwknopd v$version (file " .
        "rev: $rev_num) pid: $$ $die_msg";
    close D;
    $die_msg = '';
    return;
}

sub write_warn_msg() {
    open D, ">> $config{'FWKNOP_ERR_DIR'}/fwknopd.warn" or
        die "[*] Could not open $config{'FWKNOP_ERR_DIR'}/fwknopd.warn: $!";
    print D scalar localtime(), " fwknopd v$version (file " .
        "rev: $rev_num) pid: $$ $warn_msg";
    close D;
    $warn_msg = '';
    return;
}

sub REAPER {
    my $pid;
    while(($pid = waitpid(-1,WNOHANG)) > 0) {
        # could add code to something with the borked pid here
    }
    $SIG{'CHLD'} = \&REAPER;
    return;
}

sub is_digit() {
    my $str = shift;
    return 1 if $str =~ /^\d+$/;
    return 0;
}

sub base64_equals_padding() {
    my $msg = shift;
    my $padding = '';

    if ($debug) {
        print STDERR localtime() . " [+] base64_equals_padding() msg len: " .
            length($msg) . "\n";
    }

    return 1, $padding if $msg =~ /=$/;

    ### base64 encoding pads encoded data to a multiple of four
    ### with '=' chars, but the fwknop client strips these out
    ### before sending to make it more difficult to detect SPA
    ### traffic
    my $remainder = 4 - length($msg) % 4;

    if ($remainder == 3) {
        ### not possible for valid base64 data - should only have
        ### pad with one or two '=' chars
        print STDERR localtime() . " [-] base64_equals_padding() ",
            "msg would require three '=' chars - invalid base64 data\n"
            if $debug;
        return 0, $padding;
    }

    unless ($remainder == 4) {
        $padding .= '='x$remainder;
    }
    return 1, $padding;
}

sub null_func() {
    return;
}

sub collect_warn_die_msgs() {
    &write_die_msg() if $die_msg;
    &write_warn_msg() if $warn_msg;
    return;
}

sub required_vars() {
    for my $var qw(FW_DATA_FILE SLEEP_INTERVAL FWKNOP_DIR FWKNOP_PID_FILE
            KNOPMD_PID_FILE KNOPWATCHD_PID_FILE FWKNOP_CMDLINE_FILE P0F_FILE
            ACCESS_CONF MAX_HOPS EMAIL_ADDRESSES ALERTING_METHODS
            IPT_INPUT_ACCESS IPT_FORWARD_ACCESS AUTH_MODE PCAP_CMD_TIMEOUT
            ENABLE_PCAP_PROMISC PCAP_FILTER KNOPTM_IP_TIMEOUT_SOCK
            ENABLE_DIGEST_PERSISTENCE DIGEST_FILE FLUSH_IPT_AT_INIT PCAP_INTF
            FWKNOP_ERR_DIR FWKNOP_RUN_DIR FWKNOP_LIB_DIR ENABLE_TCP_SERVER
            TCPSERV_PORT TCPSERV_PID_FILE IPT_OUTPUT_FILE IPT_ERROR_FILE
            ENABLE_SPA_PACKET_AGING MAX_SPA_PACKET_AGE REQUIRE_SOURCE_ADDRESS
            KNOPTM_IPT_OUTPUT_FILE KNOPTM_IPT_ERROR_FILE FIREWALL_TYPE
            IPFW_RULE_NUM SYSLOG_IDENTITY SYSLOG_FACILITY SYSLOG_PRIORITY
            MIN_GNUPG_MSG_SIZE ENABLE_DIGEST_INCLUDE_SRC ENABLE_COOKED_INTF
            ENABLE_VOLUNTARY_EXITS EXIT_INTERVAL KNOPTM_SYSLOG_IDENTITY
            KNOPTM_SYSLOG_FACILITY KNOPTM_SYSLOG_PRIORITY IPFW_SET_NUM
            ENABLE_IPT_FORWARDING ENABLE_IPT_OUTPUT IPT_OUTPUT_ACCESS
            IPT_DNAT_ACCESS IPT_SNAT_ACCESS IPT_MASQUERADE_ACCESS BLACKLIST
            SNAT_TRANSLATE_IP PROC_IP_FORWARD_FILE ENABLE_PROC_IP_FORWARD
            MIN_SPA_PKT_LEN ENABLE_IPT_LOCAL_NAT LOCALE ENABLE_SYSLOG_FILE
            IPT_SYSLOG_FILE FWKNOP_MOD_DIR MAX_SNIFF_BYTES IPT_CMD_ALARM
            IPT_EXEC_STYLE IPT_EXEC_SLEEP IPT_EXEC_TRIES EXTERNAL_CMD_OPEN
            EXTERNAL_CMD_CLOSE EXTERNAL_CMD_ALARM ENABLE_EXT_CMD_PREFIX
            EXT_CMD_PREFIX ENABLE_EXTERNAL_CMDS ENABLE_SPA_OVER_HTTP
            IPFW_DYNAMIC_INTERVAL ENABLE_INTF_CHECKS INTF_CHECKS_INTERVAL
            ENABLE_INTF_RUNNING_CHECK ENABLE_INTF_EXISTS_CHECK
            ENABLE_INTF_BYTES_CHECK ENABLE_FKO_MODULE FWKNOP_SERV_SOCK
            ENABLE_UDP_SERVER UDPSERV_PORT
    ) {
        die "[*] Required variable $var is not defined in $config_file"
            unless defined $config{$var};
    }
    return;
}

sub usage() {
    my $exit_status = shift;
    print <<_HELP_;

fwknopd - Single Packet Authorization daemon

[+] Version: $version (file revision: $rev_num)
    By Michael Rash (mbr\@cipherdyne.org)
    URL: http://www.cipherdyne.org/fwknop/

Usage: fwknopd [options]

Options:
    -c, --config <file>         - Specify path to config file instead of
                                  using the default path:
                                  $config_file
    -a, --access-conf <file>    - Specify path to access.conf file.
    -i, --intf <interface>      - Manually specify interface on which to
                                  sniff.
    -T, --Test-mode             - Run in testing mode for compatibility
                                  with the fwknop test suite (sets the
                                  PCAP_FILTER var to a standard default).
    --fw-list                   - List all active fwknop firewall rules.
    --fw-flush                  - Flush all active fwknop firewall rules.
    --fw-del-chains             - Delete all fwknop iptables chains (must
                                  also use --fw-flush).
    --fw-del-ip <IP>            - Delete <IP> accept or allow rules from the
                                  firewall policy.
    --fw-type <ipfw|iptables>   - Manually specify the firewall type from
                                  the command line (usually only used by
                                  the fwknop test suite).
    -C, --Count <num>           - Exit after processing <num> SPA packets.
    -O, --Override-config <str> - Allow config variables from the normal
                                  $config_file to be superseded with values
                                  from the specified file(s).
    -K, --Kill                  - Kill all running fwknopd processes.
    -R, --Restart               - Restart all running fwknopd processes.
    -S, --Status                - Displays the status of any
                                  currently running fwknopd processes.
    --gpg-agent-info <info>     - Specify the value for the GPG_AGENT_INFO
                                  environment variable as returned by
                                  'gpg-agent --daemon'.
    --gpg-no-options            - In GnuPG mode, instruct GnuPG to not use
                                  the local ~/.gnupg/options file for config
                                  parameters.
    --no-gpg                    - Disable all GnuPG usages even if GPG_*
                                  variables are defined in the access.conf
                                  file.
    -I, --Include-all-config    - Show all configuration data (including
                                  key information) when running in --debug
                                  and --verbose mode.
    --Linux-cooked-intf         - Force fwknopd to assume that the sniffing
                                  interface is a "Linux Cooked" interface.
                                  This is useful when fwknopd uses a version
                                  of Net::Pcap that does not implement the
                                  pcap_datalink_val_to_name() function or
                                  have the pcap_datali.al file.
    -o, --os                    - Parse iptables logs and fingerprint
                                  operating systems from which tcp SYN
                                  packets have been logged.
    --fw-log <file>             - Specify path to iptables logfile. This
                                  is used only when running in --os mode.
    --Lib-dir <path>            - Path to the perl modules directory (not
                                  usually necessary).
    -d, --debug                 - Run fwknopd in debugging mode.
    --locale <locale>           - Manually define a locale setting.
    --no-locale                 - Don't set the locale to anything (the
                                  default is the "C" locale from the LOCALE
                                  variable in the fwknop.conf file).
    --no-FKO-module             - Revert to older perl implementation even if
                                  the FKO module is installed.
    --test-FKO-exists           - See if the FKO module is available to use
                                  and exit (this is used by the fwknop test
                                  suite).
    -v, --verbose               - Verbose mode.
    -V, --Version               - Display version and exit.
    -h, --help                  - Print help and exit.
_HELP_

    exit $exit_status;
}
