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.
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.
Then import it where it is used:
import {
"moonbitstack/moonip",
}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.
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
}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.
When it refuses
Every call that can refuse raises Refused, and the variant says which kind of mistake
it was and where.
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"
}
}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.
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.
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.1Classification 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.
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.
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()) // 16decode refuses a byte string that is neither four nor sixteen bytes long, with
Length(want=16, got=…).
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().
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()) // NoneHost 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.
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.
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()) // 2Split, 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.
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()) // falseexclude 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.
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.
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(),
)
}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.
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.
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