NAT rules
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 address
While 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 nat
rdr
The 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
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.
binat
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.
You must be logged in to the O'Reilly Network to post a talkback.
Showing messages 1 through 19 of 19.
-
nic setup
2004-03-09 11:11:17 gwung [Reply | View]
how would the NICs be configured using ifconfig? how about routing?
-
dmz
2004-01-10 00:10:41 anonymous2 [Reply | View]
Hello.
The question is simple...if i want to make a DMZ with a 3rd netcard and i dont want any kind of packet filtering (like no firewall at all) for that DMZ net...with only the NAT rule line is enough?
Thx in advance
-
Incorrect Information in this Article
2003-12-14 22:38:36 anonymous2 [Reply | View]
The information posted about NAT rdr is incorrect. The author says that, in order to allow connections from internal interfaces, all that is needed is to do a rdr from the internal interface as well as from the external interface.
However, this is incorrect, and only results in the packet going one way. The return packet is discarded, however, because it comes from the wrong ip:
http://www.openbsd.org/faq/pf/rdr.html
On this page, it explains how to deal with this, and it is not trivial, although the correct lines are included here. The relevant paragraph that describes the issue is here, as well:
**************
Adding a second redirection rule for the internal interface does not have the desired effect either. When the local client connects to the external address of the firewall, the initial packet of the TCP handshake reaches the firewall through the internal interface. The redirection rule does apply and the destination address gets replaced with that of the internal server. The packet gets forwarded back through the internal interface and reaches the internal server. But the source address has not been translated, and still contains the local client's address, so the server sends its replies directly to the client. The firewall never sees the reply and has no chance to properly reverse the translation. The client receives a reply from a source it never expected and drops it. The TCP handshake then fails and no connection can be established.
************
This is an issue that is a pain to deal with with ipfw (FreeBSD), as well, although I have fixed the issue there, too. I was ecstatic when I read that simply doing a rdr from the internal interface would work on pf without additional tweaking. However, it doesn't, and this article is misleading (unintentionally, I'm sure). I hope you will correct this error to save someone else the trouble of finding this out the hard way.
-Dan -
Incorrect Information in this Article
2003-12-15 12:06:55 Jacek Artymiak | [Reply | View]
It would help, if you described your LAN configuration and what exactly you are trying to achieve in a little more detail. What works in one case, may not work in another. If you do that, I'll try to help.
--Jacek -
Thank you, Jacek
2003-12-15 15:18:19 anonymous2 [Reply | View]
Thanks for the offer, although I already have it working. I will explain it for other people's benefit, though.
I want to rdr certain ports to certain internal hosts using pf, at the same time as using NAT on the external interface. The rdr rules work fine for this, but as you noted, internal hosts cannot connect to the external ip address with the single rdr rule. So you add a rdr rule on the internal NIC, right? Wrong. That's what that quote from the pf documentation is about--such a method doesn't work.
3 Hosts, let's give them real IPs just to be clear:
Gateway ($int) 10.0.0.1 ($ext) 1.2.3.4
Host2 10.0.0.2
Host3 10.0.0.3
We do NAT on ext, and redirect port 5555 to Host2:
nat on $ext proto $tcp from 10.0.0.0/24 to any -> $ext
rdr on $ext proto tcp from any to $ext port 5555 -> Host2 port 5555
Any external host connects to 1.2.3.4, the port is forwarded using the rdr rule. Host2 sees the forward, responds to the external host, and everything works.
But what if Host3 connects to 1.2.3.4? It doesn't work, so we add another rdr rule (as you suggested):
rdr on $int proto tcp from any to $ext port 5555 -> Host2 port 5555
Host3 sends a packet to 1.2.3.4, which comes in on $int. Packet looks like this:
Src: 10.0.0.3; Dest=1.2.3.4
Then, pf redirects the packet to 10.0.0.2. So far, so good. Host2 responds, with a packet like this:
Src: 10.0.0.2; Dest 10.0.0.3
Host3 sees this packet and throws it away. Host3 says, essentially "I never sent anything to 10.0.0.3--why is it responding?" The rdr rule will never work because the wrong host responds to Host3's request to connect to 1.2.3.4.
The solution from OpenBSD's pf page is to add a NAT rule that changes the source ip:
nat on $int proto tcp from 10.0.0.0/24 to Host2 port 5555 -> $int
What this does is make the initial packet appear to come from $int. Host2 then responds to $int, where pf's NAT will translate the source ip back to 1.2.3.4, so that Host3 thinks it's talking to 1.2.3.4. As the page notes, this is not exactly an elegant solution, but it works (they add another rule to tighten it a bit).
I don't believe simply adding an rdr as you did in the article works, although I may have misunderstood your configuration. The internal rdr does forward the port, but the responding packet will be dropped as it comes from the wrong host. NAT similar to the external interfaces is needed in order to make the internal host believe it is talking to the external ip.
-Dan
-
sysctl.conf, not rc.conf
2003-08-17 01:10:14 anonymous2 [Reply | View]
Shouldn't the example for activating ip.forwarding refer to sysctl.conf rather than rc.conf?
-
sysctl.conf, not rc.conf
2003-12-15 12:12:07 Jacek Artymiak | [Reply | View]
Yes, it should. Mea culpa.
--Jacek
-
I don't understand
2003-08-14 03:36:33 postleb [Reply | View]
Hi,
great articel.
But when it comes to the interesting part on page 4, redirections you are loosing me.
Just right away in the first example:
rdr on $ext_if proto tcp from any to $ext_ad port 80 ->
$dmz_ad port 8080
Why are we going on $ext_ad and what does this macro stand for?
I don't understand macro definitions:
xt_ad = "f.f.f.f/32"
prv_ad = "f.f.f.f/24"
dmz_ad = "w.w.w.w/32"
What do they stand for in "real" IP adresses?
Which article did I miss to catch up?
I thought start reading the first articel can't be wrong :-)
Pls be patient with me as I'm not a native english speaker. Learning new stuff in a foreign language is double though.
Greetings
Frank
-
I don't understand
2003-12-15 12:19:22 Jacek Artymiak | [Reply | View]
ext_ad is the external address of your firewall's external interface, i.e. the address that other host on the Internet can see and connect to;
prv_ad is the address of your firewall's interface to which you connect private hosts; that address may or may not be accessible from the outside;
dmz_ad is the address of your firewalls's interface to which you conect hosts in the DeMilitarized Zone (a separate segment of your private network that is accessible to the outside world.
--Jacek
-
Nuff Said
2003-08-07 13:14:39 anonymous2 [Reply | View]
I used your four part article for basic introduction to opebsd packet filtering. Since we are using 3.2 this article just brought it home... Good job.
-
On the article
2003-08-06 10:04:14 anonymous2 [Reply | View]
This is a lovely article. Looking forward to your further writings. Pedja
-
mistype
2003-06-05 06:07:32 anonymous2 [Reply | View]
Just wanted to mention there's a typo "logininterface" should be "loginterface" page 2
Great site by the way
-
Great article!
2003-04-09 04:19:58 anonymous2 [Reply | View]
Great article! I'd love to see one on altq... *hint hint* ;) -
Great article!
2003-04-09 07:43:52 Jacek Artymiak | [Reply | View]
Thanks! Working on altq already!
-
Typo in article - logininterface
2003-03-10 03:32:29 anonymous2 [Reply | View]
On page 2 (of the otherwise excellent article) there is a typo:
# ex. 1: collect statistics on ne1
set logininterface $ext_if
It should ofcourse say "loginterface" instead of "logininterface". Do you think you could correct it?
/Johan Torin
jtorin@myrealbox.com





nat on $ext_if proto $nat_p from $prv_ads to any -> $ext_ad
Thanks!