#!/usr/bin/env node
// ipkit.js - an IP address toolkit (IPv4 + IPv6, no dependencies)
//
// Commands:
// node ipkit.js info <ip> Validate and describe an IP address
// node ipkit.js cidr <ip/prefix> Subnet calculator
// node ipkit.js contains <cidr> <ip> Is the IP inside the network?
// node ipkit.js split <cidr> <newPrefix> Divide a network into smaller subnets
// node ipkit.js range <startIp> <endIp> Convert an IP range to minimal CIDR blocks
// node ipkit.js local List this machine's network interfaces
const os = require("os");
// ---------------------------------------------------------------- parsing
function parseIPv4(s) {
const m = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(s);
if (!m) return null;
let n = 0n;
for (let i = 1; i <= 4; i++) {
if (m[i].length > 1 && m[i][0] === "0") return null; // reject leading zeros
const octet = Number(m[i]);
if (octet > 255) return null;
n = (n << 8n) | BigInt(octet);
}
return n;
}
function parseIPv6(s) {
if (!/^[0-9a-fA-F:.]+$/.test(s) || !s.includes(":")) return null;
let str = s;
// Handle embedded IPv4 such as ::ffff:192.168.1.1
if (str.includes(".")) {
const idx = str.lastIndexOf(":");
const v4 = parseIPv4(str.slice(idx + 1));
if (v4 === null) return null;
str = str.slice(0, idx + 1) + (v4 >> 16n).toString(16) + ":" + (v4 & 0xffffn).toString(16);
}
const halves = str.split("::");
if (halves.length > 2) return null;
const head = halves[0] ? halves[0].split(":") : [];
const tail = halves.length === 2 && halves[1] ? halves[1].split(":") : [];
let groups;
if (halves.length === 2) {
if (head.length + tail.length > 7) return null;
groups = [...head, ...Array(8 - head.length - tail.length).fill("0"), ...tail];
} else {
if (head.length !== 8) return null;
groups = head;
}
let n = 0n;
for (const g of groups) {
if (!/^[0-9a-fA-F]{1,4}$/.test(g)) return null;
n = (n << 16n) | BigInt(parseInt(g, 16));
}
return n;
}
function parseIP(s) {
const v4 = parseIPv4(s);
if (v4 !== null) return { version: 4, value: v4 };
const v6 = parseIPv6(s);
if (v6 !== null) return { version: 6, value: v6 };
return null;
}
function parseCIDR(s) {
const [addr, prefixStr, ...extra] = s.split("/");
if (extra.length || prefixStr === undefined || !/^\d+$/.test(prefixStr)) return null;
const ip = parseIP(addr);
if (!ip) return null;
const bits = ip.version === 4 ? 32 : 128;
const prefix = Number(prefixStr);
if (prefix > bits) return null;
return { ...ip, bits, prefix };
}
// -------------------------------------------------------------- formatting
function formatIPv4(n) {
return [24n, 16n, 8n, 0n].map((shift) => Number((n >> shift) & 0xffn)).join(".");
}
function formatIPv6(n) {
const groups = [];
for (let i = 7; i >= 0; i--) groups.push(Number((n >> BigInt(i * 16)) & 0xffffn));
// Find the longest run of zero groups (length >= 2) to compress with "::"
let bestStart = -1, bestLen = 0;
for (let i = 0; i < 8; ) {
if (groups[i] !== 0) { i++; continue; }
let j = i;
while (j < 8 && groups[j] === 0) j++;
if (j - i > bestLen) { bestStart = i; bestLen = j - i; }
i = j;
}
const hex = groups.map((g) => g.toString(16));
if (bestLen < 2) return hex.join(":");
const left = hex.slice(0, bestStart).join(":");
const right = hex.slice(bestStart + bestLen).join(":");
return `${left}::${right}`;
}
const format = (version, n) => (version === 4 ? formatIPv4(n) : formatIPv6(n));
function toBinary(version, n) {
if (version === 4) {
return [24n, 16n, 8n, 0n]
.map((s) => Number((n >> s) & 0xffn).toString(2).padStart(8, "0"))
.join(".");
}
return n.toString(2).padStart(128, "0").match(/.{16}/g).join(":");
}
// ---------------------------------------------------------- classification
const RANGES = {
4: [
["0.0.0.0/8", "'This' network"],
["10.0.0.0/8", "Private (RFC 1918)"],
["100.64.0.0/10", "Shared address space / CGNAT"],
["127.0.0.0/8", "Loopback"],
["169.254.0.0/16", "Link-local"],
["172.16.0.0/12", "Private (RFC 1918)"],
["192.0.0.0/24", "IETF protocol assignments"],
["192.0.2.0/24", "Documentation (TEST-NET-1)"],
["192.168.0.0/16", "Private (RFC 1918)"],
["198.18.0.0/15", "Benchmarking"],
["198.51.100.0/24", "Documentation (TEST-NET-2)"],
["203.0.113.0/24", "Documentation (TEST-NET-3)"],
["224.0.0.0/4", "Multicast"],
["240.0.0.0/4", "Reserved"],
["255.255.255.255/32", "Limited broadcast"],
],
6: [
["::/128", "Unspecified"],
["::1/128", "Loopback"],
["::ffff:0:0/96", "IPv4-mapped"],
["64:ff9b::/96", "NAT64"],
["2001:db8::/32", "Documentation"],
["fc00::/7", "Unique local (private)"],
["fe80::/10", "Link-local"],
["ff00::/8", "Multicast"],
],
};
function classify(ip) {
let best = null;
for (const [cidrStr, label] of RANGES[ip.version]) {
const c = parseCIDR(cidrStr);
if (isInside(c, ip.value) && (!best || c.prefix > best.prefix)) {
best = { prefix: c.prefix, label };
}
}
return best ? best.label : "Public / global unicast";
}
// ----------------------------------------------------------------- subnets
const maskFor = (bits, prefix) =>
((1n << BigInt(bits)) - 1n) ^ ((1n << BigInt(bits - prefix)) - 1n);
function network(c) {
const mask = maskFor(c.bits, c.prefix);
const all = (1n << BigInt(c.bits)) - 1n;
const net = c.value & mask;
return { mask, net, last: net | (~mask & all) };
}
function isInside(c, value) {
const { net, last } = network(c);
return value >= net && value <= last;
}
function rangeToCidrs(version, start, end) {
const bits = version === 4 ? 32 : 128;
const out = [];
while (start <= end) {
// Largest block that is aligned at `start` and does not overshoot `end`
let size = start === 0n ? 1n << BigInt(bits) : start & -start;
while (start + size - 1n > end) size >>= 1n;
const prefix = bits - (size.toString(2).length - 1);
out.push(`${format(version, start)}/${prefix}`);
start += size;
}
return out;
}
// ---------------------------------------------------------------- commands
function cmdInfo(arg) {
const ip = parseIP(arg);
if (!ip) return fail(`"${arg}" is not a valid IPv4 or IPv6 address.`);
const rows = [
["Address", format(ip.version, ip.value)],
["Version", `IPv${ip.version}`],
["Type", classify(ip)],
["Integer", ip.value.toString()],
["Hex", "0x" + ip.value.toString(16)],
["Binary", toBinary(ip.version, ip.value)],
];
if (ip.version === 6) {
rows.push(["Expanded", Array.from({ length: 8 }, (_, i) =>
Number((ip.value >> BigInt((7 - i) * 16)) & 0xffffn).toString(16).padStart(4, "0")).join(":")]);
}
printTable(rows);
}
function cmdCidr(arg) {
const c = parseCIDR(arg);
if (!c) return fail(`"${arg}" is not valid CIDR notation (example: 192.168.1.0/24).`);
const { mask, net, last } = network(c);
const total = 1n << BigInt(c.bits - c.prefix);
const f = (n) => format(c.version, n);
const rows = [
["Network", `${f(net)}/${c.prefix}`],
["Version", `IPv${c.version}`],
];
if (c.version === 4) {
const all = 0xffffffffn;
rows.push(["Netmask", f(mask)], ["Wildcard", f(~mask & all)], ["Broadcast", f(last)]);
if (c.prefix <= 30) {
rows.push(["First host", f(net + 1n)], ["Last host", f(last - 1n)], ["Usable hosts", (total - 2n).toString()]);
} else if (c.prefix === 31) {
rows.push(["First host", f(net)], ["Last host", f(last)], ["Usable hosts", "2 (point-to-point, RFC 3021)"]);
} else {
rows.push(["First host", f(net)], ["Last host", f(net)], ["Usable hosts", "1 (single host)"]);
}
rows.push(["Total addresses", total.toString()]);
} else {
rows.push(["First address", f(net)], ["Last address", f(last)], ["Total addresses", total.toString()]);
}
rows.push(["Type", classify({ version: c.version, value: net })]);
printTable(rows);
}
function cmdContains(cidrArg, ipArg) {
const c = parseCIDR(cidrArg);
const ip = parseIP(ipArg || "");
if (!c) return fail(`"${cidrArg}" is not valid CIDR notation.`);
if (!ip) return fail(`"${ipArg}" is not a valid IP address.`);
if (c.version !== ip.version) return fail("IP version mismatch between network and address.");
const inside = isInside(c, ip.value);
console.log(`${ipArg} is ${inside ? "INSIDE" : "OUTSIDE"} ${format(c.version, network(c).net)}/${c.prefix}`);
process.exitCode = inside ? 0 : 1;
}
function cmdSplit(cidrArg, newPrefixArg) {
const c = parseCIDR(cidrArg);
if (!c) return fail(`"${cidrArg}" is not valid CIDR notation.`);
const newPrefix = Number(newPrefixArg);
if (!Number.isInteger(newPrefix) || newPrefix <= c.prefix || newPrefix > c.bits) {
return fail(`New prefix must be an integer between ${c.prefix + 1} and ${c.bits}.`);
}
const count = 1n << BigInt(newPrefix - c.prefix);
const step = 1n << BigInt(c.bits - newPrefix);
const { net } = network(c);
const LIMIT = 64n;
const shown = count < LIMIT ? count : LIMIT;
console.log(`${format(c.version, net)}/${c.prefix} -> ${count} subnets of /${newPrefix}`);
for (let i = 0n; i < shown; i++) {
console.log(` ${(i + 1n).toString().padStart(3)}. ${format(c.version, net + i * step)}/${newPrefix}`);
}
if (count > shown) console.log(` ... and ${count - shown} more`);
}
function cmdRange(a, b) {
const start = parseIP(a || "");
const end = parseIP(b || "");
if (!start || !end) return fail("Both arguments must be valid IP addresses.");
if (start.version !== end.version) return fail("Start and end must be the same IP version.");
if (start.value > end.value) return fail("Start address must not be greater than the end address.");
const blocks = rangeToCidrs(start.version, start.value, end.value);
console.log(`${a} - ${b} = ${blocks.length} CIDR block${blocks.length === 1 ? "" : "s"}:`);
blocks.forEach((blk) => console.log(` ${blk}`));
}
function cmdLocal() {
const ifaces = os.networkInterfaces();
for (const [name, addrs] of Object.entries(ifaces)) {
console.log(name);
for (const a of addrs) {
const flags = [a.internal ? "internal" : "external", a.mac !== "00:00:00:00:00:00" ? a.mac : null]
.filter(Boolean).join(", ");
console.log(` ${a.family.padEnd(4)} ${a.cidr || a.address} (${flags})`);
}
}
}
// ----------------------------------------------------------------- helpers
function printTable(rows) {
const width = Math.max(...rows.map(([k]) => k.length));
for (const [k, v] of rows) console.log(`${k.padEnd(width)} : ${v}`);
}
function fail(msg) {
console.error(`Error: ${msg}`);
process.exitCode = 2;
}
function usage() {
console.log(`ipkit - IP address toolkit
Usage:
node ipkit.js info <ip> Validate and describe an IP address
node ipkit.js cidr <ip/prefix> Subnet calculator
node ipkit.js contains <cidr> <ip> Check whether an IP is inside a network
node ipkit.js split <cidr> <newPrefix> Split a network into smaller subnets
node ipkit.js range <startIp> <endIp> Convert an IP range to CIDR blocks
node ipkit.js local List local network interfaces
Examples:
node ipkit.js info 192.168.1.10
node ipkit.js info 2001:db8::1
node ipkit.js cidr 10.0.0.0/22
node ipkit.js contains 10.0.0.0/8 10.20.30.40
node ipkit.js split 192.168.0.0/24 26
node ipkit.js range 192.168.1.5 192.168.1.20`);
}
// -------------------------------------------------------------------- main
const [cmd, ...args] = process.argv.slice(2);
switch (cmd) {
case "info": args[0] ? cmdInfo(args[0]) : fail("Missing <ip>."); break;
case "cidr": args[0] ? cmdCidr(args[0]) : fail("Missing <ip/prefix>."); break;
case "contains": args.length >= 2 ? cmdContains(args[0], args[1]) : fail("Usage: contains <cidr> <ip>"); break;
case "split": args.length >= 2 ? cmdSplit(args[0], args[1]) : fail("Usage: split <cidr> <newPrefix>"); break;
case "range": args.length >= 2 ? cmdRange(args[0], args[1]) : fail("Usage: range <startIp> <endIp>"); break;
case "local": cmdLocal(); break;
default: usage();
}
No comments:
Post a Comment
no thanks
Note: Only a member of this blog may post a comment.