IP Addressing & Subnetting — Carving Up the Network
What you'll learn
- How an IPv4 address is really just a 32-bit number, and how to read it in binary
- What CIDR notation (
192.168.10.77/26) and a subnet mask mean - How to find the network address, broadcast address and usable host range with two bit operations
- How to split a network into subnets (including different sizes: VLSM) and merge them again
- How a router picks a route with longest-prefix match
- A first look at IPv6
- You will build three tools: a CIDR calculator, a VLSM planner and a routing-table lookup
The street address analogy 🏠
A postal address has two parts: the street and the house number. The street tells the mail truck which neighbourhood to drive to. The house number only matters once it has arrived. Nobody needs a list of every house on Earth: the truck first matches the street, then the number.
An IP address works the same way. One part says which network (the street), the other says which host on that network (the house number). A subnet mask is a stencil laid over the address that shows which digits are the street. Routers only look at the street part, so a router in another country needs one entry for a whole network instead of one for every computer in it.
An IPv4 address is 32 bits
We write an IPv4 address as four decimal numbers separated by dots, each from 0 to 255. Each number is one octet, which is 8 bits. In binary the weights inside an octet are:
128 64 32 16 8 4 2 1
So 192.168.10.77 is:
192 168 10 77
11000000.10101000.00001010.01001101
(192 = 128 + 64, 168 = 128 + 32 + 8, 10 = 8 + 2, 77 = 64 + 8 + 4 + 1.) There are 2^32 = 4,294,967,296 possible addresses, and the whole address is just one big integer. That is how computers store it.
Addresses with a special meaning
Not every address is a normal public one. Learn these by heart; you will meet them daily:
- Private ranges (RFC 1918), never routed on the public Internet:
10.0.0.0/8,172.16.0.0/12(that is 172.16.0.0 to 172.31.255.255, not all of 172.x), and192.168.0.0/16. Your home Wi-Fi uses one of them, and NAT shares one public address among all those private ones. - Loopback:
127.0.0.0/8, usually127.0.0.1, "this very computer". - Link-local:
169.254.0.0/16, self-assigned when no DHCP server answers. Seeing one usually means "DHCP failed". - Shared address space:
100.64.0.0/10, used by ISPs for carrier-grade NAT. - Documentation:
192.0.2.0/24,198.51.100.0/24,203.0.113.0/24. Safe to use in examples, like in this lesson. - Multicast:
224.0.0.0/4. Unspecified:0.0.0.0. Limited broadcast:255.255.255.255.
CIDR and the subnet mask
In the early 1990s the Internet was running out of addresses because networks came only in three fixed sizes. CIDR (Classless Inter-Domain Routing) fixed that by letting the boundary between network and host sit at any bit. We write it as address/prefix length:
192.168.10.77/26
The first 26 bits are the network part, the remaining 32 - 26 = 6 bits identify the host. The same idea written as a mask is 26 one-bits followed by 6 zero-bits:
11111111.11111111.11111111.11000000 = 255.255.255.192
Two bit operations give you everything:
- network address = address AND mask (clear all host bits)
- broadcast address = network OR (NOT mask) (set all host bits)
- addresses in the subnet = 2^(32 - prefix); usable hosts = that number minus 2, because the network and broadcast addresses cannot be given to a host
Worked example for 192.168.10.77/26: the last octet is 77 = 01001101, the mask octet is 192 = 11000000, AND gives 01000000 = 64. So the network is 192.168.10.64; setting the 6 host bits gives 192.168.10.127 as the broadcast; the usable range is .65 to .126, which is 2^6 - 2 = 62 hosts.
A shortcut for exams: block size = 256 - mask octet = 256 - 192 = 64. Subnets start at multiples of 64 (0, 64, 128, 192), and 77 falls inside the block that starts at 64.
Handy sizes to remember: /24 = 256 addresses (254 usable), /25 = 128 (126), /26 = 64 (62), /27 = 32 (30), /28 = 16 (14), /29 = 8 (6), /30 = 4 (2). Two exceptions: a /31 has 2 addresses and both are usable on a point-to-point link (RFC 3021), and a /32 is exactly one host.
Run the same maths in code:
Compare the binary lines: the network address keeps the first 26 bits and zeroes the rest, the broadcast address keeps them and sets the rest to 1. Try prefix 24 or 30, or another address. Notice the >>> 0 after every operation: JavaScript bit operators work on signed 32-bit numbers, and >>> 0 turns the result back into an unsigned one.
// The whole subnet calculation is two bit operations on a 32-bit integer.
function toInt(ip) {
return ip.split(".").reduce(function (acc, octet) {
return ((acc << 8) | Number(octet)) >>> 0; // >>> 0 keeps the result unsigned
}, 0);
}
function toDotted(n) {
return [24, 16, 8, 0].map(function (shift) { return (n >>> shift) & 255; }).join(".");
}
function toBinary(n) {
return n.toString(2).padStart(32, "0").match(/.{8}/g).join(".");
}
const ip = toInt("192.168.10.77");
const prefix = 26;
const mask = (0xffffffff << (32 - prefix)) >>> 0;
const network = (ip & mask) >>> 0;
const broadcast = (network | ~mask) >>> 0;
console.log("address ", toBinary(ip), toDotted(ip));
console.log("mask ", toBinary(mask), toDotted(mask));
console.log("network ", toBinary(network), toDotted(network));
console.log("broadcast ", toBinary(broadcast), toDotted(broadcast));
console.log("usable hosts:", Math.pow(2, 32 - prefix) - 2);
Lab exercise 1
PythonGraded in Code LabBuild a CIDR calculator. Each input line is an address in CIDR notation. Use shifts and bit masks to compute the answer; do not use Python's ipaddress module (you will meet it in a moment).
Output, one line per input line:
network=<a.b.c.d> broadcast=<a.b.c.d> mask=<a.b.c.d> first=<a.b.c.d> last=<a.b.c.d> hosts=<n>
firstandlastare the first and last usable addresses, andhostsis how many there are: 2^(32 - prefix) - 2.- Special cases:
/31has two usable addresses (firstis the network address,lastthe broadcast address,hosts=2)./32is a single host: network, broadcast, first and last are all the address itself andhosts=1. - The address may have host bits set (
192.168.10.77/26); the network address is what you get after masking. - Invalid input (not four numbers 0-255, a prefix above 32, a missing prefix, anything else) prints
error: invalid CIDR. Blank lines are skipped.
The starter already parses the text and gives you dotted() to turn an integer back into a.b.c.d.
Example
Input:
192.168.10.77/26
Output:
network=192.168.10.64 broadcast=192.168.10.127 mask=255.255.255.192 first=192.168.10.65 last=192.168.10.126 hosts=62
- The worked example, /26
- Classic /8, /16 and /24 networks
- A boundary inside the third octet (/20)
- Tiny subnets: /30, /31 and /32
- The whole address space, /0
- Invalid input is reported and blank lines are skipped
Example run
- Input
192.168.10.77/26
- Expected output
network=192.168.10.64 broadcast=192.168.10.127 mask=255.255.255.192 first=192.168.10.65 last=192.168.10.126 hosts=62
import re
import sys
PATTERN = re.compile(r"(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})/(\d{1,2})", re.ASCII)
def parse_cidr(text):
"""Return (address, prefix) as integers, or None if the text is not a valid CIDR."""
m = PATTERN.fullmatch(text)
if m is None:
return None
a, b, c, d, prefix = (int(g) for g in m.groups())More lines in Code Lab: 23
Hints and instant feedback are in Code Lab. Optional practice: it does not block finishing the lesson.
JavaScript and Python also run in your browser without an account; other languages use the secure server runner and need a free account.
Is the destination on my network?
Every host with an address and a mask makes one decision before sending: is the destination in my own subnet? It ANDs the destination address with its own mask. If the result equals its own network address, the destination is on the same link and the host delivers directly (it finds the MAC address with ARP). Otherwise it hands the packet to the default gateway, which must be an address inside the host's own subnet.
Example: host 192.168.10.77/26, gateway 192.168.10.65. A packet to 192.168.10.100 stays local (it is inside 64-127). A packet to 192.168.10.200 is not (that is the 192-255 block), even though the first three octets match, so it goes to the gateway. A wrong mask is a classic cause of "I can reach some machines but not others".
Splitting a network into subnets
To make smaller subnets you borrow host bits and add them to the network part. Each borrowed bit doubles the number of subnets and halves the size of each. Splitting 192.168.10.0/24 by borrowing 2 bits gives four /26 subnets:
192.168.10.0/26 hosts .1 - .62
192.168.10.64/26 hosts .65 - .126
192.168.10.128/26 hosts .129 - .190
192.168.10.192/26 hosts .193 - .254
Equal-sized subnets waste addresses when needs differ: a point-to-point link between two routers needs 2 addresses, a floor of offices needs 100. VLSM (variable-length subnet masks) gives each subnet the smallest block that fits, and the usual method is: sort by size, largest first, and place the blocks one after another. Starting with the biggest keeps every block aligned on its own size boundary. Each block is a power of two that holds the hosts plus the network and broadcast addresses.
The reverse of splitting is summarisation: two adjacent, aligned subnets of equal size can be advertised as one bigger prefix. 192.168.0.0/24 and 192.168.1.0/24 become 192.168.0.0/23, which keeps routing tables small. (192.168.1.0/24 and 192.168.2.0/24 cannot be merged: that block does not start on a /23 boundary.)
Python ships with the ipaddress module, which does all of this for you once you understand the maths:
The library agrees with your hand calculation: four /26 subnets of 62 usable hosts, 192.168.10.100 is inside the 64-127 block and .200 is not. In real work you use the library (or ipcalc on the command line) and keep the bit-level maths for understanding and for checking surprising results.
import ipaddress
net = ipaddress.ip_network("192.168.10.0/24")
print("Splitting", net, "into /26 subnets:")
for sub in net.subnets(new_prefix=26):
print(" ", sub, "mask", sub.netmask, "usable", sub.num_addresses - 2)
host = ipaddress.ip_interface("192.168.10.77/26")
print("network of", host, "is", host.network)
print("192.168.10.100 is local:", ipaddress.ip_address("192.168.10.100") in host.network)
print("192.168.10.200 is local:", ipaddress.ip_address("192.168.10.200") in host.network)
print("merge two /24s:", ipaddress.ip_network("192.168.0.0/24").supernet())
print("172.20.5.9 private?", ipaddress.ip_address("172.20.5.9").is_private)
print("1.1.1.1 private?", ipaddress.ip_address("1.1.1.1").is_private)
Lab exercise 2
CGraded in Code LabWrite a VLSM planner in C. The first input line is the network you may use, in CIDR notation (a proper network address such as 192.168.10.0/24). Every following line is <name> <hosts>: a subnet that must hold that many hosts.
How to allocate:
- Sort the requests by
hosts, largest first. Requests with the same size keep their input order. - Each subnet gets the smallest block (a power of two, at least 4 addresses) that holds
hostsplus the network and broadcast addresses. - Place the blocks one after another starting at the network address.
- If a block no longer fits inside the network, print
<name> no spaceand carry on with the next request.
Output, one line per request in allocation order: <name> <network>/<prefix> hosts=<usable>, where usable is the block size minus 2.
The starter reads and sorts the input for you. You write the allocation loop.
Example
Input:
192.168.10.0/24
WAN 2
Sales 50
Office 100
Output:
Office 192.168.10.0/25 hosts=126
Sales 192.168.10.128/26 hosts=62
WAN 192.168.10.192/30 hosts=2
- Five subnets of different sizes (input is unsorted)
- The worked example from the prompt
- Out of space: the second request has no room
- 254 hosts fit a /24 but 255 need a /23
- Minimum block is a /30, even for one host
- Empty request list prints nothing
Example run
- Input
192.168.10.0/24 WAN-1 2 LAN-B 50 LAN-A 100 WAN-2 2 LAN-C 25
- Expected output
LAN-A 192.168.10.0/25 hosts=126 LAN-B 192.168.10.128/26 hosts=62 LAN-C 192.168.10.192/27 hosts=30 WAN-1 192.168.10.224/30 hosts=2 WAN-2 192.168.10.228/30 hosts=2
#include <stdio.h>
#include <stdlib.h>
#define MAX_REQUESTS 256
typedef struct {
char name[64];
unsigned long long hosts; /* hosts this subnet must hold */
int order; /* position in the input, used to break ties */
} Request;
/* Largest requirement first; equal sizes keep their input order. */More lines in Code Lab: 41
Hints and instant feedback are in Code Lab. Optional practice: it does not block finishing the lesson.
JavaScript and Python also run in your browser without an account; other languages use the secure server runner and need a free account.
How a router picks a route: longest-prefix match
A router's routing table maps destination prefixes to a next hop (or an outgoing interface):
destination next hop
0.0.0.0/0 203.0.113.1 (default route)
10.0.0.0/8 10.0.0.1
10.1.0.0/16 10.1.0.254
10.1.2.0/24 eth2
A packet for 10.1.2.77 matches all four lines, because every one of those prefixes contains it. The router must pick one, and the rule is longest-prefix match: the matching route with the largest prefix length wins, because it is the most specific. Here that is 10.1.2.0/24 (interface eth2). A packet for 10.1.9.9 matches the default route, the /8 and the /16, so it follows the /16. The default route 0.0.0.0/0 matches everything with prefix length 0, so it is only used when nothing more specific matches. Without any default route, an unmatched packet is dropped (the router usually answers with an ICMP "destination unreachable").
This is why aggregation is so powerful: a small ISP can announce one /16 while a customer's /24 inside it still gets its own more specific route. When two routes have the same prefix length, other rules (route type, metric) break the tie.
Let's implement the lookup yourself, with bit operations only.
Lab exercise 3
JavaScript (Node.js)Graded in Code LabImplement a routing-table lookup with longest-prefix match.
Input: the first line is N M. The next N lines are routes <CIDR> <next-hop> (the next hop is a single word: an address or an interface name; prefixes are proper network addresses; if two routes have identical prefixes the first one listed wins). The next M lines are destination IPv4 addresses.
Output: one line per destination: the next hop of the matching route with the longest prefix, or no route if no route matches. Use bit operations; do not scan strings.
Example
Input:
3 3
0.0.0.0/0 gw
10.0.0.0/8 core
10.1.2.0/24 eth2
10.1.2.7
10.9.9.9
172.16.0.1
Output:
eth2
core
gw
- Most specific route wins
- No default route: unmatched destinations have no route
- Host route (/32) and the edges of a /24
- Order in the table does not matter (a /12 containing a /18)
- Empty table: every lookup is no route
- A table with 201 routes
Example run
- Input
5 4 0.0.0.0/0 203.0.113.1 10.0.0.0/8 10.0.0.1 10.1.0.0/16 10.1.0.254 10.1.2.0/24 eth2 192.168.1.0/24 eth1 10.1.2.77 10.1.9.9 10.200.0.1 198.51.100.7
- Expected output
eth2 10.1.0.254 10.0.0.1 203.0.113.1
const lines = require("fs").readFileSync(0, "utf8").split("\n").map((l) => l.trim()).filter((l) => l !== "");
// "a.b.c.d" -> unsigned 32-bit integer
function toInt(ip) {
return ip.split(".").reduce((acc, octet) => ((acc << 8) | Number(octet)) >>> 0, 0);
}
if (lines.length > 0) {
const [n, m] = lines[0].split(/\s+/).map(Number);
const routes = [];
for (const line of lines.slice(1, 1 + n)) {More lines in Code Lab: 13
Hints and instant feedback are in Code Lab. Optional practice: it does not block finishing the lesson.
JavaScript and Python also run in your browser without an account; other languages use the secure server runner and need a free account.
A first look at IPv6
IPv4 has about 4.3 billion addresses, and the free pool at the top of the hierarchy ran out in 2011. IPv6 uses 128-bit addresses (about 3.4 x 10^38), written as eight groups of four hex digits:
2001:0db8:0000:0000:0000:0000:0000:0001
Two shortening rules make that readable:
- Drop leading zeros in each group:
0db8becomesdb8,0000becomes0. - Replace one run of all-zero groups with
::. You may do this only once per address, otherwise it would be ambiguous.
So the address above is 2001:db8::1. Prefixes work exactly like CIDR: 2001:db8:abcd:12::/64 means "the first 64 bits are the network". A normal LAN is a /64, and an organisation is typically given a /48 to /56 to split into many /64s.
Things to remember:
::1is loopback.fe80::/10is link-local: every interface has one automatically.fc00::/7is unique local (the private-ish range).2001:db8::/32is for documentation.ff00::/8is multicast.- IPv6 has no broadcast: it uses multicast instead. There is also no "network and broadcast address are unusable" rule.
- Because there are so many addresses, every device can have a public one and NAT is not needed. In practice most networks run IPv4 and IPv6 side by side (dual stack), so you need to understand both.
Question 1 of 5
How many usable host addresses does 192.168.10.0/27 contain?
Question 2 of 5
What is the network address of 172.16.37.200/20?
Question 3 of 5
Which of these addresses is NOT in a private (RFC 1918) range?
Question 4 of 5
A router has routes 10.0.0.0/8 via A, 10.20.0.0/16 via B and 0.0.0.0/0 via C. Where does a packet for 10.20.30.40 go?
Question 5 of 5
Which one is a correct shortened form of 2001:0db8:0000:0000:0000:0000:0000:0001?
Recap
- An IPv4 address is a 32-bit number; CIDR (
/prefix) says how many leading bits are the network. - network = address AND mask, broadcast = network OR NOT mask, usable hosts = 2^(32 - prefix) - 2 (except /31 and /32).
- Private ranges:
10.0.0.0/8,172.16.0.0/12,192.168.0.0/16. Know loopback, link-local and the documentation ranges too. - VLSM: sort requests largest first and place power-of-two blocks one after another. Summarisation merges aligned neighbours into a shorter prefix.
- Routers use longest-prefix match; the default route
0.0.0.0/0is the least specific. - IPv6: 128 bits,
::compression, /64 LANs, no broadcast.
Practice next: on Linux run ip addr and ip route and explain every line with what you learned here. Subnet a /24 for an imaginary office on paper, then check yourself with the lab code or Python's ipaddress. Then continue with TCP, UDP and ports.
Practice
Reading is not learning until you run something. Pick one.
Graded lab exercises
- Lab exercise 1PythonNot passed yet Try in Code Lab
- Lab exercise 2CNot passed yet Try in Code Lab
- Lab exercise 3JavaScript (Node.js)Not passed yet Try in Code Lab