29.4. PF and ALTQ

Revised and updated by John Ferrell.

Since FreeBSD 5.3, a ported version of OpenBSD's PF firewall has been included as an integrated part of the base system. PF is a complete, full-featured firewall that has optional support for ALTQ (Alternate Queuing), which provides Quality of Service (QoS).

Since the OpenBSD Project maintains the definitive reference for PF in the PF FAQ, this section of the Handbook focuses on PF as it pertains to FreeBSD, while providing some general usage information.

More information about porting PF to FreeBSD can be found at http://pf4freebsd.love2party.net/.

29.4.1. Using the PF Loadable Kernel Modules

In order to use PF, the PF kernel module must be first loaded. Add the following line to /etc/rc.conf:

pf_enable="YES"

Then, run the startup script to load the module:

# service pf start

The PF module will not load if it cannot find the ruleset configuration file. The default location is /etc/pf.conf. If the PF ruleset is located somewhere else, add a line to /etc/rc.conf which specifies the full path to the file:

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

The sample pf.conf can be found in /usr/share/examples/pf/.

The PF module can also be loaded manually from the command line:

# kldload pf.ko

Logging support for PF is provided by pflog.ko which can be loaded by adding the following line to /etc/rc.conf:

pflog_enable="YES"

Then, run the startup script to load the module:

# service pflog start

29.4.2. PF Kernel Options

While it is not necessary to compile PF support into the FreeBSD kernel, some of PF's advanced features are not included in the loadable module, namely pfsync(4), which is a pseudo-device that exposes certain changes to the state table used by PF. It can be paired with carp(4) to create failover firewalls using PF. More information on CARP can be found in of the Handbook.

The following PF kernel options can be found in /usr/src/sys/conf/NOTES:

device pf
device pflog
device pfsync

device pf enables PF support.

device pflog enables the optional pflog(4) pseudo network device which can be used to log traffic to a bpf(4) descriptor. The pflogd(8) daemon can then be used to store the logging information to disk.

device pfsync enables the optional pfsync(4) pseudo-network device that is used to monitor state changes.

29.4.3. Available rc.conf Options

The following rc.conf(5) statements can be used to configure PF and pflog(4) at boot:

pf_enable="YES"                 # Enable PF (load module if required)
pf_rules="/etc/pf.conf"         # rules definition file for pf
pf_flags=""                     # additional flags for pfctl startup
pflog_enable="YES"              # start pflogd(8)
pflog_logfile="/var/log/pflog"  # where pflogd should store the logfile
pflog_flags=""                  # additional flags for pflogd startup

If there is a LAN behind the firewall and packets need to be forwarded for the computers on the LAN, or NAT is required, add the following option:

gateway_enable="YES"            # Enable as LAN gateway

29.4.4. Creating Filtering Rules

By default, PF reads its configuration rules from /etc/pf.conf and modifies, drops, or passes packets according to the rules or definitions specified in this file. The FreeBSD installation includes several sample files located in /usr/share/examples/pf/. Refer to the PF FAQ for complete coverage of PF rulesets.

Warning:

When reading the PF FAQ, keep in mind that different versions of FreeBSD contain different versions of PF. Currently, FreeBSD 8.X is using the same version of PF as OpenBSD 4.1. FreeBSD 9.X and later is using the same version of PF as OpenBSD 4.5.

The FreeBSD packet filter mailing list is a good place to ask questions about configuring and running the PF firewall. Do not forget to check the mailing list archives before asking questions.

To control PF, use pfctl(8). Below are some useful options to this command. Review pfctl(8) for a description of all available options:

CommandPurpose
pfctl -eEnable PF.
pfctl -dDisable PF.
pfctl -F all -f /etc/pf.confFlush all NAT, filter, state, and table rules and reload /etc/pf.conf.
pfctl -s [ rules | nat state ]Report on the filter rules, NAT rules, or state table.
pfctl -vnf /etc/pf.confCheck /etc/pf.conf for errors, but do not load ruleset.

29.4.5. Enabling ALTQ

ALTQ is only available by compiling its support into the FreeBSD kernel. ALTQ is not supported by all network card drivers. Refer to altq(4) for a list of drivers that are supported by the release of FreeBSD.

The following kernel options will enable ALTQ and add additional functionality:

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)
options         ALTQ_NOPCC      # Required for SMP build

options ALTQ enables the ALTQ framework.

options ALTQ_CBQ enables Class Based Queuing (CBQ). CBQ can be used to divide a connection's bandwidth into different classes or queues to prioritize traffic based on filter rules.

options ALTQ_RED enables Random Early Detection (RED). RED is used to avoid network congestion by measuring the length of the queue and comparing it to the minimum and maximum thresholds for the queue. If the queue is over the maximum, all new packets will be dropped. RED drops packets from different connections randomly.

options ALTQ_RIO enables Random Early Detection In and Out.

options ALTQ_HFSC enables the Hierarchical Fair Service Curve Packet Scheduler HFSC. For more information, refer to http://www-2.cs.cmu.edu/~hzhang/HFSC/main.html.

options ALTQ_PRIQ enables Priority Queuing (PRIQ). PRIQ will always pass traffic that is in a higher queue first.

options ALTQ_NOPCC enables SMP support for ALTQ. This option is required on SMP systems.

29.4.6. PF Rule Sets and Tools

Contributed by Peter N. M. Hansteen.

This section demonstrates some useful PF features and PF related tools in a series of examples. A more thorough tutorial is available at http://home.nuug.no/~peter/pf/.

Tip:

security/sudo is useful for running commands like pfctl that require elevated privileges. It can be installed from the Ports Collection.

29.4.6.1. The Simplest Rule Set Ever

The simplest possible setup is for a single machine which will not run any services, and which will talk to one network which may be the Internet. A minimal /etc/pf.conf looks like this:

block in all
pass out all keep state

Here we deny any incoming traffic, allow traffic we make ourselves to pass, and retain state information on our connections. Keeping state information allows return traffic for all connections we have initiated to pass back to us. This rule set is used on machines that can be trusted. The rule set can be loaded with

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

29.4.6.2. Tighter and More Elegant

For a slightly more structured and complete setup, we start by denying everything and then allowing only those things we know that we need [6]. This gives us the opportunity to introduce two of the features which make PF such a wonderful tool: lists and macros.

We will make some changes to /etc/pf.conf, starting with

block all

Then we back up a little. Macros need to be defined before use, so at the very top of the file, we add:

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

Now we have demonstrated several things at once - what macros look like, that macros may be lists, and that PF understands rules using port names equally well as it does port numbers. The names are the ones listed in /etc/services. This gives us something to put in our rules, which we edit slightly to look like this:

block all
pass out proto tcp to any port $tcp_services keep state
pass proto udp to any port $udp_services keep state

At this point some of us will point out that UDP is stateless, but PF actually manages to maintain state information despite this. Keeping state for a UDP connection means that for example when you ask a name server about a domain name, you will be able to receive its answer.

Since we have made changes to our pf.conf, we load the new rules:

# pfctl -f /etc/pf.conf

and the new rules are applied. If there are no syntax errors, pfctl will not output any messages during the rule load. The -v flag will produce more verbose pfctl output.

If there have been extensive changes to the rule set, the rules can be tested before attempting to load them. The command to do this is

# pfctl -nf /etc/pf.conf

-n causes the rules to be interpreted only, but does not load them. This provides an opportunity to correct any errors. Under any circumstances, the last valid rule set loaded will be in force until PF is disabled or a new rule set is loaded.

Use pfctl -v to Show the Parsed Rule Set:

Adding the -v to a pfctl ruleset load (even a dry run with -n) will display the fully parsed rules exactly the way they will be loaded. This is extremely useful when debugging rules.

29.4.6.3. A Simple Gateway with NAT

To most users, a single machine setup will be of limited interest, and at this point we move on to more realistic or at least more common setups, concentrating on a machine which is running PF and also acts as a gateway for at least one other machine.

29.4.6.3.1. Gateways and the Pitfalls of in, out and on

In the single machine setup, life is relatively simple. Traffic created on it should either pass out to the rest of the world or not, and the administrator decides what to let in from elsewhere.

On a gateway, the perspective changes from me versus the network out there to I am the one who decides what to pass to or from all the networks I am connected to. The machine has at least two network interfaces, each connected to a separate net.

It is very reasonable to think that for traffic to pass from the network connected to xl1 to hosts on the network connected to xl0, a rule like this is needed:

pass in on xl1 from xl1:network to xl0:network port $ports keep state

This rule keeps track of states as well.

However, one of the most common and most complained-about mistakes in firewall configuration is not realizing that the to keyword does not in itself guarantee passage all the way there. The rule we just wrote only lets the traffic pass in to the gateway on the internal interface. To let the packets get a bit further, a matching rule is needed which says

pass out on xl0 from xl1:network to xl0:network port $ports keep state

These rules will work, but they will not necessarily achieve the desired effect.

Rules this specific are rarely needed. For the basic gateway configurations we will be dealing with here, a better rule says

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

This provides local net access to the Internet and leaves the detective work to the antispoof and scrub code. They are both pretty good these days, and we will get back to them later. For now we just accept the fact that for simple setups, interface-bound rules with in/out rules tend to add more clutter than they are worth to rule sets.

For a busy network admin, a readable rule set is a safer rule set.

For the remainder of this section, with some exceptions, we will keep the rules as simple as possible for readability.

29.4.6.3.2. What is the Local Network, Anyway?

Above, we introduced the interface:network notation. That is a nice piece of shorthand, but the rule set can be made even more readable and maintainable by taking the macro use a tiny bit further.

For example, a $localnet macro could be defined as the network directly attached to your internal interface ($xl1:network in the examples above).

Alternatively, the definition of $localnet could be changed to an IP address/netmask notation to denote a network, such as 192.168.100.1/24 for a subnet of private addresses.

If required, $localnet could even be defined as a list of networks. Whatever the specific needs, a sensible $localnet definition and a typical pass rule of the type

pass from $localnet to any port $ports keep state

could end up saving you a few headaches. We will stick to that convention from here on.

29.4.6.3.3. Setting Up

We assume that the machine has acquired another network card or at any rate there is a network connection from the local network, via PPP or other means. We will not consider the specific interface configurations.

For the discussion and examples below, only the interface names will differ between a PPP setup and an Ethernet one, and we will do our best to get rid of the actual interface names as quickly as possible.

First, we need to turn on gatewaying in order to let the machine forward the network traffic it receives on one interface to other networks via a separate interface. Initially we will do this on the command line with sysctl(8), for traditional IP version four.

# sysctl net.inet.ip.forwarding=1

If we need to forward IP version six traffic, the command is

# sysctl net.inet6.ip6.forwarding=1

In order for this to continue working after the computer has been restarted at some time in the future, enter these settings into /etc/rc.conf:

gateway_enable="YES"		#for ipv4
ipv6_gateway_enable="YES"	#for ipv6

Use ifconfig -a, or ifconfig interface_name to find out if both of the interfaces to be used are up and running.

If all traffic initiated by machines on the inside is to be allowed, /etc/pf.conf could look roughly like this [7]:

ext_if = "xl0"	# macro for external interface - use tun0 for PPPoE
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

Note the use of macros to assign logical names to the network interfaces. Here 3Com cards are used, but this is the last time during this tutorial we will find this of any interest whatsoever. In truly simple setups like this one, we may not gain very much by using macros like these, but once the rule sets grow somewhat larger, you will learn to appreciate the readability this provides.

Also note the nat rule. This is where we handle the network address translation from the non-routable address inside the local net to the sole official address we assume has been assigned.

The parentheses surrounding the last part of the nat rule ($ext_if) are there to compensate for the possibility that the IP address of the external interface may be dynamically assigned. This detail will ensure that network traffic runs without serious interruptions even if the external IP address changes.

On the other hand, this rule set probably allows more traffic to pass out of the network than actually desired. One reasonable setup could contain the macro

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

and the main pass rule

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

This may be a somewhat peculiar selection of ports, but it is based on a real life example. Individual needs probably differ at least in some specifics, but this should cover at least some of the more useful services.

In addition, we have a few other pass rules. We will be returning to some of the more interesting ones rather soon. One pass rule which is useful to those of us who want the ability to administer our machines from elsewhere is

pass in inet proto tcp to port ssh

or for that matter

pass in inet proto tcp to $ext_if port ssh

whichever is preferred. Lastly we need to make the name service work for our clients:

udp_services = "{ domain, ntp }"

This is supplemented with a rule which passes the traffic we want through our firewall:

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

Note the quick keyword in this rule. We have started writing rule sets which consist of several rules, and it is time to take a look at the relationships between the rules in a rule set. The rules are evaluated from top to bottom, in the sequence they are written in the configuration file. For each packet or connection evaluated by PF, the last matching rule in the rule set is the one which is applied. The quick keyword offers an escape from the ordinary sequence. When a packet matches a quick rule, the packet is treated according to the present rule. The rule processing stops without considering any further rules which might have matched the packet. This is very useful when a few isolated exceptions to the general rules are needed.

This rule also takes care of NTP, which is used for time synchronization. One thing common to both protocols is that they may under certain circumstances communicate alternately over TCP and UDP.

29.4.6.4. That Sad Old FTP Thing

The short list of real life TCP ports above contained, among other things, FTP. FTP is a sad old thing and a problem child, emphatically so for anyone trying to combine FTP and firewalls. FTP is an old and weird protocol, with a lot to not like. The most common points against it are

  • Passwords are transferred in the clear

  • The protocol demands the use of at least two TCP connections (control and data) on separate ports

  • When a session is established, data is communicated via ports selected at random

All of these points make for challenges security-wise, even before considering any potential weaknesses in client or server software which may lead to security issues. These things have tended to happen.

Under any circumstances, other more modern and more secure options for file transfer exist, such as sftp(1) or scp(1), which feature both authentication and data transfer via encrypted connections. Competent IT professionals should have a preference for some other form of file transfer than FTP.

Regardless of our professionalism and preferences, we are all too aware that at times we will need to handle things we would prefer not to. In the case of FTP through firewalls, the main part of our handling consists of redirecting the traffic to a small program which is written specifically for this purpose.

29.4.6.4.1. FTP Via Redirect: ftp-proxy

Enabling FTP transfers through your gateway is amazingly simple, thanks to the FTP proxy program (called ftp-proxy(8)) included in the base system on FreeBSD and other systems which offer PF.

The FTP protocol being what it is, the proxy needs to dynamically insert rules in your rule set. ftp-proxy(8) interacts with your configuration via a set of anchors where the proxy inserts and deletes the rules it constructs to handle your FTP traffic.

To enable ftp-proxy(8), add this line to /etc/rc.conf:

ftpproxy_flags=""

Starting the proxy manually by running /usr/sbin/ftp-proxy allows testing of the PF configuration changes we are about to make.

For a basic configuration, only three elements need to be added to /etc/pf.conf. First, the anchors:

nat-anchor "ftp-proxy/*"
rdr-anchor "ftp-proxy/*"

The proxy will insert the rules it generates for the FTP sessions here. A pass rule is needed to let FTP traffic in to the proxy.

Now for the actual redirection. Redirection rules and NAT rules fall into the same rule class. These rules may be referenced directly by other rules, and filtering rules may depend on these rules. Logically, rdr and nat rules need to be defined before the filtering rules.

We insert our rdr rule immediately after the nat rule in /etc/pf.conf

rdr pass on $int_if proto tcp from any to any port ftp -> 127.0.0.1 port 8021

In addition, the redirected traffic must be allowed to pass. We achieve this with

pass out proto tcp from $proxy to any port ftp

where $proxy expands to the address the proxy daemon is bound to.

Save pf.conf, then load the new rules with

# pfctl -f /etc/pf.conf

At this point, users will probably begin noticing that FTP works before they have been told.

This example covers a basic setup where the clients in the local net need to contact FTP servers elsewhere. The basic configuration here should work well with most combinations of FTP clients and servers. As shown in the man page, the proxy's behavior can be changed in various ways by adding options to the ftpproxy_flags= line. Some clients or servers may have specific quirks that must be compensated for in the configuration, or there may be a need to integrate the proxy in specific ways such as assigning FTP traffic to a specific queue. For these and other finer points of ftp-proxy(8) configuration, start by studying the man page.

For ways to run an FTP server protected by PF and ftp-proxy(8), look into running a separate ftp-proxy in reverse mode (using -R), on a separate port with its own redirecting pass rule.

29.4.6.5. Easing Troubleshooting

Making network troubleshooting friendly is a potentially large subject. At most times, the debugging or troubleshooting friendliness of a TCP/IP network depends on treatment of the Internet protocol which was designed specifically with debugging in mind, the Internet Control Message Protocol, or ICMP as it is usually abbreviated.

ICMP is the protocol for sending and receiving control messages between hosts and gateways, mainly to provide feedback to a sender about any unusual or difficult conditions enroute to the target host.

There is a lot of ICMP traffic which usually just happens in the background while users are surfing the web, reading mail or transferring files. Routers use ICMP to negotiate packet sizes and other transmission parameters in a process often referred to as path MTU discovery.

Some admins refer to ICMP as either just evil, or, if their understanding runs a little deeper, a necessary evil. The reason for this attitude is purely historical. The reason can be found a few years back when it was discovered that several operating systems contained code in their networking stack which could make a machine running one of the affected systems crash and fall over, or in some cases just do really strange things, with a sufficiently large ICMP request.

One of the companies which was hit hard was Microsoft, and you can find rather a lot of material on the ping of death bug by using your favorite search engine. This all happened in the second half of the 1990s, and all modern operating systems, at least the ones we can read, have thoroughly sanitized their network code since then. At least that is what we are led to believe.

One of the early workarounds was to simply block either all ICMP traffic or at least ICMP ECHO, which is what ping uses. Now these rule sets have been around for roughly fifteen years, and the people who put them there are still scared.

29.4.6.5.1. Then, Do We Let it All Through?

The obvious question then becomes, if ICMP is such a good and useful thing, should we not let it all through, all the time? The answer is It depends.

Letting diagnostic traffic pass unconditionally of course makes debugging easier, but also makes it relatively easy for others to extract information about your network. That means that a rule like

pass inet proto icmp from any to any

might not be optimal if the internal workings of the local network should be cloaked in a bit of mystery. In all fairness it should also be said that some ICMP traffic might be found quite harmlessly riding piggyback on keep state rules.

29.4.6.5.2. The Easy Way Out: the Buck Stops Here

The easiest solution could very well be to let all ICMP traffic from the local net through and stop probes from elsewhere at the gateway:

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

Stopping probes at the gateway might be an attractive option anyway, but let us have a look at a few other options which will show some of PF's flexibility.

29.4.6.5.3. Letting ping Through

The rule set we have developed so far has one clear disadvantage: common troubleshooting commands such as ping(8) and traceroute(8) will not work. That may not matter too much to end users, and since it was ping which scared people into filtering or blocking ICMP traffic in the first place, there are apparently some people who feel we are better off without it. If you are in my perceived target audience, you will be rather fond of having those troubleshooting tools avalable. With a couple of small additions to the rule set, they will be. ping(8) uses ICMP, and in order to keep our rule set tidy, we start by defining another macro:

icmp_types = "echoreq"

and a rule which uses the definition,

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

More or other types of ICMP packets may need to go through, and icmp_types can be expanded to a list of those packet types that are allowed.

29.4.6.5.4. Helping traceroute(8)

traceroute(8) is another command which is quite useful when users claim that the Internet is not working. By default, Unix traceroute uses UDP connections according to a set formula based on destination. The rule below works with traceroute on all unixes I've had access to, including GNU/Linux:

# allow out the default range for traceroute(8):
# "base+nhops*nqueries-1" (33434+64*3-1)
pass out on $ext_if inet proto udp from any to any port 33433 >< 33626 keep state

Experience so far indicates that traceroute implementations on other operating systems work roughly the same. Except, of course, on Microsoft Windows. On that platform, TRACERT.EXE uses ICMP ECHO for this purpose. So to let Windows traceroutes through, only the first rule is needed. Unix traceroute can be instructed to use other protocols as well, and will behave remarkably like its Microsoft counterpart if -I is used. Check the traceroute(8) man page (or its source code, for that matter) for all the details.

Under any circumstances, this solution was lifted from an openbsd-misc post. I've found that list, and the searchable list archives (accessible among other places from http://marc.theaimsgroup.com/), to be a very valuable resource whenever you need OpenBSD or PF related information.

29.4.6.5.5. Path MTU Discovery

Internet protocols are designed to be device independent, and one consequence of device independence is that the optimal packet size for a given connection cannot always be predicted reliably. The main constraint on packet size is called the Maximum Transmission Unit, or MTU, which sets the upper limit on the packet size for an interface. ifconfig(8) shows the MTU for the network interfaces.

Modern TCP/IP implementations expect to be able to determine the right packet size for a connection through a process which, simply put, involves sending packets of varying sizes with the Do not fragment flag set, expecting an ICMP return packet indicating type 3, code 4 when the upper limit has been reached. Now do not dive for the RFCs right away. Type 3 means destination unreachable, while code 4 is short for fragmentation needed, but the do-not-fragment flag is set. So if connections to networks which may have other MTUs than the local network seem sub-optimal, and there is no need to be that specific, the list of ICMP types can be changed slightly to let the destination unreachable packets through, too:

icmp_types = "{ echoreq, unreach }"

As we can see, this means we do not need to change the pass rule itself:

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

PF allows filtering on all variations of ICMP types and codes. For those who want to delve into what to pass (or not) of ICMP traffic, the list of possible types and codes are documented in the icmp(4) and icmp6(4) man pages. The background information is available in the RFCs [8].

29.4.6.6. Tables Make Life Easier

By this time it may appear that this gets awfully static and rigid. There will after all be some kinds of data which are relevant to filtering and redirection at a given time, but do not deserve to be put into a configuration file! Quite right, and PF offers mechanisms for handling these situations as well. Tables are one such feature, mainly useful as lists which can be manipulated without needing to reload the entire rule set, and where fast lookups are desirable. Table names are always enclosed in < >, like this:

table <clients> { 192.168.2.0/24, !192.168.2.5 }

Here, the network 192.168.2.0/24 is part of the table, except the address 192.168.2.5, which is excluded using the ! operator (logical NOT). It is also possible to load tables from files where each item is on a separate line, such as the file /etc/clients.

192.168.2.0/24
!192.168.2.5

which in turn is used to initialize the table in /etc/pf.conf:

table <clients> persist file "/etc/clients"

Then, for example, one of our earlier rules can be changed to read

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

to manage outgoing traffic from client computers. With this in hand, the table's contents can be manipulated live, such as

# pfctl -t clients -T add 192.168.1/16

Note that this changes the in-memory copy of the table only, meaning that the change will not survive a power failure or other reboot unless there are arrangements to store the changes.

One might opt to maintain the on-disk copy of the table using a cron(8) job which dumps the table content to disk at regular intervals, using a command such as pfctl -t clients -T show >/etc/clients. Alternatively, /etc/clients could be edited, replacing the in-memory table contents with the file data:

# pfctl -t clients -T replace -f /etc/clients

For operations performed frequently, administrators will sooner or later end up writing shell scripts for tasks such as inserting or removing items or replacing table contents. The only real limitations lie in individual needs and creativity.

29.4.6.7. Overload Tables

Those who run a Secure Shell login service which is accessible from the Internet have probably seen something like this in the authentication logs:

Sep 26 03:12:34 skapet sshd[25771]: Failed password for root from 200.72.41.31 port 40992 ssh2
Sep 26 03:12:34 skapet sshd[5279]: Failed password for root from 200.72.41.31 port 40992 ssh2
Sep 26 03:12:35 skapet sshd[5279]: Received disconnect from 200.72.41.31: 11: Bye Bye
Sep 26 03:12:44 skapet sshd[29635]: Invalid user admin from 200.72.41.31
Sep 26 03:12:44 skapet sshd[24703]: input_userauth_request: invalid user admin
Sep 26 03:12:44 skapet sshd[24703]: Failed password for invalid user admin from 200.72.41.31 port 41484 ssh2

And so on. This is what a brute force attack looks like. Essentially somebody, or more likely, a cracked computer somewhere, is trying by brute force to find a combination of user name and password which will let them into your system.

The simplest response would be to write a pf.conf rule which blocks all access. This leads to another class of problems, including what to do in order to let people with legitimate business on the system access it anyway. Some might consider moving the service to another port, but then again, the ones flooding on port 22 would probably be able to scan their way to port 22222 for a repeat performance.

Since OpenBSD 3.7, and soon after in FreeBSD version 6.0, PF has offered a slightly more elegant solution. Pass rules can be written so they maintain certain limits on what connecting hosts can do. For good measure, violators can be banished to a table of addresses which are denied some or all access. If desired, it's even possible to drop all existing connections from machines which overreach the limits. Here is how it is done:

First, set up the table. In the tables section, add

table <bruteforce> persist

Then somewhere fairly early in the rule set, add a rule to block the bruteforcers:

block quick from <bruteforce>

And finally, the pass rule.

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 first part here is identical to the main rule we constructed earlier. The part in parentheses is the new stuff which will ease network load even further.

max-src-conn is the number of simultaneous connections allowed from one host. In this example, it is set at 100. Other setups may want a slightly higher or lower value.

max-src-conn-rate is the rate of new connections allowed from any single host, here 15 connections per 5 seconds. Again, the administrator is the one to judge what suits their setup.

overload <bruteforce> means that any host which exceeds these limits gets its address added to the table bruteforce. Our rule set blocks all traffic from addresses in the bruteforce table.

Finally, flush global says that when a host reaches the limit, that host's connections will be terminated (flushed). The global part says that for good measure, this applies to connections which match other pass rules too.

The effect is dramatic. From here on, bruteforcers more often than not will end up with "Fatal: timeout before authentication" messages, getting nowhere.

Note:

These rules will not block slow bruteforcers, sometimes referred to as the Hail Mary Cloud.

Once again, please keep in mind that this example rule is intended mainly as an illustration. It is not unlikely that a particular network's needs are better served by rather different rules or combinations of rules.

If, for example, a generous number of connections in general are wanted, but the desire is to be a little more tight fisted when it comes to ssh, supplement the rule above with something like the one below, early on 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)

It should be possible to find the set of parameters which is just right for individual situations by reading the relevant man pages and the PF User Guide, and perhaps a bit of experimentation.

It May Not be Necessary to Block All Overloaders:

It is probably worth noting at this point that the overload mechanism is a general technique which does not have to apply exclusively to the ssh service, and it is not always optimal to block all traffic from offenders entirely.

For example, an overload rule could be used to protect a mail service or a web service, and the overload table could be used in a rule to assign offenders to a queue with a minimal bandwidth allocation or, in the web case, to redirect to a specific web page.

29.4.6.7.1. Expiring Table Entries with pfctl

At this point, we have tables which will be filled by our overload rules, and since we could reasonably expect our gateways to have months of uptime, the tables will grow incrementally, taking up more memory as time goes by.

Sometimes an IP address that was blocked last week due to a brute force attack was in fact a dynamically assigned one, which is now assigned to a different ISP customer who has a legitimate reason to try communicating with hosts in the local network.

Situations like these were what caused Henning Brauer to add to pfctl the ability to expire table entries not referenced in a specified number of seconds (in OpenBSD 4.1). For example, the command

# pfctl -t bruteforce -T expire 86400

will remove <bruteforce> table entries which have not been referenced for 86400 seconds.

29.4.6.7.2. The expiretable Tool

Before pfctl acquired the ability to expire table entries, Henrik Gustafsson had written expiretable, which removes table entries which have not been accessed for a specified period of time.

One useful example is to use the expiretable program as a way of removing outdated <bruteforce> table entries.

For example, let expiretable remove <bruteforce> table entries older than 24 hours by adding an entry containing the following to /etc/rc.local:

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

expiretable is in the Ports Collection on FreeBSD as security/expiretable.

29.4.6.8. Other PF Tools

Over time, a number of tools have been developed which interact with PF in various ways.

29.4.6.8.1. The pftop Traffic Viewer

Can Erkin Acar's pftop makes it possible to keep an eye on what passes into and out of the network. pftop is available through the ports system as sysutils/pftop. The name is a strong hint at what it does - pftop shows a running snapshot of traffic in a format which is strongly inspired by top(1).

29.4.6.8.2. The spamd Spam Deferral Daemon

Not to be confused with the spamd daemon which comes bundled with spamassassin, the PF companion spamd was designed to run on a PF gateway to form part of the outer defense against spam. spamd hooks into the PF configuration via a set of redirections.

The main point underlying the spamd design is the fact that spammers send a large number of messages, and the probability that you are the first person receiving a particular message is incredibly small. In addition, spam is mainly sent via a few spammer friendly networks and a large number of hijacked machines. Both the individual messages and the machines will be reported to blacklists fairly quickly, and this is the kind of data spamd can use to our advantage with blacklists.

What spamd does to SMTP connections from addresses in the blacklist is to present its banner and immediately switch to a mode where it answers SMTP traffic one byte at the time. This technique, which is intended to waste as much time as possible on the sending end while costing the receiver pretty much nothing, is called tarpitting. The specific implementation with one byte SMTP replies is often referred to as stuttering.

29.4.6.8.2.1. A Basic Blacklisting spamd

Here is the basic procedure for setting up spamd with automatically updated blacklists:

  1. Install the mail/spamd/ port. In particular, be sure to read the package message and act upon what it says. Specifically, to use spamd's greylisting features, a file descriptor file system (see fdescfs(5)) must be mounted at /dev/fd/. Do this by adding the following line to /etc/fstab:

     fdescfs /dev/fd fdescfs rw 0 0

    Make sure the fdescfs code is in the kernel, either compiled in or by loading the module with kldload(8).

  2. Next, edit the rule set to include

    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

    The two tables <spamd> and <spamd-white> are essential. SMTP traffic from the addresses in the first table plus the ones which are not in the other table are redirected to a daemon listening at port 8025.

  3. The next step is to set up spamd's own configuration in /usr/local/etc/spamd.conf supplemented by rc.conf parameters.

    The supplied sample file offers quite a bit of explanation, and the man page offers additional information, but we will recap the essentials here.

    One of the first lines without a # comment sign at the start contains the block which defines the all list, which specifies the lists actually used:

    all:\
        :traplist:whitelist:

    Here, all the desired black lists are added, separated by colons (:). To use whitelists to subtract addresses from the blacklist, add the name of the whitelist immediately after the name of each blacklist, i.e., :blacklist:whitelist:.

    Next up is a blacklist definition:

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

    Following the name, the first data field specifies the list type, in this case black. The msg field contains the message to display to blacklisted senders during the SMTP dialogue. The method field specifies how spamd-setup fetches the list data, here http. The other options are fetching via ftp, from a file in a mounted file system or via exec of an external program. Finally the file field specifies the name of the file spamd expects to receive.

    The definition of a whitelist follows much the same pattern:

    whitelist:\
        :white:\
        :method=file:\
        :file=/var/mail/whitelist.txt

    but omits the message parameters since a message is not needed.

    Choose Data Sources with Care:

    Using all the blacklists in the sample spamd.conf will end up blacklisting large blocks of the Internet, including several Asian nations. Administrators need to edit the file to end up with an optimal configuration. The administrator is the judge of which data sources to use, and using lists other than the ones suggested in the sample file is possible.

    Put the lines for spamd and any startup parameters desired in /etc/rc.conf, for example:

    spamd_flags="-v" # for normal use: "" and see spamd-setup(8)

    When done with editing the setup, reload the rule set, start spamd with the options desired using the /usr/local/etc/rc.d/obspamd script, and complete the configuration using spamd-setup. Finally, create a cron(8) job which calls spamd-setup to update the tables at reasonable intervals.

On a typical gateway in front of a mail server, hosts will start getting trapped within a few seconds to several minutes.

29.4.6.8.2.2. Adding Greylisting to the spamd Setup

spamd also supports greylisting, which works by rejecting messages from unknown hosts temporarily with 45n codes, letting messages from hosts which try again within a reasonable time through. Traffic from well behaved hosts, that is, senders which are set up to behave within the limits set up in the relevant RFCs [9], will be let through.

Greylisting as a technique was presented in a 2003 paper by Evan Harris [10], and a number of implementations followed over the next few months. OpenBSD's spamd acquired its ability to greylist in OpenBSD 3.5, which was released in May 2004.

The most amazing thing about greylisting, apart from its simplicity, is that it still works. Spammers and malware writers have been very slow to adapt.

The basic procedure for adding greylisting to your setup follows below.

  1. If not done already, make sure the file descriptor file system (see fdescfs(5)) is mounted at /dev/fd/. Do this by adding the following line to /etc/fstab:

    fdescfs /dev/fd fdescfs rw 0 0

    and make sure the fdescfs(5) code is in the kernel, either compiled in or by loading the module with kldload(8).

  2. To run spamd in greylisting mode, /etc/rc.conf must be changed slightly by adding

    spamd_grey="YES"  # use spamd greylisting if YES

    Several greylisting related parameters can be fine-tuned with spamd's command line parameters and the corresponding /etc/rc.conf settings. Check the spamd man page to see what the parameters mean.

  3. To complete the greylisting setup, restart spamd using the /usr/local/etc/rc.d/obspamd script.

Behind the scenes, rarely mentioned and barely documented are two of spamd's helpers, the spamdb database tool and the spamlogd whitelist updater, which both perform essential functions for the greylisting feature. Of the two spamlogd works quietly in the background, while spamdb has been developed to offer some interesting features.

Restart spamd to Enable Greylisting:

After following all steps in the tutorial exactly up to this point, spamlogd has been started automatically already. However, if the initial spamd configuration did not include greylisting, spamlogd may not have been started, and there may be strange symptoms, such as greylists and whitelists not getting updated properly.

Under normal circumstances, it should not be necessary to start spamlogd by hand. Restarting spamd after enabling greylisting ensures spamlogd is loaded and available too.

spamdb is the administrator's main interface to managing the black, grey and white lists via the contents of the /var/db/spamdb database.

29.4.6.8.3. Network Hygiene: Blocking, Scrubbing and so On

Our gateway does not feel quite complete without a few more items in the configuration which will make it behave a bit more sanely towards hosts on the wide net and our local network.

29.4.6.8.3.1. block-policy

block-policy is an option which can be set in the options part of the ruleset, which precedes the redirection and filtering rules. This option determines which feedback, if any, PF will give to hosts which try to create connections which are subsequently blocked. The option has two possible values, drop, which drops blocked packets with no feedback, and return, which returns with status codes such as Connection refused or similar.

The correct strategy for block policies has been the subject of rather a lot of discussion. We choose to play nicely and instruct our firewall to issue returns:

set block-policy return
29.4.6.8.3.2. scrub

In PF versions up to OpenBSD 4.5 inclusive, scrub is a keyword which enables network packet normalization, causing fragmented packets to be assembled and removing ambiguity. Enabling scrub provides a measure of protection against certain kinds of attacks based on incorrect handling of packet fragments. A number of supplementing options are available, but we choose the simplest form which is suitable for most configurations.

scrub in all

Some services, such as NFS, require some specific fragment handling options. This is extensively documented in the PF user guide and man pages provide all the information you could need.

One fairly common example is this,

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

meaning, we reassemble fragments, clear the do not fragment bit and set the maximum segment size to 1440 bytes. Other variations are possible, and you should be able to cater to various specific needs by consulting the man pages and some experimentation.

29.4.6.8.3.3. antispoof

antispoof is a common special case of filtering and blocking. This mechanism protects against activity from spoofed or forged IP addresses, mainly by blocking packets appearing on interfaces and in directions which are logically not possible.

We specify that we want to weed out spoofed traffic coming in from the rest of the world and any spoofed packets which, however unlikely, were to originate in our own network:

antispoof for $ext_if
antispoof for $int_if
29.4.6.8.3.4. Handling Non-Routable Addresses from Elsewhere

Even with a properly configured gateway to handle network address translation for your own network, you may find yourself in the unenviable position of having to compensate for other people's misconfigurations.

One depressingly common class of misconfigurations is the kind which lets traffic with non-routable addresses out to the Internet. Traffic from non-routeable addresses have also played a part in several DOS attack techniques, so it may be worth considering explicitly blocking traffic from non-routeable addresses from entering your network.

One possible solution is the one outlined below, which for good measure also blocks any attempt to initiate contact to non-routable addresses through the gateway's external interface:

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

Here, the martians macro denotes the RFC 1918 addresses and a few other ranges which are mandated by various RFCs not to be in circulation on the open Internet. Traffic to and from such addresses is quietly dropped on the gateway's external interface.

The specific details of how to implement this kind of protection will vary, among other things according to your specific network configuration. Your network design could for example dictate that you include or exclude other address ranges than these.

This completes our simple NATing firewall for a small local network. A more thorough tutorial is available at http://home.nuug.no/~peter/pf/, where you will also find slides from related presentations.



[6] Why write the rule set to default deny? The short answer is, it gives better control at the expense of some thinking. The point of packet filtering is to take control, not to run catch-up with what the bad guys do. Marcus Ranum has written a very entertaining and informative article about this, The Six Dumbest Ideas in Computer Security, and it is well written too.

[7] For dialup users, the external interface is the tun0 pseudo-device. Broadband users such as ADSL subscribers tend to have an Ethernet interface to play with, however for a significant subset of ADSL users, specifically those using PPP over Ethernet (PPPoE), the correct external interface will be the tun0 pseudo-device, not the physical Ethernet interface.

[8] The main internet RFCs describing ICMP and some related techhiques are RFC792, RFC950, RFC1191, RFC1256, RFC2521, rfc2765, while necessary updates for ICMP for IPv6 are found in RFC1885, RFC2463, RFC2466. These documents are available in a number of places on the net, such as the ietf.org and faqs.org web sites.

[9] The relevant RFCs are mainly RFC1123 and RFC2821.

[10] The original Harris paper and a number of other useful articles and resources can be found at the greylisting.org web site.

All FreeBSD documents are available for download at http://ftp.FreeBSD.org/pub/FreeBSD/doc/

Questions that are not answered by the documentation may be sent to <freebsd-questions@FreeBSD.org>.
Send questions about this document to <freebsd-doc@FreeBSD.org>.