antiabuse.pl
#!/usr/local/bin/perl
#
## Anti Abuse v.99b3
## Copyright 2005-2007 Jeremy Kister
#
## Anti Abuse may be copied and distributed under the terms found in
## the Perl "Artistic License", found in the standard Perl distribution.
#
## Deter evil hosts from repetatively sending your server spam/viruses.
## works very well with:
## qmail-1.03 + qmail-1.03.isp.patch
## ucspi-tcp-0.88 + ucspi-tcp-0.88.isp.patch
## simscan-1.2 + stabilize.patch
#
## only thing to watch out for is hosts that forward mail from mailboxes
## that they host to mailboxes you host -- spam goes to them, forwards
## to you, and they look like the abuser.
#
# To install:
# set up your database
#
#CREATE TABLE `abuse_rate` (
# `ip` varchar(15) NOT NULL default '',
# `ten_min` smallint(2) unsigned NOT NULL,
# `one_hour` smallint(2) unsigned NOT NULL,
# `one_day` smallint(2) unsigned NOT NULL,
# `timestamp` int(4) unsigned NOT NULL,
# `nextcheck` int(4) unsigned NOT NULL,
# PRIMARY KEY (`ip`),
# KEY `ten_min` (`ten_min`),
# KEY `one_hour` (`one_hour`),
# KEY `one_day` (`one_day`),
# KEY `timestamp` (`timestamp`),
# KEY `nextcheck` (`nextcheck`)
#) TYPE=MyISAM;
#
#CREATE TABLE `abuse_events` (
# `ip` varchar(15) NOT NULL,
# `timestamp` int(4) unsigned NOT NULL,
# `weight` tinyint(1) NOT NULL,
# KEY `ip` (`ip`),
# KEY `timestamp` (`timestamp`),
# KEY `weight` (`weight`)
#) TYPE=MyISAM;
# decide if you're going to use rbldns, or /etc/tcp.smtp
# if tcp.smtp:
# create your tcp.smtp.template:
# (all lines above the last will be inserted before the abuse rules)
# (the last line will be put last)
#
# echo '127.:allow,RELAYCLIENT=""' > /etc/tcp.smtp.template
# echo '192.168.0.:allow,RELAYCLIENT=""' >> /etc/tcp.smtp.template
# echo ':allow' >> /etc/tcp.smtp.template
# chmod ugo+x /usr/local/script/antiabuse.pl
# mkdir -p /var/qmail/supervise/antiabuse/log
# mkdir -p /var/log/antiabuse
# create a /var/qmail/supervise/antiabuse/run
# on ONE machine per database --- you can have
# all the machines you want sending data into
# the database, but only one relisting agent.
#
# #!/bin/sh
#
# exec /usr/local/script/antiabuse.pl --relister \
# --verbose \ # optional
# --tcprules_file="/etc/tcp.smtp" \ # optional
# --rbldns_file="/etc/rbldns/root/data" \ # optional
# --driver=mysql \
# --dbserver=mysql.example.net \
# --dbname=database_name \ # optional, depending on your setup
# --dbun=database_useranme \
# --dbpw=database_password 2>&1
# create a /var/qmail/supervise/antiabuse/log/run
#
# #!/bin/sh
# exec /usr/local/bin/setuidgid qmaill /usr/local/bin/multilog s2097152 n1 /var/log/antiabuse
# chmod ugo+x /var/qmail/supervise/antiabuse/run /var/qmail/supervise/antiabuse/log/run
# ln -s /var/qmail/supervise/antiabuse /service
# and modify your /service/qmail-smtp/log/run script:
#
# #!/bin/sh
# exec /usr/local/bin/setuidgid qmaill \
# /usr/local/script/antiabuse.pl \
# --verbose \ # optional
# --whitelist="172.24.12.0/22,10.0.0.0/24" \ # optional, local net recommended
# --blockmsg='Blocked for abuse; See ' \ # optional
# --driver=mysql --dbserver=mysql.example.net \
# --dbname=database_name \ # optional
# --dbun=database_username --dbpw=database_password -- \
# /usr/local/bin/multilog t /var/log/qmail/smtpd
# svc -du /service/qmail-smtpd/log
use strict;
use Getopt::Long;
use DBI;
use Net::CIDR::Lite;
use Sys::SigAction qw(set_sig_handler);
chdir('/');
my %opt;
GetOptions(\%opt,
'relister',
'honeypot',
'blockmsg=s',
'tcprules_file=s',
'rbldns_file=s',
'driver=s',
'dbserver=s',
'dbname=s',
'dbun=s',
'dbpw=s',
'verbose',
'whitelist=s') || die "GetOptions Error: $!\n";
foreach my $arg (qw/driver dbserver dbun dbpw/){
die "specify --${arg}\n" unless($opt{$arg});
}
my $dsn = "DBI:$opt{driver}:";
$dsn .= ($opt{driver} eq 'Sybase') ? 'server=' : 'host=';
$dsn .= $opt{dbserver};
$dsn .= ';database=' . $opt{dbname} if($opt{dbname});
my $dbh = DBI->connect($dsn, $opt{dbun}, $opt{dbpw}, {RaiseError => 1});
my $last_connect = time();
if($dbh){
warn "antiabuse: connected to database\n" if($opt{verbose});
}else{
warn "antiabuse: connect to database failed: $DBI::errstr \n" if($opt{verbose});
}
my %seconds = ('ten_min' => 600, 'one_hour' => 3600, 'one_day' => 86400);
my %threshold = ('ten_min' => 34, 'one_hour' => 72, 'one_day' => 150);
if($opt{relister}){
# the relisting agent watches the database and rebuilds the data file
if($opt{honeypot}){
warn "cannot specify both honeypot and relister\n";
sleep 10;
die;
}
unless($opt{blockmsg}){
$opt{blockmsg} = 'Blocked for abuse - IP address: ';
}
if($opt{tcprules_file} && $opt{rbldns_file}){
warn "cannot specify both tcprules_file and rbldns_file\n";
sleep 10;
die;
}
unless($opt{tcprules_file} || $opt{rbldns_file}){
warn "must specify either tcprules_file or rbldns_file\n";
sleep 10;
die;
}
my $rules_file = ($opt{tcprules_file}) ? $opt{tcprules_file} : $opt{rbldns_file};
my $rbldir;
if($opt{rbldns_file}){
if($opt{rbldns_file} =~ /^(.+)\/data$/){
$rbldir = $1;
}else{
warn "rbldns_file must end in /data - or rbldns-conf won't run\n";
sleep 10;
die;
}
}
until(-w $rules_file){
warn "cannot write to $rules_file - retry in 10 seconds\n";
sleep 10;
}
my %memory;
my $i = 0;
my $lastrun = 0;
while(dbping($dbh)){
my $relist;
# every now and then clean up the database
if($i == 0 || $i == 59){
# delete old events
my $start = time();
my $t = ($start - 600);
my $h = ($start - 3600);
my $d = ($start - 86400);
my $sql = 'DELETE FROM abuse_events WHERE timestamp < ' . $dbh->quote($d);
warn "sql: $sql\n" if($opt{verbose});
my $sth = $dbh->prepare($sql);
$sth->execute;
#recalculate abuse_rates needing it
$sql = 'SELECT ip,ten_min,one_hour,one_day FROM abuse_rate WHERE nextcheck <= ' . $start;
$sql .= ' ORDER by ip';
warn "sql: $sql\n" if($opt{verbose});
$sth = $dbh->prepare($sql);
$sth->execute;
while(my $row=$sth->fetchrow_arrayref){
my $ip = $row->[0];
my %old = ('ten_min' => $row->[1],
'one_hour' => $row->[2],
'one_day' => $row->[3]);
my %sum = (ten_min => 0, one_hour => 0, one_day => 0);
my $sqla = 'SELECT timestamp,weight FROM abuse_events WHERE ip = ' . $dbh->quote($ip);
$sqla .= ' ORDER BY timestamp DESC'; # for nextcheck prediction
warn "sqla: $sqla\n" if($opt{verbose});
my $stha = $dbh->prepare($sqla);
$stha->execute;
my %data;
while(my $rowb=$stha->fetchrow_arrayref){
$data{$rowb->[0]} += $rowb->[1];
if($rowb->[0] >= $t){
$sum{ten_min} += $rowb->[1];
$sum{one_hour} += $rowb->[1];
$sum{one_day} += $rowb->[1];
}elsif($rowb->[0] >= $h){
$sum{one_hour} += $rowb->[1];
$sum{one_day} += $rowb->[1];
}elsif($rowb->[0] >= $d){
$sum{one_day} += $rowb->[1];
}
}
my $sqlb = 'UPDATE abuse_rate SET';
my ($update,$keeprow,$nextcheck);
foreach my $field (qw/ten_min one_hour one_day/){
if($sum{$field} >= $threshold{$field}){
$keeprow = 1;
$update = 1 if($sum{$field} != $old{$field});
unless($nextcheck){
my $total;
foreach my $timestamp (reverse sort keys %data){
$total += $data{$timestamp};
foreach my $field (qw/one_day one_hour ten_min/){
if($total >= $threshold{$field}){
$nextcheck = ($timestamp + $seconds{$field});
last;
}
}
last if($nextcheck);
}
}
}
$sqlb .= " $field = " . $dbh->quote($sum{$field}) . ',';
}
if($keeprow && $update){
$sqlb .= ' nextcheck = ' . $nextcheck . ' WHERE ip = ' . $dbh->quote($ip); # preceding comma above
}elsif($keeprow){
undef $sqlb;
}else{
$sqlb = 'DELETE FROM abuse_rate WHERE ip = ' . $dbh->quote($ip);
warn "deleting $ip [$sum{ten_min}/$sum{one_hour}/$sum{one_day}]\n" if($opt{verbose});
delete $memory{$ip};
$relist=1;
}
if($sqlb){
warn "sqlb: $sqlb\n" if($opt{verbose});
my $sthb = $dbh->prepare($sqlb);
$sthb->execute;
}
}
# make sure everything we have in memory is in abuse_rate
warn "tainting %memory...\n" if($opt{verbose});
$sql = 'SELECT ip FROM abuse_rate';
$sth = $dbh->prepare($sql);
$sth->execute;
my %current;
while(my $row=$sth->fetchrow_arrayref){
$current{$row->[0]} = 1;
}
warn "DEBUG: current populated\n";
foreach my $key (keys %memory){
unless(exists($current{$key})){
warn "deleting memory{$key} as per current\n" if($opt{verbose});
delete $memory{$key};
}
}
my $diff = (time() - $start);
warn "REPROCESSED DATABASE in $diff seconds.\n" if($opt{verbose});
$i=1;
}
# find all new abusers
my $delta = ($lastrun - 10);
$lastrun = time();
my $sql = 'SELECT ip FROM abuse_rate WHERE timestamp > ' . $dbh->quote($delta);
warn "[$i] sql: $sql\n" if($opt{verbose});
my $sth = $dbh->prepare($sql);
$sth->execute;
while(my $row=$sth->fetchrow_arrayref){
next if($row->[0] == 0); ## bug??
unless(exists($memory{$row->[0]})){
$relist=1;
$memory{$row->[0]} = time();
warn "adding $row->[0]\n" if($opt{verbose});
}
}
if($relist){
my $num_hosts = (keys %memory);
warn "rebuilding data file ($num_hosts hosts)\n" if($opt{verbose});
if($opt{tcprules_file}){
my @data;
open(TEMPLATE, "$opt{tcprules_file}.template") || die "cannot open $opt{tcprules_file}.template: $!\n";
open(DATA, ">$opt{tcprules_file}") || die "cannot open $opt{tcprules_file} for writing: $!\n";
while(){
push @data, $_;
}
close TEMPLATE;
my $lastline = pop(@data);
foreach (@data){
print DATA;
}
foreach my $ip (keys %memory){
my $string = $ip . ':allow,MAXCONNIP="1",MAXCONNC="2",RBLSMTPD="' . $opt{blockmsg} . $ip . '"' . "\n";
print DATA $string;
push @data, $string;
}
print DATA $lastline; # remember below
close DATA;
open(TCPRULES, "| /usr/local/bin/tcprules $opt{tcprules_file}.cdb $opt{tcprules_file}.tmp 2>&1")
|| die "cannot fork tcprules: $!\n";
foreach (@data){
print TCPRULES;
}
print TCPRULES $lastline; # remember above
close TCPRULES;
}else{
open(DATA, ">$opt{rbldns_file}") || die "cannot open $opt{rbldns_file}: $!\n";
foreach my $ip (keys %memory){
print DATA "$ip\n";
}
print DATA ':127.0.0.2:' . $opt{blockmsg} . '$' . "\n";
close DATA;
if(chdir($rbldir)){
system('/usr/local/bin/rbldns-data');
chdir('/');
}else{
warn "could not chdir $rbldir: $!\n";
}
}
}
sleep 10;
$i++;
}
warn "lost connection to db server!!\n" if($opt{verbose});
sleep 30;
die "exiting to reconnect to database\n";
}
$|=1;
my $command = join ' ', @ARGV;
open(LOG, "| $command") || die "could not fork $command: $!\n";
my $oldfh = select LOG;
$|=1;
select $oldfh;
if($opt{honeypot}){
print LOG "antiabuse[log]: HoneyPot mode ON!\n" if($opt{verbose});
}
if($opt{whitelist}){
$opt{whitelist} .= ',127.0.0.0/8' unless($opt{whitelist} =~ /127\.0\.0\.0\/8/);
}else{
$opt{whitelist} = '127.0.0.0/8';
}
print LOG "antiabuse[log]: whitelist set: $opt{whitelist}\n" if($opt{verbose});
unless(-x $ARGV[0]){
print LOG "antiabuse[log]: cannot execute $ARGV[0]: $!\n";
sleep 2;
exit 1;
}
my $cidr = Net::CIDR::Lite->new;
foreach my $network (split /,/, $opt{whitelist}){
print LOG "antiabuse[log]: will not blacklist $network\n" if($opt{verbose});
$cidr->add($network);
}
while(){
print LOG;
eval {
my $h = set_sig_handler('ALRM', sub { die "TIMEOUT!"; } );
alarm(3);
my ($ip,$weight);
if($opt{honeypot}){
if(/tcpserver:\s+.+\s+:(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}):/ && $opt{honeypot}){
$ip = $1;
$weight = 200; # enough to block for the day by default
}
}else{
if(/qmail-smtpd:\s(.+)\sat\s(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/){
my $msg = $1;
$ip = $2;
if($msg eq 'Too many errors'){
$weight = 30;
}elsif($msg =~ /No gateway for/){
$weight = 40;
}else{
$weight = 10;
}
}elsif(/simscan:[^:]+:[^:]+\s\((\d+\.\d{2})\/\d+\.\d{2}\):[^:]+:[^:]+:(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}):/){
my $score = $1;
$ip = $2;
if($score > 15){
$weight = 40;
}elsif($score > 10){
$weight = 20;
}elsif($score < 5){
$weight = '-4';
}else{
# we treat 5-10 as almost neutral (penalize just a bit)
$weight = 2;
}
}elsif(/rblsmtpd:\s(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})/){
$ip = $1;
$weight = 2;
}
}
if($ip){
if($cidr->find($ip)){
print LOG "antiabuse[log]: ignoring whitelisted ip address: $ip\n";
}else{
my $time = time();
my $sql = 'INSERT INTO abuse_events (ip,timestamp,weight) VALUES (';
$sql .= $dbh->quote($ip) . ",${time},${weight})";
my $sth = $dbh->prepare($sql);
$sth->execute;
$sql = 'SELECT COUNT(*) FROM abuse_rate WHERE ip = ' . $dbh->quote($ip);
$sth = $dbh->prepare($sql);
$sth->execute;
my $row = $sth->fetchrow_arrayref;
unless($row->[0]){ # otherwise just let the relister deal with it
my $t = (time() - 600);
my $h = (time() - 3600);
my $d = (time() - 86400);
my $sql = 'SELECT timestamp,weight FROM abuse_events WHERE ip = ' . $dbh->quote($ip);
$sql .= ' ORDER BY timestamp DESC';
my $sth = $dbh->prepare($sql);
$sth->execute;
my (%sum,%data);
while(my $row = $sth->fetchrow_arrayref){
$data{$row->[0]} += $row->[1];
if($row->[0] >= $t){
$sum{ten_min} += $row->[1];
$sum{one_hour} += $row->[1];
$sum{one_day} += $row->[1];
}elsif($row->[0] >= $h){
$sum{one_hour} += $row->[1];
$sum{one_day} += $row->[1];
}elsif($row->[0] >= $d){
$sum{one_day} += $row->[1];
}
}
my ($dorate,$total);
foreach my $timestamp (reverse sort keys %data){
$total += $data{$timestamp};
foreach my $field (qw/one_day one_hour ten_min/){
if($total >= $threshold{$field}){
$dorate = ($timestamp + $seconds{$field});
last;
}
}
last if($dorate);
}
if($dorate){
print LOG "antiabuse[log]: threshold exceeded; blocking $ip\n" if($opt{verbose});
my $sql = 'INSERT INTO abuse_rate VALUES (' . $dbh->quote($ip) . ',';
foreach my $field (qw/ten_min one_hour one_day/){
$sql .= $dbh->quote($sum{$field}) . ',';
}
$sql .= time() . ',' . $dorate . ')'; # preceding comma from above loop
my $sth = $dbh->prepare($sql);
$sth->execute;
}
}
}
}
}; # end eval (in case dbserver dies)
alarm(0);
if($@){
# we errored
print LOG "antiabuse[log]: error procesing data: $@!\n";
unless(dbping($dbh)){
#reconnect to db if last time we tried was more than 30 seconds ago
my $now = time();
if(($now - $last_connect) > 30){
eval {
my $h = set_sig_handler('ALRM', sub { die "TIMEOUT!"; } );
alarm(3);
if($dbh = DBI->connect($dsn, $opt{dbun}, $opt{dbpw}, {RaiseError => 1})){
print LOG "antiabuse[log]: reconnected to database\n" if($opt{verbose});
}else{
print LOG "antiabuse[log]: reconnect to database failed: $DBI::errstr \n" if($opt{verbose});
}
};
alarm(0);
$last_connect = time();
}
}
}
}
sub dbping {
my $test_dbh = shift;
my $xcode = 0;
eval {
my $h = set_sig_handler('ALRM', sub { die "TIMEOUT!"; } );
alarm(1);
if($test_dbh){
if(my $sth = $test_dbh->prepare('SELECT 1')){
if(defined(my $rc = $sth->execute)){
$sth->finish;
$xcode=1;
}
}
}
};
alarm(0);
return $xcode;
}
阅读(712) | 评论(0) | 转发(0) |