iprange: Set Algebra on CIDR Blocks at C Speed, So You Can Stop Writing Python for Firewall Lists2026-09-01
You've got a threat-intel feed with 340,000 CIDRs. You've got your own allowlist with another 12,000. Ops wants to know: which of our /24s overlap the block feed? Which addresses in the feed are already covered by upstream ASN aggregates? Does 203.0.113.42 appear in any of the eight blocklists you subscribe to?
You reach for Python's ipaddress module. Twenty seconds later, you're still waiting. You try grep. It matches 10.0.0.5 in the log but misses it in 10.0.0.0/8 because textual matching doesn't understand subnets. You write yet another one-off script.
Stop. iprange — from Costa Tsaousis's FireHOL project — is a single C binary that does set algebra on CIDR ranges: union, intersection, difference, aggregation, counting. It's obscene how fast it is. Debian/Ubuntu ship it as iprange.
The basics. Every invocation reads one or more files (or stdin), normalizes and de-duplicates everything into a canonical minimal set of CIDRs, and prints the result:
# Merge eight blocklists into one optimal, deduplicated set
iprange feed1.txt feed2.txt feed3.txt ... > merged.netset
# The output is minimal: contiguous ranges are merged,
# subsumed ranges are dropped. 340k lines in, 41k out.
Set operations. This is the killer feature:
# Intersection: what's in BOTH lists?
iprange blocklist.netset --intersect our_customers.netset
# Difference: what's in the blocklist but NOT our allowlist?
iprange blocklist.netset --exclude allowlist.netset
# Is a single IP in a set? Pipe it in.
echo 203.0.113.42 | iprange - --intersect blocklist.netset
# (prints the containing CIDR, or nothing)
# Compare two lists and report overlap size in both directions
iprange --compare feed_a.netset feed_b.netset
Analysis. The lesser-known flags are where you save a whole afternoon of scripting:
# Total IPs covered (expands /24 → 256, /16 → 65536, etc.)
iprange --count-unique blocklist.netset
# Prefix histogram: how many /24s, /28s, /32s make up this set?
iprange --prefixes blocklist.netset
# Reduce to just /24 aggregates (useful for firewall table size limits)
iprange --ipset-reduce 20 --ipset-reduce-entries 10000 blocklist.netset
That last one is subtle and brilliant: it finds the minimum-count representation of your set that stays under a given entry count, trading precision (a bit of overblocking) for a table small enough to fit in ipset or a hardware ACL.
Why not just use grepcidr? grepcidr matches IPs against ranges but does nothing else — no union, no diff, no aggregation, no analysis. ipset is a kernel data structure, not a text-processing tool. Python's ipaddress is correct but you'll wait minutes and eat gigabytes of RAM on a 300k-CIDR feed. iprange chews the same input in under a second in a few MB.
Wire it into a pipeline. The - filename means stdin, so it composes with everything:
# From an access log, find requests from any Spamhaus DROP entry
awk '{print $1}' access.log | sort -u \
| iprange - --intersect spamhaus_drop.netset
# Nightly cron: what's newly appeared in the feed since yesterday?
iprange today.netset --exclude yesterday.netset > new_today.netset
One binary, no dependencies, POSIX exit codes, streams cleanly. This is the tool you should have installed the first time you found yourself in a Jupyter notebook doing for cidr in blocklist: for ip in ips: if ip in cidr:. It's been sitting in your distro's repos the entire time.
iprange gives you correct set algebra at C speed, replacing whole categories of ad-hoc Python scripts with a one-line pipeline.
