#!/usr/bin/perl -w # # Name: smtp-ddos # Description: Drop IP addresses when under a SMTP DDoS/spambot attack # Author: Paul D. Gear # Date: 2007-10-13 # Requires: shorewall, perl BerkeleyDB library # Notes: Assumes use of postfix & postgrey # Usage: # tail --follow=name /your/mail/log/file | smtp-ddos # # Copyright (c) 2007 Paul D. Gear # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # use strict; use BerkeleyDB; my $dbdir = "/var/lib/smtp-ddos"; my (%grey, %unkn, %gcount, %ucount); # open databases tie(%grey, 'BerkeleyDB::Btree', -Filename => "$dbdir/greylist.db", -Flags => DB_CREATE, ) or die "ERROR: can't find database $dbdir/greylist.db: $!\n"; tie(%unkn, 'BerkeleyDB::Btree', -Filename => "$dbdir/unknown.db", -Flags => DB_CREATE, ) or die "ERROR: can't find database $dbdir/unknown.db: $!\n"; tie(%gcount, 'BerkeleyDB::Btree', -Filename => "$dbdir/gcount.db", -Flags => DB_CREATE, ) or die "ERROR: can't find database $dbdir/gcount.db: $!\n"; tie(%ucount, 'BerkeleyDB::Btree', -Filename => "$dbdir/ucount.db", -Flags => DB_CREATE, ) or die "ERROR: can't find database $dbdir/ucount.db: $!\n"; # report stats sub stats { my $gk = keys %grey; my $uk = keys %unkn; print STDERR "Greylisted: $gcount{'ALL'} messages, $gk IPs; Unknown user: $ucount{'ALL'} messages, $uk IPs\n"; } # flush the dynamic chain and clear out old entries in the unknown table sub flush ($) { my ($flushtime) = @_; system "iptables -F dynamic"; print STDERR "Dynamic chain cleared\n"; print STDERR "Last flush was at " . localtime( $flushtime ) . "\n"; while (my ($ip, $time) = each %unkn) { if ($time < $flushtime) { print STDERR "Purging $ip: $ucount{$ip} messages, latest " . localtime( $time ). "\n"; # eliminate this count from the aggregate stats $ucount{'ALL'} -= $ucount{$ip}; $ucount{$ip} = 0; delete $unkn{$ip}; # remove from database } } } my $now = time; my $statstime = $now; my $flushtime = $now; while (<>) { next unless /postfix\/smtpd\[\d+\]: NOQUEUE: reject: RCPT from /; next unless /Recipient address rejected: /; my ($mon, $day, $time, $ip, $msg) = ($_ =~ /^(\S+) (\d+) ([\d:]+).*\[([\d.]+)\]: \d+ <\S*>: Recipient address rejected: (.*);/); if ($msg =~ /^Greylisted for \d+ seconds/) { $grey{$ip} = time; $gcount{$ip}++; $gcount{'ALL'}++; } if ($msg =~ /^User unknown in local recipient table/) { # check for greylist entry if (exists $grey{$ip}) { unless (exists $unkn{$ip}) { system "echo -n '$day $mon $time '; shorewall drop $ip"; } $unkn{$ip} = time; } $ucount{$ip}++; $ucount{'ALL'}++; } $now = time; # report stats every 2 hours if ($now - $statstime > 7200) { stats(); $statstime = $now; } # clean up old data every day if ($now - $flushtime > 86400) { flush($flushtime); $flushtime = $now; } #print STDERR "NOT MATCHED: $ip $msg\n"; } # report stats on exit stats();