Pf syntax

From wikinotes

Documentation

(FreeBSD) pf intro/examples https://www.freebsd.org/doc/handbook/firewalls-pf.html
(FreeBSD) man pf.conf https://www.freebsd.org/cgi/man.cgi?query=pf.conf&apropos=0&sektion=0&manpath=FreeBSD+12.1-RELEASE+and+Ports&arch=default&format=html
(FreeBSD) pfsense docs https://docs.netgate.com/pfsense/en/latest/trafficshaper/traffic-shaping-guide.html
(FreeBSD) man pfctl https://www.freebsd.org/cgi/man.cgi?query=pfctl&apropos=0&sektion=0&manpath=FreeBSD+12.1-RELEASE+and+Ports&arch=default&format=html

Tutorials

digitalocean guide https://www.digitalocean.com/community/tutorials/how-to-configure-packet-filter-pf-on-freebsd-12-1
traffic shaping with PF/ALTQ http://aptivate.org/en/blog/2011/08/05/traffic-shaping-with-pf-altq-and-hfsc/
beginner guide to pf http://srobb.net/pf.html

Locations

/etc/rc.conf service configuration
/etc/pf.conf ruleset
/etc/services named services/port mapping

Overview

Read man pf.conf, it is very well written.

Statement Types

Type Description
Macros variables
Tables arrays of addresses, address-ranges, or network interfaces
Queueing rule based bandwidth/priority control
Options global or scoped pf configuration options
Translation NAT, redirect, etc.
Packet Filtering rules to pass/block packets

Overview

ruleset flow
By defaut, every packet goes through every rule in the firewall (no early exits on match).
This allows you to blacklist everything, then whitelist only the traffic that you want.
You can alter this for rules using the quick keyword (exits ruleset on match).

stateful firewall
This is very confusing. My understanding is that upon traffic creation, a traffic decision is created that is reused by related traffic that follows. This avoids unecessy rule processing on traffic that is essentially the same. keep state modulate state and other keywords are used on rules to alter this behaviour.

Note that keep state is the default behaviour on OpenBSD, but must be explicitly defined on FreeBSD

See explanation https://www.openbsd.org/faq/pf/filter.html#state

Example

# =========
# Variables
# =========
ext_if = "eth0"
lo_if = "lo0"
localnet = $lo_if:network  # alternatively private addr range
private_ranges = "{ 127.0.0.0/8, 192.168.0.0/16, 172.16.0.0/12, \
                    10.0.0.0/8, 169.254.0.0/16, 192.0.2.0/24, \
                    0.0.0.0/8, 240.0.0.0/4 }"

# ==================
# General Protection
# ==================

# policy 'drop' by default
block all

# repair/protect input traffic
scrub in all fragment reassemble

# protect ext_if against forged IPs
antispoof for $ext_if

# explicitly block private address ranges 
# from accessing external internet
block drop in quick on $ext_if from $private_ranges to any
block drop out quick on $ext_if from any to $private_ranges

# reject ICMP, unless from localnet
pass inet proto icmp from $localnet to any keep state
pass inet proto icmp from any to $ext_if keep state

# ==============
# SSH Protection
# ==============

table <bruteforce> persist
block quick from <bruteforce>

# protect against quick bruteforce
pass inet proto tcp from any to $localnet port 22 \
    flags S/SA keep state \
    (max-src-conn 100,          `# max simultaneous connections` \
     max-src-conn-rate 15/5,    `# max 15conns/5s` \
     overload <bruteforce>      `# offenders added to <bruteforce> table` \
     flush global)               # on blacklisting, all connections from that host will be terminated

# =====
# Rules 
# =====

pass out any

pass in \
  on eth0 \
  proto tcp \
  to port { 22, 80, 443 }
# set <bruteforce> table entries expire if not renewed in 86400s
pfctl -t bruteforce -T expire 86400

Components

Formatting

# example comment

# multiline statements using '\'
table <private_addrs> const \
  { 10/8, 172.16/12, 192.168/16 }

Macros (variables)

# syntax
variable = "value"
array = "{ lo0 eth0 wlan0 }"  # array of interfaces
range = 137:139               # array of ports 137-139 (samba)

# example use within rule
lo_if = "lo1"
pass out on $lo_if from any to any

Tables

Unlike iptables, in pf tables are a named collection of addresses, address-ranges, and/or exclusively on OpenBSD (not FreeBSD) interfaces.
Tables can be defined in pf.conf, read from a file, or managed dynamically with pfctl.

Basics

# from pf.conf
table <private_addrs> const { 10/8, 172.16/12, 192.168/16 }

# from file
table <spam_addrs> persist file "/etc/spammers"  # [file "/etc/another" ...]

# fully dynamic
table <bad_hosts> persist

You can then refer to tables with rules.

block in on em0 to <bad_hosts>


Tables can be pre-defined and static, or dynamic - with entries created by pfctl/other-rules.
The type of table is determined by the keyword used at it's creation.

persist tables is not deleted even when no other rules refer to it.
const table cannot be modified by pfctl/other rules.
counters todo

Dynamic Table Options

You can use pfctl to manage tables dynamically (ex: expiry dates for entries).

# set <bruteforce> table entries expire if not renewed in 86400s
pfctl -t bruteforce -T expire 86400    # table 'bruteforce' entries expire if not renewed in 86400s
pfctl -t bruteforce show               # show table 'bruteforce' contents
pfctl -t bruteforce -T add 1.1.1.1     # add '1.1.1.1' to table 'bruteforce'
pfctl -t bruteforce -T delete 1.1.1.1  # delete '1.1.1.1' from table 'bruteforce'

Interface Groups

Exclusively on FreeBSD, you can refer to collections of network interfaces using interface groups. These are managed using ifconfig

ifconfig -g epair                    # list all members of epair
ifconfig epair1a  group vnet_epairs  # add epair1a to vnet_epairs
ifconfig epair1a -group vnet_epairs  # remove epair1a from vnet_epairs

Options

Options generally configure pf as a whole, but you can also use With blocks for more limited scopes. There are many options, see pf.conf for the full list.

Some Interesting Options:

set block-policy, fail-policy

Change the action that occurs on block or drop etc.
For example, do you want to silently drop the packet? or return invalid?

set fail-policy drop
set block-policy return


set skip on <ifspec>

Define network interfaces you'd like to skip packet filtering on.

set skip on lo0

Traffic Normalization

Traffic normalization sanitizes incoming packets, to try to prevent attacks where packets have been altered with misleading information. It also reassembles partial/incomplete fragments.

This is complex, see pf.conf]

# Recommended
scrub in all

# Example with params
scrub in on $ext_if all fragment reassemble

Queueing

Queueing allows you to organize/prioritize your traffic.
For all options see pf.conf

Queueing Involves 2x steps.

  1. Assign Queues to an interface (all traffic will pass through one of these queues)
  2. Define Queues (bandwidth, priority, matched traffic, etc)

1. Assign Queues

You must assign queues to an interface using altq.
This requires network card drivers, see manual section Enabling ALTQ, it may also need a kernel rebuild.

# ===========================================================================
# altq on <iface> <type> \
#   [bandwidth <bw> qlimit <limit> size <size> queue { std, http, ssh, ... }]
# ===========================================================================

# Assign Queues
altq on eth0 cbq queue { developers, business, services }

# limit queue bandwidth (so it does not take from others)
altq on eth0 cbq queue bandwidth 1MB { developers, business, services }

2. Define Queues

Define queues that altq statements refer to.

# ==============================================================
# queue <name> [general queue-options...] [type(type_args, ...)]
# ==============================================================
queue ssh bandwidth 30% priority 2 hfsc(upperlimit 99%)

priq (Priority Queueing)

Prioritize traffic, without any regard for bandwidth.
Packets of highest priority are always processed first.

cbq (Class Based Queueing)

Same as PRIQ, except in addition to priority, bandwidth is taken into consideration.
High priority is processed first, until it's the bandwidth limit is reached, then other queues get processed.
Supports composing a hierarchical tree of queues, and borrowing from parent queue's bandwidth.

Examples

# =====================================
# NOTE: !INCOMPLETE/UNRELATED EXAMPLES!
# =====================================
queue std bandwidth 10% \
  cbq(default)

queue http bandwidth 60% \
  priority 2 cbq(borrow red) \
  { employees, developers }

queue  developers bandwidth 75%  cbq(borrow)
queue  employees  bandwidth 15%

hfsc (Hierarchical Fair Service Curve)

HFSC is the most advanced queue type, and it offers real-time traffic guarantees.
Concrete information about how this works does not appear in the docs, but it seems to be the preferred type for most situations.

Guarantee min bandwidth for services


# http://aptivate.org/en/blog/2011/08/05/traffic-shaping-with-pf-altq-and-hfsc/
# We create four named queues under the root, which is 
# implicitly named root_em1. We reserve 30% of bandwidth 
# each for FTP, SSH and other traffic, and 10% for ICMP. 
# However, any class can exceed its reserved bandwidth, 
# up to the upperlimit, which defaults to 100%, which means 
# that one class can potentially cause delays to traffic 
# in other classes, so we override this to 99%.

altq on em1 hfsc bandwidth 1Mb queue { ftp, ssh, icmp, other }
queue ftp bandwidth 30% priority 0 hfsc (upperlimit 99%)
queue ssh bandwidth 30% priority 2 hfsc (upperlimit 99%)
queue icmp bandwidth 10% priority 2 hfsc (upperlimit 99%)
queue other bandwidth 30% priority 1 hfsc (default upperlimit 99%)
pass out quick on bridge0 inet proto tcp from any port 21 to any queue ftp
pass out quick on bridge0 inet proto tcp from any port 22 to any queue ssh
pass out quick on bridge0 inet proto icmp from any to any queue icmp
pass out quick on bridge0 all

Limit up/down bandwidth


# http://microsux.dk/?p=321
int_if="em0"
ext_if="em1"
internal_net="10.10.10.0/24"
external_addr="x.x.x.x"
  
altq on $ext_if hfsc bandwidth 1950Kb queue {def_up}
altq on $int_if hfsc bandwidth 1950Kb queue {def_down}
  
queue def_up bandwidth 1950Kb hfsc(default)
queue def_down bandwidth 1950Kb hfsc(default)
  
nat on $ext_if from $internal_net to any -> $external_addr
  
pass in quick on $ext_if from any to any
pass out quick on $int_if from any to any queue def_down
  
pass in quick on $int_if from any to any
pass out quick on $ext_if from any to any queue def_up

Translation (NAT/Redirect)

NOTE:

for rdr statements you must enable gateway_enable="YES" (on both sides)

Translation rules modify either the source or destination address of a packet.
See Examples, and Translation from man pf.conf.
This can be constrained to a specific port, or it could be all traffic on an interface.

# bidirectional between external/internal IP netblock
#
# $src addr traveling to $dst -- $src replaced with $ext
# $dst addr traveling to $src -- $src replaced with $ext
#
# inside/outside both think they are talking directly to middleman (and have no knowledge of the other)
binat from $src to $dst -> $ext
# nat traffic (change source)
# I belive NAT is from the point of view of the source (-> ip-address)
nat on $ext_if from 10.0.0.1 to any -> ($ext_if)
# redirect traffic (change dest)
rdr on $ext_if proto tcp from any to 1.1.1.1 port 2222 -> 2.2.2.2 port 22

rdr pass on bce0 proto tcp from any to $IP_PUB port $SQLPORT -> $SQLJAIL

Packet Filtering

Here you can block/pass matching packets.
See all options in man pf.conf].

Unlike iptables, rules to not stop being processed after the first matching rule.
Generally, you start with a very strict policy, then gradually whitelist traffic afterwards.
All packet filtering rules start with the keyword block or pass, then optionally can be configured to very explicitly match a type of traffic.

# Example
block all                  # all traffic blocked
pass in { 22, 80, 443 }    # define 'in' traffic
pass out all keep state    # all 'out' traffic allowed
# Some unrelated examples to show options
block all
block in all

pass out all \
  keep state

pass out \
  on eth0 \
  inet proto tcp \
  to port { 22, 80, 443 }

pass out \
  to any

pass out \
  to port { 22, 80, 443 }

# when quick is used, if packet matches this rule,
# it's operation is taken, and the rest of the rules are not applied to it.
# (more similar to iptables)
block in quick \
  on eth0 \
  from ! 192.168.1.1 \
  to any

Other

Anchors

You can specify alternate firewall configurations that are tailored to a specific task, then use them to protect operations (say for example an FTP download).

# /etf/ftp-anchor
pass out proto tcp from any to port 21 keep state
pass out proto tcp from any to port > 1023 keep state
# ...
anchor ftpanchor
pfctl -a ftpanchor -s rules  # temporarily replace firewall
pfctl -a ftpanchor -F rules  # restore default rules