Chinaunix首页 | 论坛 | 博客
  • 博客访问: 631235
  • 博文数量: 184
  • 博客积分: 10057
  • 博客等级: 上将
  • 技术积分: 2505
  • 用 户 组: 普通用户
  • 注册时间: 2007-05-31 16:34
文章分类

全部博文(184)

文章存档

2010年(5)

2009年(104)

2008年(75)

我的朋友

分类:

2009-10-15 12:41:52

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(