root/fwknop/tags/fwknop-1.8/knoptm

Revision 683, 15.6 kB (checked in by mbr, 2 years ago)

bugfix to never time out rules from SOURCE blocks with FW_ACCESS_TIMEOUT set to zero, added keep-state to ipfw rules

  • Property svn:eol-style set to native
  • Property svn:executable set to *
  • Property svn:keywords set to Author Date Id Revision
Line 
1 #!/usr/bin/perl -w
2 #
3 #############################################################################
4 #
5 # File: knoptm
6 #
7 # Purpose: This daemon will remove entries from the iptables chain(s) to
8 #          which fwknop has added access for certain IP addresses.  It uses
9 #          the file /var/log/fwknop/knoptm.cache file in order to determine
10 #          when access should be removed.  The format of the entries in this
11 #          file are as follows:
12 #
13 #   <rule timestamp> <timeout> <ip> <proto> <port> <table> <chain> <target>
14 #
15 # Author: Michael Rash (mbr@cipherdyne.org)
16 #
17 # Version: 1.8
18 #
19 # Copyright (C) 2004-2007 Michael Rash (mbr@cipherdyne.org)
20 #
21 # License (GNU Public License):
22 #
23 #    This program is distributed in the hope that it will be useful,
24 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
25 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
26 #    GNU General Public License for more details.
27 #
28 #    You should have received a copy of the GNU General Public License
29 #    along with this program; if not, write to the Free Software
30 #    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307
31 #    USA
32 #
33 #############################################################################
34 #
35 # $Id$
36 #
37
38 use lib '/usr/lib/fwknop';
39 use Unix::Syslog qw(:subs :macros);
40 use Net::IPv4Addr qw(ipv4_in_network);
41 use IO::Socket;
42 use IO::Handle;
43 use File::Copy;
44 use Data::Dumper;
45 use POSIX;
46 use Getopt::Long;
47 use strict;
48
49 my $config_file = '/etc/fwknop/fwknop.conf';
50 my $user_rc_file = '';
51
52 my $version = '1.8';
53 my $print_help = 0;
54 my $print_ver  = 0;
55 my $debug      = 0;
56 my $die_msg    = '';
57 my $warn_msg   = '';
58 my $timeout_sock = '';
59 my $imported_iptables_modules = 0;
60
61 my %config = ();
62 my %cmds   = ();
63 my %timeout_cache = ();
64
65 my $ip_re = qr|(?:[0-2]?\d{1,2}\.){3}[0-2]?\d{1,2}|;
66
67 ### make Getopts case sensitive
68 Getopt::Long::Configure('no_ignore_case');
69 &usage(1) unless (GetOptions(
70     'config=s' => \$config_file,
71     'Version'  => \$print_ver,
72     'help'     => \$print_help
73 ));
74
75 ### Print the version number and exit if -V given on the command line.
76 if ($print_ver) {
77     print
78 "[+] knoptm v$version (part of the fwknop project), by Michael Rash\n",
79 "    <mbr\@cipherdyne.org>\n";
80     exit 0;
81 }
82
83 &usage(0) if $print_help;
84
85 ### set things up, deal with pid's, and import config
86 &knoptm_init();
87
88 print STDERR "[+] Opening $config{'KNOPTM_IP_TIMEOUT_SOCK'} socket, ",
89     "and entering main loop.\n" if $debug;
90
91 $timeout_sock = IO::Socket::UNIX->new(
92     Type    => SOCK_STREAM,
93     Local   => $config{'KNOPTM_IP_TIMEOUT_SOCK'},
94     Listen  => SOMAXCONN,
95     Timeout => .1
96 ) or die "[*] Could not acquire auto-response domain socket: $!";
97
98 for (;;) {
99     my @fw_cache_entries = ();
100
101     my $fwknop_connection = $timeout_sock->accept();
102     if ($fwknop_connection) {
103         @fw_cache_entries = <$fwknop_connection>;
104
105         ### add new entries to the cache
106         &build_timeout_cache(\@fw_cache_entries) if @fw_cache_entries;
107     }
108
109     ### always check to see if any fw rules need to be removed
110     &timeout_cache_entries();
111
112     &append_die_msg()  if $die_msg;
113     &append_warn_msg() if $warn_msg;
114
115     sleep 1;
116 }
117 close $timeout_sock;
118 exit 0;
119 #============================ end main ==============================
120
121 sub build_timeout_cache() {
122     my $cache_entries_aref = shift;
123     LINE: for my $line (@$cache_entries_aref) {
124         if ($line =~ /^\s*\d+\s+\d+\s+$ip_re
125                 \s+\S+\s+\d+\s+\S+\s+\S+\s+\S+/x) {
126
127             $timeout_cache{$line} = '';
128         }
129     }
130     return;
131 }
132
133 sub timeout_cache_entries() {
134
135     my @del_keys = ();
136     for my $line (keys %timeout_cache) {
137         if ($line =~ /^\s*(\d+)\s+(\d+)\s+($ip_re)
138                 \s+(\S+)\s+(\d+)\s+(\S+)\s+(\S+)\s+(\S+)/x) {
139
140             my $rule_timestamp = $1;
141             my $timeout        = $2;
142             my $ip             = $3;
143             my $proto          = $4;
144             my $port           = $5;
145             my $table          = $6;
146             my $chain          = $7;
147             my $target         = $8;
148
149             ### see if the rule is still active, and remove if necessary
150             if (&rm_fw_rule($rule_timestamp, $timeout, $ip, $proto,
151                     $port, $table, $chain, $target)) {
152
153                 ### delete the entry from the in-memory cache now that
154                 ### the firewall rule has been removed
155                 push @del_keys, $line;
156             }
157         }
158     }
159     if (@del_keys) {
160         for my $key (@del_keys) {
161             delete $timeout_cache{$key};
162         }
163     }
164     return;
165 }
166
167 sub rm_fw_rule() {
168     my ($rule_timestamp, $timeout, $ip, $proto,
169         $port, $table, $chain, $target) = @_;
170
171     return 0 unless ((time() - $rule_timestamp) > $timeout);
172
173     if ($config{'FIREWALL_TYPE'} eq 'iptables') {
174
175         return &rm_ipt_rule($timeout, $ip, $proto,
176                     $port, $table, $chain, $target);
177
178     } elsif ($config{'FIREWALL_TYPE'} eq 'ipfw') {
179
180         return &rm_ipfw_rule($timeout, $ip, $proto, $port);
181     }
182
183     return 0;
184 }
185
186 sub rm_ipt_rule() {
187     my ($timeout, $ip, $proto,
188         $port, $table, $chain, $target) = @_;
189
190     my $removed_rule = 0;
191
192     my %ipt_opts = (
193         'iptables' => $cmds{'iptables'},
194         'iptout'   => $config{'KNOPTM_IPT_OUTPUT_FILE'},
195         'ipterr'   => $config{'KNOPTM_IPT_ERROR_FILE'}
196     );
197     $ipt_opts{'debug'} = 1 if $debug;
198
199     my $ipt = new IPTables::ChainMgr(%ipt_opts)
200         or die '[*] Could not acquire IPTables::ChainMgr object.';
201
202     if ($ipt->find_ip_rule($ip, '0.0.0.0/0', $table,
203             $chain, $target, {'protocol' => $proto,
204             'd_port' => $port})) {
205
206         my ($rv, $out_aref, $err_aref) = $ipt->delete_ip_rule($ip,
207             '0.0.0.0/0', $table, $chain, $target,
208             {'protocol' => $proto, 'd_port' => $port});
209
210         if ($rv) {
211             &logr('[+]', "removed iptables $chain ACCEPT rule " .
212                 "for $ip -> $proto/$port, $timeout " .
213                 "second timeout exceeded", 1);
214             $removed_rule = 1;
215         } else {
216             my $msg = "could not delete ACCEPT rule for $ip -> $proto/$port";
217             &logr('[-]', $msg, 0);
218             &psyslog_errs($err_aref);
219         }
220     }
221     return $removed_rule;
222 }
223
224 sub rm_ipfw_rule() {
225     my ($timeout, $ip, $proto, $port) = @_;
226
227     my $removed_rule = 0;
228
229     my $rulenum = &ipfw_find_ip_rule($ip, 'any', $proto, $port);
230
231     if ($rulenum) {
232         if (&ipfw_delete_ip_rule($rulenum)) {
233
234             &logr('[+]', "removed ipfw pass " .
235                     "rule for $ip -> " .
236                     "$proto/$port, $timeout " .
237                     "second timeout exceeded", 1);
238         } else {
239             my $msg = "could not delete ipfw pass rule for $ip " .
240                 "-> $proto/$port";
241             &logr('[-]', $msg, 0);
242         }
243     }
244
245     return $removed_rule;
246 }
247
248 sub ipfw_find_ip_rule() {
249     my ($src, $dst, $proto, $port) = @_;
250
251     my $rulenum = 0;
252
253     open LIST, "$cmds{'ipfw'} list |" or
254         die "[*] Could not execute 'ipfw list'";
255     while (<LIST>) {
256         if ($proto eq 'tcp' or $proto eq 'udp') {
257             ### 00002 allow tcp from 1.1.1.1 to any dst-port 22 keep-state
258             if (/^\s*(\d+)\s+allow\s+$proto\s+from\s+$src\s+to\s+
259                         $dst\s+dst-port\s+$port\s+keep-state/x) {
260                 $rulenum = $1;
261                 last;
262             }
263         } else### icmp
264             if (/^\s*(\d+)\s+allow\s+$proto\s+from\s+$src\s+to\s+$dst/x) {
265                 $rulenum = $1;
266                 last;
267             }
268         }
269     }
270     close LIST;
271
272     if ($rulenum) {
273         ### remove any leading zeros from the rule number
274         $rulenum =~ s/^0{1,4}//g;
275     }
276
277     return $rulenum;
278 }
279
280 sub ipfw_delete_ip_rule() {
281     my $rulenum = shift;
282
283     open IPFW, "| $cmds{'ipfw'} delete $rulenum" or die "[*] Could not ",
284         "execute $cmds{'ipfw'} delete $rulenum";
285     close IPFW;
286
287     return 1;
288 }
289
290 sub import_config() {
291     open C, "< $config_file" or die "[*] Could not open ",
292         "config file $config_file: $!";
293     my @lines = <C>;
294     close C;
295     for my $line (@lines) {
296         chomp $line;
297         next if ($line =~ /^\s*#/);
298         if ($line =~ /^(\S+)\s+(.*?)\;/) {
299             my $varname = $1;
300             my $val     = $2;
301             if ($val =~ m|/.+| && $varname =~ /^(\w+)Cmd$/) {
302                 ### found a command
303                 $cmds{$1} = $val;
304             } else {
305                 $config{$varname} = $val;
306             }
307         }
308     }
309     return;
310 }
311
312 sub expand_vars() {
313
314     my $has_sub_var = 1;
315     my $resolve_ctr = 0;
316
317     while ($has_sub_var) {
318         $resolve_ctr++;
319         $has_sub_var = 0;
320         if ($resolve_ctr >= 20) {
321             die "[*] Exceeded maximum variable resolution counter.";
322         }
323         for my $hr (\%config, \%cmds) {
324             for my $var (keys %$hr) {
325                 my $val = $hr->{$var};
326                 if ($val =~ m|\$(\w+)|) {
327                     my $sub_var = $1;
328                     die "[*] sub-ver $sub_var not allowed within same ",
329                         "variable $var" if $sub_var eq $var;
330                     if (defined $config{$sub_var}) {
331                         $val =~ s|\$$sub_var|$config{$sub_var}|;
332                         $hr->{$var} = $val;
333                     } else {
334                         die "[*] sub-var \"$sub_var\" not defined in ",
335                             "config for var: $var."
336                     }
337                     $has_sub_var = 1;
338                 }
339             }
340         }
341     }
342     return;
343 }
344
345 ### check paths to commands and attempt to correct if any are wrong.
346 sub check_commands() {
347     my @path = qw(
348         /bin
349         /sbin
350         /usr/bin
351         /usr/sbin
352         /usr/local/bin
353         /usr/local/sbin
354     );
355     for my $cmd (keys %cmds) {
356
357         if ($cmd eq 'iptables') {
358             next unless $config{'FIREWALL_TYPE'} eq 'iptables';
359         } elsif ($cmd eq 'ipfw') {
360             next unless $config{'FIREWALL_TYPE'} eq 'ipfw';
361         }
362         unless (-x $cmds{$cmd}) {
363             my $found = 0;
364             PATH: for my $dir (@path) {
365                 if (-x "${dir}/${cmd}") {
366                     $cmds{$cmd} = "${dir}/${cmd}";
367                     $found = 1;
368                     last PATH;
369                 }
370             }
371             unless ($found) {
372                 die "[*] Could not find $cmd anywhere!!!  Please edit the\n",
373                     "config section in $config_file to include the path to\n",
374                     "$cmd.";
375             }
376         }
377         unless (-x $cmds{$cmd}) {
378             die "[*] Command $cmd is located at $cmds{$cmd}, but ",
379                 "is not executable by uid: $<";
380         }
381     }
382     return;
383 }
384
385 sub sendmail() {
386     my $subject = shift;
387     open MAIL, "| $cmds{'mail'} -s \"$subject\" $config{'EMAIL_ADDRESSES'} " .
388         "> /dev/null" or die "[*] Could not send mail: $cmds{'mail'} -s " .
389         "$subject\" $config{'EMAIL_ADDRESSES'}: $!";
390     close MAIL;
391     return;
392 }
393
394 sub uniquepid() {
395     if (-e $config{'KNOPTM_PID_FILE'}) {
396         my $caller = $0;
397         open PIDFILE, "< $config{'KNOPTM_PID_FILE'}";
398         my $pid = <PIDFILE>;
399         close PIDFILE;
400         chomp $pid;
401         if (kill 0, $pid) {  # knoptm is already running
402             die "[*] knoptm (pid: $pid) is already running!  Exiting.\n";
403         }
404     }
405     return;
406 }
407
408 sub writepid() {
409     open P, "> $config{'KNOPTM_PID_FILE'}" or die "[*] Could not open ",
410         "$config{'KNOPTM_PID_FILE'}: $!";
411     print P $$, "\n";
412     close P;
413     chmod 0600, $config{'KNOPTM_PID_FILE'};
414     return;
415 }
416
417 sub knoptm_init() {
418
419     ### import config
420     &import_config();
421
422     &expand_vars();
423
424     ### make sure all the vars we need are actually in the config file.
425     &required_vars();
426
427     ### validate config
428     &validate_config();
429
430     &import_ipt_modules() if $config{'FIREWALL_TYPE'} eq 'iptables';
431
432     ### make sure there is not another knoptm process already running.
433     &uniquepid();
434
435     ### make sure command paths are correct
436     &check_commands();
437
438     unless ($debug) {
439         my $pid = fork();
440         exit 0 if $pid;
441         die "[*] $0: Couldn't fork: $!" unless defined $pid;
442         POSIX::setsid() or die "[*] $0: Can't start a new session: $!";
443     }
444
445     ### write our pid out to disk
446     &writepid();
447
448     ### Install signal handlers for debugging and for reaping zombie
449     ### whois processes.
450     $SIG{'__WARN__'} = \&warn_handler;
451     $SIG{'__DIE__'}  = \&die_handler;
452     $SIG{'CHLD'}     = \&REAPER;
453
454     unlink $config{'KNOPTM_IP_TIMEOUT_SOCK'}
455         if -e $config{'KNOPTM_IP_TIMEOUT_SOCK'};
456
457     return;
458 }
459
460 ### write a message to syslog (leaves off $prefix, which assigns a
461 ### "type" to the message, when writing syslog; might add it later
462 sub logr() {
463     my ($prefix, $msg, $send_email) = @_;
464     if ($debug) {
465         print STDERR "$prefix $msg\n";
466     } else {
467         unless ($config{'ALERTING_METHODS'} =~ /no.?syslog/i) {
468             ### write a message to syslog
469             openlog 'knoptm', LOG_DAEMON, LOG_LOCAL7;
470             syslog LOG_INFO, $msg;
471             closelog();
472         }
473
474         ### see if we need to send an email
475         if ($send_email and $config{'ALERTING_METHODS'} !~ /noe?mail/i) {
476             &sendmail("$prefix knoptm: $msg");
477         }
478     }
479     return;
480 }
481
482 sub psyslog_errs() {
483     my $aref = shift;
484     return if $config{'ALERTING_METHODS'} =~ /no.?syslog/i;
485
486     ### write a message to syslog
487     openlog 'knoptm', LOG_DAEMON, LOG_LOCAL7;
488     for (my $i=0; $i<5 && $i<=$#$aref; $i++) {
489         syslog LOG_INFO, $aref->[$i];
490     }
491     closelog();
492     return;
493 }
494
495 sub required_vars() {
496     for my $var qw(KNOPTM_PID_FILE FWKNOP_DIR EMAIL_ADDRESSES AUTH_MODE
497             KNOPTM_IP_TIMEOUT_SOCK ALERTING_METHODS FIREWALL_TYPE) {
498         unless (defined $config{$var}) {
499             die "[*] Variable $var is not defined in $config_file";
500         }
501     }
502     return;
503 }
504
505 sub validate_config() {
506
507     die qq([*] Invalid EMAIL_ADDRESSES value: "$config{'EMAIL_ADDRESSES'}")
508         unless $config{'EMAIL_ADDRESSES'} =~ /\S+\@\S+/;
509
510     ### translate commas into spaces
511     $config{'EMAIL_ADDRESSES'} =~ s/\s*\,\s/ /g;
512
513     unless ($config{'AUTH_MODE'} eq 'KNOCK'
514             or $config{'AUTH_MODE'} eq 'ULOG_PCAP'
515             or $config{'AUTH_MODE'} eq 'PCAP') {
516         die "[*] AUTH_MODE must be either KNOCK, ULOG_PCAP, or PCAP";
517     }
518     return;
519 }
520
521 sub import_ipt_modules() {
522
523     unless ($imported_iptables_modules) {
524
525         require IPTables::Parse;
526         require IPTables::ChainMgr;
527
528         $imported_iptables_modules = 1;
529     }
530
531     return;
532 }
533
534 sub die_handler() {
535     $die_msg = shift;
536     return;
537 }
538
539 ### write all warnings to a logfile
540 sub warn_handler() {
541     $warn_msg = shift;
542     return;
543 }
544
545 sub REAPER {
546     my $pid;
547     $pid = waitpid(-1, WNOHANG);
548 #   if (WIFEXITED($?)) {
549 #          print STDERR "[+] **  Process $pid exited.\n";
550 #      }
551     $SIG{'CHLD'} = \&REAPER;
552     return;
553 }
554
555 sub append_die_msg() {
556     open D, ">> $config{'FWKNOP_DIR'}/knoptm.die" or
557         die "[*] Could not open $config{'FWKNOP_DIR'}/knoptm.die: $!";
558     print D scalar localtime(), " $die_msg";
559     close D;
560     $die_msg = '';
561     return;
562 }
563
564 sub append_warn_msg() {
565     open D, ">> $config{'FWKNOP_DIR'}/knoptm.warn" or
566         die "[*] Could not open $config{'FWKNOP_DIR'}/knoptm.warn: $!";
567     print D scalar localtime(), " $warn_msg";
568     close D;
569     $warn_msg = '';
570     return;
571 }
572
573 sub usage() {
574     my $exit_status = shift;
575     print <<_HELP_;
576
577 knoptm; Access timeout daemon for fwknop
578
579 [+] Version: $version, by Michael Rash (mbr\@cipherdyne.org)
580     URL: http://www.cipherdyne.org/fwknop/
581
582 Usage: knoptm [-c <config file>]
583
584 Options:
585     -c, --config <file>        - Specify path to config file instead of using
586                                  the default $config_file.  This
587                                  file is used only when knoptm is run as a
588                                  daemon.
589 _HELP_
590     exit $exit_status;
591 }
Note: See TracBrowser for help on using the browser.