PF (Packet Filter)

PF is the preferred packet filtering/firewalling solution on BSD derivatives (similar to Linux's IPTables). This document describes PF in general, and a particular project to create a corporate firewall.

Project Summary

We'll repurposed a 1U server to replace a very aged firewall appliance with an OpenBSD PF firewall. This makes our firewall more secure, more flexible, and easier to service or replace in the event of a hardware failure.

We'll call this new firewall box `argus`.

                       |                 |
    NIC0 (LAN) <---> [=                   =] <---> NIC2 (T1 Internet)
         inside        |      argus      |           outside
    NIC1 (DMZ) <---> [=                   =] <---> NIC3 (Cable Internet)
                       |                 |

We'll have four network interface cards:

The outside internet connections (NIC2 and NIC3) will be configured for mutual failover—if one goes down, the other one will take over its duties as much as possible. Core internet services like mail and vendor integration will default to the more reliable T1 (NIC2). Less critical traffic like employee web access will default to the faster cable connection (NIC3).

Network address translation will occur for clients on the inside LAN and the DMZ, with a couple of exceptions. The T1 (NIC2) will have its own static IP address, and also be aliased with the IP address of our public web server.

PF Overview

PF is OpenBSD's Packet Filter subsystem. PF inspects and handles network packets at the protocol and port level. PF classifies packets based on protocol, port, packet type, source or destination address. With a reasonable degree of certainty it is also able to classify packets based on source operating system.

Although, strictly speaking, NAT is not packet filtering, for practical reasons PF handles NAT too. The same if true of load balancing and traffic shaping—they've been rolled into PF for practical reasons.

PF does not inspect packet _contents_ (i.e.—application level stuff), although PF can hand off such application level filtering to other applications in some cases.

All of these technologies are configured in the `/etc/pf.conf` config file, and controlled with the `pfctl` command line tool.

Performance & Requirements

PF performs as well or better under stress than OpenBSD's former IPFilter system or Linux iptables on the same hardware. The filtering overhead of PF is negligible. A Pentium III with 512MB RAM should be an adequate PF firewall.

Preliminary OpenBSD Configuration

export PKG_PATH=

pkg_add vim

Unlike Linux, where network devices are named like `eth0`, `eth1`, etc., BSD network devices are named after their driver (`em0`, `em1`, `sk0`, and `sk1` on argus).

On recent versions of OpenBSD, PF is enabled by default, so we don't need to turn it on or start it.

Turn on packet forwarding so we act like a gateway. The command sudo sysctl net.inet.ip.forwarding=1 turns on forwarding, but for the change to persist after reboot, add these lines to /etc/sysctl.conf:


Tools & Commands

To check the rules of the /etc/pf.conf file without actually loading them:

pfctl -nf /etc/pf.conf

To actually load the rules in /etc/pf.conf:

sudo pfctl -f /etc/pf.conf

Report on pf's status:

sudo pfctl -s info

View each rule:

sudo pfctl -vgs rules

N.B.: PF reads rules from top to bottom; the last rule in a rule set that matches a packet or connection is the one that is applied. However, the quick keyword (e.g.—pass quick in...) tells pf to not look any further down the list of rules if the packet matches this rule.

A Minimal pf.conf

block in all
pass out all

The first line (block in all) blocks all inbound traffic. The second line (pass out all) allows all outbound traffic.

Lists and Macros

Lists and macros increase the readability of a rule set. A list is two or more things of the same type contained in curly brackets. A macro is a variable—a variable assigned with an equal sign and dereferenced with a dollar sign.

pass proto tcp to port {21 80 8080}
my_special_host =

pass proto tcp to $my_special_host port 8080

PF automatically understands the mappings between ports and service names defined in /etc/services, so anywhere in pf.conf you can use a port number like "22" it's also valid to use a service name like "ssh".

Final pf.conf for argus

######## Macros ########
lan_if = "nic0"
dmz_if = "nic1"
t1_if = "nic2"
cable_if = "nic3"
lan_network = $lan_if:network
dmz_network = $dmz_if:network
t1_network = $t1_if:network
cable_network = $cable_if:network
public_internet = {$t1_network, $cable_network}
public_webserver = "***.***.215.142"
public_mailserver = "***.***.204.214"
vital_udp_services = "{domain, ntp}"

######## NAT ########
match out on $t1_if from $lan_network nat-to ($t1_if)
match out on $cable_if from $lan_network nat-to ($cable_if)
match out on $t1_if from $dmz_network nat-to ($t1_if)
match out on $cable_if from $dmz_network nat-to ($cable_if)

######## Rules ########
block all
pass from {lo0, $lan_network}
pass quick inet proto {tcp, udp} from {$lan_network, $dmz_network} to port $vital_udp_services
pass proto tcp to $public_webserver port http
pass from $dmz_network to $public_internet port {http, https}
pass inet proto tcp from $lan_network to $dmz_network port ssh

Further Reading

If you're a serious/professional PF user, I strongly recommend purchasing _The Book of PF_ by Peter N.M. Hansteen.