31.4 Packet Filter (PF)
Warning
The content in this section has not been tested in practice and has only been verified theoretically. Do not use it directly in a production environment! It is recommended to verify it in a test environment before deploying to production.
PF (Packet Filter) is a firewall originating from OpenBSD that features queue management and many other capabilities.
It should be noted that the FreeBSD version of PF has diverged significantly from the upstream OpenBSD version over the years. Not all features behave the same on FreeBSD as they do on OpenBSD, and vice versa.
This section focuses on using PF on FreeBSD, demonstrates how to enable PF, and provides several examples of creating rule sets on a FreeBSD system.
Enabling PF
The pf module is distributed with the base system. Before enabling PF, the kernel module must be loaded first:
# kldload pfNote
If the PF kernel module is not loaded, running
pfctlwill displaypfctl: /dev/pf: No such file or directory.
By default, PF reads its configuration rules from /etc/pf.conf and modifies, drops, or passes packets based on the rules or definitions specified in that file. Copy the sample file as the default configuration rule set, otherwise pf cannot start:
# cp /usr/share/examples/pf/pf.conf /etc/Note
If the configuration file is not copied, the following message will be displayed:
/etc/rc.d/pf: WARNING: /etc/pf.conf is not readable.
Additional options can also be passed when starting PF, which are detailed in pfctl(8). Add this entry to /etc/rc.conf or modify its content, specifying the desired flags between quotes (""):
pf_flags="" # Additional flags for pfctl at startupIf PF cannot find its rule set configuration file, it will not start. By default, FreeBSD does not ship with a rule set, nor does it have an /etc/pf.conf file. Sample rule sets can be found in /usr/share/examples/pf/. If a custom rule set is saved in a different location, add a line to /etc/rc.conf specifying the full path to the file:
PF logging support is provided by pflog(4). To enable logging support, execute the command:
You can also add the following lines to change the default location of the log file or specify additional flags to pass when starting pflog(4):
Finally, if there is a LAN behind the firewall and packets need to be forwarded to computers on the LAN, or if NAT is needed, enable the following option in /etc/rc.conf:
After saving the necessary edits, PF can be started with the following commands (with logging support enabled):
To control PF, use pfctl. The following summarizes some common options for this command. For a description of all available options, refer to pfctl(8):
pfctl Common Options Table
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 filter rules, NAT rules, or the state table.
pfctl -vnf /etc/pf.conf
Check /etc/pf.conf for errors without loading the rule set.
To monitor traffic passing through the PF firewall, consider installing the package or port sysutils/pftop. After installation, pftop can be run to view a real-time snapshot of traffic in a format similar to top(1).
PF Rule Sets
This section demonstrates how to create custom rule sets. Starting with the simplest rule set, concepts are gradually built up, and several examples show the practical application of PF's many features.
The simplest rule set is for a single computer that does not run any services and only needs to access one network (possibly the Internet). To create this minimal rule set, edit /etc/pf.conf so its contents are as follows:
The first rule block in all denies all incoming traffic by default.
The second rule pass out all keep state allows connections initiated by this system to pass while retaining state information for those connections. This state information allows return traffic for the connection to pass, and this feature should only be used on trusted computers. In PF, pass rules implicitly create state by default, so keep state can be omitted.
The rule set can be loaded with the following command:
In addition to maintaining state, PF also provides lists and macros that can be used when creating rules. Macros can contain lists and must be defined before use. For example, insert the following lines at the top of the rule set:
PF recognizes port names as well as port numbers, as long as those names are listed in /etc/services. This example creates two macros: the first is a list of five TCP port names (ssh, submission, domain, www, https), and the second is a single UDP port name (domain). Once defined, the macros can be used in rules. In this example, all traffic is blocked except for connections initiated by this system to the five specified TCP services and one specified UDP service:
Although UDP is generally considered a stateless protocol, PF can track certain state information. For example, when a UDP request is sent to query a domain name server, PF monitors the return traffic to allow it through.
Whenever the rule set is edited, the new rules must be loaded to take effect:
If there are no syntax errors, pfctl will not output any messages when loading rules. You can also test before attempting to load:
Specifying the -n option causes the rules to be interpreted but not loaded, making it easy to correct errors. At any time, the last valid rule set loaded takes effect until PF is disabled or a new rule set is loaded.
Tip
Adding the
-voption duringpfctlrule set validation or loading will display the fully parsed rules, showing exactly how they will be loaded. This is very useful when debugging rules.
Simple Gateway and NAT
This section demonstrates how to configure a FreeBSD system running PF to serve as a gateway for at least one other computer. A gateway requires at least two network interfaces, each connected to a different network. In this example, em0 is connected to the Internet and em1 is connected to the LAN.
First, enable gateway mode so the system can forward network traffic received on one interface to another. This sysctl setting will forward IPv4 packets:
To forward IPv6 traffic, use:
To enable these settings at system boot, use sysrc(8) to add them to /etc/rc.conf:
Use ifconfig to verify that both interfaces are up and running.
Next, create PF rules to allow the gateway to forward traffic. The following rule allows stateful traffic from internal network hosts through the gateway, but the to keyword does not guarantee that packets will travel intact from source to destination:
This rule only allows traffic to enter the gateway through the internal interface. If packets need to continue onward, a matching rule is also required:
While these two rules work, such specific rules are typically unnecessary. For busy network administrators, a more readable rule set is safer. The rest of this section demonstrates how to make rules as simple as possible for easy reading. For example, these two rules can be replaced by a single rule:
The interface:network notation can be replaced with macros to make the rule set more readable. For example, a $localnet macro can be defined to represent the network directly connected to the internal interface ($em1:network). Alternatively, the definition of $localnet can be changed to IP address/subnet mask notation to represent the network, such as 192.168.100.1/24 representing a private address subnet.
If needed, $localnet can even be defined as a list of networks. Regardless of the specific requirements, a reasonable $localnet definition can be used in a typical pass rule, as shown below:
The following example rule set allows all traffic initiated by machines on the internal network. It first defines two macros representing the gateway's external and internal 3COM interfaces.
Note
For dial-up users, the external interface will use tun0. For ADSL connections, especially those using PPPoE (PPP over Ethernet), the correct external interface is tun0, not the physical Ethernet interface.
This rule set uses the nat-to option to translate non-routable addresses from the internal network to the IP address assigned to the external interface, i.e., Network Address Translation (NAT). The parenthesized portion ($ext_if) in the nat-to option accounts for the case where the external interface's IP address is dynamically assigned, ensuring that network traffic continues to function properly even if the external IP address changes.
It should be noted that this rule set may allow more traffic than actually needed. A more reasonable approach is to create this macro:
For use in the main pass rule:
Some other pass rules may also be needed. This rule enables SSH on the external interface:
This macro definition and rule allow internal clients to use DNS and NTP:
Note the quick keyword in this rule. Since the rule set contains multiple rules, understanding the relationship between rules is important. Rules are evaluated from top to bottom in the order they appear in the rule set. For each packet or connection evaluated by PF, the last matching rule is the one that takes effect. However, when a packet matches a rule containing the quick keyword, rule processing stops and the packet is handled according to that rule. This feature is very useful when exceptions to general rules are needed.
Creating an FTP Proxy
Note
The FTP protocol has gradually been replaced by secure alternatives such as SFTP and SCP. FTP usage scenarios in modern networks have significantly decreased, and the following FTP proxy configuration is only applicable to legacy environments that still need to support FTP. This section is retained because ftp-proxy serves as a good example of demonstrating PF's anchor mechanism.
Due to the nature of the FTP protocol, configuring effective FTP rules can be complex. FTP predates firewalls by decades and has inherent security issues in its design. The most common problems with using FTP include:
Passwords are transmitted in plaintext.
The protocol requires at least two TCP connections (control connection and data connection), and they use different ports.
After a session is established, data is transmitted through randomly selected ports.
Even without considering potential security vulnerabilities in client or server software, these issues alone pose security challenges. More secure file transfer alternatives include sftp(1) or scp(1), both of which provide authentication and transmit data over encrypted connections.
When FTP must be used, PF can redirect FTP traffic to a small proxy program ftp-proxy(8), which is included in the FreeBSD base system. The proxy's role is to dynamically insert and remove rules in the rule set through a set of anchors to properly handle FTP traffic.
To enable the FTP proxy, add the following line to /etc/rc.conf:
Also, define a macro for the proxy's listening address in /etc/pf.conf (ftp-proxy listens on 127.0.0.1 by default):
Then start the proxy by running the following command:
For a basic configuration, three elements need to be added to /etc/pf.conf. First, the anchor where the proxy inserts rules generated for FTP sessions:
Second, a pass rule that redirects FTP traffic to the proxy:
Finally, allow the redirected traffic to pass:
Here, $proxy expands to the address the proxy daemon is bound to.
Save /etc/pf.conf, load the new rules, and verify FTP connectivity from a client:
This example covers a basic configuration where a client on the local network needs to connect to a remote FTP server. This basic configuration should work for most combinations of FTP clients and servers. As shown in ftp-proxy(8), the proxy's behavior can be adjusted in various ways by adding options to the ftp_proxy_flags= line. Some clients or servers may have specific differences that require corresponding adjustments in the configuration, or the proxy may need to be integrated in a specific way, such as assigning FTP traffic to a particular queue.
If you need to protect an FTP server and configure PF to work with ftp-proxy(8), you can run ftp-proxy in reverse mode on a separate port using the -R option with a dedicated redirection pass rule.
Managing ICMP
Many tools used for debugging or troubleshooting TCP/IP networks rely on the Internet Control Message Protocol (ICMP), which was originally designed for debugging purposes.
The ICMP protocol sends and receives Control Messages between hosts and gateways, primarily providing feedback to the sender about anomalies or difficulties encountered en route to the destination host. Routers use ICMP to negotiate packet sizes and other transmission parameters, a process commonly known as Path MTU Discovery.
From a firewall perspective, some ICMP control messages are vulnerable to known attack vectors. Additionally, allowing all diagnostic traffic to pass unrestricted simplifies debugging but also makes it easier for others to obtain network information. Therefore, the following rule may not be optimal:
One solution is to allow all ICMP traffic from the local network while blocking probe requests from outside the network:
PF also provides other options that demonstrate its flexibility. For example, instead of allowing all ICMP messages, specific message types used by ping(8) and traceroute(8) can be specified. First, define a macro representing these message types:
Then, the rule using this macro:
If other types of ICMP packets are needed, icmp_types can be expanded to include a list of those packet types. Refer to the icmp(4) and icmp6(4) manual pages for the ICMP message types supported by PF. You can also refer to IANA's ICMP Parameters document for an explanation of each message type.
Unix traceroute uses UDP by default, so an additional rule is needed to allow the use of Unix traceroute:
Unix traceroute uses UDP by default but can also be configured to use other protocols. If the -I parameter is specified, it uses ICMP echo request messages.
Path MTU Discovery
The Internet Protocol is designed to be device-independent, and one consequence of this device independence is that the optimal packet size for a given connection cannot always be reliably predicted. The primary constraint on packet size is the Maximum Transmission Unit (MTU), which sets the maximum packet size limit for an interface. You can enter ifconfig to view the MTU values of the system's network interfaces.
TCP/IP uses a process called Path MTU Discovery to determine the correct packet size for a connection. This process sends packets of varying sizes with the "don't fragment" flag set, expecting to receive an ICMP return packet of type "3, code 4" when the limit is reached. Type 3 indicates "destination unreachable" and code 4 indicates "fragmentation needed but don't-fragment flag set." If Path MTU Discovery needs to be allowed to support connections to links with different MTUs, the destination unreachable type must be added to the icmp_types macro:
Since the pass rule already uses this macro, no modification to the rule is needed to support the new ICMP type:
PF allows filtering based on all ICMP type and code variants. The list of possible types and codes is described in icmp(4) and icmp6(4).
Using Tables
Certain types of data are important for filtering and redirection at times, but their definitions are too long to include in the rule set file. PF supports the use of tables, which are definable lists that can be manipulated without reloading the entire rule set and support fast lookups. Table names are always enclosed in < >, as shown below:
In this example, the 192.168.2.0/24 network is part of the table, but the address 192.168.2.5 is excluded using the ! operator. Tables can also be loaded from a file, where each entry is on a separate line, as in this example /etc/clients:
To reference the file, define the table like this:
Once the table is defined, it can be referenced in rules:
The contents of a table can be dynamically manipulated using pfctl. The following example adds another network to the table:
Note that any changes made in this way take effect immediately, which is suitable for testing, but will not persist after a power loss or reboot. To make changes permanent, modify the table definition in the rule set or edit the file referenced by the table. A cron(8) job can be used to periodically save the table contents to disk, with a command example such as pfctl -t clients -T show >/etc/clients. Alternatively, /etc/clients can be updated with the in-memory table contents using the following command:
Protecting SSH with Overload Tables
Users running SSH on an external interface may see entries like the following in their authentication logs:
This indicates a brute-force attack, where someone or some program is trying to obtain the correct username and password to break into the system.
If legitimate users need to be allowed external SSH access, changing the default port used by SSH can provide some protection. However, PF offers a more elegant solution. By limiting the operations of connecting hosts, violators are moved to an address table where addresses are denied partial or complete access. Existing connections from the offending machine can even be dropped.
To configure this functionality, create the table in the table section of the rule set:
Then, add rules earlier in the rule set to block brute-force access and allow legitimate access:
The parenthesized portion defines the limits, and the numbers should be adjusted according to local requirements. It can be interpreted as follows:
max-src-conn is the number of simultaneous connections allowed from a single host.
max-src-conn-rate is the rate of new connections (15) allowed from any single host per 5 seconds.
overload <bruteforce> means that any host exceeding these limits will have its address added to the bruteforce table. The rule set blocks all traffic from addresses in the bruteforce table.
Finally, flush global means that when a host reaches the limit, all (global) connections from that host will be terminated (flush).
Note
These rules do not stop slow brute-forcers, as described at http://home.nuug.no/~peter/hailmary2013/.
This example rule set is primarily illustrative. For example, if you want to allow a large number of connections but want to be stricter regarding SSH, you can supplement with a rule like the following earlier in the rule set:
Note
The overload mechanism is a general technique that applies to SSH but is not limited to it, and it is not always necessary to completely block all violators.
For example, overload rules can be used to protect mail services or web services, and overload tables can be used in rules to assign violators to a queue with minimum bandwidth or redirect them to a specific web page.
Over time, the table will be populated by overload rules and will gradually increase in size, consuming more memory. Sometimes blocked IP addresses are dynamically assigned addresses that may have been reassigned to a host with a legitimate reason to communicate with the local network host.
For such situations, pfctl provides the ability to expire table entries. For example, the following command will remove entries from the <bruteforce> table that have not been referenced within 86400 seconds:
Network Hygiene
This section explains how to use block-policy, scrub, and antispoof to make the rule set behave more reasonably.
block-policy is an option that can be set in the options section of the rule set, which is located before redirection and filter rules. This option determines whether PF sends feedback when a rule blocks a host. The option has two possible values: drop drops blocked packets without sending feedback; return returns a status code such as Connection refused.
If not set, the default policy is drop. To change the block-policy, specify the desired value:
In PF, scrub is a keyword that enables normalization of network packets. This process drops TCP packets with invalid flag combinations. Enabling scrub provides some protection against certain attacks based on improper handling of packet fragmentation.
It is recommended to use scrub as an option of a match rule rather than as a standalone statement. The simplest form is suitable for most configurations:
For fragment reassembly, it is recommended to use set reassemble yes in the options section of the rule set:
The no-df option means that fragments with the "don't fragment" flag set will also be reassembled, and the flag will be cleared in the reassembled packet.
Some services such as NFS require specific fragment handling options. For more information, refer to Firewalling with OpenBSD's PF packet filter.
The following example sets fragment reassembly, clears the DF flag, and sets the maximum segment size to 1440 bytes through a match rule:
The antispoof mechanism prevents spoofed or forged IP address attacks, primarily by blocking packets that logically should not appear on a specific interface and direction.
These rules filter spoofed traffic from the outside world as well as any spoofed packets originating from the local network:
Handling Non-Routable Addresses
Even when a gateway is properly configured to handle network address translation, sometimes it is necessary to compensate for others' misconfigurations. A common misconfiguration is allowing traffic from non-routable addresses to access the Internet. Since traffic from non-routable addresses can be involved in various denial-of-service (DoS) attack techniques, it is recommended to explicitly block traffic from non-routable addresses entering the network's external interface.
In this example, a macro containing non-routable addresses is first defined, then used in a block rule. Traffic to and from these addresses is silently dropped at the gateway's external interface.
Other PF Features
SYN Cookie Protection
PF provides the set syncookies option for defending against SYN flood attacks. When SYN cookies are active, PF responds to each inbound TCP SYN with a syncookie SYNACK without allocating any resources. Upon receiving the client's ACK, PF evaluates the rule set and creates state if allowed.
Adaptive mode enables syncookie mode when half-open TCP connections occupy the specified percentage of the state table, and disables it when the percentage falls back to the lower threshold.
Packet Rate Limiting
The max-pkt-rate option can limit packet rates on a per-rule basis. When the specified rate is exceeded, the rule stops matching:
This rule allows a maximum of 100 ICMP packets per 10 seconds; when the rate is exceeded, the rule stops matching.
Last updated