quico 0.7.0 → 0.8.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 +245 -31
- package/index.d.ts +1034 -77
- package/index.js +7 -3
- package/package.json +7 -4
- package/src/connect_udp_client.js +9 -0
- package/src/h1_server.js +51 -9
- package/src/h2_server.js +60 -11
- package/src/h3.js +29 -60
- package/src/h3_client.js +24 -5
- package/src/h3_server.js +101 -6
- package/src/hpack_primitives.js +64 -25
- package/src/http_common.js +7 -0
- package/src/quic_connection.js +200 -23
- package/src/quic_server.js +94 -13
- package/src/server.js +266 -22
- package/src/streams.js +183 -10
- package/src/tcp_listener.js +11 -0
- package/src/tls_bridge.js +4 -0
- package/src/webtransport.js +424 -54
- 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
|
@@ -613,6 +613,13 @@ function QUICConnection(options) {
|
|
|
613
613
|
stats_bytes_sent: 0,
|
|
614
614
|
stats_packets_received: 0,
|
|
615
615
|
stats_bytes_received: 0,
|
|
616
|
+
// The application's bytes in them, for WebTransport's getStats() (W3C
|
|
617
|
+
// WebTransportConnectionStats): DATAGRAM payloads sent (new STREAM bytes
|
|
618
|
+
// are max_data_sent), the payload of the packets acknowledged, and the
|
|
619
|
+
// size of the packets declared lost.
|
|
620
|
+
stats_datagram_bytes_sent: 0,
|
|
621
|
+
stats_payload_acked: 0,
|
|
622
|
+
stats_bytes_lost: 0,
|
|
616
623
|
// Streams that hit the peer's flow-control limit since the last check —
|
|
617
624
|
// for STREAM_DATA_BLOCKED (maybeSendBlockedFrames), instead of a walk of
|
|
618
625
|
// every stream on every send pass.
|
|
@@ -888,6 +895,7 @@ function QUICConnection(options) {
|
|
|
888
895
|
// pn → { t: expiry wall-time, spans: [[sid, ranges, finLost], ...] }
|
|
889
896
|
// (ranges: the packet's flat range list of that stream)
|
|
890
897
|
expired_unacked: {},
|
|
898
|
+
limbo_n: 0, // entries in expired_unacked (kept, not counted)
|
|
891
899
|
reorder_in_count: 0, // app packets that arrived below the highest PN seen (network reordering, peer→us)
|
|
892
900
|
|
|
893
901
|
// --- BBR-lite measurement (Phase 4b, step 1+2 — measure only, no control). ---
|
|
@@ -1122,6 +1130,18 @@ function QUICConnection(options) {
|
|
|
1122
1130
|
if (changed.state && (context.state === 'draining' || context.state === 'closing' || context.state === 'closed')) handshakeEnded(false);
|
|
1123
1131
|
|
|
1124
1132
|
if (changed.state && (context.state === 'draining' || context.state === 'closing')) {
|
|
1133
|
+
// 'closing': the connection is over for the application now — the
|
|
1134
|
+
// peer's CONNECTION_CLOSE came (draining), or ours went (closing).
|
|
1135
|
+
// 'close' follows when the closing or draining period ends (RFC 9000
|
|
1136
|
+
// §10.2: up to three PTOs, here at most 3 s), which is packet
|
|
1137
|
+
// housekeeping, not news: a session above that waited for 'close'
|
|
1138
|
+
// learned the peer was gone seconds late. With the peer's code and
|
|
1139
|
+
// reason (context.peer_close) when it was the peer; null when we
|
|
1140
|
+
// closed. Once.
|
|
1141
|
+
if (!context.closing_emitted) {
|
|
1142
|
+
context.closing_emitted = true;
|
|
1143
|
+
ev.emit('closing', context.state === 'draining' ? (context.peer_close || null) : null);
|
|
1144
|
+
}
|
|
1125
1145
|
failPermitWaiters();
|
|
1126
1146
|
clearIdleTimer();
|
|
1127
1147
|
// A draining/closing endpoint must stop retransmitting CRYPTO — the
|
|
@@ -1660,6 +1680,7 @@ function QUICConnection(options) {
|
|
|
1660
1680
|
context.sending_app_pn_in_flight.clear();
|
|
1661
1681
|
context.pn_sids.clear();
|
|
1662
1682
|
context.expired_unacked = {};
|
|
1683
|
+
context.limbo_n = 0;
|
|
1663
1684
|
context.no_resend_in_flight = {};
|
|
1664
1685
|
context.control_in_flight = {};
|
|
1665
1686
|
for (var sid in context.send_streams) {
|
|
@@ -4522,6 +4543,16 @@ function QUICConnection(options) {
|
|
|
4522
4543
|
}
|
|
4523
4544
|
}
|
|
4524
4545
|
|
|
4546
|
+
// A 1-RTT packet carrying stream data or datagrams declared lost: counted
|
|
4547
|
+
// (getStats' packetsLost, and bytesLost by its size on the wire), and
|
|
4548
|
+
// Careful Resume told.
|
|
4549
|
+
function countLostPacket(pnum) {
|
|
4550
|
+
context.lost_count++;
|
|
4551
|
+
var h = context.sending_app_pn_history[pnum - (context.send_pn.app - context.sending_app_pn_history.length)];
|
|
4552
|
+
if (h) context.stats_bytes_lost += h[1];
|
|
4553
|
+
crOnLoss();
|
|
4554
|
+
}
|
|
4555
|
+
|
|
4525
4556
|
// A packet given up on — expired, or its control frames deemed lost: out
|
|
4526
4557
|
// of the bytes in flight. Returns its size (0: not in flight any more).
|
|
4527
4558
|
function forgetPacket(pn) {
|
|
@@ -4532,6 +4563,58 @@ function QUICConnection(options) {
|
|
|
4532
4563
|
return pk.bytes;
|
|
4533
4564
|
}
|
|
4534
4565
|
|
|
4566
|
+
// How many of the values in `r` (flat [from, to) pairs) `a` already
|
|
4567
|
+
// covers — flat_ranges.length(flat_ranges.intersect(a, r)), found by binary
|
|
4568
|
+
// search in `a` for each pair of `r` instead of a walk of both. Both are
|
|
4569
|
+
// the library's sorted, merged lists; `r` not so (never seen) is answered
|
|
4570
|
+
// the original way.
|
|
4571
|
+
function coveredLength(a, r) {
|
|
4572
|
+
var na = a.length, total = 0, prevTo = -Infinity;
|
|
4573
|
+
if (na === 0 || r.length === 0) return 0;
|
|
4574
|
+
for (var i = 0; i < r.length; i += 2) {
|
|
4575
|
+
var f = r[i], t = r[i + 1];
|
|
4576
|
+
if (f < prevTo) return flat_ranges.length(flat_ranges.intersect(a, r));
|
|
4577
|
+
prevTo = t;
|
|
4578
|
+
if (f >= t) continue;
|
|
4579
|
+
// first pair of a whose `to` > f
|
|
4580
|
+
var lo = 0, hi = na >> 1;
|
|
4581
|
+
while (lo < hi) {
|
|
4582
|
+
var mid = (lo + hi) >> 1;
|
|
4583
|
+
if (a[(mid << 1) + 1] <= f) lo = mid + 1; else hi = mid;
|
|
4584
|
+
}
|
|
4585
|
+
for (var k = lo << 1; k < na && a[k] < t; k += 2) {
|
|
4586
|
+
var from = a[k] > f ? a[k] : f;
|
|
4587
|
+
var to = a[k + 1] < t ? a[k + 1] : t;
|
|
4588
|
+
if (to > from) total += to - from;
|
|
4589
|
+
}
|
|
4590
|
+
}
|
|
4591
|
+
return total;
|
|
4592
|
+
}
|
|
4593
|
+
|
|
4594
|
+
// Would flat_ranges.remove(list, r) change `list`? Only if a pair of `r`
|
|
4595
|
+
// overlaps one of it (half-open; flat_ranges.overlaps, a binary search).
|
|
4596
|
+
function overlapsAny(list, r) {
|
|
4597
|
+
if (list.length === 0) return false;
|
|
4598
|
+
for (var i = 0; i < r.length; i += 2) {
|
|
4599
|
+
if (flat_ranges.overlaps(list, r[i], r[i + 1])) return true;
|
|
4600
|
+
}
|
|
4601
|
+
return false;
|
|
4602
|
+
}
|
|
4603
|
+
|
|
4604
|
+
// How many packet numbers a flat [from, to, from, to, ...] list of
|
|
4605
|
+
// inclusive ranges covers.
|
|
4606
|
+
function rangesSpan(r) {
|
|
4607
|
+
var n = 0;
|
|
4608
|
+
for (var i = 0; i < r.length; i += 2) n += r[i + 1] - r[i] + 1;
|
|
4609
|
+
return n;
|
|
4610
|
+
}
|
|
4611
|
+
|
|
4612
|
+
// Entries in the limbo table (kept as it changes — counting them meant
|
|
4613
|
+
// walking it).
|
|
4614
|
+
function limboCount() {
|
|
4615
|
+
return context.limbo_n > 0 ? context.limbo_n : 0;
|
|
4616
|
+
}
|
|
4617
|
+
|
|
4535
4618
|
function processAckFrame(space, frame) {
|
|
4536
4619
|
// RFC 9000 §13.1: "An endpoint SHOULD treat receipt of an acknowledgment
|
|
4537
4620
|
// for a packet it did not send as a connection error of type
|
|
@@ -4665,9 +4748,28 @@ function QUICConnection(options) {
|
|
|
4665
4748
|
// Mark in-flight as acked. Collect the acked PNs first, then mutate —
|
|
4666
4749
|
// deleting from a Set while iterating it with for-of is fragile.
|
|
4667
4750
|
var ackedPns = [];
|
|
4668
|
-
|
|
4751
|
+
// Walked from whichever side is smaller: the packets in flight, or the
|
|
4752
|
+
// packet numbers this ACK covers. Every in-flight packet was tested
|
|
4753
|
+
// against every range on every ACK — at a high RTT the window holds
|
|
4754
|
+
// thousands, and a page's worth of ACKs cost that many times over.
|
|
4755
|
+
// Ascending either way (the set is in send order), as before.
|
|
4756
|
+
if (rangesSpan(ackedRanges) < context.sending_app_pn_in_flight.size) {
|
|
4669
4757
|
for (var ri = 0; ri < ackedRanges.length; ri += 2) {
|
|
4670
|
-
|
|
4758
|
+
for (var rpn0 = ackedRanges[ri]; rpn0 <= ackedRanges[ri + 1]; rpn0++) {
|
|
4759
|
+
if (context.sending_app_pn_in_flight.has(rpn0)) ackedPns.push(rpn0);
|
|
4760
|
+
}
|
|
4761
|
+
}
|
|
4762
|
+
} else {
|
|
4763
|
+
// Both in ascending order (ack_frame_to_ranges sorts and merges the
|
|
4764
|
+
// ranges; the set is in packet-number order): one pointer into the
|
|
4765
|
+
// ranges, and the walk ends past the largest acknowledged — what is
|
|
4766
|
+
// in flight above it cannot be in this ACK. Each packet was tested
|
|
4767
|
+
// against every range, and the walk went on to the newest packet.
|
|
4768
|
+
var rp1 = 0, rTop1 = ackedRanges[ackedRanges.length - 1];
|
|
4769
|
+
for (var pn of context.sending_app_pn_in_flight) {
|
|
4770
|
+
if (pn > rTop1) break;
|
|
4771
|
+
while (ackedRanges[rp1 + 1] < pn) rp1 += 2;
|
|
4772
|
+
if (pn >= ackedRanges[rp1]) ackedPns.push(pn);
|
|
4671
4773
|
}
|
|
4672
4774
|
}
|
|
4673
4775
|
// A packet's ranges of a stream, acknowledged. Returns the bytes newly
|
|
@@ -4675,12 +4777,19 @@ function QUICConnection(options) {
|
|
|
4675
4777
|
// transmission and a retransmission of the same range ending up ACKed
|
|
4676
4778
|
// (routine once late ACKs for expired packets are honoured) count once.
|
|
4677
4779
|
function creditRanges(st, ranges) {
|
|
4678
|
-
|
|
4780
|
+
// The bytes of `ranges` not acknowledged before — what
|
|
4781
|
+
// length(intersect(acked, ranges)) gave, without walking all of
|
|
4782
|
+
// acked_ranges and building the intersection for every packet. After
|
|
4783
|
+
// a loss acked_ranges holds hundreds of pieces until the hole is
|
|
4784
|
+
// filled, and this ran once per acknowledged packet (coveredLength).
|
|
4785
|
+
var fresh = flat_ranges.length(ranges) - coveredLength(st.acked_ranges, ranges);
|
|
4679
4786
|
flat_ranges.add(st.acked_ranges, ranges);
|
|
4680
4787
|
// Out of missing, whatever it was: an ACK is proof — a late one too,
|
|
4681
4788
|
// for bytes already given up on and not yet resent (add_have would
|
|
4682
4789
|
// leave those missing: it defers to what not_have says).
|
|
4683
|
-
|
|
4790
|
+
// remove() walks and rebuilds the whole list; it is called only when
|
|
4791
|
+
// it would change something — when the ranges touch a missing one.
|
|
4792
|
+
if (overlapsAny(st.missing_ranges, ranges)) flat_ranges.remove(st.missing_ranges, ranges);
|
|
4684
4793
|
if (st.acked_ranges[0] === 0) sendQueueRelease(st, st.acked_ranges[1]);
|
|
4685
4794
|
return fresh > 0 ? fresh : 0;
|
|
4686
4795
|
}
|
|
@@ -4714,15 +4823,17 @@ function QUICConnection(options) {
|
|
|
4714
4823
|
var largestAckedPn = -1;
|
|
4715
4824
|
for (var lri = 0; lri < ackedRanges.length; lri += 2) if (ackedRanges[lri + 1] > largestAckedPn) largestAckedPn = ackedRanges[lri + 1];
|
|
4716
4825
|
var ackedPkts = [];
|
|
4826
|
+
var rp2 = 0; // pointer into the ranges, as for ackedPns above
|
|
4717
4827
|
for (var pke of context.pkt_in_flight) {
|
|
4718
4828
|
var ppn = pke[0];
|
|
4719
4829
|
if (ppn > largestAckedPn) break;
|
|
4720
|
-
|
|
4721
|
-
|
|
4722
|
-
}
|
|
4830
|
+
while (ackedRanges[rp2 + 1] < ppn) rp2 += 2;
|
|
4831
|
+
if (ppn >= ackedRanges[rp2]) ackedPkts.push(ppn);
|
|
4723
4832
|
}
|
|
4724
4833
|
var ackedSizes = [];
|
|
4725
4834
|
for (var api = 0; api < ackedPkts.length; api++) {
|
|
4835
|
+
var apk = context.pkt_in_flight.get(ackedPkts[api]);
|
|
4836
|
+
if (apk && apk.payload) context.stats_payload_acked += apk.payload;
|
|
4726
4837
|
var asz = forgetPacket(ackedPkts[api]);
|
|
4727
4838
|
ackedSizes.push(asz);
|
|
4728
4839
|
ackedPacketBytes += asz;
|
|
@@ -4756,14 +4867,33 @@ function QUICConnection(options) {
|
|
|
4756
4867
|
// DID arrive — the timer was simply wrong — so: credit the goodput
|
|
4757
4868
|
// (repairing the BtlBw signal), mark the spans acked (cancelling any
|
|
4758
4869
|
// retransmit that hasn't left yet), and take back the loss we counted.
|
|
4759
|
-
|
|
4760
|
-
|
|
4761
|
-
|
|
4762
|
-
|
|
4763
|
-
|
|
4870
|
+
// The limbo table holds up to ~10 s of expired packets: walking all of
|
|
4871
|
+
// it for each ACK (for-in, a string key made per entry) was the larger
|
|
4872
|
+
// part of an ACK's cost on a lossy, distant path. The covered numbers
|
|
4873
|
+
// are looked up instead when they are fewer — ascending, as for-in
|
|
4874
|
+
// visits integer keys.
|
|
4875
|
+
var lateHits = [];
|
|
4876
|
+
if (rangesSpan(ackedRanges) < limboCount()) {
|
|
4877
|
+
for (var lr = 0; lr < ackedRanges.length; lr += 2) {
|
|
4878
|
+
for (var lq = ackedRanges[lr]; lq <= ackedRanges[lr + 1]; lq++) {
|
|
4879
|
+
if (lq in context.expired_unacked) lateHits.push(String(lq));
|
|
4880
|
+
}
|
|
4881
|
+
}
|
|
4882
|
+
} else {
|
|
4883
|
+
// Ascending (integer keys), so: one pointer into the ranges, and
|
|
4884
|
+
// done past the largest acknowledged.
|
|
4885
|
+
var rp3 = 0, rTop3 = ackedRanges[ackedRanges.length - 1];
|
|
4886
|
+
for (var lpn0 in context.expired_unacked) {
|
|
4887
|
+
var lnum = Number(lpn0);
|
|
4888
|
+
if (lnum > rTop3) break;
|
|
4889
|
+
while (ackedRanges[rp3 + 1] < lnum) rp3 += 2;
|
|
4890
|
+
if (lnum >= ackedRanges[rp3]) lateHits.push(lpn0);
|
|
4764
4891
|
}
|
|
4765
|
-
|
|
4892
|
+
}
|
|
4893
|
+
for (var lhi = 0; lhi < lateHits.length; lhi++) {
|
|
4894
|
+
var lpn = lateHits[lhi];
|
|
4766
4895
|
var rec = context.expired_unacked[lpn];
|
|
4896
|
+
if (!rec) continue;
|
|
4767
4897
|
for (var si2 = 0; si2 < rec.spans.length; si2++) {
|
|
4768
4898
|
var sidL = rec.spans[si2][0];
|
|
4769
4899
|
var stL = context.send_streams[sidL];
|
|
@@ -4776,6 +4906,7 @@ function QUICConnection(options) {
|
|
|
4776
4906
|
ackedPacketBytes += rec.bytes || 0;
|
|
4777
4907
|
if (rec.early) context.stats_spurious_early++;
|
|
4778
4908
|
delete context.expired_unacked[lpn];
|
|
4909
|
+
context.limbo_n--;
|
|
4779
4910
|
if (context.lost_count > 0) context.lost_count--; // it wasn't a loss after all
|
|
4780
4911
|
}
|
|
4781
4912
|
|
|
@@ -6454,11 +6585,14 @@ function QUICConnection(options) {
|
|
|
6454
6585
|
// found by anything and held a slot of the packet budget forever.
|
|
6455
6586
|
// DATAGRAM frames are in flight too, with nothing to resend
|
|
6456
6587
|
// (no_resend_in_flight, see sendDatagram).
|
|
6457
|
-
var has_data = false, datagramBytes = 0;
|
|
6588
|
+
var has_data = false, datagramBytes = 0, payloadBytes = 0;
|
|
6458
6589
|
for (var i = 0; i < frameList.length; i++) {
|
|
6459
|
-
if (frameList[i].type === 'stream') has_data = true;
|
|
6590
|
+
if (frameList[i].type === 'stream') { has_data = true; payloadBytes += frameList[i].data ? frameList[i].data.byteLength : 0; }
|
|
6460
6591
|
else if (frameList[i].type === 'datagram') datagramBytes += frameList[i].data.byteLength;
|
|
6461
6592
|
}
|
|
6593
|
+
context.stats_datagram_bytes_sent += datagramBytes;
|
|
6594
|
+
var inFlightPk = context.pkt_in_flight.get(pn);
|
|
6595
|
+
if (inFlightPk) inFlightPk.payload = payloadBytes + datagramBytes; // getStats' bytesAcknowledged
|
|
6462
6596
|
if (has_data || datagramBytes > 0) context.sending_app_pn_in_flight.add(pn);
|
|
6463
6597
|
if (datagramBytes > 0) {
|
|
6464
6598
|
context.no_resend_in_flight[pn] = (context.no_resend_in_flight[pn] || 0) + datagramBytes;
|
|
@@ -6702,6 +6836,19 @@ function QUICConnection(options) {
|
|
|
6702
6836
|
for (var i = 0; i < w.length; i++) w[i].cb(new Error('the connection is closing'));
|
|
6703
6837
|
}
|
|
6704
6838
|
|
|
6839
|
+
function streamSendWindow(streamId) {
|
|
6840
|
+
var st = context.send_streams[Number(streamId)];
|
|
6841
|
+
if (!st) return 0;
|
|
6842
|
+
var queued = 0;
|
|
6843
|
+
for (var k in context.send_streams) {
|
|
6844
|
+
var o = context.send_streams[k];
|
|
6845
|
+
if (o.write_offset > o.max_sent_offset) queued += o.write_offset - o.max_sent_offset;
|
|
6846
|
+
}
|
|
6847
|
+
var streamRoom = st.remote_max_stream_data - st.write_offset;
|
|
6848
|
+
var connRoom = context.remote_max_data - context.max_data_sent - queued;
|
|
6849
|
+
return Math.max(0, Math.min(streamRoom, connRoom));
|
|
6850
|
+
}
|
|
6851
|
+
|
|
6705
6852
|
// The connection's figures, as WebTransport's getStats() reports them
|
|
6706
6853
|
// (W3C WebTransportConnectionStats), and what QUICO's congestion control
|
|
6707
6854
|
// knows besides. Times in ms; estimatedSendRate in bits per second (BBR's
|
|
@@ -6709,9 +6856,21 @@ function QUICConnection(options) {
|
|
|
6709
6856
|
// An exception of ours, caught: in the connection (internal_errors, the
|
|
6710
6857
|
// last one's stack) and the server's counters. The packet being processed
|
|
6711
6858
|
// is dropped (above), or HTTP/3 closes with H3_INTERNAL_ERROR (h3.js).
|
|
6859
|
+
// Reading e.stack formats the whole trace — a few hundred microseconds,
|
|
6860
|
+
// paid for every bad packet: a peer (or a scanner) sending many of them
|
|
6861
|
+
// cost the thread more in traces than in packets. The message always; the
|
|
6862
|
+
// stack, and a log line, once a minute per connection — the bug it points
|
|
6863
|
+
// at is the same one.
|
|
6712
6864
|
function countInternalError(e) {
|
|
6713
6865
|
context.internal_errors = (context.internal_errors || 0) + 1;
|
|
6714
|
-
|
|
6866
|
+
var now = Date.now();
|
|
6867
|
+
if (now - (context.internal_error_logged_at || 0) >= 60000) {
|
|
6868
|
+
context.internal_error_logged_at = now;
|
|
6869
|
+
context.last_internal_error = (e && e.stack) || String(e);
|
|
6870
|
+
console.warn('[quic] packet dropped on an internal error (' + context.internal_errors + ' on this connection so far): ' + context.last_internal_error);
|
|
6871
|
+
} else {
|
|
6872
|
+
context.last_internal_error = (e && e.message) || String(e);
|
|
6873
|
+
}
|
|
6715
6874
|
if (serverStats) serverStats.internalErrors++;
|
|
6716
6875
|
}
|
|
6717
6876
|
|
|
@@ -6720,6 +6879,11 @@ function QUICConnection(options) {
|
|
|
6720
6879
|
bytesSent: context.stats_bytes_sent,
|
|
6721
6880
|
packetsSent: context.stats_packets_sent,
|
|
6722
6881
|
packetsLost: context.lost_count,
|
|
6882
|
+
bytesLost: context.stats_bytes_lost,
|
|
6883
|
+
// The application's bytes: new STREAM data and DATAGRAM payloads sent
|
|
6884
|
+
// (no retransmissions, no framing), and the payload acknowledged.
|
|
6885
|
+
payloadBytesSent: context.max_data_sent + context.stats_datagram_bytes_sent,
|
|
6886
|
+
payloadBytesAcknowledged: context.stats_payload_acked,
|
|
6723
6887
|
bytesReceived: context.stats_bytes_received,
|
|
6724
6888
|
packetsReceived: context.stats_packets_received,
|
|
6725
6889
|
smoothedRtt: context.srtt,
|
|
@@ -7063,7 +7227,7 @@ function QUICConnection(options) {
|
|
|
7063
7227
|
// honoured (see expired_unacked above). Keyed per-PN; a packet that
|
|
7064
7228
|
// carried several streams accumulates one entry per stream.
|
|
7065
7229
|
var limbo = context.expired_unacked[pnum];
|
|
7066
|
-
if (!limbo) limbo = context.expired_unacked[pnum] = { t: now, spans: [] };
|
|
7230
|
+
if (!limbo) { limbo = context.expired_unacked[pnum] = { t: now, spans: [] }; context.limbo_n++; }
|
|
7067
7231
|
var finLost = !!(st.fin_pns && st.fin_pns[pnum]);
|
|
7068
7232
|
var lostRanges = st.in_flight_ranges[pn];
|
|
7069
7233
|
limbo.spans.push([sid, lostRanges, finLost]);
|
|
@@ -7081,7 +7245,7 @@ function QUICConnection(options) {
|
|
|
7081
7245
|
// Count the loss once per packet: the Set holds each PN once, so the
|
|
7082
7246
|
// first stream to expire it returns true; later same-PN deletes (a
|
|
7083
7247
|
// multi-stream packet) return false and don't double-count.
|
|
7084
|
-
if (context.sending_app_pn_in_flight.delete(pnum))
|
|
7248
|
+
if (context.sending_app_pn_in_flight.delete(pnum)) countLostPacket(pnum);
|
|
7085
7249
|
}
|
|
7086
7250
|
}
|
|
7087
7251
|
|
|
@@ -7098,7 +7262,7 @@ function QUICConnection(options) {
|
|
|
7098
7262
|
if (context.expired_unacked[pnum]) context.expired_unacked[pnum].early = true;
|
|
7099
7263
|
if (pnum in context.no_resend_in_flight) {
|
|
7100
7264
|
delete context.no_resend_in_flight[pnum];
|
|
7101
|
-
if (context.sending_app_pn_in_flight.delete(pnum))
|
|
7265
|
+
if (context.sending_app_pn_in_flight.delete(pnum)) countLostPacket(pnum);
|
|
7102
7266
|
}
|
|
7103
7267
|
if (context.pkt_in_flight.has(pnum)) {
|
|
7104
7268
|
var pBytes = forgetPacket(pnum);
|
|
@@ -7290,7 +7454,7 @@ function QUICConnection(options) {
|
|
|
7290
7454
|
(now - context.sending_app_pn_history[ridx][0] >= timeout);
|
|
7291
7455
|
if (!rexpired) continue;
|
|
7292
7456
|
delete context.no_resend_in_flight[rpn];
|
|
7293
|
-
if (context.sending_app_pn_in_flight.delete(rnum))
|
|
7457
|
+
if (context.sending_app_pn_in_flight.delete(rnum)) countLostPacket(rnum);
|
|
7294
7458
|
}
|
|
7295
7459
|
|
|
7296
7460
|
// And every packet past the same timeout leaves the bytes in flight —
|
|
@@ -7306,9 +7470,15 @@ function QUICConnection(options) {
|
|
|
7306
7470
|
|
|
7307
7471
|
// Bound the limbo table: entries older than ~8 timeouts (min 10s) will
|
|
7308
7472
|
// never see their ACK — the peer retires ACK ranges long before that.
|
|
7309
|
-
|
|
7310
|
-
|
|
7311
|
-
|
|
7473
|
+
// At most once a second: it walks the whole table, and this runs on
|
|
7474
|
+
// every send burst. An entry kept a second longer only means a late ACK
|
|
7475
|
+
// for it is still honoured.
|
|
7476
|
+
if (now - (context.limbo_pruned_at || 0) >= 1000) {
|
|
7477
|
+
context.limbo_pruned_at = now;
|
|
7478
|
+
var limboCutoff = now - Math.max(10000, timeout * 8);
|
|
7479
|
+
for (var lpn in context.expired_unacked) {
|
|
7480
|
+
if (context.expired_unacked[lpn].t < limboCutoff) { delete context.expired_unacked[lpn]; context.limbo_n--; }
|
|
7481
|
+
}
|
|
7312
7482
|
}
|
|
7313
7483
|
}
|
|
7314
7484
|
|
|
@@ -8284,6 +8454,13 @@ function QUICConnection(options) {
|
|
|
8284
8454
|
getJA4: function (opts) { return (tls && tls.getJA4) ? tls.getJA4(opts) : null; },
|
|
8285
8455
|
/** The peer's certificate chain, leaf first, as DER — or null. */
|
|
8286
8456
|
getPeerCertificateChain: function () { return (tls && tls.getPeerCertificateChain) ? tls.getPeerCertificateChain() : null; },
|
|
8457
|
+
// TLS keying material (RFC 8446 §7.5), as node:tls's: WebTransport's
|
|
8458
|
+
// exportKeyingMaterial builds on it. null before the handshake has it.
|
|
8459
|
+
exportKeyingMaterial: function (length, label, context) { return (tls && tls.exportKeyingMaterial) ? tls.exportKeyingMaterial(length, label, context) : null; },
|
|
8460
|
+
// How many new bytes stream `streamId` could send now under flow control
|
|
8461
|
+
// (RFC 9000 §4.1): its own window, and the connection's less what every
|
|
8462
|
+
// stream has written and not yet sent. WebTransport's atomicWrite asks.
|
|
8463
|
+
streamSendWindow: streamSendWindow,
|
|
8287
8464
|
// The client's certificate, node:tls-shaped, and whether it was verified
|
|
8288
8465
|
// (requestCert): what req.socket answers over HTTP/3.
|
|
8289
8466
|
getPeerCertificate: function () { return (tls && tls.getPeerCertificate) ? tls.getPeerCertificate() : {}; },
|
package/src/quic_server.js
CHANGED
|
@@ -201,8 +201,28 @@ function createQuicServer(options) {
|
|
|
201
201
|
// gave it, with the time, sealed (AES-256-GCM) to the client's IP address
|
|
202
202
|
// under a key of this server's own. It lives seconds, so a key drawn at
|
|
203
203
|
// start is enough: a restart costs only the Retries in flight.
|
|
204
|
+
//
|
|
205
|
+
// tokenSecret (node:quic's name, at least 16 bytes) gives the key instead:
|
|
206
|
+
// servers that pass the same one open each other's tokens. That is what
|
|
207
|
+
// several servers sharing a port need (one per thread, SO_REUSEPORT): a
|
|
208
|
+
// client back with a NEW_TOKEN from another port is hashed to another of
|
|
209
|
+
// them, and without the key its token does not open — no harm, only the
|
|
210
|
+
// shortcut lost. The AES key is derived from it (SHA-256), so a 16-byte
|
|
211
|
+
// secret serves as well as a 32-byte one.
|
|
204
212
|
var validateAddress = options.validateAddress === true;
|
|
205
|
-
var
|
|
213
|
+
var tokenSecret = checkTokenSecret(options.tokenSecret) || crypto.randomBytes(32);
|
|
214
|
+
var retryTokenKey = tokenKeyOf(tokenSecret);
|
|
215
|
+
|
|
216
|
+
function checkTokenSecret(secret) {
|
|
217
|
+
if (secret === undefined || secret === null) return null;
|
|
218
|
+
if (!(secret instanceof Uint8Array) || secret.byteLength < 16) {
|
|
219
|
+
throw new TypeError('tokenSecret must be a Buffer or Uint8Array of at least 16 bytes');
|
|
220
|
+
}
|
|
221
|
+
return secret;
|
|
222
|
+
}
|
|
223
|
+
function tokenKeyOf(secret) {
|
|
224
|
+
return crypto.createHash('sha256').update('quico address-validation token key').update(secret).digest();
|
|
225
|
+
}
|
|
206
226
|
var RETRY_TOKEN_LIFETIME = 10000; // ms
|
|
207
227
|
// Tokens of two kinds, sealed alike (RFC 9000 §8.1.3): a Retry's, good for
|
|
208
228
|
// seconds and one connection attempt; and a NEW_TOKEN's, given to a client
|
|
@@ -478,6 +498,7 @@ function createQuicServer(options) {
|
|
|
478
498
|
// in length". Shorter ones would also be easy to guess and collide.
|
|
479
499
|
if (firstPkt.dcid.byteLength < 8) return;
|
|
480
500
|
if (connectionCount >= maxConnections) return;
|
|
501
|
+
if (!accepting) return;
|
|
481
502
|
|
|
482
503
|
var retry = null, knownAddress = false;
|
|
483
504
|
if (firstPkt.token && firstPkt.token.byteLength > 0) {
|
|
@@ -723,8 +744,8 @@ function createQuicServer(options) {
|
|
|
723
744
|
var SERVER_RUNTIME = ['validateAddress', 'handshakeLoadThreshold', 'maxHandshakesPerAddress', 'earlyData', 'earlyDataMaxAge'];
|
|
724
745
|
var ELSEWHERE = {
|
|
725
746
|
key: 'setSecureContext()', cert: 'setSecureContext()', ca: 'setSecureContext()', pfx: 'setSecureContext()',
|
|
726
|
-
passphrase: 'setSecureContext()', SNICallback: 'setSecureContext()',
|
|
727
|
-
ticketKeys: 'setTicketKeys()', ech: 'setEchKeys()', resetTokenSecret: 'setResetTokenSecret()',
|
|
747
|
+
passphrase: 'setSecureContext()', SNICallback: 'addContext() or setSecureContext()',
|
|
748
|
+
ticketKeys: 'setTicketKeys()', ech: 'setEchKeys()', resetTokenSecret: 'setResetTokenSecret()', tokenSecret: 'setTokenSecret()',
|
|
728
749
|
transportParams: 'connectionOptions, per connection'
|
|
729
750
|
};
|
|
730
751
|
function optErr(Ctor, code, msg) { var e = new Ctor(msg); e.code = code; return e; }
|
|
@@ -826,6 +847,11 @@ function createQuicServer(options) {
|
|
|
826
847
|
// timeout. close() waits for them — not long (SEND_DRAIN_MAX).
|
|
827
848
|
var sendsPending = 0;
|
|
828
849
|
var shutDown = false;
|
|
850
|
+
// stopAccepting(): no new connection (an Initial for an unknown one is
|
|
851
|
+
// dropped); those open go on — a graceful close, the layer above deciding
|
|
852
|
+
// when they end.
|
|
853
|
+
var accepting = true;
|
|
854
|
+
var refed = true;
|
|
829
855
|
var closing = null;
|
|
830
856
|
var SEND_DRAIN_MAX = 200;
|
|
831
857
|
|
|
@@ -836,6 +862,7 @@ function createQuicServer(options) {
|
|
|
836
862
|
|
|
837
863
|
function listen(listenPort, host, callback) {
|
|
838
864
|
shutDown = false;
|
|
865
|
+
accepting = true;
|
|
839
866
|
if (typeof host === 'function') { callback = host; host = null; }
|
|
840
867
|
// `|| 443` turned port 0 into 443 — and port 0 is not "no port": it is how
|
|
841
868
|
// a caller asks the OS for a free one (server.js has the same fix).
|
|
@@ -919,6 +946,7 @@ function createQuicServer(options) {
|
|
|
919
946
|
ev.emit('error', err);
|
|
920
947
|
});
|
|
921
948
|
var host4 = host.indexOf('.') !== -1 ? host : '0.0.0.0';
|
|
949
|
+
if (!refed) udp4.unref();
|
|
922
950
|
udp4.bind(port, host4, function () { growBuffers(udp4); settle(null); });
|
|
923
951
|
|
|
924
952
|
udp6 = dgram.createSocket({ type: 'udp6', ipv6Only: true });
|
|
@@ -931,6 +959,7 @@ function createQuicServer(options) {
|
|
|
931
959
|
settle(err);
|
|
932
960
|
});
|
|
933
961
|
var host6 = host.indexOf(':') !== -1 ? host : '::';
|
|
962
|
+
if (!refed) udp6.unref();
|
|
934
963
|
udp6.bind(port, host6, function () { growBuffers(udp6); settle(null); });
|
|
935
964
|
|
|
936
965
|
startSweepTimer();
|
|
@@ -1004,13 +1033,47 @@ function createQuicServer(options) {
|
|
|
1004
1033
|
// External packet API (for shared UDP port scenarios)
|
|
1005
1034
|
// ============================================================
|
|
1006
1035
|
|
|
1007
|
-
// Query whether
|
|
1008
|
-
//
|
|
1009
|
-
//
|
|
1010
|
-
//
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1036
|
+
// Query whether a datagram belongs to an active QUIC connection.
|
|
1037
|
+
//
|
|
1038
|
+
// hasConnection(rinfo) — by the address it came from. Used by the demuxer
|
|
1039
|
+
// to disambiguate byte 0 in range 64-79 between QUIC short header and TURN
|
|
1040
|
+
// ChannelData (RFC 9443). Fresh connections must be classified by byte 0
|
|
1041
|
+
// range (80-127 or 192+ = QUIC).
|
|
1042
|
+
//
|
|
1043
|
+
// hasConnection(rinfo, msg) — by the connection ID the datagram is
|
|
1044
|
+
// addressed to, from wherever it came: the answer to "is this one mine"
|
|
1045
|
+
// when several servers share the port (one per thread, SO_REUSEPORT) and a
|
|
1046
|
+
// datagram can reach one that does not hold its connection — a client
|
|
1047
|
+
// whose address changed is hashed to another socket. Asked before
|
|
1048
|
+
// handlePacket: given a short-header packet for an ID it does not know,
|
|
1049
|
+
// handlePacket answers with a Stateless Reset, and that would end a
|
|
1050
|
+
// connection another server holds. The address does not count here: an ID
|
|
1051
|
+
// that leads nowhere is not ours, whatever address sent it.
|
|
1052
|
+
function hasConnection(rinfo, msg) {
|
|
1053
|
+
if (msg === undefined || msg === null) {
|
|
1054
|
+
var key = rinfo.address + ':' + rinfo.port;
|
|
1055
|
+
return (key in addressMap) && (addressMap[key] in connections);
|
|
1056
|
+
}
|
|
1057
|
+
var dcid = datagramDcid(msg);
|
|
1058
|
+
if (!dcid) return false;
|
|
1059
|
+
var k = cidKey(dcid);
|
|
1060
|
+
return (k in cidMap) && (cidMap[k] in connections);
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
// The Destination Connection ID of a datagram's first packet, without
|
|
1064
|
+
// parsing the rest (RFC 9000 §17.2, §17.3; RFC 8999 for any version): a
|
|
1065
|
+
// long header carries its length, a short header ours (SERVER_CID_LEN).
|
|
1066
|
+
// null for one too short to hold it.
|
|
1067
|
+
function datagramDcid(msg) {
|
|
1068
|
+
if (!(msg instanceof Uint8Array) || msg.length < 1) return null;
|
|
1069
|
+
if (msg[0] & 0x80) {
|
|
1070
|
+
if (msg.length < 6) return null;
|
|
1071
|
+
var len = msg[5];
|
|
1072
|
+
if (msg.length < 6 + len) return null;
|
|
1073
|
+
return msg.subarray(6, 6 + len);
|
|
1074
|
+
}
|
|
1075
|
+
if (msg.length < 1 + SERVER_CID_LEN) return null;
|
|
1076
|
+
return msg.subarray(1, 1 + SERVER_CID_LEN);
|
|
1014
1077
|
}
|
|
1015
1078
|
|
|
1016
1079
|
|
|
@@ -1021,14 +1084,21 @@ function createQuicServer(options) {
|
|
|
1021
1084
|
return {
|
|
1022
1085
|
listen: listen,
|
|
1023
1086
|
close: close,
|
|
1087
|
+
stopAccepting: function () { accepting = false; },
|
|
1088
|
+
get connectionCount() { return connectionCount; },
|
|
1089
|
+
// node:dgram's: whether the process may exit while this listens.
|
|
1090
|
+
ref: function () { refed = true; if (udp4) udp4.ref(); if (udp6) udp6.ref(); },
|
|
1091
|
+
unref: function () { refed = false; if (udp4) udp4.unref(); if (udp6) udp6.unref(); },
|
|
1024
1092
|
on: function (name, fn) { ev.on(name, fn); },
|
|
1025
1093
|
|
|
1026
1094
|
/** Feed an incoming UDP packet when running in external-socket mode.
|
|
1027
1095
|
* rinfo must be in Node's dgram format: { address, port, family, size }. */
|
|
1028
1096
|
handlePacket: onUdpMessage,
|
|
1029
1097
|
|
|
1030
|
-
/**
|
|
1031
|
-
*
|
|
1098
|
+
/** hasConnection(rinfo): the address matches an active QUIC connection.
|
|
1099
|
+
* hasConnection(rinfo, msg): the datagram is addressed to one (by its
|
|
1100
|
+
* connection ID). Used by demuxers for routing decisions on shared UDP
|
|
1101
|
+
* ports — the second when several servers share it (see above). */
|
|
1032
1102
|
hasConnection: hasConnection,
|
|
1033
1103
|
|
|
1034
1104
|
/** The session ticket keys — tls.Server's names. A new key applies to
|
|
@@ -1059,7 +1129,18 @@ function createQuicServer(options) {
|
|
|
1059
1129
|
setOptions: setOptions,
|
|
1060
1130
|
|
|
1061
1131
|
getResetTokenSecret: function () { return resetTokenSecret; },
|
|
1062
|
-
setResetTokenSecret: function (secret) { resetTokenSecret = checkResetTokenSecret(secret) || resetTokenSecret; }
|
|
1132
|
+
setResetTokenSecret: function (secret) { resetTokenSecret = checkResetTokenSecret(secret) || resetTokenSecret; },
|
|
1133
|
+
|
|
1134
|
+
/** The address-validation token secret (Retry, NEW_TOKEN — see
|
|
1135
|
+
* tokenSecret above). A new one applies from now on; tokens sealed
|
|
1136
|
+
* under the one before no longer open: a client back with a NEW_TOKEN
|
|
1137
|
+
* loses its shortcut, and one mid-Retry its attempt (it accepts no
|
|
1138
|
+
* second Retry). */
|
|
1139
|
+
getTokenSecret: function () { return tokenSecret; },
|
|
1140
|
+
setTokenSecret: function (secret) {
|
|
1141
|
+
var s = checkTokenSecret(secret);
|
|
1142
|
+
if (s) { tokenSecret = s; retryTokenKey = tokenKeyOf(s); }
|
|
1143
|
+
}
|
|
1063
1144
|
};
|
|
1064
1145
|
}
|
|
1065
1146
|
|