EthicsComputerCourses
Join training +91 90532 10052

Private Addresses & NAT

Computer Networks · Lesson 14 of 32 Intermediate 6 min read Updated

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:

RangeCIDR blockAddressesTypical use
10.0.0.0 to 10.255.255.25510.0.0.0/8about 16.7 millionLarge companies, ISPs, cloud networks
172.16.0.0 to 172.31.255.255172.16.0.0/12about 1 millionMedium networks, Docker and some VPNs
192.168.0.0 to 192.168.255.255192.168.0.0/1665,536Homes 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.

PythonRun it yourself
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}")
Output
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=False

Notice 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.

  1. Priya's laptop sends a packet from 192.168.1.6:51000 to a web server at 198.51.100.80:443.
  2. The router replaces the source with 203.0.113.25:40001 and writes a line in its NAT table: 40001 ↔ 192.168.1.6:51000.
  3. Rohan's phone also connects, from 192.168.1.9:51000 (the same port, by chance). The router gives it 203.0.113.25:40002.
  4. A reply arrives for 203.0.113.25:40001. The router looks up 40001, finds Priya, rewrites the destination to 192.168.1.6:51000 and sends it in.

Let us simulate the router.

PythonRun it yourself
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)
Output
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.

PythonRun it yourself
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))
Output
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
Python
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()
Output
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?

2. How does PAT tell apart many devices sharing one public address?

3. What happens to an incoming packet with no matching NAT entry or port forward?

4. Your router's WAN address is 100.80.12.7. What does this suggest?

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