CIDRE logo

CIDRE — 100% Pure KMP IPv4/IPv6 Representation with Complementary CIDR Math

A-SIT Plus Official GitHub license

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:

  • parsing and encoding IPv4 and IPv6 addresses from/to String and ByteArray representations

  • converting CIDR prefixes from/to netmasks

  • checking whether addresses or networks are fully contained within a network

  • comparing networks and addresses within the same family (IPv4/IPv6)

  • calculating address and host ranges, adjacency, overlap and containment checks

  • CIDR math based on custom CidrNumber type, a fixed-width, BE-optimized unsigned integer:

    • V4 range: 0, 2^32; V6 range: 0, 2^128

    • Arithmetic (+/−) returns null on overflow/underflow

    • Bitwise ops, shifts, and inversion operate within modeled width and always succeed

    • Truncation:

      • Instantiation through default constructor truncates the passed value CidrNumber.V4.MAX_VALUE / CidrNumber.V6.MAX_VALUE

      • toByteArray(truncate: Boolean = true) produces

        • truncate = true: 4/16-byte forms directly usable for Netmasks and IpAddresses

        • truncate = false: preserves the 33rd/129th bit (corresponds to MAX_VALUE), which is the size of a /0 network.

    • Be sure to check out the full API docs on 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 addresses

    • IpAddress.V6 representing IPv6 addresses

  • IpNetwork — a sealed class, following the same hierarchy:

    • IpNetwork.V4 representing an IPv4 network consisting of an IpAddress.V4 and a prefix/netmask

    • IpNetwork.V6 representing an IPv6 network consisting of an IpAddress.V6 and a prefix/netmask

  • IpInterface — 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/netmask

    • IpInterface.V6 consisting of an IpAddress.V6 and a prefix/netmask

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.

Using in your Projects

This library is available at Maven Central.

Gradle

dependencies {
api("at.asitplus:cidre:$version")
}

Working with IP Addresses

Parsing and Encoding

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.

IP Address Arithmetic

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

Platform Interop

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>

IPv4 Specifics

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 Specifics

IPv6 addresses can embed IPv4 addresses in two ways:

  • IPv4 mapped addresses: 0000:0000:0000:0000:0000:FFFF:<IPv4 Address in IPv4 Notation>

  • IPv4 compatible addresses: 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.

Working with Networks and IpInterfaces

CIDRE models two closely related concepts:

  • IpNetwork: a contiguous address range, defined by a network address and prefix.
    The network’s address itself is part of the network (and for IPv4, the broadcast address is also considered inside for membership checks).

  • 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) //false

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)

  • common flags (e.g., isLinkLocal, isLoopback, isMulticast). IPv4- and IPv6-specific flags are available on their respective interfaces (IpAddressAndPrefix.V4 / V6).

  • consistent toString() behavior ("address/prefix"); IPv4 variants also support netmask printing helpers.

Creating IpInterfaces from Networks

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.

    • If strict = true, the passed address must already be the network address (i.e., correctly masked), according to the specified prefix.

    • If 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.

Netmasks and Prefixes

Round-tripping between prefixes and netmasks is straightforward:

  • Create a netmask from a prefix:

    • For a specific IP family: prefix.toNetmask(IpAddress.Family.V4) or prefix.toNetmask(IpAddress.Family.V6)

    • For an arbitrary octet count: prefix.toNetmask(octetCount)

  • Convert a netmask back to a prefix and validate contiguity: 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.

Host Ranges and Address Spaces

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:

    • size

    • lastAddress, firstAssignableHost, lastAssignableHost

    • Sequences:

      • assignableHostRange: routable, assignable hosts inside a network.
        The interval boundaries can be accessed through:

        • firstAssignableHost

        • lastAssignableHost

      • addressSpace: the whole address space, including network address and (for IPv4) broadcast address.
        The interval boundaries can be accessed through:

      • address (network address)

      • lastAddress

    • IPv4: broadcastAddress (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 and Overlap Checks

Containment checks are explicit (and fast!):

  • Address in network: network.contains(ipAddress)

  • Interface in network: network.contains(ipInterface)

  • Network fully contained in another network: anotherNetwork.contains(network)

  • Overlap check: overlaps (= a contains b or b contains a)

  • Relations between networks

    • isSubnetOf

    • isSupernetOf

    • isAdjacentTo

Low-Level Utilities

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

Roadmap

  • More comprehensive tests for low-level ops

  • Subnet enumeration (absolute and relative, e.g., /24 or “+2 bits”)

  • Supernetting helpers (absolute and relative)

  • Some more comprehensive tests, covering subnetting and supernetting

  • As required/sensible, once API is stable, and tests are comprehensive: performance optimization

  • Even more comprehensive tests and benchmarks, ensuring optimizations are not misguided and indeed improve performance

Note that the API is still subject to subtle changes and the inner workings may be completely overhauled at some point, if deemed sensible.

Contributing

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!

All modules:

Link copied to clipboard