mirror of
https://github.com/nodejs/node.git
synced 2026-10-28 10:15:07 +08:00
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:
1 parent
b48f24afbd
commit
bc6e1ad8f9
5 files changed
+114
-7
No files matched your search
@@ -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
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
Reference in new issue
Block a user