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.
- package/README.md +2 -2
- package/package.json +4 -3
- package/src/client.js +1 -0
- package/src/connect_udp.js +5 -2
- package/src/connect_udp_client.js +1 -0
- package/src/crypto.js +6 -0
- package/src/h3.js +86 -10
- package/src/h3_client.js +1 -0
- package/src/h3_server.js +3 -0
- package/src/qpack.js +11 -2
- package/src/quic_connection.js +179 -40
- package/src/quic_server.js +121 -61
- package/src/quic_socket.js +5 -5
- package/src/server.js +24 -15
- package/src/tls_bridge.js +58 -38
- package/src/transport.js +6 -2
- package/src/utils.js +36 -0
- package/src/webtransport.js +8 -3
package/src/quic_server.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
//
|
|
906
|
-
//
|
|
907
|
-
function growBuffers(sock) {
|
|
908
|
-
|
|
909
|
-
|
|
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
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
//
|
|
918
|
-
//
|
|
919
|
-
//
|
|
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
|
-
//
|
|
923
|
-
//
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
function
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
if (
|
|
932
|
-
|
|
933
|
-
|
|
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
|
-
|
|
940
|
-
|
|
941
|
-
udp4.
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
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.
|
package/src/quic_socket.js
CHANGED
|
@@ -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
|
-
//
|
|
92
|
-
//
|
|
93
|
-
|
|
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
|
|
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,
|
|
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
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
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
|
-
|
|
670
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
package/src/webtransport.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
697
|
-
var
|
|
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;
|