ShopperCove
Menu
All writingBlogTopicsCategoriesAboutRSS
Blog
Categories
Observability & SRE62All categories
About

Plate 35

  1. Blog

ipaddress vs String Prefix Allowlist: Localhost Lab

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.

Aditya Challa·1 October 2026·3 min read

Summary
On this page
  1. Intro — what this post promises
  2. Arms
  3. Lab topology
  4. Lead table (p50 checks/s)
  5. Footgun detail
  6. Hot-path recipe
  7. Reading it
  8. IPv6 and dual-stack
  9. Where bitmask fits
  10. Ops review checklist
  11. Pitfalls
  12. Reproduce
  13. Measurement note
  14. Limits
  15. Takeaway

Intro — what this post promises

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.


Arms

ArmNotesip in IPv4Networkpre-parsed IPv4Addressip_address(s) each timeparse tax(ip_int & mask) == networkbitmasks.startswith(prefix)fragile footgun


Lab topology

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.


Lead table (p50 checks/s)

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.


Footgun detail

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.


Hot-path recipe

  1. Parse CIDRs once to ip_network (or bitmask tuples).
  2. Parse request IP once to ip_address / int.
  3. Loop networks with ip in net or bitmask.
  4. Skip string ops entirely in the deny/allow decision.

Reading it

  • Allowlists / deny lists → ipaddress (or bitmask built from it).
  • Never ship prefix strings for non-/8-aligned CIDRs.
  • Parse once at the edge (IPv4Address), check many networks.
  • IPv6 needs the same module — string prefixes get worse.

IPv6 and dual-stack

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.


Where bitmask fits

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.


Ops review checklist

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).


Pitfalls

  • Treating RFC1918 as a handful of startswith stubs.
  • Re-parsing the same IP string in an inner loop.
  • Forgetting IPv4-mapped IPv6 forms.
  • Trusting equal hit counts on a lucky synthetic mix.

Reproduce

python3 lab-evidence/119-ipaddress-vs-string-prefix/results/run_lab.py

Evidence: summary.json, summary.txt.


Measurement note

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.


Limits

One Linux box. IPv4 only. Four static CIDRs. Not radix/trie multi-million prefix tables.


Takeaway

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.

pythonipaddresscidrperformancenetworkingsecurity

Lab evidence

What I found running this

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.

Notes when a lab post goes up

Occasional email for new hands-on reviews. No sequence and no sponsors.

Related links

  • Plate 34

    ast.literal_eval vs json.loads: Localhost Lab

    1 Oct 2026

  • Plate 64

    selectors vs select.select: Localhost Lab

    1 Oct 2026

  • Plate 57

    memoryview vs bytes Slice: Localhost Lab

    1 Oct 2026

On this page

  1. Intro — what this post promises
  2. Arms
  3. Lab topology
  4. Lead table (p50 checks/s)
  5. Footgun detail
  6. Hot-path recipe
  7. Reading it
  8. IPv6 and dual-stack
  9. Where bitmask fits
  10. Ops review checklist
  11. Pitfalls
  12. Reproduce
  13. Measurement note
  14. Limits
  15. Takeaway
All writingBlogCategoriesTopicsAboutPrivacyRSS

© 2026 ShopperCove