Chinaunix首页 | 论坛 | 博客
  • 博客访问: 5376367
  • 博文数量: 1144
  • 博客积分: 11974
  • 博客等级: 上将
  • 技术积分: 12312
  • 用 户 组: 普通用户
  • 注册时间: 2005-04-13 20:06
文章存档

2017年(2)

2016年(14)

2015年(10)

2014年(28)

2013年(23)

2012年(29)

2011年(53)

2010年(86)

2009年(83)

2008年(43)

2007年(153)

2006年(575)

2005年(45)

分类: LINUX

2009-12-09 12:07:39

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