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>
This commit is contained in:
Yagiz Nizipli authored and GitHub committed 2026-09-25 14:39:01 +00:00
1 parent b48f24afbd
commit bc6e1ad8f9
5 files changed
+114 -7

No files matched your search

+11 -2
View File
@@ -315,8 +315,17 @@ class BlockList {
validateString(family, 'family');
// Fast path: pass the string directly to C++ which does
// inet_pton + Apply() without allocating a JS SocketAddress wrapper.
const af = StringPrototypeToLowerCase(family) === 'ipv4' ?
AF_INET : AF_INET6;
// 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]));
+57 -1
View File
@@ -8,6 +8,9 @@
#include "node_hash.h"
#include "node_sockaddr-inl.h" // NOLINT(build/include_inline)
#include "uv.h"
#include "v8-fast-api-calls.h"
#include <cstring>
#include <memory>
#include <string>
@@ -18,6 +21,8 @@ namespace node {
using v8::Array;
using v8::CFunction;
using v8::Context;
using v8::FastApiCallbackOptions;
using v8::FastOneByteString;
using v8::FunctionCallbackInfo;
using v8::FunctionTemplate;
using v8::Int32;
@@ -1032,6 +1037,53 @@ void SocketAddressBlockListWrap::CheckString(
args.GetReturnValue().Set(wrap->blocklist_->Apply(addr));
}
bool SocketAddressBlockListWrap::FastCheckString(
Local<Object> receiver,
const FastOneByteString& address,
int32_t family,
// NOLINTNEXTLINE(runtime/references) This is V8 api.
FastApiCallbackOptions& options) {
// FastOneByteString is not NUL-terminated. Copy onto the stack
// before any other work: a GC would invalidate `address`.
//
// uv_ip6_addr only needs the address part (≤39 chars) plus an
// optional %zone. A zone longer than UV_IF_NAMESIZE is unknown
// (scope_id 0), same as omitting it. Keep that behavior when the
// full string does not fit so Fast API and CheckString agree.
constexpr size_t kMax = INET6_ADDRSTRLEN + UV_IF_NAMESIZE;
char buf[kMax];
if (address.length < kMax) {
memcpy(buf, address.data, address.length);
buf[address.length] = '\0';
} else if (family == AF_INET6) {
// uv_ip4_addr rejects %zone. Only IPv6 may drop an overlong zone
// (unknown zone → scope_id 0, same as uv_ip6_addr).
// uv_ip6_addr copies the address part into a 40-byte buffer
// (39 chars + NUL). A 40+ char prefix (mixed notation) is
// truncated and fails inet_pton — do not keep the full prefix.
const char* percent =
static_cast<const char*>(memchr(address.data, '%', address.length));
if (percent == nullptr) return false;
const size_t addr_len = static_cast<size_t>(percent - address.data);
if (addr_len >= 40) return false;
memcpy(buf, address.data, addr_len);
buf[addr_len] = '\0';
} else {
return false;
}
USE(options);
TRACK_V8_FAST_API_CALL("blocklist.checkString");
SocketAddressBlockListWrap* wrap =
FromJSObject<SocketAddressBlockListWrap>(receiver);
SocketAddress addr;
if (!SocketAddress::New(family, buf, 0, &addr)) return false;
return wrap->blocklist_->Apply(addr);
}
CFunction SocketAddressBlockListWrap::fast_check_string_(
CFunction::Make(&SocketAddressBlockListWrap::FastCheckString));
void SocketAddressBlockListWrap::GetRules(
const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
@@ -1087,7 +1139,11 @@ Local<FunctionTemplate> SocketAddressBlockListWrap::GetConstructorTemplate(
SetProtoMethod(isolate, tmpl, "removeSubnet", RemoveSubnet);
SetFastMethod(
isolate, tmpl->PrototypeTemplate(), "check", Check, &fast_check_);
SetProtoMethod(isolate, tmpl, "checkString", CheckString);
SetFastMethod(isolate,
tmpl->PrototypeTemplate(),
"checkString",
CheckString,
&fast_check_string_);
SetProtoMethod(isolate, tmpl, "getRules", GetRules);
SetProtoMethodNoSideEffect(isolate, tmpl, "getSize", GetSize);
SetProtoMethod(isolate, tmpl, "clear", Clear);
+8
View File
@@ -9,6 +9,7 @@
#include "node.h"
#include "node_worker.h"
#include "uv.h"
#include "v8-fast-api-calls.h"
#include "v8.h"
#include <compare>
@@ -418,6 +419,12 @@ class SocketAddressBlockListWrap : public BaseObject {
static bool FastCheck(v8::Local<v8::Object> receiver,
v8::Local<v8::Object> addr_obj);
static void CheckString(const v8::FunctionCallbackInfo<v8::Value>& args);
static bool FastCheckString(
v8::Local<v8::Object> receiver,
const v8::FastOneByteString& address,
int32_t family,
// NOLINTNEXTLINE(runtime/references) This is V8 api.
v8::FastApiCallbackOptions& options);
static void GetRules(const v8::FunctionCallbackInfo<v8::Value>& args);
static void GetSize(const v8::FunctionCallbackInfo<v8::Value>& args);
static void Clear(const v8::FunctionCallbackInfo<v8::Value>& args);
@@ -465,6 +472,7 @@ class SocketAddressBlockListWrap : public BaseObject {
private:
std::shared_ptr<SocketAddressBlockList> blocklist_;
static v8::CFunction fast_check_;
static v8::CFunction fast_check_string_;
};
} // namespace node
+15 -4
View File
@@ -6,15 +6,14 @@ const assert = require('assert');
const { BlockList } = require('net');
const { internalBinding } = require('internal/test/binding');
// The fast API is on the native check() method which takes a
// SocketAddressBase object. The JS BlockList.prototype.check() routes
// string arguments to checkString() which has no fast API, so we need
// to use SocketAddress objects to exercise the fast API path.
// The native check() method takes a SocketAddressBase object.
// String arguments go through checkString(), which also has a fast API.
const { kHandle: kBlockListHandle } = require('internal/blocklist');
const {
SocketAddress,
kHandle: kSocketAddressHandle,
} = require('internal/socketaddress');
const { AF_INET } = internalBinding('block_list');
const blockList = new BlockList();
blockList.addAddress('1.1.1.1');
@@ -31,12 +30,24 @@ function testFastCheck() {
assert.strictEqual(handle.check(addr3), true);
}
function checkString(address) {
return handle.checkString(address, AF_INET);
}
eval('%PrepareFunctionForOptimization(testFastCheck)');
testFastCheck();
eval('%OptimizeFunctionOnNextCall(testFastCheck)');
testFastCheck();
eval('%PrepareFunctionForOptimization(checkString)');
assert.strictEqual(checkString('1.1.1.1'), true);
eval('%OptimizeFunctionOnNextCall(checkString)');
assert.strictEqual(checkString('1.1.1.1'), true);
assert.strictEqual(checkString('2.2.2.2'), false);
assert.strictEqual(checkString('10.0.0.5'), true);
if (common.isDebug) {
const { getV8FastApiCallCount } = internalBinding('debug');
assert.strictEqual(getV8FastApiCallCount('blocklist.check'), 3);
assert.strictEqual(getV8FastApiCallCount('blocklist.checkString'), 3);
}
+23
View File
@@ -868,6 +868,29 @@ const util = require('util');
assert.strictEqual(blockList.check('not_valid_ipv6', 'ipv6'), false);
}
// uv_ip6_addr ignores an unknown/overlong zone (scope_id 0). A Fast API
// stack limit must not turn that into a miss after JIT.
{
const blockList = new BlockList();
blockList.addAddress('fe80::1', 'ipv6');
const longZone = `fe80::1%${'z'.repeat(200)}`;
assert.strictEqual(blockList.check('fe80::1', 'ipv6'), true);
assert.strictEqual(blockList.check(longZone, 'ipv6'), true);
assert.strictEqual(blockList.check('x'.repeat(200), 'ipv6'), false);
// uv_ip4_addr rejects zone suffixes. The Fast API overflow path
// must not strip %zone and treat this as 1.1.1.1.
blockList.addAddress('1.1.1.1');
assert.strictEqual(blockList.check(`1.1.1.1%${'z'.repeat(200)}`), false);
// Mixed-notation IPv6 is 45 chars. uv_ip6_addr truncates the
// address part to 39 when a zone is present, so this is a miss.
const mixed = 'ffff:ffff:ffff:ffff:ffff:ffff:255.255.255.255';
blockList.addAddress(mixed, 'ipv6');
assert.strictEqual(blockList.check(mixed, 'ipv6'), true);
assert.strictEqual(blockList.check(`${mixed}%${'z'.repeat(200)}`, 'ipv6'), false);
}
// check() family parameter is case-insensitive.
{
const blockList = new BlockList();