ipfilter (now pf) for dummies

Our mail setup uses Postini to handle spam and viruses. It costs money but does a decent job, with no admin required (anyway, changing that isn't an option at the moment). The trouble is with the spammers who ignore MX and scan for open SMTP ports, and thus bypass Postini.

Having just upgraded our mail server to 7.0-RELEASE (yeah, started too early for 7.1), I want to address this issue, too. I spent most of the day fighting with the mailfromd milter, but I think it's not the right tool in the end.

So I'm looking for a dead-simple ipfilter configuration:
  • allow all traffic not on port 25
  • allow port 25 from 64.18.0.0/20
  • allow port 25 from 207.126.144/20
  • disallow other port 25

So trying to figure out what I need ...
Code:
pass out on bge0 from any to any
pass in on bge0 from any to any port < 25
pass in on bge0 from any to any port > 25
pass in on bge0 from 64.18.0.0/20 to any port = 25
pass in on bge0 from 207.126.144/20 to any port = 25
pass in on bge0 from 127.0.0.1 to any port = 25
block in on bge0 from any to any port = 25

Does this make sense? I'm always hesitant about firewall rules lest I block myself out! :\
 
tomh009 said:
I'm always hesitant about firewall rules lest I block myself out!

I can't evaluate your ipfilter ruleset (I've used only pf and ipfw), but your concern is definitely legit.

I recommend using an at(1) job for insurance against this. I wrote up a brief example here: http://daemonforums.org/showthread.php?t=887

I do the same thing on both FreeBSD (pf) and RHEL (iptables) boxes to be sure I don't get locked out.
 
anomie said:
I can't evaluate your ipfilter ruleset (I've used only pf and ipfw), but your concern is definitely legit.

Should I use ipfw instead of ipf? It wasn't clear to me which would be the most appropriate tool for this.

Thanks for the suggestion on at(1).
 
Well, what I can tell you is that pf and ipfw (in that order) certainly seem to be more popular. i.e. You'll likely get better peer/community support using one of those two.
 
All right ... that's the trouble with so many tools, never know which one is the best! So trying to adapt to the pf.conf syntax (not so much different), does this look reasonable?
Code:
pass out from any to any
pass in proto tcp from any to any port < 25
pass in proto tcp from any to any port > 25
pass in proto tcp from 64.18.0.0/20 to any port = 25 label "postini-1"
pass in proto tcp from 207.126.144/20 to any port = 25 label "postini-2"
pass in proto tcp from 127.0.0.1 to any port = 25 label "local"
block in proto tcp from any to any port = 25 label "spam"
pass in from any to any
 
Your last rule annihilates everything else and lets everything in.
PF and IPFILTER don't stop matching rules when one hits ;)

I'd change it slightly to this:
Code:
ext_if="bge0"
block in all
# Allow traffic to/from localhost
pass in quick on lo0 all
pass out quick on lo0 all

# We trust our own host :}
pass out on $ext_if from any to any keep state

# SMTP in
pass in quick on $ext_if proto tcp from 64.18.0.0/20 to any port = 25 label "postini-1" keep state
pass in quick on $ext_if proto tcp from 207.126.144/20 to any port = 25 label "postini-2" keep state

You can "short-circuit" rules with the quick keyword. The "keep state" keeps track of tcp/ip sessions, so you don't have to worry about the returning answer.
Not sure what you want to do with the labels though. They're only used in the logs. I have these:
Code:
block in log on $ext_if inet proto udp all label "BlockIn_ExtIF_UDP"
block in log on $ext_if inet proto icmp all label "BlockIn_ExtIF_ICMP"
block return-rst in log on $ext_if inet proto tcp all label "BlockIn_ExtIF_TCP"

Edit: Oh.. remote editing a firewall can be slightly dangerous indeed :e
Code:
# check syntax:
# pfctl -nf /etc/pf.conf
# test: run rules for 60 sec. then disable firewall
# pfctl -f /etc/pf.conf && sleep 60 && pfctl -d
# green light :)
# pfctl -f /etc/pf.conf
 
tomh009 said:
So trying to adapt to the pf.conf syntax (not so much different)

Just a couple references, if you haven't already located them on your own, to get you up to speed quickly on pf:

The first is a thorough pf tutorial and reference. The second contains some important FreeBSD-specific pf notes.

To elaborate on SirDice's point, there is one key gotcha when using pf for packet filtering: the last matching rule wins (with some exceptions, but no need to split hairs yet). This is very different than e.g. ipfw, iptables (Linux), Cisco FWSM, etc.
 
SirDice said:
Your last rule annihilates everything else and lets everything in.
PF and IPFILTER don't stop matching rules when one hits ;)

Ah -- last-match rather than first-match. Good to know! :)

So using that principle, and opening things up some more, I'm thinking that this might possibly work -- what do you think?

Code:
ext_if="bge0"
block in all
# Allow traffic to/from localhost
pass in quick on lo0 all
pass out quick on lo0 all

# Default to allow all, unless matched later
pass in on $ext_if from any to any keep state
pass out on $ext_if from any to any keep state

# Block SMTP by default
block in log on $ext_if inet proto tcp from any to any port = 25 label "Block SMTP"

# Allow SMTP from Postini
pass in quick on $ext_if proto tcp from 64.18.0.0/20 to any port = 25 label "postini-1" keep state
pass in quick on $ext_if proto tcp from 207.126.144/20 to any port = 25 label "postini-2" keep state

And thanks for the tip on testing!
 
tomh009 said:
Ah -- last-match rather than first-match. Good to know! :)
Yes. The quick keyword short-circuits this, in case of lo0 traffic (our ruleset :e ) it'll stop matching any of the rules following it.

So using that principle, and opening things up some more, I'm thinking that this might possibly work -- what do you think?

If you're going to allow all traffic anyway you could loose that block in all at the beginning. And the quick keywords in the last 2 rules are a bit pointless. But it should work fine as is though :)
 
After figuring out that the pf and pflog modules are not loaded by default (used kldload for now, and added to loader.conf for the future), I got it running. The script worked great, no trouble there. And within a few minutes ...
Code:
montecarlo 95 # tcpdump -n -r /var/log/pflog      
reading from file /var/log/pflog, link-type PFLOG (OpenBSD pflog file)
19:10:47.559784 IP 58.60.97.164.2056 > *.*.98.4.25: S 3141617944:3141617944(0) win 65535 <mss 1440,nop,nop,sackOK>
19:10:50.477568 IP 58.60.97.164.2056 > *.*.98.4.25: S 3141617944:3141617944(0) win 65535 <mss 1440,nop,nop,sackOK>
19:10:56.487070 IP 58.60.97.164.2056 > *.*.98.4.25: S 3141617944:3141617944(0) win 65535 <mss 1440,nop,nop,sackOK>

Ah-ha! So who is that?

Code:
montecarlo 97 # whois -A 58.60.97.164
% [whois.apnic.net node-1]
% Whois data copyright terms    http://www.apnic.net/db/dbcopyright.html

inetnum:      58.60.0.0 - 58.63.255.255
netname:      CHINANET-GD
descr:        CHINANET Guangdong province network
descr:        China Telecom
(...)

The usual suspects at work -- but foiled by pf! :e

Thanks very much for the help, guys!
 
To prevent problems with TCP window scaling please add flags S/SA to those 'keep states' rules.
Code:
pass in quick on $ext_if proto tcp from 64.18.0.0/20 to any port = 25 label "postini-1" [color=blue] flags S/SA[/color] keep state
pass in quick on $ext_if proto tcp from 207.126.144/20 to any port = 25 label "postini-2" [color=blue] flags S/SA[/color] keep state
From http://undeadly.org/cgi?action=article&sid=20060928081238
Create TCP states on the initial SYN packet

Ideally, TCP state entries are created when the first packet of the connection, the initial SYN is seen. You can enforce this by following a simple principle:

Use 'flags S/SA' on all 'pass proto tcp keep state' rules!

All initial SYN packets (and only those packets) have flag SYN set but flag ACK not set. When all your 'keep state' rules that can apply to TCP packets are restricted these packet, only initial SYN packets can create states. Therefore, any TCP state created is created based on an initial SYN packet.

The reason for creating state only on initial SYN packets is a TCP extention called 'window scaling' defined in RFC 1323.....
 
J65nko said:
To prevent problems with TCP window scaling please add flags S/SA to those 'keep states' rules.

Doesn't scrub in all take care of that?
 
Scrub handles various things like reassembling/normalisation of fragments, protecting/hardening of sequence numbers and/or timestamps, sequence number wrapping (paws), but does not handle window scaling. One could call it 'window cleaning' at best ;)
 
Back
Top