Sample PF for Stable

NOTE: This guide is no substitute for reading the Packet Filter Guide. In particular, you must read the Basic Configuration section and the NetAdmin Code.

Ruleset with Explanation

Here's a sample /etc/pf.conf for stable servers (do NOT use this for shell servers). We include a rule-by-rule explanation. For the complete ruleset (without commentary), see the next section.

ExtIf = "vio0"
IP4 = "10.0.0.1"
UnIP4 = "192.168.0.1"
IP6 = "2001:db8::/80"

ExtIf is the external interface (vio0 for BuyVM and VMM users), IP4 is your DDoS-filtered IPv4 address, UnIP4 is your secret unfiltered IPv4 address, and IP6 is your IPv6 subnet range.

FlushUDP = "max-pkt-rate 10000/10 keep state (max 1000, source-track rule, max-src-nodes 200, max-src-states 200)"
Flush = "keep state (max 1000, source-track rule, max-src-nodes 200, max-src-conn-rate 500/10 overload <badhosts> flush global)"
FlushStrict = "keep state (max 100, source-track rule, max-src-nodes 20, max-src-conn-rate 50/10 overload <badhosts> flush global)"

This defines 3 macros.

For FlushUDP, if the packet rate exceeds 10000 packets per 10 seconds, PF will refuse to process any further packets. It will keep track of state for ICMP and UDP packets; if there are more than 1000 state entries, it will stop accepting new packets. If there are more than 200 unique IPs in the state entry table, or if a single IP has more than 200 entries, it will stop accepting new connections.

For Flush, if there are 1000 state entries, it will stop accepting new connections. If there are more than 200 unique IPs in the state entry table, or if a single IP makes more than 500 connections in 10 seconds, it will disconnect all connections from this user and add them to the table badhosts.

FlushStrict is the same but more strict. If there are 100 state entries, it will stop accepting new connections. If there are more than 20 unique IPs in the state entry table, or if a single IP makes more than 50 connections in 10 seconds, it will disconnect all connections from this user and add them to the table badhosts.

set skip on lo0
set loginterface $ExtIf
#set ruleset-optimization profile
set syncookies adaptive (start 25%, end 12%)

We skip filtering on loopback (localhost). We are going to log all packets that pass through the external interface vio0. You can view these using tcpdump in /var/log/pflog*. You can optionally optimize the ruleset based on the profile, but I have not yet tested to see if the optimization is intelligent, so I left it commented out. We will use syncookies to defend against synflood? attacks.

table <ilines> persist file "/etc/pf/ilines"
table <badhosts> persist file "/etc/pf/badhosts"

We load two tables, one with ilines (with IRCNow-approved IPs), and another with a list of badhosts (known criminals and enemies).

block in log quick from <badhosts>
pass in log quick proto udp to {$IP4 $IP6} port domain $FlushUDP
pass in log quick proto udp to {$UnIP4 $IP6} port ntp $FlushUDP
pass in log quick proto udp to {$IP4 $IP6} port {isakmp ipsec-nat-t} $FlushUDP
block in log quick proto udp to {$IP4 $IP6}
block in log quick from urpf-failed
match in log all scrub (no-df random-id max-mss 1440)

We immediately block all packets from badhosts. We pass in all DNS UDP packets on the DDoS-filtered IPv4 and IPv6 subnet (but not the secret unfiltered IPv4 address). We pass in all NTP UDP packets for the unfiltered IPv4 address and IPv6 subnet, but not the DDoS-filtered IPv4 address because NTP packets get mangled by DDoS-filtering.

WARNING: Please follow the ntpd guide to set it up properly -- if you do not, your system's time will be wrong, causing all sorts of hard to troubleshoot problems like issues with nsd.

pass in log quick on $ExtIf inet proto icmp icmp-type 8 code 0 $FlushUDP # icmp packets
pass in log quick on $ExtIf inet proto icmp icmp-type 3 code 4 $FlushUDP # icmp needfrag (MTU)
pass in log quick on $ExtIf proto ipv6-icmp $FlushUDP

We allow in ICMP and ICMPv6 packets passing through our external interface. NOTE: Do not block ICMP packets, or else strange and hard to diagnose problems can occur. For example, blocking ICMPv6 packets can interfere with proper IPv6 routing.

pass in log quick proto tcp to {$IP4 $IP6} port domain $Flush
pass in log quick proto tcp to {$IP4 $IP6} port auth $Flush
pass in log quick proto tcp to {$IP4 $IP6} port {smtp submission smtps imap imaps pop3 pop3s} $Flush
pass in log quick proto tcp to {$IP4 $IP6} port {gopher http https} $Flush
pass in log quick proto tcp from <ilines> to {$IP4 $IP6} port { 6660:6669 6697 6997 7000 9999 16667 16697 } #irc
pass in log quick proto tcp to {$IP4 $IP6} port { 6660:6669 6697 6997 7000 9999 16667 16697 } $Flush #irc
pass in log quick proto tcp to {$IP4 $IP6} port { 1314 13140 1337 31337 } $Flush #bnc
pass in log quick proto tcp to {$IP4 $IP6} port { 12742 29173 } $Flush #wraith
pass in log quick proto tcp to {$IP4 $IP6} port 7777 $Flush #paster
pass in log quick proto tcp to {$IP4 $UnIP4 $IP6} port ssh $FlushStrict

We immediately pass in all TCP packets for the public IPv4 address and IPv6 subnet if it's for DNS (domain), ident (auth), sending mail (smtp submission smtps), reading mail (imap imaps pop3 pop3s), gopher, the web (http https).

If the sender is present on our ilines, we pass in all IRC traffic without normal Flush limits. If not, we have normal Flush limits.

We immediately pass in all TCP packets for the public IPv4 address and IPv6 subnet if it's headed for the bouncer, wraith, or the paster.

For ssh, we allow incoming packets to the secret, unfiltered IPv4 address and we apply more strict rules to prevent bruteforce attacks. The unfiltered IPv4 address will provide a hidden backdoor to access the server in case of a DDoS attack.

# road warrior vpn
pass in log inet proto udp to {$IP4 $IP6} port {isakmp, ipsec-nat-t} tag IKED
pass in log inet proto esp to {$IP4 $IP6} tag IKED
pass log on enc0 inet tagged ROADW
match out log on $ExtIf inet tagged ROADW nat-to $IP4
match in log quick on enc0 inet proto { tcp, udp } to port 53 rdr-to 127.0.0.1 port 53

This section is for IPSec VPNs using iked.

block in log all
block out log on $UnIP4
pass out quick from {$IP4 $IP6} # allow non-spoofed packets
pass out quick proto tcp from $UnIP4 to port ssh
pass out quick proto udp from $UnIP4 to port ntp
pass out quick proto {udp tcp} from $UnIP4 to port {domain}
pass out quick inet proto icmp from $UnIP4 # allow ICMP

We block all incoming packets but not immediately. By default, if the quick keyword is missing, the last rule that matches applies to a packet. We then block all outgoing packets from the secret, unfiltered IPv4 address except whitelisted traffic for ssh, ntp, dns, and ICMP packets.

Complete Ruleset

Here's the /etc/pf.conf for stable servers (do NOT use this for shell servers) without any commentary:

ExtIf = "vio0"
IP4 = "10.0.0.1"
UnIP4 = "192.168.0.1"
IP6 = "2001:db8::/80"
FlushUDP = "max-pkt-rate 10000/10 keep state (max 1000, source-track rule, max-src-nodes 200, max-src-states 200)"
Flush = "keep state (max 1000, source-track rule, max-src-nodes 200, max-src-conn-rate 500/10 overload <badhosts> flush global)"
FlushStrict = "keep state (max 100, source-track rule, max-src-nodes 20, max-src-conn-rate 50/10 overload <badhosts> flush global)"

set skip on lo0
set loginterface $ExtIf
#set ruleset-optimization profile
set syncookies adaptive (start 25%, end 12%)

table <ilines> persist file "/etc/pf/ilines"
table <badhosts> persist file "/etc/pf/badhosts"

# udp and icmp
block in log quick from <badhosts>
pass in log quick proto udp to {$IP4 $IP6} port domain $FlushUDP
pass in log quick proto udp to {$UnIP4 $IP6} port ntp $FlushUDP
pass in log quick proto udp to {$IP4 $IP6} port {isakmp ipsec-nat-t} $FlushUDP
block in log quick proto udp to {$IP4 $IP6}
block in log quick from urpf-failed
match in log all scrub (no-df random-id max-mss 1440)
pass in log quick on $ExtIf inet proto icmp icmp-type 8 code 0 $FlushUDP # icmp packets
pass in log quick on $ExtIf inet proto icmp icmp-type 3 code 4 $FlushUDP # icmp needfrag (MTU)
pass in log quick on $ExtIf proto ipv6-icmp $FlushUDP
# tcp
pass in log quick proto tcp to {$IP4 $IP6} port domain $Flush
pass in log quick proto tcp to {$IP4 $IP6} port auth $Flush
pass in log quick proto tcp to {$IP4 $IP6} port {smtp submission smtps imap imaps pop3 pop3s} $Flush
pass in log quick proto tcp to {$IP4 $IP6} port {gopher http https} $Flush
pass in log quick proto tcp from <ilines> to {$IP4 $IP6} port { 6660:6669 6697 6997 7000 9999 16667 16697 } #irc
pass in log quick proto tcp to {$IP4 $IP6} port { 6660:6669 6697 6997 7000 9999 16667 16697 } $Flush #irc
pass in log quick proto tcp to {$IP4 $IP6} port { 1314 13140 1337 31337 } $Flush #bnc
pass in log quick proto tcp to {$IP4 $IP6} port 29173 $Flush #wraith
pass in log quick proto tcp to {$IP4 $IP6} port 7777 $Flush #paster
pass in log quick proto tcp to {$IP4 $UnIP4 $IP6} port ssh $FlushStrict

# road warrior vpn
pass in log inet proto udp to {$IP4 $IP6} port {isakmp, ipsec-nat-t} tag IKED
pass in log inet proto esp to {$IP4 $IP6} tag IKED
pass log on enc0 inet tagged ROADW
match out log on $ExtIf inet tagged ROADW nat-to $IP4
match in log quick on enc0 inet proto { tcp, udp } to port 53 rdr-to 127.0.0.1 port 53

block in log all
block out log on $UnIP4
pass out quick from {$IP4 $IP6} # allow non-spoofed packets
pass out quick proto tcp from $UnIP4 to port ssh
pass out quick proto udp from $UnIP4 to port ntp
pass out quick proto {udp tcp} from $UnIP4 to port {domain}
pass out quick inet proto icmp from $UnIP4 # allow ICMP

You will then need to create a folder:

$ doas mkdir /etc/pf/

Then, add the list of ilines to /etc/pf/ilines.

198.251.89.130
198.251.83.183
209.141.39.184
209.141.39.228
198.251.84.240
198.251.80.229
198.251.81.119
209.141.39.173
198.251.89.91
198.251.81.44
209.141.38.137
198.251.81.133
2605:6400:0030:f8de::/64
2605:6400:0010:071b::/64
2605:6400:0020:0434::/64
2605:6400:0020:00b4::/64
2605:6400:0010:05bf::/64
2605:6400:0030:fc15::/64
2605:6400:0020:1290::/64
2605:6400:0020:0bb8::/64
2605:6400:0030:faa1::/64
2605:6400:0010:069d::/64
2605:6400:0020:05cc::/64
2605:6400:0010:00fe::/64

Afterwards, any badhosts can be added to /etc/pf/badhosts:

0.0.0.0/8
127.0.0.0/8
169.254.0.0/16
172.16.0.0/12
192.0.0.0/24
192.0.2.0/24
224.0.0.0/3
192.168.0.0/16
198.18.0.0/15
198.51.100.0/24
203.0.113.0/24

These are all reserved IPs and no authentic traffic should come from these source IPs. Note that we deliberately leave out 10.0.0.0/8 because we will use this subnet for IPSec.

To load the new configuration:

$ doas pfctl -f /etc/pf.conf

Troubleshooting

WARNING: When you apply new firewall rules, make sure to test that all services are working after the rules have been applied. If you do not test, you might break something for users and not notice it for days or weeks!

To test, connect to each and every one of the services you provide, both from your home IP address and another proxy (vpn, vps) that you have.

Please also set aside 24-48 hours to monitor any bug reports from users.

See Also

PF GuideDDoS Filtering Guidetcpdump