root/fwknop/tags/fwknop-1.8.3/knoptm

Revision 771, 18.3 kB (checked in by mbr, 1 year ago)

fwknop-1.8.2 release

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