#!/usr/bin/perl

use strict;
use warnings;

my $logfile = "/var/log/messages"; # replace with whatever logfile you use
my $limit = 3;			# number of times to allow for human mistakes
my $iptable = "KICKBAN";	# ip table to use (will be created if doesn't exist)
my $re = 'Illegal user \S+ from ([\d\.]+)'; # regular expression to use ($1 should be a bad host)

# Note:  kickban.pl does not do anything special with the $iptable defined above,
# it is up to you to link that into your iptables; something like this may or
# may not be appropriate for your configuration:
#
#  iptables -A INPUT -s 127.0.0.0/8 -j ACCEPT
#  iptables -A INPUT -s 192.168.0.0/16 -j ACCEPT
#  iptables -A INPUT -j KICKBAN
#
# This would prevent kickban from banning localhost, or your local network (assuming
# that you use 192.168.blah.blah for your local network).
#
# Alternatively, you could set the $iptable to "INPUT", which would mean that this
# script would alter the main INPUT table on your system.
#
# Note:  Logwatch will periodically rotate logfiles, so if you're running kickban
# in daemon mode, then you will probably want to restart it as part of the post
# script in logwatch, otherwise it will be watching an old file that never has new
# data appended to it; for example:
#
# cat /etc/logrotate.d/syslog
# /var/log/messages {
#    sharedscripts
#    postrotate
#        /bin/kill -HUP `cat /var/run/syslogd.pid 2> /dev/null` 2> /dev/null || true
#        /etc/init.d/kickban restart
#    endscript
# }


my $USAGE = "
$0 [-d]

  Called by itself, it will check all log files and exit (suitable for cron)
  With the -d flag, it will become a daemon and monitor log files

";

my $daemon_mode = undef;

if (@ARGV) {
   if (@ARGV > 1) {
      die $USAGE;
   }
   unless ($ARGV[0] eq "-d") {
      die $USAGE;
   }
   $daemon_mode = 1;
}

{
   # check iptable chain exists
   my $result = `iptables -L $iptable 2>&1`;
   if ($result =~ /Table does not exist/) {
      system("iptables", "-N", $iptable);
   }
}

if (defined $daemon_mode) {
   sleep(3);
   run_once();
   if (fork() == 0) {
      if (fork() == 0) {
	 while (1) {
	    run_daemon();
	 }
      }
   }
}
else {
   run_once();
}
exit 0;

sub run_daemon {
   # remove hosts that are already banned
   my @bans = get_current_bans();
   my %banned;
   my %bastards;
   foreach my $already (@bans) {
      $banned{$already} = 1;
   }
   # watch logfile
   if (open LOG, "tail -f $logfile|") {
      print "Daemon running, interrupt to exit.\n";
      while (my $line = <LOG>) {
	 if ($line =~ /$re/) {
	    next if ($banned{$1});
	    $bastards{$1} += 1;
	    if ($bastards{$1} >= $limit) {
	       ban_host($1);
	       delete $bastards{$1};
	       $banned{$1} = 1;
	    }
	 }
      }
      close LOG;
   }
   else {
      # hmm, can't read log file, but we previously did successfully -- we're 
      # probably in the midst of a logwatch switch; sleep and try again later
      sleep(5);
   }
}

sub run_once {
   my %bastards;
   # examine log file(s) and load in offenders
   open LOG, $logfile or die "unable to open logfile: $!";
   while (my $line = <LOG>) {
      if ($line =~ /Illegal user \S+ from ([\d\.]+)/){
	 $bastards{$1} += 1;
      }
   }
   close LOG;
   # remove hosts that are already banned
   my @bans = get_current_bans();
   foreach my $already (@bans) {
      delete $bastards{$already};
   }
   # finally, ban the leftovers
   foreach my $bastard (keys %bastards){
      if ($bastards{$bastard} >= $limit) {
	 print "Banning $bastard\n";
	 ban_host($bastard);
      }
   }
}

sub get_current_bans {

   my @bans;
   open(BANS, "iptables -n -L $iptable |") or die "Unable to open pipe from iptables : $!";
   while (my $line = <BANS>) {
      # DROP       tcp  --  63.224.101.51        anywhere
      if ($line =~ /^DROP\s+tcp\s+--\s+([\d+\.]+)/) {
	 push @bans, $1;
      }
   }
   close BANS;
   return @bans;
}

sub ban_host {
   my $host = shift;
   system("iptables", "-A", $iptable, "-p", "tcp", "-s", $host, "-j", "DROP");

}

