IP Addressing, Subnetting & Routing
IPv4 and IPv6 addresses, CIDR subnetting with worked examples, private addresses, NAT and how routers forward packets.
IP addresses
An IP address identifies a network interface so packets can be routed to it.
- IPv4 — 32 bits, written as four numbers 0–255:
192.168.1.25. About 4.3 billion addresses — not enough for today's internet. - IPv6 — 128 bits, written in hexadecimal groups:
2001:db8:85a3::8a2e:370:7334. Practically unlimited addresses;::compresses runs of zeros.
192 .168 .1 .25
11000000 10101000 00000001 00011001 ← 32 bits
CIDR notation and subnets
An address is split into a network part and a host part. CIDR notation states how many leading bits are the network: 192.168.1.0/24 means the first 24 bits identify the network, leaving 8 bits (256 addresses) for hosts.
| CIDR | Subnet mask | Total addresses | Usable hosts (IPv4) |
|---|---|---|---|
| /16 | 255.255.0.0 | 65,536 | 65,534 |
| /24 | 255.255.255.0 | 256 | 254 |
| /26 | 255.255.255.192 | 64 | 62 |
| /28 | 255.255.255.240 | 16 | 14 |
| /32 | 255.255.255.255 | 1 | a single host |
Two addresses in each classic IPv4 subnet are reserved: the network address (all host bits 0) and the broadcast address (all host bits 1). Formula: total = 2^(32 − prefix).
Python's ipaddress module does the arithmetic for you:
import ipaddress
net = ipaddress.ip_network("192.168.10.0/26")
hosts = list(net.hosts())
print("netmask: ", net.netmask)
print("addresses:", net.num_addresses)
print("network: ", net.network_address)
print("broadcast:", net.broadcast_address)
print("usable: ", hosts[0], "-", hosts[-1])
print("192.168.10.70 inside?", ipaddress.ip_address("192.168.10.70") in net)
Output:
netmask: 255.255.255.192
addresses: 64
network: 192.168.10.0
broadcast: 192.168.10.63
usable: 192.168.10.1 - 192.168.10.62
192.168.10.70 inside? False
Worked example: splitting a network
A college has 10.20.0.0/22 and wants four equal subnets for labs, staff, students and servers. Borrow 2 bits (/22 → /24):
import ipaddress
for name, subnet in zip(["labs", "staff", "students", "servers"],
ipaddress.ip_network("10.20.0.0/22").subnets(new_prefix=24)):
print(f"{name:<9} {subnet} ({subnet.num_addresses - 2} hosts)")
Output:
labs 10.20.0.0/24 (254 hosts)
staff 10.20.1.0/24 (254 hosts)
students 10.20.2.0/24 (254 hosts)
servers 10.20.3.0/24 (254 hosts)
The same skill is used when designing cloud VPCs: a VPC like 10.0.0.0/16 is carved into public and private subnets per availability zone.
Private addresses and NAT
Some ranges are reserved for private networks and are not routed on the public internet:
| Range | CIDR |
|---|---|
| 10.0.0.0 – 10.255.255.255 | 10.0.0.0/8 |
| 172.16.0.0 – 172.31.255.255 | 172.16.0.0/12 |
| 192.168.0.0 – 192.168.255.255 | 192.168.0.0/16 |
Also special: 127.0.0.0/8 (loopback, "this machine") and 169.254.0.0/16 (link-local).
NAT (Network Address Translation) lets many private devices share one public IP. Your home router rewrites outgoing packets' source address and port to its public IP, remembers the mapping, and translates replies back. NAT conserved IPv4 addresses for decades, but it means devices behind it can't easily accept incoming connections — which is why video-call apps need tricks like STUN/TURN.
How routing works
A router connects networks and forwards each packet toward its destination using a routing table. For each packet it chooses the most specific matching route — the longest prefix match:
import ipaddress
routing_table = [
("10.0.0.0/8", "core-router"),
("10.1.0.0/16", "campus-router"),
("10.1.5.0/24", "lab-switch"),
("0.0.0.0/0", "isp-gateway"), # default route: everything else
]
def next_hop(destination: str) -> str:
ip = ipaddress.ip_address(destination)
matches = [(ipaddress.ip_network(prefix), hop) for prefix, hop in routing_table
if ip in ipaddress.ip_network(prefix)]
return max(matches, key=lambda m: m[0].prefixlen)[1] # longest prefix wins
for dst in ("10.1.5.20", "10.1.9.9", "10.200.0.1", "8.8.8.8"):
print(f"{dst:<11} → {next_hop(dst)}")
Output:
10.1.5.20 → lab-switch
10.1.9.9 → campus-router
10.200.0.1 → core-router
8.8.8.8 → isp-gateway
Each router only decides the next hop; the packet hops router to router until it arrives. Each IP packet has a TTL (hop limit) that decreases at every router, so packets caught in loops eventually die — traceroute uses this to reveal the path.
Routers learn routes from routing protocols: OSPF inside an organisation, and BGP between the networks (autonomous systems) that make up the internet.
ARP: from IP to MAC
On a local network, frames are delivered by MAC address. ARP asks "who has 192.168.1.1?" and the owner replies with its MAC. Your computer sends internet-bound packets to the default gateway's MAC, and the gateway routes them onward.
Common mistakes
- Forgetting the network and broadcast addresses when counting usable hosts.
- Overlapping CIDR ranges between VPCs or offices, which makes peering/VPN impossible later.
- Binding a server to
127.0.0.1inside a container and wondering why it's unreachable — bind to0.0.0.0. - Treating NAT as a firewall — use real firewall rules.
Key takeaways
- IPv4 is 32 bits, IPv6 is 128 bits; CIDR
/ngives 2^(32 − n) IPv4 addresses. - Private ranges (10/8, 172.16/12, 192.168/16) reach the internet through NAT.
- Routers forward by longest prefix match; the default route
0.0.0.0/0catches the rest. - ARP maps IPs to MAC addresses on the local link.