quico 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -299,6 +299,7 @@ function QUICConnection(options) {
299
299
  // complete"), issues no connection IDs and sends no HANDSHAKE_DONE.
300
300
  peer_finished: false,
301
301
  handshake_done_sent: false,
302
+ new_token_for: null, // the address:port the last NEW_TOKEN was for (issueNewToken)
302
303
 
303
304
  // Connection close (RFC 9000 §10.2)
304
305
  close_frame: null, // the CONNECTION_CLOSE frame to (re)send while closing
@@ -703,13 +704,18 @@ function QUICConnection(options) {
703
704
 
704
705
  max_packets_per_burst: 20, // fixed cap — not CC-controlled, no current_/init_
705
706
 
706
- // packet payload (MTU). current_ is the size actually used; max_ is the ceiling
707
- // DPLPMTUD must not probe past; init_/floor is QUIC's guaranteed 1200-byte minimum.
707
+ // packet size (MTU). Datagram sizes are WHOLE UDP payloads: max_ is the
708
+ // ceiling DPLPMTUD must not probe past; init_ is QUIC's 1200 — RFC 9000
709
+ // §14: no datagram larger until a path has shown it carries one; current_
710
+ // is the size in use. current_limit_packet_payload is what a packet has
711
+ // for FRAMES: that datagram, within the peer's max_udp_payload_size, less
712
+ // the packet's header and AEAD tag (syncPacketPayload).
708
713
  max_limit_packet_payload: connOpts.maxUdpPayloadSize, // ceiling, maxUdpPayloadSize: 1452 (IPv6-safe Ethernet: 1500 − 48 hdr) —
709
714
  // a whole UDP datagram: what DPLPMTUD probes up to
710
- init_limit_packet_payload: 1200, // QUIC guaranteed floor — where we start
711
- current_limit_packet_payload: 1200, // = init_; DPLPMTUD raises it: the frames a packet
712
- // carries, the datagram it confirmed less its header
715
+ init_limit_datagram: 1200, // QUIC's guaranteed floor — where we start
716
+ current_limit_datagram: 1200, // = init_; DPLPMTUD raises it
717
+ current_limit_packet_payload: 1200 - 69, // frames; set by syncPacketPayload — until then,
718
+ // 1200 less the largest header (a 0-RTT packet, 20-byte CIDs)
713
719
  pmtu: null, // DPLPMTUD's state (pmtuReset)
714
720
  peer_max_udp_payload: 65527, // its max_udp_payload_size (RFC 9000 §18.2)
715
721
 
@@ -738,7 +744,7 @@ function QUICConnection(options) {
738
744
  current_limit_packets_per_sec: 12000, // = init_; BBR rewrites at round-end
739
745
  max_limit_bytes_per_sec: 14000000, // ceiling ~14 MB/s — coherent ↑
740
746
  min_limit_bytes_per_sec: 12000, // floor ~96 kbps — avoid 0/deadlock
741
- init_limit_bytes_per_sec: 14000000,
747
+ init_limit_bytes_per_sec: 14000000, // until the first RTT sample (bbrInitPacingRate)
742
748
  current_limit_bytes_per_sec: 14000000, // = init_; BBR rewrites at round-end
743
749
 
744
750
  burst_timer: null,
@@ -925,6 +931,10 @@ function QUICConnection(options) {
925
931
  // a round says how much the APPLICATION produced, not what the path can
926
932
  // carry — see the sample filter at round end.
927
933
  bbr_round_app_limited: false,
934
+ // The most bytes in flight in this round and in the last — whether the
935
+ // window is in use, for Startup's growth (growStartupWindow).
936
+ bbr_peak_inflight: 0,
937
+ bbr_prev_peak_inflight: 0,
928
938
  bbr_full_bw: 0, // highest BtlBw seen — Startup plateau detector
929
939
  bbr_full_bw_count: 0, // consecutive rounds without ≥25% BtlBw growth
930
940
  // ACK aggregation (BBR's extra_acked): how far ACKs ran ahead of what
@@ -1461,7 +1471,7 @@ function QUICConnection(options) {
1461
1471
  // the search may go there.
1462
1472
  function setMaxUdpPayload(v) {
1463
1473
  context.max_limit_packet_payload = v;
1464
- var now = context.current_limit_packet_payload + shortHeaderOverhead();
1474
+ var now = context.current_limit_datagram;
1465
1475
  var m = context.pmtu;
1466
1476
  if (now > v) pmtuApply(v);
1467
1477
  if (m) {
@@ -1798,6 +1808,8 @@ function QUICConnection(options) {
1798
1808
  // as a CRYPTO_ERROR (0x100 + alert code, RFC 9001 §4.8) — the peer learns
1799
1809
  // why, and this side reports the close now rather than at a timeout.
1800
1810
  tls.on('alert', function (code) { close(0x100 + code, 'TLS alert ' + code); });
1811
+ // The peer's quic_transport_parameters that cannot be read at all.
1812
+ tls.on('transportParamsError', function (why) { close(0x08, why); });
1801
1813
  // The reason for that alert. During the handshake it is the connection's
1802
1814
  // failure, reported like the handshake timeout: 'error', then 'close'.
1803
1815
  // The alert has already closed the connection, so nothing else to do.
@@ -1923,6 +1935,7 @@ function QUICConnection(options) {
1923
1935
  // a bound on what DPLPMTUD probes. "Values below 1200 are invalid."
1924
1936
  if (typeof p.max_udp_payload_size === 'number') {
1925
1937
  context.peer_max_udp_payload = p.max_udp_payload_size;
1938
+ syncPacketPayload();
1926
1939
  }
1927
1940
  // Stream-count limits the peer grants us (localStreamPermitted holds
1928
1941
  // back a stream past them). Seeded directly, not through set_context:
@@ -2063,10 +2076,7 @@ function QUICConnection(options) {
2063
2076
  queueControl('hd'); // retransmitted until acknowledged (§13.3)
2064
2077
  // A token for the client's next connection (NEW_TOKEN, §8.1.3):
2065
2078
  // it will need no proof of address then.
2066
- if (typeof options.newToken === 'function') {
2067
- var nt = options.newToken(peerPath().address, context.version);
2068
- if (nt && nt.byteLength > 0) queueControl('nt', { type: 'new_token', token: nt });
2069
- }
2079
+ issueNewToken(peerPath());
2070
2080
  }
2071
2081
  discardSpace('handshake');
2072
2082
  crStart();
@@ -2081,9 +2091,10 @@ function QUICConnection(options) {
2081
2091
  }
2082
2092
  });
2083
2093
 
2084
- // Note: QUIC handles key updates via key_phase bit, not TLS KeyUpdate message.
2085
- // RFC 9001 §6: "endpoints MUST treat the receipt of a TLS KeyUpdate message
2086
- // as a connection error of type 0x010a" — but we silently ignore it.
2094
+ // QUIC updates keys with the Key Phase bit, never a TLS KeyUpdate (RFC
2095
+ // 9001 §6): one received, in any space, closes the connection with 0x010a
2096
+ // (TLS_RECEIVABLE, in processCryptoFrame); one TLS would send is not sent
2097
+ // (tls_bridge).
2087
2098
  }
2088
2099
 
2089
2100
 
@@ -2365,13 +2376,22 @@ function QUICConnection(options) {
2365
2376
  );
2366
2377
  }
2367
2378
 
2368
- if (!decrypted || !decrypted.plaintext || decrypted.plaintext.byteLength === 0) {
2379
+ if (!decrypted || !decrypted.plaintext) {
2369
2380
  if (DEBUG) console.log('[quic] decrypt failed: ' + space + ' raw_len=' + pkt.raw.byteLength + ' first20=' + Array.from(pkt.raw.slice(0, 20)).map(function(b){ return b.toString(16).padStart(2,'0'); }).join(' ') + ' recv_cid_len=' + (recvCid ? recvCid.byteLength : 'null') + ' has_keys=' + !!readKeys + ' largest_pn=' + context.recv_pn_largest[space]);
2370
2381
  return;
2371
2382
  }
2372
2383
 
2373
2384
  if (DEBUG) console.log('[quic] decrypted ' + space + ' pn=' + decrypted.packet_number + ' len=' + decrypted.plaintext.byteLength);
2374
2385
 
2386
+ // The reserved bits, now that the packet is authenticated: set, it is a
2387
+ // PROTOCOL_VIOLATION (RFC 9000 §17.2: 0x0c of a long header; §17.3.1:
2388
+ // 0x18 of a short one). An empty payload goes on to the frames, where a
2389
+ // packet with none is one too (§12.4).
2390
+ if (decrypted.first_byte & ((decrypted.first_byte & 0x80) ? 0x0c : 0x18)) {
2391
+ close(0x0a, 'reserved header bits set in a ' + space + ' packet');
2392
+ return;
2393
+ }
2394
+
2375
2395
  // RFC 9000 §8.1: "A server might wish to validate the client address
2376
2396
  // before starting the cryptographic handshake. [...] the server MAY
2377
2397
  // consider the peer address validated if the client uses a connection ID
@@ -2678,10 +2698,15 @@ function QUICConnection(options) {
2678
2698
  } else if (frame.type === 'streams_blocked_bidi' || frame.type === 'streams_blocked_uni') {
2679
2699
  // The peer has used every stream we granted. Ack-eliciting like any
2680
2700
  // frame but ACK/PADDING/CONNECTION_CLOSE (it used to fall through
2681
- // unacknowledged); answered with our current limit, which also
2682
- // recovers a lost MAX_STREAMS, as DATA_BLOCKED does for MAX_DATA.
2701
+ // unacknowledged). Blocked below our current limit, it missed a
2702
+ // MAX_STREAMS: sent again — as DATA_BLOCKED recovers MAX_DATA. At it,
2703
+ // the peer has our limit already, and a MAX_STREAMS for each such
2704
+ // frame would be one frame of ours for every one of its.
2683
2705
  ackEliciting = true;
2684
- queueControl(frame.type === 'streams_blocked_bidi' ? 'ms_bidi' : 'ms_uni');
2706
+ var sbBidi = frame.type === 'streams_blocked_bidi';
2707
+ if (frame.limit < (sbBidi ? context.local_max_streams_bidi : context.local_max_streams_uni)) {
2708
+ queueControl(sbBidi ? 'ms_bidi' : 'ms_uni');
2709
+ }
2685
2710
  } else if (frame.type === 'data_blocked') {
2686
2711
  // The peer is stalled on our connection window. Our MAX_DATA may have
2687
2712
  // been lost (window updates are sent once, unrecovered) — re-send the
@@ -2951,6 +2976,21 @@ function QUICConnection(options) {
2951
2976
  // which rides in the ticket, never came back to it.
2952
2977
  var NEW_SESSION_TICKET = 0x04;
2953
2978
 
2979
+ // Which TLS messages each side may receive, in which space (RFC 9001 §4,
2980
+ // Table 1; RFC 8446 §4). TLS itself does not know the spaces — they are
2981
+ // QUIC's encryption levels — so they are held to here, for every message,
2982
+ // before TLS sees it. Never in any space: KeyUpdate (§6 — QUIC updates its
2983
+ // keys itself), EndOfEarlyData (§8.3), anything a client sends to a client
2984
+ // or a server to a server. Any other: unexpected_message (§4.8, §6).
2985
+ // 1 ClientHello · 2 ServerHello (a HelloRetryRequest included) ·
2986
+ // 4 NewSessionTicket · 8 EncryptedExtensions · 11 Certificate ·
2987
+ // 13 CertificateRequest · 15 CertificateVerify · 20 Finished ·
2988
+ // 25 CompressedCertificate (RFC 8879)
2989
+ var TLS_RECEIVABLE = {
2990
+ server: { initial: [1], handshake: [11, 15, 20, 25], app: [] },
2991
+ client: { initial: [2], handshake: [8, 11, 13, 15, 20, 25], app: [NEW_SESSION_TICKET] }
2992
+ };
2993
+
2954
2994
  // Reassembly, per space. In order: `q`, the bytes received and not yet
2955
2995
  // read as TLS messages (pieces, `qlen` of them in all), up to `end`.
2956
2996
  // Ahead of a gap: `ahead`, pieces sorted by offset that never overlap
@@ -3061,15 +3101,21 @@ function QUICConnection(options) {
3061
3101
  if (messages.length === 0) return;
3062
3102
  if (!tls) { if (DEBUG) console.log('[quic] initializing TLS bridge'); initTLS(); }
3063
3103
 
3064
- for (var mi = 0; mi < messages.length; mi++) {
3065
- var msg = messages[mi];
3066
- var msgType = msg[0];
3067
- if (space === 'app' && (context.isServer || msgType !== NEW_SESSION_TICKET)) {
3104
+ // Every message of the batch checked before TLS sees any: a Finished
3105
+ // followed by a KeyUpdate closes the connection without the Finished
3106
+ // answered first (a ticket, HANDSHAKE_DONE) — not after it.
3107
+ var receivable = TLS_RECEIVABLE[context.isServer ? 'server' : 'client'][space];
3108
+ for (var ci = 0; ci < messages.length; ci++) {
3109
+ if (receivable.indexOf(messages[ci][0]) < 0) {
3068
3110
  // TLS's unexpected_message as a CRYPTO_ERROR (RFC 9001 §4.8); a
3069
3111
  // KeyUpdate is named as one (§6).
3070
- close(0x100 + 10, 'TLS message ' + msgType + ' after the handshake');
3112
+ close(0x100 + 10, 'TLS message ' + messages[ci][0] + ' in the ' + space + ' space');
3071
3113
  return;
3072
3114
  }
3115
+ }
3116
+ for (var mi = 0; mi < messages.length; mi++) {
3117
+ var msg = messages[mi];
3118
+ var msgType = msg[0];
3073
3119
  // Closed by what an earlier message brought: the rest is not read.
3074
3120
  if (context.state === 'closing' || context.state === 'draining' || context.state === 'closed') return;
3075
3121
  if (DEBUG) console.log('[quic] → TLS msg #' + mi + ' type=0x' + msgType.toString(16) + ' len=' + msg.length);
@@ -4202,6 +4248,7 @@ function QUICConnection(options) {
4202
4248
  if (context.srtt === null) {
4203
4249
  context.srtt = adjusted;
4204
4250
  context.rttvar = adjusted / 2;
4251
+ bbrInitPacingRate();
4205
4252
  } else {
4206
4253
  context.rttvar = 0.75 * context.rttvar + 0.25 * Math.abs(context.srtt - adjusted);
4207
4254
  context.srtt = 0.875 * context.srtt + 0.125 * adjusted;
@@ -4318,10 +4365,23 @@ function QUICConnection(options) {
4318
4365
  if (extra > context.bbr_ea_round_max) context.bbr_ea_round_max = extra;
4319
4366
  }
4320
4367
 
4368
+ // Whether a window of w bytes is in use: in flight reached half of it,
4369
+ // this round or the last — slow start's test for being limited by the
4370
+ // window (Linux tcp_is_cwnd_limited; RFC 9002 §7.8).
4371
+ function windowInUse(w) {
4372
+ return 2 * Math.max(context.bbr_peak_inflight, context.bbr_prev_peak_inflight) >= w;
4373
+ }
4374
+
4321
4375
  function growStartupWindow(newlyAckedBytes) {
4322
4376
  if (context.bbr_state !== 'startup' || !(newlyAckedBytes > 0)) return;
4323
4377
  var cap = startupCap(), cur = context.current_limit_bytes_in_flight;
4324
4378
  if (cur >= cap) return;
4379
+ // Only a window in use grows (windowInUse). A sender with less to send
4380
+ // than the window — media below the path's rate — grew it by every byte
4381
+ // acknowledged, without end (1.2 Mbit/s for 3 s: 25 KB → 496 KB), a
4382
+ // window nothing had measured, and the next burst (a keyframe) went out
4383
+ // in it at once, into the bottleneck's queue.
4384
+ if (!windowInUse(cur)) return;
4325
4385
  context.current_limit_bytes_in_flight = Math.min(context.max_limit_bytes_in_flight, cap,
4326
4386
  cur + newlyAckedBytes);
4327
4387
  context.current_limit_packets_in_flight = Math.min(context.max_limit_packets_in_flight,
@@ -4382,6 +4442,24 @@ function QUICConnection(options) {
4382
4442
  Math.max(context.min_limit_packets_in_flight,
4383
4443
  Math.round(context.current_limit_bytes_in_flight / context.current_limit_packet_payload)));
4384
4444
  }
4445
+ // Startup's first pacing rate, from the first RTT sample: the initial
4446
+ // window over the round trip, at Startup's gain (BBR's BBRInitPacingRate,
4447
+ // §5.6.2; quiche the same). It was the ceiling, 14 MB/s — and Startup only
4448
+ // raises the rate, so it stayed there: Startup was not paced at all, the
4449
+ // window left at once at ~112 Mbit/s, into the queue of any slower path.
4450
+ // A media sender rarely leaves Startup, and every keyframe went so (a
4451
+ // 90 KB one, a 20 Mbit/s link with a 64 KB queue: ~20 drops a keyframe);
4452
+ // on a long path the first rounds' bursts were dropped, Startup ended on
4453
+ // those losses with ~14 of 20 Mbit/s measured, and a 4 MB transfer took
4454
+ // 3.7-3.9 s instead of 2.6 (150 ms). The cost: the initial window takes
4455
+ // RTT/2.89 to leave — a first page's body ~27 ms later at 150 ms, within
4456
+ // noise at 40. A new path starts here again (resetPathEstimates: the rate
4457
+ // back to init_, srtt null until its first sample).
4458
+ function bbrInitPacingRate() {
4459
+ if (context.bbr_state !== 'startup') return;
4460
+ setPacingRate(BBR_STARTUP_GAIN * context.init_limit_bytes_in_flight / (Math.max(context.srtt, 1) / 1000));
4461
+ }
4462
+
4385
4463
  function setPacingRate(bps) {
4386
4464
  context.current_limit_bytes_per_sec = Math.max(context.min_limit_bytes_per_sec,
4387
4465
  Math.min(context.max_limit_bytes_per_sec, bps));
@@ -4968,7 +5046,10 @@ function QUICConnection(options) {
4968
5046
  var roundDelivered = context.delivered - context.bbr_round_start_delivered;
4969
5047
  // An app-limited sample is kept only if it RAISES the estimate
4970
5048
  // (BBR delivery-rate sampling: such a sample can show the path is at
4971
- // least that fast, never that it is slower).
5049
+ // least that fast, never that it is slower). With no estimate yet,
5050
+ // any sample raises it — BBR's max_bw starts at 0 (§5.2.1): a
5051
+ // connection that never filled the pipe (media below the path's
5052
+ // rate) had no estimate at all, estimatedSendRate null throughout.
4972
5053
  //
4973
5054
  // Without this, the first round of every connection — which carries
4974
5055
  // only a small HTML response and some ACK-only packets — measured a
@@ -4985,7 +5066,7 @@ function QUICConnection(options) {
4985
5066
  var crRetreat = !!(context.cr && context.cr.phase === 'safe_retreat');
4986
5067
  if (!crRetreat && roundDur >= 1 && roundDelivered > 0) {
4987
5068
  var roundRate = (roundDelivered * 1000) / roundDur;
4988
- if (!roundAppLimited || (context.bbr_btlbw !== null && roundRate > context.bbr_btlbw)) {
5069
+ if (!roundAppLimited || roundRate > (context.bbr_btlbw || 0)) {
4989
5070
  context.bbr_btlbw_samples.push({ round: context.bbr_round_count, rate: roundRate });
4990
5071
  }
4991
5072
  }
@@ -5004,6 +5085,8 @@ function QUICConnection(options) {
5004
5085
  context.bbr_round_start_pn = context.send_pn.app; // next PN must be acked for the next round
5005
5086
  context.bbr_round_start_delivered = context.delivered;
5006
5087
  context.bbr_round_start_time = ackNow;
5088
+ context.bbr_prev_peak_inflight = context.bbr_peak_inflight;
5089
+ context.bbr_peak_inflight = context.app_bytes_in_flight;
5007
5090
 
5008
5091
  var BBR_BTLBW_WINDOW = 10; // rounds
5009
5092
  var cutoff = context.bbr_round_count - BBR_BTLBW_WINDOW;
@@ -5016,7 +5099,11 @@ function QUICConnection(options) {
5016
5099
  }
5017
5100
  }
5018
5101
  context.bbr_btlbw_samples = kept;
5019
- context.bbr_btlbw = maxRate;
5102
+ // No sample left in the window — every round since app-limited and
5103
+ // below the estimate: the estimate stands (BBR's max filter changes
5104
+ // only on a sample it takes). It went back to null after ten such
5105
+ // rounds, and the state machine below with it, mid-connection.
5106
+ if (kept.length > 0) context.bbr_btlbw = maxRate;
5020
5107
 
5021
5108
  // Derived in-flight target. Uses bbr_min_rtt (RTprop, windowed) in seconds.
5022
5109
  if (!crRetreat && context.bbr_btlbw !== null && context.bbr_min_rtt !== null) {
@@ -5097,6 +5184,9 @@ function QUICConnection(options) {
5097
5184
  // ── Write the controls, clamped to [min,max] (the hard safety net). ──
5098
5185
  var clamp = function (v, lo, hi) { return Math.max(lo, Math.min(hi, v)); };
5099
5186
 
5187
+ // The window as the ACKs left it — Startup's floor, below; read
5188
+ // before it is written.
5189
+ var ackGrown = context.current_limit_bytes_in_flight;
5100
5190
  var targetInflight = clamp(cwnd_gain * context.bbr_bdp,
5101
5191
  context.min_limit_bytes_in_flight, context.max_limit_bytes_in_flight);
5102
5192
  context.current_limit_bytes_in_flight = targetInflight;
@@ -5115,8 +5205,18 @@ function QUICConnection(options) {
5115
5205
  if (context.bbr_state === 'startup') {
5116
5206
  // …within startupCap, which may now be lower than what ACKs grew
5117
5207
  // the window to (the BDP of this round is the measured one).
5208
+ // The window before the write above: compared with what was
5209
+ // just written, the max was of a value with itself, and every
5210
+ // round end set the window to max(IW, 2·BDP) — what the ACKs
5211
+ // had grown it by, gone. Kept while it is in use; a window the
5212
+ // last rounds did not fill — a burst (a keyframe) grew it, then
5213
+ // the sender went quiet — goes back to the BDP's: the next burst
5214
+ // left in all of it at Startup's rate, into the bottleneck's
5215
+ // queue (RFC 7661: a window not in use is not validated). Not
5216
+ // while Careful Resume's jump is out: that window is its own.
5217
+ var floor = crJumpOut() || windowInUse(ackGrown) ? ackGrown : 0;
5118
5218
  targetInflight = Math.max(context.init_limit_bytes_in_flight,
5119
- Math.min(Math.max(targetInflight, context.current_limit_bytes_in_flight), startupCap()));
5219
+ Math.min(Math.max(targetInflight, floor), startupCap()));
5120
5220
  context.current_limit_bytes_in_flight = targetInflight;
5121
5221
  context.current_limit_packets_in_flight = clamp(
5122
5222
  Math.round(targetInflight / context.current_limit_packet_payload),
@@ -5376,7 +5476,8 @@ function QUICConnection(options) {
5376
5476
  var sent = { initial: 0, handshake: 0 };
5377
5477
  if (context.state === 'draining' || context.state === 'closed' || context.state === 'closing') return sent;
5378
5478
  var path = context.path;
5379
- var cap = Math.max(1200, context.current_limit_packet_payload);
5479
+ // A whole datagram: the size in use, within what the peer takes.
5480
+ var cap = Math.min(context.current_limit_datagram, context.peer_max_udp_payload);
5380
5481
  var plan = [], planBytes = 0;
5381
5482
  var nextPn = { initial: context.send_pn.initial, handshake: context.send_pn.handshake };
5382
5483
  var planned = { initial: [], handshake: [] };
@@ -5813,10 +5914,27 @@ function QUICConnection(options) {
5813
5914
  return (prev && prev.validated) ? prev : context.path;
5814
5915
  }
5815
5916
 
5917
+ // Server: a NEW_TOKEN for the address the client is at (§8.1.3) — after
5918
+ // the handshake, and again each time the client turns up at a new address
5919
+ // and proves it (§9.3: a migration, a NAT rebinding), since the client's
5920
+ // next connection comes from there. The newest replaces one not yet sent.
5921
+ function issueNewToken(path) {
5922
+ if (!context.isServer || typeof options.newToken !== 'function' || !context.handshake_done_sent) return;
5923
+ var at = path.address + ':' + path.port;
5924
+ if (context.new_token_for === at) return;
5925
+ var nt = options.newToken(path.address, context.version);
5926
+ if (!nt || nt.byteLength === 0) return;
5927
+ context.new_token_for = at;
5928
+ queueControl('nt', { type: 'new_token', token: nt });
5929
+ }
5930
+
5816
5931
  function markValidated(path) {
5817
5932
  if (path.validated) return;
5818
5933
  path.validated = true;
5819
- if (path === context.path) ev.emit('path_validated', path.address, path.port);
5934
+ if (path === context.path) {
5935
+ ev.emit('path_validated', path.address, path.port);
5936
+ issueNewToken(path);
5937
+ }
5820
5938
  if (DEBUG) console.log('[quic] path ' + path.address + ':' + path.port + ' validated (received ' + path.received + 'B, sent ' + path.sent + 'B before)');
5821
5939
  if (path.queue.length > 0) releaseAmpQueue(path);
5822
5940
  }
@@ -5824,8 +5942,8 @@ function QUICConnection(options) {
5824
5942
  // ============================================================
5825
5943
  // Path MTU discovery — DPLPMTUD (RFC 8899; RFC 9000 §14.3)
5826
5944
  //
5827
- // Packets start at what every QUIC path carries (current_limit_packet_
5828
- // payload, 1200 bytes of frames). Most paths carry more — ~1450 on
5945
+ // Packets start at what every QUIC path carries (current_limit_datagram:
5946
+ // datagrams of 1200 bytes, header and all). Most paths carry more — ~1450 on
5829
5947
  // Ethernet — and every byte more in a packet is a packet less: an
5830
5948
  // encryption, a system call, the per-packet bookkeeping, which is where a
5831
5949
  // JavaScript sender spends its time. So once the handshake is confirmed,
@@ -5885,14 +6003,30 @@ function QUICConnection(options) {
5885
6003
  function shortHeaderOverhead() {
5886
6004
  return 1 + destinationCid('1rtt', context.path).byteLength + 4 + 16;
5887
6005
  }
5888
- // The datagram every packet up to the floor's frames fits in.
5889
- function pmtuFloor() { return context.init_limit_packet_payload + shortHeaderOverhead(); }
6006
+ // The header and AEAD tag of the packets app data goes in now: 0-RTT's
6007
+ // long header until 1-RTT keys, a short header after.
6008
+ function appPacketOverhead() {
6009
+ if (!context.app_write && zeroRttSending()) {
6010
+ return 1 + 4 + 1 + destinationCid('0rtt', context.path).byteLength + 1 +
6011
+ (context.my_cids[0] ? context.my_cids[0].byteLength : 0) + 2 + 4 + 16;
6012
+ }
6013
+ return shortHeaderOverhead();
6014
+ }
6015
+ // The frames a packet takes: the datagram in use — never past the peer's
6016
+ // max_udp_payload_size (§18.2) — less its header. Again whenever either, or
6017
+ // the connection ID, may have changed.
6018
+ function syncPacketPayload() {
6019
+ var d = Math.min(context.current_limit_datagram, context.peer_max_udp_payload);
6020
+ context.current_limit_packet_payload = Math.max(64, d - appPacketOverhead());
6021
+ }
6022
+ // The floor: a datagram of 1200 bytes, header included.
6023
+ function pmtuFloor() { return context.init_limit_datagram; }
5890
6024
  function pmtuCeiling() { return Math.min(context.max_limit_packet_payload, context.peer_max_udp_payload); }
5891
6025
 
5892
- // Packets carry the frames a `size` datagram has room for — never fewer
5893
- // than the floor's.
6026
+ // Packets fill `size`-byte datagrams — never smaller than the floor.
5894
6027
  function pmtuApply(size) {
5895
- context.current_limit_packet_payload = Math.max(context.init_limit_packet_payload, size - shortHeaderOverhead());
6028
+ context.current_limit_datagram = Math.max(context.init_limit_datagram, size);
6029
+ syncPacketPayload();
5896
6030
  if (DEBUG) console.log('[quic] PMTU ' + size + ' — packets of ' + context.current_limit_packet_payload + ' bytes of frames');
5897
6031
  }
5898
6032
 
@@ -5981,7 +6115,7 @@ function QUICConnection(options) {
5981
6115
  m.confirming = false;
5982
6116
  m.lo = 0; m.fail = p.size; m.size = 0; m.tries = 0;
5983
6117
  m.bigLost = 0;
5984
- context.current_limit_packet_payload = context.init_limit_packet_payload;
6118
+ pmtuApply(context.init_limit_datagram);
5985
6119
  m.nextAt = Date.now() + PMTU_RAISE_MS;
5986
6120
  return;
5987
6121
  }
@@ -6113,8 +6247,9 @@ function QUICConnection(options) {
6113
6247
  if (path !== context.path) return;
6114
6248
 
6115
6249
  // A path already validated, failing only the expanded re-check (§8.2.3).
6116
- // Every packet quico sends fits in 1200 bytes, so a path that has carried
6117
- // them is carrying what this connection needs; it goes on.
6250
+ // On a new path packets are back at the floor — datagrams of 1200 bytes
6251
+ // at most (pmtuApply) — so a path that has carried them is carrying what
6252
+ // this connection needs; it goes on.
6118
6253
  if (path.validated) return;
6119
6254
 
6120
6255
  // §9.3.2: "an endpoint MUST revert to using the last validated peer
@@ -6137,6 +6272,7 @@ function QUICConnection(options) {
6137
6272
  if (context.candidate_path === path) context.candidate_path = null;
6138
6273
  if (DEBUG) console.log('[quic] now sending to ' + path.address + ':' + path.port + (path.validated ? '' : ' (unvalidated)'));
6139
6274
  ev.emit('path', path.address, path.port);
6275
+ if (path.validated) issueNewToken(path);
6140
6276
  }
6141
6277
 
6142
6278
  // ── Connection IDs: issuing ours, keeping the peer's (RFC 9000 §5.1) ──
@@ -6341,7 +6477,7 @@ function QUICConnection(options) {
6341
6477
  context.current_limit_bytes_in_flight = context.init_limit_bytes_in_flight;
6342
6478
  context.current_limit_packets_per_sec = context.init_limit_packets_per_sec;
6343
6479
  context.current_limit_bytes_per_sec = context.init_limit_bytes_per_sec;
6344
- context.current_limit_packet_payload = context.init_limit_packet_payload;
6480
+ pmtuApply(context.init_limit_datagram);
6345
6481
  context.srtt = null;
6346
6482
  context.rttvar = null;
6347
6483
  context.min_rtt = null;
@@ -6360,6 +6496,7 @@ function QUICConnection(options) {
6360
6496
  context.bbr_bdp = null;
6361
6497
  context.bbr_state = 'startup';
6362
6498
  context.bbr_round_app_limited = false;
6499
+ context.bbr_peak_inflight = 0; context.bbr_prev_peak_inflight = 0;
6363
6500
  context.bbr_full_bw = 0;
6364
6501
  context.bbr_full_bw_count = 0;
6365
6502
  context.bbr_ea_start = 0; context.bbr_ea_delivered = 0; context.bbr_ea_round_max = 0;
@@ -6578,6 +6715,7 @@ function QUICConnection(options) {
6578
6715
  context.pkt_in_flight.set(pn, { bytes: encrypted.length, t: Date.now() });
6579
6716
  if (context.tlp_at === 0 || context.tlp_timer === null) tlpRearm(); else context.tlp_at = Date.now() + tlpDelay();
6580
6717
  context.app_bytes_in_flight += encrypted.length;
6718
+ if (context.app_bytes_in_flight > context.bbr_peak_inflight) context.bbr_peak_inflight = context.app_bytes_in_flight;
6581
6719
  }
6582
6720
  // In flight = carrying STREAM data, which expireInFlight finds and
6583
6721
  // retires through its stream. 1-RTT CRYPTO is the control queue's to
@@ -7541,6 +7679,7 @@ function QUICConnection(options) {
7541
7679
 
7542
7680
  function plan_quic_burst() {
7543
7681
  context.send_scheduled = false; // this pass takes what writes left waiting
7682
+ syncPacketPayload(); // 0-RTT → 1-RTT, a new connection ID
7544
7683
  if (!zeroRttSending()) {
7545
7684
  if (!context.app_write) return;
7546
7685
  if (context.state !== 'connected') return;