Chapter 10 — Subnet masks: the part everyone finds hard
This is the most important chapter in Part 3. Read it twice if you need to. Once it clicks, it stays clicked forever.
In one sentence
The subnet mask marks where the network part of an address ends and the host part begins — and that single line determines who your machine can talk to directly.
Why it exists
From Chapter 9: an IP address has a network part and a host part. But
192.168.1.50 doesn't say where the split is. Is the network 192?
192.168? 192.168.1?
The mask answers that. It has to be stated separately, which is why you always see them together:
192.168.1.50 / 24 or 192.168.1.50
255.255.255.0
Those two lines mean exactly the same thing. Learn to read both.
The idea, in plain language
Line up the mask under the address. Where the mask is 1, that's network. Where it's 0, that's host.
address: 192 . 168 . 1 . 50
binary: 11000000.10101000.00000001.00110010
mask /24: 11111111.11111111.11111111.00000000
\_______________________/ \________/
NETWORK (24 ones) HOST (8 zeros)
So: network part = 192.168.1
host part = .50
/24 literally means "the first 24 bits are the network". That's all the
slash number is: a count of 1-bits in the mask. This is called CIDR notation
(Classless Inter-Domain Routing).
The four numbers you can now work out
For any address + mask, you can determine:
NETWORK ADDRESS -> set every host bit to 0 (names the network)
BROADCAST ADDRESS -> set every host bit to 1 ("everyone here")
FIRST USABLE -> network address + 1
LAST USABLE -> broadcast address - 1
HOW MANY HOSTS -> 2^(host bits) - 2
For 192.168.1.50/24:
host bits = 32 - 24 = 8
network: 192.168.1.0 (host bits all 0)
broadcast: 192.168.1.255 (host bits all 1)
usable: 192.168.1.1 to 192.168.1.254
count: 2^8 - 2 = 254 hosts
The shortcut: block size (use this, not binary)
Converting to binary every time is slow. Here is the method professionals actually use.
Step 1 — Memorise this table. It's the only thing you need to memorise in this entire guide.
| CIDR | Mask (last non-255 octet) | Block size | Usable hosts |
|---|---|---|---|
| /24 | 255.255.255.0 | 256 | 254 |
| /25 | 255.255.255.128 | 128 | 126 |
| /26 | 255.255.255.192 | 64 | 62 |
| /27 | 255.255.255.224 | 32 | 30 |
| /28 | 255.255.255.240 | 16 | 14 |
| /29 | 255.255.255.248 | 8 | 6 |
| /30 | 255.255.255.252 | 4 | 2 |
Block size = 256 − (the mask number). That's the whole trick.
/26 → mask 192 → block size 256 − 192 = 64.
Step 2 — The four-step method.
1. Find the block size: 256 - mask value
2. Count up in blocks from 0 until you pass your address;
the network address is the block you landed IN.
3. Broadcast = next network - 1
4. Usable = everything between them
Worked example 1: 192.168.1.130/26
/26 -> mask 255.255.255.192 -> block size = 256 - 192 = 64
Count in 64s: 0, 64, 128, 192, (256)
^^^
130 falls between 128 and 192, so it's in the 128 block.
NETWORK: 192.168.1.128
BROADCAST: 192.168.1.191 (next block 192, minus 1)
USABLE: 192.168.1.129 to 192.168.1.190
HOSTS: 62
Check: is 192.168.1.200 on the same network as 192.168.1.130? Count: 200 is in
the 192 block, 130 is in the 128 block. Different networks — those two devices
cannot talk directly; they need a router.
Worked example 2: 10.0.0.45/27
/27 -> mask 255.255.255.224 -> block size = 256 - 224 = 32
Count in 32s: 0, 32, 64, 96, 128, ...
^^
45 falls between 32 and 64.
NETWORK: 10.0.0.32
BROADCAST: 10.0.0.63
USABLE: 10.0.0.33 to 10.0.0.62
HOSTS: 30
Worked example 3: when the mask isn't in the last octet
172.16.20.100/22
/22 = 255.255.252.0
The "interesting" octet is the THIRD one (mask value 252).
Block size = 256 - 252 = 4 -- and we count in the THIRD octet.
Count in 4s in the third octet: 0, 4, 8, 12, 16, 20, 24, ...
^^
20 falls exactly on a boundary, so 20 IS the network.
NETWORK: 172.16.20.0
BROADCAST: 172.16.23.255 (next network is 172.16.24.0, minus 1)
USABLE: 172.16.20.1 to 172.16.23.254
HOSTS: 2^10 - 2 = 1022
How to find the interesting octet: it's the one where the mask is neither 255
nor 0. /22 = 255.255.252.0 → third octet.
Why this matters in practice: "can I reach it directly?"
Every time your machine sends a packet, it does this calculation (Chapter 6):
Is the destination's network part the same as MY network part?
YES -> it's a neighbour. ARP for it and send directly.
NO -> it's remote. Send to the default gateway (the router).
Get the mask wrong and this breaks in confusing ways. Classic symptom: you can ping some machines on your network but not others, and the internet works fine. Almost always a mask mismatch.
Practice (40 min) — do these on paper first
Set A — read the mask. For each, give network, broadcast, first/last usable, and host count. Answers in Appendix C.
1. 192.168.10.75 /24
2. 192.168.10.75 /26
3. 10.5.3.200 /25
4. 172.20.15.100 /28
5. 10.0.0.6 /30
6. 192.168.4.170 /27
7. 172.16.35.42 /23
8. 10.10.10.10 /22
Set B — are these two on the same network? Answers in Appendix C.
9. 192.168.1.10/24 and 192.168.1.200/24
10. 192.168.1.10/26 and 192.168.1.200/26
11. 10.0.0.100/25 and 10.0.0.130/25
12. 172.16.4.5/22 and 172.16.7.250/22
Set C — check your answers with a tool (only after doing them by hand):
# Install if needed: apt install ipcalc / brew install ipcalc
ipcalc 192.168.10.75/26
# Or use Python, which is everywhere:
python3 - <<'EOF'
import ipaddress
for cidr in ["192.168.10.75/24", "192.168.10.75/26", "10.5.3.200/25",
"172.20.15.100/28", "10.0.0.6/30", "192.168.4.170/27",
"172.16.35.42/23", "10.10.10.10/22"]:
n = ipaddress.ip_network(cidr, strict=False)
hosts = list(n.hosts())
print(f"{cidr:20} network={n.network_address} broadcast={n.broadcast_address}"
f" usable={hosts[0]}-{hosts[-1]} count={len(hosts)}")
EOF
Set D — check your own network:
ip -4 addr show # find your address and prefix, e.g. 192.168.1.50/24
ipcalc <your address>/<your prefix>
How many devices can your home network hold? Which addresses are usable?
Do not move on until you can do Set A without looking. Everything in Chapters 12–13 assumes it, and every networking job interview asks it.
Build it in Go (40 min) — a subnet calculator you can trust
Do the paper exercises above first. This program will check your answers, and writing it teaches you what the "four numbers" really are: bit operations on a 32-bit (or 128-bit) number.
Go's net/netip package represents addresses as small, comparable values
(netip.Addr, netip.Prefix). It handles both IPv4 and IPv6 and gives you
Masked(), Contains(), and Overlaps() for free. The program adds the parts
that netip leaves to you: the broadcast address, the usable host range, and
splitting a block into smaller ones.
Save as subnet/main.go:
// subnet: a subnet calculator and tiny IPAM tool built on net/netip.
//
// go run ./subnet info 192.168.10.77/26
// go run ./subnet split 10.0.0.0/16 24 # carve /24s out of a /16
// go run ./subnet overlap 10.0.0.0/16 10.0.128.0/20 172.16.0.0/12
// go run ./subnet alloc 10.0.0.0/16 22 10.0.0.0/24 10.0.4.0/22
package main
import (
"fmt"
"math/big"
"net/netip"
"os"
"strconv"
)
func main() {
if len(os.Args) < 3 {
fmt.Println("usage: subnet info|split|overlap|alloc <args>")
os.Exit(2)
}
var err error
switch os.Args[1] {
case "info":
err = info(os.Args[2])
case "split":
err = split(os.Args[2], os.Args[3])
case "overlap":
err = overlap(os.Args[2:])
case "alloc":
err = alloc(os.Args[2], os.Args[3], os.Args[4:])
default:
err = fmt.Errorf("unknown command %q", os.Args[1])
}
if err != nil {
fmt.Fprintln(os.Stderr, "error:", err)
os.Exit(1)
}
}
// info prints what Chapter 10 teaches you to work out by hand.
func info(s string) error {
p, err := netip.ParsePrefix(s)
if err != nil {
return err
}
host := p.Addr() // the address as typed, e.g. 192.168.10.77
p = p.Masked() // zero the host bits -> the network address
first, last := p.Addr(), lastAddr(p)
fmt.Printf("input %s\n", host)
fmt.Printf("network %s\n", p)
if host.Is4() {
fmt.Printf("netmask %s\n", mask4(p.Bits()))
fmt.Printf("broadcast %s\n", last)
}
total := new(big.Int).Lsh(big.NewInt(1), uint(host.BitLen()-p.Bits()))
usable := new(big.Int).Set(total)
if host.Is4() && p.Bits() <= 30 { // network + broadcast are not usable hosts
usable.Sub(usable, big.NewInt(2))
first, last = first.Next(), last.Prev()
}
fmt.Printf("host range %s - %s\n", first, last)
fmt.Printf("addresses %s total, %s usable\n", total, usable)
fmt.Printf("private? %v\n", host.IsPrivate())
return nil
}
// split carves a prefix into equal smaller prefixes (Chapter 11's design exercise).
func split(s, bitsStr string) error {
p, err := netip.ParsePrefix(s)
if err != nil {
return err
}
bits, err := strconv.Atoi(bitsStr)
if err != nil || bits < p.Bits() || bits > p.Addr().BitLen() {
return fmt.Errorf("new prefix length must be between /%d and /%d", p.Bits(), p.Addr().BitLen())
}
n := 0
for sub := netip.PrefixFrom(p.Masked().Addr(), bits); p.Contains(sub.Addr()); {
if n < 8 {
fmt.Println(sub)
}
n++
next := lastAddr(sub).Next()
if !next.IsValid() { // ran off the end of the address space
break
}
sub = netip.PrefixFrom(next, bits)
}
if n > 8 {
fmt.Printf("... %d subnets in total\n", n)
}
return nil
}
// overlap reports every pair of prefixes that overlap -- the check that would
// have saved the "two companies both picked 10.0.0.0/16" migration.
func overlap(args []string) error {
ps, err := parseAll(args)
if err != nil {
return err
}
clean := true
for i := range ps {
for j := i + 1; j < len(ps); j++ {
if ps[i].Overlaps(ps[j]) {
fmt.Printf("OVERLAP %s <-> %s\n", ps[i], ps[j])
clean = false
}
}
}
if clean {
fmt.Println("no overlaps")
}
return nil
}
// alloc finds the first free /bits block inside pool that overlaps none of
// the already-used prefixes. This is the core of every IPAM system.
func alloc(poolStr, bitsStr string, usedStr []string) error {
pool, err := netip.ParsePrefix(poolStr)
if err != nil {
return err
}
bits, err := strconv.Atoi(bitsStr)
if err != nil {
return err
}
used, err := parseAll(usedStr)
if err != nil {
return err
}
for cand := netip.PrefixFrom(pool.Masked().Addr(), bits); pool.Contains(cand.Addr()); {
free := true
for _, u := range used {
if cand.Overlaps(u) {
free = false
break
}
}
if free {
fmt.Println("allocated", cand)
return nil
}
next := lastAddr(cand).Next()
if !next.IsValid() {
break
}
cand = netip.PrefixFrom(next, bits)
}
return fmt.Errorf("pool %s has no free /%d", pool, bits)
}
func parseAll(ss []string) ([]netip.Prefix, error) {
var out []netip.Prefix
for _, s := range ss {
p, err := netip.ParsePrefix(s)
if err != nil {
return nil, err
}
out = append(out, p.Masked())
}
return out, nil
}
// lastAddr sets every host bit to 1: the broadcast address for IPv4.
func lastAddr(p netip.Prefix) netip.Addr {
b := p.Masked().Addr().AsSlice()
for i := p.Bits(); i < len(b)*8; i++ {
b[i/8] |= 1 << (7 - uint(i%8))
}
a, _ := netip.AddrFromSlice(b)
return a
}
func mask4(bits int) netip.Addr {
var b [4]byte
for i := 0; i < bits; i++ {
b[i/8] |= 1 << (7 - uint(i%8))
}
return netip.AddrFrom4(b)
}
Run it against the worked examples:
$ go run ./subnet info 192.168.10.77/26
input 192.168.10.77
network 192.168.10.64/26
netmask 255.255.255.192
broadcast 192.168.10.127
host range 192.168.10.65 - 192.168.10.126
addresses 64 total, 62 usable
private? true
$ go run ./subnet split 10.0.0.0/16 24
10.0.0.0/24
10.0.1.0/24
...
... 256 subnets in total
What to notice in the code:
p.Masked()is "AND the address with the mask".lastAddris "set every host bit to 1". The block-size shortcut from this chapter is the same bit arithmetic done in your head.- The usable count subtracts 2 only for IPv4 prefixes of
/30or larger. A/31(point-to-point links, RFC 3021) and a/32(a single host) don't sacrifice a network/broadcast address, and IPv6 has no broadcast at all. math/bigis needed because a single IPv6/64holds 2⁶⁴ addresses, which doesn't fit in a signed 64-bit integer.
Exercises:
- Run
infoon all of Set A and Set B from the practice above, and compare with your paper answers. - Add a
containscommand:subnet contains 192.168.1.0/25 192.168.1.200prints yes/no. This is the "can I reach it directly?" test from this chapter, the same decision your OS makes before choosing ARP or the gateway. - Add
-jsonoutput. You'll reuse it in Chapter 61's IPAM exercise.
Common confusions
- "/24 vs 255.255.255.0 — which should I use?" They're identical.
/24is the modern shorthand and is what you'll mostly see. Be able to read both. - "Why 'minus 2'?" Network address and broadcast address are reserved
(Chapter 9). Exception: a
/31is used for point-to-point links between two routers and gives you 2 usable addresses (RFC 3021), and/32means one single host. - "What about Class A/B/C?" Historical. Before 1993, the mask was implied by
the first octet. CIDR replaced this and masks are now always explicit.
Certification exams still mention classes; real networks don't care. If someone
says "a class C network", they usually just mean a
/24. - "Smaller /number = bigger network?" Yes, and it trips everyone up.
/24is 256 addresses;/16is 65,536. Fewer network bits = more host bits = bigger network.
Check yourself
- What does the
/24in192.168.1.50/24actually count? - How do you calculate block size from a mask value?
- Are
10.1.1.100/26and10.1.1.140/26on the same network? Show your working. - Why is a
/25network smaller than a/24?
Further reading
- Video (~20 min, the best free explanation): "Subnetting Mastery" series — Practical Networking (Ed Harmoush) on YouTube. Watch the whole series; it uses exactly the block-size method above and drills it properly.
- Video: "Subnetting Made Easy" — NetworkChuck. More energetic, same content.
- Practice site:
subnettingpractice.comorsubnetting.org— endless generated problems with answers. Do 20 of these. It's the only way. - Book: CompTIA Network+ Study Guide or Todd Lammle's CCNA Study Guide — both have excellent, drill-heavy subnetting chapters.
- Tool:
ipcalcon the command line;jodies.de/ipcalcin a browser.