Networking Fundamentals · intermediate · ~12 min
By the end of this lesson you will be able to: - Read a CIDR block such as `192.168.1.0/24` and say exactly which addresses it covers. - Convert a prefix length into a subnet mask and back again. - Compute the **network address**, **broadcast address**, and **usable host range** of any IPv4 block. - Count how many total and usable host addresses a subnet holds. - Test whether a given IP falls *inside* a CIDR block using bitwise mask-and-compare. - Split (subnet) a larger block into several smaller equal-sized blocks.
In the previous lesson, IP addresses: IPv4, IPv6, public vs private, you learned that an IPv4 address is 32 bits, usually written as four dotted decimal numbers like 192.168.1.10. That lesson treated an address as a single identifier. This lesson zooms out: it looks at groups of addresses that belong together on the same network.
A subnet (short for sub-network) is a contiguous range of IP addresses that share a common leading bit pattern. Instead of listing every address, we describe the whole group with two facts: a starting address and how many leading bits they all share. That shared-bit count is the prefix length, and the compact way to write it is CIDR notation: address/prefix, for example 10.0.0.0/24.
Plain-language version: think of an IP address like a full street address. The prefix is like saying "everyone on Maple Street, house numbers 0 through 255." The network part (Maple Street) is fixed for the whole block; the host part (the house number) varies from one machine to the next.
Once you can read CIDR, three everyday questions become simple arithmetic: How big is this block? What is its first and last address? and Is this particular IP inside it? Every router, firewall, cloud security group, and network scanner answers those questions constantly. CIDR is the language they all speak, so learning it unlocks the rest of networking.
Network scope is almost always handed to you as CIDR blocks, never as a hand-written list of addresses. A cloud provider gives your virtual network a range like 172.31.0.0/16. A firewall rule allows or blocks 203.0.113.0/24. An authorized security assessment defines its boundaries as a set of CIDR blocks. If you cannot read CIDR fluently, you cannot reason about any of these.
Two questions come up over and over:
10.1.0.5 versus a scope of 10.0.0.0/16, may be completely outside the block. Getting the bit math right is the difference between in-scope and out-of-scope.Because this single skill sits underneath routing, firewalls, cloud networking, and authorization boundaries, it pays off in almost every networking task you will ever do.
Definition. The prefix length is the number of leading bits of a 32-bit IPv4 address that identify the network. Everything after those bits identifies the host.
Plain-language explanation. Write the 32-bit address out as a row of bits. Draw a line after the first N bits, where N is the prefix. Bits to the left of the line are the network number and are identical for every address in the block. Bits to the right are free to vary, one combination per host.
How it works internally. 192.168.1.0/24 means the first 24 bits are fixed. 32 - 24 = 8 host bits remain, so there are 2^8 = 256 possible combinations of those 8 bits, and therefore 256 addresses in the block.
192.168.1.0/24
11000000 10101000 00000001 | 00000000
\___________ network (24) ____________/ | \_host (8)_/
fixed for whole block varies 0..255
When to use / when not to. Prefix length is the standard way to describe any block. Smaller prefix = bigger block (/8 is huge); larger prefix = smaller block (/30 holds just 4 addresses). Do not confuse "bigger number" with "bigger network" — it is the opposite.
Common pitfall. Forgetting that the prefix counts bits, not the third dotted number. /24 is not "class 24"; it is 24 one-bits.
Knowledge check: In the block
10.5.0.0/20, how many host bits are there, and how many total addresses does the block contain?
Definition. A subnet mask is a 32-bit value with a run of 1 bits (one per network bit) followed by 0 bits for the host part. It is the prefix expressed as a bit pattern.
Plain-language explanation. The mask is a stencil. Where the mask has a 1, that bit belongs to the network. Where it has a 0, that bit belongs to the host. AND-ing an address with the mask keeps only the network bits and zeroes the rest.
How it works internally. A /24 mask is 24 ones then 8 zeros: 11111111 11111111 11111111 00000000, which in dotted decimal is 255.255.255.0. You can build the mask from the prefix with a shift: mask = 0xFFFFFFFF << (32 - prefix) (using 32-bit unsigned math).
| CIDR | Mask | Total addresses | Usable hosts |
|---|---|---|---|
| /30 | 255.255.255.252 | 4 | 2 |
| /29 | 255.255.255.248 | 8 | 6 |
| /24 | 255.255.255.0 | 256 | 254 |
| /16 | 255.255.0.0 | 65,536 | 65,534 |
| /8 | 255.0.0.0 | 16,777,216 | 16,777,214 |
When to use / when not to. Masks and prefixes are two spellings of the same thing; use whichever the tool in front of you expects. Older gear and some config files use dotted masks; modern tools use /prefix.
Common pitfall. A valid mask is always a contiguous run of ones followed by zeros. 255.255.0.255 is not a legal subnet mask because the ones are not contiguous.
Knowledge check: What prefix length does the mask
255.255.255.192correspond to? (Hint: how many one-bits is that?)
Definition. The network address is the first address of a block — all host bits set to 0. The broadcast address is the last address — all host bits set to 1. Neither is normally assigned to an individual machine.
Plain-language explanation. The network address names the block itself ("this is network 10.0.0.0/24"). The broadcast address is a special "everyone on this subnet" destination. The addresses between them are the ones you can hand out to real hosts.
How it works internally. For 10.0.0.0/24:
10.0.0.0 network address (host bits 00000000)
10.0.0.1 first usable host
...
10.0.0.254 last usable host
10.0.0.255 broadcast address (host bits 11111111)
Network = ip & mask. Broadcast = network | ~mask (network bits kept, host bits all set).
When to use / when not to. The "minus two" rule (subtract network + broadcast) applies to normal IPv4 subnets. Two exceptions: a /31 is used for point-to-point links and, by RFC 3021, has 2 usable addresses (no separate broadcast); a /32 is a single host with no host bits at all.
Common pitfall. Assuming the broadcast address always ends in .255. It only does for /24. For 10.0.0.0/26 the broadcast is 10.0.0.63, not 10.0.0.255.
Knowledge check: For
192.168.1.0/26, what are the network address, the broadcast address, and the usable host range?
Definition. An address belongs to a block when masking the address gives the same result as masking the block's network address.
Plain-language explanation. Strip off the host bits of both the candidate IP and the block. If the network parts that remain are identical, the IP is inside the block; otherwise it is outside.
How it works internally. The whole test is one line of bit math:
(ip & mask) == (network & mask)
Since network already has zero host bits, this is usually written (ip & mask) == network.
When to use / when not to. This is the exact check inside every firewall rule, routing table lookup, and cloud security group. Use it any time you must decide "in scope or not." Do not eyeball it by comparing dotted numbers — the boundaries rarely line up with the dots.
Common pitfall. Building the mask with a shift of 32 (for a /0 block) is undefined behaviour in C when you write 1u << 32. Guard the edge cases.
Knowledge check: Is
10.0.1.20inside10.0.0.0/23? Work it out by finding the block's address range first.
Definition. Subnetting borrows bits from the host part to create several smaller networks inside a larger one.
Plain-language explanation. Take a /24 (256 addresses). Borrow 2 host bits and you get 2^2 = 4 equal sub-blocks, each a /26 with 64 addresses. The prefix grows by the number of borrowed bits.
How it works internally. Splitting 192.168.1.0/24 into four /26 blocks:
192.168.1.0/26 -> .0 .. .63
192.168.1.64/26 -> .64 .. .127
192.168.1.128/26 -> .128 .. .191
192.168.1.192/26 -> .192 .. .255
Each block's size is 2^(32 - newprefix), and that size is also the step between one block's network address and the next.
When to use / when not to. Subnet to isolate groups (guests vs. servers), to match address use to need, or to shrink broadcast domains. Do not over-subnet tiny networks — every subnet loses two addresses to network + broadcast.
Common pitfall. Choosing a network address that is not aligned to the block size. A /26 must start on a multiple of 64 (.0, .64, .128, .192); 192.168.1.32/26 is not a valid block base.
CIDR notation is simply address/prefix:
192.168.1.0 / 24
|__________| |__|
network prefix length (network bits, 0..32)
The core computations, expressed as C-style bitwise operations on a 32-bit unsigned integer ip:
uint32_t mask = (prefix == 0) ? 0u : (0xFFFFFFFFu << (32 - prefix));
uint32_t network = ip & mask; // clear host bits
uint32_t broadcast = network | ~mask; // set all host bits
uint32_t hosts = (prefix >= 31) ? 0u : ((1u << (32 - prefix)) - 2u); // usable
int inside = ((candidate & mask) == network);
The (prefix == 0) guard matters: shifting a 32-bit value by 32 is undefined behaviour in C, so the /0 case must be handled separately.
Subnetting splits an address space into smaller networks.
CIDR (Classless Inter-Domain Routing) notation writes a network as address/prefix. The prefix is how many leading bits form the network part.
192.168.1.0/24 means the first 24 bits are the network. The remaining 8 bits identify hosts.
255.255.255.0Two addresses are subtracted: the first is the network address and the last is the broadcast address.
| CIDR | Mask | Addresses |
|---|---|---|
| /30 | 255.255.255.252 | 4 (2 usable) |
| /24 | 255.255.255.0 | 256 |
| /16 | 255.255.0.0 | 65,536 |
| /8 | 255.0.0.0 | 16,777,216 |
For 10.0.0.0/24:
10.0.0.010.0.0.25510.0.0.1 to 10.0.0.254A scope of 10.10.0.0/16 covers about 65,000 hosts. You plan that scan very differently than a small /29.
"Is this IP inside that CIDR?" is the most common scoping check, and it is pure bit masking:
(ip & mask) == (network & mask)
#include <stdio.h>
#include <stdint.h>
/* Build a subnet mask from a prefix length (0..32). Guards the shift-by-32
case, which is undefined behaviour for a 32-bit value. */
static uint32_t make_mask(int prefix) {
if (prefix <= 0) return 0x00000000u;
if (prefix >= 32) return 0xFFFFFFFFu;
return 0xFFFFFFFFu << (32 - prefix);
}
/* Print a uint32_t IP in dotted-decimal form. */
static void print_ip(uint32_t ip) {
printf("%u.%u.%u.%u",
(ip >> 24) & 0xFFu,
(ip >> 16) & 0xFFu,
(ip >> 8) & 0xFFu,
ip & 0xFFu);
}
/* Returns 1 if ip is inside network/prefix, else 0. */
static int cidr_contains(uint32_t network, int prefix, uint32_t ip) {
uint32_t mask = make_mask(prefix);
return (ip & mask) == (network & mask);
}
int main(void) {
/* 192.168.1.0/26 -> bytes packed into one 32-bit value */
uint32_t base = (192u << 24) | (168u << 16) | (1u << 8) | 0u;
int prefix = 26;
uint32_t mask = make_mask(prefix);
uint32_t network = base & mask; /* first address */
uint32_t broadcast = network | ~mask; /* last address */
uint32_t total = (prefix >= 31) ? (1u << (32 - prefix))
: (1u << (32 - prefix));
uint32_t usable = (prefix >= 31) ? 0u : (total - 2u);
printf("Block: ");
print_ip(network); printf("/%d\n", prefix);
printf("Netmask: "); print_ip(mask); printf("\n");
printf("Network: "); print_ip(network); printf("\n");
printf("Broadcast: "); print_ip(broadcast); printf("\n");
printf("First host:"); printf(" "); print_ip(network + 1); printf("\n");
printf("Last host: "); print_ip(broadcast - 1); printf("\n");
printf("Total: %u Usable: %u\n", total, usable);
/* Membership tests */
uint32_t a = (192u << 24) | (168u << 16) | (1u << 8) | 40u; /* .40 */
uint32_t b = (192u << 24) | (168u << 16) | (1u << 8) | 90u; /* .90 */
printf("192.168.1.40 inside? %d\n", cidr_contains(network, prefix, a));
printf("192.168.1.90 inside? %d\n", cidr_contains(network, prefix, b));
return 0;
}
What it does. It packs 192.168.1.0 into a single 32-bit integer, builds the /26 mask, then derives the network address, broadcast address, usable range, and address counts using only bit operations. Finally it tests two addresses for membership.
Expected output:
Block: 192.168.1.0/26
Netmask: 255.255.255.192
Network: 192.168.1.0
Broadcast: 192.168.1.63
First host: 192.168.1.1
Last host: 192.168.1.62
Total: 64 Usable: 62
192.168.1.40 inside? 1
192.168.1.90 inside? 0
Key edge cases. A /26 block spans .0–.63, so 192.168.1.90 (in the next block, .64–.127) is correctly reported outside. For /31 and /32, the usable-host formula would go negative with the naive total - 2, which is why the code branches on prefix >= 31.
make_mask(prefix) — turns a prefix into a 32-bit mask. The guards matter: prefix <= 0 returns all zeros (a /0 matches everything), and prefix >= 32 returns all ones, because 0xFFFFFFFFu << 0 is fine but shifting by 32 is undefined. In between, 0xFFFFFFFFu << (32 - prefix) slides the ones left and fills the low bits with zeros.
Packing the base address — (192u << 24) | (168u << 16) | (1u << 8) | 0u places each dotted byte into its 8-bit slot, producing 0xC0A80100. Working on one integer lets us use plain arithmetic and bit ops.
mask = make_mask(26) — 26 ones then 6 zeros = 0xFFFFFFC0 = 255.255.255.192.
network = base & mask — clears the low 6 host bits. base already ends in .0, so network stays 192.168.1.0.
broadcast = network | ~mask — ~mask is 0x0000003F (the 6 host bits set), so OR-ing sets the host part to all ones: 192.168.1.63.
Counts — 1u << (32 - 26) = 1u << 6 = 64 total addresses; usable = 64 - 2 = 62.
print_ip — pulls each byte back out with a shift and & 0xFF and prints it dotted.
cidr_contains(network, 26, a) — for a = 192.168.1.40: a & mask clears the host bits to give 192.168.1.0, which equals network, so it returns 1. For b = 192.168.1.90: b & mask gives 192.168.1.64 (because bit value 64 is a network bit at /26), which does not equal 192.168.1.0, so it returns 0.
| Step | Expression | Value (hex) | Dotted |
|---|---|---|---|
| base | packed | 0xC0A80100 | 192.168.1.0 |
| mask | /26 | 0xFFFFFFC0 | 255.255.255.192 |
| network | base & mask | 0xC0A80100 | 192.168.1.0 |
| broadcast | network | ~mask | 0xC0A8013F | 192.168.1.63 |
| a & mask | .40 masked | 0xC0A80100 | 192.168.1.0 (inside) |
| b & mask | .90 masked | 0xC0A80140 | 192.168.1.64 (outside) |
Mistake 1: Assuming the broadcast always ends in .255.
WRONG: "192.168.1.0/26 broadcasts to 192.168.1.255"
That is the broadcast of a /24, not a /26. Why it is wrong: a /26 has only 6 host bits, so the block ends after 64 addresses.
RIGHT: 192.168.1.0/26 broadcasts to 192.168.1.63
Prevent it by always computing network | ~mask instead of guessing from the dots.
Mistake 2: Off-by-one on usable hosts (forgetting the minus two).
Saying a /24 holds 256 usable hosts. It holds 256 addresses, but the network (.0) and broadcast (.255) are not assignable, leaving 254 usable. Correct: usable = total - 2 for normal subnets.
Mistake 3: Confusing bigger prefix with bigger network.
Thinking /28 is larger than /24. It is the reverse: more prefix bits means fewer host bits means a smaller block. /24 = 256 addresses, /28 = 16.
Mistake 4: Shifting a 32-bit value by 32.
uint32_t mask = 0xFFFFFFFFu << (32 - prefix); // breaks when prefix == 0
When prefix == 0, the shift amount is 32, which is undefined behaviour in C and may produce garbage. Fix by special-casing prefix == 0 (return 0) as make_mask does.
Mistake 5: Using a mis-aligned network base.
Writing 192.168.1.32/26 as if it were a block base. A /26 must start on a multiple of 64. Masking .32 with the /26 mask actually snaps it back to 192.168.1.0, so the "block" you meant is not the block you get. Recognise this by checking that base & mask == base.
Compiler warnings. Compile with -Wall -Wextra. A warning like "left shift count >= width of type" is the compiler telling you a shift is 32 or more — the /0 or /32 edge case. Add the guard.
Signed vs unsigned surprises. If your mask math produces sign-extended garbage (huge negative dotted numbers), you probably used int instead of uint32_t, or ~mask on a signed value. Use uint32_t/unsigned throughout.
Membership test always returns the same answer. If every IP tests as "inside," your mask is probably all zeros — check that make_mask received the prefix you think it did, and that you did not pass a prefix of 0 by accident.
Wrong broadcast address. Print the mask in dotted form and confirm it has the expected number of one-bits. A mask like 255.255.255.0 for a /26 means you built the mask for the wrong prefix.
Questions to ask when it doesn't work:
ip & mask), or compare raw addresses?/0, /31, and /32 specially?base & mask == base)?Cross-check by hand or tool. ipcalc 192.168.1.0/26 (a common Linux utility) prints the network, broadcast, and host range so you can confirm your program's output.
This lesson is concept-track, but the natural implementations are in C and the same bit math powers real firewall and routing code, so the robustness concerns are worth stating precisely.
Undefined behaviour from over-wide shifts. In C, shifting a 32-bit value by 32 or more bits is undefined. Both 0xFFFFFFFFu << (32 - prefix) (when prefix == 0) and 1u << (32 - prefix) (when prefix == 0) hit this. Always guard the boundary prefixes (/0, /31, /32) as the sample code does.
Use fixed-width unsigned types. Represent addresses as uint32_t (or unsigned where 32-bit). Signed integers make ~mask and right-shifts sign-extend, silently corrupting the high byte. Unsigned math also makes wraparound well-defined.
Integer under/overflow in host counts. The naive usable = (1u << (32 - prefix)) - 2 under-flows to a gigantic number for /31 and /32. Branch on the prefix before subtracting.
Input validation (the security angle). When this logic decides "is this IP in the allowed range," it is an access-control decision. Validate that the prefix is in 0..32 and reject anything else before computing a mask — a prefix of 40 or -1 should be an error, not a silent wrong answer. When parsing untrusted CIDR strings, validate each octet is 0..255 and that the base is aligned; a permissive parser that quietly "fixes" a mis-aligned or out-of-range block can widen an allow-list far beyond what was intended. Fail closed: on any parse doubt, deny rather than allow.
Concrete uses.
10.0.0.0/16), then carved into subnets per availability zone. Security groups and firewall rules allow or deny traffic by CIDR.(ip & mask) == network test, run at line rate.203.0.113.0/24" is a membership test on every incoming connection.Professional best-practice habits.
Beginner: Always compute the network and broadcast with & mask and | ~mask rather than eyeballing the dotted numbers. Name variables meaningfully (prefix, mask, network, broadcast). Print intermediate values while learning so you can see the masking happen.
Advanced: Validate prefixes and octets on input and fail closed. Prefer well-tested libraries (inet_pton, ipaddress in Python, net/netip in Go) over hand-rolled parsers for production access-control paths. Handle /31 and /32 explicitly. Keep the membership check branch-free and unsigned for performance in hot paths. Document whether a range is inclusive of network/broadcast to avoid off-by-one bugs across a team.
Beginner 1 — Read a block.
Objective: For 172.16.5.0/24, state the subnet mask, total addresses, usable hosts, network address, and broadcast address.
Requirements: Show your reasoning (host bits = 32 - prefix). No code required.
Expected answer shape: mask 255.255.255.0, 256 total, 254 usable, network 172.16.5.0, broadcast 172.16.5.255.
Concepts: prefix length, mask, network/broadcast, minus-two rule.
Beginner 2 — Prefix ⇄ mask table.
Objective: Fill in the mask and usable-host count for /25, /27, /28, and /30.
Requirements: Build each mask by counting one-bits; compute usable hosts as 2^(32-prefix) - 2.
Hint: /27 has 5 host bits.
Concepts: mask construction, host counting.
Intermediate 1 — Membership by hand.
Objective: Determine whether each of 10.0.1.200, 10.0.2.5, and 10.0.0.9 is inside 10.0.0.0/23.
Requirements: First compute the block's address range (/23 = 512 addresses), then decide each case.
Input/output example: 10.0.1.200 -> inside; 10.0.2.5 -> outside.
Hint: A /23 covers two consecutive /24s, i.e. 10.0.0.0–10.0.1.255.
Concepts: mask-and-compare membership.
Intermediate 2 — Subnet a block.
Objective: Split 192.168.10.0/24 into four equal subnets.
Requirements: Give the new prefix and, for each subnet, its network address, broadcast address, and usable host range.
Constraint: The four subnets must be equal-sized and non-overlapping.
Hint: Borrow 2 host bits -> /26, step of 64.
Concepts: subnetting, block alignment.
Challenge — CIDR calculator in C.
Objective: Write a program that reads an address and prefix (e.g. from argv as 10.20.30.40 22) and prints the network address, broadcast address, first and last usable host, netmask, and usable host count.
Requirements: Use uint32_t; validate that the prefix is 0..32 and each octet is 0..255, printing an error and exiting non-zero on bad input. Handle /0, /31, and /32 correctly. Free/close nothing is needed, but guard every shift.
Input/output example: input 10.20.30.40 22 -> network 10.20.28.0, broadcast 10.20.31.255, usable 10.20.28.1–10.20.31.254, 1022 usable hosts.
Hint: reuse make_mask and the byte-packing idea from this lesson; parse with sscanf and check its return count.
Concepts: mask construction, network/broadcast, host counting, input validation, shift guards.
address/prefix. The prefix is the number of leading network bits; the rest are host bits (32 - prefix of them).2^(host bits). Usable hosts = that minus 2 (network + broadcast), except /31 (2 usable) and /32 (1 host).0xFFFFFFFF << (32 - prefix), guarding the shift-by-32 case at /0.ip & mask (host bits cleared). Broadcast = network | ~mask (host bits set).(ip & mask) == (network & mask).uint32_t, validate input, and fail closed when the logic guards an access decision.