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.
- package/README.md +1987 -160
- package/index.d.ts +1034 -77
- package/index.js +59 -36
- package/package.json +8 -5
- package/src/agent.js +306 -36
- package/src/client.js +447 -120
- package/src/client_profile.js +266 -0
- package/src/connect_udp.js +578 -0
- package/src/connect_udp_client.js +779 -0
- package/src/crypto.js +240 -140
- package/src/h1.js +953 -0
- package/src/h1_client.js +731 -0
- package/src/h1_server.js +666 -0
- package/src/h2.js +1325 -0
- package/src/h2_client.js +675 -0
- package/src/h2_fingerprint.js +403 -0
- package/src/h2_frames.js +582 -0
- package/src/h2_server.js +911 -0
- package/src/h3.js +1986 -520
- package/src/h3_client.js +484 -46
- package/src/h3_server.js +1448 -98
- package/src/hpack.js +455 -0
- package/src/hpack_primitives.js +559 -0
- package/src/http_common.js +908 -0
- package/src/ja4h.js +256 -0
- package/src/qpack.js +555 -0
- package/src/quic_connection.js +6702 -665
- package/src/quic_server.js +858 -82
- package/src/quic_socket.js +134 -9
- package/src/quic_version.js +143 -0
- package/src/server.js +1097 -125
- package/src/server_stats.js +93 -0
- package/src/streams.js +542 -43
- package/src/tcp_listener.js +568 -0
- package/src/tls_bridge.js +478 -73
- package/src/tls_options.js +108 -0
- package/src/transport.js +455 -46
- package/src/utils.js +94 -12
- package/src/webtransport.js +784 -417
- package/src/ws.js +744 -0
- package/src/ws_client.js +621 -0
- package/src/ws_server.js +815 -0
- package/src/ws_stream.js +250 -0
- package/src/wt_stream.js +1075 -0
package/src/quic_server.js
CHANGED
|
@@ -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 {
|
|
23
|
-
import {
|
|
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 }
|
|
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
|
-
|
|
99
|
-
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
|
|
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
|
-
|
|
170
|
-
|
|
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
|
-
|
|
176
|
-
|
|
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
|
-
//
|
|
179
|
-
//
|
|
180
|
-
//
|
|
181
|
-
//
|
|
182
|
-
|
|
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
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
-
|
|
191
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
201
|
-
|
|
202
|
-
|
|
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
|
-
//
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
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.
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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
|
-
|
|
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
|
|
343
|
-
//
|
|
344
|
-
//
|
|
345
|
-
//
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
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
|
-
/**
|
|
366
|
-
*
|
|
367
|
-
|
|
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
|
|