#!/usr/local/bin/perl # ## Anti Abuse v.98b5 ## Copyright 2005-2007 Jeremy Kister http://jeremy.kister.net./ # ## 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` decimal(5,3) unsigned NOT NULL, # `one_hour` decimal(5,3) unsigned NOT NULL, # `one_day` decimal(5,3) unsigned NOT NULL, # `timestamp` 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`) #) TYPE=MyISAM; # #CREATE TABLE `abuse_events` ( # `ip` varchar(15) NOT NULL, # `timestamp` int(4) unsigned NOT NULL, # `weight` smallint(2) 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 http://example.net/cgi-bin/abuse.pl?ip=' \ # 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; chdir('/'); my %opt; GetOptions(\%opt, 'relister', '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' => 0.3, 'one_hour' => 0.11, 'one_day' => .01); if($opt{relister}){ # the relisting agent watches the database and rebuilds the data file 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 $t = (time() - 600); my $h = (time() - 3600); my $d = (time() - 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 all abuse_rates $sql = 'SELECT ip,ten_min,one_hour,one_day FROM abuse_rate'; 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 %cache; my $sqla = 'SELECT timestamp,weight FROM abuse_events WHERE ip = ' . $dbh->quote($ip); warn "sqla: $sqla\n" if($opt{verbose}); my $stha = $dbh->prepare($sqla); $stha->execute; while(my $rowb=$stha->fetchrow_arrayref){ if($rowb->[0] >= $t){ $cache{ten_min} += $rowb->[1]; $cache{one_hour} += $rowb->[1]; $cache{one_day} += $rowb->[1]; }elsif($rowb->[0] >= $h){ $cache{one_hour} += $rowb->[1]; $cache{one_day} += $rowb->[1]; }elsif($rowb->[0] >= $d){ $cache{one_day} += $rowb->[1]; } } my $sqlb = 'UPDATE abuse_rate SET'; my ($update,$keeprow); foreach my $field (qw/ten_min one_hour one_day/){ my $rate = sprintf('%.3f',($cache{$field} / $seconds{$field})); $keeprow = 1 if($rate >= $threshold{$field}); $update = 1 if($rate != $old{$field}); $sqlb .= " $field = " . $dbh->quote($rate) . ','; } if($keeprow && $update){ chop($sqlb); # tailing comma $sqlb .= ' WHERE ip = ' . $dbh->quote($ip); }elsif($keeprow){ undef $sqlb; }else{ $sqlb = 'DELETE FROM abuse_rate WHERE ip = ' . $dbh->quote($ip); warn "deleting $ip [$cache{ten_min}/$cache{one_hour}/$cache{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; } foreach my $key (keys %memory){ unless(exists($current{$key})){ warn "deleting memory{$key} as per current\n" if($opt{verbose}); delete $memory{$key}; } } $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(