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.
- package/README.md +2 -2
- package/package.json +4 -3
- package/src/client.js +1 -0
- package/src/connect_udp.js +5 -2
- package/src/connect_udp_client.js +1 -0
- package/src/crypto.js +6 -0
- package/src/h3.js +86 -10
- package/src/h3_client.js +1 -0
- package/src/h3_server.js +3 -0
- package/src/qpack.js +11 -2
- package/src/quic_connection.js +179 -40
- package/src/quic_server.js +121 -61
- package/src/quic_socket.js +5 -5
- package/src/server.js +24 -15
- package/src/tls_bridge.js +58 -38
- package/src/transport.js +6 -2
- package/src/utils.js +36 -0
- package/src/webtransport.js +8 -3
package/src/quic_connection.js
CHANGED
|
@@ -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
|
|
707
|
-
// DPLPMTUD must not probe past; init_
|
|
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
|
-
|
|
711
|
-
|
|
712
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
//
|
|
2085
|
-
//
|
|
2086
|
-
//
|
|
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
|
|
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)
|
|
2682
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
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 ' +
|
|
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 ||
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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)
|
|
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 (
|
|
5828
|
-
//
|
|
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
|
|
5889
|
-
|
|
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
|
|
5893
|
-
// than the floor's.
|
|
6026
|
+
// Packets fill `size`-byte datagrams — never smaller than the floor.
|
|
5894
6027
|
function pmtuApply(size) {
|
|
5895
|
-
context.
|
|
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.
|
|
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
|
-
//
|
|
6117
|
-
//
|
|
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.
|
|
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;
|