"Fossies" - the Fresh Open Source Software Archive

Member "sshdfilter-1.5.7/source/sshdfilter.pl" (31 May 2010, 47757 Bytes) of package /linux/privat/old/sshdfilter-1.5.7.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Perl source code syntax highlighting (style: standard) with prefixed line numbers and code folding option. Alternatively you can here view or download the uninterpreted source code file. For more information about "sshdfilter.pl" see the Fossies "Dox" file reference documentation.

    1 #!/usr/bin/perl
    2 
    3 use strict 'vars';
    4 
    5 
    6 # sshdfilter   
    7 # greg at csc.liv.ac.uk
    8 # http://www.csc.liv.ac.uk/~greg/sshdfilter/
    9 # http://www.sshdfilter.org/
   10 my $version="1.5.6";
   11 
   12 # Description:
   13 # Picks out sshd log lines such as:
   14 #   Did not receive identification string from 217.147.176.135
   15 #   Illegal user ouoeuoeu from 192.168.9.24
   16 #   Failed password for {illegal,invalid} user oouoeu from 192.168.7.1 port 33562 ssh2
   17 #   Failed password for peter from 192.168.7.1 port 33570 ssh2
   18 # Depending on the action patterns supplied in the configuration file, these messages will
   19 # lead to an instant block, a block after some failures, or no block at all.
   20 
   21 # Looking through old logs, the longest attack from a single IP was 30 mins.
   22 # So the default block times could be reduced.
   23 
   24 my $conffile="/etc/sshdfilterrc";
   25 if( exists $ENV{"SSHDFILTERRC"} ) {
   26    $conffile=$ENV{"SSHDFILTERRC"};
   27 }
   28 
   29 open(CONF,$conffile) || die "No config file $conffile\n";
   30 
   31 
   32 # no longer used, del when it really is no longer used
   33 
   34 my $maxblocktime=3600*24*3;   # how long (seconds) after the last activity from an ip that it can be unblocked
   35 my $maxchances=3;   # how many password guesses of an existing user before going on the blocked list
   36                       # beware, sshd -e -D can report events in double, so this value needs to be twice what it shoud be
   37     
   38 # All these options are overwritten by config file 
   39     
   40 my $chain="SSHD";   # name of sshdfilter chain, just incase you want to run multiple isolated sshdfilters
   41 
   42 # firewall commands to execute. They have available: 
   43 # $ip - IP address of the offending machine.
   44 # $chain - Chain to add this IP to.
   45 # $idx - the next free ipfw index number (if you are using ipfw).
   46 my $firewalladd="";     # add DROP rule
   47 my $firewalldel="";     # delete DROP rule
   48 
   49 my $fwcmdpath="";      # path to iptables, iptables6 or ipfw. Not normally needed.
   50 
   51 my $iptables="iptables";   # iptables or iptables6  (ipfw mode assumes the ipfw command)
   52 
   53 my $iptablesoptions;   # obsolete. If this is set to anything, config file is for < 1.5.4
   54 
   55 my $sshdpath="/usr/sbin/sshd";   # where sshd lives
   56 my $sshdname="sshd";  # the name of the sshd process, only needed to identify the sshd processes
   57                                  # from a none STDIN logsource.
   58 my $logpid=0;              # parent pid of any pids reported in syslog, so we can identify our sshd processes
   59 
   60 my $ip6toip4=1;               # should IPv6 addresses (which are really IPv4 addresses) be converted to IPv4?
   61 
   62 my $debug=1;   # !0 means debug mode, more info is logged, namely the pattern matches.
   63 
   64 my $sanitise="[^-a-zA-Z0-9_]";  # remove everything but these chars from the username (^ means not)
   65 
   66 my $logsource="STDIN"; # where sshd logs come from, sshd -e -D | sshdfilter style, or via a
   67                        # named pipe setup in syslog.conf
   68 
   69 my $ipfwmin=-1;   # if >=0, ipfw will be used instead of iptables
   70 my $ipfwmax=-1;
   71 
   72 # **** Shouldn't need to change anything after this line
   73 
   74 use IO::Handle;
   75 use Sys::Syslog  qw(:DEFAULT setlogsock);
   76 use POSIX 'setsid';
   77 use Fcntl qw(F_GETFL F_SETFL O_NONBLOCK);
   78 use POSIX ":sys_wait_h";
   79 use Socket;    # for name lookups, some PAM messages use the FQDN instead of the IP
   80 
   81 
   82 # sshd log messages
   83 my @txt_pid2ip;
   84 my @txt_pidexit;
   85 my @txt_invalid;
   86 my @txt_failval;
   87 my @txt_accept;
   88 my @txt_noid;
   89 my @txt_quit;
   90 
   91 my @map_pid2ip;
   92 my @map_pidexit;
   93 my @map_invalid;
   94 my @map_failval;
   95 my @map_accept;
   96 my @map_noid;
   97 my @map_quit;
   98 
   99 # user policy, max counter, block time, and corresponding regular expression
  100 my @maxc;
  101 my @btime;
  102 my @userre;
  103 my %maxc_tags;   # INVALID, NOID, DIRTY
  104 my %btime_tags;
  105 
  106 foreach my $i (INVALID, NOID, DIRTY, DEFAULT) {
  107    $btime_tags{$i}=$maxblocktime;
  108    $maxc_tags{$i}=$maxchances;
  109  }
  110 
  111 # Regexs matching IP addresses
  112 my @ipre;
  113 my @ipaction;
  114 
  115 # Email block events policy
  116 my @mailre;
  117 my @mailaction={ 0 };
  118 my $mail;   # mail command line
  119 
  120 # multiline history
  121 my $maxhist=1;
  122 my $curhist=0;
  123 my %loghist;
  124 
  125 # config parser counters
  126 my $line=0;
  127 my $polline=0;
  128 
  129 my $confstate="";
  130 
  131 my $parse_maxc=$maxchances;
  132 my $parse_btime=$maxblocktime;
  133 
  134 while(<CONF>)
  135  {
  136    $line++;
  137    chomp;
  138    s/ #.*//g;
  139    if( /^+s*#/ || /^\s*$/ ) {
  140    } elsif( /^SECTION (.*)/ ) {
  141      $confstate=$1;
  142      $polline=0;
  143      #printf "Setting Conf $_, to $confstate\n";
  144      if ( $confstate eq "EMAILPOLICY" ){ $polline=1; }
  145 
  146    } elsif ( $confstate eq "SSHDLOG" ){
  147 
  148       if( /^\s*(.*)\s*=\s*'(.*)'/ ) {
  149          my $key=$1; my $pat=$2;
  150          #print "extracted >$key<, >$pat<\n";
  151      if ( $key =~ /^msg_/ ) { my $maxlfs=scalar split(/\n/,$pat)+1; if($maxlfs>$maxhist){ $maxhist=$maxlfs; } }
  152          if   ( $key eq "msg_pid_2_ip" ) { push @txt_pid2ip, $pat; }
  153          elsif( $key eq "map_pid_2_ip" ) { push @map_pid2ip, $pat; }
  154          elsif( $key eq "msg_pid_exit" ) { push @txt_pidexit, $pat; }
  155          elsif( $key eq "map_pid_exit" ) { push @map_pidexit, $pat; }
  156          elsif( $key eq "msg_invalid" ) { push @txt_invalid, $pat; }
  157          elsif( $key eq "map_invalid" ) { push @map_invalid, $pat; }
  158          elsif( $key eq "msg_failed_valid" ) { push @txt_failval,$pat; }
  159          elsif( $key eq "map_failed_valid" ) { push @map_failval,$pat; }
  160          elsif( $key eq "msg_accepted_user" ) { push @txt_accept,$pat; }
  161          elsif( $key eq "map_accepted_user" ) { push @map_accept,$pat; }
  162          elsif( $key eq "msg_no_id_string"   ) { push @txt_noid,$pat; }
  163          elsif( $key eq "map_no_id_string"   ) { push @map_noid,$pat; }
  164          elsif( $key eq "msg_quit"   ) { push @txt_quit,$pat; }
  165          elsif( $key eq "map_quit"   ) { push @map_quit,$pat; }
  166          else { print "Line $line, unknown keyword $key\n"; }
  167       } else {
  168          print "Ignoring line $line: %s\n",$_;
  169       }
  170 
  171    } elsif( $confstate eq "USERPOLICY" ){   # USERPOLICY section
  172 
  173       my $ismatch=0;
  174       my $mcount="";  my $t="";  my $tunits="";  my $re="";
  175       #print "Considering $_\n";
  176       if( /^\s*([0-9]+)\s*=\s*((?:DEFAULT|NOID|INVALID|DIRTY|'.+'))/) {
  177          $ismatch=1;
  178          #print "mcount=$1, userre=$2\n";
  179          $mcount=$1;  $re=$2;
  180        } elsif( /^\s*([0-9]*)\s*,\s*([0-9]*)\s*([dhms]?)\s*=\s*((?:DEFAULT|NOID|INVALID|DIRTY|'.+'))/ ) {
  181          $ismatch=1;
  182          #print "mcount=$1, t=$2, tunits=$3, userre=$4\n";
  183          $mcount=$1;  $t=$2;  $tunits=$3;  $re=$4;
  184       }
  185       if( $ismatch ){
  186          #$re =~ s/^'//;   $re =~ s/'$//;
  187          $re =~ s/^'(.*)'$/$1/;
  188          if( $mcount =~ /[0-9]/ ) { $maxc[$polline]=$mcount; } else { $maxc[$polline]=$parse_maxc; }
  189          if( $t =~ /[0-9]/ ){
  190             $btime[$polline]=$t;
  191             if( $tunits eq "d" ){ $btime[$polline]*=3600*24; } 
  192             if( $tunits eq "h" ){ $btime[$polline]*=3600; } 
  193             if( $tunits eq "m" ){ $btime[$polline]*=60; } 
  194             if( $tunits eq "s" ){ } 
  195             if( $btime[$polline] < 5 ){ print "Warning, short block time of $btime[$polline] seconds. "; print "Line $line: $_\n"; }
  196          } else { $btime[$polline]=$parse_btime; }
  197          if( $re =~ /^.+$/ ) { 
  198             $userre[$polline]=$re;
  199          } else {
  200             print "Warning, parser broke, line $line, $_\n$re\n";
  201             delete $btime[$polline];   delete $maxc[$polline];  delete $userre[$polline];
  202             $polline--;
  203          }
  204 
  205          # deal with NOID|INVALID|DIRTY and DEFAULTs effect on them
  206          if( $re eq "DEFAULT" ) {
  207             if( $mcount =~ /[0-9]/ ) { $parse_maxc=$maxc[$polline]; }
  208             if(  $t =~ /[0-9]/ ){ $parse_btime=$btime[$polline]; }
  209           } elsif( $re eq "NOID" || $re eq "INVALID" || $re eq "DIRTY" ) {
  210             if(  $mcount =~ /[0-9]/ ) { $maxc_tags{$re}=$maxc[$polline]; } else { $maxc_tags{$re}=$parse_maxc; }
  211             if(  $t =~ /[0-9]/ ){ $btime_tags{$re}=$btime[$polline]; } else { $btime_tags{$re}=$parse_btime; }
  212             if( $re eq "NOID" || $re eq "DIRTY" ) {
  213                delete $btime[$polline];   delete $maxc[$polline];  delete $userre[$polline];
  214                $polline--;
  215              }
  216           }
  217          $polline++;
  218         
  219       } else {
  220          print "USERPOLICY section, bad syntax, line $line: $_\n";
  221       }
  222 
  223    } elsif( $confstate eq "IPPOLICY" ){
  224 
  225      if( /^([\+\-])'(.*)'/ ) {
  226          push @ipre,$2;
  227          #print "Adding to whitelist $1, $2\n";
  228          if( $1 eq "+" ) { push @ipaction,0; }
  229          if( $1 eq "-" ) { push @ipaction,1; }
  230      } else {
  231        printf "Bad syntax, line $line: %s\n",$_; }
  232 
  233    } elsif( $confstate eq "EMAILPOLICY" ){
  234 
  235      if( /^([\+\-])((?:DEFAULT|DIRTY|INVALID|NOID|'.+'))/ ) {
  236          my $pos=$#mailaction+1;
  237          my $yy=$1;  my $re=$2;
  238          $re =~ s/^'(.*)'$/$1/;
  239          if( $re eq "DEFAULT" ) { $pos=0; }
  240          if( $yy eq "+" ){ $mailaction[$pos]=1; } else { $mailaction[$pos]=0; }
  241          $mailre[$pos]=$re;
  242      } else {
  243        printf "Bad syntax, line $line: %s\n",$_; }
  244 
  245    } elsif( $confstate eq "OPTIONS" ){
  246 
  247      if(    /^sanitise\s*=\s*'(.*)'/ ) { $sanitise=$1; }
  248      elsif( /^maxblocktime\s*=\s*(.*)/ ) { $maxblocktime=eval $1; }
  249      elsif( /^maxchances\s*=\s*(.*)/ ) { $maxchances=eval $1; }
  250      elsif( /^firewalladd\s*=\s*'(.*)'/ ) { $firewalladd=$1; }
  251      elsif( /^firewalldel\s*=\s*'(.*)'/ ) { $firewalldel=$1; }
  252      elsif( /^fwcmdpath\s*=\s*'(.*)'/ ) { $fwcmdpath="$1/"; }
  253      elsif( /^ipfwmin\s*=\s*([0-9]+)/ ) { $ipfwmin=$1; }
  254      elsif( /^ipfwmax\s*=\s*([0-9]+)/ ) { $ipfwmax=$1; }
  255      elsif( /^iptablesoptions\s*=\s*'(.*)'/ ) { $iptablesoptions=$1; }
  256      elsif( /^chain\s*=\s*'([^ ]*)'/ ) { $chain=$1; }
  257      elsif( /^mail\s*=\s*'(.*)'/ ) { $mail=$1; }
  258      elsif( /^ip6toip4\s*=\s*([01])/ ) { $ip6toip4=$1; }
  259      elsif( /^logsource\s*=\s*'([^ ]*)'/ ) { $logsource=$1; }
  260      elsif( /^sshdpath\s*=\s*'([^ ]*)'/ ) { $sshdpath=$1; }
  261      elsif( /^sshdname\s*=\s*'(.*)'/ ) { $sshdname=$1; }
  262      elsif( /^logpid\s*=\s*(.*)/ ) { $logpid=$1; }
  263      elsif( /^debug\s*=\s*([01234])/ ) { $debug=$1; }
  264      else { print "Unknown option line $line: $_\n"; }
  265 
  266    } else { printf "Ignoring line $line: %s\n",$_; }
  267  } # For each line in the configuration file
  268 
  269 close CONF;
  270 
  271 
  272 # apply some extra option logic
  273 if( $ip6toip4 == 0 ) {
  274    $iptables="ip6tables";
  275  }
  276     
  277 
  278 # read in ARGV if needed, lets the caller pass a pid value (well, really a parent pid)
  279 if( $logsource ne "STDIN" ) {
  280    for(my $idx=0; $idx<=$#ARGV; $idx++) {
  281        if( $ARGV[$idx] =~ /logpid\s*=\s*(.*)/ ) { $logpid=$1; }
  282     }
  283  }
  284 
  285 if( $firewalladd eq "" ) { print "no firewalladd command defined\n"; exit 1; }
  286 if( $firewalldel eq "" ) { print "no firewalldel command defined\n"; exit 1; }
  287 
  288 if( $ipfwmin!=-1 && $ipfwmin>=$ipfwmax ) { print "ipfwmin greater than ipfwmax\n"; exit 1; }
  289 if( $ipfwmin!=-1 && $ipfwmin>65535 ) { print "ipfwmin greater than allowed by ipfw\n"; exit 1; }
  290 if( $ipfwmin!=-1 && $ipfwmax>65535 ) { print "ipfwmax greater than allowed by ipfw\n"; exit 1; }
  291 if( $ipfwmin==-1 && $ipfwmax!=-1 ) { print "ipfwmax defined, but ipfwmin still undefined\n"; exit 1; }
  292 
  293 #if( $#txt_pid2ip ==-1 ) { print "msg_pid_2_ip undefined\n"; exit 1; }
  294 #if( $#map_pid2ip ==-1 ) { print "map_pid_2_ip undefined\n"; exit 1; }
  295 #if( $#txt_pidexit ==-1 ) { print "msg_pid_exit undefined\n"; exit 1; }
  296 #if( $#map_pidexit ==-1 ) { print "map_pid_exit undefined\n"; exit 1; }
  297 if( $#txt_invalid ==-1 ) { print "msg_invalid undefined\n"; exit 1; }
  298 if( $#map_invalid ==-1 ) { print "map_invalid undefined\n"; exit 1; }
  299 if( $#txt_failval ==-1 ) { print "msg_failed_valid undefined\n"; exit 1; }
  300 if( $#map_failval ==-1 ) { print "map_failed_valid undefined\n"; exit 1; }
  301 if( $#txt_accept ==-1 ) { print "msg_accepted_user undefined\n"; exit 1; }
  302 if( $#map_accept ==-1 ) { print "map_accepted_user undefined\n"; exit 1; }
  303 if( $#txt_noid ==-1 ) { print "msg_no_id_string undefined\n"; exit 1; }
  304 if( $#map_noid ==-1 ) { print "map_no_id_string undefined\n"; exit 1; }
  305 if( $#txt_quit ==-1 ) { print "msg_quit undefined\n"; exit 1; }
  306 if( $#map_quit ==-1 ) { print "map_quit undefined\n"; exit 1; }
  307 
  308 if( $#txt_pid2ip != $#map_pid2ip ) { print "#msg_pid_2_ip != #map_pid_2_ip, $#txt_pid2ip != $#map_pid2ip\n"; exit 1; }
  309 if( $#txt_pidexit != $#map_pidexit ) { print "#msg_pid_exit != #map_pid_exit, $#txt_pidexit != $#map_pidexit\n"; exit 1; }
  310 if( $#txt_invalid != $#map_invalid ) { print "#msg_invalid != #map_invalid, $#txt_invalid != $#map_invalid\n"; exit 1; }
  311 if( $#txt_failval != $#map_failval ) { print "#msg_failed_valid != #map_failed_valid, $#txt_failval != $#map_failval\n"; exit 1; }
  312 if( $#txt_accept != $#map_accept ) { print "#msg_accepted_user != #map_accepted_user, $#txt_accept != $#map_accept\n"; exit 1; }
  313 if( $#txt_noid != $#map_noid ) { print "#msg_no_id_string != #map_no_id_string, $#txt_noid != $#map_noid\n"; exit 1; }
  314 if( $#txt_quit != $#map_quit ) { print "#msg_quit != #map_quit, $#txt_quit != $#map_quit\n"; exit 1; }
  315 
  316 foreach my $i ("DEFAULT", "NOID", "DIRTY", "INVALID") {
  317    if( ! defined $maxc_tags{$i} ) { $maxc_tags{$i}=$maxc_tags{"DEFAULT"}; }
  318    if( ! defined $btime_tags{$i} ) { $btime_tags{$i}=$btime_tags{"DEFAULT"}; }
  319  }
  320 
  321 
  322 
  323 # Daemonise like sshd, better fits regular startup scripts. Function nicked from perlipc3 man page
  324 # Have the parent wait until the other deamons have started before letting the original parent quit
  325 pipe(CONSOLEPROC_RDR,  CONSOLEPROC);
  326 CONSOLEPROC->autoflush(1);
  327 
  328 chdir '/'               or die "Can't chdir to /: $!";
  329 open STDIN, '/dev/null' or die "Can't read /dev/null: $!";
  330 open STDOUT, '>/dev/null' or die "Can't write to /dev/null: $!";
  331 #open STDOUT, '>/tmp/sshdfilter.out' or die "Can't write to /tmp/sshdfilter.out: $!";
  332 
  333 my $daemonpid=0;
  334 defined($daemonpid = fork) or die "Can't fork: $!";
  335 if ($daemonpid>0) {  # wait on handle for some sign until all the children have been created
  336   close CONSOLEPROC;
  337   my $res=1;
  338   while(<CONSOLEPROC_RDR>){
  339      if(/Exit ok!/) {
  340         $res=0;
  341       }
  342    }
  343   close CONSOLEPROC_RDR;
  344   exit $res;
  345 }
  346 setsid                  or die "Can't start a new session: $!";
  347 open STDERR, '>&STDOUT' or die "Can't dup stdout: $!";
  348 
  349 
  350 
  351 # fork another process, just so we can have a separate instance of 
  352 # openlog() for sshd. Code nicked from PerlIPC man page
  353 my $SSHDLOGGER = IO::Handle->new();
  354 my $pid;
  355 if( $logsource eq "STDIN" ) {
  356    pipe(CHILD_RDR,  $SSHDLOGGER);
  357    $SSHDLOGGER->autoflush(1);
  358 
  359    $pid=fork;
  360    if ( $pid == 0 ) {   
  361       close $SSHDLOGGER;
  362       close CONSOLEPROC;
  363 
  364       chdir '/'               or die "Can't chdir to /: $!";
  365       open STDIN, '/dev/null' or die "Can't read /dev/null: $!";
  366       open STDOUT, '>/dev/null';
  367 
  368       # open logging specific to sshd
  369       setlogsock('unix');
  370       openlog 'sshd', 'pid,ndelay', 'authpriv';
  371 
  372       while(<CHILD_RDR>) # output all received lines to this log
  373        {
  374          chomp;
  375          syslog 'authpriv|notice',"$_";
  376        }
  377       closelog();
  378       close CHILD_RDR;
  379       exit 0;
  380     }
  381    die "cannot fork: $!" unless defined $pid;
  382    close CHILD_RDR;
  383  }
  384 
  385 
  386 # open logging for sshdfilter
  387 setlogsock('unix');
  388 openlog 'sshdfilt', 'pid,ndelay', 'authpriv';
  389 
  390 
  391 # check user has setup IP tables (or ipfw)
  392 
  393 if( $ipfwmin==-1 ){
  394    # iptables -L -n | grep "^SSHD *tcp"
  395    # SSHD       tcp  --  0.0.0.0/0            0.0.0.0/0          tcp dpt:22 
  396    open(TABCHECK,"$iptables -L -n | grep \"^$chain *tcp\"|") || die("couldn't check $iptables");
  397    my $aline=<TABCHECK>;
  398    close(TABCHECK);
  399    if( $aline !~ /^$chain/ ) {
  400       syslog 'authpriv|warning',"$iptables is missing $chain redirect, sshdfilter rendered useless.";
  401     }
  402 
  403    # iptables -L SSHD -n | grep "Chain SSHD"
  404    # Chain SSHD (1 references)
  405    open(TABCHECK,"$iptables -L $chain -n | grep \"^Chain $chain\"|") || die("couldn't check $iptables");
  406    $aline=<TABCHECK>;
  407    close(TABCHECK);
  408    if( $aline !~ /^Chain $chain/ ) {
  409        syslog 'authpriv|warning',"$iptables is missing $chain chain, sshdfilter rendered useless.";
  410     }
  411  }
  412 else
  413  {
  414    # Don't check ipfw configuration, although I suggest a skip-these-rules rule, it isn't
  415    # necessary, so the only additions could be block events as they happen.
  416  }
  417 
  418 
  419 # run sshd
  420 if( $logsource eq "STDIN" ) {
  421    syslog 'authpriv|notice',"sshdfilter $version starting up, running sshd proper.";
  422  } else {
  423    syslog 'authpriv|notice',"sshdfilter $version starting up.";
  424  }
  425 
  426 # maybe log the setup
  427 if( $debug ) {
  428    syslog 'authpriv|notice', "DB:OPTIONS";
  429    syslog 'authpriv|notice',"DB: maxblocktime=$maxblocktime";
  430    syslog 'authpriv|notice',"DB: maxchances=$maxchances";
  431    syslog 'authpriv|notice',"DB: firewalladd=$firewalladd";
  432    syslog 'authpriv|notice',"DB: firewalldel=$firewalldel";
  433    syslog 'authpriv|notice',"DB: fwcmdpath=$fwcmdpath";
  434    syslog 'authpriv|notice',"DB: sshdpath=$sshdpath";
  435    syslog 'authpriv|notice',"DB: sshdname=$sshdname";
  436    syslog 'authpriv|notice',"DB: logpid=$logpid";
  437    syslog 'authpriv|notice',"DB: ip6toip4=$ip6toip4";
  438    syslog 'authpriv|notice',"DB: iptables command=$iptables";
  439    syslog 'authpriv|notice',"DB: iptables chain=$chain";
  440    syslog 'authpriv|notice',"DB: ipfwmin=$ipfwmin";
  441    syslog 'authpriv|notice',"DB: ipfwmax=$ipfwmax";
  442    syslog 'authpriv|notice',"DB: debug=$debug";
  443    syslog 'authpriv|notice',"DB: logsource=$logsource";
  444    syslog 'authpriv|notice',"DB: sshd args=@ARGV";
  445    syslog 'authpriv|notice',"DB: sanitise=$sanitise";
  446    syslog 'authpriv|notice',"DB: mail=$mail";
  447    syslog 'authpriv|notice',"DB: maxhist=$maxhist";
  448 
  449    my $p=sprintf "DB:USER POLICY entries=%d",$#userre+1;   syslog 'authpriv|notice',$p;
  450    for(my $i=0;$i<=$#userre;$i++) {
  451       syslog 'authpriv|notice',"DB: $i, $maxc[$i], $btime[$i], $userre[$i]";
  452     }
  453    foreach my $i ("DEFAULT", "NOID", "DIRTY", "INVALID") {
  454       syslog 'authpriv|notice',"DB: $maxc_tags{$i}, $btime_tags{$i}, tag=$i";
  455     }
  456    
  457 
  458    $p=sprintf "DB:IP POLICY entries=%d",$#ipre+1;    syslog 'authpriv|notice',$p;
  459    for(my $i=0;$i<=$#ipre;$i++) {
  460       syslog 'authpriv|notice',"DB: $i, action=$ipaction[$i], re=$ipre[$i]";
  461     }
  462 
  463    $p=sprintf "DB:EMAIL POLICY entries=%d",$#mailaction+1;    syslog 'authpriv|notice',$p;
  464    for(my $i=0;$i<=$#mailaction;$i++) {
  465       syslog 'authpriv|notice',"DB: $i, action=$mailaction[$i], re=$mailre[$i]";
  466     }
  467 
  468    my $isdefined=0;
  469 
  470    for(my $idx=0; $idx<=$#txt_pid2ip; $idx++) 
  471     { syslog 'authpriv|notice',"DB: msg_pid_2_ip[$idx]=$txt_pid2ip[$idx]"; $isdefined++; }
  472    for(my $idx=0; $idx<=$#map_pid2ip; $idx++) 
  473     { syslog 'authpriv|notice',"DB: map_pid_2_ip[$idx]=$map_pid2ip[$idx]"; $isdefined++; }
  474    for(my $idx=0; $idx<=$#txt_pidexit; $idx++) 
  475     { syslog 'authpriv|notice',"DB: msg_pid_exit[$idx]=$txt_pidexit[$idx]"; $isdefined++; }
  476    for(my $idx=0; $idx<=$#map_pidexit; $idx++) 
  477     { syslog 'authpriv|notice',"DB: map_pid_exit[$idx]=$map_pidexit[$idx]"; $isdefined++; }
  478    for(my $idx=0; $idx<=$#txt_invalid; $idx++) 
  479     { syslog 'authpriv|notice',"DB: msg_invalid[$idx]=$txt_invalid[$idx]"; $isdefined++; }
  480    for(my $idx=0; $idx<=$#map_invalid; $idx++) 
  481     { syslog 'authpriv|notice',"DB: map_invalid[$idx]=$map_invalid[$idx]"; $isdefined++; }
  482    for(my $idx=0; $idx<=$#txt_failval; $idx++) 
  483     { syslog 'authpriv|notice',"DB: msg_failed_valid[$idx]=$txt_failval[$idx]"; $isdefined++; }
  484    for(my $idx=0; $idx<=$#map_failval; $idx++) 
  485     { syslog 'authpriv|notice',"DB: map_failed_valid[$idx]=$map_failval[$idx]"; $isdefined++; }
  486    for(my $idx=0; $idx<=$#txt_accept; $idx++) 
  487     { syslog 'authpriv|notice',"DB: msg_accepted_user[$idx]=$txt_accept[$idx]"; $isdefined++; }
  488    for(my $idx=0; $idx<=$#map_accept; $idx++) 
  489     { syslog 'authpriv|notice',"DB: map_accepted_user[$idx]=$map_accept[$idx]"; $isdefined++; }
  490    for(my $idx=0; $idx<=$#txt_noid; $idx++) 
  491     { syslog 'authpriv|notice',"DB: msg_no_id_string[$idx]=$txt_noid[$idx]"; $isdefined++; }
  492    for(my $idx=0; $idx<=$#map_noid; $idx++) 
  493     { syslog 'authpriv|notice',"DB: map_no_id_string[$idx]=$map_noid[$idx]"; $isdefined++; }
  494    for(my $idx=0; $idx<=$#txt_quit; $idx++) 
  495     { syslog 'authpriv|notice',"DB: msg_quit[$idx]=$txt_quit[$idx]"; $isdefined++; }
  496    for(my $idx=0; $idx<=$#map_quit; $idx++) 
  497     { syslog 'authpriv|notice',"DB: map_quit[$idx]=$map_quit[$idx]"; $isdefined++; }
  498 
  499    if( $isdefined<10 ) {
  500       syslog 'authpriv|err', "SSHDLOG section of /etc/sshdfilterrc is missing $isdefined regexs";
  501       syslog 'authpriv|err', "This probably means either you are using a pre 1.5 verison of the";
  502       syslog 'authpriv|err', "sshdfilterrc file, which will not work, Or, you forgot to append";
  503       syslog 'authpriv|err', "the appropriate pattern file from patterns/ to your /etc/sshdfilterrc.";
  504     }
  505  } # end of $debug flag
  506 
  507 my $SSHDHANDLE = IO::Handle->new();
  508 
  509 if( $logsource ne "STDIN" )
  510  {
  511    open($SSHDHANDLE,$logsource) || syslog 'authpriv|err', "Tried to open a named pipe $logsource for sshd log messages, but failed";
  512  }
  513 else
  514  {
  515    # fork for sshd process. open() blocked daemonise code. fork()/exec() doesn't. 
  516    # Code nicked from PerlIPC man page
  517    pipe($SSHDHANDLE,  SSHDHAND_WTR);
  518    SSHDHAND_WTR->autoflush(1);
  519 
  520    my $sshdpid=fork;
  521    if ( $sshdpid == 0 ) {   
  522        close $SSHDHANDLE;
  523        close CONSOLEPROC;
  524        close $SSHDLOGGER;
  525 
  526        chdir '/'               or die "Can't chdir to /: $!";
  527        open STDIN, '/dev/null' or die "Can't read /dev/null: $!";
  528        open STDOUT, ">&SSHDHAND_WTR"    or die "Can't dup SSHDHAND_WTR: $!";
  529        open STDERR, ">&SSHDHAND_WTR"    or die "Can't dup SSHDHAND_WTR: $!";
  530 
  531        exec "$sshdpath", @ARGV, "-e","-D";
  532 
  533        syslog 'authpriv|err',"couldn't run $sshdpath";
  534        die("couldn't run $sshdpath");
  535 
  536        close SSHDHAND_WTR;
  537        exit 0;
  538     }
  539 
  540    if( !defined $sshdpid ){  syslog 'authpriv|err',"cannot fork: $!";
  541                              die "cannot fork: $!";
  542                           }
  543    close SSHDHAND_WTR;
  544 
  545    sleep 1;
  546 
  547    # attempt to get some better sshd status information
  548    my $res=waitpid( $sshdpid, WNOHANG );
  549    if( $res!=0 ) {
  550       syslog 'authpriv|err',"ran sshd and waited one second, it died and said: status=$res error=$?";
  551     }
  552  } # If using stdin for sshd log messages, and we are running sshd ourselves
  553 
  554 
  555 if( $ipfwmin==-1 ){
  556    syslog 'authpriv|notice',"Flushing $chain chain";
  557    loggingsystem("${fwcmdpath}$iptables -F $chain");
  558  } else {
  559    syslog 'authpriv|notice',"Deleting rule numbers $ipfwmin to $ipfwmax";
  560    open(RULES,"${fwcmdpath}ipfw show |") || syslog 'authpriv|err',"Couldn't flush old rules, command failed.";
  561    if( RULES ){
  562       while(<RULES>){
  563          chomp;
  564          my ($idx,$therest)=split(/\s/,$_);
  565          if( $idx>=$ipfwmin && $idx<=$ipfwmax ){
  566             loggingsystem("${fwcmdpath}ipfw delete $idx");
  567           }
  568        }
  569       close RULES;
  570     }
  571  }
  572 
  573 # write a pid
  574 open(THISPID,">/var/run/sshdfilter.pid.$chain") || die("couldn't write pid file");
  575 printf THISPID "%d\n",$$;
  576 close(THISPID);
  577 
  578 
  579 # Run the given command, log any problems as an error
  580 sub loggingsystem {
  581    my $command=shift;
  582    if (system($command) != 0) {
  583       my $res=$?;
  584       if( $res>255 ) { $res = $res >> 8; }
  585       syslog 'authpriv|err',"system(\"$command\"); failed: $res";
  586       syslog 'authpriv|err',"Suggest trying the same command in a shell.";
  587     }
  588  }
  589 
  590 # Option to email a user(s) when a block event occurs.
  591 # We also fork to send from a child process, incase the sending
  592 # of mail hangs.
  593 sub emailonblock {
  594    my $user=shift;
  595    my $ip=shift;
  596    my $event=shift;
  597    my $releasetime=shift;
  598 
  599    if( defined $mail ){
  600 
  601       my $shouldmail=$mailaction[0];
  602       for(my $i=1;$i<=$#mailaction;$i++) {
  603          if( $user =~ /$mailre[$i]/ ) {
  604             $shouldmail=$mailaction[$i];
  605             last;
  606           }
  607        }
  608 
  609       if( $shouldmail == 0 ) { return; }
  610 
  611       my $pid=fork;
  612       if ( $pid == 0 ) {
  613 
  614          chdir '/'               or die "Can't chdir to /: $!";
  615          open STDIN, '/dev/null' or die "Can't read /dev/null: $!";
  616          open STDOUT, '>/dev/null';
  617 
  618          my $mailcommand;
  619          my $mailcomm=sprintf('$mailcommand="%s"',"$mail");
  620          if($debug){ syslog 'authpriv|debug',"DB:pre mail command is $mailcomm"; }
  621          eval "$mailcomm";
  622          if($debug){ syslog 'authpriv|debug',"DB:post mail command is $mailcommand"; }
  623          open(MAILPROC,"| $mailcommand") || syslog 'authpriv|warning',"sshdfilter couldn't email block event";
  624          if( MAILPROC ){
  625             my $ltime=localtime($releasetime);
  626             printf MAILPROC "IP $ip was blocked. $event\n";
  627             printf MAILPROC "Will remove block at %s.\n",$ltime;
  628             close( MAILPROC );
  629             exit 0;
  630          }
  631 
  632       die "cannot fork: $!" unless defined $pid;
  633 
  634       }
  635    }
  636 }
  637 
  638 # State tracking hashes
  639 my %chances;    # number of chances an ip has had. This has a purge time just 
  640                 # like blocks, so we wont build up a list of stale login attempts.
  641 my %expiretime;  # indexed on ip, storing a time a block should be removed.
  642 my %explain;   # hash indexed on ip, storing a reason for the the block event. 
  643                # This also flags a block event is active.
  644 my %pid2ip;  # hash indexed on a pid, mapping to an ip. Supports the dropbear ssh 
  645              # server, which gives only the pid as a common element on each line.
  646 
  647 my %pidtime; # cache parent pid lookups, to avoid the race condition of looking up
  648              # the parent of a process that has just exited. Still doesn't catch all
  649              # the scenarios, but, nothing will.
  650 
  651 my %ipfwindex;   # IP to ipfw index number
  652 my %ipfwfree;    # ipfw index numbers that are now free, but aren't part of the continuous set of free numbers
  653 my $ipfwcur=$ipfwmin;  # lowest free ipfw index of the continous set
  654 
  655 #   my $numipfwfree=keys %ipfwfree;
  656 #   my $numipfwindex=keys %ipfwindex;
  657 #   my $numexpiretime=keys %expiretime;
  658 #   printf("1 current=%d of %d (%d-%d), free slots=%d, blocks=%d/%d, action %s, ip=%s\n",$ipfwcur,$ipfwmax-$ipfwcur,$ipfwmin,$ipfwmax,$numipfwfree,$numipfwindex,$numexpiretime,$action,$ip);
  659 
  660 sub deldrop {
  661    my $ip=shift;  # IP address to remove from drop list
  662    if( defined $explain{$ip} ) {
  663       my $reason=$explain{$ip};
  664       delete $explain{$ip};
  665       syslog 'authpriv|notice',"Cancelled $reason block from $ip";
  666       my $fwcommand;
  667       my $fwcomm=sprintf('$fwcommand="%s"',"$fwcmdpath$firewalldel");
  668       if($debug){ syslog 'authpriv|debug',"DB: pre fw del command is $fwcomm"; }
  669       if( $ipfwmin==-1 ) {
  670          eval "$fwcomm";
  671          if($debug){ syslog 'authpriv|debug',"DB: post fw del command is $fwcommand"; }
  672          loggingsystem("$fwcommand");
  673        } else {
  674          my $idx=$ipfwindex{$ip};
  675          eval "$fwcomm";
  676          if($debug){ syslog 'authpriv|debug',"DB: post fw del command is $fwcommand"; }
  677          loggingsystem("$fwcommand");
  678          delete $ipfwindex{$ip};
  679          delete $expiretime{$ip};
  680          $ipfwfree{$idx}=1;
  681        }
  682       delete $expiretime{$ip};
  683       delete $chances{$ip};
  684     }
  685     
  686  }
  687 
  688 sub adddrop {
  689    my $ip=shift;  # IP address to block
  690    my $fwcommand;
  691    my $fwcomm=sprintf('$fwcommand="%s"',"$fwcmdpath$firewalladd");
  692    if($debug){ syslog 'authpriv|debug',"DB: pre fw add command is $fwcomm"; }
  693 
  694    if( $ipfwmin==-1 ) {  # iptables option, simple compared to ipfw and its index numbers.
  695       eval "$fwcomm";
  696       if($debug){ syslog 'authpriv|debug',"DB: post fw add command is $fwcommand"; }
  697       loggingsystem("$fwcommand");
  698     } else {
  699       my $numipfwfree=keys %ipfwfree;
  700       if( $numipfwfree > 0 )  # pick an index from the free pool
  701        {
  702          my $lowestidx=$ipfwcur;
  703          foreach my $idx (keys %ipfwfree) {
  704             if( $idx<$lowestidx ) { $lowestidx=$idx; }
  705           }
  706          delete $ipfwfree{$lowestidx};
  707 
  708          $ipfwindex{$ip}=$lowestidx;
  709      my $idx=$lowestidx;
  710          eval "$fwcomm";
  711          if($debug>2){ syslog 'authpriv|notice', "DB: recycling ipfw index $lowestidx"; }
  712        }
  713       else
  714        { # pick an index from the continous block, or forced to delete the oldest current block
  715          my $idx;
  716          if( $ipfwcur<=$ipfwmax )
  717           {
  718         $idx=$ipfwcur;
  719             $ipfwindex{$ip}=$ipfwcur++;
  720             if($debug>2){ syslog 'authpriv|notice', "DB: adding new ipfw index $ipfwindex{$ip}"; }
  721           }
  722          else
  723           {
  724         my $numexpiretime=keys %expiretime;
  725         
  726             foreach my $oldip (sort { $expiretime{$a} <=> $expiretime{$b} } keys %expiretime) {
  727            if( $oldip ne $ip ) { # skip small chance of trying to remove self
  728                   my $idx=$ipfwindex{$oldip};
  729                   if( $debug>2 ){ syslog 'authpriv|notice', "DB: Recycling ip $oldip with index $idx"; }
  730                   if(! defined $ipfwindex{$oldip} ){ syslog 'authpriv|err', "Logic error, oldip $oldip not found."; }
  731                   deldrop $oldip;
  732                   delete $ipfwfree{$idx};
  733                   $ipfwindex{$ip}=$idx;
  734                   last;
  735         }
  736              }
  737         $idx=$ipfwindex{$ip};
  738             if($debug>2){ syslog 'authpriv|notice', "DB: prematurely recycling oldest ipfw index $ipfwindex{$ip}"; }
  739           }
  740          eval "$fwcomm";
  741        }
  742       if($debug){ syslog 'authpriv|debug',"DB: post fw add command is $fwcommand"; }
  743       loggingsystem("$fwcommand");
  744     }
  745  }
  746 
  747 # about to enter main loop, so signal parent process to quit. 
  748 # We never really know if sshd started correctly, so we always exit with a success.
  749 printf CONSOLEPROC "Exit ok!\n";
  750 close CONSOLEPROC;
  751 
  752 # enable non-blocking read operation
  753 my $flags = fcntl($SSHDHANDLE, F_GETFL, 0) or die "Can't get flags for the socket: $!\n";
  754 $flags = fcntl($SSHDHANDLE, F_SETFL, $flags | O_NONBLOCK) or die "Can't set flags for the socket: $!\n";
  755 
  756 # buffer input from $SSHDHANDLE, read() may get more than one line, or a partial line
  757 my $next;
  758 my $this;
  759 
  760 my $shouldexit=0;
  761 my $purgetime=undef;
  762 
  763 sub sig_exit { $shouldexit=1; }
  764 
  765 local $SIG{TERM} = 'sig_exit';
  766 
  767 
  768 while( !$shouldexit)
  769  {
  770    my $rin ='';
  771    vec($rin,fileno($SSHDHANDLE),1) = 1;
  772    my $ein = $rin ;
  773    my $res=select($rin,undef,$ein,$purgetime);
  774 
  775    #printf "Came back with %d\n",$res;
  776 
  777    my $ttime=time;
  778 
  779    # reap any zombies (which are created by sending emails)
  780    my $kid;
  781    do {
  782         $kid = waitpid(-1, WNOHANG);
  783     } until $kid <= 0;
  784    $purgetime=undef;
  785    foreach my $ip (sort { $expiretime{$a} <=> $expiretime{$b} } keys %expiretime) {
  786       if( $expiretime{$ip} < $ttime ) {
  787          if( defined $explain{$ip} ) {
  788             my $reason=$explain{$ip};
  789             syslog 'authpriv|notice',"Cancelled $reason block from $ip";
  790             deldrop $ip;
  791             delete $explain{$ip};
  792           }
  793          delete $expiretime{$ip};
  794          delete $chances{$ip};
  795        }
  796       elsif( ! defined $purgetime ) {
  797          $purgetime=$expiretime{$ip}-$ttime+1;
  798          last
  799        }
  800       else {
  801          last;
  802        }
  803     } # end of iptables purge
  804 
  805    # trim pid cache, for most people this will always be empty
  806    foreach my $p (sort { $pidtime{$a} <=> $pidtime{$b} } keys %pidtime) {
  807       if( $pidtime{$p} < $ttime ) {
  808          delete $pidtime{$p};
  809        } elsif( !defined $purgetime || $purgetime>$pidtime{$p}-$ttime ) {
  810           $purgetime=$pidtime{$p}-$ttime+1;
  811           last;
  812        } else {
  813           last;
  814        }
  815     }
  816 
  817    if( $res >0 )
  818     {
  819       my $firstread=1;
  820       while ( 1 )
  821        {
  822          my $in;
  823          my $num=sysread($SSHDHANDLE, $in, 1024);
  824          #printf "sysread said $num and $!\n";
  825          if( $num==0 && $firstread==1 && $logsource eq "STDIN" )  # detect eof
  826           {
  827             $shouldexit=1;
  828             last;
  829           }
  830          $firstread=0;
  831          if( $num>0 )  # we have input, add it to remains of any previous input and pull out complete lines
  832           {
  833             #printf "Got >$in<\n";
  834             $in="$next$in";
  835             while( $in =~ /\n/ )
  836              {
  837                ($this, $next)=split(/\n/,$in,2);
  838                $in=$next;
  839 
  840                chomp $this;
  841                $this =~ tr/\n\r//d;   
  842                if( $logsource eq "STDIN" ) {
  843                   handlesshdoutput($this);
  844                 } elsif ( $this =~ / $sshdname\[([0-9]+)\]: (.*)/ ) {
  845                   my $lpid=$1;
  846                   my $fline=$2;
  847                   #syslog 'authpriv|notice',"lpid=$lpid, logpid=$logpid, fline=$fline";
  848                   if( $logpid<=0 || $lpid==$logpid ) {
  849                      handlesshdoutput($fline);
  850                    } elsif( $pidtime{$lpid}>$ttime ) {
  851                      $pidtime{$lpid}+=720;
  852                      #syslog 'authpriv|notice',"Using cache";
  853                      handlesshdoutput($fline);
  854                    } elsif( open(PPID,"/proc/$lpid/stat") ){ # does 8500 checks/sec on my 2.4.32 P3 660
  855                      my $ppid=<PPID>;
  856                      close PPID;
  857                      chomp $ppid;
  858                      $ppid=~ s/^[0-9]+ \($sshdname\) [A-Z] ([0-9]+) .*$/$1/;
  859                      #syslog 'authpriv|notice',"ppid=$ppid";
  860                      if( $ppid == $logpid ) {
  861                         $pidtime{$lpid}=$ttime+720;
  862                         handlesshdoutput($fline);
  863                       }
  864                    }
  865                 }
  866            
  867                #printf "ALine is <$this>\n";
  868              }
  869             #printf "Done processing buffer\n";
  870           }
  871          elsif( defined $num )  # eof for named pipes
  872           {
  873             if( $logsource ne "STDIN" )
  874              {
  875                close $SSHDHANDLE;
  876                open($SSHDHANDLE,$logsource) || syslog 'authpriv|err', "Tried to reopen a named pipe $logsource for sshd log messages, but failed";
  877                my $flags = fcntl($SSHDHANDLE, F_GETFL, 0) or die "Can't get flags for the socket: $!\n";
  878                $flags = fcntl($SSHDHANDLE, F_SETFL, $flags | O_NONBLOCK) or die "Can't set flags for the socket: $!\n";
  879                $firstread=1;
  880                $next="";
  881                last;
  882              }
  883             else
  884              {
  885                last;
  886              }
  887          }
  888         else   # num is undefined => ran out of input, not eof
  889          {
  890            last;
  891          }
  892        } # end of while(1), sysread() input
  893     }
  894  } # end of while(!shouldexit)
  895 
  896 # find the pattern that matches the given line, and map to an output array
  897 # Last two parameters are references, notice the odd syntax.
  898 
  899 sub matchre_log
  900  {
  901    my $aline=shift;  # input line
  902    my $pats=shift;   # regex match array
  903    my $maps=shift;   # map regex matches to specific elements of output
  904 
  905    my @res;
  906    for(my $idx=0; $idx<@$pats; $idx++) {
  907       if( $debug>=4){ syslog 'authpriv|err', "Comparing >$aline< to >$$pats[$idx]<"; }
  908       if( $aline =~ /$$pats[$idx]/ ) {
  909          #print "pattern was a hit, running $$maps[$idx]\n";
  910          eval $$maps[$idx];
  911          last;
  912        }
  913     }
  914    return (@res);
  915  }
  916 
  917 sub isdirty
  918  {
  919    my $un=shift;
  920    my $un2=$un;
  921    $un =~ s/$sanitise//g;
  922    if( $un ne $un2 ) 
  923      { return 1,$un; } 
  924    return 0,$un; 
  925  }
  926 
  927 sub matchre_ip
  928  {
  929    my $aline=shift; # input line, an ip address
  930    my $ips=shift;  # regex match array of IPs
  931 
  932    my $idx=0;
  933    for( ; $idx<@$ips; $idx++) {
  934       if( $aline =~ /$$ips[$idx]/ ) {
  935          return ($idx);
  936        }
  937     }
  938    return ();
  939  }
  940 
  941 
  942 sub matchre_user2maxc
  943  {
  944    my $un=shift; # a user name, which may or may not exist
  945    my $ev=shift; # INVALID, DEFAULT or DIRTY
  946 
  947    # DIRTY never looks up names for a match other than $maxc_tags{DIRTY}
  948    if( $ev eq "DIRTY" ) {
  949       return $maxc_tags{$ev};
  950     }
  951 
  952    my $mc=$maxc_tags{$ev};
  953 
  954    for(my $idx=0; $idx<=$#userre; $idx++) {
  955       if($debug>2){ syslog 'authpriv|notice', "DB:u2m: un=$un, ev=$ev, idx=$idx, userre=$userre[$idx]"; }
  956       if( $userre[$idx] eq $ev ) {
  957          if( defined $maxc[$idx] ) { $mc=$maxc[$idx]; }
  958        } elsif( $un =~ /$userre[$idx]/ ) {
  959          if( defined $maxc[$idx] ) { return $maxc[$idx]; } else { return $mc; }
  960        }
  961     }
  962 
  963    return $mc;
  964  }
  965 
  966 sub matchre_user2btime
  967  {
  968    my $un=shift; # a user name, which may or may not exist
  969    my $ev=shift; # INVALID, DEFAULT or DIRTY
  970 
  971    # DIRTY never looks up names for a match other than $btime_tags{DIRTY}
  972    if( $ev eq "DIRTY" ) {
  973       return $btime_tags{$ev};
  974     }
  975 
  976    my $t=$btime_tags{$ev};
  977 
  978    for(my $idx=0; $idx<=$#userre; $idx++) {
  979       if( $userre[$idx] eq $ev ) {
  980          if( defined $btime[$idx] ) { $t=$btime[$idx]; }
  981        } elsif( $un =~ /$userre[$idx]/ ) {
  982          if( defined $btime[$idx] ) { return $btime[$idx]; } else { return $t; }
  983        }
  984     }
  985 
  986    return $t;
  987  }
  988 
  989 
  990 # Deal with each complete line of log output from sshd.
  991 # lots of code duplication, it could be reduced but would probably be even
  992 # more unreadable. The variations in error messages add a lot of bulk.
  993 
  994 sub handlesshdlines
  995  {
  996    my $ttime=time;
  997 
  998    for (@_) {
  999       my $aline=$_;
 1000 
 1001       if( $debug>1 ) { syslog 'authpriv|notice',"DB:Aline=$_"; }
 1002 
 1003       if( my ($user, $ip)=matchre_log($_, \@txt_invalid, \@map_invalid) )
 1004        {
 1005          my $ename="INVALID";
 1006         
 1007          if( $ip6toip4 ) { $ip =~ s/^::ffff:([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)/\1/; } 
 1008 
 1009          my @addrs=gethostbyname($ip);
 1010          @addrs = map { inet_ntoa($_) } @addrs[4 .. $#addrs];
 1011          for $ip (@addrs) {
 1012 
 1013             if( my ($idx)=matchre_ip($ip, \@ipre) ) # white/black list
 1014              {
 1015                if( $ipaction[$idx]==0 ) { # white list - ignore
 1016                   syslog 'authpriv|notice',"Illegal username from white listed ip $ip, user $user";
 1017                 } else { # black list - instant block for default time
 1018                   if( $debug ) { syslog 'authpriv|notice',"DB:INVALID: ip black listed, $ip"; }
 1019                   $expiretime{$ip}=$ttime+$maxblocktime;
 1020                   if( ! exists $explain{$ip} ) {
 1021                      $explain{$ip}="Illegal user name from black listed ip, instant block of $ip";
 1022                      syslog 'authpriv|notice',$explain{$ip};
 1023                      emailonblock "INVALID", "$ip", "Illegal user name from black listed ip, instant block.", $expiretime{$ip};
 1024              adddrop $ip;
 1025                    }
 1026                }
 1027              } else { # non-exceptional INVALID route
 1028 
 1029                my ($dirty, $user) = isdirty($user);
 1030 
 1031                if( $dirty==1 ) { $ename="DIRTY"; $user="DIRTY"; }
 1032                my $maxc=matchre_user2maxc($user,$ename);
 1033                my $btime=matchre_user2btime($user,$ename);
 1034 
 1035                if( $debug ) { syslog 'authpriv|notice',"DB:INVALID: dirty=$dirty user=$user, ip=$ip"; }
 1036 
 1037                $chances{$ip}++;
 1038                $expiretime{$ip}=$ttime+$btime;
 1039 
 1040                if( $chances{$ip} > $maxc ) { # exceeded threshold, see if we need to add a DROP rule
 1041                   if( ! exists $explain{$ip} ) {
 1042                      $explain{$ip}="Illegal user name, blocking after $maxc chances";
 1043                      syslog 'authpriv|notice',"Illegal user name, blocking $ip after $maxc chances";
 1044                      emailonblock "INVALID", "$ip", "Illegal user name, blocking after $maxc chances.", $ttime+$btime;
 1045              adddrop $ip;
 1046                    }
 1047                 } 
 1048                else {
 1049                   syslog 'authpriv|notice',"Chanced illegal user name from $ip, $chances{$ip} guesses out of $maxc";
 1050                 }
 1051              } # non-black/white list option, standand INVALID or DIRTY
 1052       } # name lookup
 1053        } # End of INVALID
 1054 
 1055       elsif( my ($ip)=matchre_log($_, \@txt_noid, \@map_noid) )
 1056        {
 1057          if( $ip6toip4 ) { $ip =~ s/^::ffff:([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)/\1/; } 
 1058      
 1059          my @addrs=gethostbyname($ip);
 1060          @addrs = map { inet_ntoa($_) } @addrs[4 .. $#addrs];
 1061          for $ip (@addrs) {
 1062 
 1063             if( my ($idx)=matchre_ip($ip, \@ipre) ) # white/black list - ignore
 1064              {
 1065                if( $ipaction[$idx]== 0 ) { # white list - ignore
 1066                   syslog 'authpriv|notice',"No ssh id from white listed ip $ip, user $user";
 1067                 } else { # black list - instant block for default time
 1068                   if( $debug ) { syslog 'authpriv|notice',"DB:NOID: ip black listed, $ip"; }
 1069                   $expiretime{$ip}=$ttime+$maxblocktime;
 1070                   if( ! exists $explain{$ip} ) {
 1071                      $explain{$ip}="No ssh id from black listed ip, instant block of $ip";
 1072                      syslog 'authpriv|notice',$explain{$ip};
 1073                      emailonblock "INVALID", "$ip", "No ssh id from black listed ip, instant block.", $expiretime{$ip};
 1074              adddrop $ip;
 1075                    }
 1076                }
 1077              } else { # non-exceptional NOID route
 1078 
 1079                if( $debug ) { syslog 'authpriv|notice',"DB:NOID: ip=$ip"; }
 1080 
 1081                my $maxc=$maxc_tags{"NOID"};
 1082                my $btime=$btime_tags{"NOID"};
 1083 
 1084                $chances{$ip}++;
 1085                $expiretime{$ip}=$ttime+$btime;
 1086 
 1087                if( $chances{$ip} > $maxc ) { # exceeded threshold, see if we need to add a DROP rule
 1088                   if( ! exists $explain{$ip} ) {
 1089                      $explain{$ip}="No ssh id string from client, blocking after $maxc chances";
 1090                      syslog 'authpriv|notice',"No ssh id string from client, blocking $ip after $maxc chances";
 1091                      emailonblock "NOID", "$ip", "No ssh id string from client, blocking after $maxc chances.", $ttime+$btime;
 1092              adddrop $ip;
 1093                    }
 1094                 }
 1095                else {
 1096                   syslog 'authpriv|notice',"Chanced missing ssh id string from $ip, $chances{$ip} guesses out of $maxc";
 1097                 }
 1098              } # non-black/white list option, standand NOID
 1099       } # name lookup
 1100        } # End of NOID
 1101 
 1102       # Failure from a genuine user
 1103       elsif( my ($user, $ip)=matchre_log($_, \@txt_failval, \@map_failval) )
 1104        {
 1105          if( $ip6toip4 ) { $ip =~ s/^::ffff:([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)/\1/; } 
 1106 
 1107          my @addrs=gethostbyname($ip);
 1108          @addrs = map { inet_ntoa($_) } @addrs[4 .. $#addrs];
 1109          for $ip (@addrs) {
 1110 
 1111             if( my ($idx)=matchre_ip($ip, \@ipre) ) # white/black list - ignore
 1112              {
 1113                if( $ipaction[$idx]==0 ) { # white list - ignore
 1114                   syslog 'authpriv|notice',"Failure from valid user from white listed ip $ip, user $user";
 1115                 } else { # black list - instant block for default time
 1116                   if( $debug ) { syslog 'authpriv|notice',"DB:FAILVAL: ip black listed user=$user, ip=$ip"; }
 1117                   $expiretime{$ip}=$ttime+$maxblocktime;
 1118                   if( ! exists $explain{$ip} ) {
 1119                      $explain{$ip}="Failure from valid user on a black listed ip, instant block of $ip";
 1120                      syslog 'authpriv|notice',$explain{$ip};
 1121                      emailonblock "FAILVAL", "$ip", "Failure from a valid user on a black listed ip, instant block.", $expiretime{$ip};
 1122              adddrop $ip;
 1123                    }
 1124                 }
 1125              } else { # non-exceptional FAILVAL route
 1126 
 1127                my $maxc=matchre_user2maxc($user, "DEFAULT");
 1128                my $btime=matchre_user2btime($user, "DEFAULT");
 1129 
 1130                if( $debug ) { syslog 'authpriv|notice',"DB:FAILVAL: user=$user, ip=$ip"; }
 1131 
 1132                $chances{$ip}++;
 1133                $expiretime{$ip}=$ttime+$btime;
 1134 
 1135                if( $chances{$ip} > $maxc ) { # exceeded threshold, see if we need to add a DROP rule
 1136                   if( ! exists $explain{$ip} ) {
 1137                      $explain{$ip}="Valid user failed, blocking after $maxc chances";
 1138                      syslog 'authpriv|notice',"Valid user failed, blocking $ip after $maxc chances";
 1139                      emailonblock "$user", "$ip", "Valid user failed to login, blocking after $maxc chances.", $ttime+$btime;
 1140              adddrop $ip;
 1141                    }
 1142                 } else {
 1143                   syslog 'authpriv|notice',"Chanced valid user name from $ip, $chances{$ip} guesses out of $maxc";
 1144                 }
 1145              } # non-black/white list option, standand FAILVAL
 1146       } # expand host names
 1147        } # End of INVALID
 1148   
 1149       # a success from an ip means removing it from the list, if it exists
 1150       elsif( my ($user, $ip)=matchre_log($_, \@txt_accept, \@map_accept) )
 1151        {
 1152          if( $ip6toip4 ) { $ip =~ s/^::ffff:([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)/\1/; } 
 1153 
 1154          my @addrs=gethostbyname($ip);
 1155          @addrs = map { inet_ntoa($_) } @addrs[4 .. $#addrs];
 1156          for $ip (@addrs) {
 1157 
 1158             if( $debug ) { syslog 'authpriv|notice',"DB:ACCEPT: user=$user, ip=$ip"; }
 1159 
 1160             if( defined $explain{$ip} ) { # this would happen if the SSHD chain didn't stop this connection (eg. wrong interface)
 1161                my $reason=$explain{$ip};
 1162                syslog 'authpriv|notice',"Valid login, cancelled $reason block from $ip";
 1163            deldrop $ip;
 1164                delete $explain{$ip};
 1165              }
 1166             delete $chances{$ip};
 1167       } # expand host names
 1168        }
 1169 
 1170       # sshd quitting, 'Received signal'... more reliable than waiting for close of pipe
 1171       elsif( my ($sig)=matchre_log($_, \@txt_quit, \@map_quit) )
 1172        {
 1173          if( $debug ) { syslog 'authpriv|notice',"DB:QUIT: signal=$sig"; }
 1174 
 1175          syslog 'authpriv|notice',"sshd received signal $sig, closing sshdfilter";
 1176 
 1177          $shouldexit=1;
 1178          last;
 1179        }
 1180 
 1181        # pid to ip mapping, for dropbear.
 1182       elsif( my ($pid,$ip)=matchre_log($_, \@txt_pid2ip, \@map_pid2ip) )
 1183        {
 1184          if( $ip6toip4 ) { $ip =~ s/^::ffff:([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)/\1/; } 
 1185 
 1186          if( $debug ) { syslog 'authpriv|notice',"DB:PID2IP: pid=$pid, ip=$ip"; }
 1187 
 1188          $pid2ip{$pid}=$ip;
 1189        }
 1190 
 1191       # remove pid hash, for dropbear.
 1192       elsif( my ($pid)=matchre_log($_, \@txt_pidexit, \@map_pidexit) )
 1193        {
 1194          if( $debug ) { syslog 'authpriv|notice',"DB:PIDEXIT: pid=$pid, stored ip=$pid2ip{$ip}"; }
 1195 
 1196          delete $pid2ip{$pid};
 1197        }
 1198    
 1199       # Ignore 'Connection closed' type messages
 1200 
 1201     } # end of for(), to instanciate $_
 1202  } # end of handlesshdlines
 1203 
 1204 
 1205 # build a line that contains $maxhist lines worth of previous input lines, or as many as currently available
 1206 sub handlesshdoutput
 1207  {
 1208    for (@_) {
 1209       my $aline=$_;
 1210 
 1211       # first, add the new line to the history
 1212       $loghist{$curhist}=$aline;
 1213 
 1214       if( $logsource eq "STDIN" ) {  # log sshd output only if we started it.
 1215           print $SSHDLOGGER "$aline\n";
 1216           $SSHDLOGGER->autoflush(1);
 1217        }
 1218 
 1219       if( $debug>=3){ syslog 'authpriv|notice',"DB:incoming=$loghist{$curhist}"; }
 1220       my $idx=$curhist++;
 1221       if( $curhist>=$maxhist )
 1222        { $curhist=0; }
 1223       # position history counter to oldest entry
 1224       my $sidx=$curhist;
 1225       for(my $i=0;$i<$maxhist && !exists $loghist{$sidx};$i++)
 1226        {
 1227          $sidx++;
 1228      if($sidx>=$maxhist){ $sidx=0; }
 1229        }
 1230 
 1231       if( $debug>=3 ){ syslog 'authpriv|notice',"DB:curhist=$curhist, idx=$idx, sidx=$sidx"; }
 1232 
 1233       for(my $i=0;$i<$maxhist;$i++)
 1234        {
 1235          if( $debug>=4 ){ syslog 'authpriv|notice',"DB:loghist[$i]=$loghist{$i}"; }
 1236          $sidx++;
 1237      if($sidx>=$maxhist){ $sidx=0; }
 1238        }
 1239 
 1240       # and then build a line. This could be done by trimming and extending a single string... would it be any more readable?
 1241       my $bufline="";
 1242       my $nl="";
 1243       do 
 1244        {
 1245          if( $sidx>=$maxhist ){ $sidx=0; }
 1246          if( $debug>=4 ){ syslog 'authpriv|notice',"DB:loop[$sidx]=$loghist{$sidx}"; }
 1247          $bufline.=$nl.$loghist{$sidx};
 1248      $nl="\n";
 1249        } while( $sidx++!=$idx );
 1250 
 1251       if( $debug>=3 ){ syslog 'authpriv|notice',"DB:aline=$bufline"; }
 1252       handlesshdlines($bufline);
 1253     }
 1254  }
 1255 
 1256 
 1257 if( $logsource eq "STDIN" ){
 1258    syslog 'authpriv|notice',"sshd quit, closing sshdfilter";
 1259  } else {
 1260    syslog 'authpriv|notice',"closing sshdfilter";
 1261  }
 1262 
 1263 # Reap any zombies (which could be created when sending emails)
 1264 # This might also help with those rare occasions when '/etc/init.d/sshd restart' mysteriously fails.
 1265 my $kid;
 1266 do {
 1267      $kid = waitpid(-1, WNOHANG);
 1268  } until $kid <= 0;
 1269 
 1270 close($SSHDHANDLE);
 1271 close $SSHDLOGGER;
 1272 if( defined $pid ){ waitpid($pid,0); }
 1273 
 1274 closelog();
 1275 
 1276 unlink("/var/run/sshdfilter.pid.$chain");
 1277 
 1278 # the end
 1279 exit;
 1280