How does network address translation work?

I have a laptop in front of me and a server in the cloud. I establish a TCP connection between them by listening on the server,

jim@remote:~$ nc -l 0.0.0.0 12345

then connecting from my laptop:

jim@local:~$ nc 35.190.176.201 12345

Now a new TCP connection is established, and we can see this connection from both machines using lsof. First locally:

jim@local:~$ lsof -nPi TCP | awk 'NR==1 || /12345/'
COMMAND     PID USER   FD   TYPE            DEVICE SIZE/OFF NODE NAME
nc        36761  jim    3u  IPv4 0x86646b399e45805      0t0  TCP 192.168.1.4:64125->35.190.176.201:12345 (ESTABLISHED)

Then from the server:

jim@remote:~$ lsof -nPi TCP | awk 'NR==1 || /12345/'
COMMAND  PID USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
nc      1878  jim    4u  IPv4  21055      0t0  TCP 10.142.0.2:12345->51.6.191.203:64125 (ESTABLISHED)

A TCP connection, traditionally, is identified by four things: the client IP address and server IP address, and the client port and server port. What are these values in the above connection?

There are at least a couple of oddities here. My laptop thinks it’s connected to 35.190.176.201, but the server thinks its own IP address is 10.142.0.2. And the server thinks it’s connected to 51.6.191.203, but my laptop thinks its own IP address is 192.168.1.4.

The reason for this oddity is network address translation, or NAT. This is a process done by routers, machines which sit on the path between my laptop and my server. Each router doing NAT is in two networks, i.e. the router has two network interfaces, one in each network, each with its own assigned IP address.

My TCP connection involves at least three networks, two routers, and six IP addresses! Here they are diagrammed:

Machines          Networks
vvvvvvvv          vvvvvvvv

                +-my local area network-+
                |                       |
MY LAPTOP-----------192.168.1.4         |
                |                       |
      +-------------192.168.1.254       |
      |         |                       |
      |         +-----------------------+
MY HOME ROUTER
      |         +-Internet--------------+
      |         |                       |
      +-------------51.6.191.203        |
                |                       |
      +-------------35.190.176.201      |
      |         |                       |
      |         +-----------------------+
GCP ROUTER
      |         +-GCP subnet------------+
      |         |                       |
      +-------------10.142.0.1          |
                |                       |
MY SERVER-----------10.142.0.2          |
                |                       |
                +-----------------------+

The three networks are my local area network (LAN), the Internet proper, and a subnet on Google Cloud Platform (GCP). The routers are my home router (a Sagemcom something-or-other which my ISP provided), and an unknown router on Google Cloud Platform. Notice that each router spans two networks, and performs NAT between them. Each router translates packets between one network and the other. Both routers are doing NAT, but different types of NAT.

The GCP router’s behavior is the simpler of the two. The GCP router is doing “basic”, or one-to-one NAT. When the GCP router sees an IP packet on the Internet to 35.190.176.201, the GCP router puts a corresponding packet on the GCP subnet destined for 10.142.0.2, the IP address of my server. That is, the router modifies the destination address of the IP packet. Most other things about the IP packet are conserved, such as TCP port numbers. In the other direction, when the GCP router sees an IP packet on the subnet from 10.142.0.2, it copies the packet to the Internet, modifying the source address to 35.190.176.201. The GCP router’s policy thus creates a one-to-one relationship between the IP addresses 35.190.176.201 and 10.142.0.2. The GCP router’s one-to-one NAT procedure is stateless (except to remember the rule which binds those two addresses together).

By contrast, my home router is doing one-to-many NAT. When my home router receives a packet on my LAN, if the packet’s destination address is not in the LAN, the home router puts a corresponding packet on the Internet, with the source IP address modified to 51.6.191.203 so that the home router will receive the destination’s response. So far, so similar.

But one-to-many NAT must do more than this, because when the home router receives a response packet on the Internet, there is no way to tell which host on my LAN to forward it to. The router’s modification of the outgoing packets’ source addresses must have an inverse. This is not possible at the IP level, because there are many IP addresses on my LAN, but the router only has one source IP address available on the Internet! To get more addresses, one-to-many NAT works at the TCP level, which has a 16-bit source port field. This effectively gives the home router 65,536 addresses on the Internet.

So my home router, when copying the packet from 192.168.1.4:64125, notes down in a “translation table” that all response on the Internet to port 64125 should be forwarded to 192.168.1.4. When the home router receives a packet on the Internet, the router looks up the packet’s destination TCP port in the translation table to find the host on the LAN to forward the packet to.

What happens if another host on my LAN, say 192.168.1.6, opens a connection with source port 64125? I tried this by listening on port 12346 on the server, then connecting to it from my Android phone, telling nc to use a specific source port with the -p flag. The server shows two connections to the same IP address and port, 51.6.191.203:64125!

jim@remote:~$ lsof -nPi TCP | awk 'NR==1 || /1234/'
COMMAND   PID USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
nc      13020  jim    4u  IPv4  40075      0t0  TCP 10.142.0.2:12345->51.6.191.203:64125 (ESTABLISHED)
nc      13021  jim    4u  IPv4  40078      0t0  TCP 10.142.0.2:12346->51.6.191.203:64125 (ESTABLISHED)

Both of these connections work - but how? If the server sends both packets for both connections to 51.6.191.203:64125, how can my home router distinguish them? The answer is the distinct ports 12345 and 12346. My home router’s translation table is not as simple as port -> ipaddr. Actually the translation table is (port,port,ipaddr) -> ipaddr, and contains the entries:

(64125, 12345, 51.6.191.203) -> 192.168.1.4
(64125, 12346, 51.6.191.203) -> 192.168.1.6

Because this is a more specific lookup than just one port, the router can have more than 65,536 addresses to work with.

But still - what happens if two separate hosts on my LAN choose the same source port and they connect to the same IP address and port? I tried it, by using nc -l 0.0.0.0 12345 twice, then connecting from two hosts, both using source port 42202.

Both connections still work as expected! How? My server reports these two connections:

jim@remote:~$ lsof -nPi TCP | awk 'NR==1 || /12345/'
COMMAND   PID USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
nc      13020  jim    4u  IPv4  40075      0t0  TCP 10.142.0.2:12345->51.6.191.203:42202 (ESTABLISHED)
nc      13041  jim    4u  IPv4  40278      0t0  TCP 10.142.0.2:12345->51.6.191.203:1024 (ESTABLISHED)

Notice that the server is connected to remote port 1024. My home router noticed that there was a clash, and rewrote the source port to a new free port! So my translation table is still too simplified, and it’s actually a (port,port,ipaddr) -> (ipaddr,port), with the entries:

(42202, 12345, 51.6.191.203) -> (192.168.1.4,42202)
(1024,  12345, 51.6.191.203) -> (192.168.1.6,42202)

There are many things I have not covered in this post. For instance, when do entries get removed from the translation table? What does my home router do with packets which don’t match an entry in the table?

Get updates on Twitter

I wrote this because I felt like it. This post is not associated with my employer. This site is hosted by Netlify (who are great, but I'm not associated with them either).