A firewall is supposed to protect a network from potential attacks originating from another network. It does this by inspecting packets traveling between the two networks, and imposing restraints upon the types, targets and even content of these packets.
This section explains how to set up firewalling using PF. For this section I'll use a corporate network and firewall as an example. The network itself will consist of a TCP/IP network with network address 260.250.1.0/24. There will be a corporate webserver running on 260.250.1.3, and a firewall with two network interfaces, xl0 and xl1. The corporate network is connected to the xl1 interface, and the internet uplink is connected to xl0.
The firewalling component of PF uses a set of rules which describe actions to be taken for certain packets. These rules are read from a file and loaded into the OpenBSD kernel using the pfctl command (for more information on loading your ruleset, see section 2.11).
Such a rule file might look like the following:
block in all pass in all
Let's analyze what happens here. The first rule tells PF to block all incoming packets. Unlike some other firewalls, PF doesn't stop rule parsing when it finds a match. Instead, it notes the fact that it is planning on blocking the packet, and moves on to the next rule.
The next rule tells PF to pass all incoming packets to whatever is their destination. Again, PF notes the fact that it is planning on passing the packet, and moves on to the next rule.
Since there is no next rule, PF starts looking at what it was planning to do. In this case, the last rule that matched told PF to pass the packet, so it does.
Well, that doesn't look very useful, so let's try something more interesting. Let's allow access to the corporate webserver running at IP address 260.250.1.31. You might try something like this:
block in all pass in from any to 260.250.1.3/32
Again, the first rule tells PF to note that it should plan on blocking this packet unless it matches any other rules.
The second rule tells PF to pass packets that have their destination set to 260.250.1.3, where the '/32' denotes to PF that it should match the address on the full 32 bits.
But what about return traffic, you might ask. Well, that's fairly simple, simply allow traffic from 260.250.1.3 to go anywhere:
block in all pass in from any to 260.250.1.3/32 pass in from 260.250.1.3/32 to any
Suppose it is decided that another website will be run on the corporate webserver, this one containing sensitive corporate information, so it shouldn't be accessible from the outside. This website will, contrary to the other one, run on the non-standard TCP port 8000.
So now you'll not only have to filter on destination address, but on the TCP port number as well, to make sure nobody can connect to port 8000 from the outside and access the sensitive data:
block in all pass in proto tcp from any to 260.250.1.3/32 port = 80 pass in proto tcp from 260.250.1.3/32 port = 80 to any
As you can see, we're not only filtering on the port number, we're also telling PF to only allow packets of protocol TCP to pass through to the firewall to the corporate webserver. This is necessary, since the IP protocol itself doesn't know about port numbers. Only TCP and UDP can differentiate between different ports.
The PF firewall component is capable of remembering what TCP, UDP or ICMP sessions were created, and can filter packets according to this session table. This is called keeping state.
Whenever PF sees a packet match a rule that instructs PF to keep state, it creates a new entry in the state table based upon the information in the packet. This results in subsequent packets from the same session being passed along without going through the rule matching phase.
Keeping state on TCP connections involves the careful checking of TCP sequence numbers of packets against the state table, and dropping packets that don't match the state of the connection, thus decreasing the possibility that hosts behind the firewall lacking a good TCP stack implementation are taken advantage of. When keeping state on UDP sessions, PF allows a single return packet for each packet matching the rule which creates the state table entry.
Suppose it is decided that web-browsing should be possible from our corporate network, which requires passing both TCP connections to port 80 as well as allowing DNS requests on port 53.
Using the PF state engine for this is a safe way of allowing this without at the same time opening the entire network to outside attacks. While you're at it, you might as well use the state engine for the access to the corporate webserver as well, resulting in more secure access control to that server:
block in all pass in proto udp from 260.250.1.0/24 to any port = 53 keep state pass in proto tcp from 260.250.1.0/24 to any port = 80 keep state pass in proto tcp from any to 260.250.1.3/32 port = 80 keep state
The third rule enables hosts on the corporate network to make connections to the HTTP port on external hosts, instructing PF to create an entry in its state table for these connections. The last rule instructs PF to do the same for connections made from outside hosts to the corporate webserver, thus making obsolete the rule we needed for return traffic when we weren't using the state engine.
Using the state engine might seem like burdening the firewall with extra load, only slowing down traffic. However, state table lookups under PF are much faster than ruleset parsing. A typical ruleset of 50 rules takes about 50 rule comparisons, whereas a state table of 50,000 entries, due to its binary tree structure, takes only about 16 comparisons. This fact, combined with the added security, makes it more than worth using the state engine even for the more simple tasks, which could have easily been done without it.
Sometimes it might be appropriate to have PF immediately stop parsing the ruleset and do whatever it should do whenever a packet matches a specific rule. For this, PF has the quick keyword. A matching rule that has the quick option set will result in the termination of ruleset parsing for the matching packet.
This is especially useful for protecting your network against spoofed packets, as the following example shows:
block in all block in quick from 10.0.0.0/8 to any block in quick from 172.16.0.0/12 to any block in quick from 192.168.0.0/16 to any block in quick from 255.255.255.255/32 to any pass in all
This ruleset tells PF to immediately drop packets originating from 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 and 255.255.255.255/32. Other packets are passed through.
This also results in a big performance boost if used appropriately for rules catching lots of traffic, as PF won't try to match any subsequent rules.
It is also possible to match the network interface on which a packet is received. Let's adapt our previous anti-spoofing example to this, keeping our corporate network structure in mind:
block in all block in quick on xl0 from 10.0.0.0/8 to any block in quick on xl0 from 172.16.0.0/12 to any block in quick on xl0 from 192.168.0.0/16 to any block in quick on xl0 from 255.255.255.255/32 to any pass in all
To enable the blocking of invalid packets, you can also instruct PF to filter on TCP flags using the flags keyword, followed by a list of flags to match on, an optional forward slash, and an optional flag mask.
PF will, for every TCP packet, first clear everything but the flags that were specified in the mask, and then match on the flags that should be matched on. So saying 'flags S/SA' instructs PF to first mask out everything but the SYN and ACK flags, and then check if SYN is set.
The following flags are recognized:
For example, a packet requesting a new connection sets only the SYN flag, and a packet acknowledging a connection sets both SYN and ACK, whereas a packet indicating a refused connection sets both ACK and RST.
Using an invalid combination of TCP flags is a popular way to secretly scan hosts for open ports. Using the flags keyword, you can defend your system against these secretive scans, and force port scanners to use scanning methods that are more easily detectable.
Let's take our state-keeping example from earlier in this HOWTO. We want to enforce that only TCP packets, which of the SYN and ACK flags only have SYN set, get an entry in the state table:
block in all pass in proto udp from 260.250.1.0/24 to any port = 53 keep state pass in proto tcp from 260.250.1.0/24 to any port = 80 \ flags S/SA keep state pass in proto tcp from any to 260.250.1.3/32 port = 80 \ flags S/SA keep state
This should prevent the port scanning techniques mentioned above from passing our firewall.
It is possible to, instead of specifying a single source or destination host, specify a set of hosts. This is done by enclosing the hosts in curly braces, and by separating them by commas.
So if your old ruleset had rules in it like this:
block in quick on xl0 from 10.0.0.0/8 to any block in quick on xl0 from 172.16.0.0/12 to any block in quick on xl0 from 192.168.0.0/16 to any block in quick on xl0 from 255.255.255.255/32 to any
You can replace them with a single rule:
block in quick on xl0 from { 10.0.0.0/8, 172.16.0.0/12, \ 192.168.0.0/16, 255.255.255.255/32 } to any
This can also be done for interfaces, protocols, and ports. The pfctl program will split up such rules into one rule for each combination, so PF can optimize your ruleset with the technique described in section 2.9. Additionally, it increases readability by orders of magnitude for large sets of hosts, interfaces, protocols or ports.
PF also supports variable expansion, modelled after that of the shell. Variables are defined by assigning them a value, and expanded by prepending the variable name with a dollar sign ('$'):
webserver="260.250.1.3/32" pass in from any to $webserver port = 80 keep state
The value you want to assign to the variable must be quoted.
Unlike IPFilter, OpenBSD PF doesn't support the group keyword. The OpenBSD PF developers have chosen a scheme called skip steps, in which rulesets are optimized automatically.
Consider a ruleset looking like this:
block in quick on xl0 from 10.0.0.0/8 to any block in quick on xl0 from 172.16.0.0/12 to any block in quick on xl0 from 192.168.0.0/16 to any block in quick on xl0 from 255.255.255.255/32 to any
For each incoming packet, this ruleset is evaluated from top to bottom. Imagine a packet is received on interface xl1. The first rule is evaluated, but is found not to match. Now, since the other rules also apply to interface xl0, PF can safely skip these rules.
When you load a ruleset the following parameters are compared between successive rules (in this order):
For each rule, PF automatically calculates a so-called skip step for each of these parameters, which tells PF how many successive rules have the same value for the parameter.
If an incoming packet on interface xl1 is matched against our example ruleset, PF notices that the packet's incoming interface didn't match the one in the first rule, and since the next 3 rules also mention that interface, it skips these rules altogether.
So if you'd like to maximize your ruleset performance, you should sort your ruleset by interface, by protocol, source address and port, and finally by destination address and port, in that order.
Let's put together all that we have learned about PF using our earlier example of the corporate network. The following ruleset is the result:
# set up some variables external="xl0" internal="xl1" corporate="260.250.1.0/24" webserver="260.250.1.3/32" private="{ 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, \ 255.255.255.255/32 }" # block by default block in all # allow web-browsing from the corporate network pass in quick on $internal proto udp from $corporate to any \ port = 53 keep state pass in quick on $internal proto tcp from $corporate to any \ port = 80 flags S/SA keep state # drop spoofed packets block in quick on $external from $private to any # allow access to the corporate webserver from the internet pass in quick on $external proto tcp from any to $webserver \ port = 80 flags S/SA keep state
There you have it, a secure firewall. Of course, this is not the only thing neccesary for securing something like a corporate network. But for a first line of defense, it's doing its job quite well.
Once you're happy with your ruleset, save it as /etc/pf.conf, then either reboot your machine, or execute
pfctl -R /etc/pf.conf
To have your rule set loaded automatically during the OpenBSD boot sequence, remember to set pf=YES in /etc/rc.conf.