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