NAT with pfDespite what some
doomsayers predicted when the OpenBSD project abandoned
ipf (see the
famous CVS log entry), the new pf(4)
packet filter is doing its job very well and is steadily growing in
functionality. A lot has changed since its official appearance in OpenBSD
3.0, and it is high time to have another look at what Daniel Hartmeier and
others have been working on.
pf(4)
has undergone numerous changes since I last wrote about it. It is now
easier to use and has a greater range of available filtering options. If
you have never used pf(4)
before, you'll be delighted to know that it does packet filtering, network
address translation (NAT), load
balancing, protects against spam (coming in OpenBSD 3.3), and
provides--after the merger with
ALTQ--resource-sharing and quality of service control (QoS). In its
current shape, pf(4)
can be used to secure networks, connect multiple hosts to an external
network through a single network interface, balance load between multiple
servers, manage bandwidth, and keep logs of its activity. Because of
this, pf(4)
is quickly becoming an advanced tool for network management and security
practitioners.
|
Related Reading
TCP/IP Network Administration |
This article assumes that you are using OpenBSD 3.2 with all of the latest security patches applied. (If you still don't know how to keep your system up to date, here are some hints to get you started.) The examples presented use the DMZ network configuration described earlier in this series. (Additional sources of information on DMZ design can be found in [Zwicky, Cooper, and Chapman 2000] and [Bishop 2003].) I will also answer some recent reader questions. Because there is so much new ground to cover, this article is split into four parts: (1) network address translation, (2) packet filtering (including transparent filtering), (3) ALTQ, and (4) load balancing and a preview of what's coming in OpenBSD 3.3. For the last installment you'll need to be running OpenBSD-current, as not all of the features I will be writing about are available in OpenBSD 3.2.
It also advisable (but by no means required) that you keep a copy of a good book on the workings of TCP/IP at hand while you are reading this. When I get stuck, I usually look things up in [Hunt 2002] or [Stevens 1994]. If these don't answer my questions, I dig through man pages and RFCs. (Browsing through their book catalog, I noticed that O'Reilly & Associates have a book on Internet protocols [Hall 2000], which could be helpful, but I have not read it. Another good book is [McKusick 1996], which focuses on the design of 4.4BSD and includes a lot of information about TCP/IP itself).
A firewall does not need expensive hardware to run, just a reasonably fast machine capable of running OpenBSD 3.2, with two or more network interfaces. (Depending on the internal network topology, it could even be just one interface; for example, the DMZ design described in one of my earlier articles uses three network interfaces on the firewall.) An old Pentium or Pentium II PC should be enough to cope with traffic up to 10Mbps even with packet logging enabled. As for the system memory, 32MB of RAM is quite enough although 64MB of RAM gives the operating system more breathing space. External storage is a more important consideration when the firewall is logging large amounts of traffic. This issue was discussed earlier, but I will return to it later in this series. Finally, on the hardware side, make sure that the firewall's network interfaces are capable of working at the speeds of their connected networks, i.e. a 100Mbps card connected to a network working at speeds up to 100Mbps, not a 10Mbps card when the network is working at 100Mbps. In most cases, a 10/100Mbps card solves this problem rather well, but if you want more information about Ethernet, read [Spurgeon 2000].
|
Related Reading Ethernet: The Definitive Guide |
Remember not to run any network services and not to store any tools
beyond what's needed to run the firewall and the intrusion detection
software. The basic set of archives (base32 and
etc32) should be enough in most cases. If you need the C/C++
compiler and other software development tools (comp32), run
them on a workstation machine and transfer the binaries you create on it
to the firewall via scp(1). Similarly,
the game32, man32, misc32,
xbase32, xfont32, xserv32, and
xshare32 archives can be left uninstalled, as they only take
up valuable storage space (not to mention the fact that, in case of a
break-in, they can be used by the intruder for potential attacks).
Start the OpenBSD packet filter with pfctl(8),
its control tool:
$ sudo pfctl -e -f /etc/pf.conf
To start pf(4)
automatically on every system reboot, edit /etc/rc.conf, the
"system daemon configuration database", with vi(1)
or any other plain text editor:
$ sudo vi /etc/rc.conf
After you open /etc/rc.conf, change the following line:
pf=NO # Packet filter / NAT
to:
pf=YES # Packet filter / NAT
This file contains two more variables file related to pf(4). First
is the rule file location variable:
pf_rules=/etc/pf.conf # Packet filter rules file
Here you can change the location of the rule file from the default
/etc/pf.conf to whatever you like, but it's probably best to
leave it unchanged. Another variable of interest is:
pflogd_flags= # add more flags, ie. "-s 256"
This variable lists options for the pflogd(8)
packet filter logging daemon. We'll look at it more closely a little
later.
pf(4)
stores its filtering rules in /etc/pf.conf, a plain ASCII
file that you can edit with vi(1)
or other plain text editor. Every rule is a single, continuous line that
begins with a special keyword. Keywords define a macro, set some global
variable, or describe an action to take for packets that match a rule.
These keywords are
setscrubrdrnatbinatblockpassNote that /etc/pf.conf contains all rules for
both NAT and filtering. (They were previously stored in two separate
files.) The rule file is divided into five sections: macro definitions,
options, scrub, NAT, and filter. Any of these sections may be missing,
but the order of sections must be maintained, as in the following:
#################################################################
# macro definitions
#################################################################
# options: "set"
#################################################################
# scrub rules: "scrub"
#################################################################
# NAT rules: "rdr", "nat", "binat"
#################################################################
# filtering rules: "antispoof", "block", "pass"
When you think about it, this grouping is quite practical: later sections rely on the previous ones, so it is natural to use the order that simplifies the work of the firewall administrator. The order of the rules inside each section is a different story. It depends on your packet filtering policy and will be the subject of interest throughout this series.
Here are a few basic pfctl(8)
operations that you should memorize, as you'll use them often while
designing your own rules:
enable pf(4):
$ sudo pfctl -eenable pf(4)
and load rule file:
$ sudo pfctl -e -f /etc/pf.confonly load pf(4)
rule file (no need to disable the packet filter):
$ sudo pfctl -f /etc/pf.confparse rules, but do not load them:
$ sudo pfctl -n -f /etc/pf.confdisable pf(4):
$ sudo pfctl -dNAT and filter rules can quickly become complex, so it is very
convenient to define macros to use in place of real names of network
interfaces, addresses, protocols, ports, and other repetitive information
found in filter rules. Life is much simpler with macros; they make it
easier to adapt existing rule sets to changes in hardware configuration.
For example, the only necessary change after modifying the external
interface on the firewall machine is to edit the macro definition. Macro
names must start with a letter from the a-zA-Z range of the
lower part of the ascii(7)
set and may contain letters from the same range, digits, and underscores.
The string that the macro expands to must be enclosed in a pair of double
quotes ("). Macros are not expanded recursively for
simplicity and security. When you're referring to a macro, precede its
name with a dollar sign ($), as in:
#################################################################
# macro definitions
ext_if = "ne1"
dmz1_if = "ne2"
dmz2_if = "ne3"
#################################################################
# options: "set"
#################################################################
# scrub rules: "scrub"
scrub in on $ext_if all
scrub in on $dmz1_if all
scrub in on $dmz2_if all
|
The early release of pfctl(8)
included with OpenBSD 3.0 (and 3.1) used the -O
command-line option to specify the algorithm for filter rules
optimization. Because the new version of pf(4)
included with OpenBSD 3.2 allows more influence over the filtering engine
behavior, these options have moved to the rules file. It is simply more
convenient this way.
There are four categories of options that you may define in your packet filter rules file:
limitloginterfaceoptimizationtimeoutThe -O option is still available, but its purpose has
changed. It tells pfctl(8)
to load only the options section from the rules file, as in
$ sudo pfctl -f /etc/pf.conf -O
The already loaded NAT or filter rules will not be modified. Remember that if you disable or remove an option that was defined before, that option will be reset to its default value.
The limit frags n and the limit states
m options set hard limits on the number of memory pools
used by the packet filter. They tell pf(4)
how much memory it can use to store packet fragments (fragments are stored
in memory before reassembly and are reassembled when you use
scrub rules) and state table entries (for stateful filtering;
enable it with the keep state rules in the filter section).
If you set this option, pf(4)
will store only n fragments or
m state table entries. Administrators use this
option to avoid performance hits and to prevent attacks from overwhelming
the firewall's resources. Both limits are independent. You may set either
or both, or you may combine both in a single rule:
#################################################################
# macro definitions
#################################################################
# options: "set"
# ex. 1: limit the number of fragments kept in memory to 30000
set limit frags 30000
# ex. 2: limit the number of state table entries to 25000
set limit states 25000
# ex. 3: combine
set limit { frags 30000, states 25000 }
You can check the current limits:
$ sudo pfctl -s memory
states hard limit 25000
frags hard limit 30000
If a limit is unset, you'll see output similar to this one:
$ sudo pfctl -s memory
states unlimited
frags unlimited
Note that there is no way to unset these limits in OpenBSD 3.2. You
can change the values of limits, if you can change and reload
/etc/pf.conf:
$ sudo vi /etc/pf.conf
$ sudo pfctl -f /etc/pf.conf
To reset limits to their unlimited state, you have to comment out or
remove set limit ... rules in /etc/pf.conf, and
reboot your firewall:
$ sudo vi /etc/pf.conf
$ sudo reboot
This behavior has been fixed in the -current branch and is expected to appear in OpenBSD 3.3.
For more information about packet fragmentation and reassembly, including the issue of timeouts, consult RFC815 [Clark 1982]. If that doesn't answer your questions, read [Stevens 1994, 2:275-300].
The new loginterface ifname option specifies the
name of the network interface on which pf(4)
will collect statistics. These statistics are
The following example shows how to activate this option:
#################################################################
# macro definitions
ext_if = "ne1"
#################################################################
# options: "set"
# ex. 1: collect statistics on ne1
set loginterface $ext_if
pfctl(8)
can display these statistics:
$ sudo pfctl -s info
...
Interface Stats for ne1 IPv4 IPv6
Bytes In 760 0
Bytes Out 696 0
Packets In
Passed 10 0
Blocked 0 0
Packets Out
Passed 6 0
Blocked 0 0
...
You can collect statistics for only one interface, even if you use
multiple set loginterface rules. This snippet
set loginterface ne1
set loginterface ne2
will only collect statistics on ne2 because it was the
last loginterface rule. To switch it off, you must add the
set interface none rule (as the last set
loginterface rule) to /etc/pf.conf and reload the
options with
$ sudo pfctl -O -f /etc/pf.conf
This rule controls the packet filter engine optimization options. The
old optimization options -O found in earlier version of have
been replaced with the optimization algorithm rule.
There are six values of the algorithm argument:
default: as its name says, it's the default optimization
algorithmnormal: same as defaulthigh-latency: used for high-latency links, such as satellite
linkssatellite: same as high-latencyaggressive: expires idle connections earlier than
default; using less memory and CPU time while possibly dropping
some legitimate connectionsconservative: tries to avoid dropping any legitimate
connections at the expense of increased memory usage and CPU utilizationDon't forget to reload the new options after changing the optimization algorithm:
$ sudo pfctl -O -f /etc/pf.conf
Before you rush to enable these optimization rules, you should know that these algorithms make a difference in special cases like high-latency connections, or very busy corporate, government, or education networks. Small networks and networks with low traffic will see no noticeable performance improvements.
The optimization rule is a shortcut for setting a bunch of
timeout rules quickly. If none of them seem to work in your
particular setup, consider adjusting the timeout values
yourself, as described below.
The timeout option rule adjusts the expiration time of
stateful connections. These rules only apply to packets matching stateful
connections (established with the keep state keyword in
pass filter rules). The general syntax of this rule is
set timeout protocol.connectionstate timeout, for
example:
#################################################################
# options: "set"
# ex. 1 sets timeout of the stateful connection to 20 seconds
# after receiving the first packet from the host initializing
# this connection.
set timeout tcp.first 20
# ex. 2 sets timeout of the stateful connection to 20 seconds
# after receiving the first packet from the host initializing
# this connection, then, if the connection is established,
# every packet that matches the established state of a TCP
# connection resets the timeout of the TCP connection it is a
# part of to 10 seconds. This is very aggressive, and will result
# in a high percentage of lost valid connections on slow links.
set timeout tcp.first 20
set timeout tcp.established 10
# ex. 3 same as ex. 2, but both rules have been combined on a
# single line (the order of protocol.state rules is not relevant)
set timeout { tcp.first 20, tcp.established 10 }
Example 1 above sets a very aggressive rule. If the connection is not established in 20 seconds, it will be dropped. In example 2, the connection will be dropped if the firewall does not receive a packet that is a part of the established TCP connection in 10 seconds. This is a very aggressive setting.
The protocol.connectionstate can be one of these
values:
tcp.firsttcp.openingtcp.establishedtcp.closingtcp.finwaittcp.closedAll of the above values match various states of a TCP connection cycle. (To learn more about the TCP connection state transition cycle, consult RFC761 [Postel 1980], and if you are still looking for more information, read [Stevens 1994, 1:240-242, 2:805-807].)
You can check global timeout settings with
$ sudo pfctl -s timeouts
tcp.first 120s
tcp.opening 30s
tcp.established 86400s
tcp.closing 900s
tcp.finwait 45s
tcp.closed 90s
udp.first 60s
udp.single 30s
udp.multiple 60s
icmp.first 20s
icmp.error 10s
other.first 60s
other.single 30s
other.multiple 60s
frag 30s
interval 10s
As you can see, it is possible to control other protocols, like UDP, or
ICMP, but the number of protocol.state matches is more
limited:
udp.firstudp.singleudp.multipleicmp.firsticmp.errorother.firstother.singleother.multipleother is a catch-all category for protocols which are neither
TCP, UDP, nor ICMP.
The last two timeouts (interval and frag)
specify the interval between flushing expired states and fragments and the
time before unassembled fragments are flushed.
#################################################################
# options: "set"
# this connection.
set timeout interval 20
set timeout frags 20
Because optimization rules reset various
timeout settings, you should always list
optimization rules before your timeout
rules; otherwise your timeout settings will be overwritten
the values introduced by optimization rules.
|
Not all IP packets sent over the Internet are well-formed, which may cause problems to hosts or routers running IP stacks that cannot properly handle packet fragmentation and reassembly. Improperly formed packets may be sent by poorly written software running on some external or internal machines, or, quite frequently, by attackers trying to compromise your network.
You can scrub incoming or outgoing packets. There are two schools of
thought. One claims that it is enough to scrub only the packets arriving
at the external interface from the outside. The other claims that all
packets that match in rules on all interfaces (packets sent
from the outside and destined to the inside of your network, and packets
sent from your network to outside hosts) ought to be scrubbed. While a
paranoid mind gravitates toward the second solution, remember that every
rule costs CPU cycles and memory. Also, there are times when scrubbing
may interfere with network intrusion detection systems (NIDS) because
packet normalization may not detect rogue traffic which would be detected
if no scrubbing were done. Of course, it is possible to log such packets
and set your NIDS to take appropriate action. As you can see, there is no
one-size-fits-all solution.
#################################################################
# macro definitions
ext_if = "ne1"
#################################################################
# options: "set"
#################################################################
# scrub rules: "scrub"
# ex. 1: scrub all incoming packets on all interfaces
scrub in all
# ex. 2: scrub all incoming packets on the external interface
scrub in on $ext_if all
The process of packet normalization with the scrub rules
can be refined with these keywords:
fragment reassemble: reassemble fragmented packets. This
is the default behavior. Fragments are held in memory until all fragments
of the original packet have been collected. You can set a limit on the
number of fragments with the set frags option.fragment crop: do not reassemble fragmented packets, but
pass them on. Track fragments, drop duplicate fragments, and crop
overlaps.fragment drop-ovl: similar to fragment
crop, but drops all duplicate and overlapping fragments and their
corresponding fragments.Note that only the fragment reassemble modifier works when
NAT rules are used at the same time. The fragment crop and
fragment drop-ovl do not work with NAT in OpenBSD 3.2.
It is also possible to set some options:
no-df: clear the don't-fragment bit in the IP flags.
This lets datagrams be fragmented by other routers.min-ttl n: sets the minimum Time-To-Live for the
matching packets to n hops. The longer the TTL, the longer
the packet will circulate in the network. This modifier is only useful
for finetuning tune your network's performance.max-mss m: sets the Maximum Segment Size for a
fragment to m bytes. Generally, the larger the MMS, the better
the network performance. Again, you'll only need to worry about this option
when there is a performance problem.The above options are of interest to administrators of large networks. Their use and the way they affect performance of networks is best described in [Stevens 1994].
All scrub modifiers are listed at the end of the rule:
#################################################################
# macro definitions
ext_if = "ne1"
ext_ad = "any"
prv_ad = "f.f.f.f/24"
#################################################################
# options: "set"
#################################################################
# scrub rules: "scrub"
scrub in on $ext_if all no-df min-ttl 100 max-mss 1440 fragment reassemble
scrub in on $ext_if all no-df min-ttl 100 max-mss 1440 fragment crop
scrub in on $ext_if from $ext_ad to $prv_ad no-df min-ttl
100 max-mss 1440 fragment drop-ovl
Only IPv4 fragments can be processed in this way. IPv6 fragments are blocked.
For more information on packet normalization, read the pf.conf(5),
RFC815 [Clark 1982],
and this interesting
paper [Handley and Paxson 2001].
Network address translation (NAT) is a technique for connecting hosts hidden behind firewalls to the Internet. It can be used to redirect traffic between external and internal hosts as a proxy. It's also often used to increase the number of hosts connected to the Internet.
In the early days of the Internet, every host (or, more precisely, every network interface connecting that host to other hosts on the Internet) needed a unique IP number. The dynamic expansion of the number of hosts connected to the Internet during the last decade made it obvious that the old pool of IP numbers will soon end. Of course, the best answer for that is IPv6, but before the world switches to it, NAT will see much use as a technique for better utilization of the existing pool of IPv4 numbers, as well as for some useful security setups.

Figure 1. A general outline of the network described in this article.
By definition, if a host has no public IP address assigned to it, it cannot be reached from the outside, even if you plug it into the connector that your Internet provider gave you, which is good from the point of view of security. It gets even better when you put a firewall between the internal network and the outside network, as shown in Figure 1 above. Then you can carefully log and screen all incoming and outgoing traffic to ensure maximum security. If you split your internal network into two or more segments, then your network becomes even more secure. However, since your internal networks use a private address space (read RFC1918 [Rekhter, Moskowitz, Karrenberg, de Groot, and Lear 1996]), they are unreachable from the outside and they cannot reach outside hosts. Such a setup is secure, but not very functional, which is why we need NAT.
If you want to run NAT, you must enable IP routing before you load NAT rules. This is done with
$ sudo sysctl -w net.inet.ip.forwarding=1
net.inet.ip.forwarding: 0 -> 1
To turn IP routing off, use this command:
$ sudo sysctl -w net.inet.ip.forwarding=0
net.inet.ip.forwarding: 1 -> 0
When you're unsure if IP routing is on or off, you can check it with
$ sudo sysctl net.inet.ip.forwarding
net.inet.ip.forwarding = 0
To make changes permanent and always enable IP routing at system startup,
edit /etc/sysctl.conf and change this line:
#net.inet.ip.forwarding=1 # 1=Permit forwarding (routing) of packets
to
net.inet.ip.forwarding=1 # 1=Permit forwarding (routing) of packets
|
There are three kinds of NAT rules:
rdr: port redirectionnat: translation between groups of internal addresses and a
single external addressbinat: bidirectional translation between one internal
address and one external addressWhile experimenting with your NAT setup, the following commands will make your life a little easier:
load only NAT rules:
$ sudo pfctl -N -f /etc/pf.confshow loaded NAT rules:
$ sudo pfctl -s natflush NAT rules:
$ sudo pfctl -F natThe rdr rules redirect traffic from one port to another.
A classic example of using traffic redirection is the case of an HTTP
server hidden in a DMZ, yet accessible to hosts outside your network.
Ordinarily, such a server must listen on a privileged port 80, and the
machine it runs on must be directly accessible to external hosts. This
setup is not very safe, so you might consider moving the server behind a
firewall, into a DMZ network segment. However, if you do that, the server
is inaccessible, because it is the firewall which receives requests for
the HTTP server. The firewall must now redirect these packets to the web
server residing inside the DMZ network segment. This is accomplished with
the following rule:
#################################################################
# macro definitions
ext_if = "ne2"
ext_ad = "f.f.f.f/32"
dmz_ad = "w.w.w.w/32"
#################################################################
# NAT rules: "rdr", "nat", "binat"
rdr on $ext_if proto tcp from any to $ext_ad port 80 ->
$dmz_ad port 8080
The above rule redirects all TCP (proto tcp) packets
arriving at the firewall's external address (on $ext_if),
originating from any source address (from any) and destined
to the HTTP server, listening on port 80 (to $ext_ad port 80)
to the network interface located in the DMZ (-> $dmz_ad).
The server listens on port 8080 (port 8080). That port is
unprivileged, and the attacker has less chance of breaking things, should
the server be compromised.
As you will soon discover, this rule works for connections made from the outside to your web server, but not from your private network. This is solved by adding another rule:
#################################################################
# macro definitions
ext_if = "ne2"
prv_if = "ne1"
ext_ad = "f.f.f.f/32"
prv_ad = "f.f.f.f/24"
dmz_ad = "w.w.w.w/32"
#################################################################
# NAT rules: "rdr", "nat", "binat"
rdr on $ext_if proto tcp from any to $ext_ad port 80 ->
$dmz_ad port 8080
rdr on $prv_if proto tcp from $prv_ad to $ext_ad port 80 ->
$dmz_ad port 8080
You can rewrite it in the following way (notice that I put interface names and addresses in curly braces):
#################################################################
# macro definitions
rdr_ifs = "{ ne2, ne1 }"
rdr_ads = "{ any, p.p.p.p/24 }"
ext_ad = "f.f.f.f/32"
dmz_ad = "w.w.w.w/32"
#################################################################
# NAT rules: "rdr", "nat", "binat"
rdr on $rdr_ifs proto tcp from $rdr_ads to $ext_ad port 80
-> $dmz_ad port 8080
You can use curly braces to list interface names, protocol names, and
addresses in rdr rules. You can also replace port numbers
with their names, e.g. port www and port 80 are
equivalent. These names and numbers can be found in
/etc/services.
The above rule could be tightened a little. For example, if you want
to redirect only IPv4 packets, add the inet keyword:
#################################################################
# macro definitions
rdr_ifs = "{ ne2, ne1 }"
rdr_ads = "{ any, p.p.p.p/24 }"
ext_ad = "f.f.f.f/32"
dmz_ad = "w.w.w.w/32"
#################################################################
# NAT rules: "rdr", "nat", "binat"
rdr on $rdr_ifs inet proto tcp from $rdr_ads to $ext_ad port 80
-> $dmz_ad port 8080
Similarly, to redirect only IPv6 packets, use the inet6
keyword.
What if you wanted to redirect all queries to port 80 on all addresses to a web cache? Return to an earlier setup with two separate rules and change the second rule:
#################################################################
# macro definitions
ext_if = "ne2"
prv_if = "ne1"
ext_ad = "f.f.f.f/32"
prv_ad = "f.f.f.f/24"
www_ad = "w.w.w.w/32"
cch_ad = "c.c.c.c/32"
#################################################################
# NAT rules: "rdr", "nat", "binat"
rdr on $ext_if proto tcp from any to $ext_ad port 80
-> $www_ad port 8080
rdr on $prv_if proto tcp from $prv_ad to any port 80
-> $cch_ad port 1080
In the example above, the web cache listens on port 1080. Note that this technique of forcing everyone on the internal network to connect to the Web through the cache server is controversial, and you must not impose it on your users without careful thought. For more information consult [Wessels 2001].
What if you want to bypass the cache yourself? Use the no
modifier, as in:
#################################################################
# macro definitions
ext_if = "ne2"
prv_if = "ne1"
ext_ad = "f.f.f.f/32"
prv_ad = "p.p.p.p/24"
bos_ad = "p.p.p.b/24"
www_ad = "w.w.w.w/32"
cch_ad = "c.c.c.c/32"
#################################################################
# NAT rules: "rdr", "nat", "binat"
rdr on $ext_if proto tcp from any to $ext_ad port 80 -> $www_ad port 8080
rdr on $prv_if proto tcp from $prv_ad to any port 80 -> $cch_ad port 1080
no rdr on $prv_if proto tcp from $bos_ad to any port 80
As you can see, the no modifier makes the -> ...
part of the rule unnecessary (and such rules do not parse, as they
do not make sense).
Another modifier is !, which negates the values (interface
names, source and target addresses) it precedes:
rdr on ! ne1 inet proto tcp from ! s.s.s.s/32 to !
e.e.e.e/32 port 80 -> d.d.d.d/32 port 8080
The above rule redirects all IPv4 TCP packets arriving on any interface
except ne1 from any address except s.s.s.s/32 and
destined to any address except e.e.e.e/32.
The rdr rules are very handy because they can be used to
configure proxies, redirect traffic from a dead host to a backup host. and
so on. Recently, rdr rules have been used to fight spam. If
a suspicious host tries to connect to the smtp port on your
firewall, its request will be directed to a special program that keeps it
waiting forever for a connection confirmation; then, just when the
spammer's MTA thinks it will be able to send mail it receives an error
message and the connection closes. Such delays of several minutes
seriously slow spammers and are a good way to make their life harder.
OpenBSD 3.3 is supposed to include some interesting tools for fighting
spam.
NAT rules perform network address translation for groups of internal hosts, with private addresses hidden behind a firewall, which access the outside world through a single interface with one public IP. (The external interface could have more IP addresses assigned to it, but let's focus on the most severe case.) This not only solves the problem of connecting more than one host through a single interface, but it also hides details of your internal network's layout, the number of hosts, and other information that an intruder may find useful.
The magic is possible because the firewall keeps a record of who sent
what and where, so it can send replies to the right host. To do that it
must keep a table of sorts and mark packets it sends to the Internet.
This marking allows attackers to deduct how many hosts are hidden behind
the firewall and gives them an idea of what might be hiding behind your
firewall, provided they can capture that traffic. It is also used by
companies selling DSL access to the Internet to find out who's breaching
their contracts. (Some DSL access providers forbid their customers to use
NAT, and impose penalties on those who use it. If your provider does this,
consider switching to another.) The latest versions of pf(4)
can fool these detection systems, but you need to be running
OpenBSD-current, which is still experimental. We'll look at what -current
has to offer in part 4.
How do you connect your private network to the outside world? It's quite simple, actually:
#################################################################
# macro definitions
ext_if = "ne1"
ext_ad = "f.f.f.f/32"
prv_ads = "p.p.p.p/24"
nat_p = "{tcp, udp, icmp}"
#################################################################
# NAT rules: "rdr", "nat", "binat"
nat on $ext_if proto $nat_p from $prv_ads to any -> $ext_ad
When it is time to add a new network segment, modify the macros:
#################################################################
# macro definitions
ext_if = "ne1"
ext_ad = "f.f.f.f/32"
prv_ads = "{ p.p.p.p/24, d.d.d.d/24 }"
nat_p = "{tcp, udp, icmp}"
#################################################################
# NAT rules: "rdr", "nat", "binat"
nat on $ext_if proto $nat_p from $prv_ads to any -> $ext_ad
Shouldn't we use the names of the interfaces that connect our private networks to the firewall? No, address translation is done on the external interface.
Just like rdr rules, nat rules allow us to
use the no and ! modifiers before interface
names and private host addresses. It is also possible to limit their
scope to IPv4 or IPv6 packets (inet and inet6,
respectively). The pf.conf(5)
man page has more detailed information about using intricate modifiers
like binary or unary operators.
The last of the three NAT rules are binat rules, which
bind an external public address to an internal private address. VPN
setups use this bidirectional translation, and it can provide additional
security for hosts exposing public services. These rules are similar to
rdr rule, but they do not allow such fine degrees of
control. While the following rule set works with rdr rules,
it is not possible with binat rules.
rdr on $ext_if proto tcp from any to $ext_ad port 22 -> 192.168.1.1 port 1022
rdr on $ext_if proto tcp from any to $ext_ad port 25 -> 192.168.1.2 port 1025
rdr on $ext_if proto tcp from any to $ext_ad port 53 -> 192.168.1.3 port 1053
rdr on $ext_if proto tcp from any to $ext_ad port 80 -> 192.168.1.4 port 8080
Compare it with binat rules:
binat on $ext_if proto tcp from 192.168.1.37 to any -> $ext_ad_1
binat on $ext_if proto tcp from 192.168.1.38 to any -> $ext_ad_2
binat on $ext_if proto tcp from 192.168.1.54 to any -> $ext_ad_3
As you can see, every internal address must have its own equivalent external address. They can all be bound to the same external interface, though. If you want to know more, consult the ifconfig(8) man page (look for information about aliases).
Again, no and ! modifiers are allowed, as are
address class modifiers (inet and inet6).
Always remember that NAT rules do not filter traffic. They redirect
it. There must be another rule that filters out traffic redirected to
another interface or port. The sender of the original packet does not
know anything about what goes on behind the firewall. All it knows is
that the packet has reached its destination at $ext_ad. The
next installment of this series will cover filtering.
Jacek Artymiak started his adventure with computers in 1986 with Sinclair ZX Spectrum. He's been using various commercial and Open Source Unix systems since 1991. Today, Jacek runs devGuide.net, writes and teaches about Open Source software and security, and tries to make things happen.
Read more Securing Small Networks with OpenBSD columns.
Return to the BSD DevCenter.
Copyright © 2009 O'Reilly Media, Inc.