GeezerVison is showing github.com — quic: add block list support for endpoints · nodejs/node@e493f04
File treeExpand file treeCollapse file tree Open diff view settings Expand file treeCollapse file tree Open diff view settings Collapse file Copy file name to clipboardExpand all lines: doc/api/quic.md +80Lines changed: 80 additions & 0 deletions - Display the source diff
- Display the rich diff
| Original file line number | Diff line number | Diff line change |
|---|
@@ -189,6 +189,50 @@ object. Each rate limiter has a corresponding counter | 189 | 189 | were dropped. A non-zero value indicates the rate limiter is actively | 190 | 190 | protecting the endpoint. | 191 | 191 |
| | 192 | +#### Block lists | | 193 | + | | 194 | +Endpoints can filter incoming packets by source address using a | | 195 | +[`net.BlockList`][]. The block list is checked before any QUIC processing | | 196 | +occurs, so blocked packets consume no resources beyond the check itself. | | 197 | + | | 198 | +In **deny** mode (the default), packets from addresses in the list are dropped: | | 199 | + | | 200 | +```mjs | | 201 | +import { BlockList } from 'node:net'; | | 202 | +import { listen } from 'node:quic'; | | 203 | + | | 204 | +const blocked = new BlockList(); | | 205 | +blocked.addSubnet('192.168.1.0', 24); // Block an entire subnet | | 206 | +blocked.addAddress('10.0.0.5'); // Block a specific address | | 207 | + | | 208 | +const endpoint = await listen(onSession, { | | 209 | + endpoint: { | | 210 | + blockList: blocked, | | 211 | + blockListPolicy: 'deny', | | 212 | + }, | | 213 | + // ... | | 214 | +}); | | 215 | +``` | | 216 | + | | 217 | +In **allow** mode, only packets from addresses in the list are accepted: | | 218 | + | | 219 | +```mjs | | 220 | +const trusted = new BlockList(); | | 221 | +trusted.addSubnet('10.0.0.0', 8); | | 222 | + | | 223 | +const endpoint = await listen(onSession, { | | 224 | + endpoint: { | | 225 | + blockList: trusted, | | 226 | + blockListPolicy: 'allow', | | 227 | + }, | | 228 | + // ... | | 229 | +}); | | 230 | +``` | | 231 | + | | 232 | +The block list is evaluated live — rules added or removed after the endpoint | | 233 | +is created take effect immediately. The `endpoint.stats.packetsBlocked` | | 234 | +counter tracks how many packets have been dropped by the filter. | | 235 | + | 192 | 236 | ### Applications | 193 | 237 |
| 194 | 238 | Every `QuicSession` is associated with a single application protocol, negotiated | @@ -831,6 +875,11 @@ added: v23.8.0 | 831 | 875 | per-host rate limiter. Read only. A non-zero value indicates one or more | 832 | 876 | remote addresses are creating sessions faster than the configured rate allows. | 833 | 877 |
| | 878 | +### `endpointStats.packetsBlocked` | | 879 | + | | 880 | +* Type: {bigint} The total number of incoming packets dropped by the | | 881 | + block list filter. Read only. | | 882 | + | 834 | 883 | ## Class: `QuicSession` | 835 | 884 |
| 836 | 885 | <!-- YAML | @@ -2429,6 +2478,34 @@ added: v23.8.0 | 2429 | 2478 |
| 2430 | 2479 | If not specified the endpoint will bind to IPv4 `localhost` on a random port. | 2431 | 2480 |
| | 2481 | +#### `endpointOptions.blockList` | | 2482 | + | | 2483 | +* Type: {net.BlockList} | | 2484 | + | | 2485 | +An optional [`net.BlockList`][] instance for filtering incoming packets by | | 2486 | +source address. When configured, every received UDP packet is checked against | | 2487 | +the block list before any QUIC processing occurs, minimizing resource | | 2488 | +expenditure on blocked sources. The block list is evaluated live — rules | | 2489 | +added to the `BlockList` object after the endpoint is created take effect | | 2490 | +immediately. | | 2491 | + | | 2492 | +See [`endpointOptions.blockListPolicy`][] for how matches are interpreted. | | 2493 | + | | 2494 | +#### `endpointOptions.blockListPolicy` | | 2495 | + | | 2496 | +* Type: {string} One of `'deny'` or `'allow'`. | | 2497 | +* **Default:** `'deny'` | | 2498 | + | | 2499 | +Controls how the [`endpointOptions.blockList`][] is interpreted: | | 2500 | + | | 2501 | +* `'deny'` — Packets from addresses matching the block list are dropped. | | 2502 | + All other addresses are accepted. This is the typical blocklist mode. | | 2503 | +* `'allow'` — Only packets from addresses matching the block list are | | 2504 | + accepted. All other addresses are dropped. This is an allowlist mode | | 2505 | + for restricting access to known clients. | | 2506 | + | | 2507 | +If no block list is configured, this option has no effect. | | 2508 | + | 2432 | 2509 | #### `endpointOptions.addressLRUSize` | 2433 | 2510 |
| 2434 | 2511 | <!-- YAML | @@ -4275,6 +4352,8 @@ throughput issues caused by flow control. | 4275 | 4352 | [`endpoint.busy`]: #endpointbusy | 4276 | 4353 | [`endpoint.maxConnectionsPerHost`]: #endpointmaxconnectionsperhost | 4277 | 4354 | [`endpoint.maxConnectionsTotal`]: #endpointmaxconnectionstotal | | 4355 | +[`endpointOptions.blockListPolicy`]: #endpointoptionsblocklistpolicy | | 4356 | +[`endpointOptions.blockList`]: #endpointoptionsblocklist | 4278 | 4357 | [`endpointOptions.immediateCloseBurst`]: #endpointoptionsimmediatecloseburst | 4279 | 4358 | [`endpointOptions.immediateCloseRate`]: #endpointoptionsimmediatecloserate | 4280 | 4359 | [`endpointOptions.retryBurst`]: #endpointoptionsretryburst | @@ -4288,6 +4367,7 @@ throughput issues caused by flow control. | 4288 | 4367 | [`error.errorCode`]: #errorerrorcode | 4289 | 4368 | [`fs.promises.open(path, 'r')`]: fs.md#fspromisesopenpath-flags-mode | 4290 | 4369 | [`maxDatagramFrameSize`]: #transportparamsmaxdatagramframesize | | 4370 | +[`net.BlockList`]: net.md#class-netblocklist | 4291 | 4371 | [`quic.connect()`]: #quicconnectaddress-options | 4292 | 4372 | [`quic.listen()`]: #quiclistenonsession-options | 4293 | 4373 | [`session.close()`]: #sessioncloseoptions | |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/blocklist.js +1Lines changed: 1 addition & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -298,4 +298,5 @@ ObjectSetPrototypeOf(InternalBlockList.prototype, BlockList.prototype); | 298 | 298 | module.exports = { | 299 | 299 | BlockList, | 300 | 300 | InternalBlockList, | | 301 | + kHandle, | 301 | 302 | }; |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/quic/quic.js +21Lines changed: 21 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -36,6 +36,10 @@ if (!process.features.quic || !getOptionValue('--experimental-quic')) { | 36 | 36 | } | 37 | 37 |
| 38 | 38 | const { inspect } = require('internal/util/inspect'); | | 39 | +const { | | 40 | + BlockList, | | 41 | + kHandle: kBlockListHandle, | | 42 | +} = require('internal/blocklist'); | 39 | 43 |
| 40 | 44 | let debug = require('internal/util/debuglog').debuglog('quic', (fn) => { | 41 | 45 | debug = fn; | @@ -318,6 +322,8 @@ const endpointRegistry = new SafeSet(); | 318 | 322 | * @property {number} [immediateCloseBurst] Burst capacity for immediate close rate limiter | 319 | 323 | * @property {number} [sessionCreationRate] Per-host rate limit for session creation (per second) | 320 | 324 | * @property {number} [sessionCreationBurst] Per-host burst capacity for session creation rate limiter | | 325 | + * @property {net.BlockList} [blockList] Block list for filtering incoming packets by source address | | 326 | + * @property {'deny'|'allow'} [blockListPolicy='deny'] How to interpret the block list | 321 | 327 | * @property {ArrayBufferView} [resetTokenSecret] The reset token secret | 322 | 328 | * @property {bigint|number} [retryTokenExpiration] The retry token expiration | 323 | 329 | * @property {number} [rxDiagnosticLoss] The receive diagnostic loss probability (range 0.0-1.0) | @@ -4048,6 +4054,8 @@ class QuicEndpoint { | 4048 | 4054 | immediateCloseBurst, | 4049 | 4055 | sessionCreationRate, | 4050 | 4056 | sessionCreationBurst, | | 4057 | + blockList, | | 4058 | + blockListPolicy = 'deny', | 4051 | 4059 | rxDiagnosticLoss, | 4052 | 4060 | txDiagnosticLoss, | 4053 | 4061 | udpReceiveBufferSize, | @@ -4062,6 +4070,16 @@ class QuicEndpoint { | 4062 | 4070 | tokenSecret, | 4063 | 4071 | } = options; | 4064 | 4072 |
| | 4073 | + if (blockList !== undefined) { | | 4074 | + if (!BlockList.isBlockList(blockList)) { | | 4075 | + throw new ERR_INVALID_ARG_TYPE('options.blockList', | | 4076 | + 'net.BlockList', blockList); | | 4077 | + } | | 4078 | + } | | 4079 | + | | 4080 | + validateOneOf(blockListPolicy, 'options.blockListPolicy', | | 4081 | + ['deny', 'allow']); | | 4082 | + | 4065 | 4083 | // All of the other options will be validated internally by the C++ code | 4066 | 4084 | if (address !== undefined && !SocketAddress.isSocketAddress(address)) { | 4067 | 4085 | if (typeof address === 'string') { | @@ -4093,6 +4111,9 @@ class QuicEndpoint { | 4093 | 4111 | immediateCloseBurst, | 4094 | 4112 | sessionCreationRate, | 4095 | 4113 | sessionCreationBurst, | | 4114 | + // Pass the C++ handle, not the JS BlockList wrapper. | | 4115 | + blockList: blockList?.[kBlockListHandle], | | 4116 | + blockListPolicy, | 4096 | 4117 | rxDiagnosticLoss, | 4097 | 4118 | txDiagnosticLoss, | 4098 | 4119 | udpReceiveBufferSize, | |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/quic/stats.js +12Lines changed: 12 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -68,6 +68,7 @@ const { | 68 | 68 | IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_COUNT, | 69 | 69 | IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_RATE_LIMITED, | 70 | 70 | IDX_STATS_ENDPOINT_SESSION_CREATION_RATE_LIMITED, | | 71 | + IDX_STATS_ENDPOINT_PACKETS_BLOCKED, | 71 | 72 |
| 72 | 73 | IDX_STATS_SESSION_CREATED_AT, | 73 | 74 | IDX_STATS_SESSION_DESTROYED_AT, | @@ -136,6 +137,7 @@ assert(IDX_STATS_ENDPOINT_STATELESS_RESET_RATE_LIMITED !== undefined); | 136 | 137 | assert(IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_COUNT !== undefined); | 137 | 138 | assert(IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_RATE_LIMITED !== undefined); | 138 | 139 | assert(IDX_STATS_ENDPOINT_SESSION_CREATION_RATE_LIMITED !== undefined); | | 140 | +assert(IDX_STATS_ENDPOINT_PACKETS_BLOCKED !== undefined); | 139 | 141 | assert(IDX_STATS_SESSION_CREATED_AT !== undefined); | 140 | 142 | assert(IDX_STATS_SESSION_DESTROYED_AT !== undefined); | 141 | 143 | assert(IDX_STATS_SESSION_CLOSING_AT !== undefined); | @@ -338,6 +340,12 @@ class QuicEndpointStats { | 338 | 340 | return this.#handle[IDX_STATS_ENDPOINT_SESSION_CREATION_RATE_LIMITED]; | 339 | 341 | } | 340 | 342 |
| | 343 | + /** @type {bigint} */ | | 344 | + get packetsBlocked() { | | 345 | + assertIsQuicEndpointStats(this); | | 346 | + return this.#handle[IDX_STATS_ENDPOINT_PACKETS_BLOCKED]; | | 347 | + } | | 348 | + | 341 | 349 | toString() { | 342 | 350 | return JSONStringify(this.toJSON()); | 343 | 351 | } | @@ -363,6 +371,7 @@ class QuicEndpointStats { | 363 | 371 | immediateCloseCount, | 364 | 372 | immediateCloseRateLimited, | 365 | 373 | sessionCreationRateLimited, | | 374 | + packetsBlocked, | 366 | 375 | } = this; | 367 | 376 | return { | 368 | 377 | __proto__: null, | @@ -387,6 +396,7 @@ class QuicEndpointStats { | 387 | 396 | immediateCloseCount: `${immediateCloseCount}`, | 388 | 397 | immediateCloseRateLimited: `${immediateCloseRateLimited}`, | 389 | 398 | sessionCreationRateLimited: `${sessionCreationRateLimited}`, | | 399 | + packetsBlocked: `${packetsBlocked}`, | 390 | 400 | }; | 391 | 401 | } | 392 | 402 |
| @@ -421,6 +431,7 @@ class QuicEndpointStats { | 421 | 431 | immediateCloseCount, | 422 | 432 | immediateCloseRateLimited, | 423 | 433 | sessionCreationRateLimited, | | 434 | + packetsBlocked, | 424 | 435 | } = this; | 425 | 436 |
| 426 | 437 | return `QuicEndpointStats ${inspect({ | @@ -443,6 +454,7 @@ class QuicEndpointStats { | 443 | 454 | immediateCloseCount, | 444 | 455 | immediateCloseRateLimited, | 445 | 456 | sessionCreationRateLimited, | | 457 | + packetsBlocked, | 446 | 458 | }, opts)}`; | 447 | 459 | } | 448 | 460 |
| |
Collapse file Copy file name to clipboardExpand all lines: src/node_sockaddr.cc +8-9Lines changed: 8 additions & 9 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -434,8 +434,7 @@ void SocketAddressBlockList::AddSocketAddressMask( | 434 | 434 | rules_.emplace_front(std::move(rule)); | 435 | 435 | } | 436 | 436 |
| 437 | | -bool SocketAddressBlockList::Apply( | 438 | | - const std::shared_ptr<SocketAddress>& address) { | | 437 | +bool SocketAddressBlockList::Apply(const SocketAddress& address) { | 439 | 438 | Mutex::ScopedLock lock(mutex_); | 440 | 439 | for (const auto& rule : rules_) { | 441 | 440 | if (rule->Apply(address)) return true; | @@ -457,8 +456,8 @@ SocketAddressBlockList::SocketAddressMaskRule::SocketAddressMaskRule( | 457 | 456 | : network(network_), prefix(prefix_) {} | 458 | 457 |
| 459 | 458 | bool SocketAddressBlockList::SocketAddressRule::Apply( | 460 | | - const std::shared_ptr<SocketAddress>& address) { | 461 | | - return this->address->is_match(*address.get()); | | 459 | + const SocketAddress& address) { | | 460 | + return this->address->is_match(address); | 462 | 461 | } | 463 | 462 |
| 464 | 463 | std::string SocketAddressBlockList::SocketAddressRule::ToString() { | @@ -470,8 +469,8 @@ std::string SocketAddressBlockList::SocketAddressRule::ToString() { | 470 | 469 | } | 471 | 470 |
| 472 | 471 | bool SocketAddressBlockList::SocketAddressRangeRule::Apply( | 473 | | - const std::shared_ptr<SocketAddress>& address) { | 474 | | - return *address.get() >= *start.get() && *address.get() <= *end.get(); | | 472 | + const SocketAddress& address) { | | 473 | + return address >= *start.get() && address <= *end.get(); | 475 | 474 | } | 476 | 475 |
| 477 | 476 | std::string SocketAddressBlockList::SocketAddressRangeRule::ToString() { | @@ -485,8 +484,8 @@ std::string SocketAddressBlockList::SocketAddressRangeRule::ToString() { | 485 | 484 | } | 486 | 485 |
| 487 | 486 | bool SocketAddressBlockList::SocketAddressMaskRule::Apply( | 488 | | - const std::shared_ptr<SocketAddress>& address) { | 489 | | - return address->is_in_network(*network.get(), prefix); | | 487 | + const SocketAddress& address) { | | 488 | + return address.is_in_network(*network.get(), prefix); | 490 | 489 | } | 491 | 490 |
| 492 | 491 | std::string SocketAddressBlockList::SocketAddressMaskRule::ToString() { | @@ -656,7 +655,7 @@ void SocketAddressBlockListWrap::Check( | 656 | 655 | SocketAddressBase* addr; | 657 | 656 | ASSIGN_OR_RETURN_UNWRAP(&addr, args[0]); | 658 | 657 |
| 659 | | - args.GetReturnValue().Set(wrap->blocklist_->Apply(addr->address())); | | 658 | + args.GetReturnValue().Set(wrap->blocklist_->Apply(*addr->address())); | 660 | 659 | } | 661 | 660 |
| 662 | 661 | void SocketAddressBlockListWrap::GetRules( | |
Collapse file Copy file name to clipboardExpand all lines: src/node_sockaddr.h +9-5Lines changed: 9 additions & 5 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -258,14 +258,14 @@ class SocketAddressBlockList : public MemoryRetainer { | 258 | 258 | void AddSocketAddressMask(const std::shared_ptr<SocketAddress>& address, | 259 | 259 | int prefix); | 260 | 260 |
| 261 | | - bool Apply(const std::shared_ptr<SocketAddress>& address); | | 261 | + bool Apply(const SocketAddress& address); | 262 | 262 |
| 263 | 263 | size_t size() const { return rules_.size(); } | 264 | 264 |
| 265 | 265 | v8::MaybeLocal<v8::Array> ListRules(Environment* env); | 266 | 266 |
| 267 | 267 | struct Rule : public MemoryRetainer { | 268 | | - virtual bool Apply(const std::shared_ptr<SocketAddress>& address) = 0; | | 268 | + virtual bool Apply(const SocketAddress& address) = 0; | 269 | 269 | inline v8::MaybeLocal<v8::Value> ToV8String(Environment* env); | 270 | 270 | virtual std::string ToString() = 0; | 271 | 271 | }; | @@ -275,7 +275,7 @@ class SocketAddressBlockList : public MemoryRetainer { | 275 | 275 |
| 276 | 276 | explicit SocketAddressRule(const std::shared_ptr<SocketAddress>& address); | 277 | 277 |
| 278 | | - bool Apply(const std::shared_ptr<SocketAddress>& address) override; | | 278 | + bool Apply(const SocketAddress& address) override; | 279 | 279 | std::string ToString() override; | 280 | 280 |
| 281 | 281 | void MemoryInfo(node::MemoryTracker* tracker) const override; | @@ -290,7 +290,7 @@ class SocketAddressBlockList : public MemoryRetainer { | 290 | 290 | SocketAddressRangeRule(const std::shared_ptr<SocketAddress>& start, | 291 | 291 | const std::shared_ptr<SocketAddress>& end); | 292 | 292 |
| 293 | | - bool Apply(const std::shared_ptr<SocketAddress>& address) override; | | 293 | + bool Apply(const SocketAddress& address) override; | 294 | 294 | std::string ToString() override; | 295 | 295 |
| 296 | 296 | void MemoryInfo(node::MemoryTracker* tracker) const override; | @@ -305,7 +305,7 @@ class SocketAddressBlockList : public MemoryRetainer { | 305 | 305 | SocketAddressMaskRule(const std::shared_ptr<SocketAddress>& address, | 306 | 306 | int prefix); | 307 | 307 |
| 308 | | - bool Apply(const std::shared_ptr<SocketAddress>& address) override; | | 308 | + bool Apply(const SocketAddress& address) override; | 309 | 309 | std::string ToString() override; | 310 | 310 |
| 311 | 311 | void MemoryInfo(node::MemoryTracker* tracker) const override; | @@ -353,6 +353,10 @@ class SocketAddressBlockListWrap : public BaseObject { | 353 | 353 | std::shared_ptr<SocketAddressBlockList> blocklist = | 354 | 354 | std::make_shared<SocketAddressBlockList>()); | 355 | 355 |
| | 356 | + inline const std::shared_ptr<SocketAddressBlockList>& blocklist() const { | | 357 | + return blocklist_; | | 358 | + } | | 359 | + | 356 | 360 | void MemoryInfo(node::MemoryTracker* tracker) const override; | 357 | 361 | SET_MEMORY_INFO_NAME(SocketAddressBlockListWrap) | 358 | 362 | SET_SELF_SIZE(SocketAddressBlockListWrap) | |
Collapse file Copy file name to clipboardExpand all lines: src/quic/bindingdata.h +4Lines changed: 4 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -70,6 +70,7 @@ class SessionManager; | 70 | 70 | V(ack_delay_exponent, "ackDelayExponent") \ | 71 | 71 | V(active_connection_id_limit, "activeConnectionIDLimit") \ | 72 | 72 | V(address_lru_size, "addressLRUSize") \ | | 73 | + V(allow, "allow") \ | 73 | 74 | V(application, "application") \ | 74 | 75 | V(authoritative, "authoritative") \ | 75 | 76 | V(bbr, "bbr") \ | @@ -81,6 +82,7 @@ class SessionManager; | 81 | 82 | V(crl, "crl") \ | 82 | 83 | V(cubic, "cubic") \ | 83 | 84 | V(datagram_drop_policy, "datagramDropPolicy") \ | | 85 | + V(deny, "deny") \ | 84 | 86 | V(disable_stateless_reset, "disableStatelessReset") \ | 85 | 87 | V(draining_period_multiplier, "drainingPeriodMultiplier") \ | 86 | 88 | V(enable_connect_protocol, "enableConnectProtocol") \ | @@ -127,6 +129,8 @@ class SessionManager; | 127 | 129 | V(immediate_close_burst, "immediateCloseBurst") \ | 128 | 130 | V(session_creation_rate, "sessionCreationRate") \ | 129 | 131 | V(session_creation_burst, "sessionCreationBurst") \ | | 132 | + V(block_list, "blockList") \ | | 133 | + V(block_list_policy, "blockListPolicy") \ | 130 | 134 | V(max_stream_window, "maxStreamWindow") \ | 131 | 135 | V(max_window, "maxWindow") \ | 132 | 136 | V(min_version, "minVersion") \ | |
|