#! /usr/bin/perl -w
#
#   ssh-brute-stop
#
#       Unsophisticated script to block IPs with too many
#       illegal ssh attempts.
#
#       http://brainsik.to/ssh-brute-force
#       Jeremy Avnet >brainsik-code(at)theory.org<
#
#   How it works:
#       * Reads in your syslog auth log in real time (via fifo).
#       * Every bad SSH attempt gives the IP points.
#         (Caveat: only illegal attempts are counted, 
#                  i.e. attempts to login as non-existant users.)
#       * When a maximum point level is passed, 
#         the IP is blocked using iptables.
#       
#   Required configuration:
#       1) mkfifo /var/log/auth.fifo
#       2) edit /etc/syslog.conf and add this line:
#          auth,authpriv.*                 |/var/log/auth.fifo
#          (i put mine below the auth.log line near the top)            
#       3) /etc/init.d/sysklogd restart
#
#   Optional configuration:
#       * Whitelists: The script auto-whitelists local addresses. If you
#         want to add some more addresses to never be blocked look below for
#         "whitelist".
#       * Points: You can tweak the point values and max points. The
#         current setup blocks any IP that tries to login as 3 different
#         (non-existant) users or 9 times for a single (non-existant) user. I
#         wanted to be careful about valid users trying to login under a
#         single, wrong name (as happens sometimes).
#
#   Changelog:
#       * 20080805: First public release.
#       * 20080822: Changed regex to match "Invalid" or "Illegal".
#       
use strict;
use Socket;
use Sys::Syslog;

my $DEBUG = 0;

my $NAME        = 'ssh-brute-stop';
my $MAXPOINTS   = 9;
my $PTS_NEWILLEGAL = 3;
my $PTS_OLDILLEGAL = 1;
my $AUTH_FIFO = "/var/log/auth.fifo";

#
# bad password
#    sshd[5768]: (pam_unix) authentication failure; logname= uid=0 euid=0 tty=ssh ruser= rhost=localhost.localdomain  user=brainsik
#    sshd[5768]: error: PAM: Authentication failure for brainsik from localhost.localdomain

my( %log, %blocked, %points );

die "The required named pipe at $AUTH_FIFO does not exist! Exiting..\n" unless -p $AUTH_FIFO;


# IP whitelist
whitelistself(\%blocked);   # add local addresses to whitelist
# add any more like below
#$blocked{'127.0.0.1'} = 'never';

while ( open(AUTH_FIFO, $AUTH_FIFO) ) {
    while (<AUTH_FIFO>) {
        my( $user, $ip );
        
        # check for illegal (unknown) user attempts
        if ( /sshd\[\d+\]: (?:Illegal|Invalid) user (\w+) from (?:::ffff:)?([\d.]+)/ ) {
            ($user, $ip) = ($1, $2);
            $points{$ip} += (exists $log{$ip}{$user}) ? $PTS_OLDILLEGAL : $PTS_NEWILLEGAL;
            $log{$ip}{$user} = time;
        }
        else { next }
        blockip(\%points, \%blocked, $ip);
    }
    close AUTH_FIFO;
    print STDERR "closed $AUTH_FIFO at ", scalar localtime, "\n" if $DEBUG;
    sleep 1;
}
die "Could not open $AUTH_FIFO\n";


sub blockip {
    my $points  = shift;
    my $blocked = shift;
    my $ip      = shift;
    
    my $iptables = '/sbin/iptables -I INPUT -j DROP -s';
    
    # block if past threshold
    if ( $$points{$ip} >= $MAXPOINTS ) {
        
        # avoid duplicate iptable block lines
        unless ( exists $$blocked{$ip} ) {
                        
            system "$iptables $ip\n";
            $$blocked{$ip} = 'iptables';
            
            my $iaddr = inet_aton($ip);
            my $host  = gethostbyaddr($iaddr, AF_INET);

            openlog($NAME, 'pid', 'auth');
            syslog('notice', 'BLOCKED: %s', ( ($host) ? "$host ($ip)" : "$ip") . " [+$$points{$ip}]");
            closelog();
        }
        return 1;   # ip is blocked
    }
    return 0;   # ip not blocked this time
}

sub whitelistself {
    my $blocked  = shift;
    my @ifconfig = `/sbin/ifconfig`;
    foreach (@ifconfig) {
        next unless /inet addr:([\d.]+)/;
        $$blocked{$1} = 'never';
    }
    print "* added local addresses to whitelist:", map { " $_" } keys %blocked, "\n";
}
