| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent 8107f1b commit 11778a7
9 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -776,6 +776,12 @@ added: v23.8.0 | |||
| 776 | 776 | * Type: {bigint} The total number of immediate connection close packets | |
| 777 | 777 | dropped by the global rate limiter. Read only. | |
| 778 | 778 | ||
| 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 | + | ||
| 779 | 785 | ## Class: `QuicSession` | |
| 780 | 786 | ||
| 781 | 787 | <!-- YAML | |
@@ -2543,6 +2549,26 @@ send per second. | |||
| 2543 | 2549 | The maximum burst of immediate connection close packets allowed before rate | |
| 2544 | 2550 | limiting takes effect. | |
| 2545 | 2551 | ||
| 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 | + | ||
| 2546 | 2572 | #### `endpointOptions.retryTokenExpiration` | |
| 2547 | 2573 | ||
| 2548 | 2574 | <!-- YAML | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -315,6 +315,8 @@ const endpointRegistry = new SafeSet(); | |||
| 315 | 315 | * @property {number} [versionNegotiationBurst] Burst capacity for version negotiation rate limiter | |
| 316 | 316 | * @property {number} [immediateCloseRate] Global rate limit for immediate close packets (per second) | |
| 317 | 317 | * @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 | ||
| 318 | 320 | * @property {ArrayBufferView} [resetTokenSecret] The reset token secret | |
| 319 | 321 | * @property {bigint|number} [retryTokenExpiration] The retry token expiration | |
| 320 | 322 | * @property {number} [rxDiagnosticLoss] The receive diagnostic loss probability (range 0.0-1.0) | |
@@ -4013,6 +4015,8 @@ class QuicEndpoint { | |||
| 4013 | 4015 | versionNegotiationBurst, | |
| 4014 | 4016 | immediateCloseRate, | |
| 4015 | 4017 | immediateCloseBurst, | |
| 4018 | + sessionCreationRate, | ||
| 4019 | + sessionCreationBurst, | ||
| 4016 | 4020 | rxDiagnosticLoss, | |
| 4017 | 4021 | txDiagnosticLoss, | |
| 4018 | 4022 | udpReceiveBufferSize, | |
@@ -4056,6 +4060,8 @@ class QuicEndpoint { | |||
| 4056 | 4060 | versionNegotiationBurst, | |
| 4057 | 4061 | immediateCloseRate, | |
| 4058 | 4062 | immediateCloseBurst, | |
| 4063 | + sessionCreationRate, | ||
| 4064 | + sessionCreationBurst, | ||
| 4059 | 4065 | rxDiagnosticLoss, | |
| 4060 | 4066 | txDiagnosticLoss, | |
| 4061 | 4067 | udpReceiveBufferSize, | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -67,6 +67,7 @@ const { | |||
| 67 | 67 | IDX_STATS_ENDPOINT_STATELESS_RESET_RATE_LIMITED, | |
| 68 | 68 | IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_COUNT, | |
| 69 | 69 | IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_RATE_LIMITED, | |
| 70 | + IDX_STATS_ENDPOINT_SESSION_CREATION_RATE_LIMITED, | ||
| 70 | 71 | ||
| 71 | 72 | IDX_STATS_SESSION_CREATED_AT, | |
| 72 | 73 | IDX_STATS_SESSION_DESTROYED_AT, | |
@@ -134,6 +135,7 @@ assert(IDX_STATS_ENDPOINT_STATELESS_RESET_COUNT !== undefined); | |||
| 134 | 135 | assert(IDX_STATS_ENDPOINT_STATELESS_RESET_RATE_LIMITED !== undefined); | |
| 135 | 136 | assert(IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_COUNT !== undefined); | |
| 136 | 137 | assert(IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_RATE_LIMITED !== undefined); | |
| 138 | + assert(IDX_STATS_ENDPOINT_SESSION_CREATION_RATE_LIMITED !== undefined); | ||
| 137 | 139 | assert(IDX_STATS_SESSION_CREATED_AT !== undefined); | |
| 138 | 140 | assert(IDX_STATS_SESSION_DESTROYED_AT !== undefined); | |
| 139 | 141 | assert(IDX_STATS_SESSION_CLOSING_AT !== undefined); | |
@@ -330,6 +332,12 @@ class QuicEndpointStats { | |||
| 330 | 332 | return this.#handle[IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_RATE_LIMITED]; | |
| 331 | 333 | } | |
| 332 | 334 | ||
| 335 | + /** @type {bigint} */ | ||
| 336 | + get sessionCreationRateLimited() { | ||
| 337 | + assertIsQuicEndpointStats(this); | ||
| 338 | + return this.#handle[IDX_STATS_ENDPOINT_SESSION_CREATION_RATE_LIMITED]; | ||
| 339 | + } | ||
| 340 | + | ||
| 333 | 341 | toString() { | |
| 334 | 342 | return JSONStringify(this.toJSON()); | |
| 335 | 343 | } | |
@@ -354,6 +362,7 @@ class QuicEndpointStats { | |||
| 354 | 362 | statelessResetRateLimited, | |
| 355 | 363 | immediateCloseCount, | |
| 356 | 364 | immediateCloseRateLimited, | |
| 365 | + sessionCreationRateLimited, | ||
| 357 | 366 | } = this; | |
| 358 | 367 | return { | |
| 359 | 368 | __proto__: null, | |
@@ -377,6 +386,7 @@ class QuicEndpointStats { | |||
| 377 | 386 | statelessResetRateLimited: `${statelessResetRateLimited}`, | |
| 378 | 387 | immediateCloseCount: `${immediateCloseCount}`, | |
| 379 | 388 | immediateCloseRateLimited: `${immediateCloseRateLimited}`, | |
| 389 | + sessionCreationRateLimited: `${sessionCreationRateLimited}`, | ||
| 380 | 390 | }; | |
| 381 | 391 | } | |
| 382 | 392 | ||
@@ -410,6 +420,7 @@ class QuicEndpointStats { | |||
| 410 | 420 | statelessResetRateLimited, | |
| 411 | 421 | immediateCloseCount, | |
| 412 | 422 | immediateCloseRateLimited, | |
| 423 | + sessionCreationRateLimited, | ||
| 413 | 424 | } = this; | |
| 414 | 425 | ||
| 415 | 426 | return `QuicEndpointStats ${inspect({ | |
@@ -431,6 +442,7 @@ class QuicEndpointStats { | |||
| 431 | 442 | statelessResetRateLimited, | |
| 432 | 443 | immediateCloseCount, | |
| 433 | 444 | immediateCloseRateLimited, | |
| 445 | + sessionCreationRateLimited, | ||
| 434 | 446 | }, opts)}`; | |
| 435 | 447 | } | |
| 436 | 448 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -125,6 +125,8 @@ class SessionManager; | |||
| 125 | 125 | V(version_negotiation_burst, "versionNegotiationBurst") \ | |
| 126 | 126 | V(immediate_close_rate, "immediateCloseRate") \ | |
| 127 | 127 | V(immediate_close_burst, "immediateCloseBurst") \ | |
| 128 | + V(session_creation_rate, "sessionCreationRate") \ | ||
| 129 | + V(session_creation_burst, "sessionCreationBurst") \ | ||
| 128 | 130 | V(max_stream_window, "maxStreamWindow") \ | |
| 129 | 131 | V(max_window, "maxWindow") \ | |
| 130 | 132 | V(min_version, "minVersion") \ | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -371,8 +371,15 @@ struct TokenBucket final { | |||
| 371 | 371 | double tokens; // current token count | |
| 372 | 372 | uint64_t last_ts; // last refill timestamp (nanoseconds, uv_hrtime) | |
| 373 | 373 | ||
| 374 | + TokenBucket() : rate(0), burst(0), tokens(0), last_ts(0) {} | ||
| 374 | 375 | TokenBucket(double rate, double burst); | |
| 375 | 376 | ||
| 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 | + void InitOnce(double r, double b); | ||
| 382 | + | ||
| 376 | 383 | // Try to consume one token. Refills based on elapsed time, then | |
| 377 | 384 | // attempts to consume. Returns true if the request is allowed. | |
| 378 | 385 | bool consume(); | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -78,7 +78,8 @@ namespace quic { | |||
| 78 | 78 | V(STATELESS_RESET_COUNT, stateless_reset_count) \ | |
| 79 | 79 | V(STATELESS_RESET_RATE_LIMITED, stateless_reset_rate_limited) \ | |
| 80 | 80 | 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) | ||
| 82 | 83 | ||
| 83 | 84 | struct Endpoint::State { | |
| 84 | 85 | #define V(_, name, type) type name; | |
@@ -91,6 +92,15 @@ STAT_STRUCT(Endpoint, ENDPOINT) | |||
| 91 | 92 | TokenBucket::TokenBucket(double rate, double burst) | |
| 92 | 93 | : rate(rate), burst(burst), tokens(burst), last_ts(uv_hrtime()) {} | |
| 93 | 94 | ||
| 95 | + void TokenBucket::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 | + | ||
| 94 | 104 | // Try to consume one token. Refills based on elapsed time, then | |
| 95 | 105 | // attempts to consume. Returns true if the request is allowed. | |
| 96 | 106 | bool TokenBucket::consume() { | |
@@ -227,7 +237,8 @@ Maybe<Endpoint::Options> Endpoint::Options::From(Environment* env, | |||
| 227 | 237 | !SET(retry_rate) || !SET(retry_burst) || !SET(stateless_reset_rate) || | |
| 228 | 238 | !SET(stateless_reset_burst) || !SET(version_negotiation_rate) || | |
| 229 | 239 | !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) || | ||
| 231 | 242 | #ifdef DEBUG | |
| 232 | 243 | !SET(rx_loss) || !SET(tx_loss) || | |
| 233 | 244 | #endif | |
@@ -296,6 +307,11 @@ std::string Endpoint::Options::ToString() const { | |||
| 296 | 307 | "immediate close rate: " + std::to_string(immediate_close_rate) + "/s"; | |
| 297 | 308 | res += prefix + | |
| 298 | 309 | "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); | ||
| 299 | 315 | res += prefix + "validate address: " + boolToString(validate_address); | |
| 300 | 316 | res += prefix + | |
| 301 | 317 | "disable stateless reset: " + boolToString(disable_stateless_reset); | |
@@ -1331,6 +1347,19 @@ void Endpoint::Receive(const uint8_t* data, | |||
| 1331 | 1347 | // as a server, then we cannot accept the initial packet. | |
| 1332 | 1348 | if (is_closed() || is_closing() || !is_listening()) return; | |
| 1333 | 1349 | ||
| 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 | + | ||
| 1334 | 1363 | Debug(this, "Creating new session for %s", config.dcid); | |
| 1335 | 1364 | ||
| 1336 | 1365 | std::optional<SessionTicket> no_ticket = std::nullopt; | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -44,6 +44,14 @@ class Endpoint final : public AsyncWrap, public Packet::Listener { | |||
| 44 | 44 | static constexpr double DEFAULT_IMMEDIATE_CLOSE_RATE = 100; | |
| 45 | 45 | static constexpr double DEFAULT_IMMEDIATE_CLOSE_BURST = 200; | |
| 46 | 46 | ||
| 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 | + static constexpr double DEFAULT_SESSION_CREATION_RATE = 50; | ||
| 53 | + static constexpr double DEFAULT_SESSION_CREATION_BURST = 100; | ||
| 54 | + | ||
| 47 | 55 | // Endpoint configuration options | |
| 48 | 56 | struct Options final : public MemoryRetainer { | |
| 49 | 57 | // 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 { | |||
| 83 | 91 | double immediate_close_rate = DEFAULT_IMMEDIATE_CLOSE_RATE; | |
| 84 | 92 | double immediate_close_burst = DEFAULT_IMMEDIATE_CLOSE_BURST; | |
| 85 | 93 | ||
| 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 | + | ||
| 86 | 100 | // The validate_address parameter instructs the Endpoint to perform explicit | |
| 87 | 101 | // address validation using retry tokens. This is strongly recommended and | |
| 88 | 102 | // should only be disabled in trusted, closed environments as a performance | |
@@ -452,6 +466,7 @@ class Endpoint final : public AsyncWrap, public Packet::Listener { | |||
| 452 | 466 | struct Type final { | |
| 453 | 467 | uint64_t timestamp; | |
| 454 | 468 | bool validated; | |
| 469 | + TokenBucket session_creation_bucket; | ||
| 455 | 470 | }; | |
| 456 | 471 | ||
| 457 | 472 | static bool CheckExpired(const SocketAddress& address, const Type& type); | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -99,6 +99,16 @@ const cases = [ | |||
| 99 | 99 | valid: [0, 1, 10, 100.5, 1000], | |
| 100 | 100 | invalid: [-1, 'a', null, false, true, {}, [], () => {}] | |
| 101 | 101 | }, | |
| 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 | + }, | ||
| 102 | 112 | { | |
| 103 | 113 | key: 'validateAddress', | |
| 104 | 114 | valid: [true, false, 0, 1, 'a'], | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -95,6 +95,7 @@ const { | |||
| 95 | 95 | strictEqual(typeof endpoint.stats.statelessResetRateLimited, 'bigint'); | |
| 96 | 96 | strictEqual(typeof endpoint.stats.immediateCloseCount, 'bigint'); | |
| 97 | 97 | strictEqual(typeof endpoint.stats.immediateCloseRateLimited, 'bigint'); | |
| 98 | + strictEqual(typeof endpoint.stats.sessionCreationRateLimited, 'bigint'); | ||
| 98 | 99 | ||
| 99 | 100 | deepStrictEqual(Object.keys(endpoint.stats.toJSON()), [ | |
| 100 | 101 | 'connected', | |
@@ -115,6 +116,7 @@ const { | |||
| 115 | 116 | 'statelessResetRateLimited', | |
| 116 | 117 | 'immediateCloseCount', | |
| 117 | 118 | 'immediateCloseRateLimited', | |
| 119 | + 'sessionCreationRateLimited', | ||
| 118 | 120 | ]); | |
| 119 | 121 | strictEqual(typeof inspect(endpoint.stats), 'string'); | |
| 120 | 122 | } | |
| Back | FazBrowse Home | New Git URL |
0 commit comments