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.
- package/README.md +1768 -155
- package/index.js +55 -36
- package/package.json +3 -3
- 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 +770 -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 +624 -0
- package/src/h2.js +1325 -0
- package/src/h2_client.js +675 -0
- package/src/h2_fingerprint.js +403 -0
- package/src/h2_frames.js +582 -0
- package/src/h2_server.js +862 -0
- package/src/h3.js +1966 -469
- package/src/h3_client.js +467 -48
- package/src/h3_server.js +1349 -94
- package/src/hpack.js +455 -0
- package/src/hpack_primitives.js +520 -0
- package/src/http_common.js +901 -0
- package/src/ja4h.js +256 -0
- package/src/qpack.js +555 -0
- package/src/quic_connection.js +6517 -657
- package/src/quic_server.js +768 -73
- package/src/quic_socket.js +134 -9
- package/src/quic_version.js +143 -0
- package/src/server.js +843 -115
- package/src/server_stats.js +93 -0
- package/src/streams.js +361 -35
- package/src/tcp_listener.js +557 -0
- package/src/tls_bridge.js +474 -73
- package/src/tls_options.js +108 -0
- package/src/transport.js +455 -46
- package/src/utils.js +94 -12
- package/src/webtransport.js +433 -436
- package/src/ws.js +624 -0
- package/src/ws_client.js +641 -0
- package/src/ws_server.js +833 -0
- package/src/wt_stream.js +618 -0
package/src/quic_server.js
CHANGED
|
@@ -13,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,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
|
-
|
|
99
|
-
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
|
|
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
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
-
|
|
176
|
-
|
|
499
|
+
for (var ci = 1; ci < packets.length; ci++) {
|
|
500
|
+
if (cidKey(packets[ci].dcid) !== dcidKey) { packets.length = ci; break; }
|
|
501
|
+
}
|
|
177
502
|
|
|
178
|
-
//
|
|
179
|
-
//
|
|
180
|
-
|
|
181
|
-
|
|
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
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
-
|
|
191
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
201
|
-
|
|
202
|
-
|
|
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
|
-
//
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
|
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.
|
|
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.
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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
|
-
|
|
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
|
|