root/fwknop/tags/fwknop-1.9.3/fwknopd

Revision 1051, 167.1 kB (checked in by mbr, 8 months ago)

merged in iptables SNAT fix for parsed ipt config vars from fwknop.conf

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