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 thePF 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/.
In order to use PF, the PF kernel module must be first
loaded. Add the following line to
/etc/rc.conf
:
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:
/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
:
Then, run the startup script to load the module:
#
service pflog start
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
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”.
The following rc.conf(5) statements can be used to configure PF and pflog(4) at boot:
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:
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.
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:
Command | Purpose |
---|---|
pfctl
| Enable PF. |
pfctl
| Disable PF. |
pfctl | Flush all NAT, filter, state, and table
rules and reload
/etc/pf.conf . |
pfctl | Report on the filter rules, NAT rules, or state table. |
pfctl | Check /etc/pf.conf for
errors, but do not load ruleset. |
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
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.
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/.
security/sudo
is
useful for running commands like pfctl
that require elevated privileges. It can be installed from
the Ports Collection.
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:
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
For a slightly more structured and complete setup, we start by denying everything and then allowing only those things we know that we need [8]. 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
Then we back up a little. Macros need to be defined before use, so at the very top of the file, we add:
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:
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.
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.
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.
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:
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
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
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.
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
could end up saving you a few headaches. We will stick to that convention from here on.
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
:
Use ifconfig -a
, or
ifconfig
to
find out if both of the interfaces to be used are up and
running.interface_name
If all traffic initiated by machines on the inside is
to be allowed, /etc/pf.conf
could
look roughly like this
[9]:
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
and the main pass rule
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
or for that matter
whichever is preferred. Lastly we need to make the name service work for our clients:
This is supplemented with a rule which passes the traffic we want through our firewall:
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.
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.
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
:
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:
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
In addition, the redirected traffic must be allowed to pass. We achieve this with
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.
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.
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
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.
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:
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.
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:
and a rule which uses the definition,
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.
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:
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.
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:
As we can see, this means we do not need to change the pass rule itself:
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 [10].
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:
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
.
which in turn is used to initialize the table in
/etc/pf.conf
:
Then, for example, one of our earlier rules can be changed to read
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.
Those who run a Secure Shell login service which is accessible from the Internet have probably seen something like this in the authentication logs:
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
Then somewhere fairly early in the rule set, add a rule to block the bruteforcers:
And finally, the pass rule.
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.
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:
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 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.
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.
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
:
expiretable is in the
Ports Collection on FreeBSD as security/expiretable
.
Over time, a number of tools have been developed which interact with PF in various ways.
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).
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.
Here is the basic procedure for setting up spamd with automatically updated blacklists:
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
:
Make sure the fdescfs
code
is in the kernel, either compiled in or by loading
the module with kldload(8).
Next, edit the rule set to include
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.
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:
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:
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:
but omits the message parameters since a message is not needed.
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:
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.
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
[11], will be let
through.
Greylisting as a technique was presented in a 2003 paper by Evan Harris [12], 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.
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
:
and make sure the fdescfs(5) code is in the kernel, either compiled in or by loading the module with kldload(8).
To run spamd in
greylisting mode, /etc/rc.conf
must be changed slightly by adding
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.
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.
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.
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.
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:
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.
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,
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.
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:
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:
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.
[8] 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.
[9] 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.
[10] 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.
[11] The relevant RFCs are mainly RFC1123 and RFC2821.
[12] 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>.