[Previous: Tables] [Contents] [Next: Network Address Translation]
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 beginning of a filtering ruleset meaning that if a packet does not match any filter rule the resulting action will be pass.
action [direction] [log] [quick] [on interface] [af] [proto protocol] \
[from src_addr [port src_port]] [to dst_addr [port dst_port]] \
[flags tcp_flags] [state]
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.
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
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.
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 creates state, 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.
Starting in OpenBSD 4.1, all filter rules automatically create a state entry when a packet matches the rule. In earlier versions of OpenBSD the filter rule had to explicitly use the keep state option.
Example using OpenBSD 4.1 and later:
pass out on fxp0 proto tcp from any to any
Example using OpenBSD 4.0 and earlier:
pass out on fxp0 proto tcp from any to any keep state
These rules allow 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. Starting with OpenBSD 3.5, the modulate state option can be used in rules that specify protocols other than TCP.
Keep state on outgoing TCP, UDP, and ICMP packets and modulate TCP ISNs:
pass out on fxp0 proto { tcp, udp, icmp } from any \
to any modulate state
Another advantage of keeping state is that corresponding ICMP traffic will be passed through the firewall. For example, if a TCP connection passing through the firewall is being tracked statefully 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.
The scope of a state entry is controlled globally by the state-policy runtime option and on a per rule basis by the if-bound, group-bound, and floating state option keywords. These per rule keywords have the same meaning as when used with the state-policy option. Example:
pass out on fxp0 proto { tcp, udp, icmp } from any \
to any modulate state (if-bound)
This rule would dictate that in order for packets to match the state entry, they must be transiting the fxp0 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.
Options are specified inside parenthesis and immediately after one of the state keywords (keep state, modulate state, or synproxy state). Multiple options are separated by commas. In OpenBSD 4.1 and later, the keep state option became the implicit default for all filter rules. Despite this, when specifying stateful options, one of the state keywords must still be used in front of the options.
An example rule:
pass in on $ext_if proto tcp to $web_server \
port www keep state \
(max 200, source-track rule, max-src-nodes 100, max-src-states 3)
The rule above defines the following behavior:
A separate set of restrictions can be placed on stateful TCP connections that have completed the 3-way handshake.
Both of these options automatically invoke the source-track rule option and are incompatible with source-track global.
Since these limits are only being placed on TCP connections that have completed the 3-way handshake, more aggressive actions can be taken on offending IP addresses.
An example:
table <abusive_hosts> persist
block in quick from <abusive_hosts>
pass in on $ext_if proto tcp to $web_server \
port www flags S/SA keep state \
(max-src-conn 100, max-src-conn-rate 15/5, overload <abusive_hosts> flush)
This does the following:
To have PF inspect the TCP flags during evaluation of a rule, the flags keyword is used with the following syntax:
flags check/mask
flags any
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. Using the any keyword allows any combination of flags to be set in the header.
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.
In OpenBSD 4.1 and later, the default flags applied to TCP rules is flags S/SA. Combined with the OpenBSD 4.1 default of keep state on filter rules, these two rules become equivalent:
pass out on fxp0 proto tcp all flags S/SA keep state
pass out on fxp0 proto tcp all
Each rule will match TCP packets with the SYN flag set and ACK flag clear and each will create a state entry for matching packets. The default flags can be overridden by using the flags option as outlined above.
In OpenBSD 4.0 and earlier there were no default flags applied to any filter rules. Each rule had to specify which flag(s) to match on and also had to explicity use the keep state option.
pass out on fxp0 proto tcp all flags S/SA keep state
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 to not specify any flags at all and let PF apply the default flags to your rules. If you truly need to specify flags yourself then this combination should be safe:
. . . 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 RST) and to normalize potentially ambiguous combinations (such as SYN and FIN).
Normally when a client initiates a TCP connection to a server, PF will pass the handshake packets between the two endpoints as they arrive. PF has the ability, however, to proxy the handshake. With the handshake proxied, PF itself will complete the handshake with the client, initiate a handshake with the server, and then pass packets between the two. The benefit of this process is that no packets are sent to the server before the client completes the handshake. This eliminates the threat of spoofed TCP SYN floods affecting the server because a spoofed client connection will be unable to complete the handshake.
The TCP SYN proxy is enabled using the synproxy state keywords in filter rules. Example:
pass in on $ext_if proto tcp from any to $web_server port www \
flags S/SA synproxy state
Here, connections to the web server will be TCP proxied by PF.
Because of the way synproxy state works, it also includes the same functionality as keep state and modulate state.
The SYN proxy will not work if PF is running on a bridge(4).
PF offers some protection against address spoofing through the antispoof keyword:
antispoof [log] [quick] for interface [af]
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. It's best practice to skip filtering on loopback interfaces anyways, but this becomes a necessity when using antispoof rules:
set skip on lo0
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.
Starting in OpenBSD 4.0, PF offers a Unicast Reverse Path Forwarding (uRPF) feature. When a packet is run through the uRPF check, the source IP address of the packet is looked up in the routing table. If the outbound interface found in the routing table entry is the same as the interface that the packet just came in on, then the uRPF check passes. If the interfaces don't match, then it's possible the packet has had its source address spoofed.
The uRPF check can be performed on packets by using the urpf-failed keyword in filter rules:
block in quick from urpf-failed label uRPF
Note that the uRPF check only makes sense in an environment where routing is symmetric.
uRPF provides the same functionality as antispoof rules.
Passive OS Fingerprinting (OSFP) is a method for passively detecting the operating system of a remote host based on certain characteristics within that host's TCP SYN packets. This information can then be used as criteria within filter rules.
PF determines the remote operating system by comparing characteristics of a TCP SYN packet against the fingerprints file, which by default is /etc/pf.os. Once PF is enabled, the current fingerprint list can be viewed with this command:
# pfctl -s osfp
Within a filter rule, a fingerprint may be specified by OS class, version, or subtype/patch level. Each of these items is listed in the output of the pfctl command shown above. To specify a fingerprint in a filter rule, the os keyword is used:
pass in on $ext_if from any os OpenBSD keep state
block in on $ext_if from any os "Windows 2000"
block in on $ext_if from any os "Linux 2.4 ts"
block in on $ext_if from any os unknown
The special operating system class unknown allows for matching packets when the OS fingerprint is not known.
TAKE NOTE of the following:
pass in quick on fxp0 all allow-opts
ext_if = "fxp0"
int_if = "dc0"
lan_net = "192.168.0.0/24"
# table containing all IP addresses assigned to the firewall
table <firewall> const { self }
# don't filter on the loopback interface
set skip on lo0
# scrub incoming packets
scrub in all
# setup a default deny policy
block all
# activate spoofing protection for all interfaces
block in quick from urpf-failed
# 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
# pass all traffic to and from the local network.
# these rules will create state entries due to the default
# "keep state" option which will automatically be applied.
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.
# tcp connections will be modulated, udp/icmp will be tracked
# statefully.
pass out on $ext_if proto { tcp udp icmp } all modulate state
# allow ssh connections in on the external interface as long as they're
# NOT destined for the firewall (i.e., 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. use the tcp syn proxy to proxy the connection.
# the default flags "S/SA" will automatically be applied to the rule by
# PF.
pass in log on $ext_if proto tcp from any to ! <firewall> \
port ssh synproxy state
|
[Previous: Tables] [Contents] [Next: Network Address Translation]