quico 0.3.0 → 0.4.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.
@@ -27,6 +27,16 @@ import { QUICConnection } from './quic_connection.js';
27
27
  * opts:
28
28
  * remoteIp, remotePort — where to send packets
29
29
  * hostname — SNI / :authority for the QUIC/TLS handshake
30
+ * alpn — ALPN protocol(s) to offer. String or array.
31
+ * Defaults to ['h3']. Non-HTTP/3 consumers pass
32
+ * their own (e.g. ['hq-interop'] for HTTP/0.9,
33
+ * ['doq'] for DNS-over-QUIC).
34
+ * rejectUnauthorized — verify the server certificate (default true)
35
+ * ca — CA certificate(s) for verification
36
+ * onSocket(quic, socket) — called as soon as the QUICConnection exists,
37
+ * BEFORE the handshake starts. Use this to attach
38
+ * listeners that must observe the handshake, such
39
+ * as 'keylog'. onConnect is too late for those.
30
40
  * onConnect(quic, socket) — called once the QUIC handshake completes
31
41
  * onClose() — called when the connection closes (the socket is
32
42
  * already closed by the time this runs)
@@ -49,7 +59,48 @@ function createQuicClientSocket(opts) {
49
59
  });
50
60
 
51
61
  udpSocket.bind(0, function () {
52
- quic = new QUICConnection({ isServer: false, hostname: opts.hostname });
62
+ // Grow the kernel socket buffers before any traffic flows.
63
+ //
64
+ // Node's defaults are small (and notably small on Windows). When the send
65
+ // buffer overflows, the datagram is dropped locally with no error — it
66
+ // looks exactly like network loss, so the loss-recovery machinery kicks in
67
+ // and the connection crawls. This was the failure that killed the third
68
+ // pacer-calibration attempt (see LOSS_PTO_PLAN 7.4): larger bursts spilled
69
+ // over the buffer and turned a lossless link into a slow one.
70
+ //
71
+ // Wrapped in try/catch because the call can throw on platforms that cap
72
+ // the size; failing to enlarge the buffer is not a reason to fail the
73
+ // connection.
74
+ try { udpSocket.setSendBufferSize(2 * 1024 * 1024); } catch (e) {}
75
+ try { udpSocket.setRecvBufferSize(2 * 1024 * 1024); } catch (e) {}
76
+
77
+ quic = new QUICConnection({
78
+ isServer: false,
79
+ hostname: opts.hostname,
80
+ // Without this the client always offered 'h3' regardless of what the
81
+ // caller wanted, because TLSBridge falls back to ['h3'] when alpn is
82
+ // undefined. Normalize a bare string into an array here so callers can
83
+ // pass either form.
84
+ alpn: (function () {
85
+ var a = opts.alpn || ['h3'];
86
+ return Array.isArray(a) ? a : [a];
87
+ })(),
88
+ // Defaults to true. Pass false for self-signed certificates, or supply
89
+ // `ca` for a private CA.
90
+ rejectUnauthorized: opts.rejectUnauthorized !== false,
91
+ ca: opts.ca || null
92
+ });
93
+
94
+ // Hand the connection to the caller BEFORE connect() runs.
95
+ //
96
+ // onConnect fires after the handshake, which is too late for anything that
97
+ // has to observe the handshake itself. Key logging is the concrete case:
98
+ // lemon-tls skips building NSS lines while nothing is listening, and the
99
+ // handshake secrets are emitted during connect() — so a listener attached
100
+ // in onConnect receives nothing at all, and the client half of a capture
101
+ // is undecryptable. onSocket gives callers a hook at the only moment that
102
+ // works.
103
+ if (opts.onSocket) opts.onSocket(quic, udpSocket);
53
104
 
54
105
  quic.on('packet', function (data) {
55
106
  udpSocket.send(data, opts.remotePort, opts.remoteIp, function (err) {
package/src/streams.js CHANGED
@@ -291,7 +291,21 @@ class ServerResponse extends Writable {
291
291
  class ClientRequest extends Writable {
292
292
 
293
293
  constructor(options) {
294
- super({ highWaterMark: (options && options.highWaterMark) || 64 * 1024 });
294
+ super({
295
+ highWaterMark: (options && options.highWaterMark) || 64 * 1024,
296
+ // Node's Writable default (autoDestroy: true) marks the stream
297
+ // destroyed the moment the request BODY finishes — i.e. milliseconds
298
+ // after end(), on every GET. But a ClientRequest outlives its body: the
299
+ // transport keeps working on it until the response completes, and
300
+ // node:http's ClientRequest behaves exactly that way. With the default,
301
+ // `destroyed` stops meaning "aborted" and starts meaning "body written",
302
+ // and every liveness guard built on it silently kills healthy requests:
303
+ // the H3→H2/H1 fallback after the 3s timeout hit such a guard and died
304
+ // for every GET, and coalesced requests parked on a shared connection
305
+ // being established were dropped at wake-up the same way. abort() still
306
+ // calls destroy() explicitly, so real cancellations are unaffected.
307
+ autoDestroy: false
308
+ });
295
309
 
296
310
  options = options || {};
297
311
 
package/src/tls_bridge.js CHANGED
@@ -52,6 +52,14 @@ function TLSBridge(options) {
52
52
  var originalDcid = options.originalDcid || new Uint8Array(0);
53
53
  var localCid = options.localCid || new Uint8Array(0); // client's own SCID
54
54
  var hostname = options.hostname || null;
55
+ // Peer certificate verification. Defaults to true, matching node:tls — but
56
+ // until now the option was never plumbed through at all, so quico could not
57
+ // talk to a server whose certificate wasn't signed by a publicly trusted CA:
58
+ // no local development against self-signed certs, no internal PKI, and no
59
+ // interop (the QUIC interop runner mints its own CA and addresses hosts as
60
+ // server4 / server6, so every implementation disables verification there).
61
+ var rejectUnauthorized = options.rejectUnauthorized !== false;
62
+ var ca = options.ca || null;
55
63
  // ALPN protocol(s) to advertise. Default ['h3']; normalize string → array.
56
64
  var supportedAlpns = (function () {
57
65
  var a = options.alpn || ['h3'];
@@ -60,7 +68,12 @@ function TLSBridge(options) {
60
68
 
61
69
  function feedMessage(data) {
62
70
  if (!session) {
63
- var sessionOpts = { isServer: isServer, SNICallback: options.SNICallback };
71
+ var sessionOpts = {
72
+ isServer: isServer,
73
+ SNICallback: options.SNICallback,
74
+ rejectUnauthorized: rejectUnauthorized
75
+ };
76
+ if (ca) sessionOpts.ca = ca;
64
77
  if (!isServer) {
65
78
  sessionOpts.sessionId = new Uint8Array(0); // QUIC: no compatibility mode
66
79
  if (hostname) sessionOpts.servername = hostname;
@@ -101,7 +114,18 @@ function TLSBridge(options) {
101
114
  helloHandled = true; // ClientHello will be generated from this set_context
102
115
  }
103
116
  }
104
- session.message(data);
117
+ // Only feed real handshake bytes. QUICConnection.connect() calls
118
+ // feedMessage(new Uint8Array(0)) as a "trigger" to start the handshake,
119
+ // but the ClientHello is actually built by lemon-tls from set_context()
120
+ // above, on its own timer — the empty feed was always a no-op in intent.
121
+ //
122
+ // It stopped being harmless once lemon-tls hardened parse failures into
123
+ // fatalAlert(): an empty buffer fails as "truncated u8 at 0", which sets
124
+ // the session state to 'error' permanently, so the ClientHello timer then
125
+ // early-returns and NO handshake is ever sent. Guard here rather than
126
+ // making lemon-tls lenient — rejecting malformed handshake input is the
127
+ // correct behavior for a TLS stack facing a hostile peer.
128
+ if (data && data.length > 0) session.message(data);
105
129
  }
106
130
 
107
131
  function setupSessionEvents() {
@@ -218,6 +242,22 @@ function TLSBridge(options) {
218
242
  session.on('keyUpdate', function (info) {
219
243
  ev.emit('keyUpdate', info.direction, info.secret);
220
244
  });
245
+
246
+ // NSS SSLKEYLOGFILE lines, forwarded to whoever owns the connection.
247
+ //
248
+ // lemon-tls already formats these correctly for TLS 1.3 — including
249
+ // picking client_random from the right side based on role — and QUIC uses
250
+ // the identical label set (CLIENT_HANDSHAKE_TRAFFIC_SECRET,
251
+ // SERVER_TRAFFIC_SECRET_0, ...), so no translation is needed here.
252
+ //
253
+ // Note that subscribing has a cost: lemon-tls skips building these strings
254
+ // entirely while listenerCount('keylog') is 0. We accept that, matching
255
+ // node:tls, which also always emits 'keylog'. Two short buffers per
256
+ // handshake is not a measurable expense, and having key logs available
257
+ // without a rebuild is worth far more when debugging a live capture.
258
+ session.on('keylog', function (line) {
259
+ ev.emit('keylog', line);
260
+ });
221
261
  }
222
262
 
223
263
  return {
package/src/transport.js CHANGED
@@ -154,6 +154,94 @@ function parse_quic_datagram(array) {
154
154
  }
155
155
 
156
156
 
157
+ // ============================================================
158
+ // Version Negotiation (RFC 9000 §6, §17.2.1)
159
+ // ============================================================
160
+
161
+ /** QUIC versions this implementation can actually speak. */
162
+ var SUPPORTED_VERSIONS = [0x00000001];
163
+
164
+ /**
165
+ * Is this a version we can handle?
166
+ * Version 0 identifies a Version Negotiation packet, not a real version.
167
+ */
168
+ function is_supported_version(version) {
169
+ return SUPPORTED_VERSIONS.indexOf(version >>> 0) !== -1;
170
+ }
171
+
172
+ /**
173
+ * Build a Version Negotiation packet (RFC 9000 §17.2.1).
174
+ *
175
+ * A server MUST send one when it receives a long-header packet carrying a
176
+ * version it does not support — it is how a client discovers what the server
177
+ * speaks, and how any QUIC prober checks whether a server is alive without
178
+ * completing a handshake. The interop runner's simulator does exactly that
179
+ * before starting a test, so without this the server looks dead.
180
+ *
181
+ * The packet is unusual: no encryption, no packet number, no frames, and no
182
+ * connection state. It is valid only as an immediate response.
183
+ *
184
+ * IMPORTANT: the connection IDs are swapped. The DCID we send is the SCID we
185
+ * received, and vice versa — the client matches the reply against its own
186
+ * Source CID, so getting this backwards makes the packet be ignored.
187
+ *
188
+ * byte 0 1 (long header) + 7 unpredictable bits
189
+ * 1..4 version 0x00000000
190
+ * 5 DCID length, then DCID
191
+ * ... SCID length, then SCID
192
+ * ... list of supported versions, 4 bytes each
193
+ *
194
+ * @param {Uint8Array} recvDcid DCID from the client's packet (becomes our SCID)
195
+ * @param {Uint8Array} recvScid SCID from the client's packet (becomes our DCID)
196
+ */
197
+ function build_version_negotiation(recvDcid, recvScid) {
198
+ var dcid = recvScid || new Uint8Array(0);
199
+ var scid = recvDcid || new Uint8Array(0);
200
+
201
+ // RFC 9000 §17.2.1: the low 7 bits are unused and SHOULD be random, so that
202
+ // clients don't come to depend on a fixed value.
203
+ var first = 0x80 | (Math.floor(Math.random() * 128) & 0x7f);
204
+
205
+ var versions = SUPPORTED_VERSIONS.slice();
206
+
207
+ // A reserved ("greased") version, per RFC 9000 §6.3. Its pattern is
208
+ // 0x?a?a?a?a, and no endpoint will ever support it. Including one keeps
209
+ // clients honest: any client that assumes it understands every version in
210
+ // the list will break here rather than in the field.
211
+ var grease = ((Math.floor(Math.random() * 16) << 28) |
212
+ (0x0a << 24) |
213
+ (Math.floor(Math.random() * 16) << 20) |
214
+ (0x0a << 16) |
215
+ (Math.floor(Math.random() * 16) << 12) |
216
+ (0x0a << 8) |
217
+ (Math.floor(Math.random() * 16) << 4) |
218
+ 0x0a) >>> 0;
219
+ versions.push(grease);
220
+
221
+ var out = new Uint8Array(1 + 4 + 1 + dcid.length + 1 + scid.length + versions.length * 4);
222
+ var o = 0;
223
+
224
+ out[o++] = first;
225
+ out[o++] = 0; out[o++] = 0; out[o++] = 0; out[o++] = 0; // version 0
226
+
227
+ out[o++] = dcid.length;
228
+ out.set(dcid, o); o += dcid.length;
229
+
230
+ out[o++] = scid.length;
231
+ out.set(scid, o); o += scid.length;
232
+
233
+ for (var i = 0; i < versions.length; i++) {
234
+ var v = versions[i] >>> 0;
235
+ out[o++] = (v >>> 24) & 0xff;
236
+ out[o++] = (v >>> 16) & 0xff;
237
+ out[o++] = (v >>> 8) & 0xff;
238
+ out[o++] = v & 0xff;
239
+ }
240
+
241
+ return out;
242
+ }
243
+
244
+
157
245
  // ============================================================
158
246
  // QUIC Frame encoding (RFC 9000 §12.4)
159
247
  // ============================================================
@@ -657,6 +745,11 @@ export {
657
745
  parse_quic_datagram,
658
746
  parse_quic_packet,
659
747
 
748
+ // Version negotiation
749
+ SUPPORTED_VERSIONS,
750
+ is_supported_version,
751
+ build_version_negotiation,
752
+
660
753
  // Frame encode/decode
661
754
  encode_quic_frames,
662
755
  parse_quic_frames,