#!/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