#!/usr/bin/perl -w
#
#############################################################################
#
# File: fwknopd
#
# 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 a Netfilter 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.
#
# Author: Michael Rash (mbr@cipherdyne.org)
#
# Version: 1.0
#
# Copyright (C) 2004-2006 Michael Rash (mbr@cipherdyne.org)
#
# License (GNU Public License):
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
#    You should have received a copy of the GNU General Public License
#    along with this program; if not, write to the Free Software
#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307
#    USA
#
#############################################################################
#
# $Id: fwknopd 583 2006-11-04 20:43:01Z mbr $
#

use lib '/usr/lib/fwknop';
use Crypt::CBC;
use Unix::Syslog qw(:subs :macros);
use Net::IPv4Addr qw(ipv4_in_network);
use Net::Pcap;
use NetPacket::IP;
use NetPacket::UDP;
use NetPacket::TCP;
use NetPacket::ICMP;
use NetPacket::Ethernet;
use IPTables::Parse;
use IPTables::ChainMgr;
use Digest::MD5 'md5_base64';
use IO::Socket;
use IO::Handle;
use MIME::Base64;
use Data::Dumper;
use POSIX;
use Getopt::Long;
use strict;

my $config_file = '/etc/fwknop/fwknop.conf';
my $alerting_config_file = '/etc/fwknop/alert.conf';

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

my %config     = ();
my %cmds       = ();
my %p0f_sigs   = ();
my %p0f        = ();
my @access     = ();
my @ipt_config = ();
my %ipt_access = ();
my %ip_sequences = ();
my %md5_msg_store = ();

my $os_fprint_only = 0;
my $print_version  = 0;
my $print_help     = 0;
my $kill           = 0;
my $restart        = 0;
my $status         = 0;
my $debug          = 0;
my $ipt_list       = 0;
my $ipt_flush      = 0;
my $verbose        = 0;
my $use_gpg        = 0;
my $os_ipt_log     = '';
my $cmdline_intf   = '';
my $warn_msg       = '';
my $die_msg        = '';
my $err_wait_timer     = 30;  ### seconds
my $skipped_first_loop = 0;
my $pcap_sleep_interval = 1;  ### seconds

### mode numbers
my $command_mode = 0;
my $access_mode  = 1;

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

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

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

### 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 $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 "[+] ** Starting fwknopd (debug mode) **\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
    &pcap_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();

    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 "[+] pcap_loop()\n" if $debug;

    my $check_file_ctr = 0;

    for (;;) {

        Net::Pcap::loop($pcap_t, 1, \&pcap_process_pkt, 'fwknop_tag');

        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", 0);
                        $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", 0);
                    $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++;

            ### always check to see if we need to timeout access for IPs
            ### For AUTH_MODE set to PCAP, knoptm will timeout access
            &timeout_access();
        }

        sleep $pcap_sleep_interval;
    }

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

    return;
}

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

    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_obj = '';

    if ($config{'AUTH_MODE'} eq 'ULOG_PCAP') {
        ### The ulogd pcap writer does not include link layer information
        $ip = NetPacket::IP->decode($pkt) or return;
    } else {
        $ether_data = NetPacket::Ethernet::strip($pkt) or return;
        $ip = NetPacket::IP->decode($ether_data) or return;
    }

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

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

    if ($proto == 1) {
        $transport_obj = NetPacket::ICMP->decode($ip->{'data'});
    } elsif ($proto == 6) {
        $transport_obj = NetPacket::TCP->decode($ip->{'data'});
    } elsif ($proto == 17) {
        $transport_obj = NetPacket::UDP->decode($ip->{'data'});
    } else {
        return;
    }

    print STDERR "[+] Received packet (" if $debug;

    ### 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 unless defined $transport_obj->{'data'};

    my $enc_msg_len = 0;
    $enc_msg_len = length($transport_obj->{'data'});
    if (10 < $enc_msg_len and $enc_msg_len < 1500) {
        print STDERR "$enc_msg_len bytes)\n" if $debug;
    } else {
        print STDERR "$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_obj->{'data'};
        $data_tmp =~ s/[^\x20-\x7e]/NA/g;
        print STDERR "[+] Received data: $data_tmp\n"
            if $debug;
    }

    ### see if this packet is worthy of getting access through
    ### the firewall
    &SPA_check_grant_access($src_ip, $enc_msg_len, $transport_obj->{'data'});

    return;
}

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

    ### 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);

    unless ($access_nums_aref) {
        print STDERR "[-] Packet from $src_ip did not match any ",
              "SOURCE blocks in $config{'ACCESS_CONF'}\n" if $debug;
        return;
    }

    ### at this point we have a non-replayed packet, so see if it 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/;

        print STDERR Dumper $access_hr 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
        my ($decrypt_rv, $decrypted_msg, $gpg_sign_id, $decrypt_algo)
            = &SPA_decrypt($pkt_data, $enc_msg_len, $access_hr);
        next SOURCE unless $decrypt_rv;

        ### check for replay attacks
        my ($md5sum_rv, $md5sum)
            = &check_replay_attack($decrypted_msg, $src_ip);
        return if $md5sum_rv;

        ### see if we have a syntactically valid message
        my ($validate_rv, $msg_href) = &pcap_validate_msg(
            $decrypted_msg, $source_block_num, $access_hr);
        next SOURCE unless $validate_rv;

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

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

        ### check username
        next SOURCE unless &SPA_check_user($access_hr, $src_ip, $msg_href);

        ### check authentication method
        next SOURCE unless &SPA_check_auth_method(
            $access_hr, $src_ip, $msg_href);

        if ($msg_href->{'action_type'} == $access_mode) {
            if (&SPA_access($msg_href, $src_ip, $decrypt_algo,
                    $gpg_sign_id, $md5sum, $access_hr)) {
                last SOURCE;
            } else {
                next SOURCE;
            }
        } elsif ($msg_href->{'action_type'} == $command_mode) {
            if (&SPA_cmd($msg_href, $src_ip, $decrypt_algo,
                    $gpg_sign_id, $md5sum, $access_hr)) {
                last SOURCE;
            } else {
                next SOURCE;
            }
        }
    }
    return;
}

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

    my $decrypted_msg = '';
    my $decrypt_algo  = 'Rijndael';
    my $gpg_sign_id   = '';
    my $decrypt_rv    = 1;

    if ($enc_msg_len > 400
            and defined $access_hr->{'GPG_REMOTE_ID'}) {
        ### attempt GPG decrypt (only if the length of the encrypted
        ### payload is greater than 500 bytes; even encrypting a single
        ### byte of data with a 1024 bit GnuPG key results in 340 bytes
        ### of encrypted payload).
        ($decrypted_msg, $gpg_sign_id) =
                &pcap_GPG_decrypt_msg($pkt_data, $access_hr);
        $decrypt_algo = 'GnuPG' if $decrypted_msg;

    } elsif (defined $access_hr->{'KEY'}) {

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

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

    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') {
        if (abs((time() - $remote_time))
                > $config{'MAX_SPA_PACKET_AGE'}) {
            &logr('[-]', "remote time stamp is older than " .
                "$config{'MAX_SPA_PACKET_AGE'} second max age.", 1);
            return 0;
        }
    }
    return 1;
}

sub SPA_dump_packet() {
    my $msg_href = shift;

    print STDERR "[+] Packet fields:\n",
        "        Random data: $msg_href->{'random_number'}\n",
        "        Username:    $msg_href->{'username'}\n",
        "        Remote time: $msg_href->{'remote_time'}\n",
        "        Remote ver:  $msg_href->{'remote_version'}\n",
        "        Action type: $msg_href->{'action_type'}\n",
        "        Action:      $msg_href->{'action'}\n",
        "        MD5 sum:     $msg_href->{'md5sum'}\n";

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

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

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

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

    my $server_auth_type     = '';
    my $server_auth_crypt_pw = '';
    if ($msg_href->{'server_auth'}) {
        if ($msg_href->{'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_href->{'username'},
                            $server_auth_crypt_pw,
                            $access_hr->{'SHADOW_FILE'})) {
                    &logr('[-]', "IP: $src_ip failed server-auth UNIX " .
                        "crypt() password test", 0);
                    return 0;
                }
            }
        } else {
            &logr('[-]', "required server-auth method " .
                "\"$access_hr->{'REQUIRE_AUTH_METHOD'}\" " .
                "not supplied by $src_ip", 0);
            return 0;
        }
    }
    return 1;
}

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

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

    my $allow_ip = '';
    $allow_ip = $1 if $msg_href->{'action'} =~ /($ip_re)/;

    unless ($allow_ip) {
        &logr('[-]', "no valid IP address within action portion of SPA " .
            "packet from $src_ip", 1);
        return 0;
    }

    if ($allow_ip eq '0.0.0.0') {
        if ($config{'REQUIRE_SOURCE_ADDRESS'} eq 'Y'
                or (defined $access_hr->{'REQUIRE_SOURCE_ADDRESS'}
                    and $access_hr->{'REQUIRE_SOURCE_ADDRESS'})) {
            &logr('[-]', "IP: $src_ip sent SPA packet that " .
                "contained 0.0.0.0 (-s on the client side) " .
                "but REQUIRE_SOURCE_ADDRESS is enabled", 1);
            return 0;
        } else {
            $allow_ip = $src_ip;
        }
    }

    ### initialize to just the OPEN_PORTS directives
    my %open_ports = %{$access_hr->{'OPEN_PORTS'}};

    if ($access_hr->{'PERMIT_CLIENT_PORTS'}) {
        if ($msg_href->{'action'}
                =~ /($ip_re),(tcp|udp|icmp),(\d+)/i) {
            ### single port access format (e.g. tcp,22)
            my $allow_ip        = $1;
            my $dec_allow_port  = $2;
            my $dec_allow_proto = $3;

            $open_ports{$dec_allow_proto}{$dec_allow_port} = '';

        } elsif ($msg_href->{'action'}
                 =~ /($ip_re),(\S+)/) {
            ### multi-port access format (-A was specified by
            ### the client)
            my $allow_ip   = $1;
            my $access_str = $2;

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

                    $open_ports{$proto}{$port} = '';
                }
            }
        }
    }

    if ($decrypt_algo eq 'GnuPG') {
        if (defined $access_hr->{'GPG_REMOTE_ID'}) {
            &logr('[+]', "received valid $decrypt_algo encrypted packet " .
                qq|(signed with required key ID: "$gpg_sign_id") from: | .
                "$src_ip, remote user: $msg_href->{'username'}", 0);
        } else {
            &logr('[+]', "received valid $decrypt_algo encrypted packet " .
                "from: $src_ip, remote user: $msg_href->{'username'}", 0);
        }
    } else {
        &logr('[+]', "received valid $decrypt_algo encrypted " .
            "packet from: $src_ip, remote user: $msg_href->{'username'}", 0);
    }

    ### cache the MD5 sum
    $md5_msg_store{$md5sum} = '';

    ### write MD5 sum to disk
    &diskwrite_md5_sum($md5sum)
        if $config{'ENABLE_MD5_PERSISTENCE'} eq 'Y';

    ### grant access through the firewall
    &grant_access($allow_ip, '', \%open_ports, $access_hr);

    return 1;
}

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

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

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

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

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

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

    if ($decrypt_algo eq 'GnuPG') {
        if (defined $access_hr->{'GPG_REMOTE_ID'}) {
            &logr('[+]', "received valid $decrypt_algo encrypted packet " .
                qq|(signed with required key ID: "$gpg_sign_id") from: | .
                "$src_ip, remote user: $msg_href->{'username'}", 0);
        } else {
            &logr('[+]', "received valid $decrypt_algo encrypted packet " .
                "from: $src_ip, remote user: $msg_href->{'username'}", 0);
        }
    } else {
        &logr('[+]', "received valid $decrypt_algo encrypted " .
            "packet from: $src_ip, remote user: $msg_href->{'username'}", 0);
    }

    &logr('[+]', qq|executing command "$msg_href->{'action'} for $src_ip|, 1);

    ### cache the MD5 sum
    $md5_msg_store{$md5sum} = '';

    ### write MD5 sum to disk
    &diskwrite_md5_sum($md5sum)
        if $config{'ENABLE_MD5_PERSISTENCE'} eq 'Y';

    ### execute the command
    &exec_command($run_cmd);

    return 1;
}

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

    my $md5sum = md5_base64($decrypted_data);

    if (defined $md5sum and $md5sum =~ /\S/) {
        if (defined $md5_msg_store{$md5sum}) {
            ### Bad!  Send warning email and return.
            &logr('[-]', "attempted message replay from: $src_ip", 1);
            return 1, '';
        }
    } else {
        ### could not calculate the MD5 sum for some reason; don't
        ### trust the packet
        &logr('[-]', "could not calculate md5 sum for SPA " .
            "packet from: $src_ip", 1);
        return 1, '';
    }
    return 0, $md5sum;
}

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

    unless (-e $shadow_file) {
        &logr('[-]', "shadow file $shadow_file does not exist", 0);
        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", 0);
        return 0;
    }

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

sub knock_loop() {
    print STDERR "[+] Opening $config{'FW_DATA_FILE'}, and entering main loop.\n"
        if $debug;

    ### main server loop
    open FWLOG, $config{'FW_DATA_FILE'} or die $!;
    for (;;) {
        my @fw_pkts = <FWLOG>;
        if (@fw_pkts and ($os_fprint_only or $skipped_first_loop)) {
            &process_pkts(\@fw_pkts);
        }

        @fw_pkts = ();
        $skipped_first_loop = 1 unless $skipped_first_loop;

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

        ### always check to see if we need to timeout access for IPs
        &timeout_access();

        if ($die_msg) {
            open D, ">> $config{'FWKNOP_DIR'}/fwknopd.die" or
                die "[*] Could not open $config{'FWKNOP_DIR'}/fwknopd.die: $!";
            print D scalar localtime(), " $die_msg";
            close D;
            $die_msg = '';
        }

        if ($warn_msg) {
            open D, ">> $config{'FWKNOP_DIR'}/fwknopd.warn" or
                die "[*] Could not open $config{'FWKNOP_DIR'}/fwknopd.warn: $!";
            print D scalar localtime(), " $warn_msg";
            close D;
            $warn_msg = '';
        }

        ### 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 = ();
    my $md5sum = '';
    my $server_auth = '';
    my @fields = split /:/, $msg;

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

    my $random_number  = $fields[0] || return 0, '';
    my $username       = $fields[1] || return 0, '';
    my $remote_time    = $fields[2] || return 0, '';
    my $remote_version = $fields[3] || return 0, '';
    my $action_type    = $fields[4];
    my $action         = $fields[5] || return 0, '';
    $md5sum            = $fields[6] || return 0, '';
    if ($remote_version =~ /0\.9\.(\d+)/) {
        my $minor_ver_num = $1;
        if ($minor_ver_num > 2 and $#fields > 6) {
            $server_auth = $fields[7] || '';
        }
    }

    return 0, '' unless $action_type == $command_mode
        or $action_type == $access_mode;

    %msg = (
        'random_number'  => $random_number,
        'username'       => $username,
        'remote_time'    => $remote_time,
        'remote_version' => $remote_version,
        'action_type'    => $action_type,
        'action'         => $action,
        'md5sum'         => $md5sum,
    );

    if ($server_auth) {
        $msg{'server_auth'} = $server_auth;
    } else {
        $msg{'server_auth'} = '';
    }

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

    ### validate message
    if (&check_md5sum(\%msg)) {
        $msg{'username'}    = decode_base64($msg{'username'});
        $msg{'action'}      = decode_base64($msg{'action'});
        $msg{'server_auth'} = decode_base64($msg{'server_auth'});

        if ($debug) {
            print STDERR "[+] Decoded message: $msg{'random_number'}:",
                "$msg{'username'}:$msg{'remote_time'}:",
                "$msg{'remote_version'}:$msg{'action_type'}:",
                "$msg{'action'}:$msg{'md5sum'}";
            if ($msg{'server_auth'} and $msg{'server_auth'} =~ /^\s*(\w+),(.*)/) {
                print STDERR ":$1,";
                for (my $i=0; $i<length($2); $i++) {
                    print STDERR "*";
                }
                print STDERR "\n";
            } else {
                print STDERR "\n";
            }
        }
        return 1, \%msg;
    }

    print STDERR "[-] Key mis-match or broken message ",
        "checksum for SOURCE $access_hr->{'SOURCE'} ",
        "(# $source_block_num in access.conf)\n"
        if $debug;

    return 0, '';
}

sub check_md5sum() {
    my $msg_href = shift;

    my $md5sum = $msg_href->{'md5sum'};

    my $data_str = '';
    for my $key qw(
        random_number
        username
        remote_time
        remote_version
        action_type
        action
    ) {
        $data_str .= $msg_href->{$key} . ':';
    }

    $data_str =~ s/:$//;

    if ($msg_href->{'server_auth'}) {
        $data_str .= ':' . $msg_href->{'server_auth'};
    }

    return 1 if $md5sum eq md5_base64($data_str);
    return 0;
}

sub get_pcap_obj() {

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

    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 "[+] 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 "[+] Sniffing (promisc) packet data from interface: ",
                "$config{'PCAP_INTF'}\n" if $debug;
            $pcap_t = Net::Pcap::open_live($config{'PCAP_INTF'},
                1500, 1, 100, \$err) or die "[*] Could not open ",
                    "$config{'PCAP_INTF'}: $!";
        } else {
            print STDERR "[+] Sniffing (non-promisc) packet data from ",
                "interface: $config{'PCAP_INTF'}\n" if $debug;
            $pcap_t = Net::Pcap::open_live($config{'PCAP_INTF'},
                1500, 0, 100, \$err) or die "[*] Could not open ",
                    "$config{'PCAP_INTF'}: $!";
        }
    }

    ### 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) {
                die "[*] Could not get net information for ",
                    "$config{'PCAP_INTF'}: $!";
            }
        }

        ### 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", 0);
        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", 0);
        sleep $err_wait_timer;
    }
    return;
}

sub exec_command() {
    my $cmd = shift;
    my $pid;
    print STDERR "[+] executing command: $cmd\n" if $debug;
    if ($pid = fork()) {
        local $SIG{'ALRM'} = sub {die "[*] External script timeout.\n"};
        ### the external script should be finished within this timeout
        alarm $config{'PCAP_CMD_TIMEOUT'};
        eval {
            waitpid($pid, 0);
        };
        alarm 0;
        if ($@) {
            kill 9, $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 "[*] 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 "[-] 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 "[-] Packet from $src did not match any SOURCE in ",
                    "$config{'ACCESS_CONF'}\n" if $debug;
                next PKT;
            }
        }

        if ($proto eq 'tcp') {
            print STDERR "[+] $proto $src $sp -> $dst $dp, $flags\n" if $debug;
        } elsif ($proto eq 'udp') {
            print STDERR "[+] $proto $src $sp -> $dst $dp\n" if $debug;
        } elsif ($proto eq 'icmp') {
            print STDERR "[+] $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_href = $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_href->{'grant_ctr'} = 0
                if not defined $seq_href->{'grant_ctr'};

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

            if (defined $access_hr->{'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_href->{'enc_stime'} = time()
                        unless defined $seq_href->{'enc_stime'};

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

                    print STDERR "[+] Added $dp to encrypted sequence for $src\n"
                        if $debug;
                }

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

                    $expecting_decrypt = 1;

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

                    if ($rv) {
                        $decrypted = 1;

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

                        ### 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_href->{'grant_ctr'}
                                    > $access_hr->{'KNOCK_LIMIT'}) {
                                &logr('[-]', "$src exceeded knock limit (set to " .
                                    "$access_hr->{'KNOCK_LIMIT'} accesses)",
                                    1);
                                &logr('[-]', "access controls for $src will " .
                                    "not be modified", 1);
                                delete $ip_sequences{$src}{$num};
                                next NUM;
                            }
                        }

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

                        &grant_access($allow_ip, $seq_href,
                            \%open_ports, $access_hr);

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

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

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

                    ### if we made it to the end of the sequence then we have
                    ### a correct knock sequence
                    if ($seq_href->{'port_seq'}
                            == $#{$access_hr->{'SHARED_SEQUENCE'}}+1) {
                        print STDERR "[+] Matched knock sequence for $src\n"
                            if $debug;
                        $matched_sequence = 1;
                    }
                } else {
                    print STDERR "[-] Could not match dst port: $dp at sequence ",
                        "number: $seq_href->{'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_href->{'port_times'};
                $seq_href->{'port_seq'} = 0;

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

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

                ### 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_href->{'grant_ctr'}
                            > $access_hr->{'KNOCK_LIMIT'}) {
                        &logr('[-]', "$src exceeded knock limit (set to " .
                            "$access_hr->{'KNOCK_LIMIT'} accesses)", 1);
                        &logr('[-]', "access controls for $src will not be ".
                            "modified", 1);
                        next NUM;
                    }
                }

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

    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'}, 1);
        } elsif (defined $href->{'REQUIRE_OS_REGEX'}) {
            &logr('[-]', "could not fingerprint OS for $src, expecting OS " .
                "regex: $href->{'REQUIRE_OS_REGEX'}", 1);
        }
        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", 0);
                    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", 1);
            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", 1);
                    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", 1);
            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", 0);
            return 1;
        } else {
            &logr('[-]', "username mismatch, expected: " .
                "$href->{'REQUIRE_USERNAME'}, got: $username", 1);
            return 0;
        }
    } else {
        &logr('[-]', "missing username in encrypted " .
            "sequence, expected: $href->{'REQUIRE_USERNAME'}", 1);
        return 0;
    }
    return 0;
}

sub check_src() {
    my $src = shift;

    my @access_nums = ();

    for (my $i=0; $i<=$#access; $i++) {
        my $access_hr = $access[$i];
        my $type = $access_hr->{'TYPE'};
        if ($type eq 'ip') {
            if ($src eq $access_hr->{'SOURCE'}) {
                print STDERR "[+] Packet from $src matched IP SOURCE: $src in ",
                    "$config{'ACCESS_CONF'}\n" if $debug;
                push @access_nums, $i;
            }
        } elsif ($type eq 'net') {
            if (ipv4_in_network($access_hr->{'SOURCE'}, $src)) {
                print STDERR "[+] Packet from $src matched NET SOURCE: ",
                    "$access_hr->{'SOURCE'} in $config{'ACCESS_CONF'}\n" if $debug;
                push @access_nums, $i;
            }
        } elsif ($type eq 'any') {
            print STDERR "[+] Packet from $src matched SOURCE: ANY in ",
                "$config{'ACCESS_CONF'}\n" if $debug;
            push @access_nums, $i;
        } elsif ($type eq 'multisrc') {
            for my $access_src (keys %{$access_hr->{'SOURCE'}}) {
                if ($access_src =~ m|/|) {  ### it is a network
                    if (ipv4_in_network($access_src, $src)) {
                        print STDERR "[+] Packet from $src matched NET SOURCE: ",
                            "$access_hr->{'SOURCE'} in $config{'ACCESS_CONF'}\n" if $debug;
                        push @access_nums, $i;
                    }
                } else {
                    if ($src eq $access_src) {
                        print STDERR "[+] Packet from $src matched IP SOURCE: $src in ",
                            "$config{'ACCESS_CONF'}\n" if $debug;
                        push @access_nums, $i;
                    }
                }
            }
        }
    }
    return \@access_nums;
}

sub incr_seq() {
    my ($src, $seq_href, $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_href->{'port_seq'} > 0) {
            if (defined $access_hr->{'MAX_TIME_DIFF'}) {
                my $time = time();
                if (($time - $seq_href->{'port_times'}[$seq_href->{'port_seq'}-1])
                            > $access_hr->{'MIN_TIME_DIFF'} and
                        ($time - $seq_href->{'port_times'}[$seq_href->{'port_seq'}-1])
                            < $access_hr->{'MAX_TIME_DIFF'}) {
                    print STDERR "[+] Sequence min/max time match: ",
                        "($seq_href->{'port_seq'}) ",
                        "$access_hr->{'SHARED_SEQUENCE'}->[$seq_href->{'port_seq'}]->{'proto'}/",
                        "$access_hr->{'SHARED_SEQUENCE'}->[$seq_href->{'port_seq'}]->{'port'}\n"
                        if $debug;
                } else {
                    &logr('[-]', 'Sequence min/max_time exceeded: ' .
                        "$access_hr->{'SHARED_SEQUENCE'}->[$seq_href->{'port_seq'}]->{'proto'}/" .
                        "$access_hr->{'SHARED_SEQUENCE'}->[$seq_href->{'port_seq'}]->{'port'} " .
                        "(port sequence num: $seq_href->{'port_seq'}) ", 1);
                    $seq_href->{'port_seq'} = 0;
                    delete $seq_href->{'port_times'};
                    return 0;
                }
            } else {
                if ((time()
                        - $seq_href->{'port_times'}[$seq_href->{'port_seq'}-1])
                        > $access_hr->{'MIN_TIME_DIFF'}) {
                    print STDERR "[+] Sequence min_time match: ",
                        "($seq_href->{'port_seq'}) ",
                        "$access_hr->{'SHARED_SEQUENCE'}->[$seq_href->{'port_seq'}]->{'proto'}/",
                        "$access_hr->{'SHARED_SEQUENCE'}->[$seq_href->{'port_seq'}]->{'port'}\n"
                        if $debug;
                } else {
                    &logr('[-]', "Sequence min_time (" .
                        "$access_hr->{'MIN_TIME_DIFF'} seconds) not met: " .
                        "$access_hr->{'SHARED_SEQUENCE'}->[$seq_href->{'port_seq'}]->{'proto'}/" .
                        "$access_hr->{'SHARED_SEQUENCE'}->[$seq_href->{'port_seq'}]->{'port'} " .
                        "(port sequence num: $seq_href->{'port_seq'}) ", 1);
                    delete $seq_href->{'port_times'};
                    $seq_href->{'port_seq'} = 0;
                    return 0;
                }
            }
        } else {
            print STDERR "[+] 1 Sequence match: ",
                "($seq_href->{'port_seq'}) ",
                "$access_hr->{'SHARED_SEQUENCE'}->[$seq_href->{'port_seq'}]->{'proto'}/",
                "$access_hr->{'SHARED_SEQUENCE'}->[$seq_href->{'port_seq'}]->{'port'}\n"
                if $debug;
        }
    } elsif (defined $access_hr->{'MAX_TIME_DIFF'}) {
        if ($seq_href->{'port_seq'} > 0) {
            if ((time()
                    - $seq_href->{'port_times'}[$seq_href->{'port_seq'}-1])
                    < $access_hr->{'MAX_TIME_DIFF'}) {
                print STDERR "[+] Sequence max_time match: ",
                    "($seq_href->{'port_seq'}) ",
                    "$access_hr->{'SHARED_SEQUENCE'}->[$seq_href->{'port_seq'}]->{'proto'}/",
                    "$access_hr->{'SHARED_SEQUENCE'}->[$seq_href->{'port_seq'}]->{'port'}\n"
                    if $debug;
            } else {
                &logr('[-]', "Sequence max_time ($access_hr->{'MAX_TIME_DIFF'} seconds) exceeded: " .
                    "$access_hr->{'SHARED_SEQUENCE'}->[$seq_href->{'port_seq'}]->{'proto'}/" .
                    "$access_hr->{'SHARED_SEQUENCE'}->[$seq_href->{'port_seq'}]->{'port'}" .
                    "(port sequence num: $seq_href->{'port_seq'}) ", 1);
                delete $seq_href->{'port_times'};
                $seq_href->{'port_seq'} = 0;
                return 0;
            }
        } else {
            print STDERR "[+] Sequence match: ($seq_href->{'port_seq'}) ",
                "$access_hr->{'SHARED_SEQUENCE'}->[$seq_href->{'port_seq'}]->{'proto'}/",
                "$access_hr->{'SHARED_SEQUENCE'}->[$seq_href->{'port_seq'}]->{'port'}\n"
                if $debug;
        }
    } else {
        print STDERR "[+] Sequence match: ($seq_href->{'port_seq'}) ",
            "$access_hr->{'SHARED_SEQUENCE'}->[$seq_href->{'port_seq'}]->{'proto'}/",
            "$access_hr->{'SHARED_SEQUENCE'}->[$seq_href->{'port_seq'}]->{'port'}\n"
            if $debug;
    }

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

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

    print STDERR "[+] Attempting GnuPG decrypt...\n" if $debug;

    my @plaintext = ();

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

    $gnupg->options->hash_init(
         'batch' => 1,
         'homedir' => $access_hr->{'GPG_HOME_DIR'});

    my $input  = IO::Handle->new() or die $!;
    my $output = IO::Handle->new() or die $!;
    my $error  = IO::Handle->new() or die $!;
    my $pw     = IO::Handle->new() or die $!;
    my $status = IO::Handle->new() or die $!;

    my $handles = GnuPG::Handles->new(
        stdin      => $input,
        stdout     => $output,
        stderr     => $error,
        passphrase => $pw,
        status     => $status,
    );

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

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

    print $pw $access_hr->{'GPG_DECRYPT_PW'};
    close $pw;

    $msg .= '==' if $msg !~ /==$/;

    print $input decode_base64($msg);
    close $input;

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

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

    waitpid $pid, 0;

    my $decrypted_msg = '';

    my $found_sig = 0;
    my $gpg_sign_id = '';
    if (defined $access_hr->{'GPG_REMOTE_ID'}) {
        ### we require the message to be signed; make sure
        ### the signature is good
        ERR: for my $err (@errors) {
            for my $key_id (@{$access_hr->{'GPG_REMOTE_ID'}}) {
                if ($key_id =~ /^0x(\w+)/) {
                    $key_id = $1;
                }
                if ($err =~ /Signature\s+made.*ID\s+$key_id/) {
                    print STDERR "[+] GnuPG signature made with ",
                        "required $key_id\n" if $debug;
                    $found_sig   = 1;
                    $gpg_sign_id = $key_id;
                    last ERR;
                }
                if ($err =~ /Good\s+signature/i) {
                    $found_sig   = 1;
                    $gpg_sign_id = $key_id;
                    last ERR;
                }
            }
        }
    }

    unless ($found_sig) {
        print STDERR "[-] GnuPG message not signed by any ",
            "required key ID.\n" if $debug;
        return '', '';
    }

    $decrypted_msg .= $_ for @plaintext;
    return $decrypted_msg, $gpg_sign_id;
}

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

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

    ### make sure the trailing "==" is there for the base64 decode
    $msg .= '==' if $msg !~ /==$/;

    my $cipher = Crypt::CBC->new(
        {
            'key'    => $enc_key,
            'cipher' => $enc_alg,
        }
    );
    my $decrypted_msg;
    eval {
        $decrypted_msg = $cipher->decrypt(decode_base64($msg));
    };
    return $decrypted_msg unless $@;
    return '';
}

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

    my $cipher_txt = '';
    my $allow_ip   = '';

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

    return 0 unless $cipher_txt;

    if ($debug) {
        my @tmp_chars = split //, $cipher_txt;
        print STDERR "[+] Cipher text: ";
        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 "[+] 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), 1);
            return 0,0,0,0;
        }
        $allow_ip .= ord($octet) . '.';
    }
    $allow_ip =~ s/\.$//;

    if ($allow_ip 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') {
            ### we require the source address to be contained within
            ### the encrypted packet.
            return 0,0,0,0;
        }
        $allow_ip = $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", 1);
        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", 1);
        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", 1);
        return 0,0,0,0;
    }

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

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

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

    my %ipt_opts = (
        'iptables' => $cmds{'iptables'},
        'iptout'   => $config{'IPT_OUTPUT_FILE'},
        'ipterr'   => $config{'IPT_ERROR_FILE'}
    );
    $ipt_opts{'debug'}   = 1 if $debug;
    $ipt_opts{'verbose'} = 1 if $verbose;

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

    ### add rule for $ip unless it already exists
    for my $hr (@ipt_config) {
        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'};

        ### make sure "to_chain" exists
        my ($rv, $out_aref, $err_aref) = $ipt->create_chain($table, $to_chain);

        unless ($rv) {
            &psyslog_errs($err_aref);
            next;
        }

        ### add jump rule to the "to_chain" from the "from_chain"
        ($rv, $out_aref, $err_aref) = $ipt->add_jump_rule($table,
            $from_chain, $jump_rule_position, $to_chain);

        unless ($rv) {
            &psyslog_errs($err_aref);
            next;
        }

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

                my $ip_allowed = 0;
                my $num_chain_rules = 0;

                ($rv, $num_chain_rules) = $ipt->find_ip_rule($src, '0.0.0.0/0',
                    $table, $to_chain, $target, {'protocol' => $proto, 'd_port' => $port});

                if ($rv) {
                    &logr('[-]', "source: $src already allowed " .
                        "to connect to $proto/$port in chain: $to_chain", 1);
                } else {
                    my $msg = "adding $to_chain ACCEPT rule for " .
                        "$src -> $proto";
                    $msg .= "/$port" if $proto ne 'icmp';
                    $msg .= " ($access_hr->{'FW_ACCESS_TIMEOUT'} " .
                        "seconds)";

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

                    ($rv, $out_aref, $err_aref) = $ipt->add_ip_rule($src, '0.0.0.0/0',
                        $auto_rule_position, $table, $to_chain, $target,
                        {'protocol' => $proto, 'd_port' => $port});

                    if ($rv) {
                        ### keep track of when we first add an allow rule
                        ### for $src
                        if ($config{'AUTH_MODE'} ne 'PCAP') {
                            if (not defined $ipt_access{$src}) {
                                $ipt_access{$src}{'s_time'} = time();
                                $ipt_access{$src}{'timeout'} =
                                    $access_hr->{'FW_ACCESS_TIMEOUT'};
                            }
                            $ipt_access{$src}{'access'}{$proto}{$port} = '';
                        }

                        ### keep track of how many times we have granted access
                        $seq_href->{'grant_ctr'}++ unless
                            $access_hr->{'DATA_COLLECT_MODE'} =~ /PCAP/;

                        if ($config{'AUTH_MODE'} eq 'PCAP') {
                            ### In PCAP mode (and not in FILE_PCAP or ULOG_PCAP
                            ### modes), we only get processing time when we
                            ### receive a packet, so cache the firewall rule
                            ### addition time so that knoptm can remove it.
                            &pcap_write_knoptm_fw_cache_entry(
                                time(),
                                $access_hr->{'FW_ACCESS_TIMEOUT'},
                                $src,
                                $proto,
                                $port,
                                $table,
                                $to_chain,
                                $target
                            );
                        }
                    } else {
                        &psyslog_errs($err_aref);
                    }
                }
            }
        }
    }
    $seq_href->{'port_seq'} = 0
        unless $access_hr->{'DATA_COLLECT_MODE'} =~ /PCAP/;

    return;
}

sub pcap_write_knoptm_fw_cache_entry() {
    my ($rule_timeout, $timeout, $ip,
        $proto, $port, $table, $chain, $target) = @_;

    print STDERR "[+] Writing fw time cache entry to: ",
        "$config{'KNOPTM_TIMEOUT_FILE'} $rule_timeout $timeout\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 "$rule_timeout $timeout $ip $proto ",
        "$port $table $chain $target\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", 0);
                    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", 0);
                    delete $ip_sequences{$src}{$seq_num};
                    next;
                }
            }
        }
    }
    return;
}

sub timeout_access() {
    return unless %ipt_access;

    my %ipt_opts = (
        'iptables' => $cmds{'iptables'},
        'iptout'   => $config{'IPT_OUTPUT_FILE'},
        'ipterr'   => $config{'IPT_ERROR_FILE'}
    );
    $ipt_opts{'debug'}   = 1 if $debug;
    $ipt_opts{'verbose'} = 1 if $verbose;

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

    SRC: for my $src (keys %ipt_access) {

        ### see if the timeout has expired
        next SRC unless ((time() - $ipt_access{$src}{'s_time'})
            > $ipt_access{$src}{'timeout'});

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

            for my $proto (keys %{$ipt_access{$src}{'access'}}) {
                for my $port (keys %{$ipt_access{$src}{'access'}{$proto}}) {
                    if ($ipt->find_ip_rule($src, '0.0.0.0/0', $table,
                            $to_chain, $target, {'protocol' => $proto,
                            'd_port' => $port})) {

                        my ($rv, $out_aref, $err_aref) = $ipt->delete_ip_rule(
                            $src, '0.0.0.0/0', $table, $to_chain, $target,
                            {'protocol' => $proto, 'd_port' => $port});

                        if ($rv) {
                            &logr('[+]', "removed iptables $to_chain ACCEPT " .
                                    "rule for $src -> " .
                                    "$proto/$port, $ipt_access{$src}{'timeout'} " .
                                    "second timeout exceeded", 1);
                        } else {
                            my $msg = "could not delete ACCEPT rule for $src " .
                                "-> $proto/$port";
                            &logr('[-]', $msg, 0);
                            &psyslog_errs($err_aref);
                        }
                    }
                }
            }
        }
        delete $ipt_access{$src};
    }
    return;
}

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

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

    my ($options_aref) = &parse_tcp_options($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 "[+] os: $os, $sig\n" if $debug;
                            $p0f{$src}{$os} = $sig;
                        }
                    }
                }
            }
        }
    }
    return;
}

sub parse_tcp_options() {
    my $tcp_options = shift;
    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.', 0);
        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;
    }
    OPT: for (my $opt_kind=0; $opt_kind <= $#hex_nums;) {
        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 {
            ### get to the next option-kind field
            $opt_kind += $len;
        }
    }
    if ($debug) {
        $debug_str =~ s/\,$//;
        print STDERR "[+] $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', 0);
    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/;

        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;
            if ($line =~ m|^\s*SOURCE:\s*($ip_re)\s*;|) {
                $access_hsh{'SOURCE'} = $1;
                $access_hsh{'TYPE'} = 'ip';
            } elsif ($line =~ m|^\s*SOURCE:\s*($ip_re/\d+)\s*;|) {  ### CIDR
                $access_hsh{'SOURCE'} = $1;
                $type = 'net';
                $access_hsh{'TYPE'} = 'net';
            } elsif ($line =~ m|^\s*SOURCE:\s*($ip_re/$ip_re)\s*;|) {
                $access_hsh{'SOURCE'} = $1;
                $access_hsh{'TYPE'} = 'net';
            } elsif ($line =~ m|^\s*SOURCE:\s*ANY\s*;|) {
                $access_hsh{'SOURCE'} = 'ANY';
                $access_hsh{'TYPE'} = 'any';
            } elsif ($line =~ m|^\s*SOURCE:\s*($ip_re.*)\s*;|) {
                my @arr = split /\s,\s*/, $1;
                my %ip_net = ();
                for my $src (@arr) {
                    if ($src =~ m|$ip_re/\d+|
                            or $src =~ m|$ip_re/$ip_re|
                            or $src =~ m|$ip_re|) {
                        $ip_net{$src} = '';
                    } else {
                        die "[*] Invalid SOURCE block: $line";
                    }
                }
                if (%ip_net) {
                    $access_hsh{'SOURCE'} = \%ip_net;
                    $access_hsh{'TYPE'} = 'multisrc';
                } else {
                    die "[*] Invalid SOURCE block: $line";
                }
            }
            $i++;
            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+);/) {
                    $access_hsh{'DATA_COLLECT_MODE'} = uc($1);
                } elsif ($line =~ /^\s*ENCRYPT_SEQUENCE\s*;/) {
                    $access_hsh{'DATA_COLLECT_MODE'} = 'ENCRYPT_SEQ';
                } elsif ($line =~ /^\s*KEY:\s*(.*)\s*;/) {
                    $access_hsh{'KEY'} = $1;
                    ### pad with zeros
                    while (length($access_hsh{'KEY'}) < $enc_blocksize) {
                        $access_hsh{'KEY'} .= '0';
                    }
                } elsif ($line =~ /^\s*GPG_REMOTE_ID:\s*(.*)\s*;/) {
                    require GnuPG::Interface unless $use_gpg;
                    $use_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*;/) {
                    require GnuPG::Interface unless $use_gpg;
                    $use_gpg = 1;
                    $access_hsh{'GPG_DECRYPT_ID'} = $1;
                } elsif ($line =~ /^\s*GPG_DECRYPT_PW:\s*(.*)\s*;/) {
                    require GnuPG::Interface unless $use_gpg;
                    $use_gpg = 1;
                    $access_hsh{'GPG_DECRYPT_PW'} = $1;
                } elsif ($line =~ /^\s*GPG_HOME_DIR:\s*(\S+)\s*;/) {
                    require GnuPG::Interface unless $use_gpg;
                    $use_gpg = 1;
                    $access_hsh{'GPG_HOME_DIR'} = $1;
                } 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_SEQ';
                    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*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*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 $val = $1;
                    if ($val =~ /y/i) {
                        $access_hsh{'REQUIRE_SOURCE_ADDRESS'} = 1;
                    } else {
                        $access_hsh{'REQUIRE_SOURCE_ADDRESS'} = 0;
                    }
                } 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*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;
        }
        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.";
    }
    print STDERR Dumper @access if $debug and $verbose;
    &logr('[+]', 'imported access directives ' .
        "($valid_ctr SOURCE definitions).", 0);
    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*#/);
        if ($line =~ /^\s*(\S+)\s+(.*?)\;/) {
            my $varname = $1;
            my $val     = $2;
            if ($val =~ m|/.+| && $varname =~ /^(\w+)Cmd$/) {
                ### found a command
                $cmds{$1} = $val;
            } else {
                $config{$varname} = $val;
            }
        }
    }
    return;
}

sub validate_src_access_hsh() {
    my $src_href = shift;
    my $src = '';
    my $gpg_mode = 0;
    if (defined $src_href->{'SOURCE'}) {
        $src = $src_href->{'SOURCE'};
    } else {
        &logr('[-]', "$config{'ACCESS_CONF'}: missing SOURCE tag", 0);
        return 0;
    }

    if (not defined $src_href->{'OPEN_PORTS'} and
            not $src_href->{'PERMIT_CLIENT_PORTS'}) {
        &logr('[-]', "$config{'ACCESS_CONF'}: source $src missing " .
            "OPEN_PORTS and PERMIT_CLIENT_PORTS is disabled, see " .
            "http://www.cipherdyne.org/fwknop/docs/faq.html#ports", 0);
        return 0;
    }

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

    if ($src_href->{'DATA_COLLECT_MODE'} =~ /ENCRYPT.?SEQ/ ) {
        unless (defined $src_href->{'KEY'}) {
            &logr('[-]', "$config{'ACCESS_CONF'}: source $src missing KEY " .
                " tag for encrypt_seq collection mode", 0);
            return 0;
        }
        unless (defined $src_href->{'PORT_OFFSET'}) {
            &logr('[-]', "$config{'ACCESS_CONF'}: source $src missing " .
                "PORT_OFFSET, defaulting to $enc_port_offset", 0);
            $src_href->{'PORT_OFFSET'} = $enc_port_offset;
        }
    } elsif ($src_href->{'DATA_COLLECT_MODE'} =~ /SHARED.?SEQ/ ) {
        unless (defined $src_href->{'OPEN_PORTS'}) {
            &logr('[-]', "$config{'ACCESS_CONF'}: source $src missing " .
                "OPEN_PORTS tag.", 0);
            return 0;
        }
    } elsif ($src_href->{'DATA_COLLECT_MODE'} =~ /PCAP/ ) {
        unless (defined $src_href->{'KEY'} or
                (defined $src_href->{'GPG_REMOTE_ID'}
                and defined $src_href->{'GPG_DECRYPT_ID'}
                and defined $src_href->{'GPG_DECRYPT_PW'})) {
            &logr('[-]', "$config{'ACCESS_CONF'}: source $src missing KEY or " .
                "(GPG_DECRYPT_ID, GPG_DECRYPT_PW, or GPG_REMOTE_ID) tag for " .
                "pcap collection mode", 0);
            return 0;
        }
        $gpg_mode = 1 if defined $src_href->{'GPG_REMOTE_ID'};
    } else {
        &logr('[-]', "$config{'ACCESS_CONF'}: source $src, missing " .
            "valid DATA_COLLECT_MODE key (must be one of ENCRYPT_SEQ, " .
            "SHARED_SEQ, FILE_PCAP, ULOG_PCAP, or PCAP)", 0);
        return 0;
    }
    if ($src_href->{'DATA_COLLECT_MODE'} =~ /PCAP/) {
        if (defined ($src_href->{'REQUIRE_AUTH_METHOD'})) {
            unless (lc($src_href->{'REQUIRE_AUTH_METHOD'}) eq 'crypt') {
                &logr('[-]', "$config{'ACCESS_CONF'}: source $src, invalid " .
                    "REQUIRE_AUTH_METHOD, must be set to 'crypt'", 0);
                return 0;
            }
            unless (defined $src_href->{'SHADOW_FILE'}) {
                $src_href->{'SHADOW_FILE'} = '/etc/shadow';
            }
        }
    }
    if (defined $src_href->{'MIN_TIME_DIFF'} and
            defined $src_href->{'MAX_TIME_DIFF'}) {
        if ($src_href->{'MAX_TIME_DIFF'} <
                $src_href->{'MIN_TIME_DIFF'}) {
            &logr('[-]', "$config{'ACCESS_CONF'}: source $src MAX_TIME_DIFF " .
                "cannot be less than MIN_TIME_DIFF", 0);
            return 0;
        }
    }
    if (defined $src_href->{'KNOCK_INTERVAL'}) {
        if ($src_href->{'KNOCK_INTERVAL'} < 0) {
            &logr('[-]', "$config{'ACCESS_CONF'}: source $src " .
                "KNOCK_INTERVAL must be greater than or equal to zero", 0);
            return 0;
        }
    } else {
        unless ($src_href->{'DATA_COLLECT_MODE'} =~ /PCAP/) {
            &logr('[-]', "$config{'ACCESS_CONF'}: source $src missing " .
                "KNOCK_INTERVAL, defaulting to $knock_interval", 0);
            $src_href->{'KNOCK_INTERVAL'} = $knock_interval;
        }
    }
    if (defined $src_href->{'FW_ACCESS_TIMEOUT'}) {
        if ($src_href->{'FW_ACCESS_TIMEOUT'} < 0) {
            &logr('[-]', "$config{'ACCESS_CONF'}: source $src " .
                "FW_ACCESS_TIMEOUT must be greater than or equal to zero", 0);
            return 0;
        }
    } else {
        &logr('[-]', "$config{'ACCESS_CONF'}: source $src missing " .
            "FW_ACCESS_TIMEOUT, defaulting to $fw_access_timeout", 0);
        $src_href->{'FW_ACCESS_TIMEOUT'} = $fw_access_timeout;
    }
    if (defined $src_href->{'KNOCK_LIMIT'}) {
        if ($src_href->{'KNOCK_LIMIT'} < 0) {
            &logr('[-]', "$config{'ACCESS_CONF'}: source $src " .
                "KNOCK_LIMIT must be greater than or equal to zero", 0);
            return 0;
        }
    }
    if ($gpg_mode) {
        unless (defined $src_href->{'GPG_HOME_DIR'}) {
            $src_href->{'GPG_HOME_DIR'} = $config{'GPG_DEFAULT_HOME_DIR'};
        }
        unless (-d $src_href->{'GPG_HOME_DIR'}) {
            &logr('[-]', "GnuPG directory $src_href->{'GPG_HOME_DIR'} " .
                "does not exist.", 0);
            exit 1;
        }
    }
    return 1;
}

sub handle_command_line() {
    ### make Getopts case sensitive
    Getopt::Long::Configure('no_ignore_case');
    &usage(1) unless (GetOptions(
        'config=s'       => \$config_file,
        'os'             => \$os_fprint_only,
        'intf=s'         => \$cmdline_intf,
        'fw-log=s'       => \$os_ipt_log,
        'fw-list'        => \$ipt_list,
        'fw-flush'       => \$ipt_flush,
        'debug'          => \$debug,
        'Kill'           => \$kill,
        'Restart'        => \$restart,
        'Status'         => \$status,
        '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 @path = qw(
        /bin
        /sbin
        /usr/bin
        /usr/sbin
        /usr/local/bin
        /usr/local/sbin
    );
    for my $cmd (keys %cmds) {
        unless (-x $cmds{$cmd}) {
            my $found = 0;
            PATH: for my $dir (@path) {
                if (-x "${dir}/${cmd}") {
                    $cmds{$cmd} = "${dir}/${cmd}";
                    $found = 1;
                    last PATH;
                }
            }
            unless ($found) {
                die "[*] Could not find $cmd anywhere!!!  Please edit the\n",
                    "config section in $config_file to include the path to\n",
                    "$cmd.";
            }
        }
        unless (-x $cmds{$cmd}) {
            die "[*] Command $cmd is located at $cmds{$cmd}, but ",
                "is not executable by uid: $<";
        }
    }
    return;
}

sub sendmail() {
    my $subject = shift;
    $subject =~ s/\"//g;
    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 uniquepid() {
    if (-e $config{'FWKNOPD_PID_FILE'}) {
        my $caller = $0;
        open PIDFILE, "< $config{'FWKNOPD_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{'FWKNOPD_PID_FILE'}" or die "[*] Could not open ",
        "$config{'FWKNOPD_PID_FILE'}: $!";
    print P $$, "\n";
    close P;
    chmod 0600, $config{'FWKNOPD_PID_FILE'};
    return;
}

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

sub stop_fwknop() {
    my $rv = 0;

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

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

    ### must kill knopwatchd first since if not, it might try to restart
    ### any of the other two daemons.
    for my $pidname qw(knopwatchd knopmd knoptm fwknop_serv fwknopd) {
        my $pidfile = $pidfiles{$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 "[+] Stopping $pidname, pid: $pid\n";
                unless (kill 15, $pid) {
                    kill 9, $pid or print "[*] fwknopd: Could not kill ",
                        "$pidname, pid: $pid $!\n";
                    $rv = 1;
                } else {
                    unlink $pidfile;
                }
            } else {
                my $print = 1;
                if ($config{'AUTH_MODE'} =~ /PCAP/ and ($pidname eq 'knopmd'
                        or $pidname eq 'knoptm')) {
                    $print = 0;
                }
                if ($config{'AUTH_MODE'} eq 'PCAP' and ($pidname eq 'knoptm'
                        or $pidname eq 'knoptm')) {
                    $print = 0;
                }
                print "[-] fwknopd: $pidname is not running.\n" if $print;
                $rv = 1;
            }
        } else {
            my $print = 1;
            if ($config{'AUTH_MODE'} =~ /PCAP/ and ($pidname eq 'knopmd'
                    or $pidname eq 'knoptm')) {
                $print = 0;
            }
            if ($config{'AUTH_MODE'} eq 'PCAP' and ($pidname eq 'knoptm'
                    or $pidname eq 'knoptm')) {
                $print = 0;
            }
            print "[-] fwknopd: pid file $pidfile does not exist for ",
                "$pidname.\n" if $print;
            $rv = 1;
        }
    }
    return $rv;
}

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

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

    print "[+] Restarting fwknop daemons.\n";
    if ($cmdline) {
        system "$cmds{'fwknopd'} $cmdline";
    } else {
        system $cmds{'fwknopd'};
    }
    return 0;
}

sub status() {
    my %pidfiles = (
        'knopwatchd' => $config{'KNOPWATCHD_PID_FILE'},
        'knopmd'     => $config{'KNOPMD_PID_FILE'},
        'knoptm'     => $config{'KNOPTM_PID_FILE'},
        'fwknopd'    => $config{'FWKNOPD_PID_FILE'}
    );
    for my $pidname qw(knopwatchd knopmd knoptm fwknopd) {
        my $pidfile = $pidfiles{$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/ and ($pidname eq 'knopmd'
                        or $pidname eq 'knoptm')) {
                    $print = 0;
                }
                if ($config{'AUTH_MODE'} eq 'PCAP' and $pidname eq 'knoptm') {
                    $print = 0;
                }
                print "[+] $pidname is not currently running.\n" if $print;
            }
        } else {
            my $print = 1;
            if ($config{'AUTH_MODE'} =~ /PCAP/ and ($pidname eq 'knopmd'
                    or $pidname eq 'knoptm')) {
                $print = 0;
            }
            if ($config{'AUTH_MODE'} eq 'PCAP' and $pidname eq 'knoptm') {
                $print = 0;
            }
            print "[+] $pidname pidfile does not exist.\n" if $print;
        }
    }
    return 0;
}

sub fwknop_init() {

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

    $config{'PCAP_INTF'} = $cmdline_intf if $cmdline_intf;

    ### import alerting config (for ALERTING_METHODS keyword)
    &import_config($alerting_config_file);

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

    ### validate config
    &validate_config();

    ### build iptables config from IPT_AUTO_CHAIN keywords
    &build_ipt_config();

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

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

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

    ### --ipt-list, lists rules in FWKNOP Netfilter chains
    exit &ipt_list() if $ipt_list;

    ### --ipt-flush, flush rules in FWKNOP Netfilter chains
    exit &ipt_flush() if $ipt_flush;

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

    ### make sure command paths are correct
    &check_commands() unless $os_fprint_only;

    &logr('[+]', 'starting fwknopd');

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

    ### import passive OS fingerprints (based on p0f)
    &import_p0f_sigs() if $config{'AUTH_MODE'} eq 'KNOCK';

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

        ### import access directives
        &import_access();

        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
            ERROR_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 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'} =~ /PCAP/) {
            ### make sure knopmd is not running
            &stop_daemon($config{'KNOPMD_PID_FILE'});

            ### 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') {
                if ($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", 1);
                }
                &stop_daemon($config{'TCPSERV_PID_FILE'});
                system $cmds{'fwknop_serv'};
            }
        } else {
            system $cmds{'knopmd'};
        }
        if ($config{'AUTH_MODE'} eq 'PCAP') {
            system $cmds{'knoptm'};
        } else {
            ### make sure knoptm is not running (only need it for the PCAP
            ### mode because we block until getting packet data)
            &stop_daemon($config{'KNOPTM_PID_FILE'});
        }
        system $cmds{'knopwatchd'} unless $debug;
    }

    if ($config{'AUTH_MODE'} =~ /PCAP/) {
        &import_md5_sums() if $config{'ENABLE_MD5_PERSISTENCE'} eq 'Y';
    }

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

    return;
}

sub ipt_list() {

    my %ipt_opts = (
        'iptables' => $cmds{'iptables'},
        'iptout'   => $config{'IPT_OUTPUT_FILE'},
        'ipterr'   => $config{'IPT_ERROR_FILE'}
    );
    $ipt_opts{'debug'}   = 1 if $debug;
    $ipt_opts{'verbose'} = 1 if $verbose;

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

    print "[+] Listing chains from IPT_AUTO_CHAIN keywords...\n";
    print "\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_aref, $err_aref) =
                $ipt->run_ipt_cmd("$cmds{'iptables'} -t " .
                    "$table -n -L $to_chain -v");

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

sub ipt_flush() {

    my %ipt_opts = (
        'iptables' => $cmds{'iptables'},
        'iptout'   => $config{'IPT_OUTPUT_FILE'},
        'ipterr'   => $config{'IPT_ERROR_FILE'}
    );
    $ipt_opts{'debug'}   = 1 if $debug;
    $ipt_opts{'verbose'} = 1 if $verbose;

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

    if ($ipt_flush) {
        print "[+] Flushing Netfilter IPT_AUTO_CHAIN chains...\n";
    } else {
        &logr('[+]', 'flushing existing Netfilter IPT_AUTO_CHAIN chains', 0);
    }
    if (@ipt_config) {
        for my $hr (@ipt_config) {
            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_aref, $err_aref)
                        = $ipt->flush_chain($table, $to_chain);
                if ($rv) {
                    print "[+] Flushed: $to_chain\n" if $ipt_flush;
                } else {
                    if ($ipt_flush) {
                        print "[-] Could not flush: $to_chain\n";
                        print for @$err_aref;
                    }
                }
            } else {
                print "[-] Chain: $to_chain does not exist.\n" if $ipt_flush;
            }
        }
    } else {
        print "[-] No valid IPT_AUTO_CHAIN keywords.\n" if $ipt_flush;
    }
    return 0;
}

sub stop_daemon() {
    my $pidfile = shift;
    return unless -e $pidfile;
    open PID, "< $pidfile" or die "[*] Could not open $pidfile: $!";
    my $pid = <PID>;
    close PID;
    chomp $pid;
    if (kill 0, $pid) {
        if (kill 15, $pid) {
            unlink $pidfile;
        } else {
            kill 9, $pid;
        }
    } else {
        unlink $pidfile;
    }
    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 "$prefix $msg\n";
    } else {
        unless ($config{'ALERTING_METHODS'} =~ /no.?syslog/i) {
            ### write a message to syslog
            openlog 'fwknopd', LOG_DAEMON, LOG_LOCAL7;
            syslog LOG_INFO, $msg;
            closelog();
        }

        ### see if we need to send an email
        if ($send_email and $config{'ALERTING_METHODS'} !~ /noe?mail/i) {
            &sendmail("$prefix fwknopd: $msg");
        }
    }
    return;
}

sub psyslog_errs() {
    my $aref = shift;
    return if $config{'ALERTING_METHODS'} =~ /no.?syslog/i;

    ### write a message to syslog
    openlog 'fwknopd', LOG_DAEMON, LOG_LOCAL7;
    for (my $i=0; $i<5 && $i<=$#$aref; $i++) {
        syslog LOG_INFO, $aref->[$i];
    }
    closelog();
    return;
}

sub required_vars() {
    for my $var qw(FW_DATA_FILE SLEEP_INTERVAL FWKNOP_DIR FWKNOPD_PID_FILE
            KNOPMD_PID_FILE KNOPWATCHD_PID_FILE CMDLINE_FILE P0F_FILE
            ACCESS_CONF MAX_HOPS EMAIL_ADDRESSES ALERTING_METHODS
            IPT_AUTO_CHAIN1 AUTH_MODE PCAP_CMD_TIMEOUT ENABLE_PCAP_PROMISC
            PCAP_FILTER KNOPTM_IP_TIMEOUT_SOCK ENABLE_MD5_PERSISTENCE
            MD5_FILE FLUSH_IPT_AT_INIT PCAP_INTF ERROR_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) {

        die "[*] Required variable $var is not defined in $config_file"
            unless defined $config{$var};
    }
    return;
}

sub build_ipt_config() {

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

    my %ipt_opts = (
        'iptables' => $cmds{'iptables'},
        'iptout'   => $config{'IPT_OUTPUT_FILE'},
        'ipterr'   => $config{'IPT_ERROR_FILE'}
    );
    $ipt_opts{'debug'}   = 1 if $debug;
    $ipt_opts{'verbose'} = 1 if $verbose;

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

    my $ctr = 1;

    VAR: while (defined $config{"IPT_AUTO_CHAIN$ctr"}) {
        my $value = $config{"IPT_AUTO_CHAIN$ctr"};

        my @block = split /\s*,\s*/, $value;
        if ($#block == 4 or $#block == 6) {
            my %hsh = ();
            if ($#block == 4) {
                ### ACCEPT, src, filter, INPUT, FWKNOP_INPUT;
                %hsh = (
                    'target'     => $block[0],
                    'direction'  => $block[1],
                    'table'      => $block[2],
                    'from_chain' => $block[3],
                    'to_chain'   => $block[4],
                    'jump_rule_position' => 1,
                    'auto_rule_position' => 1
                );
                ### this is the old format; generate a warning
                my $msg = "the IPT_AUTO_CHAIN$ctr 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);
                    print STDERR "[-] build_ipt_config(): $msg\n"
                        if $debug;
            } else {
                ### ACCEPT, src, filter, INPUT, 1, FWKNOP_INPUT, 1;
                %hsh = (
                    'target'     => $block[0],
                    'direction'  => $block[1],
                    'table'      => $block[2],
                    'from_chain' => $block[3],
                    'jump_rule_position' => $block[4],
                    'to_chain'   => $block[5],
                    'auto_rule_position' => $block[6]
                );
            }
            unless ($hsh{'direction'} eq 'src' or
                        $hsh{'direction'} eq 'dst' or
                        $hsh{'direction'} eq 'both') {
                my $msg = "invalid direction $hsh{'direction'} " .
                    "in IPT_AUTO_CHAIN$ctr keyword";
                &logr('[-]', $msg);
                print STDERR "[-] build_ipt_config(): $msg\n"
                    if $debug;
                next VAR;
            }
            if ($hsh{'from_chain'} eq $hsh{'to_chain'}) {
                my $msg = "cannot have identical from_chain and to_chain " .
                    "in IPT_AUTO_CHAIN$ctr keyword";
                &logr('[-]', $msg);
                print STDERR "[-] build_ipt_config(): $msg\n"
                    if $debug;
                next VAR;
            }
            my ($rv, $out_aref, $err_aref)
                = $ipt->chain_exists($hsh{'table'}, $hsh{'from_chain'});

            if ($rv) {
                push @ipt_config, \%hsh;
            } else {
                my $msg = "invalid IPT_AUTO_CHAIN$ctr keyword, " .
                    "$hsh{'from_chain'} chain does not exist.";
                &logr('[-]', $msg);
                print STDERR "[-] build_ipt_config(): $msg\n"
                    if $debug;
            }
        } else {
            my $msg = "invalid IPT_AUTO_CHAIN$ctr variable: $value";
            &logr('[-]', $msg);
            print STDERR "[-] build_ipt_config(): $msg\n" if $debug;
        }
        $ctr++;
    }
    return;
}

sub diskwrite_md5_sum() {
    my $md5_sum = shift;

    open F, ">> $config{'MD5_FILE'}" or die "[*] Could not open ",
        "$config{'MD5_FILE'}: $!";
    print F $md5_sum, "\n";
    close F;
    return;
}

sub import_md5_sums() {
    return unless -e $config{'MD5_FILE'};
    open F, "< $config{'MD5_FILE'}" or die "[*] Could not open ",
        "$config{'MD5_FILE'}: $!";
    while (<F>) {
        if ($_ =~ /^\s*(\S+)/) {
            $md5_msg_store{$1} = '';
        }
    }
    close F;
    &logr('[+]', "imported previous md5 sums from disk " .
        "cache: $config{'MD5_FILE'}", 0);
    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{'AUTH_MODE'} eq 'KNOCK'
            or $config{'AUTH_MODE'} eq 'FILE_PCAP'
            or $config{'AUTH_MODE'} eq 'ULOG_PCAP'
            or $config{'AUTH_MODE'} eq 'PCAP') {
        die "[*] AUTH_MODE must be either KNOCK, FILE_PCAP, ULOG_PCAP or PCAP";
    }
    return;
}

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

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

sub REAPER {
    my $pid;
    $pid = waitpid(-1, WNOHANG);
#   if (WIFEXITED($?)) {
#          print STDERR "[+] **  Process $pid exited.\n";
#      }
    $SIG{'CHLD'} = \&REAPER;
    return;
}

sub null_func() {
    return;
}

sub usage() {
    my $exit_status = shift;
    print <<_HELP_;
fwknopd
[+] Version: $version (file revision: $rev_num)
[+] By Michael Rash (mbr\@cipherdyne.org, http://www.cipherdyne.org/)

Usage: fwknopd [-c <config file>] [-d] [-R] [-K] [-S] [--os] [-V] [-h]
              [-i <interface>] [-v] [--Status] [-c <config file>]
              [--fw-log <file>] [--fw-list] [--fw-flush]

Options:
    -c, --config <file>        - Specify path to config file instead of
                                 using the default $config_file.
    -o, --os                   - Parse Netfilter logs and fingerprint
                                 operating systems from which tcp SYN
                                 packets have been logged.
    -i, --intf <interface>     - Manually specify interface on which to
                                 sniff.
    --fw-log <file>            - Specify path to Netfilter logfile. This
                                 is used only when running in --os mode.
    --fw-list                  - List all active rules in the FWKNOP
                                 Netfilter chain(s).
    --fw-flush                 - Flush all active rules in the FWKNOP
                                 Netfilter chain(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.
    -d, --debug                - Run fwknopd in debugging mode.
    -v, --verbose              - Verbose mode.
    -V, --Version              - Display version and exit.
    -h, --help                 - Print help and exit.
_HELP_

    exit $exit_status;
}
