3 #############################################################################
7 # Purpose: This daemon will remove firewall rules created by fwknopd (after
8 # receiving a valid SPA packet). The fwknopd daemon communicates
9 # with knoptm via the /var/run/fwknop/knoptm_ip_timeout.sock UNIX
10 # domain socket whenever new rules are added, and knoptm removes
11 # them after the associated timer expires.
13 # The format of the rules communicated to knoptm by fwknopd are as
16 # <rule timestamp> <timeout> <src> <sport> <dst> <dport> <proto> \
17 # <table> <chain> <target> <direction> <nat_ip> <nat_port> <ext cmd> \
20 # Author: Michael Rash (mbr@cipherdyne.org)
24 # Copyright (C) 2004-2008 Michael Rash (mbr@cipherdyne.org)
26 # License (GNU Public License):
28 # This program is distributed in the hope that it will be useful,
29 # but WITHOUT ANY WARRANTY; without even the implied warranty of
30 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
31 # GNU General Public License for more details.
33 # You should have received a copy of the GNU General Public License
34 # along with this program; if not, write to the Free Software
35 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
38 #############################################################################
40 # $Id: knoptm 1533 2009-09-08 02:44:02Z mbr $
48 use POSIX ':sys_wait_h';
52 my $config_file = '/etc/fwknop/fwknop.conf';
53 my $override_config_str = '';
54 my $user_rc_file = '';
56 my $version = '1.9.12';
57 my $revision_svn = '$Revision: 1533 $';
59 ($rev_num) = $revision_svn =~ m|\$Rev.*:\s+(\S+)|;
71 my $debug_to_file = '';
72 my $debug_include_pidname = 0;
73 my $sniff_interface = '';
74 my $intf_rx_bytes = 0;
75 my $intf_tx_bytes = 0;
76 my $MAX_TIMEOUT_TRIES = 20;
77 my $no_voluntary_exits = 0;
78 my $fwknopd_com_sock = '';
79 my $imported_iptables_modules = 0;
80 my $voluntary_exit_timestamp = 0;
81 my $ipfw_is_dynamic = 0;
83 my @fw_cache_entries = ();
84 my @conntrack_ports = ();
87 my %timeout_cache = ();
89 my $ip_re = qr|(?:[0-2]?\d{1,2}\.){3}[0-2]?\d{1,2}|;
90 my $zero_ip_re = qr|(?:0\.){3}0|;
92 my $cmdline_locale = '';
100 ### make Getopts case sensitive
101 Getopt::Long::Configure('no_ignore_case');
102 exit 1 unless (GetOptions(
103 'config=s' => \$config_file,
104 'interface=s' => \$sniff_interface,
105 'Override-config=s' => \$override_config_str,
107 'Debug-to-file=s' => \$debug_to_file,
108 'Debug-include-pidname' => \$debug_include_pidname,
109 'Version' => \$print_ver,
110 'fw-type=s' => \$fw_type,
111 'no-voluntary-exits' => \$no_voluntary_exits,
112 'no-logs' => \$no_logs,
113 'Lib-dir=s' => \$lib_dir,
114 'LC_ALL=s' => \$cmdline_locale,
115 'locale=s' => \$cmdline_locale,
116 'no-LC_ALL' => \$no_locale,
117 'no-locale' => \$no_locale,
118 'help' => \$print_help
121 ### Print the version number and exit if -V given on the command line.
124 "[+] knoptm v$version (part of the fwknop project), by Michael Rash\n",
125 " <mbr\@cipherdyne.org>\n";
129 &usage(0) if $print_help;
131 ### set things up, deal with pid's, and import config
134 ### setup for the main loop
136 $fwknopd_com_sock = IO::Socket::UNIX->new(
138 Local => $config{'KNOPTM_IP_TIMEOUT_SOCK'},
141 ) or die "[*] Could not acquire fwknopd communications domain socket: $!";
145 my $dynamic_fw_loop_ctr = 0;
146 my $intf_checks_loop_ctr = 0;
150 my $fwknop_connection = $fwknopd_com_sock->accept();
152 if ($fwknop_connection) {
153 @fw_cache_entries = <$fwknop_connection>;
155 ### add new entries to the cache
156 &build_timeout_cache() if @fw_cache_entries;
159 ### always check to see if any fw rules need to be removed
160 &timeout_cache_entries();
162 &append_die_msg() if $die_msg;
163 &append_warn_msg() if $warn_msg;
165 ### see if knoptm should voluntarily exit so that it can be
166 ### restarted by knopwatchd
167 &check_voluntary_exits();
169 @fw_cache_entries = ();
171 ### when using ipfw with dynamic rules, remove the disabled
172 ### rules that have no remaining dynamic rules associated
173 ### to them on a set interval
174 if ($ipfw_is_dynamic) {
175 if ($dynamic_fw_loop_ctr == $config{'IPFW_DYNAMIC_INTERVAL'}) {
176 &remove_ipfw_rules_without_connections();
177 $dynamic_fw_loop_ctr = 0;
179 $dynamic_fw_loop_ctr++;
182 if ($config{'ENABLE_INTF_CHECKS'} eq 'Y' and $sniff_interface
183 and $sniff_interface ne 'any') {
185 if ($intf_checks_loop_ctr == $config{'INTF_CHECKS_INTERVAL'}) {
187 ### see if the interface is in an error condition (i.e. does
188 ### not exist - and optionally whether it has been
189 ### administratively downed)
190 if (&intf_error_condition()) {
192 $intf_error = 1; ### set interface error condition
194 } elsif ($intf_error) {
196 &logr('[+]', "fwknopd sniffed interface $sniff_interface " .
197 "error condition has been cleared, shutting down " .
198 "fwknopd and knopwatchd will restart", $SEND_MAIL);
200 ### the error condition has been cleared, so stop the fwknopd
201 ### daemon so that knopwatchd can restart it
202 &stop_daemon($config{'FWKNOP_PID_FILE'});
207 $intf_checks_loop_ctr = 0;
209 $intf_checks_loop_ctr++;
214 close $fwknopd_com_sock;
216 #============================ end main ==============================
218 sub build_timeout_cache() {
220 ### line format (iptables):
221 ### rule_timeout timeout src sport dst dport \
222 ### proto table chain target direction nat_ip \
223 ### nat_port external_cmd_close external_cmd_alarm
225 ### 1201982858 5 127.0.0.2 0 0.0.0.0/0 22 tcp filter FWKNOP_INPUT \
226 ### ACCEPT src 0.0.0.0/0 0 NA 0
228 ### line format (ipfw):
229 ### rule_timeout timeout src sport dst dport \
230 ### proto NA NA NA NA 0.0.0.0/0 0 external_cmd_close 0
232 for my $line (@fw_cache_entries) {
234 if ($debug or $debug_to_file) {
235 &logr("[+]", "Received line: $line", $NO_MAIL);
238 my @ar = split /\s+/, $line;
239 unless ($#ar == 14) {
240 if ($debug or $debug_to_file) {
241 &logr("[-]", "Invalid number of fields (got $#ar instead " .
242 "14), skipping", $NO_MAIL);
246 next unless &is_digit($ar[0]);
247 next unless &is_digit($ar[1]);
248 next unless $ar[2] =~ /$ip_re/;
249 next unless &is_digit($ar[3]);
250 next unless $ar[4] =~ /$ip_re/;
251 next unless &is_digit($ar[5]);
252 next unless $ar[6] =~ /\w+/;
253 next unless $ar[7] =~ /\w+/;
254 next unless $ar[8] =~ /\w+/;
255 next unless $ar[9] =~ /\w+/;
256 next unless $ar[10] =~ /\w+/;
257 next unless $ar[11] =~ /$ip_re/;
258 next unless &is_digit($ar[12]);
259 next unless $ar[13] =~ /\w+/;
260 next unless &is_digit($ar[14]);
262 ### the number represents the number of times we attempt to
264 $timeout_cache{$line} = 0;
269 sub timeout_cache_entries() {
273 CACHE_ENTRY: for my $line (keys %timeout_cache) {
275 my @ar = split /\s+/, $line;
277 my $rule_timestamp = $ar[0];
278 my $timeout = $ar[1];
287 my $direction = $ar[10];
288 my $nat_ip = $ar[11];
289 my $nat_port = $ar[12];
290 my $external_cmd_close = decode_base64($ar[13]);
291 my $external_cmd_alarm = $ar[14];
293 next CACHE_ENTRY unless ((time() - $rule_timestamp) > $timeout);
295 if ($config{'ENABLE_CONNTRACK_PERSIST'} eq 'Y'
296 and $src ne '127.0.0.1' and &is_connected($src)) {
297 ### ignore this IP for now because there is still an associated
298 ### connection in the established state
302 if ($debug or $debug_to_file) {
303 &logr("[+]", "Expiring rule: $line", $NO_MAIL);
306 ### see if the rule is still active, and remove if necessary
307 if (&rm_fw_rule($rule_timestamp, $timeout, $src, $sport, $dst,
308 $dport, $proto, $table, $chain, $target, $direction,
309 $nat_ip, $nat_port, $external_cmd_close,
310 $external_cmd_alarm)) {
312 ### delete the entry from the in-memory cache now that
313 ### the firewall rule has been removed
314 push @del_keys, $line;
317 $timeout_cache{$line}++;
319 if ($timeout_cache{$line} > $MAX_TIMEOUT_TRIES) {
321 ### it seems the rule has been lost (perhaps manually
322 ### deleted) so remove it from the cache since it is
323 ### past the timeout anyway
324 if ($external_cmd_close ne 'NA') {
325 &logr('[-]', "exceeded max close tries for " .
326 "$src running command: $external_cmd_close, " .
327 "deleting from cache", $NO_MAIL);
329 my $str = "$src -> $dst($proto/$dport)";
330 if ($direction eq 'dst') {
331 $str = "$src($proto/$sport) -> $dst";
333 &logr('[-]', "exceeded max removal tries for $str, " .
334 "deleting from cache", $NO_MAIL);
336 push @del_keys, $line;
340 for my $key (@del_keys) {
341 delete $timeout_cache{$key};
348 my ($rule_timestamp, $timeout, $src, $sport, $dst, $dport,
349 $proto, $table, $chain, $target, $direction, $nat_ip,
350 $nat_port, $external_cmd_close, $external_cmd_alarm) = @_;
352 if ($external_cmd_close ne 'NA') {
354 ### we are executing an external command (derived ultimately
355 ### from EXTERNAL_CMD_CLOSE from access.conf (or fwknop.conf
356 ### if the global override is set).
357 return &exec_external_cmd_close($external_cmd_close,
358 $external_cmd_alarm);
362 if ($config{'FIREWALL_TYPE'} eq 'iptables') {
364 return &rm_ipt_rule($timeout, $src, $sport, $dst, $dport,
365 $proto, $table, $chain, $target, $direction,
368 } elsif ($config{'FIREWALL_TYPE'} eq 'ipfw') {
370 return &disable_ipfw_rule($timeout, $src, $dst, $proto, $dport);
378 my ($timeout, $src, $sport, $dst, $dport, $proto,
379 $table, $chain, $target, $direction, $nat_ip, $nat_port) = @_;
381 my $removed_rule = 0;
383 my %extended_info = ('protocol' => $proto);
385 $extended_info{'s_port'} = $sport;
388 $extended_info{'d_port'} = $dport;
390 if ($nat_ip !~ /$zero_ip_re/ and $nat_port > 0) {
391 $extended_info{'to_ip'} = $nat_ip;
392 $extended_info{'to_port'} = $nat_port;
395 my ($find_rv, $num_chain_rules) = $ipt_obj->find_ip_rule($src, $dst,
396 $table, $chain, $target, \%extended_info);
404 for (my $try=0; $try < $config{'IPT_EXEC_TRIES'}; $try++) {
405 ($del_rv, $out_ar, $err_ar) = $ipt_obj->delete_ip_rule($src,
406 $dst, $table, $chain, $target, \%extended_info);
410 my $str = "$src -> $dst($proto/$dport)";
411 if ($direction eq 'dst') {
412 $str = "$src($proto/$sport) -> $dst";
414 if (defined $extended_info{'to_ip'}) {
415 $str = "$src -> $extended_info{'to_ip'}" .
416 "($proto/$extended_info{'to_port'})";
420 &logr('[+]', "removed iptables $chain $target rule " .
421 "for $str, $timeout sec timeout exceeded", $SEND_MAIL);
424 &logr('[-]', "could not delete $target rule for $str", $NO_MAIL);
425 &psyslog_errs($err_ar);
428 return $removed_rule;
431 sub remove_ipfw_rules_without_connections() {
433 my %rules_to_remove = ();
435 my $cmd = "$cmds{'ipfw'} -dS set $config{'IPFW_SET_NUM'} list";
437 open LIST, "$cmd |" or die "[*] Could not execute $cmd: $!";
442 last if (/^\s*##\s+Dynamic\s+rules/);
443 if (/^\s*#\s+DISABLED\s+(\d+)/) {
444 $rules_to_remove{$1} = 1;
448 die "[*] Dynamic part of rule listing missing" if (!$_);
451 if(/^\s*(\d+)\s+\d+\s+\d+\s+\(\S+\)\s+STATE/) {
452 $rules_to_remove{$1} = 0;
456 while ((my $rule, my $needs_remove) = each %rules_to_remove) {
457 &ipfw_delete_ip_rule($rule) if ($needs_remove);
462 sub disable_ipfw_rule() {
463 my ($timeout, $src, $dst, $proto, $port) = @_;
465 my $disabled_rule = 0;
467 $src = 'any' if $src =~ /$zero_ip_re/;
468 $dst = 'any' if $dst =~ /$zero_ip_re/;
470 ### FIXME, need to add specific destination IP (inspired from
471 ### the FORWARD_ACCESS capability for iptables firewalls
472 my ($rulenum, $setnum) = &ipfw_find_ip_rule($src, $dst, $proto, $port);
474 if ($ipfw_is_dynamic and $rulenum and $setnum == 0) {
475 if (&ipfw_move_rule($rulenum, $config{'IPFW_SET_NUM'})) {
477 &logr('[+]', "disabled ipfw allow " .
478 "rule for $src -> " .
479 "$proto/$port, $timeout " .
480 "second timeout exceeded", $SEND_MAIL);
483 &logr('[-]', "could not disable ipfw allow rule for $src " .
484 "-> $proto/$port", $NO_MAIL);
487 if (&ipfw_delete_ip_rule($rulenum)) {
489 &logr('[+]', "removed ipfw allow " .
490 "rule for $src -> " .
491 "$proto/$port, $timeout " .
492 "second timeout exceeded", $SEND_MAIL);
495 &logr('[-]', "could not remove ipfw allow rule for $src " .
496 "-> $proto/$port", $NO_MAIL);
500 return $disabled_rule;
503 sub ipfw_check_dynamic_rule() {
505 open LIST, "$cmds{'ipfw'} list |" or
506 die "[*] Could not execute 'ipfw list'";
509 ### from the ipfw man page:
511 # Checks the packet against the dynamic ruleset. If a match is
512 # found, execute the action associated with the rule which gener-
513 # ated this dynamic rule, otherwise move to the next rule.
514 # Check-state rules do not have a body. If no check-state rule is
515 # found, the dynamic ruleset is checked at the first keep-state or
517 $ipfw_is_dynamic = 1;
519 } elsif (/keep-state/) {
520 ### from the ipfw man page:
522 # Upon a match, the firewall will create a dynamic rule, whose
523 # default behaviour is to match bidirectional traffic between
524 # source and destination IP/port using the same protocol. The rule
525 # has a limited lifetime (controlled by a set of sysctl(8) vari-
526 # ables), and the lifetime is refreshed every time a matching
528 $ipfw_is_dynamic = 1;
530 } elsif (/allow.*to\s+any\s+established/) {
538 sub ipfw_find_ip_rule() {
539 my ($src, $dst, $proto, $port) = @_;
544 open LIST, "$cmds{'ipfw'} -S list |" or
545 die "[*] Could not execute 'ipfw list'";
547 if ($proto eq 'tcp') {
548 ### 00002 set 0 allow tcp from 1.1.1.1 to any dst-port 22 keep-state
549 if (/^\s*(\#\s+DISABLED\s+)?(\d+)\s+set\s+(\d+)\s+
550 allow\s+$proto\s+from\s+$src\s+to\s+
551 $dst\s+dst-port\s+$port\s+keep-state/x) {
556 } elsif ($proto eq 'udp') {
557 if (/^\s*(\#\s+DISABLED\s+)?(\d+)\s+set\s+(\d+)\s+
558 allow\s+$proto\s+from\s+$src\s+to\s+
559 $dst\s+dst-port\s+$port\s+keep-state/x) {
565 if (/^\s*(\#\s+DISABLED\s+)?(\d+)\s+set\s+(\d+)\s+
566 allow\s+$proto\s+from\s+$src\s+to\s+$dst/x) {
576 ### remove any leading zeros from the rule number
577 $rulenum =~ s/^0{1,4}//g;
580 return $rulenum, $set;
583 sub ipfw_delete_ip_rule() {
586 open IPFW, "| $cmds{'ipfw'} delete $rulenum" or die "[*] Could not ",
587 "execute $cmds{'ipfw'} delete $rulenum";
593 sub ipfw_move_rule() {
594 my ($rulenum, $setnum) = @_;
596 my $cmd = "$cmds{'ipfw'} set move rule $rulenum to $setnum";
598 open IPFW, "| $cmd" or die "[*] Could not execute $cmd: $!";
607 my $is_connected = 0;
609 if ($config{'FIREWALL_TYPE'} eq 'iptables') {
611 ### see if the IP is involved in a currently established connection
612 open CONNTRACK, "< $config{'IPT_CONNTRACK_FILE'}"
613 or return $is_connected;
614 CONNTRACK: while (<CONNTRACK>) {
615 ### tcp 6 431997 ESTABLISHED src=127.0.0.1 dst=127.0.0.1 sport=46202
616 ### dport=80 packets=2 bytes=112 src=127.0.0.1 dst=127.0.0.1 sport=80
617 ### dport=46202 packets=1 bytes=60 [ASSURED] mark=0 secmark=0 use=1
618 for my $port (@conntrack_ports) {
619 if (/\sESTABLISHED\s+src=$src\s+dst=
620 \S+\s+sport=\d+\s+dport=$port\s/x) {
628 } elsif ($config{'FIREWALL_TYPE'} eq 'ipfw') {
629 ### need to work out similar strategy on FreeBSD
632 return $is_connected;
635 sub exec_external_cmd_close() {
636 my ($cmd, $cmd_alarm) = @_;
639 local $SIG{'ALRM'} = sub {die "[*] External cmd timeout.\n"};
646 kill 9, $pid unless kill 15, $pid;
649 die "[*] Could not fork for external cmd: $!" unless defined $pid;
650 if ($cmd =~ /\s*>\s*/) {
653 exec qq{$cmd > /dev/null 2>&1};
659 sub import_override_configs() {
660 my @override_configs = split /,/, $override_config_str;
661 for my $file (@override_configs) {
662 die "[*] Override config file $file does not exist"
664 &import_config($file);
669 sub import_config() {
670 my $config_file = shift;
671 open C, "< $config_file" or die "[*] Could not open ",
672 "config file $config_file: $!";
675 for my $line (@lines) {
677 next if ($line =~ /^\s*#/);
678 if ($line =~ /^(\S+)\s+(.*?)\;/) {
681 if ($val =~ m|/.+| and $varname =~ /^(\w+)Cmd$/) {
683 $cmds{$1} = $val unless defined $cmds{$1};
685 $config{$varname} = $val unless defined $config{$varname};
693 my $exclude_hr = shift;
698 while ($has_sub_var) {
701 if ($resolve_ctr >= 20) {
702 die "[*] Exceeded maximum variable resolution counter.";
704 for my $hr (\%config, \%cmds) {
705 for my $var (keys %$hr) {
706 next if defined $exclude_hr->{$var};
707 my $val = $hr->{$var};
708 if ($val =~ m|\$(\w+)|) {
710 die "[*] sub-ver $sub_var not allowed within same ",
711 "variable $var" if $sub_var eq $var;
712 if (defined $config{$sub_var}) {
713 $val =~ s|\$$sub_var|$config{$sub_var}|;
716 die "[*] sub-var \"$sub_var\" not defined in ",
717 "config for var: $var."
727 ### check paths to commands and attempt to correct if any are wrong.
728 sub check_commands() {
729 my ($include_hr, $exclude_hr) = @_;
739 for my $cmd (keys %cmds) {
741 if (keys %$include_hr) {
742 next unless defined $include_hr->{$cmd};
744 if (keys %$exclude_hr) {
745 next if defined $exclude_hr->{$cmd};
748 if ($cmd eq 'iptables') {
749 next unless $config{'FIREWALL_TYPE'} eq 'iptables';
750 } elsif ($cmd eq 'ipfw') {
751 next unless $config{'FIREWALL_TYPE'} eq 'ipfw';
754 if ($cmd eq 'mail' or $cmd eq 'sendmail') {
755 next if $config{'ALERTING_METHODS'} =~ /noe?mail/i;
757 unless (-x $cmds{$cmd}) {
759 PATH: for my $dir (@path) {
760 if (-x "${dir}/${cmd}") {
761 $cmds{$cmd} = "${dir}/${cmd}";
767 die "[*] Could not find $cmd anywhere!!! Please edit the\n",
768 "config section in $config_file to include the path to\n",
769 "$cmd." unless $cmd eq 'sendmail';
772 if (-x $cmds{$cmd}) {
773 if ($cmd eq 'sendmail') {
777 die "[*] Command $cmd is located at $cmds{$cmd}, but ",
778 "is not executable by uid: $<" unless $cmd eq 'sendmail';
789 open SMAIL, "| $cmds{'sendmail'} -t" or
790 die "[*] Could not execute $cmds{'sendmail'}: $!";
791 print SMAIL "From: $config{'EMAIL_ADDRESSES'}\n",
792 "To: $config{'EMAIL_ADDRESSES'}\n",
793 "Subject: $subject\n\n";
796 open MAIL, qq{| $cmds{'mail'} -s "$subject" $config{'EMAIL_ADDRESSES'} } .
797 "> /dev/null" or die "[*] Could not send mail: $cmds{'mail'} -s " .
798 "$subject\" $config{'EMAIL_ADDRESSES'}: $!";
805 if (-e $config{'KNOPTM_PID_FILE'}) {
807 open PIDFILE, "< $config{'KNOPTM_PID_FILE'}";
811 if (kill 0, $pid) { # knoptm is already running
812 die "[*] knoptm (pid: $pid) is already running! Exiting.\n";
819 open P, "> $config{'KNOPTM_PID_FILE'}" or die "[*] Could not open ",
820 "$config{'KNOPTM_PID_FILE'}: $!";
823 chmod 0600, $config{'KNOPTM_PID_FILE'};
829 ### import any override config files first
830 &import_override_configs() if $override_config_str;
833 &import_config($config_file);
835 &expand_vars({'EXTERNAL_CMD_OPEN' => '', 'EXTERNAL_CMD_CLOSE' => ''});
837 ### make sure all the vars we need are actually in the config file.
840 ### import all necessary perl modules
841 &import_perl_modules();
846 &import_ipt_modules() if $config{'FIREWALL_TYPE'} eq 'iptables';
848 ### make sure there is not another knoptm process already running.
851 ### make sure command paths are correct
852 &check_commands({}, {'gpg' => '', 'gpg2' => '', 'mail' => ''});
854 &check_commands({'mail', ''}, {}) unless $use_sendmail;
859 die "[*] $0: Couldn't fork: $!" unless defined $pid;
860 POSIX::setsid() or die "[*] $0: Can't start a new session: $!";
863 ### write our pid out to disk
866 ### Install signal handlers for debugging and for reaping zombie
868 $SIG{'__WARN__'} = \&warn_handler;
869 $SIG{'__DIE__'} = \&die_handler;
870 $SIG{'CHLD'} = \&REAPER;
872 unlink $config{'KNOPTM_IP_TIMEOUT_SOCK'}
873 if -e $config{'KNOPTM_IP_TIMEOUT_SOCK'};
875 if ($config{'ENABLE_VOLUNTARY_EXITS'} eq 'Y') {
876 $voluntary_exit_timestamp = time();
881 &get_ipt_object() if $config{'FIREWALL_TYPE'} eq 'iptables';
882 &ipfw_check_dynamic_rule() if $config{'FIREWALL_TYPE'} eq 'ipfw';
884 if ($debug_to_file) {
885 unlink $debug_to_file if -e $debug_to_file;
886 } elsif ($debug_include_pidname) {
890 if ($debug or $debug_to_file) {
891 &logr("[+]", "knoptm pid: $$ Opening $config{'KNOPTM_IP_TIMEOUT_SOCK'} " .
892 "socket, and entering main loop.", $NO_MAIL);
898 ### write a message to syslog (leaves off $prefix, which assigns a
899 ### "type" to the message, when writing syslog; might add it later
901 my ($prefix, $msg, $send_email) = @_;
905 $msg = "knoptm: $msg" if $debug_include_pidname;
908 print STDERR localtime() . " $prefix $msg\n";
910 } elsif ($debug_to_file) {
911 open DBG, ">> $debug_to_file" or die $!;
912 print DBG localtime() . " $prefix $msg\n";
917 ### see if we need to send an email
918 if ($send_email and $config{'ALERTING_METHODS'} !~ /noe?mail/i) {
919 &sendmail("$prefix $config{'HOSTNAME'} knoptm: $msg");
922 return if $config{'ALERTING_METHODS'} =~ /no.?syslog/i;
924 ### this is an ugly hack to avoid the 'can't use string as subroutine'
925 ### error because of 'use strict'
926 if ($config{'KNOPTM_SYSLOG_FACILITY'} =~ /LOG_LOCAL7/i) {
927 openlog($config{'KNOPTM_SYSLOG_IDENTITY'}, &LOG_DAEMON(), &LOG_LOCAL7());
928 } elsif ($config{'KNOPTM_SYSLOG_FACILITY'} =~ /LOG_LOCAL6/i) {
929 openlog($config{'KNOPTM_SYSLOG_IDENTITY'}, &LOG_DAEMON(), &LOG_LOCAL6());
930 } elsif ($config{'KNOPTM_SYSLOG_FACILITY'} =~ /LOG_LOCAL5/i) {
931 openlog($config{'KNOPTM_SYSLOG_IDENTITY'}, &LOG_DAEMON(), &LOG_LOCAL5());
932 } elsif ($config{'KNOPTM_SYSLOG_FACILITY'} =~ /LOG_LOCAL4/i) {
933 openlog($config{'KNOPTM_SYSLOG_IDENTITY'}, &LOG_DAEMON(), &LOG_LOCAL4());
934 } elsif ($config{'KNOPTM_SYSLOG_FACILITY'} =~ /LOG_LOCAL3/i) {
935 openlog($config{'KNOPTM_SYSLOG_IDENTITY'}, &LOG_DAEMON(), &LOG_LOCAL3());
936 } elsif ($config{'KNOPTM_SYSLOG_FACILITY'} =~ /LOG_LOCAL2/i) {
937 openlog($config{'KNOPTM_SYSLOG_IDENTITY'}, &LOG_DAEMON(), &LOG_LOCAL2());
938 } elsif ($config{'KNOPTM_SYSLOG_FACILITY'} =~ /LOG_LOCAL1/i) {
939 openlog($config{'KNOPTM_SYSLOG_IDENTITY'}, &LOG_DAEMON(), &LOG_LOCAL1());
940 } elsif ($config{'KNOPTM_SYSLOG_FACILITY'} =~ /LOG_LOCAL0/i) {
941 openlog($config{'KNOPTM_SYSLOG_IDENTITY'}, &LOG_DAEMON(), &LOG_LOCAL0());
944 if ($config{'KNOPTM_SYSLOG_PRIORITY'} =~ /LOG_INFO/i) {
945 syslog(&LOG_INFO(), $msg);
946 } elsif ($config{'KNOPTM_SYSLOG_PRIORITY'} =~ /LOG_DEBUG/i) {
947 syslog(&LOG_DEBUG(), $msg);
948 } elsif ($config{'KNOPTM_SYSLOG_PRIORITY'} =~ /LOG_NOTICE/i) {
949 syslog(&LOG_NOTICE(), $msg);
950 } elsif ($config{'KNOPTM_SYSLOG_PRIORITY'} =~ /LOG_WARNING/i) {
951 syslog(&LOG_WARNING(), $msg);
952 } elsif ($config{'KNOPTM_SYSLOG_PRIORITY'} =~ /LOG_ERR/i) {
953 syslog(&LOG_ERR(), $msg);
954 } elsif ($config{'KNOPTM_SYSLOG_PRIORITY'} =~ /LOG_CRIT/i) {
955 syslog(&LOG_CRIT(), $msg);
956 } elsif ($config{'KNOPTM_SYSLOG_PRIORITY'} =~ /LOG_ALERT/i) {
957 syslog(&LOG_ALERT(), $msg);
958 } elsif ($config{'KNOPTM_SYSLOG_PRIORITY'} =~ /LOG_EMERG/i) {
959 syslog(&LOG_EMERG(), $msg);
969 return if $config{'ALERTING_METHODS'} =~ /no.?syslog/i;
971 ### this is an ugly hack to avoid the 'can't use string as subroutine'
972 ### error because of 'use strict'
973 if ($config{'KNOPTM_SYSLOG_FACILITY'} =~ /LOG_LOCAL7/i) {
974 openlog($config{'KNOPTM_SYSLOG_IDENTITY'},&LOG_DAEMON(), &LOG_LOCAL7());
975 } elsif ($config{'KNOPTM_SYSLOG_FACILITY'} =~ /LOG_LOCAL6/i) {
976 openlog($config{'KNOPTM_SYSLOG_IDENTITY'}, &LOG_DAEMON(), &LOG_LOCAL6());
977 } elsif ($config{'KNOPTM_SYSLOG_FACILITY'} =~ /LOG_LOCAL5/i) {
978 openlog($config{'KNOPTM_SYSLOG_IDENTITY'}, &LOG_DAEMON(), &LOG_LOCAL5());
979 } elsif ($config{'KNOPTM_SYSLOG_FACILITY'} =~ /LOG_LOCAL4/i) {
980 openlog($config{'KNOPTM_SYSLOG_IDENTITY'}, &LOG_DAEMON(), &LOG_LOCAL4());
981 } elsif ($config{'KNOPTM_SYSLOG_FACILITY'} =~ /LOG_LOCAL3/i) {
982 openlog($config{'KNOPTM_SYSLOG_IDENTITY'}, &LOG_DAEMON(), &LOG_LOCAL3());
983 } elsif ($config{'KNOPTM_SYSLOG_FACILITY'} =~ /LOG_LOCAL2/i) {
984 openlog($config{'KNOPTM_SYSLOG_IDENTITY'}, &LOG_DAEMON(), &LOG_LOCAL2());
985 } elsif ($config{'KNOPTM_SYSLOG_FACILITY'} =~ /LOG_LOCAL1/i) {
986 openlog($config{'KNOPTM_SYSLOG_IDENTITY'}, &LOG_DAEMON(), &LOG_LOCAL1());
987 } elsif ($config{'KNOPTM_SYSLOG_FACILITY'} =~ /LOG_LOCAL0/i) {
988 openlog($config{'KNOPTM_SYSLOG_IDENTITY'}, &LOG_DAEMON(), &LOG_LOCAL0());
991 if ($config{'KNOPTM_SYSLOG_PRIORITY'} =~ /LOG_INFO/i) {
992 for (my $i=0; $i<5 && $i<=$#$aref; $i++) {
993 syslog(&LOG_INFO(), $aref->[$i]);
995 } elsif ($config{'KNOPTM_SYSLOG_PRIORITY'} =~ /LOG_DEBUG/i) {
996 for (my $i=0; $i<5 && $i<=$#$aref; $i++) {
997 syslog(&LOG_DEBUG(), $aref->[$i]);
999 } elsif ($config{'KNOPTM_SYSLOG_PRIORITY'} =~ /LOG_NOTICE/i) {
1000 for (my $i=0; $i<5 && $i<=$#$aref; $i++) {
1001 syslog(&LOG_NOTICE(), $aref->[$i]);
1003 } elsif ($config{'KNOPTM_SYSLOG_PRIORITY'} =~ /LOG_WARNING/i) {
1004 for (my $i=0; $i<5 && $i<=$#$aref; $i++) {
1005 syslog(&LOG_WARNING(), $aref->[$i]);
1007 } elsif ($config{'KNOPTM_SYSLOG_PRIORITY'} =~ /LOG_ERR/i) {
1008 for (my $i=0; $i<5 && $i<=$#$aref; $i++) {
1009 syslog(&LOG_ERR(), $aref->[$i]);
1011 } elsif ($config{'KNOPTM_SYSLOG_PRIORITY'} =~ /LOG_CRIT/i) {
1012 for (my $i=0; $i<5 && $i<=$#$aref; $i++) {
1013 syslog(&LOG_CRIT(), $aref->[$i]);
1015 } elsif ($config{'KNOPTM_SYSLOG_PRIORITY'} =~ /LOG_ALERT/i) {
1016 for (my $i=0; $i<5 && $i<=$#$aref; $i++) {
1017 syslog(&LOG_ALERT(), $aref->[$i]);
1019 } elsif ($config{'KNOPTM_SYSLOG_PRIORITY'} =~ /LOG_EMERG/i) {
1020 for (my $i=0; $i<5 && $i<=$#$aref; $i++) {
1021 syslog(&LOG_EMERG(), $aref->[$i]);
1029 sub intf_error_condition() {
1032 my $intf_running = 0;
1033 my $parsed_rx_bytes = 0;
1034 my $parsed_tx_bytes = 0;
1037 ### ath0 Link encap:Ethernet HWaddr 00:01:f4:88:b2:bf
1038 ### inet addr:192.168.20.169 Bcast:192.168.20.255 Mask:255.255.255.0
1039 ### inet6 addr: fe80::201:f4ff:fe88:b2bf/64 Scope:Link
1040 ### UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
1041 ### RX packets:595268 errors:0 dropped:0 overruns:0 frame:0
1042 ### TX packets:734031 errors:0 dropped:0 overruns:0 carrier:0
1043 ### collisions:0 txqueuelen:0
1044 ### RX bytes:847407338 (808.1 MB) TX bytes:124974362 (119.1 MB)
1047 ### le0: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> metric 0 mtu 1500
1048 ### options=8<VLAN_MTU>
1049 ### ether 00:0c:29:f9:d0:ad
1050 ### inet 172.16.76.129 netmask 0xffffff00 broadcast 172.16.76.255
1051 ### media: Ethernet autoselect
1054 my $cmd = "$cmds{'ifconfig'} $sniff_interface 2> /dev/null";
1056 open IFCONFIG, "$cmd |" or die "[*] Could not execute: $cmd: $!";
1057 while (<IFCONFIG>) {
1058 $found_intf = 1 if /^\s*$sniff_interface:?/;
1059 $intf_running = 1 if /RUNNING/;
1060 $parsed_rx_bytes = $1 if /^\s+RX\s+packets.?(\d+)/;
1061 $parsed_tx_bytes = $1 if /^\s+TX\s+packets.?(\d+)/;
1065 ### interface existence check
1066 if ($config{'ENABLE_INTF_EXISTS_CHECK'} eq 'Y') {
1067 unless ($found_intf) {
1068 &logr('[-]', "fwknopd sniffed interface: " .
1069 "$sniff_interface does not exist", $NO_MAIL);
1074 ### The remaining checks are meaningless if the interface does not
1075 ### exist. If the interface does not exist and we are running the
1076 ### the "exists" check, then we would have already returned an error.
1077 ### If not, then the other checks will apply if the interface exists.
1078 return 0 unless $found_intf;
1080 ### interface "RUNNING" check
1081 if ($config{'ENABLE_INTF_RUNNING_CHECK'} eq 'Y') {
1082 unless ($intf_running) {
1083 &logr('[-]', "fwknopd sniffed interface: " .
1084 "$sniff_interface is not in the RUNNING state", $NO_MAIL);
1089 ### interface RX/TX bytes increasing check
1090 if ($config{'ENABLE_INTF_BYTES_CHECK'} eq 'Y') {
1092 if ($intf_rx_bytes > 0 and $parsed_rx_bytes < $intf_rx_bytes) {
1093 &logr('[-]', "fwknopd sniffed interface: " .
1094 "$sniff_interface RX bytes decreased", $NO_MAIL);
1097 if ($intf_tx_bytes > 0 and $parsed_tx_bytes < $intf_tx_bytes) {
1098 &logr('[-]', "fwknopd sniffed interface: " .
1099 "$sniff_interface TX bytes decreased", $NO_MAIL);
1103 $intf_rx_bytes = $parsed_rx_bytes;
1104 $intf_tx_bytes = $parsed_tx_bytes;
1106 return 1 if $return_err;
1109 return 0; ### no error condition
1112 sub check_voluntary_exits() {
1114 return unless $config{'ENABLE_VOLUNTARY_EXITS'} eq 'Y';
1115 return if $no_voluntary_exits;
1117 if ((time() - $voluntary_exit_timestamp) > $config{'EXIT_INTERVAL'}*60) {
1119 ### EXIT_INTERVAL is in minutes
1120 &logr('[+]', "voluntary exit timer expired, knopwatchd will restart",
1122 &logr('[+]', "stopping fwknopd daemon, knopwatchd will restart",
1125 &stop_daemon($config{'FWKNOP_PID_FILE'});
1134 my $pidfile = shift;
1135 return unless -e $pidfile;
1136 open PID, "< $pidfile" or die "[*] Could not open $pidfile: $!";
1141 if (kill 15, $pid) {
1152 sub required_vars() {
1153 for my $var qw(KNOPTM_PID_FILE FWKNOP_DIR FWKNOP_ERR_DIR
1154 EMAIL_ADDRESSES AUTH_MODE KNOPTM_IP_TIMEOUT_SOCK
1155 ALERTING_METHODS FIREWALL_TYPE KNOPTM_SYSLOG_IDENTITY
1156 KNOPTM_SYSLOG_FACILITY KNOPTM_SYSLOG_PRIORITY
1157 ENABLE_VOLUNTARY_EXITS EXIT_INTERVAL FWKNOP_PID_FILE
1158 LOCALE FWKNOP_MOD_DIR IPT_CMD_ALARM IPT_EXEC_STYLE
1159 IPT_EXEC_SLEEP IPT_EXEC_TRIES EXTERNAL_CMD_CLOSE
1160 IPFW_SET_NUM IPFW_DYNAMIC_INTERVAL ENABLE_INTF_CHECKS
1161 INTF_CHECKS_INTERVAL ENABLE_INTF_RUNNING_CHECK
1162 ENABLE_INTF_EXISTS_CHECK ENABLE_INTF_BYTES_CHECK
1163 ENABLE_CONNTRACK_PERSIST IPT_CONNTRACK_FILE
1164 CONNTRACK_ESTAB_PORTS
1166 die "[*] Required variable $var is not defined in $config_file"
1167 unless defined $config{$var};
1172 sub validate_config() {
1174 die qq([*] Invalid EMAIL_ADDRESSES value: "$config{'EMAIL_ADDRESSES'}")
1175 unless $config{'EMAIL_ADDRESSES'} =~ /\S+\@\S+/;
1177 ### translate commas into spaces
1178 $config{'EMAIL_ADDRESSES'} =~ s/\s*\,\s/ /g;
1181 die "[*] --fw-type must be 'iptables', 'ipfw', or 'external_cmd'"
1182 unless $fw_type eq 'iptables' or $fw_type eq 'ipfw'
1183 or $fw_type eq 'external_cmd';
1184 $config{'FIREWALL_TYPE'} = $fw_type if $fw_type;
1187 unless ($config{'AUTH_MODE'} eq 'KNOCK'
1188 or $config{'AUTH_MODE'} eq 'ULOG_PCAP'
1189 or $config{'AUTH_MODE'} eq 'FILE_PCAP'
1190 or $config{'AUTH_MODE'} eq 'PCAP'
1191 or $config{'AUTH_MODE'} eq 'SOCKET') {
1192 die "[*] AUTH_MODE must be either KNOCK, ULOG_PCAP, ",
1193 "FILE_PCAP, PCAP or SOCKET";
1196 @conntrack_ports = split /\s*,\s*/, $config{'CONNTRACK_ESTAB_PORTS'};
1200 sub import_ipt_modules() {
1202 unless ($imported_iptables_modules) {
1204 require IPTables::Parse;
1205 require IPTables::ChainMgr;
1207 $imported_iptables_modules = 1;
1218 ### write all warnings to a logfile
1219 sub warn_handler() {
1226 while(($pid = waitpid(-1,WNOHANG)) > 0) {
1227 # could add code to something with the borked pid here
1229 $SIG{'CHLD'} = \&REAPER;
1235 return 1 if $str =~ /^\d+$/;
1239 sub get_ipt_object() {
1242 'iptables' => $cmds{'iptables'},
1243 'iptout' => $config{'KNOPTM_IPT_OUTPUT_FILE'},
1244 'ipterr' => $config{'KNOPTM_IPT_ERROR_FILE'},
1245 'ipt_alarm' => $config{'IPT_CMD_ALARM'},
1246 'ipt_exec_style' => $config{'IPT_EXEC_STYLE'},
1247 'sigchld_handler' => \&REAPER
1249 $ipt_opts{'debug'} = 1 if $debug;
1250 $ipt_opts{'ipt_exec_sleep'} = $config{'IPT_EXEC_SLEEP'}
1251 if $config{'IPT_EXEC_SLEEP'} > 0;
1253 $ipt_obj = new IPTables::ChainMgr(%ipt_opts)
1254 or die '[*] Could not acquire IPTables::ChainMgr object.';
1258 sub append_die_msg() {
1259 open D, ">> $config{'FWKNOP_ERR_DIR'}/knoptm.die" or
1260 die "[*] Could not open $config{'FWKNOP_DIR'}/knoptm.die: $!";
1261 print D scalar localtime(), " knoptm v$version (file " .
1262 "rev: $rev_num) pid: $$ $die_msg";
1268 sub append_warn_msg() {
1269 open D, ">> $config{'FWKNOP_ERR_DIR'}/knoptm.warn" or
1270 die "[*] Could not open $config{'FWKNOP_DIR'}/knoptm.warn: $!";
1271 print D scalar localtime(), " knoptm v$version (file " .
1272 "rev: $rev_num) pid: $$ $warn_msg";
1278 sub handle_locale() {
1279 $config{'LOCALE'} = $cmdline_locale if $cmdline_locale;
1281 if ($config{'LOCALE'} ne 'NONE' and not $no_locale) {
1282 ### set LC_ALL env variable
1283 $ENV{'LC_ALL'} = $config{'LOCALE'};
1288 sub import_perl_modules() {
1290 my $mod_paths_ar = &get_mod_paths();
1292 if ($#$mod_paths_ar > -1) { ### /usr/lib/fwknop/ exists
1293 push @$mod_paths_ar, @INC;
1294 splice @INC, 0, $#$mod_paths_ar+1, @$mod_paths_ar;
1297 if ($debug or $debug_to_file) {
1298 &logr('[+]', "import_perl_modules INC array:", $NO_MAIL);
1300 &logr('[+]', $_, $NO_MAIL);
1304 unless ($config{'ALERTING_METHODS'} =~ /no.?syslog/i) {
1305 require Unix::Syslog;
1306 Unix::Syslog->import(qw(:subs :macros));
1312 sub get_mod_paths() {
1316 $config{'FWKNOP_MOD_DIR'} = $lib_dir if $lib_dir;
1318 unless (-d $config{'FWKNOP_MOD_DIR'}) {
1319 my $dir_tmp = $config{'FWKNOP_MOD_DIR'};
1320 $dir_tmp =~ s|lib/|lib64/|;
1322 $config{'FWKNOP_MOD_DIR'} = $dir_tmp;
1328 opendir D, $config{'FWKNOP_MOD_DIR'}
1329 or die "[*] Could not open $config{'FWKNOP_MOD_DIR'}: $!";
1330 my @dirs = readdir D;
1333 push @paths, $config{'FWKNOP_MOD_DIR'};
1335 for my $dir (@dirs) {
1336 ### get directories like "/usr/lib/fwknop/x86_64-linux"
1337 next unless -d "$config{'FWKNOP_MOD_DIR'}/$dir";
1338 push @paths, "$config{'FWKNOP_MOD_DIR'}/$dir"
1339 if $dir =~ m|linux| or $dir =~ m|thread|
1340 or (-d "$config{'FWKNOP_MOD_DIR'}/$dir/auto");
1346 my $exit_status = shift;
1349 knoptm; Access timeout daemon for fwknop
1351 [+] Version: $version, by Michael Rash (mbr\@cipherdyne.org)
1352 URL: http://www.cipherdyne.org/fwknop/
1354 Usage: knoptm [options]
1357 -c, --config <file> - Specify path to config file instead of using
1358 the default $config_file. This
1359 file is used only when knoptm is run as a
1361 --no-voluntary-exits - Disregard ENABLE_VOLUNTARY_EXITS setting.
1362 --no-logs - Do not generate any log output or emails
1363 (fwknop_test.pl uses this).
1364 --Lib-dir <path> - Specify path to the lib directory for perl
1365 module dependencies (not usually necessary).
1366 -l, --locale <locale> - Specify LC_ALL locale env variable.
1367 --no-locale - Do not set any locale variable.
1368 -V, --Version - Print version information and exit.
1369 -h, --help - Display usage information and exit.