#!/usr/bin/perl -w
#
#############################################################################
#
# File: knoptm
#
# Purpose: This daemon will remove firewall rules created by fwknopd (after
#          receiving a valid SPA packet).  The fwknopd daemon communicates
#          with knoptm via the /var/run/fwknop/knoptm_ip_timeout.sock UNIX
#          domain socket whenever new rules are added, and knoptm removes
#          them after the associated timer expires.
#
#          The format of the rules communicated to knoptm by fwknopd are as
#          follows:
#
#   <rule timestamp> <timeout> <src> <sport> <dst> <dport> <proto> \
#   <table> <chain> <target> <direction> <nat_ip> <nat_port>
#
# Author: Michael Rash (mbr@cipherdyne.org)
#
# Version: 1.9.8
#
# Copyright (C) 2004-2008 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: knoptm 1277 2008-10-01 02:37:05Z mbr $
#

use IO::Socket;
use IO::Handle;
use File::Copy;
use Data::Dumper;
use POSIX;
use Getopt::Long;
use strict;

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

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

my $print_help = 0;
my $print_ver  = 0;
my $debug      = 0;
my $lib_dir    = '';
my $die_msg    = '';
my $warn_msg   = '';
my $fw_type    = '';
my $no_logs    = 0;
my $timeout_sock = '';
my $max_timeout_tries  = 20;
my $no_voluntary_exits = 0;
my $imported_iptables_modules = 0;
my $voluntary_exit_timestamp  = 0;

my @fw_cache_entries = ();
my %config = ();
my %cmds   = ();
my %timeout_cache = ();

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

my $cmdline_locale = '';
my $no_locale = 0;

my $SEND_MAIL = 1;
my $NO_MAIL   = 0;

### make Getopts case sensitive
Getopt::Long::Configure('no_ignore_case');
exit 1 unless (GetOptions(
    'config=s'  => \$config_file,
    'debug'     => \$debug,
    'Version'   => \$print_ver,
    'fw-type=s' => \$fw_type,
    'no-voluntary-exits' => \$no_voluntary_exits,
    'no-logs'   => \$no_logs,
    'Lib-dir=s' => \$lib_dir,
    'LC_ALL=s'  => \$cmdline_locale,
    'locale=s'  => \$cmdline_locale,
    'no-LC_ALL' => \$no_locale,
    'no-locale' => \$no_locale,
    'help'      => \$print_help
));

### Print the version number and exit if -V given on the command line.
if ($print_ver) {
    print
"[+] knoptm v$version (part of the fwknop project), by Michael Rash\n",
"    <mbr\@cipherdyne.org>\n";
    exit 0;
}

&usage(0) if $print_help;

### set things up, deal with pid's, and import config
&knoptm_init();

&handle_locale();

print STDERR "[+] Opening $config{'KNOPTM_IP_TIMEOUT_SOCK'} socket, ",
    "and entering main loop.\n" if $debug;

$timeout_sock = IO::Socket::UNIX->new(
    Type    => SOCK_STREAM,
    Local   => $config{'KNOPTM_IP_TIMEOUT_SOCK'},
    Listen  => SOMAXCONN,
    Timeout => .1
) or die "[*] Could not acquire auto-response domain socket: $!";

### main loop
for (;;) {

    my $fwknop_connection = $timeout_sock->accept();
    if ($fwknop_connection) {
        @fw_cache_entries = <$fwknop_connection>;

        ### add new entries to the cache
        &build_timeout_cache() if @fw_cache_entries;
    }

    ### always check to see if any fw rules need to be removed
    &timeout_cache_entries();

    &append_die_msg()  if $die_msg;
    &append_warn_msg() if $warn_msg;

    ### see if knoptm should voluntarily exit so that it can be
    ### restarted by knopwatchd
    &check_voluntary_exits();

    @fw_cache_entries = ();

    sleep 1;
}
close $timeout_sock;
exit 0;
#============================ end main ==============================

sub build_timeout_cache() {

    ### line format:
    ### rule_timeout timeout src sport dst dport \
    ### proto table chain target direction nat_ip \
    ### nat_port

    ### 1201982858 5 127.0.0.2 0 0.0.0.0/0 22 tcp filter FWKNOP_INPUT \
    ### ACCEPT src 0.0.0.0/0 0
    for my $line (@fw_cache_entries) {

        my @ar = split /\s+/, $line;
        next unless $#ar == 12;
        next unless &is_digit($ar[0]);
        next unless &is_digit($ar[1]);
        next unless $ar[2] =~ /$ip_re/;
        next unless &is_digit($ar[3]);
        next unless $ar[4] =~ /$ip_re/;
        next unless &is_digit($ar[5]);
        next unless $ar[6] =~ /\w+/;
        next unless $ar[7] =~ /\w+/;
        next unless $ar[8] =~ /\w+/;
        next unless $ar[9] =~ /\w+/;
        next unless $ar[10] =~ /\w+/;
        next unless $ar[11] =~ /$ip_re/;
        next unless &is_digit($ar[12]);

        ### the number represents the number of times we attempt to
        ### delete the rule
        $timeout_cache{$line} = 0;

        print STDERR "[+] Received valid line: $line" if $debug;
    }
    return;
}

sub timeout_cache_entries() {

    my @del_keys = ();
    for my $line (keys %timeout_cache) {

        my @ar = split /\s+/, $line;

        my $rule_timestamp = $ar[0];
        my $timeout        = $ar[1];
        my $src            = $ar[2];
        my $sport          = $ar[3];
        my $dst            = $ar[4];
        my $dport          = $ar[5];
        my $proto          = $ar[6];
        my $table          = $ar[7];
        my $chain          = $ar[8];
        my $target         = $ar[9];
        my $direction      = $ar[10];
        my $nat_ip         = $ar[11];
        my $nat_port       = $ar[12];

        if ((time() - $rule_timestamp) > $timeout) {

            print STDERR "[+] Expiring rule: $line" if $debug;
            ### see if the rule is still active, and remove if necessary
            if (&rm_fw_rule($rule_timestamp, $timeout, $src, $sport, $dst,
                    $dport, $proto, $table, $chain, $target, $direction,
                    $nat_ip, $nat_port)) {

                ### delete the entry from the in-memory cache now that
                ### the firewall rule has been removed
                push @del_keys, $line;

            }
            $timeout_cache{$line}++;
            if ($timeout_cache{$line} > $max_timeout_tries) {
                ### it seems the rule has been lost (perhaps manually
                ### deleted) so remove it from the cache since it is
                ### past the timeout anyway
                my $str = "$src -> $dst($proto/$dport)";
                if ($direction eq 'dst') {
                    $str = "$src($proto/$sport) -> $dst";
                }
                &logr('[-]', "exceeded max removal tries for $str, " .
                    "deleting from cache", $NO_MAIL);
                push @del_keys, $line;
            }
        }
    }
    if (@del_keys) {
        for my $key (@del_keys) {
            delete $timeout_cache{$key};
        }
    }
    return;
}

sub rm_fw_rule() {
    my ($rule_timestamp, $timeout, $src, $sport, $dst, $dport,
        $proto, $table, $chain, $target, $direction, $nat_ip,
        $nat_port) = @_;

    if ($config{'FIREWALL_TYPE'} eq 'iptables') {

        return &rm_ipt_rule($timeout, $src, $sport, $dst, $dport,
                    $proto, $table, $chain, $target, $direction,
                    $nat_ip, $nat_port);

    } elsif ($config{'FIREWALL_TYPE'} eq 'ipfw') {

        return &rm_ipfw_rule($timeout, $src, $dst, $proto, $dport);
    }

    return 0;
}

sub rm_ipt_rule() {
    my ($timeout, $src, $sport, $dst, $dport, $proto,
        $table, $chain, $target, $direction, $nat_ip, $nat_port) = @_;

    my $removed_rule = 0;

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

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

    my %extended_info = ('protocol' => $proto);
    if ($sport) {
        $extended_info{'s_port'} = $sport;
    }
    if ($dport) {
        $extended_info{'d_port'} = $dport;
    }
    if ($nat_ip !~ /$zero_ip_re/ and $nat_port > 0) {
        $extended_info{'to_ip'}   = $nat_ip;
        $extended_info{'to_port'} = $nat_port;
    }

    my $out_aref = [];
    my $err_aref = [];

    my ($rv, $num_chain_rules) = $ipt->find_ip_rule($src, $dst,
            $table, $chain, $target, \%extended_info);

    if ($rv) {
        my ($rv, $out_aref, $err_aref) = $ipt->delete_ip_rule($src,
            $dst, $table, $chain, $target, \%extended_info);

        my $str = "$src -> $dst($proto/$dport)";
        if ($direction eq 'dst') {
            $str = "$src($proto/$sport) -> $dst";
        }
        if (defined $extended_info{'to_ip'}) {
            $str = "$src -> $extended_info{'to_ip'}" .
                "($proto/$extended_info{'to_port'})";
        }

        if ($rv) {
            &logr('[+]', "removed iptables $chain $target rule " .
                "for $str, $timeout sec timeout exceeded", $SEND_MAIL);
            $removed_rule = 1;
        } else {
            my $msg = "could not delete $target rule for $str";
            &logr('[-]', $msg, $NO_MAIL);
            &psyslog_errs($err_aref);
        }
    }
    return $removed_rule;
}

sub rm_ipfw_rule() {
    my ($timeout, $src, $dst, $proto, $port) = @_;

    my $removed_rule = 0;

    $src = 'any' if $src =~ /$zero_ip_re/;
    $dst = 'any' if $dst =~ /$zero_ip_re/;

    ### FIXME, need to add specific destination IP (inspired from
    ### the FORWARD_ACCESS capability for iptables firewalls
    my $rulenum = &ipfw_find_ip_rule($src, $dst, $proto, $port);

    if ($rulenum) {
        if (&ipfw_delete_ip_rule($rulenum)) {

            &logr('[+]', "removed ipfw allow " .
                    "rule for $src -> " .
                    "$proto/$port, $timeout " .
                    "second timeout exceeded", $SEND_MAIL);
            $removed_rule = 1;
        } else {
            my $msg = "could not delete ipfw allow rule for $src " .
                "-> $proto/$port";
            &logr('[-]', $msg, $NO_MAIL);
        }
    }

    return $removed_rule;
}

sub ipfw_find_ip_rule() {
    my ($src, $dst, $proto, $port) = @_;

    my $rulenum = 0;

    open LIST, "$cmds{'ipfw'} list |" or
        die "[*] Could not execute 'ipfw list'";
    while (<LIST>) {
        if ($proto eq 'tcp' or $proto eq 'udp') {
            ### 00002 allow tcp from 1.1.1.1 to any dst-port 22 keep-state
            if (/^\s*(\d+)\s+allow\s+$proto\s+from\s+$src\s+to\s+
                        $dst\s+dst-port\s+$port\s+keep-state/x) {
                $rulenum = $1;
                last;
            }
        } else {  ### icmp
            if (/^\s*(\d+)\s+allow\s+$proto\s+from\s+$src\s+to\s+$dst/x) {
                $rulenum = $1;
                last;
            }
        }
    }
    close LIST;

    if ($rulenum) {
        ### remove any leading zeros from the rule number
        $rulenum =~ s/^0{1,4}//g;
    }

    return $rulenum;
}

sub ipfw_delete_ip_rule() {
    my $rulenum = shift;

    open IPFW, "| $cmds{'ipfw'} delete $rulenum" or die "[*] Could not ",
        "execute $cmds{'ipfw'} delete $rulenum";
    close IPFW;

    return 1;
}

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

sub expand_vars() {

    my $has_sub_var = 1;
    my $resolve_ctr = 0;

    while ($has_sub_var) {
        $resolve_ctr++;
        $has_sub_var = 0;
        if ($resolve_ctr >= 20) {
            die "[*] Exceeded maximum variable resolution counter.";
        }
        for my $hr (\%config, \%cmds) {
            for my $var (keys %$hr) {
                my $val = $hr->{$var};
                if ($val =~ m|\$(\w+)|) {
                    my $sub_var = $1;
                    die "[*] sub-ver $sub_var not allowed within same ",
                        "variable $var" if $sub_var eq $var;
                    if (defined $config{$sub_var}) {
                        $val =~ s|\$$sub_var|$config{$sub_var}|;
                        $hr->{$var} = $val;
                    } else {
                        die "[*] sub-var \"$sub_var\" not defined in ",
                            "config for var: $var."
                    }
                    $has_sub_var = 1;
                }
            }
        }
    }
    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) {

        if ($cmd eq 'iptables') {
            next unless $config{'FIREWALL_TYPE'} eq 'iptables';
        } elsif ($cmd eq 'ipfw') {
            next unless $config{'FIREWALL_TYPE'} eq 'ipfw';
        }
        next if $cmd =~ /gpg/;
        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{'KNOPTM_PID_FILE'}) {
        my $caller = $0;
        open PIDFILE, "< $config{'KNOPTM_PID_FILE'}";
        my $pid = <PIDFILE>;
        close PIDFILE;
        chomp $pid;
        if (kill 0, $pid) {  # knoptm is already running
            die "[*] knoptm (pid: $pid) is already running!  Exiting.\n";
        }
    }
    return;
}

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

sub knoptm_init() {

    ### import config
    &import_config();

    &expand_vars();

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

    ### import all necessary perl modules
    &import_perl_modules();

    ### validate config
    &validate_config();

    &import_ipt_modules() if $config{'FIREWALL_TYPE'} eq 'iptables';

    ### make sure there is not another knoptm process already running.
    &uniquepid();

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

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

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

    unlink $config{'KNOPTM_IP_TIMEOUT_SOCK'}
        if -e $config{'KNOPTM_IP_TIMEOUT_SOCK'};

    if ($config{'ENABLE_VOLUNTARY_EXITS'} eq 'Y') {
        $voluntary_exit_timestamp = time();
    }

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

    return if $no_logs;

    if ($debug) {
        print STDERR "$prefix $msg\n";
        return;
    }

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

    return if $config{'ALERTING_METHODS'} =~ /no.?syslog/i;

    ### this is an ugly hack to avoid the 'can't use string as subroutine'
    ### error because of 'use strict'
    if ($config{'KNOPTM_SYSLOG_FACILITY'} =~ /LOG_LOCAL7/i) {
        openlog($config{'KNOPTM_SYSLOG_IDENTITY'}, &LOG_DAEMON(), &LOG_LOCAL7());
    } elsif ($config{'KNOPTM_SYSLOG_FACILITY'} =~ /LOG_LOCAL6/i) {
        openlog($config{'KNOPTM_SYSLOG_IDENTITY'}, &LOG_DAEMON(), &LOG_LOCAL6());
    } elsif ($config{'KNOPTM_SYSLOG_FACILITY'} =~ /LOG_LOCAL5/i) {
        openlog($config{'KNOPTM_SYSLOG_IDENTITY'}, &LOG_DAEMON(), &LOG_LOCAL5());
    } elsif ($config{'KNOPTM_SYSLOG_FACILITY'} =~ /LOG_LOCAL4/i) {
        openlog($config{'KNOPTM_SYSLOG_IDENTITY'}, &LOG_DAEMON(), &LOG_LOCAL4());
    } elsif ($config{'KNOPTM_SYSLOG_FACILITY'} =~ /LOG_LOCAL3/i) {
        openlog($config{'KNOPTM_SYSLOG_IDENTITY'}, &LOG_DAEMON(), &LOG_LOCAL3());
    } elsif ($config{'KNOPTM_SYSLOG_FACILITY'} =~ /LOG_LOCAL2/i) {
        openlog($config{'KNOPTM_SYSLOG_IDENTITY'}, &LOG_DAEMON(), &LOG_LOCAL2());
    } elsif ($config{'KNOPTM_SYSLOG_FACILITY'} =~ /LOG_LOCAL1/i) {
        openlog($config{'KNOPTM_SYSLOG_IDENTITY'}, &LOG_DAEMON(), &LOG_LOCAL1());
    } elsif ($config{'KNOPTM_SYSLOG_FACILITY'} =~ /LOG_LOCAL0/i) {
        openlog($config{'KNOPTM_SYSLOG_IDENTITY'}, &LOG_DAEMON(), &LOG_LOCAL0());
    }

    if ($config{'KNOPTM_SYSLOG_PRIORITY'} =~ /LOG_INFO/i) {
        syslog(&LOG_INFO(), $msg);
    } elsif ($config{'KNOPTM_SYSLOG_PRIORITY'} =~ /LOG_DEBUG/i) {
        syslog(&LOG_DEBUG(), $msg);
    } elsif ($config{'KNOPTM_SYSLOG_PRIORITY'} =~ /LOG_NOTICE/i) {
        syslog(&LOG_NOTICE(), $msg);
    } elsif ($config{'KNOPTM_SYSLOG_PRIORITY'} =~ /LOG_WARNING/i) {
        syslog(&LOG_WARNING(), $msg);
    } elsif ($config{'KNOPTM_SYSLOG_PRIORITY'} =~ /LOG_ERR/i) {
        syslog(&LOG_ERR(), $msg);
    } elsif ($config{'KNOPTM_SYSLOG_PRIORITY'} =~ /LOG_CRIT/i) {
        syslog(&LOG_CRIT(), $msg);
    } elsif ($config{'KNOPTM_SYSLOG_PRIORITY'} =~ /LOG_ALERT/i) {
        syslog(&LOG_ALERT(), $msg);
    } elsif ($config{'KNOPTM_SYSLOG_PRIORITY'} =~ /LOG_EMERG/i) {
        syslog(&LOG_EMERG(), $msg);
    }

    closelog();

    return;
}

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

    ### this is an ugly hack to avoid the 'can't use string as subroutine'
    ### error because of 'use strict'
    if ($config{'KNOPTM_SYSLOG_FACILITY'} =~ /LOG_LOCAL7/i) {
        openlog($config{'KNOPTM_SYSLOG_IDENTITY'},&LOG_DAEMON(), &LOG_LOCAL7());
    } elsif ($config{'KNOPTM_SYSLOG_FACILITY'} =~ /LOG_LOCAL6/i) {
        openlog($config{'KNOPTM_SYSLOG_IDENTITY'}, &LOG_DAEMON(), &LOG_LOCAL6());
    } elsif ($config{'KNOPTM_SYSLOG_FACILITY'} =~ /LOG_LOCAL5/i) {
        openlog($config{'KNOPTM_SYSLOG_IDENTITY'}, &LOG_DAEMON(), &LOG_LOCAL5());
    } elsif ($config{'KNOPTM_SYSLOG_FACILITY'} =~ /LOG_LOCAL4/i) {
        openlog($config{'KNOPTM_SYSLOG_IDENTITY'}, &LOG_DAEMON(), &LOG_LOCAL4());
    } elsif ($config{'KNOPTM_SYSLOG_FACILITY'} =~ /LOG_LOCAL3/i) {
        openlog($config{'KNOPTM_SYSLOG_IDENTITY'}, &LOG_DAEMON(), &LOG_LOCAL3());
    } elsif ($config{'KNOPTM_SYSLOG_FACILITY'} =~ /LOG_LOCAL2/i) {
        openlog($config{'KNOPTM_SYSLOG_IDENTITY'}, &LOG_DAEMON(), &LOG_LOCAL2());
    } elsif ($config{'KNOPTM_SYSLOG_FACILITY'} =~ /LOG_LOCAL1/i) {
        openlog($config{'KNOPTM_SYSLOG_IDENTITY'}, &LOG_DAEMON(), &LOG_LOCAL1());
    } elsif ($config{'KNOPTM_SYSLOG_FACILITY'} =~ /LOG_LOCAL0/i) {
        openlog($config{'KNOPTM_SYSLOG_IDENTITY'}, &LOG_DAEMON(), &LOG_LOCAL0());
    }

    if ($config{'KNOPTM_SYSLOG_PRIORITY'} =~ /LOG_INFO/i) {
        for (my $i=0; $i<5 && $i<=$#$aref; $i++) {
            syslog(&LOG_INFO(), $aref->[$i]);
        }
    } elsif ($config{'KNOPTM_SYSLOG_PRIORITY'} =~ /LOG_DEBUG/i) {
        for (my $i=0; $i<5 && $i<=$#$aref; $i++) {
            syslog(&LOG_DEBUG(), $aref->[$i]);
        }
    } elsif ($config{'KNOPTM_SYSLOG_PRIORITY'} =~ /LOG_NOTICE/i) {
        for (my $i=0; $i<5 && $i<=$#$aref; $i++) {
            syslog(&LOG_NOTICE(), $aref->[$i]);
        }
    } elsif ($config{'KNOPTM_SYSLOG_PRIORITY'} =~ /LOG_WARNING/i) {
        for (my $i=0; $i<5 && $i<=$#$aref; $i++) {
            syslog(&LOG_WARNING(), $aref->[$i]);
        }
    } elsif ($config{'KNOPTM_SYSLOG_PRIORITY'} =~ /LOG_ERR/i) {
        for (my $i=0; $i<5 && $i<=$#$aref; $i++) {
            syslog(&LOG_ERR(), $aref->[$i]);
        }
    } elsif ($config{'KNOPTM_SYSLOG_PRIORITY'} =~ /LOG_CRIT/i) {
        for (my $i=0; $i<5 && $i<=$#$aref; $i++) {
            syslog(&LOG_CRIT(), $aref->[$i]);
        }
    } elsif ($config{'KNOPTM_SYSLOG_PRIORITY'} =~ /LOG_ALERT/i) {
        for (my $i=0; $i<5 && $i<=$#$aref; $i++) {
            syslog(&LOG_ALERT(), $aref->[$i]);
        }
    } elsif ($config{'KNOPTM_SYSLOG_PRIORITY'} =~ /LOG_EMERG/i) {
        for (my $i=0; $i<5 && $i<=$#$aref; $i++) {
            syslog(&LOG_EMERG(), $aref->[$i]);
        }
    }

    closelog();
    return;
}

sub check_voluntary_exits() {

    return unless $config{'ENABLE_VOLUNTARY_EXITS'} eq 'Y';
    return if $no_voluntary_exits;

    if ((time() - $voluntary_exit_timestamp) > $config{'EXIT_INTERVAL'}*60) {

        ### EXIT_INTERVAL is in minutes
        &logr('[+]', "voluntary exit timer expired, knopwatchd will restart",
            $SEND_MAIL);
        &logr('[+]', "stopping fwknopd daemon, knopwatchd will restart",
            $SEND_MAIL);

        &stop_daemon($config{'FWKNOP_PID_FILE'});

        exit 0;
    }

    return;
}

sub stop_daemon() {
    my $pidfile = shift;
    return unless -e $pidfile;
    open PID, "< $pidfile" or die "[*] Could not open $pidfile: $!";
    my $pid = <PID>;
    close PID;
    chomp $pid;
    if (kill 0, $pid) {
        if (kill 15, $pid) {
            unlink $pidfile;
        } else {
            kill 9, $pid;
        }
    } else {
        unlink $pidfile;
    }
    return;
}

sub required_vars() {
    for my $var qw(KNOPTM_PID_FILE FWKNOP_DIR FWKNOP_ERR_DIR
            EMAIL_ADDRESSES AUTH_MODE KNOPTM_IP_TIMEOUT_SOCK
            ALERTING_METHODS FIREWALL_TYPE KNOPTM_SYSLOG_IDENTITY
            KNOPTM_SYSLOG_FACILITY KNOPTM_SYSLOG_PRIORITY
            ENABLE_VOLUNTARY_EXITS EXIT_INTERVAL FWKNOP_PID_FILE
            LOCALE FWKNOP_MOD_DIR
    ) {

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

sub validate_config() {

    die qq([*] Invalid EMAIL_ADDRESSES value: "$config{'EMAIL_ADDRESSES'}")
        unless $config{'EMAIL_ADDRESSES'} =~ /\S+\@\S+/;

    ### translate commas into spaces
    $config{'EMAIL_ADDRESSES'} =~ s/\s*\,\s/ /g;

    if ($fw_type) {
        die "[*] --fw-type must be 'iptables' or 'ipfw'"
            unless $fw_type eq 'iptables' or $fw_type eq 'ipfw';
        $config{'FIREWALL_TYPE'} = $fw_type if $fw_type;
    }

    unless ($config{'AUTH_MODE'} eq 'KNOCK'
            or $config{'AUTH_MODE'} eq 'ULOG_PCAP'
            or $config{'AUTH_MODE'} eq 'FILE_PCAP'
            or $config{'AUTH_MODE'} eq 'PCAP') {
        die "[*] AUTH_MODE must be either KNOCK, ULOG_PCAP, FILE_PCAP or PCAP";
    }
    return;
}

sub import_ipt_modules() {

    unless ($imported_iptables_modules) {

        require IPTables::Parse;
        require IPTables::ChainMgr;

        $imported_iptables_modules = 1;
    }

    return;
}

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

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

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

sub is_digit() {
    my $str = shift;
    return 1 if $str =~ /^\d+$/;
    return 0;
}

sub append_die_msg() {
    open D, ">> $config{'FWKNOP_ERR_DIR'}/knoptm.die" or
        die "[*] Could not open $config{'FWKNOP_DIR'}/knoptm.die: $!";
    print D scalar localtime(), " knoptm v$version (file " .
        "rev: $rev_num) pid: $$ $die_msg";
    close D;
    $die_msg = '';
    return;
}

sub append_warn_msg() {
    open D, ">> $config{'FWKNOP_ERR_DIR'}/knoptm.warn" or
        die "[*] Could not open $config{'FWKNOP_DIR'}/knoptm.warn: $!";
    print D scalar localtime(), " knoptm v$version (file " .
        "rev: $rev_num) pid: $$ $warn_msg";
    close D;
    $warn_msg = '';
    return;
}

sub handle_locale() {
    $config{'LOCALE'} = $cmdline_locale if $cmdline_locale;

    if ($config{'LOCALE'} ne 'NONE' and not $no_locale) {
        ### set LC_ALL env variable
        $ENV{'LC_ALL'} = $config{'LOCALE'};
    }
    return;
}

sub import_perl_modules() {

    my $mod_paths_ar = &get_mod_paths();

    if ($#$mod_paths_ar > -1) {  ### /usr/lib/fwknop/ exists
        push @$mod_paths_ar, @INC;
        splice @INC, 0, $#$mod_paths_ar+1, @$mod_paths_ar;
    }

    if ($debug) {
        print STDERR "[+] import_perl_modules(): The \@INC array:\n";
        print STDERR "$_\n" for @INC;
    }

    require Unix::Syslog unless $config{'ALERTING_METHODS'} =~ /no.?syslog/i;

    Unix::Syslog->import(qw(:subs :macros))
        unless $config{'ALERTING_METHODS'} =~ /no.?syslog/i;

    return;
}

sub get_mod_paths() {

    my @paths = ();

    $config{'FWKNOP_MOD_DIR'} = $lib_dir if $lib_dir;

    unless (-d $config{'FWKNOP_MOD_DIR'}) {
        my $dir_tmp = $config{'FWKNOP_MOD_DIR'};
        $dir_tmp =~ s|lib/|lib64/|;
        if (-d $dir_tmp) {
            $config{'FWKNOP_MOD_DIR'} = $dir_tmp;
        } else {
            return [];
        }
    }

    opendir D, $config{'FWKNOP_MOD_DIR'}
        or die "[*] Could not open $config{'FWKNOP_MOD_DIR'}: $!";
    my @dirs = readdir D;
    closedir D;

    push @paths, $config{'FWKNOP_MOD_DIR'};

    for my $dir (@dirs) {
        ### get directories like "/usr/lib/fwknop/x86_64-linux"
        next unless -d "$config{'FWKNOP_MOD_DIR'}/$dir";
        push @paths, "$config{'FWKNOP_MOD_DIR'}/$dir"
            if $dir =~ m|linux| or $dir =~ m|thread|
                or (-d "$config{'FWKNOP_MOD_DIR'}/$dir/auto");
    }
    return \@paths;
}

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

knoptm; Access timeout daemon for fwknop

[+] Version: $version, by Michael Rash (mbr\@cipherdyne.org)
    URL: http://www.cipherdyne.org/fwknop/

Usage: knoptm [options]

Options:
    -c, --config <file>     - Specify path to config file instead of using
                              the default $config_file.  This
                              file is used only when knoptm is run as a
                              daemon.
    --no-voluntary-exits    - Disregard ENABLE_VOLUNTARY_EXITS setting.
    --no-logs               - Do not generate any log output or emails
                              (fwknop_test.pl uses this).
    --Lib-dir <path>        - Specify path to the lib directory for perl
                              module dependencies (not usually necessary).
    -l, --locale <locale>   - Specify LC_ALL locale env variable.
    --no-locale             - Do not set any locale variable.
    -V, --Version           - Print version information and exit.
    -h, --help              - Display usage information and exit.
_HELP_
    exit $exit_status;
}
