BSD DevCenter
oreilly.comSafari Books Online.Conferences.

advertisement


NAT with pf
Pages: 1, 2, 3, 4

NAT rules

There are three kinds of NAT rules:



  • rdr: port redirection
  • nat: translation between groups of internal addresses and a single external address
  • binat: 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.conf
  • show loaded NAT rules:

    $ sudo pfctl -s nat
  • flush 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].

Web Caching

Related Reading

Web Caching
By Duane Wessels

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.



Sponsored by: