ipnetx
IP address range, prefix, and set operations for IPv4 and IPv6.
What is this?
Working with IP addresses in network programming often means more than checking whether two addresses are equal. Real questions look like:
- "Is this IP inside the
10.0.0.0/8corporate network?" - "Do any of the ranges in this allow-list overlap with the ranges in this block-list?"
- "I have 50 000 IPs in my firewall rules — can I reduce them to the minimal set of CIDR prefixes?"
ipnetx gives you three composable building blocks for answering those questions,
for both IPv4 and IPv6, using the address types already in std::net:
| Type | What it represents |
|---|---|
IpRange<A> |
A contiguous span of addresses: 10.0.0.0 – 10.255.255.255 |
IpPrefix<A> |
A CIDR block: 10.0.0.0/8 |
IpSet<A> |
An arbitrary collection of addresses built from any mix of ranges and prefixes |
Background: what is a CIDR prefix?
A CIDR prefix like
192.168.1.0/24is shorthand for "all addresses whose first 24 bits match192.168.1". The number after the/is the prefix length (also called the mask)./24means 24 fixed bits and 8 free bits, giving 2⁸ = 256 addresses./32(IPv4) or/128(IPv6) is a single host./0covers the entire address space.
Installation
Add this to your Cargo.toml:
[]
= "0.1"
Or: cargo add ipnetx
IpRange — working with address spans
An IpRange is the simplest primitive: a start address and an end address,
both inclusive. Any two addresses of the same family form a valid range as long
as start <= end.
use Ipv4Addr;
use IpRange;
// Create a range covering 10.0.0.0 through 10.0.0.255
let range = new;
assert!;
assert_eq!;
assert_eq!;
Parsing from a string
Use .parse() to create an IpRange from a string. The separator is ..
(two dots) — the same token that Display produces, so a round-trip always
reconstructs the original string. The separator is unambiguous for IPv6 because
no valid IPv6 address contains consecutive dots.
use ;
use IpRange;
// IPv4 — parse, then verify the round-trip
let r: = "10.0.0.0..10.0.0.255".parse.unwrap;
assert_eq!;
assert_eq!;
assert_eq!;
// IPv6 — works identically
let r6: = "2001:db8::1..2001:db8::ff".parse.unwrap;
assert!;
// Wrong separator → error
assert!;
// Bad address → error
assert!;
Containment
use Ipv4Addr;
use IpRange;
let range = new;
assert!; // inside
assert!; // start — inclusive
assert!; // end — inclusive
assert!; // just outside
Overlap
Two ranges overlap if they share at least one address. Ranges that merely touch at a single boundary point are still considered overlapping.
use Ipv4Addr;
use IpRange;
let a = new;
let b = new;
let c = new;
assert!; // share addresses 5–10
assert!; // completely separate
Converting a range to CIDR prefixes
A contiguous range of addresses can always be expressed as one or more CIDR prefixes. If the range happens to be exactly CIDR-aligned (power-of-two size, aligned start), it collapses to a single prefix.
use Ipv4Addr;
use IpRange;
// Aligned range → exactly one prefix
let aligned = new;
let prefixes = aligned.prefixes;
assert_eq!;
assert_eq!; // 192.168.1.0/24
// Unaligned range → multiple prefixes
let unaligned = new;
assert!;
Use prefix() (singular) when you only want to know whether the range is
already a single CIDR block, without allocating the full list:
use Ipv4Addr;
use IpRange;
let aligned = new;
let not_aligned = new;
assert!; // 10.0.0.0/24
assert!; // not CIDR-aligned
Invalid ranges
A range where start > end is considered invalid. IpRange::new always
succeeds — validity is checked lazily by is_valid(). Invalid ranges are
treated as empty by all operations.
use Ipv4Addr;
use IpRange;
let bad = new;
assert!;
assert!;
assert!;
IPv6
Every method works identically for IPv6. Swap Ipv4Addr for Ipv6Addr:
use Ipv6Addr;
use IpRange;
let range = new;
assert!;
assert!;
IpPrefix — working with CIDR blocks
An IpPrefix pairs an IP address with a prefix length. The constructor
validates the mask length but does not zero host bits — 192.168.1.100/24
is a valid prefix. Use masked() to get the canonical network address form.
use Ipv4Addr;
use IpPrefix;
// Ok: mask 24 is within [0, 32] for IPv4
let prefix = new.unwrap;
// Err: mask 33 exceeds 32 bits
assert!;
Parsing from a string
Use .parse() to create an IpPrefix from the standard "<address>/<length>"
notation. Host bits in the address are preserved just like IpPrefix::new —
"192.168.1.100/24" parses successfully. Use .masked() on the result if you
need the canonical network-address form.
use ;
use IpPrefix;
// IPv4 — parse, then verify the round-trip
let p: = "192.168.1.0/24".parse.unwrap;
assert_eq!;
assert_eq!;
assert_eq!;
// IPv6 — works identically
let p6: = "2001:db8::/32".parse.unwrap;
assert_eq!;
// Missing '/' → error
assert!;
// Mask out of range → error
assert!;
Containment
use Ipv4Addr;
use IpPrefix;
let prefix = new.unwrap;
assert!; // network address
assert!; // broadcast address
assert!; // somewhere in the middle
assert!; // different /24
Converting a prefix to a range
use Ipv4Addr;
use IpPrefix;
let prefix = new.unwrap;
let range = prefix.to_range;
assert_eq!;
assert_eq!;
Masking host bits
If you receive a prefix like 192.168.1.100/24 (a host address with a mask
attached) and want the canonical network address 192.168.1.0/24, use
masked():
use Ipv4Addr;
use IpPrefix;
let sloppy = new.unwrap;
let clean = sloppy.masked;
assert_eq!;
assert_eq!;
Checking for a single host
use Ipv4Addr;
use IpPrefix;
let host = new.unwrap;
let network = new.unwrap;
assert!;
assert!;
Overlap between prefixes
Two CIDR prefixes either nest (one is a sub-prefix of the other) or are completely disjoint — they cannot partially overlap the way arbitrary ranges can.
use Ipv4Addr;
use IpPrefix;
let slash8 = new.unwrap;
let slash24 = new.unwrap;
let other = new.unwrap;
assert!; // /24 is nested inside /8
assert!; // symmetric
assert!; // completely separate
IPv6
use Ipv6Addr;
use IpPrefix;
// 2001:db8::/32 — the documentation prefix (RFC 3849)
let prefix = new.unwrap;
assert!;
assert!;
IpSet — working with sets of addresses
IpSet is for when you have an arbitrary collection of ranges and prefixes and
need to answer membership or overlap questions efficiently. You build one through
IpSetBuilder, which accepts ranges and prefixes in any order, then call
build() to get a normalized, immutable IpSet.
Normalization means build() sorts all ranges by start address and merges
any that are adjacent or overlapping. The result is always a minimal list of
non-overlapping ranges, which makes binary-search lookups possible.
Building a set
use Ipv4Addr;
use IpPrefix;
use IpRange;
use IpSetBuilder;
let mut builder = new;
// Add a /8 block
builder.add_prefix;
// Add a separate /16
builder.add_prefix;
// Add an arbitrary range
builder.add_range;
let set = builder.build;
assert_eq!; // three disjoint ranges, stored in sorted order
Adding and removing individual IPs
use Ipv4Addr;
use IpPrefix;
use IpSetBuilder;
let mut builder = new;
builder.add_prefix;
// Punch a hole: remove a single IP from the middle of the range
builder.remove_ip;
let set = builder.build;
assert!; // removed
assert!; // still present
assert!; // still present
assert_eq!; // split into [0..99] and [101..255]
Membership queries
use Ipv4Addr;
use IpPrefix;
use IpRange;
use IpSetBuilder;
let mut builder = new;
builder.add_prefix;
let set = builder.build;
// Single IP lookup — O(log n)
assert!;
assert!;
// Range containment — the entire range must fit inside a single stored range
let inner = new;
let outer = new;
assert!; // fully inside 10/8
assert!; // starts before 10/8 — not contained
Overlap between two sets
use Ipv4Addr;
use IpPrefix;
use IpSetBuilder;
let mut allow = new;
allow.add_prefix;
let allow_set = allow.build;
let mut block = new;
block.add_prefix; // inside allow
let block_set = block.build;
let mut clean = new;
clean.add_prefix; // outside allow
let clean_set = clean.build;
assert!; // conflict — 10/24 is inside 10/8
assert!; // no conflict
Inspecting a set
use Ipv4Addr;
use IpPrefix;
use IpSetBuilder;
let mut builder = new;
builder.add_prefix;
builder.add_prefix;
let set = builder.build;
// Iterate over the stored ranges
for range in set.ranges
// Decompose back to CIDR prefixes
for prefix in set.prefixes
Overlapping inputs are merged automatically
You don't need to pre-sort or de-duplicate your inputs — build() handles it:
use Ipv4Addr;
use IpPrefix;
use IpSetBuilder;
let mut builder = new;
builder.add_prefix; // 10/8
builder.add_prefix; // 10.0.0/24 — inside 10/8
builder.add_prefix; // 10.0/16 — also inside 10/8
let set = builder.build;
assert_eq!; // all three collapse into a single 10/8 range
IPv6
use Ipv6Addr;
use IpPrefix;
use IpSetBuilder;
let mut builder = new;
builder.add_prefix;
let set = builder.build;
assert!;
assert!;
API reference
Full documentation is available on docs.rs/ipnetx.
License
MIT — see LICENSE.