
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:
String and ByteArray representationsCidrNumber type, a fixed-width, BE-optimized unsigned integer:
CidrNumber.V4.MAX_VALUE / CidrNumber.V6.MAX_VALUE
toByteArray(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!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 IpInterface has only a single IpAddress (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:
IpAddress — 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/netmaskIpNetwork, 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.V6Simply 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:
//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")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:
| 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> |
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.
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:
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) //falseBoth 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).toString() behavior ("address/prefix"); IPv4 variants also support netmask printing helpers.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 IpNetwork and deep-copy the IP address into the network's address property.
strict = 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.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 N is a netmask quad.toString(preferNetmaskOverPrefix = false) prints standard #.#.#.#/prefix.Conceptually:
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, lastAssignableHost
assignableHostRange: 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)The following example illustrates regular and edge cases:
//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!)Containment checks are explicit (and fast!):
network.contains(ipAddress)
network.contains(ipInterface)
anotherNetwork.contains(network)
overlaps (= a contains b or b contains a)isSubnetOfisSupernetOfisAdjacentToThe at.asitplus.cidre.byteops package provides low-level helper functions:
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.The full list of low-level ops can be found here.
/24 or “+2 bits”)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!
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:
String and ByteArray representationsCidrNumber type, a fixed-width, BE-optimized unsigned integer:
CidrNumber.V4.MAX_VALUE / CidrNumber.V6.MAX_VALUE
toByteArray(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!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 IpInterface has only a single IpAddress (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:
IpAddress — 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/netmaskIpNetwork, 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.V6Simply 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:
//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")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:
| 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> |
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.
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:
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) //falseBoth 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).toString() behavior ("address/prefix"); IPv4 variants also support netmask printing helpers.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 IpNetwork and deep-copy the IP address into the network's address property.
strict = 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.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 N is a netmask quad.toString(preferNetmaskOverPrefix = false) prints standard #.#.#.#/prefix.Conceptually:
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, lastAssignableHost
assignableHostRange: 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)The following example illustrates regular and edge cases:
//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!)Containment checks are explicit (and fast!):
network.contains(ipAddress)
network.contains(ipInterface)
anotherNetwork.contains(network)
overlaps (= a contains b or b contains a)isSubnetOfisSupernetOfisAdjacentToThe at.asitplus.cidre.byteops package provides low-level helper functions:
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.The full list of low-level ops can be found here.
/24 or “+2 bits”)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!