#!/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: # # \ # \ # # # Author: Michael Rash (mbr@cipherdyne.org) # # Version: 1.9.12 # # 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 1533 2009-09-08 02:44:02Z mbr $ # use IO::Socket; use IO::Handle; use File::Copy; use MIME::Base64; use Data::Dumper; use POSIX ':sys_wait_h'; use Getopt::Long; use strict; my $config_file = '/etc/fwknop/fwknop.conf'; my $override_config_str = ''; my $user_rc_file = ''; my $version = '1.9.12'; my $revision_svn = '$Revision: 1533 $'; 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 $ipt_obj = ''; my $use_sendmail = 0; my $debug_to_file = ''; my $debug_include_pidname = 0; my $sniff_interface = ''; my $intf_rx_bytes = 0; my $intf_tx_bytes = 0; my $MAX_TIMEOUT_TRIES = 20; my $no_voluntary_exits = 0; my $fwknopd_com_sock = ''; my $imported_iptables_modules = 0; my $voluntary_exit_timestamp = 0; my $ipfw_is_dynamic = 0; my @fw_cache_entries = (); my @conntrack_ports = (); 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; $| = 1; ### make Getopts case sensitive Getopt::Long::Configure('no_ignore_case'); exit 1 unless (GetOptions( 'config=s' => \$config_file, 'interface=s' => \$sniff_interface, 'Override-config=s' => \$override_config_str, 'debug' => \$debug, 'Debug-to-file=s' => \$debug_to_file, 'Debug-include-pidname' => \$debug_include_pidname, '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", " \n"; exit 0; } &usage(0) if $print_help; ### set things up, deal with pid's, and import config &knoptm_init(); ### setup for the main loop # $fwknopd_com_sock = IO::Socket::UNIX->new( Type => SOCK_STREAM, Local => $config{'KNOPTM_IP_TIMEOUT_SOCK'}, Listen => SOMAXCONN, Timeout => .1 ) or die "[*] Could not acquire fwknopd communications domain socket: $!"; ### main loop # my $dynamic_fw_loop_ctr = 0; my $intf_checks_loop_ctr = 0; my $intf_error = 0; for (;;) { my $fwknop_connection = $fwknopd_com_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 = (); ### when using ipfw with dynamic rules, remove the disabled ### rules that have no remaining dynamic rules associated ### to them on a set interval if ($ipfw_is_dynamic) { if ($dynamic_fw_loop_ctr == $config{'IPFW_DYNAMIC_INTERVAL'}) { &remove_ipfw_rules_without_connections(); $dynamic_fw_loop_ctr = 0; } $dynamic_fw_loop_ctr++; } if ($config{'ENABLE_INTF_CHECKS'} eq 'Y' and $sniff_interface and $sniff_interface ne 'any') { if ($intf_checks_loop_ctr == $config{'INTF_CHECKS_INTERVAL'}) { ### see if the interface is in an error condition (i.e. does ### not exist - and optionally whether it has been ### administratively downed) if (&intf_error_condition()) { $intf_error = 1; ### set interface error condition } elsif ($intf_error) { &logr('[+]', "fwknopd sniffed interface $sniff_interface " . "error condition has been cleared, shutting down " . "fwknopd and knopwatchd will restart", $SEND_MAIL); ### the error condition has been cleared, so stop the fwknopd ### daemon so that knopwatchd can restart it &stop_daemon($config{'FWKNOP_PID_FILE'}); $intf_error = 0; } $intf_checks_loop_ctr = 0; } $intf_checks_loop_ctr++; } sleep 1; } close $fwknopd_com_sock; exit 0; #============================ end main ============================== sub build_timeout_cache() { ### line format (iptables): ### rule_timeout timeout src sport dst dport \ ### proto table chain target direction nat_ip \ ### nat_port external_cmd_close external_cmd_alarm ### 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 NA 0 ### line format (ipfw): ### rule_timeout timeout src sport dst dport \ ### proto NA NA NA NA 0.0.0.0/0 0 external_cmd_close 0 for my $line (@fw_cache_entries) { if ($debug or $debug_to_file) { &logr("[+]", "Received line: $line", $NO_MAIL); } my @ar = split /\s+/, $line; unless ($#ar == 14) { if ($debug or $debug_to_file) { &logr("[-]", "Invalid number of fields (got $#ar instead " . "14), skipping", $NO_MAIL); } next; } 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]); next unless $ar[13] =~ /\w+/; next unless &is_digit($ar[14]); ### the number represents the number of times we attempt to ### delete the rule $timeout_cache{$line} = 0; } return; } sub timeout_cache_entries() { my @del_keys = (); CACHE_ENTRY: 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]; my $external_cmd_close = decode_base64($ar[13]); my $external_cmd_alarm = $ar[14]; next CACHE_ENTRY unless ((time() - $rule_timestamp) > $timeout); if ($config{'ENABLE_CONNTRACK_PERSIST'} eq 'Y' and $src ne '127.0.0.1' and &is_connected($src)) { ### ignore this IP for now because there is still an associated ### connection in the established state next CACHE_ENTRY; } if ($debug or $debug_to_file) { &logr("[+]", "Expiring rule: $line", $NO_MAIL); } ### 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, $external_cmd_close, $external_cmd_alarm)) { ### 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 if ($external_cmd_close ne 'NA') { &logr('[-]', "exceeded max close tries for " . "$src running command: $external_cmd_close, " . "deleting from cache", $NO_MAIL); } else { 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, $external_cmd_close, $external_cmd_alarm) = @_; if ($external_cmd_close ne 'NA') { ### we are executing an external command (derived ultimately ### from EXTERNAL_CMD_CLOSE from access.conf (or fwknop.conf ### if the global override is set). return &exec_external_cmd_close($external_cmd_close, $external_cmd_alarm); } else { 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 &disable_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 %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 ($find_rv, $num_chain_rules) = $ipt_obj->find_ip_rule($src, $dst, $table, $chain, $target, \%extended_info); if ($find_rv) { my $del_rv = 0; my $out_ar = []; my $err_ar = []; for (my $try=0; $try < $config{'IPT_EXEC_TRIES'}; $try++) { ($del_rv, $out_ar, $err_ar) = $ipt_obj->delete_ip_rule($src, $dst, $table, $chain, $target, \%extended_info); last if $del_rv; } 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 ($del_rv) { &logr('[+]', "removed iptables $chain $target rule " . "for $str, $timeout sec timeout exceeded", $SEND_MAIL); $removed_rule = 1; } else { &logr('[-]', "could not delete $target rule for $str", $NO_MAIL); &psyslog_errs($err_ar); } } return $removed_rule; } sub remove_ipfw_rules_without_connections() { my %rules_to_remove = (); my $cmd = "$cmds{'ipfw'} -dS set $config{'IPFW_SET_NUM'} list"; open LIST, "$cmd |" or die "[*] Could not execute $cmd: $!"; my @rules = ; for (@rules) { last if (/^\s*##\s+Dynamic\s+rules/); if (/^\s*#\s+DISABLED\s+(\d+)/) { $rules_to_remove{$1} = 1; } } die "[*] Dynamic part of rule listing missing" if (!$_); for (@rules) { if(/^\s*(\d+)\s+\d+\s+\d+\s+\(\S+\)\s+STATE/) { $rules_to_remove{$1} = 0; } } while ((my $rule, my $needs_remove) = each %rules_to_remove) { &ipfw_delete_ip_rule($rule) if ($needs_remove); } close LIST; } sub disable_ipfw_rule() { my ($timeout, $src, $dst, $proto, $port) = @_; my $disabled_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, $setnum) = &ipfw_find_ip_rule($src, $dst, $proto, $port); if ($ipfw_is_dynamic and $rulenum and $setnum == 0) { if (&ipfw_move_rule($rulenum, $config{'IPFW_SET_NUM'})) { &logr('[+]', "disabled ipfw allow " . "rule for $src -> " . "$proto/$port, $timeout " . "second timeout exceeded", $SEND_MAIL); $disabled_rule = 1; } else { &logr('[-]', "could not disable ipfw allow rule for $src " . "-> $proto/$port", $NO_MAIL); } } elsif ($rulenum) { if (&ipfw_delete_ip_rule($rulenum)) { &logr('[+]', "removed ipfw allow " . "rule for $src -> " . "$proto/$port, $timeout " . "second timeout exceeded", $SEND_MAIL); $disabled_rule = 1; } else { &logr('[-]', "could not remove ipfw allow rule for $src " . "-> $proto/$port", $NO_MAIL); } } return $disabled_rule; } sub ipfw_check_dynamic_rule() { open LIST, "$cmds{'ipfw'} list |" or die "[*] Could not execute 'ipfw list'"; while () { if (/check-state/) { ### from the ipfw man page: # check-state # Checks the packet against the dynamic ruleset. If a match is # found, execute the action associated with the rule which gener- # ated this dynamic rule, otherwise move to the next rule. # Check-state rules do not have a body. If no check-state rule is # found, the dynamic ruleset is checked at the first keep-state or # limit rule. $ipfw_is_dynamic = 1; last; } elsif (/keep-state/) { ### from the ipfw man page: # keep-state # Upon a match, the firewall will create a dynamic rule, whose # default behaviour is to match bidirectional traffic between # source and destination IP/port using the same protocol. The rule # has a limited lifetime (controlled by a set of sysctl(8) vari- # ables), and the lifetime is refreshed every time a matching # packet is found. $ipfw_is_dynamic = 1; last; } elsif (/allow.*to\s+any\s+established/) { last; } } close LIST; return; } sub ipfw_find_ip_rule() { my ($src, $dst, $proto, $port) = @_; my $rulenum = 0; my $set = -1; open LIST, "$cmds{'ipfw'} -S list |" or die "[*] Could not execute 'ipfw list'"; while () { if ($proto eq 'tcp') { ### 00002 set 0 allow tcp from 1.1.1.1 to any dst-port 22 keep-state if (/^\s*(\#\s+DISABLED\s+)?(\d+)\s+set\s+(\d+)\s+ allow\s+$proto\s+from\s+$src\s+to\s+ $dst\s+dst-port\s+$port\s+keep-state/x) { $rulenum = $2; $set = $3; last; } } elsif ($proto eq 'udp') { if (/^\s*(\#\s+DISABLED\s+)?(\d+)\s+set\s+(\d+)\s+ allow\s+$proto\s+from\s+$src\s+to\s+ $dst\s+dst-port\s+$port\s+keep-state/x) { $rulenum = $2; $set = $3; last; } } else { ### icmp if (/^\s*(\#\s+DISABLED\s+)?(\d+)\s+set\s+(\d+)\s+ allow\s+$proto\s+from\s+$src\s+to\s+$dst/x) { $rulenum = $2; $set = $3; last; } } } close LIST; if ($rulenum) { ### remove any leading zeros from the rule number $rulenum =~ s/^0{1,4}//g; } return $rulenum, $set; } 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 ipfw_move_rule() { my ($rulenum, $setnum) = @_; my $cmd = "$cmds{'ipfw'} set move rule $rulenum to $setnum"; open IPFW, "| $cmd" or die "[*] Could not execute $cmd: $!"; close IPFW; return 1; } sub is_connected() { my $src = shift; my $is_connected = 0; if ($config{'FIREWALL_TYPE'} eq 'iptables') { ### see if the IP is involved in a currently established connection open CONNTRACK, "< $config{'IPT_CONNTRACK_FILE'}" or return $is_connected; CONNTRACK: while () { ### tcp 6 431997 ESTABLISHED src=127.0.0.1 dst=127.0.0.1 sport=46202 ### dport=80 packets=2 bytes=112 src=127.0.0.1 dst=127.0.0.1 sport=80 ### dport=46202 packets=1 bytes=60 [ASSURED] mark=0 secmark=0 use=1 for my $port (@conntrack_ports) { if (/\sESTABLISHED\s+src=$src\s+dst= \S+\s+sport=\d+\s+dport=$port\s/x) { $is_connected = 1; last CONNTRACK; } } } close CONNTRACK; } elsif ($config{'FIREWALL_TYPE'} eq 'ipfw') { ### need to work out similar strategy on FreeBSD } return $is_connected; } sub exec_external_cmd_close() { my ($cmd, $cmd_alarm) = @_; my $pid; if ($pid = fork()) { local $SIG{'ALRM'} = sub {die "[*] External cmd timeout.\n"}; alarm $cmd_alarm; eval { waitpid($pid, 0); }; alarm 0; if ($@) { kill 9, $pid unless kill 15, $pid; } } else { die "[*] Could not fork for external cmd: $!" unless defined $pid; if ($cmd =~ /\s*>\s*/) { exec qq{$cmd}; } else { exec qq{$cmd > /dev/null 2>&1}; } } return 1; } sub import_override_configs() { my @override_configs = split /,/, $override_config_str; for my $file (@override_configs) { die "[*] Override config file $file does not exist" unless -e $file; &import_config($file); } return; } sub import_config() { my $config_file = shift; open C, "< $config_file" or die "[*] Could not open ", "config file $config_file: $!"; my @lines = ; 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|/.+| and $varname =~ /^(\w+)Cmd$/) { ### found a command $cmds{$1} = $val unless defined $cmds{$1}; } else { $config{$varname} = $val unless defined $config{$varname}; } } } return; } sub expand_vars() { my $exclude_hr = shift; 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) { next if defined $exclude_hr->{$var}; 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 ($include_hr, $exclude_hr) = @_; my @path = qw( /bin /sbin /usr/bin /usr/sbin /usr/local/bin /usr/local/sbin ); for my $cmd (keys %cmds) { if (keys %$include_hr) { next unless defined $include_hr->{$cmd}; } if (keys %$exclude_hr) { next if defined $exclude_hr->{$cmd}; } if ($cmd eq 'iptables') { next unless $config{'FIREWALL_TYPE'} eq 'iptables'; } elsif ($cmd eq 'ipfw') { next unless $config{'FIREWALL_TYPE'} eq 'ipfw'; } if ($cmd eq 'mail' or $cmd eq 'sendmail') { next if $config{'ALERTING_METHODS'} =~ /noe?mail/i; } 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 $cmd eq 'sendmail'; } } if (-x $cmds{$cmd}) { if ($cmd eq 'sendmail') { $use_sendmail = 1; } } else { die "[*] Command $cmd is located at $cmds{$cmd}, but ", "is not executable by uid: $<" unless $cmd eq 'sendmail'; } } return; } sub sendmail() { my $subject = shift; $subject =~ s/\"//g; if ($use_sendmail) { open SMAIL, "| $cmds{'sendmail'} -t" or die "[*] Could not execute $cmds{'sendmail'}: $!"; print SMAIL "From: $config{'EMAIL_ADDRESSES'}\n", "To: $config{'EMAIL_ADDRESSES'}\n", "Subject: $subject\n\n"; close SMAIL; } else { open MAIL, qq{| $cmds{'mail'} -s "$subject" $config{'EMAIL_ADDRESSES'} } . "> /dev/null" or die "[*] Could not send mail: $cmds{'mail'} -s " . "$subject\" $config{'EMAIL_ADDRESSES'}: $!"; close MAIL; } return; } sub uniquepid() { if (-e $config{'KNOPTM_PID_FILE'}) { my $caller = $0; open PIDFILE, "< $config{'KNOPTM_PID_FILE'}"; my $pid = ; 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 any override config files first &import_override_configs() if $override_config_str; ### import config &import_config($config_file); &expand_vars({'EXTERNAL_CMD_OPEN' => '', 'EXTERNAL_CMD_CLOSE' => ''}); ### 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({}, {'gpg' => '', 'gpg2' => '', 'mail' => ''}); &check_commands({'mail', ''}, {}) unless $use_sendmail; 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(); } &handle_locale(); &get_ipt_object() if $config{'FIREWALL_TYPE'} eq 'iptables'; &ipfw_check_dynamic_rule() if $config{'FIREWALL_TYPE'} eq 'ipfw'; if ($debug_to_file) { unlink $debug_to_file if -e $debug_to_file; } elsif ($debug_include_pidname) { $debug = 1; } if ($debug or $debug_to_file) { &logr("[+]", "knoptm pid: $$ Opening $config{'KNOPTM_IP_TIMEOUT_SOCK'} " . "socket, and entering main loop.", $NO_MAIL); } 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; $msg = "knoptm: $msg" if $debug_include_pidname; if ($debug) { print STDERR localtime() . " $prefix $msg\n"; return; } elsif ($debug_to_file) { open DBG, ">> $debug_to_file" or die $!; print DBG localtime() . " $prefix $msg\n"; close DBG; 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 intf_error_condition() { my $found_intf = 0; my $intf_running = 0; my $parsed_rx_bytes = 0; my $parsed_tx_bytes = 0; ### Linux: ### ath0 Link encap:Ethernet HWaddr 00:01:f4:88:b2:bf ### inet addr:192.168.20.169 Bcast:192.168.20.255 Mask:255.255.255.0 ### inet6 addr: fe80::201:f4ff:fe88:b2bf/64 Scope:Link ### UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1 ### RX packets:595268 errors:0 dropped:0 overruns:0 frame:0 ### TX packets:734031 errors:0 dropped:0 overruns:0 carrier:0 ### collisions:0 txqueuelen:0 ### RX bytes:847407338 (808.1 MB) TX bytes:124974362 (119.1 MB) ### FreeBSD: ### le0: flags=8843 metric 0 mtu 1500 ### options=8 ### ether 00:0c:29:f9:d0:ad ### inet 172.16.76.129 netmask 0xffffff00 broadcast 172.16.76.255 ### media: Ethernet autoselect ### status: active my $cmd = "$cmds{'ifconfig'} $sniff_interface 2> /dev/null"; open IFCONFIG, "$cmd |" or die "[*] Could not execute: $cmd: $!"; while () { $found_intf = 1 if /^\s*$sniff_interface:?/; $intf_running = 1 if /RUNNING/; $parsed_rx_bytes = $1 if /^\s+RX\s+packets.?(\d+)/; $parsed_tx_bytes = $1 if /^\s+TX\s+packets.?(\d+)/; } close IFCONFIG; ### interface existence check if ($config{'ENABLE_INTF_EXISTS_CHECK'} eq 'Y') { unless ($found_intf) { &logr('[-]', "fwknopd sniffed interface: " . "$sniff_interface does not exist", $NO_MAIL); return 1; } } ### The remaining checks are meaningless if the interface does not ### exist. If the interface does not exist and we are running the ### the "exists" check, then we would have already returned an error. ### If not, then the other checks will apply if the interface exists. return 0 unless $found_intf; ### interface "RUNNING" check if ($config{'ENABLE_INTF_RUNNING_CHECK'} eq 'Y') { unless ($intf_running) { &logr('[-]', "fwknopd sniffed interface: " . "$sniff_interface is not in the RUNNING state", $NO_MAIL); return 1; } } ### interface RX/TX bytes increasing check if ($config{'ENABLE_INTF_BYTES_CHECK'} eq 'Y') { my $return_err = 0; if ($intf_rx_bytes > 0 and $parsed_rx_bytes < $intf_rx_bytes) { &logr('[-]', "fwknopd sniffed interface: " . "$sniff_interface RX bytes decreased", $NO_MAIL); $return_err = 1; } if ($intf_tx_bytes > 0 and $parsed_tx_bytes < $intf_tx_bytes) { &logr('[-]', "fwknopd sniffed interface: " . "$sniff_interface TX bytes decreased", $NO_MAIL); $return_err = 1; } $intf_rx_bytes = $parsed_rx_bytes; $intf_tx_bytes = $parsed_tx_bytes; return 1 if $return_err; } return 0; ### no error condition } 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 = ; 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 IPT_CMD_ALARM IPT_EXEC_STYLE IPT_EXEC_SLEEP IPT_EXEC_TRIES EXTERNAL_CMD_CLOSE IPFW_SET_NUM IPFW_DYNAMIC_INTERVAL ENABLE_INTF_CHECKS INTF_CHECKS_INTERVAL ENABLE_INTF_RUNNING_CHECK ENABLE_INTF_EXISTS_CHECK ENABLE_INTF_BYTES_CHECK ENABLE_CONNTRACK_PERSIST IPT_CONNTRACK_FILE CONNTRACK_ESTAB_PORTS ) { die "[*] Required 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', 'ipfw', or 'external_cmd'" unless $fw_type eq 'iptables' or $fw_type eq 'ipfw' or $fw_type eq 'external_cmd'; $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' or $config{'AUTH_MODE'} eq 'SOCKET') { die "[*] AUTH_MODE must be either KNOCK, ULOG_PCAP, ", "FILE_PCAP, PCAP or SOCKET"; } @conntrack_ports = split /\s*,\s*/, $config{'CONNTRACK_ESTAB_PORTS'}; 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; while(($pid = waitpid(-1,WNOHANG)) > 0) { # could add code to something with the borked pid here } $SIG{'CHLD'} = \&REAPER; return; } sub is_digit() { my $str = shift; return 1 if $str =~ /^\d+$/; return 0; } sub get_ipt_object() { my %ipt_opts = ( 'iptables' => $cmds{'iptables'}, 'iptout' => $config{'KNOPTM_IPT_OUTPUT_FILE'}, 'ipterr' => $config{'KNOPTM_IPT_ERROR_FILE'}, 'ipt_alarm' => $config{'IPT_CMD_ALARM'}, 'ipt_exec_style' => $config{'IPT_EXEC_STYLE'}, 'sigchld_handler' => \&REAPER ); $ipt_opts{'debug'} = 1 if $debug; $ipt_opts{'ipt_exec_sleep'} = $config{'IPT_EXEC_SLEEP'} if $config{'IPT_EXEC_SLEEP'} > 0; $ipt_obj = new IPTables::ChainMgr(%ipt_opts) or die '[*] Could not acquire IPTables::ChainMgr object.'; return; } 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 or $debug_to_file) { &logr('[+]', "import_perl_modules INC array:", $NO_MAIL); for (@INC) { &logr('[+]', $_, $NO_MAIL); } } unless ($config{'ALERTING_METHODS'} =~ /no.?syslog/i) { require Unix::Syslog; Unix::Syslog->import(qw(:subs :macros)); } 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 - 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 - Specify path to the lib directory for perl module dependencies (not usually necessary). -l, --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; }