ipam#
IP address management for OCaml.
ipam allocates IP addresses and CIDR pools, modelled on Docker
libnetwork's two-level IPAM:
- a pool hands out individual addresses from a single CIDR range;
- an allocator carves pools out of predefined networks (or accepts caller-supplied CIDRs), rejects overlaps, and routes address requests to the owning pool.
Occupancy is tracked in EWAH-compressed bitmaps indexed by host
ordinal, so a sparsely used /8 or /64 costs only as much as the
addresses actually in use. Network and broadcast addresses (IPv4) and
the Subnet-Router anycast address (IPv6) are reserved per RFC 3021 and
RFC 4291. Every operation is pure: mutating functions return an updated
value, so allocator state can be snapshotted and persisted by the caller.
Installation#
$ opam install ipam
If opam cannot find the package, add the overlay repository first:
$ opam repo add samoht https://tangled.org/gazagnaire.org/opam-overlay.git
$ opam update
$ opam install ipam
Usage#
Pools#
A pool wraps a single CIDR. Its capacity excludes the reserved addresses,
and request hands out the lowest free host:
# let p = Ipam.Pool.v (Ipaddr.Prefix.of_string_exn "192.168.1.0/24") in
Ipam.Pool.capacity p
- : int = 254
# let p = Ipam.Pool.v (Ipaddr.Prefix.of_string_exn "192.168.1.0/24") in
match Ipam.Pool.request p with
| Ok (addr, _) -> Ipaddr.to_string addr
| Error e -> Ipam.string_of_error e
- : string = "192.168.1.1"
A /31 carries no network or broadcast reservation (RFC 3021), so both
addresses are usable:
# let p = Ipam.Pool.v (Ipaddr.Prefix.of_string_exn "10.0.0.0/31") in
Ipam.Pool.capacity p
- : int = 2
Allocator#
The allocator carves a pool dynamically from its predefined networks (libnetwork's defaults, here the first local-scope network):
# let t = Ipam.v () in
match Ipam.request_pool ~space:"local" t with
| Ok (_id, prefix, _t) -> Ipaddr.Prefix.to_string prefix
| Error e -> Ipam.string_of_error e
- : string = "172.17.0.0/16"
Allocate a static pool, then request an address from it:
# let t = Ipam.v () in
match Ipam.request_pool ~pool:(Ipaddr.Prefix.of_string_exn "10.10.0.0/24") t with
| Error e -> Ipam.string_of_error e
| Ok (id, _, t) -> (
match Ipam.request_address t id with
| Ok (addr, _) -> Ipaddr.to_string addr
| Error e -> Ipam.string_of_error e)
- : string = "10.10.0.1"
IPv6 reserves the all-zeros Subnet-Router anycast address (RFC 4291), so
allocation starts at ::1:
# let p = Ipam.Pool.v (Ipaddr.Prefix.of_string_exn "fd00::/64") in
match Ipam.Pool.request p with
| Ok (addr, _) -> Ipaddr.to_string addr
| Error e -> Ipam.string_of_error e
- : string = "fd00::1"
IPv6 range limit#
Addresses are indexed by their offset from the pool's network address in
an int-keyed bitmap, so a pool manages at most Ipam.Pool.max_addresses
(2^61) addresses. IPv4 ranges always fit. An IPv6 range wider than that
only ever allocates from its first max_addresses addresses; the rest
report Out_of_range. In practice a /64 host pool is allocated from
sequentially and never approaches the limit.
Interoperability#
test/interop/moby/ replays an operation trace generated by Docker
libnetwork's default IPAM driver and checks ipam produces identical pool
subnetting and address allocations. Regenerate the trace (needs the Go
toolchain) with:
$ dune build @traces
License#
ISC, see LICENSE.md.