NAT: Explanation & Insights

Rewrite the address on the envelope, remember where the reply goes, and let a whole house share one street number.

What It Is

NAT — Network Address Translation — is the trick that lets a roomful of machines, each with its own private address, talk to the internet through a single public one. As a packet crosses the boundary between your private network and the outside world, the NAT device rewrites the IP addresses (and usually the TCP/UDP port numbers) in the header, then rewrites them back on the reply. Your laptop thinks it's talking straight to example.com; example.com thinks it's talking to your router. Neither knows the translation is happening. That sleight of hand is going on right now between you and this page.

The reason any of this exists is a shortage. An IPv4 address is 32 bits — about 4.3 billion possible values, which sounded infinite in 1981 and ran out in the 2010s. There was never going to be a public address for every phone, fridge, and virtual machine on Earth. So the IETF carved out three ranges that are deliberately non-routable on the public internet — anyone can use them internally, nobody can reach them from outside:

10.0.0.0/8        10.0.0.0     – 10.255.255.255    (16.7M addresses)
172.16.0.0/12     172.16.0.0   – 172.31.255.255    (1M addresses)
192.168.0.0/16    192.168.0.0  – 192.168.255.255   (65K addresses)

That 192.168.1.1 your home router lives on, the 10.0.x.x your cloud VM boots with — those are private addresses, reused by millions of networks simultaneously, and NAT is the machinery that lets them all share the small pool of real, globally-unique public addresses sitting at the edge. One public IP, hundreds of private machines behind it, and a router in the middle keeping a notebook of who's talking to whom.

How It Works — The One Thing to Nail

Here is the concept every tutorial leaves muddy, and the one this page exists to make permanent: NAT is why your server can reach out but the world can't reach in by default.

Picture your server opening a connection to example.com:443. The packet leaves with a source of 10.0.0.5:51000 and a destination of 93.184.216.34:443. The NAT box intercepts it, swaps the source for its own public address and a port it picks — say 203.0.113.7:62000 — and forwards it on. Crucially, before it lets the packet go, it writes a line in a table:

inside              translated           remote
10.0.0.5:51000  ->  203.0.113.7:62000 -> 93.184.216.34:443

This is the connection-tracking table (the kernel calls it conntrack), and it is the whole game. When example.com replies to 203.0.113.7:62000, the NAT box looks up that line, sees the reply belongs to your server's conversation, rewrites the destination back to 10.0.0.5:51000, and hands it inside. Outbound just works because the act of going out created the return path. The translation remembers itself.

Now flip it around. A stranger on the internet sends a fresh packet to 203.0.113.7:62000 out of the blue. The NAT box checks its table — and there's no matching line, because nobody inside started this conversation. The packet has nowhere to go. It gets dropped. There is no entry, so there is no inside host to deliver to. Inbound from the cold doesn't work, not because of a deliberate "deny" rule, but because translation is fundamentally a memory of connections you started — and the stranger isn't in the memory.

Note

This asymmetry is the single most useful thing to internalize about NAT. Outbound needs nothing — the table fills itself. Inbound needs you to pre-arrange a translation, or there is simply nobody home to forward to. Every "my service is running but nothing can connect" mystery lives in this gap.

So translation comes in two directions, and they are not symmetric:

  • Source NAT (SNAT / masquerading) — the outbound case above. The source address gets rewritten as packets leave. When the public address is fixed you use SNAT; when it might change (a dynamic ISP lease, a failover interface) you use masquerade, which grabs whatever the outgoing interface's address happens to be at send time. This is what your home router does, what a cloud VPC's NAT gateway does, what a firewall on a bastion host does. Many machines, one public face.
  • Destination NAT (DNAT / port forwarding) — the inbound case. You tell the NAT box, in advance, "any packet arriving on my public address at port 443, rewrite its destination to 10.0.0.5:443 and forward it inside." Now there is a table entry waiting, and the stranger's packet has a home. This is how a service behind NAT becomes reachable at all.

Because many inside hosts share one outside address, plain one-to-one NAT isn't enough — you'd run out of public addresses immediately. The fix is to translate the port too, so 10.0.0.5:51000 and 10.0.0.6:51000 map to the same public IP on two different public ports. That's PAT (Port Address Translation), also called NAT overload, and it's what virtually every NAT you'll meet actually does. When people say "NAT" they almost always mean PAT.

Why It Matters For Your Server

If you run a server, NAT is either invisibly helping you or quietly blocking you, and knowing which is the difference between a five-minute fix and an afternoon of confusion.

The classic trap: you deploy a service on a cloud box, start it, watch the logs say listening on 0.0.0.0:8080, then try to hit it from your laptop and get nothing. Connection times out. The service is up. The process is bound. And nothing connects. This is almost never a bug in your app — it's a missing inbound translation somewhere on the path.

The debugging order that saves you:

  1. Confirm the service is actually listening, on the right address. Run ss: ss -ltnp shows every listening TCP socket with its process. If your service shows 127.0.0.1:8080 it's bound to loopback only — the outside world can't reach it no matter how the NAT is set up. You want 0.0.0.0:8080 (all interfaces) or the specific private IP. This is a ss question with a one-line answer, and it's the right first step because it's the cheapest.
  2. Check the host firewall. iptables or nft may be dropping the inbound packet before it ever reaches your socket. iptables -L -n or nft list ruleset shows the rules.
  3. Check the provider's NAT / security-group layer. On a cloud server there are two boundaries: the host's own firewall, and the provider's network layer (AWS security groups, GCP firewall rules, a VPC NAT gateway, a load balancer). A security group is, functionally, a DNAT-plus-filter you configure in a web console instead of with iptables. Inbound can be blocked at either layer, and the symptom is identical, so you check both.

Pro Tip

When inbound to a cloud server fails, my reflex is two checks in parallel: ss -ltnp on the box to prove the service is listening on the right interface, and the provider's security-group / firewall console to prove the port is actually allowed in. Nine times out of ten the answer is "bound to 127.0.0.1" or "port not open in the security group" — and you've found it before you ever touch a NAT rule.

And one piece of direction I'll plant firmly: avoid double-NAT if you possibly can. When traffic is translated twice — say a cloud VPC NAT and a software router you set up inside it, or a home connection where the ISP already NATs and your own router NATs again (carrier-grade NAT, "CGNAT") — the conntrack tables stack, port forwarding has to be threaded through both layers, and a network failure becomes maddening to trace because the address on the wire bears no resemblance to either endpoint. One translation is a helpful abstraction. Two is a debugging tax you pay forever. Flatten the path when the design lets you.

How I Inspect It On Linux

Linux does NAT in the kernel via netfilter, and you drive it with iptables (older) or nft (the modern replacement). For NAT to do anything, the kernel must first be willing to forward packets between interfaces — off by default, since a normal server isn't a router:

# Turn the box into a router (persist in /etc/sysctl.conf)
sysctl -w net.ipv4.ip_forward=1
# SNAT / masquerade: let everything behind this box reach the internet
# (eth0 = public-facing interface)
iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE

# DNAT / port-forward: send public :443 to an inside host
iptables -t nat -A PREROUTING -i eth0 -p tcp --dport 443 \
         -j DNAT --to-destination 10.0.0.5:443

The two chains tell the story by when they run. PREROUTING rewrites the destination the instant a packet arrives, before the kernel decides where it's headed — that's DNAT, the inbound rewrite. POSTROUTING rewrites the source on the way out the door, after routing is decided — that's SNAT/masquerade, the outbound rewrite. Inbound first, outbound last; translation bookending the routing decision.

To watch the conntrack table — the live notebook of every translated conversation:

# List active tracked connections (install conntrack-tools if missing)
conntrack -L

# Or peek at the raw kernel table
cat /proc/net/nf_conntrack
tcp  6 431999 ESTABLISHED src=10.0.0.5 dst=93.184.216.34 sport=51000 dport=443 \
        src=93.184.216.34 dst=203.0.113.7 sport=443 dport=62000 [ASSURED]

Read that as two views of one connection: the original tuple (what your inside host sent) and the reply tuple (what the outside world sends back, addressed to the translated public IP and port). The NAT box flips between them on every packet. The 431999 is the seconds left on this entry's lease — idle connections eventually expire and the table line evaporates, which is why a long-idle SSH session sometimes dies silently behind aggressive NAT.

To inspect the rules themselves: iptables -t nat -L -n -v lists the NAT table with packet counters (the counters are gold — a port-forward rule with zero hits means traffic isn't even arriving), and on nft it's nft list table nat.

Cheat Sheet

sysctl -w net.ipv4.ip_forward=1            # enable forwarding (be a router)
iptables -t nat -L -n -v                   # list NAT rules + hit counters
iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE          # SNAT outbound
iptables -t nat -A PREROUTING -i eth0 -p tcp --dport 80 \
         -j DNAT --to-destination 10.0.0.5:80                 # DNAT inbound
conntrack -L                               # live translated-connection table
conntrack -L | wc -l                       # how many tracked connections
cat /proc/sys/net/netfilter/nf_conntrack_max   # table size limit
ss -ltnp                                   # what's actually LISTENING (step 1!)
nft list table nat                         # same, nftables syntax

Warning

The conntrack table has a fixed maximum (nf_conntrack_max). A busy NAT box — or one under a flood of connections — can fill it, and once full, new connections are silently dropped while existing ones keep working. The symptom is brutal to diagnose: the network "works" for established sessions but refuses anything new. Watch dmesg for nf_conntrack: table full, dropping packet and raise the limit before it bites.

History and Philosophy

NAT was born as a hack, and a slightly embarrassing one. The internet's designers built it on the principle of end-to-end connectivity — every host a first-class citizen with its own globally-reachable address, any machine able to talk to any other. NAT breaks that principle on purpose: hosts behind it are second-class, reachable only if they speak first. Purists hated it. RFC 1631 in 1994 introduced it as a "short-term solution" to IPv4 exhaustion explicitly while the real fix — IPv6, with its 128-bit addresses, enough to give every atom on Earth's surface a few — was being readied.

The short-term solution is now thirty years old and running most of the planet. NAT worked too well: by letting one address serve a whole network, it took the pressure off IPv4 exhaustion so thoroughly that it bought the old protocol decades of extra life and, ironically, slowed IPv6 adoption to a crawl. IPv6 ends the need for NAT entirely — every device gets a real public address again, end-to-end connectivity restored — yet here we still are, masquerading, because the stopgap turned out to be load-bearing. The thing that protocols often miss is that "temporary" is the most permanent word in engineering.

There's a side effect nobody designed but everybody relies on: because inbound-from-cold simply doesn't work behind NAT, every home network and most server fleets got a crude, accidental firewall for free. It was never meant as security — and it isn't real security, since a single misconfigured port-forward or a compromised inside host blows it open — but "the world can't reach in by default" has quietly protected millions of machines that had no business being exposed. A bug that became a feature, then became an expectation. Lean on a real firewall for actual defense; treat NAT's reticence as a happy bonus, not a wall.

See Also

  • protocol — the rules NAT rewrites the headers of
  • OSI model — NAT operates at layers 3 and 4 (IP and ports)
  • TCP — the connections conntrack tracks, and the ports it translates
  • UDP — the connectionless cousin NAT must fake state for
  • ICMP — has no ports, so NAT tracks it by query ID instead
  • firewall — netfilter, where Linux does both NAT and filtering
  • DHCP — hands out the private addresses NAT then translates
  • NIC — the interfaces (eth0, eth1) packets cross to get translated
  • iptables — the classic tool for SNAT, DNAT, and masquerade rules
  • nft — the modern netfilter front-end that replaces it
  • ip — inspect addresses and routes on each interface
  • ss — prove a service is listening before you blame the NAT
  • network failure — the broader "can't connect" diagnosis

Service running fine but nothing can connect to it?

CleverUptime watches your endpoints from the outside and your server from the inside at once, so when a port stops answering you can see in a glance whether the service is even listening or the door upstream is shut — no more guessing which side of the NAT swallowed the packet.

Want to see your own server's health right now? One command, no signup, no install.

Check your server →