In my previous post I described how to set up OpenBSD and enable simple routing between interfaces. This time an explanation about one of the core features: pf, or packet filter.

The pf service allows for stateful firewalling and NAT translations when routing. Starting where we took off in part I, we have an OpenBSD 5.0 with routing enabled and interfaces configured. pf is one of the few things to be active by default, so it’s already running now, but it doesn’t do anything as no rules have been defined yet. Defining rules has to be done in /etc/pf.conf . How? Again, by using a text editor like vi. Yes, that’s ‘vi /etc/pf.conf’ on the command line. There are already some basic rules defined in pf.conf, most of them commented out as examples.

A filtering rule consists of several keywords. A complete guide is available as a manual page, but I’ll list the basics here:

  • Most used are pass and block. The first permits a packet based on parameters defined further in the rule, the second drops the packet.
  • Next is in or out. Either filter incoming or outgoing on an interface. Incoming takes place before a route look-up so filtering mostly on incoming will take less CPU.
  • This one is optional: quick. Without it, pf will check all rules and treat a packet according to the last matching rule. However, if a rule contains the word quick and the packet matches, any following rules are no longer checked. Using this keyword makes the filtering behave more like a Cisco ACL (top-to-bottom), and will take less CPU as not all rules have to be examined every time.
  • on, followed by an interface, e.g. em0. It defines on which interface the filtering takes place.
  • proto, followed by a protocol, mostly tcp, udp or icmp. It’s an optional keyword: if it’s missing, filtering will be done with layer 3 information (IPv4, IPv6) only.
  • from defines where from. It can be any, an ip, or a subnet, e.g.
  • port is optional after this and defines the source port(s). It uses operators, e.g. port = 2000 is port 2000, port < 5000 is any port below 5000, and so on. Since it’s a source port and most applications use dynamic source ports, this isn’t used that much.
  • to defines the destination host or subnet. Same parameters as from.
  • A second port, again optional, after to. This is the destination port. Since these are usually well-known ports, it’s often used.

Many more options and keywords are possible, but knowing the above is sufficient for decent stateful firewalling.

The second part is address translation, or NAT. A basic setup is as following:

  • match is the first keyword. You can also use pass, but it will pass everything unless you define certain ports in the rule also.
  • Next are in or out, with out being the most used (and easiest to troubleshoot too, in my opinion).
  • Then: on for the interface, from and to. proto and port are optional.
  • Last: nat-to, followed by an (external) IP. It defines what the host or subnet defined in from will NAT to.

We can try this in an example. Let’s assume the OpenBSD has interface em0 connected to an ISP (IP and em1 to an internal network This shows what can be done with it:

match out quick on em0 from to any nat-to

pass out quick on em0 proto tcp from to any port = 80
pass out quick on em0 proto udp from to any port = 53
block out on em0

The above will NAT any internal address from em1 to the external IP. Everything will be blocked (the last rule), except TCP traffic to port 80 (which is http traffic) and UDP port 53 to IP (which are DNS requests to the IP of Google’s DNS).

The above works, but it’s a very incomplete example of course: no https (port 443) is possible for example, as are pings and the ability to choose another DNS server in case of a DNS problem. But it demonstrates the point. Don’t forget to do ‘:wq’ to save your changes!

Now just one more thing: these rules aren’t activated by default: you’ll need ‘pfctl -nf /etc/pf.conf’ and ‘pfctl -f /etc/pf.conf’. The first commands checks if the rules don’t contain any syntax errors, which you should always check, and the second command loads the rules in the current configuration.