root/fwknop/tags/fwknop-1.8.4-pre2/fwknopd

Revision 824, 148.8 kB (checked in by mbr, 1 year ago)

fwknop-1.8.4-pre2

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