From 59b3252dff834bef42fb78300978cee17cbbec45 Mon Sep 17 00:00:00 2001 From: James M Snell Date: Sun, 2 Aug 2026 20:55:44 -0700 Subject: [PATCH 01/18] net: fix duplicate address insertion in SocketAddressBlockList Signed-off-by: James M Snell --- src/node_sockaddr.cc | 8 ++++++++ test/parallel/test-blocklist.js | 14 ++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/src/node_sockaddr.cc b/src/node_sockaddr.cc index 9348f0ac8e4d..f5b54c0da68e 100644 --- a/src/node_sockaddr.cc +++ b/src/node_sockaddr.cc @@ -402,6 +402,14 @@ SocketAddressBlockList::SocketAddressBlockList( void SocketAddressBlockList::AddSocketAddress( const std::shared_ptr& address) { Mutex::ScopedLock lock(mutex_); + // Remove any existing rule for this address to avoid orphaning + // it in the rules_ list when the address_rules_ iterator is + // overwritten. + auto existing = address_rules_.find(*address.get()); + if (existing != address_rules_.end()) { + rules_.erase(existing->second); + address_rules_.erase(existing); + } std::unique_ptr rule = std::make_unique(address); rules_.emplace_front(std::move(rule)); address_rules_[*address.get()] = rules_.begin(); diff --git a/test/parallel/test-blocklist.js b/test/parallel/test-blocklist.js index 6895efcc1c00..8438f233425d 100644 --- a/test/parallel/test-blocklist.js +++ b/test/parallel/test-blocklist.js @@ -288,6 +288,20 @@ const util = require('util'); assert(!BlockList.isBlockList({})); } +{ + // Test that adding the same address twice does not create duplicate rules. + // Previously, the second add would orphan the first rule in the internal + // list while overwriting its index entry, making it unreachable for removal + // but still evaluated during checks. + const blockList = new BlockList(); + blockList.addAddress('1.1.1.1'); + blockList.addAddress('1.1.1.1'); + + // Should have exactly one rule, not two. + assert.strictEqual(blockList.rules.length, 1); + assert(blockList.check('1.1.1.1')); +} + // Test exporting and importing the rule list to/from JSON { const ruleList = [ From 7288bdd9b9ac60c4c365c271e6d6a64966201def Mon Sep 17 00:00:00 2001 From: James M Snell Date: Sun, 2 Aug 2026 21:00:41 -0700 Subject: [PATCH 02/18] net: fix BlockList rule listing order to match apply Signed-off-by: James M Snell --- src/node_sockaddr.cc | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/node_sockaddr.cc b/src/node_sockaddr.cc index f5b54c0da68e..837709e41d53 100644 --- a/src/node_sockaddr.cc +++ b/src/node_sockaddr.cc @@ -514,13 +514,14 @@ MaybeLocal SocketAddressBlockList::ListRules(Environment* env) { bool SocketAddressBlockList::ListRules(Environment* env, LocalVector* rules) { - if (parent_ && !parent_->ListRules(env, rules)) return false; + // List local rules first, then parent rules, matching the + // evaluation order in Apply(). for (const auto& rule : rules_) { Local str; if (!rule->ToV8String(env).ToLocal(&str)) return false; rules->push_back(str); } - return true; + return !parent_ || parent_->ListRules(env, rules); } void SocketAddressBlockList::MemoryInfo(node::MemoryTracker* tracker) const { From 095cd40b89712e575176fa4ae8ac2cdacd9c06c0 Mon Sep 17 00:00:00 2001 From: James M Snell Date: Sun, 2 Aug 2026 21:02:55 -0700 Subject: [PATCH 03/18] net: add minor bound check in BlockList Signed-off-by: James M Snell --- src/node_sockaddr.cc | 1 + 1 file changed, 1 insertion(+) diff --git a/src/node_sockaddr.cc b/src/node_sockaddr.cc index 837709e41d53..711af3058567 100644 --- a/src/node_sockaddr.cc +++ b/src/node_sockaddr.cc @@ -428,6 +428,7 @@ void SocketAddressBlockList::RemoveSocketAddress( void SocketAddressBlockList::AddSocketAddressRange( const std::shared_ptr& start, const std::shared_ptr& end) { + DCHECK(!(*start > *end)); Mutex::ScopedLock lock(mutex_); std::unique_ptr rule = std::make_unique(start, end); From c1034b94c577eaa16e75f3368d3bc79626e4e636 Mon Sep 17 00:00:00 2001 From: James M Snell Date: Sun, 2 Aug 2026 21:06:37 -0700 Subject: [PATCH 04/18] net: improve performance of BlockList apply Take O(1) fast-path when possible Signed-off-by: James M Snell --- src/node_sockaddr.cc | 5 +++++ src/node_sockaddr.h | 5 ++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/node_sockaddr.cc b/src/node_sockaddr.cc index 711af3058567..6d2d96d4ed5a 100644 --- a/src/node_sockaddr.cc +++ b/src/node_sockaddr.cc @@ -445,6 +445,11 @@ void SocketAddressBlockList::AddSocketAddressMask( bool SocketAddressBlockList::Apply(const SocketAddress& address) { Mutex::ScopedLock lock(mutex_); + // Fast-path: O(1) lookup for exact same-family address matches. + // The address_rules_ map uses IpHash/IpEqual (port-insensitive, + // family-sensitive). Cross-family matches (e.g. ::ffff:1.1.1.1 + // against a 1.1.1.1 rule) fall through to the linear scan below. + if (address_rules_.count(address)) return true; for (const auto& rule : rules_) { if (rule->Apply(address)) return true; } diff --git a/src/node_sockaddr.h b/src/node_sockaddr.h index 05bb127b012f..c764d644cabf 100644 --- a/src/node_sockaddr.h +++ b/src/node_sockaddr.h @@ -322,7 +322,10 @@ class SocketAddressBlockList : public MemoryRetainer { std::shared_ptr parent_; std::list> rules_; - SocketAddress::Map>::iterator> address_rules_; + // Keyed by IP only (port-insensitive) so that Apply() can perform + // O(1) lookups regardless of the port on the checked address. + SocketAddress::IpMap>::iterator> + address_rules_; Mutex mutex_; }; From 9d645eeba683d285a201833a33429af05c1ed53f Mon Sep 17 00:00:00 2001 From: James M Snell Date: Sun, 2 Aug 2026 21:10:50 -0700 Subject: [PATCH 05/18] net: simplify BlockList internals by eliminating shared_ptr Signed-off-by: James M Snell --- src/node_sockaddr.cc | 57 ++++++++++++++++++------------------ src/node_sockaddr.h | 26 ++++++++-------- test/cctest/test_sockaddr.cc | 6 ++-- 3 files changed, 44 insertions(+), 45 deletions(-) diff --git a/src/node_sockaddr.cc b/src/node_sockaddr.cc index 6d2d96d4ed5a..205b9730ef50 100644 --- a/src/node_sockaddr.cc +++ b/src/node_sockaddr.cc @@ -399,26 +399,25 @@ SocketAddressBlockList::SocketAddressBlockList( std::shared_ptr parent) : parent_(parent) {} -void SocketAddressBlockList::AddSocketAddress( - const std::shared_ptr& address) { +void SocketAddressBlockList::AddSocketAddress(const SocketAddress& address) { Mutex::ScopedLock lock(mutex_); // Remove any existing rule for this address to avoid orphaning // it in the rules_ list when the address_rules_ iterator is // overwritten. - auto existing = address_rules_.find(*address.get()); + auto existing = address_rules_.find(address); if (existing != address_rules_.end()) { rules_.erase(existing->second); address_rules_.erase(existing); } std::unique_ptr rule = std::make_unique(address); rules_.emplace_front(std::move(rule)); - address_rules_[*address.get()] = rules_.begin(); + address_rules_[address] = rules_.begin(); } void SocketAddressBlockList::RemoveSocketAddress( - const std::shared_ptr& address) { + const SocketAddress& address) { Mutex::ScopedLock lock(mutex_); - auto it = address_rules_.find(*address.get()); + auto it = address_rules_.find(address); if (it != std::end(address_rules_)) { rules_.erase(it->second); address_rules_.erase(it); @@ -426,9 +425,9 @@ void SocketAddressBlockList::RemoveSocketAddress( } void SocketAddressBlockList::AddSocketAddressRange( - const std::shared_ptr& start, - const std::shared_ptr& end) { - DCHECK(!(*start > *end)); + const SocketAddress& start, + const SocketAddress& end) { + DCHECK(!(start > end)); Mutex::ScopedLock lock(mutex_); std::unique_ptr rule = std::make_unique(start, end); @@ -436,7 +435,7 @@ void SocketAddressBlockList::AddSocketAddressRange( } void SocketAddressBlockList::AddSocketAddressMask( - const std::shared_ptr& network, int prefix) { + const SocketAddress& network, int prefix) { Mutex::ScopedLock lock(mutex_); std::unique_ptr rule = std::make_unique(network, prefix); @@ -457,56 +456,56 @@ bool SocketAddressBlockList::Apply(const SocketAddress& address) { } SocketAddressBlockList::SocketAddressRule::SocketAddressRule( - const std::shared_ptr& address_) + const SocketAddress& address_) : address(address_) {} SocketAddressBlockList::SocketAddressRangeRule::SocketAddressRangeRule( - const std::shared_ptr& start_, - const std::shared_ptr& end_) + const SocketAddress& start_, + const SocketAddress& end_) : start(start_), end(end_) {} SocketAddressBlockList::SocketAddressMaskRule::SocketAddressMaskRule( - const std::shared_ptr& network_, int prefix_) + const SocketAddress& network_, int prefix_) : network(network_), prefix(prefix_) {} bool SocketAddressBlockList::SocketAddressRule::Apply( const SocketAddress& address) { - return this->address->is_match(address); + return this->address.is_match(address); } std::string SocketAddressBlockList::SocketAddressRule::ToString() { std::string ret = "Address: "; - ret += address->family() == AF_INET ? "IPv4" : "IPv6"; + ret += address.family() == AF_INET ? "IPv4" : "IPv6"; ret += " "; - ret += address->address(); + ret += address.address(); return ret; } bool SocketAddressBlockList::SocketAddressRangeRule::Apply( const SocketAddress& address) { - return address >= *start.get() && address <= *end.get(); + return address >= start && address <= end; } std::string SocketAddressBlockList::SocketAddressRangeRule::ToString() { std::string ret = "Range: "; - ret += start->family() == AF_INET ? "IPv4" : "IPv6"; + ret += start.family() == AF_INET ? "IPv4" : "IPv6"; ret += " "; - ret += start->address(); + ret += start.address(); ret += "-"; - ret += end->address(); + ret += end.address(); return ret; } bool SocketAddressBlockList::SocketAddressMaskRule::Apply( const SocketAddress& address) { - return address.is_in_network(*network.get(), prefix); + return address.is_in_network(network, prefix); } std::string SocketAddressBlockList::SocketAddressMaskRule::ToString() { std::string ret = "Subnet: "; - ret += network->family() == AF_INET ? "IPv4" : "IPv6"; + ret += network.family() == AF_INET ? "IPv4" : "IPv6"; ret += " "; - ret += network->address(); + ret += network.address(); ret += "/" + std::to_string(prefix); return ret; } @@ -605,7 +604,7 @@ void SocketAddressBlockListWrap::AddAddress( SocketAddressBase* addr; ASSIGN_OR_RETURN_UNWRAP(&addr, args[0]); - wrap->blocklist_->AddSocketAddress(addr->address()); + wrap->blocklist_->AddSocketAddress(*addr->address()); args.GetReturnValue().Set(true); } @@ -625,11 +624,11 @@ void SocketAddressBlockListWrap::AddRange( ASSIGN_OR_RETURN_UNWRAP(&end_addr, args[1]); // Starting address must come before the end address - if (*start_addr->address().get() > *end_addr->address().get()) + if (*start_addr->address() > *end_addr->address()) return args.GetReturnValue().Set(false); - wrap->blocklist_->AddSocketAddressRange(start_addr->address(), - end_addr->address()); + wrap->blocklist_->AddSocketAddressRange(*start_addr->address(), + *end_addr->address()); args.GetReturnValue().Set(true); } @@ -655,7 +654,7 @@ void SocketAddressBlockListWrap::AddSubnet( CHECK_IMPLIES(addr->address()->family() == AF_INET6, prefix <= 128); CHECK_GE(prefix, 0); - wrap->blocklist_->AddSocketAddressMask(addr->address(), prefix); + wrap->blocklist_->AddSocketAddressMask(*addr->address(), prefix); args.GetReturnValue().Set(true); } diff --git a/src/node_sockaddr.h b/src/node_sockaddr.h index c764d644cabf..0974265946e6 100644 --- a/src/node_sockaddr.h +++ b/src/node_sockaddr.h @@ -248,14 +248,14 @@ class SocketAddressBlockList : public MemoryRetainer { std::shared_ptr parent = {}); ~SocketAddressBlockList() = default; - void AddSocketAddress(const std::shared_ptr& address); + void AddSocketAddress(const SocketAddress& address); - void RemoveSocketAddress(const std::shared_ptr& address); + void RemoveSocketAddress(const SocketAddress& address); - void AddSocketAddressRange(const std::shared_ptr& start, - const std::shared_ptr& end); + void AddSocketAddressRange(const SocketAddress& start, + const SocketAddress& end); - void AddSocketAddressMask(const std::shared_ptr& address, + void AddSocketAddressMask(const SocketAddress& address, int prefix); bool Apply(const SocketAddress& address); @@ -271,9 +271,9 @@ class SocketAddressBlockList : public MemoryRetainer { }; struct SocketAddressRule final : Rule { - std::shared_ptr address; + SocketAddress address; - explicit SocketAddressRule(const std::shared_ptr& address); + explicit SocketAddressRule(const SocketAddress& address); bool Apply(const SocketAddress& address) override; std::string ToString() override; @@ -284,11 +284,11 @@ class SocketAddressBlockList : public MemoryRetainer { }; struct SocketAddressRangeRule final : Rule { - std::shared_ptr start; - std::shared_ptr end; + SocketAddress start; + SocketAddress end; - SocketAddressRangeRule(const std::shared_ptr& start, - const std::shared_ptr& end); + SocketAddressRangeRule(const SocketAddress& start, + const SocketAddress& end); bool Apply(const SocketAddress& address) override; std::string ToString() override; @@ -299,10 +299,10 @@ class SocketAddressBlockList : public MemoryRetainer { }; struct SocketAddressMaskRule final : Rule { - std::shared_ptr network; + SocketAddress network; int prefix; - SocketAddressMaskRule(const std::shared_ptr& address, + SocketAddressMaskRule(const SocketAddress& address, int prefix); bool Apply(const SocketAddress& address) override; diff --git a/test/cctest/test_sockaddr.cc b/test/cctest/test_sockaddr.cc index a4feefd6f4b3..62c6710dab5a 100644 --- a/test/cctest/test_sockaddr.cc +++ b/test/cctest/test_sockaddr.cc @@ -283,13 +283,13 @@ TEST(SocketAddressBlockList, Simple) { std::shared_ptr addr2 = std::make_shared( reinterpret_cast(&storage[1])); - bl.AddSocketAddress(addr1); - bl.AddSocketAddress(addr2); + bl.AddSocketAddress(*addr1); + bl.AddSocketAddress(*addr2); CHECK(bl.Apply(*addr1)); CHECK(bl.Apply(*addr2)); - bl.RemoveSocketAddress(addr1); + bl.RemoveSocketAddress(*addr1); CHECK(!bl.Apply(*addr1)); CHECK(bl.Apply(*addr2)); From e3fdfab3c0f612609e8a10f46131adc794917106 Mon Sep 17 00:00:00 2001 From: James M Snell Date: Sun, 2 Aug 2026 21:24:59 -0700 Subject: [PATCH 06/18] net: add BlockList check fast api path Signed-off-by: James M Snell --- src/node_sockaddr.cc | 17 ++++++++++++++- src/node_sockaddr.h | 3 +++ test/parallel/test-blocklist-fast-api.js | 27 ++++++++++++++++++++++++ 3 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 test/parallel/test-blocklist-fast-api.js diff --git a/src/node_sockaddr.cc b/src/node_sockaddr.cc index 205b9730ef50..bfe8ceffeb52 100644 --- a/src/node_sockaddr.cc +++ b/src/node_sockaddr.cc @@ -3,6 +3,7 @@ #include "env-inl.h" #include "memory_tracker-inl.h" #include "nbytes.h" +#include "node_debug.h" #include "node_errors.h" #include "node_hash.h" #include "node_sockaddr-inl.h" // NOLINT(build/include_inline) @@ -15,6 +16,7 @@ namespace node { using v8::Array; +using v8::CFunction; using v8::Context; using v8::FunctionCallbackInfo; using v8::FunctionTemplate; @@ -672,6 +674,18 @@ void SocketAddressBlockListWrap::Check( args.GetReturnValue().Set(wrap->blocklist_->Apply(*addr->address())); } +bool SocketAddressBlockListWrap::FastCheck(Local receiver, + Local addr_obj) { + TRACK_V8_FAST_API_CALL("blocklist.check"); + SocketAddressBlockListWrap* wrap = + FromJSObject(receiver); + SocketAddressBase* addr = FromJSObject(addr_obj); + return wrap->blocklist_->Apply(*addr->address()); +} + +CFunction SocketAddressBlockListWrap::fast_check_( + CFunction::Make(&SocketAddressBlockListWrap::FastCheck)); + void SocketAddressBlockListWrap::GetRules( const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); @@ -707,7 +721,8 @@ Local SocketAddressBlockListWrap::GetConstructorTemplate( SetProtoMethod(isolate, tmpl, "addAddress", AddAddress); SetProtoMethod(isolate, tmpl, "addRange", AddRange); SetProtoMethod(isolate, tmpl, "addSubnet", AddSubnet); - SetProtoMethod(isolate, tmpl, "check", Check); + SetFastMethod( + isolate, tmpl->PrototypeTemplate(), "check", Check, &fast_check_); SetProtoMethod(isolate, tmpl, "getRules", GetRules); env->set_blocklist_constructor_template(tmpl); } diff --git a/src/node_sockaddr.h b/src/node_sockaddr.h index 0974265946e6..f8aad7c61a5e 100644 --- a/src/node_sockaddr.h +++ b/src/node_sockaddr.h @@ -349,6 +349,8 @@ class SocketAddressBlockListWrap : public BaseObject { static void AddRange(const v8::FunctionCallbackInfo& args); static void AddSubnet(const v8::FunctionCallbackInfo& args); static void Check(const v8::FunctionCallbackInfo& args); + static bool FastCheck(v8::Local receiver, + v8::Local addr_obj); static void GetRules(const v8::FunctionCallbackInfo& args); SocketAddressBlockListWrap(Environment* env, @@ -393,6 +395,7 @@ class SocketAddressBlockListWrap : public BaseObject { private: std::shared_ptr blocklist_; + static v8::CFunction fast_check_; }; } // namespace node diff --git a/test/parallel/test-blocklist-fast-api.js b/test/parallel/test-blocklist-fast-api.js new file mode 100644 index 000000000000..f8bbec5e88bd --- /dev/null +++ b/test/parallel/test-blocklist-fast-api.js @@ -0,0 +1,27 @@ +// Flags: --allow-natives-syntax --expose-internals --no-warnings +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const { BlockList } = require('net'); +const { internalBinding } = require('internal/test/binding'); + +const blockList = new BlockList(); +blockList.addAddress('1.1.1.1'); +blockList.addSubnet('10.0.0.0', 24); + +function testFastCheck() { + assert(blockList.check('1.1.1.1')); + assert(!blockList.check('2.2.2.2')); + assert(blockList.check('10.0.0.5')); +} + +eval('%PrepareFunctionForOptimization(testFastCheck)'); +testFastCheck(); +eval('%OptimizeFunctionOnNextCall(testFastCheck)'); +testFastCheck(); + +if (common.isDebug) { + const { getV8FastApiCallCount } = internalBinding('debug'); + assert.strictEqual(getV8FastApiCallCount('blocklist.check'), 3); +} From 94e140647889f9fb5997095d3e1358b93385f537 Mon Sep 17 00:00:00 2001 From: James M Snell Date: Sun, 2 Aug 2026 21:29:56 -0700 Subject: [PATCH 07/18] net: add clear method to BlockList Signed-off-by: James M Snell --- doc/api/net.md | 8 ++++++++ lib/internal/blocklist.js | 7 +++++++ src/node_sockaddr.cc | 14 ++++++++++++++ src/node_sockaddr.h | 3 +++ test/parallel/test-blocklist.js | 26 ++++++++++++++++++++++++++ 5 files changed, 58 insertions(+) diff --git a/doc/api/net.md b/doc/api/net.md index a26abe16c5be..d9fa3dd71a0a 100644 --- a/doc/api/net.md +++ b/doc/api/net.md @@ -158,6 +158,14 @@ console.log(blockList.check('::ffff:7b7b:7b7b', 'ipv6')); // Prints: true console.log(blockList.check('::ffff:123.123.123.123', 'ipv6')); // Prints: true ``` +### `blockList.clear()` + + + +Clears all rules from the `BlockList`. + ### `blockList.rules` + +* `addresses` {string\[]|net.SocketAddress\[]} An array of IPv4 or IPv6 + addresses. +* `type` {string} Either `'ipv4'` or `'ipv6'`. **Default:** `'ipv4'`. + +Adds multiple address rules to the block list in a single operation. +This is more efficient than calling `blockList.addAddress()` repeatedly +when adding a large number of individual addresses, as the addresses +are inserted under a single internal lock acquisition. + ### `blockList.addRange(start, end[, type])` - -* Type: {string\[]} - -The list of rules added to the blocklist. - -### `BlockList.isBlockList(value)` - - - -* `value` {any} Any JS value -* Returns `true` if the `value` is a `net.BlockList`. - ### `blockList.fromJSON(value)` > Stability: 1.2 - Release candidate @@ -228,6 +205,60 @@ blockList.fromJSON(JSON.stringify(data)); * `value` Blocklist.rules +### `BlockList.isBlockList(value)` + + + +* `value` {any} Any JS value +* Returns `true` if the `value` is a `net.BlockList`. + +### `blockList.removeRange(start, end[, type])` + + + +* `start` {string|net.SocketAddress} The starting IPv4 or IPv6 address in the + range. +* `end` {string|net.SocketAddress} The ending IPv4 or IPv6 address in the range. +* `type` {string} Either `'ipv4'` or `'ipv6'`. **Default:** `'ipv4'`. + +Removes a rule that was previously added with `blockList.addRange()`. The `start` +and `end` addresses must match exactly the values used when the rule was added. +If the specified range does not exist, this is a no-op. + +### `blockList.removeSubnet(net, prefix[, type])` + + + +* `net` {string|net.SocketAddress} The network IPv4 or IPv6 address. +* `prefix` {number} The number of CIDR prefix bits. For IPv4, this + must be a value between `0` and `32`. For IPv6, this must be between + `0` and `128`. +* `type` {string} Either `'ipv4'` or `'ipv6'`. **Default:** `'ipv4'`. + +Removes a rule that was previously added with `blockList.addSubnet()`. The +network address and prefix must match exactly the values used when the rule was +added. If the specified subnet does not exist, this is a no-op. + +### `blockList.rules` + + + +* Type: {string\[]} + +The list of rules added to the blocklist. + ### `blockList.toJSON()` > Stability: 1.2 - Release candidate diff --git a/lib/internal/blocklist.js b/lib/internal/blocklist.js index 12011d2be1fd..e8cb52f48501 100644 --- a/lib/internal/blocklist.js +++ b/lib/internal/blocklist.js @@ -147,6 +147,49 @@ class BlockList { this[kHandle].addSubnet(network[kSocketAddressHandle], prefix); } + 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]); + } + + 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); + } + check(address, family = 'ipv4') { if (!SocketAddress.isSocketAddress(address)) { validateString(address, 'address'); diff --git a/src/node_sockaddr.cc b/src/node_sockaddr.cc index 57d92314db02..65baf6b363ac 100644 --- a/src/node_sockaddr.cc +++ b/src/node_sockaddr.cc @@ -465,6 +465,60 @@ bool SocketAddressBlockList::SubnetTrie::Lookup(const uint8_t* address_bytes, return false; } +bool SocketAddressBlockList::SubnetTrie::Remove(const uint8_t* address_bytes, + int prefix_length) { + if (root_ == nullptr) return false; + + // Walk the trie to find the node at prefix_length depth. + // Keep a stack of parent pointers so we can prune empty branches. + Node* node = root_.get(); + struct Ancestor { + Node* parent; + int bit; + }; + // Max depth is 128 bits for IPv6. + Ancestor ancestors[128]; + int depth = 0; + + for (int i = 0; i < prefix_length; i++) { + if (node->terminal) { + // A broader prefix exists — the specific prefix we're trying + // to remove is subsumed and doesn't exist as a separate entry. + return false; + } + int bit = GetBit(address_bytes, i); + if (node->children[bit] == nullptr) return false; + ancestors[depth++] = {node, bit}; + node = node->children[bit].get(); + } + + if (!node->terminal) return false; + + node->terminal = false; + count_--; + + // Prune empty leaf nodes up the tree. + for (int i = depth - 1; i >= 0; i--) { + Node* child = ancestors[i].parent->children[ancestors[i].bit].get(); + if (!child->terminal && + child->children[0] == nullptr && + child->children[1] == nullptr) { + ancestors[i].parent->children[ancestors[i].bit].reset(); + } else { + break; + } + } + + // If root is now empty and non-terminal, reset it. + if (!root_->terminal && + root_->children[0] == nullptr && + root_->children[1] == nullptr) { + root_.reset(); + } + + return true; +} + void SocketAddressBlockList::SubnetTrie::Clear() { root_.reset(); count_ = 0; @@ -616,6 +670,51 @@ void SocketAddressBlockList::AddSocketAddressMask( std::make_unique(network, prefix)); } +void SocketAddressBlockList::RemoveSocketAddressRange( + const SocketAddress& start, + const SocketAddress& end) { + RwLock::ScopedLock lock(mutex_); + // rules_ contains only SocketAddressRangeRule instances (subnet rules + // are stored separately in subnet_rules_). + for (auto it = rules_.begin(); it != rules_.end(); ++it) { + auto* range = static_cast(it->get()); + if (range->start == start && range->end == end) { + rules_.erase(it); + return; + } + } +} + +void SocketAddressBlockList::RemoveSocketAddressMask( + const SocketAddress& network, + int prefix) { + RwLock::ScopedLock lock(mutex_); + int bits; + const uint8_t* bytes = GetAddressBytes(network, &bits); + + if (network.family() == AF_INET) { + ipv4_subnets_.Remove(bytes, prefix); + uint8_t mapped[16] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff}; + memcpy(mapped + 12, bytes, 4); + ipv6_subnets_.Remove(mapped, prefix + 96); + } else { + ipv6_subnets_.Remove(bytes, prefix); + constexpr uint8_t v4mapped[] = { + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff}; + if (prefix >= 96 && memcmp(bytes, v4mapped, 12) == 0) { + ipv4_subnets_.Remove(bytes + 12, prefix - 96); + } + } + + // Remove from subnet_rules_ metadata list. + for (auto it = subnet_rules_.begin(); it != subnet_rules_.end(); ++it) { + if ((*it)->network == network && (*it)->prefix == prefix) { + subnet_rules_.erase(it); + return; + } + } +} + bool SocketAddressBlockList::Apply(const SocketAddress& address) { RwLock::ScopedReadLock lock(mutex_); // O(1) lookup for exact address matches. The address_rules_ map @@ -889,6 +988,44 @@ void SocketAddressBlockListWrap::AddSubnet( args.GetReturnValue().Set(true); } +void SocketAddressBlockListWrap::RemoveRange( + const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + SocketAddressBlockListWrap* wrap; + ASSIGN_OR_RETURN_UNWRAP(&wrap, args.This()); + + CHECK(SocketAddressBase::HasInstance(env, args[0])); + CHECK(SocketAddressBase::HasInstance(env, args[1])); + + SocketAddressBase* start_addr; + SocketAddressBase* end_addr; + ASSIGN_OR_RETURN_UNWRAP(&start_addr, args[0]); + ASSIGN_OR_RETURN_UNWRAP(&end_addr, args[1]); + + wrap->blocklist_->RemoveSocketAddressRange(*start_addr->address(), + *end_addr->address()); +} + +void SocketAddressBlockListWrap::RemoveSubnet( + const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + SocketAddressBlockListWrap* wrap; + ASSIGN_OR_RETURN_UNWRAP(&wrap, args.This()); + + CHECK(SocketAddressBase::HasInstance(env, args[0])); + CHECK(args[1]->IsInt32()); + + SocketAddressBase* addr; + ASSIGN_OR_RETURN_UNWRAP(&addr, args[0]); + + int32_t prefix; + if (!args[1]->Int32Value(env->context()).To(&prefix)) { + return; + } + + wrap->blocklist_->RemoveSocketAddressMask(*addr->address(), prefix); +} + void SocketAddressBlockListWrap::Check( const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); @@ -978,6 +1115,8 @@ Local SocketAddressBlockListWrap::GetConstructorTemplate( SetProtoMethod(isolate, tmpl, "addAddresses", AddAddresses); SetProtoMethod(isolate, tmpl, "addRange", AddRange); SetProtoMethod(isolate, tmpl, "addSubnet", AddSubnet); + SetProtoMethod(isolate, tmpl, "removeRange", RemoveRange); + SetProtoMethod(isolate, tmpl, "removeSubnet", RemoveSubnet); SetFastMethod( isolate, tmpl->PrototypeTemplate(), "check", Check, &fast_check_); SetProtoMethod(isolate, tmpl, "checkString", CheckString); diff --git a/src/node_sockaddr.h b/src/node_sockaddr.h index 935437dd38d5..fe1590d84256 100644 --- a/src/node_sockaddr.h +++ b/src/node_sockaddr.h @@ -257,9 +257,15 @@ class SocketAddressBlockList : public MemoryRetainer { void AddSocketAddressRange(const SocketAddress& start, const SocketAddress& end); + void RemoveSocketAddressRange(const SocketAddress& start, + const SocketAddress& end); + void AddSocketAddressMask(const SocketAddress& address, int prefix); + void RemoveSocketAddressMask(const SocketAddress& address, + int prefix); + bool Apply(const SocketAddress& address); void Clear(); @@ -330,6 +336,10 @@ class SocketAddressBlockList : public MemoryRetainer { // Returns true if the given address falls within any inserted subnet. bool Lookup(const uint8_t* address_bytes, int address_bits) const; + // Remove a previously inserted subnet. Returns true if it was found + // and removed. + bool Remove(const uint8_t* address_bytes, int prefix_length); + // Remove all entries. void Clear(); @@ -402,6 +412,8 @@ class SocketAddressBlockListWrap : public BaseObject { static void AddAddresses(const v8::FunctionCallbackInfo& args); static void AddRange(const v8::FunctionCallbackInfo& args); static void AddSubnet(const v8::FunctionCallbackInfo& args); + static void RemoveRange(const v8::FunctionCallbackInfo& args); + static void RemoveSubnet(const v8::FunctionCallbackInfo& args); static void Check(const v8::FunctionCallbackInfo& args); static bool FastCheck(v8::Local receiver, v8::Local addr_obj); diff --git a/test/parallel/test-blocklist.js b/test/parallel/test-blocklist.js index a8913246b512..6c7b820bfef3 100644 --- a/test/parallel/test-blocklist.js +++ b/test/parallel/test-blocklist.js @@ -430,3 +430,122 @@ const util = require('util'); assert.strictEqual(test5.check(i[0], i[1]), i[2]); }); } + +// removeRange: basic removal +{ + const blockList = new BlockList(); + blockList.addRange('10.0.0.1', '10.0.0.100'); + blockList.addRange('192.168.1.1', '192.168.1.50'); + assert(blockList.check('10.0.0.50')); + assert(blockList.check('192.168.1.25')); + + blockList.removeRange('10.0.0.1', '10.0.0.100'); + assert(!blockList.check('10.0.0.50')); + assert(blockList.check('192.168.1.25')); + assert.strictEqual(blockList.rules.length, 1); +} + +// removeRange: non-existent range is a no-op +{ + const blockList = new BlockList(); + blockList.addRange('10.0.0.1', '10.0.0.10'); + assert.strictEqual(blockList.rules.length, 1); + blockList.removeRange('99.99.99.1', '99.99.99.10'); + assert.strictEqual(blockList.rules.length, 1); + assert(blockList.check('10.0.0.5')); +} + +// removeRange: IPv6 range +{ + const blockList = new BlockList(); + blockList.addRange('2001:db8::1', '2001:db8::ff', 'ipv6'); + assert(blockList.check('2001:db8::50', 'ipv6')); + + blockList.removeRange('2001:db8::1', '2001:db8::ff', 'ipv6'); + assert(!blockList.check('2001:db8::50', 'ipv6')); + assert.strictEqual(blockList.rules.length, 0); +} + +// removeRange: with SocketAddress objects +{ + const blockList = new BlockList(); + const start = new SocketAddress({ address: '10.0.0.1' }); + const end = new SocketAddress({ address: '10.0.0.10' }); + blockList.addRange(start, end); + assert(blockList.check('10.0.0.5')); + + blockList.removeRange(start, end); + assert(!blockList.check('10.0.0.5')); +} + +// removeSubnet: basic IPv4 removal +{ + const blockList = new BlockList(); + blockList.addSubnet('10.0.0.0', 8); + blockList.addSubnet('192.168.0.0', 16); + assert(blockList.check('10.1.2.3')); + assert(blockList.check('192.168.5.5')); + + blockList.removeSubnet('10.0.0.0', 8); + assert(!blockList.check('10.1.2.3')); + assert(blockList.check('192.168.5.5')); + assert.strictEqual(blockList.rules.length, 1); +} + +// removeSubnet: IPv6 +{ + const blockList = new BlockList(); + blockList.addSubnet('2001:db8::', 32, 'ipv6'); + assert(blockList.check('2001:db8::1', 'ipv6')); + + blockList.removeSubnet('2001:db8::', 32, 'ipv6'); + assert(!blockList.check('2001:db8::1', 'ipv6')); + assert.strictEqual(blockList.rules.length, 0); +} + +// removeSubnet: cross-family cleanup +{ + const blockList = new BlockList(); + blockList.addSubnet('10.0.0.0', 8); + assert(blockList.check('::ffff:10.0.0.1', 'ipv6')); + + blockList.removeSubnet('10.0.0.0', 8); + assert(!blockList.check('10.0.0.1')); + assert(!blockList.check('::ffff:10.0.0.1', 'ipv6')); +} + +// removeSubnet: non-existent subnet is a no-op +{ + const blockList = new BlockList(); + blockList.addSubnet('10.0.0.0', 8); + blockList.removeSubnet('172.16.0.0', 12); + assert(blockList.check('10.1.2.3')); + assert.strictEqual(blockList.rules.length, 1); +} + +// removeSubnet: with SocketAddress objects +{ + const blockList = new BlockList(); + const net = new SocketAddress({ address: '10.0.0.0' }); + blockList.addSubnet(net, 8); + assert(blockList.check('10.1.2.3')); + + blockList.removeSubnet(net, 8); + assert(!blockList.check('10.1.2.3')); +} + +// removeRange/removeSubnet don't affect other rule types +{ + const blockList = new BlockList(); + blockList.addAddress('1.1.1.1'); + blockList.addRange('10.0.0.1', '10.0.0.100'); + blockList.addSubnet('192.168.0.0', 16); + + blockList.removeRange('10.0.0.1', '10.0.0.100'); + blockList.removeSubnet('192.168.0.0', 16); + + // Address rule should still work + assert(blockList.check('1.1.1.1')); + assert(!blockList.check('10.0.0.50')); + assert(!blockList.check('192.168.1.1')); +} From af3699a31b9098ea8cd7892dd2b4dd9e56271fc5 Mon Sep 17 00:00:00 2001 From: James M Snell Date: Sun, 2 Aug 2026 22:51:17 -0700 Subject: [PATCH 14/18] net: add cidr notation parsing to BlockList Signed-off-by: James M Snell --- doc/api/net.md | 28 +++++++++++ lib/internal/blocklist.js | 30 +++++++++++ test/parallel/test-blocklist.js | 89 +++++++++++++++++++++++++++++++++ 3 files changed, 147 insertions(+) diff --git a/doc/api/net.md b/doc/api/net.md index dd125665ef82..7a12aad82bc1 100644 --- a/doc/api/net.md +++ b/doc/api/net.md @@ -111,6 +111,20 @@ This is more efficient than calling `blockList.addAddress()` repeatedly when adding a large number of individual addresses, as the addresses are inserted under a single internal lock acquisition. +### `blockList.addCIDR(cidr)` + + + +* `cidr` {string} An IPv4 or IPv6 subnet in CIDR notation (e.g. + `'10.0.0.0/8'` or `'2001:db8::/32'`). + +Adds a subnet rule using CIDR notation. The address family is automatically +detected from the address (IPv6 if the address contains `':'`, IPv4 +otherwise). This is equivalent to calling `blockList.addSubnet()` with +the parsed network address, prefix length, and family. + ### `blockList.addRange(start, end[, type])` + +* `cidr` {string} An IPv4 or IPv6 subnet in CIDR notation (e.g. + `'10.0.0.0/8'` or `'2001:db8::/32'`). + +Removes a subnet rule using CIDR notation. The address family is automatically +detected from the address. This is equivalent to calling +`blockList.removeSubnet()` with the parsed network address, prefix length, +and family. If the specified subnet does not exist, this is a no-op. + ### `blockList.removeRange(start, end[, type])` + +* `cidrs` {string\[]} An array of IPv4 or IPv6 subnets in CIDR notation. + +Adds multiple subnet rules using CIDR notation in a single call. The address +family for each entry is automatically detected. This is equivalent to +calling `blockList.addCIDR()` for each element of the array. + ### `blockList.addRange(start, end[, type])` + +* `address` {string|net.SocketAddress} An IPv4 or IPv6 address. +* `type` {string} Either `'ipv4'` or `'ipv6'`. **Default:** `'ipv4'`. + +Removes a rule that was previously added with `blockList.addAddress()`. The +address must match exactly the value used when the rule was added. If the +specified address does not exist, this is a no-op. + ### `blockList.removeCIDR(cidr)` + +* Type: {number} + +The number of rules in the blocklist. This is equivalent to +`blockList.rules.length` but does not allocate the rules array. + ### `blockList.toJSON()` > Stability: 1.2 - Release candidate diff --git a/lib/internal/blocklist.js b/lib/internal/blocklist.js index ca4cbb1387a6..0eed5e9828ee 100644 --- a/lib/internal/blocklist.js +++ b/lib/internal/blocklist.js @@ -172,6 +172,28 @@ class BlockList { this.addSubnet(address, prefix, family); } + addCIDRs(cidrs) { + if (!ArrayIsArray(cidrs)) { + throw new ERR_INVALID_ARG_TYPE('cidrs', 'Array', cidrs); + } + for (let i = 0; i < cidrs.length; i++) { + validateString(cidrs[i], `cidrs[${i}]`); + this.addCIDR(cidrs[i]); + } + } + + removeAddress(address, family = 'ipv4') { + if (!SocketAddress.isSocketAddress(address)) { + validateString(address, 'address'); + validateString(family, 'family'); + address = new SocketAddress({ + address, + family, + }); + } + this[kHandle].removeAddress(address[kSocketAddressHandle]); + } + removeRange(start, end, family = 'ipv4') { if (!SocketAddress.isSocketAddress(start)) { validateString(start, 'start'); @@ -363,6 +385,10 @@ class BlockList { get rules() { return this[kHandle].getRules(); } + + get size() { + return this[kHandle].getSize(); + } [kClone]() { const handle = this[kHandle]; return { diff --git a/src/node_sockaddr.cc b/src/node_sockaddr.cc index 65baf6b363ac..04c9ca188558 100644 --- a/src/node_sockaddr.cc +++ b/src/node_sockaddr.cc @@ -560,6 +560,9 @@ void SocketAddressBlockList::SubnetTrie::WalkImpl(const Node* node, void SocketAddressBlockList::AddSocketAddressImpl( const SocketAddress& address) { + if (address_rules_.count(address) == 0) { + address_count_++; + } address_rules_[address] = address; // Insert the cross-family counterpart so that both IPv4 and // IPv4-mapped IPv6 lookups resolve in O(1). @@ -606,7 +609,9 @@ void SocketAddressBlockList::AddSocketAddresses( void SocketAddressBlockList::RemoveSocketAddress( const SocketAddress& address) { RwLock::ScopedLock lock(mutex_); - address_rules_.erase(address); + if (address_rules_.erase(address)) { + address_count_--; + } // Also remove the cross-family counterpart. if (address.family() == AF_INET) { std::string mapped = "::ffff:" + address.address(); @@ -751,6 +756,7 @@ void SocketAddressBlockList::Clear() { RwLock::ScopedLock lock(mutex_); rules_.clear(); address_rules_.clear(); + address_count_ = 0; ipv4_subnets_.Clear(); ipv6_subnets_.Clear(); subnet_rules_.clear(); @@ -988,6 +994,19 @@ void SocketAddressBlockListWrap::AddSubnet( args.GetReturnValue().Set(true); } +void SocketAddressBlockListWrap::RemoveAddress( + const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + SocketAddressBlockListWrap* wrap; + ASSIGN_OR_RETURN_UNWRAP(&wrap, args.This()); + + CHECK(SocketAddressBase::HasInstance(env, args[0])); + SocketAddressBase* addr; + ASSIGN_OR_RETURN_UNWRAP(&addr, args[0]); + + wrap->blocklist_->RemoveSocketAddress(*addr->address()); +} + void SocketAddressBlockListWrap::RemoveRange( const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); @@ -1082,6 +1101,13 @@ void SocketAddressBlockListWrap::GetRules( args.GetReturnValue().Set(rules); } +void SocketAddressBlockListWrap::GetSize( + const FunctionCallbackInfo& args) { + SocketAddressBlockListWrap* wrap; + ASSIGN_OR_RETURN_UNWRAP(&wrap, args.This()); + args.GetReturnValue().Set(static_cast(wrap->blocklist_->size())); +} + void SocketAddressBlockListWrap::Clear( const FunctionCallbackInfo& args) { SocketAddressBlockListWrap* wrap; @@ -1115,12 +1141,14 @@ Local SocketAddressBlockListWrap::GetConstructorTemplate( SetProtoMethod(isolate, tmpl, "addAddresses", AddAddresses); SetProtoMethod(isolate, tmpl, "addRange", AddRange); SetProtoMethod(isolate, tmpl, "addSubnet", AddSubnet); + SetProtoMethod(isolate, tmpl, "removeAddress", RemoveAddress); SetProtoMethod(isolate, tmpl, "removeRange", RemoveRange); SetProtoMethod(isolate, tmpl, "removeSubnet", RemoveSubnet); SetFastMethod( isolate, tmpl->PrototypeTemplate(), "check", Check, &fast_check_); SetProtoMethod(isolate, tmpl, "checkString", CheckString); SetProtoMethod(isolate, tmpl, "getRules", GetRules); + SetProtoMethodNoSideEffect(isolate, tmpl, "getSize", GetSize); SetProtoMethod(isolate, tmpl, "clear", Clear); env->set_blocklist_constructor_template(tmpl); } diff --git a/src/node_sockaddr.h b/src/node_sockaddr.h index fe1590d84256..48f868bdd4fa 100644 --- a/src/node_sockaddr.h +++ b/src/node_sockaddr.h @@ -271,7 +271,7 @@ class SocketAddressBlockList : public MemoryRetainer { void Clear(); size_t size() const { - return address_rules_.size() + rules_.size() + subnet_rules_.size(); + return address_count_ + rules_.size() + subnet_rules_.size(); } v8::MaybeLocal ListRules(Environment* env); @@ -381,6 +381,8 @@ class SocketAddressBlockList : public MemoryRetainer { // Apply() can perform O(1) lookups regardless of the port on the // checked address. Not included in rules_ to avoid redundant scanning. SocketAddress::IpMap address_rules_; + // User-visible address count (not inflated by cross-family dual-insert). + size_t address_count_ = 0; // Subnet/mask rules stored in radix tries for O(prefix_length) lookup. // Separate tries for IPv4 (max 32-bit depth) and IPv6 (max 128-bit). SubnetTrie ipv4_subnets_; @@ -412,6 +414,7 @@ class SocketAddressBlockListWrap : public BaseObject { static void AddAddresses(const v8::FunctionCallbackInfo& args); static void AddRange(const v8::FunctionCallbackInfo& args); static void AddSubnet(const v8::FunctionCallbackInfo& args); + static void RemoveAddress(const v8::FunctionCallbackInfo& args); static void RemoveRange(const v8::FunctionCallbackInfo& args); static void RemoveSubnet(const v8::FunctionCallbackInfo& args); static void Check(const v8::FunctionCallbackInfo& args); @@ -419,6 +422,7 @@ class SocketAddressBlockListWrap : public BaseObject { v8::Local addr_obj); static void CheckString(const v8::FunctionCallbackInfo& args); static void GetRules(const v8::FunctionCallbackInfo& args); + static void GetSize(const v8::FunctionCallbackInfo& args); static void Clear(const v8::FunctionCallbackInfo& args); SocketAddressBlockListWrap(Environment* env, diff --git a/test/parallel/test-blocklist.js b/test/parallel/test-blocklist.js index 14fb56183286..9c51da809c07 100644 --- a/test/parallel/test-blocklist.js +++ b/test/parallel/test-blocklist.js @@ -638,3 +638,127 @@ const util = require('util'); blockList.removeCIDR('192.168.0.0/16'); assert(!blockList.check('192.168.1.1')); } + +// removeAddress: basic +{ + const blockList = new BlockList(); + blockList.addAddress('1.1.1.1'); + blockList.addAddress('2.2.2.2'); + assert(blockList.check('1.1.1.1')); + + blockList.removeAddress('1.1.1.1'); + assert(!blockList.check('1.1.1.1')); + assert(blockList.check('2.2.2.2')); +} + +// removeAddress: cross-family cleanup +{ + const blockList = new BlockList(); + blockList.addAddress('3.3.3.3'); + assert(blockList.check('::ffff:3.3.3.3', 'ipv6')); + + blockList.removeAddress('3.3.3.3'); + assert(!blockList.check('3.3.3.3')); + assert(!blockList.check('::ffff:3.3.3.3', 'ipv6')); +} + +// removeAddress: IPv6 +{ + const blockList = new BlockList(); + blockList.addAddress('::1', 'ipv6'); + assert(blockList.check('::1', 'ipv6')); + + blockList.removeAddress('::1', 'ipv6'); + assert(!blockList.check('::1', 'ipv6')); +} + +// removeAddress: non-existent is a no-op +{ + const blockList = new BlockList(); + blockList.addAddress('1.1.1.1'); + blockList.removeAddress('9.9.9.9'); + assert(blockList.check('1.1.1.1')); +} + +// removeAddress: with SocketAddress object +{ + const blockList = new BlockList(); + const addr = new SocketAddress({ address: '5.5.5.5' }); + blockList.addAddress(addr); + assert(blockList.check('5.5.5.5')); + + blockList.removeAddress(addr); + assert(!blockList.check('5.5.5.5')); +} + +// addCIDRs: batch +{ + const blockList = new BlockList(); + blockList.addCIDRs(['10.0.0.0/8', '192.168.0.0/16', '2001:db8::/32']); + assert(blockList.check('10.1.2.3')); + assert(blockList.check('192.168.1.1')); + assert(blockList.check('2001:db8::1', 'ipv6')); + assert(!blockList.check('11.0.0.1')); + assert.strictEqual(blockList.rules.length, 3); +} + +// addCIDRs: validation +{ + const blockList = new BlockList(); + assert.throws(() => blockList.addCIDRs('not-an-array'), { + code: 'ERR_INVALID_ARG_TYPE', + }); + assert.throws(() => blockList.addCIDRs([123]), { + code: 'ERR_INVALID_ARG_TYPE', + }); + assert.throws(() => blockList.addCIDRs(['10.0.0.0']), { + code: 'ERR_INVALID_ARG_VALUE', + }); +} + +// addCIDRs: empty array is a no-op +{ + const blockList = new BlockList(); + blockList.addCIDRs([]); + assert.strictEqual(blockList.size, 0); +} + +// size: tracks all rule types +{ + const blockList = new BlockList(); + assert.strictEqual(blockList.size, 0); + + blockList.addAddress('1.1.1.1'); + assert.strictEqual(blockList.size, 1); + + blockList.addRange('10.0.0.1', '10.0.0.10'); + assert.strictEqual(blockList.size, 2); + + blockList.addSubnet('192.168.0.0', 16); + assert.strictEqual(blockList.size, 3); + + // Matches rules.length + assert.strictEqual(blockList.size, blockList.rules.length); + + blockList.removeAddress('1.1.1.1'); + assert.strictEqual(blockList.size, 2); + + blockList.removeRange('10.0.0.1', '10.0.0.10'); + assert.strictEqual(blockList.size, 1); + + blockList.removeSubnet('192.168.0.0', 16); + assert.strictEqual(blockList.size, 0); + + // After clear + blockList.addAddress('5.5.5.5'); + blockList.clear(); + assert.strictEqual(blockList.size, 0); +} + +// size: duplicate addAddress does not double-count +{ + const blockList = new BlockList(); + blockList.addAddress('1.1.1.1'); + blockList.addAddress('1.1.1.1'); + assert.strictEqual(blockList.size, 1); +} From e3055ea23852039a8711100b7042caf700474b18 Mon Sep 17 00:00:00 2001 From: James M Snell Date: Sun, 2 Aug 2026 23:10:43 -0700 Subject: [PATCH 16/18] net: add private subnet presets to BlockList Signed-off-by: James M Snell Assisted-by: OpenCode/Opus --- doc/api/net.md | 32 ++++++++++++++++++++++++ lib/internal/blocklist.js | 16 ++++++++++++ test/parallel/test-blocklist.js | 44 +++++++++++++++++++++++++++++++++ 3 files changed, 92 insertions(+) diff --git a/doc/api/net.md b/doc/api/net.md index e9d64f53693a..3b66be06cc5c 100644 --- a/doc/api/net.md +++ b/doc/api/net.md @@ -242,6 +242,38 @@ added: * `value` {any} Any JS value * Returns `true` if the `value` is a `net.BlockList`. +### `BlockList.PRIVATE_RANGES` + + + +* Type: {string\[]} + +A frozen array of CIDR strings representing private, loopback, and link-local +IP address ranges. This can be passed to `blockList.addCIDRs()` to quickly +populate a blocklist with all non-routable address ranges. + +The included ranges are: + +* `10.0.0.0/8` — RFC 1918 private IPv4 +* `172.16.0.0/12` — RFC 1918 private IPv4 +* `192.168.0.0/16` — RFC 1918 private IPv4 +* `127.0.0.0/8` — IPv4 loopback +* `::1/128` — IPv6 loopback +* `169.254.0.0/16` — IPv4 link-local +* `fe80::/10` — IPv6 link-local +* `fc00::/7` — IPv6 unique local (ULA) + +```js +const blockList = new net.BlockList(); +blockList.addCIDRs(net.BlockList.PRIVATE_RANGES); + +console.log(blockList.check('10.0.0.1')); // Prints: true +console.log(blockList.check('127.0.0.1')); // Prints: true +console.log(blockList.check('8.8.8.8')); // Prints: false +``` + ### `blockList.removeAddress(address[, type])`