root/fwknop/tags/fwknop-1.8.4-pre1/fwknopd

Revision 817, 142.6 kB (checked in by mbr, 1 year ago)

fwknop-1.8.4-pre1

  • Property svn:eol-style set to native
  • Property svn:executable set to *
  • Property svn:keywords set to Revision
Line 
1 #!/usr/bin/perl -w
2 #
3 #############################################################################
4 #
5 # File: fwknopd (/usr/sbin/fwknopd)
6 #
7 # URL: http://www.cipherdyne.org/fwknop
8 #
9 # Purpose: fwknopd implements the server portion of an authorization scheme
10 #          known as Single Packet Authorization (SPA) that requires only a
11 #          single encrypted packet to communicate various pieces of
12 #          information including desired access through an iptables policy
13 #          and/or specific commands to execute on the target system.  The
14 #          main application of this program is to protect services such as
15 #          SSH with an additional layer of security in order to make the
16 #          exploitation of vulnerabilities (both 0-day and unpatched code)
17 #          much more difficult.  For more information, see the fwknop(8) man
18 #          page.
19 #
20 # Author: Michael Rash (mbr@cipherdyne.org)
21 #
22 # Version: 1.8.4-pre1
23 #
24 # Copyright (C) 2004-2007 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: fwknopd 583 2006-11-04 20:43:01Z mbr $
41 #
42
43 use lib '/usr/lib/fwknop';
44 use Crypt::CBC;
45 use Unix::Syslog qw(:subs :macros);
46 use Net::IPv4Addr qw(ipv4_in_network);
47 use Net::Pcap;
48 use NetPacket::IP;
49 use NetPacket::UDP;
50 use NetPacket::TCP;
51 use NetPacket::ICMP;
52 use NetPacket::Ethernet;
53 use Digest::MD5 'md5_base64';
54 use IO::Socket;
55 use IO::Handle;
56 use MIME::Base64;
57 use Data::Dumper;
58 use POSIX;
59 use Getopt::Long;
60 use strict;
61
62 my $config_file = '/etc/fwknop/fwknop.conf';
63
64 my $version = '1.8.4-pre1';
65 my $revision_svn = '$Revision$';
66 my $rev_num = '1';
67 ($rev_num) = $revision_svn =~ m|\$Rev.*:\s+(\S+)|;
68
69 my %config     = ();
70 my %cmds       = ();
71 my %p0f_sigs   = ();
72 my %p0f        = ();
73 my @access     = ();
74 my @ipt_config = ();
75 my %fw_access  = ();
76 my %ip_sequences = ();
77 my %md5_msg_store = ();
78
79 my $os_fprint_only = 0;
80 my $print_version  = 0;
81 my $print_help     = 0;
82 my $kill           = 0;
83 my $restart        = 0;
84 my $status         = 0;
85 my $debug          = 0;
86 my $fw_list        = 0;
87 my $ipt_flush      = 0;
88 my $verbose        = 0;
89 my $use_gpg        = 0;
90 my $os_ipt_log     = '';
91 my $cmdline_intf   = '';
92 my $warn_msg       = '';
93 my $die_msg        = '';
94 my $err_wait_timer = 30;  ### seconds
95 my $gpg_agent_info = '';
96 my $skipped_first_loop = 0;
97 my $pcap_sleep_interval = 1;  ### seconds
98 my $imported_iptables_modules = 0;
99 my $include_all_config_data   = 0;
100 my $voluntary_exit_timestamp  = 0;
101
102 ### mode numbers
103 my $SPA_COMMAND_MODE = 0;
104 my $SPA_ACCESS_MODE  = 1;  ### default
105
106 ### default time values
107 my $knock_interval    = 60;
108 my $fw_access_timeout = 300;
109
110 my $enc_port_offset   = 61000;  ### default offset
111 my $enc_key           = '';
112 my $enc_alg           = 'Rijndael';
113 my $enc_blocksize     = 32;
114
115 ### there is a constant "RIJNDAEL_KEYSIZE" in the Crypt::Rijndael sources, but
116 ### it is not used; a 16 byte key size is fine.
117 my $enc_keysize       = 16;
118
119 my $ALG_RIJNDAEL = 1;
120 my $ALG_GNUPG    = 2;
121
122 my $PCAP      = 0;
123 my $FILE_PCAP = 1;
124 my $ULOG_PCAP = 2;
125 my $SHARED_SEQUENCE  = 3;
126 my $ENCRYPT_SEQUENCE = 4;
127
128 ### logr constants
129 my $SEND_MAIL = 1;
130 my $NO_MAIL   = 0;
131
132 ### packet counters
133 my $tcp_ctr  = 0;
134 my $udp_ctr  = 0;
135 my $icmp_ctr = 0;
136
137 ### tcp option types
138 my $tcp_nop_type       = 1;
139 my $tcp_mss_type       = 2;
140 my $tcp_win_scale_type = 3;
141 my $tcp_sack_type      = 4;
142 my $tcp_timestamp_type = 8;
143
144 my %tcp_p0f_opt_types = (
145     'N' => $tcp_nop_type,
146     'M' => $tcp_mss_type,
147     'W' => $tcp_win_scale_type,
148     'S' => $tcp_sack_type,
149     'T' => $tcp_timestamp_type
150 );
151
152 my %access_keys = (
153     'SOURCE' => '',
154     'TYPE'   => '',
155     'KEY'    => '',
156     'OPEN_PORTS'     => '',
157     'GPG_REMOTE_ID'  => '',
158     'GPG_DECRYPT_ID' => '',
159     'GPG_DECRYPT_PW' => '',
160     'GPG_HOME_DIR'   => '',
161     'ULOG_PCAP'      => '',
162     'FILE_PCAP'      => '',
163     'DATA_COLLECT_MODE' => '',
164     'ENCRYPT_SEQUENCE'  => '',
165     'SHARED_SEQUENCE'   => '',
166     'PORT_OFFSET'       => '',
167     'REQUIRE_AUTH_METHOD' => '',
168     'SHADOW_FILE'    => '',
169     'KNOCK_INTERVAL' => '',
170     'KNOCK_LIMIT'    => '',
171     'PERMIT_CLIENT_PORTS' => '',
172     'ENABLE_CMD_EXEC'     => '',
173     'DISABLE_FW_ACCESS'   => '',
174     'REQUIRE_SOURCE_ADDRESS' => '',
175     'CMD_REGEX'         => '',
176     'FW_ACCESS_TIMEOUT' => '',
177     'REQUIRE_USERNAME'  => '',
178     'MIN_TIME_DIFF' => '',
179     'MAX_TIME_DIFF' => '',
180     'RESTRICT_INTF' => '',
181 );
182
183 my $ip_re = qr|(?:[0-2]?\d{1,2}\.){3}[0-2]?\d{1,2}|;
184
185 my @args_cp = @ARGV;
186
187 ### run GetOpt() to get comand line args
188 &handle_command_line();
189
190 &usage(0) if $print_help;
191
192 if ($print_version) {
193     print "[+] fwknopd v$version (file revision: $rev_num)\n",
194         "      by Michael Rash <mbr\@cipherdyne.org>\n";
195     exit 0;
196 }
197
198 if ($os_fprint_only) {
199     print "[+] Entering OS fingerprinting mode.\n";
200 }
201
202 print STDERR "[+] ** Starting fwknopd (debug mode) **\n" if $debug;
203
204 ### setup to run
205 &fwknop_init();
206
207 if ($config{'AUTH_MODE'} eq 'KNOCK' or $os_fprint_only) {
208
209     ### we are running in traditional port knocking mode
210     &knock_loop();
211
212 } elsif ($config{'AUTH_MODE'} eq 'FILE_PCAP'
213         or $config{'AUTH_MODE'} eq 'ULOG_PCAP'
214         or $config{'AUTH_MODE'} eq 'PCAP') {
215
216     ### we are parsing the pcap file created by the ulogd pcap
217     ### writer, or in sniffing mode against an interface
218     &pcap_loop();
219 }
220 exit 0;
221 #============================ end main ==============================
222
223 sub pcap_loop() {
224
225     ### we use both a size and an inode check in the FILE_PCAP and
226     ### ULOG_PCAP modes to check if the file has been rotated
227     my $pcap_file_size  = 0;
228     my $pcap_file_inode = 0;
229
230     ### get pcap opject
231     my $pcap_t = &get_pcap_obj();
232
233     if ($config{'AUTH_MODE'} eq 'FILE_PCAP'
234             or $config{'AUTH_MODE'} eq 'ULOG_PCAP') {
235         ### get file size (we don't need a -e check here because
236         ### this is handled in get_pcap_obj()).
237         $pcap_file_size = -s $config{'PCAP_PKT_FILE'};
238
239         ### get inode associated with the sniffing file
240         $pcap_file_inode = (stat($config{'PCAP_PKT_FILE'}))[1];
241     }
242     print STDERR "[+] pcap_loop()\n" if $debug;
243
244     my $check_file_ctr = 0;
245
246     for (;;) {
247
248         Net::Pcap::loop($pcap_t, 1, \&pcap_process_pkt, 'fwknop_tag');
249
250         if ($config{'AUTH_MODE'} eq 'FILE_PCAP'
251                 or $config{'AUTH_MODE'} eq 'ULOG_PCAP') {
252
253             ### check to see if the pcap file has been rotated (we need to
254             ### close and re-open)
255             if ($check_file_ctr == 10) {
256                 if (-e $config{'PCAP_PKT_FILE'}) {
257                     my $size_tmp  = -s $config{'PCAP_PKT_FILE'};
258                     my $inode_tmp = (stat($config{'PCAP_PKT_FILE'}))[1];
259                     if ($inode_tmp != $pcap_file_inode
260                             or $size_tmp < $pcap_file_size) {
261
262                         ### the file was rotated or shrank, so get new
263                         ### pcap_t object
264                         Net::Pcap::close($pcap_t);
265
266                         &logr('[+]', "pcap file $config{'PCAP_PKT_FILE'} " .
267                             "shrank or was rotated, so re-opening", $NO_MAIL);
268                         $pcap_t = &get_pcap_obj();
269
270                         ### set file size and inode
271                         $pcap_file_size  = $size_tmp;
272                         $pcap_file_inode = $inode_tmp;
273                     }
274                 } else {
275                     Net::Pcap::close($pcap_t);
276                     &logr('[+]', "pcap file $config{'PCAP_PKT_FILE'} " .
277                         "was rotated, so re-opening", $NO_MAIL);
278                     $pcap_t = &get_pcap_obj();
279
280                     ### set file size and inode
281                     $pcap_file_size  = -s $config{'PCAP_PKT_FILE'};
282                     $pcap_file_inode = (stat($config{'PCAP_PKT_FILE'}))[1];
283                 }
284                 $check_file_ctr = 0;
285             }
286             $check_file_ctr++;
287
288             ### always check to see if we need to timeout access for IPs
289             ### (note that AUTH_MODE set to PCAP, knoptm will timeout access;
290             ### knoptm is not run in either the FILE_PCAP or ULOG_PCAP
291             ### modes).
292             &timeout_access();
293         }
294
295         ### see if fwknopd should voluntarily exit so that it can be
296         ### restarted by knopwatchd
297         &check_voluntary_exits();
298
299         sleep $pcap_sleep_interval;
300     }
301
302     Net::Pcap::close($pcap_t);
303
304     return;
305 }
306
307 sub pcap_process_pkt() {
308     my ($tag, $hdr, $pkt) = @_;
309
310     &write_die_msg() if $die_msg;
311     &write_warn_msg() if $warn_msg;
312
313     return unless $tag eq 'fwknop_tag';
314     return unless defined $hdr;
315     return unless defined $pkt;
316
317     my $ether_data = '';
318     my $ip         = '';
319     my $src_ip     = '';
320     my $proto      = '';
321     my $transport_obj = '';
322
323     if ($config{'AUTH_MODE'} eq 'ULOG_PCAP') {
324         ### The ulogd pcap writer does not include link layer information
325         $ip = NetPacket::IP->decode($pkt) or return;
326     } else {
327         $ether_data = NetPacket::Ethernet::strip($pkt) or return;
328         $ip = NetPacket::IP->decode($ether_data) or return;
329     }
330
331     ### get the source IP address
332     $src_ip = $ip->{'src_ip'} or return;
333
334     ### get the protocol
335     $proto = $ip->{'proto'} or return;
336
337     if ($proto == 1) {
338         $transport_obj = NetPacket::ICMP->decode($ip->{'data'});
339     } elsif ($proto == 6) {
340         $transport_obj = NetPacket::TCP->decode($ip->{'data'});
341     } elsif ($proto == 17) {
342         $transport_obj = NetPacket::UDP->decode($ip->{'data'});
343     } else {
344         return;
345     }
346
347     print STDERR "[+] Received packet ***[" .
348         localtime() . "]*** (" if $debug;
349
350     ### make sure we have _some_ data in the packet; in practice
351     ### any valid SPA message will be longer than 10 bytes, but this
352     ### check is better than nothing
353     return unless defined $transport_obj->{'data'};
354
355     my $enc_msg_len = 0;
356     $enc_msg_len = length($transport_obj->{'data'});
357     if (10 < $enc_msg_len and $enc_msg_len < 1500) {
358         print STDERR "$enc_msg_len bytes)\n" if $debug;
359     } else {
360         print STDERR "$enc_msg_len bytes, not attempting decrypt)\n"
361             if $debug;
362         return;
363     }
364
365     if ($debug) {
366         ### make sure not to print non-printable stuff
367         my $data_tmp = $transport_obj->{'data'};
368         $data_tmp =~ s/[^\x20-\x7e]/NA/g;
369         print STDERR "[+] Raw packet data (single line): $data_tmp\n";
370
371         ### print packet data out in tcpdump -X format
372         if ($verbose) {
373             print STDERR "    Raw packet data (hex dump, minus packet ",
374                 "headers):\n";
375             &hex_dump($transport_obj->{'data'});
376         }
377     }
378
379     ### see if this packet is worthy of getting access through
380     ### the firewall
381     &SPA_check_grant_access($src_ip, $enc_msg_len, $transport_obj->{'data'});
382
383     &write_die_msg() if $die_msg;
384     &write_warn_msg() if $warn_msg;
385
386     ### see if fwknopd should voluntarily exit so that it can be
387     ### restarted by knopwatchd
388     &check_voluntary_exits();
389
390     return;
391 }
392
393 sub SPA_check_grant_access() {
394     my ($src_ip, $enc_msg_len, $pkt_data) = @_;
395
396     ### first check to see if we have any matching access directives
397     ### (in access.conf) for $src_ip, and if not we will do _nothing_
398     ### with this packet.
399     my $access_nums_aref = &check_src($src_ip);
400
401     unless ($access_nums_aref) {
402         print STDERR "[-] Packet from $src_ip did not match any ",
403               "SOURCE blocks in $config{'ACCESS_CONF'}\n" if $debug;
404         return;
405     }
406
407     ### See if the packet qualifies for any access
408     SOURCE: for my $num (@$access_nums_aref) {
409         my $access_hr = $access[$num];
410
411         next SOURCE unless $access_hr->{'DATA_COLLECT_MODE'} == $PCAP
412             or $access_hr->{'DATA_COLLECT_MODE'} == $FILE_PCAP
413             or $access_hr->{'DATA_COLLECT_MODE'} == $ULOG_PCAP;
414
415         &dump_access($access_hr, $num) if $debug and $verbose;
416
417         ### keep track of which source block we are dealing with from
418         ### access.conf
419         my $source_block_num = $access_hr->{'block_num'};
420
421         ### see if we can decrypt and base64-decode
422         my ($decrypt_rv, $decrypted_msg, $gpg_sign_id, $decrypt_algo)
423             = &SPA_decrypt($pkt_data, $enc_msg_len, $access_hr);
424         next SOURCE unless $decrypt_rv;
425
426         ### check for replay attacks
427         my ($md5sum_rv, $md5sum)
428             = &check_replay_attack($decrypted_msg, $src_ip);
429         return if $md5sum_rv;
430
431         ### see if we have a syntactically valid message
432         my ($validate_rv, $msg_href) = &pcap_validate_msg(
433             $decrypted_msg, $source_block_num, $access_hr);
434         if ($debug and not $validate_rv) {
435             print STDERR "[-] Decrypted message does not ",
436                 "conform to a valid SPA packet.\n";
437         }
438         next SOURCE unless $validate_rv;
439
440         ### check to see if client side time stamp is too old
441         my $time_check_rv = &SPA_check_packet_age($msg_href->{'remote_time'});
442         next SOURCE unless $time_check_rv;
443
444         ### dump packet to stderr for debugging purposes
445         &SPA_dump_packet($msg_href) if $debug;
446
447         ### check username
448         next SOURCE unless &SPA_check_user($access_hr, $src_ip, $msg_href);
449
450         ### check authentication method
451         next SOURCE unless &SPA_check_auth_method(
452             $access_hr, $src_ip, $msg_href);
453
454         if ($msg_href->{'action_type'} == $SPA_ACCESS_MODE) {
455             if (&SPA_access($msg_href, $src_ip, $decrypt_algo,
456                     $gpg_sign_id, $md5sum, $access_hr)) {
457                 last SOURCE;
458             } else {
459                 next SOURCE;
460             }
461         } elsif ($msg_href->{'action_type'} == $SPA_COMMAND_MODE) {
462             if (&SPA_cmd($msg_href, $src_ip, $decrypt_algo,
463                     $gpg_sign_id, $md5sum, $access_hr)) {
464                 last SOURCE;
465             } else {
466                 next SOURCE;
467             }
468         }
469     }
470     return;
471 }
472
473 sub SPA_decrypt() {
474     my ($pkt_data, $enc_msg_len, $access_hr) = @_;
475
476     my $decrypted_msg = '';
477     my $decrypt_algo  = $ALG_RIJNDAEL;
478     my $gpg_sign_id   = '';
479     my $decrypt_rv    = 0;
480
481     if ($enc_msg_len > $config{'MIN_GNUPG_MSG_SIZE'}
482             and defined $access_hr->{'GPG_REMOTE_ID'}) {
483         ### attempt GPG decrypt (only if the length of the encrypted
484         ### payload is greater than the minimum size for an SPA message
485         ### encrypted with GnuPG; even encrypting a single byte of data
486         ### with a 1024 bit GnuPG key results in 340 bytes of encrypted
487         ### payload in my testing).
488         ($decrypt_rv, $decrypted_msg, $gpg_sign_id) =
489                 &pcap_GPG_decrypt_msg($pkt_data, $access_hr);
490
491         $decrypt_algo = $ALG_GNUPG if $decrypt_rv;
492     }
493
494     ### fall back to Rijndael if the GnuPG decrypt was not successful
495     ### (and note that the GnuPG decryption is only attempted if the
496     ### packet size is large enough).
497     if (defined $access_hr->{'KEY'} and not $decrypt_rv) {
498
499         ($decrypt_rv, $decrypted_msg) = &pcap_Rijndael_decrypt_msg(
500                             $pkt_data, $access_hr->{'KEY'});
501     }
502
503     if ($decrypt_rv) {
504         if ($debug) {
505             ### make sure not to print non-printable stuff
506             my $dec_tmp_msg = $decrypted_msg;
507             $dec_tmp_msg =~ s/[^\x20-\x7e]/NA/g;
508             print STDERR "[+] Decrypted ",
509                 "message: $dec_tmp_msg\n";
510             if ($verbose) {
511                 print STDERR "    Decrypted message (hex dump):\n";
512                 &hex_dump($decrypted_msg);
513             }
514         }
515     } else {
516         print STDERR "[-] Failed decrypt for SOURCE block ",
517             "$access_hr->{'SOURCE'}\n" if $debug;
518     }
519
520     return $decrypt_rv, $decrypted_msg, $gpg_sign_id, $decrypt_algo;
521 }
522
523 sub SPA_check_packet_age() {
524     my $remote_time = shift;
525
526     if ($config{'ENABLE_SPA_PACKET_AGING'} eq 'Y') {
527         if (abs((time() - $remote_time))
528                 > $config{'MAX_SPA_PACKET_AGE'}) {
529             &logr('[-]', "remote time stamp is older than " .
530                 "$config{'MAX_SPA_PACKET_AGE'} second max age.", $SEND_MAIL);
531             return 0;
532         }
533     }
534     return 1;
535 }
536
537 sub SPA_dump_packet() {
538     my $msg_href = shift;
539
540     print STDERR "[+] Packet fields:\n",
541         "        Random data: $msg_href->{'random_number'}\n",
542         "        Username:    $msg_href->{'username'}\n",
543         "        Remote time: $msg_href->{'remote_time'}\n",
544         "        Remote ver:  $msg_href->{'remote_version'}\n",
545         "        Action type: $msg_href->{'action_type'}\n",
546         "        Action:      $msg_href->{'action'}\n",
547         "        MD5 sum:     $msg_href->{'md5sum'}\n";
548
549     if ($msg_href->{'server_auth'}) {
550         if ($msg_href->{'server_auth'} =~ /^\s*(\w+),(.*)/) {
551             my $server_auth_type = lc($1);
552             my $server_auth_crypt_pw = $2;
553             if ($debug) {
554                 print STDERR "        Server auth: $server_auth_type,";
555                 for (my $i=0; $i<length($server_auth_crypt_pw); $i++) {
556                     print STDERR '*';
557                 }
558                 print STDERR "\n";
559             }
560         }
561     }
562     return;
563 }
564
565 sub SPA_check_user() {
566     my ($access_hr, $src_ip, $msg_href) = @_;
567
568     if (defined $access_hr->{'REQUIRE_USERNAME'}) {
569         my $found = 0;
570         my $user  = '';
571         for my $valid_user (@{$access_hr->{'VALID_USERS'}}) {
572             if ($valid_user eq $msg_href->{'username'}) {
573                 $found = 1;
574                 $user  = $valid_user;
575             }
576         }
577         unless ($found) {
578             &logr('[-]', "username mismatch from $src_ip, expecting " .
579                 "$access_hr->{'REQUIRE_USERNAME'}, got " .
580                 "$msg_href->{'username'}", $SEND_MAIL);
581             return 0;
582         }
583     }
584     return 1;
585 }
586
587 sub SPA_check_auth_method() {
588     my ($access_hr, $src_ip, $msg_href) = @_;
589
590     my $server_auth_type     = '';
591     my $server_auth_crypt_pw = '';
592     if ($msg_href->{'server_auth'}) {
593         if ($msg_href->{'server_auth'} =~ /^\s*(\w+),(.*)/) {
594             $server_auth_type = lc($1);
595             $server_auth_crypt_pw = $2;
596         }
597     }
598
599     if (defined $access_hr->{'REQUIRE_AUTH_METHOD'}) {
600         if ($server_auth_type
601                 eq $access_hr->{'REQUIRE_AUTH_METHOD'}) {
602             if ($server_auth_type eq 'crypt') {
603                 ### check the local UNIX crypt() password associated
604                 ### with the user
605                 unless (&server_auth_verify_crypt_pw(
606                             $msg_href->{'username'},
607                             $server_auth_crypt_pw,
608                             $access_hr->{'SHADOW_FILE'})) {
609                     &logr('[-]', "IP: $src_ip failed server-auth UNIX " .
610                         "crypt() password test", $NO_MAIL);
611                     return 0;
612                 }
613             }
614         } else {
615             &logr('[-]', "required server-auth method " .
616                 "\"$access_hr->{'REQUIRE_AUTH_METHOD'}\" " .
617                 "not supplied by $src_ip", $NO_MAIL);
618             return 0;
619         }
620     }
621     return 1;
622 }
623
624 sub SPA_access() {
625     my ($msg_href, $src_ip, $decrypt_algo, $gpg_sign_id,
626         $md5sum, $access_hr) = @_;
627
628     if ($access_hr->{'DISABLE_FW_ACCESS'}) {
629         &logr('[-]', "received fw access request from $src_ip, " .
630             "but DISABLE_FW_ACCESS is set to a true value", $NO_MAIL);
631         return 0;
632     }
633
634     my $allow_ip = '';
635     $allow_ip = $1 if $msg_href->{'action'} =~ /($ip_re)/;
636
637     unless ($allow_ip) {
638         &logr('[-]', "no valid IP address within action portion of SPA " .
639             "packet from $src_ip", $SEND_MAIL);
640         return 0;
641     }
642
643     if ($allow_ip eq '0.0.0.0') {
644         if ($config{'REQUIRE_SOURCE_ADDRESS'} eq 'Y'
645                 or (defined $access_hr->{'REQUIRE_SOURCE_ADDRESS'}
646                     and $access_hr->{'REQUIRE_SOURCE_ADDRESS'})) {
647             &logr('[-]', "IP: $src_ip sent SPA packet that " .
648                 "contained 0.0.0.0 (-s on the client side) " .
649                 "but REQUIRE_SOURCE_ADDRESS is enabled", $SEND_MAIL);
650             return 0;
651         } else {
652             $allow_ip = $src_ip;
653         }
654     }
655
656     my %open_ports = ();
657
658     ### initialize to the OPEN_PORTS directives (if defined; we know that
659     ### either OPEN_PORTS or PERMIT_CLIENT_PORTS was specified in the
660     ### access.conf file)
661     %open_ports = %{$access_hr->{'OPEN_PORTS'}}
662         if defined $access_hr->{'OPEN_PORTS'};
663
664     if ($access_hr->{'PERMIT_CLIENT_PORTS'}) {
665         if ($msg_href->{'action'}
666                 =~ /($ip_re),(tcp|udp|icmp),(\d+)/i) {
667             ### single port access format (e.g. tcp,22)
668             my $allow_ip        = $1;
669             my $dec_allow_port  = $2;
670             my $dec_allow_proto = $3;
671
672             $open_ports{$dec_allow_proto}{$dec_allow_port} = '';
673
674         } elsif ($msg_href->{'action'}
675                  =~ /($ip_re),(\S+)/) {
676             ### multi-port access format (-A was specified by
677             ### the client)
678             my $allow_ip   = $1;
679             my $access_str = $2;
680
681             my @dec_allow_ports = split /,/, $access_str;
682
683             for my $port_str (@dec_allow_ports) {
684                 if ($port_str =~ m|(\D+)/(\d+)|) {
685                     my $proto = lc($1);
686                     my $port  = $2;
687
688                     next unless ($proto eq 'tcp'
689                         or $proto eq 'udp'
690                         or $proto eq 'icmp');
691                     $port = 0 if $proto eq 'icmp';
692
693                     $open_ports{$proto}{$port} = '';
694                 }
695             }
696         }
697     }
698
699     if ($decrypt_algo == $ALG_GNUPG) {
700         if ($access_hr->{'GPG_REMOTE_ID'} ne 'ANY') {
701             &logr('[+]', "received valid GnuPG encrypted packet " .
702                 qq|(signed with required key ID: "$gpg_sign_id") from: | .
703                 "$src_ip, remote user: $msg_href->{'username'}", $NO_MAIL);
704         } else {
705             &logr('[+]', "received valid GnuPG encrypted packet " .
706                 "from: $src_ip, remote user: $msg_href->{'username'}",
707                 $NO_MAIL);
708         }
709     } else {
710         &logr('[+]', "received valid Rijndael encrypted " .
711             "packet from: $src_ip, remote user: $msg_href->{'username'}",
712             $NO_MAIL);
713     }
714
715     ### cache the MD5 sum
716     $md5_msg_store{$md5sum} = $src_ip;
717
718     ### write MD5 sum to disk
719     &diskwrite_md5_sum($md5sum, $src_ip)
720         if $config{'ENABLE_MD5_PERSISTENCE'} eq 'Y';
721
722     ### grant access through the firewall
723     &grant_access($allow_ip, '', \%open_ports, $access_hr);
724
725     return 1;
726 }
727
728 sub SPA_cmd() {
729     my ($msg_href, $src_ip, $decrypt_algo, $gpg_sign_id,
730         $md5sum, $access_hr) = @_;
731
732     unless ($access_hr->{'ENABLE_CMD_EXEC'}) {
733         &logr('[-]', qq|received command "$msg_href->{'action'}" | .
734                 "but command mode not enabled for $src_ip", $SEND_MAIL);
735         return 0;
736     }
737
738     if (defined $access_hr->{'CMD_REGEX'}) {
739         unless ($msg_href->{'action'} =~ m|$access_hr->{'CMD_REGEX'}|) {
740             &logr('[-]', qq|received command "$msg_href->{'action'}" | .
741                     "from $src_ip but CMD_REGEX did not match $src_ip",
742                     $SEND_MAIL);
743             return 0;
744         }
745     }
746
747     my $cmd = $msg_href->{'action'};
748     my $run_cmd = '';
749     my $cmd_ip  = '';
750
751     if ($cmd =~ m|^\s*($ip_re),(.*)|) {
752         $cmd_ip  = $1;
753         $run_cmd = $2;
754     } else {
755         $run_cmd = $cmd;
756     }
757
758     ### pre-1.0 versions did not prepend command string with "<ip>,"
759     if ($cmd_ip and $cmd_ip eq '0.0.0.0'
760             and $config{'REQUIRE_SOURCE_ADDRESS'} eq 'Y'
761             or (defined $access_hr->{'REQUIRE_SOURCE_ADDRESS'}
762                 and $access_hr->{'REQUIRE_SOURCE_ADDRESS'})) {
763         &logr('[-]', "IP: $src_ip sent SPA packet that " .
764             "contained 0.0.0.0 (-s on the client side) " .
765             "but REQUIRE_SOURCE_ADDRESS is enabled", $SEND_MAIL);
766         return 0;
767     }
768
769     if ($decrypt_algo == $ALG_GNUPG) {
770         if ($access_hr->{'GPG_REMOTE_ID'} ne 'ANY') {
771             &logr('[+]', "received valid GnuPG encrypted packet " .
772                 qq|(signed with required key ID: "$gpg_sign_id") from: | .
773                 "$src_ip, remote user: $msg_href->{'username'}",
774                 $NO_MAIL);
775         } else {
776             &logr('[+]', "received valid GnuPG encrypted packet " .
777                 "from: $src_ip, remote user: $msg_href->{'username'}",
778                 $NO_MAIL);
779         }
780     } else {
781         &logr('[+]', "received valid Rijndael encrypted " .
782             "packet from: $src_ip, remote user: $msg_href->{'username'}",
783             $NO_MAIL);
784     }
785
786     &logr('[+]', qq|executing command "$run_cmd" for $src_ip|, $SEND_MAIL);
787
788     ### cache the MD5 sum
789     $md5_msg_store{$md5sum} = '';
790
791     ### write MD5 sum to disk
792     &diskwrite_md5_sum($md5sum)
793         if $config{'ENABLE_MD5_PERSISTENCE'} eq 'Y';
794
795