/** * Subnet parsing utilities for IPAM system * Handles CIDR notation and IP address operations */ export interface SubnetInfo { network: string; maskBits: number; octets: [number, number, number, number]; firstUsableIp: string; lastUsableIp: string; totalHosts: number; } /** * Parse subnet CIDR notation (e.g., "10.0.10.0/24") * Returns network information needed for IP allocation */ export function parseSubnet(subnet: string): SubnetInfo { const [network, maskBitsStr] = subnet.split('/'); const maskBits = Number.parseInt(maskBitsStr, 10); if (!network || Number.isNaN(maskBits) || maskBits < 0 || maskBits > 32) { throw new Error(`Invalid subnet format: ${subnet}`); } const octets = network.split('.').map((o) => { const octet = Number.parseInt(o, 10); if (Number.isNaN(octet) || octet < 0 || octet > 255) { throw new Error(`Invalid IP address: ${network}`); } return octet; }) as [number, number, number, number]; if (octets.length !== 4) { throw new Error(`Invalid IP address: ${network}`); } // Calculate total hosts (2^(32-maskBits) - 2 for network and broadcast) const totalHosts = 2 ** (32 - maskBits) - 2; // For simplicity, we only support /24 and larger subnets if (maskBits > 24) { throw new Error(`Subnet too small: ${subnet}. Celilo requires /24 or larger subnets.`); } // Calculate first and last usable IPs // We reserve .1-.9 for infrastructure, so first usable is .10 const firstUsableIp = `${octets[0]}.${octets[1]}.${octets[2]}.10`; const lastUsableIp = `${octets[0]}.${octets[1]}.${octets[2]}.254`; return { network, maskBits, octets, firstUsableIp, lastUsableIp, totalHosts, }; } /** * Strip CIDR mask from IP address * Example: "10.0.10.10/24" → "10.0.10.10" */ export function stripCIDR(ipWithMask: string): string { return ipWithMask.split('/')[0]; } /** * Add CIDR mask to IP address * Example: "10.0.10.10", 24 → "10.0.10.10/24" */ export function addCIDR(ip: string, maskBits: number): string { return `${ip}/${maskBits}`; } /** * Check if an IP address belongs to a subnet. * * Re-exported from `@celilo/capabilities` rather than implemented here, so the * one answer serves both celilo and module scripts — which cannot import from * the backend and would otherwise carry a second copy (celilo#809). * * The implementation that used to live here compared the first three octets and * threw away the prefix it had parsed, so it was right for /24 and wrong in both * directions for anything else. It also routed through `parseSubnet`, which * REFUSES anything smaller than a /24 — an allocation rule that has no business * constraining a containment question. */ export { isInSubnet } from '@celilo/capabilities'; /** * Generate all possible IPs in a subnet range * For /24 subnet, returns IPs from .10 to .254 (reserved .1-.9 for infrastructure) */ export function* generateIPsInSubnet(subnet: string): Generator { const subnetInfo = parseSubnet(subnet); const [a, b, c] = subnetInfo.octets; // Start from .10 (reserve .1-.9 for infrastructure) // End at .254 (reserve .255 for broadcast) for (let lastOctet = 10; lastOctet <= 254; lastOctet++) { yield addCIDR(`${a}.${b}.${c}.${lastOctet}`, subnetInfo.maskBits); } } /** * Check if IP is within a reserved range */ export function isIPInRange(ip: string, rangeStart: string, rangeEnd: string | null): boolean { const ipNum = ipToNumber(ip); const startNum = ipToNumber(rangeStart); if (!rangeEnd) { // Single IP reservation return ipNum === startNum; } const endNum = ipToNumber(rangeEnd); return ipNum >= startNum && ipNum <= endNum; } /** * Convert IP address to number for comparison * Example: "10.0.10.50" → 167773234 */ function ipToNumber(ip: string): number { const octets = stripCIDR(ip) .split('.') .map((o) => Number.parseInt(o, 10)); return (octets[0] << 24) + (octets[1] << 16) + (octets[2] << 8) + octets[3]; }