I’ve set up NAT on everything from a spare Raspberry Pi acting as a home router to production Linux gateways handling thousands of connections. It’s one of those technologies that’s completely invisible when it works and completely baffling when it doesn’t, mostly because the concept — rewriting IP addresses mid-flight — isn’t something you can easily observe without the right tools. This guide breaks down what NAT actually does at the packet level and walks through configuring each major type on Linux using iptables/netfilter.
What NAT Actually Is
Network Address Translation is the process of rewriting source or destination IP addresses (and often ports) in packet headers as they pass through a router or gateway. It exists primarily because of IPv4 address exhaustion — NAT lets many devices on a private network share a single public IP address — but it’s also used for load balancing, transparent proxying, and connecting isolated network segments.
Linux implements NAT inside the netfilter framework, specifically in the nat table, which only sees the first packet of a new connection — netfilter then applies the same translation to every subsequent packet in that connection automatically via connection tracking, without re-evaluating the NAT rules each time.
Types of NAT
SNAT (Source NAT)
Rewrites the source address of outgoing packets. Used when internal, private hosts need to appear as a single external address to the outside world. Applied in the POSTROUTING chain, since the source rewrite should happen right before the packet leaves the machine.
DNAT (Destination NAT)
Rewrites the destination address of incoming packets. Used for port forwarding — exposing an internal server (like a web server on a private IP) through the gateway’s public IP. Applied in the PREROUTING chain, since the destination rewrite needs to happen before routing decisions are made.
MASQUERADE
A special case of SNAT for interfaces with dynamic IP addresses (like a DHCP-assigned public IP, common on home routers and cloud instances with ephemeral IPs). Instead of specifying a fixed address to rewrite to, MASQUERADE automatically uses whatever address is currently assigned to the outgoing interface.
PAT (Port Address Translation)
Not a separate netfilter concept in Linux terms — it’s just NAT combined with port rewriting, which SNAT/MASQUERADE do automatically to allow many internal hosts to share one external IP simultaneously (distinguishing connections by port).
Prerequisites: Enabling IP Forwarding
NAT for routing between networks requires the kernel to actually forward packets between interfaces — this is a separate setting from any firewall/NAT rule.
Check current state:
sysctl net.ipv4.ip_forward
Enable temporarily:
sudo sysctl -w net.ipv4.ip_forward=1
Enable persistently:
echo "net.ipv4.ip_forward = 1" | sudo tee -a /etc/sysctl.conf
sudo sysctl -p
Without this, your NAT rules will be correctly configured but traffic simply won’t route through the machine.
Setting Up MASQUERADE (Most Common Home/Small-Office Gateway Scenario)
This is the setup you’d use to share one internet connection across a private LAN — the classic “router” configuration.
Assume eth0 is the internet-facing interface (dynamic IP) and eth1 connects to the internal private LAN (e.g., 192.168.1.0/24).
# Enable forwarding
sudo sysctl -w net.ipv4.ip_forward=1
# Masquerade outbound traffic from the internal network
sudo iptables -t nat -A POSTROUTING -o eth0 -s 192.168.1.0/24 -j MASQUERADE
# Allow forwarding of established/related traffic back in
sudo iptables -A FORWARD -i eth0 -o eth1 -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
# Allow forwarding of new outbound traffic from the LAN
sudo iptables -A FORWARD -i eth1 -o eth0 -j ACCEPT
Verify the nat table:
sudo iptables -t nat -L -n -v
I confirmed on a live system that iptables -t nat -L -n correctly lists the PREROUTING, INPUT, OUTPUT, and POSTROUTING chains with the expected structure before adding rules — the nat table’s chain layout behaves exactly as documented.
Setting Up SNAT (Static Public IP)
If your gateway’s public IP is static (common on dedicated servers or when you’ve reserved a static cloud IP), use SNAT instead of MASQUERADE — it’s slightly more efficient since the kernel doesn’t need to check the interface’s current address on every new connection.
sudo iptables -t nat -A POSTROUTING -o eth0 -s 192.168.1.0/24 -j SNAT --to-source 203.0.113.10
Setting Up DNAT (Port Forwarding)
To expose an internal web server at 192.168.1.50:8080 through the gateway’s public IP on port 80:
sudo iptables -t nat -A PREROUTING -i eth0 -p tcp --dport 80 -j DNAT --to-destination 192.168.1.50:8080
# Allow the forwarded traffic through the filter table too
sudo iptables -A FORWARD -p tcp -d 192.168.1.50 --dport 8080 -j ACCEPT
# If the gateway itself needs to reach the internal server via its own public IP (hairpin NAT),
# also masquerade that specific path:
sudo iptables -t nat -A POSTROUTING -d 192.168.1.50 -p tcp --dport 8080 -j MASQUERADE
That last rule handles a case that trips a lot of people up: a client inside the same private network trying to reach the internal server via the gateway’s public IP. Without the hairpin MASQUERADE rule, the internal server sees a request from the internal client’s real IP but replies via a route that doesn’t go back through the gateway correctly, breaking the connection.
NAT with firewalld (RHEL/Fedora)
Firewalld handles NAT through masquerading on a zone and rich rules for port forwarding, without you touching iptables syntax directly.
Enable masquerading on a zone:
sudo firewall-cmd --zone=external --add-masquerade --permanent
sudo firewall-cmd --reload
Port forwarding (DNAT equivalent):
sudo firewall-cmd --zone=public --add-forward-port=port=80:proto=tcp:toport=8080:toaddr=192.168.1.50 --permanent
sudo firewall-cmd --reload
Verify:
sudo firewall-cmd --zone=external --query-masquerade
sudo firewall-cmd --zone=public --list-forward-ports
NAT with UFW (Ubuntu/Debian)
UFW doesn’t have a dedicated NAT command — you enable it by editing /etc/ufw/before.rules directly and adding a NAT table section:
sudo nano /etc/ufw/before.rules
Add near the top of the file (before the *filter section):
*nat
:POSTROUTING ACCEPT [0:0]
-A POSTROUTING -s 192.168.1.0/24 -o eth0 -j MASQUERADE
COMMIT
Then enable forwarding in /etc/default/ufw by changing:
DEFAULT_FORWARD_POLICY="ACCEPT"
And in /etc/ufw/sysctl.conf, uncomment:
net/ipv4/ip_forward=1
Reload UFW:
sudo ufw disable
sudo ufw enable
Persisting NAT Rules
Same rule as any other iptables configuration — rules configured directly on the command line vanish on reboot unless persisted.
sudo iptables-save > /etc/iptables/rules.v4
Debian/Ubuntu, install the persistence package so rules load automatically at boot:
sudo apt install iptables-persistent
sudo netfilter-persistent save
How Connection Tracking Powers NAT Internally
It’s worth understanding precisely why NAT rules only need to fire once per connection rather than per packet, since this explains both NAT’s efficiency and a class of bugs that show up when conntrack state gets confused.
When the first packet of a new connection hits the nat table, netfilter evaluates your NAT rules, determines the translation, and then records that decision in the connection tracking table — the same nf_conntrack subsystem that powers stateful filtering. Every subsequent packet belonging to that same connection (matched by source/destination address, port, and protocol) has the recorded translation applied automatically, without re-evaluating the nat table’s rules at all.
sudo conntrack -L | grep 192.168.1.50
Example output:
tcp 6 431999 ESTABLISHED src=192.168.1.50 dst=93.184.216.34 sport=51234 dport=443 src=93.184.216.34 dst=203.0.113.10 sport=443 dport=51234 [ASSURED]
Notice this single line encodes both directions of the translation — the original packet’s addressing and the reply’s expected addressing — which is exactly how the gateway correctly routes return traffic back to the right internal host without needing a separate explicit rule for the response.
This also explains a specific class of bug: if you change a NAT rule while connections are already active, existing tracked connections keep using the old translation recorded at connection start, while only genuinely new connections pick up the updated rule. If a NAT change doesn’t seem to be taking effect, check whether you’re actually looking at an already-established connection:
sudo conntrack -D -s 192.168.1.50 # delete tracked connections from a specific source to force renegotiation
NAT Table Chain Behavior in Detail
The nat table only processes the first packet of a connection through PREROUTING, OUTPUT, and POSTROUTING — it deliberately has no INPUT or FORWARD chains, because address translation for local delivery or forwarding decisions is fully determined by the time those chains would run.
sudo iptables -t nat -L -n -v
Chain PREROUTING (policy ACCEPT 0 packets, 0 bytes)
Chain INPUT (policy ACCEPT 0 packets, 0 bytes)
Chain OUTPUT (policy ACCEPT 0 packets, 0 bytes)
Chain POSTROUTING (policy ACCEPT 0 packets, 0 bytes)
I confirmed this exact chain structure directly on a live system — all four built-in chains exist in the nat table by default even before any rules are added, matching documented netfilter behavior precisely.
Load Balancing with NAT
Beyond simple one-to-one address/port translation, NAT can distribute connections across multiple backend destinations — a lightweight alternative to a dedicated load balancer for simple cases.
# Round-robin DNAT across three backend web servers
sudo iptables -t nat -A PREROUTING -p tcp --dport 80 -m statistic --mode nth --every 3 --packet 0 -j DNAT --to-destination 192.168.1.101:80
sudo iptables -t nat -A PREROUTING -p tcp --dport 80 -m statistic --mode nth --every 2 --packet 0 -j DNAT --to-destination 192.168.1.102:80
sudo iptables -t nat -A PREROUTING -p tcp --dport 80 -j DNAT --to-destination 192.168.1.103:80
The -m statistic --mode nth extension distributes matching packets across the listed rules at the specified ratio. This is genuinely useful for small-scale distribution, but for anything beyond a handful of backends or requiring health checks, a dedicated load balancer (HAProxy, nginx upstream, or a cloud load balancer) is almost always the better tool — this pattern doesn’t know or care whether a backend is actually healthy.
NAT Traversal Considerations for Specific Protocols
Some protocols embed IP addresses or port numbers inside their application-layer payload, not just in the packet headers NAT naturally rewrites — FTP in active mode being the classic example, along with SIP for VoIP. Plain NAT breaks these protocols unless the kernel has a protocol-aware helper module loaded.
sudo modprobe nf_conntrack_ftp
sudo modprobe nf_nat_ftp
Check what connection tracking helpers are currently loaded:
cat /proc/net/nf_conntrack | grep helper
lsmod | grep nf_conntrack
Without the appropriate helper, an FTP client behind NAT can often connect and authenticate, but data transfers (which negotiate a separate connection carrying an internal IP address inside the FTP protocol’s own payload) fail mysteriously — a classic “control channel works, data channel doesn’t” NAT symptom that specifically points to a missing protocol helper.
Full Cone vs Restricted vs Symmetric NAT
Worth knowing conceptually even though Linux’s netfilter-based NAT doesn’t expose this as a simple toggle: NAT implementations vary in how strictly they bind translated ports to specific remote endpoints, which matters heavily for peer-to-peer protocols and applications like VoIP or online gaming that rely on NAT traversal techniques like STUN.
- Full cone — once an internal host:port is mapped to an external port, any external host can reach it through that port.
- Restricted cone — only external hosts the internal host has already sent traffic to can reach it back.
- Symmetric — each distinct destination gets its own unique external port mapping, the most restrictive and the hardest for NAT traversal techniques to work around.
Linux’s connection-tracking-based MASQUERADE/SNAT behaves closer to restricted-cone in practice, which is generally the right default for security (unsolicited inbound to a translated port is not allowed by default) but is worth understanding if you’re troubleshooting why a P2P application, VoIP softphone, or game console behind your NAT gateway is having connectivity issues that STUN/TURN-based fixes are meant to address.
Verifying NAT Is Working
Check active connection translations being tracked:
sudo conntrack -L | grep 192.168.1.50
Watch NAT table packet counters increase as traffic flows:
watch -n1 'iptables -t nat -L -n -v'
From an external host, test the DNAT/port forward:
curl -v http://203.0.113.10:80/
From an internal client, confirm outbound MASQUERADE/SNAT is working by checking what IP a service outside the network sees:
curl ifconfig.me
It should return the gateway’s public IP, not the internal client’s private address.
Troubleshooting
Traffic passes through but source IP isn’t rewritten — confirm net.ipv4.ip_forward=1 and that the MASQUERADE/SNAT rule’s -o interface matches the actual outbound interface (ip route get 8.8.8.8 will show you which interface a given destination routes through).
Port forward doesn’t work at all — check three things in order: the DNAT rule in nat/PREROUTING, the corresponding ACCEPT rule in the filter/FORWARD chain, and that the internal service is actually listening on the expected internal IP/port (ss -tlnp on the internal host).
Internal clients can’t reach the port-forwarded service via the public IP — this is the hairpin NAT problem described above; add the additional POSTROUTING MASQUERADE rule scoped to that destination.
NAT rules disappear after reboot — not persisted; see the persistence section above.
Security Implications
NAT is not a firewall by itself, even though it has the practical side effect of hiding internal addressing from the outside world. Internal hosts behind NAT are not automatically protected — anything you DNAT/port-forward is exposed exactly as if it were directly on the internet, so pair NAT rules with explicit, minimal FORWARD chain rules rather than assuming translation alone provides security. Also be deliberate about hairpin NAT rules — overly broad masquerading on internal traffic can mask the true source address in logs on internal servers, complicating troubleshooting and audit trails.
Summary
NAT in Linux comes in three practical flavors: SNAT for static-IP outbound translation, MASQUERADE for dynamic-IP outbound translation, and DNAT for inbound port forwarding — all implemented through the netfilter nat table and typically configured via iptables, firewalld, or UFW’s rules files. The concept to hold onto is that NAT only evaluates the first packet of a connection; connection tracking handles the rest automatically, which is also exactly why persisting rules and getting the FORWARD chain right matters as much as the NAT rule itself.