#!/usr/bin/perl -w
## ssh_login_blocker by Adam Rosi-Kessel Copyright 2004-2005
## Permission granted to modify and redistribute under the terms of the GPL v2.0 or later
## v0.2 - 2005.09.09

use strict;
use Unix::Syslog qw(:subs :macros);  # Syslog macros
use Time::Local;

# maximum illegal usernames before banning IP
my $max_username = 6;

# maximum failed password attempts (for legit username) before banning IP
my $max_password = 20;

# number of seconds after which the counter for bad passwords gets reset
my $reset_interval = 60 * 5;

# unbannable IP addresses
# separate "whitelisted" IP addresses with an |, e.g.:
# my $match_safe = qr/127.0.0.1|192.168.1.1/;
my $match_safe = qr/127.0.0.1/;

# file to add ban string to
my $deny_file = "/etc/hosts.deny";

my $match_ip_address = qr/([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})/;

openlog ("ssh_login_blocker", LOG_PID | LOG_PERROR, LOG_AUTH);

# process start/stop arguments
if (@ARGV) {
   if ($ARGV[0] eq "stop") {
     print "Stopping ssh_login_blocker...\n";
     syslog (LOG_WARNING, "Stopping ssh_login_blocker...\n");
     local $SIG{TERM} = 'IGNORE';
     system("killall ssh_login_blocker >/dev/null 2>/dev/null");
     exit 0;
   } elsif ($ARGV[0] eq "reload" || $ARGV[0] eq "force-reload" || $ARGV[0] eq "restart") {
     print "No need to restart ssh_login_blocker (yet)...\n";
#     print "Restarting ssh_login_blocker...\n";
#     syslog (LOG_WARNING, "Restarting ssh_login_blocker...\n");
#     local $SIG{HUP} = 'IGNORE';
#     system("killall -HUP ssh_login_blocker >/dev/null 2>/dev/null");
#     exit 0;
   } elsif ($ARGV[0] ne "start") {
     die 'Usage: ssh_login_blocker {start|stop|reload|force-reload|restart}\n';
   }
}

if (open PS, "ps -fA |") {
   while (<PS>) {
     if (/perl.*ssh_login_blocker/ and (/^\w+\s+([0-9]+)\s+([0-9]+)/) and (($1 ne $$) and ($2 ne $$))) {
        die "ssh_login_blocker already started.\n";
     }
   }
   close PS;
}

# this is the only place where we write out to a file; we include
# an extra redundant check here to make sure (1) we're not banning
# a "safe" IP; and (2) to make sure we're not adding anything to
# the deny file except the IP address
sub ban {
  my $ip = shift;

  if ($ip !~ m/^$match_ip_address$/ or $ip =~ m/$match_safe/) {
    syslog (LOG_WARNING, "We were asked to ban $ip which is either not an IP address or specifically 'safe'. This shouldn't happen.");
    return;
  }

  my $tm = localtime;
  if (open OUT, ">> $deny_file") {
     print OUT "# banned by ssh_login_blocker on " . scalar localtime() . "\n" .
               "ALL: $ip\n\n";
     close OUT;
  } else {
    syslog (LOG_WARNING, "Could not ban $ip, do you have proper permissions for hosts.deny?");
  }
}

my (%blocked, %illegal_user, %illegal_password, %last_wrong);
my $ip;

if (open IN, $deny_file) {
   while (<IN>) {
     if (($ip) = m/$match_ip_address/) {
        $blocked{$ip} = 1; 
     }
   }
   close IN;
} else {
  syslog (LOG_WARNING, "Could not access hosts.deny file, do you have proper permissions for hosts.deny?");
}

unless (my $pid = fork()) {
   close STDOUT;
   close STDERR;
   close STDIN;
   # master loop for script
   while (1) {
   unless (open IN, "tail -n 1 --sleep-interval=30 --follow=name /var/log/auth.log|") {
     syslog (LOG_WARNING, "Could not access auth.log, quitting!");
     exit;
   }
   my $rin = "";;
   my $rout = "";
   vec($rin,fileno(IN),1) = 1;
   my $rotate_time = timelocal(localtime);
   while (<IN>) {
     if (/Illegal user/) {
        if (($ip) = m/$match_ip_address/) {
           if (not exists $blocked{$ip} and not ($ip =~ m/$match_safe/)) {
              if (exists $illegal_user{$ip}) {
                 if (exists $last_wrong{$ip} and (timelocal(localtime) - $last_wrong{$ip} > $reset_interval)) {
                       syslog (LOG_WARNING, "Wrong password from $ip but reset time exceeded, resetting bad password count.");
                       $illegal_password{$ip} = 0;
                       $illegal_user{$ip} = 0;
                 }
                 $last_wrong{$ip} = timelocal(localtime);
                 $illegal_user{$ip}++;
              } else {
                 $illegal_user{$ip} = 0;
              }
              if ($illegal_user{$ip} > $max_username ) {
                 syslog (LOG_WARNING, "Too many attempts for nonexistent username from $ip, banning address.");
                 ban($ip);
                 $blocked{$ip} = 1; 
              }
           }
        }
     } elsif (/Failed/) {
        if (($ip) = m/$match_ip_address/) {
           if (not exists $blocked{$ip} and not ($ip =~ m/$match_safe/)) {
              if (exists $illegal_password{$ip}) {
                 if (exists $last_wrong{$ip} and (timelocal(localtime) - $last_wrong{$ip} > $reset_interval)) {
                       syslog (LOG_WARNING, "Wrong password from $ip but reset time exceeded, resetting bad password count.");
                       $illegal_password{$ip} = 0;
                       $illegal_user{$ip} = 0;
                 }
                 $last_wrong{$ip} = timelocal(localtime);
                 $illegal_password{$ip}++;
              } else {
                 $illegal_password{$ip} = 0;
              }
              if ($illegal_password{$ip} > $max_password ) {
                 syslog (LOG_WARNING, "Too many bad passwords from $ip, banning address.");
                 ban($ip);
                 $blocked{$ip} = 1; 
              }
           }
        }
     }
# reopen auth.log once per hour in case it has rotated
# probably not necessary with follow=name, let's try without
#     close IN if ((timelocal(localtime) - $rotate_time) > 60*60);
#     close IN unless (select($rout=$rin,undef,undef,300)); 
   }
   } 
   close IN;
   
   closelog();
}

print "Starting ssh_login_blocker...\n";
syslog (LOG_WARNING, "Starting ssh_login_blocker...");
