Build the mental model
Most bugs announce themselves: a crash, a stack trace. Subnet bugs do not. Subnet math is the one place in networking where human intuition and machine arithmetic look identical right up until they diverge. We read 192.168.10.20 and 192.168.10.130 and think "same network, they both start with 192.168.10" -- and for a /24 we would be right. Every host in 192.168.10.0/24 really does share a broadcast domain, so code that decides membership by comparing the first three octets passes every test anyone writes. Then someone carves that /24 into four /26 blocks to separate warehouse scanners from finance machines, and the same code keeps returning the same confident answer while the truth underneath has quietly changed.
That is the bug class: subnet membership decided by octet intuition instead of by the mask the interface is actually configured with. It shows up as a hardcoded /24 in a helper, as a string comparison of address prefixes, as a network address computed by zeroing the last octet. All three are one mistake wearing different clothes -- the mask was assumed rather than read.
What makes it dangerous is exactly that it does not crash. A crash gets a ticket; a confidently wrong reachability report gets believed. An engineer reads DIRECT, concludes the switch path is healthy, and burns a day hunting a firewall that was never in the path. The habit that catches it is mechanical: never derive a network from an address by hand. Hand both the address and its real prefix to Python's ipaddress module and let it do the AND. Then test across a mask boundary, because a /24-only test suite cannot see this failure at all.
ONE /24 OR FOUR /26 BLOCKS?
---------------------------
Real config: 192.168.10.0/24 carved into four /26 blocks of 64
.0 ------ .63 .64 ----- .127 .128 ---- .191 .192 ---- .255
+-------------+ +-------------+ +-------------+ +-------------+
| SUBNET A | | SUBNET B | | SUBNET C | | SUBNET D |
| reception | | warehouse | | server-nas | | (unused) |
| .20 | | .70 | | .130 | | |
| sales .50 | | | | | | |
+------+------+ +------+------+ +------+------+ +-------------+
| | |
+-------+-------+-------+-------+
| |
+--------------------------------+
| GATEWAY 192.168.10.1 |
| routes between the /26 blocks |
+--------------------------------+
The script assumes /24, so it sees ONE flat subnet:
+-----------------------------------------------------------+
| 192.168.10.0/24 everybody DIRECT, gateway never needed |
+-----------------------------------------------------------+
^ this is the lieConnect it to a real scenario
Here is the office you are debugging. A single 192.168.10.0/24 range was subdivided into four /26 blocks when the shop floor was separated from the back office. Reception (.20) and the sales laptop (.50) live in 192.168.10.0/26. The warehouse scanner (.70) sits in 192.168.10.64/26. The NAS (.130) sits in 192.168.10.128/26. The gateway at 192.168.10.1 is what routes between them.
Run the script below exactly as written. It does not raise, it does not warn, and it prints a report that looks entirely professional: a host table, a rule of dashes, then one verdict per host pair. Read the host table first, because that is where the evidence is. Every row reports the same net= value, even though every row was configured with a different /26. Then read the verdicts underneath: all six pairs say DIRECT.
Now do the arithmetic by hand for one pair. A /26 is 64 addresses, so the block boundaries fall at .0, .64, .128 and .192. Which block holds .70? Which block holds .130? Once you have those two numbers written down, the report has contradicted itself in front of you, and all that is left is finding the line of code that made it lie.
Try the working example
"""Office network reachability checker.
Every host is configured in CIDR notation. Two hosts can talk directly
through the switch when they share a subnet; otherwise the packet has to
be handed to the gateway. This script prints a report -- check it against
the configured masks by hand before you trust it.
"""
import ipaddress
HOSTS = {
"reception-pc": "192.168.10.20/26",
"sales-laptop": "192.168.10.50/26",
"warehouse-scanner": "192.168.10.70/26",
"server-nas": "192.168.10.130/26",
}
GATEWAY = "192.168.10.1"
# The office is "a 192.168.10.x network", so a /24 is close enough. Right?
ASSUMED_PREFIX = 24
def network_of(cidr):
"""Return the network this host address belongs to."""
addr = cidr.split("/")[0]
return ipaddress.ip_network(addr + "/" + str(ASSUMED_PREFIX), strict=False)
def same_subnet(cidr_a, cidr_b):
"""True when both hosts sit in the same subnet."""
return network_of(cidr_a) == network_of(cidr_b)
def main():
print("HOST TABLE")
print("-" * 62)
for name in HOSTS:
cidr = HOSTS[name]
print("{:<20} {:<20} net={}".format(name, cidr, network_of(cidr)))
print()
print("REACHABILITY REPORT")
print("-" * 62)
names = list(HOSTS)
for i in range(len(names)):
for j in range(i + 1, len(names)):
a = names[i]
b = names[j]
if same_subnet(HOSTS[a], HOSTS[b]):
verdict = "DIRECT (switch only)"
else:
verdict = "VIA GATEWAY " + GATEWAY
print("{:<20} -> {:<20} {}".format(a, b, verdict))
main()
HOST TABLE
--------------------------------------------------------------
reception-pc 192.168.10.20/26 net=192.168.10.0/24
sales-laptop 192.168.10.50/26 net=192.168.10.0/24
warehouse-scanner 192.168.10.70/26 net=192.168.10.0/24
server-nas 192.168.10.130/26 net=192.168.10.0/24
REACHABILITY REPORT
--------------------------------------------------------------
reception-pc -> sales-laptop DIRECT (switch only)
reception-pc -> warehouse-scanner DIRECT (switch only)
reception-pc -> server-nas DIRECT (switch only)
sales-laptop -> warehouse-scanner DIRECT (switch only)
sales-laptop -> server-nas DIRECT (switch only)
warehouse-scanner -> server-nas DIRECT (switch only)
5-minute try-it
The symptom: the script runs clean and reports that all six host pairs can talk directly through the switch. The network team insists the warehouse scanner cannot reach the NAS without going through the router. One of them is wrong.
Start with the two rows that should look most suspicious to you: warehouse-scanner -> server-nas, and reception-pc -> server-nas. Take the configured CIDR for each of those hosts out of the HOSTS dict and work out its network address by hand, using the prefix that is actually written there -- not the one the report prints. Then compare your four network addresses against the single net= value the host table shows for every host.
When you have found the line responsible, fix it so that each host's own configured prefix is used, and re-run. A corrected report shows DIRECT for exactly one pair and VIA GATEWAY for the other five. Finally, prove the fix is real: change every host to a /24 and confirm the report flips back to all-DIRECT. If it does, you have also just demonstrated why the original bug survived testing for so long.
One important caution
Hardcoding a prefix length (almost always /24) inside a helper instead of reading each interface's configured mask. The code is correct until the day somebody subnets, and confidently wrong forever after.
Comparing IP addresses as strings. '192.168.1.10' < '192.168.1.9' is True as text and False as addresses, so sorting, range checks and ACL matching all quietly misbehave. Convert with ipaddress.ip_address() before comparing anything.
Python documentation - ipaddress: IPv4/IPv6 manipulation library — Computer Networking