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 12345

then connecting from my laptop:

jim@local:~$ nc 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/'
nc        36761  jim    3u  IPv4 0x86646b399e45805      0t0  TCP> (ESTABLISHED)

Then from the server:

jim@remote:~$ lsof -nPi TCP | awk 'NR==1 || /12345/'
nc      1878  jim    4u  IPv4  21055      0t0  TCP> (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, but the server thinks its own IP address is And the server thinks it’s connected to, but my laptop thinks its own IP address is

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-----------         |
                |                       |
      +-------------       |
      |         |                       |
      |         +-----------------------+
      |         +-Internet--------------+
      |         |                       |
      +-------------        |
                |                       |
      +-------------      |
      |         |                       |
      |         +-----------------------+
      |         +-GCP subnet------------+
      |         |                       |
      +-------------          |
                |                       |
MY SERVER-----------          |
                |                       |

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, the GCP router puts a corresponding packet on the GCP subnet destined for, 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, it copies the packet to the Internet, modifying the source address to The GCP router’s policy thus creates a one-to-one relationship between the IP addresses and 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 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, notes down in a “translation table” that all response on the Internet to port 64125 should be forwarded to 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, 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,!

jim@remote:~$ lsof -nPi TCP | awk 'NR==1 || /1234/'
nc      13020  jim    4u  IPv4  40075      0t0  TCP> (ESTABLISHED)
nc      13021  jim    4u  IPv4  40078      0t0  TCP> (ESTABLISHED)

Both of these connections work - but how? If the server sends both packets for both connections to, 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, ->
(64125, 12346, ->

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 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/'
nc      13020  jim    4u  IPv4  40075      0t0  TCP> (ESTABLISHED)
nc      13041  jim    4u  IPv4  40278      0t0  TCP> (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, -> (,42202)
(1024,  12345, -> (,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?

I wrote this because I felt like it. This post is not associated with my employer.