When working on a cloud networking feature set for an internal developer platform (IDP) recently, I ran into a surprisingly annoying bottleneck - checking if an N-number of CIDR ranges would collide.
I was doing it the manual way: punching one CIDR into an online calculator, copying the start and end IPs, then opening a second tab to do the same for another CIDR, and manually eyeballing the ranges to see if they overlapped. It was slow, clunky, and error-prone. I had spreadsheets open, tabs everywhere, and way too much context-switching for something that felt like it should be trivial. This just didn't scale.
Or maybe I'm just IP-dyslexic and my monke brain couldn't grok that many numbers and dots and slashes quickly. Who knows.
But I do know that this sits squarely in the gap between full-blown IPAM solutions and tedious one-off checks. You don't want to use your production IP address management system just to sanity-check a few CIDRs, but spreadsheets and calculator tabs aren't cutting it either.
So, instead of slogging through another round of IP arithmetic, I decided to do what builders do, I made the tool I wish I had.
Enter cidr-checkr.
What it does
cidr-checkr is a headless HTTP API service: you POST a list of CIDR ranges, and it tells you what each one covers and whether any of them overlap. Headless by design, so it drops cleanly into CI/CD pipelines, IaC tooling, or whatever homegrown automation you've got running.
If you've ever had to double-check VPC ranges before provisioning or retroactively untangle a silent collision, cidr-checkr might save you some pain (rate my salesmanship here).
The IP math
This is the part that's actually interesting, so let's not skip it.
A CIDR like 192.168.1.0/24 encodes two things: a network address and a prefix length. The prefix length tells you how many bits are fixed (the network part) and how many are free (the host part). /24 means 24 bits are fixed, leaving 8 bits for hosts. So 2^8 = 256 addresses, minus the network and broadcast addresses, giving you 254 usable hosts.
To detect overlaps programmatically, you first need the start and end of each range as comparable values. The start is just the network address. The end (the last IP in the range) is derived by OR-ing the network address with the bitwise inverse of the subnet mask:
func lastIP(network *net.IPNet) net.IP {
ip := make(net.IP, len(network.IP))
for i := range network.IP {
ip[i] = network.IP[i] | ^network.Mask[i]
}
return ip
}
For 192.168.1.0/24, the mask is 255.255.255.0 (0xFFFFFF00). Inverting it gives 0x000000FF. OR-ing that with the network address sets all host bits to 1, yielding 192.168.1.255. That's your range ceiling.
Once you have [start, end] for each CIDR, overlap detection is a classic interval problem: two ranges overlap if and only if max(start1, start2) <= min(end1, end2). If the latest start comes after the earliest end, they don't touch.
func getOverlappingRange(cidr1, cidr2 *net.IPNet) (net.IP, net.IP) {
startIP := maxIP(cidr1.IP, cidr2.IP)
endIP := minIP(lastIP(cidr1), lastIP(cidr2))
if bytes.Compare(startIP, endIP) > 0 {
return nil, nil
}
return startIP, endIP
}
This handles containment too, i.e. a /25 sitting entirely inside a /24 passes the same check, and the overlap range comes back as the full /25 extent.
For N CIDRs, you compare all N-choose-2 pairs:
for i := 0; i < len(parsedCIDRs); i++ {
for j := i + 1; j < len(parsedCIDRs); j++ {
if overlap := a.checkOverlap(parsedCIDRs[i], parsedCIDRs[j]); overlap != nil {
response.Overlaps = append(response.Overlaps, *overlap)
response.HasCollision = true
}
}
}
That's O(n²) comparisons. For the use case of a handful of VPC ranges or subnet blocks, this is entirely fine. At large scale (hundreds of CIDRs), a sweep-line algorithm over sorted intervals would be more efficient (O(n log n)), but that's premature for what this tool is actually for.
The code does sort the parsed CIDRs by starting IP before the pair loop, which sets it up nicely for a future sweep-line optimization if that ever becomes necessary.
A note on the net package
The implementation uses Go's standard net package, specifically net.IPNet and net.IP. One thing worth knowing: net.IP is a []byte under the hood, which means every IP operation involves heap allocation and slice comparisons via bytes.Compare.
Go 1.18 introduced net/netip, which provides netip.Addr and netip.Prefix as value types, no heap allocation, directly comparable with ==, and faster for IP arithmetic. A future refactor could swap out the net.IP representations for netip.Prefix, which would make the comparison logic cleaner and cut down on allocations in tight loops.
For now, the net package works, and the overhead is irrelevant at the scale this tool operates at.
Installation
- Clone the repository
git clone https://github.com/mshumayl/cidr-checkr.git
cd cidr-checkr
- Build the project
go build ./...
- Run the server
go run cmd/api/main.go
Once it's up, you're ready to start sending requests to the API.
Using the API
There's a single endpoint: POST /api/analyze-cidrs. Pass a JSON array of CIDR strings, get back per-CIDR details and a full overlap report.
Sample request
curl -X POST http://localhost:8080/api/analyze-cidrs \
-H "Content-Type: application/json" \
-d '{
"cidrs": ["192.168.1.0/24", "10.0.0.0/8", "192.168.1.0/25"]
}'
Sample response
{
"cidr_details": [
{
"cidr": "192.168.1.0/24",
"first_ip": "192.168.1.0",
"last_ip": "192.168.1.255",
"total_hosts": 254
},
{
"cidr": "10.0.0.0/8",
"first_ip": "10.0.0.0",
"last_ip": "10.255.255.255",
"total_hosts": 16777214
},
{
"cidr": "192.168.1.0/25",
"first_ip": "192.168.1.0",
"last_ip": "192.168.1.127",
"total_hosts": 126
}
],
"overlaps": [
{
"cidr1": "192.168.1.0/24",
"cidr2": "192.168.1.0/25",
"overlap_range": "192.168.1.0 - 192.168.1.127",
"overlap_hosts": 128
}
],
"has_collision": true
}
The response calls out that 192.168.1.0/25 is entirely contained within 192.168.1.0/24, 128 addresses of overlap. 10.0.0.0/8 is clean.
Forward paths
The immediate next step is a UI, either a hosted web front-end or a CLI wrapper, so you don't have to reach for curl every time. The logic is already composable; it's just a matter of building the right surface on top.
A net/netip refactor would also be a clean improvement, both for correctness (strict IPv4/IPv6 handling) and performance if the input volume ever grows.
If you're interested in contributing or following along, the GitHub repository is the place.