ONLamp.com    
 Published on ONLamp.com (http://www.onlamp.com/)
 See this if you're having trouble printing code examples


Securing Small Networks with OpenBSD Changes in pf: Packet Filtering

by Jacek Artymiak
06/26/2003

In previous installments of this series (NAT with pf and More NAT) we examined macros, options, scrub rules, and NAT rules. This time we'll look at packet filtering rules, which kick in after packets have been scrubbed and (optionally) redirected to another IP address or port with NAT rules.

Writing packet filtering rules

Once you get your scrub and NAT rules sorted, it's time to add some packet filtering rules to the mix. There are three kinds of rules:

Packets are matched against filtering rules after scrubbing and NAT'ing have been done. When you design your ruleset and plan to use NAT or port/interface redirection, remember to design your filtering rules to match packets after NAT'ing and redirection, or you will waste a lot of time debugging the ruleset and scratching your head wondering what's wrong with your design and its implementation. We will return to this subject later in this article, but first we need to focus on the packet filtering grammar.

Related Reading

Essential System Administration
Tools and Techniques for Linux and Unix Administration
By Æleen Frisch

The anatomy of a filtering rule

Packet filtering rules are written using a specialized grammar, similar to that of NAT rules, but capable of describing much finer detail required to implement detailed filtering rules. The large number of keywords and their possible configurations make them a bit overwhelming for a beginner, but there is a method behind this madness. The following guide should make them easier to digest. I divided it into small sections each describing one part of a packet filtering rule, following the order in which they are described in pf.conf(5).

Hint: if you have trouble making sense of the syntax describing those rules in pf.conf(5), remember that the items listed in square brackets ([...]) are optional, while the items separated with a vertical bar (|) are alternative values. Items in double-quotes ("...") are meant to be typed literally, but without the double quotes.

Should a packet be blocked or passed (block, pass)?

The block or pass keywords tell pf(4) what to do with the packet that matches all conditions listed after either block or pass (we are leaving the antispoof keyword aside for a while, but think of it as a special case of block rules). These keywords are required and either of them must be used at the beginning of every filtering rule. To block all incoming and outgoing packets, use:

block in  all
block out all

The opposite would be pass rules that let all traffic in and out of the firewall:

pass in  all
pass out all

With such rules in place, all traffic can move freely in or out of the firewall. Both policies are too general for practical use, but they are handy for explaining the basics. As a general rule, the more conditions you list after pass or block the more specific the rule will be. Conversely the less conditions you use, the more general the rule will be. As you will see later on, I always advise that you start your packet filtering section with a set of block {in, out} all rules. It is safer to block all traffic first and only open those routes that are absolutely necessary later. Apart from being a safer way to write pf(4) rulesets, such approach greatly simplifies the ruleset, which is good, because simple rulesets are easier to debug.

Should you notify the sender (return-icmp, return-rst) after blocking a packet?

A plain block rule drops all matching packets without sending any kind of notification back to the host that tried to initiate the connection. Silently dropping all unwanted packets is good security practice, because the firewall does not have to waste its own resources on sending redundant information, and because "silent" firewalls are harder to scan and fingerprint. (Broadly speaking, scanning is the process of looking for open ports that the attacker could use to break into your firewall or network, while fingerprinting is the process of identifying the operating system or other software running on the scanned host.)

However, as with all general rules, there are exceptions. One of these is sending the ICMP destination-unreachable message to hosts trying to connect to port 113 (auth). It is quite safe to do so and is considered to be good net citizenship. Returning that message helps some services, such as sendmail, complete connections faster, without waiting for connections to port 113 to time out.

This can be achieved with a rule that begins with:

block in all
block return-icmp in on $ext_if from any to $ext_ad port auth quick
pass in on $ext_if from any to $ext_ad port smtp quick

You can add the ICMP message number or name after the return-icmp keyword, although it is optional and only required in special cases. For more information about ICMP, read RFC792 [Postel 1981]; if you want to learn how it works in practice, and how it is implemented, especially on BSD systems, consult [Stevens, Wright 1994].

Another possibility is to answer unwanted packets with the TCP RST message. This is achieved with the return-rst keyword, which can be followed by an integer number defining the TTL (time to live) value for the returned packet as in:

block return-rst in quick on $ext_if from any to $ext_ad

or

block return-rst 100 quick in on $ext_if from any to $ext_ad port

The return-rst and return-icmp keywords are optional.

Is this packet arriving or departing (in, out)?

The next required keyword that appears after either the block (followed by optional reset-icmp or reset-rst keywords) or the pass keyword is the direction keyword. There are two direction keywords you can use: in or out. They are known to cause some confusion, especially when the firewall is equipped with more than one network interface, and when NAT rules are used along with filtering rules.

The key to understanding when a packet matches either the in or the out rule is remembering that these directions are relative to the firewall itself. Iif a packet is sent from an external host to the firewall, it matches the in rule on the firewall's external interface; when it is sent from the firewall itself, it matches the out on the external interface. Similarly, packets sent from internal hosts to the firewall and destined to external host will match in rules on the interface connecting your private network segment to the firewall and out rules on the firewall's external interface.

Do you want to log packets (log, log-all)?

You can tell pf(4) to log packets matching certain rules to the pflog0(4) interface. From there, they are picked up by pflogd(8) and stored in rotated log files located in /var/log.

To start packet logging, use the log or the log-all keywords. The difference between them lies in the way they work with rules that contain either keep state or modulate state rules (more on these later). log logs only the state-making packets, while log-all logs all packets. If you use stateful filtering and want to capture all packets that match the log rule, use log-all, otherwise, use log.

If you want to log all traffic on all interfaces, add either the log or log-all keywords to the rules that cover most traffic. For example, to log all traffic entering and leaving the firewall on the external interface, use these rules:

block  in log on $ext_if
block out log on $ext_if

Similar rules should be added to sections describing packet filtering policy for other interfaces on the firewall. If you would rather log only incoming HTTP traffic, add the log keyword to the rule that lets HTTP packets through.

Although I wrote earlier that the firewall need not be the latest, fastest machine you can get, the more traffic you log and the heavier the traffic that passes through the firewall's interface, the faster the hardware you use the better. This is especially true for the disks that must store the data. As you will learn later in this article, when I discuss the dup-to keyword, you can duplicate packets and send them to a different interface, where a dedicated packet logging machine can sit, listen, store, and analyze traffic. If you use dup-to for logging, then log, and log-all are redundant.

How can I influence the process of matching packets (quick)?

Unlike NAT (nat, binat, rdr) rules, which are processed in the "first matching rule wins" fashion, packet filtering is done in the "last matching rule wins" way. While it is possible to carefully structure your ruleset in a way that avoids letting unwanted packets through, it is more convenient and simpler to put rules that you want to process faster (like very specific blocking rules) at the top of the packet filtering section of the ruleset and add the quick keyword to such rules. Whenever this keyword is used, pf(4) will execute the matching rule and will not try to match the packet against the rest of the ruleset. This saves some processing time, which quickly adds up on a busy link, so I use this keyword frequently.

The quick keyword is added after the log or log-all keywords, or, in the absence of these keywords, after the in or out direction keywords:

pass in log-all quick on $ext_if proto tcp from any to $ext_ad port 80

or

pass in quick on $ext_if proto tcp from any to $ext_ad port 80

Typical applications of quick rules include quickly blocking addresses of problematic sites and blocking packets with spoofed addresses.

Which network interface receives packets (on)?

While the packet filtering rules grammar allows us to write general rules that apply to all interfaces, we can seldom write a good ruleset without adding rules for specific interfaces. The name of the interface is given after the on keyword that appears after the quick keyword. The following examples show a few possible variations of keywords that appear before on:

block in on $ext_if
block in log-all on $ext_if
block in log-all quick on $ext_if

Hint: If you forgot the name of the interface, check the output of dmesg | less. If OpenBSD is not recognizing your network interface, read this article for kernel modification tips. Note that if you are using a device connected to the serial interface (like a modem), such device may not be listed in dmesg output, but should still be recognized by the system. When you are not sure what name your network card falls under in OpenBSD, check the list displayed by apropos driver. Still no luck? Look at this list.

Which interface (on), IP address (from, to), and port (port) combination should be used with NAT rules?

When you don't use NAT rules, it is fairly easy to decide which interface/IP address/port ought to be used for in a filtering rule. For example, if you want to limit access to port 80 on the external interface and only let TCP packets in, you'd use something like this:

pass in quick on $ext_if proto tcp from any to $ext_ad port 80

Things are different when you add NAT rules and additional subnets. Then a single block or pass rule might not be enough to properly structure the flow of traffic through the firewall. In such cases, always remember that packets redirected to another port/interface have to be filtered on the target port/interface. For example, the following rules redirect traffic from the external interface port 80 to a DMZ HTTP server port 8080:

ext_if         = "ne1"
ext_ad         = "x.x.x.x/32"
dmz_www_ad     = "192.168.2.3/32"
rdr_www_protos = "{tcp}"
rdr_www_port   = "8080"

rdr on $ext_if proto $rdr_www_protos from any \
	to $ext_ad port www -> $dmz_www_ad port $rdr_www_port

If you want to block all incoming connections except connections to port 80 on the external interface (which are redirected to port 8080 on the DMZ HTTP server), use:

block in on $ext_if all
pass  in on $ext_if inet proto tcp from any to $dmz_www_ad port 8080

The pass rule uses the DMZ HTTP host address for the destination IP address. The same goes for the destination port (8080 instead 80). If you used the IP address of the external interface and port 80, you'd never see any packets passed to the DMZ HTTP server. Similarly, if you wanted to block a redirected packet, you'd need to write a rule that matched the destination IP address/port after redirection.

After a packet is passed on the external interface, it may have to pass another test on the DMZ interface. This is optional, but it is prudent to filter traffic going in and out of the DMZ. For example, you might want to block all external traffic except HTTP going into the DMZ segment. This is accomplished with these rules:

block out on $dmz_if all
pass  out on $dmz_if inet proto tcp from any to $dmz_www_ad port 8080

Note that these rules are for packets leaving (out) the DMZ interface. Why is that? You need to remember that all rules are written relative to the firewall; a packet sent from the outside first matches in rules on the external interface, then it matches out rules on the DMZ interface, and then it reaches the HTTP server. A packet sent from the DMZ HTTP server to the outside matches in rules on the DMZ interface, out rules on the external interface, and then it reaches some external host.

The out rules shown earlier will also match packets sent from the private network segment. Such packets match in rules on the private interface and then the out rules on the DMZ interface.

Do you want to bypass the routing table or duplicate packets (fastroute, route-to, dup-to)?

Ordinarily, packets examined by pf(4) are routed according to the entries in the firewall's routing table, which should be enough in most cases. However, there might be times when you will want to bypass the routing table or to duplicate packets for intrusion detection or logging purposes. The following three keywords allow us to influence packet routing:

The dup-to keyword is very useful for setting up a separate packet logging or intrusion detection system host. Simply add this rule to /etc/pf.conf on the firewall:

pass  in on $ext_if dup-to ($log_if $log_ad) all

Then run pf(4) on the logging host with a the following rule:

block in log on $ext_if all

If you want to tune logging parameters, read this and this.

Letting another host take care of logging or analysis of packets is a good thing, because it moves the additional load placed on the firewall's resources to another host. It makes logging setups like those described in one of my earlier articles unnecessary, while making the whole process of logging simpler and more stable. Of course, to create such setup you will need another machine and an additional network interface on the firewall (it's best to put the logging/analysis machine on a separate segment). In any case, if you plan to log traffic, read this, this, and this.

Unlike all other keywords that can be followed by some value, dup-to and route-to do not allow us to specify more than one interface or interface/address combination.

Which IP addressing family will be filtered (inet, inet6)?

pf(4) can filter packets with IPv4 (inet) and IPv6 (inet6) addresses. You select the addressing family with the inet (IPv4) or inet6 (IPv6) keywords. If you plan on dealing with IPv4 traffic only, add these rules for every interface on the firewall:

block in  quick inet6 all
block out quick inet6 all

Which protocol(s) will be filtered (proto)?

Another layer of filtering is filtering by protocol name or number. This is done with an addition of the proto keyword followed by the name(s) or number(s) of protocols that the packets are formed in accordance with. The list of protocols can be found in /etc/protocols.

For example, if you want to let in only TCP packets, use this rule:

pass in quick on $ext_if proto tcp

Tip: Almost all popular services use TCP. You should block UDP packets sent to servers that only use TCP, because such packets are almost never legitimate traffic. If you do not know which protocol is used by which services, check /etc/protocols. This file is current as of the time of release of the OpenBSD system you are using. The latest and the freshest listings are always in the IANA's online database.

What is the packet's source address (from, any, all)?

Source address filtering is typically used to stop two kinds of packets: those originating from hosts with legal IP addresses that we do not want to accept traffic from, and those that carry spoofed source addresses. In the first case, you will want to block packets from legal IP addresses if they're giving you so much trouble that you'd rather not accept traffic from them. In the second case, you ought to block packets with spoofed source addresses for your own safety as they will never be legitimate traffic. The following rule blocks packets with spoofed source addresses sent from external hosts and arriving on the firewall's external interface:

$blockIPs = {10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, \
	224.0.0.0/4, 240.0.0.0/5, 127.0.0.0/8, 0.0.0.0}

block in quick on $ext_if from $blockIPs

Conversely, we can specify addresses of hosts that we want to accept traffic from, as in:

block in quick on $ext_if from ! $allow_ad to any

Tip: Note the exclamation mark (!), which negates the value that follows it, so the rule above reads "block incoming packets (and don't match them against other filtering rules) arriving on the external interface send from all IP addresses except $allow_ad and destined for any host."

There are two shortcuts that you can use to specify wider ranges of IP addresses. One is the any keyword, which when placed after from or to, matches any source (from any) or target (to any) address:

block in quick on $log_if from any to $log_ad
block in quick on $ext_if from $blockIPs to any

The second shortcut is the all keyword which replaces from any to any. The following two rules are synonymous:

block in on $ext_if from any to any
block in on $ext_if all

Tip: from and to go together, and you cannot write a rule with just either of them.

Source address specification is a required part of any packet filtering rule, even if you use an all-encompassing any or all shortcuts.

What is the source port of the packet (port)?

For a finer degree of control, we can block or pass packets sent from a specific port on the interface from which the matching packets were sent. The port specification is listed after the source address specification and is marked with the port keyword, as in:

block in on $ext_if proto tcp from any port 80

The port 80 is equivalent to port = 80 Other possible operators are <, >, <=, >=, !=, <>, and ><. Their use is shown in the following rules:

# block packets destined for port 80
block in on $ext_if proto tcp from any to $dmz_www_ad port = 80

# block packets destined for all ports except port 80
block in on $ext_if proto tcp from any to $dmz_www_ad port != 80

# block packets destined for ports lower than port 80
block in on $ext_if proto tcp from any to $dmz_www_ad port < 80

# block packets destined for ports lower than and port 80
block in on $ext_if proto tcp from any to $dmz_www_ad port <= 80

# block packets destined for ports higher than and equal to port 80
block in on $ext_if proto tcp from any to $dmz_www_ad port > 80

# block packets destined for ports higher than and equal to port 80
block in on $ext_if proto tcp from any to $dmz_www_ad port >= 80

# block packets destined for ports higher than port 80 and lower than port 1024
block in on $ext_if proto tcp from any to $dmz_www_ad port 80 >< 1024

# block packets destined for ports lower than port 80 and higher than port 1024
block in on $ext_if proto tcp from any to $dmz_www_ad port 80 <> 1024

Specifying port numbers makes sense only for those protocols that carry source port information (like TCP or UDP). That is why you need to use the proto keyword when you use the port keyword. Otherwise, pfctl(8) will complain and refuse to load rules.

Destination IP address (to, any, all)

Destination address filtering is typically used to pass only those packets that are destined to addresses where there are servers listening for connections, for example:

pass in on $ext_if from any to $dmz_www_ad

All syntax rules for source addresses discussed earlier are applicable to destination addresses.

Source target address specification is a required part of any packet filtering rule, even if you use an all-encompassing any or all shortcuts.

Destination port (port)

The destination port specification follows the destination address specification. All rules that apply to source ports, apply to destination ports. Of course, both are independent. You will probably use destination ports more often than source ports, as such rules are usually used to only let those packets through that are destined to ports where appropriate servers are listening, for example:

pass in on $ext_if proto tcp from any to $ext_www_ad  port $ext_www_port
pass in on $ext_if proto tcp from any to $ext_smtp_ad port $ext_smtp_port
pass in on $ext_if proto tcp from any to $ext_ftp_ad  port $ext_ftp_port

Which users (user) and groups (group) are allowed to receive or send packets?

One very handy feature of pf(4) is its ability to filter packets based on user and group names of the users and groups who own the sockets on which packets are sent or received. The user and group IDs can be given in form of names or numbers and it is possible to specify ranges and lists of IDs. When you list ranges, it is possible to construct them using the operators described earlier in the section on source ports.

The user and group names are effective names, which may not be the same as the real name (as is the case with setuid and setgid processes). If you are having problems with these rules, remember that the user and group IDs are stored at the time a socket is created and they are not updated when the process creating a socket drops privileges (e.g., after a process binds to a privileged port as root, and then drops root privileges), so it may be that you need to use root ID in a rule instead of an unprivileged users's ID. Try this when you hit a stumbling block with rules user or group

In case of outgoing connections, the user IDs will match the user that opened the connection from the firewall itself. Similarly, for incoming connections, the user IDs will match the user that opened the socket for listening on the firewall. It is not possible to match usernames on connections forwarded with NAT rules. In case of forwarded connections, user or group IDs can match (or not match) a special username unknown. In this case, only two operators are allowed: = and !=.

Hint: User and group rules can only be used with TCP and UDP protocols.

Which TCP flags are allowed (flags)?

TCP packet headers contain a flag field which plays an important role in the process of establishing, maintaining, and closing connections. Flags are important from the point of view of security, because some attackers abuse the three-way-handshake mechanism and other uses of TCP flags in denial of service (DOS) attacks (see CERT-1996.21, CERT-2000.21) and other types of attacks aimed at hosts connected to the Internet.

As of OpenBSD 3.2, pf(4) recognizes the following TCP header flags:

The syntax for this portion of filtering rules is as follows: the flags keyword is followed by two lists of flags separated with a slash (/); the first is a list of flags from the second list that must be set. Those flags not on the first list must be unset. Flags not listed on the second list are ignored, and those flags from the second list missing from the first list may or may not be set.

# FIN must be set, ignore the rest
block in proto tcp all flags F/F

# FIN must be unset, ignore the rest
block in all flags /F 

# FIN must be set, the rest must be unset
block in all flags F 

# FIN must be set, ACK must be unset, ignore the rest
block in all flags F/FA

# FIN and ACK must be unset, ignore the rest
block in all flags /FA

TCP flags are described in RFC761 [Postel 1980], and RFC793 [Postel 1981]. A far more detailed discussion of TCP flags can be found in [Stevens, Wright 1994]. Note that [Stevens, Wright 1994] do not describe the ECE and CWR flags, as these were added to the TCP header after TCP/IP Illustrated was published. For more information on ECE and CWR read RFC3168 [Ramakrishann, Floyd, Black 2001] and RFC3360 [Floyd 2002].

The flags keyword makes sense only for TCP (proto tcp) packets.

Are you going to allow ICMP packets?

Bogus ICMP packets are another way attackers can make your site inoperable, which is why pf(4) has special syntax for dealing with these useful, but potentially dangerous packets. For more information about the havoc ICMP packets can wreak read this paper (pdf) from SANS [Jeon 2001] and the CERT-1996.26 advisory.

ICMPv4 packets are matched by icmp-type keyword, while ICMP IPv6 are matched by the ipv6-icmp-type keyword. Both keywords are followed by the ICMP type number and the ICMP code number, separated with the code keyword.

Explanations of ICMPv4 message types and codes can be found in RFC792 [Postel 1981], ICMPv6 message types and codes are discussed in RFC2463 [Conta, Deering 1998].

Will you use stateful filtering? (keep state, modulate state)

pf(4) is a stateful packet filter, which means that it is capable of keeping track of the state of connections. Stateful filtering has the following advantages:

The basic principle behind stateful filtering is simple. When the initial packet makes the connection on the firewall, the packet filter will create an entry in its state table for that connection. All subsequent packets that belong to the connection for which an entry in the state table exist will be let through without matching them against the whole ruleset. State tables are checked before the filter begins evaluating filtering rules.

The packet filter decides if a packet belongs to a connection for which a state exists by checking the packet's sequence number stored in the TCP header. When the sequence number falls out of a narrow window, the packet is dropped. This mechanism prevents spoofed packet injection into an established connection.

Stateful inspection of packets is turned on with the keep state keywords placed near the end of a filtering rule.

pass out on $ext_if proto TCP all keep state

To keep memory usage under control, information about connections is removed from the state table after connections are closed or after they time out.

By the way, when you use nat/binat/rdr rules, you are already using stateful filtering, as these rules create states automatically.

There are two schools of thought about state creation. Some administrators insist that only packets with the SYN flag (i.e., the packets that initialize the connection) can create state. Others say that any packet ought to be able to create state, because such rules allow existing connections to create state and continue after the state tables are flushed with pfctl -F state or after the firewall is rebooted. Rules that create state only for packets with the SYN flag set will not be able to create state for existing connections.

The following rules allow all departing TCP packets to create state. As for inbound packets, only those sent to port 80 will be able to create state:

pass in  proto tcp all port 80 keep state
pass out proto tcp all keep state

If you want to limit packets that can create state to those that have the SYN flag set, add the flags S/SA condition, as in:

pass in proto tcp all port 80 flags S/SA keep state pass
out proto tcp all flags S/SA keep state

What about UDP or ICMP packets? Can pf(4) create state for these as well? Yes, it can. With UDP packets, which do not carry sequence numbers, the filter matches them to states using only address and port information.

As for ICMP, these are treated differently depending on their category. ICMP error messages that refer to TCP or UDP packets are matched against states for connections they refer to. As such they do not require separate rules, the packet filter will take care of this automatically. ICMP queries (like ping(8)) may need their own separate rules, like:

pass out inet proto icmp all icmp-type echoreq keep state

Initial sequence numbers, if chosen carelessly, can be used in dangerous attacks that exploit the fact that some TCP stacks use easily predictable values for initial sequence numbers. For more information about these attacks read CERT Vulnerability Note VU#498440, or Rik Farrow's Sequence Number Attacks.

pf(4) can prevent these attacks with the modulate state rule. To turn it on, use modulate state instead of keep state.

pass in  proto tcp all port 80 flags S/SA keep state
pass out proto tcp all flags S/SA keep state

becomes

pass in  proto tcp all port 80 flags S/SA modulate state
pass out proto tcp all flags S/SA modulate state

Remember that modulate state can only be used with TCP connections. For other connections use keep state.

Each keep state or modulate state can have its own set of options. These options are:

A rule using state options could look like this:

pass in proto tcp all port 80 flags S/SA \
	modulate state (max 1000, tcp.established 120, tcp.closing 10)

Will IP options be allowed or blocked (allow-opts)?

IP options are blocked by default, which is good from the point of view of security. If you want to allow them, you explicitly state your wish with the allow-opts keyword.

In practice there is very little need for allowing these options, save for special application, as they may be used by attackers to mess with your network, or with other hosts on the Internet (in such cases, you might end up being accused of deliberate wrongdoing). IP options have their legitimate uses, but if you don't explicitly need them, leave them disabled. That is, do not use allow-opts.

If you're curious, read RFC791 [Postel 1981], RFC1108 [Kent 1991]. For a more detailed discussion, refer to [Stevens, Wright 1994] where you will find details of operation and implementation of IP options processing in BSD systems.

The allow-opts keyword can only be used in pass rules.

Labels (label)

Labels are used to mark rules for which pf(4) will keep separate statistics. You can display these stats with pfctl(1). A label is added with the label keyword followed by a text string. Labels are placed at the end of rules:

pass in  on rl0 all label "incoming"
pass out on rl0 all label "departing"

To view statistics, use:

$ sudo pfctl -s labels
incoming 85 26 2024
departing 86 56 6960

The numbers that follow the labels are the number of rule evaluations, packets, and bytes.

Labels can contain pre-defined macros:

That's enough theory. Next time, we'll add a packet filtering section to the ruleset described in More NAT, discuss some modifications to that ruleset, solve the FTP connection problems with a proxy, cover IPv6 filtering and build an invisible filtering bridge.

Jacek Artymiak started his adventure with computers in 1986 with Sinclair ZX Spectrum. He's been using various commercial and Open Source Unix systems since 1991. Today, Jacek runs devGuide.net, writes and teaches about Open Source software and security, and tries to make things happen.


Read more Securing Small Networks with OpenBSD columns.

Return to the BSD DevCenter.

Copyright © 2009 O'Reilly Media, Inc.