#!/usr/bin/perl -w # -------------------------------------------------------------- # timeban, a per-IP access ban manager # # Version: 1.0 # # (c) Tom Kistner # # Objectives: - keep log files uncluttered. # - keep idiots under control. # # Requires: - iptables (Linux 2.4+) # - Perl (with Sys::Syslog module) # # Installation: # ------------- # 1) Copy this script to a location of your choice # (/usr/local/bin is usually a good idea). # Then edit it and change these parameters: # The location of your iptables binary. my $iptables = '/sbin/iptables'; # # Where timeban should put its database file. # (Can safely be deleted on reboot). my $storage = '/tmp/timeban.storable'; # # Name for the iptables chain that timeban # manages. USE AN EXCLUSIVE CHAIN NAME! my $chain = 'TIMEBAN'; # # How many minutes we should keep registered # hits (infractions) in the database. my $keep_hits_for = 60; # # 2) Add two cron jobs (usually to /etc/crontab): # # 0-59/1 * * * * root /usr/local/bin/timeban cleanup # 0 0 * * * root /usr/local/bin/timeban rebuild # # The first one will lift timed-out bans. The second # one will first flush the chain and then rebuild the # bans from the database. This ensures that the ban # chain and the database are completely synced once # per day (to prevent "dead" entries in the chain). # # 3) Append a rule to your existing iptables ruleset # that jumps to the timeban chain: # # iptables -A INPUT -j TIMEBAN # # This rule must be re-created on system startup. # How you do this depends on your linux distribution. # Some distros (e.g. Gentoo) have iptables management # scripts in sysv init. # # # Usage: # ------ # # timeban add [minutes] [threshold] [delta] # # Meaning: ban for minutes if # [threshold] infractions were registered in # the last [delta] minutes. # # [minutes] defaults to 5 if unspecified. # # Both [threshold] and [delta] are optional, # allowing to instantly ban an IP address for # a specified amount of minutes. # # Other use-cases: # # timeban cleanup - Lift timed-out bans. # Usually called via Cron. # # timeban rebuild - Sync chain to db. # Usually called via cron. # # timeban dump - Dump db in Data::Dumper format. # # timeban flush - Flush db and chain. # # -------------------------------------------------------------- use strict; use Storable qw(lock_store lock_retrieve); use Sys::Syslog; use Data::Dumper; openlog 'timeban', 'pid', 'user'; open(STDERR,"> /dev/null"); system($iptables,'-N',$chain); my $commands = { 'add' => \&add, 'cleanup' => \&cleanup, 'flush' => \&flush, 'dump' => \&dump, 'rebuild' => \&rebuild }; my $command = shift @ARGV; my $store; if (defined($command) && exists($commands->{$command})) { $store = get_storage(); &{$commands->{$command}}(@ARGV); } else { print "No or unknown command.\n\nUsage: timeban [threshold] [delta]\n\nOpen 'timeban' for installation and usage instructions.\n\n"; quit(-1); } sub dump { print Dumper($store); } sub add { my $ip = shift; my $minutes = shift || 5; my $threshold = shift || 0; my $threshold_delta = shift || 5; if (($minutes > 0) && ($minutes < 9999)) { if ($ip =~ /^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$/) { unless (is_banned($ip)) { if (!$threshold || is_over_threshold($ip,$threshold,$threshold_delta)) { $store->{banned}->{$ip}->{duration} = $minutes*60; $store->{banned}->{$ip}->{timestamp} = time(); delete($store->{monitoring}->{$ip}) if (exists($store->{monitoring}->{$ip})); system($iptables,'-I',$chain,'-s',$ip,'-j','DROP'); slog("(Add) Banned $ip for $minutes minutes". ($threshold ? " (after ".($threshold+1)." hits in $threshold_delta minutes)." : ".")); } else { my $time_pid = time().'_'.$$; $store->{monitoring}->{$ip}->{$time_pid} = 1; my $hits = scalar keys %{ $store->{monitoring}->{$ip} }; slog("(Add) Registered hit for IP $ip ($hits hits, threshold is >$threshold)."); } put_storage($store); } else { slog("(Add) IP $ip is already banned (unban in ".int((($store->{banned}->{$ip}->{duration}-(time()-$store->{banned}->{$ip}->{timestamp}))/60)+1)." minute[s])."); } } else { slog("(Add) $ip is not an IP address."); quit(-1); } } else { slog("(Add) Minutes must be >0 and <9999."); quit(-1); } } sub cleanup { if (exists($store->{monitoring})) { foreach my $ip (keys %{ $store->{monitoring} }) { my $removed = 0; foreach my $time_pid (keys %{ $store->{monitoring}->{$ip} }) { my ($timestamp,$pid) = split /_/,$time_pid; if ($timestamp < (time() - ($keep_hits_for*60))) { delete $store->{monitoring}->{$ip}->{$time_pid}; $removed++; } } if ((scalar keys %{ $store->{monitoring}->{$ip} }) == 0) { slog("(Cleanup) Removed $ip from monitoring store."); delete $store->{monitoring}->{$ip}; } elsif ($removed) { slog("(Cleanup) Removed $removed timed-out hits for $ip."); } } } if (exists($store->{banned})) { foreach my $ip (keys %{ $store->{banned} }) { unless (is_banned($ip)) { slog("(Cleanup) Unbanned IP $ip."); system($iptables,'-D',$chain,'-s',$ip,'-j','DROP'); delete $store->{banned}->{$ip}; } } } put_storage($store); } sub flush { system($iptables,'-F',$chain); delete $store->{monitoring}; delete $store->{banned}; put_storage($store); slog("(Flush) Done."); } sub rebuild { system($iptables,'-F',$chain); if (exists($store->{banned})) { foreach my $ip (keys %{ $store->{banned} }) { if (is_banned($ip)) { slog("(Rebuild) Banned $ip for ".int($store->{banned}->{$ip}->{duration}/60)." minutes."); system($iptables,'-I',$chain,'-s',$ip,'-j','DROP'); } } } } sub is_over_threshold { my $ip = shift; my $threshold = shift; my $threshold_delta = shift; my $hits = 0; if (exists($store->{monitoring}->{$ip})) { foreach my $time_pid (keys %{ $store->{monitoring}->{$ip} }) { my ($timestamp,$pid) = split /_/,$time_pid; if ($timestamp > (time() - ($threshold_delta*60))) { $hits++; } } } if ($hits >= $threshold) { return 1; } else { return 0; } } sub is_banned { my $ip = shift; if (exists($store->{banned}->{$ip})) { if ($store->{banned}->{$ip}->{duration} > (time() - $store->{banned}->{$ip}->{timestamp})) { return 1; } } return 0; } sub get_storage { # Add IP to store if (-e $storage) { my $store = lock_retrieve($storage); if (defined($store)) { return $store; } else { print "Could not read timeban storage '$storage'.\n"; quit(-1); } } else { return {}; } } sub put_storage { my $store = shift; unless (lock_store $store, $storage) { slog("(Error) Could not write timeban storage '$storage'."); quit(-1); } chmod 0777, $storage; } sub slog { my $msg = shift; syslog('notice', $msg); } sub quit { closelog; exit($_[0]); }