Skip to content

Start typing to search

Network Segmentation Guide: VLANs, Layers, and Access

What Is NAT? SNAT, DNAT, and the Translation Table

What NAT really does, how SNAT and DNAT differ, how PAT reuses one address, why double NAT and CGNAT break port forwarding, and a measured look at hairpin NAT.

Written by

NAT does two jobs, and the second is skipped in most explanations: it changes the source address of an outbound packet, and then it writes that change into a ledger. Without the ledger, translation would be a one-way trick; when the reply came back, no one would know who had asked for it, and the packet would die at the edge. Understanding NAT is not knowing that the address changes. It is knowing what that ledger holds.

Advertisement

In the network segmentation guide we compared the edge of a network to passport control: inside your own country nobody asks for your papers at every step, but at the border your local identity isn’t enough and you’re handed a passport that works abroad. That telling stood on the traveler’s side of the border. This article walks into the booth, because that is where the real mechanism sits: in the ledger open on the officer’s desk.

Below, we actually open that ledger. Every command and every output here was taken in our own lab; none of it is carried over from anywhere else. On a single Linux machine we stood up a small NAT gateway, captured both sides of the translation at the same moment, printed the translation table itself, and measured how two connections leaving from the same inside port get told apart on the way out. At the end there is a fresh measurement of hairpin NAT, the case that most often gets diagnosed wrong.

What Is NAT? The Two Halves of Address Translation

NAT (Network Address Translation) is the mechanism that changes address information on a device a packet passes through and keeps that change in a table. The changing part is easy; the keeping-a-table part is what makes NAT what it is.

Picture the officer at the border. The job isn’t only to hand you a passport. The moment it’s handed over, a line goes into a ledger: “with this passport, at this time, this citizen left.” When an answer comes back from outside, the officer looks at that ledger and finds who the answer belongs to. Close the ledger and the passport is useless: people can walk out, but none of them can come back.

We stay in this scene for the whole article. The correspondences, which we open one by one:

At the borderOn the network
Local identityPrivate IP address
PassportPublic IP address
The officer’s ledgerThe NAT table, i.e. connection tracking
Sequence number written in the ledgerTranslated source port (PAT)
Identity changing on the way outSNAT
A visitor saying “I’m here to see this person”DNAT, i.e. port forwarding
A second passport in a transit countryDouble NAT
No line in the ledger for that person“Server is up but unreachable from outside”

What Does NAT Stand For?

NAT stands for Network Address Translation. In a device interface or a document, seeing NAT is never ambiguous; the context makes it clear. Searching for the bare three letters is another matter, because the same acronym carries unrelated meanings in other fields; spelling out network address translation narrows results to the right area.

Two of the Five-Tuple Change

What identifies a TCP or UDP connection is not a single address but a tuple of five pieces of information: source address, source port, destination address, destination port, and protocol. Why that tuple identifies a connection rather than a person is something we cover in the port guide.

What NAT does is change not the whole tuple but two of its parts: the source side or the destination side. Which of the two it changes is the entire distinction between SNAT and DNAT that we’re about to reach.

The subtlety is this: the moment the tuple changes, the connection becomes a different connection for the party on the outside. The far end never sees your inside address, and can’t. The only thing it knows is the number on the passport.

Why Does NAT Exist? IPv4 Scarcity and Private Address Blocks

NAT was not designed as a security feature; it was designed as an answer to scarcity. The IPv4 address space holds about 4.3 billion addresses, and that is not large enough to give every device one.

The solution came in two parts. First, blocks that anyone can use freely but that are never routed on the internet were set aside: 10.0.0.0/8, 172.16.0.0/12, and 192.168.0.0/16. Second, those addresses had to be turned into a real address on the way out. That turning is NAT.

Put back on the metaphor, the picture sharpens: a local identity is valid inside a country and the same numbers can exist for everyone, because there is no international registry to compare them against. Crossing the border needs a document that is unique in the world. NAT is the translator joining those two worlds.

There is a side effect, and most of the rest of this article wrestles with it: the outside no longer knows you individually. Sometimes that is exactly what you want, and sometimes it is exactly the problem you are trying to solve.

What Are the Types of NAT? Static, Dynamic, and PAT

Address translation isn’t one thing; it’s the shared name of three different setups. What separates them is simple: how many inside addresses map to how many outside addresses, and is the mapping permanent?

TypeMappingWhen it’s used
Static NATOne inside address, always the same outside address. Permanent, written by handWhen an inside server has to be reachable from outside
Dynamic NATA free address is handed out from a pool; it returns to the pool when the connection endsWhen you have a pool of outside addresses and fewer inside devices than the pool
PATOne outside address, thousands of connections separated by portVery nearly every home and office network today

The border-booth equivalents are clean too. Static NAT is a passport number reserved for one person and never changing. Dynamic NAT hands out a free number from a pool at the desk and takes it back when the person returns. PAT hands the same passport to everyone and writes a sequence number beside it; the rest of the article is largely about this one, because it’s the setup you’ll meet in the field.

There is a fourth distinction, and it gets mistaken for a NAT type: how strictly the setup filters packets arriving from outside. Some setups, once you’ve connected to an address, accept every packet from that address; others accept only what comes from exactly the address and port you were talking to and drop the rest. The second is more secure, but in scenarios where both ends are behind NAT (video calls, gaming, peer-to-peer) it makes the connection harder to establish. This is why the same line runs smoothly for some applications and stalls on others; the problem isn’t bandwidth, it’s the filtering behavior at the edge.

What Is the NAT Table? The Ledger Where Translation Is Kept

Now let’s open the ledger. Every measurement below was taken on a real machine; both the commands and the outputs stand as they were.

Lab: Two Separate Networks on One Machine

Seeing a NAT gateway normally takes three things: an inside network, an outside network, and a device sitting between them. We can build all three inside a single Linux machine. Before the commands, three concepts, because without them the command lines look meaningless.

A network namespace is a separate network environment inside the machine. On Linux everything normally shares one network stack: the same interfaces, the same addresses, the same routing table. A namespace lets you split that. Each namespace you create has its own interfaces, its own IP address, and its own routing table, unaware of the rest of the machine. In practice it behaves like a small virtual machine, but without a separate operating system or a separate block of memory. We use namespaces in place of the devices on the inside network.

A veth pair is a virtual cable with two ends. A packet entering one end comes out the other. We put one end inside a namespace and the other on the machine itself. It’s the software equivalent of a physical cable, the thing that connects an inside device to the gateway.

The gateway is the machine itself. A virtual server running Ubuntu 24.04; both the door for the two inside networks and the side that leaves for the outside. It goes out through the ens33 interface with the address 192.168.1.15.

The setup, in a table:

RoleWhere it livesAddressWhat it stands in for
ic1Namespace10.10.1.2First device on the inside network
ic2Namespace10.10.2.2Second device on the inside network
Gateway, inside facesThe machine itself10.10.1.1 and 10.10.2.1The devices’ default gateway
Gateway, outside faceThe machine’s ens33192.168.1.15The single address the outside sees
TargetA separate server192.168.1.10Standing in for “the outside world”

Instead of an address on the internet, we use a server on our own network as the target. For the measurement it makes no difference: what matters for NAT is that the packet leaves the gateway and the reply comes back, not where it goes after that. We make the connection to port 389, the LDAP service, because that port is already open, so there’s a real party answering.

Now the setup. First the inside network: open a namespace and put one end of the virtual cable inside it.

Bash
ip netns add ic1
ip link add veth-gw1 type veth peer name veth-ic1
ip link set veth-ic1 netns ic1

The three lines, in order: an isolated network environment named ic1 is opened, a two-ended virtual cable named veth-gw1 and veth-ic1 is created, then the veth-ic1 end of the cable is moved into ic1. From here on one end of the cable is on the machine, the other on the inside network.

Next, addressing. On the gateway end of the cable we write the gateway’s address; on the namespace end, the device’s address.

Bash
ip addr add 10.10.1.1/24 dev veth-gw1
ip link set veth-gw1 up

ip netns exec ic1 ip addr add 10.10.1.2/24 dev veth-ic1
ip netns exec ic1 ip link set veth-ic1 up
ip netns exec ic1 ip route add default via 10.10.1.1

The ip netns exec ic1 prefix means “run this command inside ic1”; without it the command would apply to the machine’s own network environment. The last line points the inside device at its default gateway: hand every packet you don’t know where to send to 10.10.1.1.

Two things left. Let the machine take packets in on one interface and pass them out another, and tell it to translate the address on the way out.

Bash
sysctl -w net.ipv4.ip_forward=1
iptables -t nat -A POSTROUTING -s 10.10.0.0/16 -o ens33 -j MASQUERADE

Without the first line the machine won’t move an incoming packet to another interface; it silently drops traffic that isn’t its own. The second line is the one doing all the work, and it deserves reading piece by piece:

  • -t nat: write the rule into the NAT table, not the ordinary firewall table.
  • POSTROUTING: apply it just before the packet leaves the machine.
  • -s 10.10.0.0/16: touch only packets from the inside networks.
  • -o ens33: touch only what leaves through the outside interface.
  • MASQUERADE: replace the source address with the address of the interface it leaves through.

That last line is the passport desk itself. One note: on Ubuntu 24.04 the iptables command actually drives the nftables engine underneath, so the rule you write goes to the new backend, not the old one; the syntax and behavior are unchanged, so it doesn’t affect this measurement.

First, Without NAT: The Packet Leaves but Doesn’t Return

The standard line when this is taught in the field is: without NAT, the packet can’t get out. We set the experiment up precisely to test that sentence.

Three steps. We delete the translation rule we just wrote (-D instead of -A, remove instead of add). We put the outside interface into listening mode so we can see what actually leaves the machine. Then we try to connect from the inside network to the target.

Bash
iptables -t nat -D POSTROUTING -s 10.10.0.0/16 -o ens33 -j MASQUERADE
tcpdump -i ens33 -nn "host 192.168.1.10 and port 389"
ip netns exec ic1 nc -p 40000 -w 3 192.168.1.10 389

The middle tcpdump line listens on ens33; -nn prints addresses and ports raw, without resolving names. The third line’s nc is the side making the connection: -p 40000 fixes the source port (we’ll see shortly why that matters), -w 3 tells it to give up after three seconds.

What shows up on the outside interface:

21:58:31.153681 IP 10.10.1.2.40000 > 192.168.1.10.389: Flags [S], seq 316966271
21:58:32.172475 IP 10.10.1.2.40000 > 192.168.1.10.389: Flags [S], seq 316966271
21:58:33.196472 IP 10.10.1.2.40000 > 192.168.1.10.389: Flags [S], seq 316966271

The standard line didn’t hold. The packet left, three times over. The source address reads 10.10.1.2, so the private address was carried onto the outside interface as-is.

What’s instructive isn’t the lines that appear, it’s the ones that don’t: there’s no reply. The same packet repeating three times is the proof. The Flags [S] at the end of each line is TCP’s connection-opening SYN packet; with no acknowledgement from the far side, the same request is retried. How a connection is established and how this three-way handshake works step by step is covered in the port guide. The target server received the packet and tried to send its answer to 10.10.1.2; that address isn’t on its network, so the reply reached nowhere.

So the problem NAT solves isn’t “getting the packet out.” The packet already gets out. What NAT solves is letting the answer come back. If no passport were issued at the border, people could still walk out; they couldn’t return, because no one would take them back in.

Seeing the Translation From Both Sides at Once

Now we put the rule back and listen to the same connection on the inside cable and on the outside interface at the same time.

On the inside, i.e. before translation:

21:58:35.240881 IP 10.10.1.2.40000 > 192.168.1.10.389: Flags [S], seq 380829920
21:58:35.242910 IP 192.168.1.10.389 > 10.10.1.2.40000: Flags [S.], ack 380829921
21:58:35.243296 IP 10.10.1.2.40000 > 192.168.1.10.389: Flags [.], ack 1

On the outside interface, i.e. after translation, in the very same second:

21:58:35.242021 IP 192.168.1.15.40000 > 192.168.1.10.389: Flags [S], seq 380829920
21:58:35.242902 IP 192.168.1.10.389 > 192.168.1.15.40000: Flags [S.], ack 380829921
21:58:35.243304 IP 192.168.1.15.40000 > 192.168.1.10.389: Flags [.], ack 1

The only difference between the two dumps is the source address: 10.10.1.2 inside, 192.168.1.15 outside. The sequence numbers (seq 380829920) are identical, the timestamps match to the millisecond. So these two dumps are the same packet seen at two different points.

This is the metaphor exactly. The same traveler appears with a local identity inside the booth and with a passport outside it. The server on the outside will never in its life learn there’s an address called 10.10.1.2.

Reading the Ledger: Two Tuples, One Line

With the connection up, we read the translation table:

Bash
conntrack -L -d 192.168.1.10
tcp 6 431997 ESTABLISHED src=10.10.1.2 dst=192.168.1.10 sport=40000 dport=389
    src=192.168.1.10 dst=192.168.1.15 sport=389 dport=40000 [ASSURED] mark=0 use=1

Reading this line is the key to understanding the whole table. There are two separate tuples in it:

  • First tuple, the outbound direction: src=10.10.1.2 dst=192.168.1.10 sport=40000 dport=389. What the inside device sent.
  • Second tuple, the expected return: src=192.168.1.10 dst=192.168.1.15 sport=389 dport=40000. How the answer will look.

That’s exactly the ledger’s line: “this citizen left, the answer will come back on this passport.” The edge device compares every packet arriving from outside against this second tuple; if a matching line exists, it applies the translation in reverse and delivers the packet to its real owner.

The practical way to tell whether an entry has been translated is right here. Put the two tuples side by side: if they mirror each other, there’s no translation; if the mirror is broken, there is. As an example, a line from the machine’s own DNS traffic:

udp 17 27 src=192.168.1.15 dst=192.168.1.10 sport=55057 dport=53
    src=192.168.1.10 dst=192.168.1.15 sport=53 dport=55057 mark=0 use=1

Here the outbound tuple’s source is 192.168.1.15 and the return tuple’s destination is also 192.168.1.15. A perfect mirror, so this traffic wasn’t translated. In the TCP line above, the outbound source is 10.10.1.2 while the return destination is 192.168.1.15; the mirror is broken, so there was translation.

The Ledger’s Lifespan: When Is an Entry Deleted?

The numbers at the start of those lines aren’t coincidental. The TCP line reads 431997, the UDP line 27. These are countdowns to how many seconds until the entry is deleted.

The difference is large and the reason is sensible. In TCP the connection’s open and close are known, so the default lifetime for an established connection is close to five days. UDP has no such signal; the translating device can only infer the flow has ended from silence, so the counter starts around thirty seconds and resets with each packet.

What the metaphor says here is also true: the officer’s ledger doesn’t stay open forever, old lines close after a while. In practice this means a connection that stays silent for a long time is quietly forgotten at the edge device. Your session doesn’t drop, you get no error; it’s just that when the next packet arrives, the ledger is checked, no line is found, and the packet is dropped. This is the most common reason long-idle VPN and SSH sessions “drop for no reason,” and it’s exactly why keepalive settings exist: to keep that line alive.

What Is PAT? One Address, Thousands of Connections

So far we’ve followed a single inside device. In real life there are hundreds of devices behind that address, all going out at once. How does a single passport number stretch to all of them?

It doesn’t. What stretches is not the passport number but the sequence number written beside it. This form, which translates the port number along with the address, is called PAT (Port Address Translation), and it’s very nearly all of the NAT you see on home and office networks today.

The Same Inside Port, Two Different Connections: Which Does NAT Change?

The textbook answer here is clear: PAT changes the source port. To test it, we connect from two separate inside networks, from the exact same source port (40000), to the same target at the same time.

Bash
ip netns exec ic1 nc -p 40000 -w 6 192.168.1.10 389 &
ip netns exec ic2 nc -p 40000 -w 6 192.168.1.10 389 &

What shows up on the outside interface:

21:58:35.242021 IP 192.168.1.15.40000 > 192.168.1.10.389: Flags [S], seq 380829920
21:58:35.242872 IP 192.168.1.15.50048 > 192.168.1.10.389: Flags [S], seq 2172617502

The answer came out finer than expected. Both connections left from the same port; on the outside one stayed 40000, the other became 50048.

Two devices on two separate inside networks connected at the same time from the exact same source port, 40000, to the same target. NAT translated both to a single outside address, 192.168.1.15. But it changed the port for only one of them: the first connection stayed 40000 on the outside, the second became 50048. So PAT rewrites the source port not always, but only when there's a collision. Two inside devices, same source port inside net 1 inside net 2 NAT 10.10.1.2:40000 10.10.2.2:40000 192.168.1.15 192.168.1.15:40000 192.168.1.15:50048 port unchanged port changed Both left from 40000. NAT rewrote only the colliding one; the first passed through as-is. Two devices on two separate inside networks connected at the same time from the exact same source port, 40000, to the same target. NAT translated both to a single outside address, 192.168.1.15. But it changed the port for only one of them: the first connection stayed 40000 on the outside, the second became 50048. So PAT rewrites the source port not always, but only when there's a collision. Two inside devices, same source port inside net 1 inside net 2 NAT 10.10.1.2:40000 10.10.2.2:40000 192.168.1.15 192.168.1.15:40000 192.168.1.15:50048 port unchanged port changed Both left from 40000. NAT rewrote only the colliding one; the first passed through as-is.
The visual of the lab measurement: both connections left from 40000, but on the outside one stayed 40000 and the other became 50048. PAT rewrites the source port not always, but only when there's a collision.

The translation table confirms it:

tcp 6 431997 ESTABLISHED src=10.10.1.2 dst=192.168.1.10 sport=40000 dport=389
    src=192.168.1.10 dst=192.168.1.15 sport=389 dport=40000 [ASSURED]
tcp 6 431997 ESTABLISHED src=10.10.2.2 dst=192.168.1.10 sport=40000 dport=389
    src=192.168.1.10 dst=192.168.1.15 sport=389 dport=50048 [ASSURED]

The two lines’ outbound tuples are nearly identical, only the inside addresses differ. In the return tuples, the ports have split. So PAT doesn’t always change the port; it changes it when it’s forced to. The first connection could keep its port because that port was free on the outside. When the second arrived, the same port was in use and translation became unavoidable.

This behavior itself depends on the software doing the translating, and the measurement was taken on Linux netfilter; another vendor’s device might choose to rewrite the port in every case. The rule that doesn’t change: the same address-and-port pair on the outside can’t belong to two connections at once, because there’s no other information to tell the return packets apart.

The border equivalent is the same. Two people leaving on the same passport can’t be given the same sequence number; if they were, the officer wouldn’t know which one to hand the returning answer to. What makes the number unique isn’t the passport, it’s the passport and the number together.

Which is also why a single external address doesn’t identify anyone on its own. Thousands of devices can sit behind one outside address at the same instant, so tying a connection back to a single device isn’t done with the address; it takes the address, the port, and the time recorded together. A record kept without the port looks present in the file and can say nothing when it’s asked, like a ledger that wrote down the passport number but skipped the sequence number.

What Is the Difference Between SNAT and DNAT?

Every example so far went from inside to outside. What was translated was always the source address, and that is called SNAT (Source NAT). MASQUERADE is a special form of SNAT where the outside address is taken automatically from the interface.

The reverse direction is a separate decision, and it’s called DNAT (Destination NAT). Here the destination address of a packet arriving from outside is changed, i.e. which inside server an incoming request from the outside world will reach.

PropertySNATDNAT
DirectionInside to outsideOutside to inside
Field changedSource address and portDestination address and port
Who starts the entryThe inside device, automatically as the packet leavesNo one; the rule must be written by hand
What happens without itThe answer can’t returnThe connection dies at the edge, never reaches inside
What the far side seesNot you, the gateway’s addressThe real client address, because the source doesn’t change
Everyday nameGetting on the internetPort forwarding

The most important difference between the two isn’t technical, it’s conceptual: SNAT and DNAT are independent on the same device and can point at different addresses. The passport you show on the way out and the address used to reach you don’t have to be the same. That sentence sounds theoretical, but in the field it’s a direct design tool: outbound can be sent through one line while inbound is answered on another, because the outbound direction is SNAT (a policy choice about where you connect to) and the inbound direction is DNAT (a separate policy choice). Binding both to the same address is only the default, not a requirement.

What Is Port Forwarding? How a NAT Rule Is Written

What’s called port forwarding in everyday language is the most common form of DNAT: traffic arriving from outside on a specific port is delivered to a specific device and port inside.

Bash
iptables -t nat -A PREROUTING -d 192.168.1.15 -p tcp --dport 8080 \
  -j DNAT --to-destination 10.10.1.2:8080

The rule reads: take TCP traffic arriving from outside on port 8080 of address 192.168.1.15, and make its destination 10.10.1.2:8080. At the border it’s an instruction handed to the officer at the desk: “send the visitor arriving for this number to that apartment.”

The asymmetry here may be the article’s most practical sentence. You don’t need to write a rule for outbound; you must write one for inbound. On the way out the entry is created on its own as the packet leaves; on the way in there’s no entry to be created, because no one inside asked for anything. There’s no line in the officer’s ledger for that visitor, and no way to know who to call.

There’s also a less-discussed form of port forwarding: you’re not limited to sending a single port inside. If you have more than one public address, you can dedicate an entire address to a single service. This is the field equivalent of what the table above called static NAT, and it earns its keep during a migration: a whole public address pointed at one service keeps that service’s traffic separate from everything else, which isolates faults and keeps logs from mixing which flow belonged to whom.

What Are Double NAT and CGNAT?

Until now there was a single border gate. In real networks there are often two: the provider’s modem does one translation, the organization’s own firewall does another. Translating traffic twice in a row is called double NAT.

On the metaphor it gets easier: you’re passing through a transit country. At your own country’s gate you’re given a passport; at the transit country’s gate that passport isn’t enough either and you’re handed a second document. The person at your destination sees only the second document and has no idea who you are.

Lab: Putting Two Translations in a Row

To show this we added one more layer. Now there are three levels: the innermost network, the middle gateway, and the outer gateway. Each does its own translation.

Bash
# The middle layer does its own translation
ip netns exec mid iptables -t nat -A POSTROUTING -s 10.10.1.0/24 -o veth-mo -j MASQUERADE
# The outer layer translates once more
iptables -t nat -A POSTROUTING -s 10.30.0.0/24 -o ens33 -j MASQUERADE

Listening to the same connection at three points at once, you can see the address change twice:

LAYER 1, on the innermost network (not translated at all):
10.10.1.2.40000 > 192.168.1.10.389: Flags [S], seq 2596541764

LAYER 2, between the two gateways (translated once):
10.30.0.2.40000 > 192.168.1.10.389: Flags [S], seq 2596541764

LAYER 3, on the outside interface (translated twice):
192.168.1.15.40000 > 192.168.1.10.389: Flags [S], seq 2596541764

All three lines share the same sequence number (seq 2596541764), so this is one packet seen at three places. The source address changed once at each layer.

  1. Inside deviceno translationsends the packet with this address10.10.1.2
  2. 1st gatewayfirst translationrewrites the source to10.30.0.2Its ledger holds only 10.10.1.2; it knows nothing of the outer gateway.
  3. 2nd gatewaysecond translationrewrites the source to192.168.1.15Its ledger holds only 10.30.0.2; it has never heard of an address called 10.10.1.2.
  4. Target serversees no translationthe only address it sees192.168.1.15
The source address changes once at each gateway, and the target server sees only the last one. The two notes underneath say the real consequence: because no gateway sees beyond its own layer, a connection coming from outside needs a separate rule at each layer.

And now there are two separate ledgers:

The middle gateway's ledger:
tcp ESTABLISHED src=10.10.1.2 dst=192.168.1.10 sport=40000 dport=389
    src=192.168.1.10 dst=10.30.0.2 sport=389 dport=40000 [ASSURED]

The outer gateway's ledger:
tcp ESTABLISHED src=10.30.0.2 dst=192.168.1.10 sport=40000 dport=389
    src=192.168.1.10 dst=192.168.1.15 sport=389 dport=40000 [ASSURED]

Each ledger knows only its own layer. The outer gateway is unaware there’s an address called 10.10.1.2; the only inside address it knows is 10.30.0.2.

Why One Rule Isn’t Enough

This is exactly where double NAT produces its faults. Say we want to reach an inside server from outside: we opened a service listening on port 8080 on the innermost network and wrote a DNAT rule on only the outer gateway.

Trying from outside, from a real Windows machine on the network:

PowerShell
Test-NetConnection -ComputerName 192.168.1.15 -Port 8080
PingSucceeded    : True
TcpTestSucceeded : False

The symptom is exactly the sentence you hear in the field: the machine is up, ping answers, but the service can’t be reached. These two lines sitting side by side matters, because PingSucceeded : True often gets read as “the network works” and the diagnosis heads the wrong way.

The reason is that the outer gateway hands the packet to the middle gateway, but the middle gateway has no rule to receive it. When we write the rule on the second layer too:

Bash
ip netns exec mid iptables -t nat -A PREROUTING -d 10.30.0.2 -p tcp --dport 8080 \
  -j DNAT --to-destination 10.10.1.2:8080
PingSucceeded    : True
TcpTestSucceeded : True

Looking at the inbound connection’s ledgers is instructive too:

Outer gateway:  src=192.168.1.224 dst=192.168.1.15 dport=8080
                src=10.30.0.2 dst=192.168.1.224 sport=8080
Middle gateway: src=192.168.1.224 dst=10.30.0.2 dport=8080
                src=10.10.1.2 dst=192.168.1.224 sport=8080

The destination address changed twice (192.168.1.15, then 10.30.0.2, then 10.10.1.2), but the source address never changed: 192.168.1.224 is the same in both ledgers. So DNAT doesn’t hide the client. What hides it is SNAT, and because the two are independent, the inside server keeps seeing the real client address.

CGNAT: When You’re Not the One Translating

CGNAT (Carrier Grade NAT) is the form of double NAT that isn’t under your control. Because of IPv4 scarcity, the provider shares a single public address across hundreds of subscribers; when you look at your modem you don’t see a public address, you see a private one assigned to you.

Technically it’s the same setup we measured above, and its one difference is decisive: the second layer isn’t yours. When an inside server needs to be reached from outside, you can’t write the second rule you’d need, because you have no access to that device. If port forwarding doesn’t work despite being configured, and your line is CGNAT, the problem is not in your configuration.

The practical consequence: in any scenario that needs remote access, a camera system, a site-to-site VPN, or a branch link, whether the line carries a real public address is a question to ask at the contract stage. Solving it afterward usually means a change of plan.

Will IPv6 Put an End to NAT?

CGNAT isn’t a preference, it’s a symptom of scarcity: it exists because the provider has run out of IPv4 addresses to hand out. So the first question that comes up is: if it was born because addresses ran out, does it disappear when addresses don’t run out?

In terms of the reason address translation exists, the answer is yes. The IPv6 address space is wide enough to give every device its own public address; scarcity is no longer a problem, so the need to cram many devices behind one address goes away. IPv6 was designed for end-to-end addressability from the start.

But there’s a second job attributed to NAT in the field, and that’s where the real confusion is. Many people think of NAT as a security layer: “inside devices aren’t visible from outside.” The mechanism we measured through this whole article shows why that’s a misreading. What stops an inbound connection isn’t NAT itself, it’s the absence of a line for that connection in the ledger. The protection was never a design, it was a side effect of translation.

In IPv6 that side effect disappears, because there’s nothing to translate. This does not mean every device becomes reachable from outside: an address being public and that address being reachable are two different things. Stopping an inbound connection is now plainly the firewall’s job, done with a stateful rule set. Most home devices already do this by default; on the enterprise side it becomes a rule you have to write.

The border-gate equivalent is clean: in IPv6 everyone has their own passport, so there’s no need for a desk handing out shared ones. But the officer at the gate is still there, still deciding who comes in. Removing the desk isn’t opening the gate.

In practice the transition is partial and long. The vast majority of networks will carry both protocols for a while yet; as long as the IPv4 side is up, so is the NAT on it. So don’t expect NAT to be a topic that vanishes from software any time soon; the diagnostic order in this article holds as long as you have an IPv4 line.

NAT Diagnosis: Server Is Up but Unreachable From Outside

In the field, most NAT-caused faults arrive as a single sentence: “the server works, we get in from inside, we can’t from outside.” In the metaphor: a visitor came to the gate, the person they’re looking for is at their desk inside, but there’s no line in the officer’s ledger for that visitor. The officer isn’t hostile, they just don’t know who to call. Diagnosis therefore runs not from “is the server up” but from “is there a line for this connection in the ledger.”

Build the order backward along the path the packet takes, because the most expensive mistake is diving inside before looking at the outermost device.

  1. Is the service really listening? On the inside machine, confirm the port is in listening mode. If it isn’t, no NAT rule saves it.
  2. Is it reachable from inside, on the same network? If it is, the problem is the path, not the service.
  3. How many layers of translation are there? If the modem and firewall are separate devices, both are probably translating. Each layer needs its own rule.
  4. Does the edge device have a real public address? If the modem’s WAN address starts with 10., 100.64. or 192.168., your line is CGNAT and reaching it from outside isn’t possible with this setup.
  5. Is the rule written in the right direction? Writing an outbound rule (SNAT) and expecting inbound is the most common mix-up in the field.

Translation and permission are two different decisions, and neither alone lets a connection form: even after a packet passes translation, it can still be stopped by a permit rule. It’s worth running this order alongside a rule review on the firewall itself, because a packet that clears translation can still catch on the allow list.

Hairpin NAT: Reaching Your Own Public Address From Inside

There’s a special case, and it’s the one that most misleads diagnosis. An inside user tries to reach a server that’s also inside, but through its public address. The packet leaves the inside, goes to the edge device, and has to come back from there. That return trip is called hairpin NAT, or NAT loopback.

The problem on the mechanism side: the edge device has to translate this packet in both the destination and the source direction. If it only translates the destination, the inside server sends its answer straight to the client; the client, expecting the answer from the public address, rejects a packet from a source it doesn’t recognize. The connection fails to form with both ends up. Some devices don’t support this double translation at all; on those that do, the traffic passes needlessly through the edge.

The clean fix usually lives on the name-resolution side rather than the NAT side: return the internal address to the inside user. We cover the setup in the DNS guide and the Active Directory DNS guide. But the mechanism itself is worth seeing measured, because “works from outside, not from inside” is a symptom people spend real time misreading.

We Measured It

We built the case on the same one-machine setup, with one condition that matters: the client and the server sit on the same inside segment (10.10.1.0/24), the way a workstation and a server share a LAN in a real office. The gateway’s public address is 203.0.113.10, the inside server listens on 8080, and there is exactly one DNAT rule, the same port-forward we wrote earlier:

Bash
iptables -t nat -A PREROUTING -d 203.0.113.10 -p tcp --dport 8080 \
  -j DNAT --to-destination 10.10.1.3:8080

From outside (the control). A client on the outside network reaches 203.0.113.10:8080. It works, and the server sees the outside client’s real address:

TEST1 (WAN) HTTP=200

On the server side, the first packet arrives from the outside client’s real address:

19:12:41.852873 IP 203.0.113.2.41452 > 10.10.1.3.8080: Flags [S], seq 3659343396, win 64240, options [...], length 0

The reply routes back through the gateway (the client is on a different network), the gateway reverses the DNAT, and the answer arrives from 203.0.113.10 as the client expects. A single DNAT rule is enough for the outside.

From inside, with only that DNAT rule. Now an inside client on the same segment reaches the same 203.0.113.10:8080. It fails:

TEST2 (LAN, hairpin SNAT yok) HTTP=000

(The result labels come from the lab script, which is in Turkish: yok means none, var means present.)

The capture on the server side says exactly why. The client’s SYN reaches the server after the DNAT, so the server sees the client’s real inside address 10.10.1.2; it then sends its SYN-ACK straight back to 10.10.1.2 over the local segment, never passing through the gateway. The client dialed 203.0.113.10, so an answer arriving from 10.10.1.3 is from a stranger, and it resets the connection at once:

19:12:42.966963 IP 10.10.1.2.44220 > 10.10.1.3.8080: Flags [S], seq 1475791315, win 64240, options [...], length 0
19:12:42.967008 IP 10.10.1.3.8080 > 10.10.1.2.44220: Flags [S.], seq 1546136757, ack 1475791316, win 65160, options [...], length 0
19:12:42.967017 IP 10.10.1.2.44220 > 10.10.1.3.8080: Flags [R], seq 1475791316, win 0, length 0

The ledger shows the connection stuck, never confirmed, the return the gateway expected having never come back through it:

tcp      6 115 SYN_SENT src=10.10.1.2 dst=203.0.113.10 sport=44220 dport=8080 [UNREPLIED] src=10.10.1.3 dst=10.10.1.2 sport=8080 dport=44220 mark=0 use=1

The fix on the NAT side. We add the second, source-side translation, so the reflected connection also gets its source rewritten to the gateway:

Bash
iptables -t nat -A POSTROUTING -s 10.10.1.0/24 -d 10.10.1.3 -p tcp --dport 8080 \
  -j SNAT --to-source 10.10.1.1

Now the same inside client reaches 203.0.113.10:8080 and it works. The server no longer sees the client; it sees the gateway 10.10.1.1, so it sends its answer to the gateway, which unwinds both translations and hands the client an answer from the address it dialed:

TEST3 (LAN, hairpin SNAT var) HTTP=200

Now the first packet the server sees comes from the gateway:

19:12:47.562131 IP 10.10.1.1.52028 > 10.10.1.3.8080: Flags [S], seq 809448235, win 64240, options [...], length 0
tcp      6 119 TIME_WAIT src=10.10.1.2 dst=203.0.113.10 sport=52028 dport=8080 src=10.10.1.3 dst=10.10.1.1 sport=8080 dport=52028 [ASSURED] mark=0 use=1

Three tests, one DNAT rule the whole time: it works from the outside, fails from the inside, and works from the inside once the source-side translation is added. That is why “works from outside, not from inside” points at hairpin, not at a broken rule. The NAT-side fix above is real, but on most networks the cleaner answer is the DNS one: hand inside users the internal address, and the packet never has to reflect at all. When a user says “it works from outside but not inside,” the first thing to check is which address they’re being handed.

When the External Identity Changes

Every example so far assumed the public address was fixed. It isn’t really yours, though: it belongs to the edge device. Switch providers, or replace the firewall, and the identity the outside world knows you by changes with it, for the reason this article opened with. The outside recognizes you not by your inside address but by the one the edge hands out, so when the edge device changes, that identity changes too.

The consequence catches people out during a migration: a technically flawless cutover can look like a mass outage from the user side if everything that depends on the old address, VPN clients, partner allowlists, DNS records pointing at it, isn’t updated ahead of time. Move the border gate to another city and the passport numbers change with it; if no one is told in advance, no one gets back in. The external identity is something to manage, not just something you configure once.

Advertisement

Conclusion

Summing NAT up as “the thing that changes the address” takes the easy half. What the measurements showed is that the real mechanism is the ledger: for each connection the translating device writes two tuples, one defining the outbound and one the expected return, and it compares every packet arriving from outside against that second tuple. When we deleted the rule, packets still left; what couldn’t leave was the answer.

The three points that earn their keep in practice:

  • Outbound and inbound are separate decisions. SNAT and DNAT can point at different addresses on the same device; splitting outbound onto one line and an inbound service onto another is a design tool, not an accident.
  • However many layers the translation is on, the inbound rule has to be on that many layers. Outbound chains itself; inbound doesn’t. Nearly every double-NAT fault comes down to that one sentence, and hairpin is the same lesson turned inward: one rule that’s enough from outside isn’t enough from inside.
  • The external identity is a managed asset. It changes when the edge device changes, and everyone who depends on it needs to know in advance.

And let’s write where the metaphor lies, because that’s where the mechanism itself sits. A real passport is yours and returns with you; NAT’s passport stays at the gate, and the outside never learns your name. More than that, the officer’s ledger isn’t permanent. A line that goes quiet for a while is deleted, and that connection drops, with both ends up and no one having done anything.

NAT doesn’t stand alone here: what an address is, how a port identifies a connection, and how a name is turned into an address are links in the same chain. Start with what a port is, or how DNS resolves a name.

Questions about NAT

SNAT changes the source address of a packet leaving the network, so it decides what identity an inside device shows to the outside world. DNAT changes the destination address of a packet arriving from outside, so it decides which inside server an incoming request reaches. The two are configured independently on the same device and can point at different addresses; the outbound identity and the inbound target are separate policy decisions.
PAT is the form of NAT that translates the port number alongside the address, and it is very nearly all of the NAT you see on home and office networks. What lets a single public address serve thousands of connections at once is not the address but the port number written next to it. A source address plus a source port makes each connection unique even when many devices share one public address.
For outbound traffic the translation entry is created on its own as the packet leaves. For traffic coming from outside there is no such entry; the edge device has no line telling it which inside address the request belongs to, so it drops the packet. Reaching an inside server from outside therefore requires a DNAT rule you write by hand, and if translation happens on more than one device it requires one rule per layer.
Double NAT is when traffic is translated twice, on two separate devices in a row; the classic case is the provider's modem and the organization's own firewall both translating. It causes trouble because a connection coming from outside has to find a matching entry in two separate tables. Outbound chains itself automatically, one entry per layer, but inbound does not, so writing the rule on only one layer leaves the connection dying on the other.
This is the hairpin NAT case, and we measured it. A single DNAT rule is enough for a client on the outside, but an inside client reaching the same public address fails: the inside server replies straight to the client over the local segment, bypassing the gateway, and the client rejects an answer that came from an address it never dialed. The clean fix is usually on the DNS side, returning the internal address to inside clients; on the NAT side it takes a second, source-side translation so the reply routes back through the gateway.

This article is adapted from a guide the author first published in Turkish on sercebilisim.com: NAT Nedir? Ağ Adresi Çevirisi Nasıl Çalışır?

Advertisement

Written by

İlker Pehlivan

Network and systems engineer, founder of Serçe Bilişim

I run the networks and servers that other people's work depends on. Before founding my own consultancy I administered the backbone network, firewalls and core systems of a large multi-site organisation with thousands of users. I write about the things that actually broke.