#!/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 a Netfilter policy and/or specific commands to
#          execute on the target system.  The main application of this
#          program is to protect services such as SSH with an additional
#          layer of security in order to make the exploitation of
#          vulnerabilities (both 0-day and unpatched code) much more
#          difficult.  For more information, see the fwknop(8) man page.
#
# Author: Michael Rash (mbr@cipherdyne.org)
#
# Version: 0.9.7
#
# Copyright (C) 2004-2006 Michael Rash (mbr@cipherdyne.org)
#
# License (GNU Public License):
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
#    You should have received a copy of the GNU General Public License
#    along with this program; if not, write to the Free Software
#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307
#    USA
#
#############################################################################
#
# $Id: fwknop 498 2006-08-05 02:04:52Z mbr $
#

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

my $version = '0.9.7';

my $print_version  = 0;
my $print_help     = 0;
my $run_last_args  = 0;
my $debug          = 0;
my $quiet          = 0;
my $verbose        = 0;
my $whatismyip_src = 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 $gpg_verbose    = 0;
my $gpg_signing_key = '';
my $gpg_recipient   = '';
my $gpg_home_dir    = '';
my $max_msg_len        = 1500;
my $whatismyip_host    = 'www.whatismyip.com';
my $no_save_last_args  = 0;
my $server_auth_method   = '';
my $spa_established_tcp  = 0;
my $server_auth_crypt_pw = '';
my $err_wait_timer       = 30;  ### seconds
my $cmdline_pcap_cmd     = '';
my $pcap_sleep_interval  = 1;  ### seconds
my $knock_dst_pre_resolve = '';

### User agent for contacting http://www.whatismyip.com/, we don't
### advertise the fwknop client (can override with --User-agent)
my $whatismyip_user_agent = "Firefox/1.0.5.4";

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

### 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 = 'root';
my $spoof_proto = 'udp';  ### default to udp

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

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

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

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

my $ip_re = '(?:\d{1,3}\.){3}\d{1,3}';

my @args_cp = @ARGV;

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

&usage(0) if $print_help;

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

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

&get_homedir();

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

if ($run_last_args) {
    ### run fwknop with same command line args as the previous
    ### execution
    &run_last_cmdline();
} elsif ($run_last_host) {
    ### run fwknop with the last args for this particular knock destination
    &run_last_host_cmdline();
}

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

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

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

unless (lc($server_mode) eq 'pcap'
        or lc($server_mode) eq 'knock'
        or lc($server_mode) eq 'shared') {
    die "[*] Unknown server mode: $server_mode ",
        qq|(must be "pcap", "knock", or "shared"\n|;
}

&validate_access_str() if $access_str;

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

    unless ($cmdline_pcap_cmd) {
        unless ($enc_allow_ip
                or $enc_source_ip
                or $whatismyip_src) {
            die "[*] Must either specify: --allow-IP <IP>, ",
                "--source-IP, or --whatismyip\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.com
        $enc_allow_ip = &get_ip_whatismyip() if $whatismyip_src;

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

### save our command line args (so -l can be used next time)
&save_args() unless $run_last_args or $run_last_host or $no_save_last_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 $msg_part1   = '';  ### contains everything before the md5 sum
    my $server_auth = '';  ### contains stuff after the md5 sum
    my $user        = '';
    my $random_num  = '';
    my $timestamp   = time();

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

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

    print "\n[+] Building encrypted single-packet authorization (SPA) ",
        "message...\n" unless $quiet;

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

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

    print "[+] Packet fields:\n\n",
        "        Random data: $random_num\n",
        "        Username:    $user\n",
        "        Timestamp:   $timestamp\n",
        "        Version:     $version\n"
        unless $quiet;

    ### append username and timestamp
    $msg_part1 = $random_num . ':' . encode_base64($user) .
        ':' . $timestamp . ':' . $version;

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

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

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

    if (lc($server_auth_method) eq 'crypt') {
        $server_auth = ':' . encode_base64("crypt,$server_auth_crypt_pw");
    }

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

    print "        MD5 sum:     $md5sum\n" unless $quiet;

    if (lc($server_auth_method) eq 'crypt') {
        print "        Server auth: $server_auth_method,";
        for (my $i=0; $i<length($server_auth_crypt_pw); $i++) {
            print '*';
        }
        print "\n";
    }
    print "\n[+] Clear text message: $msg\n" if $debug;

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

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

    $gnupg->options->default_key($gpg_signing_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 = $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 "[*] GPG encrypt failed.\n";
        unless ($gpg_verbose) {
            print "    GPG 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 ($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
                }
            });
            $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;

            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;

            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;
    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 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 = 6;
    my $enc_allow_port = 0;
    if ($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;
            }
        }
    }

    ### 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 get_ip_whatismyip() {
    my $external_ip = '';
    print "    Resolving external IP via: http://$whatismyip_host/\n"
        unless $quiet;
    my $w_ip_tmp = inet_aton($whatismyip_host)
        or die "[*] Could not resolve $whatismyip_host to an IP.";
    my $w_ip = inet_ntoa($w_ip_tmp)
        or die "[*] Could not resolve $whatismyip_host to an IP.";

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

    if (defined($sock)) {
        print $sock "GET / HTTP/1.0\r\n",
            "User-Agent: $whatismyip_user_agent\r\n",
            "Host:$whatismyip_host\r\n",
            "Accept: */*\r\n",
            "Connection: Keep-Alive\r\n\r\n";
        recv($sock, my $web_data, 1000, 0);
        if ($web_data =~ /WhatIsMyIP\.com\s+-\s+($ip_re)/i) {
            $external_ip = $1;
        }
        close $sock;
    }
    die "[*] Could not extract external IP from http://$whatismyip_host/"
        unless $external_ip;
    print "    Got external address: $external_ip\n\n" unless $quiet;
    return $external_ip;
}

sub get_key() {
    if ($get_key_file) {
        ### get the encryption key from file
        open F, "< $get_key_file" or die "[*] Could not open ",
            "$get_key_file: $!";
        my @lines = <F>;
        close F;
        for my $line (@lines) {
            chomp $line;
            if ($line =~ /$knock_dst:\s*(.*)/) {
                $enc_key = $1;
            }
        }
die "[*] Could not read encryption key for $knock_dst from $get_key_file\n",
    "    fwknop expects the format \"$knock_dst: <KEY>\n in $get_key_file\n"
            unless $enc_key;
    } else {
        if ($gpg_signing_key) {

            ### load the GnuPG::Interface module
            require GnuPG::Interface;
            print
"[+] Enter the GPG 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;
        }
        ReadMode 'noecho';
        while (1) {
            if ($gpg_signing_key) {
                $quiet == 1 ? print "GPG signing password: "
                    : print "    GPG signing password: ";
            } else {
                $quiet == 1 ? print "Encryption Key: "
                    : print "    Encryption Key: ";
            }
            my $ans = ReadLine 0;
            chomp $ans;
            next unless $ans =~ /\S/;
            if ($gpg_signing_key) {
                if (length($ans) > 5) {
                    $enc_key = $ans;
                    last;
                }
            } else {
                if (length($ans) >= 8 and length($ans) <= $enc_blocksize) {
                    $enc_key = $ans;
                    last;
                } else {
                    print "[-] The key length must be between 8 and ",
                        "$enc_blocksize chars.\n";
                }
            }
        }
        ReadMode 'normal';
        print "\n";

        die "[*] Could not read encryption key from STDIN.  Exiting."
            unless $enc_key;
    }
    unless ($gpg_signing_key) {
        ### pad out to 16 chars
        while (length($enc_key) < $enc_blocksize) {
            $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# 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 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;

    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\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');
    &usage(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,
        'user-rc=s'      => \$user_rc_file,
        'knock-dst=s'    => \$knock_dst,
        'gpg-signing-key=s' => \$gpg_signing_key,
        'gpg-recipient=s'   => \$gpg_recipient,
        'gpg-home-dir=s'    => \$gpg_home_dir,
        'gpg-verbose'    => \$gpg_verbose,
        'quiet'          => \$quiet,
        '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,
        'whatismyip'     => \$whatismyip_src,
        'User-agent=s'   => \$whatismyip_user_agent,
        'get-key=s'      => \$get_key_file,
        'Home-dir=s'     => \$cmdl_homedir,
        'debug'          => \$debug,
        'verbose'        => \$verbose,
        'Version'        => \$print_version,
        'help'           => \$print_help
    ));
    return;
}

sub run_last_cmdline() {
    my $save_file = "$homedir/.fwknop.run";
    if (-e $save_file) {
        open S, "< $save_file" or die "[*] Could not open $save_file: $!";
        my $arg_line = <S>;
        close S;
        chomp $arg_line;
        print "[+] Running with last command line args: $arg_line\n"
            unless $quiet;
        @ARGV = split /\s+/, $arg_line;

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

    } else {
        die "[*] fwknop argument save file $save_file does not exist.";
    }
    return;
}

sub run_last_host_cmdline() {
    my $hosts_file = "$homedir/.fwknop.hosts";
    if (-e $hosts_file) {
        my $arg_line = '';
        open H, "< $hosts_file" or die "[*] Could not open $hosts_file: $!";
        while (<H>) {
            if (/-k\S*\s+$run_last_host/) {
                $arg_line = $_;
                last;
            }
        }
        close H;

        if ($arg_line) {
            chomp $arg_line;
            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();

        } else {
            print "[-] No matching destination host in $hosts_file\n";
        }
    } else {
        die "[*] fwknop argument save file $hosts_file does not exist.";
    }
    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 usage() {
    my $exit_status = shift;
    print <<_HELP_;
fwknop
    version: $version, by Michael Rash (mbr\@cipherdyne.org)

Usage: fwknop [-A <port list>] [-U <user-rc file>] [-k <knock dst>] [-d]
              [--Server-mode <mode>] [--Server-port <port>] [-D] [-s] [-T]
              [--Server-cmd <cmd>] [-a <allow-IP>] [-g] [--Spoof-src <ip>]
              [--Spoof-user <user>] [--gpg-signing-key <key_id>]
              [--gpg-recipient <recip>] [--gpg-home-dir <dir>]
              [--gpg-verbose] [-l] [-v] [-V] [--Spoof-file <file>]
              [--Spoof-cmd <cmd>] [-q] [-w] [-h] [--offest <port offset>]
              [--Home-dir <home directory>] [--no-save-args]

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".
    --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).
    -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.
    -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 GPG encrypted
                                 message (e.g. "0xABCD1234").
    --gpg-recipient <recip>    - Recipient of GPG encrypted message.
    --gpg-home-dir <dir>       - Path to GPG home dir (e.g.
                                 /home/user/.gnupg).
    --gpg-verbose              - Display all output from GPG 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.
    -w, --whatismyip           - Resolve client IP via the
                                 http://www.whatismyip.com website. This is
    -U, --User-agent <string>  - Specify the user agent string to use when
                                 resolving IP via http://www.whatismyip.com
                                 (requires the -w option). This is not
                                 usually needed since fwknop specifies a
                                 user agent string of "Fwknop/$version".
    --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 ~/.fwknoprc 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.
    -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.
    --no-save-args             - Do not save command line args to
                                 ~/.fwknop.run file.
    -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;
}
