paulgorman.org/technical

pf on FreeBSD

PF (packet filter) is one of the firewall technologies available on FreeBSD. PF originated from OpenBSD, although the two versions have since diverged significantly; FreeBSD uses the same version of PF as OpenBSD 4.5.

PF is a last-matching-rule-wins firewall. (The “quick” keyword on a rule means to stop and not evaluate subsequent rules.)

See pf.conf(5) and pfctl(8) and /usr/share/examples/pf/*.

Enable PF in /etc/rc.conf with:

pf_enable="YES"
pflog_enable="YES"

By default, pf loads its rules from /etc/pf.conf. This is a very simple PF config file:

block in all
pass out all keep state

Start PF (and PF logging):

# service pf start
# service pflog start

The pfctl utility configures rulesets and parameters, and retrieves status info from PF.

pfctl -e                        Enable PF.
pfctl -d                        Disable PF.
pfctl -F all -f /etc/pf.conf    Flush all NAT, filter, state, and table rules and reload /etc/pf.conf.
pfctl -s [ rules | nat | states ]    Report on the filter rules, NAT rules, or state table.
pfctl -vnf /etc/pf.conf         Check /etc/pf.conf for errors, but do not load ruleset.
pfctl -k host                   Kill all state entries originating from "host"
pfctl -s states -vv             Show state ID's, ages, and rule numbers
pfctl -s rules -vv              Show rules with stats and rule numbers
pfctl -s Tables                 List tables
pfctl -t foo -T show            Show the contents of table "foo"
pfctl -t foo -T delete xx.xx.xx.xx    Delete address "xx.xx.xx.xx" from table "foo"

Gateway Firewall Example

First, set:

# sysctl net.inet.ip.forwarding=1
# sysctl net.inet6.ip6.forwarding=1

Add to /etc/rc.conf:

gateway_enable="YES"
ipv6_gateway_enable="YES"

And create the rules /etc/pf.conf:

########################
## Macros
########################
ext_if = "em0"
int_if = "em1"
wifi_if = "wlan0"
int_server = "10.0.1.10"
int_nets = "{ 10.0.1.0/24, 10.0.2.0/24 }"
tcp_pass_out = "{ bootpc, bootps, dhcpv6-client, dhcpv6-server, domain, https, ipp, nicname, ntp, ssh, www, 6667, 6697 }"
udp_pass_out = "{ bootpc, bootps, dhcpv6-client, dhcpv6-server, domain, nicname, ntp }"
icmp_ok_types = "{ echoreq, unreach }"
########################
## Tables
########################
table <friendly_ip_addrs> { 192.0.2.17, 198.51.100.223, 203.0.113.110 }
table <sshprobe> persist
########################
## Options
########################
set skip on lo
########################
## Normalization
########################
scrub in
########################
## Translation
########################
nat on $ext_if inet from !($ext_if) to any -> ($ext_if)
rdr on $ext_if proto tcp from any to any port 8000 -> $int_server
########################
## Filtering
########################
block in
antispoof for $ext_if
antispoof for $int_if
antispoof for $wifi_if
pass quick on $ext_if from <friendly_ip_addrs> keep state
block quick from <sshprobe>
pass in inet proto tcp from any to $ext_if port ssh keep state (max-src-conn 5, max-src-conn-rate 3/5, overload <sshprobe> flush global)
pass proto tcp from $int_nets to any port $tcp_pass_out keep state
pass proto udp from $int_nets to any port $udp_pass_out keep state
pass proto tcp from $ext_if to any port $tcp_pass_out keep state
pass proto udp from $ext_if to any port $udp_pass_out keep state
pass inet6 proto tcp from fe80::/10 to any port $tcp_pass_out keep state
pass inet6 proto udp from fe80::/10 to any port $udp_pass_out keep state
pass inet6 proto icmp6 from any
pass inet proto icmp icmp-type $icmp_ok_types keep state
pass in from $int_nets to $int_if keep state    # Anti-lockout rule

Note the use of variables like $ext_if, macros like udp_pass_out = "{ domain, ntp }", and tables like table <friendly_ip_addrs> { 192.0.2.17, 198.51.100.223, 203.0.113.110 }.

Scrub before any NAT or redirection rules. Define NAT and redirection rules before filtering rules.

Gotcha: DHCP External Interface

If the external interface receives its address via DHCP, the PF rules may fail to load if FreeBSD tries before the interface gets its address. Edit /etc/rc.conf. Change ifconfig_em0="DHCP" to ifconfig_em0="SYNCDHCP".