quico 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -13,8 +13,10 @@
13
13
  */
14
14
 
15
15
  import dgram from 'node:dgram';
16
+ import dns from 'node:dns';
17
+ import net from 'node:net';
16
18
  import crypto from 'node:crypto';
17
- import { DEBUG, Emitter } from './utils.js';
19
+ import { DEBUG, Emitter, checkedSocketBufferSize, setSocketBuffers } from './utils.js';
18
20
  import {
19
21
  parse_quic_datagram,
20
22
  build_version_negotiation
@@ -118,6 +120,10 @@ function createQuicServer(options) {
118
120
  // When socket/socket6 are provided, the UDP layer is owned externally.
119
121
  // The caller demuxes packets and feeds them via handlePacket(msg, rinfo).
120
122
  // See RFC 9443 for the shared-port multiplexing scheme.
123
+ // The UDP sockets' buffers (utils.js): 4 MB by default — external ones
124
+ // too, as before (the demultiplexer that owns them may not have, and
125
+ // asking twice costs nothing); 0 or false leaves them all at the OS's.
126
+ var socketBufferSize = checkedSocketBufferSize(options.socketBufferSize, 'socketBufferSize');
121
127
  var externalSocket4 = options.socket || null;
122
128
  var externalSocket6 = options.socket6 || null;
123
129
 
@@ -826,6 +832,17 @@ function createQuicServer(options) {
826
832
  // UDP send
827
833
  // ============================================================
828
834
 
835
+ // A client that has gone away: its port answers our datagrams with ICMP
836
+ // "port unreachable". Node does not report that on an unconnected UDP
837
+ // socket; Bun does, as ECONNREFUSED — an 'error' on the socket, or on a
838
+ // send's callback. It concerns that client alone (its connection ends at
839
+ // its idle timeout, as any silent peer's), not the socket: taken for a
840
+ // socket error it closed the server's socket, and with it every other
841
+ // client of that family (quico PR #12).
842
+ function isDepartedPeer(err) {
843
+ return !!err && err.code === 'ECONNREFUSED';
844
+ }
845
+
829
846
  function sendUdp(toIp, toPort, data) {
830
847
  var isV6 = toIp.indexOf(':') >= 0;
831
848
  // Prefer external socket (shared mode) over internally-bound socket
@@ -836,7 +853,7 @@ function createQuicServer(options) {
836
853
  sendsPending++;
837
854
  socket.send(data, toPort, toIp, function (err) {
838
855
  sendsPending--;
839
- if (err && !closing) ev.emit('error', err);
856
+ if (err && !closing && !isDepartedPeer(err)) ev.emit('error', err);
840
857
  if (closing && sendsPending === 0) closing();
841
858
  });
842
859
  }
@@ -882,14 +899,8 @@ function createQuicServer(options) {
882
899
  // decrypted a second time and dropped as a duplicate, and every non-QUIC
883
900
  // packet — media included, on the hottest path there is — went through
884
901
  // the QUIC parser before being discarded.
885
- if (externalSocket4) {
886
- try { externalSocket4.setSendBufferSize(2 * 1024 * 1024); } catch (e) {}
887
- try { externalSocket4.setRecvBufferSize(2 * 1024 * 1024); } catch (e) {}
888
- }
889
- if (externalSocket6) {
890
- try { externalSocket6.setSendBufferSize(2 * 1024 * 1024); } catch (e) {}
891
- try { externalSocket6.setRecvBufferSize(2 * 1024 * 1024); } catch (e) {}
892
- }
902
+ if (externalSocket4) setSocketBuffers(externalSocket4, socketBufferSize);
903
+ if (externalSocket6) setSocketBuffers(externalSocket6, socketBufferSize);
893
904
  startSweepTimer();
894
905
  if (typeof callback === 'function') setImmediate(callback);
895
906
  return;
@@ -902,65 +913,108 @@ function createQuicServer(options) {
902
913
  // recovery engages and throughput collapses. See LOSS_PTO_PLAN 7.4: this
903
914
  // is what defeated the third pacer-calibration attempt.
904
915
  //
905
- // try/catch because some platforms cap the value and throw; not being
906
- // able to enlarge the buffer must not stop the server from listening.
907
- function growBuffers(sock) {
908
- try { sock.setSendBufferSize(2 * 1024 * 1024); } catch (e) {}
909
- try { sock.setRecvBufferSize(2 * 1024 * 1024); } catch (e) {}
916
+ // Not being able to enlarge the buffer must not stop the server from
917
+ // listening (setSocketBuffers).
918
+ function growBuffers(sock) { setSocketBuffers(sock, socketBufferSize); }
919
+
920
+ // Which addresses: as node:net. An address binds its own family alone —
921
+ // '127.0.0.1' and '0.0.0.0' IPv4, '::1' IPv6; '::' (and no host) both;
922
+ // a hostname is looked up once, and each family it has binds its first
923
+ // address ('localhost': loopback, both families). Every address used to
924
+ // bind '::' unless it held a ':', and '0.0.0.0' unless it held a '.':
925
+ // listen(p, '127.0.0.1') answered on every IPv6 interface too, and
926
+ // listen(p, 'localhost') on every interface of both.
927
+ if (host === '::') bindFamilies('0.0.0.0', '::');
928
+ else if (net.isIP(host) === 4) bindFamilies(host, null);
929
+ else if (net.isIP(host) === 6) bindFamilies(null, host);
930
+ else {
931
+ dns.lookup(host, { all: true }, function (err, list) {
932
+ if (shutDown) return;
933
+ if (err) { ev.emit('error', err); return; }
934
+ bindFamilies(firstOf(list, 4), firstOf(list, 6));
935
+ });
910
936
  }
911
937
 
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.
938
+ function firstOf(list, family) {
939
+ for (var i = 0; i < list.length; i++) if (list[i].family === family) return list[i].address;
940
+ return null;
941
+ }
942
+
943
+ // The families one after the other, IPv4 first: after listen(0) IPv6
944
+ // takes the port the OS gave IPv4 — one server, one port, the one
945
+ // address() and Alt-Svc name. Each took a port of its own.
921
946
  //
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));
947
+ // Listening succeeds when every family binds, but for one the host does
948
+ // not have (familyMissing: no IPv6 in a container) while another did.
949
+ // Any other failure fails the listen() — the sockets that bound are
950
+ // closed, and one 'error' says why. Before, either family binding was
951
+ // enough: with IPv4's port taken (another server already on it) this
952
+ // one served IPv6 alone, and nothing said so.
953
+ function bindFamilies(addr4, addr6) {
954
+ var steps = [];
955
+ if (addr4) steps.push([false, addr4]);
956
+ if (addr6) steps.push([true, addr6]);
957
+ var errs = [], bound = 0;
958
+ (function next(i) {
959
+ if (shutDown) return;
960
+ if (i === steps.length) { settle(errs, bound); return; }
961
+ bindOne(steps[i][0], steps[i][1], function (err) {
962
+ if (!err) bound++;
963
+ else { errs.push(err); if (!familyMissing(err)) { settle(errs, bound); return; } }
964
+ next(i + 1);
965
+ });
966
+ })(0);
967
+ }
968
+
969
+ function familyMissing(err) { return err.code === 'EAFNOSUPPORT' || err.code === 'EADDRNOTAVAIL'; }
970
+
971
+ // The callback used to hang off udp6.bind alone: on a host without IPv6
972
+ // it was never called, while the IPv4 socket served. It is called once
973
+ // the families are settled, whichever bound.
974
+ function settle(errs, bound) {
975
+ var fatal = null;
976
+ for (var i = 0; i < errs.length && !fatal; i++) if (!familyMissing(errs[i])) fatal = errs[i];
977
+ if (!fatal && bound === 0) fatal = errs[0] || new Error('failed to bind UDP on port ' + port);
978
+ if (fatal) {
979
+ if (udp4) { try { udp4.close(); } catch (e) {} udp4 = null; }
980
+ if (udp6) { try { udp6.close(); } catch (e) {} udp6 = null; }
981
+ ev.emit('error', fatal);
934
982
  return;
935
983
  }
936
984
  if (typeof callback === 'function') callback();
937
985
  }
938
986
 
939
- udp4 = dgram.createSocket('udp4');
940
- udp4.on('message', onUdpMessage);
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
- });
948
- var host4 = host.indexOf('.') !== -1 ? host : '0.0.0.0';
949
- if (!refed) udp4.unref();
950
- udp4.bind(port, host4, function () { growBuffers(udp4); settle(null); });
951
-
952
- udp6 = dgram.createSocket({ type: 'udp6', ipv6Only: true });
953
- udp6.on('message', onUdpMessage);
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);
960
- });
961
- var host6 = host.indexOf(':') !== -1 ? host : '::';
962
- if (!refed) udp6.unref();
963
- udp6.bind(port, host6, function () { growBuffers(udp6); settle(null); });
987
+ // A socket whose bind fails is closed, not only dropped: it stayed open
988
+ // (no IPv6 on the host, the port taken), an object nothing could close
989
+ // once `udp4` / `udp6` no longer named it. The failure goes to settle()
990
+ // alone, which reports one error; it was also emitted here, so a
991
+ // listen() that failed emitted 'error' twice.
992
+ //
993
+ // An error after the bind is a runtime socket error: it belongs to the
994
+ // consumer, and the socket goes on serving — it is not closed. It was:
995
+ // the close ran for every error, so on Bun one departed client's
996
+ // ECONNREFUSED (isDepartedPeer, not reported at all) closed the socket,
997
+ // and the server answered no one of that family again. And `on`, not
998
+ // `once`: a second error with no listener left would throw.
999
+ function bindOne(v6, addr, done) {
1000
+ var sock = dgram.createSocket(v6 ? { type: 'udp6', ipv6Only: true } : 'udp4'), isBound = false;
1001
+ if (v6) udp6 = sock; else udp4 = sock;
1002
+ sock.on('message', onUdpMessage);
1003
+ sock.on('error', function (err) {
1004
+ if (isBound) { if (!isDepartedPeer(err)) ev.emit('error', err); return; }
1005
+ try { sock.close(); } catch (e) {}
1006
+ if (udp4 === sock) udp4 = null;
1007
+ if (udp6 === sock) udp6 = null;
1008
+ done(err);
1009
+ });
1010
+ if (!refed) sock.unref();
1011
+ sock.bind(port, addr, function () {
1012
+ isBound = true;
1013
+ growBuffers(sock);
1014
+ if (port === 0) port = sock.address().port;
1015
+ done(null);
1016
+ });
1017
+ }
964
1018
 
965
1019
  startSweepTimer();
966
1020
  }
@@ -1084,6 +1138,12 @@ function createQuicServer(options) {
1084
1138
  return {
1085
1139
  listen: listen,
1086
1140
  close: close,
1141
+ // node:dgram's address() of the socket listening — after listen(0), the
1142
+ // port the OS gave (both families have it); null when not listening.
1143
+ address: function () {
1144
+ var s = udp4 || udp6 || externalSocket4 || externalSocket6;
1145
+ try { return s ? s.address() : null; } catch (e) { return null; }
1146
+ },
1087
1147
  stopAccepting: function () { accepting = false; },
1088
1148
  get connectionCount() { return connectionCount; },
1089
1149
  // node:dgram's: whether the process may exit while this listens.
@@ -16,6 +16,7 @@ import dgram from 'node:dgram';
16
16
  import { QUICConnection } from './quic_connection.js';
17
17
  import lemonTls from 'lemon-tls';
18
18
  import { SUPPORTED_VERSIONS, parseVersionList, versionName } from './quic_version.js';
19
+ import { checkedSocketBufferSize, setSocketBuffers } from './utils.js';
19
20
 
20
21
 
21
22
  /**
@@ -67,6 +68,7 @@ function createQuicClientSocket(opts) {
67
68
  // lemon-tls's list. A name it does not know throws here.
68
69
  var sigalgs = lemonTls.parseSigalgs(opts.sigalgs);
69
70
  var isIPv6 = opts.remoteIp.indexOf(':') >= 0;
71
+ var socketBufferSize = checkedSocketBufferSize(opts.socketBufferSize, 'socketBufferSize');
70
72
  var udpSocket = dgram.createSocket(isIPv6 ? 'udp6' : 'udp4');
71
73
  var quic = null;
72
74
 
@@ -88,11 +90,9 @@ function createQuicClientSocket(opts) {
88
90
  // pacer-calibration attempt (see LOSS_PTO_PLAN 7.4): larger bursts spilled
89
91
  // over the buffer and turned a lossless link into a slow one.
90
92
  //
91
- // Wrapped in try/catch because the call can throw on platforms that cap
92
- // the size; failing to enlarge the buffer is not a reason to fail the
93
- // connection.
94
- try { udpSocket.setSendBufferSize(2 * 1024 * 1024); } catch (e) {}
95
- try { udpSocket.setRecvBufferSize(2 * 1024 * 1024); } catch (e) {}
93
+ // 4 MB by default (utils.js), socketBufferSize where given; failing to
94
+ // enlarge the buffer is not a reason to fail the connection.
95
+ setSocketBuffers(udpSocket, socketBufferSize);
96
96
 
97
97
  // One attempt; after a Version Negotiation, a second in a version the
98
98
  // server offered (RFC 9000 §6.2) — once.
package/src/server.js CHANGED
@@ -218,26 +218,16 @@ function createServer(options, handler) {
218
218
  function toContext(o, what) {
219
219
  if (o && typeof o === 'object' && o.certificateChain && o.privateKey) return o; // made already (quico.createSecureContext)
220
220
  if (!o || typeof o !== 'object') throw new TypeError(what + ' takes { key, cert[, ca, passphrase] } or a context from quico.createSecureContext()');
221
+ // createSecureContext (lemon-tls's) refuses a key that is not the
222
+ // certificate's (ERR_OSSL_X509_KEY_VALUES_MISMATCH) - before it replaces
223
+ // anything, not found out by every client's handshake failing.
221
224
  var ctx = createSecureContext(Object.assign({ passphrase: options.passphrase }, o));
222
- checkKeyMatches(ctx);
223
225
  // Its chain compressed now, not in a client's handshake (RFC 8879).
224
226
  if (typeof lemonTls.precompressCertificate === 'function' && !(Array.isArray(options.certificateCompression) && options.certificateCompression.length === 0)) {
225
227
  try { lemonTls.precompressCertificate(ctx, options.certificateCompression || undefined); } catch (e) {}
226
228
  }
227
229
  return ctx;
228
230
  }
229
- // A certificate whose key is another's is refused here — before it
230
- // replaces anything — not found out by every client's handshake failing.
231
- function checkKeyMatches(ctx) {
232
- if (!ctx.keyObj || !ctx.certObjs || !ctx.certObjs[0]) return;
233
- var a = crypto.createPublicKey(ctx.keyObj).export({ type: 'spki', format: 'der' });
234
- var b = ctx.certObjs[0].publicKey.export({ type: 'spki', format: 'der' });
235
- if (Buffer.compare(Buffer.from(a), Buffer.from(b)) !== 0) {
236
- var e = new Error('the private key does not match the certificate');
237
- e.code = 'ERR_OSSL_X509_KEY_VALUES_MISMATCH';
238
- throw e;
239
- }
240
- }
241
231
  function contextFor(servername) {
242
232
  var name = String(servername || '');
243
233
  for (var i = sniContexts.length - 1; i >= 0; i--) if (sniContexts[i][0].test(name)) return sniContexts[i][1];
@@ -412,6 +402,8 @@ function createServer(options, handler) {
412
402
  maxUdpPayloadSize: options.maxUdpPayloadSize,
413
403
  initialRtt: options.initialRtt,
414
404
  reorderWindow: options.reorderWindow,
405
+ // The UDP sockets' buffers, bytes: 4 MB by default; 0, the OS's (utils.js).
406
+ socketBufferSize: options.socketBufferSize,
415
407
  // Per QUIC connection, asked once its ClientHello is in: (info, callback)
416
408
  // → { transportParams, initialCongestionWindow, carefulResume, startupCap,
417
409
  // pmtuDiscovery, maxUdpPayloadSize, initialRtt, reorderWindow }.
@@ -945,7 +937,7 @@ function createServer(options, handler) {
945
937
  listening = true;
946
938
  closed = false;
947
939
  // After listen(0) the OS chose the port; Alt-Svc must name that one.
948
- if (port === 0 && tcpServer && tcpServer.address()) altSvcPort = tcpServer.address().port;
940
+ if (port === 0) { var a = address(); if (a && a.port) altSvcPort = a.port; }
949
941
  ev.emit('listening');
950
942
  if (typeof callback === 'function') callback();
951
943
  }
@@ -956,8 +948,22 @@ function createServer(options, handler) {
956
948
  if (remaining === 0) { remaining = 1; setImmediate(done); return server; }
957
949
 
958
950
  if (ref === false) { if (enableH3) h3srv.unref(); if (tcpServer) tcpServer.unref(); }
951
+ // No host: TCP as node:net, '::' where the host has IPv6 (both
952
+ // families), else '0.0.0.0' — as UDP. It was '0.0.0.0': HTTP/1.1 and
953
+ // HTTP/2 on IPv4 alone, HTTP/3 on both.
954
+ var tcpHost = host || undefined;
955
+ // listen(0): TCP first, then UDP on the port the OS gave TCP — the port
956
+ // Alt-Svc names and address() reports. Each took a port of its own, and
957
+ // Alt-Svc named TCP's: a browser never reached HTTP/3.
958
+ if (bindUdp && needsTcp && port === 0) {
959
+ tcpServer.listen(0, tcpHost, function () {
960
+ done();
961
+ h3srv.listen(tcpServer.address().port, host, done);
962
+ });
963
+ return server;
964
+ }
959
965
  if (bindUdp) h3srv.listen(port, host, done);
960
- if (needsTcp) tcpServer.listen(port, host || '0.0.0.0', done);
966
+ if (needsTcp) tcpServer.listen(port, tcpHost, done);
961
967
 
962
968
  return server;
963
969
  }
@@ -1056,6 +1062,9 @@ function createServer(options, handler) {
1056
1062
  var addr = tcpServer.address();
1057
1063
  if (addr) return addr;
1058
1064
  }
1065
+ // HTTP/3 alone: the UDP socket's (the same port, when both listen).
1066
+ var a3 = enableH3 && h3srv.address ? h3srv.address() : null;
1067
+ if (a3) return a3;
1059
1068
  return { address: '0.0.0.0', family: 'IPv4', port: altSvcPort };
1060
1069
  }
1061
1070
 
package/src/tls_bridge.js CHANGED
@@ -17,7 +17,6 @@ import {
17
17
  createSecureContext as _lemonCreateSecureContext,
18
18
  certificateObject
19
19
  } from 'lemon-tls';
20
- import { X509Certificate } from 'node:crypto';
21
20
  import { DEBUG, Emitter } from './utils.js';
22
21
  import { rootCertificates as lemonRoots, parseEcdhCurve as lemonParseEcdhCurve, DEFAULT_SERVER_GROUPS as lemonServerGroups,
23
22
  DEFAULT_SIGNATURE_SCHEMES as lemonSigSchemes } from 'lemon-tls';
@@ -159,6 +158,16 @@ function TLSBridge(options) {
159
158
  var handshakeSecretsEmitted = false;
160
159
  var appSecretsEmitted = false;
161
160
  var helloHandled = false;
161
+ // A handshake refused here, for what QUIC asks of TLS (RFC 9001): the
162
+ // alert closes the connection; nothing more is answered or sent.
163
+ var refused = false;
164
+ function refuse(alert, why) {
165
+ if (refused) return;
166
+ refused = true;
167
+ if (DEBUG) console.log('[tls] refused: ' + why);
168
+ ev.emit('alert', alert);
169
+ ev.emit('error', Object.assign(new Error(why), { code: 'ERR_QUIC_TLS' }));
170
+ }
162
171
  // A client's: what it keeps with each session it saves — the server's
163
172
  // transport parameters (tp), and the application's own (app: HTTP/3's
164
173
  // SETTINGS) once it has them — for 0-RTT next time.
@@ -363,31 +372,44 @@ function TLSBridge(options) {
363
372
  // 'handshakeMessage' with the parsed extensions, so we extract the 0x39 data,
364
373
  // parse it, and hand the params to the QUIC layer (which seeds remote_max_data,
365
374
  // max_ack_delay, ack_delay_exponent, etc. — previously hardcoded defaults).
375
+ //
376
+ // The extension is required (RFC 9001 §8.2): a ClientHello — every one, a
377
+ // second after a HelloRetryRequest included — or EncryptedExtensions
378
+ // without it is refused with missing_extension, before anything is
379
+ // answered. TLS itself has no reason to require it; QUIC does, so here.
366
380
  var peerParamsEmitted = false;
367
381
  session.on('handshakeMessage', function (type, data, message) {
368
- if (peerParamsEmitted || !message || !Array.isArray(message.extensions)) return;
369
- for (var i = 0; i < message.extensions.length; i++) {
370
- var e = message.extensions[i];
371
- if (e && (e.type === 0x39 || e.type === 57) && e.data) {
372
- try {
373
- var parsed = parse_transport_params(e.data, 0);
374
- peerParamsEmitted = true;
375
- if (DEBUG) console.log('[tls] peer transport params parsed (' + e.data.length + 'B)');
376
- // A client keeps, with the sessions this connection saves, the
377
- // server's parameters that 0-RTT must stay within (RFC 9000
378
- // §7.4.1) — given back with the early secret next time.
379
- if (!isServer) keepAppData('tp', rememberedParams(parsed));
380
- ev.emit('peerTransportParams', parsed);
381
- } catch (err) {
382
- if (DEBUG) console.log('[tls] peer transport params parse failed: ' + err.message);
383
- }
384
- break;
385
- }
382
+ var carrier = isServer ? 'client_hello' : 'encrypted_extensions';
383
+ if (type !== carrier || refused) return;
384
+ var ext = null;
385
+ var list = (message && Array.isArray(message.extensions)) ? message.extensions : [];
386
+ for (var i = 0; i < list.length; i++) {
387
+ var e = list[i];
388
+ if (e && (e.type === 0x39 || e.type === 57) && e.data) { ext = e; break; }
389
+ }
390
+ if (!ext) { refuse(109, 'no quic_transport_parameters in the ' + type); return; }
391
+ if (peerParamsEmitted) return;
392
+ var parsed;
393
+ try {
394
+ parsed = parse_transport_params(ext.data, 0);
395
+ } catch (err) {
396
+ // RFC 9000 §7.4: TRANSPORT_PARAMETER_ERROR, not a connection that
397
+ // goes on with the defaults.
398
+ refused = true;
399
+ ev.emit('transportParamsError', 'quic_transport_parameters: ' + err.message);
400
+ return;
386
401
  }
402
+ peerParamsEmitted = true;
403
+ if (DEBUG) console.log('[tls] peer transport params parsed (' + ext.data.length + 'B)');
404
+ // A client keeps, with the sessions this connection saves, the
405
+ // server's parameters that 0-RTT must stay within (RFC 9000
406
+ // §7.4.1) — given back with the early secret next time.
407
+ if (!isServer) keepAppData('tp', rememberedParams(parsed));
408
+ ev.emit('peerTransportParams', parsed);
387
409
  });
388
410
 
389
411
  session.on('hello', function () {
390
- if (helloHandled) return; // prevent double-fire
412
+ if (helloHandled || refused) return; // prevent double-fire
391
413
  helloHandled = true;
392
414
  if (DEBUG) console.log('[tls] hello — configuring TLS');
393
415
 
@@ -435,6 +457,15 @@ function TLSBridge(options) {
435
457
  if (data && data.length >= 2 && data[0] === 2) ev.emit('alert', data[1]);
436
458
  return;
437
459
  }
460
+ // Refused above: nothing more of this handshake goes out.
461
+ if (refused) return;
462
+ // Never sent over QUIC (RFC 9001 §6, §8.3), whatever TLS would do over
463
+ // TCP: a KeyUpdate (QUIC has its own), an EndOfEarlyData. Should TLS
464
+ // make one, the connection ends — internal_error — rather than send it.
465
+ if (type === 'key_update' || type === 'end_of_early_data') {
466
+ refuse(80, 'TLS produced a ' + type + ', which QUIC never sends');
467
+ return;
468
+ }
438
469
  var quicEpoch;
439
470
  if (epoch === 0) quicEpoch = 'initial';
440
471
  else if (epoch === 1) quicEpoch = 'handshake';
@@ -525,7 +556,6 @@ function TLSBridge(options) {
525
556
  on: function (name, fn) { ev.on(name, fn); },
526
557
  off: function (name, fn) { ev.off(name, fn); },
527
558
  feedMessage: feedMessage,
528
- requestKeyUpdate: function () { if (session) session.requestKeyUpdate(true); },
529
559
  getHandshakeSecrets: function () {
530
560
  if (!session) return null;
531
561
  var hs = session.getHandshakeSecrets();
@@ -662,27 +692,17 @@ function TLSBridge(options) {
662
692
  * fingerprint, fingerprint256, serialNumber, raw }, or {} when none. On a
663
693
  * server, the one the SNI lookup chose; on a resumed connection, the one
664
694
  * the lookup chose again for the same name.
695
+ *
696
+ * lemon-tls's certificateObject: the one implementation, the one its
697
+ * TLSSocket and getPeerCertificate use. This built it apiece, with
698
+ * subject and issuer as OpenSSL's text ('CN=a\nO=b') where node:tls
699
+ * gives objects ({ CN: 'a', O: 'b' }).
665
700
  */
666
701
  getCertificate: function () {
667
702
  var chain = session && session.getCertificate ? session.getCertificate() : null;
668
703
  if (!chain || chain.length === 0) return {};
669
- try {
670
- var der = chain[0].cert || chain[0];
671
- var x509 = new X509Certificate(Buffer.isBuffer(der) ? der : Buffer.from(der));
672
- return {
673
- subject: x509.subject,
674
- issuer: x509.issuer,
675
- subjectaltname: x509.subjectAltName,
676
- valid_from: x509.validFrom,
677
- valid_to: x509.validTo,
678
- fingerprint: x509.fingerprint,
679
- fingerprint256: x509.fingerprint256,
680
- serialNumber: x509.serialNumber,
681
- raw: der
682
- };
683
- } catch (e) {
684
- return {};
685
- }
704
+ var der = chain[0].cert || chain[0];
705
+ return certificateObject(Buffer.isBuffer(der) ? der : Buffer.from(der)) || {};
686
706
  },
687
707
 
688
708
  close: function () { if (session && session.close) session.close(); }
package/src/transport.js CHANGED
@@ -734,7 +734,9 @@ function parse_quic_frames(buf) {
734
734
  frames.push({ type: 'max_stream_data', id: id.value, max: max.value });
735
735
 
736
736
  } else if (type === 0x12 || type === 0x13) {
737
- var max = safeReadVarInt(); if (!max) break;
737
+ // A count past 2^60 is not one (RFC 9000 §19.11): FRAME_ENCODING_ERROR.
738
+ var at = offset;
739
+ var max = safeReadVarInt(); if (!max || streamCountTooLarge(buf.subarray(at, offset))) break;
738
740
  frames.push({ type: type === 0x12 ? 'max_streams_bidi' : 'max_streams_uni', max: max.value });
739
741
 
740
742
  } else if (type === 0x14) {
@@ -747,7 +749,9 @@ function parse_quic_frames(buf) {
747
749
  frames.push({ type: 'stream_data_blocked', id: id.value, limit: limit.value });
748
750
 
749
751
  } else if (type === 0x16 || type === 0x17) {
750
- var limit = safeReadVarInt(); if (!limit) break;
752
+ // Likewise (§19.14: STREAM_LIMIT_ERROR or FRAME_ENCODING_ERROR).
753
+ var at = offset;
754
+ var limit = safeReadVarInt(); if (!limit || streamCountTooLarge(buf.subarray(at, offset))) break;
751
755
  frames.push({ type: type === 0x16 ? 'streams_blocked_bidi' : 'streams_blocked_uni', limit: limit.value });
752
756
 
753
757
  } else if (type === 0x18) {
package/src/utils.js CHANGED
@@ -410,7 +410,43 @@ function creditAsRead(readable, credit) {
410
410
  return function (n) { received += n; };
411
411
  }
412
412
 
413
+ // ── UDP socket buffers ──
414
+ //
415
+ // The kernel's receive buffer is where datagrams wait for the event loop.
416
+ // The OS default (208 KB on Linux) holds some 180 of them: a burst larger
417
+ // than one turn drains is dropped there, silently — loss to QUIC, so a
418
+ // retransmission and a congestion-window cut on a lossless link. 4 MB by
419
+ // default, for the send buffer too (a full one drops what is sent, the same
420
+ // way). The kernel caps both at net.core.rmem_max / wmem_max.
421
+ // socketBufferSize: bytes; 0 or false — the OS default.
422
+ var DEFAULT_SOCKET_BUFFER_SIZE = 4 << 20;
423
+ function checkedSocketBufferSize(v, what) {
424
+ if (v === undefined || v === null) return DEFAULT_SOCKET_BUFFER_SIZE;
425
+ if (v === 0 || v === false) return 0;
426
+ if (!Number.isInteger(v) || v < 0 || v > 0x7fffffff) {
427
+ var e = new RangeError((what || 'socketBufferSize') + ' must be a positive integer (bytes), 0 or false, got ' + v);
428
+ e.code = 'ERR_OUT_OF_RANGE';
429
+ throw e;
430
+ }
431
+ return v;
432
+ }
433
+ // For dgram.createSocket: applied when the socket is bound.
434
+ function udpSocketOptions(base, size) {
435
+ return size > 0 ? Object.assign({}, base, { recvBufferSize: size, sendBufferSize: size }) : base;
436
+ }
437
+ // For a socket already bound. A platform that refuses the size does not stop
438
+ // the socket from working.
439
+ function setSocketBuffers(sock, size) {
440
+ if (!(size > 0)) return;
441
+ try { sock.setSendBufferSize(size); } catch (e) {}
442
+ try { sock.setRecvBufferSize(size); } catch (e) {}
443
+ }
444
+
413
445
  export {
446
+ DEFAULT_SOCKET_BUFFER_SIZE,
447
+ checkedSocketBufferSize,
448
+ udpSocketOptions,
449
+ setSocketBuffers,
414
450
  createEmitter,
415
451
  mergePriority,
416
452
  creditAsRead,
@@ -17,7 +17,8 @@ import dns from 'node:dns';
17
17
  import { H3Connection, wtCloseCapsule, createCapsuleReader } from './h3.js';
18
18
  import { createQuicClientSocket } from './quic_socket.js';
19
19
  import lemonTls from 'lemon-tls';
20
- import { createHash, X509Certificate } from 'node:crypto';
20
+ import { createHash } from 'node:crypto';
21
+ import { x509 } from 'cryptographic-formats';
21
22
  import { readVarInt, wt_datagram_payload } from './utils.js';
22
23
  import { createWtStream, openWtStream, createSendGroup, wtScheduling, createDatagrams, sessionStats, setErrorCode,
23
24
  exporterArgs, exportSessionKeyingMaterial,
@@ -425,6 +426,7 @@ class WebTransport extends EventEmitter {
425
426
  this._udpSocket = createQuicClientSocket({
426
427
  remoteIp: remoteIp,
427
428
  remotePort: remotePort,
429
+ socketBufferSize: this._options.socketBufferSize,
428
430
  hostname: this._hostname,
429
431
  // serverCertificateHashes pins the certificate instead of a CA (checked
430
432
  // in onConnect, before anything is sent on the connection).
@@ -693,8 +695,11 @@ class WebTransport extends EventEmitter {
693
695
  return v.length === digest.length && v.equals(digest);
694
696
  });
695
697
  if (!match) return new Error('serverCertificateHashes: the certificate matches none of them');
696
- var cert = new X509Certificate(der), now = Date.now();
697
- var from = Date.parse(cert.validFrom), to = Date.parse(cert.validTo);
698
+ // Its dates, read by cryptographic-formats
699
+ var cert;
700
+ try { cert = x509.from_der(der); } catch (e) { return new Error('serverCertificateHashes: the certificate does not parse: ' + e.message); }
701
+ var now = Date.now();
702
+ var from = cert.validity.not_before.date.getTime(), to = cert.validity.not_after.date.getTime();
698
703
  if (!(now >= from && now <= to)) return new Error('serverCertificateHashes: the certificate is not valid now');
699
704
  if (to - from > 14 * 24 * 3600 * 1000) return new Error('serverCertificateHashes: the certificate is valid for more than 14 days');
700
705
  return null;