Hold your horses, it ain't finished yet!

This is still very much an experimental anti-spam system.

The idea is to monitor the responses our mail server is sending out to others, and try to figure out who is not playing the game fairly!

During each mail exchange using the SMTP protocol, there is a small amount of chatter between the two servers while they establish who each other is, what their capabilities are and so on. Once that's over, the sending server says who the mail is from, and our server will do a quick check to see if it knows them or not and if it will accept mail from them or not (for example, if they are listed in the ordb database, the server will reject the connection before it gets any further, likewise if the senders IP address, domain name or specific user is listed in our local reject list).

After the sender is known, the sending mail server gives the address of the intended recipient (or indeed, a list of many recipients). For each address given, our server will send back a response indicating if it will accept mail, or if the user is unknown (and therefore we won't accept the mail), or even if the user has their mail blocked locally.

Here's the tricky part: we only monitor the replies sent back, and the IP address that it is sent to, nothing more. Each time a response is sent back saying "User unknown", we bump a counter for that particular IP address.

If the counter gets to some pre-set limit, we immediately add a "null route" for the offending source IP address, which prevents us sending any further replies to that address. Eventually, the sending server gives up on us and terminates the connection.

The whole process "dies" every hour, and is re-started. This helps eliminate any memory leaks and (more importantly) is a convenient way to re-set all the counters. This make sure that the system will only trigger on the pre-set number of User unknown responses within the same hour.

The code to do this is pretty simple, and should be readily adapted to most versions of unix. It gets run by cron every hour, with a simple entry in /etc/crontab:
# Watch for and de-route spammers
1      *      *      *      *      root    /root/deroute clean
The script that runs makes all sorts of presumptions, is probably NOT an example of best programming practice, but it was hacked together quickly for a specific purpose and not really intended to be let loose! For those who really want to play with it, here it is:
#! /bin/sh

# Fix (de-route) spamming scumbags
# %1 is ip address of source
# %2 is duration in hours (defaults to 12 hours if not specified)
# Special case is if #1 is "clean", will remove expired entries
# and stay in monitoring mode until next time
# Please note, this was originally developed for sendmail/FreeBSD
# Notes, where relevant, point out known changes for Linux
#
# Also note, deroutedump is an executable. Download the source
# from http://www.albury.net.au/~rossw/deroutedump.c and compile
# with the command   gcc -o deroutedump deroutedump.c -lpcap
#
# to manually invoke the dropping of an IP...
#     deroute ip hrs reason
# eg  deroute 1.2.3.4 12 Spammer
#

# comma seperated list of regular expressions to reject in "HELO" and "EHLO"
matchlist="localhost,203.15.244.13,202.3.39.,mail.albury.net.au,mail3.albury.net.au,albury.net.au,202.3.36.15"

# comma seperated list of domain parts to reverse check in HELO/EHLO 
watchdomains="yahoo.,hotmail.,.msn.,.aol.,microsoft.com,youkickedmydog.net,compuserve.com,westpac.com.au,citibank.com,combank.com.au"

list=/root/derouted.ip.addresses.list	# Wherever you choose to store the current list
log=/root/derouted.ip.addresses.log	# Wherever you choose to store a permanent log
localIP="203.17.23[45].\|203.15.244.\|64.39.30.40\|203.42.178\|202.3.39."	# Our local IP addresses. These ones never get derouted
hitcount=4				# How hard do you want to be?
					# 1 = "bastard" mode - one strike and they're out.
					# 4 = "default" mode - on the 4th strike, they're out.
					# But feel free to choose whatever you like to suit your needs.
MaxPerCblock=4				# If you get more than this addresses in a single
					# c-class ir /24 address, block the whole netblock.
firewall=bester.albury.net.au		# Where the ipfw firewall lives

#echo "`date` `whoami` $@ -->$0<--" >>dr.called.log

case "$1" in

"clean") {
	# I always had "ps -axw", but linux sooks about it. FreeBSD seems happy without it,
	# so thats whats here. If your particular FreeBSD needs it, put it back in!
	ps axw | grep "deroutedump -lts 200 port 25" | grep -v grep | awk '{system("kill -HUP "$1)}' 


	./deroutedump -lts 200 port 25 2>/dev/null | awk -v pat="$matchlist" -v wdm="$watchdomains" -v count=$hitcount '
		BEGIN{split(tolower(pat),m,",") 
		      split(wdm,wd,",")}

		{cmd=""}
		/ [hHeE][EeHh][lL][oO] / {
			#print strftime("%d/%b/%Y-%H:%M:%S"),$0 >> "helo.log"
			a=0
			for(i in m) if(gsub(m[i], "&", $4)) a++		# If helo claims to be us, increment a
			for(i in m) if(gsub(m[i], "&", $1)) a=0		# Unless source address is us, then its ok.
			#if(a) system("echo /root/reportip "$1)
			#if(a) printf("Bad HELO from %-15s --> %s : %s\n", $1, $4, $0) >> "helo.log"
			if(a) cmd="12 Impersonator"
			ip=$1
			if(gsub("\\.",".",$NF)<3 && index($0," 502 EHLO ")==0)
			{
				# Refer RFC 2821, section 3.6
				cmd="1 badHELO"
 				print strftime("%d/%b/%Y-%H:%M:%S"),"badHELO.",$0 >> "helo.log";
			}
                	d=""
			for(i in wd) if(index(tolower($NF),wd[i])) d=wd[i]
			if(d) {
				c="host "ip" 2>/dev/null"; $0=""
				c | getline; close(c);
				if(index(tolower($0),d)==0) 
				{
					cmd="1 Liar-"d
 					print strftime("%d/%b/%Y-%H:%M:%S"),"LIAR. "ip" HELO says",d,"DNS says",$0 >> "helo.log";
				}
			}
		}
		/ 550 / {
			c=substr($0,37)
			ip=$2
			if(index(c,"User unknown"))	
				if(++baddies[ip] == count) cmd="12 Unknown"

			if(index(c,"Relaying denied"))		# kill for an hour if they try to relay through us
				if(++baddies[ip] == count) cmd="8 Relay"

			if(index(c,"You fell for the trap"))	# kill for an hour if they hit our trap addresses
				if(!baddies[ip]++) cmd="12 Trap"

			if(index(c,"refused by blackhole"))	# kill for an hour if they in an RBL
				if(!baddies[ip]++) cmd="12 Blacklist"
		}
		/ 501 / {
			if(index(c,"Domain of sender address"))	# kill for a few hours if they dont resolve
				if(!baddies[ip]++) cmd="4 Unresolved"
		}
		
		{
			if(cmd != "")
			{
				#print "About to call deroute ip="ip" cmd="cmd >> "dr.rw.log"
				system("/root/deroute "ip" "cmd" &")	
			}
		}'


	# When we get here, the sniffer has been killed, so time to tidy up.
	expire=`date +"%d/%b/%Y-%H"`
	#echo "$$ About to expire ->$expire<-" >> "dr.expire.log"
	for ip in `grep "^$expire" $list | awk '{print $2}' | sort | uniq`
	do
		echo `date +"%d/%b/%Y-%H:%M "`Restoring $ip >> $log
		# Command to actually unblock an IP address, either locally, or at some arbitary router/firewall
		# But if we use the remote ipfw solution we dont need to do anything
		# ssh your.border.router "/sbin/route -q delete $ip >/dev/null"'	# ssh to router and block
		# /sbin/route -q delete $ip >/dev/null					# For FreeBSD
		# /sbin/route delete -host $ip reject >/dev/null			# For Linux
	done
	if [ -r $list.tmp ]; then sleep 1; fi
	grep -v "^$expire" $list > $list.tmp
	mv $list.tmp $list
};;

"rotate") {
	now=`date +"%d%b%Y"`
	mv helo.log helo.log.$now
	mv $log $log.$now
};;

*) {
	# If it is a local address, exit now.
	echo "$1" | grep "$localIP" >/dev/null && exit
	# If it passes other criteria to NOT deroute, exit now.
	grep "^popper : $1 : allow" /etc/hosts.allow >/dev/null && exit
	#strings /usr/local/etc/dracd.db | grep "^$1" >/dev/null && exit   # this works if you run dracd
	grep "^$1.*RELAY" /etc/mail/access >/dev/null && exit		# If they're allowed to relay the're ok

	# Otherwise, de-route this address.
	t=${2:-12}		# use hours supplied, or 12 if not.
	expire=`date -v+${t}H +"%d/%b/%Y-%H:%M"`				# FreeBSD works this way
	# expire=`date -d "now + $1 hours" +"%H:%M:%S"				# Linux does it this way
	rule=`echo $expire | sed 's/.*-/5/; s/://'`				# create rule number 5hhmm (50000-52359)
	blockIP=$1
	killProcess=$1
	Cblock=`echo $blockIP | sed 's/\.[0-9][0-9]*$//'`
	#n=`grep $Cblock $list | awk '{print $2}' | sort | uniq`		# How many times does this range already exist?
	n=`grep $Cblock $list | awk '{ip[$2]++} END{ for(i in ip) n++; print n+0}'`
	if [ $n -ge $MaxPerCblock ]; then
		#echo "IP=$blockIP  Cblock=$Cblock  Previous: $n"
		for ip in `grep " $Cblock\." $list | awk '{print $2}'`
		do
			echo `date +"%d/%b/%Y-%H:%M "`Restoring $ip >> $log
			# Command to unblock an IP address, either locally, or at some arbitary router/firewall
			# ssh your.border.router "/sbin/route -q delete $ip >/dev/null"'# ssh to router 
			#/usr/local/bin/ssh $firewall "ipfw add $rule drop ip from $ip to any"
			#echo `date +"%d/%b/%Y-%H:%M:%S"` ssh $firewall "ipfw add $rule drop ip from $ip to any $?" >> dr.log
			#/sbin/route -q delete $ip >/dev/null				# For FreeBSD
			#/sbin/route delete -host $ip reject >/dev/null			# For Linux
		done
		if [ -r $list.tmp ]; then sleep 1; fi
		grep -v " $Cblock\." $list >$list.tmp
		mv $list.tmp $list

		blockIP="$Cblock.0/24"		# and ugrade the block to a whole /24
	fi
	echo "$expire $blockIP $3" >> $list
	echo `date +"%d/%b/%Y-%H:%M "`De-routing $blockIP $3 >> $log
	# Command to actually block an IP address, either locally, or at some arbitary router/firewall
	#ssh your.border.router "/sbin/route -q add $blockIP 127.0.0.1 >/dev/null"	# ssh to router and block
	/usr/local/bin/ssh $firewall "ipfw -q add $rule deny ip from $blockIP to any" >/dev/null
	#echo `date +"%d/%b/%Y-%H:%M:%S"` "ipfw add $rule drop ip from $blockIP to any $?" >> dr.log
	#/sbin/route -q add $blockIP 127.0.0.1 >/dev/null &				# For FreeBSD
	#/sbin/route add -host $blockIP reject >/dev/null &				# For Linux

	# And with the amount of stuff hitting us of late, kill any matching processes (this is optional)
	ps axw | grep "sendmail:.*${killProcess}" | awk '/sendmail: / {system("kill "$1)}' 2>/dev/null 1>/dev/null
};;

esac