cidre
0.3.1indexedFacilitates IP handling and subnet calculations with no external dependencies. Offers IP address parsing, CIDR math, network comparisons, and planned features like subnetting and network merging.
Facilitates IP handling and subnet calculations with no external dependencies. Offers IP address parsing, CIDR math, network comparisons, and planned features like subnetting and network merging.
From orchard to endpoint: CIDRE delivers a smooth, dry balance of IP handling and subnet math, served consistently sparkling across all KMP targets. Unchaptalized — zero added dependencies; just natural, refreshing clarity.
— Sir Evander Marchbank, self-proclaimed cider cartographer, who insists that every orchard has its own “gravitational pull” affecting the bubbles.
CIDRE focuses on parsing and representing IP addresses, IP networks, and performing CIDR math. On the JVM and Android it maps from/to InetAddress/Inet4Address/Inet6Address. On native targets, it maps from/to in_addr/in6_addr.
It is not a full IP networking implementation, but you can use it to implement IP routing.
It has exactly zero external dependencies.
Currently, CIDRE provides the following functionality:
In general, CIDRE's data model has semantics influenced by netaddr: An IpNetwork covers a range of IpInterfaces, both of which consist of an address and a prefix.
Semantically, an has only a single (although no validation is performed whether it is distinct from the associated network's address), while a network spans a range.
In more technical terms, CIDRE introduces three main classes:
IpNetwork, IpAddress, and their IPv4/IPv6 specializations share the IpAddressAndPrefix interface hierarchy, which groups common semantics and functionality.
Addresses and networks are not comparable, so this is mainly an application of DRY.
This library is available at Maven Central.
dependencies {
api("at.asitplus:cidre:$version")
}
val ip4 = IpAddress("128.65.88.6") //returns an IpAddress.V4
val ip6 = IpAddress("2002:ac1d:2d64::1") //returns an IpAddress.V6
val ip4mappedIp6 = IpAddress("0000:0000:0000:0000:0000:FFFF:192.168.255.255") // returns an IPv4-mapped IpAddress.V6
Simply toString() any IP address to get its string representation, or access octets to get its network-order byte representation.
An IpAddress's companion object also provides helpful properties such as segment separator, number of octets, and readily usable Regex instances to check whether a string is a valid representation of
an IP address or a single address segment.
All operations work only within a family (IPv4 / IPv6).
In general, IP addresses are Comparable and are ordered by comparing their octets interpreted as a BE-encoded unsigned integer.
Any IP address and netmask can be converted to a CidrNumber, but arithmetical and bitwise operations are also available directly on
IP addresses:
When working with addresses or networks whose concrete family is not known yet, use withSameFamily.
It returns null for mixed IPv4/IPv6 input and enables the family-bound operations inside the scope:
val a: IpAddress<*, *> = IpAddress("192.168.0.99")
val b: IpAddress<*, *> = IpAddress("192.168.0.1")
val distance = a.withSameFamily(b) {
left - right // CidrNumber.V4(98u)
}
val masked = a.withSameFamily(b) {
left and right // 192.168.0.1
}
If code needs family-specific typed access once one side is known, branch inside the scope:
val distance = a.withSameFamily(b) {
whenFamily(
v4 = { left - right }, // left and right are IpAddress.V4 here
v6 = { left - right }, // left and right are IpAddress.V6 here
)
}
Networks have the same pattern for common same-family operations:
val first: IpNetwork<*, *> = IpNetwork("192.168.0.0/25")
val second: IpNetwork<*, *> = IpNetwork("192.168.0.128/25")
val merged = first.withSameFamily(second) {
if (left canMergeWith right) left + right else null
}
For membership checks, the network/address overloads work in both directions:
val network: IpNetwork<*, *> = IpNetwork("192.168.0.0/24")
val address: IpAddress<*, *> = IpAddress("192.168.0.42")
val contains = network.withSameFamily(address) {
address in network
}
val alsoContains = address.withSameFamily(network) {
isInNetwork()
}
CIDRE's IpAddress classes conveniently map from/to platform types.
Except for JavaScript and Wasm targets (which lack a native non-string IP address representation), creating addresses is as easy as passing a platform-native address into a CIDRE IP address constructor:
Though it has long since been superseded by CIDR, IpAddress.V4 still features a class property (albeit marked as deprecated) that indicates its pre-CIDR
address class.
For explicit classful bit-prefix modeling, IpAddress.V4.LeadingPrefix is available:
val a = IpAddress.V4.LeadingPrefix(1u, 0u) // "0"
val b = IpAddress.V4.LeadingPrefix(2, 2) // "10"
val c = IpAddress.V4.LeadingPrefix("110") // length=3, value=6
println(c) //110
IPv6 addresses can embed IPv4 addresses in two ways:
0000:0000:0000:0000:0000:FFFF:<IPv4 Address in IPv4 Notation>0000:0000:0000:0000:0000:0000:<IPv4 Address in IPv4 Notation>While the former is still very much a thing (and exposed through the isIpv4Mapped flag), the latter has been deprecated.
Still, the flag isIpv4Compatible indicates whether an IPv6 address conforms to the compatible schema.
It is possible to extract the contained IPv4 address from an IPv4-mapped or IPv4-compatible address by accessing the
embeddedIpV4Address property. It returns null if no IPv4 address is contained.
CIDRE models two closely related concepts:
IpNetwork: a contiguous address range, defined by a network address and prefix.IpInterface: a single address bound to a prefix and associated with a network, and therefore carries a reference to the associated IpNetwork.Both can be created from the same string format:
Both share the IpAddressAndPrefix interface and its respective IPv4 and IPv6 specializations and therefore expose:
address and prefix (CIDR prefix length)netmask (network-order ByteArray)isLinkLocal, isLoopback, isMulticast). IPv4- and IPv6-specific flags are available on their
respective interfaces (IpAddressAndPrefix.V4 / V6).Given an IpAddress and a prefix, it is possible to get the corresponding network in two ways:
IpNetwork(address, strict = false) to create a new and deep-copy the IP address into the network's property.
Round-tripping between prefixes and netmasks is straightforward:
prefix.toNetmask(IpAddress.Family.V4) or prefix.toNetmask(IpAddress.Family.V6)prefix.toNetmask(octetCount)netmask.toPrefix()IP addresses can be masked in-place by calling either mask(prefix) or mask(netmask).
To create a deep-copied masked version of an address, manually copy() it before masking.
For IPv4, it is also possible to get a dotted-quad representation and choose a preferred textual form when working with IpAddressAndPrefix:
netmaskToString() yields a #.#.#.# string.toString(preferNetmaskOverPrefix = true) prints A.A.A.A N.N.N.N, where A is an IP address quad and is a netmask quad.Conceptually:
The following example illustrates regular and edge cases:
Containment checks are explicit (and fast!):
network.contains(ipAddress)network.contains(ipInterface)IpNetwork supports both absolute and relative subnet/supernet operations:
subnet(newPrefix) enumerates child networks at an explicit prefix.subnetRelative(prefixDiff) enumerates child networks by extending the current prefix by .Examples:
val net = IpNetwork.V4("192.168.0.0/24")
val halves = net.subnet(25u).toList()
println(halves) // [192.168.0.0/25, 192.168.0.128/25]
val parent = IpNetwork.V4("192.168.0.128/25").supernet(24u)
println(parent) // 192.168.0.0/24
Invalid prefix requests throw IllegalArgumentException (e.g. subnet with a shorter prefix, or supernet with a longer prefix).
Behavior is regression-tested against fixture data generated from Python's ipaddress test corpus (subnetting.json, supernetting.json).
IpNetwork supports common set-style operations between two CIDRs of the same family:
Canonical output contract (for unionCollapse, unionCovering, intersection, difference, and fromRange):
Membership operators:
address in networkiface in networkchildNet in parentNetExample:
val a = IpNetwork.V4("10.0.0.0/24")
val b = IpNetwork.V4("10.0.0.128/25")
val c = IpNetwork.V4("10.0.1.0/24")
println(a.intersection(b)) // [10.0.0.128/25]
println(a.difference(b)) // [10.0.0.0/25]
println(a.unionCollapse(c)) // [10.0.0.0/24, 10.0.1.0/24]
println(a.unionCovering(c)) // [10.0.0.0/23]
println(IpAddress.V4("10.0.0.42") in a) // true
Example:
val net = IpNetwork.V4("10.0.0.0/24")
val (start, end) = net.toRange()
println("$start .. $end") // 10.0.0.0 .. 10.0.0.255
val summary = IpNetwork.fromRange(IpAddress.V4("10.0.0.5"), IpAddress.V4("10.0.0.130"))
println(summary) // canonical CIDR cover of that exact inclusive interval
println(net.relationTo(IpNetwork.V4("10.0.1.0/24"))) // ADJACENT
Why this is not a Set implementation:
2^128 addresses), so eager iteration does not match Kotlin collection expectations.The at.asitplus.cidre.byteops package provides low-level helper functions:
The full list of low-level ops can be found here.
Note that the API is still subject to subtle changes and the inner workings may be completely overhauled at some point, if deemed sensible.
External contributions are greatly appreciated! Just be sure to observe the contribution guidelines (see CONTRIBUTING.md).
The Apache License does not apply to the logos (including the A-SIT logo) and the project/module name(s), as these are the sole property of A-SIT/A-SIT Plus GmbH and may not be used in derivative works without explicit permission!
String and ByteArray representationssubnet(newPrefix) / subnetRelative(prefixDiff)supernet(newPrefix) / supernetRelative(prefixDiff)CidrNumber type, a fixed-width, BE-optimized unsigned integer:
CidrNumber.V4.MAX_VALUE / CidrNumber.V6.MAX_VALUEtoByteArray(truncate: Boolean = true) produces
truncate = true: 4/16-byte forms directly usable for Netmasks and IpAddressestruncate = false: preserves the 33rd/129th bit (corresponds to MAX_VALUE), which is the size of a /0 network.CidrNumber!IpInterfaceIpAddressIpAddress — a sealed class, specialized as:
IpAddress.V4 representing IPv4 addressesIpAddress.V6 representing IPv6 addressesIpNetwork — a sealed class, following the same hierarchy:
IpNetwork.V4 representing an IPv4 network consisting of an IpAddress.V4 and a prefix/netmaskIpNetwork.V6 representing an IPv6 network consisting of an IpAddress.V6 and a prefix/netmaskIpInterface — a concrete IP address belonging to a network. Like a network, this is a combination of IP address and prefix/netmask, but with distinctly different semantics:
IpInterface.V4 consisting of an IpAddress.V4 and a prefix/netmaskIpInterface.V6 consisting of an IpAddress.V6 and a prefix/netmask//Use qualified constructor to enforce family
val lower = IpAddress.V4("192.168.0.1")
val higher = IpAddress.V4("192.168.0.99")
println("Distance = ${lower - higher}") //null due to underflow
println("Distance = ${higher - lower}") //00000062 (=98)
println("Summed = ${lower + CidrNumber.V4(98u)}") //192.168.0.99
println("Numeric: ${lower.toCidrNumber()}") //c0a80001
var shifted = lower shl 8
println("Numeric shifted = ${shifted.toCidrNumber()}") //a8000100
println("Shifted = $shifted") //168.0.1.0 due to truncation
val maskedBits = higher.mask(24u)
val maskedCopy = higher and (24u.toNetmask(IpFamily.V4))
// Masked in-place= 192.168.0.0 (modified bits: 4), manually masked = 192.168.0.0
println("Masked in-place= $higher (modified bits: $maskedBits), manually masked = $maskedCopy")
| Runtime | JVM/Android | Mac/Linux/AndroidNative/MinGW |
|---|
| Generic creation | IpAddress(InetAddress) | not possible |
| Type-safe IPv4 creation | IpAddress.V4(InetAddress) | IpAddress(in_addr) / IpAddress.V4(in_addr) |
| Type-safe IPv6 creation | IpAddress.V6(InetAddress) | IpAddress(in6_addr) / IpAddress.V6(in6_addr) |
| To generic platform type | IpAddress.toInetAddress(): InetAddress | IpAddress.toInAddr(): CValue<out CStructVar> |
| To IPv4 platform type | IpAddress.V4.toInetAddress(): Inet4Address | IpAddress.V4.toInAddr(): CValue<in_addr> |
| To IPv6 platform type | IpAddress.V6.toInetAddress(): Inet6Address | IpAddress.V6.toInAddr(): CValue<in6_addr> |
val addrAndPrefix = "::dead/42"
val iface = IpInterface(addrAndPrefix)
val net = IpNetwork(addrAndPrefix, strict = false) //be lenient and auto-mask
println("iface: $iface") //::dead/42
println("net: $net") //::/42
//normalizes in-place and associates (not copies) the address with the network
val associated = IpNetwork.forAddress(iface.address, iface.prefix)
println("net: $associated") //::/42
println("iface: $associated") //::/42 <-- note the change here!
println(associated.address === iface.address) //true
//no normalization, but copying, so we can be strict!
val deepCopied = IpNetwork(iface.address, iface.prefix, strict = true)
println(deepCopied.address == iface.address) //true
println(deepCopied.address === iface.address) //false
toString() behavior ("address/prefix"); IPv4 variants also support netmask printing helpers.IpNetworkaddressstrict = true, the passed address must already be the network address (i.e., correctly masked), according to the specified prefix.strict = false, the passed address will be masked to the network address according to the specified prefix.IpNetwork.forAddress(address, prefix) creates a new network, referencing and masking the passed address. This avoids copying but modifies any not-correctly-masked address in-place, according to the given prefix.NtoString(preferNetmaskOverPrefix = false) prints standard #.#.#.#/prefix.An IpNetwork represents a contiguous range of addresses.
An IpInterface is a single address bound to a prefix.
The network address is part of the network; for IPv4, the broadcast address (when applicable) is also inside.
Network relations and size helpers:
sizelastAddress, firstAssignableHost, lastAssignableHostassignableHostRange: routable, assignable hosts inside a network.firstAssignableHostlastAssignableHostaddressSpace: the whole address space, including network address and (for IPv4) broadcast address.address (network address)lastAddressbroadcastAddress (when applicable; may or may not be lastAddress depending on the network)//point-to-point -> no broadcast
val pointToPoint = IpNetwork.V4("192.168.0.0/31")
println(pointToPoint.address) //192.168.0.0
println(pointToPoint.lastAddress) //192.168.0.1
println(pointToPoint.firstAssignableHost) //192.168.0.0/31
println(pointToPoint.lastAssignableHost) //192.168.0.1/31
println(pointToPoint.broadcastAddress) //null
println(pointToPoint.size) // 00000002 (= 2)
//perhaps the most used private IP range
val private = IpNetwork.V4("192.168.0.0/24")
println(private.address) //192.168.0.0
println(private.lastAddress) //192.168.0.255
println(private.firstAssignableHost) //192.168.0.1/24
println(private.lastAssignableHost) //192.168.0.254/24
println(private.broadcastAddress) //192.168.0.255/24
println(private.size) //00000100 (= 256)
//maxing out
val unspec = IpNetwork.V4("0.0.0.0/0")
println(unspec.address) //0.0.0.0
println(unspec.lastAddress) //255.255.255.255
println(unspec.firstAssignableHost) //0.0.0.1/0
println(unspec.lastAssignableHost) //255.255.255.254/0
println(unspec.broadcastAddress) //255.255.255.255/0
println(unspec.size) //0100000000 (= 2^32; observe the fifth octet required to represent it!)
anotherNetwork.contains(network)overlaps (= a contains b or b contains a)isSubnetOfisSupernetOfisAdjacentToprefixDiffsupernet(newPrefix) returns the containing parent network at an explicit prefix.supernetRelative(prefixDiff) returns the containing parent network by shortening the current prefix by prefixDiff.unionCollapse(other):
unionCovering(other):
intersection(other):
difference(other):
this - other as a CIDR listIllegalArgumentException (no implicit IPv4/IPv6 coercion)toRange():
(startAddress, endAddress) pair covered by a network.IpNetwork.fromRange(start, end):
relationTo(other):
EQUAL, CONTAINS, WITHIN, ADJACENT, DISJOINT.infix fun ByteArray.and(other: ByteArray): ByteArray performing a logical AND operation, returning a fresh ByteArray.fun ByteArray.andInplace(other: ByteArray): Int performing an in-place logical AND operation, modifying the receiver ByteArray. Returns the number of modified bits.fun ByteArray.compareUnsignedBE(other: ByteArray): Int comparing two same-sized byte arrays by interpreting their contents as unsigned BE integers.fun Prefix.toNetmask(family: IpAddress.Family): Netmask converting a UInt CIDR prefix to its byte representation.fun Netmask.toPrefix(): Prefix converting a netmask into its CIDR prefix length.ByteArray.toShortArray(bigEndian: Boolean = true): ShortArray grouping pairs of bytes into a short. Useful to get IPv6 hextets from octets.Surfaced from shared tags and platforms — no rankings paid for.