firewalld support from Gerry Reno
[fwknop.git] / perl / legacy / fwknop / knoptm
1 #!/usr/bin/perl -w
2 #
3 #############################################################################
4 #
5 # File: knoptm
6 #
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.
12 #
13 #          The format of the rules communicated to knoptm by fwknopd are as
14 #          follows:
15 #
16 #   <rule timestamp> <timeout> <src> <sport> <dst> <dport> <proto> \
17 #   <table> <chain> <target> <direction> <nat_ip> <nat_port> <ext cmd> \
18 #   <ext cmd alarm>
19 #
20 # Author: Michael Rash (mbr@cipherdyne.org)
21 #
22 # Version: 1.9.12
23 #
24 # Copyright (C) 2004-2008 Michael Rash (mbr@cipherdyne.org)
25 #
26 # License (GNU Public License):
27 #
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.
32 #
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
36 #    USA
37 #
38 #############################################################################
39 #
40 # $Id: knoptm 1533 2009-09-08 02:44:02Z mbr $
41 #
42
43 use IO::Socket;
44 use IO::Handle;
45 use File::Copy;
46 use MIME::Base64;
47 use Data::Dumper;
48 use POSIX ':sys_wait_h';
49 use Getopt::Long;
50 use strict;
51
52 my $config_file  = '/etc/fwknop/fwknop.conf';
53 my $override_config_str = '';
54 my $user_rc_file = '';
55
56 my $version = '1.9.12';
57 my $revision_svn = '$Revision: 1533 $';
58 my $rev_num = '1';
59 ($rev_num) = $revision_svn =~ m|\$Rev.*:\s+(\S+)|;
60
61 my $print_help = 0;
62 my $print_ver  = 0;
63 my $debug      = 0;
64 my $lib_dir    = '';
65 my $die_msg    = '';
66 my $warn_msg   = '';
67 my $fw_type    = '';
68 my $no_logs    = 0;
69 my $ipt_obj    = '';
70 my $use_sendmail  = 0;
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;
82
83 my @fw_cache_entries = ();
84 my @conntrack_ports  = ();
85 my %config = ();
86 my %cmds   = ();
87 my %timeout_cache = ();
88
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|;
91
92 my $cmdline_locale = '';
93 my $no_locale = 0;
94
95 my $SEND_MAIL = 1;
96 my $NO_MAIL   = 0;
97
98 $| = 1;
99
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,
106     'debug'     => \$debug,
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
119 ));
120
121 ### Print the version number and exit if -V given on the command line.
122 if ($print_ver) {
123     print
124 "[+] knoptm v$version (part of the fwknop project), by Michael Rash\n",
125 "    <mbr\@cipherdyne.org>\n";
126     exit 0;
127 }
128
129 &usage(0) if $print_help;
130
131 ### set things up, deal with pid's, and import config
132 &knoptm_init();
133
134 ### setup for the main loop
135 #
136 $fwknopd_com_sock = IO::Socket::UNIX->new(
137     Type    => SOCK_STREAM,
138     Local   => $config{'KNOPTM_IP_TIMEOUT_SOCK'},
139     Listen  => SOMAXCONN,
140     Timeout => .1
141 ) or die "[*] Could not acquire fwknopd communications domain socket: $!";
142
143 ### main loop
144 #
145 my $dynamic_fw_loop_ctr  = 0;
146 my $intf_checks_loop_ctr = 0;
147 my $intf_error = 0;
148 for (;;) {
149
150     my $fwknop_connection = $fwknopd_com_sock->accept();
151
152     if ($fwknop_connection) {
153         @fw_cache_entries = <$fwknop_connection>;
154
155         ### add new entries to the cache
156         &build_timeout_cache() if @fw_cache_entries;
157     }
158
159     ### always check to see if any fw rules need to be removed
160     &timeout_cache_entries();
161
162     &append_die_msg()  if $die_msg;
163     &append_warn_msg() if $warn_msg;
164
165     ### see if knoptm should voluntarily exit so that it can be
166     ### restarted by knopwatchd
167     &check_voluntary_exits();
168
169     @fw_cache_entries = ();
170
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;
178         }
179         $dynamic_fw_loop_ctr++;
180     }
181
182     if ($config{'ENABLE_INTF_CHECKS'} eq 'Y' and $sniff_interface
183             and $sniff_interface ne 'any') {
184
185         if ($intf_checks_loop_ctr == $config{'INTF_CHECKS_INTERVAL'}) {
186
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()) {
191
192                 $intf_error = 1;  ### set interface error condition
193
194             } elsif ($intf_error) {
195
196                 &logr('[+]', "fwknopd sniffed interface $sniff_interface " .
197                     "error condition has been cleared, shutting down " .
198                     "fwknopd and knopwatchd will restart", $SEND_MAIL);
199
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'});
203
204                 $intf_error = 0;
205             }
206
207             $intf_checks_loop_ctr = 0;
208         }
209         $intf_checks_loop_ctr++;
210     }
211
212     sleep 1;
213 }
214 close $fwknopd_com_sock;
215 exit 0;
216 #============================ end main ==============================
217
218 sub build_timeout_cache() {
219
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
224
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
227
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
231
232     for my $line (@fw_cache_entries) {
233
234         if ($debug or $debug_to_file) {
235             &logr("[+]", "Received line: $line", $NO_MAIL);
236         }
237
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);
243             }
244             next;
245         }
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]);
261
262         ### the number represents the number of times we attempt to
263         ### delete the rule
264         $timeout_cache{$line} = 0;
265     }
266     return;
267 }
268
269 sub timeout_cache_entries() {
270
271     my @del_keys = ();
272
273     CACHE_ENTRY: for my $line (keys %timeout_cache) {
274
275         my @ar = split /\s+/, $line;
276
277         my $rule_timestamp = $ar[0];
278         my $timeout        = $ar[1];
279         my $src            = $ar[2];
280         my $sport          = $ar[3];
281         my $dst            = $ar[4];
282         my $dport          = $ar[5];
283         my $proto          = $ar[6];
284         my $table          = $ar[7];
285         my $chain          = $ar[8];
286         my $target         = $ar[9];
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];
292
293         next CACHE_ENTRY unless ((time() - $rule_timestamp) > $timeout);
294
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
299             next CACHE_ENTRY;
300         }
301
302         if ($debug or $debug_to_file) {
303             &logr("[+]", "Expiring rule: $line", $NO_MAIL);
304         }
305
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)) {
311
312             ### delete the entry from the in-memory cache now that
313             ### the firewall rule has been removed
314             push @del_keys, $line;
315         }
316
317         $timeout_cache{$line}++;
318
319         if ($timeout_cache{$line} > $MAX_TIMEOUT_TRIES) {
320
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);
328             } else {
329                 my $str = "$src -> $dst($proto/$dport)";
330                 if ($direction eq 'dst') {
331                     $str = "$src($proto/$sport) -> $dst";
332                 }
333                 &logr('[-]', "exceeded max removal tries for $str, " .
334                     "deleting from cache", $NO_MAIL);
335             }
336             push @del_keys, $line;
337         }
338     }
339     if (@del_keys) {
340         for my $key (@del_keys) {
341             delete $timeout_cache{$key};
342         }
343     }
344     return;
345 }
346
347 sub rm_fw_rule() {
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) = @_;
351
352     if ($external_cmd_close ne 'NA') {
353
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);
359
360     } else {
361
362         if ($config{'FIREWALL_TYPE'} eq 'iptables') {
363
364             return &rm_ipt_rule($timeout, $src, $sport, $dst, $dport,
365                         $proto, $table, $chain, $target, $direction,
366                         $nat_ip, $nat_port);
367
368         } elsif ($config{'FIREWALL_TYPE'} eq 'ipfw') {
369
370             return &disable_ipfw_rule($timeout, $src, $dst, $proto, $dport);
371         }
372     }
373
374     return 0;
375 }
376
377 sub rm_ipt_rule() {
378     my ($timeout, $src, $sport, $dst, $dport, $proto,
379         $table, $chain, $target, $direction, $nat_ip, $nat_port) = @_;
380
381     my $removed_rule = 0;
382
383     my %extended_info = ('protocol' => $proto);
384     if ($sport) {
385         $extended_info{'s_port'} = $sport;
386     }
387     if ($dport) {
388         $extended_info{'d_port'} = $dport;
389     }
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;
393     }
394
395     my ($find_rv, $num_chain_rules) = $ipt_obj->find_ip_rule($src, $dst,
396             $table, $chain, $target, \%extended_info);
397
398     if ($find_rv) {
399
400         my $del_rv = 0;
401         my $out_ar = [];
402         my $err_ar = [];
403
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);
407             last if $del_rv;
408         }
409
410         my $str = "$src -> $dst($proto/$dport)";
411         if ($direction eq 'dst') {
412             $str = "$src($proto/$sport) -> $dst";
413         }
414         if (defined $extended_info{'to_ip'}) {
415             $str = "$src -> $extended_info{'to_ip'}" .
416                 "($proto/$extended_info{'to_port'})";
417         }
418
419         if ($del_rv) {
420             &logr('[+]', "removed iptables $chain $target rule " .
421                 "for $str, $timeout sec timeout exceeded", $SEND_MAIL);
422             $removed_rule = 1;
423         } else {
424             &logr('[-]', "could not delete $target rule for $str", $NO_MAIL);
425             &psyslog_errs($err_ar);
426         }
427     }
428     return $removed_rule;
429 }
430
431 sub remove_ipfw_rules_without_connections() {
432
433     my %rules_to_remove = ();
434
435     my $cmd = "$cmds{'ipfw'} -dS set $config{'IPFW_SET_NUM'} list";
436
437     open LIST, "$cmd |" or die "[*] Could not execute $cmd: $!";
438
439     my @rules = <LIST>;
440
441     for (@rules) {
442         last if (/^\s*##\s+Dynamic\s+rules/);
443         if (/^\s*#\s+DISABLED\s+(\d+)/) {
444             $rules_to_remove{$1} = 1;
445         }
446     }
447
448     die "[*] Dynamic part of rule listing missing" if (!$_);
449
450     for (@rules) {
451         if(/^\s*(\d+)\s+\d+\s+\d+\s+\(\S+\)\s+STATE/) {
452             $rules_to_remove{$1} = 0;
453         }
454     }
455
456     while ((my $rule, my $needs_remove) = each %rules_to_remove) {
457         &ipfw_delete_ip_rule($rule) if ($needs_remove);
458     }
459     close LIST;
460 }
461
462 sub disable_ipfw_rule() {
463     my ($timeout, $src, $dst, $proto, $port) = @_;
464
465     my $disabled_rule = 0;
466
467     $src = 'any' if $src =~ /$zero_ip_re/;
468     $dst = 'any' if $dst =~ /$zero_ip_re/;
469
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);
473
474     if ($ipfw_is_dynamic and $rulenum and $setnum == 0) {
475         if (&ipfw_move_rule($rulenum, $config{'IPFW_SET_NUM'})) {
476
477             &logr('[+]', "disabled ipfw allow " .
478                     "rule for $src -> " .
479                     "$proto/$port, $timeout " .
480                     "second timeout exceeded", $SEND_MAIL);
481             $disabled_rule = 1;
482         } else {
483             &logr('[-]', "could not disable ipfw allow rule for $src " .
484                 "-> $proto/$port", $NO_MAIL);
485         }
486     } elsif ($rulenum) {
487         if (&ipfw_delete_ip_rule($rulenum)) {
488
489             &logr('[+]', "removed ipfw allow " .
490                     "rule for $src -> " .
491                     "$proto/$port, $timeout " .
492                     "second timeout exceeded", $SEND_MAIL);
493             $disabled_rule = 1;
494         } else {
495             &logr('[-]', "could not remove ipfw allow rule for $src " .
496                 "-> $proto/$port", $NO_MAIL);
497         }
498     }
499
500     return $disabled_rule;
501 }
502
503 sub ipfw_check_dynamic_rule() {
504
505     open LIST, "$cmds{'ipfw'} list |" or
506         die "[*] Could not execute 'ipfw list'";
507     while (<LIST>) {
508         if (/check-state/) {
509             ### from the ipfw man page:
510             # check-state
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
516             #    limit rule.
517             $ipfw_is_dynamic = 1;
518             last;
519         } elsif (/keep-state/) {
520             ### from the ipfw man page:
521             # keep-state
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
527             #    packet is found.
528             $ipfw_is_dynamic = 1;
529             last;
530         } elsif (/allow.*to\s+any\s+established/) {
531             last;
532         }
533     }
534     close LIST;
535     return;
536 }
537
538 sub ipfw_find_ip_rule() {
539     my ($src, $dst, $proto, $port) = @_;
540
541     my $rulenum = 0;
542     my $set = -1;
543
544     open LIST, "$cmds{'ipfw'} -S list |" or
545         die "[*] Could not execute 'ipfw list'";
546     while (<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) {
552                 $rulenum = $2;
553                 $set = $3;
554                 last;
555             }
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) {
560                 $rulenum = $2;
561                 $set = $3;
562                 last;
563             }
564         } else {  ### icmp
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) {
567                 $rulenum = $2;
568                 $set = $3;
569                 last;
570             }
571         }
572     }
573     close LIST;
574
575     if ($rulenum) {
576         ### remove any leading zeros from the rule number
577         $rulenum =~ s/^0{1,4}//g;
578     }
579
580     return $rulenum, $set;
581 }
582
583 sub ipfw_delete_ip_rule() {
584     my $rulenum = shift;
585
586     open IPFW, "| $cmds{'ipfw'} delete $rulenum" or die "[*] Could not ",
587         "execute $cmds{'ipfw'} delete $rulenum";
588     close IPFW;
589
590     return 1;
591 }
592
593 sub ipfw_move_rule() {
594     my ($rulenum, $setnum) = @_;
595
596     my $cmd = "$cmds{'ipfw'} set move rule $rulenum to $setnum";
597
598     open IPFW, "| $cmd" or die "[*] Could not execute $cmd: $!";
599     close IPFW;
600
601     return 1;
602 }
603
604 sub is_connected() {
605     my $src = shift;
606
607     my $is_connected = 0;
608
609     if ($config{'FIREWALL_TYPE'} eq 'iptables') {
610
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) {
621                     $is_connected = 1;
622                     last CONNTRACK;
623                 }
624             }
625         }
626         close CONNTRACK;
627
628     } elsif ($config{'FIREWALL_TYPE'} eq 'ipfw') {
629         ### need to work out similar strategy on FreeBSD
630     }
631
632     return $is_connected;
633 }
634
635 sub exec_external_cmd_close() {
636     my ($cmd, $cmd_alarm) = @_;
637     my $pid;
638     if ($pid = fork()) {
639         local $SIG{'ALRM'} = sub {die "[*] External cmd timeout.\n"};
640         alarm $cmd_alarm;
641         eval {
642             waitpid($pid, 0);
643         };
644         alarm 0;
645         if ($@) {
646             kill 9, $pid unless kill 15, $pid;
647         }
648     } else {
649         die "[*] Could not fork for external cmd: $!" unless defined $pid;
650         if ($cmd =~ /\s*>\s*/) {
651             exec qq{$cmd};
652         } else {
653             exec qq{$cmd > /dev/null 2>&1};
654         }
655     }
656     return 1;
657 }
658
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"
663             unless -e $file;
664         &import_config($file);
665     }
666     return;
667 }
668
669 sub import_config() {
670     my $config_file = shift;
671     open C, "< $config_file" or die "[*] Could not open ",
672         "config file $config_file: $!";
673     my @lines = <C>;
674     close C;
675     for my $line (@lines) {
676         chomp $line;
677         next if ($line =~ /^\s*#/);
678         if ($line =~ /^(\S+)\s+(.*?)\;/) {
679             my $varname = $1;
680             my $val     = $2;
681             if ($val =~ m|/.+| and $varname =~ /^(\w+)Cmd$/) {
682                 ### found a command
683                 $cmds{$1} = $val unless defined $cmds{$1};
684             } else {
685                 $config{$varname} = $val unless defined $config{$varname};
686             }
687         }
688     }
689     return;
690 }
691
692 sub expand_vars() {
693     my $exclude_hr = shift;
694
695     my $has_sub_var = 1;
696     my $resolve_ctr = 0;
697
698     while ($has_sub_var) {
699         $resolve_ctr++;
700         $has_sub_var = 0;
701         if ($resolve_ctr >= 20) {
702             die "[*] Exceeded maximum variable resolution counter.";
703         }
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+)|) {
709                     my $sub_var = $1;
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}|;
714                         $hr->{$var} = $val;
715                     } else {
716                         die "[*] sub-var \"$sub_var\" not defined in ",
717                             "config for var: $var."
718                     }
719                     $has_sub_var = 1;
720                 }
721             }
722         }
723     }
724     return;
725 }
726
727 ### check paths to commands and attempt to correct if any are wrong.
728 sub check_commands() {
729     my ($include_hr, $exclude_hr) = @_;
730
731     my @path = qw(
732         /bin
733         /sbin
734         /usr/bin
735         /usr/sbin
736         /usr/local/bin
737         /usr/local/sbin
738     );
739     for my $cmd (keys %cmds) {
740
741         if (keys %$include_hr) {
742             next unless defined $include_hr->{$cmd};
743         }
744         if (keys %$exclude_hr) {
745             next if defined $exclude_hr->{$cmd};
746         }
747
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';
752         }
753
754         if ($cmd eq 'mail' or $cmd eq 'sendmail') {
755             next if $config{'ALERTING_METHODS'} =~ /noe?mail/i;
756         }
757         unless (-x $cmds{$cmd}) {
758             my $found = 0;
759             PATH: for my $dir (@path) {
760                 if (-x "${dir}/${cmd}") {
761                     $cmds{$cmd} = "${dir}/${cmd}";
762                     $found = 1;
763                     last PATH;
764                 }
765             }
766             unless ($found) {
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';
770             }
771         }
772         if (-x $cmds{$cmd}) {
773             if ($cmd eq 'sendmail') {
774                 $use_sendmail = 1;
775             }
776         } else {
777             die "[*] Command $cmd is located at $cmds{$cmd}, but ",
778                 "is not executable by uid: $<" unless $cmd eq 'sendmail';
779         }
780     }
781     return;
782 }
783
784 sub sendmail() {
785     my $subject = shift;
786     $subject =~ s/\"//g;
787
788     if ($use_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";
794         close SMAIL;
795     } else {
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'}: $!";
799         close MAIL;
800     }
801     return;
802 }
803
804 sub uniquepid() {
805     if (-e $config{'KNOPTM_PID_FILE'}) {
806         my $caller = $0;
807         open PIDFILE, "< $config{'KNOPTM_PID_FILE'}";
808         my $pid = <PIDFILE>;
809         close PIDFILE;
810         chomp $pid;
811         if (kill 0, $pid) {  # knoptm is already running
812             die "[*] knoptm (pid: $pid) is already running!  Exiting.\n";
813         }
814     }
815     return;
816 }
817
818 sub writepid() {
819     open P, "> $config{'KNOPTM_PID_FILE'}" or die "[*] Could not open ",
820         "$config{'KNOPTM_PID_FILE'}: $!";
821     print P $$, "\n";
822     close P;
823     chmod 0600, $config{'KNOPTM_PID_FILE'};
824     return;
825 }
826
827 sub knoptm_init() {
828
829     ### import any override config files first
830     &import_override_configs() if $override_config_str;
831
832     ### import config
833     &import_config($config_file);
834
835     &expand_vars({'EXTERNAL_CMD_OPEN' => '', 'EXTERNAL_CMD_CLOSE' => ''});
836
837     ### make sure all the vars we need are actually in the config file.
838     &required_vars();
839
840     ### import all necessary perl modules
841     &import_perl_modules();
842
843     ### validate config
844     &validate_config();
845
846     &import_ipt_modules() if $config{'FIREWALL_TYPE'} eq 'iptables';
847
848     ### make sure there is not another knoptm process already running.
849     &uniquepid();
850
851     ### make sure command paths are correct
852     &check_commands({}, {'gpg' => '', 'gpg2' => '', 'mail' => ''});
853
854     &check_commands({'mail', ''}, {}) unless $use_sendmail;
855
856     unless ($debug) {
857         my $pid = fork();
858         exit 0 if $pid;
859         die "[*] $0: Couldn't fork: $!" unless defined $pid;
860         POSIX::setsid() or die "[*] $0: Can't start a new session: $!";
861     }
862
863     ### write our pid out to disk
864     &writepid();
865
866     ### Install signal handlers for debugging and for reaping zombie
867     ### whois processes.
868     $SIG{'__WARN__'} = \&warn_handler;
869     $SIG{'__DIE__'}  = \&die_handler;
870     $SIG{'CHLD'}     = \&REAPER;
871
872     unlink $config{'KNOPTM_IP_TIMEOUT_SOCK'}
873         if -e $config{'KNOPTM_IP_TIMEOUT_SOCK'};
874
875     if ($config{'ENABLE_VOLUNTARY_EXITS'} eq 'Y') {
876         $voluntary_exit_timestamp = time();
877     }
878
879     &handle_locale();
880
881     &get_ipt_object() if $config{'FIREWALL_TYPE'} eq 'iptables';
882     &ipfw_check_dynamic_rule() if $config{'FIREWALL_TYPE'} eq 'ipfw';
883
884     if ($debug_to_file) {
885         unlink $debug_to_file if -e $debug_to_file;
886     } elsif ($debug_include_pidname) {
887         $debug = 1;
888     }
889
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);
893     }
894
895     return;
896 }
897
898 ### write a message to syslog (leaves off $prefix, which assigns a
899 ### "type" to the message, when writing syslog; might add it later
900 sub logr() {
901     my ($prefix, $msg, $send_email) = @_;
902
903     return if $no_logs;
904
905     $msg = "knoptm: $msg" if $debug_include_pidname;
906
907     if ($debug) {
908         print STDERR localtime() . " $prefix $msg\n";
909         return;
910     } elsif ($debug_to_file) {
911         open DBG, ">> $debug_to_file" or die $!;
912         print DBG localtime() . " $prefix $msg\n";
913         close DBG;
914         return;
915     }
916
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");
920     }
921
922     return if $config{'ALERTING_METHODS'} =~ /no.?syslog/i;
923
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());
942     }
943
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);
960     }
961
962     closelog();
963
964     return;
965 }
966
967 sub psyslog_errs() {
968     my $aref = shift;
969     return if $config{'ALERTING_METHODS'} =~ /no.?syslog/i;
970
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());
989     }
990
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]);
994         }
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]);
998         }
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]);
1002         }
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]);
1006         }
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]);
1010         }
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]);
1014         }
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]);
1018         }
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]);
1022         }
1023     }
1024
1025     closelog();
1026     return;
1027 }
1028
1029 sub intf_error_condition() {
1030
1031     my $found_intf   = 0;
1032     my $intf_running = 0;
1033     my $parsed_rx_bytes = 0;
1034     my $parsed_tx_bytes = 0;
1035
1036     ### Linux:
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)
1045
1046     ### FreeBSD:
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
1052     ###     status: active
1053
1054     my $cmd = "$cmds{'ifconfig'} $sniff_interface 2> /dev/null";
1055
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+)/;
1062     }
1063     close IFCONFIG;
1064
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);
1070             return 1;
1071         }
1072     }
1073
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;
1079
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);
1085             return 1;
1086         }
1087     }
1088
1089     ### interface RX/TX bytes increasing check
1090     if ($config{'ENABLE_INTF_BYTES_CHECK'} eq 'Y') {
1091         my $return_err = 0;
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);
1095             $return_err = 1;
1096         }
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);
1100             $return_err = 1;
1101         }
1102
1103         $intf_rx_bytes = $parsed_rx_bytes;
1104         $intf_tx_bytes = $parsed_tx_bytes;
1105
1106         return 1 if $return_err;
1107     }
1108
1109     return 0;  ### no error condition
1110 }
1111
1112 sub check_voluntary_exits() {
1113
1114     return unless $config{'ENABLE_VOLUNTARY_EXITS'} eq 'Y';
1115     return if $no_voluntary_exits;
1116
1117     if ((time() - $voluntary_exit_timestamp) > $config{'EXIT_INTERVAL'}*60) {
1118
1119         ### EXIT_INTERVAL is in minutes
1120         &logr('[+]', "voluntary exit timer expired, knopwatchd will restart",
1121             $SEND_MAIL);
1122         &logr('[+]', "stopping fwknopd daemon, knopwatchd will restart",
1123             $SEND_MAIL);
1124
1125         &stop_daemon($config{'FWKNOP_PID_FILE'});
1126
1127         exit 0;
1128     }
1129
1130     return;
1131 }
1132
1133 sub stop_daemon() {
1134     my $pidfile = shift;
1135     return unless -e $pidfile;
1136     open PID, "< $pidfile" or die "[*] Could not open $pidfile: $!";
1137     my $pid = <PID>;
1138     close PID;
1139     chomp $pid;
1140     if (kill 0, $pid) {
1141         if (kill 15, $pid) {
1142             unlink $pidfile;
1143         } else {
1144             kill 9, $pid;
1145         }
1146     } else {
1147         unlink $pidfile;
1148     }
1149     return;
1150 }
1151
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
1165     ) {
1166         die "[*] Required variable $var is not defined in $config_file"
1167             unless defined $config{$var};
1168     }
1169     return;
1170 }
1171
1172 sub validate_config() {
1173
1174     die qq([*] Invalid EMAIL_ADDRESSES value: "$config{'EMAIL_ADDRESSES'}")
1175         unless $config{'EMAIL_ADDRESSES'} =~ /\S+\@\S+/;
1176
1177     ### translate commas into spaces
1178     $config{'EMAIL_ADDRESSES'} =~ s/\s*\,\s/ /g;
1179
1180     if ($fw_type) {
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;
1185     }
1186
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";
1194     }
1195
1196     @conntrack_ports = split /\s*,\s*/, $config{'CONNTRACK_ESTAB_PORTS'};
1197     return;
1198 }
1199
1200 sub import_ipt_modules() {
1201
1202     unless ($imported_iptables_modules) {
1203
1204         require IPTables::Parse;
1205         require IPTables::ChainMgr;
1206
1207         $imported_iptables_modules = 1;
1208     }
1209
1210     return;
1211 }
1212
1213 sub die_handler() {
1214     $die_msg = shift;
1215     return;
1216 }
1217
1218 ### write all warnings to a logfile
1219 sub warn_handler() {
1220     $warn_msg = shift;
1221     return;
1222 }
1223
1224 sub REAPER {
1225     my $pid;
1226     while(($pid = waitpid(-1,WNOHANG)) > 0) {
1227         # could add code to something with the borked pid here
1228     }
1229     $SIG{'CHLD'} = \&REAPER;
1230     return;
1231 }
1232
1233 sub is_digit() {
1234     my $str = shift;
1235     return 1 if $str =~ /^\d+$/;
1236     return 0;
1237 }
1238
1239 sub get_ipt_object() {
1240
1241     my %ipt_opts = (
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
1248     );
1249     $ipt_opts{'debug'} = 1 if $debug;
1250     $ipt_opts{'ipt_exec_sleep'} = $config{'IPT_EXEC_SLEEP'}
1251         if $config{'IPT_EXEC_SLEEP'} > 0;
1252
1253     $ipt_obj = new IPTables::ChainMgr(%ipt_opts)
1254         or die '[*] Could not acquire IPTables::ChainMgr object.';
1255     return;
1256 }
1257
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";
1263     close D;
1264     $die_msg = '';
1265     return;
1266 }
1267
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";
1273     close D;
1274     $warn_msg = '';
1275     return;
1276 }
1277
1278 sub handle_locale() {
1279     $config{'LOCALE'} = $cmdline_locale if $cmdline_locale;
1280
1281     if ($config{'LOCALE'} ne 'NONE' and not $no_locale) {
1282         ### set LC_ALL env variable
1283         $ENV{'LC_ALL'} = $config{'LOCALE'};
1284     }
1285     return;
1286 }
1287
1288 sub import_perl_modules() {
1289
1290     my $mod_paths_ar = &get_mod_paths();
1291
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;
1295     }
1296
1297     if ($debug or $debug_to_file) {
1298         &logr('[+]', "import_perl_modules INC array:", $NO_MAIL);
1299         for (@INC) {
1300             &logr('[+]', $_, $NO_MAIL);
1301         }
1302     }
1303
1304     unless ($config{'ALERTING_METHODS'} =~ /no.?syslog/i) {
1305         require Unix::Syslog;
1306         Unix::Syslog->import(qw(:subs :macros));
1307     }
1308
1309     return;
1310 }
1311
1312 sub get_mod_paths() {
1313
1314     my @paths = ();
1315
1316     $config{'FWKNOP_MOD_DIR'} = $lib_dir if $lib_dir;
1317
1318     unless (-d $config{'FWKNOP_MOD_DIR'}) {
1319         my $dir_tmp = $config{'FWKNOP_MOD_DIR'};
1320         $dir_tmp =~ s|lib/|lib64/|;
1321         if (-d $dir_tmp) {
1322             $config{'FWKNOP_MOD_DIR'} = $dir_tmp;
1323         } else {
1324             return [];
1325         }
1326     }
1327
1328     opendir D, $config{'FWKNOP_MOD_DIR'}
1329         or die "[*] Could not open $config{'FWKNOP_MOD_DIR'}: $!";
1330     my @dirs = readdir D;
1331     closedir D;
1332
1333     push @paths, $config{'FWKNOP_MOD_DIR'};
1334
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");
1341     }
1342     return \@paths;
1343 }
1344
1345 sub usage() {
1346     my $exit_status = shift;
1347     print <<_HELP_;
1348
1349 knoptm; Access timeout daemon for fwknop
1350
1351 [+] Version: $version, by Michael Rash (mbr\@cipherdyne.org)
1352     URL: http://www.cipherdyne.org/fwknop/
1353
1354 Usage: knoptm [options]
1355
1356 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
1360                               daemon.
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.
1370 _HELP_
1371     exit $exit_status;
1372 }