#!/usr/bin/perl -w
#
#############################################################################
#
# File: fwknop
#
# Purpose: fwknop implements an authorization scheme known as Single Packet
#          Authorization (SPA) that requires only a single encrypted packet
#          to communicate various pieces of information including desired
#          access through an iptables policy and/or specific commands to
#          execute on the target system.  The main application of this
#          program is to protect services such as SSH with an additional
#          layer of security in order to make the exploitation of
#          vulnerabilities (both 0-day and unpatched code) much more
#          difficult.  For more information, see the fwknop(8) man page.
#
# Author: Michael Rash (mbr@cipherdyne.org)
#
# Version: 1.9.1
#
# Copyright (C) 2004-2007 Michael Rash (mbr@cipherdyne.org)
#
# License (GNU Public License):
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
#    You should have received a copy of the GNU General Public License
#    along with this program; if not, write to the Free Software
#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307
#    USA
#
#############################################################################
#
# $Id: fwknop 586 2006-11-04 20:45:49Z mbr $
#

use lib '/usr/lib64/fwknop';
use Crypt::CBC;
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 $version = '1.9.1';
my $revision_svn = '$Revision: 979 $';
my $rev_num = '1';
($rev_num) = $revision_svn =~ m|\$Rev.*:\s+(\S+)|;

my $print_version = 0;
my $print_help    = 0;
my $run_last_args = 0;
my $debug         = 0;
my $quiet         = 0;
my $verbose       = 0;
my $test_mode     = 0;
my $cmdl_homedir  = '';
my $knock_sleep   = 1;  ### default to 1 second difference between port knocks
my $knock_dst     = '';
my $homedir       = '';
my $spoof_src     = '';
my $server_mode   = 'pcap';
my $user_rc_file  = '';
my $server_proto  = '';
my $run_last_host = '';
my $show_last_host_cmd = '';
my $show_last_cmd = 0;
my $gpg_home_dir  = '';
my $gpg_recipient = '';
my $use_gpg_agent = 0;
my $gpg_agent_info = '';
my $max_msg_len   = 1500;
my $gpg_verbose   = 0;
my $gpg_default_key = 0;
my $err_wait_timer  = 30;  ### seconds
my $resolve_ip_url  = 'http://www.whatismyip.org/';
my $gpg_signing_key = '';
my $save_packet_mode = 0;
my $save_packet_file = '';
my $cmdline_pcap_cmd   = '';
my $no_save_last_args  = 0;
my $save_destination   = 0;
my $server_auth_method = '';
my $spa_established_tcp  = 0;
my $resolve_external_ip  = 0;
my $server_auth_crypt_pw = '';
my $pcap_sleep_interval  = 1;  ### seconds
my $knock_dst_pre_resolve = '';

### User agent for contacting http://www.whatismyip.org/, (can
### override with --User-agent)
my $ext_resolve_user_agent = "Fwknop/$version";
$ext_resolve_user_agent =~ s|-pre\d+||;

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

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

### default to root (client must run as root in this mode)
my $spoof_username = '';
my $spoof_proto    = 'udp';  ### default to udp

### encrypted port knock vars (these are only used in the legacy
### port knocking mode).
my $cmdline_offset  = 0;
my $enc_port_offset = 61000;  ### default offset
my $enc_key         = '';
my $enc_alg         = 'Rijndael';
my $enc_blocksize   = 32;

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

my $enc_shared_secret = '';
my $enc_allow_ip      = '';
my $enc_source_ip     = '';
my $enc_rotate_proto  = 0;
my $get_key_file      = '';  ### get key from file
my $enc_pcap_port     = 62201;  ### default pcap port
my $access_str        = '';
my $forward_access_str = '';  ### for access through the iptables FORWARD chain

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

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

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

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

my @args_cp = @ARGV;

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

&usage(0) if $print_help;

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

die "[*] Cannot run in both --quiet and --verbose modes simultaneously"
    if $quiet and $verbose;

die "[*] Must also specify a GnuPG signing key with --gpg-signing-key or\n",
    "    use --gpg-default-key to use a default key (specified in\n",
    "    ~/.gnupg/options with the default-key variable).\n"
    if ($gpg_recipient and (not $gpg_default_key and not $gpg_signing_key));

die "[*] Must specify a GnuPG recipient key (on the fwknopd side) with\n",
    "    --gpg-recipient"
    if (($gpg_default_key or $gpg_signing_key) and not $gpg_recipient);

die "[*] Cannot spoof source address for a real TCP socket."
    if ($spoof_src and $spa_established_tcp);

die "[*] Server auth method not supported in FORWARD access mode.\n"
    if $server_auth_method and $forward_access_str;

### this is only necessary for older versions of perl (newer versions
### call srand() automatically at the first usage of rand() if srand()
### was not already called).
srand();

&get_homedir();

### save a copy
$knock_dst_pre_resolve = $knock_dst;

if ($run_last_args or $show_last_cmd) {

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

} elsif ($run_last_host or $show_last_host_cmd) {

    $run_last_host = $show_last_host_cmd if $show_last_host_cmd;

    ### run fwknop with the last args for this particular knock destination
    &run_last_host_cmdline();
}

die "[*] Must specify a destination server with -D <IP|Host>"
    unless $knock_dst;

my $print_mode = '';
if (lc($server_mode) eq 'pcap') {
    $print_mode = 'SPA';
} elsif (lc($server_mode) eq 'knock') {
    $print_mode = 'encrypted port knocking';
} elsif (lc($server_mode) eq 'shared') {
    $print_mode = 'shared sequence port knocking';
} else {
    die "[*] Unknown server mode: $server_mode ",
        qq|(must be "pcap", "knock", or "shared").|;
}

if ($debug) {
    print "[+] ***DEBUG*** Starting fwknop client ($print_mode mode)...\n";
} else {
    print "[+] Starting fwknop client ($print_mode mode)...\n"
        unless $quiet;
}

if ($verbose) {
    print "[+] Command line: @args_cp\n";
}

unless ($knock_dst =~ /$ip_re/) {
    print "[+] Resolving hostname: $knock_dst\n" unless $quiet;
    ### resolve to an IP
    my $iaddr = inet_aton($knock_dst)
        or die "[*] Could not resolve $knock_dst to an IP.";
    my $addr = inet_ntoa($iaddr)
        or die "[*] Could not resolve $knock_dst to an IP.";
    $knock_dst = $addr;
}

&validate_access_str() if $access_str;
&validate_forward_access_str() if $forward_access_str;

if (lc($server_mode) eq 'pcap' or lc($server_mode) eq 'knock') {
    die "[*] Must also specify: -D <knock destination>\n"
        unless $knock_dst;

    unless ($enc_allow_ip
            or $enc_source_ip
            or $resolve_external_ip) {
        die "[*] Must either specify: --allow-IP <IP>, ",
            "--source-IP, or --Resolve-external-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;

    ### resolve the extenal IP via http://www.whatismyip.org
    $enc_allow_ip = &resolve_external_ip() if $resolve_external_ip;

    if ($spoof_src) {
        $< == 0 && $> == 0 or
            die '[*] You must be root (or equivalent ',
                "UID 0 account) to spoof the source address.\n";
    }
    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 (lc($server_mode) eq 'pcap') {
            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 (lc($server_mode) eq 'pcap') {
        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.';
    }
}

if ($save_packet_mode) {
    ### save of copy of the packet
    unless ($save_packet_file) {
        $save_packet_file = "$homedir/fwknop_save_packet.$$";
    }
    unlink $save_packet_file if -e $save_packet_file;
}

### save our command line args (so -l can be used next time)
unless ($run_last_args or $run_last_host or $no_save_last_args
        or $show_last_cmd or $show_last_host_cmd) {
    &save_args();
}

if (lc($server_mode) eq 'pcap' or lc($server_mode) eq 'knock') {

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

    &handle_server_auth_method() if $server_auth_method;

    if (lc($server_mode) eq 'pcap') {

        ### 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 non-encrypted port knocking mode, so get
    ### the port sequence
    &knock_ports(&import_shared_sequence());
}
exit 0;
#============================ end main ==============================

sub pcap_build_enc_msg() {

    my $msg         = '';
    my $server_auth = '';  ### contains stuff after the md5 sum
    my $user        = '';
    my $random_num  = '';
    my $md5sum      = '';
    my $timestamp   = time();

    ### message format (all fields are separated by ":" characters
    #
    #  random number (16 bytes)
    #  username
    #  timestamp
    #  software version
    #  message type and content:
    #    0 => command mode / command to execute
    #    1 => access mode / IP,proto,port
    #    2 => forward access mode / IP,proto,port / internalIP,externalNATPort
    #  (optional) server_auth (post 0.9.2 release)
    #  message md5 sum

    unless ($quiet) {
        print "\n[+] Building encrypted Single Packet Authorization (SPA) ",
            "message...\n";
        print "[+] Packet fields:\n\n";
    }

    ### start the SPA message with 16 bytes of random data
    $msg = &SPA_random_number();

    ### append the username
    $msg .= &SPA_user();

    ### append the timestamp
    $msg .= &SPA_timestamp();

    ### append the fwknop client version
    $msg .= &SPA_version();

    ### append the message type (integer)
    $msg .= &SPA_message_type();

    ### append the SPA message (this is usually just a request for
    ### access to a port/protocol combination)
    $msg .= &SPA_message();

    ### append server authentication method (optional)
    $msg .= &SPA_server_auth();

    ### append FORWARD access requirement (optional)
    $msg .= &SPA_forward_access();

    ### append MD5 sum
    $msg =~ s/\n//g;
    $msg .= &SPA_md5sum($msg);

    print "\n[+] Clear text message (fields base64 encoded): $msg\n"
        if $debug;

    if ($gpg_signing_key or $gpg_default_key) {
        return &pcap_GPG_encrypt_msg($msg);
    }
    return &pcap_Rijndael_encrypt_msg($msg);
}

sub SPA_random_number() {
    my $random_num = int(rand(100000000000000));
    $random_num .= int(rand(10)) while (length($random_num) < 16);

    print "        Random data:    $random_num\n" unless $quiet;

    return $random_num;
}

sub SPA_user() {
    my $user = 'root';
    if ($spoof_src) {
        if ($spoof_username) {
            $user = $spoof_username;
        }
    } else {
        ### getlogin() is better than using ENV{'USER'}, which is
        ### easily manipulated, so only use as a last resort.
        if ($spoof_username) {
            $user = $spoof_username;
        } else {
            $user = getlogin() || getpwuid($<) ||
                die "[*] Could not determine user; try using the ",
                    "--Spoof-user option";
        }
    }
    print  "        Username:       $user\n" unless $quiet;
    return ':' . encode_base64($user);
}

sub SPA_timestamp() {
    my $timestamp = time();
    print "        Timestamp:      $timestamp\n" unless $quiet;
    return ':' . $timestamp;
}

sub SPA_version() {
    print "        Version:        $version\n" unless $quiet;
    return ':' . $version;
}

sub SPA_message_type() {
    if ($cmdline_pcap_cmd) {
        print "        Type:           $SPA_COMMAND_MODE (command mode)\n"
            unless $quiet;
        return ':' . $SPA_COMMAND_MODE;
    } elsif ($forward_access_str) {
        print "        Type:           $SPA_FORWARD_ACCESS_MODE ",
            "(FORWARD access mode)\n" unless $quiet;
        return ':' . $SPA_FORWARD_ACCESS_MODE;
    }
    print "        Type:           $SPA_ACCESS_MODE (access mode)\n"
        unless $quiet;
    return ':' . $SPA_ACCESS_MODE;
}

sub SPA_message() {
    ### a specific command will be executed on the server.  Note we
    ### prepend the command string with the $enc_allow_ip so that the
    ### fwknopd server can apply the REQUIRE_SOURCE_ADDRESS criteria.
    if ($cmdline_pcap_cmd) {
        print "        Cmd:            $cmdline_pcap_cmd\n" unless $quiet;
        return ':' . encode_base64("$enc_allow_ip,$cmdline_pcap_cmd");

    }
    unless ($access_str) {
        $access_str = 'none,0';
    }
    ### access to port(s)/protocol(s) will be granted on the
    ### server
    print "        Access:         $enc_allow_ip,$access_str\n"
        unless $quiet;
    return ':' . encode_base64("$enc_allow_ip,$access_str");
}

sub SPA_server_auth() {
    if (lc($server_auth_method) eq 'crypt') {
        unless ($quiet) {
            print "        Server auth:   $server_auth_method,";
            for (my $i=0; $i<length($server_auth_crypt_pw); $i++) {
                print '*';
            }
            print "\n";
        }
        return ':' . encode_base64("crypt,$server_auth_crypt_pw");
    }
    return '';
}

sub SPA_forward_access() {
    if ($forward_access_str) {
        print "        Forward access: $forward_access_str\n"
            unless $quiet;
        return ':' . encode_base64($forward_access_str);
    }
    return '';
}

sub SPA_md5sum() {
    my $msg = shift;
    my $sum = md5_base64($msg);
    print "        MD5 sum:        $sum\n" unless $quiet;
    return ':' . $sum;
}

sub pcap_GPG_encrypt_msg() {
    my $msg = shift;

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

    $gpg_home_dir = "$homedir/.gnupg" unless $gpg_home_dir;

    if ($gpg_verbose) {
        $gnupg->options->hash_init(
            'homedir' => $gpg_home_dir);
    } else {
        $gnupg->options->hash_init(
            'batch' => 1,
            'homedir' => $gpg_home_dir);
    }

    ### if --gpg-default-key is given, then we trust that the user has
    ### set the default key with the default-key variable in ~/.gnupg/options
    $gnupg->options->default_key($gpg_signing_key) unless $gpg_default_key;

    $gnupg->options->push_recipients($gpg_recipient);

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

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

    my $pid;

    if ($use_gpg_agent or $gpg_agent_info) {
        if ($gpg_agent_info) {
            $ENV{'GPG_AGENT_INFO'} = $gpg_agent_info;
        }
        $pid = $gnupg->sign_and_encrypt('handles' => $handles,
            'command_args' => [ qw( --use-agent ) ]);
    } else {
        $pid = $gnupg->sign_and_encrypt('handles' => $handles);
    }

    print $pw $enc_key;
    close $pw;

    print $input $msg;
    close $input;

    my @ciphertext = <$output>;
    close $output;

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

    waitpid $pid, 0;

    my $ctext = '';
    if (@ciphertext) {
        $ctext = join '', @ciphertext;
    }

    unless ($ctext) {
        print "[*] GnuPG encrypt failed.\n";
        unless ($gpg_verbose) {
            print "    GnuPG errors:\n";
            print for @errors;
        }
        exit 1;
    }

    my $encoded_msg = encode_base64($ctext);

    $encoded_msg =~ s/=*$//;
    $encoded_msg =~ s/\n//g;

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

sub pcap_Rijndael_encrypt_msg() {
    my $msg = shift;

    my $cipher = Crypt::CBC->new(
        {
            'key'             => $enc_key,
            'cipher'          => $enc_alg
        }
    );
    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 ($verbose) {
        print "\n[+] Packet data:\n\n", $msg, "\n\n" unless $quiet;
    }

    if ($save_packet_mode) {
        print "    Saving packet data to: $save_packet_file\n" unless $quiet;
        open F, "> $save_packet_file" or die "[*] Could not open ",
            "$save_packet_file: $!";
        print F $msg;
        close F;
    }

    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
"\n[+] Sending $msg_len byte message to $knock_dst over $spoof_proto",
    "/$enc_pcap_port\n    (spoofed src ip: $spoof_src).\n" unless $quiet;
        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;

        ### use Net::RawIP to spoof the packets
        require Net::RawIP;

        if ($spoof_proto eq 'udp') {
            my $rawpkt = new Net::RawIP({
                ip => {
                    saddr => $spoof_src,
                    daddr => $knock_dst
                },
                udp =>{}});
            $rawpkt->set({ ip => {
                    saddr => $spoof_src,
                    daddr => $knock_dst
                },
                udp => {
                    source => $rand_src_port,
                    dest   => $enc_pcap_port,
                    data   => $msg,
                }
            });
            $rawpkt->send();
        } elsif ($spoof_proto eq 'icmp') {
            my $rawpkt = new Net::RawIP({
                ip => {
                    saddr => $spoof_src,
                    daddr => $knock_dst
                },
                icmp =>{}});
            $rawpkt->set({ ip => {
                    saddr => $spoof_src,
                    daddr => $knock_dst
                },
                icmp => {
                    type => 0,
                    code => 0,
                    sequence => 0,
                    data => $msg
                }
            });
            $rawpkt->send();
        } elsif ($spoof_proto eq 'tcp') {
            my $rawpkt = new Net::RawIP({
                ip => {
                    saddr => $spoof_src,
                    daddr => $knock_dst
                },
                tcp =>{}});
            $rawpkt->set({ ip => {
                    saddr => $spoof_src,
                    daddr  => $knock_dst
                },
                tcp => {
                    ack => 1,
                    source => $rand_src_port,
                    dest   => $enc_pcap_port,
                    data => $msg
                }
            });
            if ($test_mode) {
                print "    --Test-mode enabled, SPA packet not sent.\n";
            } else {
                $rawpkt->send();
            }
        }
    } else {

        if ($spa_established_tcp) {  ### useful for Tor
            print "\n[+] Sending $msg_len byte message to $knock_dst ",
                "over established tcp/$enc_pcap_port socket...\n"
                unless $quiet;

            if ($test_mode) {
                print "    --Test-mode enabled, SPA packet not sent.\n";
            } else {
                my $socket = IO::Socket::INET->new(
                    PeerAddr => $knock_dst,
                    PeerPort => $enc_pcap_port,
                    Proto    => 'tcp',
                    Timeout  => 1
                ) or die "[*] Could not acquire TCP/$enc_pcap_port socket ",
                        "with $knock_dst: $!";

                $socket->send($msg);
                undef $socket;
            }
        } else {
            print "\n[+] Sending $msg_len byte message to $knock_dst ",
                "over udp/$enc_pcap_port...\n" unless $quiet;

            if ($test_mode) {
                print "    --Test-mode enabled, SPA packet not sent.\n";
            } else {
                my $socket = IO::Socket::INET->new(
                    PeerAddr => $knock_dst,
                    PeerPort => $enc_pcap_port,
                    Proto    => 'udp',
                    Timeout  => 1
                ) or die "[*] Could not acquire UDP socket: $!";

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

sub knock_ports() {
    my $ports_aref = shift;

    print "[+] Sending port knocking sequence to knock server: $knock_dst\n"
        unless $quiet;
    my $packet_ctr = 0;
    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 {
            print "    -> $knock_dst $proto/$port (packet: $packet_ctr)\n";
            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;
        }
        $packet_ctr++;
    }
    print "[+] Finished knock sequence.\n";
    return;
}

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

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

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

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

    my $proto_num      = 0;
    my $enc_allow_port = 0;
    if ($access_str =~ /tcp/i) {
        $proto_num = 6;
        if ($access_str =~ /(\d+)/) {
            $enc_allow_port = $1;
        }
    } elsif ($access_str =~ /udp/i) {
        $proto_num = 17;
        if ($access_str =~ /(\d+)/) {
            $enc_allow_port = $1;
        }
    } elsif ($access_str =~ /icmp/i) {
        $proto_num = 1;
        $enc_allow_port = 0;
    }

    unless ($enc_allow_port) {
        die "[*] Must specify port to open."
            if $proto_num != 1;
    }
    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;
            }
        }
    }

    my @tmp_chars = split //, $clear_txt;
    print '[+] Clear-text sequence (' . length($clear_txt) . ' bytes): ';
    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 (' . length($cipher_txt) . ' bytes): ';
    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 resolve_external_ip() {

    my $external_ip = '';
    my $site_host   = '';
    my $url         = '/';

    if ($resolve_ip_url) {
        die "[*] $resolve_ip_url does not begin with http://"
            unless $resolve_ip_url =~ m|http://|i;

        if ($resolve_ip_url =~ m|http://(\S+?)/(\S+)|i) {
            $site_host = $1;
            $url       = "/$2";
        } elsif ($resolve_ip_url =~ m|http://(\S+?)/|i) {
            $site_host = $1;
        } elsif ($resolve_ip_url =~ m|http://(\S+)|i) {
            ### there is no trailing slash
            $site_host = $1;
        } else {
            die "[*] Could not get external hostname from $resolve_ip_url";
        }
    }

    print "    Resolving external IP via: $resolve_ip_url\n"
        unless $quiet;
    my $w_ip_tmp = inet_aton($site_host)
        or die "[*] Could not resolve $site_host to an IP.";
    my $w_ip = inet_ntoa($w_ip_tmp)
        or die "[*] Could not resolve $site_host to an IP.";

    my $sock = new IO::Socket::INET(
        PeerAddr => $w_ip,
        PeerPort => 80,
        Proto    => 'tcp',
        Timeout  => 7)
    or die "[*] Could not open tcp/80 socket with $resolve_ip_url";

    if (defined($sock)) {
        print $sock "GET $url HTTP/1.0\r\n",
            "Host: $site_host\r\n",
            "User-Agent: $ext_resolve_user_agent\r\n",
            "Accept: */*\r\n",
            "Connection: Keep-Alive\r\n\r\n";
        recv($sock, my $web_data, 1500, 0);
        close $sock;
        $web_data =~ s/[^\w\.]/ /g;
        if ($debug) {
            print "[+] Web server data from: $resolve_ip_url\n",
                $web_data, "\n";
        }
        if ($resolve_ip_url =~ /whatismyip/i) {
            if ($web_data =~ /WhatIsMyIP\.com\s+-\s+($ip_re)\b/i) {
                $external_ip = $1;
            }
        }
        unless ($external_ip) {
            ### greedy match to the last instance of a matching
            ### IP regex so that we get past any HTTP header info
            ### that might happen to match the IP regex
            if ($web_data =~ /.*\s($ip_re)\b/i) {
                $external_ip = $1;
            }
        }
    }
    unless ($external_ip) {
        print "[*] Could not extract external IP from $resolve_ip_url\n";
        unless ($debug) {
            print
"    You might try running with --debug and looking at the response from\n",
"    the webserver. Maybe it is trying to set a cookie?\n";
        }
        exit 1;
    }

    print "    Got external address: $external_ip\n\n" unless $quiet;
    return $external_ip;
}

sub get_key() {

    if ($gpg_signing_key or $gpg_default_key) {

        ### load the GnuPG::Interface module
        require GnuPG::Interface;

        ### we don't need a password if we are going to acquire
        ### a password from gpg-agent
        return if $use_gpg_agent;
    }

    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;
                last;
            } elsif ($line =~ /$knock_dst_pre_resolve:\s*(.*)/) {
                $enc_key = $1;
                last;
            }
        }
die "[*] Could not read encryption key for $knock_dst from $get_key_file\n",
    "    fwknop expects the format \"$knock_dst: <KEY>\n    in file: ",
    "$get_key_file\n"
            unless $enc_key;
    } else {
        if ($gpg_signing_key or $gpg_default_key) {
            print
"[+] Enter the GnuPG password for signing key: $gpg_signing_key\n\n"
            unless $quiet;
        } else {
            print
"[+] Enter an encryption key. This key must match a key in the file\n",
"    /etc/fwknop/access.conf on the remote system.\n\n" unless $quiet;
        }
        my $try = 0;
        my $max_tries = 20;
        ReadMode 'noecho';
        KEY: while (1) {
            $try++;
            die "[*] Exceeded $max_tries tries to read valid password."
                if $try >= $max_tries;
            if ($gpg_signing_key or $gpg_default_key) {
                print "GnuPG signing password: ";
            } else {
                print "Encryption Key: ";
            }
            my $ans = ReadLine 0;
            next KEY unless defined $ans;
            next KEY unless $ans =~ /\S/;
            chomp $ans;
            if ($gpg_signing_key or $gpg_default_key) {
                $enc_key = $ans;
                last KEY;
            } else {
                if (length($ans) >= 8) {
                    $enc_key = $ans;
                    last KEY;
                } else {
                    print "\n[-] The symmetric key must be at least ",
                        "8 characters long.\n";
                }
            }
        }
        ReadMode 'normal';
        print "\n";

        die "[*] Could not read encryption key from STDIN.  Exiting."
            unless $enc_key;
    }
    unless ($gpg_signing_key or $gpg_default_key) {
        unless (length($enc_key) >= 8) {
            die "\n[-] The symmetric key must be at least ",
                "8 characters long.\n";
        }
        ### pad out to the key size
        while (length($enc_key) < $enc_keysize) {
            $enc_key .= '0';
        }
    }
    return;
}

sub import_shared_sequence() {
    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# <knockdst>: <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 > 0;
            $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 get_homedir() {
    my $uid = $<;
    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 handle_server_auth_method() {
    if (lc($server_auth_method) eq 'crypt') {
        ReadMode 'noecho';
        while (1) {
            $quiet == 1 ? print "UNIX crypt() password: "
                : print "    UNIX crypt() password: ";
            my $ans = ReadLine 0;
            chomp $ans;
            next unless $ans =~ /\S/;
            $server_auth_crypt_pw = $ans;
            last;
        }
        ReadMode 'normal';
        print "\n";
        return;
    }
    die "[*] --Server-auth must be 'crypt'";
}

sub save_args() {
    my $save_file  = "$homedir/.fwknop.run";
    my $hosts_file = "$homedir/.fwknop.hosts";

    open S, "> $save_file" or die "[*] Could not open $save_file";
    print S "@args_cp\n";
    close S;

    if ($save_destination) {
        open D, "> $homedir/.fwknop.save"
            or die "[*] Could not open $homedir/.fwknop.save";
        print D "@args_cp\n";
        close D;
    }

    my @host_lines = ();
    my $matched_dst = 0;
    if (-e $hosts_file) {
        open F, "< $hosts_file" or die "[*] Could not open $hosts_file";
        while (<F>) {
            if (/-(k|D)\S*\s+$knock_dst_pre_resolve/) {
                ### if an older command is for the same knock destination
                ### then substitute the current command (doesn't yet support
                ### multiple commands per knock destination since we would
                ### need a way to select among them)
                push @host_lines, "@args_cp\n";
                $matched_dst = 1;
            } else {
                push @host_lines, $_;
            }
        }
        close F;
    }
    push @host_lines, "@args_cp\n" unless $matched_dst;

    open H, "> $hosts_file" or die "[*] Could not open $hosts_file";
    print H for @host_lines;
    close H;
    return;
}

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

    exit 1 unless GetOptions(
        'Server-port=i'  => \$enc_pcap_port,
        'Server-mode=s'  => \$server_mode,
        'Server-cmd=s'   => \$cmdline_pcap_cmd,
        'Server-proto=s' => \$server_proto,
        'Server-auth=s'  => \$server_auth_method,
        'Spoof-src=s'    => \$spoof_src,
        'Spoof-user=s'   => \$spoof_username,
        'Spoof-proto=s'  => \$spoof_proto,
        'Save-packet'    => \$save_packet_mode,
        'Save-packet-file=s'  => \$save_packet_file,
        'Save-dst'       => \$save_destination,
        'user-rc=s'      => \$user_rc_file,
        'knock-dst=s'    => \$knock_dst,
        'Destination=s'  => \$knock_dst,
        'gpg-signing-key=s' => \$gpg_signing_key,
        'gpg-recipient=s'   => \$gpg_recipient,
        'gpg-default-key'   => \$gpg_default_key,
        'gpg-home-dir=s'    => \$gpg_home_dir,
        'gpg-verbose'    => \$gpg_verbose,
        'gpg-agent'      => \$use_gpg_agent,
        'gpg-agent-info=s' => \$gpg_agent_info,
        'quiet'          => \$quiet,
        'Forward-access=s' => \$forward_access_str,
        'TCP-sock'       => \$spa_established_tcp,
        'Access=s'       => \$access_str,
        'allow-IP=s'     => \$enc_allow_ip,
        'source-IP'      => \$enc_source_ip,
        'rotate-proto'   => \$enc_rotate_proto,
        'offset=i'       => \$cmdline_offset,
        'time-delay=i'   => \$knock_sleep,
        'last-cmd'       => \$run_last_args,
        'no-save-args'   => \$no_save_last_args,
        'Last-host=s'    => \$run_last_host,
        'Show-last-cmd'  => \$show_last_cmd,
        'Show-host-cmd=s' => \$show_last_host_cmd,
        'Resolve-external-IP' => \$resolve_external_ip,
        'whatismyip'     => \$resolve_external_ip, # for backwards compatibility
        'URL=s'          => \$resolve_ip_url,
        'User-agent=s'   => \$ext_resolve_user_agent,
        'get-key=s'      => \$get_key_file,
        'Home-dir=s'     => \$cmdl_homedir,
        'Test-mode'      => \$test_mode,
        'debug'          => \$debug,
        'verbose'        => \$verbose,
        'Version'        => \$print_version,
        'help'           => \$print_help
    );
    return;
}

sub run_last_cmdline() {

    my $found_file = 0;

    for my $save_file ("$homedir/.fwknop.save", "$homedir/.fwknop.run") {
        next unless -e $save_file;
        $found_file = 1;

        open S, "< $save_file" or die "[*] Could not open $save_file: $!";
        my $arg_line = <S>;
        close S;
        chomp $arg_line;

        if ($show_last_cmd) {
            print "[+] Last fwknop client command line: $arg_line\n";
            exit 0;
        }
        print "[+] Running with last command line args: $arg_line\n"
            unless $quiet;
        @ARGV = split /\s+/, $arg_line;

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

        last;
    }

    unless ($found_file) {
        die "[*] fwknop argument save files (~/.fwknop.save and ",
            "~/.fwknop.run) not found.";
    }
    return;
}

sub run_last_host_cmdline() {

    my $found_file = 0;
    my $found_host = 0;
    for my $save_file ("$homedir/.fwknop.save", "$homedir/.fwknop.hosts") {
        next unless -e $save_file;
        $found_file = 1;

        my $arg_line = '';
        open H, "< $save_file" or die "[*] Could not open $save_file: $!";
        while (<H>) {
            if (/-(k|D)\S*\s+$run_last_host/) {
                $arg_line = $_;
                last;
            }
        }
        close H;

        if ($arg_line) {
            chomp $arg_line;

            if ($show_last_host_cmd) {
                print "[+] Last command run for host: $show_last_host_cmd\n",
                    "    $arg_line\n";
                exit 0;
            }
            print "[+] Running with last command line args: $arg_line\n"
                unless $quiet;
            @ARGV = split /\s+/, $arg_line;

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

            $found_host = 1;
            last;
        }
    }

    unless ($found_file) {
        die "[*] fwknop argument save files (~/.fwknop.save and ",
            "~/.fwknop.hosts) not found.";
    }

    unless ($found_host) {
        print "[-] No matching destination host in ~/.fwknop.save ",
            "or ~/.fwknop.hosts\n";
    }
    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_forward_access_str() {
    $forward_access_str = lc($forward_access_str);
    unless ($forward_access_str =~ /^$ip_re,\d+$/) {
        die "[*] Must specify '<internal_IP>,<external_port>'";
    }
    return;
}

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

fwknop; Single Packet Authorization client

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

Usage: fwknop -A <port list> [-s|-R|-a] -D <spa_server> [options]

Options:
    -A, --Access  <port list>  - Provide a list of ports/protocols to open
                                 on the server. The format is
                                 "<proto>/<port>...<proto>/<port>". E.g.
                                 "tcp/22,udp/53".
    -D, --Destination <IP>     - The IP address of the fwknopd server (the
                                 IP want to connect to).
    -l, --last-cmd             - Run the fwknop with the same command line
                                 arguments as in the previous invocation.
                                 The args are stored in ~/fwknop.run.
    -L, --Last-host <host>     - Run last command line arguments for <host>.
    --gpg-signing-key <key ID> - ID for key used to sign GnuPG encrypted
                                 message (e.g. "0xABCD1234").
    --gpg-recipient <recip>    - Recipient of GnuPG encrypted message.
    --gpg-default-key          - Use the key that GnuPG defines as the
                                 default (i.e. the key that is specified
                                 by the default-key option in
                                 ~/.gnupg/options).
    --gpg-home-dir <dir>       - Path to GnuPG home dir (e.g.
                                 /home/user/.gnupg).
    --gpg-agent                - Acquire GnuPG signing password from a
                                 running gpg-agent.
    --gpg-agent-info <info>    - Specify the value for the GPG_AGENT_INFO
                                  environment variable as returned by
                                  'gpg-agent --daemon'.
    --gpg-verbose              - Display all output from GnuPG process.
    -a, --allow-IP <IP>        - IP to instruct the remote fwknop server to
                                 allow through the firewall ruleset.
    -s, --source-IP            - Inform the destination fwknop server to use
                                 the source address from which the SPA
                                 packet originates (useful for
                                 authenticating to the SPA server from
                                 behind a NAT device). Note that the -w
                                 option should really be used instead.
    -R, --Resolve-external-IP  - Resolve client IP via the
                                 http://www.whatismyip.org/ website. This is
                                 useful if fwknop is deployed on an internal
                                 system behind a NAT device.
    -w, --whatismyip           - (Synonym for --Resolve-external-IP option).
    --URL <external IP URL>    - Specify a URL from which to determine the
                                 external IP (the default is
                                 http://www.whatismyip.org/).
    --User-agent <string>      - Specify the user agent string to use when
                                 resolving IP via http://www.whatismyip.org
                                 (must use the -R option). The default user
                                 agent is: $ext_resolve_user_agent
    --Save-dst                 - Save the command line args for this
                                 invocation against the destination to the
                                 special file ~/.fwknop.save (this file
                                 provides a priority location that is only
                                 overwritten with --Save-dst and is useful
                                 for an fwknop client command that you want
                                 to always preserve).
    --Save-packet              - Save a copy of an encrypted SPA packet to
                                 to a file (~/fwknop_save_packet.<pid> by
                                 default).
    --Save-packet-file         - Specify the path to the file where the
                                 encrypted SPA packet is stored when the
                                 --Save-packet argument is used.
    --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" or "shared").
    --Server-cmd <cmd>         - Specify a complete command that an fwknop
                                 server should execute (as root).
    --Server-auth <method>     - Provide additional authentication
                                 information that the fwknopd server can
                                 apply (such as integration with crypt()).
    --Spoof-src <IP>           - Spoof the source IP address (requires
                                 fwknop to be run as root).
    --Spoof-user <username>    - Supply a non-root username when spoofing
                                 the source address.
    --Spoof-proto <protocol>   - Send authentication packet over the
                                 specified protocol (tcp, udp, or icmp)
                                 when spoofing the source address.
    -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.
    --get-key <file>           - Get encryption key from <file> instead of
                                 from STDIN.
    --TCP-sock                 - Send SPA packets over an established TCP
                                 socket with the fwknopd server.  This
                                 allows SPA packets to be sent over the Tor
                                 network.
    --no-save-args             - Do not save command line args to
                                 ~/.fwknop.run file.
    --Show-last-cmd            - Display the last fwknop command and exit.
    --Show-host-cmd <host>     - Display the last fwknop command that was
                                 executed for <host> and exit.
    -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.
    -H, --Home-dir <directory> - Specify the home directory of the current
                                 user that is running fwknop.
    -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.
    -k, --knock-dst <IP>       - Connection destination IP address for port
                                 knock sequence (synonym for -D).
    --Test-mode                - Build SPA packet data but do not send it
                                 over the network.
    -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;
}
