Commit 11778a7

Browse files
jasnelladuh95
authored andcommitted
quic: add session creation 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 8107f1b commit 11778a7

9 files changed

Lines changed: 111 additions & 2 deletions

File tree

‎doc/api/quic.md‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -776,6 +776,12 @@ added: v23.8.0
776776
* Type: {bigint} The total number of immediate connection close packets
777777
dropped by the global rate limiter. Read only.
778778

779+
### `endpointStats.sessionCreationRateLimited`
780+
781+
* Type: {bigint} The total number of session creation attempts dropped by the
782+
per-host rate limiter. Read only. A non-zero value indicates one or more
783+
remote addresses are creating sessions faster than the configured rate allows.
784+
779785
## Class: `QuicSession`
780786

781787
<!-- YAML
@@ -2543,6 +2549,26 @@ send per second.
25432549
The maximum burst of immediate connection close packets allowed before rate
25442550
limiting takes effect.
25452551

2552+
#### `endpointOptions.sessionCreationRate`
2553+
2554+
* Type: {number}
2555+
***Default:**`50`
2556+
2557+
The maximum number of new sessions that a single remote address can create per
2558+
second. This is a per-host rate limit tracked in the address validation LRU
2559+
cache. It prevents a validated remote address from churning through sessions
2560+
(rapidly opening and abandoning connections) faster than the server can handle.
2561+
For benchmarking where traffic comes from a single source, set this to a high
2562+
value.
2563+
2564+
#### `endpointOptions.sessionCreationBurst`
2565+
2566+
* Type: {number}
2567+
***Default:**`100`
2568+
2569+
The maximum burst of new session creations allowed from a single remote address
2570+
before rate limiting takes effect.
2571+
25462572
#### `endpointOptions.retryTokenExpiration`
25472573

25482574
<!-- YAML

‎lib/internal/quic/quic.js‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,8 @@ const endpointRegistry = new SafeSet();
315315
* @property {number} [versionNegotiationBurst] Burst capacity for version negotiation rate limiter
316316
* @property {number} [immediateCloseRate] Global rate limit for immediate close packets (per second)
317317
* @property {number} [immediateCloseBurst] Burst capacity for immediate close rate limiter
318+
* @property {number} [sessionCreationRate] Per-host rate limit for session creation (per second)
319+
* @property {number} [sessionCreationBurst] Per-host burst capacity for session creation rate limiter
318320
* @property {ArrayBufferView} [resetTokenSecret] The reset token secret
319321
* @property {bigint|number} [retryTokenExpiration] The retry token expiration
320322
* @property {number} [rxDiagnosticLoss] The receive diagnostic loss probability (range 0.0-1.0)
@@ -4013,6 +4015,8 @@ class QuicEndpoint {
40134015
versionNegotiationBurst,
40144016
immediateCloseRate,
40154017
immediateCloseBurst,
4018+
sessionCreationRate,
4019+
sessionCreationBurst,
40164020
rxDiagnosticLoss,
40174021
txDiagnosticLoss,
40184022
udpReceiveBufferSize,
@@ -4056,6 +4060,8 @@ class QuicEndpoint {
40564060
versionNegotiationBurst,
40574061
immediateCloseRate,
40584062
immediateCloseBurst,
4063+
sessionCreationRate,
4064+
sessionCreationBurst,
40594065
rxDiagnosticLoss,
40604066
txDiagnosticLoss,
40614067
udpReceiveBufferSize,

‎lib/internal/quic/stats.js‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ const {
6767
IDX_STATS_ENDPOINT_STATELESS_RESET_RATE_LIMITED,
6868
IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_COUNT,
6969
IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_RATE_LIMITED,
70+
IDX_STATS_ENDPOINT_SESSION_CREATION_RATE_LIMITED,
7071

7172
IDX_STATS_SESSION_CREATED_AT,
7273
IDX_STATS_SESSION_DESTROYED_AT,
@@ -134,6 +135,7 @@ assert(IDX_STATS_ENDPOINT_STATELESS_RESET_COUNT !== undefined);
134135
assert(IDX_STATS_ENDPOINT_STATELESS_RESET_RATE_LIMITED!==undefined);
135136
assert(IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_COUNT!==undefined);
136137
assert(IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_RATE_LIMITED!==undefined);
138+
assert(IDX_STATS_ENDPOINT_SESSION_CREATION_RATE_LIMITED!==undefined);
137139
assert(IDX_STATS_SESSION_CREATED_AT!==undefined);
138140
assert(IDX_STATS_SESSION_DESTROYED_AT!==undefined);
139141
assert(IDX_STATS_SESSION_CLOSING_AT!==undefined);
@@ -330,6 +332,12 @@ class QuicEndpointStats {
330332
returnthis.#handle[IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_RATE_LIMITED];
331333
}
332334

335+
/** @type {bigint} */
336+
getsessionCreationRateLimited(){
337+
assertIsQuicEndpointStats(this);
338+
returnthis.#handle[IDX_STATS_ENDPOINT_SESSION_CREATION_RATE_LIMITED];
339+
}
340+
333341
toString(){
334342
returnJSONStringify(this.toJSON());
335343
}
@@ -354,6 +362,7 @@ class QuicEndpointStats {
354362
statelessResetRateLimited,
355363
immediateCloseCount,
356364
immediateCloseRateLimited,
365+
sessionCreationRateLimited,
357366
}=this;
358367
return{
359368
__proto__: null,
@@ -377,6 +386,7 @@ class QuicEndpointStats {
377386
statelessResetRateLimited: `${statelessResetRateLimited}`,
378387
immediateCloseCount: `${immediateCloseCount}`,
379388
immediateCloseRateLimited: `${immediateCloseRateLimited}`,
389+
sessionCreationRateLimited: `${sessionCreationRateLimited}`,
380390
};
381391
}
382392

@@ -410,6 +420,7 @@ class QuicEndpointStats {
410420
statelessResetRateLimited,
411421
immediateCloseCount,
412422
immediateCloseRateLimited,
423+
sessionCreationRateLimited,
413424
}=this;
414425

415426
return`QuicEndpointStats ${inspect({
@@ -431,6 +442,7 @@ class QuicEndpointStats {
431442
statelessResetRateLimited,
432443
immediateCloseCount,
433444
immediateCloseRateLimited,
445+
sessionCreationRateLimited,
434446
},opts)}`;
435447
}
436448

‎src/quic/bindingdata.h‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,8 @@ class SessionManager;
125125
V(version_negotiation_burst, "versionNegotiationBurst") \
126126
V(immediate_close_rate, "immediateCloseRate") \
127127
V(immediate_close_burst, "immediateCloseBurst") \
128+
V(session_creation_rate, "sessionCreationRate") \
129+
V(session_creation_burst, "sessionCreationBurst") \
128130
V(max_stream_window, "maxStreamWindow") \
129131
V(max_window, "maxWindow") \
130132
V(min_version, "minVersion") \

‎src/quic/defs.h‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -371,8 +371,15 @@ struct TokenBucket final {
371371
double tokens; // current token count
372372
uint64_t last_ts; // last refill timestamp (nanoseconds, uv_hrtime)
373373

374+
TokenBucket() : rate(0), burst(0), tokens(0), last_ts(0) {}
374375
TokenBucket(double rate, double burst);
375376

377+
// Reinitialize the bucket with new rate/burst parameters if it
378+
// hasn't been initialized yet (last_ts == 0). Used for per-host
379+
// buckets in the address LRU where the rate/burst aren't known
380+
// at construction time.
381+
voidInitOnce(double r, double b);
382+
376383
// Try to consume one token. Refills based on elapsed time, then
377384
// attempts to consume. Returns true if the request is allowed.
378385
boolconsume();

‎src/quic/endpoint.cc‎

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,8 @@ namespace quic {
7878
V(STATELESS_RESET_COUNT, stateless_reset_count) \
7979
V(STATELESS_RESET_RATE_LIMITED, stateless_reset_rate_limited) \
8080
V(IMMEDIATE_CLOSE_COUNT, immediate_close_count) \
81-
V(IMMEDIATE_CLOSE_RATE_LIMITED, immediate_close_rate_limited)
81+
V(IMMEDIATE_CLOSE_RATE_LIMITED, immediate_close_rate_limited) \
82+
V(SESSION_CREATION_RATE_LIMITED, session_creation_rate_limited)
8283

8384
structEndpoint::State {
8485
#defineV(_, name, type) type name;
@@ -91,6 +92,15 @@ STAT_STRUCT(Endpoint, ENDPOINT)
9192
TokenBucket::TokenBucket(double rate, double burst)
9293
: rate(rate), burst(burst), tokens(burst), last_ts(uv_hrtime()) {}
9394

95+
voidTokenBucket::InitOnce(double r, double b) {
96+
if (last_ts == 0) {
97+
rate = r;
98+
burst = b;
99+
tokens = b;
100+
last_ts = uv_hrtime();
101+
}
102+
}
103+
94104
// Try to consume one token. Refills based on elapsed time, then
95105
// attempts to consume. Returns true if the request is allowed.
96106
boolTokenBucket::consume() {
@@ -227,7 +237,8 @@ Maybe<Endpoint::Options> Endpoint::Options::From(Environment* env,
227237
!SET(retry_rate) || !SET(retry_burst) || !SET(stateless_reset_rate) ||
228238
!SET(stateless_reset_burst) || !SET(version_negotiation_rate) ||
229239
!SET(version_negotiation_burst) || !SET(immediate_close_rate) ||
230-
!SET(immediate_close_burst) ||
240+
!SET(immediate_close_burst) || !SET(session_creation_rate) ||
241+
!SET(session_creation_burst) ||
231242
#ifdef DEBUG
232243
!SET(rx_loss) || !SET(tx_loss) ||
233244
#endif
@@ -296,6 +307,11 @@ std::string Endpoint::Options::ToString() const {
296307
"immediate close rate: " + std::to_string(immediate_close_rate) + "/s";
297308
res += prefix +
298309
"immediate close burst: " + std::to_string(immediate_close_burst);
310+
res += prefix +
311+
"session creation rate: " + std::to_string(session_creation_rate) +
312+
"/s";
313+
res += prefix +
314+
"session creation burst: " + std::to_string(session_creation_burst);
299315
res += prefix + "validate address: " + boolToString(validate_address);
300316
res += prefix +
301317
"disable stateless reset: " + boolToString(disable_stateless_reset);
@@ -1331,6 +1347,19 @@ void Endpoint::Receive(const uint8_t* data,
13311347
// as a server, then we cannot accept the initial packet.
13321348
if (is_closed() || is_closing() || !is_listening()) return;
13331349

1350+
// Per-host session creation rate limit. The bucket is initialized
1351+
// on first access with the configured rate/burst from options.
1352+
auto info = addr_validation_lru_.Upsert(config.remote_address);
1353+
info->session_creation_bucket.InitOnce(options_.session_creation_rate,
1354+
options_.session_creation_burst);
1355+
if (!info->session_creation_bucket.consume()) {
1356+
Debug(this,
1357+
"Session creation rate limit exceeded for %s",
1358+
config.remote_address);
1359+
STAT_INCREMENT(Stats, session_creation_rate_limited);
1360+
return;
1361+
}
1362+
13341363
Debug(this, "Creating new session for %s", config.dcid);
13351364

13361365
std::optional<SessionTicket> no_ticket = std::nullopt;

‎src/quic/endpoint.h‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,14 @@ class Endpoint final : public AsyncWrap, public Packet::Listener {
4444
staticconstexprdoubleDEFAULT_IMMEDIATE_CLOSE_RATE = 100;
4545
staticconstexprdoubleDEFAULT_IMMEDIATE_CLOSE_BURST = 200;
4646

47+
// Per-host session creation rate limit. This is tracked per validated
48+
// remote address in the address LRU, preventing a single source from
49+
// churning through sessions faster than the server can handle. Unlike
50+
// the global stateless response buckets, this only applies after address
51+
// validation (spoofed sources can't reach this path).
52+
staticconstexprdoubleDEFAULT_SESSION_CREATION_RATE = 50;
53+
staticconstexprdoubleDEFAULT_SESSION_CREATION_BURST = 100;
54+
4755
// Endpoint configuration options
4856
structOptionsfinal : public MemoryRetainer {
4957
// The local socket address to which the UDP port will be bound. The port
@@ -83,6 +91,12 @@ class Endpoint final : public AsyncWrap, public Packet::Listener {
8391
double immediate_close_rate = DEFAULT_IMMEDIATE_CLOSE_RATE;
8492
double immediate_close_burst = DEFAULT_IMMEDIATE_CLOSE_BURST;
8593

94+
// Per-host session creation rate limit. Tracked per validated remote
95+
// address in the address LRU. Set to high values for benchmarking
96+
// where traffic comes from a single source.
97+
double session_creation_rate = DEFAULT_SESSION_CREATION_RATE;
98+
double session_creation_burst = DEFAULT_SESSION_CREATION_BURST;
99+
86100
// The validate_address parameter instructs the Endpoint to perform explicit
87101
// address validation using retry tokens. This is strongly recommended and
88102
// should only be disabled in trusted, closed environments as a performance
@@ -452,6 +466,7 @@ class Endpoint final : public AsyncWrap, public Packet::Listener {
452466
structTypefinal {
453467
uint64_t timestamp;
454468
bool validated;
469+
TokenBucket session_creation_bucket;
455470
};
456471

457472
staticboolCheckExpired(const SocketAddress& address, const Type& type);

‎test/parallel/test-quic-internal-endpoint-options.mjs‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,16 @@ const cases = [
9999
valid: [0,1,10,100.5,1000],
100100
invalid: [-1,'a',null,false,true,{},[],()=>{}]
101101
},
102+
{
103+
key: 'sessionCreationRate',
104+
valid: [0,1,10,100.5,1000,Infinity],
105+
invalid: [-1,'a',null,false,true,{},[],()=>{}]
106+
},
107+
{
108+
key: 'sessionCreationBurst',
109+
valid: [0,1,10,100.5,1000,Infinity],
110+
invalid: [-1,'a',null,false,true,{},[],()=>{}]
111+
},
102112
{
103113
key: 'validateAddress',
104114
valid: [true,false,0,1,'a'],

‎test/parallel/test-quic-internal-endpoint-stats-state.mjs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ const {
9595
strictEqual(typeofendpoint.stats.statelessResetRateLimited,'bigint');
9696
strictEqual(typeofendpoint.stats.immediateCloseCount,'bigint');
9797
strictEqual(typeofendpoint.stats.immediateCloseRateLimited,'bigint');
98+
strictEqual(typeofendpoint.stats.sessionCreationRateLimited,'bigint');
9899

99100
deepStrictEqual(Object.keys(endpoint.stats.toJSON()),[
100101
'connected',
@@ -115,6 +116,7 @@ const {
115116
'statelessResetRateLimited',
116117
'immediateCloseCount',
117118
'immediateCloseRateLimited',
119+
'sessionCreationRateLimited',
118120
]);
119121
strictEqual(typeofinspect(endpoint.stats),'string');
120122
}

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 11778a7

Browse files
jasnelladuh95
authored andcommitted
quic: add session creation 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 8107f1b commit 11778a7

9 files changed

Lines changed: 111 additions & 2 deletions

File tree

‎doc/api/quic.md‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -776,6 +776,12 @@ added: v23.8.0
776776
* Type: {bigint} The total number of immediate connection close packets
777777
dropped by the global rate limiter. Read only.
778778

779+
### `endpointStats.sessionCreationRateLimited`
780+
781+
* Type: {bigint} The total number of session creation attempts dropped by the
782+
per-host rate limiter. Read only. A non-zero value indicates one or more
783+
remote addresses are creating sessions faster than the configured rate allows.
784+
779785
## Class: `QuicSession`
780786

781787
<!-- YAML
@@ -2543,6 +2549,26 @@ send per second.
25432549
The maximum burst of immediate connection close packets allowed before rate
25442550
limiting takes effect.
25452551

2552+
#### `endpointOptions.sessionCreationRate`
2553+
2554+
* Type: {number}
2555+
***Default:**`50`
2556+
2557+
The maximum number of new sessions that a single remote address can create per
2558+
second. This is a per-host rate limit tracked in the address validation LRU
2559+
cache. It prevents a validated remote address from churning through sessions
2560+
(rapidly opening and abandoning connections) faster than the server can handle.
2561+
For benchmarking where traffic comes from a single source, set this to a high
2562+
value.
2563+
2564+
#### `endpointOptions.sessionCreationBurst`
2565+
2566+
* Type: {number}
2567+
***Default:**`100`
2568+
2569+
The maximum burst of new session creations allowed from a single remote address
2570+
before rate limiting takes effect.
2571+
25462572
#### `endpointOptions.retryTokenExpiration`
25472573

25482574
<!-- YAML

‎lib/internal/quic/quic.js‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,8 @@ const endpointRegistry = new SafeSet();
315315
* @property {number} [versionNegotiationBurst] Burst capacity for version negotiation rate limiter
316316
* @property {number} [immediateCloseRate] Global rate limit for immediate close packets (per second)
317317
* @property {number} [immediateCloseBurst] Burst capacity for immediate close rate limiter
318+
* @property {number} [sessionCreationRate] Per-host rate limit for session creation (per second)
319+
* @property {number} [sessionCreationBurst] Per-host burst capacity for session creation rate limiter
318320
* @property {ArrayBufferView} [resetTokenSecret] The reset token secret
319321
* @property {bigint|number} [retryTokenExpiration] The retry token expiration
320322
* @property {number} [rxDiagnosticLoss] The receive diagnostic loss probability (range 0.0-1.0)
@@ -4013,6 +4015,8 @@ class QuicEndpoint {
40134015
versionNegotiationBurst,
40144016
immediateCloseRate,
40154017
immediateCloseBurst,
4018+
sessionCreationRate,
4019+
sessionCreationBurst,
40164020
rxDiagnosticLoss,
40174021
txDiagnosticLoss,
40184022
udpReceiveBufferSize,
@@ -4056,6 +4060,8 @@ class QuicEndpoint {
40564060
versionNegotiationBurst,
40574061
immediateCloseRate,
40584062
immediateCloseBurst,
4063+
sessionCreationRate,
4064+
sessionCreationBurst,
40594065
rxDiagnosticLoss,
40604066
txDiagnosticLoss,
40614067
udpReceiveBufferSize,

‎lib/internal/quic/stats.js‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ const {
6767
IDX_STATS_ENDPOINT_STATELESS_RESET_RATE_LIMITED,
6868
IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_COUNT,
6969
IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_RATE_LIMITED,
70+
IDX_STATS_ENDPOINT_SESSION_CREATION_RATE_LIMITED,
7071

7172
IDX_STATS_SESSION_CREATED_AT,
7273
IDX_STATS_SESSION_DESTROYED_AT,
@@ -134,6 +135,7 @@ assert(IDX_STATS_ENDPOINT_STATELESS_RESET_COUNT !== undefined);
134135
assert(IDX_STATS_ENDPOINT_STATELESS_RESET_RATE_LIMITED!==undefined);
135136
assert(IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_COUNT!==undefined);
136137
assert(IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_RATE_LIMITED!==undefined);
138+
assert(IDX_STATS_ENDPOINT_SESSION_CREATION_RATE_LIMITED!==undefined);
137139
assert(IDX_STATS_SESSION_CREATED_AT!==undefined);
138140
assert(IDX_STATS_SESSION_DESTROYED_AT!==undefined);
139141
assert(IDX_STATS_SESSION_CLOSING_AT!==undefined);
@@ -330,6 +332,12 @@ class QuicEndpointStats {
330332
returnthis.#handle[IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_RATE_LIMITED];
331333
}
332334

335+
/** @type {bigint} */
336+
getsessionCreationRateLimited(){
337+
assertIsQuicEndpointStats(this);
338+
returnthis.#handle[IDX_STATS_ENDPOINT_SESSION_CREATION_RATE_LIMITED];
339+
}
340+
333341
toString(){
334342
returnJSONStringify(this.toJSON());
335343
}
@@ -354,6 +362,7 @@ class QuicEndpointStats {
354362
statelessResetRateLimited,
355363
immediateCloseCount,
356364
immediateCloseRateLimited,
365+
sessionCreationRateLimited,
357366
}=this;
358367
return{
359368
__proto__: null,
@@ -377,6 +386,7 @@ class QuicEndpointStats {
377386
statelessResetRateLimited: `${statelessResetRateLimited}`,
378387
immediateCloseCount: `${immediateCloseCount}`,
379388
immediateCloseRateLimited: `${immediateCloseRateLimited}`,
389+
sessionCreationRateLimited: `${sessionCreationRateLimited}`,
380390
};
381391
}
382392

@@ -410,6 +420,7 @@ class QuicEndpointStats {
410420
statelessResetRateLimited,
411421
immediateCloseCount,
412422
immediateCloseRateLimited,
423+
sessionCreationRateLimited,
413424
}=this;
414425

415426
return`QuicEndpointStats ${inspect({
@@ -431,6 +442,7 @@ class QuicEndpointStats {
431442
statelessResetRateLimited,
432443
immediateCloseCount,
433444
immediateCloseRateLimited,
445+
sessionCreationRateLimited,
434446
},opts)}`;
435447
}
436448

‎src/quic/bindingdata.h‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,8 @@ class SessionManager;
125125
V(version_negotiation_burst, "versionNegotiationBurst") \
126126
V(immediate_close_rate, "immediateCloseRate") \
127127
V(immediate_close_burst, "immediateCloseBurst") \
128+
V(session_creation_rate, "sessionCreationRate") \
129+
V(session_creation_burst, "sessionCreationBurst") \
128130
V(max_stream_window, "maxStreamWindow") \
129131
V(max_window, "maxWindow") \
130132
V(min_version, "minVersion") \

‎src/quic/defs.h‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -371,8 +371,15 @@ struct TokenBucket final {
371371
double tokens; // current token count
372372
uint64_t last_ts; // last refill timestamp (nanoseconds, uv_hrtime)
373373

374+
TokenBucket() : rate(0), burst(0), tokens(0), last_ts(0) {}
374375
TokenBucket(double rate, double burst);
375376

377+
// Reinitialize the bucket with new rate/burst parameters if it
378+
// hasn't been initialized yet (last_ts == 0). Used for per-host
379+
// buckets in the address LRU where the rate/burst aren't known
380+
// at construction time.
381+
voidInitOnce(double r, double b);
382+
376383
// Try to consume one token. Refills based on elapsed time, then
377384
// attempts to consume. Returns true if the request is allowed.
378385
boolconsume();

‎src/quic/endpoint.cc‎

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,8 @@ namespace quic {
7878
V(STATELESS_RESET_COUNT, stateless_reset_count) \
7979
V(STATELESS_RESET_RATE_LIMITED, stateless_reset_rate_limited) \
8080
V(IMMEDIATE_CLOSE_COUNT, immediate_close_count) \
81-
V(IMMEDIATE_CLOSE_RATE_LIMITED, immediate_close_rate_limited)
81+
V(IMMEDIATE_CLOSE_RATE_LIMITED, immediate_close_rate_limited) \
82+
V(SESSION_CREATION_RATE_LIMITED, session_creation_rate_limited)
8283

8384
structEndpoint::State {
8485
#defineV(_, name, type) type name;
@@ -91,6 +92,15 @@ STAT_STRUCT(Endpoint, ENDPOINT)
9192
TokenBucket::TokenBucket(double rate, double burst)
9293
: rate(rate), burst(burst), tokens(burst), last_ts(uv_hrtime()) {}
9394

95+
voidTokenBucket::InitOnce(double r, double b) {
96+
if (last_ts == 0) {
97+
rate = r;
98+
burst = b;
99+
tokens = b;
100+
last_ts = uv_hrtime();
101+
}
102+
}
103+
94104
// Try to consume one token. Refills based on elapsed time, then
95105
// attempts to consume. Returns true if the request is allowed.
96106
boolTokenBucket::consume() {
@@ -227,7 +237,8 @@ Maybe<Endpoint::Options> Endpoint::Options::From(Environment* env,
227237
!SET(retry_rate) || !SET(retry_burst) || !SET(stateless_reset_rate) ||
228238
!SET(stateless_reset_burst) || !SET(version_negotiation_rate) ||
229239
!SET(version_negotiation_burst) || !SET(immediate_close_rate) ||
230-
!SET(immediate_close_burst) ||
240+
!SET(immediate_close_burst) || !SET(session_creation_rate) ||
241+
!SET(session_creation_burst) ||
231242
#ifdef DEBUG
232243
!SET(rx_loss) || !SET(tx_loss) ||
233244
#endif
@@ -296,6 +307,11 @@ std::string Endpoint::Options::ToString() const {
296307
"immediate close rate: " + std::to_string(immediate_close_rate) + "/s";
297308
res += prefix +
298309
"immediate close burst: " + std::to_string(immediate_close_burst);
310+
res += prefix +
311+
"session creation rate: " + std::to_string(session_creation_rate) +
312+
"/s";
313+
res += prefix +
314+
"session creation burst: " + std::to_string(session_creation_burst);
299315
res += prefix + "validate address: " + boolToString(validate_address);
300316
res += prefix +
301317
"disable stateless reset: " + boolToString(disable_stateless_reset);
@@ -1331,6 +1347,19 @@ void Endpoint::Receive(const uint8_t* data,
13311347
// as a server, then we cannot accept the initial packet.
13321348
if (is_closed() || is_closing() || !is_listening()) return;
13331349

1350+
// Per-host session creation rate limit. The bucket is initialized
1351+
// on first access with the configured rate/burst from options.
1352+
auto info = addr_validation_lru_.Upsert(config.remote_address);
1353+
info->session_creation_bucket.InitOnce(options_.session_creation_rate,
1354+
options_.session_creation_burst);
1355+
if (!info->session_creation_bucket.consume()) {
1356+
Debug(this,
1357+
"Session creation rate limit exceeded for %s",
1358+
config.remote_address);
1359+
STAT_INCREMENT(Stats, session_creation_rate_limited);
1360+
return;
1361+
}
1362+
13341363
Debug(this, "Creating new session for %s", config.dcid);
13351364

13361365
std::optional<SessionTicket> no_ticket = std::nullopt;

‎src/quic/endpoint.h‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,14 @@ class Endpoint final : public AsyncWrap, public Packet::Listener {
4444
staticconstexprdoubleDEFAULT_IMMEDIATE_CLOSE_RATE = 100;
4545
staticconstexprdoubleDEFAULT_IMMEDIATE_CLOSE_BURST = 200;
4646

47+
// Per-host session creation rate limit. This is tracked per validated
48+
// remote address in the address LRU, preventing a single source from
49+
// churning through sessions faster than the server can handle. Unlike
50+
// the global stateless response buckets, this only applies after address
51+
// validation (spoofed sources can't reach this path).
52+
staticconstexprdoubleDEFAULT_SESSION_CREATION_RATE = 50;
53+
staticconstexprdoubleDEFAULT_SESSION_CREATION_BURST = 100;
54+
4755
// Endpoint configuration options
4856
structOptionsfinal : public MemoryRetainer {
4957
// The local socket address to which the UDP port will be bound. The port
@@ -83,6 +91,12 @@ class Endpoint final : public AsyncWrap, public Packet::Listener {
8391
double immediate_close_rate = DEFAULT_IMMEDIATE_CLOSE_RATE;
8492
double immediate_close_burst = DEFAULT_IMMEDIATE_CLOSE_BURST;
8593

94+
// Per-host session creation rate limit. Tracked per validated remote
95+
// address in the address LRU. Set to high values for benchmarking
96+
// where traffic comes from a single source.
97+
double session_creation_rate = DEFAULT_SESSION_CREATION_RATE;
98+
double session_creation_burst = DEFAULT_SESSION_CREATION_BURST;
99+
86100
// The validate_address parameter instructs the Endpoint to perform explicit
87101
// address validation using retry tokens. This is strongly recommended and
88102
// should only be disabled in trusted, closed environments as a performance
@@ -452,6 +466,7 @@ class Endpoint final : public AsyncWrap, public Packet::Listener {
452466
structTypefinal {
453467
uint64_t timestamp;
454468
bool validated;
469+
TokenBucket session_creation_bucket;
455470
};
456471

457472
staticboolCheckExpired(const SocketAddress& address, const Type& type);

‎test/parallel/test-quic-internal-endpoint-options.mjs‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,16 @@ const cases = [
9999
valid: [0,1,10,100.5,1000],
100100
invalid: [-1,'a',null,false,true,{},[],()=>{}]
101101
},
102+
{
103+
key: 'sessionCreationRate',
104+
valid: [0,1,10,100.5,1000,Infinity],
105+
invalid: [-1,'a',null,false,true,{},[],()=>{}]
106+
},
107+
{
108+
key: 'sessionCreationBurst',
109+
valid: [0,1,10,100.5,1000,Infinity],
110+
invalid: [-1,'a',null,false,true,{},[],()=>{}]
111+
},
102112
{
103113
key: 'validateAddress',
104114
valid: [true,false,0,1,'a'],

‎test/parallel/test-quic-internal-endpoint-stats-state.mjs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ const {
9595
strictEqual(typeofendpoint.stats.statelessResetRateLimited,'bigint');
9696
strictEqual(typeofendpoint.stats.immediateCloseCount,'bigint');
9797
strictEqual(typeofendpoint.stats.immediateCloseRateLimited,'bigint');
98+
strictEqual(typeofendpoint.stats.sessionCreationRateLimited,'bigint');
9899

99100
deepStrictEqual(Object.keys(endpoint.stats.toJSON()),[
100101
'connected',
@@ -115,6 +116,7 @@ const {
115116
'statelessResetRateLimited',
116117
'immediateCloseCount',
117118
'immediateCloseRateLimited',
119+
'sessionCreationRateLimited',
118120
]);
119121
strictEqual(typeofinspect(endpoint.stats),'string');
120122
}

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 11778a7

Browse files
jasnelladuh95
authored andcommitted
quic: add session creation 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 8107f1b commit 11778a7

9 files changed

Lines changed: 111 additions & 2 deletions

File tree

‎doc/api/quic.md‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -776,6 +776,12 @@ added: v23.8.0
776776
* Type: {bigint} The total number of immediate connection close packets
777777
dropped by the global rate limiter. Read only.
778778

779+
### `endpointStats.sessionCreationRateLimited`
780+
781+
* Type: {bigint} The total number of session creation attempts dropped by the
782+
per-host rate limiter. Read only. A non-zero value indicates one or more
783+
remote addresses are creating sessions faster than the configured rate allows.
784+
779785
## Class: `QuicSession`
780786

781787
<!-- YAML
@@ -2543,6 +2549,26 @@ send per second.
25432549
The maximum burst of immediate connection close packets allowed before rate
25442550
limiting takes effect.
25452551

2552+
#### `endpointOptions.sessionCreationRate`
2553+
2554+
* Type: {number}
2555+
***Default:**`50`
2556+
2557+
The maximum number of new sessions that a single remote address can create per
2558+
second. This is a per-host rate limit tracked in the address validation LRU
2559+
cache. It prevents a validated remote address from churning through sessions
2560+
(rapidly opening and abandoning connections) faster than the server can handle.
2561+
For benchmarking where traffic comes from a single source, set this to a high
2562+
value.
2563+
2564+
#### `endpointOptions.sessionCreationBurst`
2565+
2566+
* Type: {number}
2567+
***Default:**`100`
2568+
2569+
The maximum burst of new session creations allowed from a single remote address
2570+
before rate limiting takes effect.
2571+
25462572
#### `endpointOptions.retryTokenExpiration`
25472573

25482574
<!-- YAML

‎lib/internal/quic/quic.js‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,8 @@ const endpointRegistry = new SafeSet();
315315
* @property {number} [versionNegotiationBurst] Burst capacity for version negotiation rate limiter
316316
* @property {number} [immediateCloseRate] Global rate limit for immediate close packets (per second)
317317
* @property {number} [immediateCloseBurst] Burst capacity for immediate close rate limiter
318+
* @property {number} [sessionCreationRate] Per-host rate limit for session creation (per second)
319+
* @property {number} [sessionCreationBurst] Per-host burst capacity for session creation rate limiter
318320
* @property {ArrayBufferView} [resetTokenSecret] The reset token secret
319321
* @property {bigint|number} [retryTokenExpiration] The retry token expiration
320322
* @property {number} [rxDiagnosticLoss] The receive diagnostic loss probability (range 0.0-1.0)
@@ -4013,6 +4015,8 @@ class QuicEndpoint {
40134015
versionNegotiationBurst,
40144016
immediateCloseRate,
40154017
immediateCloseBurst,
4018+
sessionCreationRate,
4019+
sessionCreationBurst,
40164020
rxDiagnosticLoss,
40174021
txDiagnosticLoss,
40184022
udpReceiveBufferSize,
@@ -4056,6 +4060,8 @@ class QuicEndpoint {
40564060
versionNegotiationBurst,
40574061
immediateCloseRate,
40584062
immediateCloseBurst,
4063+
sessionCreationRate,
4064+
sessionCreationBurst,
40594065
rxDiagnosticLoss,
40604066
txDiagnosticLoss,
40614067
udpReceiveBufferSize,

‎lib/internal/quic/stats.js‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ const {
6767
IDX_STATS_ENDPOINT_STATELESS_RESET_RATE_LIMITED,
6868
IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_COUNT,
6969
IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_RATE_LIMITED,
70+
IDX_STATS_ENDPOINT_SESSION_CREATION_RATE_LIMITED,
7071

7172
IDX_STATS_SESSION_CREATED_AT,
7273
IDX_STATS_SESSION_DESTROYED_AT,
@@ -134,6 +135,7 @@ assert(IDX_STATS_ENDPOINT_STATELESS_RESET_COUNT !== undefined);
134135
assert(IDX_STATS_ENDPOINT_STATELESS_RESET_RATE_LIMITED!==undefined);
135136
assert(IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_COUNT!==undefined);
136137
assert(IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_RATE_LIMITED!==undefined);
138+
assert(IDX_STATS_ENDPOINT_SESSION_CREATION_RATE_LIMITED!==undefined);
137139
assert(IDX_STATS_SESSION_CREATED_AT!==undefined);
138140
assert(IDX_STATS_SESSION_DESTROYED_AT!==undefined);
139141
assert(IDX_STATS_SESSION_CLOSING_AT!==undefined);
@@ -330,6 +332,12 @@ class QuicEndpointStats {
330332
returnthis.#handle[IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_RATE_LIMITED];
331333
}
332334

335+
/** @type {bigint} */
336+
getsessionCreationRateLimited(){
337+
assertIsQuicEndpointStats(this);
338+
returnthis.#handle[IDX_STATS_ENDPOINT_SESSION_CREATION_RATE_LIMITED];
339+
}
340+
333341
toString(){
334342
returnJSONStringify(this.toJSON());
335343
}
@@ -354,6 +362,7 @@ class QuicEndpointStats {
354362
statelessResetRateLimited,
355363
immediateCloseCount,
356364
immediateCloseRateLimited,
365+
sessionCreationRateLimited,
357366
}=this;
358367
return{
359368
__proto__: null,
@@ -377,6 +386,7 @@ class QuicEndpointStats {
377386
statelessResetRateLimited: `${statelessResetRateLimited}`,
378387
immediateCloseCount: `${immediateCloseCount}`,
379388
immediateCloseRateLimited: `${immediateCloseRateLimited}`,
389+
sessionCreationRateLimited: `${sessionCreationRateLimited}`,
380390
};
381391
}
382392

@@ -410,6 +420,7 @@ class QuicEndpointStats {
410420
statelessResetRateLimited,
411421
immediateCloseCount,
412422
immediateCloseRateLimited,
423+
sessionCreationRateLimited,
413424
}=this;
414425

415426
return`QuicEndpointStats ${inspect({
@@ -431,6 +442,7 @@ class QuicEndpointStats {
431442
statelessResetRateLimited,
432443
immediateCloseCount,
433444
immediateCloseRateLimited,
445+
sessionCreationRateLimited,
434446
},opts)}`;
435447
}
436448

‎src/quic/bindingdata.h‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,8 @@ class SessionManager;
125125
V(version_negotiation_burst, "versionNegotiationBurst") \
126126
V(immediate_close_rate, "immediateCloseRate") \
127127
V(immediate_close_burst, "immediateCloseBurst") \
128+
V(session_creation_rate, "sessionCreationRate") \
129+
V(session_creation_burst, "sessionCreationBurst") \
128130
V(max_stream_window, "maxStreamWindow") \
129131
V(max_window, "maxWindow") \
130132
V(min_version, "minVersion") \

‎src/quic/defs.h‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -371,8 +371,15 @@ struct TokenBucket final {
371371
double tokens; // current token count
372372
uint64_t last_ts; // last refill timestamp (nanoseconds, uv_hrtime)
373373

374+
TokenBucket() : rate(0), burst(0), tokens(0), last_ts(0) {}
374375
TokenBucket(double rate, double burst);
375376

377+
// Reinitialize the bucket with new rate/burst parameters if it
378+
// hasn't been initialized yet (last_ts == 0). Used for per-host
379+
// buckets in the address LRU where the rate/burst aren't known
380+
// at construction time.
381+
voidInitOnce(double r, double b);
382+
376383
// Try to consume one token. Refills based on elapsed time, then
377384
// attempts to consume. Returns true if the request is allowed.
378385
boolconsume();

‎src/quic/endpoint.cc‎

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,8 @@ namespace quic {
7878
V(STATELESS_RESET_COUNT, stateless_reset_count) \
7979
V(STATELESS_RESET_RATE_LIMITED, stateless_reset_rate_limited) \
8080
V(IMMEDIATE_CLOSE_COUNT, immediate_close_count) \
81-
V(IMMEDIATE_CLOSE_RATE_LIMITED, immediate_close_rate_limited)
81+
V(IMMEDIATE_CLOSE_RATE_LIMITED, immediate_close_rate_limited) \
82+
V(SESSION_CREATION_RATE_LIMITED, session_creation_rate_limited)
8283

8384
structEndpoint::State {
8485
#defineV(_, name, type) type name;
@@ -91,6 +92,15 @@ STAT_STRUCT(Endpoint, ENDPOINT)
9192
TokenBucket::TokenBucket(double rate, double burst)
9293
: rate(rate), burst(burst), tokens(burst), last_ts(uv_hrtime()) {}
9394

95+
voidTokenBucket::InitOnce(double r, double b) {
96+
if (last_ts == 0) {
97+
rate = r;
98+
burst = b;
99+
tokens = b;
100+
last_ts = uv_hrtime();
101+
}
102+
}
103+
94104
// Try to consume one token. Refills based on elapsed time, then
95105
// attempts to consume. Returns true if the request is allowed.
96106
boolTokenBucket::consume() {
@@ -227,7 +237,8 @@ Maybe<Endpoint::Options> Endpoint::Options::From(Environment* env,
227237
!SET(retry_rate) || !SET(retry_burst) || !SET(stateless_reset_rate) ||
228238
!SET(stateless_reset_burst) || !SET(version_negotiation_rate) ||
229239
!SET(version_negotiation_burst) || !SET(immediate_close_rate) ||
230-
!SET(immediate_close_burst) ||
240+
!SET(immediate_close_burst) || !SET(session_creation_rate) ||
241+
!SET(session_creation_burst) ||
231242
#ifdef DEBUG
232243
!SET(rx_loss) || !SET(tx_loss) ||
233244
#endif
@@ -296,6 +307,11 @@ std::string Endpoint::Options::ToString() const {
296307
"immediate close rate: " + std::to_string(immediate_close_rate) + "/s";
297308
res += prefix +
298309
"immediate close burst: " + std::to_string(immediate_close_burst);
310+
res += prefix +
311+
"session creation rate: " + std::to_string(session_creation_rate) +
312+
"/s";
313+
res += prefix +
314+
"session creation burst: " + std::to_string(session_creation_burst);
299315
res += prefix + "validate address: " + boolToString(validate_address);
300316
res += prefix +
301317
"disable stateless reset: " + boolToString(disable_stateless_reset);
@@ -1331,6 +1347,19 @@ void Endpoint::Receive(const uint8_t* data,
13311347
// as a server, then we cannot accept the initial packet.
13321348
if (is_closed() || is_closing() || !is_listening()) return;
13331349

1350+
// Per-host session creation rate limit. The bucket is initialized
1351+
// on first access with the configured rate/burst from options.
1352+
auto info = addr_validation_lru_.Upsert(config.remote_address);
1353+
info->session_creation_bucket.InitOnce(options_.session_creation_rate,
1354+
options_.session_creation_burst);
1355+
if (!info->session_creation_bucket.consume()) {
1356+
Debug(this,
1357+
"Session creation rate limit exceeded for %s",
1358+
config.remote_address);
1359+
STAT_INCREMENT(Stats, session_creation_rate_limited);
1360+
return;
1361+
}
1362+
13341363
Debug(this, "Creating new session for %s", config.dcid);
13351364

13361365
std::optional<SessionTicket> no_ticket = std::nullopt;

‎src/quic/endpoint.h‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,14 @@ class Endpoint final : public AsyncWrap, public Packet::Listener {
4444
staticconstexprdoubleDEFAULT_IMMEDIATE_CLOSE_RATE = 100;
4545
staticconstexprdoubleDEFAULT_IMMEDIATE_CLOSE_BURST = 200;
4646

47+
// Per-host session creation rate limit. This is tracked per validated
48+
// remote address in the address LRU, preventing a single source from
49+
// churning through sessions faster than the server can handle. Unlike
50+
// the global stateless response buckets, this only applies after address
51+
// validation (spoofed sources can't reach this path).
52+
staticconstexprdoubleDEFAULT_SESSION_CREATION_RATE = 50;
53+
staticconstexprdoubleDEFAULT_SESSION_CREATION_BURST = 100;
54+
4755
// Endpoint configuration options
4856
structOptionsfinal : public MemoryRetainer {
4957
// The local socket address to which the UDP port will be bound. The port
@@ -83,6 +91,12 @@ class Endpoint final : public AsyncWrap, public Packet::Listener {
8391
double immediate_close_rate = DEFAULT_IMMEDIATE_CLOSE_RATE;
8492
double immediate_close_burst = DEFAULT_IMMEDIATE_CLOSE_BURST;
8593

94+
// Per-host session creation rate limit. Tracked per validated remote
95+
// address in the address LRU. Set to high values for benchmarking
96+
// where traffic comes from a single source.
97+
double session_creation_rate = DEFAULT_SESSION_CREATION_RATE;
98+
double session_creation_burst = DEFAULT_SESSION_CREATION_BURST;
99+
86100
// The validate_address parameter instructs the Endpoint to perform explicit
87101
// address validation using retry tokens. This is strongly recommended and
88102
// should only be disabled in trusted, closed environments as a performance
@@ -452,6 +466,7 @@ class Endpoint final : public AsyncWrap, public Packet::Listener {
452466
structTypefinal {
453467
uint64_t timestamp;
454468
bool validated;
469+
TokenBucket session_creation_bucket;
455470
};
456471

457472
staticboolCheckExpired(const SocketAddress& address, const Type& type);

‎test/parallel/test-quic-internal-endpoint-options.mjs‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,16 @@ const cases = [
9999
valid: [0,1,10,100.5,1000],
100100
invalid: [-1,'a',null,false,true,{},[],()=>{}]
101101
},
102+
{
103+
key: 'sessionCreationRate',
104+
valid: [0,1,10,100.5,1000,Infinity],
105+
invalid: [-1,'a',null,false,true,{},[],()=>{}]
106+
},
107+
{
108+
key: 'sessionCreationBurst',
109+
valid: [0,1,10,100.5,1000,Infinity],
110+
invalid: [-1,'a',null,false,true,{},[],()=>{}]
111+
},
102112
{
103113
key: 'validateAddress',
104114
valid: [true,false,0,1,'a'],

‎test/parallel/test-quic-internal-endpoint-stats-state.mjs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ const {
9595
strictEqual(typeofendpoint.stats.statelessResetRateLimited,'bigint');
9696
strictEqual(typeofendpoint.stats.immediateCloseCount,'bigint');
9797
strictEqual(typeofendpoint.stats.immediateCloseRateLimited,'bigint');
98+
strictEqual(typeofendpoint.stats.sessionCreationRateLimited,'bigint');
9899

99100
deepStrictEqual(Object.keys(endpoint.stats.toJSON()),[
100101
'connected',
@@ -115,6 +116,7 @@ const {
115116
'statelessResetRateLimited',
116117
'immediateCloseCount',
117118
'immediateCloseRateLimited',
119+
'sessionCreationRateLimited',
118120
]);
119121
strictEqual(typeofinspect(endpoint.stats),'string');
120122
}

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 11778a7

Browse files
jasnelladuh95
authored andcommitted
quic: add session creation 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 8107f1b commit 11778a7

9 files changed

Lines changed: 111 additions & 2 deletions

File tree

‎doc/api/quic.md‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -776,6 +776,12 @@ added: v23.8.0
776776
* Type: {bigint} The total number of immediate connection close packets
777777
dropped by the global rate limiter. Read only.
778778

779+
### `endpointStats.sessionCreationRateLimited`
780+
781+
* Type: {bigint} The total number of session creation attempts dropped by the
782+
per-host rate limiter. Read only. A non-zero value indicates one or more
783+
remote addresses are creating sessions faster than the configured rate allows.
784+
779785
## Class: `QuicSession`
780786

781787
<!-- YAML
@@ -2543,6 +2549,26 @@ send per second.
25432549
The maximum burst of immediate connection close packets allowed before rate
25442550
limiting takes effect.
25452551

2552+
#### `endpointOptions.sessionCreationRate`
2553+
2554+
* Type: {number}
2555+
***Default:**`50`
2556+
2557+
The maximum number of new sessions that a single remote address can create per
2558+
second. This is a per-host rate limit tracked in the address validation LRU
2559+
cache. It prevents a validated remote address from churning through sessions
2560+
(rapidly opening and abandoning connections) faster than the server can handle.
2561+
For benchmarking where traffic comes from a single source, set this to a high
2562+
value.
2563+
2564+
#### `endpointOptions.sessionCreationBurst`
2565+
2566+
* Type: {number}
2567+
***Default:**`100`
2568+
2569+
The maximum burst of new session creations allowed from a single remote address
2570+
before rate limiting takes effect.
2571+
25462572
#### `endpointOptions.retryTokenExpiration`
25472573

25482574
<!-- YAML

‎lib/internal/quic/quic.js‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,8 @@ const endpointRegistry = new SafeSet();
315315
* @property {number} [versionNegotiationBurst] Burst capacity for version negotiation rate limiter
316316
* @property {number} [immediateCloseRate] Global rate limit for immediate close packets (per second)
317317
* @property {number} [immediateCloseBurst] Burst capacity for immediate close rate limiter
318+
* @property {number} [sessionCreationRate] Per-host rate limit for session creation (per second)
319+
* @property {number} [sessionCreationBurst] Per-host burst capacity for session creation rate limiter
318320
* @property {ArrayBufferView} [resetTokenSecret] The reset token secret
319321
* @property {bigint|number} [retryTokenExpiration] The retry token expiration
320322
* @property {number} [rxDiagnosticLoss] The receive diagnostic loss probability (range 0.0-1.0)
@@ -4013,6 +4015,8 @@ class QuicEndpoint {
40134015
versionNegotiationBurst,
40144016
immediateCloseRate,
40154017
immediateCloseBurst,
4018+
sessionCreationRate,
4019+
sessionCreationBurst,
40164020
rxDiagnosticLoss,
40174021
txDiagnosticLoss,
40184022
udpReceiveBufferSize,
@@ -4056,6 +4060,8 @@ class QuicEndpoint {
40564060
versionNegotiationBurst,
40574061
immediateCloseRate,
40584062
immediateCloseBurst,
4063+
sessionCreationRate,
4064+
sessionCreationBurst,
40594065
rxDiagnosticLoss,
40604066
txDiagnosticLoss,
40614067
udpReceiveBufferSize,

‎lib/internal/quic/stats.js‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ const {
6767
IDX_STATS_ENDPOINT_STATELESS_RESET_RATE_LIMITED,
6868
IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_COUNT,
6969
IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_RATE_LIMITED,
70+
IDX_STATS_ENDPOINT_SESSION_CREATION_RATE_LIMITED,
7071

7172
IDX_STATS_SESSION_CREATED_AT,
7273
IDX_STATS_SESSION_DESTROYED_AT,
@@ -134,6 +135,7 @@ assert(IDX_STATS_ENDPOINT_STATELESS_RESET_COUNT !== undefined);
134135
assert(IDX_STATS_ENDPOINT_STATELESS_RESET_RATE_LIMITED!==undefined);
135136
assert(IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_COUNT!==undefined);
136137
assert(IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_RATE_LIMITED!==undefined);
138+
assert(IDX_STATS_ENDPOINT_SESSION_CREATION_RATE_LIMITED!==undefined);
137139
assert(IDX_STATS_SESSION_CREATED_AT!==undefined);
138140
assert(IDX_STATS_SESSION_DESTROYED_AT!==undefined);
139141
assert(IDX_STATS_SESSION_CLOSING_AT!==undefined);
@@ -330,6 +332,12 @@ class QuicEndpointStats {
330332
returnthis.#handle[IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_RATE_LIMITED];
331333
}
332334

335+
/** @type {bigint} */
336+
getsessionCreationRateLimited(){
337+
assertIsQuicEndpointStats(this);
338+
returnthis.#handle[IDX_STATS_ENDPOINT_SESSION_CREATION_RATE_LIMITED];
339+
}
340+
333341
toString(){
334342
returnJSONStringify(this.toJSON());
335343
}
@@ -354,6 +362,7 @@ class QuicEndpointStats {
354362
statelessResetRateLimited,
355363
immediateCloseCount,
356364
immediateCloseRateLimited,
365+
sessionCreationRateLimited,
357366
}=this;
358367
return{
359368
__proto__: null,
@@ -377,6 +386,7 @@ class QuicEndpointStats {
377386
statelessResetRateLimited: `${statelessResetRateLimited}`,
378387
immediateCloseCount: `${immediateCloseCount}`,
379388
immediateCloseRateLimited: `${immediateCloseRateLimited}`,
389+
sessionCreationRateLimited: `${sessionCreationRateLimited}`,
380390
};
381391
}
382392

@@ -410,6 +420,7 @@ class QuicEndpointStats {
410420
statelessResetRateLimited,
411421
immediateCloseCount,
412422
immediateCloseRateLimited,
423+
sessionCreationRateLimited,
413424
}=this;
414425

415426
return`QuicEndpointStats ${inspect({
@@ -431,6 +442,7 @@ class QuicEndpointStats {
431442
statelessResetRateLimited,
432443
immediateCloseCount,
433444
immediateCloseRateLimited,
445+
sessionCreationRateLimited,
434446
},opts)}`;
435447
}
436448

‎src/quic/bindingdata.h‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,8 @@ class SessionManager;
125125
V(version_negotiation_burst, "versionNegotiationBurst") \
126126
V(immediate_close_rate, "immediateCloseRate") \
127127
V(immediate_close_burst, "immediateCloseBurst") \
128+
V(session_creation_rate, "sessionCreationRate") \
129+
V(session_creation_burst, "sessionCreationBurst") \
128130
V(max_stream_window, "maxStreamWindow") \
129131
V(max_window, "maxWindow") \
130132
V(min_version, "minVersion") \

‎src/quic/defs.h‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -371,8 +371,15 @@ struct TokenBucket final {
371371
double tokens; // current token count
372372
uint64_t last_ts; // last refill timestamp (nanoseconds, uv_hrtime)
373373

374+
TokenBucket() : rate(0), burst(0), tokens(0), last_ts(0) {}
374375
TokenBucket(double rate, double burst);
375376

377+
// Reinitialize the bucket with new rate/burst parameters if it
378+
// hasn't been initialized yet (last_ts == 0). Used for per-host
379+
// buckets in the address LRU where the rate/burst aren't known
380+
// at construction time.
381+
voidInitOnce(double r, double b);
382+
376383
// Try to consume one token. Refills based on elapsed time, then
377384
// attempts to consume. Returns true if the request is allowed.
378385
boolconsume();

‎src/quic/endpoint.cc‎

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,8 @@ namespace quic {
7878
V(STATELESS_RESET_COUNT, stateless_reset_count) \
7979
V(STATELESS_RESET_RATE_LIMITED, stateless_reset_rate_limited) \
8080
V(IMMEDIATE_CLOSE_COUNT, immediate_close_count) \
81-
V(IMMEDIATE_CLOSE_RATE_LIMITED, immediate_close_rate_limited)
81+
V(IMMEDIATE_CLOSE_RATE_LIMITED, immediate_close_rate_limited) \
82+
V(SESSION_CREATION_RATE_LIMITED, session_creation_rate_limited)
8283

8384
structEndpoint::State {
8485
#defineV(_, name, type) type name;
@@ -91,6 +92,15 @@ STAT_STRUCT(Endpoint, ENDPOINT)
9192
TokenBucket::TokenBucket(double rate, double burst)
9293
: rate(rate), burst(burst), tokens(burst), last_ts(uv_hrtime()) {}
9394

95+
voidTokenBucket::InitOnce(double r, double b) {
96+
if (last_ts == 0) {
97+
rate = r;
98+
burst = b;
99+
tokens = b;
100+
last_ts = uv_hrtime();
101+
}
102+
}
103+
94104
// Try to consume one token. Refills based on elapsed time, then
95105
// attempts to consume. Returns true if the request is allowed.
96106
boolTokenBucket::consume() {
@@ -227,7 +237,8 @@ Maybe<Endpoint::Options> Endpoint::Options::From(Environment* env,
227237
!SET(retry_rate) || !SET(retry_burst) || !SET(stateless_reset_rate) ||
228238
!SET(stateless_reset_burst) || !SET(version_negotiation_rate) ||
229239
!SET(version_negotiation_burst) || !SET(immediate_close_rate) ||
230-
!SET(immediate_close_burst) ||
240+
!SET(immediate_close_burst) || !SET(session_creation_rate) ||
241+
!SET(session_creation_burst) ||
231242
#ifdef DEBUG
232243
!SET(rx_loss) || !SET(tx_loss) ||
233244
#endif
@@ -296,6 +307,11 @@ std::string Endpoint::Options::ToString() const {
296307
"immediate close rate: " + std::to_string(immediate_close_rate) + "/s";
297308
res += prefix +
298309
"immediate close burst: " + std::to_string(immediate_close_burst);
310+
res += prefix +
311+
"session creation rate: " + std::to_string(session_creation_rate) +
312+
"/s";
313+
res += prefix +
314+
"session creation burst: " + std::to_string(session_creation_burst);
299315
res += prefix + "validate address: " + boolToString(validate_address);
300316
res += prefix +
301317
"disable stateless reset: " + boolToString(disable_stateless_reset);
@@ -1331,6 +1347,19 @@ void Endpoint::Receive(const uint8_t* data,
13311347
// as a server, then we cannot accept the initial packet.
13321348
if (is_closed() || is_closing() || !is_listening()) return;
13331349

1350+
// Per-host session creation rate limit. The bucket is initialized
1351+
// on first access with the configured rate/burst from options.
1352+
auto info = addr_validation_lru_.Upsert(config.remote_address);
1353+
info->session_creation_bucket.InitOnce(options_.session_creation_rate,
1354+
options_.session_creation_burst);
1355+
if (!info->session_creation_bucket.consume()) {
1356+
Debug(this,
1357+
"Session creation rate limit exceeded for %s",
1358+
config.remote_address);
1359+
STAT_INCREMENT(Stats, session_creation_rate_limited);
1360+
return;
1361+
}
1362+
13341363
Debug(this, "Creating new session for %s", config.dcid);
13351364

13361365
std::optional<SessionTicket> no_ticket = std::nullopt;

‎src/quic/endpoint.h‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,14 @@ class Endpoint final : public AsyncWrap, public Packet::Listener {
4444
staticconstexprdoubleDEFAULT_IMMEDIATE_CLOSE_RATE = 100;
4545
staticconstexprdoubleDEFAULT_IMMEDIATE_CLOSE_BURST = 200;
4646

47+
// Per-host session creation rate limit. This is tracked per validated
48+
// remote address in the address LRU, preventing a single source from
49+
// churning through sessions faster than the server can handle. Unlike
50+
// the global stateless response buckets, this only applies after address
51+
// validation (spoofed sources can't reach this path).
52+
staticconstexprdoubleDEFAULT_SESSION_CREATION_RATE = 50;
53+
staticconstexprdoubleDEFAULT_SESSION_CREATION_BURST = 100;
54+
4755
// Endpoint configuration options
4856
structOptionsfinal : public MemoryRetainer {
4957
// The local socket address to which the UDP port will be bound. The port
@@ -83,6 +91,12 @@ class Endpoint final : public AsyncWrap, public Packet::Listener {
8391
double immediate_close_rate = DEFAULT_IMMEDIATE_CLOSE_RATE;
8492
double immediate_close_burst = DEFAULT_IMMEDIATE_CLOSE_BURST;
8593

94+
// Per-host session creation rate limit. Tracked per validated remote
95+
// address in the address LRU. Set to high values for benchmarking
96+
// where traffic comes from a single source.
97+
double session_creation_rate = DEFAULT_SESSION_CREATION_RATE;
98+
double session_creation_burst = DEFAULT_SESSION_CREATION_BURST;
99+
86100
// The validate_address parameter instructs the Endpoint to perform explicit
87101
// address validation using retry tokens. This is strongly recommended and
88102
// should only be disabled in trusted, closed environments as a performance
@@ -452,6 +466,7 @@ class Endpoint final : public AsyncWrap, public Packet::Listener {
452466
structTypefinal {
453467
uint64_t timestamp;
454468
bool validated;
469+
TokenBucket session_creation_bucket;
455470
};
456471

457472
staticboolCheckExpired(const SocketAddress& address, const Type& type);

‎test/parallel/test-quic-internal-endpoint-options.mjs‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,16 @@ const cases = [
9999
valid: [0,1,10,100.5,1000],
100100
invalid: [-1,'a',null,false,true,{},[],()=>{}]
101101
},
102+
{
103+
key: 'sessionCreationRate',
104+
valid: [0,1,10,100.5,1000,Infinity],
105+
invalid: [-1,'a',null,false,true,{},[],()=>{}]
106+
},
107+
{
108+
key: 'sessionCreationBurst',
109+
valid: [0,1,10,100.5,1000,Infinity],
110+
invalid: [-1,'a',null,false,true,{},[],()=>{}]
111+
},
102112
{
103113
key: 'validateAddress',
104114
valid: [true,false,0,1,'a'],

‎test/parallel/test-quic-internal-endpoint-stats-state.mjs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ const {
9595
strictEqual(typeofendpoint.stats.statelessResetRateLimited,'bigint');
9696
strictEqual(typeofendpoint.stats.immediateCloseCount,'bigint');
9797
strictEqual(typeofendpoint.stats.immediateCloseRateLimited,'bigint');
98+
strictEqual(typeofendpoint.stats.sessionCreationRateLimited,'bigint');
9899

99100
deepStrictEqual(Object.keys(endpoint.stats.toJSON()),[
100101
'connected',
@@ -115,6 +116,7 @@ const {
115116
'statelessResetRateLimited',
116117
'immediateCloseCount',
117118
'immediateCloseRateLimited',
119+
'sessionCreationRateLimited',
118120
]);
119121
strictEqual(typeofinspect(endpoint.stats),'string');
120122
}

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 11778a7

Browse files
jasnelladuh95
authored andcommitted
quic: add session creation 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 8107f1b commit 11778a7

9 files changed

Lines changed: 111 additions & 2 deletions

File tree

‎doc/api/quic.md‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -776,6 +776,12 @@ added: v23.8.0
776776
* Type: {bigint} The total number of immediate connection close packets
777777
dropped by the global rate limiter. Read only.
778778

779+
### `endpointStats.sessionCreationRateLimited`
780+
781+
* Type: {bigint} The total number of session creation attempts dropped by the
782+
per-host rate limiter. Read only. A non-zero value indicates one or more
783+
remote addresses are creating sessions faster than the configured rate allows.
784+
779785
## Class: `QuicSession`
780786

781787
<!-- YAML
@@ -2543,6 +2549,26 @@ send per second.
25432549
The maximum burst of immediate connection close packets allowed before rate
25442550
limiting takes effect.
25452551

2552+
#### `endpointOptions.sessionCreationRate`
2553+
2554+
* Type: {number}
2555+
***Default:**`50`
2556+
2557+
The maximum number of new sessions that a single remote address can create per
2558+
second. This is a per-host rate limit tracked in the address validation LRU
2559+
cache. It prevents a validated remote address from churning through sessions
2560+
(rapidly opening and abandoning connections) faster than the server can handle.
2561+
For benchmarking where traffic comes from a single source, set this to a high
2562+
value.
2563+
2564+
#### `endpointOptions.sessionCreationBurst`
2565+
2566+
* Type: {number}
2567+
***Default:**`100`
2568+
2569+
The maximum burst of new session creations allowed from a single remote address
2570+
before rate limiting takes effect.
2571+
25462572
#### `endpointOptions.retryTokenExpiration`
25472573

25482574
<!-- YAML

‎lib/internal/quic/quic.js‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,8 @@ const endpointRegistry = new SafeSet();
315315
* @property {number} [versionNegotiationBurst] Burst capacity for version negotiation rate limiter
316316
* @property {number} [immediateCloseRate] Global rate limit for immediate close packets (per second)
317317
* @property {number} [immediateCloseBurst] Burst capacity for immediate close rate limiter
318+
* @property {number} [sessionCreationRate] Per-host rate limit for session creation (per second)
319+
* @property {number} [sessionCreationBurst] Per-host burst capacity for session creation rate limiter
318320
* @property {ArrayBufferView} [resetTokenSecret] The reset token secret
319321
* @property {bigint|number} [retryTokenExpiration] The retry token expiration
320322
* @property {number} [rxDiagnosticLoss] The receive diagnostic loss probability (range 0.0-1.0)
@@ -4013,6 +4015,8 @@ class QuicEndpoint {
40134015
versionNegotiationBurst,
40144016
immediateCloseRate,
40154017
immediateCloseBurst,
4018+
sessionCreationRate,
4019+
sessionCreationBurst,
40164020
rxDiagnosticLoss,
40174021
txDiagnosticLoss,
40184022
udpReceiveBufferSize,
@@ -4056,6 +4060,8 @@ class QuicEndpoint {
40564060
versionNegotiationBurst,
40574061
immediateCloseRate,
40584062
immediateCloseBurst,
4063+
sessionCreationRate,
4064+
sessionCreationBurst,
40594065
rxDiagnosticLoss,
40604066
txDiagnosticLoss,
40614067
udpReceiveBufferSize,

‎lib/internal/quic/stats.js‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ const {
6767
IDX_STATS_ENDPOINT_STATELESS_RESET_RATE_LIMITED,
6868
IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_COUNT,
6969
IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_RATE_LIMITED,
70+
IDX_STATS_ENDPOINT_SESSION_CREATION_RATE_LIMITED,
7071

7172
IDX_STATS_SESSION_CREATED_AT,
7273
IDX_STATS_SESSION_DESTROYED_AT,
@@ -134,6 +135,7 @@ assert(IDX_STATS_ENDPOINT_STATELESS_RESET_COUNT !== undefined);
134135
assert(IDX_STATS_ENDPOINT_STATELESS_RESET_RATE_LIMITED!==undefined);
135136
assert(IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_COUNT!==undefined);
136137
assert(IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_RATE_LIMITED!==undefined);
138+
assert(IDX_STATS_ENDPOINT_SESSION_CREATION_RATE_LIMITED!==undefined);
137139
assert(IDX_STATS_SESSION_CREATED_AT!==undefined);
138140
assert(IDX_STATS_SESSION_DESTROYED_AT!==undefined);
139141
assert(IDX_STATS_SESSION_CLOSING_AT!==undefined);
@@ -330,6 +332,12 @@ class QuicEndpointStats {
330332
returnthis.#handle[IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_RATE_LIMITED];
331333
}
332334

335+
/** @type {bigint} */
336+
getsessionCreationRateLimited(){
337+
assertIsQuicEndpointStats(this);
338+
returnthis.#handle[IDX_STATS_ENDPOINT_SESSION_CREATION_RATE_LIMITED];
339+
}
340+
333341
toString(){
334342
returnJSONStringify(this.toJSON());
335343
}
@@ -354,6 +362,7 @@ class QuicEndpointStats {
354362
statelessResetRateLimited,
355363
immediateCloseCount,
356364
immediateCloseRateLimited,
365+
sessionCreationRateLimited,
357366
}=this;
358367
return{
359368
__proto__: null,
@@ -377,6 +386,7 @@ class QuicEndpointStats {
377386
statelessResetRateLimited: `${statelessResetRateLimited}`,
378387
immediateCloseCount: `${immediateCloseCount}`,
379388
immediateCloseRateLimited: `${immediateCloseRateLimited}`,
389+
sessionCreationRateLimited: `${sessionCreationRateLimited}`,
380390
};
381391
}
382392

@@ -410,6 +420,7 @@ class QuicEndpointStats {
410420
statelessResetRateLimited,
411421
immediateCloseCount,
412422
immediateCloseRateLimited,
423+
sessionCreationRateLimited,
413424
}=this;
414425

415426
return`QuicEndpointStats ${inspect({
@@ -431,6 +442,7 @@ class QuicEndpointStats {
431442
statelessResetRateLimited,
432443
immediateCloseCount,
433444
immediateCloseRateLimited,
445+
sessionCreationRateLimited,
434446
},opts)}`;
435447
}
436448

‎src/quic/bindingdata.h‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,8 @@ class SessionManager;
125125
V(version_negotiation_burst, "versionNegotiationBurst") \
126126
V(immediate_close_rate, "immediateCloseRate") \
127127
V(immediate_close_burst, "immediateCloseBurst") \
128+
V(session_creation_rate, "sessionCreationRate") \
129+
V(session_creation_burst, "sessionCreationBurst") \
128130
V(max_stream_window, "maxStreamWindow") \
129131
V(max_window, "maxWindow") \
130132
V(min_version, "minVersion") \

‎src/quic/defs.h‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -371,8 +371,15 @@ struct TokenBucket final {
371371
double tokens; // current token count
372372
uint64_t last_ts; // last refill timestamp (nanoseconds, uv_hrtime)
373373

374+
TokenBucket() : rate(0), burst(0), tokens(0), last_ts(0) {}
374375
TokenBucket(double rate, double burst);
375376

377+
// Reinitialize the bucket with new rate/burst parameters if it
378+
// hasn't been initialized yet (last_ts == 0). Used for per-host
379+
// buckets in the address LRU where the rate/burst aren't known
380+
// at construction time.
381+
voidInitOnce(double r, double b);
382+
376383
// Try to consume one token. Refills based on elapsed time, then
377384
// attempts to consume. Returns true if the request is allowed.
378385
boolconsume();

‎src/quic/endpoint.cc‎

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,8 @@ namespace quic {
7878
V(STATELESS_RESET_COUNT, stateless_reset_count) \
7979
V(STATELESS_RESET_RATE_LIMITED, stateless_reset_rate_limited) \
8080
V(IMMEDIATE_CLOSE_COUNT, immediate_close_count) \
81-
V(IMMEDIATE_CLOSE_RATE_LIMITED, immediate_close_rate_limited)
81+
V(IMMEDIATE_CLOSE_RATE_LIMITED, immediate_close_rate_limited) \
82+
V(SESSION_CREATION_RATE_LIMITED, session_creation_rate_limited)
8283

8384
structEndpoint::State {
8485
#defineV(_, name, type) type name;
@@ -91,6 +92,15 @@ STAT_STRUCT(Endpoint, ENDPOINT)
9192
TokenBucket::TokenBucket(double rate, double burst)
9293
: rate(rate), burst(burst), tokens(burst), last_ts(uv_hrtime()) {}
9394

95+
voidTokenBucket::InitOnce(double r, double b) {
96+
if (last_ts == 0) {
97+
rate = r;
98+
burst = b;
99+
tokens = b;
100+
last_ts = uv_hrtime();
101+
}
102+
}
103+
94104
// Try to consume one token. Refills based on elapsed time, then
95105
// attempts to consume. Returns true if the request is allowed.
96106
boolTokenBucket::consume() {
@@ -227,7 +237,8 @@ Maybe<Endpoint::Options> Endpoint::Options::From(Environment* env,
227237
!SET(retry_rate) || !SET(retry_burst) || !SET(stateless_reset_rate) ||
228238
!SET(stateless_reset_burst) || !SET(version_negotiation_rate) ||
229239
!SET(version_negotiation_burst) || !SET(immediate_close_rate) ||
230-
!SET(immediate_close_burst) ||
240+
!SET(immediate_close_burst) || !SET(session_creation_rate) ||
241+
!SET(session_creation_burst) ||
231242
#ifdef DEBUG
232243
!SET(rx_loss) || !SET(tx_loss) ||
233244
#endif
@@ -296,6 +307,11 @@ std::string Endpoint::Options::ToString() const {
296307
"immediate close rate: " + std::to_string(immediate_close_rate) + "/s";
297308
res += prefix +
298309
"immediate close burst: " + std::to_string(immediate_close_burst);
310+
res += prefix +
311+
"session creation rate: " + std::to_string(session_creation_rate) +
312+
"/s";
313+
res += prefix +
314+
"session creation burst: " + std::to_string(session_creation_burst);
299315
res += prefix + "validate address: " + boolToString(validate_address);
300316
res += prefix +
301317
"disable stateless reset: " + boolToString(disable_stateless_reset);
@@ -1331,6 +1347,19 @@ void Endpoint::Receive(const uint8_t* data,
13311347
// as a server, then we cannot accept the initial packet.
13321348
if (is_closed() || is_closing() || !is_listening()) return;
13331349

1350+
// Per-host session creation rate limit. The bucket is initialized
1351+
// on first access with the configured rate/burst from options.
1352+
auto info = addr_validation_lru_.Upsert(config.remote_address);
1353+
info->session_creation_bucket.InitOnce(options_.session_creation_rate,
1354+
options_.session_creation_burst);
1355+
if (!info->session_creation_bucket.consume()) {
1356+
Debug(this,
1357+
"Session creation rate limit exceeded for %s",
1358+
config.remote_address);
1359+
STAT_INCREMENT(Stats, session_creation_rate_limited);
1360+
return;
1361+
}
1362+
13341363
Debug(this, "Creating new session for %s", config.dcid);
13351364

13361365
std::optional<SessionTicket> no_ticket = std::nullopt;

‎src/quic/endpoint.h‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,14 @@ class Endpoint final : public AsyncWrap, public Packet::Listener {
4444
staticconstexprdoubleDEFAULT_IMMEDIATE_CLOSE_RATE = 100;
4545
staticconstexprdoubleDEFAULT_IMMEDIATE_CLOSE_BURST = 200;
4646

47+
// Per-host session creation rate limit. This is tracked per validated
48+
// remote address in the address LRU, preventing a single source from
49+
// churning through sessions faster than the server can handle. Unlike
50+
// the global stateless response buckets, this only applies after address
51+
// validation (spoofed sources can't reach this path).
52+
staticconstexprdoubleDEFAULT_SESSION_CREATION_RATE = 50;
53+
staticconstexprdoubleDEFAULT_SESSION_CREATION_BURST = 100;
54+
4755
// Endpoint configuration options
4856
structOptionsfinal : public MemoryRetainer {
4957
// The local socket address to which the UDP port will be bound. The port
@@ -83,6 +91,12 @@ class Endpoint final : public AsyncWrap, public Packet::Listener {
8391
double immediate_close_rate = DEFAULT_IMMEDIATE_CLOSE_RATE;
8492
double immediate_close_burst = DEFAULT_IMMEDIATE_CLOSE_BURST;
8593

94+
// Per-host session creation rate limit. Tracked per validated remote
95+
// address in the address LRU. Set to high values for benchmarking
96+
// where traffic comes from a single source.
97+
double session_creation_rate = DEFAULT_SESSION_CREATION_RATE;
98+
double session_creation_burst = DEFAULT_SESSION_CREATION_BURST;
99+
86100
// The validate_address parameter instructs the Endpoint to perform explicit
87101
// address validation using retry tokens. This is strongly recommended and
88102
// should only be disabled in trusted, closed environments as a performance
@@ -452,6 +466,7 @@ class Endpoint final : public AsyncWrap, public Packet::Listener {
452466
structTypefinal {
453467
uint64_t timestamp;
454468
bool validated;
469+
TokenBucket session_creation_bucket;
455470
};
456471

457472
staticboolCheckExpired(const SocketAddress& address, const Type& type);

‎test/parallel/test-quic-internal-endpoint-options.mjs‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,16 @@ const cases = [
9999
valid: [0,1,10,100.5,1000],
100100
invalid: [-1,'a',null,false,true,{},[],()=>{}]
101101
},
102+
{
103+
key: 'sessionCreationRate',
104+
valid: [0,1,10,100.5,1000,Infinity],
105+
invalid: [-1,'a',null,false,true,{},[],()=>{}]
106+
},
107+
{
108+
key: 'sessionCreationBurst',
109+
valid: [0,1,10,100.5,1000,Infinity],
110+
invalid: [-1,'a',null,false,true,{},[],()=>{}]
111+
},
102112
{
103113
key: 'validateAddress',
104114
valid: [true,false,0,1,'a'],

‎test/parallel/test-quic-internal-endpoint-stats-state.mjs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ const {
9595
strictEqual(typeofendpoint.stats.statelessResetRateLimited,'bigint');
9696
strictEqual(typeofendpoint.stats.immediateCloseCount,'bigint');
9797
strictEqual(typeofendpoint.stats.immediateCloseRateLimited,'bigint');
98+
strictEqual(typeofendpoint.stats.sessionCreationRateLimited,'bigint');
9899

99100
deepStrictEqual(Object.keys(endpoint.stats.toJSON()),[
100101
'connected',
@@ -115,6 +116,7 @@ const {
115116
'statelessResetRateLimited',
116117
'immediateCloseCount',
117118
'immediateCloseRateLimited',
119+
'sessionCreationRateLimited',
118120
]);
119121
strictEqual(typeofinspect(endpoint.stats),'string');
120122
}

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 11778a7

Browse files
jasnelladuh95
authored andcommitted
quic: add session creation 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 8107f1b commit 11778a7

9 files changed

Lines changed: 111 additions & 2 deletions

File tree

‎doc/api/quic.md‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -776,6 +776,12 @@ added: v23.8.0
776776
* Type: {bigint} The total number of immediate connection close packets
777777
dropped by the global rate limiter. Read only.
778778

779+
### `endpointStats.sessionCreationRateLimited`
780+
781+
* Type: {bigint} The total number of session creation attempts dropped by the
782+
per-host rate limiter. Read only. A non-zero value indicates one or more
783+
remote addresses are creating sessions faster than the configured rate allows.
784+
779785
## Class: `QuicSession`
780786

781787
<!-- YAML
@@ -2543,6 +2549,26 @@ send per second.
25432549
The maximum burst of immediate connection close packets allowed before rate
25442550
limiting takes effect.
25452551

2552+
#### `endpointOptions.sessionCreationRate`
2553+
2554+
* Type: {number}
2555+
***Default:**`50`
2556+
2557+
The maximum number of new sessions that a single remote address can create per
2558+
second. This is a per-host rate limit tracked in the address validation LRU
2559+
cache. It prevents a validated remote address from churning through sessions
2560+
(rapidly opening and abandoning connections) faster than the server can handle.
2561+
For benchmarking where traffic comes from a single source, set this to a high
2562+
value.
2563+
2564+
#### `endpointOptions.sessionCreationBurst`
2565+
2566+
* Type: {number}
2567+
***Default:**`100`
2568+
2569+
The maximum burst of new session creations allowed from a single remote address
2570+
before rate limiting takes effect.
2571+
25462572
#### `endpointOptions.retryTokenExpiration`
25472573

25482574
<!-- YAML

‎lib/internal/quic/quic.js‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,8 @@ const endpointRegistry = new SafeSet();
315315
* @property {number} [versionNegotiationBurst] Burst capacity for version negotiation rate limiter
316316
* @property {number} [immediateCloseRate] Global rate limit for immediate close packets (per second)
317317
* @property {number} [immediateCloseBurst] Burst capacity for immediate close rate limiter
318+
* @property {number} [sessionCreationRate] Per-host rate limit for session creation (per second)
319+
* @property {number} [sessionCreationBurst] Per-host burst capacity for session creation rate limiter
318320
* @property {ArrayBufferView} [resetTokenSecret] The reset token secret
319321
* @property {bigint|number} [retryTokenExpiration] The retry token expiration
320322
* @property {number} [rxDiagnosticLoss] The receive diagnostic loss probability (range 0.0-1.0)
@@ -4013,6 +4015,8 @@ class QuicEndpoint {
40134015
versionNegotiationBurst,
40144016
immediateCloseRate,
40154017
immediateCloseBurst,
4018+
sessionCreationRate,
4019+
sessionCreationBurst,
40164020
rxDiagnosticLoss,
40174021
txDiagnosticLoss,
40184022
udpReceiveBufferSize,
@@ -4056,6 +4060,8 @@ class QuicEndpoint {
40564060
versionNegotiationBurst,
40574061
immediateCloseRate,
40584062
immediateCloseBurst,
4063+
sessionCreationRate,
4064+
sessionCreationBurst,
40594065
rxDiagnosticLoss,
40604066
txDiagnosticLoss,
40614067
udpReceiveBufferSize,

‎lib/internal/quic/stats.js‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ const {
6767
IDX_STATS_ENDPOINT_STATELESS_RESET_RATE_LIMITED,
6868
IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_COUNT,
6969
IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_RATE_LIMITED,
70+
IDX_STATS_ENDPOINT_SESSION_CREATION_RATE_LIMITED,
7071

7172
IDX_STATS_SESSION_CREATED_AT,
7273
IDX_STATS_SESSION_DESTROYED_AT,
@@ -134,6 +135,7 @@ assert(IDX_STATS_ENDPOINT_STATELESS_RESET_COUNT !== undefined);
134135
assert(IDX_STATS_ENDPOINT_STATELESS_RESET_RATE_LIMITED!==undefined);
135136
assert(IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_COUNT!==undefined);
136137
assert(IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_RATE_LIMITED!==undefined);
138+
assert(IDX_STATS_ENDPOINT_SESSION_CREATION_RATE_LIMITED!==undefined);
137139
assert(IDX_STATS_SESSION_CREATED_AT!==undefined);
138140
assert(IDX_STATS_SESSION_DESTROYED_AT!==undefined);
139141
assert(IDX_STATS_SESSION_CLOSING_AT!==undefined);
@@ -330,6 +332,12 @@ class QuicEndpointStats {
330332
returnthis.#handle[IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_RATE_LIMITED];
331333
}
332334

335+
/** @type {bigint} */
336+
getsessionCreationRateLimited(){
337+
assertIsQuicEndpointStats(this);
338+
returnthis.#handle[IDX_STATS_ENDPOINT_SESSION_CREATION_RATE_LIMITED];
339+
}
340+
333341
toString(){
334342
returnJSONStringify(this.toJSON());
335343
}
@@ -354,6 +362,7 @@ class QuicEndpointStats {
354362
statelessResetRateLimited,
355363
immediateCloseCount,
356364
immediateCloseRateLimited,
365+
sessionCreationRateLimited,
357366
}=this;
358367
return{
359368
__proto__: null,
@@ -377,6 +386,7 @@ class QuicEndpointStats {
377386
statelessResetRateLimited: `${statelessResetRateLimited}`,
378387
immediateCloseCount: `${immediateCloseCount}`,
379388
immediateCloseRateLimited: `${immediateCloseRateLimited}`,
389+
sessionCreationRateLimited: `${sessionCreationRateLimited}`,
380390
};
381391
}
382392

@@ -410,6 +420,7 @@ class QuicEndpointStats {
410420
statelessResetRateLimited,
411421
immediateCloseCount,
412422
immediateCloseRateLimited,
423+
sessionCreationRateLimited,
413424
}=this;
414425

415426
return`QuicEndpointStats ${inspect({
@@ -431,6 +442,7 @@ class QuicEndpointStats {
431442
statelessResetRateLimited,
432443
immediateCloseCount,
433444
immediateCloseRateLimited,
445+
sessionCreationRateLimited,
434446
},opts)}`;
435447
}
436448

‎src/quic/bindingdata.h‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,8 @@ class SessionManager;
125125
V(version_negotiation_burst, "versionNegotiationBurst") \
126126
V(immediate_close_rate, "immediateCloseRate") \
127127
V(immediate_close_burst, "immediateCloseBurst") \
128+
V(session_creation_rate, "sessionCreationRate") \
129+
V(session_creation_burst, "sessionCreationBurst") \
128130
V(max_stream_window, "maxStreamWindow") \
129131
V(max_window, "maxWindow") \
130132
V(min_version, "minVersion") \

‎src/quic/defs.h‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -371,8 +371,15 @@ struct TokenBucket final {
371371
double tokens; // current token count
372372
uint64_t last_ts; // last refill timestamp (nanoseconds, uv_hrtime)
373373

374+
TokenBucket() : rate(0), burst(0), tokens(0), last_ts(0) {}
374375
TokenBucket(double rate, double burst);
375376

377+
// Reinitialize the bucket with new rate/burst parameters if it
378+
// hasn't been initialized yet (last_ts == 0). Used for per-host
379+
// buckets in the address LRU where the rate/burst aren't known
380+
// at construction time.
381+
voidInitOnce(double r, double b);
382+
376383
// Try to consume one token. Refills based on elapsed time, then
377384
// attempts to consume. Returns true if the request is allowed.
378385
boolconsume();

‎src/quic/endpoint.cc‎

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,8 @@ namespace quic {
7878
V(STATELESS_RESET_COUNT, stateless_reset_count) \
7979
V(STATELESS_RESET_RATE_LIMITED, stateless_reset_rate_limited) \
8080
V(IMMEDIATE_CLOSE_COUNT, immediate_close_count) \
81-
V(IMMEDIATE_CLOSE_RATE_LIMITED, immediate_close_rate_limited)
81+
V(IMMEDIATE_CLOSE_RATE_LIMITED, immediate_close_rate_limited) \
82+
V(SESSION_CREATION_RATE_LIMITED, session_creation_rate_limited)
8283

8384
structEndpoint::State {
8485
#defineV(_, name, type) type name;
@@ -91,6 +92,15 @@ STAT_STRUCT(Endpoint, ENDPOINT)
9192
TokenBucket::TokenBucket(double rate, double burst)
9293
: rate(rate), burst(burst), tokens(burst), last_ts(uv_hrtime()) {}
9394

95+
voidTokenBucket::InitOnce(double r, double b) {
96+
if (last_ts == 0) {
97+
rate = r;
98+
burst = b;
99+
tokens = b;
100+
last_ts = uv_hrtime();
101+
}
102+
}
103+
94104
// Try to consume one token. Refills based on elapsed time, then
95105
// attempts to consume. Returns true if the request is allowed.
96106
boolTokenBucket::consume() {
@@ -227,7 +237,8 @@ Maybe<Endpoint::Options> Endpoint::Options::From(Environment* env,
227237
!SET(retry_rate) || !SET(retry_burst) || !SET(stateless_reset_rate) ||
228238
!SET(stateless_reset_burst) || !SET(version_negotiation_rate) ||
229239
!SET(version_negotiation_burst) || !SET(immediate_close_rate) ||
230-
!SET(immediate_close_burst) ||
240+
!SET(immediate_close_burst) || !SET(session_creation_rate) ||
241+
!SET(session_creation_burst) ||
231242
#ifdef DEBUG
232243
!SET(rx_loss) || !SET(tx_loss) ||
233244
#endif
@@ -296,6 +307,11 @@ std::string Endpoint::Options::ToString() const {
296307
"immediate close rate: " + std::to_string(immediate_close_rate) + "/s";
297308
res += prefix +
298309
"immediate close burst: " + std::to_string(immediate_close_burst);
310+
res += prefix +
311+
"session creation rate: " + std::to_string(session_creation_rate) +
312+
"/s";
313+
res += prefix +
314+
"session creation burst: " + std::to_string(session_creation_burst);
299315
res += prefix + "validate address: " + boolToString(validate_address);
300316
res += prefix +
301317
"disable stateless reset: " + boolToString(disable_stateless_reset);
@@ -1331,6 +1347,19 @@ void Endpoint::Receive(const uint8_t* data,
13311347
// as a server, then we cannot accept the initial packet.
13321348
if (is_closed() || is_closing() || !is_listening()) return;
13331349

1350+
// Per-host session creation rate limit. The bucket is initialized
1351+
// on first access with the configured rate/burst from options.
1352+
auto info = addr_validation_lru_.Upsert(config.remote_address);
1353+
info->session_creation_bucket.InitOnce(options_.session_creation_rate,
1354+
options_.session_creation_burst);
1355+
if (!info->session_creation_bucket.consume()) {
1356+
Debug(this,
1357+
"Session creation rate limit exceeded for %s",
1358+
config.remote_address);
1359+
STAT_INCREMENT(Stats, session_creation_rate_limited);
1360+
return;
1361+
}
1362+
13341363
Debug(this, "Creating new session for %s", config.dcid);
13351364

13361365
std::optional<SessionTicket> no_ticket = std::nullopt;

‎src/quic/endpoint.h‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,14 @@ class Endpoint final : public AsyncWrap, public Packet::Listener {
4444
staticconstexprdoubleDEFAULT_IMMEDIATE_CLOSE_RATE = 100;
4545
staticconstexprdoubleDEFAULT_IMMEDIATE_CLOSE_BURST = 200;
4646

47+
// Per-host session creation rate limit. This is tracked per validated
48+
// remote address in the address LRU, preventing a single source from
49+
// churning through sessions faster than the server can handle. Unlike
50+
// the global stateless response buckets, this only applies after address
51+
// validation (spoofed sources can't reach this path).
52+
staticconstexprdoubleDEFAULT_SESSION_CREATION_RATE = 50;
53+
staticconstexprdoubleDEFAULT_SESSION_CREATION_BURST = 100;
54+
4755
// Endpoint configuration options
4856
structOptionsfinal : public MemoryRetainer {
4957
// The local socket address to which the UDP port will be bound. The port
@@ -83,6 +91,12 @@ class Endpoint final : public AsyncWrap, public Packet::Listener {
8391
double immediate_close_rate = DEFAULT_IMMEDIATE_CLOSE_RATE;
8492
double immediate_close_burst = DEFAULT_IMMEDIATE_CLOSE_BURST;
8593

94+
// Per-host session creation rate limit. Tracked per validated remote
95+
// address in the address LRU. Set to high values for benchmarking
96+
// where traffic comes from a single source.
97+
double session_creation_rate = DEFAULT_SESSION_CREATION_RATE;
98+
double session_creation_burst = DEFAULT_SESSION_CREATION_BURST;
99+
86100
// The validate_address parameter instructs the Endpoint to perform explicit
87101
// address validation using retry tokens. This is strongly recommended and
88102
// should only be disabled in trusted, closed environments as a performance
@@ -452,6 +466,7 @@ class Endpoint final : public AsyncWrap, public Packet::Listener {
452466
structTypefinal {
453467
uint64_t timestamp;
454468
bool validated;
469+
TokenBucket session_creation_bucket;
455470
};
456471

457472
staticboolCheckExpired(const SocketAddress& address, const Type& type);

‎test/parallel/test-quic-internal-endpoint-options.mjs‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,16 @@ const cases = [
9999
valid: [0,1,10,100.5,1000],
100100
invalid: [-1,'a',null,false,true,{},[],()=>{}]
101101
},
102+
{
103+
key: 'sessionCreationRate',
104+
valid: [0,1,10,100.5,1000,Infinity],
105+
invalid: [-1,'a',null,false,true,{},[],()=>{}]
106+
},
107+
{
108+
key: 'sessionCreationBurst',
109+
valid: [0,1,10,100.5,1000,Infinity],
110+
invalid: [-1,'a',null,false,true,{},[],()=>{}]
111+
},
102112
{
103113
key: 'validateAddress',
104114
valid: [true,false,0,1,'a'],

‎test/parallel/test-quic-internal-endpoint-stats-state.mjs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ const {
9595
strictEqual(typeofendpoint.stats.statelessResetRateLimited,'bigint');
9696
strictEqual(typeofendpoint.stats.immediateCloseCount,'bigint');
9797
strictEqual(typeofendpoint.stats.immediateCloseRateLimited,'bigint');
98+
strictEqual(typeofendpoint.stats.sessionCreationRateLimited,'bigint');
9899

99100
deepStrictEqual(Object.keys(endpoint.stats.toJSON()),[
100101
'connected',
@@ -115,6 +116,7 @@ const {
115116
'statelessResetRateLimited',
116117
'immediateCloseCount',
117118
'immediateCloseRateLimited',
119+
'sessionCreationRateLimited',
118120
]);
119121
strictEqual(typeofinspect(endpoint.stats),'string');
120122
}

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 11778a7

Browse files
jasnelladuh95
authored andcommitted
quic: add session creation 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 8107f1b commit 11778a7

9 files changed

Lines changed: 111 additions & 2 deletions

File tree

‎doc/api/quic.md‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -776,6 +776,12 @@ added: v23.8.0
776776
* Type: {bigint} The total number of immediate connection close packets
777777
dropped by the global rate limiter. Read only.
778778

779+
### `endpointStats.sessionCreationRateLimited`
780+
781+
* Type: {bigint} The total number of session creation attempts dropped by the
782+
per-host rate limiter. Read only. A non-zero value indicates one or more
783+
remote addresses are creating sessions faster than the configured rate allows.
784+
779785
## Class: `QuicSession`
780786

781787
<!-- YAML
@@ -2543,6 +2549,26 @@ send per second.
25432549
The maximum burst of immediate connection close packets allowed before rate
25442550
limiting takes effect.
25452551

2552+
#### `endpointOptions.sessionCreationRate`
2553+
2554+
* Type: {number}
2555+
***Default:**`50`
2556+
2557+
The maximum number of new sessions that a single remote address can create per
2558+
second. This is a per-host rate limit tracked in the address validation LRU
2559+
cache. It prevents a validated remote address from churning through sessions
2560+
(rapidly opening and abandoning connections) faster than the server can handle.
2561+
For benchmarking where traffic comes from a single source, set this to a high
2562+
value.
2563+
2564+
#### `endpointOptions.sessionCreationBurst`
2565+
2566+
* Type: {number}
2567+
***Default:**`100`
2568+
2569+
The maximum burst of new session creations allowed from a single remote address
2570+
before rate limiting takes effect.
2571+
25462572
#### `endpointOptions.retryTokenExpiration`
25472573

25482574
<!-- YAML

‎lib/internal/quic/quic.js‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,8 @@ const endpointRegistry = new SafeSet();
315315
* @property {number} [versionNegotiationBurst] Burst capacity for version negotiation rate limiter
316316
* @property {number} [immediateCloseRate] Global rate limit for immediate close packets (per second)
317317
* @property {number} [immediateCloseBurst] Burst capacity for immediate close rate limiter
318+
* @property {number} [sessionCreationRate] Per-host rate limit for session creation (per second)
319+
* @property {number} [sessionCreationBurst] Per-host burst capacity for session creation rate limiter
318320
* @property {ArrayBufferView} [resetTokenSecret] The reset token secret
319321
* @property {bigint|number} [retryTokenExpiration] The retry token expiration
320322
* @property {number} [rxDiagnosticLoss] The receive diagnostic loss probability (range 0.0-1.0)
@@ -4013,6 +4015,8 @@ class QuicEndpoint {
40134015
versionNegotiationBurst,
40144016
immediateCloseRate,
40154017
immediateCloseBurst,
4018+
sessionCreationRate,
4019+
sessionCreationBurst,
40164020
rxDiagnosticLoss,
40174021
txDiagnosticLoss,
40184022
udpReceiveBufferSize,
@@ -4056,6 +4060,8 @@ class QuicEndpoint {
40564060
versionNegotiationBurst,
40574061
immediateCloseRate,
40584062
immediateCloseBurst,
4063+
sessionCreationRate,
4064+
sessionCreationBurst,
40594065
rxDiagnosticLoss,
40604066
txDiagnosticLoss,
40614067
udpReceiveBufferSize,

‎lib/internal/quic/stats.js‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ const {
6767
IDX_STATS_ENDPOINT_STATELESS_RESET_RATE_LIMITED,
6868
IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_COUNT,
6969
IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_RATE_LIMITED,
70+
IDX_STATS_ENDPOINT_SESSION_CREATION_RATE_LIMITED,
7071

7172
IDX_STATS_SESSION_CREATED_AT,
7273
IDX_STATS_SESSION_DESTROYED_AT,
@@ -134,6 +135,7 @@ assert(IDX_STATS_ENDPOINT_STATELESS_RESET_COUNT !== undefined);
134135
assert(IDX_STATS_ENDPOINT_STATELESS_RESET_RATE_LIMITED!==undefined);
135136
assert(IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_COUNT!==undefined);
136137
assert(IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_RATE_LIMITED!==undefined);
138+
assert(IDX_STATS_ENDPOINT_SESSION_CREATION_RATE_LIMITED!==undefined);
137139
assert(IDX_STATS_SESSION_CREATED_AT!==undefined);
138140
assert(IDX_STATS_SESSION_DESTROYED_AT!==undefined);
139141
assert(IDX_STATS_SESSION_CLOSING_AT!==undefined);
@@ -330,6 +332,12 @@ class QuicEndpointStats {
330332
returnthis.#handle[IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_RATE_LIMITED];
331333
}
332334

335+
/** @type {bigint} */
336+
getsessionCreationRateLimited(){
337+
assertIsQuicEndpointStats(this);
338+
returnthis.#handle[IDX_STATS_ENDPOINT_SESSION_CREATION_RATE_LIMITED];
339+
}
340+
333341
toString(){
334342
returnJSONStringify(this.toJSON());
335343
}
@@ -354,6 +362,7 @@ class QuicEndpointStats {
354362
statelessResetRateLimited,
355363
immediateCloseCount,
356364
immediateCloseRateLimited,
365+
sessionCreationRateLimited,
357366
}=this;
358367
return{
359368
__proto__: null,
@@ -377,6 +386,7 @@ class QuicEndpointStats {
377386
statelessResetRateLimited: `${statelessResetRateLimited}`,
378387
immediateCloseCount: `${immediateCloseCount}`,
379388
immediateCloseRateLimited: `${immediateCloseRateLimited}`,
389+
sessionCreationRateLimited: `${sessionCreationRateLimited}`,
380390
};
381391
}
382392

@@ -410,6 +420,7 @@ class QuicEndpointStats {
410420
statelessResetRateLimited,
411421
immediateCloseCount,
412422
immediateCloseRateLimited,
423+
sessionCreationRateLimited,
413424
}=this;
414425

415426
return`QuicEndpointStats ${inspect({
@@ -431,6 +442,7 @@ class QuicEndpointStats {
431442
statelessResetRateLimited,
432443
immediateCloseCount,
433444
immediateCloseRateLimited,
445+
sessionCreationRateLimited,
434446
},opts)}`;
435447
}
436448

‎src/quic/bindingdata.h‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,8 @@ class SessionManager;
125125
V(version_negotiation_burst, "versionNegotiationBurst") \
126126
V(immediate_close_rate, "immediateCloseRate") \
127127
V(immediate_close_burst, "immediateCloseBurst") \
128+
V(session_creation_rate, "sessionCreationRate") \
129+
V(session_creation_burst, "sessionCreationBurst") \
128130
V(max_stream_window, "maxStreamWindow") \
129131
V(max_window, "maxWindow") \
130132
V(min_version, "minVersion") \

‎src/quic/defs.h‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -371,8 +371,15 @@ struct TokenBucket final {
371371
double tokens; // current token count
372372
uint64_t last_ts; // last refill timestamp (nanoseconds, uv_hrtime)
373373

374+
TokenBucket() : rate(0), burst(0), tokens(0), last_ts(0) {}
374375
TokenBucket(double rate, double burst);
375376

377+
// Reinitialize the bucket with new rate/burst parameters if it
378+
// hasn't been initialized yet (last_ts == 0). Used for per-host
379+
// buckets in the address LRU where the rate/burst aren't known
380+
// at construction time.
381+
voidInitOnce(double r, double b);
382+
376383
// Try to consume one token. Refills based on elapsed time, then
377384
// attempts to consume. Returns true if the request is allowed.
378385
boolconsume();

‎src/quic/endpoint.cc‎

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,8 @@ namespace quic {
7878
V(STATELESS_RESET_COUNT, stateless_reset_count) \
7979
V(STATELESS_RESET_RATE_LIMITED, stateless_reset_rate_limited) \
8080
V(IMMEDIATE_CLOSE_COUNT, immediate_close_count) \
81-
V(IMMEDIATE_CLOSE_RATE_LIMITED, immediate_close_rate_limited)
81+
V(IMMEDIATE_CLOSE_RATE_LIMITED, immediate_close_rate_limited) \
82+
V(SESSION_CREATION_RATE_LIMITED, session_creation_rate_limited)
8283

8384
structEndpoint::State {
8485
#defineV(_, name, type) type name;
@@ -91,6 +92,15 @@ STAT_STRUCT(Endpoint, ENDPOINT)
9192
TokenBucket::TokenBucket(double rate, double burst)
9293
: rate(rate), burst(burst), tokens(burst), last_ts(uv_hrtime()) {}
9394

95+
voidTokenBucket::InitOnce(double r, double b) {
96+
if (last_ts == 0) {
97+
rate = r;
98+
burst = b;
99+
tokens = b;
100+
last_ts = uv_hrtime();
101+
}
102+
}
103+
94104
// Try to consume one token. Refills based on elapsed time, then
95105
// attempts to consume. Returns true if the request is allowed.
96106
boolTokenBucket::consume() {
@@ -227,7 +237,8 @@ Maybe<Endpoint::Options> Endpoint::Options::From(Environment* env,
227237
!SET(retry_rate) || !SET(retry_burst) || !SET(stateless_reset_rate) ||
228238
!SET(stateless_reset_burst) || !SET(version_negotiation_rate) ||
229239
!SET(version_negotiation_burst) || !SET(immediate_close_rate) ||
230-
!SET(immediate_close_burst) ||
240+
!SET(immediate_close_burst) || !SET(session_creation_rate) ||
241+
!SET(session_creation_burst) ||
231242
#ifdef DEBUG
232243
!SET(rx_loss) || !SET(tx_loss) ||
233244
#endif
@@ -296,6 +307,11 @@ std::string Endpoint::Options::ToString() const {
296307
"immediate close rate: " + std::to_string(immediate_close_rate) + "/s";
297308
res += prefix +
298309
"immediate close burst: " + std::to_string(immediate_close_burst);
310+
res += prefix +
311+
"session creation rate: " + std::to_string(session_creation_rate) +
312+
"/s";
313+
res += prefix +
314+
"session creation burst: " + std::to_string(session_creation_burst);
299315
res += prefix + "validate address: " + boolToString(validate_address);
300316
res += prefix +
301317
"disable stateless reset: " + boolToString(disable_stateless_reset);
@@ -1331,6 +1347,19 @@ void Endpoint::Receive(const uint8_t* data,
13311347
// as a server, then we cannot accept the initial packet.
13321348
if (is_closed() || is_closing() || !is_listening()) return;
13331349

1350+
// Per-host session creation rate limit. The bucket is initialized
1351+
// on first access with the configured rate/burst from options.
1352+
auto info = addr_validation_lru_.Upsert(config.remote_address);
1353+
info->session_creation_bucket.InitOnce(options_.session_creation_rate,
1354+
options_.session_creation_burst);
1355+
if (!info->session_creation_bucket.consume()) {
1356+
Debug(this,
1357+
"Session creation rate limit exceeded for %s",
1358+
config.remote_address);
1359+
STAT_INCREMENT(Stats, session_creation_rate_limited);
1360+
return;
1361+
}
1362+
13341363
Debug(this, "Creating new session for %s", config.dcid);
13351364

13361365
std::optional<SessionTicket> no_ticket = std::nullopt;

‎src/quic/endpoint.h‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,14 @@ class Endpoint final : public AsyncWrap, public Packet::Listener {
4444
staticconstexprdoubleDEFAULT_IMMEDIATE_CLOSE_RATE = 100;
4545
staticconstexprdoubleDEFAULT_IMMEDIATE_CLOSE_BURST = 200;
4646

47+
// Per-host session creation rate limit. This is tracked per validated
48+
// remote address in the address LRU, preventing a single source from
49+
// churning through sessions faster than the server can handle. Unlike
50+
// the global stateless response buckets, this only applies after address
51+
// validation (spoofed sources can't reach this path).
52+
staticconstexprdoubleDEFAULT_SESSION_CREATION_RATE = 50;
53+
staticconstexprdoubleDEFAULT_SESSION_CREATION_BURST = 100;
54+
4755
// Endpoint configuration options
4856
structOptionsfinal : public MemoryRetainer {
4957
// The local socket address to which the UDP port will be bound. The port
@@ -83,6 +91,12 @@ class Endpoint final : public AsyncWrap, public Packet::Listener {
8391
double immediate_close_rate = DEFAULT_IMMEDIATE_CLOSE_RATE;
8492
double immediate_close_burst = DEFAULT_IMMEDIATE_CLOSE_BURST;
8593

94+
// Per-host session creation rate limit. Tracked per validated remote
95+
// address in the address LRU. Set to high values for benchmarking
96+
// where traffic comes from a single source.
97+
double session_creation_rate = DEFAULT_SESSION_CREATION_RATE;
98+
double session_creation_burst = DEFAULT_SESSION_CREATION_BURST;
99+
86100
// The validate_address parameter instructs the Endpoint to perform explicit
87101
// address validation using retry tokens. This is strongly recommended and
88102
// should only be disabled in trusted, closed environments as a performance
@@ -452,6 +466,7 @@ class Endpoint final : public AsyncWrap, public Packet::Listener {
452466
structTypefinal {
453467
uint64_t timestamp;
454468
bool validated;
469+
TokenBucket session_creation_bucket;
455470
};
456471

457472
staticboolCheckExpired(const SocketAddress& address, const Type& type);

‎test/parallel/test-quic-internal-endpoint-options.mjs‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,16 @@ const cases = [
9999
valid: [0,1,10,100.5,1000],
100100
invalid: [-1,'a',null,false,true,{},[],()=>{}]
101101
},
102+
{
103+
key: 'sessionCreationRate',
104+
valid: [0,1,10,100.5,1000,Infinity],
105+
invalid: [-1,'a',null,false,true,{},[],()=>{}]
106+
},
107+
{
108+
key: 'sessionCreationBurst',
109+
valid: [0,1,10,100.5,1000,Infinity],
110+
invalid: [-1,'a',null,false,true,{},[],()=>{}]
111+
},
102112
{
103113
key: 'validateAddress',
104114
valid: [true,false,0,1,'a'],

‎test/parallel/test-quic-internal-endpoint-stats-state.mjs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ const {
9595
strictEqual(typeofendpoint.stats.statelessResetRateLimited,'bigint');
9696
strictEqual(typeofendpoint.stats.immediateCloseCount,'bigint');
9797
strictEqual(typeofendpoint.stats.immediateCloseRateLimited,'bigint');
98+
strictEqual(typeofendpoint.stats.sessionCreationRateLimited,'bigint');
9899

99100
deepStrictEqual(Object.keys(endpoint.stats.toJSON()),[
100101
'connected',
@@ -115,6 +116,7 @@ const {
115116
'statelessResetRateLimited',
116117
'immediateCloseCount',
117118
'immediateCloseRateLimited',
119+
'sessionCreationRateLimited',
118120
]);
119121
strictEqual(typeofinspect(endpoint.stats),'string');
120122
}

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 11778a7

Browse files
jasnelladuh95
authored andcommitted
quic: add session creation 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 8107f1b commit 11778a7

9 files changed

Lines changed: 111 additions & 2 deletions

File tree

‎doc/api/quic.md‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -776,6 +776,12 @@ added: v23.8.0
776776
* Type: {bigint} The total number of immediate connection close packets
777777
dropped by the global rate limiter. Read only.
778778

779+
### `endpointStats.sessionCreationRateLimited`
780+
781+
* Type: {bigint} The total number of session creation attempts dropped by the
782+
per-host rate limiter. Read only. A non-zero value indicates one or more
783+
remote addresses are creating sessions faster than the configured rate allows.
784+
779785
## Class: `QuicSession`
780786

781787
<!-- YAML
@@ -2543,6 +2549,26 @@ send per second.
25432549
The maximum burst of immediate connection close packets allowed before rate
25442550
limiting takes effect.
25452551

2552+
#### `endpointOptions.sessionCreationRate`
2553+
2554+
* Type: {number}
2555+
***Default:**`50`
2556+
2557+
The maximum number of new sessions that a single remote address can create per
2558+
second. This is a per-host rate limit tracked in the address validation LRU
2559+
cache. It prevents a validated remote address from churning through sessions
2560+
(rapidly opening and abandoning connections) faster than the server can handle.
2561+
For benchmarking where traffic comes from a single source, set this to a high
2562+
value.
2563+
2564+
#### `endpointOptions.sessionCreationBurst`
2565+
2566+
* Type: {number}
2567+
***Default:**`100`
2568+
2569+
The maximum burst of new session creations allowed from a single remote address
2570+
before rate limiting takes effect.
2571+
25462572
#### `endpointOptions.retryTokenExpiration`
25472573

25482574
<!-- YAML

‎lib/internal/quic/quic.js‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,8 @@ const endpointRegistry = new SafeSet();
315315
* @property {number} [versionNegotiationBurst] Burst capacity for version negotiation rate limiter
316316
* @property {number} [immediateCloseRate] Global rate limit for immediate close packets (per second)
317317
* @property {number} [immediateCloseBurst] Burst capacity for immediate close rate limiter
318+
* @property {number} [sessionCreationRate] Per-host rate limit for session creation (per second)
319+
* @property {number} [sessionCreationBurst] Per-host burst capacity for session creation rate limiter
318320
* @property {ArrayBufferView} [resetTokenSecret] The reset token secret
319321
* @property {bigint|number} [retryTokenExpiration] The retry token expiration
320322
* @property {number} [rxDiagnosticLoss] The receive diagnostic loss probability (range 0.0-1.0)
@@ -4013,6 +4015,8 @@ class QuicEndpoint {
40134015
versionNegotiationBurst,
40144016
immediateCloseRate,
40154017
immediateCloseBurst,
4018+
sessionCreationRate,
4019+
sessionCreationBurst,
40164020
rxDiagnosticLoss,
40174021
txDiagnosticLoss,
40184022
udpReceiveBufferSize,
@@ -4056,6 +4060,8 @@ class QuicEndpoint {
40564060
versionNegotiationBurst,
40574061
immediateCloseRate,
40584062
immediateCloseBurst,
4063+
sessionCreationRate,
4064+
sessionCreationBurst,
40594065
rxDiagnosticLoss,
40604066
txDiagnosticLoss,
40614067
udpReceiveBufferSize,

‎lib/internal/quic/stats.js‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ const {
6767
IDX_STATS_ENDPOINT_STATELESS_RESET_RATE_LIMITED,
6868
IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_COUNT,
6969
IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_RATE_LIMITED,
70+
IDX_STATS_ENDPOINT_SESSION_CREATION_RATE_LIMITED,
7071

7172
IDX_STATS_SESSION_CREATED_AT,
7273
IDX_STATS_SESSION_DESTROYED_AT,
@@ -134,6 +135,7 @@ assert(IDX_STATS_ENDPOINT_STATELESS_RESET_COUNT !== undefined);
134135
assert(IDX_STATS_ENDPOINT_STATELESS_RESET_RATE_LIMITED!==undefined);
135136
assert(IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_COUNT!==undefined);
136137
assert(IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_RATE_LIMITED!==undefined);
138+
assert(IDX_STATS_ENDPOINT_SESSION_CREATION_RATE_LIMITED!==undefined);
137139
assert(IDX_STATS_SESSION_CREATED_AT!==undefined);
138140
assert(IDX_STATS_SESSION_DESTROYED_AT!==undefined);
139141
assert(IDX_STATS_SESSION_CLOSING_AT!==undefined);
@@ -330,6 +332,12 @@ class QuicEndpointStats {
330332
returnthis.#handle[IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_RATE_LIMITED];
331333
}
332334

335+
/** @type {bigint} */
336+
getsessionCreationRateLimited(){
337+
assertIsQuicEndpointStats(this);
338+
returnthis.#handle[IDX_STATS_ENDPOINT_SESSION_CREATION_RATE_LIMITED];
339+
}
340+
333341
toString(){
334342
returnJSONStringify(this.toJSON());
335343
}
@@ -354,6 +362,7 @@ class QuicEndpointStats {
354362
statelessResetRateLimited,
355363
immediateCloseCount,
356364
immediateCloseRateLimited,
365+
sessionCreationRateLimited,
357366
}=this;
358367
return{
359368
__proto__: null,
@@ -377,6 +386,7 @@ class QuicEndpointStats {
377386
statelessResetRateLimited: `${statelessResetRateLimited}`,
378387
immediateCloseCount: `${immediateCloseCount}`,
379388
immediateCloseRateLimited: `${immediateCloseRateLimited}`,
389+
sessionCreationRateLimited: `${sessionCreationRateLimited}`,
380390
};
381391
}
382392

@@ -410,6 +420,7 @@ class QuicEndpointStats {
410420
statelessResetRateLimited,
411421
immediateCloseCount,
412422
immediateCloseRateLimited,
423+
sessionCreationRateLimited,
413424
}=this;
414425

415426
return`QuicEndpointStats ${inspect({
@@ -431,6 +442,7 @@ class QuicEndpointStats {
431442
statelessResetRateLimited,
432443
immediateCloseCount,
433444
immediateCloseRateLimited,
445+
sessionCreationRateLimited,
434446
},opts)}`;
435447
}
436448

‎src/quic/bindingdata.h‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,8 @@ class SessionManager;
125125
V(version_negotiation_burst, "versionNegotiationBurst") \
126126
V(immediate_close_rate, "immediateCloseRate") \
127127
V(immediate_close_burst, "immediateCloseBurst") \
128+
V(session_creation_rate, "sessionCreationRate") \
129+
V(session_creation_burst, "sessionCreationBurst") \
128130
V(max_stream_window, "maxStreamWindow") \
129131
V(max_window, "maxWindow") \
130132
V(min_version, "minVersion") \

‎src/quic/defs.h‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -371,8 +371,15 @@ struct TokenBucket final {
371371
double tokens; // current token count
372372
uint64_t last_ts; // last refill timestamp (nanoseconds, uv_hrtime)
373373

374+
TokenBucket() : rate(0), burst(0), tokens(0), last_ts(0) {}
374375
TokenBucket(double rate, double burst);
375376

377+
// Reinitialize the bucket with new rate/burst parameters if it
378+
// hasn't been initialized yet (last_ts == 0). Used for per-host
379+
// buckets in the address LRU where the rate/burst aren't known
380+
// at construction time.
381+
voidInitOnce(double r, double b);
382+
376383
// Try to consume one token. Refills based on elapsed time, then
377384
// attempts to consume. Returns true if the request is allowed.
378385
boolconsume();

‎src/quic/endpoint.cc‎

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,8 @@ namespace quic {
7878
V(STATELESS_RESET_COUNT, stateless_reset_count) \
7979
V(STATELESS_RESET_RATE_LIMITED, stateless_reset_rate_limited) \
8080
V(IMMEDIATE_CLOSE_COUNT, immediate_close_count) \
81-
V(IMMEDIATE_CLOSE_RATE_LIMITED, immediate_close_rate_limited)
81+
V(IMMEDIATE_CLOSE_RATE_LIMITED, immediate_close_rate_limited) \
82+
V(SESSION_CREATION_RATE_LIMITED, session_creation_rate_limited)
8283

8384
structEndpoint::State {
8485
#defineV(_, name, type) type name;
@@ -91,6 +92,15 @@ STAT_STRUCT(Endpoint, ENDPOINT)
9192
TokenBucket::TokenBucket(double rate, double burst)
9293
: rate(rate), burst(burst), tokens(burst), last_ts(uv_hrtime()) {}
9394

95+
voidTokenBucket::InitOnce(double r, double b) {
96+
if (last_ts == 0) {
97+
rate = r;
98+
burst = b;
99+
tokens = b;
100+
last_ts = uv_hrtime();
101+
}
102+
}
103+
94104
// Try to consume one token. Refills based on elapsed time, then
95105
// attempts to consume. Returns true if the request is allowed.
96106
boolTokenBucket::consume() {
@@ -227,7 +237,8 @@ Maybe<Endpoint::Options> Endpoint::Options::From(Environment* env,
227237
!SET(retry_rate) || !SET(retry_burst) || !SET(stateless_reset_rate) ||
228238
!SET(stateless_reset_burst) || !SET(version_negotiation_rate) ||
229239
!SET(version_negotiation_burst) || !SET(immediate_close_rate) ||
230-
!SET(immediate_close_burst) ||
240+
!SET(immediate_close_burst) || !SET(session_creation_rate) ||
241+
!SET(session_creation_burst) ||
231242
#ifdef DEBUG
232243
!SET(rx_loss) || !SET(tx_loss) ||
233244
#endif
@@ -296,6 +307,11 @@ std::string Endpoint::Options::ToString() const {
296307
"immediate close rate: " + std::to_string(immediate_close_rate) + "/s";
297308
res += prefix +
298309
"immediate close burst: " + std::to_string(immediate_close_burst);
310+
res += prefix +
311+
"session creation rate: " + std::to_string(session_creation_rate) +
312+
"/s";
313+
res += prefix +
314+
"session creation burst: " + std::to_string(session_creation_burst);
299315
res += prefix + "validate address: " + boolToString(validate_address);
300316
res += prefix +
301317
"disable stateless reset: " + boolToString(disable_stateless_reset);
@@ -1331,6 +1347,19 @@ void Endpoint::Receive(const uint8_t* data,
13311347
// as a server, then we cannot accept the initial packet.
13321348
if (is_closed() || is_closing() || !is_listening()) return;
13331349

1350+
// Per-host session creation rate limit. The bucket is initialized
1351+
// on first access with the configured rate/burst from options.
1352+
auto info = addr_validation_lru_.Upsert(config.remote_address);
1353+
info->session_creation_bucket.InitOnce(options_.session_creation_rate,
1354+
options_.session_creation_burst);
1355+
if (!info->session_creation_bucket.consume()) {
1356+
Debug(this,
1357+
"Session creation rate limit exceeded for %s",
1358+
config.remote_address);
1359+
STAT_INCREMENT(Stats, session_creation_rate_limited);
1360+
return;
1361+
}
1362+
13341363
Debug(this, "Creating new session for %s", config.dcid);
13351364

13361365
std::optional<SessionTicket> no_ticket = std::nullopt;

‎src/quic/endpoint.h‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,14 @@ class Endpoint final : public AsyncWrap, public Packet::Listener {
4444
staticconstexprdoubleDEFAULT_IMMEDIATE_CLOSE_RATE = 100;
4545
staticconstexprdoubleDEFAULT_IMMEDIATE_CLOSE_BURST = 200;
4646

47+
// Per-host session creation rate limit. This is tracked per validated
48+
// remote address in the address LRU, preventing a single source from
49+
// churning through sessions faster than the server can handle. Unlike
50+
// the global stateless response buckets, this only applies after address
51+
// validation (spoofed sources can't reach this path).
52+
staticconstexprdoubleDEFAULT_SESSION_CREATION_RATE = 50;
53+
staticconstexprdoubleDEFAULT_SESSION_CREATION_BURST = 100;
54+
4755
// Endpoint configuration options
4856
structOptionsfinal : public MemoryRetainer {
4957
// The local socket address to which the UDP port will be bound. The port
@@ -83,6 +91,12 @@ class Endpoint final : public AsyncWrap, public Packet::Listener {
8391
double immediate_close_rate = DEFAULT_IMMEDIATE_CLOSE_RATE;
8492
double immediate_close_burst = DEFAULT_IMMEDIATE_CLOSE_BURST;
8593

94+
// Per-host session creation rate limit. Tracked per validated remote
95+
// address in the address LRU. Set to high values for benchmarking
96+
// where traffic comes from a single source.
97+
double session_creation_rate = DEFAULT_SESSION_CREATION_RATE;
98+
double session_creation_burst = DEFAULT_SESSION_CREATION_BURST;
99+
86100
// The validate_address parameter instructs the Endpoint to perform explicit
87101
// address validation using retry tokens. This is strongly recommended and
88102
// should only be disabled in trusted, closed environments as a performance
@@ -452,6 +466,7 @@ class Endpoint final : public AsyncWrap, public Packet::Listener {
452466
structTypefinal {
453467
uint64_t timestamp;
454468
bool validated;
469+
TokenBucket session_creation_bucket;
455470
};
456471

457472
staticboolCheckExpired(const SocketAddress& address, const Type& type);

‎test/parallel/test-quic-internal-endpoint-options.mjs‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,16 @@ const cases = [
9999
valid: [0,1,10,100.5,1000],
100100
invalid: [-1,'a',null,false,true,{},[],()=>{}]
101101
},
102+
{
103+
key: 'sessionCreationRate',
104+
valid: [0,1,10,100.5,1000,Infinity],
105+
invalid: [-1,'a',null,false,true,{},[],()=>{}]
106+
},
107+
{
108+
key: 'sessionCreationBurst',
109+
valid: [0,1,10,100.5,1000,Infinity],
110+
invalid: [-1,'a',null,false,true,{},[],()=>{}]
111+
},
102112
{
103113
key: 'validateAddress',
104114
valid: [true,false,0,1,'a'],

‎test/parallel/test-quic-internal-endpoint-stats-state.mjs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ const {
9595
strictEqual(typeofendpoint.stats.statelessResetRateLimited,'bigint');
9696
strictEqual(typeofendpoint.stats.immediateCloseCount,'bigint');
9797
strictEqual(typeofendpoint.stats.immediateCloseRateLimited,'bigint');
98+
strictEqual(typeofendpoint.stats.sessionCreationRateLimited,'bigint');
9899

99100
deepStrictEqual(Object.keys(endpoint.stats.toJSON()),[
100101
'connected',
@@ -115,6 +116,7 @@ const {
115116
'statelessResetRateLimited',
116117
'immediateCloseCount',
117118
'immediateCloseRateLimited',
119+
'sessionCreationRateLimited',
118120
]);
119121
strictEqual(typeofinspect(endpoint.stats),'string');
120122
}

0 commit comments

Comments
 (0)