#!/usr/bin/env perl
$0 = "portlimit";
## modules
use strict;
use File::Tail;
use POSIX qw(setsid);
chdir '/' or die "Can't chdir to /: $!";
umask 0;
open STDIN, '/dev/null' or die "Can't read /dev/null: $!";
open STDOUT, '>/dev/null' or die "Can't write to /dev/null: $!";
open STDERR, '>/dev/null' or die "Can't write to /dev/null: $!";
defined(my $pid = fork) or die "Can't fork: $!";
exit if $pid;
setsid or die "Can't start a new session: $!";
open(PID, ">/var/run/portlimit.pid") or die $!;
print PID $$;
close(PID);
## conf
my $DLOG = "/var/log/portlimit";
my $LOGFILE = "/var/log/auth.log";
my $LOGRETRY = "5";
my $ATTEMPTS = 3; # after how many attempts we impose a block
my $BTTL = 60; # initially blocked for that many seconds, gets incremented
# by BTTL for each firewall-block-event
my $STTL = 30; # remove ip from tracker if it was last seen STTL seconds ago
## main
my %badip;
my $ret;
my $curpos;
$| = 1;
# flush everything and restart from scratch
$ret = `pfctl -q -t blocks -T flush`;
# create the badguys table lookup
#$ret = `ipfw add 10000 deny ip from table\(1\) to any in`;
# precompiled re's
my $r_ip = qr/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/;
my @tail;
my $timeout = 5;
push(@tail, File::Tail->new(
name=>$LOGFILE,
maxinterval=>2,
interval=>1,
resetafter=>5
));
debug("started up");
while(1)
{
my($nfound, $timeleft, @pending) =
File::Tail::select(undef,undef,undef,$timeout,@tail);
unless ($nfound)
{
# housekeeping stuff here
my $now = time;
while(my($ip, $val) = each(%badip))
{
# this is also a parachute if bttl gets too high
if($now - $badip{$ip}{'seen'} > 600)
{
debug("delete $ip because not seen anymore");
delete($badip{$ip});
next;
}
if(! defined($badip{$ip}{'blocked_since'}))
{
next;
}
if($now - $badip{$ip}{'blocked_since'} < $badip{$ip}{'bttl'})
{
next;
}
if( ipfw('delete', $ip) == 0)
{
debug("deleted $ip");
delete $badip{$ip}{'blocked_since'};
$badip{$ip}{'count'} = $ATTEMPTS - 1;
}
else
{
debug("couldnt delete ip: $ip via ipfw(4)");
}
}
}
else
{
# got a new log line somewhere
foreach(@pending)
{
my $line = $_->read;
my $ip;
if
(
($line =~ /authentication error .*?\b($r_ip)\b/i) ||
($line =~ /sshd.*invalid user.*from.*?\b($r_ip)\b/i)
)
{
$ip = $1;
if( $badip{$ip}{'count'} >= $ATTEMPTS &&
(! $badip{$ip}{'blocked_since'})
)
{
if( ipfw('add', $ip) == 0 )
{
$badip{$ip}{'blocked_since'} = time;
$badip{$ip}{'seen'} = $badip{$ip}{'blocked_since'};
$badip{$ip}{'bttl'} += $BTTL;
debug("added $ip to table 1");
}
}
if( ++$badip{$ip}{'count'} < $ATTEMPTS )
{
$badip{$ip}{'seen'} = time;
debug("seen $ip");
}
}
}
}
}
#
# usage: errno = block( [ add | del ], ip )
# executes ipfw(4) and returns its exits status
#
sub ipfw($,$)
{
my $cmd = shift;
my $ip = shift;
if($cmd !~ /^(add|delete)$/)
{
return(1);
}
my $output = `pfctl -q -t blocks -T $cmd $ip/32; pfctl -q -k $ip`;
my $exitcode = $? >> 8;
return($exitcode);
}
sub debug
{
open(DLOG, ">>$DLOG") or die $!;
my $msg = join(' ', @_);
print DLOG gmtime()."[UTC]: $0: debug: $msg\n";
close(DLOG);
}
# vim: ts=4:sw=4