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 replaced, in both features and flexibility and has even become a part of FreeBSD as of 5.3-RELEASE. PF shares many features with Linux’s Netfilter, and while Netfilter is more easily extensible with modules, PF outshines it in its traffic normalization capabilities and enhanced logging features.
OpenBSD supports PF out of the box. However, under FreeBSD, you’ll need to enable at minimum the following kernel configuration options:
device pf device pflog
If you don’t have these options enabled, add them in and rebuild and reinstall your kernel. For more information on how to do that, see the “Building and Installing a Custom Kernel” section of the FreeBSD Handbook.
To communicate with the kernel portion of PF, you’ll need to use the pfctl
command. Unlike the iptables
command that is used with Linux’s Netfilter, pfctl
is not used to specify individual rules, but instead uses its own configuration and rule specification language. To actually configure PF, you 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. You don’t need to use all of the sections; if you don’t need a specific type of rule, you can simply omit that section from 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, a macro must start with a letter and 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 }"
You can reference a macro 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.
Thus, rather than using the macro from the previous example, you could 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. Specify tables 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, 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 this command:
pfctl -t spammers -T show
In addition to IP addresses, you can also specify hostnames. 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, you can control session timeouts, defragmentation timeouts, state-table transitions, statistic collection, and other behaviors. Specify options by using the set
keyword. The available options are 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, you can use return
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
, you can collect other statistics, such as packet and byte counts, for an interface. To enable this for an interface, add a line similar to this to the configuration file:
set loginterface de0
Note that you can collect these statistics on only 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, you can also modify the session-timeout values. Setting the timeout interval 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 one 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 can 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, you could put these lines in your pf.conf file:
set limit states 20000 set limit frags 15000
Alternatively, you could combine these entries into a single statement like this:
set limit { states 20000, frags 15000 }
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, you can get quite detailed in specifying what you want normalized and how you want to normalize it. Since you can use PF’s general filtering-rule syntax to determine what types of packets a scrub rule will match, you 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, you 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 your firewall ensures that the IP IDs in all the packets leaving your network are totally random, it will be pretty hard to match them against a known pattern for an operating system.
IP ID randomization 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.
To enable random ID generation on an interface, put a line like this in /etc/pf.conf:
scrub out on de0 all random-id
You 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 you want to limit reassembly to just a single interface, change it to something like:
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 those. 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, you can modify the block
action’s behavior for specific rules by specifying block
along with the action, as in block drop
or block return
. Additionally, you can use block return-icmp
, which will return an ICMP unreachable message by default. You can also specify an ICMP type, in which case that type of ICMP message will be returned.
For most purposes, you’ll want to start out with a default deny policy; that way, you can later add rules to allow the specific traffic that you 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 you can add rules to allow traffic through the firewall. First, keep the loopback interface unfiltered by using this rule:
pass quick on lo0 all
Notice the use of the quick
keyword. Normally, PF will continue through the 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 in the configuration file will drop the packet. The use of the quick
keyword modifies this behavior, causing 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, you can use the antispoof
keyword:
antispoof quick for $INT_IF inet
Next, you’ll want to block any packets that have a nonroutable RFC 1918 IP address from entering or leaving your external interface. Such packets, unless explicitly allowed later, would be caught by your default deny policy. However, if you use a rule to specifically match these packets and use the quick
keyword, as follows, you can increase performance:
block drop quick on $EXT_IF from any to <rfc1918>
If you want to allow traffic destined for a specific web server (say, 192.168.1.20) into the network, 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 new connections (i.e., if the SYN
flag is set), and will enter the connections into the state table. The modulate
keyword ensures that a high-quality
initial sequence number (ISN) 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 you want to pass traffic to and from a particular email server (say, IP address 192.168.1.21), 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 you can specify multiple ports for a rule by separating them with commas and enclosing them in curly braces. You can also use service names, as defined in /etc/services, instead of specifying the services’ port numbers.
To allow traffic to a specific DNS server (say, 192.168.1.18), 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 it through, add a rule like this:
pass in on $EXT_IF proto udp from any to 192.168.1.18 port 53 \ keep state
Notice that even though this is a rule for UDP packets, you 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. Using keep state
instead specifies stateful inspection when not modulating ISNs. In addition, since UDP datagrams do not contain flags, simply omit them.
Now you’ll want to allow connections initiated within the network to pass through the firewall. To do this, you 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
In the past few releases, the popular passive OS fingerprinting tool p0f has been integrated into PF. This enables PF to ascertain the operating systems running on hosts sending traffic to or through a system running PF. Consequently, you can create PF rules that are operating-system-specific. For instance, if you want to block traffic from any system that isn’t running Linux, you can use something like this:
block in pass in from any os "Linux"
But beware that OS fingerprinting is far from perfect. It’s entirely possible for someone to modify the characteristics of her TCP/IP stack to mimic another operating system [Hack #65].
Once you’re done editing pf.conf, you can enable PF by running the following commands:
#pfctl -e
#pfctl -f /etc/pf.conf
The first line enables PF, and the second line loads your configuration. If you want to make changes to your configuration, just run pfctl -f /etc/pf.conf
again. To enable PF automatically at startup under OpenBSD, add the following line to /etc/rc.conf.local:
pf=YES
FreeBSD is slightly different. You’ll instead need to add the following line to /etc/rc.conf:
pf_enable="YES"
The next time you reboot, PF should be enabled.
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, look at the excellent PF documentation available online, or the pf.conf manpage.