Use OpenBSD’s firewalling features to protect your network.
PacketFilter, commonly known as PF, is the firewalling system available in OpenBSD. While it is a relatively new addition to the operating system, it has already surpassed IPFilter, the system it has replaced, in both features and flexibility. PF shares many features with Linux’s Netfilter. Although Linux’s Netfilter is more easily extensible with modules, PF outshines it in its traffic normalization capabilities and enhanced logging features.
To communicate with the kernel portion of PF, we need to use the pfctl command. Unlike the iptables command that is used with Linux’s Netfilter, it is not used to specify individual rules, but instead uses its own configuration and rule specification language. To actually configure PF, we must edit /etc/pf.conf. PF’s rule specification language is actually very powerful, flexible, and easy to use. The pf.conf file is split up into seven sections, each of which contains a particular type of rule. Not all sections need to be used—if you don’t need a specific type of rule, that section can simply be left out of the file.
The first section is for macros. In this section you can specify variables to hold either single values or lists of values for use in later sections of the configuration file. Like an environment variable or a programming-language identifier, macros must start with a letter and also may contain digits and underscores.
Here are some example macros:
EXT_IF=”de0″
INT_IF=”de1″
RFC1918=”{ 192.168.0.0/16, 172.16.0.0/12, 10.0.0.0/8 }”
A macro can be referenced later by prefixing it with the $ character:
block drop quick on $EXT_IF from any to $RFC1918
The second section allows you to specify tables of IP addresses to use in later rules. Using tables for lists of IP addresses is much faster than using a macro, especially for large numbers of IP addresses, because when a macro is used in a rule, it will expand to multiple rules, with each one matching on a single value contained in the macro. Using a table adds just a single rule when it is expanded.
Rather than using the macro from our previous example, we can define a table to hold the nonroutable RFC 1918 IP addresses:
table <rfc1918> const { 192.168.0.0/16, 172.16.0.0/12, 10.0.0.0/8 }
The const keyword ensures that this table cannot be modified once it has been created. Tables are specified in a rule in the same way that they were created:
block drop quick on $EXT_IF from any to <rfc1918>
You can also load a list of IP addresses into a table by using the file keyword:
table <spammers> file “/etc/spammers.table”
If you elect not to use the const keyword, then you can add addresses to a table by running a command such as this:
pfctl -t spammers -T add 10.1.1.1
Additionally, you can delete an address by running a command like this:
pfctl -t spammers -T delete 10.1.1.1
To list the contents of a table, you can run:
pfctl -t spammers -T show
In addition to IP addresses, hostnames may also be specified. In this case, all valid addresses returned by the resolver will be inserted into the table.
The next section of the configuration file contains options that affect the behavior of PF. By modifying options, we can control session timeouts, defragmentation timeouts, state-table transitions, statistic collection, and other behaviors. Options are specified by using the set keyword. The number of options is too numerous to discuss all of them in any meaningful detail; however, we will discuss the most pertinent and useful ones.
One of the most important options is block-policy. This option specifies the default behavior of the block keyword and can be configured to silently drop matching packets by specifying drop. Alternatively, return may be used, to specify that packets matching a block rule will generate a TCP reset or an ICMP unreachable packet, depending on whether the triggering packet is TCP or UDP. This is similar to the REJECT target in Linux’s Netfilter.
For example, to have PF drop packets silently by default, add a line like this to /etc/pf.conf:
set block-policy drop
In addition to setting the block-policy, additional statistics such as packet and byte counts can be collected for an interface. To enable this for an interface, add a line similar to this to the configuration file:
set loginterface de0
However, these statistics can only be collected on a single interface at a time. If you do not want to collect any statistics, you can replace the interface name with the none keyword.
To better utilize resources on busy networks, we can also modify the session-timeout values. Setting this to a low value can help improve the performance of the firewall on high-traffic networks, but at the expense of dropping valid idle connections.
To set the session timeout (in seconds), put a line similar to this in /etc/pf.conf:
set timeout interval 20
With this setting in place, any TCP connection that is idle for 20 seconds will automatically be reset.
PF can also optimize performance on low-end hardware by tuning its memory use regarding how many states may be stored at any one time or how many fragments may reside in memory for fragment reassembly. For example, to set the number of states to 20,000 and the number of entries used by the fragment reassembler to 15,000, we could put this in our pf.conf:
set limit states 20000
set limit frags 15000
Alternatively, we could combine these entries into a single one, like this:
set limit { states 20000, frags 15000 }
Moving on, the next section is for traffic normalization rules. Rules of this type ensure that packets passing through the firewall meet certain criteria regarding fragmentation, IP IDs, minimum TTLs, and other attributes of a TCP datagram. Rules in this section are all prefixed by the scrub keyword. In general, just putting scrub all is fine. However, if necessary, we can get quite detailed in specifying what we want normalized and how we want to normalize it. Since we can use PF’s general filtering-rule syntax to determine what types of packets a scrub rule will match, we can normalize packets with a great deal of control.
One of the more interesting possibilities is to randomize all IP IDs in the packets leaving your network for the outside world. In doing this, we can make sure that passive operating system determination methods based on IP IDs will break when trying to figure out the operating system of a system protected by the firewall. Because such methods depend on analyzing how the host operating system increments the IP IDs in its outgoing packets, and our firewall ensures that the IP IDs in all the packets leaving our network are totally random, it’s pretty hard to match them against a known pattern for an operating system. This also helps to prevent enumeration of machines in a network address translated (NAT) environment. Without random IP IDs, someone outside the network can perform a statistical analysis of the IP IDs being emitted by the NAT gateway in order to count the number of machines on the private network. Randomizing the IP IDs defeats this kind of attack.
To enable random ID generation on an interface, put a line such as this in /etc/pf.conf:
scrub out on de0 all random-id
We can also use the scrub directive to reassemble fragmented packets before forwarding them to their destinations. This helps prevent specially fragmented packets (such as packets that overlap) from evading intrusion-detection systems that are sitting behind the firewall.
To enable fragment reassembly on all interfaces, simply put the following line in the configuration file:
scrub fragment reassemble
If we want to limit reassembly to just a single interface, we can change this to:
scrub in on de0 all fragment reassemble
This will enable fragment reassembly for the de0 interface.
The next two sections of the pf.conf file involve packet queuing and address translation, but since this hack focuses on packet filtering, we’ll skip these. This brings us to the last section, which contains the actual packet-filtering rules. In general, the syntax for a filter rule can be defined by the following:
action direction [log] [quick] on int [af] [proto protocol] \
from src_addr [port src_port] to dst_addr [port dst_port] \
[tcp_flags] [state]
In PF, a rule can have only two actions: block and pass. As discussed previously, the block policy affects the behavior of the block action. However, this can be modified for specific rules by specifying it along with an action, such as block drop or block return. Additionally, block return-icmp can be used, which will return an ICMP unreachable message by default. An ICMP type can be specified as well, in which case that type of ICMP message will be returned.
For most purposes, we want to start out with a default deny policy; that way we can later add rules to allow the specific traffic that we want through the firewall.
To set up a default deny policy for all interfaces, put the following line in /etc/pf.conf:
block all
Now we can add rules to allow traffic through our firewall. First we’ll keep the loopback interface unfiltered. To accomplish this, we’ll use this rule:
pass quick on lo0 all
Notice the use of the quick keyword. Normally PF will continue through our rule list even if a rule has already allowed a packet to pass, in order to see whether a more specific rule that appears later on in the configuration file will drop the packet. The use of the quick keyword modifies this behavior and causes PF to stop processing the packet at this rule if it matches the packet and to take the specified action. With careful use, this can greatly improve the performance of a ruleset.
To prevent external hosts from spoofing internal addresses, we can use the antispoof keyword:
antispoof quick for $INT_IF inet
Next we’ll want to block any packets from entering or leaving our external interface that have a nonroutable RFC 1918 IP address. Such packets, unless explicitly allowed later, would be caught by our default deny policy. However, if we use a rule to specifically match these packets and use the quick keyword, we can increase performance by adding a rule like this:
block drop quick on $EXT_IF from any to <rfc1918>
If we wanted to allow traffic into our network destined for a web server at 192.168.1.20, we could use a rule like this:
pass in on $EXT_IF proto tcp from any to 192.168.1.20 port 80 \
modulate state flags S/SA
This will allow packets destined to TCP port 80 at 192.168.1.20 only if they are establishing a new connection (i.e., the SYN flag is set), and will enter the connection into the state table. The modulate keyword ensures that a high-quality initial sequence number is generated for the session, which is important if the operating system in use at either end of the connection uses a poor algorithm for generating its ISNs.
Similarly, if we wanted to pass traffic to and from an email server at the IP address 192.168.1.21, we could use this rule:
pass in on $EXT_IF proto tcp from any to 192.168.1.21 \
port { smtp, pop3, imap2, imaps } modulate state flags S/SA
Notice that multiple ports can be specified for a rule by separating them with commas and enclosing them in curly braces. We can also use service names, as defined in /etc/services, instead of specifying the service’s port number.
To allow traffic to a DNS server at 192.168.1.18, we can add a rule like this:
pass in on $EXT_IF proto tcp from any to 192.168.1.18 port 53 \
modulate state flags S/SA
This still leaves the firewall blocking UDP DNS traffic. To allow this through, add this rule:
pass in on $EXT_IF proto udp from any to 192.168.1.18 port 53 \
keep state
Notice here that even though this is a rule for UDP packets we have still used the state keyword. In this case, PF will keep track of the connection using the source and destination IP address and port pairs. Also, since UDP datagrams do not contain sequence numbers, the modulate keyword is not applicable. We use keep state instead, which is how to specify stateful inspection when not modulating ISNs. In addition, since UDP datagrams do not contain flags, we simply omit them.
Now we’ll want to allow connections initiated from the internal network to pass through the firewall. To do this, we’ll need to add the following rules to let the traffic into the internal interface of the firewall:
pass in on $INT_IF from $INT_IF:network to any
pass out on $INT_IF from any to $INT_IF:network
pass out on $EXT_IF proto tcp all modulate state flags S/SA
pass out on $EXT_IF proto { icmp, udp } all keep state
As you can see, OpenBSD has a very powerful and flexible firewalling system. There are too many features and possibilities to discuss here. For more information, you can look at the excellent PF documentation available online or the pf.conf manpage.