LinuxXXV · Firewallsnftables NAT
nftables NAT - source NAT, destination NAT, and masquerade
What you'll learn
- Distinguish SNAT, DNAT, and masquerade
- Configure a host as a NAT gateway
- Forward external traffic to an internal host
- Choose between SNAT and masquerade
Prerequisites
Verified against Ubuntu 24.04 LTS · Debian 12 (Bookworm) · RHEL 9.x · Rocky Linux 9.x · AlmaLinux 9.x · Linux kernel 6.1 LTS / 6.6 LTS · systemd 255+ · OpenSSH 8.7p1 (RHEL 9) / 9.6p1 (Ubuntu 24.04) · nftables 1.0.x · chrony 4.x · Pacemaker 2.1.x · Corosync 3.1.x · 2026-08-09
NAT (Network Address Translation) rewrites the source or destination IP of packets as they traverse a host. It is the mechanism behind home routers, cloud egress, and exposing internal services to the internet.
Three flavours
| Type | Verdict in nft | Valid only in | Use case |
|---|---|---|---|
| SNAT | snat to ADDR | postrouting, input | Replace source IP of outbound traffic with a fixed address |
| DNAT | dnat to ADDR | prerouting, output | Replace destination IP of inbound traffic to redirect |
| Masquerade | masquerade | postrouting | Like SNAT, but the address is dynamic (e.g. DHCP uplink) |
| Redirect | redirect to :PORT | prerouting, output | DNAT to the local host itself |
The “valid only in” column is a kernel restriction, not a
convention. A destination rewrite has to happen before the
routing decision (prerouting) or before a locally generated
packet is routed (output); a source rewrite has to happen after
it (postrouting). Put a statement in the wrong hook and the
kernel refuses the rule at load time.
Source NAT (gateway)
A host with two interfaces (internal 10.0.0.0/24, external on DHCP) needs to rewrite source addresses for outbound traffic:
# Enable forwarding
echo 1 > /proc/sys/net/ipv4/ip_forward
# NAT
nft add table inet nat
nft add chain inet nat postrouting { type nat hook postrouting priority 100 \; }
nft add rule inet nat postrouting oifname "eth1" ip saddr 10.0.0.0/24 snat ip to 192.0.2.10
192.0.2.10 is the host’s external IP. Internal hosts can
reach the internet; replies come back to 192.0.2.10 and
conntrack matches them to the right internal host.
Note oifname "eth1" and not oif eth1. oif matches the
interface index, which nft resolves at the moment the rule is
loaded: the rule fails to load if the interface does not exist
yet, and it stays bound to that index if the interface is later
recreated. oifname compares the name on every packet, so
neither happens. Use the name forms everywhere except lo.
Masquerade (dynamic uplink)
When the external IP is dynamic (DHCP, PPPoE), use
masquerade instead of snat:
nft add rule inet nat postrouting oifname "eth1" ip saddr 10.0.0.0/24 masquerade
Masquerade looks up the current IP of eth1 for every
packet. It is slightly slower than SNAT but handles IP
changes automatically.
Destination NAT (port forwarding)
To expose an internal web server (10.0.0.5:80) to the internet (192.0.2.10:80):
nft add chain inet nat prerouting { type nat hook prerouting priority -100 \; }
nft add rule inet nat prerouting iifname "eth1" tcp dport 80 dnat ip to 10.0.0.5:80
# Allow the forwarded traffic through the filter
nft add chain inet filter forward { type filter hook forward priority 0 \; policy drop \; }
nft add rule inet filter forward ct state established,related accept
nft add rule inet filter forward iifname "eth1" oifname "eth0" ip daddr 10.0.0.5 tcp dport 80 accept
The full chain: incoming traffic hits PREROUTING, gets DNAT’d to 10.0.0.5, then routes through the host, then the filter FORWARD chain accepts it, then it goes out eth0.
Note the ip keyword in dnat ip to. An inet table carries
both IPv4 and IPv6, so nft cannot infer the family of a bare
literal address and rejects the rule with specify 'dnat ip' or 'dnat ip6' in inet table to disambiguate. The snat rules
earlier in this lesson parse without it only because an ip saddr match has already pinned the family; write the keyword
anyway so a later reordering of matches cannot break the file.
Port forwarding specific ports
# Forward external port 2222 to internal port 22
nft add rule inet nat prerouting iifname "eth1" tcp dport 2222 dnat ip to 10.0.0.5:22
# Or to a different host's port
nft add rule inet nat prerouting iifname "eth1" tcp dport 8080 dnat ip to 10.0.0.10:80
Loopback NAT (hairpin)
Hairpin NAT is what you need when an internal client reaches an internal server by its public address - typically because DNS returns the same public name inside and outside. The packet enters the gateway on the internal interface and has to leave on the same internal interface.
It takes two rules, and neither of them is about replies.
# 1. DNAT the public address to the real backend, in PREROUTING
nft add rule inet nat prerouting iifname "eth0" ip daddr 192.0.2.10 \
tcp dport 80 dnat ip to 10.0.0.5:80
# 2. SNAT the same flow in POSTROUTING, so the reply comes back via the gateway
nft add rule inet nat postrouting iifname "eth0" oifname "eth0" \
ip saddr 10.0.0.0/24 ip daddr 10.0.0.5 tcp dport 80 masquerade
Why the second rule exists: after the DNAT, the packet still
carries the client’s own address, 10.0.0.20. The server at
10.0.0.5 is on the same subnet, so it replies directly to
10.0.0.20 rather than via the gateway. The client is waiting
for a reply from 192.0.2.10, receives one from 10.0.0.5, and
drops it as unrelated. Masquerading in postrouting makes the
request appear to come from the gateway, so the reply must return
through the gateway, where conntrack reverses both translations.
NAT and conntrack
NAT depends on conntrack. The first packet of a flow is matched in the NAT chain, the connection is added to conntrack, and subsequent packets use the existing entry without re-matching the NAT rules.
If conntrack is exhausted, NAT fails for new connections:
conntrack -S | grep drop
Drops in the conntrack statistics indicate table exhaustion.
Common NAT patterns
Egress NAT (internal hosts access internet):
table inet nat {
chain postrouting {
type nat hook postrouting priority 100;
oifname "eth1" ip saddr 10.0.0.0/24 snat to 192.0.2.10
}
}
Port forwarding:
table inet nat {
chain prerouting {
type nat hook prerouting priority -100;
tcp dport 80 dnat ip to 10.0.0.5
}
chain postrouting {
type nat hook postrouting priority 100;
# SNAT for replies to the forwarded traffic
ip saddr 10.0.0.5 oifname "eth1" snat to 192.0.2.10
}
}
Internal load balancer:
table inet nat {
chain prerouting {
type nat hook prerouting priority -100;
tcp dport 80 dnat ip to numgen random mod 2 map { 0 : 10.0.0.5, 1 : 10.0.0.6 }
}
}
This randomly load-balances incoming traffic between two backends.
NAT and filtering: order of evaluation
prerouting, input, forward and postrouting are different
hooks, not different priorities. A packet traverses them in a
fixed order set by its path through the network stack, and no
priority number changes that order. Priority orders multiple base
chains registered on the same hook — lower runs first — and
nothing else.
The distinction matters the first time you register two chains on one hook, which every host running both a NAT table and a filter table eventually does.
What the fixed path means in practice:
- DNAT happens in
prerouting(prioritydstnat, -100), which is before the routing decision and therefore beforeforwardorinput. - So your filter rules must match the post-DNAT destination —
the internal address. That is why the forward rule earlier in
this lesson matches
ip daddr 10.0.0.5, not the public192.0.2.10the client actually addressed. Writing the public address there is the classic “the DNAT works but nothing gets through” bug. - SNAT and masquerade happen in
postrouting(prioritysrcnat, 100), after filtering. Filter rules still see the original internal source address, so an egress rule matchingip saddr 10.0.0.0/24works as written.
Prefer the named priorities (dstnat, srcnat, filter) over
the bare numbers where your nft version accepts them: they say
what the chain is for, and they stay correct if the numeric values
ever shift.
Knowledge check
Knowledge check · 5 questions
Q1. What is the difference between SNAT and masquerade?
Q2. Port forwarding requires both PREROUTING (DNAT) and POSTROUTING (SNAT) in some cases.
Q3. Which of the following are valid NAT types? Select all that apply.
Q4. A ruleset passes `nft -c -f ruleset.nft` but fails to load with "Error: Could not process rule: Not supported" on a line containing `dnat` in the postrouting chain. What is the cause?
Q5. Internal clients on 10.0.0.0/24 reach a web server at 10.0.0.5 via the public address 192.0.2.10. DNAT in prerouting is in place, but the connections hang. What is the most likely cause?
Passing score: 75%. Answers are checked in this browser.