quico 0.4.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.
@@ -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,293 @@ 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
+ 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
+
89
299
  // Build SNICallback
90
300
  var sniCallback = options.SNICallback || null;
91
301
 
92
302
  if (!sniCallback && options.key && options.cert) {
93
- 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);
94
309
  sniCallback = function (servername, cb) { cb(null, defaultCtx); };
95
310
  }
96
311
 
97
312
  // ---- Connection tracking ----
98
- var connections = {}; // connId → { quic, peer }
99
- var addressMap = {}; // "ip:port" → connId
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;
100
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
+ }
101
393
  var sweepTimer = null;
102
394
 
395
+ function cidKey(cid) {
396
+ return Buffer.from(cid.buffer, cid.byteOffset, cid.byteLength).toString('hex');
397
+ }
398
+
103
399
 
104
400
  // ============================================================
105
401
  // UDP → connection routing
106
402
  // ============================================================
107
403
 
108
404
  function onUdpMessage(msg, rinfo) {
405
+ // Closed (or closing, its last datagrams leaving): nothing more is taken.
406
+ if (shutDown) return;
109
407
  var data = new Uint8Array(msg);
110
408
  var fromIp = rinfo.address;
111
409
  var fromPort = rinfo.port;
112
410
  if (DEBUG) console.log('[qserver] UDP from ' + fromIp + ':' + fromPort + ' len=' + data.length);
113
411
 
114
- var addressKey = fromIp + ':' + fromPort;
115
- var packets = parse_quic_datagram(data);
412
+ var packets = parse_quic_datagram(data, SERVER_CID_LEN);
116
413
  if (packets.length === 0) return;
117
414
 
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
415
  // ── Version Negotiation (RFC 9000 §6) ──
124
416
  // Must happen before any connection is created. A long-header packet
125
417
  // carrying a version we don't speak gets a Version Negotiation packet in
126
418
  // reply — that is how a client discovers what we support, and how a prober
127
419
  // checks we are alive without completing a handshake.
128
420
  //
129
- // Getting this wrong is silent and confusing: without it, findConnectionId
421
+ // Getting this wrong is silent and confusing: without it, the router
130
422
  // below happily creates a connection for the unknown-version Initial, maps
131
423
  // the address to it, and every subsequent probe is routed into a
132
424
  // connection that cannot do anything with it. The peer sees no reply at
@@ -134,7 +426,7 @@ function createQuicServer(options) {
134
426
  var firstPkt = packets[0];
135
427
  if (firstPkt.form === 'long' &&
136
428
  firstPkt.type !== 'version_negotiation' &&
137
- !is_supported_version(firstPkt.version)) {
429
+ quicVersions.indexOf(firstPkt.version) < 0) {
138
430
 
139
431
  // Anti-amplification (RFC 9000 §14.1): only answer datagrams that are
140
432
  // at least 1200 bytes, so we can't be used to reflect traffic at a
@@ -146,70 +438,366 @@ function createQuicServer(options) {
146
438
  return;
147
439
  }
148
440
 
149
- var vn = build_version_negotiation(firstPkt.dcid, firstPkt.scid);
441
+ var vn = build_version_negotiation(firstPkt.dcid, firstPkt.scid, quicVersions);
150
442
  if (DEBUG) console.log('[qserver] version 0x' +
151
443
  firstPkt.version.toString(16) + ' unsupported — sending Version Negotiation');
152
444
  sendUdp(fromIp, fromPort, vn);
153
445
  return;
154
446
  }
155
447
 
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);
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
+ }
163
460
  }
164
461
 
165
- connections[connId].quic.feedPackets(fromIp, fromPort, packets);
166
- }
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;
167
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;
168
481
 
169
- function findConnectionId(pkt, addressKey) {
170
- var dcidStr = null;
171
- if (pkt.dcid && pkt.dcid.byteLength > 0) {
172
- dcidStr = Array.from(pkt.dcid).join('');
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;
173
497
  }
174
498
 
175
- if (dcidStr && dcidStr in connections) return dcidStr;
176
- if (addressKey in addressMap && addressMap[addressKey] in connections) return addressMap[addressKey];
499
+ for (var ci = 1; ci < packets.length; ci++) {
500
+ if (cidKey(packets[ci].dcid) !== dcidKey) { packets.length = ci; break; }
501
+ }
177
502
 
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;
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);
183
507
 
184
- var id = dcidStr || String(Math.floor(Math.random() * 9007199254740991));
185
- addressMap[addressKey] = id;
186
- return id;
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
+ }
187
518
  }
188
519
 
189
520
 
190
- function createConnection(connId, fromIp, fromPort, addressKey) {
191
- var quic = new QUICConnection({ SNICallback: sniCallback, alpn: alpn });
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
+ }
192
553
 
193
- connections[connId] = {
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 = {
194
628
  quic: quic,
195
- 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)
196
634
  };
197
-
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;
198
653
  addressMap[addressKey] = connId;
199
654
 
200
- // Send packets back via UDP
201
- quic.on('packet', function (data) {
202
- sendUdp(fromIp, fromPort, data);
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);
203
659
  });
204
660
 
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];
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;
209
675
  });
676
+ quic.on('path_validated', function () {
677
+ entry.peer.address = quic.remoteAddress;
678
+ entry.peer.port = quic.remotePort;
679
+ });
680
+
681
+ // Drop our bookkeeping when the connection closes.
682
+ quic.on('close', function () { dropConnection(connId); });
210
683
 
211
684
  // Hand the raw connection to the application layer (H3, DoQ, ...).
212
- ev.emit('connection', quic, { address: fromIp, port: fromPort });
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);
213
801
  }
214
802
 
215
803
 
@@ -224,10 +812,22 @@ function createQuicServer(options) {
224
812
  ? (externalSocket6 || udp6)
225
813
  : (externalSocket4 || udp4);
226
814
  if (!socket) return;
815
+ sendsPending++;
227
816
  socket.send(data, toPort, toIp, function (err) {
228
- if (err) ev.emit('error', err);
817
+ sendsPending--;
818
+ if (err && !closing) ev.emit('error', err);
819
+ if (closing && sendsPending === 0) closing();
229
820
  });
230
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;
231
831
 
232
832
 
233
833
  // ============================================================
@@ -235,8 +835,11 @@ function createQuicServer(options) {
235
835
  // ============================================================
236
836
 
237
837
  function listen(listenPort, host, callback) {
838
+ shutDown = false;
238
839
  if (typeof host === 'function') { callback = host; host = null; }
239
- port = listenPort || 443;
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;
240
843
  host = host || '::';
241
844
 
242
845
  if (isExternal) {
@@ -244,13 +847,19 @@ function createQuicServer(options) {
244
847
  // Starting the sweep timer is safe here (idempotent check below).
245
848
  // Externally-owned sockets are already bound, so enlarge them here.
246
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.
247
858
  if (externalSocket4) {
248
- externalSocket4.on('message', onUdpMessage);
249
859
  try { externalSocket4.setSendBufferSize(2 * 1024 * 1024); } catch (e) {}
250
860
  try { externalSocket4.setRecvBufferSize(2 * 1024 * 1024); } catch (e) {}
251
861
  }
252
862
  if (externalSocket6) {
253
- externalSocket6.on('message', onUdpMessage);
254
863
  try { externalSocket6.setSendBufferSize(2 * 1024 * 1024); } catch (e) {}
255
864
  try { externalSocket6.setRecvBufferSize(2 * 1024 * 1024); } catch (e) {}
256
865
  }
@@ -273,20 +882,56 @@ function createQuicServer(options) {
273
882
  try { sock.setRecvBufferSize(2 * 1024 * 1024); } catch (e) {}
274
883
  }
275
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
+
276
912
  udp4 = dgram.createSocket('udp4');
277
913
  udp4.on('message', onUdpMessage);
278
- udp4.on('error', function (err) { ev.emit('error', err); });
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
+ });
279
921
  var host4 = host.indexOf('.') !== -1 ? host : '0.0.0.0';
280
- udp4.bind(port, host4, function () { growBuffers(udp4); });
922
+ udp4.bind(port, host4, function () { growBuffers(udp4); settle(null); });
281
923
 
282
924
  udp6 = dgram.createSocket({ type: 'udp6', ipv6Only: true });
283
925
  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();
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);
289
932
  });
933
+ var host6 = host.indexOf(':') !== -1 ? host : '::';
934
+ udp6.bind(port, host6, function () { growBuffers(udp6); settle(null); });
290
935
 
291
936
  startSweepTimer();
292
937
  }
@@ -297,30 +942,50 @@ function createQuicServer(options) {
297
942
  if (sweepTimer) return; // idempotent
298
943
  sweepTimer = setInterval(function () {
299
944
  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
- }
945
+ if (connections[id].quic.state === 'closed') dropConnection(id);
308
946
  }
309
947
  }, 30000);
310
948
  if (sweepTimer.unref) sweepTimer.unref();
311
949
  }
312
950
 
313
951
  function close(callback) {
952
+ shutDown = true;
314
953
  // Stop sweep
315
954
  if (sweepTimer) { clearInterval(sweepTimer); sweepTimer = null; }
316
955
 
317
956
  // Close all connections
318
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).
319
963
  try { connections[id].quic.close(0, 'server closing'); } catch (e) { }
320
964
  }
321
965
  connections = {};
966
+ cidMap = {};
322
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
+ }
323
987
 
988
+ function closeSockets(callback) {
324
989
  var closed = 0;
325
990
  var total = (udp4 ? 1 : 0) + (udp6 ? 1 : 0);
326
991
  if (total === 0) { if (callback) callback(); return; }
@@ -364,7 +1029,37 @@ function createQuicServer(options) {
364
1029
 
365
1030
  /** Returns true if the 5-tuple matches an active QUIC connection.
366
1031
  * Used by demuxers for routing decisions on shared UDP ports. */
367
- 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; }
368
1063
  };
369
1064
  }
370
1065