Скрипт проверки принадлежности подсети (JavaScript)

Этот Node.js-скрипт проверяет, попадает ли IP-адрес (IPv4 или IPv6) в один из списка CIDR-блоков, используя только битовую арифметику над BigInt — он не делает сетевых запросов и не обращается ни к какому API, поскольку принадлежность подсети — это чистая математика над уже имеющимися данными, а не факт, на который должен отвечать какой-либо API.

Как запустить этот скрипт

Зависимости: нет — только встроенные модули Node.js 18+ (глобальный fetch, модули net/fs). npm install и package.json не нужны.

  1. Скачайте subnet-membership.js ниже.
  2. Запустите: node subnet-membership.js 10.0.5.9 10.0.0.0/8 192.168.0.0/16
  3. Скрипт выводит все CIDR-блоки из списка, в которые попадает адрес, либо сообщает, что совпадений нет.
  4. require('./subnet-membership.js') также работает из вашего собственного Node-скрипта — см. экспортируемые функции ниже.

Полный исходный код

// 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
};

Скрипт распространяется по лицензии MIT (заголовок SPDX-License-Identifier: MIT в начале файла) — копируйте, изменяйте и используйте свободно в своём проекте, указывать авторство не обязательно.

Скрипт проверки принадлежности подсети (JavaScript)

И IPv4-, и IPv6-адреса переводятся в единый BigInt (32 или 128 бит), поэтому вопрос «входит ли этот адрес в этот CIDR» сводится к одной побитовой операции И с заранее вычисленной маской и сравнению — та же операция, что выполняет таблица маршрутизации, только на чистом JavaScript без нативных/WASM-зависимостей. Именно BigInt (а не Number) необходим для IPv6, так как 128 бит выходят за пределы безопасного 53-битного диапазона целых чисел JS-числа double.

Разбор намеренно строгий, а не снисходительный: ведущий ноль в октете IPv4 («010.0.0.1») сразу отклоняется, а не молча переинтерпретируется, поскольку некоторые старые стеки читают ведущий ноль как восьмеричную запись — именно такая неоднозначность становилась причиной реальных обходов SSRF в других местах. Сжатие «::» в IPv6 поддерживается (не более одного раза, согласно RFC 5952), но встроенная запись IPv4-хвоста («::ffff:1.2.3.4») — известное, задокументированное ограничение, не поддерживаемое этим рецептом.

isInCIDR возвращает null (а не false), когда любой из входов не парсится либо адреса относятся к разным семействам — это различие null и false CLI выводит как предупреждение, а не молча сообщает «не входит» для CIDR-записи, которая попросту была некорректной. findMatchingCIDRs для удобства молча пропускает некорректные записи списка; тому, кому нужно знать о некорректной записи, стоит провалидировать список через parseCIDR самостоятельно, поскольку не каждому сценарию использования списка нужен этот шум.

Глоссарий

BigInt
Тип целых чисел произвольной точности в JavaScript (суффикс 123n) — требуется здесь, поскольку 128-битный адрес IPv6 не помещается в безопасный 53-битный диапазон обычного Number.
Нотация CIDR
Classless Inter-Domain Routing — адрес плюс слэш и длина префикса (например, 10.0.0.0/8), обозначающие непрерывный блок адресов с общими старшими битами в этом количестве.
Адрес сети
Адрес CIDR-блока с обнулёнными битами хоста — «10.1.2.3/8» и «10.0.0.0/8» дают один и тот же адрес сети и, соответственно, одно и то же множество принадлежности.
Сжатие «::»
Сокращённая запись IPv6, схлопывающая одну последовательность идущих подряд нулевых групп в «::», допустима не более одного раза на адрес (RFC 5952) — «2001:db8::1» разворачивается в 8 явных групп.
Биты хоста
Младшие биты адреса, НЕ покрытые длиной префикса — например, последние 24 бита в /8 — обнуляются при вычислении адреса сети CIDR-блока.
module.exports (лёгкий UMD)
Паттерн, по которому этот файл одновременно является самостоятельным CLI-скриптом Node (`node subnet-membership.js ...`) и модулем, подключаемым через require для собственного набора тестов node --test, без какого-либо сборщика.

Аналог в API

Этот скрипт не обращается к API — этот рецепт намеренно демонстрирует противоположность всем остальным в разделе: принадлежность подсети — локальная битовая математика, а не факт об IP, на который должен отвечать наш (или любой) API. Если нужны ещё и живые факты об IP (геолокация, ASN, признаки угроз/фрода) — это описано в документации API:

Читать документацию API

Часто задаваемые вопросы

Почему этот рецепт не обращается к API ip.xakki.pro, как остальные?
Потому что спрашивать нечего — проверка вхождения адреса в уже имеющийся CIDR-блок это арифметика, а не поиск. Этот рецепт включён специально, чтобы явно показать это различие, а не придумывать вызов API для задачи, которая в нём не нуждается.
Поддерживает ли он адреса вида «::ffff:1.2.3.4» со встроенным IPv4?
Нет — это задокументированное ограничение. parseIPv6 ожидает, что каждая группа — обычный шестнадцатеричный hextet; передавайте адрес в обычной форме IPv6 либо в обычной форме IPv4.
Почему BigInt, а не просто разбиение адреса на 4 байта для IPv4?
Разбиение на байты само по себе прекрасно работает для IPv4, но этот скрипт намеренно использует один и тот же код для обоих семейств — BigInt единственный встроенный в JS целочисленный тип, достаточно широкий для 128-битного адреса IPv6, поэтому его использование и для IPv4 позволяет parseCIDR/isInCIDR оставаться одинаковыми для обоих.
Что произойдёт, если в списке окажется некорректный CIDR?
CLI выводит предупреждение в stderr и пропускает его; findMatchingCIDRs (используется внутри) молча игнорирует некорректные записи, чтобы одна плохая строка в длинном списке не прерывала всю проверку.
Считается ли сам адрес сети принадлежащим собственному CIDR-блоку?
Да, как и последний, «широковещательный» адрес — оба граничных адреса блока считаются принадлежащими ему (см. граничные случаи в наборе тестов node --test для точных примеров).

Понравился инструмент? Бесплатный аккаунт нашего API даёт 1000 запросов в день.Смотреть документацию API →