#!/usr/bin/perl ###################################################################### # # http://www.unspecific.com/spa/ # # # Copyright (c) 2005, MadHat (madhat@unspecific.com) # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions # are met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in # the documentation and/or other materials provided with the distribution. # * Neither the name of MadHat Productions nor the names of its # contributors may be used to endorse or promote products derived # from this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED # TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # ###################################################################### # config file for the server my $server_conf = '/usr/local/etc/spa.conf'; # passphrase for decrypting GPG packets (TMP Solution) my $passphrase = "simplenomad"; ###################################################################### # You should not change anything below here unless you # know what you are doing use GnuPG::Interface; use Sys::Syslog qw(:DEFAULT setlogsock); my $VERSION = 0.4; my %loaded, %config; # If we receive a HUP, we reload all configs # user_config, server_config, firewall state $SIG{HUP}=\&reload_conf; # Call reload to load config the first time &reload_conf; if ($config{DEBUG}) { if (setlogsock('unix')) { openlog('spad_engine', '', LOG_USER); syslog(LOG_INFO, 'Launching spa_engine', ''); } else { die("Failed to open syslog"); } } syslog(LOG_INFO, 'OPENING FIFO', '') if ($config{DEBUG}); open(FIFO, "< /var/spa/spa") or syslog(LOG_INFO, "Can't open FIFO: $!", ''); syslog(LOG_INFO, 'READING FIFO', '') if ($config{DEBUG}); # while wwe have data coming from the spad while () { # data passed via FIFO (/var/spa/spa) # src_ip data_size data # OR # src_ip dst_port syslog(LOG_INFO, "Adding " . length($_) . " bytes of new data from FIFO", '') if ($config{DEBUG} > 2); if ($config{DEBUG} > 4) { # If we are running in DEBUG mode, display the packet in HEX in the # syslog, 8 bytes per line my @bytes = split('', $_); my $raw_data = ''; for (0..$#bytes) { $raw_data .= " " . unpack('H*', $bytes[$_]); if (($_ + 1) % 8 == 0 and $raw_data) { syslog(LOG_INFO, "PACKET:%s", $raw_data); undef $raw_data; } } syslog(LOG_INFO, "PACKET:%s", $raw_data); } $data .= $_; if (!$length) { @test_data = split(' ', $_); $length = $test_data[1]; } if ($test_data[1] =~ /^d{1,5}$/) { syslog(LOG_INFO, "FIFO reported $test_data[1] bytes of data sent", '') if ($config{DEBUG} > 2); my $diff = abs(length($_) - $test_data[1]); syslog(LOG_INFO, "FIFO is off by %d bytes", length($_) - $test_data[1]) if ($config{DEBUG} > 2); } elsif (length($data) >= $length) { syslog(LOG_INFO, "Looks like we got it all, processing", '') if ($config{DEBUG} > 2); &process($data); $data = ''; $length = ''; } else { syslog(LOG_INFO, "Total Data Collected: %d bytes (expecting $length bytes)", length($data)) if ($config{DEBUG} > 2); next; } } syslog(LOG_INFO, 'CLOSING FIFO', '') if ($config{DEBUG}); if (&save_user_config($config{UserConf}) == -1) { syslog(LOG_INFO, 'ERROR WRITING CONFIG', ''); } exit 0; ###################################################################### # Process the packet from the FIFO # sub process { my ($buffer) = @_; my $new_seq; syslog(LOG_INFO, "Processing %d bytes of FIFO Data", length($buffer)) if ($config{DEBUG}); if ($config{DEBUG} > 4) { # If we are running in DEBUG mode, display the packet in HEX in the # syslog, 8 bytes per line my @bytes = split('', $buffer); for (0..$#bytes) { $raw_data .= " " . unpack('H*', $bytes[$_]); if (($_ + 1) % 8 == 0 and $raw_data) { syslog(LOG_INFO, "PACKET:%s", $raw_data); undef $raw_data; } } syslog(LOG_INFO, "PACKET:%s", $raw_data); undef $raw_data; } $buffer =~ /^([\d\.]+) (\d+) (.+)$/s; my $src_ip = $1; my $length = $2; my $gpg = $3; if ( $gpg ) { syslog(LOG_INFO, 'Data in FIFO looks encrypted', '') if ($config{DEBUG}); if ($src_ip and $config{DEBUG}) { syslog(LOG_INFO, "Received " . length($gpg) . " bytes of data from: $src_ip", ''); } my $gnupg = GnuPG::Interface->new(); syslog(LOG_INFO, "Setting homedir for gnupg: $config{GPGDir}", '') if ($config{DEBUG}); $gnupg->options->hash_init( homedir => $config{GPGDir} ); # added $passphrase_fh -SN my ( $input, $output, $error, $passphrase_fh, $status_fh ) = ( IO::Handle->new(), IO::Handle->new(), IO::Handle->new(), IO::Handle->new(), IO::Handle->new(), # added -SN ); syslog(LOG_INFO, 'Setting handles for gnupg', '') if ($config{DEBUG}); my $handles = GnuPG::Handles->new( stdin => $input, stdout => $output, stderr => $error, passphrase => $passphrase_fh, # set handle -SN status => $status_fh, ); syslog(LOG_INFO, 'Launching gnupg', '') if ($config{DEBUG}); my $pid = $gnupg->decrypt( handles => $handles ); print $passphrase_fh $passphrase; # load in passphrase -SN close $passphrase_fh; # close handle -SN syslog(LOG_INFO, "Launched gnupg %s", $pid) if ($config{DEBUG}); print $input $gpg; close $input; # <== moved this before reading the output -SN waitpid $pid, 0; my @plaintext = <$output>; my @error = <$error>; my @stat = <$status_fh>; close $output; close $error; close $status_fh; if (@error) { for my $e (@error) { syslog(LOG_INFO, "GnuPG Error: %s", $e) if ($config{DEBUG} > 2); if ($e =~ /using \w+ key ID (\w+)/) { $gpg_id = $1; } elsif ($gpg_id and ($e =~ /Good signature from/)) { $good = 1; } } if (!$gpg_id or !$good) { syslog(LOG_INFO, "Unable to verify signature", ); return; } } if (@plaintext and $config{DEBUG}) { syslog(LOG_INFO, "DECODED with gnupg decrypt: %s", join(' ', @plaintext)); } if (@stat and $config{DEBUG} > 2) { for my $m (@stat) { syslog(LOG_INFO, "GnuPG status: %s", $m); } } my ($time, $seq, $user_ip, $mode, $data) = split(':', join(' ', @plaintext)); syslog(LOG_INFO, "Decrypted data: Time %s Seq %s IP %s Mode %s Data %s", $time, $seq, $user_ip, $mode, $data) if ($config{DEBUG}); if (!$time) { return; } if (!$user_ip or $user_ip == 0) { $user_ip = $src_ip; } my @now = gmtime(); $now[5] = $now[5] + 1900; for ($i = 0; $i < 5; $i++) { if (length($now[$i]) < 2) { $now[$i] = "0$now[$i]"; } } my $now = "$now[5]$now[4]$now[3]$now[2]$now[1]$now[0]"; if (abs($time - $now) > $config{PacketTimeout} ) { syslog(LOG_INFO, "packet appeared to be 'old'", '') if ($config{DEBUG}); syslog(LOG_INFO, "$time vs. $now\n", '') if ($config{DEBUG}); next; # next fifo } my @new_seq = split(',', $seq); for my $seq (@new_seq) { if ($seq == $config{$gpg_id}{seq1} or $seq == $config{$gpg_id}{seq2}) { $new_seq = $new_seq[1] . "," . $new_seq[2]; last; } else { syslog(LOG_INFO, "ERROR R:$seq S1:$config{$gpg_id}{seq1} S2:$config{$gpg_id}{seq2} ", '') if ($config{DEBUG}); next; } } if (!$new_seq) { syslog(LOG_INFO, 'Sequence numbers did not match', '') if ($config{DEBUG}); next; # next fifo } else { $config{$gpg_id}{seq1} = $config{$gpg_id}{seq2}; $config{$gpg_id}{seq2} = $new_seq[2]; } # save config if (&save_user_config($config{UserConf}) == -1) { syslog(LOG_INFO, 'ERROR WRITING CONFIG', ''); } # Modes # 1 Firewall # IP port # 2 spa defined scrpts (eventually) # callback # 3 user defined # /usr/local/bin/command if ($config{$gpg_id}{$mode}) { # if Mode is the firewall flipper if ($mode == 1) { # flip the firewall my $rc = &flipfw($user_ip, $data, 2); # if it opened the firewall if ($rc == 1) { syslog( LOG_INFO, "initializing timeout counter ($user_ip -> $data)\n", '') if ($config{DEBUG}); $loaded{$user_ip}{$data} = time; # if it closed the firewall } elsif($rc == -1) { syslog( LOG_INFO, "clearing timeout counter ($user_ip -> $data)\n", '') if ($config{DEBUG}); undef($loaded{$user_ip}{$data}); } &save_fw_state($config{FirewallState}); } elsif ($mode == 2) { } elsif ($mode == 3) { # exec a command my @commands = split (',', $config{$gpg_id}{$mode}); for my $cmd (@commands) { if ($cmd eq $data ) { system($data); } } } else { next; # next fifo } } else { syslog( LOG_INFO, "Command request not allowed by $gpg_id\n",''); } } else { my ($src_ip, $dst_port) = @data; if ($config{DEBUG} > 2) { syslog( LOG_INFO, $#data + 1 . " pieces of data in FIFO\n",''); for my $ip (sort keys %loaded) { syslog( LOG_INFO, "Listing: $ip\n",''); for my $port (sort keys %{$loaded{$ip}}) { syslog( LOG_INFO, " $key -=> $loaded{$ip}{$port}\n",''); } } } if ($loaded{$src_ip}{$dst_port}) { if (&flipfw($src_ip, $dst_port, 1) == 1) { if ( $loaded{$src_ip}{$dst_port} - time > $config{FirewallTimeout}) { my $rc = &flipfw($src_ip, $data, 2); if ($rc == -1) { syslog( LOG_INFO, "closed fw due to timeout\n",'') if ($config{DEBUG}); undef $loaded{$src_ip}{$dst_port}; } } else { syslog( LOG_INFO, "updated last time port was used\n",'') if ($config{DEBUG}); $loaded{$src_ip}{$dst_port} = time; } } else { undef $loaded{$src_ip}{$dst_port}; } &save_fw_state($config{FirewallState}); } else { syslog( LOG_INFO, "packet didn't match\n",'') if ($config{DEBUG}); } } } ###################################################################### # Loading of the server config # sub load_server_config { my($file) = @_; syslog( LOG_INFO, "SERVER CONFIG: loading from $file\n",'') if ($config{DEBUG}); open(FH, $file) or syslog(LOG_CRIT, "SERVER CONFIG ERROR: Unable to open $file: $!\n", ''); while () { chomp; next if (/^#/ or /^\s*$/); my ($key, $value) = split(' ', $_); $config{$key} = $value; } } ###################################################################### sub load_user_config { # User config Format # gpg_id local_username seq_no2,seq_no3 mode:data [mode:data] # 987654 local_user seq_no1,seq_no2 1:21,110,445 2:spa_scripts 3:/usr/local/bin/blah,/usr/bin/blah my($file) = @_; syslog( LOG_INFO, "USER CONFIG: loading from $file\n",'') if ($config{DEBUG}); open(FH, $file) or syslog(LOG_CRIT, "USER CONFIG ERROR: Unable to open $file: $!\n", ''); while () { chomp; next if (/^#/ or /^\s*$/); my($gpg_id, $user, $seq, @data) = split(' ', $_); $config{$gpg_id}{user} = $user; my $c = 0; for $inseq (split(',', $seq)) { $c++; $config{$gpg_id}{"seq$c"} = $inseq; } for $command (@data) { my ($mode, $allowed) = split(':', $command); $config{$gpg_id}{$mode} = $allowed; } } if ($config{DEBUG} > 2) { for my $gpg_id (sort keys %config) { syslog( LOG_INFO, "Loading: $gpg_id\n",''); for my $key (sort keys %{$config{$gpg_id}}) { syslog( LOG_INFO, " $key -=> $config{$gpg_id}{$key}\n",'') if ($key); } } } } ###################################################################### # sub save_user_config { my($file) = @_; # User config Format # gpg_id local_username seq_no2,seq_no3 mode:data [mode:data] syslog( LOG_INFO, "Write Config\n",'') if ($config{DEBUG}); open(FH, ">$file") or return -1; for my $gpg_id (sort keys %config) { # since %config has data other than user config, we test # for GPGID format before writing next if ($gpg_id !~ /^[0-9A-F]{8}$/); syslog( LOG_INFO, "Writing: $gpg_id\n",'') if ($config{DEBUG}); print FH "$gpg_id $config{$gpg_id}{user} $config{$gpg_id}{seq2},$config{$gpg_id}{seq2} 1:$config{$gpg_id}{1} 2:$config{$gpg_id}{2} 3:$config{$gpg_id}{3}\n"; if ($config{DEBUG} > 1) { syslog( LOG_INFO, "$gpg_id $config{$gpg_id}{user} $config{$gpg_id}{seq1},$config{$gpg_id}{seq2} 1:$config{$gpg_id}{1} 2:$config{$gpg_id}{2} 3:$config{$gpg_id}{3}\n",''); for my $key (sort keys %{$config{$gpg_id}}) { syslog( LOG_INFO, " $key -=> $config{$gpg_id}{$key}\n",''); } } } close(FH); } ###################################################################### sub flipfw { my ($src, $dpt, $return) = @_; # src => src ip # dpt => dst port # return, default == 1 # 1 => return state == -1 closed, 0 error, 1 open # 2 => flip and return == -1 closed, 0 error, 1 open if ($return != 2) { $return = 1; } if ($dpt !~ /^\d{1,5}$/ or $src !~ /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/) { syslog( LOG_INFO, "Sent bad data\n", '') if ($config{DEBUG}); return 0; } if ($config{IPFW} =~ /(iptables|ipchains)$/) { $list = `$config{IPFW} -L --line-numbers -n`; } elsif ($config{IPFW} =~ /ipfw$/ ) { $list = `$config{IPFW} list`; } else { syslog( LOG_INFO, "unknown IPFW variable\n", '') if ($config{DEBUG}); return 0; } @list = split "\n", $list; for my $line (@list) { next if ($line =~ /^\s*$/); next if ($line =~ /^num/); syslog(LOG_INFO, "$line\n", '') if ($config{DEBUG} > 2); if ( ($config{IPFW}=~ /(iptables|ipchains)$/ ) and ($line =~ /^Chain input /i) ) { ($C, $i) = split(' ', $line); $chain = $i; syslog(LOG_INFO, "Found chain: $chain\n", '') if ($config{DEBUG} > 1); next; } elsif ( ($line =~ /^Chain /) and ($config{IPFW} =~ /(iptables|ipchains)$/) ) { if ($chain and !$rule) { syslog(LOG_INFO, " Port is not open on FW\n", '') if ($config{DEBUG}); if ($return == 2) { syslog(LOG_INFO, "opening port (tcp) $src -> here:$dpt\n", '') if ($config{DEBUG}); syslog( LOG_INFO, "$config{IPFW} -I $chain 1 -p tcp --dport $dpt --src $src -j ACCEPT\n", '') if ($config{DEBUG} > 2); $rc = `$config{IPFW} -I $chain 1 -p tcp --dport $dpt --src $src -j ACCEPT`; syslog(LOG_INFO, $rc, '') if ($rc and $config{DEBUG} > 2); return 1; } else { syslog(LOG_INFO, "port is closed from $src to here:$dpt", '') if ($config{DEBUG} > 1); return -1; } } elsif ($chain and $rule) { syslog(LOG_INFO, "Port is open on FW\n", '') if ($config{DEBUG}); if ($return == 2) { syslog(LOG_INFO, "closing port (tcp) $src -> here:$dpt", '') if ($config{DEBUG}); syslog(LOG_INFO, "$config{IPFW} -D $chain $rule\n", '') if ($config{DEBUG} > 2); $rc = `$config{IPFW} -D $chain $rule`; syslog(LOG_INFO, $rc, '') if ($rc and $config{DEBUG} > 2); return -1; } else { syslog(LOG_INFO, "port is open from $src to here:$dpt", '') if ($config{DEBUG} > 1); return 1; } } $chain = ''; next; } elsif ( $config{IPFW} =~ /ipfw$/ and $rule) { if ($return == 2) { syslog(LOG_INFO, "closing port (tcp) $src -> here:$dpt", '') if ($config{DEBUG}); syslog( LOG_INFO, "$config{IPFW} delete $rule\n", '') if ($config{DEBUG} > 2); $rc = `$config{IPFW} delete $rule`; syslog(LOG_INFO, $rc, '') if ($rc and $config{DEBUG} > 2); return -1; } else { syslog(LOG_INFO, "port is open from $src to here:$dpt", '') if ($config{DEBUG} > 1); return 1; } } elsif ( $config{IPFW} =~ /ipfw$/ and !$rule) { $chain = 1; } if ($chain) { if ($config{IPFW} =~ /(iptables|ipchains)$/) { ($num, $target, $prot, $opt, $source, $destination, @other) = split(' ', $line); } elsif ($config{IPFW} =~ /ipfw$/) { ($num, $act, $prot, $from, $source, $to, $destination, $dstport, @other) = split(' ', $line); } push @num, $num; if ($source eq $src and ( ($other[1] eq "dpt:$dpt" and $config{IPFW} =~ /iptables$/) or ($other[2] == $dpt and $config{IPFW} =~ /ipchains$/) or ($dstport == $dpt and $config{IPFW} =~ /ipfw$/) ) ) { $rule = $num; } } } if ($return == 2) { syslog(LOG_INFO, "opening port (tcp) $src -> here:$dpt", '') if ($config{DEBUG}); $insert = 0; while ($num[$insert] == $insert + 1) { $insert++; } $insert++; syslog(LOG_INFO, "ipfw add $insert allow tcp from $src to any $dpt\n", '') if ($config{DEBUG} > 2); $rc = `ipfw add $insert allow tcp from $src to any $dpt`; syslog(LOG_INFO, $rc, '') if ($config{DEBUG}); return 1; } else { syslog(LOG_INFO, "port is closed from $src to here:$dpt", '') if ($config{DEBUG} > 1); return -1; } } ###################################################################### sub save_fw_state { my ($file) = @_; open(FW, ">$file") or return -1; for my $ip (sort keys %loaded) { for my $port (sort keys %{$loaded{$ip}}) { print FW "$ip $port $loaded{$ip}{$port}\n"; } } close(FW); } ###################################################################### sub load_fw_state { my ($file) = @_; open(FW, "$fw") or return -1; while () { my ($ip, $port, $time) = split(' '); if (&flipfw($ip, $port, 1) == 1) { $loaded{$ip}{$port} = $time; } } close(FW); } ###################################################################### sub reload_conf { undef %config; undef %loaded; &load_user_config($config{UserConf}); &load_server_config($server_conf); &load_fw_state($config{FirewallState}); }