NAT: SNAT, DNAT, masquerade
How address translation works: getting a private network onto the internet, forwarding ports, and what NAT isn't.
A laptop at home, a server in the office, a container on a host: all of them have private addresses, and all of them reach the internet anyway. From outside, though, nobody can reach them until someone sets up port forwarding. NAT is behind both effects. This lesson covers how a gateway rewrites addresses, how SNAT differs from DNAT, and why NAT doesn't replace a firewall.
Why addresses get rewritten
Private addresses (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16) aren't routable on the internet. For a machine at 192.168.1.10 to reach an external site, something along the way has to rewrite its address to a public one. That rewrite is NAT (network address translation). This is how a whole office, or all the containers on a host, reach the outside through a single public IP.
Without the rewrite, the request might even reach the site, if the gateway simply forwards packets. But the reply never comes back: the site sends it to 192.168.1.10, and no such address exists on its network. From outside, this looks like a timeout.
The gateway keeps a translation table: which internal "address:port" it mapped to which external one. The reply packet arrives at the public address, the gateway finds the entry in the table, and returns the packet to the internal host.
How this works in Linux
NAT in Linux is handled by the same netfilter that does filtering (the lesson "Firewall and
routing"). The rewrite rules live in the nat table, and which chain they sit in matters:
POSTROUTING: the packet is already set to go out, the outgoing interface is known. This is where the sender's address gets changed (SNAT, MASQUERADE).PREROUTING: the packet just arrived, the route hasn't been chosen yet. This is where the destination address gets changed (DNAT), so the packet goes out to the new address.
The kernel's translation table is called conntrack (connection tracking). You can see it with
the conntrack tool from the conntrack-tools package. Each entry stores both sides of the
connection: how the packet looked when it was sent, and what reply the gateway expects back.
conntrack -L
# tcp 6 118 TIME_WAIT src=192.168.1.10 dst=198.51.100.7 sport=57688 dport=80 src=198.51.100.7 dst=203.0.113.5 sport=80 dport=57688 [ASSURED] ...
The first half of the line shows the original request: the internal host 192.168.1.10 reaches the
site 198.51.100.7. The second half shows the expected reply: the site answers at 203.0.113.5, the
gateway's external address. So the sender's address was rewritten. If there was no reply yet, the
line shows [UNREPLIED].
You check the rule counters like this:
iptables -t nat -L POSTROUTING -n -v
A growing pkts counter means the rule is firing. The counter grows only for a connection's first
packet: the kernel rewrites the rest based on the conntrack entry alone.
SNAT: inside to outside
SNAT (source NAT) rewrites the sender's address. The classic rule on a gateway with a fixed public IP:
iptables -t nat -A POSTROUTING -s 192.168.1.0/24 -o eth0 \
-j SNAT --to-source 203.0.113.5
Masquerade is the same SNAT, but the address isn't hardcoded: it takes whatever address the outgoing interface currently has. You need it when the public IP is dynamic (a home router, an ISP using DHCP):
iptables -t nat -A POSTROUTING -s 192.168.1.0/24 -o eth0 -j MASQUERADE
The -o eth0 matters here: it's the interface that faces outward. You can tell which of the
gateway's interfaces is external from ip addr and ip route. Mix it up, and the rule never fires.
Docker does the same thing for containers: it creates masquerading rules in the nat table for
bridge networks, which is why containers reach the internet without having public addresses. You
can turn this off for a specific network with
com.docker.network.bridge.enable_ip_masquerade=false.
DNAT: outside to inside
SNAT handles the outgoing direction, but you still can't get from outside to inside: as far as the outside world is concerned, internal addresses don't exist. For incoming traffic, you use DNAT (destination NAT): rewriting the destination address, also known as port forwarding.
# everything that arrives on the gateway's port 8080 goes to 192.168.1.10:80
iptables -t nat -A PREROUTING -p tcp --dport 8080 \
-j DNAT --to-destination 192.168.1.10:80
Without such a rule, a packet to the gateway's port 8080 is addressed to the gateway itself. If
nobody listens on that port on the gateway, the client gets Connection refused right away.
docker run -p 8080:80 works the same way: Docker adds a rule in the nat table mapping a host
port to a container port.
DNAT has a condition that's easy to forget: the internal server's reply must come back through the same gateway. Only the gateway remembers that the destination address was rewritten, and it will rewrite the sender's address back on the reply. If the server's default route bypasses the gateway, the client never gets a reply.
NAT is not a firewall
NAT often gets mistaken for protection: "you can't reach it from outside, it's behind NAT." The side effect looks similar, but the mechanism is different. NAT simply has no idea who to hand an unsolicited incoming packet to: it has no entry in the translation table. That's not a deny policy, it's a missing route.
A firewall makes "allow or deny" decisions based on rules. NAT rewrites addresses. One doesn't replace the other: a DNAT port forward opens up an internal service to the outside immediately, with no checks at all. And IPv6 has enough addresses for everyone, so NAT usually isn't used there, and a firewall becomes unavoidable.
Common mistakes
- Packet forwarding is enabled on the gateway (
net.ipv4.ip_forward=1), but SNAT or MASQUERADE wasn't added. The request goes out, the reply is lost, the client sees a timeout. - The MASQUERADE rule names the wrong interface in
-o. The rule exists, thepktscounter sits at zero. - DNAT ended up in
POSTROUTING, or SNAT inPREROUTING.iptableswon't accept that: DNAT and SNAT targets are only allowed in their own chains. - After DNAT, the server's reply bypasses the gateway. The entry in
conntrack -Lhangs with[UNREPLIED]. - NAT gets treated as protection. Any port forward opens a service to the outside, so access still needs to be restricted with firewall rules.
What you'll need for the tasks
docker exec <container> <command>: every lab command runs inside the lab environment's containers, nosudoneeded;ip addrandip route: which addresses the gateway has and which interface faces outward;iptables -t nat -A <chain> ...with theMASQUERADE,SNAT, andDNATtargets;iptables -t nat -L -n -v: thenattable's rules and their counters;conntrack -L: the translation table;wget -T 3 -q -O - <url>: a request from the client.
The next lesson covers IPv6 and SLAAC: addresses that don't need NAT.
Done reading?
Mark the lesson as read to move your track progress forward.
Lesson marked as done.
Go through it with an AI agent
The agent explains anything unclear in plain words, gives you 2 labs for this lesson and drills you on interview questions. $49 one-time, lifetime access.
Tell the agent: use MCP izi, give me lesson networking-basics-11-nat
Progress saved in this browser
Clear your history or open this on your phone, and the marks will be gone. Leave your email and they'll move to your account. No password needed.
Done. moved to your account. You can see your progress on any device. My progress