quico 0.3.0 → 0.7.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 +2489 -147
- package/index.js +56 -34
- package/package.json +3 -3
- package/src/agent.js +364 -36
- package/src/client.js +474 -118
- package/src/client_profile.js +266 -0
- package/src/connect_udp.js +578 -0
- package/src/connect_udp_client.js +770 -0
- package/src/crypto.js +257 -140
- package/src/h1.js +953 -0
- package/src/h1_client.js +731 -0
- package/src/h1_server.js +624 -0
- package/src/h2.js +1325 -0
- package/src/h2_client.js +675 -0
- package/src/h2_fingerprint.js +403 -0
- package/src/h2_frames.js +582 -0
- package/src/h2_server.js +862 -0
- package/src/h3.js +1966 -469
- package/src/h3_client.js +476 -48
- package/src/h3_server.js +1349 -94
- package/src/hpack.js +455 -0
- package/src/hpack_primitives.js +520 -0
- package/src/http_common.js +901 -0
- package/src/ja4h.js +256 -0
- package/src/qpack.js +555 -0
- package/src/quic_connection.js +6666 -632
- package/src/quic_server.js +822 -69
- package/src/quic_socket.js +183 -7
- package/src/quic_version.js +143 -0
- package/src/server.js +843 -115
- package/src/server_stats.js +93 -0
- package/src/streams.js +376 -36
- package/src/tcp_listener.js +557 -0
- package/src/tls_bridge.js +516 -75
- package/src/tls_options.js +108 -0
- package/src/transport.js +541 -39
- package/src/utils.js +94 -12
- package/src/webtransport.js +433 -436
- package/src/ws.js +624 -0
- package/src/ws_client.js +641 -0
- package/src/ws_server.js +833 -0
- package/src/wt_stream.js +618 -0
package/src/quic_server.js
CHANGED
|
@@ -13,10 +13,18 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import dgram from 'node:dgram';
|
|
16
|
+
import crypto from 'node:crypto';
|
|
16
17
|
import { DEBUG, Emitter } from './utils.js';
|
|
17
|
-
import {
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
import {
|
|
19
|
+
parse_quic_datagram,
|
|
20
|
+
build_version_negotiation
|
|
21
|
+
} from './transport.js';
|
|
22
|
+
import { build_retry_packet } from './crypto.js';
|
|
23
|
+
import { QUICConnection, checkConnectionOptions, connectionOptionDefaults, CONNECTION_OPTIONS } from './quic_connection.js';
|
|
24
|
+
import { createSecureContext, defaultTransportParams } from './tls_bridge.js';
|
|
25
|
+
import { serverTlsOptions } from './tls_options.js';
|
|
26
|
+
import lemonTls from 'lemon-tls';
|
|
27
|
+
import { QUIC_V1, QUIC_V2, parseVersionList } from './quic_version.js';
|
|
20
28
|
|
|
21
29
|
|
|
22
30
|
// ============================================================
|
|
@@ -36,7 +44,9 @@ import { createSecureContext } from './tls_bridge.js';
|
|
|
36
44
|
//
|
|
37
45
|
// Events:
|
|
38
46
|
// 'connection' (quic, peer) — new QUICConnection created for a peer.
|
|
39
|
-
// peer = { address, port }
|
|
47
|
+
// peer = { address, port }, kept current
|
|
48
|
+
// if the client's address changes (as are
|
|
49
|
+
// quic.remoteAddress / remotePort). Handlers
|
|
40
50
|
// attach quic.on('connect'|'stream'|
|
|
41
51
|
// 'datagram'|'close') as needed.
|
|
42
52
|
// 'error' (err) — UDP socket / send error.
|
|
@@ -55,6 +65,46 @@ import { createSecureContext } from './tls_bridge.js';
|
|
|
55
65
|
|
|
56
66
|
function createQuicServer(options) {
|
|
57
67
|
options = options || {};
|
|
68
|
+
// The transport parameters every connection sends, where the application
|
|
69
|
+
// set them (tls_bridge) — checked once, now, rather than per connection.
|
|
70
|
+
var transportParams = options.transportParams || null;
|
|
71
|
+
defaultTransportParams(transportParams);
|
|
72
|
+
// What each new connection is made with (quic_connection's
|
|
73
|
+
// CONNECTION_OPTIONS: initialCongestionWindow, carefulResume, startupCap,
|
|
74
|
+
// pmtuDiscovery, maxUdpPayloadSize, initialRtt, reorderWindow) — the
|
|
75
|
+
// defaults, what createServer set, and setOptions since. A connection
|
|
76
|
+
// keeps what it was made with; connectionOptions and conn.setOptions
|
|
77
|
+
// change one connection's.
|
|
78
|
+
// 0-RTT (RFC 9001 §4.6), off unless asked for: tickets that allow it and
|
|
79
|
+
// taking it, for connections made from then on. earlyDataMaxAge: how far
|
|
80
|
+
// a ticket's stated age may be from what this server's clock says;
|
|
81
|
+
// earlyDataReplay(id, ttl, cb): whether a ticket carries early data for the
|
|
82
|
+
// first time (default: remembered in this process). See quic_connection.
|
|
83
|
+
function earlyMaxAge(v) {
|
|
84
|
+
if (v === undefined || v === null) return 10000;
|
|
85
|
+
if (typeof v !== 'number' || !(v > 0) || v > 600000) { var e = new RangeError('earlyDataMaxAge must be between 1 and 600000 (ms), got ' + v); e.code = 'ERR_OUT_OF_RANGE'; throw e; }
|
|
86
|
+
return v;
|
|
87
|
+
}
|
|
88
|
+
if (options.earlyData != null && typeof options.earlyData !== 'boolean') { var ee = new TypeError('earlyData must be a boolean, got ' + options.earlyData); ee.code = 'ERR_INVALID_ARG_TYPE'; throw ee; }
|
|
89
|
+
if (options.earlyDataReplay != null && typeof options.earlyDataReplay !== 'function') { var er = new TypeError('earlyDataReplay must be a function (id, ttl, callback)'); er.code = 'ERR_INVALID_ARG_TYPE'; throw er; }
|
|
90
|
+
var earlyData = options.earlyData === true;
|
|
91
|
+
var earlyDataMaxAge = earlyMaxAge(options.earlyDataMaxAge);
|
|
92
|
+
var earlyDataReplay = options.earlyDataReplay || null;
|
|
93
|
+
|
|
94
|
+
var connDefaults = (function () {
|
|
95
|
+
var given = {};
|
|
96
|
+
for (var k in CONNECTION_OPTIONS) if (options[k] !== undefined && options[k] !== null) given[k] = options[k];
|
|
97
|
+
return Object.assign(connectionOptionDefaults(), checkConnectionOptions(given, 'createServer'));
|
|
98
|
+
})();
|
|
99
|
+
// Each connection's own settings, asked once its ClientHello is in
|
|
100
|
+
// (quic_connection: connectionOptions).
|
|
101
|
+
if (options.connectionOptions != null && typeof options.connectionOptions !== 'function')
|
|
102
|
+
throw new TypeError('connectionOptions must be a function (info, callback)');
|
|
103
|
+
var connectionOptions = options.connectionOptions || null;
|
|
104
|
+
// The QUIC versions accepted, in preference (quic_version.js): v1 first —
|
|
105
|
+
// a client that offers v2 too (version_information) stays in v1 unless
|
|
106
|
+
// the application puts v2 first; one that starts in v2 is served in it.
|
|
107
|
+
var quicVersions = parseVersionList(options.quicVersions, 'quicVersions') || [QUIC_V1, QUIC_V2];
|
|
58
108
|
|
|
59
109
|
var ev = Emitter();
|
|
60
110
|
var udp4 = null;
|
|
@@ -82,97 +132,672 @@ function createQuicServer(options) {
|
|
|
82
132
|
|
|
83
133
|
var isExternal = !!(externalSocket4 || externalSocket6);
|
|
84
134
|
|
|
135
|
+
// grease_quic_bit (RFC 9287): invite clients to send the fixed bit
|
|
136
|
+
// unpredictable, so the bit stays free to change. On by default, but not
|
|
137
|
+
// with external sockets: a port shared with other protocols (STUN, TURN,
|
|
138
|
+
// DTLS — RFC 9443) tells QUIC apart by that very bit (§3.1). An external
|
|
139
|
+
// socket that carries QUIC alone: greaseQuicBit: true.
|
|
140
|
+
if (options.greaseQuicBit != null && typeof options.greaseQuicBit !== 'boolean') {
|
|
141
|
+
throw new TypeError('greaseQuicBit must be a boolean');
|
|
142
|
+
}
|
|
143
|
+
var greaseQuicBit = options.greaseQuicBit != null ? options.greaseQuicBit : !isExternal;
|
|
144
|
+
|
|
145
|
+
// Session tickets: ONE key for every connection of this server, so a
|
|
146
|
+
// ticket issued on one connection opens on the next. Left to lemon-tls, each
|
|
147
|
+
// connection drew its own and no client ever resumed. Random when not given:
|
|
148
|
+
// tickets then last until this server restarts; pass ticketKeys (48 bytes)
|
|
149
|
+
// kept across restarts to have them outlive it.
|
|
150
|
+
var ticketKeys = options.ticketKeys || crypto.randomBytes(48);
|
|
151
|
+
var ticketData = typeof options.ticketData === 'function' ? options.ticketData : null;
|
|
152
|
+
// Encrypted Client Hello keys (lemon-tls): read per connection, so a new
|
|
153
|
+
// set (setEchKeys) applies from the next one.
|
|
154
|
+
var ech = options.ech || null;
|
|
155
|
+
// OCSP stapling: (certificate, issuer, callback), asked per connection.
|
|
156
|
+
var ocspRequest = typeof options.ocspRequest === 'function' ? options.ocspRequest : null;
|
|
157
|
+
// node:tls's options — client certificates, ciphers, curves, signature
|
|
158
|
+
// schemes, the ticket lifetime, the handshake timeout — read once, for QUIC
|
|
159
|
+
// (TLS 1.3 only: tls_options.js).
|
|
160
|
+
var tlsOptions = serverTlsOptions(options, true);
|
|
161
|
+
// The server's counters (server_stats.js): createServer's; none on a
|
|
162
|
+
// QUIC server made alone.
|
|
163
|
+
var stats = options.stats || null;
|
|
164
|
+
|
|
165
|
+
// Stateless reset (RFC 9000 §10.3). A client whose server has lost its
|
|
166
|
+
// connection — a restart, a crash, a connection already gone — goes on
|
|
167
|
+
// sending to it and hears nothing, until its idle timeout: 30 seconds of a
|
|
168
|
+
// tab that looks alive and loads nothing. A Stateless Reset tells it at
|
|
169
|
+
// once, and the browser opens a new connection instead.
|
|
170
|
+
//
|
|
171
|
+
// The token that makes a reset believable is HMAC(secret, connection ID),
|
|
172
|
+
// cut to 16 bytes (§10.3.2): one secret for every connection, so a reset
|
|
173
|
+
// can be made for a connection this server no longer has — which is when
|
|
174
|
+
// it is needed. A random secret (the default) covers connections lost
|
|
175
|
+
// while this server runs; one kept across restarts (resetTokenSecret —
|
|
176
|
+
// node:quic's name for it) covers those lost in a restart as well.
|
|
177
|
+
// Whoever holds it can end any connection of this server; kept like the
|
|
178
|
+
// ticket keys.
|
|
179
|
+
var resetTokenSecret = checkResetTokenSecret(options.resetTokenSecret) || crypto.randomBytes(32);
|
|
180
|
+
|
|
181
|
+
function checkResetTokenSecret(secret) {
|
|
182
|
+
if (secret === undefined || secret === null) return null;
|
|
183
|
+
if (!(secret instanceof Uint8Array) || secret.byteLength < 16) {
|
|
184
|
+
throw new TypeError('resetTokenSecret must be a Buffer or Uint8Array of at least 16 bytes');
|
|
185
|
+
}
|
|
186
|
+
return secret;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Address validation with Retry (RFC 9000 §8.1.2). While it is on, a new
|
|
190
|
+
// client's first Initial gets no connection — no state, no key exchange,
|
|
191
|
+
// no signature — only a small Retry packet with a token. A client that is
|
|
192
|
+
// really at its address comes back with it, a round trip later, and only
|
|
193
|
+
// then is a connection made; one that is not (a spoofed flood) never does.
|
|
194
|
+
//
|
|
195
|
+
// Off unless asked for, and never switched by quico itself: whether the
|
|
196
|
+
// round trip is worth paying is the application's call — always, never, or
|
|
197
|
+
// when it sees load — through validateAddress (node:quic's name) and
|
|
198
|
+
// setValidateAddress() while running.
|
|
199
|
+
//
|
|
200
|
+
// The token is the client's first DCID and the connection ID the Retry
|
|
201
|
+
// gave it, with the time, sealed (AES-256-GCM) to the client's IP address
|
|
202
|
+
// under a key of this server's own. It lives seconds, so a key drawn at
|
|
203
|
+
// start is enough: a restart costs only the Retries in flight.
|
|
204
|
+
var validateAddress = options.validateAddress === true;
|
|
205
|
+
var retryTokenKey = crypto.randomBytes(32);
|
|
206
|
+
var RETRY_TOKEN_LIFETIME = 10000; // ms
|
|
207
|
+
// Tokens of two kinds, sealed alike (RFC 9000 §8.1.3): a Retry's, good for
|
|
208
|
+
// seconds and one connection attempt; and a NEW_TOKEN's, given to a client
|
|
209
|
+
// after its handshake for its next connection — from the same address, in
|
|
210
|
+
// the same QUIC version — whose first flight then needs no proof of
|
|
211
|
+
// address: no Retry, and no limit of three times what it sent (§8.1), so
|
|
212
|
+
// a certificate chain larger than that goes out in one round trip. A day,
|
|
213
|
+
// as quic-go's; a restart (a new key) only costs a client its shortcut.
|
|
214
|
+
var TOKEN_RETRY = 1, TOKEN_NEW = 2;
|
|
215
|
+
var NEW_TOKEN_LIFETIME = 24 * 3600 * 1000; // ms
|
|
216
|
+
|
|
217
|
+
// Bound to the client's IP address and the QUIC version of its Initial
|
|
218
|
+
// (the AAD): the Initial that comes back with it must be in that version
|
|
219
|
+
// (RFC 9369 §4.1 — a client MUST NOT switch versions there).
|
|
220
|
+
function tokenAad(ip, version) {
|
|
221
|
+
return Buffer.concat([Buffer.from(ip), Buffer.from([(version >>> 24) & 0xff, (version >>> 16) & 0xff, (version >>> 8) & 0xff, version & 0xff])]);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function sealRetryToken(originalDcid, sourceCid, ip, version) {
|
|
225
|
+
var plain = Buffer.alloc(8 + 1 + originalDcid.length + sourceCid.length);
|
|
226
|
+
plain[0] = TOKEN_RETRY;
|
|
227
|
+
plain.writeUIntBE(Date.now(), 2, 6);
|
|
228
|
+
plain[8] = originalDcid.length;
|
|
229
|
+
Buffer.from(originalDcid).copy(plain, 9);
|
|
230
|
+
Buffer.from(sourceCid).copy(plain, 9 + originalDcid.length);
|
|
231
|
+
return seal(plain, ip, version);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// A NEW_TOKEN's token: the kind and the time, sealed to the address and
|
|
235
|
+
// the version.
|
|
236
|
+
function sealNewToken(ip, version) {
|
|
237
|
+
var plain = Buffer.alloc(8);
|
|
238
|
+
plain[0] = TOKEN_NEW;
|
|
239
|
+
plain.writeUIntBE(Date.now(), 2, 6);
|
|
240
|
+
return seal(plain, ip, version);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function seal(plain, ip, version) {
|
|
244
|
+
var nonce = crypto.randomBytes(12);
|
|
245
|
+
var cipher = crypto.createCipheriv('aes-256-gcm', retryTokenKey, nonce);
|
|
246
|
+
cipher.setAAD(tokenAad(ip, version));
|
|
247
|
+
var sealed = Buffer.concat([nonce, cipher.update(plain), cipher.final(), cipher.getAuthTag()]);
|
|
248
|
+
return new Uint8Array(sealed);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// A token this server sealed, for this address and version:
|
|
252
|
+
// { kind: 'retry', originalDcid, sourceCid } — a Retry's, for the ID the
|
|
253
|
+
// packet is addressed to, and still fresh;
|
|
254
|
+
// { kind: 'new' } — a NEW_TOKEN's, still within its lifetime.
|
|
255
|
+
// null for a token that is not ours (it does not open), or an expired
|
|
256
|
+
// NEW_TOKEN one — treated as no token at all (§8.1.3: the client is then
|
|
257
|
+
// as one without a validated address); false for a Retry token of ours
|
|
258
|
+
// that fails the rest — §8.1.2: the packet is discarded, and the client,
|
|
259
|
+
// which accepts only one Retry, times out.
|
|
260
|
+
function openToken(token, ip, dcid, version) {
|
|
261
|
+
if (token.byteLength < 12 + 8 + 16) return null;
|
|
262
|
+
var t = Buffer.from(token.buffer, token.byteOffset, token.byteLength);
|
|
263
|
+
var plain;
|
|
264
|
+
try {
|
|
265
|
+
var decipher = crypto.createDecipheriv('aes-256-gcm', retryTokenKey, t.subarray(0, 12));
|
|
266
|
+
decipher.setAAD(tokenAad(ip, version));
|
|
267
|
+
decipher.setAuthTag(t.subarray(t.length - 16));
|
|
268
|
+
plain = Buffer.concat([decipher.update(t.subarray(12, t.length - 16)), decipher.final()]);
|
|
269
|
+
} catch (e) {
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
var age = Date.now() - plain.readUIntBE(2, 6);
|
|
273
|
+
if (plain[0] === TOKEN_NEW && plain.length === 8) {
|
|
274
|
+
return (age >= 0 && age <= NEW_TOKEN_LIFETIME) ? { kind: 'new' } : null;
|
|
275
|
+
}
|
|
276
|
+
if (plain[0] !== TOKEN_RETRY) return null;
|
|
277
|
+
if (age < 0 || age > RETRY_TOKEN_LIFETIME) return false;
|
|
278
|
+
var odcidLen = plain[8];
|
|
279
|
+
if (plain.length !== 9 + odcidLen + SERVER_CID_LEN) return false;
|
|
280
|
+
var sourceCid = new Uint8Array(plain.subarray(9 + odcidLen));
|
|
281
|
+
if (cidKey(sourceCid) !== cidKey(dcid)) return false;
|
|
282
|
+
return { kind: 'retry', originalDcid: new Uint8Array(plain.subarray(9, 9 + odcidLen)), sourceCid: sourceCid };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// The Retry: from a connection ID the client is to use next — the one its
|
|
286
|
+
// connection will have — carrying the token.
|
|
287
|
+
function sendRetry(pkt, toIp, toPort) {
|
|
288
|
+
var sourceCid = newLocalCid();
|
|
289
|
+
var token = sealRetryToken(pkt.dcid, sourceCid, toIp, pkt.version);
|
|
290
|
+
if (DEBUG) console.log('[qserver] Retry to ' + toIp + ':' + toPort);
|
|
291
|
+
// In the version of the client's Initial (RFC 9369 §4.1).
|
|
292
|
+
sendUdp(toIp, toPort, build_retry_packet(pkt.scid, sourceCid, token, pkt.dcid, pkt.version));
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function resetToken(cid) {
|
|
296
|
+
return new Uint8Array(crypto.createHmac('sha256', resetTokenSecret).update(cid).digest().subarray(0, 16));
|
|
297
|
+
}
|
|
298
|
+
|
|
85
299
|
// Build SNICallback
|
|
86
300
|
var sniCallback = options.SNICallback || null;
|
|
87
301
|
|
|
88
302
|
if (!sniCallback && options.key && options.cert) {
|
|
89
|
-
var defaultCtx = createSecureContext({ key: options.key, cert: options.cert, ca: options.ca });
|
|
303
|
+
var defaultCtx = createSecureContext({ key: options.key, cert: options.cert, ca: options.ca, passphrase: tlsOptions.passphrase });
|
|
304
|
+
// Its chain compressed now, in the background, for the first client's
|
|
305
|
+
// handshake (see tcp_listener; the cache is shared, so one server's two
|
|
306
|
+
// stacks compress it once).
|
|
307
|
+
if (!(tlsOptions.certificateCompression && tlsOptions.certificateCompression.length === 0))
|
|
308
|
+
lemonTls.precompressCertificate(defaultCtx, tlsOptions.certificateCompression || undefined);
|
|
90
309
|
sniCallback = function (servername, cb) { cb(null, defaultCtx); };
|
|
91
310
|
}
|
|
92
311
|
|
|
93
312
|
// ---- Connection tracking ----
|
|
94
|
-
|
|
95
|
-
|
|
313
|
+
//
|
|
314
|
+
// A connection is found by the connection ID a datagram is addressed to
|
|
315
|
+
// (RFC 9000 §5.2), never by where it came from: a phone behind a NAT that
|
|
316
|
+
// changes its port keeps sending to the same ID, and two clients behind one
|
|
317
|
+
// address never share a connection.
|
|
318
|
+
//
|
|
319
|
+
// Each connection is addressed by an ID this server chose: SERVER_CID_LEN
|
|
320
|
+
// random bytes. One length for all of them is what makes routing possible
|
|
321
|
+
// at all — a short header carries its DCID without a length, and only by
|
|
322
|
+
// knowing it can the router read it. The client's own choice, the DCID of
|
|
323
|
+
// its first Initial, also leads to the connection: until our first Initial
|
|
324
|
+
// reaches it, a client has no other (§7.2), and a retransmitted Initial
|
|
325
|
+
// must find the connection its first one made.
|
|
326
|
+
//
|
|
327
|
+
// connections our ID (hex) → { quic, peer, cids, addressKey }
|
|
328
|
+
// cidMap every ID that leads to a connection (hex) → our ID (hex)
|
|
329
|
+
// addressMap "ip:port" → our ID — not for routing: for hasConnection(),
|
|
330
|
+
// which a demultiplexer sharing the port asks by address.
|
|
331
|
+
var SERVER_CID_LEN = 8;
|
|
332
|
+
var connections = {};
|
|
333
|
+
var cidMap = {};
|
|
334
|
+
var addressMap = {};
|
|
335
|
+
var connectionCount = 0;
|
|
96
336
|
var maxConnections = options.maxConnections || 10000;
|
|
337
|
+
|
|
338
|
+
// ── Handshakes under way ────────────────────────────────────────────────
|
|
339
|
+
//
|
|
340
|
+
// A connection counts from when it is made until the client's Finished is
|
|
341
|
+
// verified (or it goes). What load is, and what to do about it — Retry
|
|
342
|
+
// for every new client (setValidateAddress), refusing work, scaling out —
|
|
343
|
+
// is the application's call: it is told (getPendingHandshakes, and
|
|
344
|
+
// 'handshakeLoad' at handshakeLoadThreshold, again when the count is back
|
|
345
|
+
// under half of it). The cap per address is not a judgement of load:
|
|
346
|
+
// past maxHandshakesPerAddress, an Initial from that address makes no
|
|
347
|
+
// connection — the one place an address with many spoofed or real
|
|
348
|
+
// attempts could otherwise fill maxConnections. High enough for many
|
|
349
|
+
// clients behind one address (a carrier NAT), each handshake lasting a
|
|
350
|
+
// round trip or two. All three can be changed while running.
|
|
351
|
+
var pendingHandshakes = 0;
|
|
352
|
+
var pendingByAddress = new Map(); // IP → handshakes under way from it
|
|
353
|
+
var handshakeLoadHigh = false;
|
|
354
|
+
var handshakeLoadThreshold = handshakeCount(options.handshakeLoadThreshold, 500, 'handshakeLoadThreshold');
|
|
355
|
+
var maxHandshakesPerAddress = handshakeCount(options.maxHandshakesPerAddress, 64, 'maxHandshakesPerAddress');
|
|
356
|
+
|
|
357
|
+
function handshakeCount(v, dflt, name) {
|
|
358
|
+
if (v === undefined || v === null) return dflt;
|
|
359
|
+
if (typeof v !== 'number' || !Number.isInteger(v) || v < 0) {
|
|
360
|
+
var e = new RangeError(name + ' must be a whole number ≥ 0 (0: off), got ' + v); e.code = 'ERR_OUT_OF_RANGE'; throw e;
|
|
361
|
+
}
|
|
362
|
+
return v;
|
|
363
|
+
}
|
|
364
|
+
// An IPv4 client on a dual-stack socket is one address either way.
|
|
365
|
+
function addressOf(ip) {
|
|
366
|
+
return ip && ip.indexOf('::ffff:') === 0 && ip.indexOf('.') !== -1 ? ip.substring(7) : ip;
|
|
367
|
+
}
|
|
368
|
+
function handshakeStarted(entry, ip) {
|
|
369
|
+
entry.pendingIp = addressOf(ip);
|
|
370
|
+
pendingHandshakes++;
|
|
371
|
+
pendingByAddress.set(entry.pendingIp, (pendingByAddress.get(entry.pendingIp) || 0) + 1);
|
|
372
|
+
checkHandshakeLoad();
|
|
373
|
+
}
|
|
374
|
+
// Done, or gone — once.
|
|
375
|
+
function handshakeEnded(entry) {
|
|
376
|
+
if (entry.pendingIp === undefined) return;
|
|
377
|
+
var n = (pendingByAddress.get(entry.pendingIp) || 1) - 1;
|
|
378
|
+
if (n > 0) pendingByAddress.set(entry.pendingIp, n); else pendingByAddress.delete(entry.pendingIp);
|
|
379
|
+
entry.pendingIp = undefined;
|
|
380
|
+
pendingHandshakes--;
|
|
381
|
+
checkHandshakeLoad();
|
|
382
|
+
}
|
|
383
|
+
function checkHandshakeLoad() {
|
|
384
|
+
var t = handshakeLoadThreshold;
|
|
385
|
+
if (!handshakeLoadHigh && t > 0 && pendingHandshakes >= t) {
|
|
386
|
+
handshakeLoadHigh = true;
|
|
387
|
+
ev.emit('handshakeLoad', { pending: pendingHandshakes, high: true });
|
|
388
|
+
} else if (handshakeLoadHigh && (t === 0 || pendingHandshakes < t / 2)) {
|
|
389
|
+
handshakeLoadHigh = false;
|
|
390
|
+
ev.emit('handshakeLoad', { pending: pendingHandshakes, high: false });
|
|
391
|
+
}
|
|
392
|
+
}
|
|
97
393
|
var sweepTimer = null;
|
|
98
394
|
|
|
395
|
+
function cidKey(cid) {
|
|
396
|
+
return Buffer.from(cid.buffer, cid.byteOffset, cid.byteLength).toString('hex');
|
|
397
|
+
}
|
|
398
|
+
|
|
99
399
|
|
|
100
400
|
// ============================================================
|
|
101
401
|
// UDP → connection routing
|
|
102
402
|
// ============================================================
|
|
103
403
|
|
|
104
404
|
function onUdpMessage(msg, rinfo) {
|
|
405
|
+
// Closed (or closing, its last datagrams leaving): nothing more is taken.
|
|
406
|
+
if (shutDown) return;
|
|
105
407
|
var data = new Uint8Array(msg);
|
|
106
408
|
var fromIp = rinfo.address;
|
|
107
409
|
var fromPort = rinfo.port;
|
|
108
410
|
if (DEBUG) console.log('[qserver] UDP from ' + fromIp + ':' + fromPort + ' len=' + data.length);
|
|
109
411
|
|
|
110
|
-
var
|
|
111
|
-
var packets = parse_quic_datagram(data);
|
|
412
|
+
var packets = parse_quic_datagram(data, SERVER_CID_LEN);
|
|
112
413
|
if (packets.length === 0) return;
|
|
113
414
|
|
|
114
|
-
//
|
|
115
|
-
//
|
|
116
|
-
//
|
|
117
|
-
//
|
|
118
|
-
//
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
415
|
+
// ── Version Negotiation (RFC 9000 §6) ──
|
|
416
|
+
// Must happen before any connection is created. A long-header packet
|
|
417
|
+
// carrying a version we don't speak gets a Version Negotiation packet in
|
|
418
|
+
// reply — that is how a client discovers what we support, and how a prober
|
|
419
|
+
// checks we are alive without completing a handshake.
|
|
420
|
+
//
|
|
421
|
+
// Getting this wrong is silent and confusing: without it, the router
|
|
422
|
+
// below happily creates a connection for the unknown-version Initial, maps
|
|
423
|
+
// the address to it, and every subsequent probe is routed into a
|
|
424
|
+
// connection that cannot do anything with it. The peer sees no reply at
|
|
425
|
+
// all and concludes the server is down.
|
|
426
|
+
var firstPkt = packets[0];
|
|
427
|
+
if (firstPkt.form === 'long' &&
|
|
428
|
+
firstPkt.type !== 'version_negotiation' &&
|
|
429
|
+
quicVersions.indexOf(firstPkt.version) < 0) {
|
|
430
|
+
|
|
431
|
+
// Anti-amplification (RFC 9000 §14.1): only answer datagrams that are
|
|
432
|
+
// at least 1200 bytes, so we can't be used to reflect traffic at a
|
|
433
|
+
// spoofed source. A genuine Initial is always padded to that size.
|
|
434
|
+
if (data.length < 1200) {
|
|
435
|
+
if (DEBUG) console.log('[qserver] unsupported version 0x' +
|
|
436
|
+
firstPkt.version.toString(16) + ' in a short datagram (' +
|
|
437
|
+
data.length + 'B) — dropping without VN');
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
var vn = build_version_negotiation(firstPkt.dcid, firstPkt.scid, quicVersions);
|
|
442
|
+
if (DEBUG) console.log('[qserver] version 0x' +
|
|
443
|
+
firstPkt.version.toString(16) + ' unsupported — sending Version Negotiation');
|
|
444
|
+
sendUdp(fromIp, fromPort, vn);
|
|
445
|
+
return;
|
|
126
446
|
}
|
|
127
447
|
|
|
128
|
-
|
|
129
|
-
|
|
448
|
+
// RFC 9000 §14.1: "A server MUST discard an Initial packet that is
|
|
449
|
+
// carried in a UDP datagram with a payload that is smaller than the
|
|
450
|
+
// smallest allowed maximum datagram size of 1200 bytes." A real client
|
|
451
|
+
// pads every datagram carrying an Initial to that size. One that does not
|
|
452
|
+
// costs an attacker little and would buy a connection's state, a key
|
|
453
|
+
// exchange and a signature — and, with the address unvalidated, three
|
|
454
|
+
// times its own size in replies. Checked before any connection exists.
|
|
455
|
+
for (var pi = 0; pi < packets.length; pi++) {
|
|
456
|
+
if (packets[pi].type === 'initial' && data.length < 1200) {
|
|
457
|
+
if (DEBUG) console.log('[qserver] Initial in a ' + data.length + 'B datagram — dropped (min 1200)');
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// The whole datagram goes where its first packet is addressed, parsed
|
|
463
|
+
// once, here: the connection gets the packets, not the bytes. §12.2:
|
|
464
|
+
// "Senders MUST NOT coalesce QUIC packets with different connection IDs
|
|
465
|
+
// into a single UDP datagram. Receivers SHOULD ignore any subsequent
|
|
466
|
+
// packets with a different Destination Connection ID than the first."
|
|
467
|
+
var dcidKey = cidKey(firstPkt.dcid);
|
|
468
|
+
var entry = (dcidKey in cidMap) ? connections[cidMap[dcidKey]] : null;
|
|
130
469
|
|
|
470
|
+
if (!entry) {
|
|
471
|
+
// Only an Initial may start a connection. Anything else addressed to
|
|
472
|
+
// an ID we do not know — a packet for a connection already gone, or
|
|
473
|
+
// one that never was — is not given a connection of its own. A 1-RTT
|
|
474
|
+
// one is answered with a Stateless Reset; the rest are dropped.
|
|
475
|
+
if (firstPkt.form === 'short') { sendStatelessReset(firstPkt.dcid, data.length, fromIp, fromPort); return; }
|
|
476
|
+
if (firstPkt.type !== 'initial') return;
|
|
477
|
+
// §7.2: the DCID of a client's first Initial "MUST be at least 8 bytes
|
|
478
|
+
// in length". Shorter ones would also be easy to guess and collide.
|
|
479
|
+
if (firstPkt.dcid.byteLength < 8) return;
|
|
480
|
+
if (connectionCount >= maxConnections) return;
|
|
131
481
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
482
|
+
var retry = null, knownAddress = false;
|
|
483
|
+
if (firstPkt.token && firstPkt.token.byteLength > 0) {
|
|
484
|
+
var tok = openToken(firstPkt.token, fromIp, firstPkt.dcid, firstPkt.version);
|
|
485
|
+
if (tok === false) return;
|
|
486
|
+
if (tok && tok.kind === 'retry') retry = tok;
|
|
487
|
+
else if (tok && tok.kind === 'new') knownAddress = true;
|
|
488
|
+
}
|
|
489
|
+
if (!retry && !knownAddress && validateAddress) { sendRetry(firstPkt, fromIp, fromPort); return; }
|
|
490
|
+
// Past the cap for its address: no connection (see "Handshakes under
|
|
491
|
+
// way"). After the Retry: a spoofed address never comes back with a
|
|
492
|
+
// token, and this count is of addresses that did.
|
|
493
|
+
if (maxHandshakesPerAddress > 0 && (pendingByAddress.get(addressOf(fromIp)) || 0) >= maxHandshakesPerAddress) return;
|
|
494
|
+
|
|
495
|
+
entry = createConnection(firstPkt.dcid, fromIp, fromPort, retry, firstPkt.version, knownAddress);
|
|
496
|
+
if (!entry) return;
|
|
136
497
|
}
|
|
137
498
|
|
|
138
|
-
|
|
139
|
-
|
|
499
|
+
for (var ci = 1; ci < packets.length; ci++) {
|
|
500
|
+
if (cidKey(packets[ci].dcid) !== dcidKey) { packets.length = ci; break; }
|
|
501
|
+
}
|
|
140
502
|
|
|
141
|
-
//
|
|
142
|
-
//
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
if (pkt.type !== 'initial') return null;
|
|
503
|
+
// The datagram's size goes with it: the connection charges it to the
|
|
504
|
+
// anti-amplification budget (RFC 9000 §8.1).
|
|
505
|
+
var isNew = !entry.started;
|
|
506
|
+
entry.quic.feedPackets(fromIp, fromPort, packets, data.length);
|
|
146
507
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
508
|
+
// A connection is kept only if its first datagram began a handshake (an
|
|
509
|
+
// Initial CRYPTO frame that decrypted). One that did not — a PING, junk
|
|
510
|
+
// under a random DCID, from an address nobody has proven — cost a
|
|
511
|
+
// connection's state and a place under maxConnections; enough of them,
|
|
512
|
+
// spoofed, kept every real client out (QUICtester found 10 of 19 servers
|
|
513
|
+
// doing this). It goes at once, unanswered.
|
|
514
|
+
if (isNew) {
|
|
515
|
+
if (!entry.quic.handshakeStarted) { entry.quic.discard(); dropConnection(entry.connId); return; }
|
|
516
|
+
entry.started = true;
|
|
517
|
+
}
|
|
150
518
|
}
|
|
151
519
|
|
|
152
520
|
|
|
153
|
-
|
|
154
|
-
|
|
521
|
+
/**
|
|
522
|
+
* Who this connection is, for an SNICallback that wants to know — the QUIC
|
|
523
|
+
* counterpart of connectionInfo() in tcp_listener, with the same fields.
|
|
524
|
+
*
|
|
525
|
+
* A client dialling a bare IP address sends no SNI (RFC 6066 §3 forbids an
|
|
526
|
+
* IP literal there), so the callback's only way to choose a certificate is
|
|
527
|
+
* this third argument. Without it, a browser that loaded https://<ip> over
|
|
528
|
+
* TCP, saw Alt-Svc and moved to QUIC, had its handshake refused and reported
|
|
529
|
+
* ERR_QUIC_PROTOCOL_ERROR — HTTP/3 simply did not work by IP.
|
|
530
|
+
*
|
|
531
|
+
* localAddress is null, and that is not an omission. A UDP socket bound to
|
|
532
|
+
* a wildcard address cannot tell which of the host's addresses a datagram
|
|
533
|
+
* was sent to — Node exposes no IP_PKTINFO — so the honest answer is
|
|
534
|
+
* "unknown", and the family the client used is what is reported instead.
|
|
535
|
+
*/
|
|
536
|
+
function connectionInfo(fromIp, fromPort) {
|
|
537
|
+
var remote = fromIp;
|
|
538
|
+
var family = remote && remote.indexOf(':') !== -1 ? 'IPv6' : 'IPv4';
|
|
539
|
+
// An IPv4 peer arriving on a dual-stack IPv6 socket.
|
|
540
|
+
if (remote && remote.indexOf('::ffff:') === 0 && /^\d+\.\d+\.\d+\.\d+$/.test(remote.substring(7))) {
|
|
541
|
+
remote = remote.substring(7);
|
|
542
|
+
family = 'IPv4';
|
|
543
|
+
}
|
|
544
|
+
return {
|
|
545
|
+
transport: 'quic',
|
|
546
|
+
localAddress: null,
|
|
547
|
+
localPort: port || null,
|
|
548
|
+
family: family,
|
|
549
|
+
remoteAddress: remote,
|
|
550
|
+
remotePort: fromPort
|
|
551
|
+
};
|
|
552
|
+
}
|
|
155
553
|
|
|
156
|
-
|
|
554
|
+
// retry: { originalDcid, sourceCid } when the client came back with a valid
|
|
555
|
+
// Retry token — its address is proven, and the connection keeps the ID the
|
|
556
|
+
// Retry gave it.
|
|
557
|
+
// knownAddress: the client came with a NEW_TOKEN token of ours — its
|
|
558
|
+
// address is proven, as by a Retry, but the connection keeps its IDs.
|
|
559
|
+
function createConnection(originalDcid, fromIp, fromPort, retry, version, knownAddress) {
|
|
560
|
+
// Bound per connection, with its details as a third argument — after cb,
|
|
561
|
+
// so a (servername, cb) callback keeps working and never reads it. Only a
|
|
562
|
+
// callback the application supplied gets it; the built-in one for a fixed
|
|
563
|
+
// key/cert has no use for it.
|
|
564
|
+
var connSni = options.SNICallback
|
|
565
|
+
? (function (info) {
|
|
566
|
+
return function (servername, cb) { return options.SNICallback(servername, cb, info); };
|
|
567
|
+
})(connectionInfo(fromIp, fromPort))
|
|
568
|
+
: sniCallback;
|
|
569
|
+
|
|
570
|
+
// Our ID for the connection: the one our Retry gave the client, or a new
|
|
571
|
+
// one. (After a Retry the client addresses nothing else, so its first
|
|
572
|
+
// DCID leads nowhere and is not mapped.)
|
|
573
|
+
var localCid = retry ? retry.sourceCid : newLocalCid();
|
|
574
|
+
var connId = cidKey(localCid);
|
|
575
|
+
if (connId in cidMap) return null; // a Retry ID taken meanwhile — 2^-64
|
|
576
|
+
|
|
577
|
+
// The peer's address goes in with it: the connection keeps it — and the
|
|
578
|
+
// address it moves to, if the client's NAT changes it (RFC 9000 §9.3) —
|
|
579
|
+
// as quic.remoteAddress / remotePort, where anything that keys on the
|
|
580
|
+
// peer (a rate limiter, a log line, an IP screen) reads it. Over TCP that
|
|
581
|
+
// comes free from the socket; QUIC has no socket per connection.
|
|
582
|
+
var quic = new QUICConnection({
|
|
583
|
+
SNICallback: connSni, alpn: alpn, ticketKeys: ticketKeys, ticketData: ticketData, ech: ech, ocspRequest: ocspRequest, tlsOptions: tlsOptions,
|
|
584
|
+
// The CAs a client certificate is checked against (requestCert).
|
|
585
|
+
ca: tlsOptions.requestCert ? (options.ca || null) : null,
|
|
586
|
+
localCid: localCid, statelessResetToken: resetToken(localCid), retry: retry,
|
|
587
|
+
remoteAddress: fromIp, remotePort: fromPort, transportParams: transportParams,
|
|
588
|
+
initialCongestionWindow: connDefaults.initialCongestionWindow,
|
|
589
|
+
carefulResume: connDefaults.carefulResume,
|
|
590
|
+
startupCap: connDefaults.startupCap,
|
|
591
|
+
pmtuDiscovery: connDefaults.pmtuDiscovery,
|
|
592
|
+
maxUdpPayloadSize: connDefaults.maxUdpPayloadSize,
|
|
593
|
+
initialRtt: connDefaults.initialRtt,
|
|
594
|
+
reorderWindow: connDefaults.reorderWindow,
|
|
595
|
+
earlyData: earlyData, earlyDataMaxAge: earlyDataMaxAge, earlyDataReplay: earlyDataReplay,
|
|
596
|
+
// The application protocol's part of the early-data context (h3_server: its SETTINGS).
|
|
597
|
+
earlyDataAppContext: typeof options.earlyDataAppContext === 'string' ? options.earlyDataAppContext : undefined,
|
|
598
|
+
connectionOptions: connectionOptions,
|
|
599
|
+
// The version of the client's first Initial, and the ones accepted.
|
|
600
|
+
version: version, quicVersions: quicVersions,
|
|
601
|
+
addressValidated: !!knownAddress,
|
|
602
|
+
// A token for the client's next connection (NEW_TOKEN, RFC 9000
|
|
603
|
+
// §8.1.3), for the address it is at then, in the connection's version.
|
|
604
|
+
newToken: function (ip, v) { return new Uint8Array(sealNewToken(ip, v)); },
|
|
605
|
+
// More connection IDs for the connection (NEW_CONNECTION_ID, for
|
|
606
|
+
// migration, RFC 9000 §9.5): each routed here from when it is made,
|
|
607
|
+
// with its stateless reset token, until the client retires it.
|
|
608
|
+
issueConnectionId: function () {
|
|
609
|
+
var cid = newLocalCid();
|
|
610
|
+
var k = cidKey(cid);
|
|
611
|
+
if (k in cidMap) return null;
|
|
612
|
+
cidMap[k] = connId;
|
|
613
|
+
entry.cids.push(k);
|
|
614
|
+
return { cid: cid, token: resetToken(cid) };
|
|
615
|
+
},
|
|
616
|
+
retireConnectionId: function (cid) {
|
|
617
|
+
var k = cidKey(cid);
|
|
618
|
+
if (cidMap[k] === connId) delete cidMap[k];
|
|
619
|
+
var i = entry.cids.indexOf(k);
|
|
620
|
+
if (i >= 0) entry.cids.splice(i, 1);
|
|
621
|
+
},
|
|
622
|
+
greaseQuicBit: greaseQuicBit,
|
|
623
|
+
stats: stats
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
var addressKey = fromIp + ':' + fromPort;
|
|
627
|
+
var entry = {
|
|
157
628
|
quic: quic,
|
|
158
|
-
peer: { address: fromIp, port: fromPort }
|
|
629
|
+
peer: { address: fromIp, port: fromPort },
|
|
630
|
+
cids: retry ? [connId] : [connId, cidKey(originalDcid)],
|
|
631
|
+
addressKey: addressKey,
|
|
632
|
+
connId: connId,
|
|
633
|
+
started: false // its first datagram began a handshake (see onUdpMessage)
|
|
159
634
|
};
|
|
160
|
-
|
|
635
|
+
connections[connId] = entry;
|
|
636
|
+
connectionCount++;
|
|
637
|
+
handshakeStarted(entry, fromIp);
|
|
638
|
+
quic.on('handshake_ended', function (confirmed) {
|
|
639
|
+
handshakeEnded(entry);
|
|
640
|
+
if (!stats) return;
|
|
641
|
+
stats.handshake('quic', confirmed, confirmed && quic.isSessionReused && quic.isSessionReused());
|
|
642
|
+
if (confirmed) stats.protocol(quic.alpnProtocol);
|
|
643
|
+
});
|
|
644
|
+
if (stats) {
|
|
645
|
+
stats.connections.quic++;
|
|
646
|
+
// How it ended: the CONNECTION_CLOSE either way is counted by the
|
|
647
|
+
// connection as it goes or comes; one that ended with neither, here.
|
|
648
|
+
quic.on('close', function () {
|
|
649
|
+
if (!quic.context.peer_close && !quic.context.close_frame) stats.closes.quic.silent++;
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
for (var i = 0; i < entry.cids.length; i++) cidMap[entry.cids[i]] = connId;
|
|
161
653
|
addressMap[addressKey] = connId;
|
|
162
654
|
|
|
163
|
-
//
|
|
164
|
-
|
|
165
|
-
|
|
655
|
+
// Each packet goes where the connection says — its current path, or the
|
|
656
|
+
// address a PATH_CHALLENGE or PATH_RESPONSE is for.
|
|
657
|
+
quic.on('packet', function (data, address, port) {
|
|
658
|
+
sendUdp(address, port, data);
|
|
166
659
|
});
|
|
167
660
|
|
|
168
|
-
//
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
661
|
+
// The connection now sends to another address. Routing is by connection
|
|
662
|
+
// ID and needs nothing; what follows the address is hasConnection(), and
|
|
663
|
+
// the peer object the 'connection' event handed out.
|
|
664
|
+
//
|
|
665
|
+
// The peer object is NOT moved with it: it is what the application keys
|
|
666
|
+
// on — rate limits, access rules, logs — and an address is given to it
|
|
667
|
+
// only once the client has proven it receives there (path_validated).
|
|
668
|
+
// A packet with a rewritten source address moved it at once.
|
|
669
|
+
quic.on('path', function (address, port) {
|
|
670
|
+
if (addressMap[entry.addressKey] === connId) delete addressMap[entry.addressKey];
|
|
671
|
+
entry.addressKey = address + ':' + port;
|
|
672
|
+
addressMap[entry.addressKey] = connId;
|
|
673
|
+
entry.peer.address = quic.remoteAddress;
|
|
674
|
+
entry.peer.port = quic.remotePort;
|
|
675
|
+
});
|
|
676
|
+
quic.on('path_validated', function () {
|
|
677
|
+
entry.peer.address = quic.remoteAddress;
|
|
678
|
+
entry.peer.port = quic.remotePort;
|
|
172
679
|
});
|
|
173
680
|
|
|
681
|
+
// Drop our bookkeeping when the connection closes.
|
|
682
|
+
quic.on('close', function () { dropConnection(connId); });
|
|
683
|
+
|
|
174
684
|
// Hand the raw connection to the application layer (H3, DoQ, ...).
|
|
175
|
-
ev.emit('connection', quic,
|
|
685
|
+
ev.emit('connection', quic, entry.peer);
|
|
686
|
+
return entry;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// A connection ID of ours: 64 random bits — unguessable, which a
|
|
690
|
+
// connection relies on to take its use by the client as proof of the
|
|
691
|
+
// client's address (§8.1) — and not in use already, which the check makes
|
|
692
|
+
// certain rather than merely likely.
|
|
693
|
+
function newLocalCid() {
|
|
694
|
+
var cid;
|
|
695
|
+
do { cid = new Uint8Array(crypto.randomBytes(SERVER_CID_LEN)); } while (cidKey(cid) in cidMap);
|
|
696
|
+
return cid;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
// The one place a connection leaves the maps: every ID that led to it, and
|
|
700
|
+
// its address unless a newer connection has taken that over.
|
|
701
|
+
function dropConnection(connId) {
|
|
702
|
+
var entry = connections[connId];
|
|
703
|
+
if (!entry) return;
|
|
704
|
+
for (var i = 0; i < entry.cids.length; i++) {
|
|
705
|
+
if (cidMap[entry.cids[i]] === connId) delete cidMap[entry.cids[i]];
|
|
706
|
+
}
|
|
707
|
+
if (addressMap[entry.addressKey] === connId) delete addressMap[entry.addressKey];
|
|
708
|
+
delete connections[connId];
|
|
709
|
+
connectionCount--;
|
|
710
|
+
handshakeEnded(entry);
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
|
|
714
|
+
// ── setOptions / getOptions ────────────────────────────────────────────
|
|
715
|
+
//
|
|
716
|
+
// One call for every option that can change while the server runs, by
|
|
717
|
+
// the names createServer takes: the server's own (validateAddress,
|
|
718
|
+
// handshakeLoadThreshold, maxHandshakesPerAddress — from the next client
|
|
719
|
+
// on, the threshold at once) and what new connections are made with
|
|
720
|
+
// (CONNECTION_OPTIONS — connections already open keep theirs). Checked
|
|
721
|
+
// whole before anything changes: one bad value, and nothing does. A name
|
|
722
|
+
// that cannot change this way says what can.
|
|
723
|
+
var SERVER_RUNTIME = ['validateAddress', 'handshakeLoadThreshold', 'maxHandshakesPerAddress', 'earlyData', 'earlyDataMaxAge'];
|
|
724
|
+
var ELSEWHERE = {
|
|
725
|
+
key: 'setSecureContext()', cert: 'setSecureContext()', ca: 'setSecureContext()', pfx: 'setSecureContext()',
|
|
726
|
+
passphrase: 'setSecureContext()', SNICallback: 'setSecureContext()',
|
|
727
|
+
ticketKeys: 'setTicketKeys()', ech: 'setEchKeys()', resetTokenSecret: 'setResetTokenSecret()',
|
|
728
|
+
transportParams: 'connectionOptions, per connection'
|
|
729
|
+
};
|
|
730
|
+
function optErr(Ctor, code, msg) { var e = new Ctor(msg); e.code = code; return e; }
|
|
731
|
+
function setOptions(o) {
|
|
732
|
+
if (!o || typeof o !== 'object') throw optErr(TypeError, 'ERR_INVALID_ARG_TYPE', 'setOptions() takes an object');
|
|
733
|
+
var conn = {}, srv = {};
|
|
734
|
+
for (var k in o) {
|
|
735
|
+
if (o[k] === undefined) continue;
|
|
736
|
+
if (k in CONNECTION_OPTIONS) conn[k] = o[k];
|
|
737
|
+
else if (SERVER_RUNTIME.indexOf(k) !== -1) srv[k] = o[k];
|
|
738
|
+
else if (k in ELSEWHERE) throw optErr(TypeError, 'ERR_INVALID_ARG_VALUE', k + ' cannot be changed with setOptions(): use ' + ELSEWHERE[k]);
|
|
739
|
+
else throw optErr(TypeError, 'ERR_INVALID_ARG_VALUE', k + ' cannot be changed while the server runs');
|
|
740
|
+
}
|
|
741
|
+
conn = checkConnectionOptions(conn, 'setOptions');
|
|
742
|
+
if ('validateAddress' in srv && typeof srv.validateAddress !== 'boolean') throw optErr(TypeError, 'ERR_INVALID_ARG_TYPE', 'validateAddress must be a boolean, got ' + srv.validateAddress);
|
|
743
|
+
if ('handshakeLoadThreshold' in srv) srv.handshakeLoadThreshold = handshakeCount(srv.handshakeLoadThreshold, 500, 'handshakeLoadThreshold');
|
|
744
|
+
if ('maxHandshakesPerAddress' in srv) srv.maxHandshakesPerAddress = handshakeCount(srv.maxHandshakesPerAddress, 64, 'maxHandshakesPerAddress');
|
|
745
|
+
if ('earlyData' in srv && typeof srv.earlyData !== 'boolean') throw optErr(TypeError, 'ERR_INVALID_ARG_TYPE', 'earlyData must be a boolean, got ' + srv.earlyData);
|
|
746
|
+
if ('earlyDataMaxAge' in srv) srv.earlyDataMaxAge = earlyMaxAge(srv.earlyDataMaxAge);
|
|
747
|
+
// All good: applied.
|
|
748
|
+
Object.assign(connDefaults, conn);
|
|
749
|
+
if ('validateAddress' in srv) validateAddress = srv.validateAddress;
|
|
750
|
+
if ('earlyData' in srv) earlyData = srv.earlyData;
|
|
751
|
+
if ('earlyDataMaxAge' in srv) earlyDataMaxAge = srv.earlyDataMaxAge;
|
|
752
|
+
if ('maxHandshakesPerAddress' in srv) maxHandshakesPerAddress = srv.maxHandshakesPerAddress;
|
|
753
|
+
if ('handshakeLoadThreshold' in srv) { handshakeLoadThreshold = srv.handshakeLoadThreshold; checkHandshakeLoad(); }
|
|
754
|
+
}
|
|
755
|
+
function getOptions() {
|
|
756
|
+
return Object.assign({
|
|
757
|
+
validateAddress: validateAddress,
|
|
758
|
+
earlyData: earlyData,
|
|
759
|
+
earlyDataMaxAge: earlyDataMaxAge,
|
|
760
|
+
handshakeLoadThreshold: handshakeLoadThreshold,
|
|
761
|
+
maxHandshakesPerAddress: maxHandshakesPerAddress
|
|
762
|
+
}, connDefaults);
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// ============================================================
|
|
766
|
+
// Stateless Reset (RFC 9000 §10.3)
|
|
767
|
+
// ============================================================
|
|
768
|
+
|
|
769
|
+
// At most this many a second, all addresses together (§10.3.3 suggests a
|
|
770
|
+
// limit). Answering every packet for an unknown ID without one would let
|
|
771
|
+
// anyone who sends junk make this server answer each of them; a real
|
|
772
|
+
// client needs one reset, and gets more than enough within this.
|
|
773
|
+
var RESET_RATE = 100;
|
|
774
|
+
var resetBudget = RESET_RATE;
|
|
775
|
+
var resetBudgetTime = Date.now();
|
|
776
|
+
|
|
777
|
+
function sendStatelessReset(dcid, receivedBytes, toIp, toPort) {
|
|
778
|
+
// §10.3.3: every reset smaller than the packet that prompted it — two
|
|
779
|
+
// endpoints that both reset what they cannot place would otherwise
|
|
780
|
+
// answer each other forever — and at least 21 bytes, the smallest a
|
|
781
|
+
// short-header packet can be (§10.3). Up to 43 bytes, one byte shorter,
|
|
782
|
+
// as §10.3 advises; above that, a varying size, so the length alone does
|
|
783
|
+
// not mark it.
|
|
784
|
+
if (!dcid || receivedBytes <= 21) return;
|
|
785
|
+
|
|
786
|
+
var now = Date.now();
|
|
787
|
+
resetBudget = Math.min(RESET_RATE, resetBudget + (now - resetBudgetTime) * RESET_RATE / 1000);
|
|
788
|
+
resetBudgetTime = now;
|
|
789
|
+
if (resetBudget < 1) return;
|
|
790
|
+
resetBudget -= 1;
|
|
791
|
+
|
|
792
|
+
var random = crypto.randomBytes(receivedBytes);
|
|
793
|
+
var size = receivedBytes <= 43 ? receivedBytes - 1 : Math.min(receivedBytes - 1, 43 + random[0] % 58);
|
|
794
|
+
var reset = new Uint8Array(size);
|
|
795
|
+
reset.set(random.subarray(1, 1 + size - 16));
|
|
796
|
+
// Fixed bits 01: to anyone but the client, a short-header packet (§10.3).
|
|
797
|
+
reset[0] = 0x40 | (reset[0] & 0x3f);
|
|
798
|
+
reset.set(resetToken(dcid), size - 16);
|
|
799
|
+
if (DEBUG) console.log('[qserver] stateless reset (' + size + 'B) to ' + toIp + ':' + toPort);
|
|
800
|
+
sendUdp(toIp, toPort, reset);
|
|
176
801
|
}
|
|
177
802
|
|
|
178
803
|
|
|
@@ -187,10 +812,22 @@ function createQuicServer(options) {
|
|
|
187
812
|
? (externalSocket6 || udp6)
|
|
188
813
|
: (externalSocket4 || udp4);
|
|
189
814
|
if (!socket) return;
|
|
815
|
+
sendsPending++;
|
|
190
816
|
socket.send(data, toPort, toIp, function (err) {
|
|
191
|
-
|
|
817
|
+
sendsPending--;
|
|
818
|
+
if (err && !closing) ev.emit('error', err);
|
|
819
|
+
if (closing && sendsPending === 0) closing();
|
|
192
820
|
});
|
|
193
821
|
}
|
|
822
|
+
// Datagrams handed to a socket and not yet sent. dgram.send() leaves on a
|
|
823
|
+
// later turn (it resolves the address first), and a socket closed before
|
|
824
|
+
// then drops what it was given: the CONNECTION_CLOSE close() sends every
|
|
825
|
+
// connection never left, and each client found out only by its idle
|
|
826
|
+
// timeout. close() waits for them — not long (SEND_DRAIN_MAX).
|
|
827
|
+
var sendsPending = 0;
|
|
828
|
+
var shutDown = false;
|
|
829
|
+
var closing = null;
|
|
830
|
+
var SEND_DRAIN_MAX = 200;
|
|
194
831
|
|
|
195
832
|
|
|
196
833
|
// ============================================================
|
|
@@ -198,37 +835,103 @@ function createQuicServer(options) {
|
|
|
198
835
|
// ============================================================
|
|
199
836
|
|
|
200
837
|
function listen(listenPort, host, callback) {
|
|
838
|
+
shutDown = false;
|
|
201
839
|
if (typeof host === 'function') { callback = host; host = null; }
|
|
202
|
-
port
|
|
840
|
+
// `|| 443` turned port 0 into 443 — and port 0 is not "no port": it is how
|
|
841
|
+
// a caller asks the OS for a free one (server.js has the same fix).
|
|
842
|
+
port = (listenPort === undefined || listenPort === null) ? 443 : listenPort;
|
|
203
843
|
host = host || '::';
|
|
204
844
|
|
|
205
845
|
if (isExternal) {
|
|
206
846
|
// External sockets already bound — wire up incoming messages.
|
|
207
847
|
// Starting the sweep timer is safe here (idempotent check below).
|
|
848
|
+
// Externally-owned sockets are already bound, so enlarge them here.
|
|
849
|
+
// The owner may not have done it, and the cost of asking twice is nil.
|
|
850
|
+
// NOT listening on them. The owner of an external socket is the one
|
|
851
|
+
// reading it — typically a demultiplexer that also carries STUN, DTLS
|
|
852
|
+
// and SRTP on the same port — and it feeds QUIC packets in through
|
|
853
|
+
// handlePacket(). This used to attach a 'message' listener as well, so
|
|
854
|
+
// every datagram on the socket reached QUIC twice: each QUIC packet was
|
|
855
|
+
// decrypted a second time and dropped as a duplicate, and every non-QUIC
|
|
856
|
+
// packet — media included, on the hottest path there is — went through
|
|
857
|
+
// the QUIC parser before being discarded.
|
|
208
858
|
if (externalSocket4) {
|
|
209
|
-
externalSocket4.
|
|
859
|
+
try { externalSocket4.setSendBufferSize(2 * 1024 * 1024); } catch (e) {}
|
|
860
|
+
try { externalSocket4.setRecvBufferSize(2 * 1024 * 1024); } catch (e) {}
|
|
210
861
|
}
|
|
211
862
|
if (externalSocket6) {
|
|
212
|
-
externalSocket6.
|
|
863
|
+
try { externalSocket6.setSendBufferSize(2 * 1024 * 1024); } catch (e) {}
|
|
864
|
+
try { externalSocket6.setRecvBufferSize(2 * 1024 * 1024); } catch (e) {}
|
|
213
865
|
}
|
|
214
866
|
startSweepTimer();
|
|
215
867
|
if (typeof callback === 'function') setImmediate(callback);
|
|
216
868
|
return;
|
|
217
869
|
}
|
|
218
870
|
|
|
871
|
+
// Grow the kernel socket buffers once the socket is bound.
|
|
872
|
+
//
|
|
873
|
+
// Node's defaults are small. A full send buffer drops the datagram
|
|
874
|
+
// locally and silently — indistinguishable from network loss, so loss
|
|
875
|
+
// recovery engages and throughput collapses. See LOSS_PTO_PLAN 7.4: this
|
|
876
|
+
// is what defeated the third pacer-calibration attempt.
|
|
877
|
+
//
|
|
878
|
+
// try/catch because some platforms cap the value and throw; not being
|
|
879
|
+
// able to enlarge the buffer must not stop the server from listening.
|
|
880
|
+
function growBuffers(sock) {
|
|
881
|
+
try { sock.setSendBufferSize(2 * 1024 * 1024); } catch (e) {}
|
|
882
|
+
try { sock.setRecvBufferSize(2 * 1024 * 1024); } catch (e) {}
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
// Both sockets are attempted, and listening succeeds if EITHER binds.
|
|
886
|
+
//
|
|
887
|
+
// The callback used to hang off udp6.bind alone. On a host without IPv6 —
|
|
888
|
+
// a container, a CI runner, plenty of cloud instances — that bind fails
|
|
889
|
+
// with EAFNOSUPPORT and the callback is never called: listen() returns, the
|
|
890
|
+
// v4 socket is up and working, and the caller waits forever for a
|
|
891
|
+
// 'listening' that cannot arrive. Nothing logs, because the error goes to
|
|
892
|
+
// the 'error' event that a caller waiting on the callback has not reached
|
|
893
|
+
// yet.
|
|
894
|
+
//
|
|
895
|
+
// A failure on one family is also not fatal on its own. Only if BOTH fail
|
|
896
|
+
// is there nothing to listen on, and that is reported as an error.
|
|
897
|
+
var pending = 2;
|
|
898
|
+
var bound = 0;
|
|
899
|
+
var lastErr = null;
|
|
900
|
+
|
|
901
|
+
function settle(err) {
|
|
902
|
+
if (err) lastErr = err;
|
|
903
|
+
else bound++;
|
|
904
|
+
if (--pending > 0) return;
|
|
905
|
+
if (bound === 0) {
|
|
906
|
+
ev.emit('error', lastErr || new Error('failed to bind UDP on port ' + port));
|
|
907
|
+
return;
|
|
908
|
+
}
|
|
909
|
+
if (typeof callback === 'function') callback();
|
|
910
|
+
}
|
|
911
|
+
|
|
219
912
|
udp4 = dgram.createSocket('udp4');
|
|
220
913
|
udp4.on('message', onUdpMessage);
|
|
221
|
-
udp4.
|
|
914
|
+
udp4.once('error', function (err) {
|
|
915
|
+
// A bind failure arrives before we are listening; anything later is a
|
|
916
|
+
// runtime socket error and belongs to the consumer.
|
|
917
|
+
udp4 = null;
|
|
918
|
+
settle(err);
|
|
919
|
+
ev.emit('error', err);
|
|
920
|
+
});
|
|
222
921
|
var host4 = host.indexOf('.') !== -1 ? host : '0.0.0.0';
|
|
223
|
-
udp4.bind(port, host4);
|
|
922
|
+
udp4.bind(port, host4, function () { growBuffers(udp4); settle(null); });
|
|
224
923
|
|
|
225
924
|
udp6 = dgram.createSocket({ type: 'udp6', ipv6Only: true });
|
|
226
925
|
udp6.on('message', onUdpMessage);
|
|
227
|
-
udp6.
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
926
|
+
udp6.once('error', function (err) {
|
|
927
|
+
udp6 = null;
|
|
928
|
+
// No IPv6 on this host is ordinary, not an error the application needs
|
|
929
|
+
// to see — the v4 socket is serving. Only a total failure is emitted,
|
|
930
|
+
// from settle() above.
|
|
931
|
+
settle(err);
|
|
231
932
|
});
|
|
933
|
+
var host6 = host.indexOf(':') !== -1 ? host : '::';
|
|
934
|
+
udp6.bind(port, host6, function () { growBuffers(udp6); settle(null); });
|
|
232
935
|
|
|
233
936
|
startSweepTimer();
|
|
234
937
|
}
|
|
@@ -239,30 +942,50 @@ function createQuicServer(options) {
|
|
|
239
942
|
if (sweepTimer) return; // idempotent
|
|
240
943
|
sweepTimer = setInterval(function () {
|
|
241
944
|
for (var id in connections) {
|
|
242
|
-
|
|
243
|
-
if (st === 'closed') {
|
|
244
|
-
delete connections[id];
|
|
245
|
-
// Clean address map
|
|
246
|
-
for (var addr in addressMap) {
|
|
247
|
-
if (addressMap[addr] === id) delete addressMap[addr];
|
|
248
|
-
}
|
|
249
|
-
}
|
|
945
|
+
if (connections[id].quic.state === 'closed') dropConnection(id);
|
|
250
946
|
}
|
|
251
947
|
}, 30000);
|
|
252
948
|
if (sweepTimer.unref) sweepTimer.unref();
|
|
253
949
|
}
|
|
254
950
|
|
|
255
951
|
function close(callback) {
|
|
952
|
+
shutDown = true;
|
|
256
953
|
// Stop sweep
|
|
257
954
|
if (sweepTimer) { clearInterval(sweepTimer); sweepTimer = null; }
|
|
258
955
|
|
|
259
956
|
// Close all connections
|
|
260
957
|
for (var id in connections) {
|
|
958
|
+
// Transport NO_ERROR (0x1c, code 0) — deliberately NOT an HTTP/3 code.
|
|
959
|
+
// This server is application-agnostic: `alpn` is an option, and a DoQ or
|
|
960
|
+
// custom-protocol server built on it has no business emitting
|
|
961
|
+
// H3_NO_ERROR. An application layer that wants its own code closes the
|
|
962
|
+
// connection itself (h3_client / agent / webtransport all do).
|
|
261
963
|
try { connections[id].quic.close(0, 'server closing'); } catch (e) { }
|
|
262
964
|
}
|
|
263
965
|
connections = {};
|
|
966
|
+
cidMap = {};
|
|
264
967
|
addressMap = {};
|
|
968
|
+
connectionCount = 0;
|
|
969
|
+
|
|
970
|
+
// The sockets close once what was sent — the CONNECTION_CLOSEs just
|
|
971
|
+
// above — has left (see sendsPending).
|
|
972
|
+
if (sendsPending > 0 && (udp4 || udp6)) {
|
|
973
|
+
var drained = false;
|
|
974
|
+
var finish = function () {
|
|
975
|
+
if (drained) return;
|
|
976
|
+
drained = true;
|
|
977
|
+
clearTimeout(drainTimer);
|
|
978
|
+
closing = null;
|
|
979
|
+
closeSockets(callback);
|
|
980
|
+
};
|
|
981
|
+
closing = finish;
|
|
982
|
+
var drainTimer = setTimeout(finish, SEND_DRAIN_MAX);
|
|
983
|
+
return;
|
|
984
|
+
}
|
|
985
|
+
closeSockets(callback);
|
|
986
|
+
}
|
|
265
987
|
|
|
988
|
+
function closeSockets(callback) {
|
|
266
989
|
var closed = 0;
|
|
267
990
|
var total = (udp4 ? 1 : 0) + (udp6 ? 1 : 0);
|
|
268
991
|
if (total === 0) { if (callback) callback(); return; }
|
|
@@ -306,7 +1029,37 @@ function createQuicServer(options) {
|
|
|
306
1029
|
|
|
307
1030
|
/** Returns true if the 5-tuple matches an active QUIC connection.
|
|
308
1031
|
* Used by demuxers for routing decisions on shared UDP ports. */
|
|
309
|
-
hasConnection: hasConnection
|
|
1032
|
+
hasConnection: hasConnection,
|
|
1033
|
+
|
|
1034
|
+
/** The session ticket keys — tls.Server's names. A new key applies to
|
|
1035
|
+
* connections made from now on. */
|
|
1036
|
+
getTicketKeys: function () { return ticketKeys; },
|
|
1037
|
+
setTicketKeys: function (keys) { ticketKeys = keys; },
|
|
1038
|
+
|
|
1039
|
+
/** The Encrypted Client Hello keys; a new set applies to connections
|
|
1040
|
+
* made from now on. */
|
|
1041
|
+
getEchKeys: function () { return ech; },
|
|
1042
|
+
setEchKeys: function (keys) { ech = keys || null; },
|
|
1043
|
+
|
|
1044
|
+
/** The stateless reset secret. A new one applies to connections made
|
|
1045
|
+
* from now on; a reset for one made before cannot be recognised. */
|
|
1046
|
+
/** Address validation with Retry (see validateAddress above): on or off,
|
|
1047
|
+
* from the next new client on. */
|
|
1048
|
+
getValidateAddress: function () { return validateAddress; },
|
|
1049
|
+
setValidateAddress: function (on) { validateAddress = on === true; },
|
|
1050
|
+
|
|
1051
|
+
/** Handshakes under way (see "Handshakes under way" above), and the
|
|
1052
|
+
* two limits on them — changed while running, they apply at once: the
|
|
1053
|
+
* threshold to the count as it stands, the cap to the next Initial. */
|
|
1054
|
+
getPendingHandshakes: function () { return pendingHandshakes; },
|
|
1055
|
+
|
|
1056
|
+
/** The options that can change while the server runs, by createServer's
|
|
1057
|
+
* names (see setOptions). */
|
|
1058
|
+
getOptions: getOptions,
|
|
1059
|
+
setOptions: setOptions,
|
|
1060
|
+
|
|
1061
|
+
getResetTokenSecret: function () { return resetTokenSecret; },
|
|
1062
|
+
setResetTokenSecret: function (secret) { resetTokenSecret = checkResetTokenSecret(secret) || resetTokenSecret; }
|
|
310
1063
|
};
|
|
311
1064
|
}
|
|
312
1065
|
|