Lesson 13 / 18
NAT and PAT
Types of address translation, the translation table's five-component key, capacity calculations, and the breakdown of end-to-end connectivity.
Contents
Every device on the example network now has an address. But all of these addresses come
from the 192.168.10.0/24 block, and that block is not routed on the public Internet:
even if a packet with source address 192.168.10.196 reaches the provider’s router,
there is no path by which a return packet could be brought back.
The organization has exactly one public address: 203.0.113.10. This lesson’s question
is how 182 internal addresses can go outbound through a single public address.
Three Kinds of Translation
Network address translation (NAT) is changing the addresses in a packet’s header while it passes through. Three forms are distinguished.
| Type | Mapping | How many public addresses | Inbound connection |
|---|---|---|---|
| Static NAT | One-to-one, fixed | As many as internal hosts | Can be established |
| Dynamic NAT | One-to-one, from a pool | As many as concurrent hosts | Cannot be established |
| Port address translation (PAT) | Many-to-one | One address is enough | Only if configured |
Static NAT permanently binds one internal address to one public address. It is used for a server that must be reachable from outside.
Dynamic NAT establishes a temporary mapping from a pool of public addresses. When the pool is exhausted, new hosts cannot go outbound. Since the organization has only one public address, this type does not suit the example network.
Port address translation (PAT) fits a large number of internal hosts into a single public address by changing the source port number along with the address. This is the method the example network uses. In common usage, when people say “NAT” they usually mean this type.
The Translation Table
The device performing translation must know which return packet belongs to which internal host. For this it keeps a translation table. The table’s key has five components: source address, source port, destination address, destination port, and protocol.
EXTERNAL_ADDRESS = "203.0.113.10" class Pat: """A translation table that performs port translation.""" def __init__(self, external_address: str, initial_port: int = 61000) -> None: self.external_address = external_address self.next_port = initial_port self.forward: dict[tuple, tuple] = {} # internal key -> external key self.reverse: dict[tuple, tuple] = {} # external key -> internal key def outbound(self, internal_ip: str, internal_port: int, dest_ip: str, dest_port: int) -> tuple[str, int]: key = (internal_ip, internal_port, dest_ip, dest_port) if key not in self.forward: self.next_port += 1 external = (self.external_address, self.next_port) self.forward[key] = external self.reverse[(external[0], external[1], dest_ip, dest_port)] = (internal_ip, internal_port) return self.forward[key] def inbound(self, external_ip: str, external_port: int, source_ip: str, source_port: int) -> tuple[str, int] | None: return self.reverse.get((external_ip, external_port, source_ip, source_port)) pat = Pat(EXTERNAL_ADDRESS) flows = [ ("192.168.10.196", 49152, "198.51.100.20", 443), ("192.168.10.197", 49152, "198.51.100.20", 443), ("192.168.10.196", 49153, "198.51.100.20", 443), ("192.168.10.60", 51000, "198.51.100.20", 80), ] print(f"{'internal source':22s} {'external source':22s} {'destination'}") for internal_ip, internal_port, dest_ip, dest_port in flows: ext_ip, ext_port = pat.outbound(internal_ip, internal_port, dest_ip, dest_port) print(f"{internal_ip + ':' + str(internal_port):22s} {ext_ip + ':' + str(ext_port):22s} " f"{dest_ip + ':' + str(dest_port)}") print() print("return packet 203.0.113.10:61001 <- 198.51.100.20:443 ->", pat.inbound("203.0.113.10", 61001, "198.51.100.20", 443)) print("return packet 203.0.113.10:61003 <- 198.51.100.20:443 ->", pat.inbound("203.0.113.10", 61003, "198.51.100.20", 443)) print("unsolicited packet 203.0.113.10:61999 <- 198.51.100.20:443 ->", pat.inbound("203.0.113.10", 61999, "198.51.100.20", 443))
internal source external source destination
192.168.10.196:49152 203.0.113.10:61001 198.51.100.20:443
192.168.10.197:49152 203.0.113.10:61002 198.51.100.20:443
192.168.10.196:49153 203.0.113.10:61003 198.51.100.20:443
192.168.10.60:51000 203.0.113.10:61004 198.51.100.20:80
return packet 203.0.113.10:61001 <- 198.51.100.20:443 -> ('192.168.10.196', 49152)
return packet 203.0.113.10:61003 <- 198.51.100.20:443 -> ('192.168.10.196', 49153)
unsolicited packet 203.0.113.10:61999 <- 198.51.100.20:443 -> None
The second row shows why translation is necessary: two different internal hosts used the same source port number (49152). If the numbers were not changed, the return packets could not be told apart.
The last line is translation’s most visible side effect: a packet with no counterpart in the table is dropped. No connection arriving from outside that was not initiated from inside can get in.
Fields That Change
When a packet is translated, it is not only the address fields that change. Since the source address in the IP header and the source port in the transport header change, the checksums that cover them must be recomputed too.
| Field | On the outbound packet | On the return packet |
|---|---|---|
| IP source address | Internal address → public address | Unchanged |
| IP destination address | Unchanged | Public address → internal address |
| Transport source port | Internal number → translated number | Unchanged |
| Transport destination port | Unchanged | Translated number → internal number |
| IP header checksum | Recomputed | Recomputed |
| Transport checksum | Recomputed | Recomputed |
The reason the transport checksum also changes is that TCP and UDP checksums are computed over a pseudo-header that includes the IP addresses. This detail makes concrete that address translation is a layer violation: a device operating at the network layer is forced to modify the transport-layer header.
For the same reason, a translator that cannot read the transport header cannot work. A transport layer encrypted end to end makes translation impossible.
Capacity Limit
The number of simultaneous translations that can be done through a single public address is limited by the available port numbers.
available = 65535 - 1024 + 1 print("available ports:", available) for n in (182, 1000, 4000): print(f"{n:5d} clients -> concurrent flows per client: {available // n}")
available ports: 64512 182 clients -> concurrent flows per client: 354 1000 clients -> concurrent flows per client: 64 4000 clients -> concurrent flows per client: 16
For the example network’s 182 hosts, that works out to 354 concurrent flows per client — a generous margin. But the number is inversely proportional to the host count and drops to 64 at a thousand hosts. Applications that open dozens of connections per page push against this limit.
The calculation is pessimistic: because the translation key also includes the destination, flows going to different destinations can share the same translated number. Still, the limit is real, and this is why large networks use multiple public addresses.
A second limit is memory: a table entry is kept for every flow. Entries are held for a period and time out — this produces one of translation’s most visible side effects.
The Breakdown of End-to-End Connectivity
The end-to-end principle introduced in the TCP/IP Model lesson says that state should not be placed inside the network. Address translation directly violates this principle: the translation table is state kept in the middle of the network. Its consequences can be listed.
A connection cannot be established from outside. A flow that was not initiated from
inside has no counterpart in the table. For a device inside to be reachable from
outside, a rule must be written by hand — port forwarding. If the internal server
192.168.10.226 on the example network is to be exposed, a rule is defined that
forwards traffic arriving at 203.0.113.10:443 to it.
Idle flows drop. When table entries are deleted by timeout, the return path is lost for a connection that has not carried data in a long time. Both ends think the connection is still open, but packets do not get through. Applications prevent this with regular keep-alive packets; the TCP lesson details this mechanism.
Peers cannot meet directly. If both ends are behind translation, neither can connect to the other. The workaround is for both ends to connect to a shared external server, learn their own external address and port, and then send packets to each other simultaneously. This method only works if the translator’s mapping behavior is predictable; when it does not work, traffic is carried through a relay server and latency increases.
An internal client may not be able to reach its own external address. A packet from
a device inside trying to connect to 203.0.113.10:443 arrives at the translator from
its own internal interface and needs to turn back around. This behavior is called
hairpinning, and not every implementation supports it. The symptom is typical: a
service works from outside, but not from inside the same network.
Translation Is Not a Security Mechanism
Not being able to establish a connection from outside looks like security, but translation was not designed as a security mechanism and should not be treated as one.
The access barrier is a side effect, not a policy. Every flow initiated from inside is opened outward; malicious software gets past the barrier by initiating the connection from inside. Translation also does not look at the content of the traffic passing through; it does not inspect what is carried inside a permitted flow.
Access policy is established through explicitly written filtering rules. Address translation solves address scarcity; access control is the name of a separate job.
Carrier-Grade Translation
When address scarcity is felt at the provider level too, translation is applied one
more layer: subscribers are given addresses from the 100.64.0.0/10 block instead of a
public address, and outbound traffic goes through the provider’s public addresses. This
is called carrier-grade NAT.
Under two layers of translation, port forwarding cannot be defined by the subscriber, peer-to-peer rendezvous fails more often, and address-based diagnosis loses its meaning because a large number of subscribers share one external address.
IPv6 removes the need for any of these layers: once every endpoint can be given a public address, the translation table becomes unnecessary too. This is one of the rationales for the transition methods covered in the previous lesson.
Summary
- Private-use addresses are not routed on the public Internet; outbound access is provided through address translation.
- Static translation is one-to-one and permanent, dynamic translation is temporary from a pool, and port address translation is many-to-one.
- The translation table’s key has five components; return packets are told apart even when different internal hosts use the same source port.
- Because the address and port change, both the IP and transport checksums are recomputed; this is evidence that translation is a layer violation.
- A single public address has 64,512 ports; that works out to 354 concurrent flows per host for 182 hosts and 64 for a thousand hosts.
- Translation breaks end-to-end connectivity: connections cannot be established from outside, idle flows drop, and peers cannot meet directly. It is not a security mechanism.
Next Step
Throughout this topic, the term “port” was used without being defined: in the translation table’s key, in the leasing protocol’s numbers 67 and 68, in the destination server’s 443. What these numbers are, and how an operating system decides which application an incoming segment goes to, is the transport layer’s subject. The next lesson covers ports, the concept of a socket, and the four-tuple that makes a connection unique.
To keep your progress and take notes, Log in
My notes
Log in to take notes.