Skip to main content

FreeBSD Firewall Configuration with PF

Published on:
.
25 min read
.
For German Version

Firewalls allow for the filtering of a system's incoming and outgoing traffic. A firewall utilizes one or more sets of "rules" to evaluate network packets as they enter or exit network connections and either permit or prohibit the traffic. A firewall's rules check one or more packet attributes, including the protocol type, source/destination host address, and source/destination port.

Firewalls increase the security of a network or host by protecting and isolating the applications, services, and devices of an internal network from undesired Internet traffic. They are used to limit or disable the access of hosts on the internal network to Internet services. Moreover, firewalls support network address translation (NAT), which enables an internal network to utilize private IP addresses and share a single connection to the public Internet using a single IP address or a pool of automatically allocated public addresses.

In this article, we will cover which firewalls FreeBSD uses, what the PF, Packet Filter, is, and how you can easily configure PF firewall rules on your FreeBSD server.

What Firewall does FreeBSD Use?

FreeBSD's base system has the following three firewalls:

  • PF

  • IPFW

  • IPFILTER, also known as IPF.

FreeBSD supports several firewalls to accommodate the diverse needs and preferences of a large number of users. Each firewall employs rules to govern packet access to and from a FreeBSD system, but in various methods and with distinct rule syntax. Each user must choose which firewall best suits their requirements.

Additionally, FreeBSD includes two traffic shapers for managing bandwidth utilization:

  • ALTQ

  • dummynet

ALTQ has historically had a tight relationship with PF and dummynet with IPFW.

Get Started with Zenarmor Today For Free

What is PF?

Packet Filter, also known as PF or pf, is a BSD-licensed stateful packet filter that is used to filter TCP/IP traffic and perform Network Address Translation (NAT.) PF was created by Daniel Hartmeier for OpenBSD and is currently maintained and developed by the OpenBSD team. PF has been ported to several other operating systems, like FreeBSD and DragonflyBSD, today. Since OpenBSD 3.0, PF has been a component of the GENERIC kernel.

PF functions mostly in kernel space, inside the network code. Based on the source or destination of a packet, as well as the protocol, connection, and port for which it is intended, PF is able to identify where the packet should be directed or whether it should be let through.

One of the most essential features of PF is that it can detect and block traffic that you do not wish to let into your local network or let out to the outside world. In addition to normalizing and conditioning TCP/IP traffic, PF offers bandwidth management and packet priority. The packet filter is a highly flexible and valuable instrument for controlling network activity.

BEST PRACTICE

In addition the its effective L4 packet filtering and routing features, FreeBSD also provides next-generation firewall capabilities such as web control and application control. This is provided by an external tool called Zenarmor.

Zenarmor NGFW extension allows you to easily upgrade your firewall to a Next Generation Firewall in seconds. NG Firewalls empower you to combat modern-day cyber attacks that are becoming more sophisticated every day.

Some of the capabilities are layer-7 application/user aware blocking, granular filtering policies, commercial-grade web filtering utilizing cloud-delivered AI-based Threat Intelligence, parental controls, and the industry's best network analytics and reporting.

Zenarmor Free Edition is available at no cost for all FreeBSD users.

What is the History of PF?

The Packet Filter subsystem of OpenBSD, which is commonly referred to by its abbreviation 'PF', was written by Daniel Hartmeier and a number of other OpenBSD developers during the northern hemisphere summer and autumn months of 2001 and was released as a default component of the OpenBSD 3.0 base system in December of 2001.

IPFilter by Darren Reed has been included in the OpenBSD base install since 1996. It was eliminated because its licensing was incompatible with OpenBSD's mission to provide software that is free to use, alter, and redistribute for everyone.

OpenBSD required a new firewalling software subsystem when Darren Reed informed the world that IPFilter, which had been deeply incorporated into the operating system, was not in fact BSD licensed. In reality, the opposite is true. The license was almost identical to the BSD license, with the exception of the permission to modify the code and distribute the modified version. According to the license, the OpenBSD version of IPFilter featured several modifications and customizations that were not permitted. IPFilter was deleted from the OpenBSD source tree on May 29, 2001, and OpenBSD-current lacked any firewalling software for many weeks.

Fortunately, Daniel Hartmeier was already doing modest kernel hacking tests in the networking code in Switzerland. After connecting a little function of his own to the networking stack and allowing packets to travel through it, he eventually began to consider filtering. Then, a licensing dilemma occurred.

On May 29th, IPFilter was removed from the source tree. The initial commit of the PF code occurred on June 24, 2001.

Following a few months of quite active development, the version of PF released with OpenBSD 3.0 had a fairly comprehensive implementation of packet filtering, including network address translation.

Daniel Hartmeier and the other PF developers seem to have made effective use of their knowledge of the IPFilter code. Daniel presented a presentation at USENIX 2002 with performance experiments demonstrating that OpenBSD 3.1 PF performed as well as or better under stress than IPFilter on the same platform or iptables on Linux. In addition, the original PF from OpenBSD 3.0 has been tested. These tests mostly demonstrated that the code had become more efficient between versions 3.0 and 3.1.

OpenBSD already featured the ALTQ functionality to manage load balancing and traffic shaping before PF was implemented. After some time, ALTQ was included in PF. primarily for pragmatic considerations. As a consequence, all of these functions are accessible through a single configuration file, pf.conf, which is located in the /etc/ directory and is practically human-readable.

This is currently part of the basic system of OpenBSD, FreeBSD, NetBSD, and DragonflyBSD. On FreeBSD, PF is one of three firewalling systems that may be loaded on demand.

How to Configure PF Firewall on FreeBSD?

You may easily configure PF Firewall on FreeBSD to protect your server or network from cyber threats by following the instructions explained in the following steps.

How to Enable PF?

Before PF can be used, its kernel module must be loaded. You may enable PF by following the steps below:

  1. Run the next command to add the line to /etc/rc.conf the configuration file.
echo 'pf_enable="YES"' >> /etc/rc.conf
  1. PF will not start if its ruleset configuration file cannot be located. Example rulesets are available in the /usr/share/examples/pf directory. In our example, we will add our firewall rules to the /etc/pf.conffile. If your custom ruleset was stored elsewhere, add a line to /etc/rc.conf that provides the file's entire path:
echo 'pf_rules="/etc/pf.conf"' >> /etc/rc.conf
  1. To enable logging functionality, add pflog enable=yes to /etc/rc.conf by running next command:
echo 'pflog_enable="YES"' >> /etc/rc.conf
  1. Additionally, run the following commands to alter the default location of the log file or to provide extra options to give to pflog when it is started:
echo 'pflog_logfile="/var/log/pflog"' >> /etc/rc.conf
echo 'pflog_flags=""' >> /etc/rc.conf
  1. Run the following command if there is a LAN behind the firewall and packets must be sent to the machines on the LAN, or if NAT is required:
echo 'gateway_enable="YES"' >> /etc/rc.conf
  1. To start PF with logging support type the following commands:
service pf start
service pflog start

You should see the following output. If you don't have a firewall ruleset such as /etc/pf.conf, a warning message will be displayed.

# service pf start
pf_enable: NO -> yes
pf_enable: yes -> yes
/etc/rc.d/pf: WARNING:/etc/pf.conf is not readable.
# service pflog start
pf_enable: yes -> yes
pf_enable: yes -> yes
Starting pflog.
  1. You may control PF using pfctl utility. To enable PF you may run the following command. Since our firewall ruleset is not ready, we don't start pf firewall in this step.
pfctl -e

How to Use pfctl?

pfctl is a utility that talks with the packet filter device using the ioctl interface for controlling the packet filter (PF) device. It permits ruleset and parameter configuration and the retrieval of packet filter status information. To view pfctl options run the next command:

pfctl -h

You should see the following output:

pfctl [-AdeghMmNnOPqRrvz] [-a  anchor] [-D  macro= value] [-F  modifier]
[-f file] [-i interface] [-K host | network] [-k host | network |
label | id | gateway] [-o level] [-p device] [-s modifier] [-t
table -T command [address ...]] [-x level]
  • To enable packet filter (PF), run the following command:
pfctl -e
  • To disable packet filter (PF), run the following command:
pfctl -d
  • To flush all NAT, filter, state, and table rules and reload /etc/pf.conf, run the following command:
pfctl -F all -f /etc/pf.conf
  • To view the currently loaded filter rules, NAT rules, state table or all, run the following command:
pfctl -s [ rules | nat | states | all]
  • Check firewall ruleset, /etc/pf.conf, for errors without loading the ruleset, run the next command:
pfctl -vnf /etc/pf.conf

How to Create PF Rulesets?

At startup time, PF receives its configuration rules from pf.conf, as loaded by rc scripts. Note that although pf.conf is the default configuration file and is imported by the system rc scripts, it is only a text file loaded and processed by pfctl and placed into pf. Some apps may load additional rulesets from other files upon startup.

The pf.conf file, firewall ruleset, has several sections:

  • Macros: Variables defined by the user that may include IP addresses, interface names, etc. Macros may include lists and must be specified before usage.

  • Tables: a structure used to store IP address listings.

  • Options: different configuration settings for PF.

  • Filter Rules: permits the selective filtering or stopping of packets as they traverse any interface.

Lines that begin with # are considered comments and are ignored if they are blank.

What are Macros and Lists?

A list enables numerous comparable criteria to be specified inside a rule. For example, numerous protocols, port numbers, addresses, etc. Instead of setting a separate filter rule for each IP address that must be prohibited, a list of IP addresses may be specified in a single rule. Brackets, { }, are used to define items within a list. When pfctl encounters a list during the ruleset loading process, it generates several rules, one for each list item. For example:

block out on vtnet0 from { 192.168.0.1, 192.168.0.2 } to any

This pf ruleset with a list gets expanded to the following two rules:

block out on vtnet0 from 192.168.0.1 to any
block out on vtnet0 from 192.168.0.2 to any

Macros are user-defined variables that may include IP addresses, port numbers, and interface names, among other information. Macros minimize the complexity of PF rulesets and simplify their maintenance.

tip

The names of macros must begin with a letter and may include letters, numbers, and underscores. The reserved terms pass, out, and queue cannot be used as macro names.

tip

When a macro is referred to after it's been created, its name is preceded with a $ character

ext_if = "vtnet0"
block in on $ext_if from any to any

This creates a macro named ext_if.

Macros may expand to lists, like given below:

web_servers = "{ 192.168.1.1, 192.168.1.2, 192.168.1.3 }"

This section illustrates how to design an individualized ruleset. It begins with the simplest of rulesets and develops upon them using several examples to illustrate the practical use of PF's various capabilities.

The simplest ruleset is for a single computer that runs no services and requires access to a single network, such as the Internet. To generate this simple set of rules, update /etc/pf.conf by running the next commands:

echo 'block in all' >> /etc/pf.conf
echo 'pass out all keep state' >> /etc/pf.conf

By default, the first rule rejects all incoming traffic.

The second rule permits connections generated by this system to be sent out while keeping their state information. This state information enables return traffic for certain connections and should only be utilized on trustworthy computers.

What is Table in PF?

A table is used to store a collection of IPv4 and/or IPv6 addresses. Table lookups are very quick and utilize less memory and CPU time than list lookups. Consequently, a table is perfect for storing a big number of addresses, since the search time for a table containing 50,000 addresses is just somewhat more than that of a table containing 50 addresses. Uses for tables include the following:

  • Source and/or destination address in rule source and/or destination

  • The rule alternatives for translation and redirection are nat-to and rdr-to, respectively.

  • The destination address for the route-to, reply-to, and dup-to rule options.

Tables may be created in either pf.conf or pfctl.

How to add Table?

You may create tables using the table directive in your ruleset configuration file, pf.conf. For each table, the following properties may be specified:

  • const: The table's contents cannot be altered after the table has been established. If not provided, pfctl may be used to add or delete addresses from the table at any time, even while operating with a securelevel of two or higher.

  • persist: Instructs the kernel to retain the table in memory even if no rules reference it. Without this property, the kernel will delete the table when the last rule that references it is flushed.

table <goodguys> { 12.1.0.0/24 }
table <rfc1918> const { 192.168.0.0/16, 172.16.0.0/12, 10.0.0.0/8 }
table <spammers> persist
block in on vtnet0 from { <rfc1918>, <spammers> } to any
pass in on vtnet0 from <goodguys> to any

Table names are always surrounded in <> angle brackets.

Text files that include a list of IP addresses and networks may also be used to fill tables.

table <spammers> persist file "/etc/spammers"
block in on vtnet0 from <spammers> to any

The file /etc/spammers would have a line-separated list of IP addresses and/or CIDR network blocks.

Using pfctl, tables may be modified on the fly. For instance, to add entries to the <spammers> table created above, run the next command:

pfctl -t spammers -T add 23.23.11.0/24

This will create the <spammers> table if it doesn't already exist. To view the addresses in a table, run the next command:

pfctl -t spammers -T show

The -v argument is used with -T show argument to display statistics for each table entry. To remove addresses from a table, run the next command:

pfctl -t spammers -T delete 23.23.11.0/24

How to Packet Filtering?

Packet filtering is the selective transmission or rejection of data packets across a network interface. When evaluating packets, pf employs criteria based on the Layer 3 (IPv4 and IPv6) and Layer 4 (TCP, UDP, ICMP, and ICMPv6) headers. Source and destination address, source and destination port, and protocol are the most often used criteria.

Filter rules describe the criteria that a packet must meet and the action that is done if a match is detected, either block or pass. Evaluation of filter rules is performed in sequence, from first to last. Before the final action is made, the packet will be assessed against all filter rules unless it meets a rule containing the fast keyword. The action to be taken on the packet is determined by the "winning" rule, which is the last rule to match. There is an implicit pass all at the beginning of a filtering ruleset, indicating that if a packet does not meet any filter rule, the consequent action will be pass.

What is Rule Syntax?

The simplified general syntax for filter rules in PF is given below:

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: The action to be performed on packets that match, either pass or block. The pass action returns the packet to the kernel for further processing, while the block action responds depending on the block-policy option setting. You may override the default response by providing either block drop or block return.

  • direction: The packet's movement on an interface, either in or out.

  • log: Specifies that pflogd should log the packet. If the rule establishes a state, just the packet establishing the state is recorded. Use log to record all packets regardless (all).

  • quick: If a packet matches a rule that specifies quick, that rule is regarded as the last matching rule, and the given action is carried out.

  • state: Specifies whether packets matching this rule retain state information.

    • no state: TCP, UDP, and ICMP are supported. Statefully, PF will not trace this connection. For TCP connections, flags any is often necessary as well.
    • keep state: compatible with TCP, UDP, and ICMP. This is the default setting for all filter rules.
    • modulate state: only supported by TCP. Initial Sequence Numbers (ISNs) will be generated by PF for packets meeting this rule.
    • synproxy state proxies incoming TCP connections to defend servers from faked TCP SYN floods. This option combines the features of maintain state and modify state.

What is Default Deny Rule?

When configuring a firewall, it is advised to use a "default deny" policy. That is, to refuse all traffic and then selectively let some across the firewall. This implicit deny method is suggested since it errs on the side of caution and simplifies the creation of a ruleset.

The first filter rule for a default-deny filter policy should be as given below:

block all

This will block all traffic on all interfaces from everywhere to anyplace in both directions.

How to Define Passing Traffic Rule?

After adding a Default Deny rule, traffic must now be expressly let via the firewall. otherwise, the default refuse policy will discard it. Here, packet characteristics such as source/destination port, source/destination address, and protocol come into play. Whenever traffic is allowed to traverse the firewall, the corresponding rule(s) should be as restrictive as feasible. This is done to guarantee that only the desired traffic is allowed to pass.

Some examples:

# Pass traffic on vtnet0 from the local network, 192.168.0.0/24, to the IP address 192.168.0.1 of the FreeBSD computer. Additionally, transmit the return traffic on vtnet0.

pass in on vtnet0 from 192.168.0.0/24 to 192.168.0.1
pass out on vtnet0 from 192.168.0.1 to 192.168.0.0/24


# Allow the web server running on FreeBSD to receive TCP traffic.

pass in on egress proto tcp from any to egress port www

How to Use quick Keyword?

As stated before, each packet is examined against the filter ruleset in ascending order. The packet is designated for passing by default, which may be altered by any rule and may be altered several times until the conclusion of the filter rules. One exception exists to the rule that the last matched rule wins: The fast option on a filtering rule prevents the rule from being processed further and executes the given action. Let's look at a few examples:

block in on egress proto tcp to port ssh
pass in all

In this example, the block line may be assessed, but it will have no impact since it is immediately followed by a line that will pass everything. It is better to write the rule as given below:

block in quick on egress proto tcp to port ssh
pass in all

These rules are assessed somewhat differently. If the block line is matched by the fast option, the packet is blocked and the remainder of the ruleset is disregarded.

How to Keep State?

"Keeping state" or "stateful inspection" is an essential ability of the PF. Stateful inspection refers to PF's capacity to monitor the status or development of a network connection. By keeping information about each connection in a state table, PF can rapidly assess whether a packet going through the firewall is part of an already-established connection. If so, the packet is allowed to travel through the firewall without undergoing ruleset review.

Keeping state offers several benefits, such as simplified rulesets and enhanced packet filtering performance. PF is able to match packets going in either direction to state table entries, eliminating the requirement for filter rules that permit returning traffic. Due to the fact that packets matching stateful connections do not undergo ruleset review, the amount of time PF spends processing these packets may be significantly decreased.

When a rule establishes state, the first packet meeting the rule establishes "state" between the sender and recipient. Now, not only do packets traveling from sender to receiver match the state entry and skip ruleset evaluation, but also reply packets traveling from receiver to sender do so as well.

When a packet meets a pass rule, each rule immediately creates a state entry. This may be deactivated directly using the no state option.

pass out on egress proto tcp from any to any

This rule authorizes outgoing TCP traffic on the egress interface and also allows reply traffic to flow through the firewall. Keeping state drastically increases speed, since state lookups are far quicker than passing a packet through the filter rules.

The modulate state option functions identically to the keep state option, but it only applies to TCP packets. modulate state randomizes the initial sequence number (ISN) of outbound connections. This is helpful for safeguarding connections launched by operating systems that choose ISNs improperly. The modulate state option may be used in rules that specify protocols other than TCP to simplify rulesets. In such instances, the state is considered to be kept.

To keep state of outgoing TCP, UDP, and ICMP packets, and modulate TCP ISNs, you may use the next ruleset:

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

Additionally, keeping state allows relevant ICMP traffic to flow past the firewall. For instance, if a TCP connection traversing the firewall is being monitored statefully and an ICMP source-quench message pertaining to this TCP connection comes, it will be matched to the corresponding state item and routed through the firewall.

Globally, the state-policy runtime option and per-rule, the if-bound and floating state option keywords determine the scope of a state entry. These per-rule keywords have the same significance as when they are used in conjunction with the state-policy option. For instance:

pass out on egress proto { tcp, udp, icmp } from any to any modulate state (if-bound)

This rule would stipulate that packets must be traversing the egress interface in order to match the status entry.

How to Block Spoofed Packets?

Address spoofing occurs when a malevolent user forges the source IP address of sent packets in order to conceal the actual address or impersonate another network node. Once the address has been faked, a network attack may be conducted without disclosing the assault's actual origin. Additionally, an attacker might try to access network services that are limited to certain IP addresses.

The antispoof keyword gives some protection against address spoofing in PF:

antispoof [log] [quick] for interface [af]
  • log: Specifies that packets matching the criteria should be reported by pflogd (8).

  • quick: If a packet meets this rule, it will be deemed the "winning" rule and examination of the ruleset will cease.

  • interface: The network interface on which spoofing protection should be activated. This may be a list of interfaces as well.

  • af: The address family to enable spoofing protection for, either inet or inet6 for IPv4 and IPv6, respectively.

antispoof for vtnet0 inet

Any instances of the antispoof term in a loaded ruleset are extended into two filter rules. The aforementioned antispoof rule would be expanded to:

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

These guidelines achieve two goals:

  • Prevents any traffic from the 10.0.0.0/24 network from entering via any other interface than vtnet0. Since the 10.0.0.0/24 network is associated with the vtnet0 interface, packets having a source address in this network block should never arrive on any other interface.

  • Prevents all incoming traffic from the IP address 10.0.0.1 on vtnet0. Any inbound packets with a source address corresponding to the host computer should be deemed malicious.

The expanded filter rules for the antispoof rule will additionally block packets delivered to local addresses through the loopback link. It is great practice to skip filtering on loopback interfaces anyhow, but antispoof standards need this.

set skip on lo0
antispoof for vtnet0 inet

Only IP-assigned interfaces should be permitted to use antispoof. Using antispoof on an interface without an IP address will result in such filter rules as:

block drop in on ! vtnet0 inet all
block drop in inet all

There is a possibility of banning all incoming traffic on all interfaces with these rules.

How to Create PF Ruleset For a Web Server?

PF understands port names as well as port numbers, as long as the names are listed in /etc/services. In this example, all traffic is blocked except for the connections initiated by this system for the specified TCP and UDP services:

int_tcp_services = "{ domain, ntp, smtp, www, https, ftp, ssh }"
int_udp_services = "{ domain, ntp }"
block all
pass out proto tcp to any port $tcp_services keep state
pass proto udp to any port $udp_services keep state

You may use the following PF ruleset sample to protect your web server:

## Set your public interface ##
ext_if="vtnet0"

## Set your server public IP address ##
ext_if_ip="22.33.44.55"

## Set and drop these IP ranges on public interface ##

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

## Set http(80)/https (443) port here ##

webports = "{http, https}"

## enable these services ##

int_tcp_services = "{domain, ntp, smtp, www, https, ftp, ssh}"
int_udp_services = "{domain, ntp}"

## Skip loop back interface - Skip all PF processing on interface ##

set skip on lo

## Sets the interface for which PF should gather statistics such as bytes in/out and packets passed/blocked ##

set loginterface $ext_if

## Set default policy ##

block return in log all
block out all

# Deal with attacks based on incorrect handling of packet fragments

scrub in all

# Drop all Non-Routable Addresses

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

## Blocking spoofed packets

antispoof quick for $ext_if

# Open SSH port which is listening on port 22 from 22.22.33.33 IP only

# We do not allow or accept ssh traffic from ALL for security reasons

pass in quick on $ext_if inet proto tcp from 22.22.33.33 to $ext_if_ip port = ssh flags S/SA keep state label "USER_RULE: Allow SSH from 22.22.33.33"

## Use the following rule to enable ssh for ALL users from any IP address #

## pass in inet proto tcp to $ext_if port ssh

### [ OR ] ###

## pass in inet proto tcp to $ext_if port 22

# Allow Ping

pass inet proto icmp icmp-type echoreq

# All access to our Nginx/Apache/Lighttpd Webserver ports

pass proto tcp from any to $ext_if port $webports

# Allow essential outgoing traffic

pass out quick on $ext_if proto tcp to any port $int_tcp_services

pass out quick on $ext_if proto udp to any port $int_udp_services

# Add custom rules below

How to Configure Gateway with NAT?

This section explains how to setup a FreeBSD system running PF to serve as a gateway for at least one additional system. Each network interface on the gateway must be linked to a different network. xl0 is linked to the Internet in this case, whereas xl1 is connected to the internal network To configure a simple gateway with NAT on your FreeBSD, you may follow the steps given below.

  1. Enable the gateway so that network traffic received on one interface may be sent to another interface. Run the following sysctl setting command to forward IPv4 packets:
sysctl net.inet.ip.forwarding=1
  1. To forward IPv6 traffic, run the next command:
sysctl net.inet6.ip6.forwarding=1
  1. To activate these options during system startup, add them to /etc/rc.conf using sysrc:
sysrc gateway_enable=yes
sysrc ipv6_gateway_enable=yes
  1. Create the PF rules that will let the gateway to transmit traffic. While the following rule permits stateful traffic from internal network hosts to flow through the gateway, the to keyword does not ensure transit from source to destination:
pass in on xl1 from xl1:network to xl0:network port $ports keep state
  1. Above rule only permits traffic to enter the gateway via its internal interface. To allow packets to proceed, a matching rule is required:
pass out on xl0 from xl1:network to xl0:network port $ports keep state
tip

These two rules might be consolidated into one:

pass from xl1:network to any port $ports keep state

$localnet macro could be defined as the network directly attached to the internal interface ($xl1:network). $localnet might also be defined using an IP address/netmask notation, such as 192.168.100.1/24 for a subnet of private addresses.

$localnet might be configured as a list of networks if necessary. Regardless of the precise requirements, the following definition of $localnet might be used in a standard pass rule:

pass from $localnet to any port $ports keep state

The following ruleset adds the nat rule, which manages network address translation from non-routable addresses inside the internal network to the IP address allocated to the external interface. When the IP address of the external interface is dynamically allocated, the parentheses enclosing the last portion of the nat rule ($ext if) must be present. It guarantees that network traffic continues to operate normally even if the external IP address changes.

ext_if = "xl0"  # macro for external interface
int_if = "xl1" # macro for internal interface
localnet = $int_if:network

# ext_if IP address could be dynamic, hence ($ext_if)

nat on $ext_if from $localnet to any -> ($ext_if)
block all
pass from { lo0, $localnet } to any keep state
tip

This ruleset probably permits more traffic to leave the network than is required. One such configuration may generate this macro:

client_out = "{ ftp-data, ftp, ssh, domain, pop3, auth, nntp, http, https, cvspserver, 1528, 5999, 8000, 8080 }"

Use above macro in the main pass rule:

pass inet proto tcp from $localnet to any port $client_out flags S/SA keep state

Several more pass rules may be required. The following ruleset provides SSH access on the external interface:

pass in inet proto tcp to $ext_if port ssh

Next macro definition and rule enables internal clients to use DNS and NTP:

udp_services = "{ domain, ntp }"
pass quick inet proto { tcp, udp } to any port $udp_services keep state

How to Create FTP Proxy?

Due to the nature of the FTP protocol, establishing valid FTP rules may be challenging. FTP predates firewalls by many decades and was designed insecurely. The most prevalent arguments against FTP include:

  • Passwords are sent in plaintext.

  • The protocol requires at least two TCP connections (control and data) on distinct ports.

  • When a session is formed, data is sent via ports chosen at random.

Before examining any possible security flaws in client or server software, all of these areas provide security issues. There are more secure alternatives to ftp, such as sftp and scp, which both provide authentication and data transmission via encrypted connections.

When FTP is necessary, PF redirects FTP traffic to a tiny proxy software named ftp-proxy, which is part of the FreeBSD base system. Using a series of anchors, the proxy is responsible for dynamically inserting and removing rules from the ruleset in order to appropriately manage FTP traffic.

To activate the FTP proxy, add the following line to /etc/rc.conf:

ftpproxy enable="YES"

Then initiate the proxy by executing:

service ftp-proxy start

/etc/pf.conf must have three parts for a basic setup.

  1. First, the anchors used by the proxy to insert the rules it creates for FTP sessions:
nat-anchor "ftp-proxy/*"
rdr-anchor "ftp-proxy/*"
  1. A pass rule is required for FTP traffic to enter the proxy.

  2. Redirection and NAT rules must be established prior to filtering rules. Insert this rdr rule directly after the nat rule:

rdr pass on $int_if proto tcp from any to any port ftp -> 127.0.0.1 port 8021
  1. Allow the redirected traffic to proceed:
pass out proto tcp from $proxy to any port ftp

where $proxy is the proxy daemon's bound address.

  1. Save /etc/pf.conf, load the updated rules, and test that FTP connections are functioning from a client by running the next command:
pfctl -f /etc/pf.conf

This example demonstrates a simple configuration in which clients on the local network must communicate with remote FTP servers. This setup should work with the majority of FTP client and server combinations. Adding options to the ftpproxy_flags= line modifies the proxy's behavior in a variety of ways. Some clients or servers may have peculiarities that must be accommodated in the setup, or it may be necessary to integrate the proxy in specific ways, such as allocating FTP traffic to a particular queue.

Configure a second ftp-proxy in reverse mode with -R on a different port with its own redirecting pass rule in order to operate an FTP server protected by PF and ftp-proxy.

How to Manage ICMP Traffic?

Numerous debugging and troubleshooting tools for TCP/IP networks depend on the Internet Control Message Protocol (ICMP), which was created with debugging in mind. The ICMP protocol transmits and receives control messages between hosts and gateways, primarily to inform a sender of any unexpected or challenging circumstances on route to the destination host. Routers employ the Internet Control Message Protocol (ICMP) to negotiate packet sizes and other transmission characteristics, a process often referred to as route MTU discovery.

From the standpoint of a firewall, some ICMP control messages are susceptible to known attack vectors. Also, allowing all diagnostic traffic to flow unconditionally helps debugging, but makes it simpler for others to harvest network information. Because of these considerations, the following rule may not be optimal:

pass inet proto icmp from any to any.

Allowing all ICMP traffic from the local network to pass while blocking all probes from outside the network is one method.

pass inet proto icmp from $localnet to any keep state
pass inet proto icmp from any to $ext_if keep state

There are more alternatives that illustrate some of PF's adaptability. Rather than accepting all ICMP messages, for instance, one might select just those utilized by ping and traceroute. Define a macro for that sort of message to get started and a rule:

icmp_types = "echoreq"
pass inet proto icmp all icmp-type $icmp_types keep state

Since Unix traceroute utilizes UDP by default, the following rule is required to enable it:

# allow out the default range for traceroute(8):

pass out on $ext_if inet proto udp from any to any port 33433 >< 33615 keep state

As TRACERT.EXE on Microsoft Windows computers use ICMP echo request messages, just the first rule is required to permit network traces from these systems. Unix traceroute may be taught to utilize more protocols, and if -I is used, it will employ ICMP echo request messages.

How to Manage Path MTU Discovery?

Internet protocols are meant to be device agnostic, and as a result, the ideal packet size for a specific connection cannot always be accurately anticipated. The Maximum Transmission Unit (MTU), which determines the highest limit on packet size for an interface, is the primary restraint on packet size. To examine the MTUs for a system's network interfaces, enter ifconfig.

Path MTU discovery is used by TCP/IP to identify the optimal packet size for a connection. This procedure delivers packets of varied sizes with the "Do not fragment" flag set, anticipating an ICMP reply packet of "type 3, code 4" when the maximum size is achieved. Type 3 indicates an inaccessible destination, whereas code 4 indicates fragmentation is required but the do-not-fragment flag is set. Add the destination unreachable type to the icmp types macro to enable path MTU detection in order to facilitate connections to different MTUs.

icmp_types = "{ echoreq, unreach }"

Since the pass rule already employs this macro, it is not necessary to modify it to handle the new ICMP type:

pass inet proto icmp all icmp-type $icmp_types keep state

PF permits filtering on all ICMP type and code variants. The set of available kinds and codes is specified in the icmp and icmp6 specifications.

How to Use Overload Tables to Protect SSH Server?

Those that use SSH on an external interface have likely encountered the following in their authentication logs:

Sep 15 03:12:34 myserver sshd[25771]: Failed password for root from 33.44.55.66 port 40991 ssh2
Sep 15 03:12:34 myserver sshd[5279]: Failed password for root from 33.44.55.66 port 40991 ssh2
Sep 15 03:12:35 myserver sshd[5279]: Received disconnect from 33.44.55.66: 11: Bye Bye
Sep 15 03:12:44 myserver sshd[29635]: Invalid user admin from 33.44.55.66
Sep 15 03:12:44 myserver sshd[24703]: input_userauth_request: invalid user admin
Sep 15 03:12:44 myserver sshd[24703]: Failed password for invalid user admin from 33.44.55.66 port 41485 ssh2

This is indicative of a brute-force assault in which an individual or software is attempting to brute-force the system's username and password.

If external SSH access is required for authorized users, altering the default SSH port may provide protection. Nevertheless, PF offers a more elegant method. Pass rules may specify restrictions on what connected hosts are permitted to do, and violators can be added to a database of addresses to whom access is prohibited in whole or in part. It is even feasible to terminate all connections from computers that exceed the limit.

  1. Create the following table in the tables section of the ruleset to configure:
table <bruteforce> persist
  1. Early on in the ruleset, add rules to prevent brute-force access while permitting genuine access:
block quick from <bruteforce>
pass inet proto tcp from any to $localnet port $tcp_services \
flags S/SA keep state \
(max-src-conn 100, max-src-conn-rate 15/5, \
overload <bruteforce> flush global)

The portion included in parentheses specifies the restrictions, and the values should be modified to fit local specifications. It might be written like follows:

  • max-src-conn: the maximum number of concurrent connections permitted from a single host.

  • max-src-conn-rate: the maximum number of new connections permitted from a single host per number of seconds.

  • overload <bruteforce>: Any host that exceeds these restrictions will have its address added to the brute-force database if the overload is enabled. The ruleset prohibits all traffic from the brute-force table.

  • flush global: This option specifies that when a host hits its connection limit, all global connections for that host shall be terminated.

This sample ruleset is provided only for illustrative purposes. For instance, if a large number of connections in general is desired, but ssh restrictions are desired, the rule above should be supplemented by the following rule early in the rule set:

pass quick proto { tcp, udp } from any to any port ssh flags S/SA keep state (max-src-conn 15, max-src-conn-rate 5/3, overload <bruteforce> flush global)
tip

It may not be required to block every overloader.

Notably, the overload mechanism is a generic strategy that is not unique to SSH, and it is not always optimum to completely block all communication from offenders.

For instance, an overload rule may be used to defend a mail service or a web service, while the overload table could be used in a rule to assign offenders to a queue with a low bandwidth allotment or to redirect to a certain web page.

Over time, tables will be populated by overflow rules, and their size will gradually increase, requiring more RAM. Sometimes a banned IP address is a dynamically issued IP address that has later been assigned to a host with a valid cause to connect with other hosts on the local network.

pfctl gives the ability to expire table entries in such instances. This command will delete <bruteforce> table items that have not been accessed for 86400 seconds, for instance:

pfctl -t bruteforce -T expire 86400

Similar functionality is offered by the security/expiretable module, which eliminates table items that have not been visited within a given time frame.

After installation, the expiretable command may be used to delete <bruteforce> table entries older than a given age. This example deletes all items older than twenty-four hours:

/usr/local/sbin/expiretable -v -d -t 24h bruteforce

How to Prevent SPAM?

The spamd daemon that is packed with Spamassassin may be configured to offer an outer defense against SPAM when used with PF. This spamd integrates with the PF settings via a collection of redirections.

The majority of spam originates from a small number of spammer-friendly networks and a big number of hijacked workstations, both of which are immediately reported to blocklists.

When spamd receives an SMTP connection from an address on a blocklist, it displays its banner and instantly changes to a mode in which it responds to SMTP traffic bytes at a time. This tactic is called tarpitting, and its purpose is to spend as much time as possible on the spammer's end. The implementation that uses one-byte SMTP responses is sometimes referred to as stuttering.

This example illustrates the fundamental steps required to configure spamd with automatically updated blocklists:

  1. Install the mail/spamd port or package.

  2. To use the greylisting capabilities of spamd, fdescfs must be mounted at /dev/fd. Add the line below to the /etc/fstab file:

fdescfs /dev/fd fdescfs rw 0 0
  1. Mount the filesystem by running the next command:
mount fdescfs
  1. Add the following lines to your PF ruleset:
table <spamd> persist
table <spamd-white> persist
rdr pass on $ext_if inet proto tcp from <spamd> to \
{ $ext_if, $localnet } port smtp -> 127.0.0.1 port 8025
rdr pass on $ext_if inet proto tcp from !<spamd-white> to \
{ $ext_if, $localnet } port smtp -> 127.0.0.1 port 8025

Essential are the tables <spamd> and <spamd-white>. SMTP traffic from addresses mentioned in <spamd> but not <spamd-white> is sent to the spamd daemon running on port 8025.

  1. Setup spamd in /usr/local/etc/spamd.conf and add rc.conf parameters. The mail/spamd installation provides an example configuration file (/usr/local/etc/spamd.conf.sample). One of the first lines in the configuration file that does not begin with # includes the block that defines the all list, which specifies the lists to employ:
all:\
:traplist:allowlist:

This entry adds the blocklists you provide, separated by colons (:). To use an allowlist to remove addresses from a blocklist, put the allowlist's name directly after the blocklist's name. For instance: blocklist:allowlist:.

The definition of the provided blocklist follows:

traplist:\
:black:\
:msg="SPAM. Your address %A has sent spam within the last 24 hours":\
:method=http:\
:file=www.openbsd.org/spamd/traplist.gz

where the first line defines the blocklist's name and the second line indicates the list type. During the SMTP conversation, the msg field includes the message to show to blocklisted senders. The method parameter determines how spamd-setup retrieves the list data; available ways are HTTP, FTP, from a file on a mounted file system, and exec. The file parameter indicates the name of the file spamd anticipates receiving.

Similar to the previous definition, but without the msg field since no message is required:

allowlist:\
:white:\
:method=file:\
:file=/var/mail/allowlist.txt
Select Data Sources With Caution:

Using all of the blocklists in the example spamd.conf will prevent access to substantial portions of the Internet. Administrators must change the configuration file to build an appropriate configuration that employs suitable data sources and, if required, custom lists.

  1. Add the following entry to /etc/rc.conf:
spamd_flags="-v" # use "" and see spamd-setup(8) for flags
  1. Reload the ruleset by running the next command:
pfctl -F all -f /etc/pf.conf
  1. Start spamd by typing the next command:
service obspamd start
  1. Complete the configuration using spamd-setup.

  2. Finally, create a cron job that calls spamd-setup to update the tables at reasonable intervals.

How to Define Greylist?

PF also supports greylisting, which blocks temporary communications with 45n codes from unknown hosts. Messages from greylisted hosts that retry within a reasonable timeframe are allowed to pass. Traffic from senders configured to behave within the parameters specified by RFC 1123 and RFC 2821 is instantly permitted.

The most remarkable aspect about greylisting, besides its simplicity, is that it still works. Spammers and virus authors have adapted very slowly to circumvent this strategy.

The fundamental configuration steps for greylisting are as follows:

  1. Ensure that fdescfshas been mounted as indicated in Step 2 of the preceding procedure.

  2. To enable greylisting mode for spamd, add the following line to /etc/rc.conf:

spamd grey="YES" # use spamd greylisting when YES
  1. To conclude the greylisting configuration run the following commands:
restart service obspamd
service obspamlogd start

The spamdb database tool and the spamlogd whitelist updater conduct critical greylisting activities in the background. The /var/db/spamdb database serves as the administrator's primary interface for administering the block, grey, and allow lists.

How to Provide Network Hygiene?

This section illustrates how block-policy, scrub, and antispoof are applied to the ruleset to make it act sensibly.

The block-policy is an option that is configured in the ruleset's options section, which comes before the redirection and filtering rules. This option specifies the kind of feedback, if any, that PF gives to sites that have been banned by a rule. There are two potential values for this option:

  • drop: drops stopped packets with no feedback.

  • return: returns a status code such as Connection Refused

The default policy, if unset, is drop. To modify the block-policy, enter the new value:

set block-policy return

scrub is a term in PF that permits normalizing of network packets. This procedure reassembles fragmented TCP packets and discards those with incorrect flag combinations. Scrub offers some protection against attacks based on the improper processing of packet fragments when enabled. There are a lot of choices, but the basic version is adequate for the majority of configurations:

scrub in all

This example reassembles pieces and sets the maximum segment size to 1440 bytes:

scrub in all fragment reassemble no-df max-mss 1440

The antispoof system defends against activity from faked or counterfeit IP addresses primarily by preventing packets arriving on interfaces and in logically impossible directions.

These rules eliminate faked traffic from the rest of the world as well as spoofed packets originating from the local network.

antispoof for $ext_if
antispoof for $int_if

How to Handle Non-Routable Addresses?

Even with a correctly configured gateway to handle network address translation, it may be necessary to account for the misconfigurations of others. A typical setup error is allowing non-routable addresses to access the Internet. Since traffic from non-routable addresses might contribute to a variety of DoS attack tactics, it may be prudent to expressly exclude such traffic from accessing the network via the external interface.

In this example, a macro containing non-routable addresses is created and then used in blocking rules. On the gateway's external interface, traffic to and from these addresses is discretely discarded.

martians = "{ 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 $martians to any
block drop out quick on $ext_if from any to $martians

How to Enable ALTQ or QoS?

ALTQ may be used with PF to offer Quality of Service on FreeBSD (QOS). Once ALTQ is enabled, queues that determine the processing priority of outgoing packets may be configured in the ruleset.

ALTQ is not accessible as a loadable kernel module. Create a custom kernel based on the instructions in Configuring the FreeBSD Kernel if the system's interfaces support ALTQ. The possible kernel choices are shown below. The first is required to activate ALTQ. At least one of the other choices is required to indicate the algorithm for the queueing scheduler:

options ALTQ
options ALTQ_CBQ # Class Based Queuing (CBQ)
options ALTQ_RED # Random Early Detection (RED)
options ALTQ_RIO # RED In/Out
options ALTQ_HFSC # Hierarchical Packet Scheduler (HFSC)
options ALTQ_PRIQ # Priority Queuing (PRIQ)

The scheduling algorithms listed below are available:

  • CBQ: Class Based Queuing (CBQ) is used to split a connection's bandwidth into distinct classes or queues in order to prioritize traffic according to filter rules.

  • RED: Random Early Detection (RED) is used to prevent network congestion by monitoring the queue length and comparing it to the queue's minimum and maximum criteria. When the queue is full, all new packets are arbitrarily discarded.

  • RIO: In Random Early Detection In and Out (RIO) mode, RED keeps track of several average queue lengths and threshold values, one for each QOS level.

  • HFSC: The HFSC Hierarchical Fair Service Curve Packet Scheduler is explained at http://www-2.cs.cmu.edu/hzhang/HFSC/main.html.

  • PRIQ: Priority Queuing (PRIQ) always gives precedence to traffic in a queue with a higher priority.

How to Manage PF Service?

To start PF firewall, you may run the next command:

service pf start

To stop PF firewall, you may run the next command:

service pf stop

To check PF for syntax error, you may run the next command:

service pf check

To restart PF firewall, you may run the next command:

service pf restart

To view PF status, you may run the next command:

service pf status

To start/stop/restart pflog service you may type the following commands:

service pflog start
service pflog stop
service pflog restart

How to View PF Logs?

PF logs are in binary format. To view PF logs, you may run the following command:

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

To view logs in realtime from the pflog0 interface, run the next command:

tcpdump -n -e -ttt -i pflog0

You may also use pftop utility that is a tool for quickly viewing firewall activity in realtime. You need to install the pftop package by running the next command:

pkg install pftop

You may view logs in realtime by running the next command:

pftop