#!/usr/bin/perl -w
#
#############################################################################
#
# File: fwknop
#
# Purpose: fwknop combines port knocking functionality with passive OS
#          fingerprinting, and uses iptables log messages as the
#          communication transport between the port-knocking server and
#          the client.
#
# Author: Michael Rash (mbr@cipherdyne.org)
#
# Version: 0.9.1
#
# Copyright (C) 2004 Michael Rash (mbr@cipherdyne.org)
#
# License (GNU Public License):
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
#    You should have received a copy of the GNU General Public License
#    along with this program; if not, write to the Free Software
#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307
#    USA
#
#############################################################################
#
# $Id: fwknop,v 1.101 2005/07/29 08:53:58 mbr Exp $
#

use lib '/usr/lib/fwknop';
use Crypt::CBC;
use Unix::Syslog qw(:subs :macros);
use Net::IPv4Addr qw(ipv4_in_network);
use Net::Ping::External qw(ping);
use Digest::MD5 'md5_base64';
use IO::Socket;
use IO::Handle;
use MIME::Base64;
use Data::Dumper;
use POSIX;
use Term::ReadKey;
use Getopt::Long;
use strict;

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

my $version = '0.9.1';

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 $run_last_args  = 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 $cmdl_homedir   = '';
my $knock_sleep    = 1;  ### default to 1 second difference between port knocks
my $knock_dst      = '';
my $spoof_src      = '';
my $os_ipt_log     = '';
my $warn_msg       = '';
my $die_msg        = '';
my $server_mode    = 'pcap';
my $cmdline_pcap_cmd   = '';
my $skipped_first_loop = 0;
my $max_msg_len = 1500;

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

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

my $spoof_cmd = '/usr/sbin/knopspoof';
my $spoof_cache_file = '/tmp/.fwknopspoof.cache';
### default to root (client must run as root in this mode)
my $spoof_username = 'root';
my $spoof_proto = 'udp';  ### default to udp

### encrypted port knock vars
my $encrypt           = 0;
my $cmdline_offset    = 0;
my $enc_port_offset   = 61000;  ### default offset
my $enc_key           = '';
my $enc_alg           = 'Rijndael';
my $enc_init_vector   = 'fwkn0pec';
my $enc_blocksize     = 16;
my $enc_shared_secret = '';
my $enc_allow_ip      = '';
my $enc_source_ip     = '';
my $enc_allow_port    = 0;
my $enc_allow_proto   = '';
my $enc_rotate_proto  = 0;
my $get_key_file      = 0;  ### get key from file
my $enc_pcap_port     = 62201;  ### default pcap port
my $access_str        = '';

### 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 = '(?:\d{1,3}\.){3}\d{1,3}';

my @args_cp = @ARGV;

### make Getopts case sensitive
Getopt::Long::Configure('no_ignore_case');
&usage(1) unless (GetOptions(
    'config=s'      => \$config_file,
    'Server-port=i' => \$enc_pcap_port,
    'Server-mode=s' => \$server_mode,
    'Server-cmd=s'  => \$cmdline_pcap_cmd,
    'Spoof-src=s'   => \$spoof_src,
    'Spoof-cmd=s'   => \$spoof_cmd,
    'Spoof-file=s'  => \$spoof_cache_file,
    'Spoof-user=s'  => \$spoof_username,
    'Spoof-proto=s' => \$spoof_proto,
    'user-rc=s'     => \$user_rc_file,
    'knock-dst=s'   => \$knock_dst,
    'encrypt'       => \$encrypt,
    'Access=s'      => \$access_str,
    'allow-ip=s'    => \$enc_allow_ip,
    'source-ip'     => \$enc_source_ip,
    'port=i'        => \$enc_allow_port,
    'Proto=s'       => \$enc_allow_proto,
    'rotate-proto'  => \$enc_rotate_proto,
    'offset=i'      => \$cmdline_offset,
    'os'            => \$os_fprint_only,
    'time-delay=i'  => \$knock_sleep,
    'ipt-log=s'     => \$os_ipt_log,
    'ipt-list'      => \$ipt_list,
    'ipt-flush'     => \$ipt_flush,
    'last-cmd'      => \$run_last_args,
    'get-key=s'     => \$get_key_file,
    'Home-dir=s'    => \$cmdl_homedir,
    'debug'         => \$debug,
    'Kill'          => \$kill,
    'Restart'       => \$restart,
    'Status'        => \$status,
    'verbose'       => \$verbose,
    'Version'       => \$print_version,
    'help'          => \$print_help
));

&usage(0) if $print_help;

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

### run fwknop with same command line args as the previous
### execution
&run_last_cmdline() if $run_last_args;

if ($knock_dst) {

    print "[+] ** Running in client debug mode. **\n" if $debug;
    print "[+] Starting fwknop client...\n";

    unless ($knock_dst =~ /$ip_re/) {
        print "[+] Resolving hostname: $knock_dst\n";
        ### resolve to an IP
        my $iaddr = inet_aton($knock_dst)
            or die "[*] Could not resolve $knock_dst to IP.";
        my $addr = inet_ntoa($iaddr)
            or die "[*] Could not resolve $knock_dst to IP.";
        $knock_dst = $addr;
    }
    if ($server_mode =~ /pcap/i) {
        $encrypt = 1;
    } elsif ($server_mode =~ /knock/i) {
    } else {
        die "[*] Unknown server mode: $server_mode ",
            "(must be \"pcap\" or \"knock\"\n";
    }

    &validate_access_str() if $access_str;

    if ($encrypt) {
        die "[*] Must also specify: -k <knock destination>\n"
            unless $knock_dst;

        unless ($cmdline_pcap_cmd) {
            unless ($enc_allow_ip or $enc_source_ip) {
                die "[*] Must either specify: --allow-ip <IP>, ",
                    "or --source-ip\n";
            }

            ### make fwknop server see "0.0.0.0" in the encrypted sequence.
            ### This will instruct the server to open the port for whatever
            ### source IP the sequence comes from.  This is useful for
            ### clients that are behind a NAT device.
            $enc_allow_ip = '0.0.0.0' if $enc_source_ip;

            unless ($server_mode =~ /pcap/i) {
                die "[*] Must also specify: -P <proto> "
                    unless $enc_allow_proto;

                unless ($enc_allow_proto =~ /tcp/i or
                        $enc_allow_proto =~ /udp/i or
                        $enc_allow_proto =~ /icmp/i) {
                    die "[*] --Proto must either be tcp, udp, or icmp";
                }
            }
            if ($spoof_src) {
                $< == 0 && $> == 0 or
                    die '[*] You must be root (or equivalent ',
                        "UID 0 account) to spoof the source address.\n";
            }
            if ($enc_allow_proto =~ /icmp/i) {
                $enc_allow_port = 0;
            } else {
                unless ($server_mode =~ /pcap/i) {
                    unless ($enc_allow_port) {
                        die "[*] Must also specify: -p <port> ";
                    }
                }
            }
            if ($enc_allow_proto =~ /tcp/i or
                    $enc_allow_proto =~ /udp/i) {
                unless ($enc_allow_port < 65536 and $enc_allow_port >= 0) {
                    die "[*] --port must be 0 < port < 65536";
                }
            }
            unless ($enc_allow_ip =~ /$ip_re/) {
                ### resolve to an IP
                my $iaddr = inet_aton($enc_allow_ip)
                    or die "[*] Could not resolve $enc_allow_ip to IP.";
                my $addr = inet_ntoa($iaddr)
                    or die "[*] Could not resolve $enc_allow_ip to IP.";
                $enc_allow_ip = $addr;
            }
        }
        if ($cmdline_offset) {
            if ($server_mode =~ /pcap/i) {
                die "[*] Port offset is meaningless in pcap mode ",
                    "(only a single packet is sent).";
            }
            unless ($cmdline_offset < 65280 and $cmdline_offset > 0) {
                die "[*] Port offset must be 0 < port < 65280";
            }
            $enc_port_offset = $cmdline_offset;
        }
        if ($server_mode =~ /pcap/i) {
            unless ($enc_pcap_port < 65535 and $enc_pcap_port > 0) {
                die "[*] Port offset must be 0 < port < 65535";
            }
        }
    } else {
        if ($enc_rotate_proto) {
            die '[*] Can only specify --rotate-proto with ',
                'encrypted sequences (-e).';
        }
    }

    ### save our command line args (so -l can be used next time
    &save_args() unless $run_last_args;

    if ($encrypt) {

        ### get the encryption key from the --get-key file
        ### or from STDIN if it's not in the file.
        &get_key();

        if ($server_mode =~ /pcap/i) {

            ### construct and send the encrypted message to the server
            ### (sends a single packet).
            &pcap_send_encrypted_msg(&pcap_build_enc_msg());

        } else {
            ### we are running in port knocking mode, so get the
            ### encrypted port sequence (16 ports)
            &knock_ports(&encrypt_sequence());
        }
    } else {
        ### we are running in port knocking mode, so get the port
        ### sequence
        &knock_ports(&import_shared_sequence());
    }
} else {  ### we are running in server mode

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

    print STDERR "[+] ** Starting fwknop (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 '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 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'}/fwknop.die" or
                die "[*] Could not open $config{'FWKNOP_DIR'}/fwknop.die: $!";
            print D scalar localtime(), " $die_msg";
            close D;
            $die_msg = '';
        }

        if ($warn_msg) {
            open D, ">> $config{'FWKNOP_DIR'}/fwknop.warn" or
                die "[*] Could not open $config{'FWKNOP_DIR'}/fwknop.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_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 $udp_data   = '';
    my $proto      = '';

    if ($config{'AUTH_MODE'} eq 'ULOG_PCAP') {
        $ip = NetPacket::IP->decode($pkt);
        $udp_data = NetPacket::UDP->decode(NetPacket::IP::ip_strip($pkt));
    } else {
        $ether_data = NetPacket::Ethernet::strip($pkt);
        $ip = NetPacket::IP->decode($ether_data);
        $udp_data = NetPacket::UDP->decode($ip->{'data'});
    }

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

    print STDERR "[+] Received data: $udp_data->{'data'}\n"
        if $debug;

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

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

        next NUM unless defined $access_vars_href->{'ULOG_PCAP'}
            or defined $access_vars_href->{'PCAP'};

        print STDERR Dumper $access_vars_href if $debug and $verbose;

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

        ### see if we can decrypt and base64-decode
        my $decrypted_msg =
            &pcap_decrypt_msg($udp_data->{'data'}, $access_vars_href->{'KEY'});

        if ($decrypted_msg) {
            print STDERR "[+] Decrypted message: $decrypted_msg\n" if $debug;
        } else {
            print STDERR "[-] Failed decrypt for SOURCE block ",
                "$access_vars_href->{'SOURCE'}\n" if $debug;
            next NUM;
        }

        ### compare with md5 store (note we calculated the md5 sum
        ### against the original encrypted packet since we want to
        ### allow the same command to be executed; randomness in
        ### the encrypted packet changes the md5 sum, we just don't
        ### want the exact same encrypted packet to be replayed).
        my $md5sum = md5_base64($decrypted_msg);

        if (defined $md5_msg_store{$md5sum}) {
            ### Bad!  Just return.
            &logr('[-]', "attempted message replay from: $src_ip", 1);
            return;
        }

        ### store the md5 sum
        $md5_msg_store{$md5sum} = '';

        ### see if we have a valid message
        my $msg_href = &pcap_validate_msg($decrypted_msg);

        unless ($msg_href) {
            print STDERR "[-] Shared key mis-match or broken message ",
                "checksum for SOURCE $access_vars_href->{'SOURCE'}\n"
                if $debug;
            next NUM;
        }

        print "[+] Packet fields:\n",
            "      Random number: $msg_href->{'random_number'}\n",
            "      Username: $msg_href->{'username'}\n",
            "      Remote timestamp: $msg_href->{'remote_time'}\n",
            "      Remote version: $msg_href->{'remote_version'}\n",
            "      Action type: $msg_href->{'action_type'}\n",
            "      Action: $msg_href->{'action'}\n",
            "      MD5 sum: $msg_href->{'md5sum'}\n" if $debug;

        if (defined $access_vars_href->{'REQUIRE_USERNAME'}) {
            unless ($access_vars_href->{'REQUIRE_USERNAME'}
                    eq $msg_href->{'username'}) {
                &logr('[+]', "username mis-match from $src_ip, expecting " .
                    "$access_vars_href->{'REQUIRE_USERNAME'}, got " .
                    "$msg_href->{'username'}", 0);
                next NUM;
            }
        }

        ### all criteria met; grant access or execute command
        &logr('[+]', "received valid encrypted packet from: $src_ip", 0);

        if ($msg_href->{'action_type'} == $access_mode) {
            if (not defined $access_vars_href->{'DISABLE_FW_ACCESS'}) {
                if (not defined $access_vars_href->{'PERMIT_CLIENT_PORTS'}) {

                    ### we don't allow the client to influence which ports
                    ### are opened; only those that are already defined in
                    ### OPEN_PORTS will be opened
                    if ($msg_href->{'action'} =~ /($ip_re)/) {
                        my $allow_ip = $1;

                        $allow_ip = $src_ip if $allow_ip eq '0.0.0.0';

                        &grant_access($allow_ip, '', $access_vars_href);
                    }
                } else {
                    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;

                        $allow_ip = $src_ip if $allow_ip eq '0.0.0.0';

                        $access_vars_href->{'OPEN_PORTS'}
                            ->{$dec_allow_proto}->{$dec_allow_port} = '';

                        &grant_access($allow_ip, '', $access_vars_href);

                    } elsif ($msg_href->{'action'}
                             =~ /($ip_re),none,0/) {
                        my $allow_ip   = $1;
                        $allow_ip = $src_ip if $allow_ip eq '0.0.0.0';

                        &grant_access($allow_ip, '', $access_vars_href);

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

                        $allow_ip = $src_ip if $allow_ip eq '0.0.0.0';
                        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';
                                $access_vars_href->{'OPEN_PORTS'}
                                    ->{$proto}->{$port} = '';
                            }
                        }
                        &grant_access($allow_ip, '', $access_vars_href);
                    }
                }
            } else {
                &logr('[-]', "received fw access request from $src_ip, " .
                    "but DISABLE_FW_ACCESS is set", 0);
            }
        } elsif ($msg_href->{'action_type'} == $command_mode) {
            if ($access_vars_href->{'ENABLE_CMD_EXEC'}) {
                my $regex_match = 1;
                if (defined $access_vars_href->{'CMD_REGEX'}) {
                    $regex_match = 0 unless $msg_href->{'action'}
                        =~ /$access_vars_href->{'CMD_REGEX'}/;
                }
                if ($regex_match) {
                    ### execute the command
                    &logr('[+]',
                        "executing command $msg_href->{'action'} " .
                        "for $src_ip", 1);
                    &exec_command($msg_href->{'action'});
                } else {
                    &logr('[-]',
                        "received command \"$msg_href->{'action'} " .
                            "from $src_ip\" but CMD_REGEX did not " .
                            "match $src_ip", 1);
                }
            } else {
                &logr('[-]',
                    "received command \"$msg_href->{'action'}\" " .
                        "but command mode not enabled for $src_ip", 1);
            }
        }
    }
    return;
}

sub pcap_validate_msg() {
    my $msg = shift;

    my $pre_msg = '';
    if ($msg =~ /(.*):/) {
        $pre_msg = $1;
    }

    my %msg_hash = ();
    my @fields = split /:/, $msg;

    return '' unless @fields;

    my $random_number  = $fields[0] || return '';
    my $username       = $fields[1] || return '';
    my $remote_time    = $fields[2] || return '';
    my $remote_version = $fields[3] || return '';
    my $action_type    = $fields[4];
    my $action         = $fields[5] || return '';
    my $md5sum         = $fields[6] || return '';

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

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

    print "[+] Decoded message: $msg_hash{'random_number'}:",
        "$msg_hash{'username'}:$msg_hash{'remote_time'}:",
        "$msg_hash{'remote_version'}:$msg_hash{'action_type'}:",
        "$msg_hash{'action'}:$msg_hash{'md5sum'}\n" if $debug;

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

    ### valid message
    return \%msg_hash if $md5sum eq md5_base64($pre_msg);

    return '';
}

sub pcap_loop() {
    print STDERR "[+] pcap_loop()\n" if $debug;

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

    if ($config{'AUTH_MODE'} eq 'ULOG_PCAP') {
        die "[*] ulog pcap file: $config{'ULOG_PCAP_FILE'} does not exist.\n",
            "    Is ulogd running and setup with the pcap writer?"
            unless -e $config{'ULOG_PCAP_FILE'};

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

        if ($config{'ULOG_PCAP_FILTER'} ne 'NONE') {
            ### set the filter on the traffic
            Net::Pcap::compile($pcap_t, \$filter, $config{'ULOG_PCAP_FILTER'},
                0, '0.0.0.0')
                && die '[*] Unable to compile packet capture filter';
            Net::Pcap::setfilter($pcap_t, $filter)
                && die '[*] Unable to set packet capture filter';
        }
    } else {
        if ($config{'ENABLE_PCAP_PROMISC'} eq 'Y') {
            $pcap_t = Net::Pcap::open_live($config{'PCAP_INTF'},
                1500, 1, 100, \$err) or die "[*] Could not open ",
                    "$config{'PCAP_INTF'}: $!";
        } else {
            $pcap_t = Net::Pcap::open_live($config{'PCAP_INTF'},
                1500, 1, 100, \$err) or die "[*] Could not open ",
                    "$config{'PCAP_INTF'}: $!";
        }
        if ($config{'PCAP_FILTER'} ne 'NONE') {
            if (Net::Pcap::lookupnet($config{'PCAP_INTF'}, \$address,
                    \$netmask, \$err)) {
                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';
        }
    }

    if ($config{'AUTH_MODE'} eq 'ULOG_PCAP') {
        ### get past any packets that were from a previous fwknop
        ### execution.
        Net::Pcap::loop($pcap_t, -1, \&null_func, 'fwknop_tag');
    }

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

        if ($config{'AUTH_MODE'} eq 'ULOG_PCAP') {
            ### always check to see if we need to timeout access for IPs
            ### For AUTH_MODE set to PCAP, knoptm will timeout access
            ### (see the 
            &timeout_access();
        }

        sleep 1;
    }

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

    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;
        exec qq{$cmd};
    }
    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_vars_href = $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_vars_href->{'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_vars_href->{'ENCRYPT_SEQUENCE'}) {
                if ($dp >= $access_vars_href->{'PORT_OFFSET'} and
                        $dp < $access_vars_href->{'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_vars_href);

                    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_vars_href)) {
                            delete $ip_sequences{$src}{$num};
                            next NUM;
                        }

                        ### see if we need to match the username
                        unless (&matched_username($username,
                                $access_vars_href)) {
                            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_vars_href->{'KNOCK_LIMIT'}) {
                            if ($seq_href->{'grant_ctr'}
                                    > $access_vars_href->{'KNOCK_LIMIT'}) {
                                &logr('[-]', "$src exceeded knock limit (set to " .
                                    "$access_vars_href->{'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
                        $access_vars_href->{'OPEN_PORTS'}
                            ->{$dec_allow_proto}->{$dec_allow_port} = '';
                        &grant_access($allow_ip, $seq_href, $access_vars_href);

                    }
                    delete $ip_sequences{$src}{$num};
                    next NUM;
                }
            } elsif (defined $access_vars_href->{'SHARED_SEQUENCE'}) {
                $seq_href->{'port_seq'} = 0
                    unless defined $seq_href->{'port_seq'};
                if ($dp == $access_vars_href->{'SHARED_SEQUENCE'}->
                            [$seq_href->{'port_seq'}]->{'port'}
                        and $proto eq $access_vars_href->{'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_vars_href);

                    ### if we made it to the end of the sequence then we have
                    ### a correct knock sequence
                    if ($seq_href->{'port_seq'}
                            == $#{$access_vars_href->{'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_vars_href->{'KNOCK_LIMIT'}) {
                    if ($seq_href->{'grant_ctr'}
                            > $access_vars_href->{'KNOCK_LIMIT'}) {
                        &logr('[-]', "$src exceeded knock limit (set to " .
                            "$access_vars_href->{'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_vars_href);
            }
        }
        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 knock_ports() {
    my $ports_aref = shift;

    print "[+] Sending port knocking sequence to knock server: $knock_dst\n";
    for my $href (@$ports_aref) {
        my $proto = $href->{'proto'};
        my $port  = $href->{'port'};
        ### note that we never care if the destination replies with a
        ### RST or icmp echo reply (or anything else).  In fact, hopefully
        ### the remote firewall is configued to not reply at all
        if ($proto eq 'icmp') {
            print "[+] icmp echo request -> $knock_dst\n";
            ping(hostname => "$knock_dst", count => 1, timeout => 1);
            sleep $knock_sleep;
        } else {
            printf "%-14s%s\n", "[+] $proto/$port", "-> $knock_dst";
            my $socket = IO::Socket::INET->new(
                PeerAddr => $knock_dst,
                PeerPort => $port,
                Proto    => $proto,
                Timeout  => 1
            );  ### note there is no "or die" here since we just want to throw
                ### packets on the network
            if (defined $socket and $proto eq 'udp') {
                $socket->send('0');  ### have to actually send something for udp
                sleep $knock_sleep;
            }
            if ($proto eq 'tcp' and $knock_sleep > 1) {
                sleep $knock_sleep;
            }
            undef $socket if defined $socket;
        }
    }
    print "[+] Finished knock sequence.\n";
    return;
}

sub check_src() {
    my $src = shift;

    my @access_nums = ();

    for (my $i=0; $i<=$#access; $i++) {
        my $access_href = $access[$i];
        my $type = $access_href->{'TYPE'};
        if ($type eq 'ip') {
            if ($src eq $access_href->{'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_href->{'SOURCE'}, $src)) {
                print STDERR "[+] Packet from $src matched NET SOURCE: ",
                    "$access_href->{'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_href->{'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_href->{'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_vars_href) = @_;
    if (defined $access_vars_href->{'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_vars_href->{'MAX_TIME_DIFF'}) {
                my $time = time();
                if (($time - $seq_href->{'port_times'}[$seq_href->{'port_seq'}-1])
                            > $access_vars_href->{'MIN_TIME_DIFF'} and
                        ($time - $seq_href->{'port_times'}[$seq_href->{'port_seq'}-1])
                            < $access_vars_href->{'MAX_TIME_DIFF'}) {
                    print STDERR "[+] Sequence min/max time match: ",
                        "($seq_href->{'port_seq'}) ",
                        "$access_vars_href->{'SHARED_SEQUENCE'}->[$seq_href->{'port_seq'}]->{'proto'}/",
                        "$access_vars_href->{'SHARED_SEQUENCE'}->[$seq_href->{'port_seq'}]->{'port'}\n"
                        if $debug;
                } else {
                    &logr('[-]', 'Sequence min/max_time exceeded: ' .
                        "$access_vars_href->{'SHARED_SEQUENCE'}->[$seq_href->{'port_seq'}]->{'proto'}/" .
                        "$access_vars_href->{'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_vars_href->{'MIN_TIME_DIFF'}) {
                    print STDERR "[+] Sequence min_time match: ",
                        "($seq_href->{'port_seq'}) ",
                        "$access_vars_href->{'SHARED_SEQUENCE'}->[$seq_href->{'port_seq'}]->{'proto'}/",
                        "$access_vars_href->{'SHARED_SEQUENCE'}->[$seq_href->{'port_seq'}]->{'port'}\n"
                        if $debug;
                } else {
                    &logr('[-]', "Sequence min_time (" .
                        "$access_vars_href->{'MIN_TIME_DIFF'} seconds) not met: " .
                        "$access_vars_href->{'SHARED_SEQUENCE'}->[$seq_href->{'port_seq'}]->{'proto'}/" .
                        "$access_vars_href->{'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_vars_href->{'SHARED_SEQUENCE'}->[$seq_href->{'port_seq'}]->{'proto'}/",
                "$access_vars_href->{'SHARED_SEQUENCE'}->[$seq_href->{'port_seq'}]->{'port'}\n"
                if $debug;
        }
    } elsif (defined $access_vars_href->{'MAX_TIME_DIFF'}) {
        if ($seq_href->{'port_seq'} > 0) {
            if ((time()
                    - $seq_href->{'port_times'}[$seq_href->{'port_seq'}-1])
                    < $access_vars_href->{'MAX_TIME_DIFF'}) {
                print STDERR "[+] Sequence max_time match: ",
                    "($seq_href->{'port_seq'}) ",
                    "$access_vars_href->{'SHARED_SEQUENCE'}->[$seq_href->{'port_seq'}]->{'proto'}/",
                    "$access_vars_href->{'SHARED_SEQUENCE'}->[$seq_href->{'port_seq'}]->{'port'}\n"
                    if $debug;
            } else {
                &logr('[-]', "Sequence max_time ($access_vars_href->{'MAX_TIME_DIFF'} seconds) exceeded: " .
                    "$access_vars_href->{'SHARED_SEQUENCE'}->[$seq_href->{'port_seq'}]->{'proto'}/" .
                    "$access_vars_href->{'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_vars_href->{'SHARED_SEQUENCE'}->[$seq_href->{'port_seq'}]->{'proto'}/",
                "$access_vars_href->{'SHARED_SEQUENCE'}->[$seq_href->{'port_seq'}]->{'port'}\n"
                if $debug;
        }
    } else {
        print STDERR "[+] Sequence match: ($seq_href->{'port_seq'}) ",
            "$access_vars_href->{'SHARED_SEQUENCE'}->[$seq_href->{'port_seq'}]->{'proto'}/",
            "$access_vars_href->{'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_build_enc_msg() {

    ### message format (all fields are separated by ":" characters
    #
    #  random number (16 bytes)
    #  username
    #  timestamp
    #  software version
    #  mode (command mode (0) or access mode (1))
    #  if command mode => command to execute
    #  if access mode  => IP,proto,port
    #  message md5 sum

    ### some of the fields below might happen to contain ":" chars,
    ### so we base64 encode them

    print "[+] Building encrypted single-packet authorization message...\n";

    my $user = '';
    if ($spoof_src) {
        $user = $spoof_username;
    } else {
        ### getlogin() is better than using ENV{'USER'}, which is
        ### easily manipulated, so only use as a last resort.
        $user = getlogin() || getpwuid($<) ||
            die '[*] Could not determine user.'
    }

    my $data_rand = int(rand(100000000000000));
    $data_rand .= int(rand(10)) while (length($data_rand) < 16);

    my $timestamp = time();

    print "[+] Packet fields:\n",
        "      Random number: $data_rand\n",
        "      Username: $user\n",
        "      Timestamp: $timestamp\n",
        "      Version: $version\n";

    ### append username and timestamp
    my $msg = $data_rand . ':' . encode_base64($user) .
        ':' . $timestamp . ':' . $version;

    if ($cmdline_pcap_cmd) {
        ### a specific command will be executed on the server
        $msg .= ":$command_mode:" . encode_base64($cmdline_pcap_cmd);

        print "      Action: $command_mode (command mode)\n",
            "    Cmd: $cmdline_pcap_cmd\n";
    } else {
        ### access to port(s)/protocol(s) will be granted on the
        ### server
        $msg .= ":$access_mode:";
        print "      Action: $access_mode (access mode)\n";
        if ($enc_allow_proto) {
            $msg .=
                encode_base64("$enc_allow_ip,$enc_allow_proto,$enc_allow_port");
            print "      Access: $enc_allow_ip,$enc_allow_proto,",
                "$enc_allow_port\n";
        } else {
            if ($access_str) {
                $msg .= encode_base64("$enc_allow_ip,$access_str");
                print "      Access: $enc_allow_ip,$access_str\n";
            } else {
                $msg .= encode_base64("$enc_allow_ip,none,0");
                print "      Access: $enc_allow_ip,none,0\n";
            }
        }
    }

    $msg =~ s/\n//g;

    ### calculate md5 hash over entire message
    my $md5sum = md5_base64($msg);
    $msg .= ":$md5sum";

    print "      MD5 sum: $md5sum\n";
    print "[+] Clear text message: $msg\n" if $debug;

    return &pcap_encrypt_msg($msg);
}

sub pcap_encrypt_msg() {
    my $msg = shift;
    my $cipher = Crypt::CBC->new(
        {
            'key'             => $enc_key,
            'cipher'          => $enc_alg,
            'iv'              => $enc_init_vector,
            'prepend_iv'      => 0,
            'regenerate_key'  => 0,
        }
    );
    my $encoded_msg = encode_base64($cipher->encrypt($msg));

    ### remove trailing "==" (the decrypt function will put
    ### them back before attempting to decrypt)... this is to
    ### make it more difficult for an IDS to detect fwknop
    ### traffic
    $encoded_msg =~ s/=*$//;
    $encoded_msg =~ s/\n//g;

    print "[+] Encrypted message: $encoded_msg\n" if $debug;
    return $encoded_msg;
}

sub pcap_send_encrypted_msg() {
    my $msg = shift;

    my $msg_len = length($msg);

    if ($msg_len > $max_msg_len) {
        die "[*] Message length is too long ($msg_len bytes), ",
            "must be less than $max_msg_len bytes";
    }

    if ($spoof_src) {
        unless ($spoof_src =~ /$ip_re/) {
            ### resolve to an IP
            my $iaddr = inet_aton($spoof_src)
                or die "[*] Could not resolve $spoof_src to IP.";
            my $addr = inet_ntoa($iaddr)
                or die "[*] Could not resolve $spoof_src to IP.";
            $spoof_src = $addr;
        }

        print
"[+] Sending $msg_len byte message to $knock_dst over $spoof_proto",
    "/$enc_pcap_port\n    (spoofed src ip: $spoof_src).\n";
        my $rand_src_port = int(rand(65535));
        $rand_src_port = 65001 if $rand_src_port > 65535;
        $rand_src_port += 1024 if $rand_src_port < 1024;
        open S, "> $spoof_cache_file"
            or die "[*] Could not open $spoof_cache_file: $!";
        print S "$spoof_src $knock_dst $spoof_proto $rand_src_port ",
            "$enc_pcap_port $msg\n";
        close S;
        open CMD, "$spoof_cmd $spoof_cache_file |"
            or die "[*] Could not execute $spoof_cmd ",
                "$spoof_cache_file: $!";
        close CMD;
        unlink $spoof_cache_file;
    } else {
        print "[+] Sending $msg_len byte message to $knock_dst ",
            "over udp/$enc_pcap_port...\n";

        my $socket = IO::Socket::INET->new(
            PeerAddr => $knock_dst,
            PeerPort => $enc_pcap_port,
            Proto    => 'udp',
            Timeout  => 1
        );  ### note there is no "or die" here since we just want to throw
            ### packets on the network

        if (defined $socket) {
            $socket->send($msg);
            undef $socket;
        }
    }
    return;
}

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

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

    my $cipher = Crypt::CBC->new(
        {
            'key'             => $enc_key,
            'cipher'          => $enc_alg,
            'iv'              => $enc_init_vector,
            'prepend_iv'      => 0,
            'regenerate_key'  => 0,
        }
    );
    return $cipher->decrypt(decode_base64($msg));
}

sub encrypt_sequence() {
    my $clear_txt = '';
    my $checksum = 0;
    my @encrypted_seq = ();

    my $cipher = Crypt::CBC->new(
        {
            'key'             => $enc_key,
            'cipher'          => $enc_alg,
            'iv'              => $enc_init_vector,
            'prepend_iv'      => 0,
            'regenerate_key'  => 0,
        }
    );

    my @octets = split /\./, $enc_allow_ip;

    $clear_txt .= chr($_) for @octets;
    $checksum += $_ for @octets;

    my $proto_num = 6;
    if ($enc_allow_proto =~ /udp/i) {
        $proto_num = 17;
    } elsif ($enc_allow_proto =~ /icmp/i) {
        $proto_num = 1;
        $enc_allow_port = 0;
    }

    my $port_upper_bits = $enc_allow_port;
    my $port_lower_bits = $enc_allow_port;

    if ($enc_allow_port == 0) {
        $port_upper_bits = 0;
        $port_lower_bits = 0;
    } else {
        $port_upper_bits = $port_upper_bits >> 8;
        $port_lower_bits = $port_lower_bits % 256;
    }

    $clear_txt .= chr($port_upper_bits);
    $clear_txt .= chr($port_lower_bits);

    $checksum += $port_upper_bits;
    $checksum += $port_lower_bits;

    $clear_txt .= chr($proto_num);
    $checksum += $proto_num;

    $checksum = $checksum % 256;

    $clear_txt .= chr($checksum);

    ### append username
    ### FIXME: either the checksum should be removed, or it should
    ### be applied to the username as well.
    my $username = getlogin() || getpwuid($<) || die "[*] Could not ",
        "get process username.";

    if ($username) {
        my @chars = split //, $username;
        for my $char (@chars) {
            if (length($clear_txt) < $enc_blocksize-1) {
                $clear_txt .= $char;
            }
        }
    }

    ### pad out with zeros until we have a full block (actually
    ### 15 bytes)
    while (length($clear_txt) < $enc_blocksize-1) {
        $clear_txt .= chr(0);
    }

    my @tmp_chars = split //, $clear_txt;
    print "[+] clear text sequence: ";
    print ord($_) . ' ' for @tmp_chars;
    print "\n";

    my $cipher_txt = $cipher->encrypt($clear_txt);
    undef $cipher;

    @tmp_chars = split //, $cipher_txt;
    print "[+] cipher text sequence: ";
    print ord($_) . ' ' for @tmp_chars;
    print "\n";

    my @chars = split //, $cipher_txt;
    my $char_ctr = 0;
    for my $char (@chars) {
        my %hsh;
        if ($enc_rotate_proto) {
            ### alternate between tcp and udp protocols
            if ($char_ctr % 2 == 0) {
                %hsh = ('port' => ord($char) + $enc_port_offset,
                    'proto' => 'tcp');
            } else {
                %hsh = ('port' => ord($char) + $enc_port_offset,
                    'proto' => 'udp');
            }
        } else {
            ### hardcode knock sequence proto as tcp
            %hsh = ('port' => ord($char) + $enc_port_offset,
                'proto' => 'tcp');
        }
        push @encrypted_seq, \%hsh;
        $char_ctr++;
    }
    return \@encrypted_seq;
}

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

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

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

    return 0 unless $cipher_txt;

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

    my $cipher = Crypt::CBC->new(
        {
            'key'             => $access_vars_href->{'KEY'},
            'cipher'          => $enc_alg,
            'iv'              => $enc_init_vector,
            'prepend_iv'      => 0,
            'regenerate_key'  => 0,
        }
    );

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

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

    unless ($plain_txt) {
        return 0,0,0,0;
    }

    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.
        $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 get_key() {
    if ($get_key_file) {
        ### get the encryption key from file
        open F, "< $get_key_file" or die "[*] Could not open ",
            "$get_key_file: $!";
        my @lines = <F>;
        close F;
        for my $line (@lines) {
            chomp $line;
            if ($line =~ /$knock_dst:\s*(.*)/) {
                $enc_key = $1;
            }
        }
die "[*] Could not read encryption key for $knock_dst from $get_key_file\n",
    "    fwknop expects the format \"$knock_dst: <KEY>\n in $get_key_file\n"
            unless $enc_key;
    } else {
        print "[+] Enter an encryption key (must be at least 8 chars, ",
            "but less than $enc_blocksize chars).\n",
            "    This key must match a key in the file ",
            "/etc/fwknop/access.conf\n    on the remote system.\n\n";
        ReadMode 'noecho';
        while (1) {
            print "[+] Encryption Key: ";
            my $ans = ReadLine 0;
            chomp $ans;
            next unless $ans =~ /\S/;
            if (length($ans) >= 8 and length($ans) <= $enc_blocksize) {
                $enc_key = $ans;
                last;
            } else {
                print "[-] The key length must be between 8 and ",
                    "$enc_blocksize chars.\n";
            }
        }
        ReadMode 'normal';
        print "\n";

        die "[*] Could not read encryption key from STDIN.  Exiting."
            unless $enc_key;
    }
    ### pad out to 16 chars
    while (length($enc_key) < $enc_blocksize) {
        $enc_key .= '0';
    }
    return;
}

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

    return unless defined $access_vars_href->{'OPEN_PORTS'};

    my $ipt = new IPTables::ChainMgr(
        'iptables' => $cmds{'iptables'}
    ) 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'};

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

        unless ($rv) {
            &logr('[-]', $status_msg, 0);
            next;
        }

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

        unless ($rv) {
            &logr('[-]', $status_msg, 0);
            next;
        }

        for my $proto (keys %{$access_vars_href->{'OPEN_PORTS'}}) {
            for my $port (keys %{$access_vars_href->{'OPEN_PORTS'}->{$proto}}) {

                my $ip_allowed = 0;

                if ($ipt->find_ip_rule($src, '0.0.0.0/0',
                    $table, $to_chain, $target,
                        {'protocol' => $proto, 'd_port' => $port})) {
                    &logr('[-]', "source: $src already allowed " .
                        "to connect to $proto/$port in chain: $to_chain", 1);
                } else {
                    my $msg = "adding $to_chain ACCEPT rule for source: " .
                        "$src to connect via $proto";
                    $msg .= "/$port" if $proto ne 'icmp';

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

                    ($rv, $status_msg) = $ipt->add_ip_rule($src, '0.0.0.0/0',
                        $config{'IPTABLES_AUTO_RULENUM'}, $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_vars_href->{'FW_ACCESS_TIMEOUT'};
                            }
                            $ipt_access{$src}{'access'}{$proto}{$port} = '';
                        }

                        ### keep track of how many times we have granted access
                        $seq_href->{'grant_ctr'}++ unless defined
                            $access_vars_href->{'ULOG_PCAP'}
                            or defined $access_vars_href->{'PCAP'};

                        if ($config{'AUTH_MODE'} eq 'PCAP') {
                            ### In PCAP mode (and not in ULOG_PCAP mode), we
                            ### only get processing time when we receive a
                            ### packet, so cache the firewall rule addition
                            ### time so that knoptm can remove it.
                            &pcap_append_fw_cache_entry(
                                time(),
                                $access_vars_href->{'FW_ACCESS_TIMEOUT'},
                                $src,
                                $proto,
                                $port,
                                $table,
                                $to_chain,
                                $target
                            );
                        }
                    } else {
                        &logr('[-]', $status_msg, 1);
                        print STDERR "[-] ipt_block(): $status_msg\n" if $debug;
                    }
                }
            }
        }
    }
    $seq_href->{'port_seq'} = 0
        unless defined $access_vars_href->{'ULOG_PCAP'} or
            defined $access_vars_href->{'PCAP'};

    return;
}

sub pcap_append_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 FWC, ">> $config{'KNOPTM_TIMEOUT_FILE'}" or die "[*] Could not ",
        "open $config{'KNOPTM_TIMEOUT_FILE'}: $!";
    print FWC "$rule_timeout $timeout $ip $proto ",
        "$port $table $chain $target\n";
    close FWC;

    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 = new IPTables::ChainMgr(
        'iptables' => $cmds{'iptables'}
    ) 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, $status_msg) = $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 to $proto/$port, $ipt_access{$src}{'timeout'} " .
                                "second timeout exceeded", 1);
                        } else {
                            &logr('[-]', $status_msg, 0);
                        }
                    }
                }
            }
        }
        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*ENCRYPT_SEQUENCE\s*;/) {
                    $access_hsh{'ENCRYPT_SEQUENCE'} = '';
                } 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*ULOG_PCAP\s*;/) {
                    ### used in ulog pcap mode
                    $access_hsh{'ULOG_PCAP'} =  '';
                } elsif ($line =~ /^\s*PCAP\s*;/) {
                    ### used in pcap mode
                    $access_hsh{'PCAP'} =  '';
                } elsif ($line =~ /^\s*SHARED_SEQUENCE:\s*(.*)\s*;/) {
                    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*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*ENABLE_CMD_EXEC\s*;/) {
                    $access_hsh{'ENABLE_CMD_EXEC'} = 1;
                } elsif ($line =~ /^\s*DISABLE_FW_ACCESS\s*;/) {
                    $access_hsh{'DISABLE_FW_ACCESS'} = 1;
                } elsif ($line =~ /^\s*CMD_REGEX:\s*(.*)\s*;/) {
                    $access_hsh{'CMD_REGEX'} = $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--;
        }
        if (&validate_src_access_hsh(\%access_hsh)) {
            push @access, \%access_hsh;
            $valid_ctr++;
        }
    }

    if ($valid_ctr == 0) {
        die "[*] No valid SOURCE blocks defined in ",
            "$config{'ACCESS_CONF'}.  Exiting.";
    }
    print STDERR Dumper @access if $debug and $verbose;
    &logr('[+]', 'imported port knocking 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;
            }
        }
    }
    if ($cmdline_offset) {
        $config{'ENCRYPTED_PORT_OFFSET'} = $cmdline_offset;
    }
    return;
}

sub import_shared_sequence() {

    my $homedir = &get_homedir();

    my $connect_file = '';
    my @lines = ();
    if ($user_rc_file and -e $user_rc_file) {
        $connect_file = $user_rc_file;
    } elsif (-e "$homedir/.fwknoprc") { ### this is the default unless -f was given
        $connect_file = "$homedir/.fwknoprc";
    } else {
        unless ($user_rc_file) {
            print "[+] Creating fwknop rc file: $homedir/.fwknoprc\n",
                "    This file is used only to define shared knock sequences.  ",
                "If you want\n    to send an encrypted sequence, use the ",
                "--encrypt argument.\n\n[+] To send a shared sequence you will ",
                "first need to define\n    the sequence in $homedir/.fwknoprc\n";
            open F, "> $homedir/.fwknoprc" or
                die "[*] Could not open $homedir/.fwknoprc: $!";
print F "# Shared knock sequence config file for fwknop.  This file adheres to the\n",
    "# following format:\n# src: <proto/port>, ..., <proto/port>.  See the example ",
    "# below:\n\n# 192.168.10.2: tcp/5501, tcp/5502, udp/1001, tcp/5504\n\n";
            close F;
            exit 1;
        }
    }

    open F, "< $connect_file" or die "[*] Could not open ",
        "$connect_file: $!";
    @lines = <F>;
    close F;

    ### parse out the knock sequence
    my @knock_sequence = ();
    my $dst = '';
    my $found_dst = 0;
    for my $line (@lines) {
        chomp $line;
        next unless $line =~ /\S/;
        next if $line =~ /^\s*#/;
        if ($line =~ /^\s*(\S+):\s*(.*)/) {
            my $dst   = $1;
            my $ports = $2;
            next unless $dst;
            next unless $dst eq $knock_dst;
            my @ports_arr = split /\s*\,\s*/, $ports;
            next unless @ports_arr and $#ports_arr > 1;
            $found_dst = 1;
            for my $port (@ports_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 @knock_sequence, \%hsh;
            }
        }
    }
    die "[*] Could not find destination: $knock_dst in $connect_file"
        unless $found_dst;
    die "[*] No port sequence defined for $knock_dst in $connect_file"
        unless @knock_sequence;
    return \@knock_sequence;
}

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

    if (defined $src_href->{'ENCRYPT_SEQUENCE'} ) {
        unless (defined $src_href->{'KEY'}) {
            &logr('[-]', "$config{'ACCESS_CONF'}: source $src missing KEY " .
                "tag for ENCRYPT_SEQUENCE", 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 (defined $src_href->{'SHARED_SEQUENCE'}) {
        unless (defined $src_href->{'OPEN_PORTS'}) {
            &logr('[-]', "$config{'ACCESS_CONF'}: source $src missing " .
                "OPEN_PORTS tag.", 0);
            return 0;
        }
    } elsif (defined $src_href->{'ULOG_PCAP'}) {
        unless (defined $src_href->{'KEY'}) {
            &logr('[-]', "$config{'ACCESS_CONF'}: source $src missing KEY " .
                "tag for ENCRYPT_SEQUENCE", 0);
            return 0;
        }
    } elsif (defined $src_href->{'PCAP'}) {
        unless (defined $src_href->{'KEY'}) {
            &logr('[-]', "$config{'ACCESS_CONF'}: source $src missing KEY " .
                "tag for ENCRYPT_SEQUENCE", 0);
            return 0;
        }
    } else {
        &logr('[-]', "$config{'ACCESS_CONF'}: source $src, missing " .
            "ENCRYPT_SEQUENCE, SHARED_SEQUENCE, ULOG_PCAP, or PCAP tag", 0);
        return 0;
    }
    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 (defined $src_href->{'ULOG_PCAP'}
                or defined $src_href->{'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;
        }
    }
    return 1;
}

sub get_homedir() {
    my $uid = $<;
    my $homedir = '';
    if ($cmdl_homedir) {
        $homedir = $cmdl_homedir;
    } else {
        ### prefer homedir specified in /etc/passwd (if it exists)
        if (-e '/etc/passwd') {
            open P, "< /etc/passwd" or die "[*] Could not open /etc/passwd. ",
                "Exiting.\n";
            my @lines = <P>;
            close P;
            for my $line (@lines) {
                ### mbr:x:222:222:Michael Rash:/home/mbr:/bin/bash
                chomp $line;
                if ($line =~ /^(?:.*:){2}$uid:(?:.*:){2}(\S+):/) {
                    $homedir = $1;
                    last;
                }
            }
        }
        unless ($homedir and -d $homedir) {
            $homedir = $ENV{'HOME'} if defined $ENV{'HOME'};
        }
    }
    die '[*] Could not determine homedir, use --Home option.'
        unless ($homedir and -d $homedir);
    return $homedir;
}

sub save_args() {
    my $homedir = &get_homedir();
    my $save_file = "$homedir/.fwknop.run";
    ### this best-effort; not having this file doesn't mean
    ### that we should not continue to run
    open S, "> $save_file" or return;
    print S "@args_cp\n";
    close S;
    return;
}

sub run_last_cmdline() {
    my $homedir = &get_homedir();
    my $save_file = "$homedir/.fwknop.run";
    if (-e $save_file) {
        open S, "< $save_file" or die "[*] Could not open $save_file: $!";
        my $arg_line = <S>;
        close S;
        chomp $arg_line;
        print "[+] Running with last command line args: $arg_line\n";
        @ARGV = split /\s+/, $arg_line;
        &usage(1) unless (GetOptions(
            'config=s'      => \$config_file,
            'Server-port=i' => \$enc_pcap_port,
            'Server-mode=s' => \$server_mode,
            'Server-cmd=s'  => \$cmdline_pcap_cmd,
            'Spoof-src=s'   => \$spoof_src,
            'Spoof-cmd=s'   => \$spoof_cmd,
            'Spoof-file=s'  => \$spoof_cache_file,
            'Spoof-user=s'  => \$spoof_username,
            'user-rc=s'     => \$user_rc_file,
            'knock-dst=s'   => \$knock_dst,
            'encrypt'       => \$encrypt,
            'Access=s'      => \$access_str,
            'allow-ip=s'    => \$enc_allow_ip,
            'source-ip'     => \$enc_source_ip,
            'port=i'        => \$enc_allow_port,
            'Proto=s'       => \$enc_allow_proto,
            'rotate-proto'  => \$enc_rotate_proto,
            'offset=i'      => \$cmdline_offset,
            'os'            => \$os_fprint_only,
            'time-delay=i'  => \$knock_sleep,
            'ipt-log=s'     => \$os_ipt_log,
            'ipt-list'      => \$ipt_list,
            'ipt-flush'     => \$ipt_flush,
            'last-cmd'      => \$run_last_args,
            'get-key=s'     => \$get_key_file,
            'Home-dir=s'    => \$cmdl_homedir,
            'debug'         => \$debug,
            'Kill'          => \$kill,
            'Restart'       => \$restart,
            'Status'        => \$status,
            'verbose'       => \$verbose,
            'Version'       => \$print_version,
            'help'          => \$print_help
        ));
    } else {
        die "[*] fwknop argument save file $save_file does not exist.";
    }
    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{'FWKNOP_PID_FILE'}) {
        my $caller = $0;
        open PIDFILE, "< $config{'FWKNOP_PID_FILE'}";
        my $pid = <PIDFILE>;
        close PIDFILE;
        chomp $pid;
        if (kill 0, $pid) {  # fwknop is already running
            die "[*] fwknop (pid: $pid) is already running!  Exiting.\n";
        }
    }
    return;
}

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

sub writecmdline() {
    open C, "> $config{'CMDLINE_FILE'}" or die "[*] Could not open ",
        "$config{'CMDLINE_FILE'}: $!";
    print C @ARGV, "\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'     => $config{'FWKNOP_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) {
        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 "[*] fwknop: 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 "[-] fwknop: $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 "[-] fwknop: 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{'fwknop'} $cmdline";
    } else {
        system $cmds{'fwknop'};
    }
    return 0;
}

sub status() {
    my %pidfiles = (
        'knopwatchd' => $config{'KNOPWATCHD_PID_FILE'},
        'knopmd'     => $config{'KNOPMD_PID_FILE'},
        'knoptm'     => $config{'KNOPTM_PID_FILE'},
        'fwknop'     => $config{'FWKNOP_PID_FILE'}
    );
    for my $pidname qw(knopwatchd knopmd knoptm fwknop) {
        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() {

    ### load up the server perl modules... this is done very early
    ### so as to not create dependency bugs
    &load_server_perl_modules();

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

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

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

    unless ($os_fprint_only) {
        ### 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: $!";
        }

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

        ### 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'});
        } else {
            system $cmds{'knopmd'};
        }
        if ($config{'AUTH_MODE'} eq 'PCAP') {
            system $cmds{'knoptm'};
        } else {
            ### make sure knoptm is not running
            &stop_daemon($config{'KNOPTM_PID_FILE'});
        }
        system $cmds{'knopwatchd'} unless $debug;
    }

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

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

    return;
}

sub load_server_perl_modules() {

    print "[+] Loading server perl modules.\n" if $debug;

    require Net::Pcap;
    require NetPacket::IP;
    require NetPacket::UDP;
    require NetPacket::TCP;
    require NetPacket::ICMP;
    require NetPacket::Ethernet;
    require IPTables::Parse;
    require IPTables::ChainMgr;

    return;
}

sub ipt_list() {
    my $ipt = new IPTables::ChainMgr(
        'iptables' => $cmds{'iptables'}
    ) 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, $output_aref) =
                $ipt->run_ipt_cmd_output("$cmds{'iptables'} -t " .
                    "$table -n -L $to_chain -v");

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

sub ipt_flush() {
    my $ipt = new IPTables::ChainMgr(
        'iptables' => $cmds{'iptables'}
    ) or die '[*] Could not acquire IPTables::ChainMgr object.';

    print "[+] Flushing Netfilter IPT_AUTO_CHAIN chains...\n";
    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)) {
                if ($ipt->flush_chain($table, $to_chain)) {
                    print "[+] Flushed: $to_chain\n";
                } else {
                    print "[-] Could not flush: $to_chain\n";
                }
            } else {
                print "[-] Chain: $to_chain does not exist.\n";
            }
        }
    } else {
        print "[-] No valid IPT_AUTO_CHAIN keywords.\n";
    }
    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 'fwknop', 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 fwknop: $msg");
        }
    }
    return;
}

sub required_vars() {
    for my $var qw(FW_DATA_FILE SLEEP_INTERVAL FWKNOP_DIR FWKNOP_PID_FILE
            KNOPMD_PID_FILE KNOPWATCHD_PID_FILE CMDLINE_FILE P0F_FILE
            ACCESS_CONF MAX_HOPS IPTABLES_AUTO_RULENUM EMAIL_ADDRESSES
            ALERTING_METHODS IPT_AUTO_CHAIN1 AUTH_MODE PCAP_CMD_TIMEOUT
            ENABLE_PCAP_PROMISC PCAP_FILTER ULOG_PCAP_FILTER
            KNOPTM_TIMEOUT_FILE) {
        unless (defined $config{$var}) {
            die "[*] Variable $var is not defined in $config_file";
        }
    }
    return;
}

sub build_ipt_config() {

    my $ipt = new IPTables::ChainMgr(
        'iptables' => $cmds{'iptables'}
    ) 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"};

        ### DROP, src, filter, INPUT, FWKNOP_INPUT
        my @block = split /\s*,\s*/, $value;
        if ($#block == 4) {
            my %hsh = (
                'target'     => $block[0],
                'direction'  => $block[1],
                'table'      => $block[2],
                'from_chain' => $block[3],
                'to_chain'   => $block[4]
            );
            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, 0);
                print STDERR "[-] build_ipt_config(): $msg\n"
                    if $debug;
                next VAR;
            }
            if ($ipt->chain_exists($hsh{'table'}, $hsh{'from_chain'})) {
                push @ipt_config, \%hsh;
            } else {
                my $msg = "invalid IPT_AUTO_CHAIN$ctr keyword, " .
                    "$hsh{'from_chain'} chain does not exist.";
                &logr('[-]', $msg, 0);
                print STDERR "[-] build_ipt_config(): $msg\n"
                    if $debug;
            }
        } else {
            my $msg = "invalid IPT_AUTO_CHAIN$ctr variable: $value";
            &logr('[-]', $msg, 0);
            print STDERR "[-] build_ipt_config(): $msg\n" if $debug;
        }
        $ctr++;
    }
    return;
}

sub validate_access_str() {
    $access_str = lc($access_str);
    my @ports = split /,/, $access_str;
    for my $str (@ports) {
        unless ($str =~ m|(\D+)/(\d+)|) {
            die "[*] -A format is: <proto>/<port>,...,<proto>/<port>\n",
                "    e.g.: tcp/22,udp/53,icmp/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 'ULOG_PCAP'
            or $config{'AUTH_MODE'} eq 'PCAP') {
        die "[*] AUTH_MODE must be either KNOCK, 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_;
fwknop
    version: $version, by Michael Rash (mbr\@cipherdyne.org)

Usage: fwknop [-c <config file>] [-u <user-rc file>] [-k <knock dst>] [-d]
              [--Server-mode <mode>] [--Server-port <port>] [-D] [-e] [-R]
              [--Server-cmd <cmd>] [-a <allow-ip>] [-p <port>] [-P <protocol>]
              [--Spoof-src <ip>] [--Spoof-user <user>] [--Spoof-file <file>]
              [--Spoof-cmd <cmd>] [-g] [--offest <port offset>] [--os]
              [-i <iptables-log>] [-l] [-v] [--Status] [-K] [-V] [-h]
              [--Home-dir <home directory>]

Options:
    -c, --config <file>        - Specify path to config file instead of using
                                 the default $config_file.  This
                                 file is used only when fwknop is run as a
                                 daemon.
    --Server-port <port>       - Specify the port number to which to send
                                 the single authentication packet (this is
                                 only used for an fwknop server that is
                                 operating in pcap mode).
    --Server-mode <mode>       - Run in legacy port knocking mode ("mode" =
                                 "knock").
    --Server-cmd <cmd>         - Specify a complete command that an fwknop
                                 server should execute (as root).
    -u, --user-rc <rc-file>    - Specify path to user connect rc file instead
                                 of using the default ~/.fwknoprc.  This file
                                 is not referenced for encrypted port
                                 sequences; only for shared sequences.
    -k, --knock-dst <ip>       - Connection destination IP address for port
                                 knock sequence.
    -t, --time-delay <seconds> - Introduce a time delay between each
                                 connection in a knock sequence.  This is
                                 mainly used in conjunction with the
                                 MIN_TIME_DIFF access control directive.
    -l, --last-cmd             - Run the fwknop with the same command line
                                 arguments as in the previous invocation.
    -e, --encrypt              - Encrypt knock sequence with Rijndael AES
                                 algorithm.
    -a, --allow-ip <ip>        - IP to instruct the remote fwknop server to
                                 allow through the firewall ruleset.  This
                                 IP is only used when running fwknop in
                                 --encrypt mode.
    -s, --source-ip            - Inform the destination fwknop server to use
                                 the source address from which the knock
                                 sequence originates (useful for
                                 authenticating to the knock server from
                                 behind a NAT device).  This option really
                                 only makes sense when sending an encrypted
                                 knock sequence.
    -p, --port <port>          - Specify port to open on the remote fwknop
                                 server.  This is only used when run in
                                 --encrypt knock mode.
    -P, --Proto <protocol>     - Specify protocol of port to open on the
                                 remote fwknop server (tcp or udp).  This
                                 is only used when run in --encrypt knock
                                 mode.
    -r, --rotate-proto         - Rotate protocol (tcp and udp only) for
                                 encrypted sequences.
    --offset <port>            - Specify port offset to use when run in
                                 --encrypt knock mode.  The default is
                                 $enc_port_offset.
    --os                       - Parse iptables logs and fingerprint
                                 operating systems from which tcp SYN
                                 packets have been logged.
    --ipt-log                  - Specify path to iptable logfile.  This is
                                 used only when running in --os mode.
    --ipt-list                 - List all active rules in the FWKNOP
                                 Netfilter chain(s).
    --ipt-flush                - Flush all active rules in the FWKNOP
                                 Netfilter chain(s).
    -g, --get-key <file>       - Get encryption key from ~/.fwknoprc file
                                 instead of from STDIN.
    -K, --Kill                 - Kill all running fwknop processes.
    -R, --Restart              - Restart all running fwknop processes.
    --Status                   - Displays the status of any
                                 currently running fwknop processes.
    -H, --Home-dir <directory> - Specify the home directory of the current
                                 user that is running fwknop.
    -d, --debug                - Run fwknop in debugging mode.
    -v, --verbose              - Verbose mode.
    -V, --Version              - Display version and exit.
    -h, --help                 - Print help and exit.
_HELP_

    exit $exit_status;
}
