Hands-on ipaddress CIDR containment vs naive string prefix allowlists: real checks/s, measured on Linux localhost today in this hands-on lab for SREs.
3 min read
Check whether client IPs fall in allowlist CIDRs via ipaddress vs naive str.startswith vs a precomputed int bitmask. This lab reports checks/s on Linux localhost.
Related links:
chainmap vs dict merge localhost lab
tomllib vs json localhost lab
path glob vs fnmatch localhost lab
dataclass replace vs manual localhost lab
zoneinfo vs utc offset localhost lab
heapq merge vs sorted localhost lab
islice vs list slice localhost lab
stat vs path stat localhost lab
Lab honesty (1 Oct 2026 IST): Python 3.13.5. Affiliates: 0. 20000 IPv4 strings; CIDRs 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 203.0.113.0/24.
Verdict up front: int bitmask ~6853718 checks/s; string startswith ~6223453; ipaddress (pre-parsed) ~2589147; parse-each-str ~508942. Prefer ipaddress for correctness; bitmask if you own the hot path and pre-parse.
ArmNotesip in IPv4Networkpre-parsed IPv4Addressip_address(s) each timeparse tax(ip_int & mask) == networkbitmasks.startswith(prefix)fragile footgun
n=20000 · 4 CIDRs · 7 rounds · p50
metric: checks/s = n / p50_s
Script: lab-evidence/119-ipaddress-vs-string-prefix/results/run_lab.py.
Armchecks/sint bitmask6853718string startswith6223453ipaddress pre-parsed2589147ipaddress parse each508942
Bitmask matched ipaddress hits (15000). String matched this fixture’s count by luck but is unsafe for 172.16.0.0/12 — 172.32.1.1 is outside yet can match a sloppy "172.3" prefix.
172.16.0.0/12 covers 172.16.0.0–172.31.255.255. A prefix like "172.3" also matches 172.32.0.0/16. Equal hit counts on a lucky synthetic mix are not a correctness proof — the counterexample disagrees.
ip_network (or bitmask tuples).ip_address / int.ip in net or bitmask.ipaddress (or bitmask built from it).IPv4Address), check many networks.String prefixes collapse completely for IPv6 compressed forms (2001:db8::1). ipaddress normalizes representation; keep allowlists as CIDR objects, not display strings. Dual-stack frontends should parse once and branch on version.
Edge proxies that already store IPs as ints (or can atomically convert) can keep a tiny tuple list (network, mask) beside the ip_network objects used for admin UI. Build bitmasks from ipaddress, never from hand-written hex.
Reject PRs that gate admin/VPN access on ip.startswith("10.") alone. Require CIDR lists, tests including 172.31 vs 172.32, and a documented parser (ipaddress or equivalent).
startswith stubs.python3 lab-evidence/119-ipaddress-vs-string-prefix/results/run_lab.py
Evidence: summary.json, summary.txt.
Checks/s counts one allowlist decision per synthetic client IP against four CIDRs (first match wins). Absolute rates move with CIDR count and hit position — put frequent office/VPN ranges earlier in the list after you measure production histograms.
One Linux box. IPv4 only. Four static CIDRs. Not radix/trie multi-million prefix tables.
Pre-parsed ipaddress ~2589147 checks/s; bitmask ~6853718 if you need more speed. Keep string prefixes out of production CIDR checks — correctness beats a lucky microbench tie.
Lab evidence
Ran the supplied ipaddress allowlist benchmark on the Linux localhost fixture and checked the rendered body against the source. The correctness footgun was the important result: a lucky equal hit count does not make a string prefix safe for non-aligned CIDRs. I verified the comparison table and related links render as intended.