#!/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.2
#
# 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.49 2004/07/31 14:37:07 mbr Exp $
#

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

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

my $version = '0.2';

my %config     = ();
my %cmds       = ();
my %p0f_sigs   = ();
my %p0f        = ();
my %posf_sigs  = ();
my %posf       = ();
my %posf_tcp   = ();
my %posf_udp   = ();
my %posf_icmp  = ();
my %access     = ();
my %ipt_chains = ();
my %ipt_access = ();

my $os_fprint_only = 0;
my $print_version  = 0;
my $print_help     = 0;
my $debug          = 0;
my $verbose        = 0;
my $knock_sleep    = 1;  ### default to 1 second difference between port knocks
my $knock_dst      = '';
my $os_ipt_log     = '';
my $skipped_first_loop = 0;

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

### 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_allow_ip     = '';
my $enc_source_ip    = 0;
my $enc_allow_port   = 0;
my $enc_allow_proto  = '';
my $enc_rotate_proto = 0;
my $get_key_from_rc  = 0;  ### get key from ~/.fwknoprc

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

### make Getopts case sensitive
Getopt::Long::Configure('no_ignore_case');
&usage(1) unless (GetOptions(
    'config=s'     => \$config_file,
    'user-rc=s'    => \$user_rc_file,
    'knock-dst=s'  => \$knock_dst,
    'encrypt'      => \$encrypt,
    '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,
    'get-key'      => \$get_key_from_rc,
    'debug'        => \$debug,
    '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;
}

if ($encrypt) {
    unless ($knock_dst) {
        die "[*] Must also specifiy: -k <knock destination\n";
    }
    unless ($enc_allow_ip or $enc_source_ip) {
        die "[*] Must either specifiy: --allow-ip <IP>, or --source-ip\n";
    }
    if ($enc_source_ip) {
        ### 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 (useful for clients that
        ### are behind a NAT device).
        $enc_allow_ip = '0.0.0.0';
    }
    unless ($enc_allow_proto) {
        die "[*] Must also specify: -P <proto> with -e";
    }
    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 ($enc_allow_proto =~ /icmp/i) {
        $enc_allow_port = 0;
    } else {
        unless ($enc_allow_proto) {
            die "[*] Must also specify: -p <port> with -e";
        }
    }
    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/) {
        die "[*] Invalid IP address: $enc_allow_ip";
    }
    if ($cmdline_offset) {
        unless ($cmdline_offset < 65280 and $cmdline_offset > 0) {
            die "[*] Port offset must be 0 < port < 65280";
        }
        $enc_port_offset = $cmdline_offset;
    }
} else {
    if ($enc_rotate_proto) {
        die "[*] Can only specify --rotate-proto with ",
            "encrypted sequences (-e).";
    }
}

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

if ($knock_dst) {

    my $ports_aref = '';

    if ($encrypt) {

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

        ### get the encrypted port sequence (16 ports)
        $ports_aref = &encrypt_sequence();

    } else {
        ### first see what ports we are supposed to knock
        $ports_aref = &import_shared_sequence();
    }

    ### we are running in port knocking mode, so try to connect
    &knock_ports($ports_aref);

} else {  ### we are running in access control mode

    print STDERR "[+] ** Starting fwknop (debug mode) **\n" if $debug;

    ### import config
    &import_config();

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

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

    ### import passive OS fingerprints (old technique)
    &import_posf_sigs();

    unless ($os_fprint_only) {
        ### import access directives
        &import_access();

        ### make sure we can interact with iptables
        &get_iptables_chains();

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

        ### write any command line args out to disk (in daemon mode there
        ### are probably no command line args, but there _could_ be -c
        ### and possibly -v).
        &writecmdline();

        ### start knopmd and knopwatchd here (if they are already running
        ### it is ok, another instance will not be started).
        system $cmds{'knopmd'};
        system $cmds{'knopwatchd'} unless $debug;
    }

    print STDERR "[+] Opening $config{'FW_DATA_FILE'}, and entering main loop.\n"
        if $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";
    }

    ### 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 access for IPs
        &timeout_access();

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

        sleep $config{'SLEEP_INTERVAL'};
    }
    close FWLOG;
}
exit 0;
#============================ end main ==============================

### 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/;
            if ($pkt =~ /\sOPT\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_aref = '';
        $access_aref = &check_src($src) unless $os_fprint_only;

        unless ($os_fprint_only) {
            unless ($access_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);

### FIXME: only if we want to do old-style passive OS fingerprinting
#            } elsif ($port_seq{$src} < 100) {
                ### (give up on this old technique after 100 packets)
                ### fingerprint remote OS with posf_tcp since we got a
                ### tcp SYN packet.
#                &posf_tcp($src, $len, $tos, $ttl, $id, $win)
            }
        }

        next PKT if $os_fprint_only;

        SRC: for my $href (@$access_aref) {

            ### we are going to put port data within each access hash ref.
            ### This is a bit ugly, but provides an easy alternative to having
            ### to create separate data structures which ultimately have to be
            ### associated with each access href anyway.  Note that dynamic
            ### value are associated with lower case hash keys, where true
            ### access control directives are associated with upper case key
            ### (such as "ENCRYPT_SEQUENCE").
            $href->{'port_seq'} = 0 unless defined $href->{'port_seq'};
            $href->{'grant_ctr'} = 1 if not defined $href->{'grant_ctr'};

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

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

                    ### see if the start time is past the knock timeout
                    if (defined $href->{'enc_stime'} and
                            time() - $href->{'enc_stime'} > $href->{'KNOCK_INTERVAL'}) {
                        print STDERR "[-] Timeout exceeded for $src\n" if $debug;
                        delete $href->{'enc_ports'};
                        delete $href->{'enc_stime'};
                    }

                    ### add the destination port to the encrypted sequence
                    push @{$href->{'enc_ports'}}, $dp;
                    $href->{'enc_stime'} = time() unless defined $href->{'enc_stime'};
                }

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

                    &logr('[+]', "decrypting knock sequence for $src", 0);

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

                    if ($rv) {

                        ### see if we need to match the OS
                        unless (&matched_os($src, $href)) {
                            delete $href->{'enc_ports'};
                            next SRC;
                        }

                        ### see if we need to match the username
                        unless (&matched_username($username, $href)) {
                            delete $href->{'enc_ports'};
                            next SRC;
                        }

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

                        ### all criteria met; grant access
                        &grant_access($allow_ip, $dec_allow_port,
                            $dec_allow_proto, $href);

                    } else {
                        &logr('[-]', "sequence decrypt failed for $src", 1);
                    }
                    delete $href->{'enc_ports'};
                }
            } elsif (defined $href->{'SHARED_SEQUENCE'}) {
                if ($dp == $href->{'SHARED_SEQUENCE'}->
                            [$href->{'port_seq'}]->{'port'}
                        and $proto eq $href->{'SHARED_SEQUENCE'}->
                            [$href->{'port_seq'}]->{'proto'}) {

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

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

                    ### if we made it to the end of the sequence then we have
                    ### a correct knock sequence
                    if ($href->{'port_seq'} == $#{$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: $href->{'port_seq'}\n"
                        if $debug;
                    delete $href->{'port_times'};
                    $href->{'port_seq'} = 0;
                    next SRC;
                }
            }
            if ($matched_sequence) {
                ### we matched the knock sequence, so reset for new
                ### sequence (note we may have other criteria to meet
                ### before actually granting access).
                delete $href->{'port_times'};
                $href->{'port_seq'} = 0;

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

                next SRC unless &matched_os($src, $href);

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

                ### 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, -1, '', $href);
            }
        }
    }

    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} or defined $posf{$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('[+]', "p0f 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('[-]', "p0f OS fingerprint mismatch for $src: " .
                "expected: $href->{'REQUIRE_OS'}, " .
                "received: $first_os_key", 1);
            return 0;

        } elsif (defined $posf{$src}) {
            if ($posf{$src} eq $href->{'REQUIRE_OS'}) {
                &logr('[+]', "OS fingerprint: $posf{$src} " .
                    "matched for $src", 0);
                return 1;
            } else {
                &logr('[-]', "OS fingerprint mismatch for $src: " .
                    "expected: $href->{'REQUIRE_OS'}, " .
                    "received: $posf{$src}", 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('[+]', "p0f OS guess: $os " .
                        "regex matched for $src", 1);
                    return 1;
                }
            }

            ### there may be more than one OS fingerprint, but
            ### just print one.
            &logr('[-]', "p0f OS fingerprint regex mismatch for $src: " .
                "expected: $href->{'REQUIRE_OS_REGEX'}, " .
                "received: $first_os_key", 1);
            return 0;
        } elsif (defined $posf{$src}) {
            if ($posf{$src} =~ m|$href->{'REQUIRE_OS_REGEX'}|i) {
                &logr('[+]', "OS fingerprint: $posf{$src} " .
                    "matched for $src", 1);
                return 1;
            } else {
                &logr('[-]', "OS fingerprint mismatch for $src: " .
                    "expected: $href->{'REQUIRE_OS_REGEX'}, " .
                    "received: $posf{$src}", 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 @arr = ();

    ### give preference to specific IP addresses
    if (defined $access{'ip'}) {
        for my $href (@{$access{'ip'}}) {
            if ($src eq $href->{'SOURCE'}) {
                print STDERR "[+] Packet from $src matched IP SOURCE: $src in ",
                "$config{'ACCESS_CONF'}\n" if $debug;
                push @arr, $href;
            }
        }
    }

    ### check specific networks
    if (defined $access{'net'}) {
        for my $href (@{$access{'net'}}) {
            if (ipv4_in_network($href->{'SOURCE'}, $src)) {
                print STDERR "[+] Packet from $src matched NET SOURCE: ",
                    "$href->{'SOURCE'} in $config{'ACCESS_CONF'}\n" if $debug;
                push @arr, $href;
            }
        }
    }

    ### if we get here, then make a final check to see if a source
    ### of ANY was defined in access.conf
    if (defined $access{'any'}) {
        for my $href (@{$access{'any'}}) {
            print STDERR "[+] Packet from $src matched SOURCE: ANY in ",
                "$config{'ACCESS_CONF'}\n" if $debug;
            push @arr, $href;
        }
    }
    return \@arr;
}

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

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

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

    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, $access_href) = @_;

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

    $cipher_txt .= chr($_ - $access_href->{'PORT_OFFSET'})
        for @{$access_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_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) {
        &logr('[-]', "could not decrypt knock sequence from $src", 0);
        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), 0);
            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", 0);
        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", 0);
        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_from_rc) {
        ### get the encryption key out of the ~/.fwknoprc
        my $connect_file = '';
        my @lines = ();
        my $homedir = &get_homedir();

        if ($user_rc_file and -e $user_rc_file) {
            $connect_file = $user_rc_file;
        } elsif (-e "$homedir/.fwknoprc") { ### this is the default unless -u was given
            $connect_file = "$homedir/.fwknoprc";
        } else {
            die "[*] Could not find connection definition file.  The default ",
                "path is\n    $homedir/.fwknoprc";
        }
        open F, "< $connect_file" or die "[*] Could not open ",
            "$connect_file: $!";
        @lines = <F>;
        close F;
    } else {
        print "[+] Enter an encryption key (must be as least 8 chars, ",
            "but less than $enc_blocksize chars).\n",
            "    This key must match the 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";

        ### pad out to 16 chars
        while (length($enc_key) < $enc_blocksize) {
            $enc_key .= '0';
        }
    }
    return;
}

sub grant_access() {
    my ($src, $dec_allow_port, $dec_allow_proto, $access_href) = @_;
    for my $chain (keys %ipt_chains) {
        ### note that &ipt_allow() checks to make sure access is not
        ### already granted for $src to $proto/$port
        &ipt_allow($src, $dec_allow_port, $dec_allow_proto,
            $access_href, $chain);
    }
    $access_href->{'port_seq'} = 0;
    return;
}

sub ipt_allow() {
    my ($src, $dec_allow_port, $dec_allow_proto, $access_href, $chain) = @_;
    open IPT, "$cmds{'iptables'} -t filter -nL $chain |" or
        die "[*] Could not execute: iptables -t filter -nL $chain";
    my @rules = <IPT>;
    close IPT;

    if ($dec_allow_port > -1) {
        $dec_allow_proto = 'tcp' unless $dec_allow_proto;
        $dec_allow_port = 0 if $dec_allow_proto eq 'icmp';
        $access_href->{'OPEN_PORTS'}->{$dec_allow_proto}->{$dec_allow_port}
            = '';
    }

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

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

            my $ip_allowed = 0;
            RULE: for my $rule (@rules) {
                chomp $rule;
                next RULE if $rule =~ /^\s*Chain\s/;
                next RULE if $rule =~ /^\s*target\s/;

                ### ACCEPT  icmp --  0.0.0.0/0            0.0.0.0/0
                if ($proto eq 'icmp') {
                    if ($rule =~ m|^\s*ACCEPT\s+icmp\s+--\s+$src\s+
                            0\.0\.0\.0/0|x) {
                        $ip_allowed = 1;
                        last RULE;
                    }
                } else {
                    ### ACCEPT  tcp  --  128.0.0.1  0.0.0.0/0  tcp dpt:23
                    ### ACCEPT  tcp  --  128.0.0.1  0.0.0.0/0  tcp dpts:6000:6100
                    if ($rule =~ m|^\s*ACCEPT\s+$proto\s+--\s+$src\s+
                            0\.0\.0\.0/0\s+$proto\s+dpt:$port\s*$|x) {
                        $ip_allowed = 1;
                        last RULE;
                    } elsif ($rule =~ m|^\s*ACCEPT\s+$proto\s+--\s+$src\s+
                            0\.0\.0\.0/0\s+$proto\s+dpts:$port\s*$|x) {
                        ### port ranges can be specified in access.conf
                        ### like "6000:6100"
                        $ip_allowed = 1;
                        last RULE;
                    }
                }
            }
            if ($ip_allowed) {
                &logr('[-]', "source: $src already allowed " .
                    "to connect to $proto/$port in chain: $chain", 1);
            } else {
                ### open up iptables for this source, protocol, and port
                my $rv = 0;
                if ($proto eq 'icmp') {
                    &logr('[+]', "adding $chain ACCEPT rule for source: " .
                        "$src to connect via icmp", 1);
                    $rv = system("$cmds{'iptables'} -I $chain " .
                        "$config{'IPTABLES_ACCESS_RULENUM'} -p $proto -s $src " .
                        "-j ACCEPT 2> /dev/null 1>&2") >> 8;
                } else {
                    &logr('[+]', "adding $chain ACCEPT rule for source: " .
                        "$src to connect to $proto/$port", 1);
                    $rv = system("$cmds{'iptables'} -I $chain " .
                        "$config{'IPTABLES_ACCESS_RULENUM'} -p $proto -s $src " .
                        "--dport $port -j ACCEPT 2> /dev/null 1>&2") >> 8;
                }
                if ($rv == 0) {
                    ### keep track of when we first add an allow rule
                    ### for $src
                    if (not defined $ipt_access{$src}) {
                        $ipt_access{$src}{'s_time'} = time();
                        $ipt_access{$src}{'timeout'} =
                            $access_href->{'FW_ACCESS_TIMEOUT'};
                    }
                    $ipt_access{$src}{$proto}{$chain}{$port} = '';

                    ### keep track of how many times we have granted access
                    $access_href->{'grant_ctr'}++;
                } else {
                    if ($proto eq 'icmp') {
                        &logr('[-]', "there was a problem problem adding " .
                            "iptables $chain ACCEPT rule for $src via icmp", 1);
                    } else {
                        &logr('[-]', "there was a problem problem adding " .
                            "iptables $chain ACCEPT rule for $src to $proto/$port",
                            1);
                    }
                }
            }
        }
    }
    return;
}

sub timeout_access() {
    return unless %ipt_access;
    SRC: for my $src (keys %ipt_access) {
        next SRC unless ((time() - $ipt_access{$src}{'s_time'})
            > $ipt_access{$src}{'timeout'});
        for my $proto qw(tcp udp) {
            next unless defined $ipt_access{$src}{$proto};
            for my $chain (keys %{$ipt_access{$src}{$proto}}) {
                for my $port (keys %{$ipt_access{$src}{$proto}{$chain}}) {
                    if ($proto eq 'icmp') {
                        my $rv = system("$cmds{'iptables'} -D $chain " .
                            "-p $proto -s $src -j ACCEPT 2> " .
                            "/dev/null 1>&2") >> 8;
                        if ($rv == 0) {
                            &logr('[+]', "removed iptables $chain ACCEPT rule " .
                                "for $src via icmp, $ipt_access{$src}{'timeout'} " .
                                "second timeout exceeded", 1);
                        } else {
                            &logr('[-]', "there was a problem problem removing " .
                                "iptables $chain ACCEPT rule for $src via " .
                                'icmp', 1);
                        }
                    } else {
                        my $rv = system("$cmds{'iptables'} -D $chain " .
                            "-p $proto -s $src --dport $port -j ACCEPT 2> " .
                            "/dev/null 1>&2") >> 8;
                        if ($rv == 0) {
                            &logr('[+]', "removed iptables $chain ACCEPT rule " .
                                "for $src to $proto/$port, $ipt_access{$src}{'timeout'} " .
                                "second timeout exceeded", 1);
                        } else {
                            &logr('[-]', "there was a problem problem removing " .
                                "iptables $chain ACCEPT rule for $src to " .
                                "$proto/$port", 1);
                        }
                    }
                }
            }
        }
        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 posf_tcp() {
    my ($src, $len, $tos, $ttl, $id, $win) = @_;

    my $min_ttl;
    my $max_ttl;
    my $id_str;

    $posf_tcp{$src}{'len'}{$len}++;
    $posf_tcp{$src}{'tos'}{$tos}++;
    $posf_tcp{$src}{'ttl'}{$ttl}++;
    $posf_tcp{$src}{'win'}{$win}++;
    $posf_tcp{$src}{'ctr'}++;
    push @{$posf_tcp{$src}{'id'}}, $id;  ### need to maintain ordering

    print STDERR "[+] posf_tcp():  $src  LEN: $len, TOS: $tos, TTL: $ttl, ",
        "ID: $id, WIN: $win\n" if $debug;

    $id_str = &id_incr(\@{$posf_tcp{$src}{'id'}});
    for my $os (keys %posf_sigs) {
        if ($posf_tcp{$src}{'ctr'} >= $posf_sigs{$os}{'numpkts'}) {
            ($min_ttl, $max_ttl) = &ttl_range($posf_tcp{$src}{'ttl'});  ### FIXME
            if (defined $posf_tcp{$src}{'win'}{$posf_sigs{$os}{'win'}}
#                    and defined $posf_tcp{$src}{'tos'}{$posf_sigs{$os}{'tos'}}
                    and defined $posf_tcp{$src}{'len'}{$posf_sigs{$os}{'len'}}
                        ### TTL's only increase
                    and ($min_ttl > ($posf_sigs{$os}{'ttl'}-$config{'MAX_HOPS'}))
                    and ($max_ttl <= $posf_sigs{$os}{'ttl'})
                    and $id_str eq $posf_sigs{$os}{'id'}) {
                $posf{$src} = $os;
                print STDERR "[+] posf_tcp(): matched OS: $os\n" if $debug;
                return;
            }
        }
    }
    return;
}

sub id_incr() {
    my $aref = shift;
    for (my $i=0; $i<$#$aref; $i++) {
        return 'RANDOM'
            unless ($aref->[$i] < $aref->[$i+1]
                and ($aref->[$i+1] - $aref->[$i]) < 1000);
    }
    return 'SMALLINCR';
}

sub ttl_range() {
    my $href = shift;
    my $min_ttl = 256;
    my $max_ttl = 0;
    for my $ttl (keys %$href) {
        $min_ttl = $ttl if $ttl < $min_ttl;
        $max_ttl = $ttl if $ttl > $max_ttl;
    }
    return $min_ttl, $max_ttl;
}

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_posf_sigs() {
    my $posf_file = $config{'POSF_FILE'};
    open P, "< $posf_file" or die '[*] Could not open ',
        "$posf_file: $!";
    my @lines = <P>;
    close P;
    my $os = '';
    for my $line (@lines) {
        chomp $line;
        next if $line =~ /^\s*#/;
        next unless $line =~ /\S/;
        if ($line =~ /^\s*OS\s+(.*);/) {
            $os = $1;
        } elsif ($line =~ /^\s*NUMPKTS\s+(\d+);/) {
            $posf_sigs{$os}{'numpkts'} = $1;
        } elsif ($line =~ /^\s*TOS\s+(\w+);/) {
            $posf_sigs{$os}{'tos'} = $1;
        } elsif ($line =~ /^\s*LEN\s+(\d+);/) {
            $posf_sigs{$os}{'len'} = $1;
        } elsif ($line =~ /^\s*TTL\s+(\d+);/) {
            $posf_sigs{$os}{'ttl'} = $1;
        } elsif ($line =~ /^\s*ID\s+(\w+);/) {
            $posf_sigs{$os}{'id'} = $1;
        } elsif ($line =~ /^\s*WINDOW\s+(\d+);/) {
            $posf_sigs{$os}{'win'} = $1;
        }
    }
    ### make sure each of the os signatures has all fields defined
    OS: for my $os (keys %posf_sigs) {
        unless (defined $posf_sigs{$os}{'numpkts'}) {
            &logr('[-]', "$posf_file: missing NUMPKTS for os: $os", 0);
            delete $posf_sigs{$os};
            next OS;
        }
        unless (defined $posf_sigs{$os}{'tos'}) {
            &logr('[-]', "$posf_file: missing TOS for os: $os", 0);
            delete $posf_sigs{$os};
            next OS;
        }
        unless (defined $posf_sigs{$os}{'len'}) {
            &logr('[-]', "$posf_file: missing LEN for os: $os", 0);
            delete $posf_sigs{$os};
            next OS;
        }
        unless (defined $posf_sigs{$os}{'ttl'}) {
            &logr('[-]', "$posf_file: missing TTL for os: $os", 0);
            delete $posf_sigs{$os};
            next OS;
        }
        unless (defined $posf_sigs{$os}{'id'}) {
            &logr('[-]', "$posf_file: missing ID for os: $os", 0);
            delete $posf_sigs{$os};
            next OS;
        } else {
            unless ($posf_sigs{$os}{'id'} eq 'SMALLINCR'
                    || $posf_sigs{$os}{'id'} eq 'RANDOM') {
                &logr('[-]', "$posf_file: ID must " .
                    "be either SMALLINCR or RANDOM for os: $os", 0);
                delete $posf_sigs{$os};
                next OS;
            }
        }
        unless (defined $posf_sigs{$os}{'win'}) {
            &logr('[-]', "$posf_file: missing WINDOW for os: $os", 0);
            delete $posf_sigs{$os};
            next OS;
        }
    }
    print STDERR Dumper %posf_sigs if $debug and $verbose;
    &logr('[+]', 'imported backup 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;
    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:/) {
            if ($line =~ m|^\s*SOURCE:\s*($ip_re)\s*;|) {
                $access_hsh{'SOURCE'} = $1;
                $type = 'ip';
            } elsif ($line =~ m|^\s*SOURCE:\s*($ip_re/\d+)\s*;|) {  ### CIDR
                $access_hsh{'SOURCE'} = $1;
                $type = 'net';
            } elsif ($line =~ m|^\s*SOURCE:\s*($ip_re/$ip_re)\s*;|) {
                $access_hsh{'SOURCE'} = $1;
                $type = 'net';
            } elsif ($line =~ m|^\s*SOURCE:\s*ANY\s*;|) {
                $access_hsh{'SOURCE'} = 'ANY';
                $type = 'any';
            }
            $i++;
            while (defined $lines[$i] and $lines[$i] !~ /^\s*SOURCE:/) {
                my $l = $lines[$i];
                $i++;
                chomp $l;
                next if $l =~ /^\s*#/;
                next unless $l =~ /\S/;

                if ($l =~ /^\s*ENCRYPT_SEQUENCE\s*;/) {
                    $access_hsh{'ENCRYPT_SEQUENCE'} = '';
                } elsif ($l =~ /^\s*KEY:\s*(.*)\s*;/) {
                    $access_hsh{'KEY'} = $1;
                    ### pad with zeros
                    while (length($access_hsh{'KEY'}) < $enc_blocksize) {
                        $access_hsh{'KEY'} .= '0';
                    }
                } elsif ($l =~ /^\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 ($l =~ /^\s*PORT_OFFSET:\s*(\d+)\s*;/) {
                    $access_hsh{'PORT_OFFSET'} = $1;
                } elsif ($l =~ /^\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 ($l =~ /^\s*KNOCK_INTERVAL:\s*(\d+)\s*;/) {
                    $access_hsh{'KNOCK_INTERVAL'} = $1;
                } elsif ($l =~ /^\s*KNOCK_LIMIT:\s*(\d+)\s*;/) {
                    $access_hsh{'KNOCK_LIMIT'} = $1;
                } elsif ($l =~ /^\s*FW_ACCESS_TIMEOUT:\s*(\d+)\s*;/) {
                    $access_hsh{'FW_ACCESS_TIMEOUT'} = $1;
                } elsif ($l =~ /^\s*REQUIRE_OS:\s*(.*)\s*;/) {
                    $access_hsh{'REQUIRE_OS'} = $1;
                } elsif ($l =~ /^\s*REQUIRE_OS_REGEX:\s*(.*)\s*;/) {
                    $access_hsh{'REQUIRE_OS_REGEX'} = $1;
                } elsif ($l =~ /^\s*REQUIRE_USERNAME:\s*(.*)\s*;/) {
                    $access_hsh{'REQUIRE_USERNAME'} = $1;
                } elsif ($l =~ /^\s*MIN_TIME_DIFF:\s*(\d+)\s*;/) {
                    $access_hsh{'MIN_TIME_DIFF'} = $1;
                } elsif ($l =~ /^\s*MAX_TIME_DIFF:\s*(\d+)\s*;/) {
                    $access_hsh{'MAX_TIME_DIFF'} = $1;
                } elsif ($l =~ /^\s*RESTRICT_INTF:\s*(\w+)\s*;/) {
                    $access_hsh{'RESTRICT_INTF'} = $1;
                }
            }
            $i--;
        }
        if (&validate_src_access_hsh(\%access_hsh)) {
            push @{$access{$type}}, \%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.', 0);
    return;
}

sub import_config() {
    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+(.*?)\;/) {
            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;
        }
    } else {
        &logr('[-]', "$config{'ACCESS_CONF'}: source $src, missing " .
            "ENCRYPT_SEQUENCE or SHARED_SEQUENCE 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 {
        &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 = $<;
    open P, "< /etc/passwd" or die "[*] Could not open /etc/passwd.  ",
        "Exiting.\n";
    my @lines = <P>;
    close P;
    my $homedir = '';
    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;
        }
    }
    die "[*] Could not extract homedir from /etc/passwd for ",
        "uid: $uid.  Exiting.\n" unless $homedir;
    die "[*] homedir: $homedir does not exist.  ",
        "Exiting.\n" unless -d $homedir;
    return $homedir;
}

sub get_iptables_chains() {
    ### FUTURE: masquerading support?
    my $chain = 'INPUT';
    my $rv = 1;
    eval {
        $rv = (system "$cmds{'iptables'} -nL -t filter " .
            "> /dev/null 2>&1") >> 8;
    };
    if ($rv == 0) {
        eval {
            $rv = (system "$cmds{'iptables'} -t filter -I " .
                "$chain 1 -s 127.0.0.2 -j DROP > /dev/null " .
                "2>&1") >> 8;
        };
        if ($rv == 0) {
            eval {
                $rv = (system "$cmds{'iptables'} -t filter -D " .
                    "$chain 1 > /dev/null 2>&1") >> 8;
            };
        }
        if ($rv == 0) {
            $ipt_chains{$chain} = '';
        }
    }
    die "[*] Iptables does not appear to know anything about the INPUT ",
        "chain.\n    in the filter table." unless %ipt_chains;
    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;
    open MAIL, "| $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{'PID_FILE'}) {
        my $caller = $0;
        open PIDFILE, "< $config{'PID_FILE'}";
        my $pid = <PIDFILE>;
        close PIDFILE;
        chomp $pid;
        if (kill 0, $pid) {  # psad is already running
            die "[*] fwknop (pid: $pid) is already running!  Exiting.\n";
        }
    }
    return;
}

sub writepid() {
    open P, "> $config{'PID_FILE'}" or die "[*] Could not open ",
        "$config{'PID_FILE'}: $!";
    print P $$, "\n";
    close P;
    chmod 0600, $config{'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;
}

### 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 {
        ### write a message to syslog and return
        openlog 'fwknop', LOG_DAEMON, LOG_LOCAL7;
        syslog LOG_INFO, $msg;
        closelog();

        ### see if we need to send an email
        if ($send_email) {
            &sendmail("$prefix fwknop: $msg");
        }
    }
    return;
}

sub required_vars() {
    for my $var qw(FW_DATA_FILE SLEEP_INTERVAL FWKNOP_DIR PID_FILE
            CMDLINE_FILE POSF_FILE P0F_FILE ACCESS_CONF MAX_HOPS
            IPTABLES_ACCESS_RULENUM EMAIL_ADDRESSES) {
        unless (defined $config{$var}) {
            die "[*] Variable $var is not defined in $config_file";
        }
    }
    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]
              [-D] [-e] [-a <allow-ip>] [-p <port>] [-P <protocol>] [-g]
              [--offest <port offset>] [--os] [-i <iptables-log>] [-v] [-V]
              [-h]

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.
    -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.
    -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.
    -i, --ipt-log              - Specify path to iptable logfile.  This is
                                 used only when running in --os mode.
    -g, --get-key              - Get encryption key from ~/.fwknoprc file
                                 instead of from STDIN.
    -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;
}
