#!/usr/local/bin/perl -Tw # Copyright (c) 2000 River of Stars, LLC. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions # are met: # 1. Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # 2. 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. # 3. All advertising materials mentioning features or use of this software # must display the following acknowledgement: # This product includes software developed by River of Stars, LLC. # 4. The name of the author may not be used to endorse or promote products # derived from this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``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 AUTHOR 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. my $DEBUG = 0; ##### # web heartbeat system utilities ##### package hbu; require 5.000; require Exporter; @ISA = qw(Exporter); @EXPORT = qw(ip4v keyg keyv keyl concatkey parsekey julsec secjul stalekey namecode keyr ruleset parmv failbail); @EXPORT_OK = qw($sip $reapt $t $e $w); use strict; use Sys::Syslog; BEGIN { push(@INC, '/usr/local/libexec/authhb'); } ##### # external constants must be visible ##### use vars '$sip'; use vars '$reapt'; use vars '$t'; use vars '$e'; use vars '$w'; use vars '$dr'; $sip = '128.117.128.1'; # ip of heartbeat web server $reapt = 91; # ips reapable after this many secs dead $t = int ($reapt / 4); # normal refresh period in seconds $e = "https://$sip/login.html"; # error/timeout redirect URL $w = 45; # view time before error/timeout redirect $dr = '/var/authhb/db'; # database directory ##### # setup/constants ##### my $r = '< /dev/arandom'; # random number device my $kl = 4; # key random number length in bytes my $maxun = 16; # max username length in bytes my $maxut = 3; # max usertype length in bytes my $julsecl = 9; # length of timestamp on key my $sep = '-'; # separator for key parts my $filt = '/usr/local/sbin/sipf'; # filter binary for adding/removing rules my $nat = '/usr/local/sbin/sipnat'; # filter binary for adding/removing rules my %rules = (); # the actual filter rules $rules{0} = 'log in on xl1 proto tcp from USERIP/32 to any flags S'; $rules{1} = 'pass in quick on xl1 proto tcp/udp from USERIP/32 to any keep state keep frags'; $rules{2} = 'pass in quick on xl1 proto icmp from USERIP/32 to any keep state'; $rules{3} = 'pass in quick on xl1 proto ah from USERIP/32 to any'; $rules{4} = 'pass in quick on xl1 proto esp from USERIP/32 to any'; $rules{5} = 'pass in quick on xl0 proto ah from any to USERIP/32'; $rules{6} = 'pass in quick on xl0 proto esp from any to USERIP/32'; my %nats = (); $nats{0} = 'rdr xl1 from USERIP/32 to any port = 80 -> 128.117.128.1 port 80 tcp'; $nats{1} = 'rdr xl1 from USERIP/32 to any port = 443 -> 128.117.128.1 port 443 tcp'; $nats{2} = 'rdr xl1 from USERIP/32 to any port = 53 -> 128.117.128.1 port 50053 udp'; my $authbin = '/usr/local/libexec/authhb/login'; $ENV{'PATH'} = '/usr/bin:/usr'; sub ip4v ($@) { ##### # check provided dotted quad ip for syntax validity, optional subnet restrictions ##### my $ip = shift @_; my @snets = @_; my($x, @p, $nip); chomp $ip; if ( $ip =~ m/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/ ) { @p = ($1, $2, $3, $4); for ($x=0; $x<4; $x++) { if ( ($p[$x] > 255) || ($p[$x] < 0) ) { $ip = join ('.', @p); syslog ('err', 'octet out of range in IP %s', $ip); return 0; } } $nip = join ('.', @p); # if () { # need to write subnet restrictions # # } return $nip; } else { syslog ('err', 'DOH! supposed IPv4 address was not dotted quad'); return 0; } } sub keyg ($) { ##### # get key for a particular ip ##### my $ip = shift @_; my ($path, $k, $rl); if ( $ip ) { $path = "<" . $dr . "/" . $ip; if ( ! open (KEY, $path) ) { return 'ERRKEYG-1'; } $rl = $maxun * 2 + $maxut * 2 + $kl * 2 + $julsecl; if ( ! read (KEY, $k, $rl) ) { close (KEY); return 'ERRKEYG-2'; } close (KEY); # syslog ('debug', "read old key for %s: %s", $ip, $k); return $k; } else { return 'ERRKEYG-9'; } } sub keyv ($$) { ##### # check provided key for match with the one in the db ##### my $ip = shift @_; my $k = shift @_; my $o; $o = keyg ($ip); if ( $k =~ m/ERRKEY/ ) { return $k; } if ( $o eq $k ) { return 1; } else { return 'ERRKEYV-3'; } } sub keyl ($$$) { ##### # generate and store new random key for next time, replacing any old key ##### my $un = shift @_; # usename my $ut = shift @_; # usertype my $ip = shift @_; # user's current ip address my(@s, $c, $tim, $path, $k); if ( ! $ip ) { syslog ('err', 'DOH! cannot create key without ip'); return 'ERRKEYL-9'; } else { if ( ! open (RAND, $r) ) { # create random key return 'ERRKEYL-1'; } if ( ! read (RAND, $c, $kl) ) { close (RAND); return 'ERRKEYL-2'; } close RAND; @s = unpack ('h*', $c); # present random in hex $c = join ('', @s); $tim = julsec (time); # syslog ('debug', 'key parts: %s, %s, %s %s', $un, $ut, $c, $tim); $k = concatkey ($un, $ut, $c, $tim); # syslog ('debug', 'concatenated key: %s', $k); # save it for later $path = ">" . $dr . "/" . $ip; if ( ! open (KEY, $path) ) { return 'ERRKEYL-3'; } if ( ! print (KEY $k) ) { close (KEY); return 'ERRKEYL-4'; } close (KEY); return $k; } } sub julsec ($) { ##### # julian date epoch this year with secs since midnight gmt, from seconds ##### my $tim = shift @_; my ($jul, $sec); my @bunch; @bunch = gmtime($tim); $jul = $bunch[7]; $sec = $bunch[0] + $bunch[1]*60 + $bunch[2]*3600; return $jul . "." . $sec; } sub secjul ($) { ##### # seconds, from julian date epoch this year with secs since midnight gmt ##### my $js = shift @_; my ($jul, $sec); if ( defined $js ) { ($jul, $sec) = split (/\./, $js); $sec += $jul * 86400; return $sec; } else { return 0; } } sub stalekey ($) { ##### # is the key stale? ##### my $k = shift @_; my ($un, $ut, $r, $js, $window, $now, $left); ($un, $ut, $r, $js) = parsekey ($k); # pull timestamp out of key $window = secjul ($js) + $reapt; # adjust for lifetime $now = secjul (julsec (time)); # current time in seconds, this epoch $left = $window - $now; # the lifetime left if ( $left < 0 ) { return abs ($left); # no life left, return time of death } else { return 0; } } sub namecode ($$) { ##### # encode or decode username ##### my $un = shift @_; # username my $encode = shift @_; # do we encode or decode? if ( defined $un ) { if ( $encode ) { $un = unpack ('h*', $un); } else { $un = pack ('h*', $un); } return $un; } else { return 'NULL'; } } sub keyr ($) { ##### # remove an old set of rules and key, if it exists ##### my $ip = shift @_; my ($removed, $er, $path); if ( $ip ) { $path = $dr . "/" . $ip; # remove rules, log failures but continue to remove db entries if ( $er = ruleset ($ip, 'RMV') ) { syslog ('info', '%s: rule removal failure for %s', $er, $ip); } $removed = unlink($path); if ( $removed ) { syslog ('info', 'removed key file "%s" for IP %s', $path, $ip); } else { syslog ('info', 'no key file "%s" to remove for IP %s', $path, $ip); } return undef; } else { syslog ('err', 'DOH! no key specified for removal'); return 'ERRKEYR-1'; } return undef; } sub concatkey ($$$$) { ##### # build key out of parts ##### my ($un, $ut, $r, $j) = @_; my $k; $un = namecode ($un, 1); $ut = namecode ($ut, 1); $k = join ($sep, $un, $ut, $r, $j); return $k; } sub parsekey ($) { ##### # split key into decoded parts ##### my $k = shift @_; my ($un, $ut, $r, $j); ($un, $ut, $r, $j) = split (/$sep/o, $k); $un = namecode ($un, 0); $ut = namecode ($ut, 0); return ($un, $ut, $r, $j); } sub ruleset ($$) { ##### # add or remove filter/nat rules ##### my $ip = shift @_; my $ar = shift @_; my ($ipr, $ipn, $resp, $command, $rule, $num, $an); if ( $ar eq 'ADD' ) { $ar = ''; $an = '-r'; } else { $ar = '-r'; $an = ''; } foreach $rule ( sort (keys (%rules)) ) { $ipr = $rules{$rule}; $ipr =~ s/USERIP/$ip/; $command = "echo \"$ipr\" | $filt $ar -f -"; # syslog ('debug', 'DEBUG: generated filter line "%s"', $command); $resp = `$command`; # syslog ('debug', 'output from filter application "%s"', $resp); } foreach $rule ( sort (keys (%nats)) ) { $ipn = $nats{$rule}; $ipn =~ s/USERIP/$ip/; $command = "echo \"$ipn\" | $nat $an -f -"; syslog ('debug', 'DEBUG: generated nat line "%s"', $command); $resp = `$command`; # syslog ('debug', 'output from nat application "%s"', $resp); } # clear lingering RDR from state table to accomodate linux resolver # which reuses the same source port for subsequent queries $command = "$nat -F"; $resp = `$command`; # syslog ('info', 'DEBUG: flush system %s returned %s', $command, $resp); return undef; ##################################### # NOTE -- after secure setuid rule addition application is written (and example # rules above moved there), this routine will be redone so it returns an error # code if the rule sets fail. ##################################### return $resp; } sub parmv ($@) { ##### # validate parameters: number, name, and type of each ##### # CGI must be included by routines using this package my $p = shift @_; my @parml = sort (@_); my @parms = sort ($p->param); return undef; # stub: anything goes for now due to 'submit' problem # make sure all expected parameters are present, and no others # if ( @parml != @parms ) { # return 'ERRPARM-1'; # } else { # # still need to write type checking # return undef; # } } sub failbail ($$$) { ##### # heartbeat failure, bailout ##### # CGI must be included by routines using this package my $q = shift @_; my $ip = shift @_; my $message = shift @_; print $q->header(-Refresh=>"$w; URL=$e"), $q->start_html(-title=>"Error Report", -bgcolor=>"white", -text=>"black"), $q->h2("Error Report"), $q->p("The system reports the following error:"), $q->p("$message"), $q->p("You may be able to use the back button on your browser to correct the problem. If that does not help, you might try logging in again from the top."); if ( $DEBUG ) { print $q->hr, $q->p("Here is some debugging information for the test."), $q->p("The argument list on the previous fetch was"), $q->dump, $q->p("You connected from IP address: $ip"), $q->p("The HTTP refresh header on this page tells your browser to fetch URL=$e after $w seconds."); } print $q->end_html; closelog (); exit 0; } 1;