[OpenBSD]

[Previous: Traffic Redirection] [Contents] [Next: Logging]

PF: Packet Filtering


Table of Contents


Introduction

Packet filtering is the selective passing or blocking of data packets as they pass through a network interface. The criteria that pf(4) uses when inspecting packets is based on the Layer 3 (IPv4 and IPv6) and Layer 4 (TCP, UDP, ICMP, and ICMPv6) headers. The most often used criteria are source and destination address, source and destination port, and protocol.

Filter rules specify the criteria that a packet must match and the resulting action, either block or pass, that is taken when a match is found. Filter rules are evaluated in sequential order, first to last. Unless the packet matches a rule containing the quick keyword, the packet will be evaluated against all filter rules before the final action is taken. The last rule to match is the "winner" and will dictate what action to take on the packet. There is an implicit pass all at the end of a filtering ruleset meaning that if a packet does not match any filter rule the resulting action will be pass.

Rule Syntax

The general, highly simplified syntax for filter rules is:
action direction [log] [quick] on int [af] [proto protocol] \
   from src_addr [port src_port] to dst_addr [port dst_port] \
   [tcp_flags] [state]
action
The action to be taken for matching packets, either pass or block. The pass action will pass the packet back to the kernel for further processing while the block action will react based on the setting of the block-policy option. The default reaction may be overridden by specifying either block drop or block return.
direction
The direction the packet is moving on an interface, either in or out.
log
Specifies that the packet should be logged via pflogd(8). If the rule specifies the keep state or modulate state option then only the packet which establishes the state is logged. To log all packets regardless, use log-all.
quick
If a packet matches a rule specifying quick then that rule is considered the last matching rule and the specified action is taken.
int
The name of the network interface that the packet is moving through.
af
The address family of the packet, either inet for IPv4 or inet6 for IPv6. PF is usually able to determine this parameter based on the source and/or destination address(es).
protocol
The Layer 4 protocol of the packet:
src_addr, dst_addr
The source/destination address in the IP header. Addresses can be specified as:
src_port, dst_port
The source/destination port in the Layer 4 packet header. Ports can be specified as:
tcp_flags
Specifies the flags that must be set in the TCP header when using proto tcp. Flags are specified as flags check/mask. For example: flags S/SA - this instructs PF to only look at the S and A (SYN and ACK) flags and to match if the SYN flag is "on".
state
Specifies whether state information is kept on packets matching this rule.

Default Deny

The recommended practice when setting up a firewall is to take a "default deny" approach. That is, to deny everything and then selectively allow certain traffic through the firewall. This approach is recommended because it errs on the side of caution and also makes writing a ruleset easier.

To create a default deny filter policy, the first two filter rules should be:

block in  all
block out all

This will block all traffic on all interfaces in either direction from anywhere to anywhere.

Passing Traffic

Traffic must now be explicitly passed through the firewall or it will be dropped by the default deny policy. This is where packet criteria such as source/destination port, source/destination address, and protocol come into play. Whenever traffic is permitted to pass through the firewall the rule(s) should be written to be as restrictive as possible. This is to ensure that the intended traffic, and only the intended traffic, is permitted to pass.

Some examples:

# Pass traffic in on dc0 from the local network, 192.168.0.0/24,
# to the OpenBSD machine's IP address 192.168.0.1. Also, pass the
# return traffic out on dc0.
pass in  on dc0 from 192.168.0.0/24 to 192.168.0.1
pass out on dc0 from 192.168.0.1 to 192.168.0.0/24


# Pass TCP traffic in on fxp0 to the web server running on the
# OpenBSD machine. The interface name, fxp0, is used as the
# destination address so that packets will only match this rule if
# they're destined for the OpenBSD machine.
pass in on fxp0 proto tcp from any to fxp0 port www

Keeping State

One of Packet Filter's important abilities is "keeping state" or "stateful inspection". Stateful inspection refers to PF's ability to track the state, or progress, of a network connection. By storing information about each connection in a state table, PF is able to quickly determine if a packet passing through the firewall belongs to an already established connection. If it does, it is passed through the firewall without going through ruleset evaluation.

Keeping state has many advantages including simpler rulesets and better packet filtering performance. PF is able to match packets moving in either direction to state table entries meaning that filter rules which pass returning traffic don't need to be written. And, since packets matching stateful connections don't go through ruleset evaluation, the time PF spends processing those packets can be greatly lessened.

When a rule has the keep state option, the first packet matching the rule creates a "state" between the sender and receiver. Now, not only do packets going from the sender to receiver match the state entry and bypass ruleset evaluation, but so do the reply packets from receiver to sender. For example:

pass out on fxp0 proto tcp from any to any keep state

This allows any outbound TCP traffic on the fxp0 interface and also permits the reply traffic to pass back through the firewall. While keeping state is a nice feature, its use significantly improves the performance of your firewall as state lookups are dramatically faster than running a packet through the filter rules.

The modulate state option works just like keep state except that it only applies to TCP packets. With modulate state, the Initial Sequence Number (ISN) of outgoing connections is randomized. This is useful for protecting connections initiated by certain operating systems that do a poor job of choosing ISNs.

Keep state on outgoing TCP, UDP, and ICMP packets and modulate TCP ISNs:

pass out on fxp0 proto tcp from any to any modulate state
pass out on fxp0 proto { udp, icmp } from any to any keep state

Another advantage of keeping state is that corresponding ICMP traffic will be passed through the firewall. For example, if keep state is specified for a TCP connection and an ICMP source-quench message referring to this TCP connection arrives, it will be matched to the appropriate state entry and passed through the firewall.

It's important to note that stateful connections are limited to the interface they were created on. This is particularly important on routers and firewalls running PF, especially when a "default deny" policy is implemented as outlined above. If a firewall is keeping state on all outgoing connections on the external interface, those packets must still be explicitly passed through the internal interface.

Note that nat, binat, and rdr rules implicitly create state for matching connections as long as the connection is passed by the filter ruleset.

Keeping State for UDP

One will sometimes hear it said that, "One can not create state with UDP as UDP is a stateless protocol!" While it is true that a UDP communication session does not have any concept of state (an explicit start and stop of communications), this does not have any impact on PF's ability to create state for a UDP session. In the case of protocols without "start" and "end" packets, PF simply keeps track of how long it has been since a matching packet has gone through. If the timeout is reached, the state is cleared. The timeout values can be set in the options section of the /etc/pf.conf file.

TCP Flags

Matching TCP packets based on flags is most often used to filter TCP packets that are attempting to open a new connection. The TCP flags and their meanings are listed here:

To have PF inspect the TCP flags during evaluation of a rule, the flags keyword is used with the following syntax:

flags check/mask

The mask part tells PF to only inspect the specified flags and the check part specifies which flag(s) must be "on" in the header for a match to occur.

pass in on fxp0 proto tcp from any to any port ssh flags S/SA

The above rule passes TCP traffic with the SYN flag set while only looking at the SYN and ACK flags. A packet with the SYN and ECE flags would match the above rule while a packet with SYN and ACK or just ACK would not.

Note: in previous versions of OpenBSD, the following syntax was supported:

. . . flags S

This is no longer true. A mask must now always be specified.

Flags are often used in conjunction with keep state rules to help control the creation of state entries:

pass out on fxp0 proto tcp all flags S/SA keep state

This would permit the creation of state on any outgoing TCP packet with the SYN flag set out of the SYN and ACK flags.

One should be careful with using flags -- understand what you are doing and why, and be careful with the advice people give as a lot of it is bad. Some people have suggested creating state "only if the SYN flag is set and no others". Such a rule would end with:

     . . . flags S/FSRPAUEW  bad idea!!

The theory is, create state only on the start of the TCP session, and the session should start with a SYN flag, and no others. The problem is some sites are starting to use the ECN flag and any site using ECN that tries to connect to you would be rejected by such a rule. A much better guideline is:

. . . flags S/SAFR

While this is practical and safe, it is also unnecessary to check the FIN and RST flags if traffic is also being scrubbed. The scrubbing process will cause PF to drop any incoming packets with illegal TCP flag combinations (such as SYN and FIN or SYN and RST). It's highly recommended to always scrub incoming traffic:

scrub in on fxp0
.
.
.
pass in on fxp0 proto tcp from any to any port ssh flags S/SA \
   keep state

The quick Keyword

As indicated earlier, each packet is evaluated against the filter ruleset from top to bottom. By default, the packet is marked for passage, which can be changed by any rule, and could be changed back and forth several times before the end of the filter rules. The last matching rule "wins". There is an exception to this: The quick option on a filtering rule has the effect of canceling any further rule processing and causes the specified action to be taken. Let's look at a couple examples:

Wrong:

block in on fxp0 proto tcp from any to any port ssh
pass  in all

In this case, the block line may be evaluated, but will never have any effect, as it is then followed by a line which will pass everything.

Better:

block in quick on fxp0 proto tcp from any to any port ssh
pass  in all

These rules are evaluated a little differently. If the block line is matched, due to the quick option, the packet will be blocked, and the rest of the ruleset will be ignored.

Blocking Spoofed Packets

Address "spoofing" is when an malicious user fakes the source IP address in packets they transmit in order to either hide their real address or to impersonate another node on the network. Once the user has spoofed their address they can launch a network attack without revealing the true source of the attack or attempt to gain access to network services that are restricted to certain IP addresses.

PF offers some protection against address spoofing through the antispoof keyword:

antispoof [log] [quick] for interface [af]
log
Specifies that matching packets should be logged via pflogd(8).
quick
If a packet matches this rule then it will be considered the "winning" rule and ruleset evaluation will stop.
interface
The network interface to activate spoofing protection on. This can also be a list of interfaces.
af
The address family to activate spoofing protection for, either inet for IPv4 or inet6 for IPv6.

Example:

antispoof for fxp0 inet

When a ruleset is loaded, any occurrences of the antispoof keyword are expanded into two filter rules. Assuming that interface fxp0 has IP address 10.0.0.1 and a subnet mask of 255.255.255.0 (i.e., a /24), the above antispoof rule would expand to:

block in on ! fxp0 inet from 10.0.0.0/24 to any
block in inet from 10.0.0.1 to any

These rules accomplish two things:

NOTE: The filter rules that the antispoof rule expands to will also block packets sent over the loopback interface to local addresses. These addresses should be passed explicitly. Example:

pass in quick on lo0 all

antispoof for fxp0 inet

Usage of antispoof should be restricted to interfaces that have been assigned an IP address. Using antispoof on an interface without an IP address will result in filter rules such as:

block drop in on ! fxp0 inet all
block drop in inet all

With these rules there is a risk of blocking all inbound traffic on all interfaces.

IP Options

By default, PF blocks packets with IP options set. This can make the job more difficult for "OS fingerprinting" utilities like nmap. If you have an application that requires the passing of these packets, such as multicast or IGMP, you can use the allow-opts directive:
pass in quick on fxp0 all allow-opts

Filtering Ruleset Example

Below is an example of a filtering ruleset. The machine running PF is acting as a firewall between a small, internal network and the Internet. Only the filter rules are shown; queueing, nat, rdr, etc., have been left out of this example.

ext_if  = "fxp0"
int_if  = "dc0"
lan_net = "192.168.0.0/24"

# scrub incoming packets
scrub in all

# setup a default deny policy
block in  all
block out all

# pass traffic on the loopback interface in either direction
pass quick on lo0 all

# activate spoofing protection for the internal interface.
antispoof quick for $int_if inet

# only allow ssh connections from the local network if it's from the
# trusted computer, 192.168.0.15. use "block return" so that a TCP RST is
# sent to close blocked connections right away. use "quick" so that this
# rule is not overridden by the "pass" rules below.
block return in quick on $int_if proto tcp from ! 192.168.0.15 \
   to $int_if port ssh flags S/SA

# pass all traffic to and from the local network
pass in  on $int_if from $lan_net to any
pass out on $int_if from any to $lan_net

# pass tcp, udp, and icmp out on the external (Internet) interface. 
# keep state on udp and icmp and modulate state on tcp.
pass out on $ext_if proto tcp all modulate state flags S/SA
pass out on $ext_if proto { udp, icmp } all keep state

# allow ssh connections in on the external interface as long as they're
# NOT destined for the firewall (ie, they're destined for a machine on
# the local network). log the initial packet so that we can later tell
# who is trying to connect.
pass in log on $ext_if proto tcp from any to { !$ext_if, !$int_if } \
   port ssh flags S/SA keep state

[Previous: Traffic Redirection] [Contents] [Next: Logging]


[back] www@openbsd.org
Originally [OpenBSD: filter.html,v 1.14 ]
$Translation: filter.html,v 1.1 2003/11/27 17:34:57 sl Exp $
$OpenBSD: filter.html,v 1.1 2003/11/27 19:39:09 horacio Exp $