Packet Filter, also known as PF or pf, is a BSD-licensed stateful packet filter used to filter TCP/IP traffic and perform Network Address Translation (NAT.) Originally created by OpenBSD, PF has been ported to FreeBSD since 5.3-RELEASE.

PF can identify where a packet should be directed or if it should even be allowed through; this can be decided based on the source and destination of that individual packet. PF can detect and block traffic you want to keep out of or in the local network. The firewall is highly flexible and even offers bandwidth management and packet priority.

1 . Enabling PF

PF relies on its kernel module, this must be enabled through /etc/rc.conf to start PF:

# sysrc pf_enable=yes

Additional options can be enabled when PF is started, this can be done by adding the following line to /etc/rc.conf. Required flags can later be specified between the two quotes(“”):

pf_flags="" # additional flags for pfctl startup

To start, PF will need to find its ruleset configuration file. FreeBSD does not ship with a ruleset or /etc/pf.conf. Custom rulesets can be used by specifying the path in /etc/rc.conf

pf_rules="/path/to/pf.conf"

Logging support for PF is provided by pflog(4). To enable logging support, add pflog_enable=yes to /etc/rc.conf:

# sysrc pflog_enable=yes

The following lines can also be added to change the default location of the log file or to specify any additional flags to pass to pflog(4) when it is started:

pflog_logfile="/var/log/pflog" # where pflogd should store the logfile
pflog_flags="" # additional flags for pflogd startup

The following option will enable NAT if there is a LAN behind the firewall:

gateway_enable="YES"

PF can now be started with logging support:

# service pf start
# service pflog start

 

2. Using pfctl

PF can be controlled using pfctl, refer to pfctl(8) for a description of all available options. Here are some of the more common pfctl options:

pfctl -e Enable PF.
pfctl -d Disable PF.
pfctl -F all -f /etc/pf.conf Flush all NAT, filter, state, and table rules and reload /etc/pf.conf.
pfctl -s [ rules | nat | states ] Report on the filter rules, NAT rules, or state table.
pfctl -vnf /etc/pf.conf Checks /etc/pf.conf for errors, but does not load ruleset.

 

3. Creating a Base PF Ruleset

PF depends on a ruleset, which can be customized to best serve any system. Creating a base ruleset is the first step in customizing your firewall that can be further augmented and specified. Create the ruleset in

Start by creating a simple ruleset that applies to only a single machine, relies on one network, and does not run services:

block in all

pass out all keep state

This rule will deny all incoming traffic, while the second rule allows connections created by this system to pass out while retaining state information on those connections. Load this new ruleset with the following:

# pfctl -e ; pfctl -f /etc/pf.conf

In addition to keeping state, PF provides lists and macros that can be defined when creating rules. Macros can include lists and need to be defined before use. As an example, insert these lines at the very top of the ruleset:

tcp_services = "{ ssh, smtp, domain, www, pop3, auth, pop3s }"

udp_services = "{ domain }"

PF can use port names and numbers if the names are listed in /etc/services. In this example, all traffic is blocked except for the connections initiated by this system for the seven specified TCP services and the one specified UDP service:

tcp_services = "{ ssh, smtp, domain, www, pop3, auth, pop3s }"
udp_services = "{ domain }"
block all pass out proto tcp to any port $tcp_services keep state
pass proto udp to any port $udp_services keep state

Next, at the top of your ruleset, create a set skip rule for the loopback device because it does not need to filter traffic and would likely bring your server to a crawl. 

set skip on lo0

Finally, add a pass out inet rule for the ICMP protocol, which allows you to use the ping(8) utility for troubleshooting

pass out inet proto icmp icmp-type { echoreq }

The ruleset should now resemble the following: 

set skip on lo0
tcp_services = "{ ssh, smtp, domain, www, pop3, auth, pop3s }"
udp_services = "{ domain }"
block all
pass out proto tcp to any port $tcp_services keep state
pass proto udp to any port $udp_services keep state
pass out inet proto icmp icmp-type { echoreq }

After each edit, the ruleset needs to be loaded again:

# pfctl -f /etc/pf.conf

pfctl will not output any messages unless there are syntax errors that will need to be fixed. During the rule load, instead of loading the ruleset, a test can be run with:

# pfctl -nf /etc/pf.conf

Including -n causes the rules to be interpreted only but not loaded. This provides an opportunity to correct any errors. The last valid ruleset loaded will be enforced until either PF is disabled or a new ruleset is loaded.

 

4. Testing Your Base Ruleset

Testing your ruleset between major edits is crucial to ensure that PF functions properly. 

First test for internet connectivity and DNS service:

# ping -c 3 google.com

Check that the pkgs repository can be reached: 

# pkg upgrade

And finally, reboot:

# reboot

Give your server a few minutes to reboot. Next, you will expand the ruleset with more advanced features to see some possible applications of the PF ruleset.

5. Example Rulesets and Their Application

Now that you have created a base ruleset, the base ruleset can be built upon for more advanced PF functions. While this guide won’t cover every possible function or customization, these basic rulesets may be helpful for your system, or offer insight into how PF may be used. After each example, make sure to test the base ruleset.

5.1 Blocking Spoofed Packets

Address spoofing is a method where an outside user forges the source IP of sent packets to conceal the actual address, essentially impersonating another internet node. This opens the door for a network attack that does not disclose where it’s originating.

The antispoof PF keyword can help protect against spoof packets:

antispoof [log] [quick] for interface [af]

log: Specifies that packets matching the criteria should be reported by pflogd (8).
quick: This ensured that if a packet meets this rules, examination of the ruleset will cease.
interface: Specify the specific network where spoofing protection will be activated.
af: Address family (i.e., inet or inet6 for IPv4 and IPv6)

The most basic way to weed out spoofed traffic coming in from external sources, as well as any spoofed packets that originate in the local network:

antispoof for $ext_if
antispoof for $int_if

 

5.2 Protecting SSH Ports

A typical exploit is to target SSH ports, which are open to the public. This is often done with brute force attacks and can succeed if the server has weak passwords. PF has built-in features that help deal with brute-force attacks. PF can limit the simultaneous connection attempts a single host allows. Once a host exceeds this number, the connection will be dropped, and they will be banned from the server. PF’s overload mechanism has a table of banned IP addresses.

Modify your previous base ruleset to limit simultaneous connections from a single host:

pass in on $vtnet0 proto tcp to port { 22 } \

keep state (max-src-conn 15, max-src-conn-rate 3/1, \

overload <bruteforce> flush global)

keep state: Allows you to define the state criteria for the overload table.
max-src-conn: Specifies the number of simultaneous connections allowed from a single host per second.
max-src-conn-rate: Specifies the number of new connections allowed from a single host per second.

If any host exceeds the specified limits, the PF overload mechanism will add the source IP to the <bruteforce> table. If a host exceeds these limits, the overload mechanism adds the source IP to the <bruteforce> table, which bans them from the server. The connection will immediately be dropped due to the flush global parameter. 

Before this ruleset can be loaded, the table you defined needs to be declared in the ruleset: 

Specify the <bruteforce> table underneath the previous icmp_types macro

icmp_types = "{ echoreq }"

table <bruteforce> persist

The persist keyword allows an empty table to exist in the ruleset. Without it, PF will complain that there are no IP addresses in the table.

5.3 Handling Non-Routable Addresses

As much as you can properly configure your system to be precise, some configuration may be needed to compensate for other people’s misconfigurations. One common mistake is to let traffic with non-routable addresses out to the Internet. Since non-routable addresses can be used in DoS attacks, consider blocking this traffic from entering the network.

Define a macro containing non-routable addresses, then use it in blocking rules. Traffic to and from these addresses is dropped on the gateway’s external interface.

external = "{ 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 }"

block drop in quick on $ext_if from $external to any
block drop out quick on $ext_if from any to $external

6. Viewing PF Logs

To view PF logs:

tcpdump -n -e -ttt -r /var/log/pflog

To view logs in real-time from the pflog0 interface, run the following command:

tcpdump -n -e -ttt -i pflog0

The pftop utility is a tool for quickly viewing firewall activity in real-time; it can be installed and started with:

pkg install pftop

pftop