Greylisting with PFby Dan Langille
This article will show you how I am using PF (the Packet Filter from the OpenBSD project) and
spamd (also from OpenBSD) to implement greylisting and greatly reduce incoming spam. This solution is completely MTA agnostic. You do not have to make any changes to your existing mail server configuration to use
spamd/PF. In fact, you can easily use PF and
spamd to guard any SMTP mail server; even MS Exchange.
I first read about PF when reading Micheal Lucas's Absolute OpenBSD. If you have never read one of his books, I urge you to do so. His writing style is very clear and easy to follow. The book contains about thirty pages on PF, yet there is so much packed into that short chapter that when I read it I knew one day I'd start using PF. That day came a few weeks ago. My gateway at home has been happily running PF since early October. And more importantly, I've been happy. One of the best features of any software product is simplicity of use. PF is simple to start and easy to extend when you need the more advanced features.
On a related note, I'll also give you a short introduction to OS fingerprinting and how you can use PF to block/pass packets based on the sending OS.
How Greylisting Works
spamd and PF work together. PF will direct any known good clients directly to the mail server. Similarly, it will send known bad clients to the tarpit, a slow-responding mail server that does not add much load to your system. Finally, it asks new clients, not known to be good or bad, to try again later.
This makes three lists of clients:
- Whitelist: known good clients.
- Blacklist: known bad clients.
- Greylist: we don't know if they are good or bad yet, but we will soon decide.
The key to greylisting is knowing that good and kind mail servers do not mind being politely asked to come back later. This is part of the STMP protocol. Spammers, however, are cheap and nasty. They don't like this. They probably won't come back later. Why? Queue management is tricky. If you're sending to millions of email addresses, why worry about a few messages that you cannot deliver? Just skip along to the next one. Spammers work on volume. Greylisting exploits that characteristic.
There are several ways to enable PF, including compiling it into the kernel, or loading the module. I added a few options to my /etc/rc.conf file to ensure PF starts up at boot time. It also starts the logging daemon.
pf_enable="YES" pflog_enable="YES" pf_rules="/etc/pf.rules"
Note that I have chosen a nondefault value for my PF rules. The default value, as found in /etc/default/rc.conf, is /etc/pf.conf. To avoid any merge conflicts with
mergemaster(8), I chose a different filename. The default install comes with many fine examples in /etc/pf.conf and I urge you to read them.
Designing a PF rule set is beyond the scope of this article, but the OpenBSD project has a good PF rule set example.
The primary interface between PF and the outside world is
pfctl. To load PF on a running system, issue the command:
# kldload pf # kldstat Id Refs Address Size Name 1 8 0xc0400000 6721fc kernel 2 1 0xc0a73000 58554 acpi.ko 3 1 0xc4eb5000 16000 linux.ko 4 1 0xc5e20000 2d000 pf.ko
What does loading PF give you? To see all the filter parameters:
# pfctl -s all No ALTQ support in kernel ALTQ related functions disabled FILTER RULES: INFO: Status: Disabled Debug: None Hostid: 0x595cedd1 State Table Total Rate current entries 0 searches 0 0.0/s inserts 0 0.0/s removals 0 0.0/s Counters match 0 0.0/s bad-offset 0 0.0/s fragment 0 0.0/s short 0 0.0/s normalize 0 0.0/s memory 0 0.0/s bad-timestamp 0 0.0/s congestion 0 0.0/s ip-option 0 0.0/s proto-cksum 0 0.0/s state-mismatch 0 0.0/s state-insert 0 0.0/s state-limit 0 0.0/s src-limit 0 0.0/s synproxy 0 0.0/s TIMEOUTS: tcp.first 120s tcp.opening 30s tcp.established 86400s tcp.closing 900s tcp.finwait 45s tcp.closed 90s tcp.tsdiff 30s udp.first 60s udp.single 30s udp.multiple 60s icmp.first 20s icmp.error 10s other.first 60s other.single 30s other.multiple 60s frag 30s interval 10s adaptive.start 0 states adaptive.end 0 states src.track 0s LIMITS: states hard limit 10000 src-nodes hard limit 10000 frags hard limit 5000