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.
- package/README.md +854 -125
- package/index.js +5 -2
- package/package.json +2 -2
- package/src/agent.js +58 -0
- package/src/client.js +34 -5
- package/src/crypto.js +17 -0
- package/src/h3_client.js +9 -0
- package/src/quic_connection.js +187 -13
- package/src/quic_server.js +61 -3
- package/src/quic_socket.js +52 -1
- package/src/streams.js +15 -1
- package/src/tls_bridge.js +42 -2
- package/src/transport.js +93 -0
package/src/quic_socket.js
CHANGED
|
@@ -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
|
-
|
|
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({
|
|
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 = {
|
|
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
|
-
|
|
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,
|