Files
Yagiz Nizipli bc6e1ad8f9 net: speed up BlockList.check for strings
Skip toLowerCase() for the documented ipv4/ipv6 families and add a
V8 Fast API for one-byte address strings. The FastOneByteString
callback takes FastApiCallbackOptions so V8 can use the fast path
reliably (required for string-shaped Fast API arguments).

Overlong IPv6 zone ids are parsed like uv_ip6_addr (unknown zone
is scope_id 0; address parts 40+ chars stay a miss). IPv4 zone
suffixes stay a miss, matching uv_ip4_addr.

Assisted-by: a closed-source coding agent
Signed-off-by: Yagiz Nizipli <yagiz@nizipli.com>
PR-URL: https://github.com/nodejs/node/pull/66166
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Tim Perry <pimterry@gmail.com>
2026-09-25 14:39:01 +00:00

501 lines
13 KiB
JavaScript

'use strict';
const {
ArrayIsArray,
Boolean,
JSONParse,
NumberIsNaN,
NumberParseInt,
ObjectFreeze,
ObjectSetPrototypeOf,
StringPrototypeIncludes,
StringPrototypeLastIndexOf,
StringPrototypeSlice,
StringPrototypeToLowerCase,
Symbol,
} = primordials;
const {
BlockList: BlockListHandle,
AF_INET,
AF_INET6,
} = internalBinding('block_list');
const {
customInspectSymbol: kInspect,
} = require('internal/util');
const {
SocketAddress,
kHandle: kSocketAddressHandle,
} = require('internal/socketaddress');
const {
markTransferMode,
kClone,
kDeserialize,
} = require('internal/worker/js_transferable');
const { inspect } = require('internal/util/inspect');
const kHandle = Symbol('kHandle');
const { owner_symbol } = internalBinding('symbols');
const {
ERR_INVALID_ARG_VALUE,
ERR_INVALID_ARG_TYPE,
} = require('internal/errors').codes;
const { validateArray, validateInt32, validateString } = require('internal/validators');
function parseCIDR(cidr) {
validateString(cidr, 'cidr');
const slash = StringPrototypeLastIndexOf(cidr, '/');
if (slash === -1) {
throw new ERR_INVALID_ARG_VALUE('cidr', cidr, 'must contain a prefix length (e.g. "10.0.0.0/8")');
}
const address = StringPrototypeSlice(cidr, 0, slash);
const prefixStr = StringPrototypeSlice(cidr, slash + 1);
const prefix = NumberParseInt(prefixStr, 10);
if (NumberIsNaN(prefix) || `${prefix}` !== prefixStr) {
throw new ERR_INVALID_ARG_VALUE('cidr', cidr, 'prefix length must be a valid integer');
}
const family = StringPrototypeIncludes(address, ':') ? 'ipv6' : 'ipv4';
return { address, prefix, family };
}
class BlockList {
constructor() {
markTransferMode(this, true, false);
this[kHandle] = new BlockListHandle();
this[kHandle][owner_symbol] = this;
}
/**
* Returns true if the value is a BlockList
* @param {any} value
* @returns {boolean}
*/
static isBlockList(value) {
return value?.[kHandle] !== undefined;
}
static PRIVATE_RANGES = ObjectFreeze([
// RFC 1918 - Private IPv4
'10.0.0.0/8',
'172.16.0.0/12',
'192.168.0.0/16',
// Loopback
'127.0.0.0/8',
'::1/128',
// Link-local
'169.254.0.0/16',
'fe80::/10',
// Unique local (ULA)
'fc00::/7',
]);
[kInspect](depth, options) {
if (depth < 0)
return this;
const opts = {
...options,
depth: options.depth == null ? null : options.depth - 1,
};
return `BlockList ${inspect({
rules: this.rules,
}, opts)}`;
}
/**
* @param {string|SocketAddress} address
* @param {string} [family]
*/
addAddress(address, family = 'ipv4') {
if (!SocketAddress.isSocketAddress(address)) {
validateString(address, 'address');
validateString(family, 'family');
address = new SocketAddress({
address,
family,
});
}
this[kHandle].addAddress(address[kSocketAddressHandle]);
}
/**
*
* @param {(string|SocketAddress)[]} addresses
* @param {string} [family]
*/
addAddresses(addresses, family = 'ipv4') {
validateArray(addresses, 'addresses');
validateString(family, 'family');
const handles = [];
for (let i = 0; i < addresses.length; i++) {
let address = addresses[i];
if (!SocketAddress.isSocketAddress(address)) {
validateString(address, `addresses[${i}]`);
address = new SocketAddress({ address, family });
}
handles.push(address[kSocketAddressHandle]);
}
this[kHandle].addAddresses(handles);
}
/**
* @param {string|SocketAddress} start
* @param {string|SocketAddress} end
* @param {string} [family]
*/
addRange(start, end, family = 'ipv4') {
if (!SocketAddress.isSocketAddress(start)) {
validateString(start, 'start');
validateString(family, 'family');
start = new SocketAddress({
address: start,
family,
});
}
if (!SocketAddress.isSocketAddress(end)) {
validateString(end, 'end');
validateString(family, 'family');
end = new SocketAddress({
address: end,
family,
});
}
const ret = this[kHandle].addRange(
start[kSocketAddressHandle],
end[kSocketAddressHandle]);
if (ret === false)
throw new ERR_INVALID_ARG_VALUE('start', start, 'must come before end');
}
/**
* @param {string|SocketAddress} network
* @param {number} prefix
* @param {string} [family]
*/
addSubnet(network, prefix, family = 'ipv4') {
if (!SocketAddress.isSocketAddress(network)) {
validateString(network, 'network');
validateString(family, 'family');
network = new SocketAddress({
address: network,
family,
});
}
switch (network.family) {
case 'ipv4':
validateInt32(prefix, 'prefix', 0, 32);
break;
case 'ipv6':
validateInt32(prefix, 'prefix', 0, 128);
break;
}
// Coerce -0 to +0.
prefix += 0;
this[kHandle].addSubnet(network[kSocketAddressHandle], prefix);
}
/**
* @param {string} cidr
*/
addCIDR(cidr) {
const { address, prefix, family } = parseCIDR(cidr);
this.addSubnet(address, prefix, family);
}
/**
* @param {string[]} cidrs
*/
addCIDRs(cidrs) {
validateArray(cidrs, 'cidrs');
// Validate and parse all entries first so that an exception mid-array
// does not leave the blocklist half-modified.
const parsed = [];
for (let i = 0; i < cidrs.length; i++) {
validateString(cidrs[i], `cidrs[${i}]`);
parsed.push(parseCIDR(cidrs[i]));
}
for (let i = 0; i < parsed.length; i++) {
const { address, prefix, family } = parsed[i];
this.addSubnet(address, prefix, family);
}
}
/**
* @param {string|SocketAddress} address
* @param {string} [family]
*/
removeAddress(address, family = 'ipv4') {
if (!SocketAddress.isSocketAddress(address)) {
validateString(address, 'address');
validateString(family, 'family');
address = new SocketAddress({
address,
family,
});
}
this[kHandle].removeAddress(address[kSocketAddressHandle]);
}
/**
* @param {string|SocketAddress} start
* @param {string|SocketAddress} end
* @param {string} [family]
*/
removeRange(start, end, family = 'ipv4') {
if (!SocketAddress.isSocketAddress(start)) {
validateString(start, 'start');
validateString(family, 'family');
start = new SocketAddress({
address: start,
family,
});
}
if (!SocketAddress.isSocketAddress(end)) {
validateString(end, 'end');
validateString(family, 'family');
end = new SocketAddress({
address: end,
family,
});
}
this[kHandle].removeRange(
start[kSocketAddressHandle],
end[kSocketAddressHandle]);
}
/**
* @param {string|SocketAddress} network
* @param {number} prefix
* @param {string} [family]
*/
removeSubnet(network, prefix, family = 'ipv4') {
if (!SocketAddress.isSocketAddress(network)) {
validateString(network, 'network');
validateString(family, 'family');
network = new SocketAddress({
address: network,
family,
});
}
switch (network.family) {
case 'ipv4':
validateInt32(prefix, 'prefix', 0, 32);
break;
case 'ipv6':
validateInt32(prefix, 'prefix', 0, 128);
break;
}
prefix += 0;
this[kHandle].removeSubnet(network[kSocketAddressHandle], prefix);
}
/**
* @param {string} cidr
*/
removeCIDR(cidr) {
const { address, prefix, family } = parseCIDR(cidr);
this.removeSubnet(address, prefix, family);
}
/**
* @param {string|SocketAddress} address
* @param {string} [family]
* @returns {boolean}
*/
check(address, family = 'ipv4') {
if (!SocketAddress.isSocketAddress(address)) {
validateString(address, 'address');
validateString(family, 'family');
// Fast path: pass the string directly to C++ which does
// inet_pton + Apply() without allocating a JS SocketAddress wrapper.
// The documented values are 'ipv4' / 'ipv6' (and the IPvX aliases).
// Avoid toLowerCase on that hot path.
let af;
if (family === 'ipv4' || family === 'IPv4') {
af = AF_INET;
} else if (family === 'ipv6' || family === 'IPv6') {
af = AF_INET6;
} else {
af = StringPrototypeToLowerCase(family) === 'ipv4' ?
AF_INET : AF_INET6;
}
return this[kHandle].checkString(address, af);
}
return Boolean(this[kHandle].check(address[kSocketAddressHandle]));
}
/**
* Removes all rules from the block list.
*/
clear() {
this[kHandle].clear();
}
/*
* @param {string[]} data
* @example
* const data = [
* // IPv4 examples
* 'Subnet: IPv4 192.168.1.0/24',
* 'Address: IPv4 10.0.0.5',
* 'Range: IPv4 192.168.2.1-192.168.2.10',
* 'Range: IPv4 10.0.0.1-10.0.0.10',
*
* // IPv6 examples
* 'Subnet: IPv6 2001:0db8:85a3:0000:0000:8a2e:0370:7334/64',
* 'Address: IPv6 2001:0db8:85a3:0000:0000:8a2e:0370:7334',
* 'Range: IPv6 2001:0db8:85a3:0000:0000:8a2e:0370:7334-2001:0db8:85a3:0000:0000:8a2e:0370:7335',
* 'Subnet: IPv6 2001:db8:1234::/48',
* 'Address: IPv6 2001:db8:1234::1',
* 'Range: IPv6 2001:db8:1234::1-2001:db8:1234::10'
* ];
*/
#parseIPInfo(data) {
for (const item of data) {
if (item.includes('IPv4')) {
const subnetMatch = item.match(
/Subnet: IPv4 (\d{1,3}(?:\.\d{1,3}){3})\/(\d{1,2})/,
);
if (subnetMatch) {
const { 1: network, 2: prefix } = subnetMatch;
this.addSubnet(network, NumberParseInt(prefix));
continue;
}
const addressMatch = item.match(/Address: IPv4 (\d{1,3}(?:\.\d{1,3}){3})/);
if (addressMatch) {
const { 1: address } = addressMatch;
this.addAddress(address);
continue;
}
const rangeMatch = item.match(
/Range: IPv4 (\d{1,3}(?:\.\d{1,3}){3})-(\d{1,3}(?:\.\d{1,3}){3})/,
);
if (rangeMatch) {
const { 1: start, 2: end } = rangeMatch;
this.addRange(start, end);
continue;
}
}
// IPv6 parsing with support for compressed addresses
if (item.includes('IPv6')) {
// IPv6 subnet pattern: supports both full and compressed formats
// Examples:
// - 2001:0db8:85a3:0000:0000:8a2e:0370:7334/64 (full)
// - 2001:db8:85a3::8a2e:370:7334/64 (compressed)
// - 2001:db8:85a3::192.0.2.128/64 (mixed)
const ipv6SubnetMatch = item.match(
/Subnet: IPv6 ([0-9a-fA-F:.]{1,45})\/([0-9]{1,3})/i,
);
if (ipv6SubnetMatch) {
const { 1: network, 2: prefix } = ipv6SubnetMatch;
this.addSubnet(network, NumberParseInt(prefix), 'ipv6');
continue;
}
// IPv6 address pattern: supports both full and compressed formats
// Examples:
// - 2001:0db8:85a3:0000:0000:8a2e:0370:7334 (full)
// - 2001:db8:85a3::8a2e:370:7334 (compressed)
// - 2001:db8:85a3::192.0.2.128 (mixed)
const ipv6AddressMatch = item.match(/Address: IPv6 ([0-9a-fA-F:.]{1,45})/i);
if (ipv6AddressMatch) {
const { 1: address } = ipv6AddressMatch;
this.addAddress(address, 'ipv6');
continue;
}
// IPv6 range pattern: supports both full and compressed formats
// Examples:
// - 2001:0db8:85a3:0000:0000:8a2e:0370:7334-2001:0db8:85a3:0000:0000:8a2e:0370:7335 (full)
// - 2001:db8:85a3::8a2e:370:7334-2001:db8:85a3::8a2e:370:7335 (compressed)
// - 2001:db8:85a3::192.0.2.128-2001:db8:85a3::192.0.2.129 (mixed)
const ipv6RangeMatch = item.match(/Range: IPv6 ([0-9a-fA-F:.]{1,45})-([0-9a-fA-F:.]{1,45})/i);
if (ipv6RangeMatch) {
const { 1: start, 2: end } = ipv6RangeMatch;
this.addRange(start, end, 'ipv6');
continue;
}
}
}
}
toJSON() {
return this.rules;
}
fromJSON(data) {
// The data argument must be a string, or an array of strings that
// is JSON parseable.
if (ArrayIsArray(data)) {
for (const n of data) {
if (typeof n !== 'string') {
throw new ERR_INVALID_ARG_TYPE('data', ['string', 'string[]'], data);
}
}
} else if (typeof data !== 'string') {
throw new ERR_INVALID_ARG_TYPE('data', ['string', 'string[]'], data);
} else {
data = JSONParse(data);
if (!ArrayIsArray(data)) {
throw new ERR_INVALID_ARG_TYPE('data', ['string', 'string[]'], data);
}
for (const n of data) {
if (typeof n !== 'string') {
throw new ERR_INVALID_ARG_TYPE('data', ['string', 'string[]'], data);
}
}
}
this.#parseIPInfo(data);
}
get rules() {
return this[kHandle].getRules();
}
get size() {
return this[kHandle].getSize();
}
[kClone]() {
const handle = this[kHandle];
return {
data: { handle },
deserializeInfo: 'internal/blocklist:InternalBlockList',
};
}
[kDeserialize]({ handle }) {
this[kHandle] = handle;
this[kHandle][owner_symbol] = this;
}
}
class InternalBlockList {
constructor(handle) {
markTransferMode(this, true, false);
this[kHandle] = handle;
if (handle !== undefined)
handle[owner_symbol] = this;
}
}
InternalBlockList.prototype.constructor = BlockList.prototype.constructor;
ObjectSetPrototypeOf(InternalBlockList.prototype, BlockList.prototype);
module.exports = {
BlockList,
InternalBlockList,
kHandle,
};