#!/usr/bin/perl # Name: spamfirewall # Description: Program to firewall spammers and their mailservers # Version: 1.2 # Authors: Jason Jorgensen # Copyright (C) 2003 Jason Jorgensen # # 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 ########################################################################### # User Variables $threshold = 2; # number of spam emails received in 1 day to trigger blockage $offenseweight = 1; $expireblock = 432000; # number of seconds the block is in effect for $expirequeue = 172800; $expireoffender = 432000; # number of seconds the block is in effect for $time = time; # 1 hour = 3600 # 1 day = 86400 # 1 week = 432000 my $version = '1.2'; my $spamchain = 'INPUT_SPAM'; my $linkchain = 'INPUT'; my $iptablesbin = 'iptables'; my $iptablesopt = '-n -v -x --line-numbers -L'; # always use the parameter for list chain last. that parameter is currently '-L' my $cmdlink = "$iptablesbin -I $linkchain 1 -j $spamchain"; my $cmdmake = "$iptablesbin -N $spamchain"; my $spamtarget = 'DROP'; # what should the target for the spam be? I suggest DROP or REJECT my $dbuser = 'root'; my $dbhost = '127.0.0.1'; my $dbport = 3306; my $dbpassword = 'PASSWARD'; my $dbtype = 'mysql'; my $dbname = 'spamfirewall'; ########################################################################### use DBI; use DBIx::Broker; use NetAddr::IP; $debug = 0; $db = DBIx::Broker->new($dbtype, $dbname, $dbhost, $dbport, $dbuser, $dbpassword); if (! $db) { die "Could not establish a connection to the database. Dying\n"; } @blocks = $db->select_all('blocked', "", 1); @queued = $db->select_all('queue', "", 1); @repeats = $db->select_all('repeatoffenders', "", 1); print "Totals.. Blocks:".($#blocks+1)." Queued:".($#queued+1)." RepeatOffenders:".($#repeats+1)."\n"; %blocks; %queued; my %importantips = &importantips(); print "ARGV[0] :$ARGV[0]\n" if $debug; if ($ARGV[0] =~ /search/) { &search(); } elsif ($ARGV[0] =~ /unblock/) { &unblock($ARGV[1]); } elsif ($ARGV[0] =~ /add/) { &addentry(); } elsif ($ARGV[0] =~ /.*\?|.*help/) { &usage(); } else { &defaultmaintenance(); } exit; sub usage { print "spamfirewallmaintenance Version $version\n"; print "spamfirewallmaintenance \n"; print " Comands\n"; print " unblock - Unblock an IP in the block\n"; print " search - Search for an IP in the block or queue list(NOT IMPLEMENTED YET)\n"; print "\n If no command is specified, normal maintenance is run\n"; } sub search { print "SEarChORinG!!!!111!!!H!!!!\n"; } sub defaultmaintenance { # Remove important ip blocks foreach $ip (keys %importantips) { print "DEBUG: Unblocking important ip $ip\n" if $debug; &unblock($ip); &unqueue($ip); } # Remove Blocked Dups foreach $block (@blocks) { if ($blocks{$block->{'address'}}) { print "DEBUG: Unblocking duplicate $block->{'blockedid'} - $block->{'address'}\n" if $debug; &unblockbyid($block->{'blockedid'}); } else { $blocks{$block->{'address'}}++; } } # Remove Queued Dups foreach $queue (@queued) { if ($queued{$queue->{'address'}}) { print "DEBUG: Removing duplicate $queue->{'queueid'} - $queue->{'address'}\n" if $debug; &unqueue($queue->{'address'}); } else { $queued{$queue->{'address'}}++; } } undef @blocks; undef @queued; undef %blocks; undef %queued; @blocks = $db->select_all('blocked', "", 1); @queued = $db->select_all('queue', "", 1); @repeats = $db->select_all('repeatoffenders', "", 1); print "After pruning... Blocks:".($#blocks+1)." Queued:".($#queued+1)." RepeaterOffenders:".($#repeats+1)."\n"; # Expire blocks foreach $block (@blocks) { # first get offender level to see if there is a multiplier on the expire time for the block $level = 1; if ($db->count('repeatoffenders', "WHERE address='".$block->{'address'}."'")) { $level = $db->select_one_value('level', 'repeatoffenders', "WHERE address='".$block->{'address'}."'", 0); } if (($block->{'time'} + ($expireblock * $level)) <= $time) { print "DEBUG: expiring block: $block->{'address'} with a time of ".localtime($block->{'time'})."\n" if $debug; &unblockbyid($block->{'blockedid'}); &queueminus($block->{'address'}); } } # Expire queues that havent been blocked foreach $queue (@queued) { #print "DEBUG: (($queue->{'count'} < $threshold) \&\& ((".($queue->{'time'} + $expirequeue).") <= $time)) ??\n" if $debug; if ((! $db->count('blocked', "WHERE address='".$queue->{'address'}."'")) && (($queue->{'time'} + $expirequeue) <= $time)){ print "DEBUG: expiring queue: $queue->{'address'} with a time of ".localtime($queue->{'time'})." and a count of $queue->{'count'}\n" if $debug; &queueminus($queue->{'address'}); } } # Expire/decriment offender levels for offenders that are not currently blocked, and have been good since their last offense. foreach $repeat (@repeats) { if (($db->count('repeatoffenders', "WHERE address='".$repeat->{'address'}."'")) && (($repeat->{'time'} + $expireoffender) <= $time) && (! $db->count('blocked', "WHERE address='".$queue->{'address'}."'"))){ print "DEBUG: expiring repeat: $repeat->{'address'} with a time of ".localtime($repeat->{'time'})." and a level of $repeat->{'level'}\n" if $debug; &offenderminus($repeat->{'address'}); } } undef @blocks; undef @queued; @blocks = $db->select_all('blocked', "", 1); @queued = $db->select_all('queue', "", 1); @repeats = $db->select_all('repeatoffenders', "", 1); print "After expiring.. Blocks:".($#blocks+1)." Queued:".($#queued+1)." RepeaterOffenders:".($#repeats+1)."\n"; &verifyfirewall(); } sub unqueue { $address = shift; @unqueues = $db->select_all('queue', "WHERE address='$address'", 1); foreach $unqueue (@unqueues) { print "DEBUG: sub unqueue: unqueueing address $address: $unqueue->{'queueid'}" if $debug; $db->delete('queue', "WHERE queueid='$unqueue->{'queueid'}'"); } } sub block { my $address = shift; print "iptables -I $spamchain 1 -s $address -j DROP\n" if $debug; my %data = ( "address", $address, "time", time, ); $db->insert('blocked', \%data); system "iptables -I $spamchain 1 -s $address -j DROP\n"; } sub reblock { my $address = shift; print "iptables -I $spamchain 1 -s $address -j DROP WITHOUT MYSQL ENTRY\n" if $debug; system "iptables -I $spamchain 1 -s $address -j DROP\n"; } sub unblockraw { my $address = shift; print "DEBUG: sub unblockiptables: unblocking address $address: $address" if $debug; system "iptables -D $spamchain -s $address -j DROP\n"; } sub unblock { my $address = shift; @unblocks = $db->select_all('blocked', "WHERE address='$address'", 1); foreach $unblock (@unblocks) { print "DEBUG: sub unblock: unblocking address $address: $unblock->{'blockedid'}" if $debug; $db->delete('blocked', "WHERE blockedid='$unblock->{'blockedid'}'"); system "iptables -D $spamchain -s $unblock->{'address'} -j DROP\n"; } if ((! @unblocks) && ($ARGV[0])) { print "No address matching '$address' was found in the block list\n"; } } sub unblockbyid { my $id = shift; $address = $db->select_one_value('address', 'blocked', "WHERE blockedid='$id'", 0); print "DEBUG: sub unblock: unblocking id: $id, address: $address" if $debug; $db->delete('blocked', "WHERE blockedid='$id'"); system "iptables -D $spamchain -s $address -j DROP\n"; } sub unqueue { my $address = shift; $db->delete('queue', "WHERE address='$address'"); } sub offenderminus { my $address = shift; $origlevel = $db->select_one_value('level', 'repeatoffenders', "WHERE address='$address'", 0); $level = $origlevel; if ($level >= $threshold) { $level = ($threshold - 1); } elsif ($level >= 1) { $level--; } else { $level = 0; } if ($level == 0) { print "DEBUG: sub queueminus: deleting entry for $address since level is going to 0\n" if $debug; $db->delete('repeatoffenders', "WHERE address='$address'"); } else { my %data = ( "level", $level, "time", time, ); print "DEBUG: sub queueminus: setting $address level from $origlevel to $level\n" if $debug; $db->update('repeatoffenders', \%data, "WHERE address='$address'"); } } sub queueminus { my $address = shift; $origcount = $db->select_one_value('count', 'queue', "WHERE address='$address'", 0); $count = $origcount; if ($count >= $threshold) { $count = ($threshold - 1); } elsif ($count >= 1) { $count--; } else { $count = 0; } if ($count == 0) { print "DEBUG: sub queueminus: deleting entry for $address since count is going to 0\n" if $debug; $db->delete('queue', "WHERE address='$address'"); } else { my %data = ( "count", $count, "time", time, ); print "DEBUG: sub queueminus: setting $address count from $origcount to $count\n" if $debug; $db->update('queue', \%data, "WHERE address='$address'"); } } sub importantips { # grab some important ip addresses that we should never block my %importantips; # grab the ip networks of the currently running interfaces open IFCONFIG, "ifconfig|"; while (my $line = ) { if ($line =~ /inet\ addr/) { ($addr) = ($line =~ /inet addr:(\d{1,3}.\d{1,3}.\d{1,3}.\d{1,3})/); ($mask) = ($line =~ /mask:(\d{1,3}.\d{1,3}.\d{1,3}.\d{1,3})/); if (!$mask) { $mask = '255.255.255.255'; } print "DEBUG: address: $addr\n" if $debug; print "DEBUG: subnet mask: $mask\n" if $debug; my $ipman = new NetAddr::IP "$addr/$mask"; $importantips{$ipman->prefix} = 1; } } close IFCONFIG; # grab the ip of the default gateway open ROUTE, "route -n|"; while (my $line = ) { if ($line =~ /^0.0.0.0\ /) { ($addr) = ($line =~ /^0.0.0.0\s+(\d{1,3}.\d{1,3}.\d{1,3}.\d{1,3})/); print "DEBUG: address: $addr\n" if $debug; $importantips{$addr} = 1; } } close ROUTE; return %importantips; } sub verifyfirewall { my %linkchain = &readchain($linkchain); my %spamchain = &readchain($spamchain); my %iptablesblocks; my %mysqlblocks; # check to see if spamchain even exists if (! %spamchain) { system "$cmdmake"; } # make sure spamchain is referenced from linkchain my $linkedproper = 0; foreach my $rule (keys %linkchain) { #print "DEBUG: \$linkchain{$rule}{target}: $chain{$rule}{target}\n" if $debug; #print "DEBUG: if ($linkchain{$rule}{target} eq $spamchain) { $linkedproper = 1; last; }\n" if $debug; if ($linkchain{$rule}{target} eq $spamchain) { $linkedproper = 1; last; } } if (! $linkedproper) { print "DEBUG: '$spamchain' not properly linked to '$linkchain', linking now...\n" if $debug; system "$cmdlink"; } else { print "DEBUG: '$spamchain' linked to '$linkchain' just fine.\n" if $debug; } # put all iptables blocks into an easy to use hash please $iptableblockcount = 0; foreach my $rule (keys %spamchain) { $iptablesblocks{$spamchain{$rule}{source}} = 1; $iptableblockcount++; } print "Blocks in iptables.. $iptableblockcount\n"; # put all mysql blocks into an easy to use hash please $iptableblockcount = 0; foreach my $entry (@blocks) { $mysqlblocks{$entry->{'address'}} = 1; $mysqlblockcount++; } print "Blocks in mysql.. $mysqlblockcount\n"; # go through each mysql rule. if its not in iptables, add it my $reblockcount = 0; foreach my $entry (@blocks) { if (! $iptablesblocks{$entry->{'address'}}) { print "DEBUG: block in mysql not in iptables, blocking now: $entry->{'address'}\n" if $debug; &reblock($entry->{'address'}); $reblockcount++; } } print "Reblocks in iptables.. $reblockcount\n"; my $unblockcount = 0; foreach my $rule (keys %spamchain) { if (! $mysqlblocks{$spamchain{$rule}{source}}){ print "DEBUG: block in iptables not in mysql, unblocking now: $spamchain{$rule}{source}}\n" if $debug; &unblockraw($spamchain{$rule}{source}); $unblockcount++; } } print "Unblocks in iptables.. $unblockcount\n"; } sub readchain { my $chain = shift; my %chain; print "open CHAIN, \"$iptablesbin $iptablesopt $chain\";\n" if $debug; open CHAIN, "$iptablesbin $iptablesopt $chain 2>&1|"; while (my $line = ) { my @line = split /\s+/, $line; my $num = $line[0]; my $pkts = $line[1]; my $bytes = $line[2]; my $target = $line[3]; my $prot = $line[4]; my $opt = $line[5]; my $in = $line[6]; my $out = $line[7]; my $source = $line[8]; my $destination = $line[9]; # @therest = ($line[8]..$line[$#line]); if (($num =~ /\d+/) && ($pkts =~ /^\d+.*/) && ($bytes =~ /^\d+.*/) && ($source =~ /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/) && ($destination =~ /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/)) { print "DEBUG: $num\t$pkts\t$bytes\t$target\t$prot\t$pot\t$in\t$out\t$source\t$destination\n" if $debug; $chain{$num}{pkts} = $pkts; $chain{$num}{bytes} = $bytes; $chain{$num}{target} = $target; $chain{$num}{prot} = $prot; $chain{$num}{opt} = $opt; $chain{$num}{in} = $in; $chain{$num}{out} = $out; $chain{$num}{source} = $source; $chain{$num}{destination} = $destination; } } close CHAIN; return %chain; }