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.
| Chain | Fires | Used for |
|---|---|---|
PREROUTING | before routing, on arrival | DNAT, REDIRECT, destination rewriting |
OUTPUT | locally generated packets, before routing | DNAT for local traffic |
POSTROUTING | after routing, on the way out | SNAT, MASQUERADE, source rewriting |
INPUT | locally destined packets | rare, source rewriting for local delivery |
| Target | Rewrites | Chain |
|---|---|---|
SNAT --to-source | source address, fixed | POSTROUTING |
MASQUERADE | source address, taken from the outgoing interface | POSTROUTING |
DNAT --to-destination | destination address and port | PREROUTING, OUTPUT |
REDIRECT --to-ports | destination to a local port on this host | PREROUTING, OUTPUT |
NETMAP --to | a whole prefix, one to one | PREROUTING, POSTROUTING |
MASQUERADE --random | source, with randomised port choice | POSTROUTING |
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
| Choice | Pick it when |
|---|---|
MASQUERADE | the outgoing address can change, or you do not want to hardcode it |
SNAT | the 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, soFORWARDsees 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
| Symptom | Cause |
|---|---|
| Works from the internet, times out from the LAN | missing hairpin DNAT |
| DNAT added, still times out from the LAN | missing hairpin SNAT, replies bypass the router |
| Server logs show the router's address, not the client's | expected, hairpin SNAT hides the real source |
| Works from the LAN by internal address only | DNS 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.
| Check | Command |
|---|---|
| Rule is being hit | -t nat -L -n -v, watch the counters |
| Translation is correct | conntrack -L and read the reply tuple |
| Forwarding is on | sysctl net.ipv4.ip_forward |
| FORWARD is not dropping it | iptables -L FORWARD -n -v |
| Backend is actually listening | ss -ltnp on the backend host |
| Return path exists | the backend's default route must point at this router |
NAT rules that bite
| Gotcha | Detail |
|---|---|
| NAT is per connection | changing a rule does not affect flows already in conntrack, flush with conntrack -D |
| Only the first packet sees the nat table | a rule that matches on ESTABLISHED in nat never fires |
| DNAT before filtering | write FORWARD rules against the internal address |
| The backend needs a route back | asymmetric routing silently breaks DNAT |
MASQUERADE on a bridge | often wrong, bridged traffic is not routed |
| NAT and IPsec | translation breaks AH outright and needs NAT-T for ESP |
| Conntrack table size | a busy NAT box fills nf_conntrack_max and starts dropping |
| IPv6 | use routed prefixes, NAT66 exists but is a last resort |