root/fwknop/tags/fwknop-1.9.2/fwknopd

Revision 1028, 164.6 kB (checked in by mbr, 9 months ago)

bumped version to 1.9.2

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