OpenBSD PF - Network Address Translation [Contents]



Introduction

Network Address Translation (NAT) is a way to map an entire network (or networks) to a single IP address. It is necessary, for example, when the number of IP addresses assigned to a customer by an internet service provider is less than the total number of computers in that household that need internet access. NAT is described in RFC 1631.

NAT allows an administrator to take advantage of the reserved address blocks described in RFC 1918. Typically, the internal network will be set up to use one or more of these network blocks:

10.0.0.0/8       (10.0.0.0 - 10.255.255.255)
172.16.0.0/12    (172.16.0.0 - 172.31.255.255)
192.168.0.0/16   (192.168.0.0 - 192.168.255.255)
An OpenBSD system doing NAT will have at least two network interfaces: one to the internet and the other for the internal network. NAT will be translating requests from the internal network so they appear to all be coming from the OpenBSD NAT system.

How NAT Works

When a client on the internal network contacts a machine on the internet, it sends out IP packets destined for that machine. These packets contain all the addressing information necessary to get them to their destination. NAT is concerned with these pieces of information: source IP address and source TCP or UDP port.

When the packets pass through the NAT gateway, they will be modified so that they appear to be coming from the NAT gateway itself. The NAT gateway will record the changes it makes in its state table so that it can reverse the changes on return packets and ensure that return packets are passed through the firewall and are not blocked. For example, the following changes might be made:

Neither the internal machine nor the internet host is aware of these translation steps. To the internal machine, the NAT system is simply an internet gateway. To the internet host, the packets appear to come directly from the NAT system. It is completely unaware that the internal workstation even exists.

When the internet host replies to the internal machine's packets, they will be addressed to the NAT gateway's external IP at the translation port. The NAT gateway will then search the state table to determine if the reply packets match an already established connection. A unique match will be found based on the IP/port combination which tells PF the packets belong to a connection initiated by the internal machine. PF will then make the opposite changes it made to the outgoing packets and forward the reply packets on to the internal machine.

Translation of ICMP packets happens in a similar fashion but without the source port modification.

IP Forwarding

Since NAT is almost always used on routers and network gateways, it will probably be necessary to enable IP forwarding so that packets can travel between network interfaces on the OpenBSD machine. IP forwarding is enabled using the sysctl(2) mechanism:
# sysctl net.inet.ip.forwarding=1
# echo  'net.inet.ip.forwarding=1' >> /etc/sysctl.conf
Or, for IPv6:
# sysctl net.inet6.ip6.forwarding=1
# echo  'net.inet6.ip6.forwarding=1' >> /etc/sysctl.conf

Configuring NAT

NAT is specified as an optional nat-to parameter to an outbound pass rule. Often, rather than being set directly on the pass rule, a match rule is used. When a packet is selected by a match rule, parameters (e.g. nat-to) in that rule are remembered and are applied to the packet when a pass rule matching the packet is reached. This permits a whole class of packets to be handled by a single match rule and then specific decisions on whether to allow the traffic can be made with block and pass rules.

The general format in pf.conf looks something like this:

match out on interface [af] \
   from src_addr to dst_addr \
   nat-to ext_addr [pool_type] [static-port]
[...]
pass out [log] on interface [af] [proto protocol] \
   from ext_addr [port src_port] \
   to dst_addr [port dst_port]
match
When a packet traverses the ruleset and matches a match rule, any optional parameters specified in that rule are remembered for future use (made "sticky").
If a match rule applies a network address translation (nat-to, etc), any subsequent rules will compare against the new translated address, not the original address.

pass
This rule allows the packet to be transmitted. If the packet was previously matched by a match rule where parameters were specified, they will be applied to this packet. pass rules may have their own parameters; these take priority over parameters specified in a match rule.

out
Specifies the direction of packet flow where this rule applies. nat-to may only be specified for outbound packets.

log
Log matching packets via pflogd(8). Normally, only the first packet that matches will be logged. To log all matching packets, use log (all).

interface
The name or group of the network interface to transmit packets on.

af
The address family, either inet for IPv4 or inet6 for IPv6. PF is usually able to determine this parameter based on the source/destination address(es).

protocol
The protocol (e.g. tcp, udp, icmp) of packets to allow. If src_port or dst_port is specified, the protocol must also be given.

src_addr
The source (internal) address of packets that will be translated. The source address can be specified as:

src_port
The source port in the Layer 4 packet header. Ports can be specified as: The port option is not usually used in nat rules because the goal is usually to NAT all traffic regardless of the port(s) being used.
dst_addr
The destination address of packets to be translated. The destination address is specified in the same way as the source address.

dst_port
The destination port in the Layer 4 packet header. This port is specified in the same way as the source port.

ext_addr
The external (translation) address on the NAT gateway that packets will be translated to. The external address can be specified as:
pool_type
Specifies the type of address pool to use for translation.

static-port
Tells PF not to translate the source port in TCP and UDP packets.

This would lead to a most basic form of these lines similar to this:

pass out on tl0 from 192.168.1.0/24 to any nat-to 198.51.100.1
This rule says to perform NAT on the tl0 interface for any packets coming from 192.168.1.0/24 and to replace the source IP address with 198.51.100.1.

A more advanced form might take care of address translation first (possibly much earlier in the ruleset), and then pass permitted traffic:

# using "tag"
match out inet from 192.168.1.0/24 to any nat-to 198.51.100.1 tag natted
pass out on tl0 to any tagged natted

# or using "received-on"
match out inet from dc0:network to any nat-to 195.51.100.1
pass out on tl0 received-on dc0
Rather than hard-coding addresses, it is also possible to specify interface names (and optional modifiers) instead:
match out inet from dc0:network to any nat-to tl0
pass out on tl0 received-on dc0
The advantage should be fairly clear: the IP addresses of either interface can be changed without changing this rule. For nat-to, you will usually want to avoid translating IPv6 traffic: inet will restrict to only matching IPv4 traffic, avoiding unexpected surprises.

When specifying an interface name for the translation address as above, the IP address is determined at pf.conf load time, not on the fly. If DHCP or PPP is being used to configure the external interface, this can be a problem: if the assigned IP address changes, NAT will continue translating outgoing packets using the old IP address. This will cause outgoing connections to stop functioning. To get around this, PF can automatically update the translation address by putting parentheses around the interface name:

match out inet from dc0:network to any nat-to (tl0)
pass out on tl0 received-on dc0
The downside is that it incurs a slight overhead of looking up the current address for each new connection.

Bidirectional Mapping (1:1 Mapping)

A bidirectional mapping can be established by using the binat-to parameter. A binat-to rule establishes a one-to-one mapping between an internal IP address and an external address. This can be useful, for example, to provide a web server on the internal network with its own external IP address. Connections from the internet to the external address will be translated to the internal address and connections from the web server (such as DNS requests) will be translated to the external address. TCP and UDP ports are never modified with binat-to rules as they are with nat rules.

Example:

web_serv_int = "192.168.1.100"
web_serv_ext = "198.51.100.6"

pass on tl0 from $web_serv_int to any binat-to $web_serv_ext

Checking NAT Status

To view the active NAT translations, pfctl(8) is used with the -s state option. This option will list all the current NAT sessions:
# pfctl -s state
all tcp 192.168.1.35:2132 (198.51.100.1:53136) -> 198.51.100.10:22 TIME_WAIT:TIME_WAIT
all udp 192.168.1.35:2491 (198.51.100.1:60527) -> 198.51.100.33:53   MULTIPLE:SINGLE
Explanations (first line only):
all
Indicates the interface that the state is bound to if the state-policy is if-bound, otherwise the word all will appear if the state is floating (the default).

TCP
The protocol being used by the connection.

192.168.1.35:2132
The IP address (192.168.1.35) of the machine on the internal network. The source port (2132) is shown after the address. This is also the address that is replaced in the IP header.

198.51.100.1:53136
The IP address (198.51.100.1) and port (53136) on the gateway that packets are being translated to.

198.51.100.10:22
The IP address (198.51.100.10) and the port (22) that the internal machine is connecting to.

TIME_WAIT:TIME_WAIT
This indicates what state PF believes the TCP connection to be in.