Private Addresses & NAT
By the end of this lesson you will be able to recognise private IPv4 addresses, explain how NAT and PAT share one public address among many devices, read a NAT translation table, and say why port forwarding and CGNAT exist.
IPv4 has about 4.3 billion addresses. That sounds like a lot until you count the phones, laptops, TVs and cameras in the world. The internet ran short of IPv4 addresses years ago. Two ideas kept it working: private addresses and NAT.
Private address ranges
RFC 1918 sets aside three ranges that anyone may use inside their own network, without asking anyone:
| Range | CIDR block | Addresses | Typical use |
|---|---|---|---|
| 10.0.0.0 to 10.255.255.255 | 10.0.0.0/8 | about 16.7 million | Large companies, ISPs, cloud networks |
| 172.16.0.0 to 172.31.255.255 | 172.16.0.0/12 | about 1 million | Medium networks, Docker and some VPNs |
| 192.168.0.0 to 192.168.255.255 | 192.168.0.0/16 | 65,536 | Homes and small offices |
These are private addresses. Routers on the public internet do not carry them. Thousands of homes in Kochi can all use 192.168.1.0/24 at the same time, because those addresses never leave each home. A public address is unique on the whole internet and is given out by ISPs and regional registries (APNIC for the Asia-Pacific region).
Think of extension numbers in office buildings. Every office in a city can have an "extension 101". Inside the building, 101 reaches one desk. Calls from outside come through the building's single main number.
Let us check some addresses with Python.
import ipaddress
for a in ["192.168.1.6", "10.20.30.40", "172.20.1.1", "172.32.1.1", "8.8.8.8", "100.72.10.5"]:
ip = ipaddress.ip_address(a)
print(f"{a:>12}: private={ip.is_private!s:<5} global={ip.is_global}")192.168.1.6: private=True global=False
10.20.30.40: private=True global=False
172.20.1.1: private=True global=False
172.32.1.1: private=False global=True
8.8.8.8: private=False global=True
100.72.10.5: private=False global=FalseNotice two traps. 172.32.1.1 is public: the private block stops at 172.31. And 100.72.10.5 is neither private nor global. It is in the shared range used for carrier-grade NAT, which you will meet at the end of this lesson.
NAT: network address translation
If private addresses cannot go on the internet, how does your laptop at 192.168.1.6 open a website? Your home router does NAT (network address translation). It rewrites the source address of each outgoing packet to the router's own public address, and rewrites replies on the way back in.
There are three kinds:
- Static NAT: one private address is always mapped to one public address. Used for servers that must be reachable.
- Dynamic NAT: private addresses borrow public addresses from a pool, one each, while they need them.
- PAT (port address translation), also called NAPT or "NAT overload": many private addresses share one public address. The router tells connections apart by port number. Every home router does this.
How PAT works, step by step
Priya (192.168.1.6) and Rohan (192.168.1.9) share a router whose public address is 203.0.113.25.
- Priya's laptop sends a packet from
192.168.1.6:51000to a web server at198.51.100.80:443. - The router replaces the source with
203.0.113.25:40001and writes a line in its NAT table:40001 ↔ 192.168.1.6:51000. - Rohan's phone also connects, from
192.168.1.9:51000(the same port, by chance). The router gives it203.0.113.25:40002. - A reply arrives for
203.0.113.25:40001. The router looks up 40001, finds Priya, rewrites the destination to192.168.1.6:51000and sends it in.
Let us simulate the router.
class PatRouter:
def __init__(self, public_ip):
self.public_ip = public_ip
self.next_port = 40001
self.out_map = {} # (private ip, port) -> public port
self.in_map = {} # public port -> (private ip, port)
def outgoing(self, src_ip, src_port, dst):
key = (src_ip, src_port)
if key not in self.out_map:
self.out_map[key] = self.next_port
self.in_map[self.next_port] = key
self.next_port += 1
pub = self.out_map[key]
print(f"OUT {src_ip}:{src_port} -> {dst} becomes {self.public_ip}:{pub} -> {dst}")
def incoming(self, src, dst_port):
if dst_port not in self.in_map:
print(f"IN from {src} to port {dst_port}: no entry, dropped")
return
ip, port = self.in_map[dst_port]
print(f"IN from {src} to port {dst_port} delivered to {ip}:{port}")
r = PatRouter("203.0.113.25")
r.outgoing("192.168.1.6", 51000, "198.51.100.80:443")
r.outgoing("192.168.1.9", 51000, "198.51.100.80:443")
r.incoming("198.51.100.80:443", 40002)
r.incoming("192.0.2.66:5555", 40050)OUT 192.168.1.6:51000 -> 198.51.100.80:443 becomes 203.0.113.25:40001 -> 198.51.100.80:443 OUT 192.168.1.9:51000 -> 198.51.100.80:443 becomes 203.0.113.25:40002 -> 198.51.100.80:443 IN from 198.51.100.80:443 to port 40002 delivered to 192.168.1.9:51000 IN from 192.0.2.66:5555 to port 40050: no entry, dropped
The last line matters. A packet that arrives without a matching table entry has nowhere to go, so it is dropped. That is why devices behind NAT can start connections but strangers on the internet cannot reach them directly.
Port forwarding
Sometimes you want an incoming connection, for example to reach a CCTV recorder at your shop. Port forwarding is a static entry in the NAT table: "anything arriving on public port 8443 goes to 192.168.1.50:443". Use it carefully. Every forwarded port is a door into your network, so keep the device updated, use strong passwords, and prefer a VPN where possible.
forwards = {8443: ("192.168.1.50", 443)}
def inbound(port):
if port in forwards:
ip, p = forwards[port]
return f"public port {port} -> {ip}:{p}"
return f"public port {port} -> dropped"
for port in [8443, 22, 3389]:
print(inbound(port))public port 8443 -> 192.168.1.50:443 public port 22 -> dropped public port 3389 -> dropped
Carrier-grade NAT
Many ISPs, especially mobile networks, do not have enough public IPv4 addresses to give one to every customer. They add a second NAT inside their own network, called CGNAT (carrier-grade NAT). RFC 6598 reserves 100.64.0.0/10 for the space between the customer and the ISP's NAT. If your router's "WAN address" starts with 100.64 to 100.127, you are behind CGNAT, and port forwarding from outside will not work without the ISP's help.
Common mistakes
- Thinking 172.0.0.0/8 is private. Only 172.16.0.0 to 172.31.255.255 is private.
- Using a private address to reach a device from the internet. Private addresses do not route on the internet. You need port forwarding, a public address or a VPN.
- Thinking PAT needs many public addresses. PAT shares one public address by using different ports.
- Treating NAT as complete security. Use a firewall and keep devices updated.
Key points
- RFC 1918 private ranges: 10.0.0.0/8, 172.16.0.0/12 and 192.168.0.0/16.
- Private addresses can be reused in every network but are not routed on the internet.
- NAT rewrites addresses at the border; PAT shares one public address using port numbers.
- The NAT table maps public ports back to private address and port pairs; unknown incoming packets are dropped.
- Port forwarding adds a fixed inbound mapping; use it carefully.
- CGNAT (100.64.0.0/10) is a second NAT run by the ISP.
Exercise
Extend PatRouter with a method table() that prints every NAT entry as public_port -> private_ip:private_port. Then make three outgoing connections from two different laptops (one laptop opens two connections from different source ports) and print the table.
Show solution
class PatRouter:
def __init__(self, public_ip):
self.public_ip = public_ip
self.next_port = 40001
self.out_map = {}
self.in_map = {}
def outgoing(self, src_ip, src_port):
key = (src_ip, src_port)
if key not in self.out_map:
self.out_map[key] = self.next_port
self.in_map[self.next_port] = key
self.next_port += 1
def table(self):
for pub, (ip, port) in self.in_map.items():
print(f"{pub} -> {ip}:{port}")
r = PatRouter("203.0.113.25")
r.outgoing("192.168.1.6", 51000)
r.outgoing("192.168.1.6", 51001)
r.outgoing("192.168.1.9", 60000)
r.table()40001 -> 192.168.1.6:51000 40002 -> 192.168.1.6:51001 40003 -> 192.168.1.9:60000
Each unique pair of private address and port gets its own public port, so even two connections from the same laptop are kept apart.
Quick Quiz
1. Which address is private under RFC 1918?
The private 172 block is 172.16.0.0/12, which covers 172.16 to 172.31.
2. How does PAT tell apart many devices sharing one public address?
Each connection gets its own public port in the NAT table.
3. What happens to an incoming packet with no matching NAT entry or port forward?
The router does not know which private host should receive it.
4. Your router's WAN address is 100.80.12.7. What does this suggest?
100.64.0.0/10 is the shared range reserved for CGNAT.
Learn Computer Networks with a mentor
Free lessons get you started. Our Computer Science Fundamentals training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
