Offline validator for IP addressing plans that catches the class of mistakes LLMs make when generating network configurations.
Keywords: subnet calculator, VLSM, IP address management, IPAM, network automation, CIDR, overlap detection, Cisco, network engineer, LLM network config validation
Large language models generate network configurations with a predictable class of errors:
- Host bits set in a network address (
10.0.0.5/24instead of10.0.0.0/24) - Overlapping subnets silently allocated to different VLANs
- Gateways outside the subnet or equal to the network/broadcast address
- Prefix lengths too small to fit the required host count
- Duplicate allocation names that silently stomp each other in downstream tooling
These errors are individually trivial but collectively cause misrouted traffic, failed
switch provisioning, and hours of debug time. subnet-sanity catches all of them before
a config reaches a device.
No dependencies. Python 3.10 or later, standard library only.
git clone https://github.com/aharwelik/subnet-sanity
cd subnet-sanity
Run directly from the repo root:
python3 -m subnetsanity --help
python3 -m subnetsanity check examples/plan.bad.json
Sample output:
----------------------------------------------------------------------
subnet-sanity report: examples/plan.bad.json
----------------------------------------------------------------------
ERRORS (6)
----------------------------------------
HOST_BITS_SET [core]
Host bits are set in '10.0.0.5/24'. Did you mean network address 10.0.0.0/24?
GATEWAY_OUTSIDE_SUBNET [servers]
Gateway 10.0.0.254 is not contained in subnet 10.0.0.0/25.
GATEWAY_OUTSIDE_SUBNET [tiny]
Gateway 192.168.99.1 is not contained in subnet 10.0.1.0/29.
INSUFFICIENT_CAPACITY [tiny]
Subnet 10.0.1.0/29 provides 6 usable hosts, but 10 are required.
Use /28 which provides 14 usable hosts.
DUPLICATE_NAME [core]
Allocation name 'core' appears 2 times.
OVERLAPPING_SUBNETS
Allocations 'core' (10.0.0.0/24) and 'servers' (10.0.0.0/25) overlap.
----------------------------------------------------------------------
Summary: 6 error(s), 4 info(s)
----------------------------------------------------------------------
Exit code is 1 when any error-level finding exists, 0 for a clean plan.
python3 -m subnetsanity check examples/plan.bad.json --json
Emits a JSON object with a findings array, each element containing severity,
code, message, allocation (when applicable), and detail (structured data for
programmatic use).
python3 -m subnetsanity carve 10.0.0.0/16 24
Sample output:
----------------------------------------------------------------------
subnet-sanity carve: 10.0.0.0/16 -> /24
----------------------------------------------------------------------
Produced 256 subnet(s):
1. 10.0.0.0/24
2. 10.0.1.0/24
...
256. 10.0.255.0/24
----------------------------------------------------------------------
Carved subnets are validated using the same checks as check, so any structural
problem in the resulting blocks surfaces immediately.
{
"allocations": [
{
"name": "core",
"cidr": "10.0.0.0/24",
"gateway": "10.0.0.1",
"required_hosts": 200,
"vlan": 10
}
]
}Fields gateway, required_hosts, and vlan are optional.
| Code | Severity | Description |
|---|---|---|
| INVALID_CIDR | error | CIDR is not parseable |
| HOST_BITS_SET | error | Host bits set; corrected network reported |
| OVERLAPPING_SUBNETS | error | Two allocations share address space |
| GATEWAY_OUTSIDE_SUBNET | error | Gateway not contained in its subnet |
| GATEWAY_IS_NETWORK_ADDRESS | error | Gateway equals the network address |
| GATEWAY_IS_BROADCAST_ADDRESS | error | Gateway equals the broadcast address |
| DUPLICATE_NAME | error | Allocation name used more than once |
| INSUFFICIENT_CAPACITY | error | Prefix too small for required_hosts; smallest valid prefix suggested |
| CLASSIFICATION | info | RFC1918 private vs public vs reserved |
This tool implements the Identity Normalization invariant from the proof-carrying-ops model.
The canonical identity of a network allocation is its network address and prefix
length -- not the address a user typed. Any comparison, overlap check, or
containment test is only sound after both operands have been normalized to this
canonical form. When an LLM writes 10.0.0.5/24, the host bits in the address encode
no routing information; they are noise that causes two logically identical allocations
to appear distinct to a naive string-equality check, and causes an overlap detector
that compares only the first address to miss real conflicts.
subnet-sanity normalizes every input CIDR through ipaddress.IPv4Network(cidr, strict=False) before any comparison, then separately flags the pre-normalization form
as a HOST_BITS_SET error so the source config can be corrected. This
normalize-then-validate pattern mirrors the proof obligation in the broader
proof-carrying-ops framework: a claim about an allocation is only well-formed when
identities are canonical.
See the full proof-carrying-ops model at: https://github.com/aharwelik/proof-carrying-ops
MIT. See LICENSE.
Anthony Harwelik -- aharwelik@gmail.com -- https://github.com/aharwelik