quico 0.7.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
 
@@ -201,8 +207,28 @@ function createQuicServer(options) {
201
207
  // gave it, with the time, sealed (AES-256-GCM) to the client's IP address
202
208
  // under a key of this server's own. It lives seconds, so a key drawn at
203
209
  // start is enough: a restart costs only the Retries in flight.
210
+ //
211
+ // tokenSecret (node:quic's name, at least 16 bytes) gives the key instead:
212
+ // servers that pass the same one open each other's tokens. That is what
213
+ // several servers sharing a port need (one per thread, SO_REUSEPORT): a
214
+ // client back with a NEW_TOKEN from another port is hashed to another of
215
+ // them, and without the key its token does not open — no harm, only the
216
+ // shortcut lost. The AES key is derived from it (SHA-256), so a 16-byte
217
+ // secret serves as well as a 32-byte one.
204
218
  var validateAddress = options.validateAddress === true;
205
- var retryTokenKey = crypto.randomBytes(32);
219
+ var tokenSecret = checkTokenSecret(options.tokenSecret) || crypto.randomBytes(32);
220
+ var retryTokenKey = tokenKeyOf(tokenSecret);
221
+
222
+ function checkTokenSecret(secret) {
223
+ if (secret === undefined || secret === null) return null;
224
+ if (!(secret instanceof Uint8Array) || secret.byteLength < 16) {
225
+ throw new TypeError('tokenSecret must be a Buffer or Uint8Array of at least 16 bytes');
226
+ }
227
+ return secret;
228
+ }
229
+ function tokenKeyOf(secret) {
230
+ return crypto.createHash('sha256').update('quico address-validation token key').update(secret).digest();
231
+ }
206
232
  var RETRY_TOKEN_LIFETIME = 10000; // ms
207
233
  // Tokens of two kinds, sealed alike (RFC 9000 §8.1.3): a Retry's, good for
208
234
  // seconds and one connection attempt; and a NEW_TOKEN's, given to a client
@@ -478,6 +504,7 @@ function createQuicServer(options) {
478
504
  // in length". Shorter ones would also be easy to guess and collide.
479
505
  if (firstPkt.dcid.byteLength < 8) return;
480
506
  if (connectionCount >= maxConnections) return;
507
+ if (!accepting) return;
481
508
 
482
509
  var retry = null, knownAddress = false;
483
510
  if (firstPkt.token && firstPkt.token.byteLength > 0) {
@@ -723,8 +750,8 @@ function createQuicServer(options) {
723
750
  var SERVER_RUNTIME = ['validateAddress', 'handshakeLoadThreshold', 'maxHandshakesPerAddress', 'earlyData', 'earlyDataMaxAge'];
724
751
  var ELSEWHERE = {
725
752
  key: 'setSecureContext()', cert: 'setSecureContext()', ca: 'setSecureContext()', pfx: 'setSecureContext()',
726
- passphrase: 'setSecureContext()', SNICallback: 'setSecureContext()',
727
- ticketKeys: 'setTicketKeys()', ech: 'setEchKeys()', resetTokenSecret: 'setResetTokenSecret()',
753
+ passphrase: 'setSecureContext()', SNICallback: 'addContext() or setSecureContext()',
754
+ ticketKeys: 'setTicketKeys()', ech: 'setEchKeys()', resetTokenSecret: 'setResetTokenSecret()', tokenSecret: 'setTokenSecret()',
728
755
  transportParams: 'connectionOptions, per connection'
729
756
  };
730
757
  function optErr(Ctor, code, msg) { var e = new Ctor(msg); e.code = code; return e; }
@@ -805,6 +832,17 @@ function createQuicServer(options) {
805
832
  // UDP send
806
833
  // ============================================================
807
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
+
808
846
  function sendUdp(toIp, toPort, data) {
809
847
  var isV6 = toIp.indexOf(':') >= 0;
810
848
  // Prefer external socket (shared mode) over internally-bound socket
@@ -815,7 +853,7 @@ function createQuicServer(options) {
815
853
  sendsPending++;
816
854
  socket.send(data, toPort, toIp, function (err) {
817
855
  sendsPending--;
818
- if (err && !closing) ev.emit('error', err);
856
+ if (err && !closing && !isDepartedPeer(err)) ev.emit('error', err);
819
857
  if (closing && sendsPending === 0) closing();
820
858
  });
821
859
  }
@@ -826,6 +864,11 @@ function createQuicServer(options) {
826
864
  // timeout. close() waits for them — not long (SEND_DRAIN_MAX).
827
865
  var sendsPending = 0;
828
866
  var shutDown = false;
867
+ // stopAccepting(): no new connection (an Initial for an unknown one is
868
+ // dropped); those open go on — a graceful close, the layer above deciding
869
+ // when they end.
870
+ var accepting = true;
871
+ var refed = true;
829
872
  var closing = null;
830
873
  var SEND_DRAIN_MAX = 200;
831
874
 
@@ -836,6 +879,7 @@ function createQuicServer(options) {
836
879
 
837
880
  function listen(listenPort, host, callback) {
838
881
  shutDown = false;
882
+ accepting = true;
839
883
  if (typeof host === 'function') { callback = host; host = null; }
840
884
  // `|| 443` turned port 0 into 443 — and port 0 is not "no port": it is how
841
885
  // a caller asks the OS for a free one (server.js has the same fix).
@@ -855,14 +899,8 @@ function createQuicServer(options) {
855
899
  // decrypted a second time and dropped as a duplicate, and every non-QUIC
856
900
  // packet — media included, on the hottest path there is — went through
857
901
  // the QUIC parser before being discarded.
858
- if (externalSocket4) {
859
- try { externalSocket4.setSendBufferSize(2 * 1024 * 1024); } catch (e) {}
860
- try { externalSocket4.setRecvBufferSize(2 * 1024 * 1024); } catch (e) {}
861
- }
862
- if (externalSocket6) {
863
- try { externalSocket6.setSendBufferSize(2 * 1024 * 1024); } catch (e) {}
864
- try { externalSocket6.setRecvBufferSize(2 * 1024 * 1024); } catch (e) {}
865
- }
902
+ if (externalSocket4) setSocketBuffers(externalSocket4, socketBufferSize);
903
+ if (externalSocket6) setSocketBuffers(externalSocket6, socketBufferSize);
866
904
  startSweepTimer();
867
905
  if (typeof callback === 'function') setImmediate(callback);
868
906
  return;
@@ -875,63 +913,108 @@ function createQuicServer(options) {
875
913
  // recovery engages and throughput collapses. See LOSS_PTO_PLAN 7.4: this
876
914
  // is what defeated the third pacer-calibration attempt.
877
915
  //
878
- // try/catch because some platforms cap the value and throw; not being
879
- // able to enlarge the buffer must not stop the server from listening.
880
- function growBuffers(sock) {
881
- try { sock.setSendBufferSize(2 * 1024 * 1024); } catch (e) {}
882
- 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
+ });
883
936
  }
884
937
 
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.
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.
894
946
  //
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));
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);
907
982
  return;
908
983
  }
909
984
  if (typeof callback === 'function') callback();
910
985
  }
911
986
 
912
- udp4 = dgram.createSocket('udp4');
913
- udp4.on('message', onUdpMessage);
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
- });
921
- var host4 = host.indexOf('.') !== -1 ? host : '0.0.0.0';
922
- udp4.bind(port, host4, function () { growBuffers(udp4); settle(null); });
923
-
924
- udp6 = dgram.createSocket({ type: 'udp6', ipv6Only: true });
925
- udp6.on('message', onUdpMessage);
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);
932
- });
933
- var host6 = host.indexOf(':') !== -1 ? host : '::';
934
- 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
+ }
935
1018
 
936
1019
  startSweepTimer();
937
1020
  }
@@ -1004,13 +1087,47 @@ function createQuicServer(options) {
1004
1087
  // External packet API (for shared UDP port scenarios)
1005
1088
  // ============================================================
1006
1089
 
1007
- // Query whether the given 5-tuple matches an active QUIC connection.
1008
- // Used by the demuxer to disambiguate byte 0 in range 64-79 between
1009
- // QUIC short header and TURN ChannelData (RFC 9443). Fresh connections
1010
- // must be classified by byte 0 range (80-127 or 192+ = QUIC).
1011
- function hasConnection(rinfo) {
1012
- var key = rinfo.address + ':' + rinfo.port;
1013
- return (key in addressMap) && (addressMap[key] in connections);
1090
+ // Query whether a datagram belongs to an active QUIC connection.
1091
+ //
1092
+ // hasConnection(rinfo) — by the address it came from. Used by the demuxer
1093
+ // to disambiguate byte 0 in range 64-79 between QUIC short header and TURN
1094
+ // ChannelData (RFC 9443). Fresh connections must be classified by byte 0
1095
+ // range (80-127 or 192+ = QUIC).
1096
+ //
1097
+ // hasConnection(rinfo, msg) — by the connection ID the datagram is
1098
+ // addressed to, from wherever it came: the answer to "is this one mine"
1099
+ // when several servers share the port (one per thread, SO_REUSEPORT) and a
1100
+ // datagram can reach one that does not hold its connection — a client
1101
+ // whose address changed is hashed to another socket. Asked before
1102
+ // handlePacket: given a short-header packet for an ID it does not know,
1103
+ // handlePacket answers with a Stateless Reset, and that would end a
1104
+ // connection another server holds. The address does not count here: an ID
1105
+ // that leads nowhere is not ours, whatever address sent it.
1106
+ function hasConnection(rinfo, msg) {
1107
+ if (msg === undefined || msg === null) {
1108
+ var key = rinfo.address + ':' + rinfo.port;
1109
+ return (key in addressMap) && (addressMap[key] in connections);
1110
+ }
1111
+ var dcid = datagramDcid(msg);
1112
+ if (!dcid) return false;
1113
+ var k = cidKey(dcid);
1114
+ return (k in cidMap) && (cidMap[k] in connections);
1115
+ }
1116
+
1117
+ // The Destination Connection ID of a datagram's first packet, without
1118
+ // parsing the rest (RFC 9000 §17.2, §17.3; RFC 8999 for any version): a
1119
+ // long header carries its length, a short header ours (SERVER_CID_LEN).
1120
+ // null for one too short to hold it.
1121
+ function datagramDcid(msg) {
1122
+ if (!(msg instanceof Uint8Array) || msg.length < 1) return null;
1123
+ if (msg[0] & 0x80) {
1124
+ if (msg.length < 6) return null;
1125
+ var len = msg[5];
1126
+ if (msg.length < 6 + len) return null;
1127
+ return msg.subarray(6, 6 + len);
1128
+ }
1129
+ if (msg.length < 1 + SERVER_CID_LEN) return null;
1130
+ return msg.subarray(1, 1 + SERVER_CID_LEN);
1014
1131
  }
1015
1132
 
1016
1133
 
@@ -1021,14 +1138,27 @@ function createQuicServer(options) {
1021
1138
  return {
1022
1139
  listen: listen,
1023
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
+ },
1147
+ stopAccepting: function () { accepting = false; },
1148
+ get connectionCount() { return connectionCount; },
1149
+ // node:dgram's: whether the process may exit while this listens.
1150
+ ref: function () { refed = true; if (udp4) udp4.ref(); if (udp6) udp6.ref(); },
1151
+ unref: function () { refed = false; if (udp4) udp4.unref(); if (udp6) udp6.unref(); },
1024
1152
  on: function (name, fn) { ev.on(name, fn); },
1025
1153
 
1026
1154
  /** Feed an incoming UDP packet when running in external-socket mode.
1027
1155
  * rinfo must be in Node's dgram format: { address, port, family, size }. */
1028
1156
  handlePacket: onUdpMessage,
1029
1157
 
1030
- /** Returns true if the 5-tuple matches an active QUIC connection.
1031
- * Used by demuxers for routing decisions on shared UDP ports. */
1158
+ /** hasConnection(rinfo): the address matches an active QUIC connection.
1159
+ * hasConnection(rinfo, msg): the datagram is addressed to one (by its
1160
+ * connection ID). Used by demuxers for routing decisions on shared UDP
1161
+ * ports — the second when several servers share it (see above). */
1032
1162
  hasConnection: hasConnection,
1033
1163
 
1034
1164
  /** The session ticket keys — tls.Server's names. A new key applies to
@@ -1059,7 +1189,18 @@ function createQuicServer(options) {
1059
1189
  setOptions: setOptions,
1060
1190
 
1061
1191
  getResetTokenSecret: function () { return resetTokenSecret; },
1062
- setResetTokenSecret: function (secret) { resetTokenSecret = checkResetTokenSecret(secret) || resetTokenSecret; }
1192
+ setResetTokenSecret: function (secret) { resetTokenSecret = checkResetTokenSecret(secret) || resetTokenSecret; },
1193
+
1194
+ /** The address-validation token secret (Retry, NEW_TOKEN — see
1195
+ * tokenSecret above). A new one applies from now on; tokens sealed
1196
+ * under the one before no longer open: a client back with a NEW_TOKEN
1197
+ * loses its shortcut, and one mid-Retry its attempt (it accepts no
1198
+ * second Retry). */
1199
+ getTokenSecret: function () { return tokenSecret; },
1200
+ setTokenSecret: function (secret) {
1201
+ var s = checkTokenSecret(secret);
1202
+ if (s) { tokenSecret = s; retryTokenKey = tokenKeyOf(s); }
1203
+ }
1063
1204
  };
1064
1205
  }
1065
1206
 
@@ -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.