Sign in
← Networking: basics
12 min · medium · Lesson 10 of 13

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:

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

What you'll need for the tasks

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.

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.

Get access for $49

Tell the agent: use MCP izi, give me lesson networking-basics-11-nat