RouteHardenHire us
Back to Networking Fundamentals
Networking Fundamentals · Part 3 of 12·Network Hardening··22 min read·introductory

IPv4 addressing and subnetting deep dive

IPv4 from first principles: CIDR, prefix math, route aggregation, RFC 1918, VLSM, and the subnetting mistakes operators keep repeating.

If you can do IPv4 subnet math fluently, you'll never be one of the people who freezes during incident response when someone asks "what's our prefix on rack 7?" — and you'll be much harder to bullshit in vendor sales calls. The mechanics are not difficult, but they reward thinking in binary, and almost every commercial training course teaches them as a memorization exercise. We're not going to do that here. We'll build subnet math from the ground up, then move into the operational side: route aggregation, address planning, the private-address bargain, and the misconceptions that produce most of the subnet-related production fires.

Prerequisites

You should also be comfortable converting between decimal and binary in your head for at least the first 8 bits. If "194 in binary" is not immediate, take ten minutes with a binary table before continuing — the rest of this module will go faster.

Learning objectives

By the end of this module you should be able to:

  1. Derive the network address, broadcast address, and usable host range from any IPv4 prefix without classful shortcuts.
  2. Explain why CIDR replaced classful addressing and how route aggregation suppresses global routing-table growth.
  3. Subdivide a parent prefix into variable-length child subnets that match specific host-count requirements (VLSM).
  4. Distinguish cleanly among addressing, forwarding, NAT, and firewalling — four things that get confused into "the same thing" in everyday speech.
  5. Run our CIDR calculator to sanity-check your work, and also do the math without it when you have to.

IPv4 addresses as scoped identifiers, not just dotted decimals

An IPv4 address is a 32-bit unsigned integer. That's it. The dotted-decimal form (10.0.0.1, 192.0.2.42) is a humans-only convention: each of the four numbers is one byte, written in decimal, separated by dots. The address itself is a single 32-bit quantity, no internal structure.

The integer form is sometimes useful. 10.0.0.1 written in binary is:

00001010 00000000 00000000 00000001

Or, packed as a single 32-bit number: 0x0A000001, decimal 167772161.

The common error is to treat the dots as if they were meaningful. They aren't — they're a typographical convenience. The 32-bit value is what gets compared, masked, and forwarded. When we talk about "the network part" and "the host part" of an address, we mean a notional split of those 32 bits at some bit boundary. The address itself doesn't encode where that boundary is. The split is announced by a separate piece of information called the prefix length (or subnet mask, the older notation).

This is why we write 10.0.0.1/24, not 10.0.0.1. The /24 says "the first 24 bits of this address are the network identifier; the remaining 8 are the host identifier." Two interfaces with the same first-24-bit pattern are on the same subnet; their hosts can talk directly via ARP and Ethernet. Two interfaces whose first-24-bit patterns differ are not on the same subnet; their hosts need a router between them.

Classful addressing and why it failed

Before CIDR, the prefix length was implicit in the address itself. The IPv4 specification (RFC 791, 1981) divided the address space into classes:

ClassLeading bitsFirst octet rangeImplied prefix
A0...0 – 127/8
B10...128 – 191/16
C110...192 – 223/24
D (multicast)1110...224 – 239
E (reserved)1111...240 – 255

Under this model, if you saw 10.x.x.x you knew it was a Class A and the network was 10.0.0.0/8. If you saw 192.168.x.x you knew it was Class C and the network was 192.168.x.0/24. The prefix length didn't have to be communicated separately because the address pattern told you.

This was elegant for 1981 and hopelessly broken by 1992. Two simultaneous failures killed it:

Address waste. A small organization that needed 500 hosts didn't fit in a Class C (256 addresses, ~254 usable). The next size up was a Class B (65,536 addresses, ~65,000 usable). They got a Class B and used 0.7% of it. Multiply by every mid-size organization on the internet and the available Class B space ran out.

Routing-table explosion. Backbone routers had to track every announced network individually. As the internet grew from a few hundred to a few hundred thousand routes, the table size grew linearly, and the routers of the day couldn't keep up. The Cisco AGS+ — the iconic backbone router of the late 1980s — couldn't hold the table.

CIDR, defined in RFC 4632, fixed both problems by abandoning fixed class boundaries. Any prefix length from /0 to /32 is allowed. Allocations can be exactly the size needed. Multiple allocations can be summarized into one larger advertised route. The class system survives only as a vestigial influence on default subnet masks in some commercial GUIs and as a teaching example.

Prefix lengths from first principles

The prefix length, written /n, says: "the first n bits of the 32-bit address identify the network. The last 32 − n bits identify hosts within that network."

For each prefix length there is a corresponding subnet mask: a 32-bit value with the first n bits set to 1 and the rest to 0. So /24 has the mask 255.255.255.0, written in binary as:

11111111 11111111 11111111 00000000

The mask is a piece of historical baggage. Modern documentation uses /n notation almost exclusively because it's shorter and unambiguous. But routers and operating systems still represent the mask as 32 bits internally; the two notations are equivalent.

A few common prefix lengths and their immediate properties:

PrefixMaskHosts (per RFC 791 rules)Typical use
/8255.0.0.016,777,214Whole regional allocations; rarely used as a single subnet
/16255.255.0.065,534Large enterprise; container/VM aggregates
/24255.255.255.0254Default office subnet
/27255.255.255.22430Small office / VLAN
/30255.255.255.2522Point-to-point router links (legacy)
/31255.255.255.2542Point-to-point router links (RFC 3021)
/32255.255.255.2551Loopback / single host

The "host count" math is 2^(32-n) − 2. The minus 2 reflects the long-standing rule that the all-zero host portion is the network address (used as an identifier, not assignable to a host) and the all-one host portion is the broadcast address (sends to everyone in the subnet). Both lose two host slots per subnet. There are exceptions — /31 and /32 — that we'll get to.

Network, broadcast, and host range derivation

Given an address A and a prefix length /n, you can derive everything you need to know about its subnet with three operations:

  1. Network address. Bitwise AND of A with the mask. This zeros out the host bits. The result is the all-zero-host-portion address — the canonical name of the subnet.
  2. Broadcast address. Bitwise OR of A with the inverse mask (i.e., a value with the host bits all set). This sets all host bits to 1.
  3. Usable host range. Network + 1 through Broadcast − 1.

Worked example: address 192.0.2.74 with prefix /27.

/27 mask is 27 ones followed by 5 zeros:

11111111 11111111 11111111 11100000  →  255.255.255.224

Address in binary:

11000000 00000000 00000010 01001010  →  192.0.2.74

AND them bit by bit:

11000000 00000000 00000010 01001010
11111111 11111111 11111111 11100000
-----------------------------------
11000000 00000000 00000010 01000000

Convert that last row back to dotted decimal:

11000000 = 192
00000000 = 0
00000010 = 2
01000000 = 64

So the network is 192.0.2.64. The mask /27 leaves 5 host bits, so the broadcast is the network with all 5 host bits flipped to 1 — that's 01000000 becoming 01011111 (decimal 95). Broadcast: 192.0.2.95. Usable hosts: 192.0.2.65 through 192.0.2.94 — a total of 30 addresses.

This is the math. There is no trick; you do this three times and your hands learn it. Once your hands have learned it, you can quickly check whether 192.0.2.100 is in the same subnet (no — 100 is past 95, it's in the next /27).

For everyday work, no one does this on paper anymore. The standard tool is the ipaddress module in Python (or our hosted CIDR calculator that does the same math live in the browser). But you should be able to do it on paper, because subnetting interviews still expect it and because it builds the intuition you need for the next two sections.

Special cases: /31 and /32

Two edge cases break the "minus 2 hosts" rule:

/32 identifies a single host. There are no host bits. Network and broadcast both equal the address. There is no usable host range; the host is the address. This is what loopback addresses use, what DNS A records resolving to a single endpoint represent, and what some routing-protocol mechanisms use for service IPs.

/31 allocates exactly 2 addresses, with no broadcast. This is, until recently, "impossible" by classic IPv4 rules — both addresses would be the network and broadcast, leaving 0 usable hosts. RFC 3021 (2000) explicitly redefines /31 for point-to-point links: there's no broadcast on a point-to-point link (it would just be the other end), so the broadcast address concept is dropped, and both addresses are usable. Modern routers running point-to-point links between sites use /31 to save the two addresses you'd otherwise burn on a /30.

/30 is the classical point-to-point allocation: 4 addresses, network + broadcast + 2 usable. It still works on every router but wastes addresses, and modern provisioning has largely shifted to /31. If your /30 count is up in the hundreds, switching to /31 recovers significant address space — RFC 3021 was originally motivated by exactly that situation.

Variable-length subnet masks (VLSM)

Real organizations don't have one giant subnet. They have a mix:

  • A server VLAN that needs ~50 addresses (round up to /25, 126 hosts).
  • Three Wi-Fi VLANs that need ~250 each (/24, 254 hosts).
  • A management VLAN that needs ~12 (/28, 14 hosts).
  • 8 point-to-point links to other sites (each /31, 2 hosts).

VLSM lets you cut a parent prefix into differently-sized children. The math is recursive: you take the parent, allocate a chunk for the largest child, then allocate from what's left for the next-largest, and so on.

Here's a worked plan inside 10.42.0.0/16 — a parent that gives us 65,534 addresses to play with:

10.42.0.0/16  parent (65,534 addresses)
├─ 10.42.0.0/22    /22 server farm (1,022 hosts) — biggest, allocate first
├─ 10.42.4.0/24    /24 staff Wi-Fi A (254 hosts)
├─ 10.42.5.0/24    /24 staff Wi-Fi B (254 hosts)
├─ 10.42.6.0/24    /24 guest Wi-Fi (254 hosts)
├─ 10.42.7.0/27    /27 management VLAN (30 hosts)
├─ 10.42.7.32/27   /27 IoT / printers (30 hosts)
├─ 10.42.7.64/31   /31 point-to-point link 1
├─ 10.42.7.66/31   /31 point-to-point link 2
├─ ... (continue)
└─ 10.42.128.0/17  unallocated, reserved for growth

Three principles that make this kind of plan good:

Allocate largest first. Subnets must align on their own size boundaries — a /24 must start at an address whose last 8 bits are zero. If you allocate small children first, you can paint yourself into a corner where there's no aligned space left for a large child later. Always plan top-down by size.

Leave room. The fact that you have 65,000 addresses doesn't mean you should consume them. Reserve at least half of any large parent for growth and unforeseen needs. Re-numbering a network is one of the highest-pain operational tasks an org can take on; planning to avoid it is cheap.

Document the plan. Write down what each prefix is for, the rationale for its size, and the unused-but-reserved space. A wiki page or a README in your IaC repo. The single biggest predictor of whether a network plan ages well is whether it's documented.

The Python script in the hands-on section will let you check that a VLSM plan has no overlapping prefixes — a class of error that looks fine on paper and breaks routing in production.

CIDR and route aggregation

CIDR did two things. The first was the bit-boundary subnet math we've been doing. The second is route aggregation — the operational reason the global internet routing table didn't blow up to a billion entries by 2010.

A regional ISP receives an allocation from its registry, say 203.0.113.0/22 (1,022 addresses). It carves that up into customer prefixes:

203.0.113.0/24    customer A
203.0.113.16/28   customer B
203.0.113.128/25  customer C
... etc

The ISP has four-or-so internal routes for the customer prefixes, but it doesn't advertise those individually to the global internet. Instead, it advertises the single aggregate 203.0.113.0/22 to BGP peers. The rest of the internet sees one route to the ISP, and the ISP handles all internal forwarding. If a packet bound for 203.0.113.50 arrives at the ISP's edge, the edge router uses longest-prefix match to find the most-specific internal route — 203.0.113.0/24 for customer A — and sends the packet there.

Aggregation works because subnets nest. A /22 contains four /24s. A /24 contains four /26s. Any prefix is the union of the prefixes one bit longer than it. As long as your ISP's carving stays inside the parent prefix, the parent can be advertised externally without losing any reachability.

Aggregation is also why routes have priorities. If 203.0.113.0/22 and 203.0.113.0/24 are both in the routing table — say, the /22 is a default learned from a peering session and the /24 is a more-specific override — the router picks the longer match for traffic that fits. Longest-prefix match is the rule that makes overlapping advertisements coherent. It's why a default route (0.0.0.0/0, the shortest possible match — every address fits) coexists with everything else; it's just the catch-all when no longer match exists.

RFC 1918 and the private-address bargain

Most organizations don't get a globally-unique IPv4 allocation; the address space ran out. So RFC 1918 (1996) carved out three ranges that anyone is allowed to use internally without coordination:

  • 10.0.0.0/8 — 16.7 million addresses
  • 172.16.0.0/12 — 1 million addresses
  • 192.168.0.0/16 — 65,000 addresses

Internet routers are configured to never forward packets with these source or destination addresses. They're guaranteed not to collide with any public assignment because nothing public is assigned in those ranges. So your home network and your office network and a thousand other organizations all use 192.168.1.0/24 simultaneously and nobody notices.

The bargain RFC 1918 strikes is real. You get to have IP networking without paying for or applying for address space. You give up:

Direct end-to-end addressability. A host with a private IP can't be reached from outside your network without NAT (network address translation) or a VPN between networks. The internet's original promise — that any two endpoints could speak directly — does not apply to private IPs.

Renumbering pain when contexts change. If you decide to merge two organizations both using 10.0.0.0/16, you have a renumbering project on your hands. The pain compounds with every service that hard-codes IPs in configs, every firewall rule that references specific subnets, every database whose ACL is written in CIDR. A common architecture decision — "let's use 10.42.0.0/16 for our private prefix" — locks you out of every other organization that already used 10.0.0.0/16 if you ever need to peer or merge.

Operational dependence on NAT. Your hosts can talk to the internet only because somewhere on the way out, a NAT (typically your home router or your cloud's VPC gateway) is rewriting source addresses. NAT is a band-aid, and it has real costs: protocol fragility, application quirks, latency, scale ceilings. We'll cover NAT in detail in Module 1.12.

The pragmatic choice for almost every organization is still RFC 1918 + NAT for outbound traffic. The cost is modest. But it's not free, and pretending it's free leads to architecture that gets surprised later.

A note on the recently-added RFC 6598 range, 100.64.0.0/10. This is "carrier-grade NAT" (CGN) space, allocated in 2012 specifically for ISPs that have run out of public addresses and need a private range that won't collide with their customers' private ranges. If you're a regular operator, treat it as another private range with the same constraints. If you're an ISP, treat it as the only option.

Special-purpose ranges you actually need to know

Beyond the RFC 1918 ranges, several IPv4 ranges have specific reserved meanings and should never appear in unexpected places:

RangeMeaning
0.0.0.0/8"this network" — should never appear as a source or destination on the wire
127.0.0.0/8Loopback (localhost). Internal to the host.
169.254.0.0/16Link-local. Used when DHCP fails.
224.0.0.0/4IPv4 multicast. Class D in old terminology.
192.0.2.0/24, 198.51.100.0/24, 203.0.113.0/24Documentation. Use these in examples — they're guaranteed never to be assigned.
100.64.0.0/10CGNAT. ISP-internal use.
255.255.255.255/32Limited broadcast. Sends to everyone on the local segment.

The documentation ranges are particularly useful when you're writing examples in code or articles. They're guaranteed never to belong to a real customer, so traffic to them won't unexpectedly hit a third party.

NAT is not subnetting

This is the single most-confused pair of concepts in IPv4 operations, so we'll spell it out:

  • Subnetting is the partitioning of an address space into smaller blocks, each of which can be allocated to a network segment. It's a planning activity. It changes nothing about packet contents at runtime.
  • Forwarding is the act of moving a packet from one network to another based on a routing table. It uses the destination address as a lookup key. It changes nothing about the packet contents (other than decrementing TTL).
  • NAT is a runtime rewriting of packet headers — usually source address and source port — as packets cross a boundary. It introduces state (the NAT table) that must be maintained per flow.
  • Firewalling is policy enforcement: deciding which packets are allowed and which are dropped. It can use addresses, ports, protocols, or any combination as predicates.

These four activities are independent. You can subnet without NAT. You can NAT without firewalling. You can firewall without NAT. They are often combined in the same device — a typical home router does all four — but they do different things and have different failure modes. Conflating them is what produces "we need to subnet differently to allow this traffic" when the actual problem is a missing firewall rule.

A useful exercise: when an engineer at your company says "the IP address can't reach the server," figure out which of the four they're actually talking about. Subnetting? "It's not in any subnet that has a route." Forwarding? "There's no route in the table for that destination." NAT? "The translation table doesn't have an entry, or the rewrite is wrong." Firewalling? "An ACL is dropping the packet on the way through." Each has a different debugging approach, and the wrong diagnosis will burn a lot of time.

A practical subnetting workflow

A workable sequence for laying out a new network from scratch:

Step 1: collect requirements. What segments do you need? Servers, staff, IoT, guests, point-to-points, management, container networks. For each, how many hosts (round up generously)? What ACL boundaries should be visible at the IP layer (so policy is easy to write)? What spaces are you reserving for growth?

Step 2: pick a parent prefix. RFC 1918, large enough. 10.0.0.0/8 is the standard "give me lots of room" pick; 10.42.0.0/16 is more typical for a single site. Avoid 192.168.0.0/16 for any organization expecting to grow or peer — it's the most-collided range on the planet.

Step 3: allocate top-down by size. Largest segments first, smallest last. Align each child on its own size boundary. Use our CIDR calculator or python3 -c "import ipaddress; ..." to confirm your math.

Step 4: build in headroom. For every type of segment you have, reserve at least one more of the same size. For the parent overall, reserve at least 50%.

Step 5: document. A README, a wiki page, an IaC repo with comments — anywhere persistent. Name each prefix, include its size justification, and mark the reserved space clearly. The most expensive subnetting mistakes are made by the engineer who joins after the original planner left.

Step 6: codify in tools. Most organizations end up with their network plan in code — Terraform, Pulumi, network configuration generators. The codified plan should match the documented plan; drift between them is a yellow flag.

A network plan that follows this sequence has a reasonable chance of lasting 5–10 years without painful renumbering. One that skips even one step typically breaks within 18 months.

Hands-on exercise

Exercise 1 — Use Python to derive subnet facts

The ipaddress module in Python's standard library has the entire arithmetic baked in. Save and run:

import ipaddress

examples = [
    "192.0.2.74/27",
    "10.42.0.0/22",
    "172.16.5.0/16",
    "203.0.113.4/31",
    "198.51.100.42/32",
]

for cidr in examples:
    net = ipaddress.ip_network(cidr, strict=False)
    print(f"\n{cidr}")
    print(f"  network:        {net.network_address}")
    print(f"  broadcast:      {net.broadcast_address}")
    print(f"  netmask:        {net.netmask}")
    print(f"  prefix:         /{net.prefixlen}")
    print(f"  total addrs:    {net.num_addresses}")
    if net.prefixlen >= 31:
        # /31 and /32 don't use the standard host-count formula.
        print(f"  usable hosts:   {list(net.hosts()) or '(special, no host range)'}")
    else:
        hosts = list(net.hosts())
        print(f"  first usable:   {hosts[0]}")
        print(f"  last usable:    {hosts[-1]}")
        print(f"  usable count:   {len(hosts)}")

Run it. Notice that for /27 you get 30 usable hosts (the math we did by hand); for /31 you get the special two-address case; for /32 you get one address that's both network and broadcast.

Exercise 2 — Plan a small office network

Allocate prefixes for a small office out of 10.42.0.0/16. Requirements:

  • 1 server VLAN, ~30 hosts now, growing to ~100 in 2 years.
  • 2 staff Wi-Fi VLANs, ~200 hosts each.
  • 1 guest Wi-Fi VLAN, ~150 hosts.
  • 1 management VLAN, ~8 hosts.
  • 4 point-to-point links to remote offices.
  • Reserve at least half of 10.42.0.0/16 for growth.

Write down your allocation. Then check it with this Python script (it detects overlaps):

import ipaddress

# Replace with your plan.
plan = {
    "servers":         "10.42.0.0/25",   # /25 = 126 hosts, room for 100
    "staff-wifi-a":    "10.42.1.0/24",
    "staff-wifi-b":    "10.42.2.0/24",
    "guest-wifi":      "10.42.3.0/24",
    "management":      "10.42.4.0/28",
    "p2p-link-1":      "10.42.4.16/31",
    "p2p-link-2":      "10.42.4.18/31",
    "p2p-link-3":      "10.42.4.20/31",
    "p2p-link-4":      "10.42.4.22/31",
    "future-growth":   "10.42.128.0/17",
}

nets = {name: ipaddress.ip_network(cidr) for name, cidr in plan.items()}
for a_name, a_net in nets.items():
    for b_name, b_net in nets.items():
        if a_name >= b_name:
            continue
        if a_net.overlaps(b_net):
            print(f"OVERLAP: {a_name} ({a_net}) and {b_name} ({b_net})")

print("done")

If the script prints nothing but done, your plan has no overlaps. If you see OVERLAP lines, the plan needs revision.

A common mistake: putting the management VLAN at 10.42.4.0/24 and the p2p links at 10.42.4.0/30, which silently overlaps the larger management range. The script catches it; the human eye often doesn't.

Common misconceptions

"A /24 is the default normal subnet." It's common, not privileged. Use the prefix length the requirements justify. A /24 for a 4-host VLAN wastes 250 addresses and can produce more broadcast noise than a tighter /29 would.

"Subnetting is just about host counts." Host counts are one input. Other equally-important inputs: failure-domain boundaries (you don't want a single bad host to take out a 1,000-host VLAN), ACL boundaries (firewall rules are easier when they line up with subnet boundaries), summarization (can the design be aggregated cleanly?), and growth (can you add a /24 next to the current one without re-numbering?).

"Private IPv4 space is secure because it isn't public." RFC 1918 addresses are unrouted on the public internet — that's it. Inside your network, every host on a private subnet can talk to every other one with the same impunity it would have on a public network, unless you've placed actual firewalling between them. Internal exposure still needs real security policy.

"NAT and subnetting are the same thing." NAT rewrites packet headers at runtime. Subnetting partitions address space at planning time. They have entirely different mechanisms, failure modes, and implications. Confusing them produces architecture that doesn't survive contact with reality.

"A /31 cannot be used because there are no host addresses." RFC 3021 explicitly enables /31 point-to-point links by dropping the broadcast-address concept. Both addresses in the /31 are usable hosts. Modern routers support this universally; if your equipment doesn't, it's old enough to budget replacing.

Further reading

  1. RFC 791 — Internet Protocol. The original IPv4 specification. Worth reading once for the historical perspective on what address fields were and weren't designed to do.
  2. RFC 4632 — Classless Inter-domain Routing (CIDR). The clearest short document on why prefix aggregation matters operationally and how it changed the routing landscape.
  3. RFC 1918 — Address Allocation for Private Internets. The defining statement of the private-address bargain and its consequences.
  4. RFC 3021 — Using 31-Bit Prefixes on IPv4 Point-to-Point Links. A short, well-motivated deviation from classic IPv4 rules that's now the dominant practice for point-to-point links.
  5. Larry Peterson and Bruce Davie, Computer Networks: A Systems Approach, book.systemsapproach.org. The internet chapter is the cleanest bridge between subnet math and how routers actually behave with the resulting prefixes.
  6. RouteHarden's CIDR calculator. Real-time browser-side, no data leaves the page. Useful for sanity-checking allocations as you design.

The next module — IPv6 fundamentals — picks up by showing how IPv6 abandoned almost every IPv4 design choice we covered here, and what changed because of it.