Frontend Labs
⋯

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". lastAddr is "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 /30 or 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/big is needed because a single IPv6 /64 holds 2⁶⁴ addresses, which doesn't fit in a signed 64-bit integer.

Exercises:

  1. Run info on all of Set A and Set B from the practice above, and compare with your paper answers.
  2. Add a contains command: subnet contains 192.168.1.0/25 192.168.1.200 prints 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.
  3. Add -json output. 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. /24 is 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 /31 is used for point-to-point links between two routers and gives you 2 usable addresses (RFC 3021), and /32 means 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. /24 is 256 addresses; /16 is 65,536. Fewer network bits = more host bits = bigger network.

Check yourself

  1. What does the /24 in 192.168.1.50/24 actually count?
  2. How do you calculate block size from a mask value?
  3. Are 10.1.1.100/26 and 10.1.1.140/26 on the same network? Show your working.
  4. Why is a /25 network 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.com or subnetting.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: ipcalc on the command line; jodies.de/ipcalc in a browser.

Generated from real-life-guide-v1.md · 8 Oct 2026 16:36