This Node.js script checks whether an IP address (IPv4 or IPv6) falls inside any of a list of CIDR blocks, using nothing but BigInt bitwise arithmetic — it makes no network request and calls no API at all, since subnet membership is pure math over data you already have, not a fact any API needs to answer.
placeholder
// SPDX-License-Identifier: MIT
// subnet-membership.js -- check whether an IP address falls inside any of a
// list of CIDR blocks, entirely with local BigInt arithmetic.
//
// This recipe does NOT call the ip.xakki.pro API at all, or any network
// endpoint -- subnet membership is pure math over an address and a set of
// prefixes you already have, not a fact our (or anyone else's) API needs to
// answer. It is included in this section deliberately, as an honest
// counter-example to the other recipes here: not every IP task is an API
// call, and a tool that fabricates a network round-trip for arithmetic it
// could do locally is worse, not better.
//
// Zero dependencies -- plain Node.js built-ins only (BigInt is a language
// primitive since Node 10.4; no npm install). Works as a CLI script and as
// a Node module (module.exports below, used by this recipe's own
// `node --test` suite, subnet-membership.test.js).
//
// CLI usage:
// node subnet-membership.js <ip> <cidr1> [cidr2 ...]
// node subnet-membership.js 10.0.5.9 10.0.0.0/8 192.168.0.0/16
// node subnet-membership.js 2001:db8::1 2001:db8::/32
'use strict';
// parseIPv4(str) -> BigInt | null -- strict dotted-quad, each octet 0-255,
// exactly 4 octets, no leading-zero ambiguity (leading zeros rejected --
// some stacks read "010" as octal, a classic SSRF-adjacent footgun).
function parseIPv4(str) {
var parts = str.split('.');
if (parts.length !== 4) return null;
var value = 0n;
for (var i = 0; i < 4; i++) {
var part = parts[i];
if (!/^\d{1,3}$/.test(part) || (part.length > 1 && part[0] === '0')) return null;
var n = Number(part);
if (n > 255) return null;
value = (value << 8n) | BigInt(n);
}
return value;
}
// parseIPv6(str) -> BigInt | null -- supports "::" compression (at most
// once), 2-8 colon-separated hextets, and rejects malformed input. Does
// NOT support embedded-IPv4 tail notation (e.g. "::ffff:1.2.3.4") -- a
// known, documented limitation (see the FAQ on the page for this recipe).
function parseIPv6(str) {
if (str.indexOf(':') === -1) return null;
var doubleColonCount = (str.match(/::/g) || []).length;
if (doubleColonCount > 1) return null;
var halves = str.split('::');
if (halves.length > 2) return null;
var headParts = halves[0] === '' ? [] : halves[0].split(':');
var tailParts = halves.length === 2 ? (halves[1] === '' ? [] : halves[1].split(':')) : [];
if (halves.length === 1) {
if (headParts.length !== 8) return null;
} else {
var missing = 8 - (headParts.length + tailParts.length);
if (missing < 1) return null;
var zeros = [];
for (var z = 0; z < missing; z++) zeros.push('0');
headParts = headParts.concat(zeros, tailParts);
}
if (headParts.length !== 8) return null;
var value = 0n;
for (var i = 0; i < 8; i++) {
if (!/^[0-9a-fA-F]{1,4}$/.test(headParts[i])) return null;
value = (value << 16n) | BigInt(parseInt(headParts[i], 16));
}
return value;
}
// ipToBigInt(str) -> { version: 4 | 6, value: BigInt } | null
function ipToBigInt(str) {
var v4 = parseIPv4(str);
if (v4 !== null) return { version: 4, value: v4 };
var v6 = parseIPv6(str);
if (v6 !== null) return { version: 6, value: v6 };
return null;
}
// parseCIDR(str) -> { version, network: BigInt, prefixLen, maxBits } | null
// -- the network address is the input's own address MASKED down to
// prefixLen (so "10.1.2.3/8" behaves like "10.0.0.0/8" for membership,
// matching how routers actually match traffic).
function parseCIDR(str) {
var slash = str.lastIndexOf('/');
if (slash === -1) return null;
var addrPart = str.slice(0, slash);
var prefixPart = str.slice(slash + 1);
if (!/^\d{1,3}$/.test(prefixPart)) return null;
var prefixLen = Number(prefixPart);
var parsed = ipToBigInt(addrPart);
if (parsed === null) return null;
var maxBits = parsed.version === 4 ? 32 : 128;
if (prefixLen > maxBits) return null;
var hostBits = BigInt(maxBits - prefixLen);
var mask = hostBits === 0n ? (2n ** BigInt(maxBits)) - 1n : (((1n << BigInt(prefixLen)) - 1n) << hostBits);
var network = parsed.value & mask;
return { version: parsed.version, network: network, prefixLen: prefixLen, maxBits: maxBits };
}
// isInCIDR(ipStr, cidrStr) -> true | false | null (null = either input
// failed to parse -- distinct from a real "not a member" false).
function isInCIDR(ipStr, cidrStr) {
var ip = ipToBigInt(ipStr);
var cidr = parseCIDR(cidrStr);
if (ip === null || cidr === null || ip.version !== cidr.version) return null;
var hostBits = BigInt(cidr.maxBits - cidr.prefixLen);
var ipNetworkPart = hostBits === 0n ? ip.value : (ip.value >> hostBits) << hostBits;
return ipNetworkPart === cidr.network;
}
// findMatchingCIDRs(ipStr, cidrList) -> array of the CIDR strings from
// cidrList that ipStr falls inside. Malformed CIDR entries in the list are
// silently skipped (not a member, not an error) -- callers who need to
// know about a malformed entry should validate the list with parseCIDR
// themselves first.
function findMatchingCIDRs(ipStr, cidrList) {
var matches = [];
for (var i = 0; i < cidrList.length; i++) {
if (isInCIDR(ipStr, cidrList[i]) === true) matches.push(cidrList[i]);
}
return matches;
}
function runCLI(argv) {
if (argv.length < 2) {
console.error('usage: node subnet-membership.js <ip> <cidr1> [cidr2 ...]');
process.exitCode = 1;
return;
}
var ipStr = argv[0];
var cidrList = argv.slice(1);
if (ipToBigInt(ipStr) === null) {
console.error('error: "' + ipStr + '" is not a valid IPv4 or IPv6 address');
process.exitCode = 1;
return;
}
var anyInvalidCIDR = false;
for (var i = 0; i < cidrList.length; i++) {
if (parseCIDR(cidrList[i]) === null) {
console.error('warning: skipping invalid CIDR "' + cidrList[i] + '"');
anyInvalidCIDR = true;
}
}
var matches = findMatchingCIDRs(ipStr, cidrList);
if (matches.length === 0) {
console.log(ipStr + ' is NOT inside any of the given subnets.');
process.exitCode = anyInvalidCIDR ? 1 : 2;
} else {
console.log(ipStr + ' is inside:');
for (var m = 0; m < matches.length; m++) console.log(' ' + matches[m]);
}
}
if (require.main === module) {
runCLI(process.argv.slice(2));
}
module.exports = {
parseIPv4: parseIPv4,
parseIPv6: parseIPv6,
ipToBigInt: ipToBigInt,
parseCIDR: parseCIDR,
isInCIDR: isInCIDR,
findMatchingCIDRs: findMatchingCIDRs
};
This script is MIT-licensed (SPDX-License-Identifier: MIT header at the top of the file) — copy, modify and reuse it freely in your own project, no attribution required.
Both IPv4 and IPv6 addresses are converted to a single BigInt (32 bits or 128 bits wide) so that "is this address inside this CIDR" reduces to one bitwise AND against a precomputed mask and a comparison — the same operation a router's forwarding table performs, done here in plain JavaScript with no native/WASM dependency. BigInt (not Number) is required for IPv6, since 128 bits overflows a JS double's 53-bit safe integer range.
Parsing is deliberately strict, not lenient: a leading zero in an IPv4 octet ("010.0.0.1") is rejected outright rather than silently reinterpreted, since some older stacks read a leading zero as octal — exactly the kind of ambiguity that has caused real SSRF bypasses elsewhere. IPv6 "::" compression is supported (at most once, per RFC 5952), but the embedded-IPv4-tail notation ("::ffff:1.2.3.4") is a known, documented limitation — not supported by this recipe.
isInCIDR returns null (not false) when either input fails to parse or the two are different address families — a null-vs-false distinction the CLI surfaces as a warning rather than silently reporting "not a member" for a CIDR entry that was simply malformed. findMatchingCIDRs skips invalid list entries silently for convenience; a caller who needs to know about a malformed entry validates the list with parseCIDR itself first, since not every use of the list wants that noise.
placeholder
Like this tool? Our API gives free accounts 1,000 requests/day.See the API docs →