Commit 8107f1b

Browse files
jasnelladuh95
authored andcommitted
quic: refine rate limiting
Signed-off-by: James M Snell <jasnell@gmail.com> Assisted-by: Opencode:Opus 4.6 PR-URL: #63483 Backport-PR-URL: #64675 Reviewed-By: Stephen Belanger <admin@stephenbelanger.com> Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
1 parent 3f0d737 commit 8107f1b

12 files changed

Lines changed: 433 additions & 183 deletions

‎doc/api/quic.md‎

Lines changed: 84 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -726,31 +726,55 @@ added: v23.8.0
726726
added: v23.8.0
727727
-->
728728

729-
* Type: {bigint} The total number of QUIC retry attempts on this endpoint. Read only.
729+
* Type: {bigint} The total number of retry packets sent by this endpoint. Read only.
730+
731+
### `endpointStats.retryRateLimited`
732+
733+
* Type: {bigint} The total number of retry packets dropped by the global rate
734+
limiter. Read only. A non-zero value indicates the endpoint is under retry
735+
flood pressure.
730736

731737
### `endpointStats.versionNegotiationCount`
732738

733739
<!-- YAML
734740
added: v23.8.0
735741
-->
736742

737-
* Type: {bigint} The total number of sessions rejected due to QUIC version mismatch. Read only.
743+
* Type: {bigint} The total number of version negotiation packets sent by this
744+
endpoint. Read only.
745+
746+
### `endpointStats.versionNegotiationRateLimited`
747+
748+
* Type: {bigint} The total number of version negotiation packets dropped by
749+
the global rate limiter. Read only.
738750

739751
### `endpointStats.statelessResetCount`
740752

741753
<!-- YAML
742754
added: v23.8.0
743755
-->
744756

745-
* Type: {bigint} The total number of stateless resets handled by this endpoint. Read only.
757+
* Type: {bigint} The total number of stateless reset packets sent by this
758+
endpoint. Read only.
759+
760+
### `endpointStats.statelessResetRateLimited`
761+
762+
* Type: {bigint} The total number of stateless reset packets dropped by the
763+
global rate limiter. Read only.
746764

747765
### `endpointStats.immediateCloseCount`
748766

749767
<!-- YAML
750768
added: v23.8.0
751769
-->
752770

753-
* Type: {bigint} The total number of sessions that were closed before handshake completed. Read only.
771+
* Type: {bigint} The total number of immediate connection close packets sent
772+
by this endpoint. Read only.
773+
774+
### `endpointStats.immediateCloseRateLimited`
775+
776+
* Type: {bigint} The total number of immediate connection close packets
777+
dropped by the global rate limiter. Read only.
754778

755779
## Class: `QuicSession`
756780

@@ -2455,25 +2479,69 @@ addresses. When the limit is reached, new connections are refused with
24552479
This limit can also be changed dynamically after construction via
24562480
[`endpoint.maxConnectionsTotal`][].
24572481

2458-
#### `endpointOptions.maxRetries`
2482+
#### `endpointOptions.retryRate`
24592483

2460-
<!-- YAML
2461-
added: v23.8.0
2462-
-->
2484+
* Type: {number}
2485+
***Default:**`100`
24632486

2464-
* Type: {bigint|number}
2487+
The maximum number of QUIC retry packets the endpoint will send per second.
2488+
This is a global rate limit (not per-host) that caps the total server-wide
2489+
retry response rate, preventing spoofed-source floods from consuming unbounded
2490+
resources.
24652491

2466-
Specifies the maximum number of QUIC retry attempts allowed per remote peer address.
2492+
#### `endpointOptions.retryBurst`
24672493

2468-
#### `endpointOptions.maxStatelessResetsPerHost`
2494+
* Type: {number}
2495+
***Default:**`200`
24692496

2470-
<!-- YAML
2471-
added: v23.8.0
2472-
-->
2497+
The maximum burst of retry packets allowed before rate limiting takes effect.
24732498

2474-
* Type: {bigint|number}
2499+
#### `endpointOptions.statelessResetRate`
2500+
2501+
* Type: {number}
2502+
***Default:**`100`
2503+
2504+
The maximum number of stateless reset packets the endpoint will send per second.
2505+
2506+
#### `endpointOptions.statelessResetBurst`
2507+
2508+
* Type: {number}
2509+
***Default:**`200`
2510+
2511+
The maximum burst of stateless reset packets allowed before rate limiting
2512+
takes effect.
2513+
2514+
#### `endpointOptions.versionNegotiationRate`
2515+
2516+
* Type: {number}
2517+
***Default:**`100`
2518+
2519+
The maximum number of version negotiation packets the endpoint will send per
2520+
second.
2521+
2522+
#### `endpointOptions.versionNegotiationBurst`
2523+
2524+
* Type: {number}
2525+
***Default:**`200`
2526+
2527+
The maximum burst of version negotiation packets allowed before rate limiting
2528+
takes effect.
2529+
2530+
#### `endpointOptions.immediateCloseRate`
2531+
2532+
* Type: {number}
2533+
***Default:**`100`
2534+
2535+
The maximum number of immediate connection close packets the endpoint will
2536+
send per second.
2537+
2538+
#### `endpointOptions.immediateCloseBurst`
2539+
2540+
* Type: {number}
2541+
***Default:**`200`
24752542

2476-
Specifies the maximum number of stateless resets that are allowed per remote peer address.
2543+
The maximum burst of immediate connection close packets allowed before rate
2544+
limiting takes effect.
24772545

24782546
#### `endpointOptions.retryTokenExpiration`
24792547

‎lib/internal/quic/quic.js‎

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -307,8 +307,14 @@ const endpointRegistry = new SafeSet();
307307
* @property {boolean} [reusePort] Enable SO_REUSEPORT for multi-process load balancing
308308
* @property {bigint|number} [maxConnectionsPerHost] The maximum number of connections per host
309309
* @property {bigint|number} [maxConnectionsTotal] The maximum number of total connections
310-
* @property {bigint|number} [maxRetries] The maximum number of retries
311-
* @property {bigint|number} [maxStatelessResetsPerHost] The maximum number of stateless resets per host
310+
* @property {number} [retryRate] Global rate limit for retry packets (per second)
311+
* @property {number} [retryBurst] Burst capacity for retry rate limiter
312+
* @property {number} [statelessResetRate] Global rate limit for stateless reset packets (per second)
313+
* @property {number} [statelessResetBurst] Burst capacity for stateless reset rate limiter
314+
* @property {number} [versionNegotiationRate] Global rate limit for version negotiation packets (per second)
315+
* @property {number} [versionNegotiationBurst] Burst capacity for version negotiation rate limiter
316+
* @property {number} [immediateCloseRate] Global rate limit for immediate close packets (per second)
317+
* @property {number} [immediateCloseBurst] Burst capacity for immediate close rate limiter
312318
* @property {ArrayBufferView} [resetTokenSecret] The reset token secret
313319
* @property {bigint|number} [retryTokenExpiration] The retry token expiration
314320
* @property {number} [rxDiagnosticLoss] The receive diagnostic loss probability (range 0.0-1.0)
@@ -3997,10 +4003,16 @@ class QuicEndpoint {
39974003
tokenExpiration,
39984004
maxConnectionsPerHost =100,
39994005
maxConnectionsTotal =10_000,
4000-
maxStatelessResetsPerHost,
40014006
disableStatelessReset,
40024007
addressLRUSize,
4003-
maxRetries,
4008+
retryRate,
4009+
retryBurst,
4010+
statelessResetRate,
4011+
statelessResetBurst,
4012+
versionNegotiationRate,
4013+
versionNegotiationBurst,
4014+
immediateCloseRate,
4015+
immediateCloseBurst,
40044016
rxDiagnosticLoss,
40054017
txDiagnosticLoss,
40064018
udpReceiveBufferSize,
@@ -4034,10 +4046,16 @@ class QuicEndpoint {
40344046
// Connection limits are set on the state buffer, not passed to C++.
40354047
maxConnectionsPerHost,
40364048
maxConnectionsTotal,
4037-
maxStatelessResetsPerHost,
40384049
disableStatelessReset,
40394050
addressLRUSize,
4040-
maxRetries,
4051+
retryRate,
4052+
retryBurst,
4053+
statelessResetRate,
4054+
statelessResetBurst,
4055+
versionNegotiationRate,
4056+
versionNegotiationBurst,
4057+
immediateCloseRate,
4058+
immediateCloseBurst,
40414059
rxDiagnosticLoss,
40424060
txDiagnosticLoss,
40434061
udpReceiveBufferSize,

‎lib/internal/quic/stats.js‎

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,13 @@ const {
6060
IDX_STATS_ENDPOINT_CLIENT_SESSIONS,
6161
IDX_STATS_ENDPOINT_SERVER_BUSY_COUNT,
6262
IDX_STATS_ENDPOINT_RETRY_COUNT,
63+
IDX_STATS_ENDPOINT_RETRY_RATE_LIMITED,
6364
IDX_STATS_ENDPOINT_VERSION_NEGOTIATION_COUNT,
65+
IDX_STATS_ENDPOINT_VERSION_NEGOTIATION_RATE_LIMITED,
6466
IDX_STATS_ENDPOINT_STATELESS_RESET_COUNT,
67+
IDX_STATS_ENDPOINT_STATELESS_RESET_RATE_LIMITED,
6568
IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_COUNT,
69+
IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_RATE_LIMITED,
6670

6771
IDX_STATS_SESSION_CREATED_AT,
6872
IDX_STATS_SESSION_DESTROYED_AT,
@@ -123,9 +127,13 @@ assert(IDX_STATS_ENDPOINT_SERVER_SESSIONS !== undefined);
123127
assert(IDX_STATS_ENDPOINT_CLIENT_SESSIONS!==undefined);
124128
assert(IDX_STATS_ENDPOINT_SERVER_BUSY_COUNT!==undefined);
125129
assert(IDX_STATS_ENDPOINT_RETRY_COUNT!==undefined);
130+
assert(IDX_STATS_ENDPOINT_RETRY_RATE_LIMITED!==undefined);
126131
assert(IDX_STATS_ENDPOINT_VERSION_NEGOTIATION_COUNT!==undefined);
132+
assert(IDX_STATS_ENDPOINT_VERSION_NEGOTIATION_RATE_LIMITED!==undefined);
127133
assert(IDX_STATS_ENDPOINT_STATELESS_RESET_COUNT!==undefined);
134+
assert(IDX_STATS_ENDPOINT_STATELESS_RESET_RATE_LIMITED!==undefined);
128135
assert(IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_COUNT!==undefined);
136+
assert(IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_RATE_LIMITED!==undefined);
129137
assert(IDX_STATS_SESSION_CREATED_AT!==undefined);
130138
assert(IDX_STATS_SESSION_DESTROYED_AT!==undefined);
131139
assert(IDX_STATS_SESSION_CLOSING_AT!==undefined);
@@ -280,24 +288,48 @@ class QuicEndpointStats {
280288
returnthis.#handle[IDX_STATS_ENDPOINT_RETRY_COUNT];
281289
}
282290

291+
/** @type {bigint} */
292+
getretryRateLimited(){
293+
assertIsQuicEndpointStats(this);
294+
returnthis.#handle[IDX_STATS_ENDPOINT_RETRY_RATE_LIMITED];
295+
}
296+
283297
/** @type {bigint} */
284298
getversionNegotiationCount(){
285299
assertIsQuicEndpointStats(this);
286300
returnthis.#handle[IDX_STATS_ENDPOINT_VERSION_NEGOTIATION_COUNT];
287301
}
288302

303+
/** @type {bigint} */
304+
getversionNegotiationRateLimited(){
305+
assertIsQuicEndpointStats(this);
306+
returnthis.#handle[IDX_STATS_ENDPOINT_VERSION_NEGOTIATION_RATE_LIMITED];
307+
}
308+
289309
/** @type {bigint} */
290310
getstatelessResetCount(){
291311
assertIsQuicEndpointStats(this);
292312
returnthis.#handle[IDX_STATS_ENDPOINT_STATELESS_RESET_COUNT];
293313
}
294314

315+
/** @type {bigint} */
316+
getstatelessResetRateLimited(){
317+
assertIsQuicEndpointStats(this);
318+
returnthis.#handle[IDX_STATS_ENDPOINT_STATELESS_RESET_RATE_LIMITED];
319+
}
320+
295321
/** @type {bigint} */
296322
getimmediateCloseCount(){
297323
assertIsQuicEndpointStats(this);
298324
returnthis.#handle[IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_COUNT];
299325
}
300326

327+
/** @type {bigint} */
328+
getimmediateCloseRateLimited(){
329+
assertIsQuicEndpointStats(this);
330+
returnthis.#handle[IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_RATE_LIMITED];
331+
}
332+
301333
toString(){
302334
returnJSONStringify(this.toJSON());
303335
}
@@ -315,9 +347,13 @@ class QuicEndpointStats {
315347
clientSessions,
316348
serverBusyCount,
317349
retryCount,
350+
retryRateLimited,
318351
versionNegotiationCount,
352+
versionNegotiationRateLimited,
319353
statelessResetCount,
354+
statelessResetRateLimited,
320355
immediateCloseCount,
356+
immediateCloseRateLimited,
321357
}=this;
322358
return{
323359
__proto__: null,
@@ -334,9 +370,13 @@ class QuicEndpointStats {
334370
clientSessions: `${clientSessions}`,
335371
serverBusyCount: `${serverBusyCount}`,
336372
retryCount: `${retryCount}`,
373+
retryRateLimited: `${retryRateLimited}`,
337374
versionNegotiationCount: `${versionNegotiationCount}`,
375+
versionNegotiationRateLimited: `${versionNegotiationRateLimited}`,
338376
statelessResetCount: `${statelessResetCount}`,
377+
statelessResetRateLimited: `${statelessResetRateLimited}`,
339378
immediateCloseCount: `${immediateCloseCount}`,
379+
immediateCloseRateLimited: `${immediateCloseRateLimited}`,
340380
};
341381
}
342382

@@ -363,9 +403,13 @@ class QuicEndpointStats {
363403
clientSessions,
364404
serverBusyCount,
365405
retryCount,
406+
retryRateLimited,
366407
versionNegotiationCount,
408+
versionNegotiationRateLimited,
367409
statelessResetCount,
410+
statelessResetRateLimited,
368411
immediateCloseCount,
412+
immediateCloseRateLimited,
369413
}=this;
370414

371415
return`QuicEndpointStats ${inspect({
@@ -380,9 +424,13 @@ class QuicEndpointStats {
380424
clientSessions,
381425
serverBusyCount,
382426
retryCount,
427+
retryRateLimited,
383428
versionNegotiationCount,
429+
versionNegotiationRateLimited,
384430
statelessResetCount,
431+
statelessResetRateLimited,
385432
immediateCloseCount,
433+
immediateCloseRateLimited,
386434
},opts)}`;
387435
}
388436

‎node.gyp‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -449,6 +449,7 @@
449449
'test/cctest/test_quic_cid.cc',
450450
'test/cctest/test_quic_error.cc',
451451
'test/cctest/test_quic_preferredaddress.cc',
452+
'test/cctest/test_quic_tokenbucket.cc',
452453
'test/cctest/test_quic_tokens.cc',
453454
],
454455
'node_cctest_inspector_sources': [

‎src/quic/bindingdata.h‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,8 +117,14 @@ class SessionManager;
117117
V(idle_timeout, "idleTimeout") \
118118
V(max_idle_timeout, "maxIdleTimeout") \
119119
V(max_payload_size, "maxPayloadSize") \
120-
V(max_retries, "maxRetries") \
121-
V(max_stateless_resets, "maxStatelessResetsPerHost") \
120+
V(retry_rate, "retryRate") \
121+
V(retry_burst, "retryBurst") \
122+
V(stateless_reset_rate, "statelessResetRate") \
123+
V(stateless_reset_burst, "statelessResetBurst") \
124+
V(version_negotiation_rate, "versionNegotiationRate") \
125+
V(version_negotiation_burst, "versionNegotiationBurst") \
126+
V(immediate_close_rate, "immediateCloseRate") \
127+
V(immediate_close_burst, "immediateCloseBurst") \
122128
V(max_stream_window, "maxStreamWindow") \
123129
V(max_window, "maxWindow") \
124130
V(min_version, "minVersion") \

‎src/quic/defs.h‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,24 @@ constexpr auto kSocketAddressInfoTimeout = 60 * NGTCP2_SECONDS;
360360
constexprsize_tkMaxVectorCount = 16;
361361
constexpr stream_id kMaxStreamId = std::numeric_limits<stream_id>::max();
362362

363+
// A token bucket rate limiter using lazy refill. No timer needed — tokens
364+
// are computed on demand from the elapsed time since the last check.
365+
// Used to cap the total rate of stateless responses (retry, reset,
366+
// version negotiation, immediate close) regardless of source address,
367+
// preventing spoofed-source floods from bypassing per-host limits.
368+
structTokenBucketfinal {
369+
double rate; // tokens per second (refill rate)
370+
double burst; // maximum tokens (bucket capacity)
371+
double tokens; // current token count
372+
uint64_t last_ts; // last refill timestamp (nanoseconds, uv_hrtime)
373+
374+
TokenBucket(double rate, double burst);
375+
376+
// Try to consume one token. Refills based on elapsed time, then
377+
// attempts to consume. Returns true if the request is allowed.
378+
boolconsume();
379+
};
380+
363381
classDebugIndentScopefinal {
364382
public:
365383
inlineDebugIndentScope() { ++indent_; }

0 commit comments

Comments
 (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Commit 8107f1b

Browse files
jasnelladuh95
authored andcommitted
quic: refine rate limiting
Signed-off-by: James M Snell <jasnell@gmail.com> Assisted-by: Opencode:Opus 4.6 PR-URL: #63483 Backport-PR-URL: #64675 Reviewed-By: Stephen Belanger <admin@stephenbelanger.com> Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
1 parent 3f0d737 commit 8107f1b

12 files changed

Lines changed: 433 additions & 183 deletions

‎doc/api/quic.md‎

Lines changed: 84 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -726,31 +726,55 @@ added: v23.8.0
726726
added: v23.8.0
727727
-->
728728

729-
* Type: {bigint} The total number of QUIC retry attempts on this endpoint. Read only.
729+
* Type: {bigint} The total number of retry packets sent by this endpoint. Read only.
730+
731+
### `endpointStats.retryRateLimited`
732+
733+
* Type: {bigint} The total number of retry packets dropped by the global rate
734+
limiter. Read only. A non-zero value indicates the endpoint is under retry
735+
flood pressure.
730736

731737
### `endpointStats.versionNegotiationCount`
732738

733739
<!-- YAML
734740
added: v23.8.0
735741
-->
736742

737-
* Type: {bigint} The total number of sessions rejected due to QUIC version mismatch. Read only.
743+
* Type: {bigint} The total number of version negotiation packets sent by this
744+
endpoint. Read only.
745+
746+
### `endpointStats.versionNegotiationRateLimited`
747+
748+
* Type: {bigint} The total number of version negotiation packets dropped by
749+
the global rate limiter. Read only.
738750

739751
### `endpointStats.statelessResetCount`
740752

741753
<!-- YAML
742754
added: v23.8.0
743755
-->
744756

745-
* Type: {bigint} The total number of stateless resets handled by this endpoint. Read only.
757+
* Type: {bigint} The total number of stateless reset packets sent by this
758+
endpoint. Read only.
759+
760+
### `endpointStats.statelessResetRateLimited`
761+
762+
* Type: {bigint} The total number of stateless reset packets dropped by the
763+
global rate limiter. Read only.
746764

747765
### `endpointStats.immediateCloseCount`
748766

749767
<!-- YAML
750768
added: v23.8.0
751769
-->
752770

753-
* Type: {bigint} The total number of sessions that were closed before handshake completed. Read only.
771+
* Type: {bigint} The total number of immediate connection close packets sent
772+
by this endpoint. Read only.
773+
774+
### `endpointStats.immediateCloseRateLimited`
775+
776+
* Type: {bigint} The total number of immediate connection close packets
777+
dropped by the global rate limiter. Read only.
754778

755779
## Class: `QuicSession`
756780

@@ -2455,25 +2479,69 @@ addresses. When the limit is reached, new connections are refused with
24552479
This limit can also be changed dynamically after construction via
24562480
[`endpoint.maxConnectionsTotal`][].
24572481

2458-
#### `endpointOptions.maxRetries`
2482+
#### `endpointOptions.retryRate`
24592483

2460-
<!-- YAML
2461-
added: v23.8.0
2462-
-->
2484+
* Type: {number}
2485+
***Default:**`100`
24632486

2464-
* Type: {bigint|number}
2487+
The maximum number of QUIC retry packets the endpoint will send per second.
2488+
This is a global rate limit (not per-host) that caps the total server-wide
2489+
retry response rate, preventing spoofed-source floods from consuming unbounded
2490+
resources.
24652491

2466-
Specifies the maximum number of QUIC retry attempts allowed per remote peer address.
2492+
#### `endpointOptions.retryBurst`
24672493

2468-
#### `endpointOptions.maxStatelessResetsPerHost`
2494+
* Type: {number}
2495+
***Default:**`200`
24692496

2470-
<!-- YAML
2471-
added: v23.8.0
2472-
-->
2497+
The maximum burst of retry packets allowed before rate limiting takes effect.
24732498

2474-
* Type: {bigint|number}
2499+
#### `endpointOptions.statelessResetRate`
2500+
2501+
* Type: {number}
2502+
***Default:**`100`
2503+
2504+
The maximum number of stateless reset packets the endpoint will send per second.
2505+
2506+
#### `endpointOptions.statelessResetBurst`
2507+
2508+
* Type: {number}
2509+
***Default:**`200`
2510+
2511+
The maximum burst of stateless reset packets allowed before rate limiting
2512+
takes effect.
2513+
2514+
#### `endpointOptions.versionNegotiationRate`
2515+
2516+
* Type: {number}
2517+
***Default:**`100`
2518+
2519+
The maximum number of version negotiation packets the endpoint will send per
2520+
second.
2521+
2522+
#### `endpointOptions.versionNegotiationBurst`
2523+
2524+
* Type: {number}
2525+
***Default:**`200`
2526+
2527+
The maximum burst of version negotiation packets allowed before rate limiting
2528+
takes effect.
2529+
2530+
#### `endpointOptions.immediateCloseRate`
2531+
2532+
* Type: {number}
2533+
***Default:**`100`
2534+
2535+
The maximum number of immediate connection close packets the endpoint will
2536+
send per second.
2537+
2538+
#### `endpointOptions.immediateCloseBurst`
2539+
2540+
* Type: {number}
2541+
***Default:**`200`
24752542

2476-
Specifies the maximum number of stateless resets that are allowed per remote peer address.
2543+
The maximum burst of immediate connection close packets allowed before rate
2544+
limiting takes effect.
24772545

24782546
#### `endpointOptions.retryTokenExpiration`
24792547

‎lib/internal/quic/quic.js‎

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -307,8 +307,14 @@ const endpointRegistry = new SafeSet();
307307
* @property {boolean} [reusePort] Enable SO_REUSEPORT for multi-process load balancing
308308
* @property {bigint|number} [maxConnectionsPerHost] The maximum number of connections per host
309309
* @property {bigint|number} [maxConnectionsTotal] The maximum number of total connections
310-
* @property {bigint|number} [maxRetries] The maximum number of retries
311-
* @property {bigint|number} [maxStatelessResetsPerHost] The maximum number of stateless resets per host
310+
* @property {number} [retryRate] Global rate limit for retry packets (per second)
311+
* @property {number} [retryBurst] Burst capacity for retry rate limiter
312+
* @property {number} [statelessResetRate] Global rate limit for stateless reset packets (per second)
313+
* @property {number} [statelessResetBurst] Burst capacity for stateless reset rate limiter
314+
* @property {number} [versionNegotiationRate] Global rate limit for version negotiation packets (per second)
315+
* @property {number} [versionNegotiationBurst] Burst capacity for version negotiation rate limiter
316+
* @property {number} [immediateCloseRate] Global rate limit for immediate close packets (per second)
317+
* @property {number} [immediateCloseBurst] Burst capacity for immediate close rate limiter
312318
* @property {ArrayBufferView} [resetTokenSecret] The reset token secret
313319
* @property {bigint|number} [retryTokenExpiration] The retry token expiration
314320
* @property {number} [rxDiagnosticLoss] The receive diagnostic loss probability (range 0.0-1.0)
@@ -3997,10 +4003,16 @@ class QuicEndpoint {
39974003
tokenExpiration,
39984004
maxConnectionsPerHost =100,
39994005
maxConnectionsTotal =10_000,
4000-
maxStatelessResetsPerHost,
40014006
disableStatelessReset,
40024007
addressLRUSize,
4003-
maxRetries,
4008+
retryRate,
4009+
retryBurst,
4010+
statelessResetRate,
4011+
statelessResetBurst,
4012+
versionNegotiationRate,
4013+
versionNegotiationBurst,
4014+
immediateCloseRate,
4015+
immediateCloseBurst,
40044016
rxDiagnosticLoss,
40054017
txDiagnosticLoss,
40064018
udpReceiveBufferSize,
@@ -4034,10 +4046,16 @@ class QuicEndpoint {
40344046
// Connection limits are set on the state buffer, not passed to C++.
40354047
maxConnectionsPerHost,
40364048
maxConnectionsTotal,
4037-
maxStatelessResetsPerHost,
40384049
disableStatelessReset,
40394050
addressLRUSize,
4040-
maxRetries,
4051+
retryRate,
4052+
retryBurst,
4053+
statelessResetRate,
4054+
statelessResetBurst,
4055+
versionNegotiationRate,
4056+
versionNegotiationBurst,
4057+
immediateCloseRate,
4058+
immediateCloseBurst,
40414059
rxDiagnosticLoss,
40424060
txDiagnosticLoss,
40434061
udpReceiveBufferSize,

‎lib/internal/quic/stats.js‎

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,13 @@ const {
6060
IDX_STATS_ENDPOINT_CLIENT_SESSIONS,
6161
IDX_STATS_ENDPOINT_SERVER_BUSY_COUNT,
6262
IDX_STATS_ENDPOINT_RETRY_COUNT,
63+
IDX_STATS_ENDPOINT_RETRY_RATE_LIMITED,
6364
IDX_STATS_ENDPOINT_VERSION_NEGOTIATION_COUNT,
65+
IDX_STATS_ENDPOINT_VERSION_NEGOTIATION_RATE_LIMITED,
6466
IDX_STATS_ENDPOINT_STATELESS_RESET_COUNT,
67+
IDX_STATS_ENDPOINT_STATELESS_RESET_RATE_LIMITED,
6568
IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_COUNT,
69+
IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_RATE_LIMITED,
6670

6771
IDX_STATS_SESSION_CREATED_AT,
6872
IDX_STATS_SESSION_DESTROYED_AT,
@@ -123,9 +127,13 @@ assert(IDX_STATS_ENDPOINT_SERVER_SESSIONS !== undefined);
123127
assert(IDX_STATS_ENDPOINT_CLIENT_SESSIONS!==undefined);
124128
assert(IDX_STATS_ENDPOINT_SERVER_BUSY_COUNT!==undefined);
125129
assert(IDX_STATS_ENDPOINT_RETRY_COUNT!==undefined);
130+
assert(IDX_STATS_ENDPOINT_RETRY_RATE_LIMITED!==undefined);
126131
assert(IDX_STATS_ENDPOINT_VERSION_NEGOTIATION_COUNT!==undefined);
132+
assert(IDX_STATS_ENDPOINT_VERSION_NEGOTIATION_RATE_LIMITED!==undefined);
127133
assert(IDX_STATS_ENDPOINT_STATELESS_RESET_COUNT!==undefined);
134+
assert(IDX_STATS_ENDPOINT_STATELESS_RESET_RATE_LIMITED!==undefined);
128135
assert(IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_COUNT!==undefined);
136+
assert(IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_RATE_LIMITED!==undefined);
129137
assert(IDX_STATS_SESSION_CREATED_AT!==undefined);
130138
assert(IDX_STATS_SESSION_DESTROYED_AT!==undefined);
131139
assert(IDX_STATS_SESSION_CLOSING_AT!==undefined);
@@ -280,24 +288,48 @@ class QuicEndpointStats {
280288
returnthis.#handle[IDX_STATS_ENDPOINT_RETRY_COUNT];
281289
}
282290

291+
/** @type {bigint} */
292+
getretryRateLimited(){
293+
assertIsQuicEndpointStats(this);
294+
returnthis.#handle[IDX_STATS_ENDPOINT_RETRY_RATE_LIMITED];
295+
}
296+
283297
/** @type {bigint} */
284298
getversionNegotiationCount(){
285299
assertIsQuicEndpointStats(this);
286300
returnthis.#handle[IDX_STATS_ENDPOINT_VERSION_NEGOTIATION_COUNT];
287301
}
288302

303+
/** @type {bigint} */
304+
getversionNegotiationRateLimited(){
305+
assertIsQuicEndpointStats(this);
306+
returnthis.#handle[IDX_STATS_ENDPOINT_VERSION_NEGOTIATION_RATE_LIMITED];
307+
}
308+
289309
/** @type {bigint} */
290310
getstatelessResetCount(){
291311
assertIsQuicEndpointStats(this);
292312
returnthis.#handle[IDX_STATS_ENDPOINT_STATELESS_RESET_COUNT];
293313
}
294314

315+
/** @type {bigint} */
316+
getstatelessResetRateLimited(){
317+
assertIsQuicEndpointStats(this);
318+
returnthis.#handle[IDX_STATS_ENDPOINT_STATELESS_RESET_RATE_LIMITED];
319+
}
320+
295321
/** @type {bigint} */
296322
getimmediateCloseCount(){
297323
assertIsQuicEndpointStats(this);
298324
returnthis.#handle[IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_COUNT];
299325
}
300326

327+
/** @type {bigint} */
328+
getimmediateCloseRateLimited(){
329+
assertIsQuicEndpointStats(this);
330+
returnthis.#handle[IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_RATE_LIMITED];
331+
}
332+
301333
toString(){
302334
returnJSONStringify(this.toJSON());
303335
}
@@ -315,9 +347,13 @@ class QuicEndpointStats {
315347
clientSessions,
316348
serverBusyCount,
317349
retryCount,
350+
retryRateLimited,
318351
versionNegotiationCount,
352+
versionNegotiationRateLimited,
319353
statelessResetCount,
354+
statelessResetRateLimited,
320355
immediateCloseCount,
356+
immediateCloseRateLimited,
321357
}=this;
322358
return{
323359
__proto__: null,
@@ -334,9 +370,13 @@ class QuicEndpointStats {
334370
clientSessions: `${clientSessions}`,
335371
serverBusyCount: `${serverBusyCount}`,
336372
retryCount: `${retryCount}`,
373+
retryRateLimited: `${retryRateLimited}`,
337374
versionNegotiationCount: `${versionNegotiationCount}`,
375+
versionNegotiationRateLimited: `${versionNegotiationRateLimited}`,
338376
statelessResetCount: `${statelessResetCount}`,
377+
statelessResetRateLimited: `${statelessResetRateLimited}`,
339378
immediateCloseCount: `${immediateCloseCount}`,
379+
immediateCloseRateLimited: `${immediateCloseRateLimited}`,
340380
};
341381
}
342382

@@ -363,9 +403,13 @@ class QuicEndpointStats {
363403
clientSessions,
364404
serverBusyCount,
365405
retryCount,
406+
retryRateLimited,
366407
versionNegotiationCount,
408+
versionNegotiationRateLimited,
367409
statelessResetCount,
410+
statelessResetRateLimited,
368411
immediateCloseCount,
412+
immediateCloseRateLimited,
369413
}=this;
370414

371415
return`QuicEndpointStats ${inspect({
@@ -380,9 +424,13 @@ class QuicEndpointStats {
380424
clientSessions,
381425
serverBusyCount,
382426
retryCount,
427+
retryRateLimited,
383428
versionNegotiationCount,
429+
versionNegotiationRateLimited,
384430
statelessResetCount,
431+
statelessResetRateLimited,
385432
immediateCloseCount,
433+
immediateCloseRateLimited,
386434
},opts)}`;
387435
}
388436

‎node.gyp‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -449,6 +449,7 @@
449449
'test/cctest/test_quic_cid.cc',
450450
'test/cctest/test_quic_error.cc',
451451
'test/cctest/test_quic_preferredaddress.cc',
452+
'test/cctest/test_quic_tokenbucket.cc',
452453
'test/cctest/test_quic_tokens.cc',
453454
],
454455
'node_cctest_inspector_sources': [

‎src/quic/bindingdata.h‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,8 +117,14 @@ class SessionManager;
117117
V(idle_timeout, "idleTimeout") \
118118
V(max_idle_timeout, "maxIdleTimeout") \
119119
V(max_payload_size, "maxPayloadSize") \
120-
V(max_retries, "maxRetries") \
121-
V(max_stateless_resets, "maxStatelessResetsPerHost") \
120+
V(retry_rate, "retryRate") \
121+
V(retry_burst, "retryBurst") \
122+
V(stateless_reset_rate, "statelessResetRate") \
123+
V(stateless_reset_burst, "statelessResetBurst") \
124+
V(version_negotiation_rate, "versionNegotiationRate") \
125+
V(version_negotiation_burst, "versionNegotiationBurst") \
126+
V(immediate_close_rate, "immediateCloseRate") \
127+
V(immediate_close_burst, "immediateCloseBurst") \
122128
V(max_stream_window, "maxStreamWindow") \
123129
V(max_window, "maxWindow") \
124130
V(min_version, "minVersion") \

‎src/quic/defs.h‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,24 @@ constexpr auto kSocketAddressInfoTimeout = 60 * NGTCP2_SECONDS;
360360
constexprsize_tkMaxVectorCount = 16;
361361
constexpr stream_id kMaxStreamId = std::numeric_limits<stream_id>::max();
362362

363+
// A token bucket rate limiter using lazy refill. No timer needed — tokens
364+
// are computed on demand from the elapsed time since the last check.
365+
// Used to cap the total rate of stateless responses (retry, reset,
366+
// version negotiation, immediate close) regardless of source address,
367+
// preventing spoofed-source floods from bypassing per-host limits.
368+
structTokenBucketfinal {
369+
double rate; // tokens per second (refill rate)
370+
double burst; // maximum tokens (bucket capacity)
371+
double tokens; // current token count
372+
uint64_t last_ts; // last refill timestamp (nanoseconds, uv_hrtime)
373+
374+
TokenBucket(double rate, double burst);
375+
376+
// Try to consume one token. Refills based on elapsed time, then
377+
// attempts to consume. Returns true if the request is allowed.
378+
boolconsume();
379+
};
380+
363381
classDebugIndentScopefinal {
364382
public:
365383
inlineDebugIndentScope() { ++indent_; }

0 commit comments

Comments
 (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Commit 8107f1b

Browse files
jasnelladuh95
authored andcommitted
quic: refine rate limiting
Signed-off-by: James M Snell <jasnell@gmail.com> Assisted-by: Opencode:Opus 4.6 PR-URL: #63483 Backport-PR-URL: #64675 Reviewed-By: Stephen Belanger <admin@stephenbelanger.com> Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
1 parent 3f0d737 commit 8107f1b

12 files changed

Lines changed: 433 additions & 183 deletions

‎doc/api/quic.md‎

Lines changed: 84 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -726,31 +726,55 @@ added: v23.8.0
726726
added: v23.8.0
727727
-->
728728

729-
* Type: {bigint} The total number of QUIC retry attempts on this endpoint. Read only.
729+
* Type: {bigint} The total number of retry packets sent by this endpoint. Read only.
730+
731+
### `endpointStats.retryRateLimited`
732+
733+
* Type: {bigint} The total number of retry packets dropped by the global rate
734+
limiter. Read only. A non-zero value indicates the endpoint is under retry
735+
flood pressure.
730736

731737
### `endpointStats.versionNegotiationCount`
732738

733739
<!-- YAML
734740
added: v23.8.0
735741
-->
736742

737-
* Type: {bigint} The total number of sessions rejected due to QUIC version mismatch. Read only.
743+
* Type: {bigint} The total number of version negotiation packets sent by this
744+
endpoint. Read only.
745+
746+
### `endpointStats.versionNegotiationRateLimited`
747+
748+
* Type: {bigint} The total number of version negotiation packets dropped by
749+
the global rate limiter. Read only.
738750

739751
### `endpointStats.statelessResetCount`
740752

741753
<!-- YAML
742754
added: v23.8.0
743755
-->
744756

745-
* Type: {bigint} The total number of stateless resets handled by this endpoint. Read only.
757+
* Type: {bigint} The total number of stateless reset packets sent by this
758+
endpoint. Read only.
759+
760+
### `endpointStats.statelessResetRateLimited`
761+
762+
* Type: {bigint} The total number of stateless reset packets dropped by the
763+
global rate limiter. Read only.
746764

747765
### `endpointStats.immediateCloseCount`
748766

749767
<!-- YAML
750768
added: v23.8.0
751769
-->
752770

753-
* Type: {bigint} The total number of sessions that were closed before handshake completed. Read only.
771+
* Type: {bigint} The total number of immediate connection close packets sent
772+
by this endpoint. Read only.
773+
774+
### `endpointStats.immediateCloseRateLimited`
775+
776+
* Type: {bigint} The total number of immediate connection close packets
777+
dropped by the global rate limiter. Read only.
754778

755779
## Class: `QuicSession`
756780

@@ -2455,25 +2479,69 @@ addresses. When the limit is reached, new connections are refused with
24552479
This limit can also be changed dynamically after construction via
24562480
[`endpoint.maxConnectionsTotal`][].
24572481

2458-
#### `endpointOptions.maxRetries`
2482+
#### `endpointOptions.retryRate`
24592483

2460-
<!-- YAML
2461-
added: v23.8.0
2462-
-->
2484+
* Type: {number}
2485+
***Default:**`100`
24632486

2464-
* Type: {bigint|number}
2487+
The maximum number of QUIC retry packets the endpoint will send per second.
2488+
This is a global rate limit (not per-host) that caps the total server-wide
2489+
retry response rate, preventing spoofed-source floods from consuming unbounded
2490+
resources.
24652491

2466-
Specifies the maximum number of QUIC retry attempts allowed per remote peer address.
2492+
#### `endpointOptions.retryBurst`
24672493

2468-
#### `endpointOptions.maxStatelessResetsPerHost`
2494+
* Type: {number}
2495+
***Default:**`200`
24692496

2470-
<!-- YAML
2471-
added: v23.8.0
2472-
-->
2497+
The maximum burst of retry packets allowed before rate limiting takes effect.
24732498

2474-
* Type: {bigint|number}
2499+
#### `endpointOptions.statelessResetRate`
2500+
2501+
* Type: {number}
2502+
***Default:**`100`
2503+
2504+
The maximum number of stateless reset packets the endpoint will send per second.
2505+
2506+
#### `endpointOptions.statelessResetBurst`
2507+
2508+
* Type: {number}
2509+
***Default:**`200`
2510+
2511+
The maximum burst of stateless reset packets allowed before rate limiting
2512+
takes effect.
2513+
2514+
#### `endpointOptions.versionNegotiationRate`
2515+
2516+
* Type: {number}
2517+
***Default:**`100`
2518+
2519+
The maximum number of version negotiation packets the endpoint will send per
2520+
second.
2521+
2522+
#### `endpointOptions.versionNegotiationBurst`
2523+
2524+
* Type: {number}
2525+
***Default:**`200`
2526+
2527+
The maximum burst of version negotiation packets allowed before rate limiting
2528+
takes effect.
2529+
2530+
#### `endpointOptions.immediateCloseRate`
2531+
2532+
* Type: {number}
2533+
***Default:**`100`
2534+
2535+
The maximum number of immediate connection close packets the endpoint will
2536+
send per second.
2537+
2538+
#### `endpointOptions.immediateCloseBurst`
2539+
2540+
* Type: {number}
2541+
***Default:**`200`
24752542

2476-
Specifies the maximum number of stateless resets that are allowed per remote peer address.
2543+
The maximum burst of immediate connection close packets allowed before rate
2544+
limiting takes effect.
24772545

24782546
#### `endpointOptions.retryTokenExpiration`
24792547

‎lib/internal/quic/quic.js‎

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -307,8 +307,14 @@ const endpointRegistry = new SafeSet();
307307
* @property {boolean} [reusePort] Enable SO_REUSEPORT for multi-process load balancing
308308
* @property {bigint|number} [maxConnectionsPerHost] The maximum number of connections per host
309309
* @property {bigint|number} [maxConnectionsTotal] The maximum number of total connections
310-
* @property {bigint|number} [maxRetries] The maximum number of retries
311-
* @property {bigint|number} [maxStatelessResetsPerHost] The maximum number of stateless resets per host
310+
* @property {number} [retryRate] Global rate limit for retry packets (per second)
311+
* @property {number} [retryBurst] Burst capacity for retry rate limiter
312+
* @property {number} [statelessResetRate] Global rate limit for stateless reset packets (per second)
313+
* @property {number} [statelessResetBurst] Burst capacity for stateless reset rate limiter
314+
* @property {number} [versionNegotiationRate] Global rate limit for version negotiation packets (per second)
315+
* @property {number} [versionNegotiationBurst] Burst capacity for version negotiation rate limiter
316+
* @property {number} [immediateCloseRate] Global rate limit for immediate close packets (per second)
317+
* @property {number} [immediateCloseBurst] Burst capacity for immediate close rate limiter
312318
* @property {ArrayBufferView} [resetTokenSecret] The reset token secret
313319
* @property {bigint|number} [retryTokenExpiration] The retry token expiration
314320
* @property {number} [rxDiagnosticLoss] The receive diagnostic loss probability (range 0.0-1.0)
@@ -3997,10 +4003,16 @@ class QuicEndpoint {
39974003
tokenExpiration,
39984004
maxConnectionsPerHost =100,
39994005
maxConnectionsTotal =10_000,
4000-
maxStatelessResetsPerHost,
40014006
disableStatelessReset,
40024007
addressLRUSize,
4003-
maxRetries,
4008+
retryRate,
4009+
retryBurst,
4010+
statelessResetRate,
4011+
statelessResetBurst,
4012+
versionNegotiationRate,
4013+
versionNegotiationBurst,
4014+
immediateCloseRate,
4015+
immediateCloseBurst,
40044016
rxDiagnosticLoss,
40054017
txDiagnosticLoss,
40064018
udpReceiveBufferSize,
@@ -4034,10 +4046,16 @@ class QuicEndpoint {
40344046
// Connection limits are set on the state buffer, not passed to C++.
40354047
maxConnectionsPerHost,
40364048
maxConnectionsTotal,
4037-
maxStatelessResetsPerHost,
40384049
disableStatelessReset,
40394050
addressLRUSize,
4040-
maxRetries,
4051+
retryRate,
4052+
retryBurst,
4053+
statelessResetRate,
4054+
statelessResetBurst,
4055+
versionNegotiationRate,
4056+
versionNegotiationBurst,
4057+
immediateCloseRate,
4058+
immediateCloseBurst,
40414059
rxDiagnosticLoss,
40424060
txDiagnosticLoss,
40434061
udpReceiveBufferSize,

‎lib/internal/quic/stats.js‎

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,13 @@ const {
6060
IDX_STATS_ENDPOINT_CLIENT_SESSIONS,
6161
IDX_STATS_ENDPOINT_SERVER_BUSY_COUNT,
6262
IDX_STATS_ENDPOINT_RETRY_COUNT,
63+
IDX_STATS_ENDPOINT_RETRY_RATE_LIMITED,
6364
IDX_STATS_ENDPOINT_VERSION_NEGOTIATION_COUNT,
65+
IDX_STATS_ENDPOINT_VERSION_NEGOTIATION_RATE_LIMITED,
6466
IDX_STATS_ENDPOINT_STATELESS_RESET_COUNT,
67+
IDX_STATS_ENDPOINT_STATELESS_RESET_RATE_LIMITED,
6568
IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_COUNT,
69+
IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_RATE_LIMITED,
6670

6771
IDX_STATS_SESSION_CREATED_AT,
6872
IDX_STATS_SESSION_DESTROYED_AT,
@@ -123,9 +127,13 @@ assert(IDX_STATS_ENDPOINT_SERVER_SESSIONS !== undefined);
123127
assert(IDX_STATS_ENDPOINT_CLIENT_SESSIONS!==undefined);
124128
assert(IDX_STATS_ENDPOINT_SERVER_BUSY_COUNT!==undefined);
125129
assert(IDX_STATS_ENDPOINT_RETRY_COUNT!==undefined);
130+
assert(IDX_STATS_ENDPOINT_RETRY_RATE_LIMITED!==undefined);
126131
assert(IDX_STATS_ENDPOINT_VERSION_NEGOTIATION_COUNT!==undefined);
132+
assert(IDX_STATS_ENDPOINT_VERSION_NEGOTIATION_RATE_LIMITED!==undefined);
127133
assert(IDX_STATS_ENDPOINT_STATELESS_RESET_COUNT!==undefined);
134+
assert(IDX_STATS_ENDPOINT_STATELESS_RESET_RATE_LIMITED!==undefined);
128135
assert(IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_COUNT!==undefined);
136+
assert(IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_RATE_LIMITED!==undefined);
129137
assert(IDX_STATS_SESSION_CREATED_AT!==undefined);
130138
assert(IDX_STATS_SESSION_DESTROYED_AT!==undefined);
131139
assert(IDX_STATS_SESSION_CLOSING_AT!==undefined);
@@ -280,24 +288,48 @@ class QuicEndpointStats {
280288
returnthis.#handle[IDX_STATS_ENDPOINT_RETRY_COUNT];
281289
}
282290

291+
/** @type {bigint} */
292+
getretryRateLimited(){
293+
assertIsQuicEndpointStats(this);
294+
returnthis.#handle[IDX_STATS_ENDPOINT_RETRY_RATE_LIMITED];
295+
}
296+
283297
/** @type {bigint} */
284298
getversionNegotiationCount(){
285299
assertIsQuicEndpointStats(this);
286300
returnthis.#handle[IDX_STATS_ENDPOINT_VERSION_NEGOTIATION_COUNT];
287301
}
288302

303+
/** @type {bigint} */
304+
getversionNegotiationRateLimited(){
305+
assertIsQuicEndpointStats(this);
306+
returnthis.#handle[IDX_STATS_ENDPOINT_VERSION_NEGOTIATION_RATE_LIMITED];
307+
}
308+
289309
/** @type {bigint} */
290310
getstatelessResetCount(){
291311
assertIsQuicEndpointStats(this);
292312
returnthis.#handle[IDX_STATS_ENDPOINT_STATELESS_RESET_COUNT];
293313
}
294314

315+
/** @type {bigint} */
316+
getstatelessResetRateLimited(){
317+
assertIsQuicEndpointStats(this);
318+
returnthis.#handle[IDX_STATS_ENDPOINT_STATELESS_RESET_RATE_LIMITED];
319+
}
320+
295321
/** @type {bigint} */
296322
getimmediateCloseCount(){
297323
assertIsQuicEndpointStats(this);
298324
returnthis.#handle[IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_COUNT];
299325
}
300326

327+
/** @type {bigint} */
328+
getimmediateCloseRateLimited(){
329+
assertIsQuicEndpointStats(this);
330+
returnthis.#handle[IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_RATE_LIMITED];
331+
}
332+
301333
toString(){
302334
returnJSONStringify(this.toJSON());
303335
}
@@ -315,9 +347,13 @@ class QuicEndpointStats {
315347
clientSessions,
316348
serverBusyCount,
317349
retryCount,
350+
retryRateLimited,
318351
versionNegotiationCount,
352+
versionNegotiationRateLimited,
319353
statelessResetCount,
354+
statelessResetRateLimited,
320355
immediateCloseCount,
356+
immediateCloseRateLimited,
321357
}=this;
322358
return{
323359
__proto__: null,
@@ -334,9 +370,13 @@ class QuicEndpointStats {
334370
clientSessions: `${clientSessions}`,
335371
serverBusyCount: `${serverBusyCount}`,
336372
retryCount: `${retryCount}`,
373+
retryRateLimited: `${retryRateLimited}`,
337374
versionNegotiationCount: `${versionNegotiationCount}`,
375+
versionNegotiationRateLimited: `${versionNegotiationRateLimited}`,
338376
statelessResetCount: `${statelessResetCount}`,
377+
statelessResetRateLimited: `${statelessResetRateLimited}`,
339378
immediateCloseCount: `${immediateCloseCount}`,
379+
immediateCloseRateLimited: `${immediateCloseRateLimited}`,
340380
};
341381
}
342382

@@ -363,9 +403,13 @@ class QuicEndpointStats {
363403
clientSessions,
364404
serverBusyCount,
365405
retryCount,
406+
retryRateLimited,
366407
versionNegotiationCount,
408+
versionNegotiationRateLimited,
367409
statelessResetCount,
410+
statelessResetRateLimited,
368411
immediateCloseCount,
412+
immediateCloseRateLimited,
369413
}=this;
370414

371415
return`QuicEndpointStats ${inspect({
@@ -380,9 +424,13 @@ class QuicEndpointStats {
380424
clientSessions,
381425
serverBusyCount,
382426
retryCount,
427+
retryRateLimited,
383428
versionNegotiationCount,
429+
versionNegotiationRateLimited,
384430
statelessResetCount,
431+
statelessResetRateLimited,
385432
immediateCloseCount,
433+
immediateCloseRateLimited,
386434
},opts)}`;
387435
}
388436

‎node.gyp‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -449,6 +449,7 @@
449449
'test/cctest/test_quic_cid.cc',
450450
'test/cctest/test_quic_error.cc',
451451
'test/cctest/test_quic_preferredaddress.cc',
452+
'test/cctest/test_quic_tokenbucket.cc',
452453
'test/cctest/test_quic_tokens.cc',
453454
],
454455
'node_cctest_inspector_sources': [

‎src/quic/bindingdata.h‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,8 +117,14 @@ class SessionManager;
117117
V(idle_timeout, "idleTimeout") \
118118
V(max_idle_timeout, "maxIdleTimeout") \
119119
V(max_payload_size, "maxPayloadSize") \
120-
V(max_retries, "maxRetries") \
121-
V(max_stateless_resets, "maxStatelessResetsPerHost") \
120+
V(retry_rate, "retryRate") \
121+
V(retry_burst, "retryBurst") \
122+
V(stateless_reset_rate, "statelessResetRate") \
123+
V(stateless_reset_burst, "statelessResetBurst") \
124+
V(version_negotiation_rate, "versionNegotiationRate") \
125+
V(version_negotiation_burst, "versionNegotiationBurst") \
126+
V(immediate_close_rate, "immediateCloseRate") \
127+
V(immediate_close_burst, "immediateCloseBurst") \
122128
V(max_stream_window, "maxStreamWindow") \
123129
V(max_window, "maxWindow") \
124130
V(min_version, "minVersion") \

‎src/quic/defs.h‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,24 @@ constexpr auto kSocketAddressInfoTimeout = 60 * NGTCP2_SECONDS;
360360
constexprsize_tkMaxVectorCount = 16;
361361
constexpr stream_id kMaxStreamId = std::numeric_limits<stream_id>::max();
362362

363+
// A token bucket rate limiter using lazy refill. No timer needed — tokens
364+
// are computed on demand from the elapsed time since the last check.
365+
// Used to cap the total rate of stateless responses (retry, reset,
366+
// version negotiation, immediate close) regardless of source address,
367+
// preventing spoofed-source floods from bypassing per-host limits.
368+
structTokenBucketfinal {
369+
double rate; // tokens per second (refill rate)
370+
double burst; // maximum tokens (bucket capacity)
371+
double tokens; // current token count
372+
uint64_t last_ts; // last refill timestamp (nanoseconds, uv_hrtime)
373+
374+
TokenBucket(double rate, double burst);
375+
376+
// Try to consume one token. Refills based on elapsed time, then
377+
// attempts to consume. Returns true if the request is allowed.
378+
boolconsume();
379+
};
380+
363381
classDebugIndentScopefinal {
364382
public:
365383
inlineDebugIndentScope() { ++indent_; }

0 commit comments

Comments
 (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Commit 8107f1b

Browse files
jasnelladuh95
authored andcommitted
quic: refine rate limiting
Signed-off-by: James M Snell <jasnell@gmail.com> Assisted-by: Opencode:Opus 4.6 PR-URL: #63483 Backport-PR-URL: #64675 Reviewed-By: Stephen Belanger <admin@stephenbelanger.com> Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
1 parent 3f0d737 commit 8107f1b

12 files changed

Lines changed: 433 additions & 183 deletions

‎doc/api/quic.md‎

Lines changed: 84 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -726,31 +726,55 @@ added: v23.8.0
726726
added: v23.8.0
727727
-->
728728

729-
* Type: {bigint} The total number of QUIC retry attempts on this endpoint. Read only.
729+
* Type: {bigint} The total number of retry packets sent by this endpoint. Read only.
730+
731+
### `endpointStats.retryRateLimited`
732+
733+
* Type: {bigint} The total number of retry packets dropped by the global rate
734+
limiter. Read only. A non-zero value indicates the endpoint is under retry
735+
flood pressure.
730736

731737
### `endpointStats.versionNegotiationCount`
732738

733739
<!-- YAML
734740
added: v23.8.0
735741
-->
736742

737-
* Type: {bigint} The total number of sessions rejected due to QUIC version mismatch. Read only.
743+
* Type: {bigint} The total number of version negotiation packets sent by this
744+
endpoint. Read only.
745+
746+
### `endpointStats.versionNegotiationRateLimited`
747+
748+
* Type: {bigint} The total number of version negotiation packets dropped by
749+
the global rate limiter. Read only.
738750

739751
### `endpointStats.statelessResetCount`
740752

741753
<!-- YAML
742754
added: v23.8.0
743755
-->
744756

745-
* Type: {bigint} The total number of stateless resets handled by this endpoint. Read only.
757+
* Type: {bigint} The total number of stateless reset packets sent by this
758+
endpoint. Read only.
759+
760+
### `endpointStats.statelessResetRateLimited`
761+
762+
* Type: {bigint} The total number of stateless reset packets dropped by the
763+
global rate limiter. Read only.
746764

747765
### `endpointStats.immediateCloseCount`
748766

749767
<!-- YAML
750768
added: v23.8.0
751769
-->
752770

753-
* Type: {bigint} The total number of sessions that were closed before handshake completed. Read only.
771+
* Type: {bigint} The total number of immediate connection close packets sent
772+
by this endpoint. Read only.
773+
774+
### `endpointStats.immediateCloseRateLimited`
775+
776+
* Type: {bigint} The total number of immediate connection close packets
777+
dropped by the global rate limiter. Read only.
754778

755779
## Class: `QuicSession`
756780

@@ -2455,25 +2479,69 @@ addresses. When the limit is reached, new connections are refused with
24552479
This limit can also be changed dynamically after construction via
24562480
[`endpoint.maxConnectionsTotal`][].
24572481

2458-
#### `endpointOptions.maxRetries`
2482+
#### `endpointOptions.retryRate`
24592483

2460-
<!-- YAML
2461-
added: v23.8.0
2462-
-->
2484+
* Type: {number}
2485+
***Default:**`100`
24632486

2464-
* Type: {bigint|number}
2487+
The maximum number of QUIC retry packets the endpoint will send per second.
2488+
This is a global rate limit (not per-host) that caps the total server-wide
2489+
retry response rate, preventing spoofed-source floods from consuming unbounded
2490+
resources.
24652491

2466-
Specifies the maximum number of QUIC retry attempts allowed per remote peer address.
2492+
#### `endpointOptions.retryBurst`
24672493

2468-
#### `endpointOptions.maxStatelessResetsPerHost`
2494+
* Type: {number}
2495+
***Default:**`200`
24692496

2470-
<!-- YAML
2471-
added: v23.8.0
2472-
-->
2497+
The maximum burst of retry packets allowed before rate limiting takes effect.
24732498

2474-
* Type: {bigint|number}
2499+
#### `endpointOptions.statelessResetRate`
2500+
2501+
* Type: {number}
2502+
***Default:**`100`
2503+
2504+
The maximum number of stateless reset packets the endpoint will send per second.
2505+
2506+
#### `endpointOptions.statelessResetBurst`
2507+
2508+
* Type: {number}
2509+
***Default:**`200`
2510+
2511+
The maximum burst of stateless reset packets allowed before rate limiting
2512+
takes effect.
2513+
2514+
#### `endpointOptions.versionNegotiationRate`
2515+
2516+
* Type: {number}
2517+
***Default:**`100`
2518+
2519+
The maximum number of version negotiation packets the endpoint will send per
2520+
second.
2521+
2522+
#### `endpointOptions.versionNegotiationBurst`
2523+
2524+
* Type: {number}
2525+
***Default:**`200`
2526+
2527+
The maximum burst of version negotiation packets allowed before rate limiting
2528+
takes effect.
2529+
2530+
#### `endpointOptions.immediateCloseRate`
2531+
2532+
* Type: {number}
2533+
***Default:**`100`
2534+
2535+
The maximum number of immediate connection close packets the endpoint will
2536+
send per second.
2537+
2538+
#### `endpointOptions.immediateCloseBurst`
2539+
2540+
* Type: {number}
2541+
***Default:**`200`
24752542

2476-
Specifies the maximum number of stateless resets that are allowed per remote peer address.
2543+
The maximum burst of immediate connection close packets allowed before rate
2544+
limiting takes effect.
24772545

24782546
#### `endpointOptions.retryTokenExpiration`
24792547

‎lib/internal/quic/quic.js‎

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -307,8 +307,14 @@ const endpointRegistry = new SafeSet();
307307
* @property {boolean} [reusePort] Enable SO_REUSEPORT for multi-process load balancing
308308
* @property {bigint|number} [maxConnectionsPerHost] The maximum number of connections per host
309309
* @property {bigint|number} [maxConnectionsTotal] The maximum number of total connections
310-
* @property {bigint|number} [maxRetries] The maximum number of retries
311-
* @property {bigint|number} [maxStatelessResetsPerHost] The maximum number of stateless resets per host
310+
* @property {number} [retryRate] Global rate limit for retry packets (per second)
311+
* @property {number} [retryBurst] Burst capacity for retry rate limiter
312+
* @property {number} [statelessResetRate] Global rate limit for stateless reset packets (per second)
313+
* @property {number} [statelessResetBurst] Burst capacity for stateless reset rate limiter
314+
* @property {number} [versionNegotiationRate] Global rate limit for version negotiation packets (per second)
315+
* @property {number} [versionNegotiationBurst] Burst capacity for version negotiation rate limiter
316+
* @property {number} [immediateCloseRate] Global rate limit for immediate close packets (per second)
317+
* @property {number} [immediateCloseBurst] Burst capacity for immediate close rate limiter
312318
* @property {ArrayBufferView} [resetTokenSecret] The reset token secret
313319
* @property {bigint|number} [retryTokenExpiration] The retry token expiration
314320
* @property {number} [rxDiagnosticLoss] The receive diagnostic loss probability (range 0.0-1.0)
@@ -3997,10 +4003,16 @@ class QuicEndpoint {
39974003
tokenExpiration,
39984004
maxConnectionsPerHost =100,
39994005
maxConnectionsTotal =10_000,
4000-
maxStatelessResetsPerHost,
40014006
disableStatelessReset,
40024007
addressLRUSize,
4003-
maxRetries,
4008+
retryRate,
4009+
retryBurst,
4010+
statelessResetRate,
4011+
statelessResetBurst,
4012+
versionNegotiationRate,
4013+
versionNegotiationBurst,
4014+
immediateCloseRate,
4015+
immediateCloseBurst,
40044016
rxDiagnosticLoss,
40054017
txDiagnosticLoss,
40064018
udpReceiveBufferSize,
@@ -4034,10 +4046,16 @@ class QuicEndpoint {
40344046
// Connection limits are set on the state buffer, not passed to C++.
40354047
maxConnectionsPerHost,
40364048
maxConnectionsTotal,
4037-
maxStatelessResetsPerHost,
40384049
disableStatelessReset,
40394050
addressLRUSize,
4040-
maxRetries,
4051+
retryRate,
4052+
retryBurst,
4053+
statelessResetRate,
4054+
statelessResetBurst,
4055+
versionNegotiationRate,
4056+
versionNegotiationBurst,
4057+
immediateCloseRate,
4058+
immediateCloseBurst,
40414059
rxDiagnosticLoss,
40424060
txDiagnosticLoss,
40434061
udpReceiveBufferSize,

‎lib/internal/quic/stats.js‎

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,13 @@ const {
6060
IDX_STATS_ENDPOINT_CLIENT_SESSIONS,
6161
IDX_STATS_ENDPOINT_SERVER_BUSY_COUNT,
6262
IDX_STATS_ENDPOINT_RETRY_COUNT,
63+
IDX_STATS_ENDPOINT_RETRY_RATE_LIMITED,
6364
IDX_STATS_ENDPOINT_VERSION_NEGOTIATION_COUNT,
65+
IDX_STATS_ENDPOINT_VERSION_NEGOTIATION_RATE_LIMITED,
6466
IDX_STATS_ENDPOINT_STATELESS_RESET_COUNT,
67+
IDX_STATS_ENDPOINT_STATELESS_RESET_RATE_LIMITED,
6568
IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_COUNT,
69+
IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_RATE_LIMITED,
6670

6771
IDX_STATS_SESSION_CREATED_AT,
6872
IDX_STATS_SESSION_DESTROYED_AT,
@@ -123,9 +127,13 @@ assert(IDX_STATS_ENDPOINT_SERVER_SESSIONS !== undefined);
123127
assert(IDX_STATS_ENDPOINT_CLIENT_SESSIONS!==undefined);
124128
assert(IDX_STATS_ENDPOINT_SERVER_BUSY_COUNT!==undefined);
125129
assert(IDX_STATS_ENDPOINT_RETRY_COUNT!==undefined);
130+
assert(IDX_STATS_ENDPOINT_RETRY_RATE_LIMITED!==undefined);
126131
assert(IDX_STATS_ENDPOINT_VERSION_NEGOTIATION_COUNT!==undefined);
132+
assert(IDX_STATS_ENDPOINT_VERSION_NEGOTIATION_RATE_LIMITED!==undefined);
127133
assert(IDX_STATS_ENDPOINT_STATELESS_RESET_COUNT!==undefined);
134+
assert(IDX_STATS_ENDPOINT_STATELESS_RESET_RATE_LIMITED!==undefined);
128135
assert(IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_COUNT!==undefined);
136+
assert(IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_RATE_LIMITED!==undefined);
129137
assert(IDX_STATS_SESSION_CREATED_AT!==undefined);
130138
assert(IDX_STATS_SESSION_DESTROYED_AT!==undefined);
131139
assert(IDX_STATS_SESSION_CLOSING_AT!==undefined);
@@ -280,24 +288,48 @@ class QuicEndpointStats {
280288
returnthis.#handle[IDX_STATS_ENDPOINT_RETRY_COUNT];
281289
}
282290

291+
/** @type {bigint} */
292+
getretryRateLimited(){
293+
assertIsQuicEndpointStats(this);
294+
returnthis.#handle[IDX_STATS_ENDPOINT_RETRY_RATE_LIMITED];
295+
}
296+
283297
/** @type {bigint} */
284298
getversionNegotiationCount(){
285299
assertIsQuicEndpointStats(this);
286300
returnthis.#handle[IDX_STATS_ENDPOINT_VERSION_NEGOTIATION_COUNT];
287301
}
288302

303+
/** @type {bigint} */
304+
getversionNegotiationRateLimited(){
305+
assertIsQuicEndpointStats(this);
306+
returnthis.#handle[IDX_STATS_ENDPOINT_VERSION_NEGOTIATION_RATE_LIMITED];
307+
}
308+
289309
/** @type {bigint} */
290310
getstatelessResetCount(){
291311
assertIsQuicEndpointStats(this);
292312
returnthis.#handle[IDX_STATS_ENDPOINT_STATELESS_RESET_COUNT];
293313
}
294314

315+
/** @type {bigint} */
316+
getstatelessResetRateLimited(){
317+
assertIsQuicEndpointStats(this);
318+
returnthis.#handle[IDX_STATS_ENDPOINT_STATELESS_RESET_RATE_LIMITED];
319+
}
320+
295321
/** @type {bigint} */
296322
getimmediateCloseCount(){
297323
assertIsQuicEndpointStats(this);
298324
returnthis.#handle[IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_COUNT];
299325
}
300326

327+
/** @type {bigint} */
328+
getimmediateCloseRateLimited(){
329+
assertIsQuicEndpointStats(this);
330+
returnthis.#handle[IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_RATE_LIMITED];
331+
}
332+
301333
toString(){
302334
returnJSONStringify(this.toJSON());
303335
}
@@ -315,9 +347,13 @@ class QuicEndpointStats {
315347
clientSessions,
316348
serverBusyCount,
317349
retryCount,
350+
retryRateLimited,
318351
versionNegotiationCount,
352+
versionNegotiationRateLimited,
319353
statelessResetCount,
354+
statelessResetRateLimited,
320355
immediateCloseCount,
356+
immediateCloseRateLimited,
321357
}=this;
322358
return{
323359
__proto__: null,
@@ -334,9 +370,13 @@ class QuicEndpointStats {
334370
clientSessions: `${clientSessions}`,
335371
serverBusyCount: `${serverBusyCount}`,
336372
retryCount: `${retryCount}`,
373+
retryRateLimited: `${retryRateLimited}`,
337374
versionNegotiationCount: `${versionNegotiationCount}`,
375+
versionNegotiationRateLimited: `${versionNegotiationRateLimited}`,
338376
statelessResetCount: `${statelessResetCount}`,
377+
statelessResetRateLimited: `${statelessResetRateLimited}`,
339378
immediateCloseCount: `${immediateCloseCount}`,
379+
immediateCloseRateLimited: `${immediateCloseRateLimited}`,
340380
};
341381
}
342382

@@ -363,9 +403,13 @@ class QuicEndpointStats {
363403
clientSessions,
364404
serverBusyCount,
365405
retryCount,
406+
retryRateLimited,
366407
versionNegotiationCount,
408+
versionNegotiationRateLimited,
367409
statelessResetCount,
410+
statelessResetRateLimited,
368411
immediateCloseCount,
412+
immediateCloseRateLimited,
369413
}=this;
370414

371415
return`QuicEndpointStats ${inspect({
@@ -380,9 +424,13 @@ class QuicEndpointStats {
380424
clientSessions,
381425
serverBusyCount,
382426
retryCount,
427+
retryRateLimited,
383428
versionNegotiationCount,
429+
versionNegotiationRateLimited,
384430
statelessResetCount,
431+
statelessResetRateLimited,
385432
immediateCloseCount,
433+
immediateCloseRateLimited,
386434
},opts)}`;
387435
}
388436

‎node.gyp‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -449,6 +449,7 @@
449449
'test/cctest/test_quic_cid.cc',
450450
'test/cctest/test_quic_error.cc',
451451
'test/cctest/test_quic_preferredaddress.cc',
452+
'test/cctest/test_quic_tokenbucket.cc',
452453
'test/cctest/test_quic_tokens.cc',
453454
],
454455
'node_cctest_inspector_sources': [

‎src/quic/bindingdata.h‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,8 +117,14 @@ class SessionManager;
117117
V(idle_timeout, "idleTimeout") \
118118
V(max_idle_timeout, "maxIdleTimeout") \
119119
V(max_payload_size, "maxPayloadSize") \
120-
V(max_retries, "maxRetries") \
121-
V(max_stateless_resets, "maxStatelessResetsPerHost") \
120+
V(retry_rate, "retryRate") \
121+
V(retry_burst, "retryBurst") \
122+
V(stateless_reset_rate, "statelessResetRate") \
123+
V(stateless_reset_burst, "statelessResetBurst") \
124+
V(version_negotiation_rate, "versionNegotiationRate") \
125+
V(version_negotiation_burst, "versionNegotiationBurst") \
126+
V(immediate_close_rate, "immediateCloseRate") \
127+
V(immediate_close_burst, "immediateCloseBurst") \
122128
V(max_stream_window, "maxStreamWindow") \
123129
V(max_window, "maxWindow") \
124130
V(min_version, "minVersion") \

‎src/quic/defs.h‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,24 @@ constexpr auto kSocketAddressInfoTimeout = 60 * NGTCP2_SECONDS;
360360
constexprsize_tkMaxVectorCount = 16;
361361
constexpr stream_id kMaxStreamId = std::numeric_limits<stream_id>::max();
362362

363+
// A token bucket rate limiter using lazy refill. No timer needed — tokens
364+
// are computed on demand from the elapsed time since the last check.
365+
// Used to cap the total rate of stateless responses (retry, reset,
366+
// version negotiation, immediate close) regardless of source address,
367+
// preventing spoofed-source floods from bypassing per-host limits.
368+
structTokenBucketfinal {
369+
double rate; // tokens per second (refill rate)
370+
double burst; // maximum tokens (bucket capacity)
371+
double tokens; // current token count
372+
uint64_t last_ts; // last refill timestamp (nanoseconds, uv_hrtime)
373+
374+
TokenBucket(double rate, double burst);
375+
376+
// Try to consume one token. Refills based on elapsed time, then
377+
// attempts to consume. Returns true if the request is allowed.
378+
boolconsume();
379+
};
380+
363381
classDebugIndentScopefinal {
364382
public:
365383
inlineDebugIndentScope() { ++indent_; }

0 commit comments

Comments
 (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Commit 8107f1b

Browse files
jasnelladuh95
authored andcommitted
quic: refine rate limiting
Signed-off-by: James M Snell <jasnell@gmail.com> Assisted-by: Opencode:Opus 4.6 PR-URL: #63483 Backport-PR-URL: #64675 Reviewed-By: Stephen Belanger <admin@stephenbelanger.com> Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
1 parent 3f0d737 commit 8107f1b

12 files changed

Lines changed: 433 additions & 183 deletions

‎doc/api/quic.md‎

Lines changed: 84 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -726,31 +726,55 @@ added: v23.8.0
726726
added: v23.8.0
727727
-->
728728

729-
* Type: {bigint} The total number of QUIC retry attempts on this endpoint. Read only.
729+
* Type: {bigint} The total number of retry packets sent by this endpoint. Read only.
730+
731+
### `endpointStats.retryRateLimited`
732+
733+
* Type: {bigint} The total number of retry packets dropped by the global rate
734+
limiter. Read only. A non-zero value indicates the endpoint is under retry
735+
flood pressure.
730736

731737
### `endpointStats.versionNegotiationCount`
732738

733739
<!-- YAML
734740
added: v23.8.0
735741
-->
736742

737-
* Type: {bigint} The total number of sessions rejected due to QUIC version mismatch. Read only.
743+
* Type: {bigint} The total number of version negotiation packets sent by this
744+
endpoint. Read only.
745+
746+
### `endpointStats.versionNegotiationRateLimited`
747+
748+
* Type: {bigint} The total number of version negotiation packets dropped by
749+
the global rate limiter. Read only.
738750

739751
### `endpointStats.statelessResetCount`
740752

741753
<!-- YAML
742754
added: v23.8.0
743755
-->
744756

745-
* Type: {bigint} The total number of stateless resets handled by this endpoint. Read only.
757+
* Type: {bigint} The total number of stateless reset packets sent by this
758+
endpoint. Read only.
759+
760+
### `endpointStats.statelessResetRateLimited`
761+
762+
* Type: {bigint} The total number of stateless reset packets dropped by the
763+
global rate limiter. Read only.
746764

747765
### `endpointStats.immediateCloseCount`
748766

749767
<!-- YAML
750768
added: v23.8.0
751769
-->
752770

753-
* Type: {bigint} The total number of sessions that were closed before handshake completed. Read only.
771+
* Type: {bigint} The total number of immediate connection close packets sent
772+
by this endpoint. Read only.
773+
774+
### `endpointStats.immediateCloseRateLimited`
775+
776+
* Type: {bigint} The total number of immediate connection close packets
777+
dropped by the global rate limiter. Read only.
754778

755779
## Class: `QuicSession`
756780

@@ -2455,25 +2479,69 @@ addresses. When the limit is reached, new connections are refused with
24552479
This limit can also be changed dynamically after construction via
24562480
[`endpoint.maxConnectionsTotal`][].
24572481

2458-
#### `endpointOptions.maxRetries`
2482+
#### `endpointOptions.retryRate`
24592483

2460-
<!-- YAML
2461-
added: v23.8.0
2462-
-->
2484+
* Type: {number}
2485+
***Default:**`100`
24632486

2464-
* Type: {bigint|number}
2487+
The maximum number of QUIC retry packets the endpoint will send per second.
2488+
This is a global rate limit (not per-host) that caps the total server-wide
2489+
retry response rate, preventing spoofed-source floods from consuming unbounded
2490+
resources.
24652491

2466-
Specifies the maximum number of QUIC retry attempts allowed per remote peer address.
2492+
#### `endpointOptions.retryBurst`
24672493

2468-
#### `endpointOptions.maxStatelessResetsPerHost`
2494+
* Type: {number}
2495+
***Default:**`200`
24692496

2470-
<!-- YAML
2471-
added: v23.8.0
2472-
-->
2497+
The maximum burst of retry packets allowed before rate limiting takes effect.
24732498

2474-
* Type: {bigint|number}
2499+
#### `endpointOptions.statelessResetRate`
2500+
2501+
* Type: {number}
2502+
***Default:**`100`
2503+
2504+
The maximum number of stateless reset packets the endpoint will send per second.
2505+
2506+
#### `endpointOptions.statelessResetBurst`
2507+
2508+
* Type: {number}
2509+
***Default:**`200`
2510+
2511+
The maximum burst of stateless reset packets allowed before rate limiting
2512+
takes effect.
2513+
2514+
#### `endpointOptions.versionNegotiationRate`
2515+
2516+
* Type: {number}
2517+
***Default:**`100`
2518+
2519+
The maximum number of version negotiation packets the endpoint will send per
2520+
second.
2521+
2522+
#### `endpointOptions.versionNegotiationBurst`
2523+
2524+
* Type: {number}
2525+
***Default:**`200`
2526+
2527+
The maximum burst of version negotiation packets allowed before rate limiting
2528+
takes effect.
2529+
2530+
#### `endpointOptions.immediateCloseRate`
2531+
2532+
* Type: {number}
2533+
***Default:**`100`
2534+
2535+
The maximum number of immediate connection close packets the endpoint will
2536+
send per second.
2537+
2538+
#### `endpointOptions.immediateCloseBurst`
2539+
2540+
* Type: {number}
2541+
***Default:**`200`
24752542

2476-
Specifies the maximum number of stateless resets that are allowed per remote peer address.
2543+
The maximum burst of immediate connection close packets allowed before rate
2544+
limiting takes effect.
24772545

24782546
#### `endpointOptions.retryTokenExpiration`
24792547

‎lib/internal/quic/quic.js‎

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -307,8 +307,14 @@ const endpointRegistry = new SafeSet();
307307
* @property {boolean} [reusePort] Enable SO_REUSEPORT for multi-process load balancing
308308
* @property {bigint|number} [maxConnectionsPerHost] The maximum number of connections per host
309309
* @property {bigint|number} [maxConnectionsTotal] The maximum number of total connections
310-
* @property {bigint|number} [maxRetries] The maximum number of retries
311-
* @property {bigint|number} [maxStatelessResetsPerHost] The maximum number of stateless resets per host
310+
* @property {number} [retryRate] Global rate limit for retry packets (per second)
311+
* @property {number} [retryBurst] Burst capacity for retry rate limiter
312+
* @property {number} [statelessResetRate] Global rate limit for stateless reset packets (per second)
313+
* @property {number} [statelessResetBurst] Burst capacity for stateless reset rate limiter
314+
* @property {number} [versionNegotiationRate] Global rate limit for version negotiation packets (per second)
315+
* @property {number} [versionNegotiationBurst] Burst capacity for version negotiation rate limiter
316+
* @property {number} [immediateCloseRate] Global rate limit for immediate close packets (per second)
317+
* @property {number} [immediateCloseBurst] Burst capacity for immediate close rate limiter
312318
* @property {ArrayBufferView} [resetTokenSecret] The reset token secret
313319
* @property {bigint|number} [retryTokenExpiration] The retry token expiration
314320
* @property {number} [rxDiagnosticLoss] The receive diagnostic loss probability (range 0.0-1.0)
@@ -3997,10 +4003,16 @@ class QuicEndpoint {
39974003
tokenExpiration,
39984004
maxConnectionsPerHost =100,
39994005
maxConnectionsTotal =10_000,
4000-
maxStatelessResetsPerHost,
40014006
disableStatelessReset,
40024007
addressLRUSize,
4003-
maxRetries,
4008+
retryRate,
4009+
retryBurst,
4010+
statelessResetRate,
4011+
statelessResetBurst,
4012+
versionNegotiationRate,
4013+
versionNegotiationBurst,
4014+
immediateCloseRate,
4015+
immediateCloseBurst,
40044016
rxDiagnosticLoss,
40054017
txDiagnosticLoss,
40064018
udpReceiveBufferSize,
@@ -4034,10 +4046,16 @@ class QuicEndpoint {
40344046
// Connection limits are set on the state buffer, not passed to C++.
40354047
maxConnectionsPerHost,
40364048
maxConnectionsTotal,
4037-
maxStatelessResetsPerHost,
40384049
disableStatelessReset,
40394050
addressLRUSize,
4040-
maxRetries,
4051+
retryRate,
4052+
retryBurst,
4053+
statelessResetRate,
4054+
statelessResetBurst,
4055+
versionNegotiationRate,
4056+
versionNegotiationBurst,
4057+
immediateCloseRate,
4058+
immediateCloseBurst,
40414059
rxDiagnosticLoss,
40424060
txDiagnosticLoss,
40434061
udpReceiveBufferSize,

‎lib/internal/quic/stats.js‎

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,13 @@ const {
6060
IDX_STATS_ENDPOINT_CLIENT_SESSIONS,
6161
IDX_STATS_ENDPOINT_SERVER_BUSY_COUNT,
6262
IDX_STATS_ENDPOINT_RETRY_COUNT,
63+
IDX_STATS_ENDPOINT_RETRY_RATE_LIMITED,
6364
IDX_STATS_ENDPOINT_VERSION_NEGOTIATION_COUNT,
65+
IDX_STATS_ENDPOINT_VERSION_NEGOTIATION_RATE_LIMITED,
6466
IDX_STATS_ENDPOINT_STATELESS_RESET_COUNT,
67+
IDX_STATS_ENDPOINT_STATELESS_RESET_RATE_LIMITED,
6568
IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_COUNT,
69+
IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_RATE_LIMITED,
6670

6771
IDX_STATS_SESSION_CREATED_AT,
6872
IDX_STATS_SESSION_DESTROYED_AT,
@@ -123,9 +127,13 @@ assert(IDX_STATS_ENDPOINT_SERVER_SESSIONS !== undefined);
123127
assert(IDX_STATS_ENDPOINT_CLIENT_SESSIONS!==undefined);
124128
assert(IDX_STATS_ENDPOINT_SERVER_BUSY_COUNT!==undefined);
125129
assert(IDX_STATS_ENDPOINT_RETRY_COUNT!==undefined);
130+
assert(IDX_STATS_ENDPOINT_RETRY_RATE_LIMITED!==undefined);
126131
assert(IDX_STATS_ENDPOINT_VERSION_NEGOTIATION_COUNT!==undefined);
132+
assert(IDX_STATS_ENDPOINT_VERSION_NEGOTIATION_RATE_LIMITED!==undefined);
127133
assert(IDX_STATS_ENDPOINT_STATELESS_RESET_COUNT!==undefined);
134+
assert(IDX_STATS_ENDPOINT_STATELESS_RESET_RATE_LIMITED!==undefined);
128135
assert(IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_COUNT!==undefined);
136+
assert(IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_RATE_LIMITED!==undefined);
129137
assert(IDX_STATS_SESSION_CREATED_AT!==undefined);
130138
assert(IDX_STATS_SESSION_DESTROYED_AT!==undefined);
131139
assert(IDX_STATS_SESSION_CLOSING_AT!==undefined);
@@ -280,24 +288,48 @@ class QuicEndpointStats {
280288
returnthis.#handle[IDX_STATS_ENDPOINT_RETRY_COUNT];
281289
}
282290

291+
/** @type {bigint} */
292+
getretryRateLimited(){
293+
assertIsQuicEndpointStats(this);
294+
returnthis.#handle[IDX_STATS_ENDPOINT_RETRY_RATE_LIMITED];
295+
}
296+
283297
/** @type {bigint} */
284298
getversionNegotiationCount(){
285299
assertIsQuicEndpointStats(this);
286300
returnthis.#handle[IDX_STATS_ENDPOINT_VERSION_NEGOTIATION_COUNT];
287301
}
288302

303+
/** @type {bigint} */
304+
getversionNegotiationRateLimited(){
305+
assertIsQuicEndpointStats(this);
306+
returnthis.#handle[IDX_STATS_ENDPOINT_VERSION_NEGOTIATION_RATE_LIMITED];
307+
}
308+
289309
/** @type {bigint} */
290310
getstatelessResetCount(){
291311
assertIsQuicEndpointStats(this);
292312
returnthis.#handle[IDX_STATS_ENDPOINT_STATELESS_RESET_COUNT];
293313
}
294314

315+
/** @type {bigint} */
316+
getstatelessResetRateLimited(){
317+
assertIsQuicEndpointStats(this);
318+
returnthis.#handle[IDX_STATS_ENDPOINT_STATELESS_RESET_RATE_LIMITED];
319+
}
320+
295321
/** @type {bigint} */
296322
getimmediateCloseCount(){
297323
assertIsQuicEndpointStats(this);
298324
returnthis.#handle[IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_COUNT];
299325
}
300326

327+
/** @type {bigint} */
328+
getimmediateCloseRateLimited(){
329+
assertIsQuicEndpointStats(this);
330+
returnthis.#handle[IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_RATE_LIMITED];
331+
}
332+
301333
toString(){
302334
returnJSONStringify(this.toJSON());
303335
}
@@ -315,9 +347,13 @@ class QuicEndpointStats {
315347
clientSessions,
316348
serverBusyCount,
317349
retryCount,
350+
retryRateLimited,
318351
versionNegotiationCount,
352+
versionNegotiationRateLimited,
319353
statelessResetCount,
354+
statelessResetRateLimited,
320355
immediateCloseCount,
356+
immediateCloseRateLimited,
321357
}=this;
322358
return{
323359
__proto__: null,
@@ -334,9 +370,13 @@ class QuicEndpointStats {
334370
clientSessions: `${clientSessions}`,
335371
serverBusyCount: `${serverBusyCount}`,
336372
retryCount: `${retryCount}`,
373+
retryRateLimited: `${retryRateLimited}`,
337374
versionNegotiationCount: `${versionNegotiationCount}`,
375+
versionNegotiationRateLimited: `${versionNegotiationRateLimited}`,
338376
statelessResetCount: `${statelessResetCount}`,
377+
statelessResetRateLimited: `${statelessResetRateLimited}`,
339378
immediateCloseCount: `${immediateCloseCount}`,
379+
immediateCloseRateLimited: `${immediateCloseRateLimited}`,
340380
};
341381
}
342382

@@ -363,9 +403,13 @@ class QuicEndpointStats {
363403
clientSessions,
364404
serverBusyCount,
365405
retryCount,
406+
retryRateLimited,
366407
versionNegotiationCount,
408+
versionNegotiationRateLimited,
367409
statelessResetCount,
410+
statelessResetRateLimited,
368411
immediateCloseCount,
412+
immediateCloseRateLimited,
369413
}=this;
370414

371415
return`QuicEndpointStats ${inspect({
@@ -380,9 +424,13 @@ class QuicEndpointStats {
380424
clientSessions,
381425
serverBusyCount,
382426
retryCount,
427+
retryRateLimited,
383428
versionNegotiationCount,
429+
versionNegotiationRateLimited,
384430
statelessResetCount,
431+
statelessResetRateLimited,
385432
immediateCloseCount,
433+
immediateCloseRateLimited,
386434
},opts)}`;
387435
}
388436

‎node.gyp‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -449,6 +449,7 @@
449449
'test/cctest/test_quic_cid.cc',
450450
'test/cctest/test_quic_error.cc',
451451
'test/cctest/test_quic_preferredaddress.cc',
452+
'test/cctest/test_quic_tokenbucket.cc',
452453
'test/cctest/test_quic_tokens.cc',
453454
],
454455
'node_cctest_inspector_sources': [

‎src/quic/bindingdata.h‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,8 +117,14 @@ class SessionManager;
117117
V(idle_timeout, "idleTimeout") \
118118
V(max_idle_timeout, "maxIdleTimeout") \
119119
V(max_payload_size, "maxPayloadSize") \
120-
V(max_retries, "maxRetries") \
121-
V(max_stateless_resets, "maxStatelessResetsPerHost") \
120+
V(retry_rate, "retryRate") \
121+
V(retry_burst, "retryBurst") \
122+
V(stateless_reset_rate, "statelessResetRate") \
123+
V(stateless_reset_burst, "statelessResetBurst") \
124+
V(version_negotiation_rate, "versionNegotiationRate") \
125+
V(version_negotiation_burst, "versionNegotiationBurst") \
126+
V(immediate_close_rate, "immediateCloseRate") \
127+
V(immediate_close_burst, "immediateCloseBurst") \
122128
V(max_stream_window, "maxStreamWindow") \
123129
V(max_window, "maxWindow") \
124130
V(min_version, "minVersion") \

‎src/quic/defs.h‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,24 @@ constexpr auto kSocketAddressInfoTimeout = 60 * NGTCP2_SECONDS;
360360
constexprsize_tkMaxVectorCount = 16;
361361
constexpr stream_id kMaxStreamId = std::numeric_limits<stream_id>::max();
362362

363+
// A token bucket rate limiter using lazy refill. No timer needed — tokens
364+
// are computed on demand from the elapsed time since the last check.
365+
// Used to cap the total rate of stateless responses (retry, reset,
366+
// version negotiation, immediate close) regardless of source address,
367+
// preventing spoofed-source floods from bypassing per-host limits.
368+
structTokenBucketfinal {
369+
double rate; // tokens per second (refill rate)
370+
double burst; // maximum tokens (bucket capacity)
371+
double tokens; // current token count
372+
uint64_t last_ts; // last refill timestamp (nanoseconds, uv_hrtime)
373+
374+
TokenBucket(double rate, double burst);
375+
376+
// Try to consume one token. Refills based on elapsed time, then
377+
// attempts to consume. Returns true if the request is allowed.
378+
boolconsume();
379+
};
380+
363381
classDebugIndentScopefinal {
364382
public:
365383
inlineDebugIndentScope() { ++indent_; }

0 commit comments

Comments
 (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Commit 8107f1b

Browse files
jasnelladuh95
authored andcommitted
quic: refine rate limiting
Signed-off-by: James M Snell <jasnell@gmail.com> Assisted-by: Opencode:Opus 4.6 PR-URL: #63483 Backport-PR-URL: #64675 Reviewed-By: Stephen Belanger <admin@stephenbelanger.com> Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
1 parent 3f0d737 commit 8107f1b

12 files changed

Lines changed: 433 additions & 183 deletions

‎doc/api/quic.md‎

Lines changed: 84 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -726,31 +726,55 @@ added: v23.8.0
726726
added: v23.8.0
727727
-->
728728

729-
* Type: {bigint} The total number of QUIC retry attempts on this endpoint. Read only.
729+
* Type: {bigint} The total number of retry packets sent by this endpoint. Read only.
730+
731+
### `endpointStats.retryRateLimited`
732+
733+
* Type: {bigint} The total number of retry packets dropped by the global rate
734+
limiter. Read only. A non-zero value indicates the endpoint is under retry
735+
flood pressure.
730736

731737
### `endpointStats.versionNegotiationCount`
732738

733739
<!-- YAML
734740
added: v23.8.0
735741
-->
736742

737-
* Type: {bigint} The total number of sessions rejected due to QUIC version mismatch. Read only.
743+
* Type: {bigint} The total number of version negotiation packets sent by this
744+
endpoint. Read only.
745+
746+
### `endpointStats.versionNegotiationRateLimited`
747+
748+
* Type: {bigint} The total number of version negotiation packets dropped by
749+
the global rate limiter. Read only.
738750

739751
### `endpointStats.statelessResetCount`
740752

741753
<!-- YAML
742754
added: v23.8.0
743755
-->
744756

745-
* Type: {bigint} The total number of stateless resets handled by this endpoint. Read only.
757+
* Type: {bigint} The total number of stateless reset packets sent by this
758+
endpoint. Read only.
759+
760+
### `endpointStats.statelessResetRateLimited`
761+
762+
* Type: {bigint} The total number of stateless reset packets dropped by the
763+
global rate limiter. Read only.
746764

747765
### `endpointStats.immediateCloseCount`
748766

749767
<!-- YAML
750768
added: v23.8.0
751769
-->
752770

753-
* Type: {bigint} The total number of sessions that were closed before handshake completed. Read only.
771+
* Type: {bigint} The total number of immediate connection close packets sent
772+
by this endpoint. Read only.
773+
774+
### `endpointStats.immediateCloseRateLimited`
775+
776+
* Type: {bigint} The total number of immediate connection close packets
777+
dropped by the global rate limiter. Read only.
754778

755779
## Class: `QuicSession`
756780

@@ -2455,25 +2479,69 @@ addresses. When the limit is reached, new connections are refused with
24552479
This limit can also be changed dynamically after construction via
24562480
[`endpoint.maxConnectionsTotal`][].
24572481

2458-
#### `endpointOptions.maxRetries`
2482+
#### `endpointOptions.retryRate`
24592483

2460-
<!-- YAML
2461-
added: v23.8.0
2462-
-->
2484+
* Type: {number}
2485+
***Default:**`100`
24632486

2464-
* Type: {bigint|number}
2487+
The maximum number of QUIC retry packets the endpoint will send per second.
2488+
This is a global rate limit (not per-host) that caps the total server-wide
2489+
retry response rate, preventing spoofed-source floods from consuming unbounded
2490+
resources.
24652491

2466-
Specifies the maximum number of QUIC retry attempts allowed per remote peer address.
2492+
#### `endpointOptions.retryBurst`
24672493

2468-
#### `endpointOptions.maxStatelessResetsPerHost`
2494+
* Type: {number}
2495+
***Default:**`200`
24692496

2470-
<!-- YAML
2471-
added: v23.8.0
2472-
-->
2497+
The maximum burst of retry packets allowed before rate limiting takes effect.
24732498

2474-
* Type: {bigint|number}
2499+
#### `endpointOptions.statelessResetRate`
2500+
2501+
* Type: {number}
2502+
***Default:**`100`
2503+
2504+
The maximum number of stateless reset packets the endpoint will send per second.
2505+
2506+
#### `endpointOptions.statelessResetBurst`
2507+
2508+
* Type: {number}
2509+
***Default:**`200`
2510+
2511+
The maximum burst of stateless reset packets allowed before rate limiting
2512+
takes effect.
2513+
2514+
#### `endpointOptions.versionNegotiationRate`
2515+
2516+
* Type: {number}
2517+
***Default:**`100`
2518+
2519+
The maximum number of version negotiation packets the endpoint will send per
2520+
second.
2521+
2522+
#### `endpointOptions.versionNegotiationBurst`
2523+
2524+
* Type: {number}
2525+
***Default:**`200`
2526+
2527+
The maximum burst of version negotiation packets allowed before rate limiting
2528+
takes effect.
2529+
2530+
#### `endpointOptions.immediateCloseRate`
2531+
2532+
* Type: {number}
2533+
***Default:**`100`
2534+
2535+
The maximum number of immediate connection close packets the endpoint will
2536+
send per second.
2537+
2538+
#### `endpointOptions.immediateCloseBurst`
2539+
2540+
* Type: {number}
2541+
***Default:**`200`
24752542

2476-
Specifies the maximum number of stateless resets that are allowed per remote peer address.
2543+
The maximum burst of immediate connection close packets allowed before rate
2544+
limiting takes effect.
24772545

24782546
#### `endpointOptions.retryTokenExpiration`
24792547

‎lib/internal/quic/quic.js‎

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -307,8 +307,14 @@ const endpointRegistry = new SafeSet();
307307
* @property {boolean} [reusePort] Enable SO_REUSEPORT for multi-process load balancing
308308
* @property {bigint|number} [maxConnectionsPerHost] The maximum number of connections per host
309309
* @property {bigint|number} [maxConnectionsTotal] The maximum number of total connections
310-
* @property {bigint|number} [maxRetries] The maximum number of retries
311-
* @property {bigint|number} [maxStatelessResetsPerHost] The maximum number of stateless resets per host
310+
* @property {number} [retryRate] Global rate limit for retry packets (per second)
311+
* @property {number} [retryBurst] Burst capacity for retry rate limiter
312+
* @property {number} [statelessResetRate] Global rate limit for stateless reset packets (per second)
313+
* @property {number} [statelessResetBurst] Burst capacity for stateless reset rate limiter
314+
* @property {number} [versionNegotiationRate] Global rate limit for version negotiation packets (per second)
315+
* @property {number} [versionNegotiationBurst] Burst capacity for version negotiation rate limiter
316+
* @property {number} [immediateCloseRate] Global rate limit for immediate close packets (per second)
317+
* @property {number} [immediateCloseBurst] Burst capacity for immediate close rate limiter
312318
* @property {ArrayBufferView} [resetTokenSecret] The reset token secret
313319
* @property {bigint|number} [retryTokenExpiration] The retry token expiration
314320
* @property {number} [rxDiagnosticLoss] The receive diagnostic loss probability (range 0.0-1.0)
@@ -3997,10 +4003,16 @@ class QuicEndpoint {
39974003
tokenExpiration,
39984004
maxConnectionsPerHost =100,
39994005
maxConnectionsTotal =10_000,
4000-
maxStatelessResetsPerHost,
40014006
disableStatelessReset,
40024007
addressLRUSize,
4003-
maxRetries,
4008+
retryRate,
4009+
retryBurst,
4010+
statelessResetRate,
4011+
statelessResetBurst,
4012+
versionNegotiationRate,
4013+
versionNegotiationBurst,
4014+
immediateCloseRate,
4015+
immediateCloseBurst,
40044016
rxDiagnosticLoss,
40054017
txDiagnosticLoss,
40064018
udpReceiveBufferSize,
@@ -4034,10 +4046,16 @@ class QuicEndpoint {
40344046
// Connection limits are set on the state buffer, not passed to C++.
40354047
maxConnectionsPerHost,
40364048
maxConnectionsTotal,
4037-
maxStatelessResetsPerHost,
40384049
disableStatelessReset,
40394050
addressLRUSize,
4040-
maxRetries,
4051+
retryRate,
4052+
retryBurst,
4053+
statelessResetRate,
4054+
statelessResetBurst,
4055+
versionNegotiationRate,
4056+
versionNegotiationBurst,
4057+
immediateCloseRate,
4058+
immediateCloseBurst,
40414059
rxDiagnosticLoss,
40424060
txDiagnosticLoss,
40434061
udpReceiveBufferSize,

‎lib/internal/quic/stats.js‎

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,13 @@ const {
6060
IDX_STATS_ENDPOINT_CLIENT_SESSIONS,
6161
IDX_STATS_ENDPOINT_SERVER_BUSY_COUNT,
6262
IDX_STATS_ENDPOINT_RETRY_COUNT,
63+
IDX_STATS_ENDPOINT_RETRY_RATE_LIMITED,
6364
IDX_STATS_ENDPOINT_VERSION_NEGOTIATION_COUNT,
65+
IDX_STATS_ENDPOINT_VERSION_NEGOTIATION_RATE_LIMITED,
6466
IDX_STATS_ENDPOINT_STATELESS_RESET_COUNT,
67+
IDX_STATS_ENDPOINT_STATELESS_RESET_RATE_LIMITED,
6568
IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_COUNT,
69+
IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_RATE_LIMITED,
6670

6771
IDX_STATS_SESSION_CREATED_AT,
6872
IDX_STATS_SESSION_DESTROYED_AT,
@@ -123,9 +127,13 @@ assert(IDX_STATS_ENDPOINT_SERVER_SESSIONS !== undefined);
123127
assert(IDX_STATS_ENDPOINT_CLIENT_SESSIONS!==undefined);
124128
assert(IDX_STATS_ENDPOINT_SERVER_BUSY_COUNT!==undefined);
125129
assert(IDX_STATS_ENDPOINT_RETRY_COUNT!==undefined);
130+
assert(IDX_STATS_ENDPOINT_RETRY_RATE_LIMITED!==undefined);
126131
assert(IDX_STATS_ENDPOINT_VERSION_NEGOTIATION_COUNT!==undefined);
132+
assert(IDX_STATS_ENDPOINT_VERSION_NEGOTIATION_RATE_LIMITED!==undefined);
127133
assert(IDX_STATS_ENDPOINT_STATELESS_RESET_COUNT!==undefined);
134+
assert(IDX_STATS_ENDPOINT_STATELESS_RESET_RATE_LIMITED!==undefined);
128135
assert(IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_COUNT!==undefined);
136+
assert(IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_RATE_LIMITED!==undefined);
129137
assert(IDX_STATS_SESSION_CREATED_AT!==undefined);
130138
assert(IDX_STATS_SESSION_DESTROYED_AT!==undefined);
131139
assert(IDX_STATS_SESSION_CLOSING_AT!==undefined);
@@ -280,24 +288,48 @@ class QuicEndpointStats {
280288
returnthis.#handle[IDX_STATS_ENDPOINT_RETRY_COUNT];
281289
}
282290

291+
/** @type {bigint} */
292+
getretryRateLimited(){
293+
assertIsQuicEndpointStats(this);
294+
returnthis.#handle[IDX_STATS_ENDPOINT_RETRY_RATE_LIMITED];
295+
}
296+
283297
/** @type {bigint} */
284298
getversionNegotiationCount(){
285299
assertIsQuicEndpointStats(this);
286300
returnthis.#handle[IDX_STATS_ENDPOINT_VERSION_NEGOTIATION_COUNT];
287301
}
288302

303+
/** @type {bigint} */
304+
getversionNegotiationRateLimited(){
305+
assertIsQuicEndpointStats(this);
306+
returnthis.#handle[IDX_STATS_ENDPOINT_VERSION_NEGOTIATION_RATE_LIMITED];
307+
}
308+
289309
/** @type {bigint} */
290310
getstatelessResetCount(){
291311
assertIsQuicEndpointStats(this);
292312
returnthis.#handle[IDX_STATS_ENDPOINT_STATELESS_RESET_COUNT];
293313
}
294314

315+
/** @type {bigint} */
316+
getstatelessResetRateLimited(){
317+
assertIsQuicEndpointStats(this);
318+
returnthis.#handle[IDX_STATS_ENDPOINT_STATELESS_RESET_RATE_LIMITED];
319+
}
320+
295321
/** @type {bigint} */
296322
getimmediateCloseCount(){
297323
assertIsQuicEndpointStats(this);
298324
returnthis.#handle[IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_COUNT];
299325
}
300326

327+
/** @type {bigint} */
328+
getimmediateCloseRateLimited(){
329+
assertIsQuicEndpointStats(this);
330+
returnthis.#handle[IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_RATE_LIMITED];
331+
}
332+
301333
toString(){
302334
returnJSONStringify(this.toJSON());
303335
}
@@ -315,9 +347,13 @@ class QuicEndpointStats {
315347
clientSessions,
316348
serverBusyCount,
317349
retryCount,
350+
retryRateLimited,
318351
versionNegotiationCount,
352+
versionNegotiationRateLimited,
319353
statelessResetCount,
354+
statelessResetRateLimited,
320355
immediateCloseCount,
356+
immediateCloseRateLimited,
321357
}=this;
322358
return{
323359
__proto__: null,
@@ -334,9 +370,13 @@ class QuicEndpointStats {
334370
clientSessions: `${clientSessions}`,
335371
serverBusyCount: `${serverBusyCount}`,
336372
retryCount: `${retryCount}`,
373+
retryRateLimited: `${retryRateLimited}`,
337374
versionNegotiationCount: `${versionNegotiationCount}`,
375+
versionNegotiationRateLimited: `${versionNegotiationRateLimited}`,
338376
statelessResetCount: `${statelessResetCount}`,
377+
statelessResetRateLimited: `${statelessResetRateLimited}`,
339378
immediateCloseCount: `${immediateCloseCount}`,
379+
immediateCloseRateLimited: `${immediateCloseRateLimited}`,
340380
};
341381
}
342382

@@ -363,9 +403,13 @@ class QuicEndpointStats {
363403
clientSessions,
364404
serverBusyCount,
365405
retryCount,
406+
retryRateLimited,
366407
versionNegotiationCount,
408+
versionNegotiationRateLimited,
367409
statelessResetCount,
410+
statelessResetRateLimited,
368411
immediateCloseCount,
412+
immediateCloseRateLimited,
369413
}=this;
370414

371415
return`QuicEndpointStats ${inspect({
@@ -380,9 +424,13 @@ class QuicEndpointStats {
380424
clientSessions,
381425
serverBusyCount,
382426
retryCount,
427+
retryRateLimited,
383428
versionNegotiationCount,
429+
versionNegotiationRateLimited,
384430
statelessResetCount,
431+
statelessResetRateLimited,
385432
immediateCloseCount,
433+
immediateCloseRateLimited,
386434
},opts)}`;
387435
}
388436

‎node.gyp‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -449,6 +449,7 @@
449449
'test/cctest/test_quic_cid.cc',
450450
'test/cctest/test_quic_error.cc',
451451
'test/cctest/test_quic_preferredaddress.cc',
452+
'test/cctest/test_quic_tokenbucket.cc',
452453
'test/cctest/test_quic_tokens.cc',
453454
],
454455
'node_cctest_inspector_sources': [

‎src/quic/bindingdata.h‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,8 +117,14 @@ class SessionManager;
117117
V(idle_timeout, "idleTimeout") \
118118
V(max_idle_timeout, "maxIdleTimeout") \
119119
V(max_payload_size, "maxPayloadSize") \
120-
V(max_retries, "maxRetries") \
121-
V(max_stateless_resets, "maxStatelessResetsPerHost") \
120+
V(retry_rate, "retryRate") \
121+
V(retry_burst, "retryBurst") \
122+
V(stateless_reset_rate, "statelessResetRate") \
123+
V(stateless_reset_burst, "statelessResetBurst") \
124+
V(version_negotiation_rate, "versionNegotiationRate") \
125+
V(version_negotiation_burst, "versionNegotiationBurst") \
126+
V(immediate_close_rate, "immediateCloseRate") \
127+
V(immediate_close_burst, "immediateCloseBurst") \
122128
V(max_stream_window, "maxStreamWindow") \
123129
V(max_window, "maxWindow") \
124130
V(min_version, "minVersion") \

‎src/quic/defs.h‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,24 @@ constexpr auto kSocketAddressInfoTimeout = 60 * NGTCP2_SECONDS;
360360
constexprsize_tkMaxVectorCount = 16;
361361
constexpr stream_id kMaxStreamId = std::numeric_limits<stream_id>::max();
362362

363+
// A token bucket rate limiter using lazy refill. No timer needed — tokens
364+
// are computed on demand from the elapsed time since the last check.
365+
// Used to cap the total rate of stateless responses (retry, reset,
366+
// version negotiation, immediate close) regardless of source address,
367+
// preventing spoofed-source floods from bypassing per-host limits.
368+
structTokenBucketfinal {
369+
double rate; // tokens per second (refill rate)
370+
double burst; // maximum tokens (bucket capacity)
371+
double tokens; // current token count
372+
uint64_t last_ts; // last refill timestamp (nanoseconds, uv_hrtime)
373+
374+
TokenBucket(double rate, double burst);
375+
376+
// Try to consume one token. Refills based on elapsed time, then
377+
// attempts to consume. Returns true if the request is allowed.
378+
boolconsume();
379+
};
380+
363381
classDebugIndentScopefinal {
364382
public:
365383
inlineDebugIndentScope() { ++indent_; }

0 commit comments

Comments
 (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Commit 8107f1b

Browse files
jasnelladuh95
authored andcommitted
quic: refine rate limiting
Signed-off-by: James M Snell <jasnell@gmail.com> Assisted-by: Opencode:Opus 4.6 PR-URL: #63483 Backport-PR-URL: #64675 Reviewed-By: Stephen Belanger <admin@stephenbelanger.com> Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
1 parent 3f0d737 commit 8107f1b

12 files changed

Lines changed: 433 additions & 183 deletions

‎doc/api/quic.md‎

Lines changed: 84 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -726,31 +726,55 @@ added: v23.8.0
726726
added: v23.8.0
727727
-->
728728

729-
* Type: {bigint} The total number of QUIC retry attempts on this endpoint. Read only.
729+
* Type: {bigint} The total number of retry packets sent by this endpoint. Read only.
730+
731+
### `endpointStats.retryRateLimited`
732+
733+
* Type: {bigint} The total number of retry packets dropped by the global rate
734+
limiter. Read only. A non-zero value indicates the endpoint is under retry
735+
flood pressure.
730736

731737
### `endpointStats.versionNegotiationCount`
732738

733739
<!-- YAML
734740
added: v23.8.0
735741
-->
736742

737-
* Type: {bigint} The total number of sessions rejected due to QUIC version mismatch. Read only.
743+
* Type: {bigint} The total number of version negotiation packets sent by this
744+
endpoint. Read only.
745+
746+
### `endpointStats.versionNegotiationRateLimited`
747+
748+
* Type: {bigint} The total number of version negotiation packets dropped by
749+
the global rate limiter. Read only.
738750

739751
### `endpointStats.statelessResetCount`
740752

741753
<!-- YAML
742754
added: v23.8.0
743755
-->
744756

745-
* Type: {bigint} The total number of stateless resets handled by this endpoint. Read only.
757+
* Type: {bigint} The total number of stateless reset packets sent by this
758+
endpoint. Read only.
759+
760+
### `endpointStats.statelessResetRateLimited`
761+
762+
* Type: {bigint} The total number of stateless reset packets dropped by the
763+
global rate limiter. Read only.
746764

747765
### `endpointStats.immediateCloseCount`
748766

749767
<!-- YAML
750768
added: v23.8.0
751769
-->
752770

753-
* Type: {bigint} The total number of sessions that were closed before handshake completed. Read only.
771+
* Type: {bigint} The total number of immediate connection close packets sent
772+
by this endpoint. Read only.
773+
774+
### `endpointStats.immediateCloseRateLimited`
775+
776+
* Type: {bigint} The total number of immediate connection close packets
777+
dropped by the global rate limiter. Read only.
754778

755779
## Class: `QuicSession`
756780

@@ -2455,25 +2479,69 @@ addresses. When the limit is reached, new connections are refused with
24552479
This limit can also be changed dynamically after construction via
24562480
[`endpoint.maxConnectionsTotal`][].
24572481

2458-
#### `endpointOptions.maxRetries`
2482+
#### `endpointOptions.retryRate`
24592483

2460-
<!-- YAML
2461-
added: v23.8.0
2462-
-->
2484+
* Type: {number}
2485+
***Default:**`100`
24632486

2464-
* Type: {bigint|number}
2487+
The maximum number of QUIC retry packets the endpoint will send per second.
2488+
This is a global rate limit (not per-host) that caps the total server-wide
2489+
retry response rate, preventing spoofed-source floods from consuming unbounded
2490+
resources.
24652491

2466-
Specifies the maximum number of QUIC retry attempts allowed per remote peer address.
2492+
#### `endpointOptions.retryBurst`
24672493

2468-
#### `endpointOptions.maxStatelessResetsPerHost`
2494+
* Type: {number}
2495+
***Default:**`200`
24692496

2470-
<!-- YAML
2471-
added: v23.8.0
2472-
-->
2497+
The maximum burst of retry packets allowed before rate limiting takes effect.
24732498

2474-
* Type: {bigint|number}
2499+
#### `endpointOptions.statelessResetRate`
2500+
2501+
* Type: {number}
2502+
***Default:**`100`
2503+
2504+
The maximum number of stateless reset packets the endpoint will send per second.
2505+
2506+
#### `endpointOptions.statelessResetBurst`
2507+
2508+
* Type: {number}
2509+
***Default:**`200`
2510+
2511+
The maximum burst of stateless reset packets allowed before rate limiting
2512+
takes effect.
2513+
2514+
#### `endpointOptions.versionNegotiationRate`
2515+
2516+
* Type: {number}
2517+
***Default:**`100`
2518+
2519+
The maximum number of version negotiation packets the endpoint will send per
2520+
second.
2521+
2522+
#### `endpointOptions.versionNegotiationBurst`
2523+
2524+
* Type: {number}
2525+
***Default:**`200`
2526+
2527+
The maximum burst of version negotiation packets allowed before rate limiting
2528+
takes effect.
2529+
2530+
#### `endpointOptions.immediateCloseRate`
2531+
2532+
* Type: {number}
2533+
***Default:**`100`
2534+
2535+
The maximum number of immediate connection close packets the endpoint will
2536+
send per second.
2537+
2538+
#### `endpointOptions.immediateCloseBurst`
2539+
2540+
* Type: {number}
2541+
***Default:**`200`
24752542

2476-
Specifies the maximum number of stateless resets that are allowed per remote peer address.
2543+
The maximum burst of immediate connection close packets allowed before rate
2544+
limiting takes effect.
24772545

24782546
#### `endpointOptions.retryTokenExpiration`
24792547

‎lib/internal/quic/quic.js‎

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -307,8 +307,14 @@ const endpointRegistry = new SafeSet();
307307
* @property {boolean} [reusePort] Enable SO_REUSEPORT for multi-process load balancing
308308
* @property {bigint|number} [maxConnectionsPerHost] The maximum number of connections per host
309309
* @property {bigint|number} [maxConnectionsTotal] The maximum number of total connections
310-
* @property {bigint|number} [maxRetries] The maximum number of retries
311-
* @property {bigint|number} [maxStatelessResetsPerHost] The maximum number of stateless resets per host
310+
* @property {number} [retryRate] Global rate limit for retry packets (per second)
311+
* @property {number} [retryBurst] Burst capacity for retry rate limiter
312+
* @property {number} [statelessResetRate] Global rate limit for stateless reset packets (per second)
313+
* @property {number} [statelessResetBurst] Burst capacity for stateless reset rate limiter
314+
* @property {number} [versionNegotiationRate] Global rate limit for version negotiation packets (per second)
315+
* @property {number} [versionNegotiationBurst] Burst capacity for version negotiation rate limiter
316+
* @property {number} [immediateCloseRate] Global rate limit for immediate close packets (per second)
317+
* @property {number} [immediateCloseBurst] Burst capacity for immediate close rate limiter
312318
* @property {ArrayBufferView} [resetTokenSecret] The reset token secret
313319
* @property {bigint|number} [retryTokenExpiration] The retry token expiration
314320
* @property {number} [rxDiagnosticLoss] The receive diagnostic loss probability (range 0.0-1.0)
@@ -3997,10 +4003,16 @@ class QuicEndpoint {
39974003
tokenExpiration,
39984004
maxConnectionsPerHost =100,
39994005
maxConnectionsTotal =10_000,
4000-
maxStatelessResetsPerHost,
40014006
disableStatelessReset,
40024007
addressLRUSize,
4003-
maxRetries,
4008+
retryRate,
4009+
retryBurst,
4010+
statelessResetRate,
4011+
statelessResetBurst,
4012+
versionNegotiationRate,
4013+
versionNegotiationBurst,
4014+
immediateCloseRate,
4015+
immediateCloseBurst,
40044016
rxDiagnosticLoss,
40054017
txDiagnosticLoss,
40064018
udpReceiveBufferSize,
@@ -4034,10 +4046,16 @@ class QuicEndpoint {
40344046
// Connection limits are set on the state buffer, not passed to C++.
40354047
maxConnectionsPerHost,
40364048
maxConnectionsTotal,
4037-
maxStatelessResetsPerHost,
40384049
disableStatelessReset,
40394050
addressLRUSize,
4040-
maxRetries,
4051+
retryRate,
4052+
retryBurst,
4053+
statelessResetRate,
4054+
statelessResetBurst,
4055+
versionNegotiationRate,
4056+
versionNegotiationBurst,
4057+
immediateCloseRate,
4058+
immediateCloseBurst,
40414059
rxDiagnosticLoss,
40424060
txDiagnosticLoss,
40434061
udpReceiveBufferSize,

‎lib/internal/quic/stats.js‎

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,13 @@ const {
6060
IDX_STATS_ENDPOINT_CLIENT_SESSIONS,
6161
IDX_STATS_ENDPOINT_SERVER_BUSY_COUNT,
6262
IDX_STATS_ENDPOINT_RETRY_COUNT,
63+
IDX_STATS_ENDPOINT_RETRY_RATE_LIMITED,
6364
IDX_STATS_ENDPOINT_VERSION_NEGOTIATION_COUNT,
65+
IDX_STATS_ENDPOINT_VERSION_NEGOTIATION_RATE_LIMITED,
6466
IDX_STATS_ENDPOINT_STATELESS_RESET_COUNT,
67+
IDX_STATS_ENDPOINT_STATELESS_RESET_RATE_LIMITED,
6568
IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_COUNT,
69+
IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_RATE_LIMITED,
6670

6771
IDX_STATS_SESSION_CREATED_AT,
6872
IDX_STATS_SESSION_DESTROYED_AT,
@@ -123,9 +127,13 @@ assert(IDX_STATS_ENDPOINT_SERVER_SESSIONS !== undefined);
123127
assert(IDX_STATS_ENDPOINT_CLIENT_SESSIONS!==undefined);
124128
assert(IDX_STATS_ENDPOINT_SERVER_BUSY_COUNT!==undefined);
125129
assert(IDX_STATS_ENDPOINT_RETRY_COUNT!==undefined);
130+
assert(IDX_STATS_ENDPOINT_RETRY_RATE_LIMITED!==undefined);
126131
assert(IDX_STATS_ENDPOINT_VERSION_NEGOTIATION_COUNT!==undefined);
132+
assert(IDX_STATS_ENDPOINT_VERSION_NEGOTIATION_RATE_LIMITED!==undefined);
127133
assert(IDX_STATS_ENDPOINT_STATELESS_RESET_COUNT!==undefined);
134+
assert(IDX_STATS_ENDPOINT_STATELESS_RESET_RATE_LIMITED!==undefined);
128135
assert(IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_COUNT!==undefined);
136+
assert(IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_RATE_LIMITED!==undefined);
129137
assert(IDX_STATS_SESSION_CREATED_AT!==undefined);
130138
assert(IDX_STATS_SESSION_DESTROYED_AT!==undefined);
131139
assert(IDX_STATS_SESSION_CLOSING_AT!==undefined);
@@ -280,24 +288,48 @@ class QuicEndpointStats {
280288
returnthis.#handle[IDX_STATS_ENDPOINT_RETRY_COUNT];
281289
}
282290

291+
/** @type {bigint} */
292+
getretryRateLimited(){
293+
assertIsQuicEndpointStats(this);
294+
returnthis.#handle[IDX_STATS_ENDPOINT_RETRY_RATE_LIMITED];
295+
}
296+
283297
/** @type {bigint} */
284298
getversionNegotiationCount(){
285299
assertIsQuicEndpointStats(this);
286300
returnthis.#handle[IDX_STATS_ENDPOINT_VERSION_NEGOTIATION_COUNT];
287301
}
288302

303+
/** @type {bigint} */
304+
getversionNegotiationRateLimited(){
305+
assertIsQuicEndpointStats(this);
306+
returnthis.#handle[IDX_STATS_ENDPOINT_VERSION_NEGOTIATION_RATE_LIMITED];
307+
}
308+
289309
/** @type {bigint} */
290310
getstatelessResetCount(){
291311
assertIsQuicEndpointStats(this);
292312
returnthis.#handle[IDX_STATS_ENDPOINT_STATELESS_RESET_COUNT];
293313
}
294314

315+
/** @type {bigint} */
316+
getstatelessResetRateLimited(){
317+
assertIsQuicEndpointStats(this);
318+
returnthis.#handle[IDX_STATS_ENDPOINT_STATELESS_RESET_RATE_LIMITED];
319+
}
320+
295321
/** @type {bigint} */
296322
getimmediateCloseCount(){
297323
assertIsQuicEndpointStats(this);
298324
returnthis.#handle[IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_COUNT];
299325
}
300326

327+
/** @type {bigint} */
328+
getimmediateCloseRateLimited(){
329+
assertIsQuicEndpointStats(this);
330+
returnthis.#handle[IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_RATE_LIMITED];
331+
}
332+
301333
toString(){
302334
returnJSONStringify(this.toJSON());
303335
}
@@ -315,9 +347,13 @@ class QuicEndpointStats {
315347
clientSessions,
316348
serverBusyCount,
317349
retryCount,
350+
retryRateLimited,
318351
versionNegotiationCount,
352+
versionNegotiationRateLimited,
319353
statelessResetCount,
354+
statelessResetRateLimited,
320355
immediateCloseCount,
356+
immediateCloseRateLimited,
321357
}=this;
322358
return{
323359
__proto__: null,
@@ -334,9 +370,13 @@ class QuicEndpointStats {
334370
clientSessions: `${clientSessions}`,
335371
serverBusyCount: `${serverBusyCount}`,
336372
retryCount: `${retryCount}`,
373+
retryRateLimited: `${retryRateLimited}`,
337374
versionNegotiationCount: `${versionNegotiationCount}`,
375+
versionNegotiationRateLimited: `${versionNegotiationRateLimited}`,
338376
statelessResetCount: `${statelessResetCount}`,
377+
statelessResetRateLimited: `${statelessResetRateLimited}`,
339378
immediateCloseCount: `${immediateCloseCount}`,
379+
immediateCloseRateLimited: `${immediateCloseRateLimited}`,
340380
};
341381
}
342382

@@ -363,9 +403,13 @@ class QuicEndpointStats {
363403
clientSessions,
364404
serverBusyCount,
365405
retryCount,
406+
retryRateLimited,
366407
versionNegotiationCount,
408+
versionNegotiationRateLimited,
367409
statelessResetCount,
410+
statelessResetRateLimited,
368411
immediateCloseCount,
412+
immediateCloseRateLimited,
369413
}=this;
370414

371415
return`QuicEndpointStats ${inspect({
@@ -380,9 +424,13 @@ class QuicEndpointStats {
380424
clientSessions,
381425
serverBusyCount,
382426
retryCount,
427+
retryRateLimited,
383428
versionNegotiationCount,
429+
versionNegotiationRateLimited,
384430
statelessResetCount,
431+
statelessResetRateLimited,
385432
immediateCloseCount,
433+
immediateCloseRateLimited,
386434
},opts)}`;
387435
}
388436

‎node.gyp‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -449,6 +449,7 @@
449449
'test/cctest/test_quic_cid.cc',
450450
'test/cctest/test_quic_error.cc',
451451
'test/cctest/test_quic_preferredaddress.cc',
452+
'test/cctest/test_quic_tokenbucket.cc',
452453
'test/cctest/test_quic_tokens.cc',
453454
],
454455
'node_cctest_inspector_sources': [

‎src/quic/bindingdata.h‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,8 +117,14 @@ class SessionManager;
117117
V(idle_timeout, "idleTimeout") \
118118
V(max_idle_timeout, "maxIdleTimeout") \
119119
V(max_payload_size, "maxPayloadSize") \
120-
V(max_retries, "maxRetries") \
121-
V(max_stateless_resets, "maxStatelessResetsPerHost") \
120+
V(retry_rate, "retryRate") \
121+
V(retry_burst, "retryBurst") \
122+
V(stateless_reset_rate, "statelessResetRate") \
123+
V(stateless_reset_burst, "statelessResetBurst") \
124+
V(version_negotiation_rate, "versionNegotiationRate") \
125+
V(version_negotiation_burst, "versionNegotiationBurst") \
126+
V(immediate_close_rate, "immediateCloseRate") \
127+
V(immediate_close_burst, "immediateCloseBurst") \
122128
V(max_stream_window, "maxStreamWindow") \
123129
V(max_window, "maxWindow") \
124130
V(min_version, "minVersion") \

‎src/quic/defs.h‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,24 @@ constexpr auto kSocketAddressInfoTimeout = 60 * NGTCP2_SECONDS;
360360
constexprsize_tkMaxVectorCount = 16;
361361
constexpr stream_id kMaxStreamId = std::numeric_limits<stream_id>::max();
362362

363+
// A token bucket rate limiter using lazy refill. No timer needed — tokens
364+
// are computed on demand from the elapsed time since the last check.
365+
// Used to cap the total rate of stateless responses (retry, reset,
366+
// version negotiation, immediate close) regardless of source address,
367+
// preventing spoofed-source floods from bypassing per-host limits.
368+
structTokenBucketfinal {
369+
double rate; // tokens per second (refill rate)
370+
double burst; // maximum tokens (bucket capacity)
371+
double tokens; // current token count
372+
uint64_t last_ts; // last refill timestamp (nanoseconds, uv_hrtime)
373+
374+
TokenBucket(double rate, double burst);
375+
376+
// Try to consume one token. Refills based on elapsed time, then
377+
// attempts to consume. Returns true if the request is allowed.
378+
boolconsume();
379+
};
380+
363381
classDebugIndentScopefinal {
364382
public:
365383
inlineDebugIndentScope() { ++indent_; }

0 commit comments

Comments
 (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Commit 8107f1b

Browse files
jasnelladuh95
authored andcommitted
quic: refine rate limiting
Signed-off-by: James M Snell <jasnell@gmail.com> Assisted-by: Opencode:Opus 4.6 PR-URL: #63483 Backport-PR-URL: #64675 Reviewed-By: Stephen Belanger <admin@stephenbelanger.com> Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
1 parent 3f0d737 commit 8107f1b

12 files changed

Lines changed: 433 additions & 183 deletions

‎doc/api/quic.md‎

Lines changed: 84 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -726,31 +726,55 @@ added: v23.8.0
726726
added: v23.8.0
727727
-->
728728

729-
* Type: {bigint} The total number of QUIC retry attempts on this endpoint. Read only.
729+
* Type: {bigint} The total number of retry packets sent by this endpoint. Read only.
730+
731+
### `endpointStats.retryRateLimited`
732+
733+
* Type: {bigint} The total number of retry packets dropped by the global rate
734+
limiter. Read only. A non-zero value indicates the endpoint is under retry
735+
flood pressure.
730736

731737
### `endpointStats.versionNegotiationCount`
732738

733739
<!-- YAML
734740
added: v23.8.0
735741
-->
736742

737-
* Type: {bigint} The total number of sessions rejected due to QUIC version mismatch. Read only.
743+
* Type: {bigint} The total number of version negotiation packets sent by this
744+
endpoint. Read only.
745+
746+
### `endpointStats.versionNegotiationRateLimited`
747+
748+
* Type: {bigint} The total number of version negotiation packets dropped by
749+
the global rate limiter. Read only.
738750

739751
### `endpointStats.statelessResetCount`
740752

741753
<!-- YAML
742754
added: v23.8.0
743755
-->
744756

745-
* Type: {bigint} The total number of stateless resets handled by this endpoint. Read only.
757+
* Type: {bigint} The total number of stateless reset packets sent by this
758+
endpoint. Read only.
759+
760+
### `endpointStats.statelessResetRateLimited`
761+
762+
* Type: {bigint} The total number of stateless reset packets dropped by the
763+
global rate limiter. Read only.
746764

747765
### `endpointStats.immediateCloseCount`
748766

749767
<!-- YAML
750768
added: v23.8.0
751769
-->
752770

753-
* Type: {bigint} The total number of sessions that were closed before handshake completed. Read only.
771+
* Type: {bigint} The total number of immediate connection close packets sent
772+
by this endpoint. Read only.
773+
774+
### `endpointStats.immediateCloseRateLimited`
775+
776+
* Type: {bigint} The total number of immediate connection close packets
777+
dropped by the global rate limiter. Read only.
754778

755779
## Class: `QuicSession`
756780

@@ -2455,25 +2479,69 @@ addresses. When the limit is reached, new connections are refused with
24552479
This limit can also be changed dynamically after construction via
24562480
[`endpoint.maxConnectionsTotal`][].
24572481

2458-
#### `endpointOptions.maxRetries`
2482+
#### `endpointOptions.retryRate`
24592483

2460-
<!-- YAML
2461-
added: v23.8.0
2462-
-->
2484+
* Type: {number}
2485+
***Default:**`100`
24632486

2464-
* Type: {bigint|number}
2487+
The maximum number of QUIC retry packets the endpoint will send per second.
2488+
This is a global rate limit (not per-host) that caps the total server-wide
2489+
retry response rate, preventing spoofed-source floods from consuming unbounded
2490+
resources.
24652491

2466-
Specifies the maximum number of QUIC retry attempts allowed per remote peer address.
2492+
#### `endpointOptions.retryBurst`
24672493

2468-
#### `endpointOptions.maxStatelessResetsPerHost`
2494+
* Type: {number}
2495+
***Default:**`200`
24692496

2470-
<!-- YAML
2471-
added: v23.8.0
2472-
-->
2497+
The maximum burst of retry packets allowed before rate limiting takes effect.
24732498

2474-
* Type: {bigint|number}
2499+
#### `endpointOptions.statelessResetRate`
2500+
2501+
* Type: {number}
2502+
***Default:**`100`
2503+
2504+
The maximum number of stateless reset packets the endpoint will send per second.
2505+
2506+
#### `endpointOptions.statelessResetBurst`
2507+
2508+
* Type: {number}
2509+
***Default:**`200`
2510+
2511+
The maximum burst of stateless reset packets allowed before rate limiting
2512+
takes effect.
2513+
2514+
#### `endpointOptions.versionNegotiationRate`
2515+
2516+
* Type: {number}
2517+
***Default:**`100`
2518+
2519+
The maximum number of version negotiation packets the endpoint will send per
2520+
second.
2521+
2522+
#### `endpointOptions.versionNegotiationBurst`
2523+
2524+
* Type: {number}
2525+
***Default:**`200`
2526+
2527+
The maximum burst of version negotiation packets allowed before rate limiting
2528+
takes effect.
2529+
2530+
#### `endpointOptions.immediateCloseRate`
2531+
2532+
* Type: {number}
2533+
***Default:**`100`
2534+
2535+
The maximum number of immediate connection close packets the endpoint will
2536+
send per second.
2537+
2538+
#### `endpointOptions.immediateCloseBurst`
2539+
2540+
* Type: {number}
2541+
***Default:**`200`
24752542

2476-
Specifies the maximum number of stateless resets that are allowed per remote peer address.
2543+
The maximum burst of immediate connection close packets allowed before rate
2544+
limiting takes effect.
24772545

24782546
#### `endpointOptions.retryTokenExpiration`
24792547

‎lib/internal/quic/quic.js‎

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -307,8 +307,14 @@ const endpointRegistry = new SafeSet();
307307
* @property {boolean} [reusePort] Enable SO_REUSEPORT for multi-process load balancing
308308
* @property {bigint|number} [maxConnectionsPerHost] The maximum number of connections per host
309309
* @property {bigint|number} [maxConnectionsTotal] The maximum number of total connections
310-
* @property {bigint|number} [maxRetries] The maximum number of retries
311-
* @property {bigint|number} [maxStatelessResetsPerHost] The maximum number of stateless resets per host
310+
* @property {number} [retryRate] Global rate limit for retry packets (per second)
311+
* @property {number} [retryBurst] Burst capacity for retry rate limiter
312+
* @property {number} [statelessResetRate] Global rate limit for stateless reset packets (per second)
313+
* @property {number} [statelessResetBurst] Burst capacity for stateless reset rate limiter
314+
* @property {number} [versionNegotiationRate] Global rate limit for version negotiation packets (per second)
315+
* @property {number} [versionNegotiationBurst] Burst capacity for version negotiation rate limiter
316+
* @property {number} [immediateCloseRate] Global rate limit for immediate close packets (per second)
317+
* @property {number} [immediateCloseBurst] Burst capacity for immediate close rate limiter
312318
* @property {ArrayBufferView} [resetTokenSecret] The reset token secret
313319
* @property {bigint|number} [retryTokenExpiration] The retry token expiration
314320
* @property {number} [rxDiagnosticLoss] The receive diagnostic loss probability (range 0.0-1.0)
@@ -3997,10 +4003,16 @@ class QuicEndpoint {
39974003
tokenExpiration,
39984004
maxConnectionsPerHost =100,
39994005
maxConnectionsTotal =10_000,
4000-
maxStatelessResetsPerHost,
40014006
disableStatelessReset,
40024007
addressLRUSize,
4003-
maxRetries,
4008+
retryRate,
4009+
retryBurst,
4010+
statelessResetRate,
4011+
statelessResetBurst,
4012+
versionNegotiationRate,
4013+
versionNegotiationBurst,
4014+
immediateCloseRate,
4015+
immediateCloseBurst,
40044016
rxDiagnosticLoss,
40054017
txDiagnosticLoss,
40064018
udpReceiveBufferSize,
@@ -4034,10 +4046,16 @@ class QuicEndpoint {
40344046
// Connection limits are set on the state buffer, not passed to C++.
40354047
maxConnectionsPerHost,
40364048
maxConnectionsTotal,
4037-
maxStatelessResetsPerHost,
40384049
disableStatelessReset,
40394050
addressLRUSize,
4040-
maxRetries,
4051+
retryRate,
4052+
retryBurst,
4053+
statelessResetRate,
4054+
statelessResetBurst,
4055+
versionNegotiationRate,
4056+
versionNegotiationBurst,
4057+
immediateCloseRate,
4058+
immediateCloseBurst,
40414059
rxDiagnosticLoss,
40424060
txDiagnosticLoss,
40434061
udpReceiveBufferSize,

‎lib/internal/quic/stats.js‎

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,13 @@ const {
6060
IDX_STATS_ENDPOINT_CLIENT_SESSIONS,
6161
IDX_STATS_ENDPOINT_SERVER_BUSY_COUNT,
6262
IDX_STATS_ENDPOINT_RETRY_COUNT,
63+
IDX_STATS_ENDPOINT_RETRY_RATE_LIMITED,
6364
IDX_STATS_ENDPOINT_VERSION_NEGOTIATION_COUNT,
65+
IDX_STATS_ENDPOINT_VERSION_NEGOTIATION_RATE_LIMITED,
6466
IDX_STATS_ENDPOINT_STATELESS_RESET_COUNT,
67+
IDX_STATS_ENDPOINT_STATELESS_RESET_RATE_LIMITED,
6568
IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_COUNT,
69+
IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_RATE_LIMITED,
6670

6771
IDX_STATS_SESSION_CREATED_AT,
6872
IDX_STATS_SESSION_DESTROYED_AT,
@@ -123,9 +127,13 @@ assert(IDX_STATS_ENDPOINT_SERVER_SESSIONS !== undefined);
123127
assert(IDX_STATS_ENDPOINT_CLIENT_SESSIONS!==undefined);
124128
assert(IDX_STATS_ENDPOINT_SERVER_BUSY_COUNT!==undefined);
125129
assert(IDX_STATS_ENDPOINT_RETRY_COUNT!==undefined);
130+
assert(IDX_STATS_ENDPOINT_RETRY_RATE_LIMITED!==undefined);
126131
assert(IDX_STATS_ENDPOINT_VERSION_NEGOTIATION_COUNT!==undefined);
132+
assert(IDX_STATS_ENDPOINT_VERSION_NEGOTIATION_RATE_LIMITED!==undefined);
127133
assert(IDX_STATS_ENDPOINT_STATELESS_RESET_COUNT!==undefined);
134+
assert(IDX_STATS_ENDPOINT_STATELESS_RESET_RATE_LIMITED!==undefined);
128135
assert(IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_COUNT!==undefined);
136+
assert(IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_RATE_LIMITED!==undefined);
129137
assert(IDX_STATS_SESSION_CREATED_AT!==undefined);
130138
assert(IDX_STATS_SESSION_DESTROYED_AT!==undefined);
131139
assert(IDX_STATS_SESSION_CLOSING_AT!==undefined);
@@ -280,24 +288,48 @@ class QuicEndpointStats {
280288
returnthis.#handle[IDX_STATS_ENDPOINT_RETRY_COUNT];
281289
}
282290

291+
/** @type {bigint} */
292+
getretryRateLimited(){
293+
assertIsQuicEndpointStats(this);
294+
returnthis.#handle[IDX_STATS_ENDPOINT_RETRY_RATE_LIMITED];
295+
}
296+
283297
/** @type {bigint} */
284298
getversionNegotiationCount(){
285299
assertIsQuicEndpointStats(this);
286300
returnthis.#handle[IDX_STATS_ENDPOINT_VERSION_NEGOTIATION_COUNT];
287301
}
288302

303+
/** @type {bigint} */
304+
getversionNegotiationRateLimited(){
305+
assertIsQuicEndpointStats(this);
306+
returnthis.#handle[IDX_STATS_ENDPOINT_VERSION_NEGOTIATION_RATE_LIMITED];
307+
}
308+
289309
/** @type {bigint} */
290310
getstatelessResetCount(){
291311
assertIsQuicEndpointStats(this);
292312
returnthis.#handle[IDX_STATS_ENDPOINT_STATELESS_RESET_COUNT];
293313
}
294314

315+
/** @type {bigint} */
316+
getstatelessResetRateLimited(){
317+
assertIsQuicEndpointStats(this);
318+
returnthis.#handle[IDX_STATS_ENDPOINT_STATELESS_RESET_RATE_LIMITED];
319+
}
320+
295321
/** @type {bigint} */
296322
getimmediateCloseCount(){
297323
assertIsQuicEndpointStats(this);
298324
returnthis.#handle[IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_COUNT];
299325
}
300326

327+
/** @type {bigint} */
328+
getimmediateCloseRateLimited(){
329+
assertIsQuicEndpointStats(this);
330+
returnthis.#handle[IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_RATE_LIMITED];
331+
}
332+
301333
toString(){
302334
returnJSONStringify(this.toJSON());
303335
}
@@ -315,9 +347,13 @@ class QuicEndpointStats {
315347
clientSessions,
316348
serverBusyCount,
317349
retryCount,
350+
retryRateLimited,
318351
versionNegotiationCount,
352+
versionNegotiationRateLimited,
319353
statelessResetCount,
354+
statelessResetRateLimited,
320355
immediateCloseCount,
356+
immediateCloseRateLimited,
321357
}=this;
322358
return{
323359
__proto__: null,
@@ -334,9 +370,13 @@ class QuicEndpointStats {
334370
clientSessions: `${clientSessions}`,
335371
serverBusyCount: `${serverBusyCount}`,
336372
retryCount: `${retryCount}`,
373+
retryRateLimited: `${retryRateLimited}`,
337374
versionNegotiationCount: `${versionNegotiationCount}`,
375+
versionNegotiationRateLimited: `${versionNegotiationRateLimited}`,
338376
statelessResetCount: `${statelessResetCount}`,
377+
statelessResetRateLimited: `${statelessResetRateLimited}`,
339378
immediateCloseCount: `${immediateCloseCount}`,
379+
immediateCloseRateLimited: `${immediateCloseRateLimited}`,
340380
};
341381
}
342382

@@ -363,9 +403,13 @@ class QuicEndpointStats {
363403
clientSessions,
364404
serverBusyCount,
365405
retryCount,
406+
retryRateLimited,
366407
versionNegotiationCount,
408+
versionNegotiationRateLimited,
367409
statelessResetCount,
410+
statelessResetRateLimited,
368411
immediateCloseCount,
412+
immediateCloseRateLimited,
369413
}=this;
370414

371415
return`QuicEndpointStats ${inspect({
@@ -380,9 +424,13 @@ class QuicEndpointStats {
380424
clientSessions,
381425
serverBusyCount,
382426
retryCount,
427+
retryRateLimited,
383428
versionNegotiationCount,
429+
versionNegotiationRateLimited,
384430
statelessResetCount,
431+
statelessResetRateLimited,
385432
immediateCloseCount,
433+
immediateCloseRateLimited,
386434
},opts)}`;
387435
}
388436

‎node.gyp‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -449,6 +449,7 @@
449449
'test/cctest/test_quic_cid.cc',
450450
'test/cctest/test_quic_error.cc',
451451
'test/cctest/test_quic_preferredaddress.cc',
452+
'test/cctest/test_quic_tokenbucket.cc',
452453
'test/cctest/test_quic_tokens.cc',
453454
],
454455
'node_cctest_inspector_sources': [

‎src/quic/bindingdata.h‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,8 +117,14 @@ class SessionManager;
117117
V(idle_timeout, "idleTimeout") \
118118
V(max_idle_timeout, "maxIdleTimeout") \
119119
V(max_payload_size, "maxPayloadSize") \
120-
V(max_retries, "maxRetries") \
121-
V(max_stateless_resets, "maxStatelessResetsPerHost") \
120+
V(retry_rate, "retryRate") \
121+
V(retry_burst, "retryBurst") \
122+
V(stateless_reset_rate, "statelessResetRate") \
123+
V(stateless_reset_burst, "statelessResetBurst") \
124+
V(version_negotiation_rate, "versionNegotiationRate") \
125+
V(version_negotiation_burst, "versionNegotiationBurst") \
126+
V(immediate_close_rate, "immediateCloseRate") \
127+
V(immediate_close_burst, "immediateCloseBurst") \
122128
V(max_stream_window, "maxStreamWindow") \
123129
V(max_window, "maxWindow") \
124130
V(min_version, "minVersion") \

‎src/quic/defs.h‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,24 @@ constexpr auto kSocketAddressInfoTimeout = 60 * NGTCP2_SECONDS;
360360
constexprsize_tkMaxVectorCount = 16;
361361
constexpr stream_id kMaxStreamId = std::numeric_limits<stream_id>::max();
362362

363+
// A token bucket rate limiter using lazy refill. No timer needed — tokens
364+
// are computed on demand from the elapsed time since the last check.
365+
// Used to cap the total rate of stateless responses (retry, reset,
366+
// version negotiation, immediate close) regardless of source address,
367+
// preventing spoofed-source floods from bypassing per-host limits.
368+
structTokenBucketfinal {
369+
double rate; // tokens per second (refill rate)
370+
double burst; // maximum tokens (bucket capacity)
371+
double tokens; // current token count
372+
uint64_t last_ts; // last refill timestamp (nanoseconds, uv_hrtime)
373+
374+
TokenBucket(double rate, double burst);
375+
376+
// Try to consume one token. Refills based on elapsed time, then
377+
// attempts to consume. Returns true if the request is allowed.
378+
boolconsume();
379+
};
380+
363381
classDebugIndentScopefinal {
364382
public:
365383
inlineDebugIndentScope() { ++indent_; }

0 commit comments

Comments
 (0)