quick
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 the resulting action will be pass
if a
packet does not match any filter rule.
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]
action
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
in
or out
.
log
log (all)
.
quick
quick
, then that rule
is considered the last matching rule and the specified
action
is taken.
interface
egress
group, which contains the interface(s) that holds
the default route(s).
ppp
or carp
.
ppp
or carp
interface, respectively.
af
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
tcp
udp
icmp
icmp6
/etc/protocols
src_addr
, dst_addr
/netmask
(i.e., /24
).
Each IP address on the interface is combined with the netmask to form
a CIDR network block which is substituted into the rule.
( )
.
This tells PF to update the rule if the IP address(es) on the named
interface change.
This is useful on an interface that gets its IP address via DHCP or
dial-up as the ruleset doesn't have to be reloaded each time the
address changes.
:network
- substitutes the CIDR network block
(e.g., 192.168.0.0/24)
:broadcast
- substitutes the network broadcast address
(e.g., 192.168.0.255)
:peer
- substitutes the peer's IP address on a
point-to-point link
In addition, the :0
modifier can be appended to either
an interface name or to any of the above modifiers to indicate that
PF should not include aliased IP addresses in the substitution.
These modifiers can also be used when the interface is contained in
parentheses.
Example: fxp0:network:0
urpf-failed
can be used for the source address
to indicate that it should be run through the
uRPF check.
!
("not") modifier.
any
meaning all addresses
all
which is short for
from any to any
.
src_port
, dst_port
/etc/services
!=
(not equal)
<
(less than)
>
(greater than)
<=
(less than or equal)
>=
(greater than or equal)
><
(range)
<>
(inverse range)
The last two are binary operators (they take two arguments) and do not include the arguments in the range.
:
(inclusive range)
The inclusive range operator is also a binary operator and does include the arguments in the range.
tcp_flags
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 only the SYN flag is "on"
(and is applied to all TCP rules by default).
flags any
instructs PF not to check flags.
state
no state
- works with TCP, UDP, and ICMP.
PF will not track this connection statefully.
For TCP connections, flags any
is usually also required.
keep state
- works with TCP, UDP, and ICMP.
This option is the default for all filter rules.
modulate state
- works only with TCP.
PF will generate strong Initial Sequence Numbers (ISNs) for packets
matching this rule.
synproxy state
- proxies incoming TCP connections to help
protect servers from spoofed TCP SYN floods.
This option includes the functionality of keep state
and
modulate state
.
To create a default deny filter policy, the first filter rule should be:
block allThis 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 to the web server running on the OpenBSD machine. pass in on egress proto tcp from any to egress port www
quick
Keywordquick
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 egress proto tcp to port ssh pass in allIn 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 egress proto tcp to port ssh pass in allThese rules are evaluated a little differently. If the
block
line is matched due to the quick
option, the packet is 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. Since packets matching stateful connections don't go through ruleset evaluation, the time PF spends processing those packets can be greatly reduced.
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 the receiver to the sender.
All pass rules automatically create a state entry when a packet matches
the rule.
This can be explicitly disabled by using the no state
option.
pass out on egress proto tcp from any to anyThis rule allows any outbound TCP traffic on the egress interface and also permits the reply traffic to pass back through the firewall. Keeping state significantly improves the performance, 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.
To allow simpler rulesets, the modulate state
option can be
used in rules that specify protocols other than TCP.
In those cases, it is treated as keep state
.
Keep state on outgoing TCP, UDP and ICMP packets and modulate TCP ISNs:
pass out on egress proto { tcp, udp, icmp } from any to any modulate stateAnother 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
and floating
state option keywords.
These per-rule keywords have the same meaning as when used with the
state-policy
option.
For example:
pass out on egress 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 egress interface.
pf.conf
file.
max number
no state
source-track
source-track rule
- The maximum number of states
created by this rule is limited by the rule's
max-src-nodes
and max-src-states
options.
Only state entries created by this particular rule count toward
the rule's limits.
source-track global
- The number of states created by
all rules that use this option is limited.
Each rule can specify different max-src-nodes
and
max-src-states
options, however state entries created by
any participating rule count towards each individual rule's limits.
src-nodes
runtime option.
max-src-nodes number
source-track
option is used,
max-src-nodes
will limit the number of source IP addresses that can simultaneously
create state.
This option can only be used with source-track rule
.
max-src-states number
source-track
option is used,
max-src-states
will limit the number of simultaneous state entries that can be created
per source IP address.
The scope of this limit (i.e., states created by this rule only or
states created by all rules that use source-track
) is
dependent on the source-track
option specified.
keep state
, modulate state
, or
synproxy state
).
Multiple options are separated by commas.
The keep state
option is 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 egress 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:
max-src-conn number
max-src-conn-rate number / interval
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.
overload <table>
flush [global]
global
is specified, kill all states matching this source
IP, regardless of which rule created the state.
table <abusive_hosts> persist block in quick from <abusive_hosts> pass in on egress 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:
<abusive_hosts>
table
flags
keyword is used with the following syntax:
flags check/mask flags anyThe
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 egress proto tcp from any to any port ssh flags S/SA pass in on egress proto tcp from any to any port sshAs
flags S/SA
is set by default, the above rules are equivalent,
Each of these rules 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 rules, while
a packet with SYN and ACK or just ACK would not.
The default flags can be overridden by using the flags
option
as outlined above.
Be careful with using flags -- understand what is being done and why. 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 to 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 use the ECN flag, and any site using ECN that tries to connect to the client machine 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. If flags truly need to be specified, this combination should be safe:
[...] flags S/SAFRWhile 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).
The TCP SYN proxy is enabled using the synproxy state
keywords
in filter rules.
For example:
pass in on egress proto tcp to $web_server port www synproxy stateHere, 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]
log
quick
interface
af
inet
for IPv4 or inet6
for IPv6.
Example:
antispoof for fxp0 inetWhen a ruleset is loaded, any occurrences of the
antispoof
keyword
are expanded into two filter rules.
Assuming that the egress interface 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 anyThese rules accomplish two things:
fxp0
interface.
Since the 10.0.0.0/24 network is on the fxp0
interface,
packets with a source address in that network block should never be seen
coming in on any other interface.
fxp0
.
The host machine should never send packets to itself through an external
interface, so any incoming packets with a source address belonging to
the machine can be considered malicious.
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 inetUsage 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 allWith these rules, there is a risk of blocking all inbound traffic on all interfaces.
The uRPF check can be performed on packets by using the
urpf-failed
keyword in filter rules:
block in quick from urpf-failed label uRPFNote that the uRPF check only makes sense in an environment where routing is symmetric.
uRPF provides the same functionality as antispoof rules.
PF determines the remote operating system by comparing characteristics of a TCP SYN packet against the fingerprints file, which is pf.os(5) by default. Once PF is enabled, the current fingerprint list can be viewed with this command:
# pfctl -s osfpWithin 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 egress proto tcp from any os OpenBSD block in on egress proto tcp from any os "Windows 2000" block in on egress proto tcp from any os "Linux 2.4 ts" block in on egress proto tcp from any os unknownThe special operating system class
unknown
allows for matching
packets when the OS fingerprint is not known.
Take note of the following:
allow-opts
directive can be used:
pass in quick on fxp0 all allow-opts
queueing
,
nat
,
rdr
,
etc, have been left out of this example.
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 match in all scrub (no-df) # set up 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 pass out on $int_if 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 egress 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. # Uncomment last part to use the tcp syn proxy to proxy the connection. pass in log on egress proto tcp to ! <firewall> port ssh # synproxy state