Wednesday, September 19, 2007

Blocking Spam with Sendmail

Like a great many people on the net, I found myself increasingly annoyed by the rising tide of spam. By which I do not mean the delectible Hormel canned meat product (which the little lady especially likes fried in a fried egg sandwich), but the phenomenon of unsolicited commercial email.

Spam is unlike regular junk mail from the post office for several reasons:


Normal junk mail costs money to send. Spam does not. The cost is born by the recipients and by the servers in between. While normal junk mail is a revenue source for the post office and actually helps to pay for delivering regular mail, spam pays for nothing. Spam can hence be viewed as theft of services.

Spam has detrimental effects. Historically Internet mail servers have passed along email between third parties as a gesture of friendship and good will. Much like tossing your neighbor's paper over the fence if the paperboy misses his throw. This is happening less and less because spammers take advantage of it.

Spam reduces the utility of email. People become discouraged about checking their mailboxes if they are always cluttered with spam.

Enough ranting. I decided to do something. Being the sort who favors technical fixes over legal ones, I started doing some research on the web, ordered a copy of the Bat book, and spent some time reading my sendmail configuration and scratching my head. I present here the result.
First, if you're not using sendmail I can't help you. Second, you need the latest version of sendmail or these tricks won't work. And finally, we had several conditions that had to be met:


We wanted to be able to block spam by domain name, network number, or by specific address for maximum flexibility.

We needed to be able to allow spam to selected mailboxes for customers who do not want spam blocked. I may disagree with them, but they are paying for a service and it is, after all, their mail. Spam blocking had to be a value added service that could be turned off.

We host a number of "virtual domains" and needed to be able to route email for them to the proper mailboxes. We had already been doing that, but it was a factor that had to be considered in our antispam measures so that spam could be blocked or not as desired by the mailbox owners.

We wanted to stop "third party relay" going through our mail server while allowing for exceptions for customers with their own domains and mail servers or for roaming customers.

I think I have come up with a set of sendmail rules which accomplish this.
First, we need to add a few entries in the local configuration section:


LOCAL_CONFIG

Fw/etc/vdomain.cw
Kvdomain hash /etc/vdomain

# list of people who like spam
F{fools} /etc/WantSpam

# list of known spammers
Kjunk hash -a@JUNK /etc/spammers

# List of network addresses we will relay for
F{LocalIP} /etc/LocalIP

# List of domains we will relay to
F{RelayTo} /etc/RelayTo


Click on the filename of any of these files for an explanation of its purpose and contents.
Now we get to the rules themselves. First, an entry must be added to your local rule zero, like so:

LOCAL_RULE_0
R$* $: $>vmap $1

Not very interesting, is it? It just calls another rule set, named "vmap", which handles virtual domain address mapping. Note: I don't know what the "right way" is to do these things, but it works to just list all the rest of these rulesets right under LOCAL_RULE_0, so that's what I do. Here then is the "vmap" ruleset:
Svmap
R$+ < @ $+ . > $: $1 < @ $2 > .
R$+ < @ $+ > $* $: $(vdomain $1@$2 $: $1 < @ $2 > $3 $)
R$+ < @ $+ > $* $: $(vdomain $2 $: $1 < @ $2 > $3 $)
R$+ < @ $+ > . $: $1 < @ $2 . >

I made this a separate ruleset since I do it again in the rest of the rules, as you will see. I have obscure reasons for not just calling local rule zero as needed.
Next I define a "junk" ruleset to look up a domain name or email address in the /etc/spammers.db database:

Sjunk
R$* $: $(junk $1$) look for host in spammer list
R$+@JUNK $@ $1@JUNK return message if found
R@JUNK $@ Spam refused @JUNK generic message
R$-.$+ $: $1 . $>junk $2 retry skipping lead subdomain
R$-.$+@JUNK $@ $2@JUNK return message if found

Next, a "junkIP" ruleset to look up an IP address or network number in the /etc/spammers.db database:
SjunkIP
R$* $: $(junk $1$) look for host in spammer list
R$+@JUNK $@ $1@JUNK return message if found
R@JUNK $@ Spam refused @JUNK generic message
R$+.$- $: $2 . $>junkIP $1 retry without trailing number
R$-.$+@JUNK $@ $2@JUNK return message if found
R$-.$+ $@ $2.$1 fix order if not spammer

Now for the heart of it, the "check_rcpt" ruleset. Spam blocking is more often done in the "check_mail" ruleset, but we can't do it that way since we need to check the recipient to see if they want spam. Hence, this ruleset gets a bit long.
Scheck_rcpt
R$* $: $>vmap $>3 $1 normalize address

# Refuse to relay mail between nonlocal systems
R$* $: $(dequote "" $&{client_addr} $) $ $1
R0 $ $* $@ ok no client addr: directly invoked
R$={LocalIP}$* $ $* $@ ok from here
R$* $ $* $: $2 not from local, check recipient
R$*<@$=w.>$* $>3 $1 $3 remove our aliases, maybe repeatedly
R$*<@$*$={RelayTo}.>$* $>3 $1 $4 remove domains we relay to
# still something left?
R$*<@$+>$* $#error $@ 5.5.4 $: "554 we do not relay from " $&{client_name} " to " $1@$2$3

# Allow mail to fools who like spam, and otherwise block spammers
R$={fools} $@ ok recipient listed as wanting spam

# Block by host or domain name
R$* $: $(dequote "" $&{client_name} $)
R$* $: $>junk $1
R$*@JUNK $#error $@ 5.5.4 $: "554 " $1 ": " $&{client_name}

# Block by network or host IP address
R$* $: $(dequote "" $&{client_addr} $)
R$* $: $>junkIP $1
R$*@JUNK $#error $@ 5.5.4 $: "554 " $1 ": " $&{client_addr}

# Block by specific email address
R$* $: $(dequote "" $&f $)
R$* $: $>junk $1
R$*@JUNK $#error $@ 5.5.4 $: "554 " $1 ": " $&f
R$* @ $* $: $1 @ $>junk $2
R$* @ $*@JUNK $#error $@ 5.5.4 $: "554 " $2 ": " $&f

# Block mail from invalid addresses
R$* $: $>3 $1 make domain canonical
R$* < @ $+ .> $* $@ ok name resolved ok
# Killer case -- single token domain
R$* < @ $- > $* $#error $@ 5.5.1 $: "551 Invalid host name: " $2
# Delay case -- domain doesn't resolve
R$* < @ $+ > $* $#error $@ 4.5.1 $: "451 Unknown domain: " $2

And that's it. If you'd like, you can download a text version of this for easier editing.
Oh, one last thing. The rejection messages all get logged in /var/log/maillog (at least on our system). Here's a PERL script for maillog.scan that gives us a nightly report of spam blocks:

#!/usr/bin/perl

while($lt;$gt;) {
if(/rejection:.*\.\.\. ? ?(.*)/) {
$spam{$1} += 1;
}
}

print "\nSpam blocks:\n\n";

foreach $msg (sort keys %spam) {
printf "%5d %s\n", $spam{$msg}, $msg;
}

print "\n";



Anand Shah

No comments: