root/fwknop/tags/fwknop-1.8.2-pre4/knoptm

Revision 706, 15.8 kB (checked in by mbr, 1 year ago)

updated warnings and die messages to go to the FWKNOP_ERR_DIR directory

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