quico 0.7.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 +247 -33
- package/index.d.ts +1034 -77
- package/index.js +7 -3
- package/package.json +9 -5
- package/src/client.js +1 -0
- package/src/connect_udp.js +5 -2
- package/src/connect_udp_client.js +10 -0
- package/src/crypto.js +6 -0
- package/src/h1_server.js +51 -9
- package/src/h2_server.js +60 -11
- package/src/h3.js +115 -70
- package/src/h3_client.js +25 -5
- package/src/h3_server.js +104 -6
- package/src/hpack_primitives.js +64 -25
- package/src/http_common.js +7 -0
- package/src/qpack.js +11 -2
- package/src/quic_connection.js +379 -63
- package/src/quic_server.js +213 -72
- package/src/quic_socket.js +5 -5
- package/src/server.js +277 -24
- package/src/streams.js +183 -10
- package/src/tcp_listener.js +11 -0
- package/src/tls_bridge.js +62 -38
- package/src/transport.js +6 -2
- package/src/utils.js +36 -0
- package/src/webtransport.js +432 -57
- package/src/ws.js +120 -0
- package/src/ws_client.js +68 -88
- package/src/ws_server.js +35 -53
- package/src/ws_stream.js +250 -0
- package/src/wt_stream.js +533 -76
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
|
|
@@ -613,6 +614,13 @@ function QUICConnection(options) {
|
|
|
613
614
|
stats_bytes_sent: 0,
|
|
614
615
|
stats_packets_received: 0,
|
|
615
616
|
stats_bytes_received: 0,
|
|
617
|
+
// The application's bytes in them, for WebTransport's getStats() (W3C
|
|
618
|
+
// WebTransportConnectionStats): DATAGRAM payloads sent (new STREAM bytes
|
|
619
|
+
// are max_data_sent), the payload of the packets acknowledged, and the
|
|
620
|
+
// size of the packets declared lost.
|
|
621
|
+
stats_datagram_bytes_sent: 0,
|
|
622
|
+
stats_payload_acked: 0,
|
|
623
|
+
stats_bytes_lost: 0,
|
|
616
624
|
// Streams that hit the peer's flow-control limit since the last check —
|
|
617
625
|
// for STREAM_DATA_BLOCKED (maybeSendBlockedFrames), instead of a walk of
|
|
618
626
|
// every stream on every send pass.
|
|
@@ -696,13 +704,18 @@ function QUICConnection(options) {
|
|
|
696
704
|
|
|
697
705
|
max_packets_per_burst: 20, // fixed cap — not CC-controlled, no current_/init_
|
|
698
706
|
|
|
699
|
-
// packet
|
|
700
|
-
// 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).
|
|
701
713
|
max_limit_packet_payload: connOpts.maxUdpPayloadSize, // ceiling, maxUdpPayloadSize: 1452 (IPv6-safe Ethernet: 1500 − 48 hdr) —
|
|
702
714
|
// a whole UDP datagram: what DPLPMTUD probes up to
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
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)
|
|
706
719
|
pmtu: null, // DPLPMTUD's state (pmtuReset)
|
|
707
720
|
peer_max_udp_payload: 65527, // its max_udp_payload_size (RFC 9000 §18.2)
|
|
708
721
|
|
|
@@ -731,7 +744,7 @@ function QUICConnection(options) {
|
|
|
731
744
|
current_limit_packets_per_sec: 12000, // = init_; BBR rewrites at round-end
|
|
732
745
|
max_limit_bytes_per_sec: 14000000, // ceiling ~14 MB/s — coherent ↑
|
|
733
746
|
min_limit_bytes_per_sec: 12000, // floor ~96 kbps — avoid 0/deadlock
|
|
734
|
-
init_limit_bytes_per_sec: 14000000,
|
|
747
|
+
init_limit_bytes_per_sec: 14000000, // until the first RTT sample (bbrInitPacingRate)
|
|
735
748
|
current_limit_bytes_per_sec: 14000000, // = init_; BBR rewrites at round-end
|
|
736
749
|
|
|
737
750
|
burst_timer: null,
|
|
@@ -888,6 +901,7 @@ function QUICConnection(options) {
|
|
|
888
901
|
// pn → { t: expiry wall-time, spans: [[sid, ranges, finLost], ...] }
|
|
889
902
|
// (ranges: the packet's flat range list of that stream)
|
|
890
903
|
expired_unacked: {},
|
|
904
|
+
limbo_n: 0, // entries in expired_unacked (kept, not counted)
|
|
891
905
|
reorder_in_count: 0, // app packets that arrived below the highest PN seen (network reordering, peer→us)
|
|
892
906
|
|
|
893
907
|
// --- BBR-lite measurement (Phase 4b, step 1+2 — measure only, no control). ---
|
|
@@ -917,6 +931,10 @@ function QUICConnection(options) {
|
|
|
917
931
|
// a round says how much the APPLICATION produced, not what the path can
|
|
918
932
|
// carry — see the sample filter at round end.
|
|
919
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,
|
|
920
938
|
bbr_full_bw: 0, // highest BtlBw seen — Startup plateau detector
|
|
921
939
|
bbr_full_bw_count: 0, // consecutive rounds without ≥25% BtlBw growth
|
|
922
940
|
// ACK aggregation (BBR's extra_acked): how far ACKs ran ahead of what
|
|
@@ -1122,6 +1140,18 @@ function QUICConnection(options) {
|
|
|
1122
1140
|
if (changed.state && (context.state === 'draining' || context.state === 'closing' || context.state === 'closed')) handshakeEnded(false);
|
|
1123
1141
|
|
|
1124
1142
|
if (changed.state && (context.state === 'draining' || context.state === 'closing')) {
|
|
1143
|
+
// 'closing': the connection is over for the application now — the
|
|
1144
|
+
// peer's CONNECTION_CLOSE came (draining), or ours went (closing).
|
|
1145
|
+
// 'close' follows when the closing or draining period ends (RFC 9000
|
|
1146
|
+
// §10.2: up to three PTOs, here at most 3 s), which is packet
|
|
1147
|
+
// housekeeping, not news: a session above that waited for 'close'
|
|
1148
|
+
// learned the peer was gone seconds late. With the peer's code and
|
|
1149
|
+
// reason (context.peer_close) when it was the peer; null when we
|
|
1150
|
+
// closed. Once.
|
|
1151
|
+
if (!context.closing_emitted) {
|
|
1152
|
+
context.closing_emitted = true;
|
|
1153
|
+
ev.emit('closing', context.state === 'draining' ? (context.peer_close || null) : null);
|
|
1154
|
+
}
|
|
1125
1155
|
failPermitWaiters();
|
|
1126
1156
|
clearIdleTimer();
|
|
1127
1157
|
// A draining/closing endpoint must stop retransmitting CRYPTO — the
|
|
@@ -1441,7 +1471,7 @@ function QUICConnection(options) {
|
|
|
1441
1471
|
// the search may go there.
|
|
1442
1472
|
function setMaxUdpPayload(v) {
|
|
1443
1473
|
context.max_limit_packet_payload = v;
|
|
1444
|
-
var now = context.
|
|
1474
|
+
var now = context.current_limit_datagram;
|
|
1445
1475
|
var m = context.pmtu;
|
|
1446
1476
|
if (now > v) pmtuApply(v);
|
|
1447
1477
|
if (m) {
|
|
@@ -1660,6 +1690,7 @@ function QUICConnection(options) {
|
|
|
1660
1690
|
context.sending_app_pn_in_flight.clear();
|
|
1661
1691
|
context.pn_sids.clear();
|
|
1662
1692
|
context.expired_unacked = {};
|
|
1693
|
+
context.limbo_n = 0;
|
|
1663
1694
|
context.no_resend_in_flight = {};
|
|
1664
1695
|
context.control_in_flight = {};
|
|
1665
1696
|
for (var sid in context.send_streams) {
|
|
@@ -1777,6 +1808,8 @@ function QUICConnection(options) {
|
|
|
1777
1808
|
// as a CRYPTO_ERROR (0x100 + alert code, RFC 9001 §4.8) — the peer learns
|
|
1778
1809
|
// why, and this side reports the close now rather than at a timeout.
|
|
1779
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); });
|
|
1780
1813
|
// The reason for that alert. During the handshake it is the connection's
|
|
1781
1814
|
// failure, reported like the handshake timeout: 'error', then 'close'.
|
|
1782
1815
|
// The alert has already closed the connection, so nothing else to do.
|
|
@@ -1902,6 +1935,7 @@ function QUICConnection(options) {
|
|
|
1902
1935
|
// a bound on what DPLPMTUD probes. "Values below 1200 are invalid."
|
|
1903
1936
|
if (typeof p.max_udp_payload_size === 'number') {
|
|
1904
1937
|
context.peer_max_udp_payload = p.max_udp_payload_size;
|
|
1938
|
+
syncPacketPayload();
|
|
1905
1939
|
}
|
|
1906
1940
|
// Stream-count limits the peer grants us (localStreamPermitted holds
|
|
1907
1941
|
// back a stream past them). Seeded directly, not through set_context:
|
|
@@ -2042,10 +2076,7 @@ function QUICConnection(options) {
|
|
|
2042
2076
|
queueControl('hd'); // retransmitted until acknowledged (§13.3)
|
|
2043
2077
|
// A token for the client's next connection (NEW_TOKEN, §8.1.3):
|
|
2044
2078
|
// it will need no proof of address then.
|
|
2045
|
-
|
|
2046
|
-
var nt = options.newToken(peerPath().address, context.version);
|
|
2047
|
-
if (nt && nt.byteLength > 0) queueControl('nt', { type: 'new_token', token: nt });
|
|
2048
|
-
}
|
|
2079
|
+
issueNewToken(peerPath());
|
|
2049
2080
|
}
|
|
2050
2081
|
discardSpace('handshake');
|
|
2051
2082
|
crStart();
|
|
@@ -2060,9 +2091,10 @@ function QUICConnection(options) {
|
|
|
2060
2091
|
}
|
|
2061
2092
|
});
|
|
2062
2093
|
|
|
2063
|
-
//
|
|
2064
|
-
//
|
|
2065
|
-
//
|
|
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).
|
|
2066
2098
|
}
|
|
2067
2099
|
|
|
2068
2100
|
|
|
@@ -2344,13 +2376,22 @@ function QUICConnection(options) {
|
|
|
2344
2376
|
);
|
|
2345
2377
|
}
|
|
2346
2378
|
|
|
2347
|
-
if (!decrypted || !decrypted.plaintext
|
|
2379
|
+
if (!decrypted || !decrypted.plaintext) {
|
|
2348
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]);
|
|
2349
2381
|
return;
|
|
2350
2382
|
}
|
|
2351
2383
|
|
|
2352
2384
|
if (DEBUG) console.log('[quic] decrypted ' + space + ' pn=' + decrypted.packet_number + ' len=' + decrypted.plaintext.byteLength);
|
|
2353
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
|
+
|
|
2354
2395
|
// RFC 9000 §8.1: "A server might wish to validate the client address
|
|
2355
2396
|
// before starting the cryptographic handshake. [...] the server MAY
|
|
2356
2397
|
// consider the peer address validated if the client uses a connection ID
|
|
@@ -2657,10 +2698,15 @@ function QUICConnection(options) {
|
|
|
2657
2698
|
} else if (frame.type === 'streams_blocked_bidi' || frame.type === 'streams_blocked_uni') {
|
|
2658
2699
|
// The peer has used every stream we granted. Ack-eliciting like any
|
|
2659
2700
|
// frame but ACK/PADDING/CONNECTION_CLOSE (it used to fall through
|
|
2660
|
-
// unacknowledged)
|
|
2661
|
-
//
|
|
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.
|
|
2662
2705
|
ackEliciting = true;
|
|
2663
|
-
|
|
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
|
+
}
|
|
2664
2710
|
} else if (frame.type === 'data_blocked') {
|
|
2665
2711
|
// The peer is stalled on our connection window. Our MAX_DATA may have
|
|
2666
2712
|
// been lost (window updates are sent once, unrecovered) — re-send the
|
|
@@ -2930,6 +2976,21 @@ function QUICConnection(options) {
|
|
|
2930
2976
|
// which rides in the ticket, never came back to it.
|
|
2931
2977
|
var NEW_SESSION_TICKET = 0x04;
|
|
2932
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
|
+
|
|
2933
2994
|
// Reassembly, per space. In order: `q`, the bytes received and not yet
|
|
2934
2995
|
// read as TLS messages (pieces, `qlen` of them in all), up to `end`.
|
|
2935
2996
|
// Ahead of a gap: `ahead`, pieces sorted by offset that never overlap
|
|
@@ -3040,15 +3101,21 @@ function QUICConnection(options) {
|
|
|
3040
3101
|
if (messages.length === 0) return;
|
|
3041
3102
|
if (!tls) { if (DEBUG) console.log('[quic] initializing TLS bridge'); initTLS(); }
|
|
3042
3103
|
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
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) {
|
|
3047
3110
|
// TLS's unexpected_message as a CRYPTO_ERROR (RFC 9001 §4.8); a
|
|
3048
3111
|
// KeyUpdate is named as one (§6).
|
|
3049
|
-
close(0x100 + 10, 'TLS message ' +
|
|
3112
|
+
close(0x100 + 10, 'TLS message ' + messages[ci][0] + ' in the ' + space + ' space');
|
|
3050
3113
|
return;
|
|
3051
3114
|
}
|
|
3115
|
+
}
|
|
3116
|
+
for (var mi = 0; mi < messages.length; mi++) {
|
|
3117
|
+
var msg = messages[mi];
|
|
3118
|
+
var msgType = msg[0];
|
|
3052
3119
|
// Closed by what an earlier message brought: the rest is not read.
|
|
3053
3120
|
if (context.state === 'closing' || context.state === 'draining' || context.state === 'closed') return;
|
|
3054
3121
|
if (DEBUG) console.log('[quic] → TLS msg #' + mi + ' type=0x' + msgType.toString(16) + ' len=' + msg.length);
|
|
@@ -4181,6 +4248,7 @@ function QUICConnection(options) {
|
|
|
4181
4248
|
if (context.srtt === null) {
|
|
4182
4249
|
context.srtt = adjusted;
|
|
4183
4250
|
context.rttvar = adjusted / 2;
|
|
4251
|
+
bbrInitPacingRate();
|
|
4184
4252
|
} else {
|
|
4185
4253
|
context.rttvar = 0.75 * context.rttvar + 0.25 * Math.abs(context.srtt - adjusted);
|
|
4186
4254
|
context.srtt = 0.875 * context.srtt + 0.125 * adjusted;
|
|
@@ -4297,10 +4365,23 @@ function QUICConnection(options) {
|
|
|
4297
4365
|
if (extra > context.bbr_ea_round_max) context.bbr_ea_round_max = extra;
|
|
4298
4366
|
}
|
|
4299
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
|
+
|
|
4300
4375
|
function growStartupWindow(newlyAckedBytes) {
|
|
4301
4376
|
if (context.bbr_state !== 'startup' || !(newlyAckedBytes > 0)) return;
|
|
4302
4377
|
var cap = startupCap(), cur = context.current_limit_bytes_in_flight;
|
|
4303
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;
|
|
4304
4385
|
context.current_limit_bytes_in_flight = Math.min(context.max_limit_bytes_in_flight, cap,
|
|
4305
4386
|
cur + newlyAckedBytes);
|
|
4306
4387
|
context.current_limit_packets_in_flight = Math.min(context.max_limit_packets_in_flight,
|
|
@@ -4361,6 +4442,24 @@ function QUICConnection(options) {
|
|
|
4361
4442
|
Math.max(context.min_limit_packets_in_flight,
|
|
4362
4443
|
Math.round(context.current_limit_bytes_in_flight / context.current_limit_packet_payload)));
|
|
4363
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
|
+
|
|
4364
4463
|
function setPacingRate(bps) {
|
|
4365
4464
|
context.current_limit_bytes_per_sec = Math.max(context.min_limit_bytes_per_sec,
|
|
4366
4465
|
Math.min(context.max_limit_bytes_per_sec, bps));
|
|
@@ -4522,6 +4621,16 @@ function QUICConnection(options) {
|
|
|
4522
4621
|
}
|
|
4523
4622
|
}
|
|
4524
4623
|
|
|
4624
|
+
// A 1-RTT packet carrying stream data or datagrams declared lost: counted
|
|
4625
|
+
// (getStats' packetsLost, and bytesLost by its size on the wire), and
|
|
4626
|
+
// Careful Resume told.
|
|
4627
|
+
function countLostPacket(pnum) {
|
|
4628
|
+
context.lost_count++;
|
|
4629
|
+
var h = context.sending_app_pn_history[pnum - (context.send_pn.app - context.sending_app_pn_history.length)];
|
|
4630
|
+
if (h) context.stats_bytes_lost += h[1];
|
|
4631
|
+
crOnLoss();
|
|
4632
|
+
}
|
|
4633
|
+
|
|
4525
4634
|
// A packet given up on — expired, or its control frames deemed lost: out
|
|
4526
4635
|
// of the bytes in flight. Returns its size (0: not in flight any more).
|
|
4527
4636
|
function forgetPacket(pn) {
|
|
@@ -4532,6 +4641,58 @@ function QUICConnection(options) {
|
|
|
4532
4641
|
return pk.bytes;
|
|
4533
4642
|
}
|
|
4534
4643
|
|
|
4644
|
+
// How many of the values in `r` (flat [from, to) pairs) `a` already
|
|
4645
|
+
// covers — flat_ranges.length(flat_ranges.intersect(a, r)), found by binary
|
|
4646
|
+
// search in `a` for each pair of `r` instead of a walk of both. Both are
|
|
4647
|
+
// the library's sorted, merged lists; `r` not so (never seen) is answered
|
|
4648
|
+
// the original way.
|
|
4649
|
+
function coveredLength(a, r) {
|
|
4650
|
+
var na = a.length, total = 0, prevTo = -Infinity;
|
|
4651
|
+
if (na === 0 || r.length === 0) return 0;
|
|
4652
|
+
for (var i = 0; i < r.length; i += 2) {
|
|
4653
|
+
var f = r[i], t = r[i + 1];
|
|
4654
|
+
if (f < prevTo) return flat_ranges.length(flat_ranges.intersect(a, r));
|
|
4655
|
+
prevTo = t;
|
|
4656
|
+
if (f >= t) continue;
|
|
4657
|
+
// first pair of a whose `to` > f
|
|
4658
|
+
var lo = 0, hi = na >> 1;
|
|
4659
|
+
while (lo < hi) {
|
|
4660
|
+
var mid = (lo + hi) >> 1;
|
|
4661
|
+
if (a[(mid << 1) + 1] <= f) lo = mid + 1; else hi = mid;
|
|
4662
|
+
}
|
|
4663
|
+
for (var k = lo << 1; k < na && a[k] < t; k += 2) {
|
|
4664
|
+
var from = a[k] > f ? a[k] : f;
|
|
4665
|
+
var to = a[k + 1] < t ? a[k + 1] : t;
|
|
4666
|
+
if (to > from) total += to - from;
|
|
4667
|
+
}
|
|
4668
|
+
}
|
|
4669
|
+
return total;
|
|
4670
|
+
}
|
|
4671
|
+
|
|
4672
|
+
// Would flat_ranges.remove(list, r) change `list`? Only if a pair of `r`
|
|
4673
|
+
// overlaps one of it (half-open; flat_ranges.overlaps, a binary search).
|
|
4674
|
+
function overlapsAny(list, r) {
|
|
4675
|
+
if (list.length === 0) return false;
|
|
4676
|
+
for (var i = 0; i < r.length; i += 2) {
|
|
4677
|
+
if (flat_ranges.overlaps(list, r[i], r[i + 1])) return true;
|
|
4678
|
+
}
|
|
4679
|
+
return false;
|
|
4680
|
+
}
|
|
4681
|
+
|
|
4682
|
+
// How many packet numbers a flat [from, to, from, to, ...] list of
|
|
4683
|
+
// inclusive ranges covers.
|
|
4684
|
+
function rangesSpan(r) {
|
|
4685
|
+
var n = 0;
|
|
4686
|
+
for (var i = 0; i < r.length; i += 2) n += r[i + 1] - r[i] + 1;
|
|
4687
|
+
return n;
|
|
4688
|
+
}
|
|
4689
|
+
|
|
4690
|
+
// Entries in the limbo table (kept as it changes — counting them meant
|
|
4691
|
+
// walking it).
|
|
4692
|
+
function limboCount() {
|
|
4693
|
+
return context.limbo_n > 0 ? context.limbo_n : 0;
|
|
4694
|
+
}
|
|
4695
|
+
|
|
4535
4696
|
function processAckFrame(space, frame) {
|
|
4536
4697
|
// RFC 9000 §13.1: "An endpoint SHOULD treat receipt of an acknowledgment
|
|
4537
4698
|
// for a packet it did not send as a connection error of type
|
|
@@ -4665,9 +4826,28 @@ function QUICConnection(options) {
|
|
|
4665
4826
|
// Mark in-flight as acked. Collect the acked PNs first, then mutate —
|
|
4666
4827
|
// deleting from a Set while iterating it with for-of is fragile.
|
|
4667
4828
|
var ackedPns = [];
|
|
4668
|
-
|
|
4829
|
+
// Walked from whichever side is smaller: the packets in flight, or the
|
|
4830
|
+
// packet numbers this ACK covers. Every in-flight packet was tested
|
|
4831
|
+
// against every range on every ACK — at a high RTT the window holds
|
|
4832
|
+
// thousands, and a page's worth of ACKs cost that many times over.
|
|
4833
|
+
// Ascending either way (the set is in send order), as before.
|
|
4834
|
+
if (rangesSpan(ackedRanges) < context.sending_app_pn_in_flight.size) {
|
|
4669
4835
|
for (var ri = 0; ri < ackedRanges.length; ri += 2) {
|
|
4670
|
-
|
|
4836
|
+
for (var rpn0 = ackedRanges[ri]; rpn0 <= ackedRanges[ri + 1]; rpn0++) {
|
|
4837
|
+
if (context.sending_app_pn_in_flight.has(rpn0)) ackedPns.push(rpn0);
|
|
4838
|
+
}
|
|
4839
|
+
}
|
|
4840
|
+
} else {
|
|
4841
|
+
// Both in ascending order (ack_frame_to_ranges sorts and merges the
|
|
4842
|
+
// ranges; the set is in packet-number order): one pointer into the
|
|
4843
|
+
// ranges, and the walk ends past the largest acknowledged — what is
|
|
4844
|
+
// in flight above it cannot be in this ACK. Each packet was tested
|
|
4845
|
+
// against every range, and the walk went on to the newest packet.
|
|
4846
|
+
var rp1 = 0, rTop1 = ackedRanges[ackedRanges.length - 1];
|
|
4847
|
+
for (var pn of context.sending_app_pn_in_flight) {
|
|
4848
|
+
if (pn > rTop1) break;
|
|
4849
|
+
while (ackedRanges[rp1 + 1] < pn) rp1 += 2;
|
|
4850
|
+
if (pn >= ackedRanges[rp1]) ackedPns.push(pn);
|
|
4671
4851
|
}
|
|
4672
4852
|
}
|
|
4673
4853
|
// A packet's ranges of a stream, acknowledged. Returns the bytes newly
|
|
@@ -4675,12 +4855,19 @@ function QUICConnection(options) {
|
|
|
4675
4855
|
// transmission and a retransmission of the same range ending up ACKed
|
|
4676
4856
|
// (routine once late ACKs for expired packets are honoured) count once.
|
|
4677
4857
|
function creditRanges(st, ranges) {
|
|
4678
|
-
|
|
4858
|
+
// The bytes of `ranges` not acknowledged before — what
|
|
4859
|
+
// length(intersect(acked, ranges)) gave, without walking all of
|
|
4860
|
+
// acked_ranges and building the intersection for every packet. After
|
|
4861
|
+
// a loss acked_ranges holds hundreds of pieces until the hole is
|
|
4862
|
+
// filled, and this ran once per acknowledged packet (coveredLength).
|
|
4863
|
+
var fresh = flat_ranges.length(ranges) - coveredLength(st.acked_ranges, ranges);
|
|
4679
4864
|
flat_ranges.add(st.acked_ranges, ranges);
|
|
4680
4865
|
// Out of missing, whatever it was: an ACK is proof — a late one too,
|
|
4681
4866
|
// for bytes already given up on and not yet resent (add_have would
|
|
4682
4867
|
// leave those missing: it defers to what not_have says).
|
|
4683
|
-
|
|
4868
|
+
// remove() walks and rebuilds the whole list; it is called only when
|
|
4869
|
+
// it would change something — when the ranges touch a missing one.
|
|
4870
|
+
if (overlapsAny(st.missing_ranges, ranges)) flat_ranges.remove(st.missing_ranges, ranges);
|
|
4684
4871
|
if (st.acked_ranges[0] === 0) sendQueueRelease(st, st.acked_ranges[1]);
|
|
4685
4872
|
return fresh > 0 ? fresh : 0;
|
|
4686
4873
|
}
|
|
@@ -4714,15 +4901,17 @@ function QUICConnection(options) {
|
|
|
4714
4901
|
var largestAckedPn = -1;
|
|
4715
4902
|
for (var lri = 0; lri < ackedRanges.length; lri += 2) if (ackedRanges[lri + 1] > largestAckedPn) largestAckedPn = ackedRanges[lri + 1];
|
|
4716
4903
|
var ackedPkts = [];
|
|
4904
|
+
var rp2 = 0; // pointer into the ranges, as for ackedPns above
|
|
4717
4905
|
for (var pke of context.pkt_in_flight) {
|
|
4718
4906
|
var ppn = pke[0];
|
|
4719
4907
|
if (ppn > largestAckedPn) break;
|
|
4720
|
-
|
|
4721
|
-
|
|
4722
|
-
}
|
|
4908
|
+
while (ackedRanges[rp2 + 1] < ppn) rp2 += 2;
|
|
4909
|
+
if (ppn >= ackedRanges[rp2]) ackedPkts.push(ppn);
|
|
4723
4910
|
}
|
|
4724
4911
|
var ackedSizes = [];
|
|
4725
4912
|
for (var api = 0; api < ackedPkts.length; api++) {
|
|
4913
|
+
var apk = context.pkt_in_flight.get(ackedPkts[api]);
|
|
4914
|
+
if (apk && apk.payload) context.stats_payload_acked += apk.payload;
|
|
4726
4915
|
var asz = forgetPacket(ackedPkts[api]);
|
|
4727
4916
|
ackedSizes.push(asz);
|
|
4728
4917
|
ackedPacketBytes += asz;
|
|
@@ -4756,14 +4945,33 @@ function QUICConnection(options) {
|
|
|
4756
4945
|
// DID arrive — the timer was simply wrong — so: credit the goodput
|
|
4757
4946
|
// (repairing the BtlBw signal), mark the spans acked (cancelling any
|
|
4758
4947
|
// retransmit that hasn't left yet), and take back the loss we counted.
|
|
4759
|
-
|
|
4760
|
-
|
|
4761
|
-
|
|
4762
|
-
|
|
4763
|
-
|
|
4948
|
+
// The limbo table holds up to ~10 s of expired packets: walking all of
|
|
4949
|
+
// it for each ACK (for-in, a string key made per entry) was the larger
|
|
4950
|
+
// part of an ACK's cost on a lossy, distant path. The covered numbers
|
|
4951
|
+
// are looked up instead when they are fewer — ascending, as for-in
|
|
4952
|
+
// visits integer keys.
|
|
4953
|
+
var lateHits = [];
|
|
4954
|
+
if (rangesSpan(ackedRanges) < limboCount()) {
|
|
4955
|
+
for (var lr = 0; lr < ackedRanges.length; lr += 2) {
|
|
4956
|
+
for (var lq = ackedRanges[lr]; lq <= ackedRanges[lr + 1]; lq++) {
|
|
4957
|
+
if (lq in context.expired_unacked) lateHits.push(String(lq));
|
|
4958
|
+
}
|
|
4764
4959
|
}
|
|
4765
|
-
|
|
4960
|
+
} else {
|
|
4961
|
+
// Ascending (integer keys), so: one pointer into the ranges, and
|
|
4962
|
+
// done past the largest acknowledged.
|
|
4963
|
+
var rp3 = 0, rTop3 = ackedRanges[ackedRanges.length - 1];
|
|
4964
|
+
for (var lpn0 in context.expired_unacked) {
|
|
4965
|
+
var lnum = Number(lpn0);
|
|
4966
|
+
if (lnum > rTop3) break;
|
|
4967
|
+
while (ackedRanges[rp3 + 1] < lnum) rp3 += 2;
|
|
4968
|
+
if (lnum >= ackedRanges[rp3]) lateHits.push(lpn0);
|
|
4969
|
+
}
|
|
4970
|
+
}
|
|
4971
|
+
for (var lhi = 0; lhi < lateHits.length; lhi++) {
|
|
4972
|
+
var lpn = lateHits[lhi];
|
|
4766
4973
|
var rec = context.expired_unacked[lpn];
|
|
4974
|
+
if (!rec) continue;
|
|
4767
4975
|
for (var si2 = 0; si2 < rec.spans.length; si2++) {
|
|
4768
4976
|
var sidL = rec.spans[si2][0];
|
|
4769
4977
|
var stL = context.send_streams[sidL];
|
|
@@ -4776,6 +4984,7 @@ function QUICConnection(options) {
|
|
|
4776
4984
|
ackedPacketBytes += rec.bytes || 0;
|
|
4777
4985
|
if (rec.early) context.stats_spurious_early++;
|
|
4778
4986
|
delete context.expired_unacked[lpn];
|
|
4987
|
+
context.limbo_n--;
|
|
4779
4988
|
if (context.lost_count > 0) context.lost_count--; // it wasn't a loss after all
|
|
4780
4989
|
}
|
|
4781
4990
|
|
|
@@ -4837,7 +5046,10 @@ function QUICConnection(options) {
|
|
|
4837
5046
|
var roundDelivered = context.delivered - context.bbr_round_start_delivered;
|
|
4838
5047
|
// An app-limited sample is kept only if it RAISES the estimate
|
|
4839
5048
|
// (BBR delivery-rate sampling: such a sample can show the path is at
|
|
4840
|
-
// 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.
|
|
4841
5053
|
//
|
|
4842
5054
|
// Without this, the first round of every connection — which carries
|
|
4843
5055
|
// only a small HTML response and some ACK-only packets — measured a
|
|
@@ -4854,7 +5066,7 @@ function QUICConnection(options) {
|
|
|
4854
5066
|
var crRetreat = !!(context.cr && context.cr.phase === 'safe_retreat');
|
|
4855
5067
|
if (!crRetreat && roundDur >= 1 && roundDelivered > 0) {
|
|
4856
5068
|
var roundRate = (roundDelivered * 1000) / roundDur;
|
|
4857
|
-
if (!roundAppLimited ||
|
|
5069
|
+
if (!roundAppLimited || roundRate > (context.bbr_btlbw || 0)) {
|
|
4858
5070
|
context.bbr_btlbw_samples.push({ round: context.bbr_round_count, rate: roundRate });
|
|
4859
5071
|
}
|
|
4860
5072
|
}
|
|
@@ -4873,6 +5085,8 @@ function QUICConnection(options) {
|
|
|
4873
5085
|
context.bbr_round_start_pn = context.send_pn.app; // next PN must be acked for the next round
|
|
4874
5086
|
context.bbr_round_start_delivered = context.delivered;
|
|
4875
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;
|
|
4876
5090
|
|
|
4877
5091
|
var BBR_BTLBW_WINDOW = 10; // rounds
|
|
4878
5092
|
var cutoff = context.bbr_round_count - BBR_BTLBW_WINDOW;
|
|
@@ -4885,7 +5099,11 @@ function QUICConnection(options) {
|
|
|
4885
5099
|
}
|
|
4886
5100
|
}
|
|
4887
5101
|
context.bbr_btlbw_samples = kept;
|
|
4888
|
-
|
|
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;
|
|
4889
5107
|
|
|
4890
5108
|
// Derived in-flight target. Uses bbr_min_rtt (RTprop, windowed) in seconds.
|
|
4891
5109
|
if (!crRetreat && context.bbr_btlbw !== null && context.bbr_min_rtt !== null) {
|
|
@@ -4966,6 +5184,9 @@ function QUICConnection(options) {
|
|
|
4966
5184
|
// ── Write the controls, clamped to [min,max] (the hard safety net). ──
|
|
4967
5185
|
var clamp = function (v, lo, hi) { return Math.max(lo, Math.min(hi, v)); };
|
|
4968
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;
|
|
4969
5190
|
var targetInflight = clamp(cwnd_gain * context.bbr_bdp,
|
|
4970
5191
|
context.min_limit_bytes_in_flight, context.max_limit_bytes_in_flight);
|
|
4971
5192
|
context.current_limit_bytes_in_flight = targetInflight;
|
|
@@ -4984,8 +5205,18 @@ function QUICConnection(options) {
|
|
|
4984
5205
|
if (context.bbr_state === 'startup') {
|
|
4985
5206
|
// …within startupCap, which may now be lower than what ACKs grew
|
|
4986
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;
|
|
4987
5218
|
targetInflight = Math.max(context.init_limit_bytes_in_flight,
|
|
4988
|
-
Math.min(Math.max(targetInflight,
|
|
5219
|
+
Math.min(Math.max(targetInflight, floor), startupCap()));
|
|
4989
5220
|
context.current_limit_bytes_in_flight = targetInflight;
|
|
4990
5221
|
context.current_limit_packets_in_flight = clamp(
|
|
4991
5222
|
Math.round(targetInflight / context.current_limit_packet_payload),
|
|
@@ -5245,7 +5476,8 @@ function QUICConnection(options) {
|
|
|
5245
5476
|
var sent = { initial: 0, handshake: 0 };
|
|
5246
5477
|
if (context.state === 'draining' || context.state === 'closed' || context.state === 'closing') return sent;
|
|
5247
5478
|
var path = context.path;
|
|
5248
|
-
|
|
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);
|
|
5249
5481
|
var plan = [], planBytes = 0;
|
|
5250
5482
|
var nextPn = { initial: context.send_pn.initial, handshake: context.send_pn.handshake };
|
|
5251
5483
|
var planned = { initial: [], handshake: [] };
|
|
@@ -5682,10 +5914,27 @@ function QUICConnection(options) {
|
|
|
5682
5914
|
return (prev && prev.validated) ? prev : context.path;
|
|
5683
5915
|
}
|
|
5684
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
|
+
|
|
5685
5931
|
function markValidated(path) {
|
|
5686
5932
|
if (path.validated) return;
|
|
5687
5933
|
path.validated = true;
|
|
5688
|
-
if (path === context.path)
|
|
5934
|
+
if (path === context.path) {
|
|
5935
|
+
ev.emit('path_validated', path.address, path.port);
|
|
5936
|
+
issueNewToken(path);
|
|
5937
|
+
}
|
|
5689
5938
|
if (DEBUG) console.log('[quic] path ' + path.address + ':' + path.port + ' validated (received ' + path.received + 'B, sent ' + path.sent + 'B before)');
|
|
5690
5939
|
if (path.queue.length > 0) releaseAmpQueue(path);
|
|
5691
5940
|
}
|
|
@@ -5693,8 +5942,8 @@ function QUICConnection(options) {
|
|
|
5693
5942
|
// ============================================================
|
|
5694
5943
|
// Path MTU discovery — DPLPMTUD (RFC 8899; RFC 9000 §14.3)
|
|
5695
5944
|
//
|
|
5696
|
-
// Packets start at what every QUIC path carries (
|
|
5697
|
-
//
|
|
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
|
|
5698
5947
|
// Ethernet — and every byte more in a packet is a packet less: an
|
|
5699
5948
|
// encryption, a system call, the per-packet bookkeeping, which is where a
|
|
5700
5949
|
// JavaScript sender spends its time. So once the handshake is confirmed,
|
|
@@ -5754,14 +6003,30 @@ function QUICConnection(options) {
|
|
|
5754
6003
|
function shortHeaderOverhead() {
|
|
5755
6004
|
return 1 + destinationCid('1rtt', context.path).byteLength + 4 + 16;
|
|
5756
6005
|
}
|
|
5757
|
-
// The
|
|
5758
|
-
|
|
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; }
|
|
5759
6024
|
function pmtuCeiling() { return Math.min(context.max_limit_packet_payload, context.peer_max_udp_payload); }
|
|
5760
6025
|
|
|
5761
|
-
// Packets
|
|
5762
|
-
// than the floor's.
|
|
6026
|
+
// Packets fill `size`-byte datagrams — never smaller than the floor.
|
|
5763
6027
|
function pmtuApply(size) {
|
|
5764
|
-
context.
|
|
6028
|
+
context.current_limit_datagram = Math.max(context.init_limit_datagram, size);
|
|
6029
|
+
syncPacketPayload();
|
|
5765
6030
|
if (DEBUG) console.log('[quic] PMTU ' + size + ' — packets of ' + context.current_limit_packet_payload + ' bytes of frames');
|
|
5766
6031
|
}
|
|
5767
6032
|
|
|
@@ -5850,7 +6115,7 @@ function QUICConnection(options) {
|
|
|
5850
6115
|
m.confirming = false;
|
|
5851
6116
|
m.lo = 0; m.fail = p.size; m.size = 0; m.tries = 0;
|
|
5852
6117
|
m.bigLost = 0;
|
|
5853
|
-
context.
|
|
6118
|
+
pmtuApply(context.init_limit_datagram);
|
|
5854
6119
|
m.nextAt = Date.now() + PMTU_RAISE_MS;
|
|
5855
6120
|
return;
|
|
5856
6121
|
}
|
|
@@ -5982,8 +6247,9 @@ function QUICConnection(options) {
|
|
|
5982
6247
|
if (path !== context.path) return;
|
|
5983
6248
|
|
|
5984
6249
|
// A path already validated, failing only the expanded re-check (§8.2.3).
|
|
5985
|
-
//
|
|
5986
|
-
//
|
|
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.
|
|
5987
6253
|
if (path.validated) return;
|
|
5988
6254
|
|
|
5989
6255
|
// §9.3.2: "an endpoint MUST revert to using the last validated peer
|
|
@@ -6006,6 +6272,7 @@ function QUICConnection(options) {
|
|
|
6006
6272
|
if (context.candidate_path === path) context.candidate_path = null;
|
|
6007
6273
|
if (DEBUG) console.log('[quic] now sending to ' + path.address + ':' + path.port + (path.validated ? '' : ' (unvalidated)'));
|
|
6008
6274
|
ev.emit('path', path.address, path.port);
|
|
6275
|
+
if (path.validated) issueNewToken(path);
|
|
6009
6276
|
}
|
|
6010
6277
|
|
|
6011
6278
|
// ── Connection IDs: issuing ours, keeping the peer's (RFC 9000 §5.1) ──
|
|
@@ -6210,7 +6477,7 @@ function QUICConnection(options) {
|
|
|
6210
6477
|
context.current_limit_bytes_in_flight = context.init_limit_bytes_in_flight;
|
|
6211
6478
|
context.current_limit_packets_per_sec = context.init_limit_packets_per_sec;
|
|
6212
6479
|
context.current_limit_bytes_per_sec = context.init_limit_bytes_per_sec;
|
|
6213
|
-
context.
|
|
6480
|
+
pmtuApply(context.init_limit_datagram);
|
|
6214
6481
|
context.srtt = null;
|
|
6215
6482
|
context.rttvar = null;
|
|
6216
6483
|
context.min_rtt = null;
|
|
@@ -6229,6 +6496,7 @@ function QUICConnection(options) {
|
|
|
6229
6496
|
context.bbr_bdp = null;
|
|
6230
6497
|
context.bbr_state = 'startup';
|
|
6231
6498
|
context.bbr_round_app_limited = false;
|
|
6499
|
+
context.bbr_peak_inflight = 0; context.bbr_prev_peak_inflight = 0;
|
|
6232
6500
|
context.bbr_full_bw = 0;
|
|
6233
6501
|
context.bbr_full_bw_count = 0;
|
|
6234
6502
|
context.bbr_ea_start = 0; context.bbr_ea_delivered = 0; context.bbr_ea_round_max = 0;
|
|
@@ -6447,6 +6715,7 @@ function QUICConnection(options) {
|
|
|
6447
6715
|
context.pkt_in_flight.set(pn, { bytes: encrypted.length, t: Date.now() });
|
|
6448
6716
|
if (context.tlp_at === 0 || context.tlp_timer === null) tlpRearm(); else context.tlp_at = Date.now() + tlpDelay();
|
|
6449
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;
|
|
6450
6719
|
}
|
|
6451
6720
|
// In flight = carrying STREAM data, which expireInFlight finds and
|
|
6452
6721
|
// retires through its stream. 1-RTT CRYPTO is the control queue's to
|
|
@@ -6454,11 +6723,14 @@ function QUICConnection(options) {
|
|
|
6454
6723
|
// found by anything and held a slot of the packet budget forever.
|
|
6455
6724
|
// DATAGRAM frames are in flight too, with nothing to resend
|
|
6456
6725
|
// (no_resend_in_flight, see sendDatagram).
|
|
6457
|
-
var has_data = false, datagramBytes = 0;
|
|
6726
|
+
var has_data = false, datagramBytes = 0, payloadBytes = 0;
|
|
6458
6727
|
for (var i = 0; i < frameList.length; i++) {
|
|
6459
|
-
if (frameList[i].type === 'stream') has_data = true;
|
|
6728
|
+
if (frameList[i].type === 'stream') { has_data = true; payloadBytes += frameList[i].data ? frameList[i].data.byteLength : 0; }
|
|
6460
6729
|
else if (frameList[i].type === 'datagram') datagramBytes += frameList[i].data.byteLength;
|
|
6461
6730
|
}
|
|
6731
|
+
context.stats_datagram_bytes_sent += datagramBytes;
|
|
6732
|
+
var inFlightPk = context.pkt_in_flight.get(pn);
|
|
6733
|
+
if (inFlightPk) inFlightPk.payload = payloadBytes + datagramBytes; // getStats' bytesAcknowledged
|
|
6462
6734
|
if (has_data || datagramBytes > 0) context.sending_app_pn_in_flight.add(pn);
|
|
6463
6735
|
if (datagramBytes > 0) {
|
|
6464
6736
|
context.no_resend_in_flight[pn] = (context.no_resend_in_flight[pn] || 0) + datagramBytes;
|
|
@@ -6702,6 +6974,19 @@ function QUICConnection(options) {
|
|
|
6702
6974
|
for (var i = 0; i < w.length; i++) w[i].cb(new Error('the connection is closing'));
|
|
6703
6975
|
}
|
|
6704
6976
|
|
|
6977
|
+
function streamSendWindow(streamId) {
|
|
6978
|
+
var st = context.send_streams[Number(streamId)];
|
|
6979
|
+
if (!st) return 0;
|
|
6980
|
+
var queued = 0;
|
|
6981
|
+
for (var k in context.send_streams) {
|
|
6982
|
+
var o = context.send_streams[k];
|
|
6983
|
+
if (o.write_offset > o.max_sent_offset) queued += o.write_offset - o.max_sent_offset;
|
|
6984
|
+
}
|
|
6985
|
+
var streamRoom = st.remote_max_stream_data - st.write_offset;
|
|
6986
|
+
var connRoom = context.remote_max_data - context.max_data_sent - queued;
|
|
6987
|
+
return Math.max(0, Math.min(streamRoom, connRoom));
|
|
6988
|
+
}
|
|
6989
|
+
|
|
6705
6990
|
// The connection's figures, as WebTransport's getStats() reports them
|
|
6706
6991
|
// (W3C WebTransportConnectionStats), and what QUICO's congestion control
|
|
6707
6992
|
// knows besides. Times in ms; estimatedSendRate in bits per second (BBR's
|
|
@@ -6709,9 +6994,21 @@ function QUICConnection(options) {
|
|
|
6709
6994
|
// An exception of ours, caught: in the connection (internal_errors, the
|
|
6710
6995
|
// last one's stack) and the server's counters. The packet being processed
|
|
6711
6996
|
// is dropped (above), or HTTP/3 closes with H3_INTERNAL_ERROR (h3.js).
|
|
6997
|
+
// Reading e.stack formats the whole trace — a few hundred microseconds,
|
|
6998
|
+
// paid for every bad packet: a peer (or a scanner) sending many of them
|
|
6999
|
+
// cost the thread more in traces than in packets. The message always; the
|
|
7000
|
+
// stack, and a log line, once a minute per connection — the bug it points
|
|
7001
|
+
// at is the same one.
|
|
6712
7002
|
function countInternalError(e) {
|
|
6713
7003
|
context.internal_errors = (context.internal_errors || 0) + 1;
|
|
6714
|
-
|
|
7004
|
+
var now = Date.now();
|
|
7005
|
+
if (now - (context.internal_error_logged_at || 0) >= 60000) {
|
|
7006
|
+
context.internal_error_logged_at = now;
|
|
7007
|
+
context.last_internal_error = (e && e.stack) || String(e);
|
|
7008
|
+
console.warn('[quic] packet dropped on an internal error (' + context.internal_errors + ' on this connection so far): ' + context.last_internal_error);
|
|
7009
|
+
} else {
|
|
7010
|
+
context.last_internal_error = (e && e.message) || String(e);
|
|
7011
|
+
}
|
|
6715
7012
|
if (serverStats) serverStats.internalErrors++;
|
|
6716
7013
|
}
|
|
6717
7014
|
|
|
@@ -6720,6 +7017,11 @@ function QUICConnection(options) {
|
|
|
6720
7017
|
bytesSent: context.stats_bytes_sent,
|
|
6721
7018
|
packetsSent: context.stats_packets_sent,
|
|
6722
7019
|
packetsLost: context.lost_count,
|
|
7020
|
+
bytesLost: context.stats_bytes_lost,
|
|
7021
|
+
// The application's bytes: new STREAM data and DATAGRAM payloads sent
|
|
7022
|
+
// (no retransmissions, no framing), and the payload acknowledged.
|
|
7023
|
+
payloadBytesSent: context.max_data_sent + context.stats_datagram_bytes_sent,
|
|
7024
|
+
payloadBytesAcknowledged: context.stats_payload_acked,
|
|
6723
7025
|
bytesReceived: context.stats_bytes_received,
|
|
6724
7026
|
packetsReceived: context.stats_packets_received,
|
|
6725
7027
|
smoothedRtt: context.srtt,
|
|
@@ -7063,7 +7365,7 @@ function QUICConnection(options) {
|
|
|
7063
7365
|
// honoured (see expired_unacked above). Keyed per-PN; a packet that
|
|
7064
7366
|
// carried several streams accumulates one entry per stream.
|
|
7065
7367
|
var limbo = context.expired_unacked[pnum];
|
|
7066
|
-
if (!limbo) limbo = context.expired_unacked[pnum] = { t: now, spans: [] };
|
|
7368
|
+
if (!limbo) { limbo = context.expired_unacked[pnum] = { t: now, spans: [] }; context.limbo_n++; }
|
|
7067
7369
|
var finLost = !!(st.fin_pns && st.fin_pns[pnum]);
|
|
7068
7370
|
var lostRanges = st.in_flight_ranges[pn];
|
|
7069
7371
|
limbo.spans.push([sid, lostRanges, finLost]);
|
|
@@ -7081,7 +7383,7 @@ function QUICConnection(options) {
|
|
|
7081
7383
|
// Count the loss once per packet: the Set holds each PN once, so the
|
|
7082
7384
|
// first stream to expire it returns true; later same-PN deletes (a
|
|
7083
7385
|
// multi-stream packet) return false and don't double-count.
|
|
7084
|
-
if (context.sending_app_pn_in_flight.delete(pnum))
|
|
7386
|
+
if (context.sending_app_pn_in_flight.delete(pnum)) countLostPacket(pnum);
|
|
7085
7387
|
}
|
|
7086
7388
|
}
|
|
7087
7389
|
|
|
@@ -7098,7 +7400,7 @@ function QUICConnection(options) {
|
|
|
7098
7400
|
if (context.expired_unacked[pnum]) context.expired_unacked[pnum].early = true;
|
|
7099
7401
|
if (pnum in context.no_resend_in_flight) {
|
|
7100
7402
|
delete context.no_resend_in_flight[pnum];
|
|
7101
|
-
if (context.sending_app_pn_in_flight.delete(pnum))
|
|
7403
|
+
if (context.sending_app_pn_in_flight.delete(pnum)) countLostPacket(pnum);
|
|
7102
7404
|
}
|
|
7103
7405
|
if (context.pkt_in_flight.has(pnum)) {
|
|
7104
7406
|
var pBytes = forgetPacket(pnum);
|
|
@@ -7290,7 +7592,7 @@ function QUICConnection(options) {
|
|
|
7290
7592
|
(now - context.sending_app_pn_history[ridx][0] >= timeout);
|
|
7291
7593
|
if (!rexpired) continue;
|
|
7292
7594
|
delete context.no_resend_in_flight[rpn];
|
|
7293
|
-
if (context.sending_app_pn_in_flight.delete(rnum))
|
|
7595
|
+
if (context.sending_app_pn_in_flight.delete(rnum)) countLostPacket(rnum);
|
|
7294
7596
|
}
|
|
7295
7597
|
|
|
7296
7598
|
// And every packet past the same timeout leaves the bytes in flight —
|
|
@@ -7306,9 +7608,15 @@ function QUICConnection(options) {
|
|
|
7306
7608
|
|
|
7307
7609
|
// Bound the limbo table: entries older than ~8 timeouts (min 10s) will
|
|
7308
7610
|
// never see their ACK — the peer retires ACK ranges long before that.
|
|
7309
|
-
|
|
7310
|
-
|
|
7311
|
-
|
|
7611
|
+
// At most once a second: it walks the whole table, and this runs on
|
|
7612
|
+
// every send burst. An entry kept a second longer only means a late ACK
|
|
7613
|
+
// for it is still honoured.
|
|
7614
|
+
if (now - (context.limbo_pruned_at || 0) >= 1000) {
|
|
7615
|
+
context.limbo_pruned_at = now;
|
|
7616
|
+
var limboCutoff = now - Math.max(10000, timeout * 8);
|
|
7617
|
+
for (var lpn in context.expired_unacked) {
|
|
7618
|
+
if (context.expired_unacked[lpn].t < limboCutoff) { delete context.expired_unacked[lpn]; context.limbo_n--; }
|
|
7619
|
+
}
|
|
7312
7620
|
}
|
|
7313
7621
|
}
|
|
7314
7622
|
|
|
@@ -7371,6 +7679,7 @@ function QUICConnection(options) {
|
|
|
7371
7679
|
|
|
7372
7680
|
function plan_quic_burst() {
|
|
7373
7681
|
context.send_scheduled = false; // this pass takes what writes left waiting
|
|
7682
|
+
syncPacketPayload(); // 0-RTT → 1-RTT, a new connection ID
|
|
7374
7683
|
if (!zeroRttSending()) {
|
|
7375
7684
|
if (!context.app_write) return;
|
|
7376
7685
|
if (context.state !== 'connected') return;
|
|
@@ -8284,6 +8593,13 @@ function QUICConnection(options) {
|
|
|
8284
8593
|
getJA4: function (opts) { return (tls && tls.getJA4) ? tls.getJA4(opts) : null; },
|
|
8285
8594
|
/** The peer's certificate chain, leaf first, as DER — or null. */
|
|
8286
8595
|
getPeerCertificateChain: function () { return (tls && tls.getPeerCertificateChain) ? tls.getPeerCertificateChain() : null; },
|
|
8596
|
+
// TLS keying material (RFC 8446 §7.5), as node:tls's: WebTransport's
|
|
8597
|
+
// exportKeyingMaterial builds on it. null before the handshake has it.
|
|
8598
|
+
exportKeyingMaterial: function (length, label, context) { return (tls && tls.exportKeyingMaterial) ? tls.exportKeyingMaterial(length, label, context) : null; },
|
|
8599
|
+
// How many new bytes stream `streamId` could send now under flow control
|
|
8600
|
+
// (RFC 9000 §4.1): its own window, and the connection's less what every
|
|
8601
|
+
// stream has written and not yet sent. WebTransport's atomicWrite asks.
|
|
8602
|
+
streamSendWindow: streamSendWindow,
|
|
8287
8603
|
// The client's certificate, node:tls-shaped, and whether it was verified
|
|
8288
8604
|
// (requestCert): what req.socket answers over HTTP/3.
|
|
8289
8605
|
getPeerCertificate: function () { return (tls && tls.getPeerCertificate) ? tls.getPeerCertificate() : {}; },
|