moonip
00 · guide

Ten steps · every output real

From text
to a block
and back.

Each step carries the output it gives. Everything here is in one package and needs no socket.

01–10
step 01

Install it

Add the package. It depends on nothing but the standard library, and it does no I/O, so it compiles for every target the compiler has.

moon add moonbitstack/moonip

Then import it where it is used:

moon.pkgmoonbit
import {
  "moonbitstack/moonip",
}
step 02

Read an address

Addr::parse takes the text forms of RFC 4291. It refuses anything ambiguous: a leading zero in an IPv4 field would read as octal to some resolvers, so it is an error here rather than a guess.

main.mbtmoonbit
fn main raise {
  let a = @moonip.Addr::parse("2001:0DB8:0000:0000:0000:0000:0000:0001")
  println(a)                     // 2001:db8::1
  println(a.exploded())          // 2001:0db8:0000:0000:0000:0000:0000:0001
  println(a.bits())              // 128

  let b = @moonip.Addr::parse("192.0.2.1")
  println(b.bits())              // 32
  println(b.mapped())            // ::ffff:192.0.2.1
}
out2001:db8::1 2001:0db8:0000:0000:0000:0000:0000:0001 128 32 ::ffff:192.0.2.1

The canonical form is RFC 5952's: lower case, no leading zeros in a group, the longest run of zero groups replaced by ::, the leftmost run when two are the same length, and an IPv4-mapped address written with its four dotted fields at the end.

step 03

When it refuses

Every call that can refuse raises Refused, and the variant says which kind of mistake it was and where.

why.mbtmoonbit
fn why(text : String) -> String {
  try {
    let addr = @moonip.Addr::parse(text)
    "read as " + addr.to_string()
  } catch {
    @moonip.Malformed(at~) => "nothing readable at " + at.to_string()
    @moonip.OutOfRange(limit~, got~) =>
      got.to_string() + " does not fit in " + limit.to_string()
    _ => "refused"
  }
}
outwhy("192.0.2.1") read as 192.0.2.1 why("192.0.2.256") 256 does not fit in 255 why("1.2.3.4.5") nothing readable at 7

Malformed(at=…) counts characters from zero, so a caller can point at the exact place. OutOfRange(limit=…, got=…) is for a number that was well formed and too big.

step 04

Zones and the two families

An IPv6 link-local address can carry a zone: the interface it is reachable through. It is part of the value, part of the text, and it survives next and prev because a neighbour is on the same link.

zone.mbtmoonbit
let link = @moonip.Addr::parse("fe80::1%eth0")
println(link.zone())            // Some("eth0")
println(link.next())            // Some(fe80::2%eth0)
println(link.is_link_local())   // true
println(link.encode().length()) // 16, a zone is not part of the address

let mapped = @moonip.Addr::parse("::ffff:192.0.2.1")
println(mapped.is_v6())         // true
println(mapped.unmap())         // 192.0.2.1
outSome("eth0") Some(fe80::2%eth0) true 16 true 192.0.2.1

Classification asks about the address a packet would be routed to, so an IPv4-mapped address is judged by the IPv4 address inside it — the same rule the kernel applies.

step 05

Bytes in, bytes out

Every wire format wants the four or sixteen bytes, not the text. encode writes them big-endian and decode reads them back.

bytes.mbtmoonbit
let a = @moonip.Addr::parse("192.0.2.1")
let raw = a.encode()
println(raw.length())                    // 4
println(@moonip.Addr::decode(raw) == a)  // true

let v6 = @moonip.Addr::parse("2001:db8::1")
println(v6.encode().length())            // 16
out4 true 16

decode refuses a byte string that is neither four nor sixteen bytes long, with Length(want=16, got=…).

step 06

A block

A Prefix is an address and a mask length. It keeps the address it was written with, so the same value can stand for an interface on that block; the questions about the block itself work on masked().

block.mbtmoonbit
let block = @moonip.Prefix::parse("192.0.2.0/24")
println(block)                    // 192.0.2.0/24
println(block.netmask())          // 255.255.255.0
println(block.hostmask())         // 0.0.0.255
println(block.broadcast())        // Some(192.0.2.255)
println(block.contains(@moonip.Addr::parse("192.0.2.7")))  // true

let link = @moonip.Prefix::parse("192.0.2.5/24", strict=false)
println(link)                     // 192.0.2.0/24

println(@moonip.Prefix::parse("2001:db8::/32").broadcast())  // None
out192.0.2.0/24 255.255.255.0 0.0.0.255 Some(192.0.2.255) true 192.0.2.0/24 None

Host bits are refused by default and cleared with strict=false. Prefix::new keeps them, so Prefix::new(@moonip.Addr::parse("192.0.2.5"), 24) is the interface form and masked() is the block.

step 07

Walk a block

hosts() is the addresses an interface can be given: everything except the network and broadcast addresses for IPv4, and except the subnet-router anycast address for IPv6. A /31 and a /127 keep both their addresses, because both ends of a point-to-point link are usable (RFC 3021, RFC 6164). The iterator is lazy — walking a /0 is possible and takes a while.

hosts.mbtmoonbit
let block = @moonip.Prefix::parse("192.0.2.0/30")
for host in block.hosts() {
  println(host)
}
println(@moonip.Prefix::parse("192.0.2.0/31").hosts().collect().length())  // 2
out192.0.2.1 192.0.2.2 2
step 08

Split, join, cut

Four operations cover most of the arithmetic: splitting a block into smaller ones, joining a range back into the fewest blocks, cutting a block out of another, and collapsing a set. num_addresses counts what a block holds and is_single_ip says whether that is one address.

blocks.mbtmoonbit
let block = @moonip.Prefix::parse("192.0.2.0/24")

for part in block.subnets(bits=26) {
  println(part)
}
// 192.0.2.0/26  192.0.2.64/26  192.0.2.128/26  192.0.2.192/26

for part in @moonip.summarize(
  @moonip.Addr::parse("192.0.2.0"),
  @moonip.Addr::parse("192.0.2.130"),
) {
  println(part)
}
// 192.0.2.0/25  192.0.2.128/31  192.0.2.130/32

for part in block.exclude(@moonip.Prefix::parse("192.0.2.64/26")) {
  println(part)
}
// 192.0.2.128/25  192.0.2.0/26

println(block.supernet(bits=16))  // 192.0.0.0/16
println(block.num_addresses())          // Some(256)
println(block.is_single_ip())           // false
out192.0.2.0/26 192.0.2.64/26 192.0.2.128/26 192.0.2.192/26 192.0.2.0/25 192.0.2.128/31 192.0.2.130/32 192.0.2.128/25 192.0.2.0/26 192.0.0.0/16 Some(256) false

exclude answers with the exact difference even when the block being cut out does not sit inside, so it never has to refuse: a block that merely touches the edge leaves it whole, and one that covers it leaves nothing. collapse takes a list and returns the fewest blocks holding the same addresses, IPv4 first, then IPv6.

step 09

Classify

The predicates follow the two IANA special-purpose address registries. is_private means "a registry marks this as not globally reachable", which is wider than the three RFC 1918 blocks: it includes loopback, link local, documentation and benchmarking ranges too.

classify.mbtmoonbit
let samples = [
  "10.1.2.3", "100.64.0.1", "8.8.8.8", "192.0.2.1",
  "fc00::1", "2001:4860::1", "2001:db8::1",
]
for text in samples {
  let a = @moonip.Addr::parse(text)
  println(
    text + "  private=" + a.is_private().to_string() +
    "  global=" + a.is_global().to_string(),
  )
}
out10.1.2.3 private=true global=false 100.64.0.1 private=false global=false 8.8.8.8 private=false global=true 192.0.2.1 private=true global=false fc00::1 private=true global=false 2001:4860::1 private=false global=true 2001:db8::1 private=true global=false

Shared address space, 100.64.0.0/10, is the one block that is neither private nor reachable: it answers false to both questions. The other predicates — is_loopback, is_unspecified, is_multicast, is_link_local, is_site_local, is_reserved — are the rows of the registry, one for one. Two more read the scope field a multicast address carries: is_interface_local_multicast is ff01::/16, and is_link_local_multicast is 224.0.0.0/24 and ff02::/16.

step 10

An address with a port

AddrPort reads the form a URL or a log line uses. Brackets are what tells the last colon apart from the ones inside an IPv6 address, so an unbracketed IPv6 address with a port is refused rather than guessed at.

port.mbtmoonbit
let end = @moonip.AddrPort::parse("[2001:db8::1]:443")
println(end)        // [2001:db8::1]:443
println(end.port()) // 443
println(end.addr()) // 2001:db8::1

let local = @moonip.AddrPort::parse("192.0.2.1:8080")
println(local)      // 192.0.2.1:8080
out[2001:db8::1]:443 443 2001:db8::1 192.0.2.1:8080
top top