iptables & nftables Cheatsheet

NAT and Port Forwarding

Use this iptables & nftables reference while you build software engineering projects, review code, or refresh the syntax you reach for most.

The nat table

NAT rules live in the nat table and are consulted once per connection, on the first packet. Conntrack then applies the same translation to every later packet in that flow, in both directions, which is why there is never a reverse rule to write.

ChainFiresUsed for
PREROUTINGbefore routing, on arrivalDNAT, REDIRECT, destination rewriting
OUTPUTlocally generated packets, before routingDNAT for local traffic
POSTROUTINGafter routing, on the way outSNAT, MASQUERADE, source rewriting
INPUTlocally destined packetsrare, source rewriting for local delivery
TargetRewritesChain
SNAT --to-sourcesource address, fixedPOSTROUTING
MASQUERADEsource address, taken from the outgoing interfacePOSTROUTING
DNAT --to-destinationdestination address and portPREROUTING, OUTPUT
REDIRECT --to-portsdestination to a local port on this hostPREROUTING, OUTPUT
NETMAP --toa whole prefix, one to onePREROUTING, POSTROUTING
MASQUERADE --randomsource, with randomised port choicePOSTROUTING
sudo sysctl -w net.ipv4.ip_forward=1        # nothing routes without this
sudo iptables -t nat -L -n -v
sudo iptables -t nat -S

Source NAT, sharing one address

# dynamic public address, such as DHCP or PPP
sudo iptables -t nat -A POSTROUTING -o wan0 -s 10.0.0.0/24 -j MASQUERADE

# static public address, cheaper and more predictable
sudo iptables -t nat -A POSTROUTING -o wan0 -s 10.0.0.0/24 \
  -j SNAT --to-source 203.0.113.10

# a pool of public addresses, load spread across them
sudo iptables -t nat -A POSTROUTING -o wan0 -s 10.0.0.0/24 \
  -j SNAT --to-source 203.0.113.10-203.0.113.14

# keep the source port when possible, or force a range
sudo iptables -t nat -A POSTROUTING -o wan0 -j SNAT \
  --to-source 203.0.113.10:20000-30000
ChoicePick it when
MASQUERADEthe outgoing address can change, or you do not want to hardcode it
SNATthe address is static, it skips the per-packet interface lookup

MASQUERADE also drops conntrack entries when the interface goes down, which is correct for dial-up style links and a nuisance on a stable one. On a server with a fixed address, prefer SNAT.

Destination NAT, publishing a service

# forward a public port to an internal host
sudo iptables -t nat -A PREROUTING -i wan0 -p tcp --dport 443 \
  -j DNAT --to-destination 10.0.0.20:443

# change the port on the way in
sudo iptables -t nat -A PREROUTING -i wan0 -p tcp --dport 8443 \
  -j DNAT --to-destination 10.0.0.20:443

# a port range, mapped one to one
sudo iptables -t nat -A PREROUTING -i wan0 -p tcp --dport 5000:5010 \
  -j DNAT --to-destination 10.0.0.20

# spread across backends, round robin per connection
sudo iptables -t nat -A PREROUTING -i wan0 -p tcp --dport 80 \
  -j DNAT --to-destination 10.0.0.21-10.0.0.23:80

# the FORWARD rule is still required
sudo iptables -A FORWARD -i wan0 -o lan0 -p tcp -d 10.0.0.20 --dport 443 \
  -m conntrack --ctstate NEW -j ACCEPT
sudo iptables -A FORWARD -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT

DNAT happens in PREROUTING, so FORWARD sees the rewritten destination. Match -d 10.0.0.20 --dport 443, not the public address and port. Filtering on the published port is the single most common port-forwarding mistake.

Redirecting to a local port

REDIRECT is DNAT to this host, without naming an address.

# transparent proxy: outbound web traffic into a local proxy
sudo iptables -t nat -A PREROUTING -i lan0 -p tcp --dport 80 \
  -j REDIRECT --to-ports 3128

# let an unprivileged service answer on port 80
sudo iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to-ports 8080

# the same for traffic this host generates itself
sudo iptables -t nat -A OUTPUT -p tcp --dport 80 -o lo -j REDIRECT --to-ports 8080

PREROUTING never sees packets a local process sends to a local address, so a redirect you test with curl localhost needs the OUTPUT rule too. That asymmetry explains most "it works from another machine but not from here" reports.

Hairpin NAT

Internal clients reaching a published service by its public address need both a DNAT and a matching SNAT. Without the SNAT the reply goes straight from the server to the client, bypassing the router, and the client discards it because the source address does not match the connection it opened.

LAN=10.0.0.0/24
SRV=10.0.0.20
PUB=203.0.113.10

# rewrite the destination for internal clients too
sudo iptables -t nat -A PREROUTING -s $LAN -d $PUB -p tcp --dport 443 \
  -j DNAT --to-destination $SRV:443

# and rewrite the source so the reply comes back through the router
sudo iptables -t nat -A POSTROUTING -s $LAN -d $SRV -p tcp --dport 443 \
  -j SNAT --to-source 10.0.0.1
SymptomCause
Works from the internet, times out from the LANmissing hairpin DNAT
DNAT added, still times out from the LANmissing hairpin SNAT, replies bypass the router
Server logs show the router's address, not the client'sexpected, hairpin SNAT hides the real source
Works from the LAN by internal address onlyDNS is the simpler fix, split-horizon records

Split-horizon DNS (an internal zone answering with 10.0.0.20) avoids all of this and keeps client addresses visible in the server logs.

Verifying NAT

# is the rule matching at all
sudo iptables -t nat -L PREROUTING -n -v --line-numbers

# what conntrack thinks the translation is
sudo conntrack -L -p tcp --dport 443
sudo conntrack -E                 # live event stream, watch a connection form

# trace a packet through every table and chain
sudo modprobe nf_log_ipv4
sudo sysctl -w net.netfilter.nf_log.2=nf_log_ipv4
sudo iptables -t raw -A PREROUTING -p tcp --dport 443 -j TRACE
sudo dmesg -w | grep TRACE
# then remove it, TRACE is very loud
sudo iptables -t raw -D PREROUTING -p tcp --dport 443 -j TRACE
tcp 6 118 ESTABLISHED src=198.51.100.5 dst=203.0.113.10 sport=51422 dport=443
  src=10.0.0.20 dst=198.51.100.5 sport=443 dport=51422 [ASSURED]

The second line is the reply-direction tuple. Reading it left to right tells you exactly what the translation did, which beats guessing from the rules.

CheckCommand
Rule is being hit-t nat -L -n -v, watch the counters
Translation is correctconntrack -L and read the reply tuple
Forwarding is onsysctl net.ipv4.ip_forward
FORWARD is not dropping itiptables -L FORWARD -n -v
Backend is actually listeningss -ltnp on the backend host
Return path existsthe backend's default route must point at this router

NAT rules that bite

GotchaDetail
NAT is per connectionchanging a rule does not affect flows already in conntrack, flush with conntrack -D
Only the first packet sees the nat tablea rule that matches on ESTABLISHED in nat never fires
DNAT before filteringwrite FORWARD rules against the internal address
The backend needs a route backasymmetric routing silently breaks DNAT
MASQUERADE on a bridgeoften wrong, bridged traffic is not routed
NAT and IPsectranslation breaks AH outright and needs NAT-T for ESP
Conntrack table sizea busy NAT box fills nf_conntrack_max and starts dropping
IPv6use routed prefixes, NAT66 exists but is a last resort