Two offices merge. Both teams have been happily using 10.34.x.x for years. The network team stitches the sites together with a new VPN tunnel, updates the firewall, and goes home.
Three days later the help desk floods with tickets: the build server answers pings but not SSH, a printer works on Tuesdays, and one department swears the CRM "loads eventually, if you refresh five times."
Nothing is on fire. Everything is intermittent. Welcome to the worst failure mode in subnetting: overlapping subnets.
The case that started it
Site A was assigned 10.34.12.0/23. That block covers:
10.34.12.0 - 10.34.13.255 (512 addresses)
Site B, allocated by a different admin in a different year, got 10.34.13.0/24:
10.34.13.0 - 10.34.13.255 (256 addresses)
On paper both allocations look fine, and neither admin ever saw the other's spreadsheet. But B's entire /24 is inside A's /23. Every one of B's 256 addresses is claimed twice.
Once the tunnel comes up, packets for 10.34.13.x have two valid destinations. The routing table picks whichever route was installed last, ARP caches fight over the same targets, and traffic lands on whichever site the router currently prefers. Intermittent, direction-dependent, and maddening to troubleshoot.
The math is simpler than the debugging
Two subnets overlap exactly when their address intervals share at least one address. Forget prefix lengths — expand both blocks into a start and end address and compare intervals:
overlap(A, B) = start(A) <= end(B) AND start(B) <= end(A)
That's it. It works for equal prefixes, different prefixes, a /23 versus a /24, or a single host versus a whole /8.
In Python, the whole check fits on a napkin:
def ip_to_int(ip):
a, b, c, d = (int(x) for x in ip.split("."))
return (a << 24) | (b << 16) | (c << 8) | d
def mask_of(prefix):
return (0xFFFFFFFF << (32 - prefix)) & 0xFFFFFFFF
def block_span(ip, prefix):
base = ip_to_int(ip) & mask_of(prefix)
return base, base + (1 << (32 - prefix)) - 1
def overlaps(a_ip, a_pfx, b_ip, b_pfx):
a_start, a_end = block_span(a_ip, a_pfx)
b_start, b_end = block_span(b_ip, b_pfx)
return a_start <= b_end and b_start <= a_end
print(overlaps("10.34.12.0", 23, "10.34.28.0", 23)) # False - 3,584 apart, fine
print(overlaps("10.34.12.0", 23, "10.34.13.0", 24)) # True - the incident above
When the blocks do not overlap, the same numbers tell you how much room is left between them — useful when you're about to hand out the next slice.
Three mistakes I keep seeing
1. Comparing prefixes instead of intervals. "It's a /23 and a /24, so they're different" is not a safety check. Nested prefixes overlap by definition. So do 10.34.13.0/24 and 10.34.13.128/25.
2. Trusting the network address someone typed. 10.34.12.7/23 and 10.34.12.0/23 are the same block — the network address is where the mask says it is, not where the host octets happen to sit. Always normalize before comparing.
3. Assuming private space is infinite and therefore safe. RFC 1918 gives you a lot of room, but mergers are exactly when two orgs discover they both carved up 10.x.x.x differently:
| Block | Range | Addresses | Typical owner |
|---|---|---|---|
10.0.0.0/8 |
10.0.0.0 – 10.255.255.255 | 16,777,216 | Large enterprises |
172.16.0.0/12 |
172.16.0.0 – 172.31.255.255 | 1,048,576 | Mid-size networks, Docker defaults |
192.168.0.0/16 |
192.168.0.0 – 192.168.255.255 | 65,536 | Home and small office |
One more trap: 100.64.0.0/10 is not private. RFC 6598 reserved it for carrier-grade NAT, and mobile carriers hand addresses from it to millions of devices. It looks internal (it isn't globally routed), but it isn't yours to subnet either. If you inherit an address plan using that block, treat it as someone else's shared space, not a free pool.
The 30-second pre-flight check
Before any allocation goes into the IPAM tool, run the interval check against every existing block in the same region:
- Normalize the new block to its true start and end address.
- Compare intervals against all neighbors — different prefix lengths included.
- If anything overlaps, stop and re-plan. If nothing does, note the gap so the next allocation starts cleanly.
You can do this in a REPL, in a spreadsheet, or in a tool built for exactly this job — I keep a private IP range calculator bookmarked for the check, because it also tells me which RFC 1918 block (or special range like CGNAT, link-local, or loopback) an address falls into, which catches the third mistake in the same pass.
Why overlap is worse than a full outage
A hard outage gets paged immediately and fixed loudly. An overlap fails quietly: it usually doesn't break the path the monitoring probes use, so dashboards stay green while a subset of hosts flips between destinations. The failure surface is routing order, ARP timing, and DHCP lease renewals — all nondeterministic from the help desk's point of view.
That's why prevention is the whole game. The check costs 30 seconds before deployment; the incident costs a week of packet captures after.
Top comments (0)