/** * Address parsing, enough to answer two questions the validator asks: which family is * this, and is it inside that range. * * Written rather than depended on because it is small, and because the one rule it * exists to enforce — public segments use documentation ranges — is the difference * between a lab that reproduces the internet and one that silently never forms a mesh. */ export type Family = "v4" | "v6"; export interface Cidr { family: Family; /** Network address, as an integer. */ base: bigint; prefix: number; text: string; } const V4_BITS = 32n; const V6_BITS = 128n; export function familyOf(address: string): Family { return address.includes(":") ? "v6" : "v4"; } function parseV4(text: string): bigint { const parts = text.split("."); if (parts.length !== 4) throw new Error(`not an IPv4 address: ${text}`); let value = 0n; for (const part of parts) { if (!/^\d{1,3}$/.test(part)) throw new Error(`not an IPv4 address: ${text}`); const octet = Number(part); if (octet > 255) throw new Error(`octet out of range in ${text}`); value = (value << 8n) | BigInt(octet); } return value; } function parseV6(text: string): bigint { // Reject the forms this does not implement rather than mis-parsing them. An embedded // IPv4 suffix is legal and rare; getting it wrong silently would be worse than refusing. if (text.includes(".")) throw new Error(`IPv4-in-IPv6 form is not supported: ${text}`); const halves = text.split("::"); if (halves.length > 2) throw new Error(`not an IPv6 address: ${text}`); const head = halves[0] ? halves[0].split(":").filter(Boolean) : []; const tail = halves.length === 2 && halves[1] ? halves[1].split(":").filter(Boolean) : []; const explicit = head.length + tail.length; if (explicit > 8) throw new Error(`too many groups in ${text}`); if (halves.length === 1 && explicit !== 8) throw new Error(`not an IPv6 address: ${text}`); const groups = [...head, ...Array(8 - explicit).fill("0"), ...tail]; let value = 0n; for (const group of groups) { if (!/^[0-9a-fA-F]{1,4}$/.test(group)) throw new Error(`not an IPv6 address: ${text}`); value = (value << 16n) | BigInt(parseInt(group, 16)); } return value; } export function parseAddress(text: string): { family: Family; value: bigint } { const family = familyOf(text); return { family, value: family === "v4" ? parseV4(text) : parseV6(text) }; } export function parseCidr(text: string): Cidr { const slash = text.lastIndexOf("/"); if (slash === -1) throw new Error(`not a CIDR range (no prefix length): ${text}`); const addressText = text.slice(0, slash); const prefix = Number(text.slice(slash + 1)); const { family, value } = parseAddress(addressText); const bits = family === "v4" ? V4_BITS : V6_BITS; if (!Number.isInteger(prefix) || prefix < 0 || BigInt(prefix) > bits) { throw new Error(`prefix length out of range for ${family}: ${text}`); } const hostBits = bits - BigInt(prefix); const base = (value >> hostBits) << hostBits; return { family, base, prefix, text }; } export function contains(range: Cidr, address: string): boolean { const { family, value } = parseAddress(address); if (family !== range.family) return false; const bits = family === "v4" ? V4_BITS : V6_BITS; const hostBits = bits - BigInt(range.prefix); return ((value >> hostBits) << hostBits) === range.base; }