Skip to main content
RunBook Academy

LinuxXXV · Firewallsnftables NAT

nftables NAT - source NAT, destination NAT, and masquerade

Intermediate⏱ ~12 minnftip

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

Not yet marked complete on this device.

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

TypeVerdict in nftValid only inUse case
SNATsnat to ADDRpostrouting, inputReplace source IP of outbound traffic with a fixed address
DNATdnat to ADDRprerouting, outputReplace destination IP of inbound traffic to redirect
MasquerademasqueradepostroutingLike SNAT, but the address is dynamic (e.g. DHCP uplink)
Redirectredirect to :PORTprerouting, outputDNAT 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.

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 (priority dstnat, -100), which is before the routing decision and therefore before forward or input.
  • 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 public 192.0.2.10 the 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 (priority srcnat, 100), after filtering. Filter rules still see the original internal source address, so an egress rule matching ip saddr 10.0.0.0/24 works 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

  1. Q1. What is the difference between SNAT and masquerade?

  2. Q2. Port forwarding requires both PREROUTING (DNAT) and POSTROUTING (SNAT) in some cases.

  3. Q3. Which of the following are valid NAT types? Select all that apply.

  4. 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?

  5. 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.