diff --git a/LICENSE b/LICENSE
index 0443d15f3c60c2..12114764d2609d 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1316,6 +1316,58 @@ The externally maintained libraries used by Node.js are:
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
"""
+- ngtcp2, located at deps/ngtcp2, is licensed as follows:
+ """
+ The MIT License
+
+ Copyright (c) 2016 ngtcp2 contributors
+
+ Permission is hereby granted, free of charge, to any person obtaining
+ a copy of this software and associated documentation files (the
+ "Software"), to deal in the Software without restriction, including
+ without limitation the rights to use, copy, modify, merge, publish,
+ distribute, sublicense, and/or sell copies of the Software, and to
+ permit persons to whom the Software is furnished to do so, subject to
+ the following conditions:
+
+ The above copyright notice and this permission notice shall be
+ included in all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ """
+
+- nghttp3, located at deps/nghttp3, is licensed as follows:
+ """
+ The MIT License
+
+ Copyright (c) 2019 nghttp3 contributors
+
+ Permission is hereby granted, free of charge, to any person obtaining
+ a copy of this software and associated documentation files (the
+ "Software"), to deal in the Software without restriction, including
+ without limitation the rights to use, copy, modify, merge, publish,
+ distribute, sublicense, and/or sell copies of the Software, and to
+ permit persons to whom the Software is furnished to do so, subject to
+ the following conditions:
+
+ The above copyright notice and this permission notice shall be
+ included in all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ """
+
- node-inspect, located at deps/node-inspect, is licensed as follows:
"""
Copyright Node.js contributors. All rights reserved.
diff --git a/configure.py b/configure.py
index 87d99f75be6573..33cba46354bcb5 100755
--- a/configure.py
+++ b/configure.py
@@ -122,6 +122,11 @@
dest='error_on_warn',
help='Turn compiler warnings into errors for node core sources.')
+parser.add_option('--experimental-quic',
+ action='store_true',
+ dest='experimental_quic',
+ help='enable experimental quic support')
+
parser.add_option('--gdb',
action='store_true',
dest='gdb',
@@ -269,6 +274,48 @@
dest='shared_nghttp2_libpath',
help='a directory to search for the shared nghttp2 DLLs')
+shared_optgroup.add_option('--shared-ngtcp2',
+ action='store_true',
+ dest='shared_ngtcp2',
+ help='link to a shared ngtcp2 DLL instead of static linking')
+
+shared_optgroup.add_option('--shared-ngtcp2-includes',
+ action='store',
+ dest='shared_ngtcp2_includes',
+ help='directory containing ngtcp2 header files')
+
+shared_optgroup.add_option('--shared-ngtcp2-libname',
+ action='store',
+ dest='shared_ngtcp2_libname',
+ default='ngtcp2',
+ help='alternative lib name to link to [default: %default]')
+
+shared_optgroup.add_option('--shared-ngtcp2-libpath',
+ action='store',
+ dest='shared_ngtcp2_libpath',
+ help='a directory to search for the shared ngtcp2 DLLs')
+
+shared_optgroup.add_option('--shared-nghttp3',
+ action='store_true',
+ dest='shared_nghttp3',
+ help='link to a shared nghttp3 DLL instead of static linking')
+
+shared_optgroup.add_option('--shared-nghttp3-includes',
+ action='store',
+ dest='shared_nghttp3_includes',
+ help='directory containing nghttp3 header files')
+
+shared_optgroup.add_option('--shared-nghttp3-libname',
+ action='store',
+ dest='shared_nghttp3_libname',
+ default='nghttp3',
+ help='alternative lib name to link to [default: %default]')
+
+shared_optgroup.add_option('--shared-nghttp3-libpath',
+ action='store',
+ dest='shared_nghttp3_libpath',
+ help='a directory to search for the shared nghttp3 DLLs')
+
shared_optgroup.add_option('--shared-openssl',
action='store_true',
dest='shared_openssl',
@@ -1178,6 +1225,14 @@ def configure_node(o):
else:
o['variables']['debug_nghttp2'] = 'false'
+ if options.experimental_quic:
+ if options.shared_openssl:
+ raise Exception('QUIC requires a modified version of OpenSSL and '
+ 'cannot be enabled when using --shared-openssl.')
+ o['variables']['experimental_quic'] = 1
+ else:
+ o['variables']['experimental_quic'] = 'false'
+
o['variables']['node_no_browser_globals'] = b(options.no_browser_globals)
o['variables']['node_shared'] = b(options.shared)
@@ -1309,6 +1364,8 @@ def without_ssl_error(option):
without_ssl_error('--openssl-fips')
if options.openssl_default_cipher_list:
without_ssl_error('--openssl-default-cipher-list')
+ if options.experimental_quic:
+ without_ssl_error('--experimental-quic')
return
if options.use_openssl_ca_store:
diff --git a/deps/openssl/openssl.gyp b/deps/openssl/openssl.gyp
index 4609d83baabac1..3dfe0b8c8a01f7 100644
--- a/deps/openssl/openssl.gyp
+++ b/deps/openssl/openssl.gyp
@@ -16,6 +16,13 @@
'OPENSSL_NO_HW',
],
'conditions': [
+ [
+ # Disable building QUIC support in openssl if experimental_quic
+ # is not enabled.
+ 'experimental_quic!=1', {
+ 'defines': ['OPENSSL_NO_QUIC=1'],
+ }
+ ],
[ 'openssl_no_asm==1', {
'includes': ['./openssl_no_asm.gypi'],
}, 'target_arch=="arm64" and OS=="win"', {
diff --git a/doc/api/errors.md b/doc/api/errors.md
index 3079e2031d7a9f..fdc3cc1e6ecb42 100644
--- a/doc/api/errors.md
+++ b/doc/api/errors.md
@@ -1717,6 +1717,125 @@ Accessing `Object.prototype.__proto__` has been forbidden using
[`Object.setPrototypeOf`][] should be used to get and set the prototype of an
object.
+
+### `ERR_QUIC_CANNOT_SET_GROUPS`
+
+> Stability: 1 - Experimental
+
+TBD
+
+
+### `ERR_QUIC_ERROR`
+
+> Stability: 1 - Experimental
+
+TBD
+
+
+### `ERR_QUIC_TLS13_REQUIRED`
+
+> Stability: 1 - Experimental
+
+TBD
+
+
+### `ERR_QUICCLIENTSESSION_FAILED`
+
+> Stability: 1 - Experimental
+
+TBD
+
+
+### `ERR_QUICCLIENTSESSION_FAILED_SETSOCKET`
+
+> Stability: 1 - Experimental
+
+TBD
+
+
+### `ERR_QUICSESSION_DESTROYED`
+
+> Stability: 1 - Experimental
+
+TBD
+
+
+### `ERR_QUICSESSION_INVALID_DCID`
+
+> Stability: 1 - Experimental
+
+TBD
+
+
+### `ERR_QUICSESSION_UPDATEKEY`
+
+> Stability: 1 - Experimental
+
+TBD
+
+
+### `ERR_QUICSESSION_VERSION_NEGOTIATION`
+
+> Stability: 1 - Experimental
+
+TBD
+
+
+### `ERR_QUICSOCKET_DESTROYED`
+
+> Stability: 1 - Experimental
+
+TBD
+
+
+### `ERR_QUICSOCKET_INVALID_STATELESS_RESET_SECRET_LENGTH`
+
+> Stability: 1 - Experimental
+
+TBD
+
+
+### `ERR_QUICSOCKET_LISTENING`
+
+> Stability: 1 - Experimental
+
+TBD
+
+
+### `ERR_QUICSOCKET_UNBOUND`
+
+> Stability: 1 - Experimental
+
+TBD
+
+
+### `ERR_QUICSTREAM_DESTROYED`
+
+> Stability: 1 - Experimental
+
+TBD
+
+
+### `ERR_QUICSTREAM_INVALID_PUSH`
+
+> Stability: 1 - Experimental
+
+TBD
+
+
+### `ERR_QUICSTREAM_OPEN_FAILED`
+
+> Stability: 1 - Experimental
+
+TBD
+
+
+### `ERR_QUICSTREAM_UNSUPPORTED_PUSH`
+
+> Stability: 1 - Experimental
+
+TBD
+
### `ERR_REQUIRE_ESM`
diff --git a/doc/api/index.md b/doc/api/index.md
index ec760f5342dc3f..ad9eeea2e6c694 100644
--- a/doc/api/index.md
+++ b/doc/api/index.md
@@ -44,6 +44,7 @@
* [Process](process.html)
* [Punycode](punycode.html)
* [Query Strings](querystring.html)
+* [QUIC](quic.html)
* [Readline](readline.html)
* [REPL](repl.html)
* [Report](report.html)
diff --git a/doc/api/net.md b/doc/api/net.md
index 89b3de2f3b8471..2e6861d4aa48c1 100644
--- a/doc/api/net.md
+++ b/doc/api/net.md
@@ -1105,6 +1105,14 @@ immediately initiates connection with
[`socket.connect(port[, host][, connectListener])`][`socket.connect(port)`],
then returns the `net.Socket` that starts the connection.
+## `net.createQuicSocket([options])`
+
+
+Creates and returns a new `QuicSocket`. Please refer to the [QUIC documentation][]
+for details.
+
## `net.createServer([options][, connectionListener])`
+
+> Stability: 1 - Experimental
+
+The `net` module provides an implementation of the QUIC protocol. To
+access it, the Node.js binary must be compiled using the
+`--experimental-quic` configuration flag.
+
+```js
+const { createQuicSocket } = require('net');
+```
+
+## Example
+
+```js
+'use strict';
+
+const key = getTLSKeySomehow();
+const cert = getTLSCertSomehow();
+
+const { createQuicSocket } = require('net');
+
+// Create the QUIC UDP IPv4 socket bound to local IP port 1234
+const socket = createQuicSocket({ endpoint: { port: 1234 } });
+
+// Tell the socket to operate as a server using the given
+// key and certificate to secure new connections, using
+// the fictional 'hello' application protocol.
+socket.listen({ key, cert, alpn: 'hello' });
+
+socket.on('session', (session) => {
+ // A new server side session has been created!
+
+ session.on('secure', () => {
+ // Once the TLS handshake is completed, we can
+ // open streams...
+ const uni = session.openStream({ halfOpen: true });
+ uni.write('hi ');
+ uni.end('from the server!');
+ });
+
+ // The peer opened a new stream!
+ session.on('stream', (stream) => {
+ // Let's say hello
+ stream.end('Hello World');
+
+ // Let's see what the peer has to say...
+ stream.setEncoding('utf8');
+ stream.on('data', console.log);
+ stream.on('end', () => console.log('stream ended'));
+ });
+});
+
+socket.on('listening', () => {
+ // The socket is listening for sessions!
+});
+```
+
+## QUIC Basics
+
+QUIC is a UDP-based network transport protocol that includes built-in security
+via TLS 1.3, flow control, error correction, connection migration,
+multiplexing, and more.
+
+Within the Node.js implementation of the QUIC protocol, there are three main
+components: the `QuicSocket`, the `QuicSession` and the `QuicStream`.
+
+### QuicSocket
+
+A `QuicSocket` encapsulates a binding to one or more local UDP ports. It is
+used to send data to, and receive data from, remote endpoints. Once created,
+a `QuicSocket` is associated with a local network address and IP port and can
+act as both a QUIC client and server simultaneously. User code at the
+JavaScript level interacts with the `QuicSocket` object to:
+
+* Query or modified the properties of the local UDP binding;
+* Create client `QuicSession` instances;
+* Wait for server `QuicSession` instances; or
+* Query activity statistics
+
+Unlike the `net.Socket` and `tls.TLSSocket`, a `QuicSocket` instance cannot be
+directly used by user code at the JavaScript level to send or receive data over
+the network.
+
+### Client and Server QuicSessions
+
+A `QuicSession` represents a logical connection between two QUIC endpoints (a
+client and a server). In the JavaScript API, each is represented by the
+`QuicClientSession` and `QuicServerSession` specializations.
+
+At any given time, a `QuicSession` exists is one of four possible states:
+
+* `Initial` - Entered as soon as the `QuicSession` is created.
+* `Handshake` - Entered as soon as the TLS 1.3 handshake between the client and
+ server begins. The handshake is always initiated by the client.
+* `Ready` - Entered as soon as the TLS 1.3 handshake completes. Once the
+ `QuicSession` enters the `Ready` state, it may be used to exchange
+ application data using `QuicStream` instances.
+* `Closed` - Entere as soon as the `QuicSession` connection has been
+ terminated.
+
+New instances of `QuicClientSession` are created using the `connect()`
+function on a `QuicSocket` as in the example below:
+
+```js
+const { createQuicSocket } = require('net');
+
+// Create a QuicSocket associated with localhost and port 1234
+const socket = createQuicSocket({ endpoint: { port: 1234 } });
+
+const client = socket.connect({
+ address: 'example.com',
+ port: 4567,
+ alpn: 'foo'
+});
+```
+
+As soon as the `QuicClientSession` is created, the `address` provided in
+the connect options will be resolved to an IP address (if necessary), and
+the TLS 1.3 handshake will begin. The `QuicClientSession` cannot be used
+to exchange application data until after the `'secure'` event has been
+emitted by the `QuicClientSession` object, signaling the completion of
+the TLS 1.3 handshake.
+
+```js
+client.on('secure', () => {
+ // The QuicClientSession can now be used for application data
+});
+```
+
+New instances of `QuicServerSession` are created internally by the
+`QuicSocket` if it has been configured to listen for new connections
+using the `listen()` method.
+
+```js
+const key = getTLSKeySomehow();
+const cert = getTLSCertSomehow();
+
+socket.listen({
+ key,
+ cert,
+ alpn: 'foo'
+});
+
+socket.on('session', (session) => {
+ session.on('secure', () => {
+ // The QuicServerSession can now be used for application data
+ });
+});
+```
+
+As with client `QuicSession` instances, the `QuicServerSession` cannot be
+used to exhange application data until the `'secure'` event has been emitted.
+
+### QuicSession and ALPN
+
+QUIC uses the TLS 1.3 [ALPN][] ("Application-Layer Protocol Negotiation")
+extension to identify the application level protocol that is using the QUIC
+connection. Every `QuicSession` instance has an ALPN identifier that *must* be
+specified in either the `connect()` or `listen()` options. ALPN identifiers that
+are known to Node.js (such as the ALPN identifier for HTTP/3) will alter how the
+`QuicSession` and `QuicStream` objects operate internally, but the QUIC
+implementation for Node.js has been designed to allow any ALPN to be specified
+and used.
+
+### QuicStream
+
+Once a `QuicSession` transitions to the `Ready` state, `QuicStream` instances
+may be created and used to exchange application data. On a general level, all
+`QuicStream` instances are simply Node.js Duplex Streams that allow
+bidirectional data flow between the QUIC client and server. However, the
+application protocol negotiated for the `QuicSession` may alter the semantics
+and operation of a `QuicStream` associated with the session. Specifically,
+some features of the `QuicStream` (e.g. headers) are enabled only if the
+application protocol selected is known by Node.js to support those features.
+
+Once the `QuicSession` is ready, a `QuicStream` may be created by either the
+client or server, and may be unidirectional or bidirectional.
+
+The `openStream()` method is used to create a new `QuicStream`:
+
+```js
+// Create a new bidirectional stream
+const stream1 = session.openStream();
+
+// Create a new unidirectional stream
+const stream2 = session.openStream({ halfOpen: true });
+```
+
+As suggested by the names, a bidirectional stream allows data to be sent on
+a stream in both directions, by both client and server, regardless of which
+peer opened the stream. A unidirectional stream can be written to only by the
+QuicSession that opened it.
+
+The `'stream'` event is emitted by the `QuicSession` when a new `QuicStream`
+has been initated by the connected peer:
+
+```js
+session.on('stream', (stream) => {
+ if (stream.bidirectional) {
+ stream.write('Hello World');
+ stream.end();
+ }
+ stream.on('data', console.log);
+ stream.on('end', () => {});
+});
+```
+
+#### QuicStream Headers
+
+Some QUIC application protocols (like HTTP/3) make use of headers.
+
+There are four kinds of headers that the Node.js QUIC implementation
+is capable of handling dependent entirely on known application protocol
+support:
+
+* Informational Headers
+* Initial Headers
+* Trailing Headers
+* Push Headers
+
+These categories correlate exactly with the equivalent HTTP
+concepts:
+
+* Informational Headers: Any response headers transmitted within
+ a block of headers using a `1xx` status code.
+* Initial Headers: HTTP request or response headers
+* Trailing Headers: A block of headers that follow the body of a
+ request or response.
+* Push Promise Headers: A block of headers included in a promised
+ push stream.
+
+If headers are supported by the application protocol in use for
+a given `QuicSession`, the `'initialHeaders'`, `'informationalHeaders'`,
+and `'trailingHeaders'` events will be emitted by the `QuicStream`
+object when headers are received; and the `submitInformationalHeaders()`,
+`submitInitialHeaders()`, and `submitTrailingHeaders()` methods can be
+used to send headers.
+
+## QUIC and HTTP/3
+
+HTTP/3 is an application layer protocol that uses QUIC as the transport.
+
+TBD
+
+## QUIC JavaScript API
+
+### net.createQuicSocket(\[options\])
+
+
+* `options` {Object}
+ * `client` {Object} A default configuration for QUIC client sessions created
+ using `quicsocket.connect()`.
+ * `endpoint` {Object} An object describing the local address to bind to.
+ * `address` {string} The local address to bind to. This may be an IPv4 or
+ IPv6 address or a host name. If a host name is given, it will be resolved
+ to an IP address.
+ * `port` {number} The local port to bind to.
+ * `type` {string} Either `'udp4'` or `'upd6'` to use either IPv4 or IPv6,
+ respectively.
+ * `ipv6Only` {boolean}
+ * `lookup` {Function} A custom DNS lookup function. Default `dns.lookup()`.
+ * `maxConnections` {number} The maximum number of total active inbound
+ connections.
+ * `maxConnectionsPerHost` {number} The maximum number of inbound connections
+ allowed per remote host. Default: `100`.
+ * `maxStatelessResetsPerHost` {number} The maximum number of stateless
+ resets that the `QuicSocket` is permitted to send per remote host.
+ Default: `10`.
+ * `qlog` {boolean} Whether to emit ['qlog'][] events for incoming sessions.
+ (For outgoing client sessions, set `client.qlog`.) Default: `false`.
+ * `retryTokenTimeout` {number} The maximum number of *seconds* for retry token
+ validation. Default: `10` seconds.
+ * `server` {Object} A default configuration for QUIC server sessions.
+ * `validateAddress` {boolean} When `true`, the `QuicSocket` will use explicit
+ address validation using a QUIC `RETRY` frame when listening for new server
+ sessions. Default: `false`.
+ * `validateAddressLRU` {boolean} When `true`, validation will be skipped if
+ the address has been recently validated. Currently, only the 10 most
+ recently validated addresses are remembered. Setting `validateAddressLRU`
+ to `true`, will enable the `validateAddress` option as well. Default:
+ `false`.
+
+The `net.createQuicSocket()` function is used to create new `QuicSocket`
+instances associated with a local UDP address.
+
+### Class: QuicEndpoint
+
+
+The `QuicEndpoint` wraps a local UDP binding used by a `QuicSocket` to send
+and receive data. A single `QuicSocket` may be bound to multiple
+`QuicEndpoint` instances at any given time.
+
+Users will not create instances of `QuicEndpoint` directly.
+
+#### quicendpoint.addMembership(address, iface)
+
+
+* `address` {string}
+* `iface` {string}
+
+Tells the kernel to join a multicast group at the given `multicastAddress` and
+`multicastInterface` using the `IP_ADD_MEMBERSHIP` socket option. If the
+`multicastInterface` argument is not specified, the operating system will
+choose one interface and will add membership to it. To add membership to every
+available interface, call `addMembership()` multiple times, once per
+interface.
+
+#### quicendpoint.address
+
+
+* Type: Address
+
+An object containing the address information for a bound `QuicEndpoint`.
+
+The object will contain the properties:
+
+* `address` {string} The local IPv4 or IPv6 address to which the `QuicEndpoint` is
+ bound.
+* `family` {string} Either `'IPv4'` or `'IPv6'`.
+* `port` {number} The local IP port to which the `QuicEndpoint` is bound.
+
+If the `QuicEndpoint` is not bound, `quicendpoint.address` is an empty object.
+
+#### quicendpoint.bound
+
+
+* Type: {boolean}
+
+Set to `true` if the `QuicEndpoint` is bound to the local UDP port.
+
+#### quicendpoint.closing
+
+
+* Type: {boolean}
+
+Set to `true` if the `QuicEndpoint` is in the process of closing.
+
+#### quicendpoint.destroy(\[error\])
+
+
+* `error` {Object} An `Error` object.
+
+Closes and destroys the `QuicEndpoint` instance making it usuable.
+
+#### quicendpoint.destroyed
+
+
+* Type: {boolean}
+
+Set to `true` if the `QuicEndpoint` has been destroyed.
+
+#### quicendpoint.dropMembership(address, iface)
+
+
+* `address` {string}
+* `iface` {string}
+
+Instructs the kernel to leave a multicast group at `multicastAddress` using the
+`IP_DROP_MEMBERSHIP` socket option. This method is automatically called by the
+kernel when the socket is closed or the process terminates, so most apps will
+never have reason to call this.
+
+If `multicastInterface` is not specified, the operating system will attempt to
+drop membership on all valid interfaces.
+
+#### quicendpoint.fd
+
+
+* Type: {integer}
+
+The system file descriptor the `QuicEndpoint` is bound to. This property
+is not set on Windows.
+
+#### quicendpoint.pending
+
+
+* Type: {boolean}
+
+Set to `true` if the `QuicEndpoint` is in the process of binding to
+the local UDP port.
+
+#### quicendpoint.ref()
+
+
+#### quicendpoint.setBroadcast(\[on\])
+
+
+* `on` {boolean}
+
+Sets or clears the `SO_BROADCAST` socket option. When set to `true`, UDP
+packets may be sent to a local interface's broadcast address.
+
+#### quicendpoint.setMulticastInterface(iface)
+
+
+* `iface` {string}
+
+All references to scope in this section are referring to IPv6 Zone Indices,
+which are defined by [RFC 4007][]. In string form, an IP with a scope index
+is written as `'IP%scope'` where scope is an interface name or interface
+number.
+
+Sets the default outgoing multicast interface of the socket to a chosen
+interface or back to system interface selection. The multicastInterface must
+be a valid string representation of an IP from the socket's family.
+
+For IPv4 sockets, this should be the IP configured for the desired physical
+interface. All packets sent to multicast on the socket will be sent on the
+interface determined by the most recent successful use of this call.
+
+For IPv6 sockets, multicastInterface should include a scope to indicate the
+interface as in the examples that follow. In IPv6, individual send calls can
+also use explicit scope in addresses, so only packets sent to a multicast
+address without specifying an explicit scope are affected by the most recent
+successful use of this call.
+
+##### Examples: IPv6 Outgoing Multicast Interface
+
+On most systems, where scope format uses the interface name:
+
+```js
+const { createQuicSocket } = require('net');
+const socket = createQuicSocket({ endpoint: { type: 'udp6', port: 1234 } });
+
+socket.on('ready', () => {
+ socket.endpoints[0].setMulticastInterface('::%eth1');
+});
+```
+
+On Windows, where scope format uses an interface number:
+
+```js
+const { createQuicSocket } = require('net');
+const socket = createQuicSocket({ endpoint: { type: 'udp6', port: 1234 } });
+
+socket.on('ready', () => {
+ socket.endpoints[0].setMulticastInterface('::%2');
+});
+```
+
+##### Example: IPv4 Outgoing Multicast Interface
+
+All systems use an IP of the host on the desired physical interface:
+
+```js
+const { createQuicSocket } = require('net');
+const socket = createQuicSocket({ endpoint: { type: 'udp4', port: 1234 } });
+
+socket.on('ready', () => {
+ socket.endpoints[0].setMulticastInterface('10.0.0.2');
+});
+```
+
+##### Call Results
+
+A call on a socket that is not ready to send or no longer open may throw a
+Not running Error.
+
+If multicastInterface can not be parsed into an IP then an `EINVAL` System
+Error is thrown.
+
+On IPv4, if `multicastInterface` is a valid address but does not match any
+interface, or if the address does not match the family then a System Error
+such as `EADDRNOTAVAIL` or `EPROTONOSUP` is thrown.
+
+On IPv6, most errors with specifying or omitting scope will result in the
+socket continuing to use (or returning to) the system's default interface
+selection.
+
+A socket's address family's ANY address (IPv4 `'0.0.0.0'` or IPv6 `'::'`)
+can be used to return control of the sockets default outgoing interface to
+the system for future multicast packets.
+
+#### quicendpoint.setMulticastLoopback(\[on\])
+
+
+* `on` {boolean}
+
+Sets or clears the `IP_MULTICAST_LOOP` socket option. When set to `true`,
+multicast packets will also be received on the local interface.
+
+#### quicendpoint.setMulticastTTL(ttl)
+
+
+* `ttl` {number}
+
+Sets the `IP_MULTICAST_TTL` socket option. While TTL generally stands for
+"Time to Live", in this context it specifies the number of IP hops that a
+packet is allowed to travel through, specifically for multicast traffic. Each
+router or gateway that forwards a packet decrements the TTL. If the TTL is
+decremented to `0` by a router, it will not be forwarded.
+
+The argument passed to `setMulticastTTL()` is a number of hops between
+`0` and `255`. The default on most systems is `1` but can vary.
+
+#### quicendpoint.setTTL(ttl)
+
+
+* `ttl` {number}
+
+Sets the `IP_TTL` socket option. While TTL generally stands for "Time to Live",
+in this context it specifies the number of IP hops that a packet is allowed to
+travel through. Each router or gateway that forwards a packet decrements the
+TTL. If the TTL is decremented to `0` by a router, it will not be forwarded.
+Changing TTL values is typically done for network probes or when multicasting.
+
+The argument to `setTTL()` is a number of hops between `1` and `255`.
+The default on most systems is `64` but can vary.
+
+#### quicendpoint.unref()
+
+
+### Class: QuicSession extends EventEmitter
+
+* Extends: {EventEmitter}
+
+The `QuicSession` is an abstract base class that defines events, methods, and
+properties that are shared by both `QuicClientSession` and `QuicServerSession`.
+
+Users will not create instances of `QuicSession` directly.
+
+#### Event: `'close'`
+
+
+Emitted after the `QuicSession` has been destroyed and is no longer usable.
+
+The `'close'` event will not be emitted more than once.
+
+#### Event: `'error'`
+
+
+Emitted immediately before the `'close'` event if the `QuicSession` was
+destroyed with an error.
+
+The callback will be invoked with a single argument:
+
+* `error` {Object} An `Error` object.
+
+The `'error'` event will not be emitted more than once.
+
+#### Event: `'keylog'`
+
+
+Emitted when key material is generated or received by a `QuicSession`
+(typically during or immediately following the handshake process). This keying
+material can be stored for debugging, as it allows captured TLS traffic to be
+decrypted. It may be emitted multiple times per `QuicSession` instance.
+
+The callback will be invoked with a single argument:
+
+* `line` Line of ASCII text, in NSS SSLKEYLOGFILE format.
+
+A typical use case is to append received lines to a common text file, which is
+later used by software (such as Wireshark) to decrypt the traffic:
+
+```js
+const log = fs.createWriteStream('/tmp/ssl-keys.log', { flags: 'a' });
+// ...
+session.on('keylog', (line) => log.write(line));
+```
+
+The `'keylog'` event will be emitted multiple times.
+
+#### Event: `'pathValidation'`
+
+
+Emitted when a path validation result has been determined. This event
+is strictly informational. When path validation is successful, the
+`QuicSession` will automatically update to use the new validated path.
+
+The callback will be invoked with three arguments:
+
+* `result` {string} Either `'failure'` or `'success'`, denoting the status
+ of the path challenge.
+* `local` {Object} The local address component of the tested path.
+* `remote` {Object} The remote address component of the tested path.
+
+The `'pathValidation'` event will be emitted multiple times.
+
+#### Event: `'qlog'`
+
+
+* `jsonChunk` {string} A JSON fragment.
+
+Emitted if the `qlog: true` option was passed to `quicsocket.connect()` or
+`net.createQuicSocket()` functions.
+
+The argument is a JSON fragment according to the [qlog standard][].
+
+The `'qlog'` event will be emitted multiple times.
+
+#### Event: `'secure'`
+
+
+Emitted after the TLS handshake has been completed.
+
+The callback will be invoked with two arguments:
+
+* `servername` {string} The SNI servername requested by the client.
+* `alpnProtocol` {string} The negotiated ALPN protocol.
+* `cipher` {Object} Information about the selected cipher algorithm.
+ * `name` {string} The cipher algorithm name.
+ * `version` {string} The TLS version (currently always `'TLSv1.3'`).
+
+These will also be available using the `quicsession.servername`,
+`quicsession.alpnProtocol`, and `quicsession.cipher` properties.
+
+The `'secure'` event will not be emitted more than once.
+
+#### Event: `'stream'`
+
+
+Emitted when a new `QuicStream` has been initiated by the connected peer.
+
+The `'stream'` event may be emitted multiple times.
+
+#### quicsession.ackDelayRetransmitCount
+
+
+* Type: {bigint}
+
+A `BigInt` representing the number of retransmissions caused by delayed
+acknowledgements.
+
+#### quicsession.address
+
+
+* Type: {Object}
+ * `address` {string} The local IPv4 or IPv6 address to which the `QuicSession`
+ is bound.
+ * `family` {string} Either `'IPv4'` or `'IPv6'`.
+ * `port` {number} The local IP port to which the `QuicSocket` is bound.
+
+An object containing the local address information for the `QuicSocket` to which
+the `QuicSession` is currently associated.
+
+#### quicsession.alpnProtocol
+
+
+* Type: {string}
+
+The ALPN protocol identifier negotiated for this session.
+
+#### quicsession.authenticated
+
+* Type: {boolean}
+
+True if the certificate provided by the peer during the TLS 1.3
+handshake has been verified.
+
+#### quicsession.authenticationError
+
+* Type: {Object} An error object
+
+If `quicsession.authenticated` is false, returns an `Error` object
+representing the reason the peer certificate verification failed.
+
+#### quicsession.bidiStreamCount
+
+
+* Type: {bigint}
+
+A `BigInt` representing the total number of bidirectional streams
+created for this `QuicSession`.
+
+#### quicsession.blockCount
+
+
+* Type: {bigint}
+
+A `BigInt` representing the total number of times the `QuicSession` has
+been blocked from sending stream data due to flow control.
+
+Such blocks indicate that transmitted stream data is not being consumed
+quickly enough by the connected peer.
+
+#### quicsession.bytesInFlight
+
+
+* Type: {number}
+
+The total number of unacknowledged bytes this QUIC endpoint has transmitted
+to the connected peer.
+
+#### quicsession.bytesReceived
+
+
+* Type: {bigint}
+
+A `BigInt` representing the total number of bytes received from the peer.
+
+#### quicsession.bytesSent
+
+
+* Type: {bigint}
+
+A `BigInt` representing the total number of bytes sent to the peer.
+
+#### quicsession.cipher
+
+
+* Type: {Object}
+ * `name` {string} The cipher algorithm name.
+ * `type` {string} The TLS version (currently always `'TLSv1.3'`).
+
+Information about the cipher algorithm selected for the session.
+
+#### quicsession.close(\[callback\])
+
+
+* `callback` {Function} Callback invoked when the close operation is completed
+
+Begins a graceful close of the `QuicSession`. Existing `QuicStream` instances
+will be permitted to close naturally. New `QuicStream` instances will not be
+permitted. Once all `QuicStream` instances have closed, the `QuicSession`
+instance will be destroyed.
+
+#### quicsession.closeCode
+
+* Type: {Object}
+ * `code` {number} The error code reported when the `QuicSession` closed.
+ * `family` {number} The type of error code reported (`0` indicates a QUIC
+ protocol level error, `1` indicates a TLS error, `2` represents an
+ application level error.)
+
+#### quicsession.closing
+
+
+* Type: {boolean}
+
+Set to `true` if the `QuicSession` is in the process of a graceful shutdown.
+
+#### quicsession.destroy(\[error\])
+
+
+* `error` {any}
+
+Destroys the `QuicSession` immediately causing the `close` event to be emitted.
+If `error` is not `undefined`, the `error` event will be emitted immediately
+before the `close` event.
+
+Any `QuicStream` instances that are still opened will be abruptly closed.
+
+#### quicsession.destroyed
+
+
+* Type: {boolean}
+
+Set to `true` if the `QuicSession` has been destroyed.
+
+#### quicsession.duration
+
+
+* Type: {bigint}
+
+A `BigInt` representing the length of time the `QuicSession` was active.
+
+#### quicsession.getCertificate()
+
+
+* Returns: {Object} A [Certificate Object][].
+
+Returns an object representing the *local* certificate. The returned object has
+some properties corresponding to the fields of the certificate.
+
+If there is no local certificate, or if the `QuicSession` has been destroyed,
+an empty object will be returned.
+
+#### quicsession.getPeerCertificate(\[detailed\])
+
+
+* `detailed` {boolean} Include the full certificate chain if `true`, otherwise
+ include just the peer's certificate. **Default**: `false`.
+* Returns: {Object} A [Certificate Object][].
+
+Returns an object representing the peer's certificate. If the peer does not
+provide a certificate, or if the `QuicSession` has been destroyed, an empty
+object will be returned.
+
+If the full certificate chain was requested (`details` equals `true`), each
+certificate will include an `issuerCertificate` property containing an object
+representing the issuer's certificate.
+
+#### quicsession.handshakeAckHistogram
+
+
+TBD
+
+#### quicsession.handshakeContinuationHistogram
+
+
+TBD
+
+#### quicsession.handshakeComplete
+
+
+* Type: {boolean}
+
+Set to `true` if the TLS handshake has completed.
+
+#### quicsession.handshakeConfirmed
+
+
+* Type: {boolean}
+
+Set to `true` when the TLS handshake completion has been confirmed.
+
+#### quicsession.handshakeDuration
+
+
+* Type: {bigint}
+
+A `BigInt` representing the length of time taken to complete the TLS handshake.
+
+#### quicsession.idleTimeout
+
+
+* Type: {boolean}
+
+Set to `true` if the `QuicSession` was closed due to an idle timeout.
+
+#### quicsession.keyUpdateCount
+
+
+* Type: {bigint}
+
+A `BigInt` representing the number of key update operations that have
+occured.
+
+#### quicsession.latestRTT
+
+
+* Type: {bigint}
+
+The most recently recorded RTT for this `QuicSession`.
+
+#### quicsession.lossRetransmitCount
+
+
+* Type: {bigint}
+
+A `BigInt` representing the number of lost-packet retransmissions that have been
+performed on this `QuicSession`.
+
+#### quicsession.maxDataLeft
+
+
+* Type: {number}
+
+The total number of bytes the `QuicSession` is *currently* allowed to
+send to the connected peer.
+
+#### quicsession.maxInFlightBytes
+
+
+* Type: {bigint}
+
+A `BigInt` representing the maximum number of in-flight bytes recorded
+for this `QuicSession`.
+
+#### quicsession.maxStreams
+
+
+* Type: {Object}
+ * `uni` {number} The maximum number of unidirectional streams.
+ * `bidi` {number} The maximum number of bidirectional streams.
+
+The highest cumulative number of bidirectional and unidirectional streams
+that can currently be opened. The values are set initially by configuration
+parameters when the `QuicSession` is created, then updated over the lifespan
+of the `QuicSession` as the connected peer allows new streams to be created.
+
+#### quicsession.minRTT
+
+
+* Type: {bigint}
+
+The minimum RTT recorded so far for this `QuicSession`.
+
+#### quicsession.openStream(\[options\])
+
+* `options` {Object}
+ * `halfOpen` {boolean} Set to `true` to open a unidirectional stream, `false`
+ to open a bidirectional stream. **Default**: `true`.
+ * `highWaterMark` {number} Total number of bytes that the `QuicStream` may
+ buffer internally before the `quicstream.write()` function starts returning
+ `false`. Default: `16384`.
+ * `defaultEncoding` {string} The default encoding that is used when no
+ encoding is specified as an argument to `quicstream.write()`. Default:
+ `'utf8'`.
+* Returns: {QuicStream}
+
+Returns a new `QuicStream`.
+
+An error will be thrown if the `QuicSession` has been destroyed or is in the
+process of a graceful shutdown.
+
+#### quicsession.ping()
+
+
+The `ping()` method will trigger the underlying QUIC connection to serialize
+any frames currently pending in the outbound queue if it is able to do so.
+This has the effect of keeping the connection with the peer active and resets
+the idle and retransmission timers. The `ping()` method is a best-effort
+that ignores any errors that may occur during the serialization and send
+operations. There is no return value and there is no way to monitor the status
+of the `ping()` operation.
+
+#### quicsession.peerInitiatedStreamCount
+
+
+* Type: {bigint}
+
+A `BigInt` representing the total number of `QuicStreams` initiated by the
+connected peer.
+
+#### quicsession.remoteAddress
+
+
+* Type: {Object}
+ * `address` {string} The local IPv4 or IPv6 address to which the `QuicSession`
+ is connected.
+ * `family` {string} Either `'IPv4'` or `'IPv6'`.
+ * `port` {number} The local IP port to which the `QuicSocket` is bound.
+
+An object containing the remote address information for the connected peer.
+
+#### quicsession.selfInitiatedStreamCount
+
+
+* Type: {bigint}
+
+A `BigInt` representing the total number of `QuicStream` instances initiated
+by this `QuicSession`.
+
+#### quicsession.servername
+
+
+* Type: {string}
+
+The SNI servername requested for this session by the client.
+
+#### quicsession.smoothedRTT
+
+
+* Type: {bigint}
+
+The modified RTT calculated for this `QuicSession`.
+
+#### quicsession.socket
+
+
+* Type: {QuicSocket}
+
+The `QuicSocket` the `QuicSession` is associated with.
+
+#### quicsession.statelessReset
+
+
+* Type: {boolean}
+
+True if the `QuicSession` was closed due to QUIC stateless reset.
+
+#### quicsession.uniStreamCount
+
+
+* Type: {bigint}
+
+A `BigInt` representing the total number of unidirectional streams
+created on this `QuicSession`.
+
+#### quicsession.updateKey()
+
+
+* Returns: {boolean} `true` if the key update operation is successfully
+ initiated.
+
+Initiates QuicSession key update.
+
+An error will be thrown if called before `quicsession.handshakeConfirmed`
+is equal to `true`.
+
+#### quicsession.usingEarlyData
+
+
+* Type: {boolean}
+
+On server `QuicSession` instances, set to `true` on completion of the TLS
+handshake if early data is enabled. On client `QuicSession` instances,
+set to true on handshake completion if early data is enabled *and* was
+accepted by the server.
+
+### Class: QuicClientSession extends QuicSession
+
+
+* Extends: {QuicSession}
+
+The `QuicClientSession` class implements the client side of a QUIC connection.
+Instances are created using the `quicsocket.connect()` method.
+
+#### Event: `'OCSPResponse'`
+
+
+Emitted when the `QuicClientSession` receives a requested OCSP certificate
+status response from the QUIC server peer.
+
+The callback is invoked with a single argument:
+
+* `response` {Buffer}
+
+Node.js does not perform any automatic validation or processing of the
+response.
+
+The `'OCSPResponse'` event will not be emitted more than once.
+
+#### Event: `'sessionTicket'`
+
+
+The `'sessionTicket'` event is emitted when a new TLS session ticket has been
+generated for the current `QuicClientSession`. The callback is invoked with
+two arguments:
+
+* `sessionTicket` {Buffer} The serialized session ticket.
+* `remoteTransportParams` {Buffer} The serialized remote transport parameters
+ provided by the QUIC server.
+
+The `sessionTicket` and `remoteTransportParams` are useful when creating a new
+`QuicClientSession` to more quickly resume an existing session.
+
+The `'sessionTicket'` event may be emitted multiple times.
+
+#### Event: `'usePreferredAddress'`
+
+
+The `'usePreferredAddress'` event is emitted when the client `QuicSession`
+is updated to use the server-advertised preferred address. The callback is
+invoked with a single `address` argument:
+
+* `address` {Object}
+ * `address` {string} The preferred host name
+ * `port` {number} The preferred IP port
+ * `type` {string} Either `'udp4'` or `'udp6'`.
+
+This event is purely informational and will be emitted only when
+`preferredAddressPolicy` is set to `'accept'`.
+
+The `'usePreferredAddress'` event will not be emitted more than once.
+
+#### quicclientsession.ephemeralKeyInfo
+
+
+* Type: {Object}
+
+An object representing the type, name, and size of parameter of an ephemeral
+key exchange in Perfect Forward Secrecy on a client connection. It is an
+empty object when the key exchange is not ephemeral. The supported types are
+`'DH'` and `'ECDH'`. The `name` property is available only when type is
+`'ECDH'`.
+
+For example: `{ type: 'ECDH', name: 'prime256v1', size: 256 }`.
+
+#### quicclientsession.ready
+
+
+* Type: {boolean}
+
+Set to `true` if the `QuicClientSession` is ready for use. False if the
+`QuicSocket` has not yet been bound.
+
+#### quicclientsession.setSocket(socket, callback])
+
+
+* `socket` {QuicSocket} A `QuicSocket` instance to move this session to.
+* `callback` {Function} A callback function that will be invoked once the
+ migration to the new `QuicSocket` is complete.
+
+Migrates the `QuicClientSession` to the given `QuicSocket` instance. If the new
+`QuicSocket` has not yet been bound to a local UDP port, it will be bound prior
+to attempting the migration. If the `QuicClientSession` is not yet ready to
+migrate, the callback will be invoked with an `Error` using the code
+`ERR_QUICCLIENTSESSION_FAILED_SETSOCKET`.
+
+### Class: QuicServerSession extends QuicSession
+
+
+* Extends: {QuicSession}
+
+The `QuicServerSession` class implements the server side of a QUIC connection.
+Instances are created internally and are emitted using the `QuicSocket`
+`'session'` event.
+
+#### Event: `'clientHello'`
+
+
+Emitted at the start of the TLS handshake when the `QuicServerSession` receives
+the initial TLS Client Hello.
+
+The event handler is given a callback function that *must* be invoked for the
+handshake to continue.
+
+The callback is invoked with four arguments:
+
+* `alpn` {string} The ALPN protocol identifier requested by the client.
+* `servername` {string} The SNI servername requested by the client.
+* `ciphers` {string[]} The list of TLS cipher algorithms requested by the
+ client.
+* `callback` {Function} A callback function that must be called in order for
+ the TLS handshake to continue.
+
+The `'clientHello'` event will not be emitted more than once.
+
+#### Event: `'OCSPRequest'`
+
+
+Emitted when the `QuicServerSession` has received a OCSP certificate status
+request as part of the TLS handshake.
+
+The callback is invoked with three arguments:
+
+* `servername` {string}
+* `context` {tls.SecureContext}
+* `callback` {Function}
+
+The callback *must* be invoked in order for the TLS handshake to continue.
+
+The `'OCSPRequest'` event will not be emitted more than once.
+
+#### quicserversession.addContext(servername\[, context\])
+
+
+* `servername` {string} A DNS name to associate with the given context.
+* `context` {tls.SecureContext} A TLS SecureContext to associate with the `servername`.
+
+TBD
+
+### Class: QuicSocket
+
+
+New instances of `QuicSocket` are created using the `net.createQuicSocket()`
+method.
+
+Once created, a `QuicSocket` can be configured to work as both a client and a
+server.
+
+#### Event: `'busy'`
+
+
+Emitted when the server busy state has been toggled using
+`quicSocket.setServerBusy()`. The callback is invoked with a single
+boolean argument indicating `true` if busy status is enabled,
+`false` otherwise. This event is strictly informational.
+
+```js
+const { createQuicSocket } = require('net');
+
+const socket = createQuicSocket();
+
+socket.on('busy', (busy) => {
+ if (busy)
+ console.log('Server is busy');
+ else
+ console.log('Server is not busy');
+});
+
+socket.setServerBusy(true);
+socket.setServerBusy(false);
+```
+
+This `'busy'` event may be emitted multiple times.
+
+#### Event: `'close'`
+
+
+Emitted after the `QuicSocket` has been destroyed and is no longer usable.
+
+The `'close'` event will not be emitted multiple times.
+
+#### Event: `'error'`
+
+
+Emitted before the `'close'` event if the `QuicSocket` was destroyed with an
+`error`.
+
+The `'error'` event will not be emitted multiple times.
+
+#### Event: `'ready'`
+
+
+Emitted once the `QuicSocket` has been bound to a local UDP port.
+
+The `'ready'` event will not be emitted multiple times.
+
+#### Event: `'session'`
+
+
+Emitted when a new `QuicServerSession` has been created.
+
+The `'session'` event will be emitted multiple times.
+
+#### quicsocket.addEndpoint(options)
+
+
+* `options`: {Object} An object describing the local address to bind to.
+ * `address` {string} The local address to bind to. This may be an IPv4 or
+ IPv6 address or a host name. If a host name is given, it will be resolved
+ to an IP address.
+ * `port` {number} The local port to bind to.
+ * `type` {string} Either `'udp4'` or `'upd6'` to use either IPv4 or IPv6,
+ respectively.
+ * `ipv6Only` {boolean}
+* Returns: {QuicEndpoint}
+
+Creates and adds a new `QuicEndpoint` to the `QuicSocket` instance.
+
+#### quicsocket.bound
+
+
+* Type: {boolean}
+
+Will be `true` if the `QuicSocket` has been successfully bound to the local UDP
+port.
+
+#### quicsocket.boundDuration
+
+
+* Type: {bigint}
+
+A `BigInt` representing the length of time this `QuicSocket` has been bound
+to a local port.
+
+#### quicsocket.bytesReceived
+
+
+* Type: {bigint}
+
+A `BigInt` representing the number of bytes received by this `QuicSocket`.
+
+#### quicsocket.bytesSent
+
+
+* Type: {bigint}
+
+A `BigInt` representing the number of bytes sent by this `QuicSocket`.
+
+#### quicsocket.clientSessions
+
+
+* Type: {bigint}
+
+A `BigInt` representing the number of client `QuicSession` instances that
+have been associated with this `QuicSocket`.
+
+#### quicsocket.close(\[callback\])
+
+
+* `callback` {Function}
+
+Gracefully closes the `QuicSocket`. Existing `QuicSession` instances will be
+permitted to close naturally. New `QuicClientSession` and `QuicServerSession`
+instances will not be allowed.
+
+#### quicsocket.connect(\[options\])
+
+
+* `options` {Object}
+ * `address` {string} The domain name or IP address of the QUIC server
+ endpoint.
+ * `alpn` {string} An ALPN protocol identifier.
+ * `ca` {string|string[]|Buffer|Buffer[]} Optionally override the trusted CA
+ certificates. Default is to trust the well-known CAs curated by Mozilla.
+ Mozilla's CAs are completely replaced when CAs are explicitly specified
+ using this option. The value can be a string or `Buffer`, or an `Array` of
+ strings and/or `Buffer`s. Any string or `Buffer` can contain multiple PEM
+ CAs concatenated together. The peer's certificate must be chainable to a CA
+ trusted by the server for the connection to be authenticated. When using
+ certificates that are not chainable to a well-known CA, the certificate's CA
+ must be explicitly specified as a trusted or the connection will fail to
+ authenticate.
+ If the peer uses a certificate that doesn't match or chain to one of the
+ default CAs, use the `ca` option to provide a CA certificate that the peer's
+ certificate can match or chain to.
+ For self-signed certificates, the certificate is its own CA, and must be
+ provided.
+ For PEM encoded certificates, supported types are "TRUSTED CERTIFICATE",
+ "X509 CERTIFICATE", and "CERTIFICATE".
+ * `cert` {string|string[]|Buffer|Buffer[]} Cert chains in PEM format. One cert
+ chain should be provided per private key. Each cert chain should consist of
+ the PEM formatted certificate for a provided private `key`, followed by the
+ PEM formatted intermediate certificates (if any), in order, and not
+ including the root CA (the root CA must be pre-known to the peer, see `ca`).
+ When providing multiple cert chains, they do not have to be in the same
+ order as their private keys in `key`. If the intermediate certificates are
+ not provided, the peer will not be able to validate the certificate, and the
+ handshake will fail.
+ * `ciphers` {string} Cipher suite specification, replacing the default. For
+ more information, see [modifying the default cipher suite][]. Permitted
+ ciphers can be obtained via [`tls.getCiphers()`][]. Cipher names must be
+ uppercased in order for OpenSSL to accept them.
+ * `clientCertEngine` {string} Name of an OpenSSL engine which can provide the
+ client certificate.
+ * `crl` {string|string[]|Buffer|Buffer[]} PEM formatted CRLs (Certificate
+ Revocation Lists).
+ * `defaultEncoding` {string} The default encoding that is used when no
+ encoding is specified as an argument to `quicstream.write()`. Default:
+ `'utf8'`.
+ * `dhparam` {string|Buffer} Diffie Hellman parameters, required for
+ [Perfect Forward Secrecy][]. Use `openssl dhparam` to create the parameters.
+ The key length must be greater than or equal to 1024 bits, otherwise an
+ error will be thrown. It is strongly recommended to use 2048 bits or larger
+ for stronger security. If omitted or invalid, the parameters are silently
+ discarded and DHE ciphers will not be available.
+ * `ecdhCurve` {string} A string describing a named curve or a colon separated
+ list of curve NIDs or names, for example `P-521:P-384:P-256`, to use for
+ ECDH key agreement. Set to `auto` to select the
+ curve automatically. Use [`crypto.getCurves()`][] to obtain a list of
+ available curve names. On recent releases, `openssl ecparam -list_curves`
+ will also display the name and description of each available elliptic curve.
+ **Default:** [`tls.DEFAULT_ECDH_CURVE`][].
+ * `highWaterMark` {number} Total number of bytes that the `QuicStream` may
+ buffer internally before the `quicstream.write()` function starts returning
+ `false`. Default: `16384`.
+ * `honorCipherOrder` {boolean} Attempt to use the server's cipher suite
+ preferences instead of the client's. When `true`, causes
+ `SSL_OP_CIPHER_SERVER_PREFERENCE` to be set in `secureOptions`, see
+ [OpenSSL Options][] for more information.
+ * `idleTimeout` {number}
+ * `ipv6Only` {boolean}
+ * `key` {string|string[]|Buffer|Buffer[]|Object[]} Private keys in PEM format.
+ PEM allows the option of private keys being encrypted. Encrypted keys will
+ be decrypted with `options.passphrase`. Multiple keys using different
+ algorithms can be provided either as an array of unencrypted key strings or
+ buffers, or an array of objects in the form `{pem: [,
+ passphrase: ]}`. The object form can only occur in an array.
+ `object.passphrase` is optional. Encrypted keys will be decrypted with
+ `object.passphrase` if provided, or `options.passphrase` if it is not.
+ * `activeConnectionIdLimit` {number} Must be a value between `2` and `8`
+ (inclusive). Default: `2`.
+ * `maxAckDelay` {number}
+ * `maxData` {number}
+ * `maxPacketSize` {number}
+ * `maxStreamDataBidiLocal` {number}
+ * `maxStreamDataBidiRemote` {number}
+ * `maxStreamDataUni` {number}
+ * `maxStreamsBidi` {number}
+ * `maxStreamsUni` {number}
+ * `h3` {Object} HTTP/3 Specific Configuration Options
+ * `qpackMaxTableCapacity` {number}
+ * `qpackBlockedStreams` {number}
+ * `maxHeaderListSize` {number}
+ * `maxPushes` {number}
+ * `passphrase` {string} Shared passphrase used for a single private key and/or
+ a PFX.
+ * `pfx` {string|string[]|Buffer|Buffer[]|Object[]} PFX or PKCS12 encoded
+ private key and certificate chain. `pfx` is an alternative to providing
+ `key` and `cert` individually. PFX is usually encrypted, if it is,
+ `passphrase` will be used to decrypt it. Multiple PFX can be provided either
+ as an array of unencrypted PFX buffers, or an array of objects in the form
+ `{buf: [, passphrase: ]}`. The object form can only
+ occur in an array. `object.passphrase` is optional. Encrypted PFX will be
+ decrypted with `object.passphrase` if provided, or `options.passphrase` if
+ it is not.
+ * `port` {number} The IP port of the remote QUIC server.
+ * `preferredAddressPolicy` {string} `'accept'` or `'reject'`. When `'accept'`,
+ indicates that the client will automatically use the preferred address
+ advertised by the server.
+ * `remoteTransportParams` {Buffer|TypedArray|DataView} The serialized remote
+ transport parameters from a previously established session. These would
+ have been provided as part of the `'sessionTicket'` event on a previous
+ `QuicClientSession` object.
+ * `qlog` {boolean} Whether to emit ['qlog'][] events for this session.
+ Default: `false`.
+ * `requestOCSP` {boolean} If `true`, specifies that the OCSP status request
+ extension will be added to the client hello and an `'OCSPResponse'` event
+ will be emitted before establishing a secure communication.
+ * `secureOptions` {number} Optionally affect the OpenSSL protocol behavior,
+ which is not usually necessary. This should be used carefully if at all!
+ Value is a numeric bitmask of the `SSL_OP_*` options from
+ [OpenSSL Options][].
+ * `servername` {string} The SNI servername.
+ * `sessionTicket`: {Buffer|TypedArray|DataView} The serialized TLS Session
+ Ticket from a previously established session. These would have been
+ provided as part of the `'sessionTicket`' event on a previous
+ `QuicClientSession` object.
+ * `type`: {string} Identifies the type of UDP socket. The value must either
+ be `'udp4'`, indicating UDP over IPv4, or `'udp6'`, indicating UDP over
+ IPv6. Defaults to `'udp4'`.
+
+Create a new `QuicClientSession`. This function can be called multiple times
+to create sessions associated with different endpoints on the same
+client endpoint.
+
+#### quicsocket.destroy(\[error\])
+
+
+* `error` {any}
+
+Destroys the `QuicSocket` then emits the `'close'` event when done. The `'error'`
+event will be emitted after `'close'` if the `error` is not `undefined`.
+
+#### quicsocket.destroyed
+
+
+* Type: {boolean}
+
+Will be `true` if the `QuicSocket` has been destroyed.
+
+#### quicsocket.duration
+
+
+* Type: {bigint}
+
+A `BigInt` representing the length of time this `QuicSocket` has been active,
+
+#### quicsocket.endpoints
+
+
+* Type: {QuicEndpoint[]}
+
+An array of `QuicEndpoint` instances associated with the `QuicSocket`.
+
+#### quicsocket.listen(\[options\]\[, callback\])
+
+
+* `options` {Object}
+ * `alpn` {string} A required ALPN protocol identifier.
+ * `ca` {string|string[]|Buffer|Buffer[]} Optionally override the trusted CA
+ certificates. Default is to trust the well-known CAs curated by Mozilla.
+ Mozilla's CAs are completely replaced when CAs are explicitly specified
+ using this option. The value can be a string or `Buffer`, or an `Array` of
+ strings and/or `Buffer`s. Any string or `Buffer` can contain multiple PEM
+ CAs concatenated together. The peer's certificate must be chainable to a CA
+ trusted by the server for the connection to be authenticated. When using
+ certificates that are not chainable to a well-known CA, the certificate's CA
+ must be explicitly specified as a trusted or the connection will fail to
+ authenticate.
+ If the peer uses a certificate that doesn't match or chain to one of the
+ default CAs, use the `ca` option to provide a CA certificate that the peer's
+ certificate can match or chain to.
+ For self-signed certificates, the certificate is its own CA, and must be
+ provided.
+ For PEM encoded certificates, supported types are "TRUSTED CERTIFICATE",
+ "X509 CERTIFICATE", and "CERTIFICATE".
+ * `cert` {string|string[]|Buffer|Buffer[]} Cert chains in PEM format. One cert
+ chain should be provided per private key. Each cert chain should consist of
+ the PEM formatted certificate for a provided private `key`, followed by the
+ PEM formatted intermediate certificates (if any), in order, and not
+ including the root CA (the root CA must be pre-known to the peer, see `ca`).
+ When providing multiple cert chains, they do not have to be in the same
+ order as their private keys in `key`. If the intermediate certificates are
+ not provided, the peer will not be able to validate the certificate, and the
+ handshake will fail.
+ * `ciphers` {string} Cipher suite specification, replacing the default. For
+ more information, see [modifying the default cipher suite][]. Permitted
+ ciphers can be obtained via [`tls.getCiphers()`][]. Cipher names must be
+ uppercased in order for OpenSSL to accept them.
+ * `clientCertEngine` {string} Name of an OpenSSL engine which can provide the
+ client certificate.
+ * `crl` {string|string[]|Buffer|Buffer[]} PEM formatted CRLs (Certificate
+ Revocation Lists).
+ * `defaultEncoding` {string} The default encoding that is used when no
+ encoding is specified as an argument to `quicstream.write()`. Default:
+ `'utf8'`.
+ * `dhparam` {string|Buffer} Diffie Hellman parameters, required for
+ [Perfect Forward Secrecy][]. Use `openssl dhparam` to create the parameters.
+ The key length must be greater than or equal to 1024 bits, otherwise an
+ error will be thrown. It is strongly recommended to use 2048 bits or larger
+ for stronger security. If omitted or invalid, the parameters are silently
+ discarded and DHE ciphers will not be available.
+ * `earlyData` {boolean} Set to `false` to disable 0RTT early data.
+ Default: `true`.
+ * `ecdhCurve` {string} A string describing a named curve or a colon separated
+ list of curve NIDs or names, for example `P-521:P-384:P-256`, to use for
+ ECDH key agreement. Set to `auto` to select the
+ curve automatically. Use [`crypto.getCurves()`][] to obtain a list of
+ available curve names. On recent releases, `openssl ecparam -list_curves`
+ will also display the name and description of each available elliptic curve.
+ **Default:** [`tls.DEFAULT_ECDH_CURVE`][].
+ * `highWaterMark` {number} Total number of bytes that `QuicStream` instances
+ may buffer internally before the `quicstream.write()` function starts
+ returning `false`. Default: `16384`.
+ * `honorCipherOrder` {boolean} Attempt to use the server's cipher suite
+ references instead of the client's. When `true`, causes
+ `SSL_OP_CIPHER_SERVER_PREFERENCE` to be set in `secureOptions`, see
+ [OpenSSL Options][] for more information.
+ * `idleTimeout` {number}
+ * `key` {string|string[]|Buffer|Buffer[]|Object[]} Private keys in PEM format.
+ PEM allows the option of private keys being encrypted. Encrypted keys will
+ be decrypted with `options.passphrase`. Multiple keys using different
+ algorithms can be provided either as an array of unencrypted key strings or
+ buffers, or an array of objects in the form `{pem: [,
+ passphrase: ]}`. The object form can only occur in an array.
+ `object.passphrase` is optional. Encrypted keys will be decrypted with
+ `object.passphrase` if provided, or `options.passphrase` if it is not.
+ * `activeConnectionIdLimit` {number}
+ * `maxAckDelay` {number}
+ * `maxData` {number}
+ * `maxPacketSize` {number}
+ * `maxStreamsBidi` {number}
+ * `maxStreamsUni` {number}
+ * `maxStreamDataBidiLocal` {number}
+ * `maxStreamDataBidiRemote` {number}
+ * `maxStreamDataUni` {number}
+ * `passphrase` {string} Shared passphrase used for a single private key
+ and/or a PFX.
+ * `pfx` {string|string[]|Buffer|Buffer[]|Object[]} PFX or PKCS12 encoded
+ private key and certificate chain. `pfx` is an alternative to providing
+ `key` and `cert` individually. PFX is usually encrypted, if it is,
+ `passphrase` will be used to decrypt it. Multiple PFX can be provided either
+ as an array of unencrypted PFX buffers, or an array of objects in the form
+ `{buf: [, passphrase: ]}`. The object form can only
+ occur in an array. `object.passphrase` is optional. Encrypted PFX will be
+ decrypted with `object.passphrase` if provided, or `options.passphrase` if
+ it is not.
+ * `preferredAddress` {Object}
+ * `address` {string}
+ * `port` {number}
+ * `type` {string} `'udp4'` or `'udp6'`.
+ * `requestCert` {boolean} Request a certificate used to authenticate the
+ client.
+ * `rejectUnauthorized` {boolean} If not `false` the server will reject any
+ connection which is not authorized with the list of supplied CAs. This
+ option only has an effect if `requestCert` is `true`. Default: `true`.
+ * `secureOptions` {number} Optionally affect the OpenSSL protocol behavior,
+ which is not usually necessary. This should be used carefully if at all!
+ Value is a numeric bitmask of the `SSL_OP_*` options from
+ [OpenSSL Options][].
+ * `sessionIdContext` {string} Opaque identifier used by servers to ensure
+ session state is not shared between applications. Unused by clients.
+
+* `callback` {Function}
+
+Listen for new peer-initiated sessions.
+
+If a `callback` is given, it is registered as a handler for the
+`'session'` event.
+
+#### quicsocket.listenDuration
+
+
+* Type: {bigint}
+
+A `BigInt` representing the length of time this `QuicSocket` has been listening
+for connections.
+
+#### quicsocket.listening
+
+
+* Type: {boolean}
+
+Set to `true` if the `QuicSocket` is listening for new connections.
+
+#### quicsocket.packetsIgnored
+
+
+* Type: {bigint}
+
+A `BigInt` representing the number of packets received by this `QuicSocket` that
+have been ignored.
+
+#### quicsocket.packetsReceived
+
+
+* Type: {bigint}
+
+A `BigInt` representing the number of packets successfully received by this
+`QuicSocket`.
+
+#### quicsocket.packetsSent
+
+
+* Type: {bigint}
+
+A `BigInt` representing the number of packets sent by this `QuicSocket`.
+
+#### quicsocket.pending
+
+
+* Type: {boolean}
+
+Set to `true` if the socket is not yet bound to the local UDP port.
+
+#### quicsocket.ref()
+
+
+#### quicsocket.serverBusyCount
+
+
+* Type: {bigint}
+
+A `BigInt` representing the number of `QuicSession` instances rejected
+due to server busy status.
+
+#### quicsocket.serverSessions
+
+
+* Type: {bigint}
+
+A `BigInt` representing the number of server `QuicSession` instances that
+have been associated with this `QuicSocket`.
+
+#### quicsocket.setDiagnosticPacketLoss(options)
+
+
+* `options` {Object}
+ * `rx` {number} A value in the range `0.0` to `1.0` that specifies the
+ probability of received packet loss.
+ * `tx` {number} A value in the range `0.0` to `1.0` that specifies the
+ probability of transmitted packet loss.
+
+The `quicsocket.setDiagnosticPacketLoss()` method is a diagnostic only tool
+that can be used to *simulate* packet loss conditions for this `QuicSocket`
+by artificially dropping received or transmitted packets.
+
+This method is *not* to be used in production applications.
+
+#### quicsocket.setServerBusy(\[on\])
+
+
+* `on` {boolean} When `true`, the `QuicSocket` will reject new connections.
+ **Defaults**: `true`.
+
+Calling `setServerBusy()` or `setServerBusy(true)` will tell the `QuicSocket`
+to reject all new incoming connection requests using the `SERVER_BUSY` QUIC
+error code. To begin receiving connections again, disable busy mode by calling
+`setServerBusy(false)`.
+
+#### quicsocket.statelessResetCount
+
+
+* Type: {bigint}
+
+A `BigInt` that represents the number of stateless resets that have been sent.
+
+#### quicsocket.toggleStatelessReset()
+
+
+* Returns {boolean} `true` if stateless reset processing is enabled; `false`
+ if disabled.
+
+By default, a listening `QuicSocket` will generate stateless reset tokens when
+appropriate. The `disableStatelessReset` option may be set when the
+`QuicSocket` is created to disable generation of stateless resets. The
+`toggleStatelessReset()` function allows stateless reset to be turned on and
+off dynamically through the lifetime of the `QuicSocket`.
+
+#### quicsocket.unref();
+
+
+### Class: QuicStream extends stream.Duplex
+
+
+* Extends: {stream.Duplex}
+
+#### Event: `'blocked'`
+
+
+Emitted when the `QuicStream` has been prevented from sending queued data for
+the `QuicStream` due to congestion control.
+
+#### Event: `'close'`
+
+
+Emitted when the `QuicStream` has is completely closed and the underlying
+resources have been freed.
+
+#### Event: `'data'`
+
+
+#### Event: `'end'`
+
+
+#### Event: `'error'`
+
+
+#### Event: `'informationalHeaders'`
+
+
+Emitted when the `QuicStream` has received a block of informational headers.
+
+Support for headers depends entirely on the QUIC Application used as identified
+by the `alpn` configuration option. In QUIC Applications that support headers,
+informational header blocks typically come before initial headers.
+
+The event handler is invoked with a single argument representing the block of
+Headers as an object.
+
+```js
+stream('informationalHeaders', (headers) => {
+ // Use headers
+});
+```
+
+#### Event: `'initialHeaders'`
+
+
+Emitted when the `QuicStream` has received a block of initial headers.
+
+Support for headers depends entirely on the QUIC Application used as identified
+by the `alpn` configuration option. HTTP/3, for instance, supports two kinds of
+initial headers: request headers for HTTP request messages and response headers
+for HTTP response messages. For HTTP/3 QUIC streams, request and response
+headers are each emitted using the `'initialHeaders'` event.
+
+The event handler is invoked with a single argument representing the block of
+Headers as an object.
+
+```js
+stream('initialHeaders', (headers) => {
+ // Use headers
+});
+```
+
+#### Event: `'ready'`
+
+
+Emitted when the underlying `QuicSession` has emitted its `secure` event
+this stream has received its id, which is accessible as `stream.id` once this
+event is emitted.
+
+#### Event: `'trailingHeaders'`
+
+
+Emitted when the `QuicStream` has received a block of trailing headers.
+
+Support for headers depends entirely on the QUIC Application used as identified
+by the `alpn` configuration option. Trailing headers typically follow any data
+transmitted on the `QuicStream`, and therefore typically emit sometime after the
+last `'data'` event but before the `'close'` event. The precise timing may
+vary from one QUIC application to another.
+
+The event handler is invoked with a single argument representing the block of
+Headers as an object.
+
+```js
+stream('trailingHeaders', (headers) => {
+ // Use headers
+});
+```
+
+#### Event: `'readable'`
+
+
+#### quicstream.aborted
+
+* Type: {boolean}
+
+True if dataflow on the `QuicStream` was prematurely terminated.
+
+#### quicstream.bidirectional
+
+
+* Type: {boolean}
+
+Set to `true` if the `QuicStream` is bidirectional.
+
+#### quicstream.bytesReceived
+
+
+* Type: {bigint}
+
+A `BigInt` representing the total number of bytes received for this
+`QuicStream`.
+
+#### quicstream.bytesSent
+
+
+* Type: {bigint}
+
+A `BigInt` representing the total number of bytes sent by this
+`QuicStream`.
+
+#### quicstream.clientInitiated
+
+
+* Type: {boolean}
+
+Set to `true` if the `QuicStream` was initiated by a `QuicClientSession`
+instance.
+
+#### quicstream.close(code)
+
+
+* `code` {number}
+
+Closes the `QuicStream`.
+
+#### quicstream.dataAckHistogram
+
+
+TBD
+
+#### quicstream.dataRateHistogram
+
+
+TBD
+
+#### quicstream.dataSizeHistogram
+
+TBD
+
+#### quicstream.duration
+
+
+* Type: {bigint}
+
+A `BigInt` representing the length of time the `QuicStream` has been active.
+
+#### quicstream.finalSize
+
+
+* Type: {bigint}
+
+A `BigInt` specifying the total number of bytes successfully received by the
+`QuicStream`.
+
+#### quicstream.id
+
+
+* Type: {number}
+
+The numeric identifier of the `QuicStream`.
+
+#### quicstream.maxAcknowledgedOffset
+
+
+* Type: {bigint}
+
+A `BigInt` representing the highest acknowledged data offset received
+for this `QuicStream`.
+
+#### quicstream.maxExtendedOffset
+
+
+* Type: {bigint}
+
+A `BigInt` representing the maximum extended data offset that has been
+reported to the connected peer.
+
+#### quicstream.maxReceivedOffset
+
+
+* Type: {bigint}
+
+A `BigInt` representing the maximum received offset for this `QuicStream`.
+
+#### quicstream.pending
+
+
+* {boolean}
+
+This property is `true` if the underlying session is not finished yet,
+i.e. before the `'ready'` event is emitted.
+
+#### quicstream.pushStream(headers\[, options\])
+
+
+* `headers` {Object} An object representing a block of headers to be
+ transmitted with the push promise.
+* `options` {Object}
+ * `highWaterMark` {number} Total number of bytes that the `QuicStream` may
+ buffer internally before the `quicstream.write()` function starts returning
+ `false`. Default: `16384`.
+ * `defaultEncoding` {string} The default encoding that is used when no
+ encoding is specified as an argument to `quicstream.write()`. Default:
+ `'utf8'`.
+
+* Returns: {QuicStream}
+
+If the selected QUIC application protocol supports push streams, then the
+`pushStream()` method will initiate a new push promise and create a new
+unidirectional `QuicStream` object used to fulfill that push.
+
+Currently only HTTP/3 supports the use of `pushStream()`.
+
+If the selected QUIC application protocol does not support push streams, an
+error will be thrown.
+
+#### quicstream.serverInitiated
+
+
+* Type: {boolean}
+
+Set to `true` if the `QuicStream` was initiated by a `QuicServerSession`
+instance.
+
+#### quicstream.session
+
+
+* Type: {QuicSession}
+
+The `QuicServerSession` or `QuicClientSession`.
+
+#### quicstream.sendFD(fd\[, options\])
+
+
+* `fd` {number|FileHandle} A readable file descriptor.
+* `options` {Object}
+ * `offset` {number} The offset position at which to begin reading.
+ Default: `-1`.
+ * `length` {number} The amount of data from the fd to send.
+ Default: `-1`.
+
+Instead of using a `Quicstream` as a writable stream, send data from a given
+file descriptor.
+
+If `offset` is set to a non-negative number, reading starts from that position
+and the file offset will not be advanced.
+If `length` is set to a non-negative number, it gives the maximum number of
+bytes that are read from the file.
+
+The file descriptor or `FileHandle` is not closed when the stream is closed,
+so it will need to be closed manually once it is no longer needed.
+Using the same file descriptor concurrently for multiple streams
+is not supported and may result in data loss. Re-using a file descriptor
+after a stream has finished is supported.
+
+#### quicstream.sendFile(path\[, options\])
+
+
+* `path` {string|Buffer|URL}
+* `options` {Object}
+ * `onError` {Function} Callback function invoked in the case of an
+ error before send.
+ * `offset` {number} The offset position at which to begin reading.
+ Default: `-1`.
+ * `length` {number} The amount of data from the fd to send.
+ Default: `-1`.
+
+Instead of using a `QuicStream` as a writable stream, send data from a given
+file path.
+
+The `options.onError` callback will be called if the file could not be opened.
+If `offset` is set to a non-negative number, reading starts from that position.
+If `length` is set to a non-negative number, it gives the maximum number of
+bytes that are read from the file.
+
+#### quicstream.submitInformationalHeaders(headers)
+
+* `headers` {Object}
+
+TBD
+
+#### quicstream.submitInitialHeaders(headers)
+
+* `headers` {Object}
+
+TBD
+
+#### quicstream.submitTrailingHeaders(headers)
+
+* `headers` {Object}
+
+TBD
+
+#### quicstream.unidirectional
+
+
+* Type: {boolean}
+
+Set to `true` if the `QuicStream` is unidirectional.
+
+[`crypto.getCurves()`]: crypto.html#crypto_crypto_getcurves
+[`tls.DEFAULT_ECDH_CURVE`]: #tls_tls_default_ecdh_curve
+[`tls.getCiphers()`]: tls.html#tls_tls_getciphers
+[ALPN]: https://tools.ietf.org/html/rfc7301
+[RFC 4007]: https://tools.ietf.org/html/rfc4007
+[Certificate Object]: https://nodejs.org/dist/latest-v12.x/docs/api/tls.html#tls_certificate_object
+[modifying the default cipher suite]: tls.html#tls_modifying_the_default_tls_cipher_suite
+[OpenSSL Options]: crypto.html#crypto_openssl_options
+[Perfect Forward Secrecy]: #tls_perfect_forward_secrecy
+['qlog']: #quic_event_qlog
+[qlog standard]: https://tools.ietf.org/id/draft-marx-qlog-event-definitions-quic-h3-00.html
diff --git a/lib/internal/errors.js b/lib/internal/errors.js
index 9f5d8403f65d92..34ef6368218aac 100644
--- a/lib/internal/errors.js
+++ b/lib/internal/errors.js
@@ -1312,6 +1312,61 @@ E('ERR_PACKAGE_PATH_NOT_EXPORTED', (pkgPath, subpath, base = undefined) => {
return `Package subpath '${subpath}' is not defined by "exports" in ${
pkgPath} imported from ${base}`;
}, Error);
+E('ERR_QUICCLIENTSESSION_FAILED',
+ 'Failed to create a new QuicClientSession: %s', Error);
+E('ERR_QUICCLIENTSESSION_FAILED_SETSOCKET',
+ 'Failed to set the QuicSocket', Error);
+E('ERR_QUICSESSION_DESTROYED',
+ 'Cannot call %s after a QuicSession has been destroyed', Error);
+E('ERR_QUICSESSION_INVALID_DCID', 'Invalid DCID value: %s', Error);
+E('ERR_QUICSESSION_UPDATEKEY', 'Unable to update QuicSession keys', Error);
+E('ERR_QUICSESSION_VERSION_NEGOTIATION',
+ (version, requestedVersions, supportedVersions) => {
+ return 'QUIC session received version negotiation from server. ' +
+ `Version: ${version}. Requested: ${requestedVersions.join(', ')} ` +
+ `Supported: ${supportedVersions.join(', ')}`;
+ },
+ Error);
+E('ERR_QUICSOCKET_DESTROYED',
+ 'Cannot call %s after a QuicSocket has been destroyed', Error);
+E('ERR_QUICSOCKET_INVALID_STATELESS_RESET_SECRET_LENGTH',
+ 'The stateResetToken must be exactly 16-bytes in length',
+ Error);
+E('ERR_QUICSOCKET_LISTENING',
+ 'This QuicSocket is already listening', Error);
+E('ERR_QUICSOCKET_UNBOUND',
+ 'Cannot call %s before a QuicSocket has been bound', Error);
+E('ERR_QUICSTREAM_DESTROYED',
+ 'Cannot call %s after a QuicStream has been destroyed', Error);
+E('ERR_QUICSTREAM_INVALID_PUSH',
+ 'Push streams are only supported on client-initiated, bidirectional streams',
+ Error);
+E('ERR_QUICSTREAM_OPEN_FAILED', 'Opening a new QuicStream failed', Error);
+E('ERR_QUICSTREAM_UNSUPPORTED_PUSH',
+ 'Push streams are not supported on this QuicSession', Error);
+E('ERR_QUIC_ERROR', function(code, family) {
+ const {
+ constants: {
+ QUIC_ERROR_APPLICATION,
+ QUIC_ERROR_CRYPTO,
+ QUIC_ERROR_SESSION,
+ }
+ } = internalBinding('quic');
+ let familyType = 'unknown';
+ switch (family) {
+ case QUIC_ERROR_APPLICATION:
+ familyType = 'application';
+ break;
+ case QUIC_ERROR_CRYPTO:
+ familyType = 'crypto';
+ break;
+ case QUIC_ERROR_SESSION:
+ familyType = 'session';
+ break;
+ }
+ return `QUIC session closed with ${familyType} error code ${code}`;
+}, Error);
+E('ERR_QUIC_TLS13_REQUIRED', 'QUIC requires TLS version 1.3', Error);
E('ERR_REQUIRE_ESM',
(filename, parentPath = null, packageJsonPath = null) => {
let msg = `Must use import to load ES Module: ${filename}`;
diff --git a/lib/internal/quic/core.js b/lib/internal/quic/core.js
new file mode 100644
index 00000000000000..fd73cbe6cb2dea
--- /dev/null
+++ b/lib/internal/quic/core.js
@@ -0,0 +1,3352 @@
+'use strict';
+
+/* eslint-disable no-use-before-define */
+
+const {
+ assertCrypto,
+ customInspectSymbol: kInspect,
+} = require('internal/util');
+
+assertCrypto();
+
+const {
+ Array,
+ BigInt64Array,
+ Boolean,
+ Error,
+ Map,
+ RegExp,
+ Set,
+ Symbol,
+} = primordials;
+
+const { Buffer } = require('buffer');
+const { isArrayBufferView } = require('internal/util/types');
+const {
+ getAllowUnauthorized,
+ getSocketType,
+ lookup4,
+ lookup6,
+ setTransportParams,
+ toggleListeners,
+ validateNumber,
+ validateCloseCode,
+ validateTransportParams,
+ validateQuicClientSessionOptions,
+ validateQuicSocketOptions,
+ validateQuicStreamOptions,
+ validateQuicSocketListenOptions,
+ validateQuicEndpointOptions,
+ validateCreateSecureContextOptions,
+ validateQuicSocketConnectOptions,
+} = require('internal/quic/util');
+const util = require('util');
+const assert = require('internal/assert');
+const EventEmitter = require('events');
+const fs = require('fs');
+const fsPromisesInternal = require('internal/fs/promises');
+const { Duplex } = require('stream');
+const {
+ createSecureContext: _createSecureContext
+} = require('tls');
+const {
+ translatePeerCertificate
+} = require('_tls_common');
+const {
+ defaultTriggerAsyncIdScope,
+ symbols: {
+ async_id_symbol,
+ owner_symbol,
+ },
+} = require('internal/async_hooks');
+const dgram = require('dgram');
+const internalDgram = require('internal/dgram');
+const {
+ assertValidPseudoHeader,
+ assertValidPseudoHeaderResponse,
+ assertValidPseudoHeaderTrailer,
+ mapToHeaders,
+} = require('internal/http2/util');
+
+const {
+ constants: {
+ UV_UDP_IPV6ONLY,
+ UV_UDP_REUSEADDR,
+ }
+} = internalBinding('udp_wrap');
+
+const {
+ writeGeneric,
+ writevGeneric,
+ onStreamRead,
+ kAfterAsyncWrite,
+ kMaybeDestroy,
+ kUpdateTimer,
+ kHandle,
+ setStreamTimeout // eslint-disable-line no-unused-vars
+} = require('internal/stream_base_commons');
+
+const {
+ ShutdownWrap,
+ kReadBytesOrError,
+ streamBaseState
+} = internalBinding('stream_wrap');
+
+const {
+ codes: {
+ ERR_INVALID_ARG_TYPE,
+ ERR_INVALID_CALLBACK,
+ ERR_QUIC_ERROR,
+ ERR_QUICSESSION_DESTROYED,
+ ERR_QUICSESSION_VERSION_NEGOTIATION,
+ ERR_QUICSOCKET_DESTROYED,
+ ERR_QUICSOCKET_LISTENING,
+ ERR_QUICCLIENTSESSION_FAILED,
+ ERR_QUICCLIENTSESSION_FAILED_SETSOCKET,
+ ERR_QUICSESSION_UPDATEKEY,
+ ERR_QUICSTREAM_DESTROYED,
+ ERR_QUICSTREAM_INVALID_PUSH,
+ ERR_QUICSTREAM_UNSUPPORTED_PUSH,
+ ERR_QUICSTREAM_OPEN_FAILED,
+ ERR_TLS_DH_PARAM_SIZE,
+ },
+ errnoException,
+ exceptionWithHostPort
+} = require('internal/errors');
+
+const { FileHandle } = internalBinding('fs');
+const { StreamPipe } = internalBinding('stream_pipe');
+const { UV_EOF } = internalBinding('uv');
+
+const {
+ QuicSocket: QuicSocketHandle,
+ QuicEndpoint: QuicEndpointHandle,
+ initSecureContext,
+ initSecureContextClient,
+ createClientSession: _createClientSession,
+ openBidirectionalStream: _openBidirectionalStream,
+ openUnidirectionalStream: _openUnidirectionalStream,
+ setCallbacks,
+ constants: {
+ AF_INET,
+ AF_INET6,
+ IDX_QUIC_SESSION_MAX_PACKET_SIZE_DEFAULT,
+ IDX_QUIC_SESSION_STATE_MAX_STREAMS_BIDI,
+ IDX_QUIC_SESSION_STATE_MAX_STREAMS_UNI,
+ IDX_QUIC_SESSION_STATE_MAX_DATA_LEFT,
+ IDX_QUIC_SESSION_STATE_HANDSHAKE_CONFIRMED,
+ IDX_QUIC_SESSION_STATE_IDLE_TIMEOUT,
+ IDX_QUIC_SESSION_STATE_BYTES_IN_FLIGHT,
+ IDX_QUIC_SESSION_STATS_CREATED_AT,
+ IDX_QUIC_SESSION_STATS_HANDSHAKE_START_AT,
+ IDX_QUIC_SESSION_STATS_BYTES_RECEIVED,
+ IDX_QUIC_SESSION_STATS_BYTES_SENT,
+ IDX_QUIC_SESSION_STATS_BIDI_STREAM_COUNT,
+ IDX_QUIC_SESSION_STATS_UNI_STREAM_COUNT,
+ IDX_QUIC_SESSION_STATS_STREAMS_IN_COUNT,
+ IDX_QUIC_SESSION_STATS_STREAMS_OUT_COUNT,
+ IDX_QUIC_SESSION_STATS_KEYUPDATE_COUNT,
+ IDX_QUIC_SESSION_STATS_LOSS_RETRANSMIT_COUNT,
+ IDX_QUIC_SESSION_STATS_ACK_DELAY_RETRANSMIT_COUNT,
+ IDX_QUIC_SESSION_STATS_MAX_BYTES_IN_FLIGHT,
+ IDX_QUIC_SESSION_STATS_BLOCK_COUNT,
+ IDX_QUIC_SESSION_STATS_MIN_RTT,
+ IDX_QUIC_SESSION_STATS_SMOOTHED_RTT,
+ IDX_QUIC_SESSION_STATS_LATEST_RTT,
+ IDX_QUIC_STREAM_STATS_CREATED_AT,
+ IDX_QUIC_STREAM_STATS_BYTES_RECEIVED,
+ IDX_QUIC_STREAM_STATS_BYTES_SENT,
+ IDX_QUIC_STREAM_STATS_MAX_OFFSET,
+ IDX_QUIC_STREAM_STATS_FINAL_SIZE,
+ IDX_QUIC_STREAM_STATS_MAX_OFFSET_ACK,
+ IDX_QUIC_STREAM_STATS_MAX_OFFSET_RECV,
+ IDX_QUIC_SOCKET_STATS_CREATED_AT,
+ IDX_QUIC_SOCKET_STATS_BOUND_AT,
+ IDX_QUIC_SOCKET_STATS_LISTEN_AT,
+ IDX_QUIC_SOCKET_STATS_BYTES_RECEIVED,
+ IDX_QUIC_SOCKET_STATS_BYTES_SENT,
+ IDX_QUIC_SOCKET_STATS_PACKETS_RECEIVED,
+ IDX_QUIC_SOCKET_STATS_PACKETS_IGNORED,
+ IDX_QUIC_SOCKET_STATS_PACKETS_SENT,
+ IDX_QUIC_SOCKET_STATS_SERVER_SESSIONS,
+ IDX_QUIC_SOCKET_STATS_CLIENT_SESSIONS,
+ IDX_QUIC_SOCKET_STATS_STATELESS_RESET_COUNT,
+ IDX_QUIC_SOCKET_STATS_SERVER_BUSY_COUNT,
+ ERR_FAILED_TO_CREATE_SESSION,
+ ERR_INVALID_REMOTE_TRANSPORT_PARAMS,
+ ERR_INVALID_TLS_SESSION_TICKET,
+ NGTCP2_PATH_VALIDATION_RESULT_FAILURE,
+ NGTCP2_NO_ERROR,
+ QUIC_ERROR_APPLICATION,
+ QUICSERVERSESSION_OPTION_REJECT_UNAUTHORIZED,
+ QUICSERVERSESSION_OPTION_REQUEST_CERT,
+ QUICCLIENTSESSION_OPTION_REQUEST_OCSP,
+ QUICCLIENTSESSION_OPTION_VERIFY_HOSTNAME_IDENTITY,
+ QUICSOCKET_OPTIONS_VALIDATE_ADDRESS,
+ QUICSOCKET_OPTIONS_VALIDATE_ADDRESS_LRU,
+ QUICSTREAM_HEADERS_KIND_NONE,
+ QUICSTREAM_HEADERS_KIND_INFORMATIONAL,
+ QUICSTREAM_HEADERS_KIND_INITIAL,
+ QUICSTREAM_HEADERS_KIND_TRAILING,
+ QUICSTREAM_HEADERS_KIND_PUSH,
+ QUICSTREAM_HEADER_FLAGS_NONE,
+ QUICSTREAM_HEADER_FLAGS_TERMINAL,
+ }
+} = internalBinding('quic');
+
+const {
+ Histogram,
+ kDestroy: kDestroyHistogram
+} = require('internal/histogram');
+
+const {
+ validateBoolean,
+ validateInteger,
+ validateObject,
+ validateString,
+} = require('internal/validators');
+
+const emit = EventEmitter.prototype.emit;
+
+const kAddSession = Symbol('kAddSession');
+const kAddStream = Symbol('kAddStream');
+const kClose = Symbol('kClose');
+const kCert = Symbol('kCert');
+const kClientHello = Symbol('kClientHello');
+const kContinueBind = Symbol('kContinueBind');
+const kContinueConnect = Symbol('kContinueConnect');
+const kDestroy = Symbol('kDestroy');
+const kEndpointBound = Symbol('kEndpointBound');
+const kEndpointClose = Symbol('kEndpointClose');
+const kGetStreamOptions = Symbol('kGetStreamOptions');
+const kHandshake = Symbol('kHandshake');
+const kHandshakePost = Symbol('kHandshakePost');
+const kHeaders = Symbol('kHeaders');
+const kMaybeBind = Symbol('kMaybeBind');
+const kSocketReady = Symbol('kSocketReady');
+const kRemoveSession = Symbol('kRemove');
+const kRemoveStream = Symbol('kRemoveStream');
+const kServerBusy = Symbol('kServerBusy');
+const kSetHandle = Symbol('kSetHandle');
+const kSetSocket = Symbol('kSetSocket');
+const kStreamClose = Symbol('kStreamClose');
+const kStreamReset = Symbol('kStreamReset');
+const kTrackWriteState = Symbol('kTrackWriteState');
+const kUDPHandleForTesting = Symbol('kUDPHandleForTesting');
+const kUsePreferredAddress = Symbol('kUsePreferredAddress');
+const kVersionNegotiation = Symbol('kVersionNegotiation');
+
+const kSocketUnbound = 0;
+const kSocketPending = 1;
+const kSocketBound = 2;
+const kSocketClosing = 3;
+const kSocketDestroyed = 4;
+
+let diagnosticPacketLossWarned = false;
+let warnedVerifyHostnameIdentity = false;
+
+assert(process.versions.ngtcp2 !== undefined);
+
+// Called by the C++ internals when the socket is closed.
+// When this is called, the only thing left to do is destroy
+// the QuicSocket instance.
+function onSocketClose() {
+ this[owner_symbol].destroy();
+}
+
+// Called by the C++ internals when an error occurs on the socket.
+// When this is called, the only thing left to do is destroy
+// the QuicSocket instance with an error.
+// TODO(@jasnell): Should consolidate this with onSocketClose
+function onSocketError(err) {
+ this[owner_symbol].destroy(errnoException(err));
+}
+
+// Called by the C++ internals when the server busy state of
+// the QuicSocket has been changed.
+function onSocketServerBusy(on) {
+ this[owner_symbol][kServerBusy](!!on);
+}
+
+// Called by the C++ internals when a new server QuicSession has been created.
+function onSessionReady(handle) {
+ const socket = this[owner_symbol];
+ const session =
+ new QuicServerSession(
+ socket,
+ handle,
+ socket[kGetStreamOptions]());
+ process.nextTick(emit.bind(socket, 'session', session));
+}
+
+// During an immediate close, all currently open QuicStreams are
+// abruptly closed. If they are still writable or readable, an abort
+// event will be emitted, and RESET_STREAM and STOP_SENDING frames
+// will be transmitted as necessary. Once streams have been
+// shutdown, a CONNECTION_CLOSE frame will be sent and the
+// session will enter the closing period, after which it will
+// be destroyed either when the idle timeout expires, the
+// QuicSession is silently closed, or destroy is called.
+function onSessionClose(code, family) {
+ if (this[owner_symbol]) {
+ this[owner_symbol][kClose](family, code);
+ } else {
+ // When there's no owner_symbol, the session was closed
+ // before it could be fully set up. Just immediately
+ // close everything down on the native side.
+ this.destroy(code, family);
+ }
+}
+
+// Called by the C++ internals when a QuicSession has been destroyed.
+// When this is called, the QuicSession is no longer usable. Removing
+// the handle and emitting close is the only action.
+// TODO(@jasnell): In the future, this will need to act differently
+// for QuicClientSessions when autoResume is enabled.
+function onSessionDestroyed() {
+ const session = this[owner_symbol];
+ this[owner_symbol] = undefined;
+
+ if (session) {
+ session[kSetHandle]();
+ process.nextTick(emit.bind(session, 'close'));
+ }
+}
+
+// Used only within the onSessionClientHello function. Invoked
+// to complete the client hello process.
+function clientHelloCallback(err, ...args) {
+ if (err) {
+ this[owner_symbol].destroy(err);
+ return;
+ }
+ try {
+ this.onClientHelloDone(...args);
+ } catch (err) {
+ this[owner_symbol].destroy(err);
+ }
+}
+
+// This callback is invoked at the start of the TLS handshake to provide
+// some basic information about the ALPN, SNI, and Ciphers that are
+// being requested. It is only called if the 'clientHello' event is
+// listened for.
+function onSessionClientHello(alpn, servername, ciphers) {
+ this[owner_symbol][kClientHello](
+ alpn,
+ servername,
+ ciphers,
+ clientHelloCallback.bind(this));
+}
+
+// Used only within the onSessionCert function. Invoked
+// to complete the session cert process.
+function sessionCertCallback(err, context, ocspResponse) {
+ if (err) {
+ this[owner_symbol].destroy(err);
+ return;
+ }
+ if (context != null && !context.context) {
+ this[owner_symbol].destroy(
+ new ERR_INVALID_ARG_TYPE(
+ 'context',
+ 'SecureContext',
+ context));
+ }
+ if (ocspResponse != null) {
+ if (typeof ocspResponse === 'string')
+ ocspResponse = Buffer.from(ocspResponse);
+ if (!isArrayBufferView(ocspResponse)) {
+ this[owner_symbol].destroy(
+ new ERR_INVALID_ARG_TYPE(
+ 'ocspResponse',
+ ['string', 'Buffer', 'TypedArray', 'DataView'],
+ ocspResponse));
+ }
+ }
+ try {
+ this.onCertDone(context ? context.context : undefined, ocspResponse);
+ } catch (err) {
+ this[owner_symbol].destroy(err);
+ }
+}
+
+// This callback is only ever invoked for QuicServerSession instances,
+// and is used to trigger OCSP request processing when needed. The
+// user callback must invoke .onCertDone() in order for the
+// TLS handshake to continue.
+function onSessionCert(servername) {
+ this[owner_symbol][kCert](servername, sessionCertCallback.bind(this));
+}
+
+// This callback is only ever invoked for QuicClientSession instances,
+// and is used to deliver the OCSP response as provided by the server.
+// If the requestOCSP configuration option is false, this will never
+// be called.
+function onSessionStatus(response) {
+ this[owner_symbol][kCert](response);
+}
+
+// Called by the C++ internals when the TLS handshake is completed.
+function onSessionHandshake(
+ servername,
+ alpn,
+ cipher,
+ cipherVersion,
+ maxPacketLength,
+ verifyErrorReason,
+ verifyErrorCode,
+ earlyData) {
+ this[owner_symbol][kHandshake](
+ servername,
+ alpn,
+ cipher,
+ cipherVersion,
+ maxPacketLength,
+ verifyErrorReason,
+ verifyErrorCode,
+ earlyData);
+}
+
+// Called by the C++ internals when TLS session ticket data is
+// available. This is generally most useful on the client side
+// where the session ticket needs to be persisted for session
+// resumption and 0RTT.
+function onSessionTicket(sessionTicket, transportParams) {
+ if (this[owner_symbol]) {
+ process.nextTick(
+ emit.bind(
+ this[owner_symbol],
+ 'sessionTicket',
+ sessionTicket,
+ transportParams));
+ }
+}
+
+// Called by the C++ internals when path validation is completed.
+// This is a purely informational event that is emitted only when
+// there is a listener present for the pathValidation event.
+function onSessionPathValidation(res, local, remote) {
+ const session = this[owner_symbol];
+ if (session) {
+ process.nextTick(
+ emit.bind(
+ session,
+ 'pathValidation',
+ res === NGTCP2_PATH_VALIDATION_RESULT_FAILURE ? 'failure' : 'success',
+ local,
+ remote));
+ }
+}
+
+function onSessionUsePreferredAddress(address, port, family) {
+ const session = this[owner_symbol];
+ session[kUsePreferredAddress]({
+ address,
+ port,
+ type: family === AF_INET6 ? 'udp6' : 'udp4'
+ });
+}
+
+// Called by the C++ internals to emit a QLog record.
+function onSessionQlog(str) {
+ if (this.qlogBuffer === undefined) this.qlogBuffer = '';
+
+ const session = this[owner_symbol];
+
+ if (session && session.listenerCount('qlog') > 0) {
+ // Emit this chunk along with any previously buffered data.
+ str = this.qlogBuffer + str;
+ this.qlogBuffer = '';
+ if (str === '') return;
+ session.emit('qlog', str);
+ } else {
+ // Buffer the data until both the JS session object and a listener
+ // become available.
+ this.qlogBuffer += str;
+
+ if (!session || this.waitingForQlogListener) return;
+ this.waitingForQlogListener = true;
+
+ function onNewListener(ev) {
+ if (ev === 'qlog') {
+ session.removeListener('newListener', onNewListener);
+ process.nextTick(() => {
+ onSessionQlog.call(this, '');
+ });
+ }
+ }
+
+ session.on('newListener', onNewListener);
+ }
+}
+
+// Called when an error occurs in a QuicSession. When this happens,
+// the only remedy is to destroy the session.
+function onSessionError(error) {
+ if (this[owner_symbol]) {
+ this[owner_symbol].destroy(error);
+ }
+}
+
+// Called by the C++ internals when a client QuicSession receives
+// a version negotiation response from the server.
+function onSessionVersionNegotiation(
+ version,
+ requestedVersions,
+ supportedVersions) {
+ if (this[owner_symbol]) {
+ this[owner_symbol][kVersionNegotiation](
+ version,
+ requestedVersions,
+ supportedVersions);
+ }
+}
+
+// Called by the C++ internals to emit keylogging details for a
+// QuicSession.
+function onSessionKeylog(line) {
+ if (this[owner_symbol]) {
+ this[owner_symbol].emit('keylog', line);
+ }
+}
+
+// Called by the C++ internals when a new QuicStream has been created.
+function onStreamReady(streamHandle, id, push_id) {
+ const session = this[owner_symbol];
+
+ // onStreamReady should never be called if the stream is in a closing
+ // state because new streams should not have been accepted at the C++
+ // level.
+ assert(!session.closing);
+
+ // TODO(@jasnell): Get default options from session
+ const uni = id & 0b10;
+ const {
+ highWaterMark,
+ defaultEncoding,
+ } = session[kGetStreamOptions]();
+ const stream = new QuicStream({
+ writable: !uni,
+ highWaterMark,
+ defaultEncoding,
+ }, session, push_id);
+ stream[kSetHandle](streamHandle);
+ if (uni)
+ stream.end();
+ session[kAddStream](id, stream);
+ process.nextTick(emit.bind(session, 'stream', stream));
+}
+
+// Called by the C++ internals when a stream is closed and
+// needs to be destroyed on the JavaScript side.
+function onStreamClose(id, appErrorCode) {
+ this[owner_symbol][kStreamClose](id, appErrorCode);
+}
+
+// Called by the C++ internals when a stream has been reset
+function onStreamReset(id, appErrorCode) {
+ this[owner_symbol][kStreamReset](id, appErrorCode);
+}
+
+// Called when an error occurs in a QuicStream
+function onStreamError(streamHandle, error) {
+ streamHandle[owner_symbol].destroy(error);
+}
+
+// Called when a block of headers has been fully
+// received for the stream. Not all QuicStreams
+// will support headers. The headers argument
+// here is an Array of name-value pairs.
+function onStreamHeaders(id, headers, kind, push_id) {
+ this[owner_symbol][kHeaders](id, headers, kind, push_id);
+}
+
+// During a silent close, all currently open QuicStreams are abruptly
+// closed. If they are still writable or readable, an abort event will be
+// emitted, otherwise the stream is just destroyed. No RESET_STREAM or
+// STOP_SENDING is transmitted to the peer.
+function onSessionSilentClose(statelessReset, code, family) {
+ this[owner_symbol][kDestroy](statelessReset, family, code);
+}
+
+// When a stream is flow control blocked, causes a blocked event
+// to be emitted. This is a purely informational event.
+function onStreamBlocked() {
+ process.nextTick(emit.bind(this[owner_symbol], 'blocked'));
+}
+
+// Register the callbacks with the QUIC internal binding.
+setCallbacks({
+ onSocketClose,
+ onSocketError,
+ onSocketServerBusy,
+ onSessionReady,
+ onSessionCert,
+ onSessionClientHello,
+ onSessionClose,
+ onSessionDestroyed,
+ onSessionError,
+ onSessionHandshake,
+ onSessionKeylog,
+ onSessionQlog,
+ onSessionSilentClose,
+ onSessionStatus,
+ onSessionTicket,
+ onSessionVersionNegotiation,
+ onStreamReady,
+ onStreamClose,
+ onStreamError,
+ onStreamReset,
+ onSessionPathValidation,
+ onSessionUsePreferredAddress,
+ onStreamHeaders,
+ onStreamBlocked,
+});
+
+// connectAfterLookup is invoked when the QuicSocket connect()
+// method has been invoked. The first step is to resolve the given
+// remote hostname into an ip address. Once resolution is complete,
+// the resolved ip address needs to be passed on to the [kContinueConnect]
+// function or the QuicClientSession needs to be destroyed.
+function connectAfterLookup(type, err, ip) {
+ if (err) {
+ this.destroy(err);
+ return;
+ }
+ this[kContinueConnect](type, ip);
+}
+
+// Creates the SecureContext used by QuicSocket instances that are listening
+// for new connections.
+function createSecureContext(options, init_cb) {
+ const sc_options = validateCreateSecureContextOptions(options);
+ const { groups, earlyData } = sc_options;
+ const sc = _createSecureContext(sc_options);
+ // TODO(@jasnell): Determine if it's really necessary to pass in groups here.
+ init_cb(sc.context, groups, earlyData);
+ return sc;
+}
+
+function onNewListener(event) {
+ toggleListeners(this[kHandle], event, true);
+}
+
+function onRemoveListener(event) {
+ toggleListeners(this[kHandle], event, false);
+}
+
+// QuicEndpoint wraps a UDP socket and is owned
+// by a QuicSocket. It does not exist independently
+// of the QuicSocket.
+class QuicEndpoint {
+ #state = kSocketUnbound;
+ #socket = undefined;
+ #udpSocket = undefined;
+ #address = undefined;
+ #ipv6Only = undefined;
+ #lookup = undefined;
+ #port = undefined;
+ #reuseAddr = undefined;
+ #type = undefined;
+ #fd = undefined;
+
+ constructor(socket, options) {
+ const {
+ address,
+ ipv6Only,
+ lookup,
+ port = 0,
+ reuseAddr,
+ type,
+ preferred,
+ } = validateQuicEndpointOptions(options);
+ this.#socket = socket;
+ this.#address = address || (type === AF_INET6 ? '::' : '0.0.0.0');
+ this.#ipv6Only = !!ipv6Only;
+ this.#lookup = lookup || (type === AF_INET6 ? lookup6 : lookup4);
+ this.#port = port;
+ this.#reuseAddr = !!reuseAddr;
+ this.#udpSocket = dgram.createSocket(type === AF_INET6 ? 'udp6' : 'udp4');
+
+ // kUDPHandleForTesting is only used in the Node.js test suite to
+ // artificially test the endpoint. This code path should never be
+ // used in user code.
+ if (typeof options[kUDPHandleForTesting] === 'object') {
+ this.#udpSocket.bind(options[kUDPHandleForTesting]);
+ this.#state = kSocketBound;
+ this.#socket[kEndpointBound](this);
+ }
+ const udpHandle = this.#udpSocket[internalDgram.kStateSymbol].handle;
+ const handle = new QuicEndpointHandle(socket[kHandle], udpHandle);
+ handle[owner_symbol] = this;
+ this[kHandle] = handle;
+ socket[kHandle].addEndpoint(handle, !!preferred);
+ }
+
+ [kInspect]() {
+ const obj = {
+ address: this.address,
+ fd: this.#fd,
+ type: this.#type
+ };
+ return `QuicEndpoint ${util.format(obj)}`;
+ }
+
+ // afterLookup is invoked when binding a QuicEndpoint. The first
+ // step to binding is to resolve the given hostname into an ip
+ // address. Once resolution is complete, the ip address needs to
+ // be passed on to the [kContinueBind] function or the QuicEndpoint
+ // needs to be destroyed.
+ static #afterLookup = function(err, ip) {
+ if (err) {
+ this.destroy(err);
+ return;
+ }
+ this[kContinueBind](ip);
+ };
+
+ // kMaybeBind binds the endpoint on-demand if it is not already
+ // bound. If it is bound, we return immediately, otherwise put
+ // the endpoint into the pending state and initiate the binding
+ // process by calling the lookup to resolve the IP address.
+ [kMaybeBind]() {
+ if (this.#state !== kSocketUnbound)
+ return;
+ this.#state = kSocketPending;
+ this.#lookup(this.#address, QuicEndpoint.#afterLookup.bind(this));
+ }
+
+ // IP address resolution is completed and we're ready to finish
+ // binding to the local port.
+ [kContinueBind](ip) {
+ const udpHandle = this.#udpSocket[internalDgram.kStateSymbol].handle;
+ if (udpHandle == null) {
+ // TODO(@jasnell): We may need to throw an error here. Under
+ // what conditions does this happen?
+ return;
+ }
+ const flags =
+ (this.#reuseAddr ? UV_UDP_REUSEADDR : 0) |
+ (this.#ipv6Only ? UV_UDP_IPV6ONLY : 0);
+
+ const ret = udpHandle.bind(ip, this.#port, flags);
+ if (ret) {
+ this.destroy(exceptionWithHostPort(ret, 'bind', ip, this.#port || 0));
+ return;
+ }
+
+ // On Windows, the fd will be meaningless, but we always record it.
+ this.#fd = udpHandle.fd;
+ this.#state = kSocketBound;
+
+ // Notify the owning socket that the QuicEndpoint has been successfully
+ // bound to the local UDP port.
+ this.#socket[kEndpointBound](this);
+ }
+
+ [kDestroy](error) {
+ const handle = this[kHandle];
+ if (handle !== undefined) {
+ this[kHandle] = undefined;
+ handle[owner_symbol] = undefined;
+ handle.ondone = () => {
+ this.#udpSocket.close((err) => {
+ if (err) error = err;
+ this.#socket[kEndpointClose](this, error);
+ });
+ };
+ handle.waitForPendingCallbacks();
+ }
+ }
+
+ // If the QuicEndpoint is bound, returns an object detailing
+ // the local IP address, port, and address type to which it
+ // is bound. Otherwise, returns an empty object.
+ get address() {
+ if (this.#state !== kSocketDestroyed) {
+ try {
+ return this.#udpSocket.address();
+ } catch (err) {
+ if (err.code === 'EBADF') {
+ // If there is an EBADF error, the socket is not bound.
+ // Return empty object. Else, rethrow the error because
+ // something else bad happened.
+ return {};
+ }
+ throw err;
+ }
+ }
+ return {};
+ }
+
+ get fd() {
+ return this.#fd;
+ }
+
+ // True if the QuicEndpoint has been destroyed and is
+ // no longer usable.
+ get destroyed() {
+ return this.#state === kSocketDestroyed;
+ }
+
+ // True if binding has been initiated and is in progress.
+ get pending() {
+ return this.#state === kSocketPending;
+ }
+
+ // True if the QuicEndpoint has been bound to the local
+ // UDP port.
+ get bound() {
+ return this.#state === kSocketBound;
+ }
+
+ setTTL(ttl) {
+ if (this.#state === kSocketDestroyed)
+ throw new ERR_QUICSOCKET_DESTROYED('setTTL');
+ this.#udpSocket.setTTL(ttl);
+ return this;
+ }
+
+ setMulticastTTL(ttl) {
+ if (this.#state === kSocketDestroyed)
+ throw new ERR_QUICSOCKET_DESTROYED('setMulticastTTL');
+ this.#udpSocket.setMulticastTTL(ttl);
+ return this;
+ }
+
+ setBroadcast(on = true) {
+ if (this.#state === kSocketDestroyed)
+ throw new ERR_QUICSOCKET_DESTROYED('setBroadcast');
+ this.#udpSocket.setBroadcast(on);
+ return this;
+ }
+
+ setMulticastLoopback(on = true) {
+ if (this.#state === kSocketDestroyed)
+ throw new ERR_QUICSOCKET_DESTROYED('setMulticastLoopback');
+ this.#udpSocket.setMulticastLoopback(on);
+ return this;
+ }
+
+ setMulticastInterface(iface) {
+ if (this.#state === kSocketDestroyed)
+ throw new ERR_QUICSOCKET_DESTROYED('setMulticastInterface');
+ this.#udpSocket.setMulticastInterface(iface);
+ return this;
+ }
+
+ addMembership(address, iface) {
+ if (this.#state === kSocketDestroyed)
+ throw new ERR_QUICSOCKET_DESTROYED('addMembership');
+ this.#udpSocket.addMembership(address, iface);
+ return this;
+ }
+
+ dropMembership(address, iface) {
+ if (this.#state === kSocketDestroyed)
+ throw new ERR_QUICSOCKET_DESTROYED('dropMembership');
+ this.#udpSocket.dropMembership(address, iface);
+ return this;
+ }
+
+ ref() {
+ if (this.#state === kSocketDestroyed)
+ throw new ERR_QUICSOCKET_DESTROYED('ref');
+ this.#udpSocket.ref();
+ return this;
+ }
+
+ unref() {
+ if (this.#state === kSocketDestroyed)
+ throw new ERR_QUICSOCKET_DESTROYED('unref');
+ this.#udpSocket.unref();
+ return this;
+ }
+
+ destroy(error) {
+ // If the QuicEndpoint is already destroyed, do nothing
+ if (this.#state === kSocketDestroyed)
+ return;
+
+ // Mark the QuicEndpoint as being destroyed.
+ this.#state = kSocketDestroyed;
+
+ this[kDestroy](error);
+ }
+}
+
+// QuicSocket wraps a UDP socket plus the associated TLS context and QUIC
+// Protocol state. There may be *multiple* QUIC connections (QuicSession)
+// associated with a single QuicSocket.
+class QuicSocket extends EventEmitter {
+ #alpn = undefined;
+ #autoClose = undefined;
+ #client = undefined;
+ #defaultEncoding = undefined;
+ #endpoints = new Set();
+ #highWaterMark = undefined;
+ #lookup = undefined;
+ #server = undefined;
+ #serverBusy = false;
+ #serverListening = false;
+ #serverSecureContext = undefined;
+ #sessions = new Set();
+ #state = kSocketUnbound;
+ #stats = undefined;
+
+ constructor(options) {
+ const {
+ endpoint,
+
+ // True if the QuicSocket should automatically enter a graceful shutdown
+ // if it is not listening as a server and the last QuicClientSession
+ // closes
+ autoClose,
+
+ // Default configuration for QuicClientSessions
+ client,
+
+ // The maximum number of connections
+ maxConnections,
+
+ // The maximum number of connections per host
+ maxConnectionsPerHost,
+
+ // The maximum number of stateless resets per host
+ maxStatelessResetsPerHost,
+
+ // The maximum number of seconds for retry token
+ retryTokenTimeout,
+
+ // The DNS lookup function
+ lookup,
+
+ // Default configuration for QuicServerSessions
+ server,
+
+ // UDP type
+ type,
+
+ // True if address verification should be used.
+ validateAddress,
+
+ // True if an LRU should be used for add validation
+ validateAddressLRU,
+
+ // Whether qlog should be enabled for sessions
+ qlog,
+
+ // Stateless reset token secret (16 byte buffer)
+ statelessResetSecret,
+
+ // When true, stateless resets will not be sent (default false)
+ disableStatelessReset,
+ } = validateQuicSocketOptions(options);
+ super({ captureRejections: true });
+
+ this.#autoClose = autoClose;
+ this.#client = client;
+ this.#lookup = lookup || (type === AF_INET6 ? lookup6 : lookup4);
+ this.#server = server;
+
+ const socketOptions =
+ (validateAddress ? QUICSOCKET_OPTIONS_VALIDATE_ADDRESS : 0) |
+ (validateAddressLRU ? QUICSOCKET_OPTIONS_VALIDATE_ADDRESS_LRU : 0);
+
+ this[kSetHandle](
+ new QuicSocketHandle(
+ socketOptions,
+ retryTokenTimeout,
+ maxConnections,
+ maxConnectionsPerHost,
+ maxStatelessResetsPerHost,
+ qlog,
+ statelessResetSecret,
+ disableStatelessReset));
+
+ this.addEndpoint({
+ lookup: this.#lookup,
+ // Keep the lookup and ...endpoint in this order
+ // to allow the passed in endpoint options to
+ // override the lookup specifically for that endpoint
+ ...endpoint,
+ preferred: true
+ });
+ }
+
+ // Returns the default QuicStream options for peer-initiated
+ // streams. These are passed on to new client and server
+ // QuicSession instances when they are created.
+ [kGetStreamOptions]() {
+ return {
+ highWaterMark: this.#highWaterMark,
+ defaultEncoding: this.#defaultEncoding,
+ };
+ }
+
+ [kSetHandle](handle) {
+ this[kHandle] = handle;
+ if (handle !== undefined) {
+ handle[owner_symbol] = this;
+ this[async_id_symbol] = handle.getAsyncId();
+ }
+ }
+
+ [kInspect]() {
+ const obj = {
+ endpoints: this.endpoints,
+ sessions: this.#sessions,
+ };
+ return `QuicSocket ${util.format(obj)}`;
+ }
+
+ [kAddSession](session) {
+ this.#sessions.add(session);
+ }
+
+ [kRemoveSession](session) {
+ this.#sessions.delete(session);
+ this[kMaybeDestroy]();
+ }
+
+ // Bind all QuicEndpoints on-demand, only if they haven't already been bound.
+ // Function is a non-op if the socket is already bound or in the process of
+ // being bound, and will call the callback once the socket is ready.
+ [kMaybeBind](callback = () => {}) {
+ if (this.#state === kSocketBound)
+ return process.nextTick(callback);
+
+ this.once('ready', callback);
+
+ if (this.#state === kSocketPending)
+ return;
+ this.#state = kSocketPending;
+
+ for (const endpoint of this.#endpoints)
+ endpoint[kMaybeBind]();
+ }
+
+ [kEndpointBound](endpoint) {
+ if (this.#state === kSocketBound)
+ return;
+ this.#state = kSocketBound;
+
+ // Once the QuicSocket has been bound, we notify all currently
+ // existing QuicSessions. QuicSessions created after this
+ // point will automatically be notified that the QuicSocket
+ // is ready.
+ for (const session of this.#sessions)
+ session[kSocketReady]();
+
+ // The ready event indicates that the QuicSocket is ready to be
+ // used to either listen or connect. No QuicServerSession should
+ // exist before this event, and all QuicClientSession will remain
+ // in Initial states until ready is invoked.
+ process.nextTick(emit.bind(this, 'ready'));
+ }
+
+ // Called when a QuicEndpoint closes
+ [kEndpointClose](endpoint, error) {
+ this.#endpoints.delete(endpoint);
+ process.nextTick(emit.bind(this, 'endpointClose', endpoint, error));
+
+ // If there are no more QuicEndpoints, the QuicSocket is no
+ // longer usable.
+ if (this.#endpoints.size === 0) {
+ // Ensure that there are absolutely no additional sessions
+ for (const session of this.#sessions)
+ session.destroy(error);
+
+ if (error) process.nextTick(emit.bind(this, 'error', error));
+ process.nextTick(emit.bind(this, 'close'));
+ }
+ }
+
+ // kDestroy is called to actually free the QuicSocket resources and
+ // cause the error and close events to be emitted.
+ [kDestroy](error) {
+ // The QuicSocket will be destroyed once all QuicEndpoints
+ // are destroyed. See [kEndpointClose].
+ for (const endpoint of this.#endpoints)
+ endpoint.destroy(error);
+ }
+
+ // kMaybeDestroy is called one or more times after the close() method
+ // is called. The QuicSocket will be destroyed if there are no remaining
+ // open sessions.
+ [kMaybeDestroy]() {
+ if (this.closing && this.#sessions.size === 0) {
+ this.destroy();
+ return true;
+ }
+ return false;
+ }
+
+ // Called by the C++ internals to notify when server busy status is toggled.
+ [kServerBusy](on) {
+ this.#serverBusy = on;
+ process.nextTick(emit.bind(this, 'busy', on));
+ }
+
+ addEndpoint(options = {}) {
+ if (this.#state === kSocketDestroyed)
+ throw new ERR_QUICSOCKET_DESTROYED('listen');
+
+ // TODO(@jasnell): Also forbid adding an endpoint if
+ // the QuicSocket is closing.
+
+ const endpoint = new QuicEndpoint(this, options);
+ this.#endpoints.add(endpoint);
+
+ // If the QuicSocket is already bound at this point,
+ // also bind the newly created QuicEndpoint.
+ if (this.#state !== kSocketUnbound)
+ endpoint[kMaybeBind]();
+
+ return endpoint;
+ }
+
+ // Used only from within the #continueListen function. When a preferred
+ // address has been provided, the hostname given must be resolved into an
+ // IP address, which must be passed on to #completeListen or the QuicSocket
+ // needs to be destroyed.
+ static #afterPreferredAddressLookup = function(
+ transportParams,
+ port,
+ type,
+ err,
+ address) {
+ if (err) {
+ this.destroy(err);
+ return;
+ }
+ this.#completeListen(transportParams, { address, port, type });
+ };
+
+ // The #completeListen function is called after all of the necessary
+ // DNS lookups have been performed and we're ready to let the C++
+ // internals begin listening for new QuicServerSession instances.
+ #completeListen = function(transportParams, preferredAddress) {
+ const {
+ address,
+ port,
+ type = AF_INET,
+ } = { ...preferredAddress };
+
+ const {
+ rejectUnauthorized = !getAllowUnauthorized(),
+ requestCert = false,
+ } = transportParams;
+
+ // Transport Parameters are passed to the C++ side using a shared array.
+ // These are the transport parameters that will be used when a new
+ // server QuicSession is established. They are transmitted to the client
+ // as part of the server's initial TLS handshake. Once they are set, they
+ // cannot be modified.
+ setTransportParams(transportParams);
+
+ const options =
+ (rejectUnauthorized ? QUICSERVERSESSION_OPTION_REJECT_UNAUTHORIZED : 0) |
+ (requestCert ? QUICSERVERSESSION_OPTION_REQUEST_CERT : 0);
+
+ // When the handle is told to listen, it will begin acting as a QUIC
+ // server and will emit session events whenever a new QuicServerSession
+ // is created.
+ this[kHandle].listen(
+ this.#serverSecureContext.context,
+ address,
+ type,
+ port,
+ this.#alpn,
+ options);
+ process.nextTick(emit.bind(this, 'listening'));
+ };
+
+ // When the QuicSocket listen() function is called, the first step is to bind
+ // the underlying QuicEndpoint's. Once at least one endpoint has been bound,
+ // the continueListen function is invoked to continue the process of starting
+ // to listen.
+ //
+ // The preferredAddress is a preferred network endpoint that the server wishes
+ // connecting clients to use. If specified, this will be communicate to the
+ // client as part of the tranport parameters exchanged during the TLS
+ // handshake.
+ #continueListen = function(transportParams, lookup) {
+ const { preferredAddress } = transportParams;
+
+ // TODO(@jasnell): Currently, we wait to start resolving the
+ // preferred address until after we know the QuicSocket is
+ // bound. That's not strictly necessary as we can resolve the
+ // preferred address in parallel, which out to be faster but
+ // is a slightly more complicated to coordinate. In the future,
+ // we should do those things in parallel.
+ if (preferredAddress && typeof preferredAddress === 'object') {
+ const {
+ address,
+ port,
+ type = 'udp4',
+ } = { ...preferredAddress };
+ const typeVal = getSocketType(type);
+ // If preferred address is set, we need to perform a lookup on it
+ // to get the IP address. Only after that lookup completes can we
+ // continue with the listen operation, passing in the resolved
+ // preferred address.
+ lookup(
+ address || (typeVal === AF_INET6 ? '::' : '0.0.0.0'),
+ QuicSocket.#afterPreferredAddressLookup.bind(
+ this,
+ transportParams,
+ port,
+ typeVal));
+ return;
+ }
+ // If preferred address is not set, we can skip directly to the listen
+ this.#completeListen(transportParams);
+ };
+
+ // Begin listening for server connections. The callback that may be
+ // passed to this function is registered as a handler for the
+ // on('session') event. Errors may be thrown synchronously by this
+ // function.
+ listen(options, callback) {
+ if (this.#serverListening)
+ throw new ERR_QUICSOCKET_LISTENING();
+
+ if (this.#state === kSocketDestroyed ||
+ this.#state === kSocketClosing) {
+ throw new ERR_QUICSOCKET_DESTROYED('listen');
+ }
+
+ if (typeof options === 'function') {
+ callback = options;
+ options = {};
+ }
+
+ if (callback !== undefined && typeof callback !== 'function')
+ throw new ERR_INVALID_CALLBACK(callback);
+
+ options = {
+ ...this.#server,
+ ...options,
+ minVersion: 'TLSv1.3',
+ maxVersion: 'TLSv1.3',
+ };
+
+ // The ALPN protocol identifier is strictly required.
+ const {
+ alpn,
+ defaultEncoding,
+ highWaterMark,
+ transportParams,
+ } = validateQuicSocketListenOptions(options);
+
+ // Store the secure context so that it is not garbage collected
+ // while we still need to make use of it.
+ this.#serverSecureContext =
+ createSecureContext(
+ options,
+ initSecureContext);
+
+ this.#highWaterMark = highWaterMark;
+ this.#defaultEncoding = defaultEncoding;
+ this.#serverListening = true;
+ this.#alpn = alpn;
+
+ // If the callback function is provided, it is registered as a
+ // handler for the on('session') event and will be called whenever
+ // there is a new QuicServerSession instance created.
+ if (callback)
+ this.on('session', callback);
+
+ // Bind the QuicSocket to the local port if it hasn't been bound already.
+ this[kMaybeBind](this.#continueListen.bind(
+ this,
+ transportParams,
+ this.#lookup));
+ }
+
+ // When the QuicSocket connect() function is called, the first step is to bind
+ // the underlying QuicEndpoint's. Once at least one endpoint has been bound,
+ // the connectAfterBind function is invoked to continue the connection
+ // process.
+ //
+ // The immediate next step is to resolve the address into an ip address.
+ #continueConnect = function(session, lookup, address, type) {
+ // TODO(@jasnell): Currently, we perform the DNS resolution after
+ // the QuicSocket has been bound. We don't have to. We could do
+ // it in parallel while we're waitint to be bound but doing so
+ // is slightly more complicated.
+
+ // Notice here that connectAfterLookup is bound to the QuicSession
+ // that was created...
+ lookup(
+ address || (type === AF_INET6 ? '::' : '0.0.0.0'),
+ connectAfterLookup.bind(session, type));
+ };
+
+ // Creates and returns a new QuicClientSession.
+ connect(options, callback) {
+ if (this.#state === kSocketDestroyed ||
+ this.#state === kSocketClosing) {
+ throw new ERR_QUICSOCKET_DESTROYED('connect');
+ }
+
+ if (typeof options === 'function') {
+ callback = options;
+ options = undefined;
+ }
+
+ options = {
+ ...this.#client,
+ ...options
+ };
+
+ const {
+ type,
+ address,
+ } = validateQuicSocketConnectOptions(options);
+
+ const session = new QuicClientSession(this, options);
+
+ // TODO(@jasnell): This likely should listen for the secure event
+ // rather than the ready event
+ if (typeof callback === 'function')
+ session.once('ready', callback);
+
+ this[kMaybeBind](this.#continueConnect.bind(
+ this,
+ session,
+ this.#lookup,
+ address,
+ type));
+
+ return session;
+ }
+
+ // Initiate a Graceful Close of the QuicSocket.
+ // Existing QuicClientSession and QuicServerSession instances will be
+ // permitted to close naturally and gracefully on their own.
+ // The QuicSocket will be immediately closed and freed as soon as there
+ // are no additional session instances remaining. If there are no
+ // QuicClientSession or QuicServerSession instances, the QuicSocket
+ // will be immediately closed.
+ //
+ // If specified, the callback will be registered for once('close').
+ //
+ // No additional QuicServerSession instances will be accepted from
+ // remote peers, and calls to connect() to create QuicClientSession
+ // instances will fail. The QuicSocket will be otherwise usable in
+ // every other way.
+ //
+ // Subsequent calls to close(callback) will register the close callback
+ // if one is defined but will otherwise do nothing.
+ //
+ // Once initiated, a graceful close cannot be canceled. The graceful
+ // close can be interupted, however, by abruptly destroying the
+ // QuicSocket using the destroy() method.
+ //
+ // If close() is called before the QuicSocket has been bound (before
+ // either connect() or listen() have been called, or the QuicSocket
+ // is still in the pending state, the callback is registered for the
+ // once('close') event (if specified) and the QuicSocket is destroyed
+ // immediately.
+ close(callback) {
+ if (this.#state === kSocketDestroyed)
+ throw new ERR_QUICSOCKET_DESTROYED('close');
+
+ // If a callback function is specified, it is registered as a
+ // handler for the once('close') event. If the close occurs
+ // immediately, the close event will be emitted as soon as the
+ // process.nextTick queue is processed. Otherwise, the close
+ // event will occur at some unspecified time in the near future.
+ if (callback) {
+ if (typeof callback !== 'function')
+ throw new ERR_INVALID_CALLBACK();
+ this.once('close', callback);
+ }
+
+ // If we are already closing, do nothing else and wait
+ // for the close event to be invoked.
+ if (this.#state === kSocketClosing)
+ return;
+
+ // If the QuicSocket is otherwise not bound to the local
+ // port, destroy the QuicSocket immediately.
+ if (this.#state !== kSocketBound) {
+ this.destroy();
+ }
+
+ // Mark the QuicSocket as closing to prevent re-entry
+ this.#state = kSocketClosing;
+
+ // Otherwise, gracefully close each QuicSession, with
+ // [kMaybeDestroy]() being called after each closes.
+ const maybeDestroy = this[kMaybeDestroy].bind(this);
+
+ // Tell the underlying QuicSocket C++ object to stop
+ // listening for new QuicServerSession connections.
+ // New initial connection packets for currently unknown
+ // DCID's will be ignored.
+ if (this[kHandle]) {
+ this[kHandle].stopListening();
+ }
+ this.#serverListening = false;
+
+ // If there are no sessions, calling maybeDestroy
+ // will immediately and synchronously destroy the
+ // QuicSocket.
+ if (maybeDestroy())
+ return;
+
+ // If we got this far, there a QuicClientSession and
+ // QuicServerSession instances still, we need to trigger
+ // a graceful close for each of them. As each closes,
+ // they will call the kMaybeDestroy function. When there
+ // are no remaining session instances, the QuicSocket
+ // will be closed and destroyed.
+ for (const session of this.#sessions)
+ session.close(maybeDestroy);
+ }
+
+ // Initiate an abrupt close and destruction of the QuicSocket.
+ // Existing QuicClientSession and QuicServerSession instances will be
+ // immediately closed. If error is specified, it will be forwarded
+ // to each of the session instances.
+ //
+ // When the session instances are closed, an attempt to send a final
+ // CONNECTION_CLOSE will be made.
+ //
+ // The JavaScript QuicSocket object will be marked destroyed and will
+ // become unusable. As soon as all pending outbound UDP packets are
+ // flushed from the QuicSocket's queue, the QuicSocket C++ instance
+ // will be destroyed and freed from memory.
+ destroy(error) {
+ // If the QuicSocket is already destroyed, do nothing
+ if (this.#state === kSocketDestroyed)
+ return;
+
+ // Mark the QuicSocket as being destroyed.
+ this.#state = kSocketDestroyed;
+
+ // Immediately close any sessions that may be remaining.
+ // If the udp socket is in a state where it is able to do so,
+ // a final attempt to send CONNECTION_CLOSE frames for each
+ // closed session will be made.
+ for (const session of this.#sessions)
+ session.destroy(error);
+
+ this[kDestroy](error);
+ }
+
+ ref() {
+ if (this.#state === kSocketDestroyed)
+ throw new ERR_QUICSOCKET_DESTROYED('ref');
+ for (const endpoint of this.#endpoints)
+ endpoint.ref();
+ return this;
+ }
+
+ unref() {
+ if (this.#state === kSocketDestroyed)
+ throw new ERR_QUICSOCKET_DESTROYED('unref');
+ for (const endpoint of this.#endpoints)
+ endpoint.unref();
+ return this;
+ }
+
+ get endpoints() {
+ return Array.from(this.#endpoints);
+ }
+
+ // The sever secure context is the SecureContext specified when calling
+ // listen. It is the context that will be used with all new server
+ // QuicSession instances.
+ get serverSecureContext() {
+ return this.#serverSecureContext;
+ }
+
+ // True if at least one associated QuicEndpoint has been successfully
+ // bound to a local UDP port.
+ get bound() {
+ return this.#state === kSocketBound;
+ }
+
+ // True if graceful close has been initiated by calling close()
+ get closing() {
+ return this.#state === kSocketClosing;
+ }
+
+ // True if the QuicSocket has been destroyed and is no longer usable
+ get destroyed() {
+ return this.#state === kSocketDestroyed;
+ }
+
+ // True if listen() has been called successfully
+ get listening() {
+ return this.#serverListening;
+ }
+
+ // True if the QuicSocket is currently waiting on at least one
+ // QuicEndpoint to succesfully bind.g
+ get pending() {
+ return this.#state === kSocketPending;
+ }
+
+ // Marking a server as busy will cause all new
+ // connection attempts to fail with a SERVER_BUSY CONNECTION_CLOSE.
+ setServerBusy(on = true) {
+ if (this.#state === kSocketDestroyed)
+ throw new ERR_QUICSOCKET_DESTROYED('setServerBusy');
+ validateBoolean(on, 'on');
+ if (this.#serverBusy !== on)
+ this[kHandle].setServerBusy(on);
+ }
+
+ get duration() {
+ // TODO(@jasnell): If the object is destroyed, it should
+ // use a fixed duration rather than calculating from now
+ const now = process.hrtime.bigint();
+ const stats = this.#stats || this[kHandle].stats;
+ return now - stats[IDX_QUIC_SOCKET_STATS_CREATED_AT];
+ }
+
+ get boundDuration() {
+ // TODO(@jasnell): If the object is destroyed, it should
+ // use a fixed duration rather than calculating from now
+ const now = process.hrtime.bigint();
+ const stats = this.#stats || this[kHandle].stats;
+ return now - stats[IDX_QUIC_SOCKET_STATS_BOUND_AT];
+ }
+
+ get listenDuration() {
+ // TODO(@jasnell): If the object is destroyed, it should
+ // use a fixed duration rather than calculating from now
+ const now = process.hrtime.bigint();
+ const stats = this.#stats || this[kHandle].stats;
+ return now - stats[IDX_QUIC_SOCKET_STATS_LISTEN_AT];
+ }
+
+ get bytesReceived() {
+ const stats = this.#stats || this[kHandle].stats;
+ return stats[IDX_QUIC_SOCKET_STATS_BYTES_RECEIVED];
+ }
+
+ get bytesSent() {
+ const stats = this.#stats || this[kHandle].stats;
+ return stats[IDX_QUIC_SOCKET_STATS_BYTES_SENT];
+ }
+
+ get packetsReceived() {
+ const stats = this.#stats || this[kHandle].stats;
+ return stats[IDX_QUIC_SOCKET_STATS_PACKETS_RECEIVED];
+ }
+
+ get packetsSent() {
+ const stats = this.#stats || this[kHandle].stats;
+ return stats[IDX_QUIC_SOCKET_STATS_PACKETS_SENT];
+ }
+
+ get packetsIgnored() {
+ const stats = this.#stats || this[kHandle].stats;
+ return stats[IDX_QUIC_SOCKET_STATS_PACKETS_IGNORED];
+ }
+
+ get serverBusy() {
+ return this.#serverBusy;
+ }
+
+ get serverSessions() {
+ const stats = this.#stats || this[kHandle].stats;
+ return stats[IDX_QUIC_SOCKET_STATS_SERVER_SESSIONS];
+ }
+
+ get clientSessions() {
+ const stats = this.#stats || this[kHandle].stats;
+ return stats[IDX_QUIC_SOCKET_STATS_CLIENT_SESSIONS];
+ }
+
+ get statelessResetCount() {
+ const stats = this.#stats || this[kHandle].stats;
+ return stats[IDX_QUIC_SOCKET_STATS_STATELESS_RESET_COUNT];
+ }
+
+ get serverBusyCount() {
+ const stats = this.#stats || this[kHandle].stats;
+ return stats[IDX_QUIC_SOCKET_STATS_SERVER_BUSY_COUNT];
+ }
+
+ // Diagnostic packet loss is a testing mechanism that allows simulating
+ // pseudo-random packet loss for rx or tx. The value specified for each
+ // option is a number between 0 and 1 that identifies the possibility of
+ // packet loss in the given direction.
+ setDiagnosticPacketLoss(options) {
+ if (this.#state === kSocketDestroyed)
+ throw new ERR_QUICSOCKET_DESTROYED('setDiagnosticPacketLoss');
+ const {
+ rx = 0.0,
+ tx = 0.0
+ } = { ...options };
+ validateNumber(
+ rx,
+ 'options.rx',
+ /* min */ 0.0,
+ /* max */ 1.0);
+ validateNumber(
+ tx,
+ 'options.tx',
+ /* min */ 0.0,
+ /* max */ 1.0);
+ if ((rx > 0.0 || tx > 0.0) && !diagnosticPacketLossWarned) {
+ diagnosticPacketLossWarned = true;
+ process.emitWarning(
+ 'QuicSocket diagnostic packet loss is enabled. Received or ' +
+ 'transmitted packets will be randomly ignored to simulate ' +
+ 'network packet loss.');
+ }
+ this[kHandle].setDiagnosticPacketLoss(rx, tx);
+ }
+
+ // Toggles stateless reset on/off. By default, stateless reset tokens
+ // are generated when necessary. The disableStatelessReset option may
+ // be used when the QuicSocket is created to disable generation of
+ // stateless resets. The toggleStatelessReset method allows the setting
+ // to be switched on/off dynamically through the lifetime of the
+ // socket.
+ toggleStatelessReset() {
+ if (this.#state === kSocketDestroyed)
+ throw new ERR_QUICSOCKET_DESTROYED('toggleStatelessReset');
+ return this[kHandle].toggleStatelessReset();
+ }
+}
+
+class QuicSession extends EventEmitter {
+ #alpn = undefined;
+ #cipher = undefined;
+ #cipherVersion = undefined;
+ #closeCode = NGTCP2_NO_ERROR;
+ #closeFamily = QUIC_ERROR_APPLICATION;
+ #closing = false;
+ #destroyed = false;
+ #earlyData = false;
+ #handshakeComplete = false;
+ #idleTimeout = false;
+ #maxPacketLength = IDX_QUIC_SESSION_MAX_PACKET_SIZE_DEFAULT;
+ #servername = undefined;
+ #socket = undefined;
+ #statelessReset = false;
+ #stats = undefined;
+ #pendingStreams = new Set();
+ #streams = new Map();
+ #verifyErrorReason = undefined;
+ #verifyErrorCode = undefined;
+ #handshakeAckHistogram = undefined;
+ #handshakeContinuationHistogram = undefined;
+ #highWaterMark = undefined;
+ #defaultEncoding = undefined;
+
+ constructor(socket, options) {
+ const {
+ alpn,
+ servername,
+ highWaterMark,
+ defaultEncoding,
+ } = options;
+ super({ captureRejections: true });
+ this.on('newListener', onNewListener);
+ this.on('removeListener', onRemoveListener);
+ this.#socket = socket;
+ this.#servername = servername;
+ this.#alpn = alpn;
+ this.#highWaterMark = highWaterMark;
+ this.#defaultEncoding = defaultEncoding;
+ socket[kAddSession](this);
+ }
+
+ // kGetStreamOptions is called to get the configured options
+ // for peer initiated QuicStream instances.
+ [kGetStreamOptions]() {
+ return {
+ highWaterMark: this.#highWaterMark,
+ defaultEncoding: this.#defaultEncoding,
+ };
+ }
+
+ // Sets the internal handle for the QuicSession instance. For
+ // server QuicSessions, this is called immediately as the
+ // handle is created before the QuicServerSession JS object.
+ // For client QuicSession instances, the connect() method
+ // must first perform DNS resolution on the provided address
+ // before the underlying QuicSession handle can be created.
+ [kSetHandle](handle) {
+ this[kHandle] = handle;
+ if (handle !== undefined) {
+ handle[owner_symbol] = this;
+ this.#handshakeAckHistogram = new Histogram(handle.ack);
+ this.#handshakeContinuationHistogram = new Histogram(handle.rate);
+ } else {
+ if (this.#handshakeAckHistogram)
+ this.#handshakeAckHistogram[kDestroyHistogram]();
+ if (this.#handshakeContinuationHistogram)
+ this.#handshakeContinuationHistogram[kDestroyHistogram]();
+ }
+ }
+
+ // Called when a client QuicSession instance receives a version
+ // negotiation packet from the server peer. The client QuicSession
+ // is destroyed immediately. This is not called at all for server
+ // QuicSessions.
+ [kVersionNegotiation](version, requestedVersions, supportedVersions) {
+ const err =
+ new ERR_QUICSESSION_VERSION_NEGOTIATION(
+ version,
+ requestedVersions,
+ supportedVersions);
+ err.detail = {
+ version,
+ requestedVersions,
+ supportedVersions,
+ };
+ this.destroy(err);
+ }
+
+ // Causes the QuicSession to be immediately destroyed, but with
+ // additional metadata set.
+ [kDestroy](statelessReset, family, code) {
+ this.#statelessReset = !!statelessReset;
+ this.#closeCode = code;
+ this.#closeFamily = family;
+ this.destroy();
+ }
+
+ // Immediate close has been initiated for the session. Any
+ // still open QuicStreams must be abandoned and shutdown
+ // with RESET_STREAM and STOP_SENDING frames transmitted
+ // as appropriate. Once the streams have been shutdown, a
+ // CONNECTION_CLOSE will be generated and sent, switching
+ // the session into the closing period.
+ [kClose](family, code) {
+ // Do nothing if the QuicSession has already been destroyed.
+ if (this.#destroyed)
+ return;
+
+ // Set the close code and family so we can keep track.
+ this.#closeCode = code;
+ this.#closeFamily = family;
+
+ // Shutdown all pending streams. These are Streams that
+ // have been created but do not yet have a handle assigned.
+ for (const stream of this.#pendingStreams)
+ stream[kClose](family, code);
+
+ // Shutdown all of the remaining streams
+ for (const stream of this.#streams.values())
+ stream[kClose](family, code);
+
+ // By this point, all necessary RESET_STREAM and
+ // STOP_SENDING frames ought to have been sent,
+ // so now we just trigger sending of the
+ // CONNECTION_CLOSE frame.
+ this[kHandle].close(family, code);
+ }
+
+ // Closes the specified stream with the given code. The
+ // QuicStream object will be destroyed.
+ [kStreamClose](id, code) {
+ const stream = this.#streams.get(id);
+ if (stream === undefined)
+ return;
+
+ stream.destroy();
+ }
+
+ // Delivers a block of headers to the appropriate QuicStream
+ // instance. This will only be called if the ALPN selected
+ // is known to support headers.
+ [kHeaders](id, headers, kind, push_id) {
+ const stream = this.#streams.get(id);
+ if (stream === undefined)
+ return;
+
+ stream[kHeaders](headers, kind, push_id);
+ }
+
+ [kStreamReset](id, code) {
+ const stream = this.#streams.get(id);
+ if (stream === undefined)
+ return;
+
+ stream[kStreamReset](code);
+ }
+
+ [kInspect]() {
+ const obj = {
+ alpn: this.#alpn,
+ cipher: this.cipher,
+ closing: this.closing,
+ closeCode: this.closeCode,
+ destroyed: this.destroyed,
+ earlyData: this.#earlyData,
+ maxStreams: this.maxStreams,
+ servername: this.servername,
+ streams: this.#streams.size,
+ stats: {
+ handshakeAck: this.handshakeAckHistogram,
+ handshakeContinuation: this.handshakeContinuationHistogram,
+ }
+ };
+ return `${this.constructor.name} ${util.format(obj)}`;
+ }
+
+ [kSetSocket](socket) {
+ this.#socket = socket;
+ }
+
+ // Called at the completion of the TLS handshake for the local peer
+ [kHandshake](
+ servername,
+ alpn,
+ cipher,
+ cipherVersion,
+ maxPacketLength,
+ verifyErrorReason,
+ verifyErrorCode,
+ earlyData) {
+ this.#handshakeComplete = true;
+ this.#servername = servername;
+ this.#alpn = alpn;
+ this.#cipher = cipher;
+ this.#cipherVersion = cipherVersion;
+ this.#maxPacketLength = maxPacketLength;
+ this.#verifyErrorReason = verifyErrorReason;
+ this.#verifyErrorCode = verifyErrorCode;
+ this.#earlyData = earlyData;
+ if (!this[kHandshakePost]())
+ return;
+
+ process.nextTick(
+ emit.bind(this, 'secure', servername, alpn, this.cipher));
+ }
+
+ // Non-op for the default case. QuicClientSession
+ // overrides this with some client-side specific
+ // checks
+ [kHandshakePost]() {
+ return true;
+ }
+
+ [kRemoveStream](stream) {
+ this.#streams.delete(stream.id);
+ }
+
+ [kAddStream](id, stream) {
+ stream.once('close', this[kMaybeDestroy].bind(this));
+ this.#streams.set(id, stream);
+ }
+
+ // The QuicSession will be destroyed if closing has been
+ // called and there are no remaining streams
+ [kMaybeDestroy]() {
+ if (this.#closing && this.#streams.size === 0)
+ this.destroy();
+ }
+
+ // Called when a client QuicSession has opted to use the
+ // server provided preferred address. This is a purely
+ // informationational notification. It is not called on
+ // server QuicSession instances.
+ [kUsePreferredAddress](address) {
+ process.nextTick(
+ emit.bind(this, 'usePreferredAddress', address));
+ }
+
+ // Closing allows any existing QuicStream's to complete
+ // normally but disallows any new QuicStreams from being
+ // opened. Calls to openStream() will fail, and new streams
+ // from the peer will be rejected/ignored.
+ close(callback) {
+ if (this.#destroyed)
+ throw new ERR_QUICSESSION_DESTROYED('close');
+
+ if (callback) {
+ if (typeof callback !== 'function')
+ throw new ERR_INVALID_CALLBACK();
+ this.once('close', callback);
+ }
+
+ // If we're already closing, do nothing else.
+ // Callback will be invoked once the session
+ // has been destroyed
+ if (this.#closing)
+ return;
+
+ this.#closing = true;
+ this[kHandle].gracefulClose();
+
+ // See if we can close immediately.
+ this[kMaybeDestroy]();
+ }
+
+ // Destroying synchronously shuts down and frees the
+ // QuicSession immediately, even if there are still open
+ // streams.
+ //
+ // A CONNECTION_CLOSE packet is sent to the
+ // connected peer and the session is immediately
+ // destroyed.
+ //
+ // If destroy is called with an error argument, the
+ // 'error' event is emitted on next tick.
+ //
+ // Once destroyed, and after the 'error' event (if any),
+ // the close event is emitted on next tick.
+ destroy(error) {
+ // Destroy can only be called once. Multiple calls will be ignored
+ if (this.#destroyed)
+ return;
+ this.#destroyed = true;
+ this.#closing = false;
+
+ if (typeof error === 'number' ||
+ (error != null &&
+ typeof error === 'object' &&
+ !(error instanceof Error))) {
+ const {
+ closeCode,
+ closeFamily
+ } = validateCloseCode(error);
+ this.#closeCode = closeCode;
+ this.#closeFamily = closeFamily;
+ error = new ERR_QUIC_ERROR(closeCode, closeFamily);
+ }
+
+ // Destroy any pending streams immediately. These
+ // are streams that have been created but have not
+ // yet been assigned an internal handle.
+ for (const stream of this.#pendingStreams)
+ stream.destroy(error);
+
+ // Destroy any remaining streams immediately.
+ for (const stream of this.#streams.values())
+ stream.destroy(error);
+
+ this.removeListener('newListener', onNewListener);
+ this.removeListener('removeListener', onRemoveListener);
+
+ if (error) process.nextTick(emit.bind(this, 'error', error));
+
+ const handle = this[kHandle];
+ if (handle !== undefined) {
+ // Copy the stats for use after destruction
+ this.#stats = new BigInt64Array(handle.stats);
+ this.#idleTimeout = !!handle.state[IDX_QUIC_SESSION_STATE_IDLE_TIMEOUT];
+ // Calling destroy will cause a CONNECTION_CLOSE to be
+ // sent to the peer and will destroy the QuicSession
+ // handler immediately.
+ handle.destroy(this.#closeCode, this.#closeFamily);
+ } else {
+ process.nextTick(emit.bind(this, 'close'));
+ }
+
+ // Remove the QuicSession JavaScript object from the
+ // associated QuicSocket.
+ this.#socket[kRemoveSession](this);
+ this.#socket = undefined;
+ }
+
+ // For server QuicSession instances, true if earlyData is
+ // enabled. For client QuicSessions, true only if session
+ // resumption is used and early data was accepted during
+ // the TLS handshake. The value is set only after the
+ // TLS handshake is completed (immeditely before the
+ // secure event is emitted)
+ get usingEarlyData() {
+ return this.#earlyData;
+ }
+
+ get maxStreams() {
+ let bidi = 0;
+ let uni = 0;
+ if (this[kHandle]) {
+ bidi = this[kHandle].state[IDX_QUIC_SESSION_STATE_MAX_STREAMS_BIDI];
+ uni = this[kHandle].state[IDX_QUIC_SESSION_STATE_MAX_STREAMS_UNI];
+ }
+ return { bidi, uni };
+ }
+
+ get address() {
+ return this.#socket ? this.#socket.address : {};
+ }
+
+ get maxDataLeft() {
+ return this[kHandle] ?
+ this[kHandle].state[IDX_QUIC_SESSION_STATE_MAX_DATA_LEFT] : 0;
+ }
+
+ get bytesInFlight() {
+ return this[kHandle] ?
+ this[kHandle].state[IDX_QUIC_SESSION_STATE_BYTES_IN_FLIGHT] : 0;
+ }
+
+ get blockCount() {
+ return this[kHandle] ?
+ this[kHandle].state[IDX_QUIC_SESSION_STATS_BLOCK_COUNT] : 0;
+ }
+
+ get authenticated() {
+ // Specifically check for null. Undefined means the check has not
+ // been performed yet, another other value other than null means
+ // there was an error
+ return this.#verifyErrorReason == null;
+ }
+
+ get authenticationError() {
+ if (this.authenticated)
+ return undefined;
+ // eslint-disable-next-line no-restricted-syntax
+ const err = new Error(this.#verifyErrorReason);
+ const code = 'ERR_QUIC_VERIFY_' + this.#verifyErrorCode;
+ err.name = `Error [${code}]`;
+ err.code = code;
+ return err;
+ }
+
+ get remoteAddress() {
+ const out = {};
+ if (this[kHandle])
+ this[kHandle].getRemoteAddress(out);
+ return out;
+ }
+
+ get handshakeComplete() {
+ return this.#handshakeComplete;
+ }
+
+ get handshakeConfirmed() {
+ return Boolean(this[kHandle] ?
+ this[kHandle].state[IDX_QUIC_SESSION_STATE_HANDSHAKE_CONFIRMED] : 0);
+ }
+
+ get idleTimeout() {
+ return this.#idleTimeout;
+ }
+
+ get alpnProtocol() {
+ return this.#alpn;
+ }
+
+ get cipher() {
+ const name = this.#cipher;
+ const version = this.#cipherVersion;
+ return this.handshakeComplete ? { name, version } : {};
+ }
+
+ getCertificate() {
+ return this[kHandle] ?
+ translatePeerCertificate(this[kHandle].getCertificate() || {}) : {};
+ }
+
+ getPeerCertificate(detailed = false) {
+ return this[kHandle] ?
+ translatePeerCertificate(
+ this[kHandle].getPeerCertificate(detailed) || {}) : {};
+ }
+
+ ping() {
+ if (!this[kHandle])
+ throw new ERR_QUICSESSION_DESTROYED('ping');
+ this[kHandle].ping();
+ }
+
+ get servername() {
+ return this.#servername;
+ }
+
+ get destroyed() {
+ return this.#destroyed;
+ }
+
+ get closing() {
+ return this.#closing;
+ }
+
+ get closeCode() {
+ return {
+ code: this.#closeCode,
+ family: this.#closeFamily
+ };
+ }
+
+ get socket() {
+ return this.#socket;
+ }
+
+ get statelessReset() {
+ return this.#statelessReset;
+ }
+
+ openStream(options) {
+ if (this.#destroyed || this.#closing)
+ throw new ERR_QUICSESSION_DESTROYED('openStream');
+ const {
+ halfOpen, // Unidirectional or Bidirectional
+ highWaterMark,
+ defaultEncoding,
+ } = validateQuicStreamOptions(options);
+
+ const stream = new QuicStream({
+ highWaterMark,
+ defaultEncoding,
+ readable: !halfOpen
+ }, this);
+
+ // TODO(@jasnell): This really shouldn't be necessary
+ if (halfOpen) {
+ stream.push(null);
+ stream.read();
+ }
+
+ this.#pendingStreams.add(stream);
+
+ // If early data is being used, we can create the internal QuicStream on the
+ // ready event, that is immediately after the internal QuicSession handle
+ // has been created. Otherwise, we have to wait until the secure event
+ // signaling the completion of the TLS handshake.
+ const makeStream = QuicSession.#makeStream.bind(this, stream, halfOpen);
+ let deferred = false;
+ if (this.allowEarlyData && !this.ready) {
+ deferred = true;
+ this.once('ready', makeStream);
+ } else if (!this.handshakeComplete) {
+ deferred = true;
+ this.once('secure', makeStream);
+ }
+
+ if (!deferred)
+ makeStream(stream, halfOpen);
+
+ return stream;
+ }
+
+ static #makeStream = function(stream, halfOpen) {
+ this.#pendingStreams.delete(stream);
+ const handle =
+ halfOpen ?
+ _openUnidirectionalStream(this[kHandle]) :
+ _openBidirectionalStream(this[kHandle]);
+
+ if (handle === undefined) {
+ stream.destroy(new ERR_QUICSTREAM_OPEN_FAILED());
+ return;
+ }
+
+ stream[kSetHandle](handle);
+ this[kAddStream](stream.id, stream);
+ };
+
+ get duration() {
+ const now = process.hrtime.bigint();
+ const stats = this.#stats || this[kHandle].stats;
+ return now - stats[IDX_QUIC_SESSION_STATS_CREATED_AT];
+ }
+
+ get handshakeDuration() {
+ const stats = this.#stats || this[kHandle].stats;
+ const end =
+ this.handshakeComplete ?
+ stats[4] : process.hrtime.bigint();
+ return end - stats[IDX_QUIC_SESSION_STATS_HANDSHAKE_START_AT];
+ }
+
+ get bytesReceived() {
+ const stats = this.#stats || this[kHandle].stats;
+ return stats[IDX_QUIC_SESSION_STATS_BYTES_RECEIVED];
+ }
+
+ get bytesSent() {
+ const stats = this.#stats || this[kHandle].stats;
+ return stats[IDX_QUIC_SESSION_STATS_BYTES_SENT];
+ }
+
+ get bidiStreamCount() {
+ const stats = this.#stats || this[kHandle].stats;
+ return stats[IDX_QUIC_SESSION_STATS_BIDI_STREAM_COUNT];
+ }
+
+ get uniStreamCount() {
+ const stats = this.#stats || this[kHandle].stats;
+ return stats[IDX_QUIC_SESSION_STATS_UNI_STREAM_COUNT];
+ }
+
+ get maxInFlightBytes() {
+ const stats = this.#stats || this[kHandle].stats;
+ return stats[IDX_QUIC_SESSION_STATS_MAX_BYTES_IN_FLIGHT];
+ }
+
+ get lossRetransmitCount() {
+ const stats = this.#stats || this[kHandle].stats;
+ return stats[IDX_QUIC_SESSION_STATS_LOSS_RETRANSMIT_COUNT];
+ }
+
+ get ackDelayRetransmitCount() {
+ const stats = this.#stats || this[kHandle].stats;
+ return stats[IDX_QUIC_SESSION_STATS_ACK_DELAY_RETRANSMIT_COUNT];
+ }
+
+ get peerInitiatedStreamCount() {
+ const stats = this.#stats || this[kHandle].stats;
+ return stats[IDX_QUIC_SESSION_STATS_STREAMS_IN_COUNT];
+ }
+
+ get selfInitiatedStreamCount() {
+ const stats = this.#stats || this[kHandle].stats;
+ return stats[IDX_QUIC_SESSION_STATS_STREAMS_OUT_COUNT];
+ }
+
+ get keyUpdateCount() {
+ const stats = this.#stats || this[kHandle].stats;
+ return stats[IDX_QUIC_SESSION_STATS_KEYUPDATE_COUNT];
+ }
+
+ get minRTT() {
+ const stats = this.#stats || this[kHandle].stats;
+ return stats[IDX_QUIC_SESSION_STATS_MIN_RTT];
+ }
+
+ get latestRTT() {
+ const stats = this.#stats || this[kHandle].stats;
+ return stats[IDX_QUIC_SESSION_STATS_LATEST_RTT];
+ }
+
+ get smoothedRTT() {
+ const stats = this.#stats || this[kHandle].stats;
+ return stats[IDX_QUIC_SESSION_STATS_SMOOTHED_RTT];
+ }
+
+ updateKey() {
+ // Initiates a key update for the connection.
+ if (this.#destroyed || this.#closing)
+ throw new ERR_QUICSESSION_DESTROYED('updateKey');
+ if (!this.handshakeConfirmed)
+ throw new ERR_QUICSESSION_UPDATEKEY();
+ return this[kHandle].updateKey();
+ }
+
+ get handshakeAckHistogram() {
+ return this.#handshakeAckHistogram;
+ }
+
+ get handshakeContinuationHistogram() {
+ return this.#handshakeContinuationHistogram;
+ }
+
+ // TODO(addaleax): This is a temporary solution for testing and should be
+ // removed later.
+ removeFromSocket() {
+ return this[kHandle].removeFromSocket();
+ }
+}
+
+class QuicServerSession extends QuicSession {
+ #contexts = [];
+
+ constructor(socket, handle, options) {
+ const {
+ highWaterMark,
+ defaultEncoding,
+ } = options;
+ super(socket, { highWaterMark, defaultEncoding });
+ this[kSetHandle](handle);
+
+ // Both the handle and socket are immediately usable
+ // at this point so trigger the ready event.
+ this[kSocketReady]();
+ }
+
+ // Called only when a clientHello event handler is registered.
+ // Allows user code an opportunity to interject into the start
+ // of the TLS handshake.
+ [kClientHello](alpn, servername, ciphers, callback) {
+ this.emit(
+ 'clientHello',
+ alpn,
+ servername,
+ ciphers,
+ callback.bind(this[kHandle]));
+ }
+
+ // Called only when an OCSPRequest event handler is registered.
+ // Allows user code the ability to answer the OCSP query.
+ [kCert](servername, callback) {
+ const { serverSecureContext } = this.socket;
+ let { context } = serverSecureContext;
+
+ for (var i = 0; i < this.#contexts.length; i++) {
+ const elem = this.#contexts[i];
+ if (elem[0].test(servername)) {
+ context = elem[1];
+ break;
+ }
+ }
+
+ this.emit(
+ 'OCSPRequest',
+ servername,
+ context,
+ callback.bind(this[kHandle]));
+ }
+
+ [kSocketReady]() {
+ process.nextTick(emit.bind(this, 'ready'));
+ }
+
+ get ready() { return true; }
+
+ get allowEarlyData() { return false; }
+
+ addContext(servername, context = {}) {
+ validateString(servername, 'servername');
+ validateObject(context, 'context');
+
+ // TODO(@jasnell): Consider unrolling regex
+ const re = new RegExp('^' +
+ servername.replace(/([.^$+?\-\\[\]{}])/g, '\\$1')
+ .replace(/\*/g, '[^.]*') +
+ '$');
+ this.#contexts.push([re, _createSecureContext(context)]);
+ }
+}
+
+class QuicClientSession extends QuicSession {
+ #allowEarlyData = false;
+ #autoStart = true;
+ #dcid = undefined;
+ #handshakeStarted = false;
+ #ipv6Only = undefined;
+ #minDHSize = undefined;
+ #port = undefined;
+ #ready = 0;
+ #remoteTransportParams = undefined;
+ #requestOCSP = undefined;
+ #secureContext = undefined;
+ #sessionTicket = undefined;
+ #transportParams = undefined;
+ #preferredAddressPolicy;
+ #verifyHostnameIdentity = true;
+ #qlogEnabled = false;
+
+ constructor(socket, options) {
+ const sc_options = {
+ ...options,
+ minVersion: 'TLSv1.3',
+ maxVersion: 'TLSv1.3',
+ };
+ const {
+ autoStart,
+ alpn,
+ dcid,
+ ipv6Only,
+ minDHSize,
+ port,
+ preferredAddressPolicy,
+ remoteTransportParams,
+ requestOCSP,
+ servername,
+ sessionTicket,
+ verifyHostnameIdentity,
+ qlog,
+ highWaterMark,
+ defaultEncoding,
+ } = validateQuicClientSessionOptions(options);
+
+ if (!verifyHostnameIdentity && !warnedVerifyHostnameIdentity) {
+ warnedVerifyHostnameIdentity = true;
+ process.emitWarning(
+ 'QUIC hostname identity verification is disabled. This violates QUIC ' +
+ 'specification requirements and reduces security. Hostname identity ' +
+ 'verification should only be disabled for debugging purposes.'
+ );
+ }
+
+ super(socket, { servername, alpn, highWaterMark, defaultEncoding });
+ this.#autoStart = autoStart;
+ this.#handshakeStarted = autoStart;
+ this.#dcid = dcid;
+ this.#ipv6Only = ipv6Only;
+ this.#minDHSize = minDHSize;
+ this.#port = port || 0;
+ this.#preferredAddressPolicy = preferredAddressPolicy;
+ this.#requestOCSP = requestOCSP;
+ this.#secureContext =
+ createSecureContext(
+ sc_options,
+ initSecureContextClient);
+ this.#transportParams = validateTransportParams(options);
+ this.#verifyHostnameIdentity = verifyHostnameIdentity;
+ this.#qlogEnabled = qlog;
+
+ // If provided, indicates that the client is attempting to
+ // resume a prior session. Early data would be enabled.
+ this.#remoteTransportParams = remoteTransportParams;
+ this.#sessionTicket = sessionTicket;
+ this.#allowEarlyData =
+ remoteTransportParams !== undefined &&
+ sessionTicket !== undefined;
+
+ if (socket.bound)
+ this[kSocketReady]();
+ }
+
+ [kHandshakePost]() {
+ const { type, size } = this.ephemeralKeyInfo;
+ if (type === 'DH' && size < this.#minDHSize) {
+ this.destroy(new ERR_TLS_DH_PARAM_SIZE(size));
+ return false;
+ }
+ return true;
+ }
+
+ [kCert](response) {
+ this.emit('OCSPResponse', response);
+ }
+
+ [kContinueConnect](type, ip) {
+ setTransportParams(this.#transportParams);
+
+ const options =
+ (this.#verifyHostnameIdentity ?
+ QUICCLIENTSESSION_OPTION_VERIFY_HOSTNAME_IDENTITY : 0) |
+ (this.#requestOCSP ?
+ QUICCLIENTSESSION_OPTION_REQUEST_OCSP : 0);
+
+ const handle =
+ _createClientSession(
+ this.socket[kHandle],
+ type,
+ ip,
+ this.#port,
+ this.#secureContext.context,
+ this.servername || ip,
+ this.#remoteTransportParams,
+ this.#sessionTicket,
+ this.#dcid,
+ this.#preferredAddressPolicy,
+ this.alpnProtocol,
+ options,
+ this.#qlogEnabled,
+ this.#autoStart);
+
+ // We no longer need these, unset them so
+ // memory can be garbage collected.
+ this.#remoteTransportParams = undefined;
+ this.#sessionTicket = undefined;
+ this.#dcid = undefined;
+
+ // If handle is a number, creating the session failed.
+ if (typeof handle === 'number') {
+ let reason;
+ switch (handle) {
+ case ERR_FAILED_TO_CREATE_SESSION:
+ reason = 'QuicSession bootstrap failed';
+ break;
+ case ERR_INVALID_REMOTE_TRANSPORT_PARAMS:
+ reason = 'Invalid Remote Transport Params';
+ break;
+ case ERR_INVALID_TLS_SESSION_TICKET:
+ reason = 'Invalid TLS Session Ticket';
+ break;
+ default:
+ reason = `${handle}`;
+ }
+ this.destroy(new ERR_QUICCLIENTSESSION_FAILED(reason));
+ return;
+ }
+
+ this[kSetHandle](handle);
+
+ // Listeners may have been added before the handle was created.
+ // Ensure that we toggle those listeners in the handle state.
+
+ if (this.listenerCount('keylog') > 0)
+ toggleListeners(handle, 'keylog', true);
+
+ if (this.listenerCount('pathValidation') > 0)
+ toggleListeners(handle, 'pathValidation', true);
+
+ if (this.listenerCount('usePreferredAddress') > 0)
+ toggleListeners(handle, 'usePreferredAddress', true);
+
+ this.#maybeReady(0x2);
+ }
+
+ [kSocketReady]() {
+ this.#maybeReady(0x1);
+ }
+
+ // The QuicClientSession is ready for use only after
+ // (a) The QuicSocket has been bound and
+ // (b) The internal handle has been created
+ #maybeReady = function(flag) {
+ this.#ready |= flag;
+ if (this.ready)
+ process.nextTick(emit.bind(this, 'ready'));
+ };
+
+ get allowEarlyData() {
+ return this.#allowEarlyData;
+ }
+
+ get ready() {
+ return this.#ready === 0x3;
+ }
+
+ get handshakeStarted() {
+ return this.#handshakeStarted;
+ }
+
+ startHandshake() {
+ if (this.destroyed)
+ throw new ERR_QUICSESSION_DESTROYED('startHandshake');
+ if (this.#handshakeStarted)
+ return;
+ this.#handshakeStarted = true;
+ if (!this.ready) {
+ this.once('ready', () => this[kHandle].startHandshake());
+ } else {
+ this[kHandle].startHandshake();
+ }
+ }
+
+ get ephemeralKeyInfo() {
+ return this[kHandle] !== undefined ?
+ this[kHandle].getEphemeralKeyInfo() :
+ {};
+ }
+
+ #setSocketAfterBind = function(socket, callback) {
+ if (socket.destroyed) {
+ callback(new ERR_QUICSOCKET_DESTROYED('setSocket'));
+ return;
+ }
+
+ if (!this[kHandle].setSocket(socket[kHandle])) {
+ callback(new ERR_QUICCLIENTSESSION_FAILED_SETSOCKET());
+ return;
+ }
+
+ if (this.socket) {
+ this.socket[kRemoveSession](this);
+ this[kSetSocket](undefined);
+ }
+ socket[kAddSession](this);
+ this[kSetSocket](socket);
+
+ callback();
+ };
+
+ setSocket(socket, callback) {
+ if (!(socket instanceof QuicSocket))
+ throw new ERR_INVALID_ARG_TYPE('socket', 'QuicSocket', socket);
+
+ if (typeof callback !== 'function')
+ throw new ERR_INVALID_CALLBACK();
+
+ socket[kMaybeBind](() => this.#setSocketAfterBind(socket, callback));
+ }
+}
+
+function streamOnResume() {
+ if (!this.destroyed)
+ this[kHandle].readStart();
+}
+
+function streamOnPause() {
+ if (!this.destroyed /* && !this.pending */)
+ this[kHandle].readStop();
+}
+
+class QuicStream extends Duplex {
+ #closed = false;
+ #aborted = false;
+ #defaultEncoding = undefined;
+ #didRead = false;
+ #id = undefined;
+ #highWaterMark = undefined;
+ #push_id = undefined;
+ #resetCode = undefined;
+ #session = undefined;
+ #dataRateHistogram = undefined;
+ #dataSizeHistogram = undefined;
+ #dataAckHistogram = undefined;
+ #stats = undefined;
+
+ constructor(options, session, push_id) {
+ const {
+ highWaterMark,
+ defaultEncoding,
+ } = options;
+ super({
+ highWaterMark,
+ defaultEncoding,
+ allowHalfOpen: true,
+ decodeStrings: true,
+ emitClose: true,
+ autoDestroy: false,
+ captureRejections: true,
+ });
+ this.#highWaterMark = highWaterMark;
+ this.#defaultEncoding = defaultEncoding;
+ this.#session = session;
+ this.#push_id = push_id;
+ this._readableState.readingMore = true;
+ this.on('pause', streamOnPause);
+
+ // See src/node_quic_stream.h for an explanation
+ // of the initial states for unidirectional streams.
+ if (this.unidirectional) {
+ if (session instanceof QuicServerSession) {
+ if (this.serverInitiated) {
+ // Close the readable side
+ this.push(null);
+ this.read();
+ } else {
+ // Close the writable side
+ this.end();
+ }
+ } else if (this.serverInitiated) {
+ // Close the writable side
+ this.end();
+ } else {
+ this.push(null);
+ this.read();
+ }
+ }
+
+ // The QuicStream writes are corked until kSetHandle
+ // is set, ensuring that writes are buffered in JavaScript
+ // until we have somewhere to send them.
+ this.cork();
+ }
+
+ // Set handle is called once the QuicSession has been able
+ // to complete creation of the internal QuicStream handle.
+ // This will happen only after the QuicSession's own
+ // internal handle has been created. The QuicStream object
+ // is still minimally usable before this but any data
+ // written will be buffered until kSetHandle is called.
+ [kSetHandle](handle) {
+ this[kHandle] = handle;
+ if (handle !== undefined) {
+ handle.onread = onStreamRead;
+ handle[owner_symbol] = this;
+ this[async_id_symbol] = handle.getAsyncId();
+ this.#id = handle.id();
+ this.#dataRateHistogram = new Histogram(handle.rate);
+ this.#dataSizeHistogram = new Histogram(handle.size);
+ this.#dataAckHistogram = new Histogram(handle.ack);
+ this.uncork();
+ this.emit('ready');
+ } else {
+ if (this.#dataRateHistogram)
+ this.#dataRateHistogram[kDestroyHistogram]();
+ if (this.#dataSizeHistogram)
+ this.#dataSizeHistogram[kDestroyHistogram]();
+ if (this.#dataAckHistogram)
+ this.#dataAckHistogram[kDestroyHistogram]();
+ }
+ }
+
+ [kStreamReset](code) {
+ this.#resetCode = code | 0;
+ this.push(null);
+ this.read();
+ }
+
+ [kHeaders](headers, kind, push_id) {
+ // TODO(@jasnell): Convert the headers into a proper object
+ let name;
+ switch (kind) {
+ case QUICSTREAM_HEADERS_KIND_NONE:
+ // Fall through
+ case QUICSTREAM_HEADERS_KIND_INITIAL:
+ name = 'initialHeaders';
+ break;
+ case QUICSTREAM_HEADERS_KIND_INFORMATIONAL:
+ name = 'informationalHeaders';
+ break;
+ case QUICSTREAM_HEADERS_KIND_TRAILING:
+ name = 'trailingHeaders';
+ break;
+ case QUICSTREAM_HEADERS_KIND_PUSH:
+ name = 'pushHeaders';
+ break;
+ default:
+ assert.fail('Invalid headers kind');
+ }
+ process.nextTick(emit.bind(this, name, headers, push_id));
+ }
+
+ [kClose](family, code) {
+ // Trigger the abrupt shutdown of the stream. If the stream is
+ // already no-longer readable or writable, this does nothing. If
+ // the stream is readable or writable, then the abort event will
+ // be emitted immediately after triggering the send of the
+ // RESET_STREAM and STOP_SENDING frames. The stream will no longer
+ // be readable or writable, but will not be immediately destroyed
+ // as we need to wait until ngtcp2 recognizes the stream as
+ // having been closed to be destroyed.
+
+ // Do nothing if we've already been destroyed
+ if (this.destroyed || this.#closed)
+ return;
+
+ if (this.pending)
+ return this.once('ready', () => this[kClose](family, code));
+
+ this.#closed = true;
+
+ this.#aborted = this.readable || this.writable;
+
+ // Trigger scheduling of the RESET_STREAM and STOP_SENDING frames
+ // as appropriate. Notify ngtcp2 that the stream is to be shutdown.
+ // Once sent, the stream will be closed and destroyed as soon as
+ // the shutdown is acknowledged by the peer.
+ this[kHandle].resetStream(code, family);
+
+ // Close down the readable side of the stream
+ if (this.readable) {
+ this.push(null);
+ this.read();
+ }
+
+ // It is important to call shutdown on the handle before shutting
+ // down the writable side of the stream in order to prevent an
+ // empty STREAM frame with fin set to be sent to the peer.
+ if (this.writable)
+ this.end();
+ }
+
+ [kAfterAsyncWrite]({ bytes }) {
+ // TODO(@jasnell): Implement this
+ }
+
+ [kInspect]() {
+ const direction = this.bidirectional ? 'bidirectional' : 'unidirectional';
+ const initiated = this.serverInitiated ? 'server' : 'client';
+ const obj = {
+ id: this.#id,
+ direction,
+ initiated,
+ writableState: this._writableState,
+ readableState: this._readableState,
+ stats: {
+ dataRate: this.dataRateHistogram,
+ dataSize: this.dataSizeHistogram,
+ dataAck: this.dataAckHistogram,
+ }
+ };
+ return `QuicStream ${util.format(obj)}`;
+ }
+
+ [kTrackWriteState](stream, bytes) {
+ // TODO(@jasnell): Not yet sure what we want to do with these
+ // this.#writeQueueSize += bytes;
+ // this.#writeQueueSize += bytes;
+ // this[kHandle].chunksSentSinceLastWrite = 0;
+ }
+
+ [kUpdateTimer]() {
+ // TODO(@jasnell): Implement this later
+ }
+
+ get pending() {
+ // The id is set in the kSetHandle function
+ return this.#id === undefined;
+ }
+
+ get aborted() {
+ return this.#aborted;
+ }
+
+ get serverInitiated() {
+ return !!(this.#id & 0b01);
+ }
+
+ get clientInitiated() {
+ return !this.serverInitiated;
+ }
+
+ get unidirectional() {
+ return !!(this.#id & 0b10);
+ }
+
+ get bidirectional() {
+ return !this.unidirectional;
+ }
+
+ #writeGeneric = function(writev, data, encoding, cb) {
+ if (this.destroyed)
+ return; // TODO(addaleax): Can this happen?
+
+ // The stream should be corked while still pending
+ // but just in case uncork
+ // was called early, defer the actual write until the
+ // ready event is emitted.
+ if (this.pending) {
+ return this.once('ready', () => {
+ this.#writeGeneric(writev, data, encoding, cb);
+ });
+ }
+
+ this[kUpdateTimer]();
+ const req = (writev) ?
+ writevGeneric(this, data, cb) :
+ writeGeneric(this, data, encoding, cb);
+
+ this[kTrackWriteState](this, req.bytes);
+ };
+
+ _write(data, encoding, cb) {
+ this.#writeGeneric(false, data, encoding, cb);
+ }
+
+ _writev(data, cb) {
+ this.#writeGeneric(true, data, '', cb);
+ }
+
+ // Called when the last chunk of data has been
+ // acknowledged by the peer and end has been
+ // called. By calling shutdown, we're telling
+ // the native side that no more data will be
+ // coming so that a fin stream packet can be
+ // sent.
+ _final(cb) {
+ // The QuicStream should be corked while pending
+ // so this shouldn't be called, but just in case
+ // the stream was prematurely uncorked, defer the
+ // operation until the ready event is emitted.
+ if (this.pending)
+ return this.once('ready', () => this._final(cb));
+
+ const handle = this[kHandle];
+ if (handle === undefined) {
+ cb();
+ return;
+ }
+
+ const req = new ShutdownWrap();
+ req.oncomplete = () => cb();
+ req.handle = handle;
+ const err = handle.shutdown(req);
+ if (err === 1)
+ return cb();
+ }
+
+ _read(nread) {
+ if (this.pending)
+ return this.once('ready', () => this._read(nread));
+
+ if (this.destroyed) { // TODO(addaleax): Can this happen?
+ this.push(null);
+ return;
+ }
+ if (!this.#didRead) {
+ this._readableState.readingMore = false;
+ this.#didRead = true;
+ }
+
+ streamOnResume.call(this);
+ }
+
+ sendFile(path, options = {}) {
+ fs.open(path, 'r', QuicStream.#onFileOpened.bind(this, options));
+ }
+
+ static #onFileOpened = function(options, err, fd) {
+ const onError = options.onError;
+ if (err) {
+ if (onError) {
+ this.close();
+ onError(err);
+ } else {
+ this.destroy(err);
+ }
+ return;
+ }
+
+ if (this.destroyed || this.closed) {
+ fs.close(fd, (err) => { if (err) throw err; });
+ return;
+ }
+
+ this.sendFD(fd, options, true);
+ };
+
+ sendFD(fd, { offset = -1, length = -1 } = {}, ownsFd = false) {
+ if (this.destroyed || this.#closed)
+ return;
+
+ validateInteger(offset, 'options.offset', /* min */ -1);
+ validateInteger(length, 'options.length', /* min */ -1);
+
+ if (fd instanceof fsPromisesInternal.FileHandle)
+ fd = fd.fd;
+ else if (typeof fd !== 'number')
+ throw new ERR_INVALID_ARG_TYPE('fd', ['number', 'FileHandle'], fd);
+
+ if (this.pending) {
+ return this.once('ready', () => {
+ this.sendFD(fd, { offset, length }, ownsFd);
+ });
+ }
+
+ this[kUpdateTimer]();
+ this.ownsFd = ownsFd;
+
+ // Close the writable side of the stream, but only as far as the writable
+ // stream implementation is concerned.
+ this._final = null;
+ this.end();
+
+ defaultTriggerAsyncIdScope(this[async_id_symbol],
+ QuicStream.#startFilePipe,
+ this, fd, offset, length);
+ }
+
+ static #startFilePipe = (stream, fd, offset, length) => {
+ const handle = new FileHandle(fd, offset, length);
+ handle.onread = QuicStream.#onPipedFileHandleRead;
+ handle.stream = stream;
+
+ const pipe = new StreamPipe(handle, stream[kHandle]);
+ pipe.onunpipe = QuicStream.#onFileUnpipe;
+ pipe.start();
+
+ // Exact length of the file doesn't matter here, since the
+ // stream is closing anyway - just use 1 to signify that
+ // a write does exist
+ stream[kTrackWriteState](stream, 1);
+ }
+
+ static #onFileUnpipe = function() { // Called on the StreamPipe instance.
+ const stream = this.sink[owner_symbol];
+ if (stream.ownsFd)
+ this.source.close().catch(stream.destroy.bind(stream));
+ else
+ this.source.releaseFD();
+ };
+
+ static #onPipedFileHandleRead = function() {
+ const err = streamBaseState[kReadBytesOrError];
+ if (err < 0 && err !== UV_EOF) {
+ this.stream.destroy(errnoException(err, 'sendFD'));
+ }
+ };
+
+ get resetReceived() {
+ return (this.#resetCode !== undefined) ?
+ { code: this.#resetCode | 0 } :
+ undefined;
+ }
+
+ get bufferSize() {
+ // TODO(@jasnell): Implement this
+ return undefined;
+ }
+
+ get id() {
+ return this.#id;
+ }
+
+ get push_id() {
+ return this.#push_id;
+ }
+
+ close(code) {
+ this[kClose](QUIC_ERROR_APPLICATION, code);
+ }
+
+ get session() {
+ return this.#session;
+ }
+
+ _destroy(error, callback) {
+ this.#session[kRemoveStream](this);
+ const handle = this[kHandle];
+ // Do not use handle after this point as the underlying C++
+ // object has been destroyed. Any attempt to use the object
+ // will segfault and crash the process.
+ if (handle !== undefined) {
+ this.#stats = new BigInt64Array(handle.stats);
+ handle.destroy();
+ }
+ // The destroy callback must be invoked in a nextTick
+ process.nextTick(() => callback(error));
+ }
+
+ _onTimeout() {
+ // TODO(@jasnell): Implement this
+ }
+
+ get dataRateHistogram() {
+ return this.#dataRateHistogram;
+ }
+
+ get dataSizeHistogram() {
+ return this.#dataSizeHistogram;
+ }
+
+ get dataAckHistogram() {
+ return this.#dataAckHistogram;
+ }
+
+ pushStream(headers = {}, options = {}) {
+ if (this.destroyed)
+ throw new ERR_QUICSTREAM_DESTROYED('push');
+
+ const {
+ highWaterMark = this.#highWaterMark,
+ defaultEncoding = this.#defaultEncoding,
+ } = validateQuicStreamOptions(options);
+
+ validateObject(headers, 'headers');
+
+ // Push streams are only supported on QUIC servers, and
+ // only if the original stream is bidirectional.
+ // TODO(@jasnell): This is really an http/3 specific
+ // requirement so if we end up later with another
+ // QUIC application protocol that has a similar
+ // notion of push streams without this restriction,
+ // then we'll need to check alpn value here also.
+ if (!this.clientInitiated && !this.bidirectional)
+ throw new ERR_QUICSTREAM_INVALID_PUSH();
+
+ // TODO(@jasnell): The assertValidPseudoHeader validator
+ // here is HTTP/3 specific. If we end up with another
+ // QUIC application protocol that supports push streams,
+ // we will need to select a validator based on the
+ // alpn value.
+ const handle = this[kHandle].submitPush(
+ mapToHeaders(headers, assertValidPseudoHeader));
+
+ // If undefined is returned, it either means that push
+ // streams are not supported by the underlying application,
+ // or push streams are currently disabled/blocked. This
+ // will typically be the case with HTTP/3 when the client
+ // peer has disabled push.
+ if (handle === undefined)
+ throw new ERR_QUICSTREAM_UNSUPPORTED_PUSH();
+
+ const stream = new QuicStream({
+ readable: false,
+ highWaterMark,
+ defaultEncoding,
+ }, this.session);
+
+ // TODO(@jasnell): The null push and subsequent read shouldn't be necessary
+ stream.push(null);
+ stream.read();
+
+ stream[kSetHandle](handle);
+ this.session[kAddStream](stream.id, stream);
+ return stream;
+ }
+
+ submitInformationalHeaders(headers = {}) {
+ // TODO(@jasnell): Likely better to throw here instead of return false
+ if (this.destroyed)
+ return false;
+
+ validateObject(headers, 'headers');
+
+ // TODO(@jasnell): The validators here are specific to the QUIC
+ // protocol. In the case below, these are the http/3 validators
+ // (which are identical to the rules for http/2). We need to
+ // find a way for this to be easily abstracted based on the
+ // selected alpn.
+
+ let validator;
+ if (this.session instanceof QuicServerSession) {
+ validator =
+ this.clientInitiated ?
+ assertValidPseudoHeaderResponse :
+ assertValidPseudoHeader;
+ } else { // QuicClientSession
+ validator =
+ this.clientInitiated ?
+ assertValidPseudoHeader :
+ assertValidPseudoHeaderResponse;
+ }
+
+ return this[kHandle].submitInformationalHeaders(
+ mapToHeaders(headers, validator));
+ }
+
+ submitInitialHeaders(headers = {}, options = {}) {
+ // TODO(@jasnell): Likely better to throw here instead of return false
+ if (this.destroyed)
+ return false;
+
+ const { terminal } = { ...options };
+
+ if (terminal !== undefined)
+ validateBoolean(terminal, 'options.terminal');
+ validateObject(headers, 'headers');
+
+ // TODO(@jasnell): The validators here are specific to the QUIC
+ // protocol. In the case below, these are the http/3 validators
+ // (which are identical to the rules for http/2). We need to
+ // find a way for this to be easily abstracted based on the
+ // selected alpn.
+
+ let validator;
+ if (this.session instanceof QuicServerSession) {
+ validator =
+ this.clientInitiated ?
+ assertValidPseudoHeaderResponse :
+ assertValidPseudoHeader;
+ } else { // QuicClientSession
+ validator =
+ this.clientInitiated ?
+ assertValidPseudoHeader :
+ assertValidPseudoHeaderResponse;
+ }
+
+ return this[kHandle].submitHeaders(
+ mapToHeaders(headers, validator),
+ terminal ?
+ QUICSTREAM_HEADER_FLAGS_TERMINAL :
+ QUICSTREAM_HEADER_FLAGS_NONE);
+ }
+
+ submitTrailingHeaders(headers = {}) {
+ // TODO(@jasnell): Likely better to throw here instead of return false
+ if (this.destroyed)
+ return false;
+
+ validateObject(headers, 'headers');
+
+ // TODO(@jasnell): The validators here are specific to the QUIC
+ // protocol. In the case below, these are the http/3 validators
+ // (which are identical to the rules for http/2). We need to
+ // find a way for this to be easily abstracted based on the
+ // selected alpn.
+
+ return this[kHandle].submitTrailers(
+ mapToHeaders(headers, assertValidPseudoHeaderTrailer));
+ }
+
+ get duration() {
+ const now = process.hrtime.bigint();
+ const stats = this.#stats || this[kHandle].stats;
+ return now - stats[IDX_QUIC_STREAM_STATS_CREATED_AT];
+ }
+
+ get bytesReceived() {
+ const stats = this.#stats || this[kHandle].stats;
+ return stats[IDX_QUIC_STREAM_STATS_BYTES_RECEIVED];
+ }
+
+ get bytesSent() {
+ const stats = this.#stats || this[kHandle].stats;
+ return stats[IDX_QUIC_STREAM_STATS_BYTES_SENT];
+ }
+
+ get maxExtendedOffset() {
+ const stats = this.#stats || this[kHandle].stats;
+ return stats[IDX_QUIC_STREAM_STATS_MAX_OFFSET];
+ }
+
+ get finalSize() {
+ const stats = this.#stats || this[kHandle].stats;
+ return stats[IDX_QUIC_STREAM_STATS_FINAL_SIZE];
+ }
+
+ get maxAcknowledgedOffset() {
+ const stats = this.#stats || this[kHandle].stats;
+ return stats[IDX_QUIC_STREAM_STATS_MAX_OFFSET_ACK];
+ }
+
+ get maxReceivedOffset() {
+ const stats = this.#stats || this[kHandle].stats;
+ return stats[IDX_QUIC_STREAM_STATS_MAX_OFFSET_RECV];
+ }
+}
+
+function createSocket(options) {
+ return new QuicSocket(options);
+}
+
+module.exports = {
+ createSocket,
+ kUDPHandleForTesting
+};
+
+/* eslint-enable no-use-before-define */
+
+// A single QuicSocket may act as both a Server and a Client.
+// There are two kinds of sessions:
+// * QuicServerSession
+// * QuicClientSession
+//
+// It is important to understand that QUIC sessions are
+// independent of the QuicSocket. A default configuration
+// for QuicServerSession and QuicClientSessions may be
+// set when the QuicSocket is created, but the actual
+// configuration for a particular QuicSession instance is
+// not set until the session itself is created.
+//
+// QuicSockets and QuicSession instances have distinct
+// configuration options that apply independently:
+//
+// QuicSocket Options:
+// * `lookup` {Function} A function used to resolve DNS names.
+// * `type` {string} Either `'udp4'` or `'udp6'`, defaults to
+// `'udp4'`.
+// * `port` {number} The local IP port the QuicSocket will
+// bind to.
+// * `address` {string} The local IP address or hostname that
+// the QuicSocket will bind to. If a hostname is given, the
+// `lookup` function will be invoked to resolve an IP address.
+// * `ipv6Only`
+// * `reuseAddr`
+//
+// Keep in mind that while all QUIC network traffic is encrypted
+// using TLS 1.3, every QuicSession maintains it's own SecureContext
+// that is completely independent of the QuicSocket. Every
+// QuicServerSession and QuicClientSession could, in theory,
+// use a completely different TLS 1.3 configuration. To keep it
+// simple, however, we use the same SecureContext for all QuicServerSession
+// instances, but that may be something we want to revisit later.
+//
+// Every QuicSession has two sets of configuration parameters:
+// * Options
+// * Transport Parameters
+//
+// Options establish implementation specific operation parameters,
+// such as the default highwatermark for new QuicStreams. Transport
+// Parameters are QUIC specific and are passed to the peer as part
+// of the TLS handshake.
+//
+// Every QuicSession may have separate options and transport
+// parameters, even within the same QuicSocket, so the configuration
+// must be established when the session is created.
+//
+// When creating a QuicSocket, it is possible to set a default
+// configuration for both QuicServerSession and QuicClientSession
+// options.
+//
+// const soc = createSocket({
+// type: 'udp4',
+// port: 0,
+// server: {
+// // QuicServerSession configuration defaults
+// },
+// client: {
+// // QuicClientSession configuration defaults
+// }
+// });
+//
+// When calling listen() on the created QuicSocket, the server
+// specific configuration that will be used for all new
+// QuicServerSession instances will be given, with the values
+// provided to createSocket() using the server option used
+// as a default.
+//
+// When calling connect(), the client specific configuration
+// will be given, with the values provided to the createSocket()
+// using the client option used as a default.
+
+
+// Some lifecycle documentation for the various objects:
+//
+// QuicSocket
+// Close
+// * Close all existing Sessions
+// * Do not allow any new Sessions (inbound or outbound)
+// * Destroy once there are no more sessions
+
+// Destroy
+// * Destroy all remaining sessions
+// * Destroy and free the QuicSocket handle immediately
+// * If Error, emit Error event
+// * Emit Close event
+
+// QuicClientSession
+// Close
+// * Allow existing Streams to complete normally
+// * Do not allow any new Streams (inbound or outbound)
+// * Destroy once there are no more streams
+
+// Destroy
+// * Send CONNECTION_CLOSE
+// * Destroy all remaining Streams
+// * Remove Session from Parent Socket
+// * Destroy and free the QuicSession handle immediately
+// * If Error, emit Error event
+// * Emit Close event
+
+// QuicServerSession
+// Close
+// * Allow existing Streams to complete normally
+// * Do not allow any new Streams (inbound or outbound)
+// * Destroy once there are no more streams
+// Destroy
+// * Send CONNECTION_CLOSE
+// * Destroy all remaining Streams
+// * Remove Session from Parent Socket
+// * Destroy and free the QuicSession handle immediately
+// * If Error, emit Error event
+// * Emit Close event
+
+// QuicStream
+// Destroy
+// * Remove Stream From Parent Session
+// * Destroy and free the QuicStream handle immediately
+// * If Error, emit Error event
+// * Emit Close event
+
+// QuicEndpoint States:
+// Initial -- Created, Endpoint not yet bound to local UDP
+// port.
+// Pending -- Binding to local UDP port has been initialized.
+// Bound -- Binding to local UDP port has completed.
+// Closed -- Endpoint has been unbound from the local UDP
+// port.
+// Error -- An error has been encountered, endpoint has
+// been unbound and is no longer usable.
+//
+// QuicSocket States:
+// Initial -- Created, QuicSocket has at least one
+// QuicEndpoint. All QuicEndpoints are in the
+// Initial state.
+// Pending -- QuicSocket has at least one QuicEndpoint in the
+// Pending state.
+// Bound -- QuicSocket has at least one QuicEndpoint in the
+// Bound state.
+// Closed -- All QuicEndpoints are in the closed state.
+// Destroyed -- QuicSocket is no longer usable
+// Destroyed-With-Error -- An error has been encountered, socket is no
+// longer usable
+//
+// QuicSession States:
+// Initial -- Created, QuicSocket state undetermined,
+// no internal QuicSession Handle assigned.
+// Ready
+// QuicSocket Ready -- QuicSocket in Bound state.
+// Handle Assigned -- Internal QuicSession Handle assigned.
+// TLS Handshake Started --
+// TLS Handshake Completed --
+// TLS Handshake Confirmed --
+// Closed -- Graceful Close Initiated
+// Destroyed -- QuicSession is no longer usable
+// Destroyed-With-Error -- An error has been encountered, session is no
+// longer usable
+//
+// QuicStream States:
+// Initial Writable/Corked -- Created, QuicSession in Initial State
+// Open Writable/Uncorked -- QuicSession in Ready State
+// Closed -- Graceful Close Initiated
+// Destroyed -- QuicStream is no longer usable
+// Destroyed-With-Error -- An error has been encountered, stream is no
+// longer usable
diff --git a/lib/internal/quic/util.js b/lib/internal/quic/util.js
new file mode 100644
index 00000000000000..bf12bc1a61f412
--- /dev/null
+++ b/lib/internal/quic/util.js
@@ -0,0 +1,773 @@
+'use strict';
+
+const {
+ NumberINFINITY,
+ NumberNEGATIVE_INFINITY,
+} = primordials;
+
+const {
+ codes: {
+ ERR_INVALID_ARG_TYPE,
+ ERR_INVALID_ARG_VALUE,
+ ERR_OUT_OF_RANGE,
+ ERR_QUICSESSION_INVALID_DCID,
+ ERR_QUICSOCKET_INVALID_STATELESS_RESET_SECRET_LENGTH,
+ },
+} = require('internal/errors');
+
+const assert = require('internal/assert');
+assert(process.versions.ngtcp2 !== undefined);
+
+const {
+ isIP,
+} = require('internal/net');
+
+const {
+ getOptionValue,
+ getAllowUnauthorized,
+} = require('internal/options');
+
+const { Buffer } = require('buffer');
+
+const {
+ sessionConfig,
+ http3Config,
+ constants: {
+ AF_INET,
+ AF_INET6,
+ NGTCP2_ALPN_H3,
+ DEFAULT_RETRYTOKEN_EXPIRATION,
+ DEFAULT_MAX_CONNECTIONS,
+ DEFAULT_MAX_CONNECTIONS_PER_HOST,
+ DEFAULT_MAX_STATELESS_RESETS_PER_HOST,
+ IDX_QUIC_SESSION_ACTIVE_CONNECTION_ID_LIMIT,
+ IDX_QUIC_SESSION_MAX_STREAM_DATA_BIDI_LOCAL,
+ IDX_QUIC_SESSION_MAX_STREAM_DATA_BIDI_REMOTE,
+ IDX_QUIC_SESSION_MAX_STREAM_DATA_UNI,
+ IDX_QUIC_SESSION_MAX_DATA,
+ IDX_QUIC_SESSION_MAX_STREAMS_BIDI,
+ IDX_QUIC_SESSION_MAX_STREAMS_UNI,
+ IDX_QUIC_SESSION_MAX_IDLE_TIMEOUT,
+ IDX_QUIC_SESSION_MAX_ACK_DELAY,
+ IDX_QUIC_SESSION_MAX_PACKET_SIZE,
+ IDX_QUIC_SESSION_CONFIG_COUNT,
+ IDX_QUIC_SESSION_STATE_CERT_ENABLED,
+ IDX_QUIC_SESSION_STATE_CLIENT_HELLO_ENABLED,
+ IDX_QUIC_SESSION_STATE_KEYLOG_ENABLED,
+ IDX_QUIC_SESSION_STATE_PATH_VALIDATED_ENABLED,
+ IDX_QUIC_SESSION_STATE_USE_PREFERRED_ADDRESS_ENABLED,
+ IDX_HTTP3_QPACK_MAX_TABLE_CAPACITY,
+ IDX_HTTP3_QPACK_BLOCKED_STREAMS,
+ IDX_HTTP3_MAX_HEADER_LIST_SIZE,
+ IDX_HTTP3_MAX_PUSHES,
+ IDX_HTTP3_MAX_HEADER_PAIRS,
+ IDX_HTTP3_MAX_HEADER_LENGTH,
+ IDX_HTTP3_CONFIG_COUNT,
+ MAX_RETRYTOKEN_EXPIRATION,
+ MIN_RETRYTOKEN_EXPIRATION,
+ NGTCP2_NO_ERROR,
+ NGTCP2_MAX_CIDLEN,
+ NGTCP2_MIN_CIDLEN,
+ QUIC_PREFERRED_ADDRESS_IGNORE,
+ QUIC_PREFERRED_ADDRESS_USE,
+ QUIC_ERROR_APPLICATION,
+ }
+} = internalBinding('quic');
+
+const {
+ validateBoolean,
+ validateBuffer,
+ validateInteger,
+ validateObject,
+ validatePort,
+ validateString,
+} = require('internal/validators');
+
+const kDefaultQuicCiphers = 'TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:' +
+ 'TLS_CHACHA20_POLY1305_SHA256';
+const kDefaultGroups = 'P-256:X25519:P-384:P-521';
+
+let dns;
+
+function lazyDNS() {
+ if (!dns)
+ dns = require('dns');
+ return dns;
+}
+
+// This is here rather than in internal/validators to
+// prevent performance degredation in default cases.
+function validateNumber(value, name,
+ min = NumberNEGATIVE_INFINITY,
+ max = NumberINFINITY) {
+ if (typeof value !== 'number')
+ throw new ERR_INVALID_ARG_TYPE(name, 'number', value);
+ if (value < min || value > max)
+ throw new ERR_OUT_OF_RANGE(name, `>= ${min} && <= ${max}`, value);
+}
+
+function getSocketType(type = 'udp4') {
+ switch (type) {
+ case 'udp4': return AF_INET;
+ case 'udp6': return AF_INET6;
+ }
+ throw new ERR_INVALID_ARG_VALUE('options.type', type);
+}
+
+function lookup4(address, callback) {
+ const { lookup } = lazyDNS();
+ lookup(address || '127.0.0.1', 4, callback);
+}
+
+function lookup6(address, callback) {
+ const { lookup } = lazyDNS();
+ lookup(address || '::1', 6, callback);
+}
+
+function validateCloseCode(code) {
+ if (code != null && typeof code === 'object') {
+ return {
+ closeCode: code.code || NGTCP2_NO_ERROR,
+ closeFamily: code.family || QUIC_ERROR_APPLICATION,
+ };
+ } else if (typeof code === 'number') {
+ return {
+ closeCode: code,
+ closeFamily: QUIC_ERROR_APPLICATION,
+ };
+ }
+ throw new ERR_INVALID_ARG_TYPE('code', ['number', 'Object'], code);
+}
+
+function validateLookup(lookup) {
+ if (lookup && typeof lookup !== 'function')
+ throw new ERR_INVALID_ARG_TYPE('options.lookup', 'Function', lookup);
+}
+
+function validatePreferredAddress(address) {
+ if (address !== undefined) {
+ validateObject(address, 'options.preferredAddress');
+ validateString(address.address, 'options.preferredAddress.address');
+ if (address.port !== undefined)
+ validatePort(address.port, 'options.preferredAddress.port');
+ getSocketType(address.type);
+ }
+ return address;
+}
+
+// Validate known transport parameters, ignoring any that are not
+// supported. Ensures that only supported parameters are passed on.
+function validateTransportParams(params) {
+ const {
+ activeConnectionIdLimit,
+ maxStreamDataBidiLocal,
+ maxStreamDataBidiRemote,
+ maxStreamDataUni,
+ maxData,
+ maxStreamsBidi,
+ maxStreamsUni,
+ idleTimeout,
+ maxPacketSize,
+ maxAckDelay,
+ preferredAddress,
+ rejectUnauthorized,
+ requestCert,
+ h3: {
+ qpackMaxTableCapacity,
+ qpackBlockedStreams,
+ maxHeaderListSize,
+ maxPushes,
+ maxHeaderPairs,
+ maxHeaderLength = getOptionValue('--max-http-header-size'),
+ },
+ } = { h3: {}, ...params };
+
+ if (activeConnectionIdLimit !== undefined) {
+ validateInteger(
+ activeConnectionIdLimit,
+ 'options.activeConnectionIdLimit',
+ /* min */ 2,
+ /* max */ 8);
+ }
+ if (maxStreamDataBidiLocal !== undefined) {
+ validateInteger(
+ maxStreamDataBidiLocal,
+ 'options.maxStreamDataBidiLocal',
+ /* min */ 0);
+ }
+ if (maxStreamDataBidiRemote !== undefined) {
+ validateInteger(
+ maxStreamDataBidiRemote,
+ 'options.maxStreamDataBidiRemote',
+ /* min */ 0);
+ }
+ if (maxStreamDataUni !== undefined) {
+ validateInteger(
+ maxStreamDataUni,
+ 'options.maxStreamDataUni',
+ /* min */ 0);
+ }
+ if (maxData !== undefined) {
+ validateInteger(
+ maxData,
+ 'options.maxData',
+ /* min */ 0);
+ }
+ if (maxStreamsBidi !== undefined) {
+ validateInteger(
+ maxStreamsBidi,
+ 'options.maxStreamsBidi',
+ /* min */ 0);
+ }
+ if (maxStreamsUni !== undefined) {
+ validateInteger(
+ maxStreamsUni,
+ 'options.maxStreamsUni',
+ /* min */ 0);
+ }
+ if (idleTimeout !== undefined) {
+ validateInteger(
+ idleTimeout,
+ 'options.idleTimeout',
+ /* min */ 0);
+ }
+ if (maxPacketSize !== undefined) {
+ validateInteger(
+ maxPacketSize,
+ 'options.maxPacketSize',
+ /* min */ 0);
+ }
+ if (maxAckDelay !== undefined) {
+ validateInteger(
+ maxAckDelay,
+ 'options.maxAckDelay',
+ /* min */ 0);
+ }
+ if (qpackMaxTableCapacity !== undefined) {
+ validateInteger(
+ qpackMaxTableCapacity,
+ 'options.h3.qpackMaxTableCapacity',
+ /* min */ 0);
+ }
+ if (qpackBlockedStreams !== undefined) {
+ validateInteger(
+ qpackBlockedStreams,
+ 'options.h3.qpackBlockedStreams',
+ /* min */ 0);
+ }
+ if (maxHeaderListSize !== undefined) {
+ validateInteger(
+ maxHeaderListSize,
+ 'options.h3.maxHeaderListSize',
+ /* min */ 0);
+ }
+ if (maxPushes !== undefined) {
+ validateInteger(
+ maxPushes,
+ 'options.h3.maxPushes',
+ /* min */ 0);
+ }
+ if (maxHeaderPairs !== undefined) {
+ validateInteger(
+ maxHeaderPairs,
+ 'options.h3.maxHeaderPairs',
+ /* min */ 0);
+ }
+ if (maxHeaderLength !== undefined) {
+ validateInteger(
+ maxHeaderLength,
+ 'options.h3.maxHeaderLength',
+ /* min */ 0);
+ }
+
+ validatePreferredAddress(preferredAddress);
+
+ return {
+ activeConnectionIdLimit,
+ maxStreamDataBidiLocal,
+ maxStreamDataBidiRemote,
+ maxStreamDataUni,
+ maxData,
+ maxStreamsBidi,
+ maxStreamsUni,
+ idleTimeout,
+ maxPacketSize,
+ maxAckDelay,
+ preferredAddress,
+ rejectUnauthorized,
+ requestCert,
+ h3: {
+ qpackMaxTableCapacity,
+ qpackBlockedStreams,
+ maxHeaderListSize,
+ maxPushes,
+ maxHeaderPairs,
+ maxHeaderLength,
+ }
+ };
+}
+
+function validateQuicClientSessionOptions(options = {}) {
+ if (options !== null && typeof options !== 'object')
+ throw new ERR_INVALID_ARG_TYPE('options', 'Object', options);
+ const {
+ autoStart = true,
+ address = 'localhost',
+ alpn = '',
+ dcid: dcid_value,
+ ipv6Only = false,
+ minDHSize = 1024,
+ port = 0,
+ preferredAddressPolicy = 'ignore',
+ remoteTransportParams,
+ requestOCSP = false,
+ servername = (isIP(address) ? '' : address),
+ sessionTicket,
+ verifyHostnameIdentity = true,
+ qlog = false,
+ highWaterMark,
+ defaultEncoding,
+ } = options;
+
+ validateBoolean(autoStart, 'options.autoStart');
+ validateNumber(minDHSize, 'options.minDHSize');
+ validatePort(port, 'options.port');
+ validateString(address, 'options.address');
+ validateString(alpn, 'options.alpn');
+ validateString(servername, 'options.servername');
+
+ if (isIP(servername)) {
+ throw new ERR_INVALID_ARG_VALUE(
+ 'options.servername',
+ servername,
+ 'cannot be an IP address');
+ }
+
+ if (remoteTransportParams !== undefined)
+ validateBuffer(remoteTransportParams, 'options.remoteTransportParams');
+
+ if (sessionTicket !== undefined)
+ validateBuffer(sessionTicket, 'options.sessionTicket');
+
+ let dcid;
+ if (dcid_value !== undefined) {
+ if (typeof dcid_value === 'string') {
+ // If it's a string, it must be a hex encoded string
+ try {
+ dcid = Buffer.from(dcid_value, 'hex');
+ } catch {
+ throw new ERR_QUICSESSION_INVALID_DCID(dcid);
+ }
+ }
+
+ validateBuffer(
+ dcid_value,
+ 'options.dcid',
+ ['string', 'Buffer', 'TypedArray', 'DataView']);
+
+ if (dcid_value.length > NGTCP2_MAX_CIDLEN ||
+ dcid_value.length < NGTCP2_MIN_CIDLEN) {
+ throw new ERR_QUICSESSION_INVALID_DCID(dcid_value.toString('hex'));
+ }
+
+ dcid = dcid_value;
+ }
+
+ if (preferredAddressPolicy !== undefined)
+ validateString(preferredAddressPolicy, 'options.preferredAddressPolicy');
+
+ validateBoolean(ipv6Only, 'options.ipv6Only');
+ validateBoolean(requestOCSP, 'options.requestOCSP');
+ validateBoolean(verifyHostnameIdentity, 'options.verifyHostnameIdentity');
+ validateBoolean(qlog, 'options.qlog');
+
+ return {
+ autoStart,
+ address,
+ alpn,
+ dcid,
+ ipv6Only,
+ minDHSize,
+ port,
+ preferredAddressPolicy:
+ preferredAddressPolicy === 'accept' ?
+ QUIC_PREFERRED_ADDRESS_USE :
+ QUIC_PREFERRED_ADDRESS_IGNORE,
+ remoteTransportParams,
+ requestOCSP,
+ servername,
+ sessionTicket,
+ verifyHostnameIdentity,
+ qlog,
+ ...validateQuicStreamOptions({ highWaterMark, defaultEncoding })
+ };
+}
+
+function validateQuicStreamOptions(options = {}) {
+ validateObject(options);
+ const {
+ defaultEncoding = 'utf8',
+ halfOpen,
+ highWaterMark,
+ } = options;
+ if (!Buffer.isEncoding(defaultEncoding)) {
+ throw new ERR_INVALID_ARG_VALUE(
+ 'options.defaultEncoding',
+ defaultEncoding,
+ 'is not a valid encoding');
+ }
+ if (halfOpen !== undefined)
+ validateBoolean(halfOpen, 'options.halfOpen');
+ if (highWaterMark !== undefined) {
+ validateInteger(
+ highWaterMark,
+ 'options.highWaterMark',
+ /* min */ 0);
+ }
+ return {
+ defaultEncoding,
+ halfOpen,
+ highWaterMark,
+ };
+}
+
+function validateQuicEndpointOptions(options = {}, name = 'options') {
+ validateObject(options, name);
+ if (options === null || typeof options !== 'object')
+ throw new ERR_INVALID_ARG_TYPE('options', 'Object', options);
+ const {
+ address,
+ ipv6Only = false,
+ lookup,
+ port = 0,
+ reuseAddr = false,
+ type = 'udp4',
+ preferred = false,
+ } = options;
+ if (address !== undefined)
+ validateString(address, 'options.address');
+ validatePort(port, 'options.port');
+ validateString(type, 'options.type');
+ validateLookup(lookup);
+ validateBoolean(ipv6Only, 'options.ipv6Only');
+ validateBoolean(reuseAddr, 'options.reuseAddr');
+ validateBoolean(preferred, 'options.preferred');
+ return {
+ address,
+ ipv6Only,
+ lookup,
+ port,
+ preferred,
+ reuseAddr,
+ type: getSocketType(type),
+ };
+}
+
+function validateQuicSocketOptions(options = {}) {
+ validateObject(options, 'options');
+
+ const {
+ autoClose = false,
+ client = {},
+ disableStatelessReset = false,
+ endpoint = { port: 0, type: 'udp4' },
+ lookup,
+ maxConnections = DEFAULT_MAX_CONNECTIONS,
+ maxConnectionsPerHost = DEFAULT_MAX_CONNECTIONS_PER_HOST,
+ maxStatelessResetsPerHost = DEFAULT_MAX_STATELESS_RESETS_PER_HOST,
+ qlog = false,
+ retryTokenTimeout = DEFAULT_RETRYTOKEN_EXPIRATION,
+ server = {},
+ statelessResetSecret,
+ type = endpoint.type || 'udp4',
+ validateAddressLRU = false,
+ validateAddress = false,
+ } = options;
+
+ validateQuicEndpointOptions(endpoint, 'options.endpoint');
+ validateObject(client, 'options.client');
+ validateObject(server, 'options.server');
+ validateString(type, 'options.type');
+ validateLookup(lookup);
+ validateBoolean(validateAddress, 'options.validateAddress');
+ validateBoolean(validateAddressLRU, 'options.validateAddressLRU');
+ validateBoolean(autoClose, 'options.autoClose');
+ validateBoolean(qlog, 'options.qlog');
+ validateBoolean(disableStatelessReset, 'options.disableStatelessReset');
+
+ if (retryTokenTimeout !== undefined) {
+ validateInteger(
+ retryTokenTimeout,
+ 'options.retryTokenTimeout',
+ /* min */ MIN_RETRYTOKEN_EXPIRATION,
+ /* max */ MAX_RETRYTOKEN_EXPIRATION);
+ }
+ if (maxConnections !== undefined) {
+ validateInteger(
+ maxConnections,
+ 'options.maxConnections',
+ /* min */ 1);
+ }
+ if (maxConnectionsPerHost !== undefined) {
+ validateInteger(
+ maxConnectionsPerHost,
+ 'options.maxConnectionsPerHost',
+ /* min */ 1);
+ }
+ if (maxStatelessResetsPerHost !== undefined) {
+ validateInteger(
+ maxStatelessResetsPerHost,
+ 'options.maxStatelessResetsPerHost',
+ /* min */ 1);
+ }
+
+ if (statelessResetSecret !== undefined) {
+ validateBuffer(statelessResetSecret, 'options.statelessResetSecret');
+ if (statelessResetSecret.length !== 16)
+ throw new ERR_QUICSOCKET_INVALID_STATELESS_RESET_SECRET_LENGTH();
+ }
+
+ return {
+ endpoint,
+ autoClose,
+ client,
+ lookup,
+ maxConnections,
+ maxConnectionsPerHost,
+ maxStatelessResetsPerHost,
+ retryTokenTimeout,
+ server,
+ type: getSocketType(type),
+ validateAddress: validateAddress || validateAddressLRU,
+ validateAddressLRU,
+ qlog,
+ statelessResetSecret,
+ disableStatelessReset,
+ };
+}
+
+function validateQuicSocketListenOptions(options = {}) {
+ validateObject(options);
+ const {
+ alpn = NGTCP2_ALPN_H3,
+ defaultEncoding,
+ highWaterMark,
+ requestCert,
+ rejectUnauthorized,
+ } = options;
+ validateString(alpn, 'options.alpn');
+ if (rejectUnauthorized !== undefined)
+ validateBoolean(rejectUnauthorized, 'options.rejectUnauthorized');
+ if (requestCert !== undefined)
+ validateBoolean(requestCert, 'options.requestCert');
+
+ const transportParams =
+ validateTransportParams(
+ options,
+ NGTCP2_MAX_CIDLEN,
+ NGTCP2_MIN_CIDLEN);
+
+ return {
+ alpn,
+ rejectUnauthorized,
+ requestCert,
+ transportParams,
+ ...validateQuicStreamOptions({ highWaterMark, defaultEncoding })
+ };
+}
+
+function validateQuicSocketConnectOptions(options = {}) {
+ validateObject(options);
+ const {
+ type = 'udp4',
+ address,
+ } = options;
+ if (address !== undefined)
+ validateString(address, 'options.address');
+ return {
+ type: getSocketType(type),
+ address,
+ };
+}
+
+function validateCreateSecureContextOptions(options = {}) {
+ validateObject(options);
+ const {
+ ca,
+ cert,
+ ciphers = kDefaultQuicCiphers,
+ clientCertEngine,
+ crl,
+ dhparam,
+ ecdhCurve,
+ groups = kDefaultGroups,
+ honorCipherOrder,
+ key,
+ earlyData = true, // Early data is enabled by default
+ passphrase,
+ pfx,
+ sessionIdContext,
+ secureProtocol
+ } = options;
+ validateString(ciphers, 'option.ciphers');
+ validateString(groups, 'option.groups');
+ if (earlyData !== undefined)
+ validateBoolean(earlyData, 'option.earlyData');
+
+ // Additional validation occurs within the tls
+ // createSecureContext function.
+ return {
+ ca,
+ cert,
+ ciphers,
+ clientCertEngine,
+ crl,
+ dhparam,
+ ecdhCurve,
+ groups,
+ honorCipherOrder,
+ key,
+ earlyData,
+ passphrase,
+ pfx,
+ sessionIdContext,
+ secureProtocol
+ };
+}
+
+function setConfigField(buffer, val, index) {
+ if (typeof val === 'number') {
+ buffer[index] = val;
+ return 1 << index;
+ }
+ return 0;
+}
+
+// Extracts configuration options and updates the aliased buffer
+// arrays that are used to communicate config choices to the c++
+// internals.
+function setTransportParams(config) {
+ const {
+ activeConnectionIdLimit,
+ maxStreamDataBidiLocal,
+ maxStreamDataBidiRemote,
+ maxStreamDataUni,
+ maxData,
+ maxStreamsBidi,
+ maxStreamsUni,
+ idleTimeout,
+ maxPacketSize,
+ maxAckDelay,
+ h3: {
+ qpackMaxTableCapacity,
+ qpackBlockedStreams,
+ maxHeaderListSize,
+ maxPushes,
+ maxHeaderPairs,
+ maxHeaderLength,
+ },
+ } = { h3: {}, ...config };
+
+ // The const flags is a bitmap that is used to communicate whether or not a
+ // given configuration value has been explicitly provided.
+ const flags = setConfigField(sessionConfig,
+ activeConnectionIdLimit,
+ IDX_QUIC_SESSION_ACTIVE_CONNECTION_ID_LIMIT) |
+ setConfigField(sessionConfig,
+ maxStreamDataBidiLocal,
+ IDX_QUIC_SESSION_MAX_STREAM_DATA_BIDI_LOCAL) |
+ setConfigField(sessionConfig,
+ maxStreamDataBidiRemote,
+ IDX_QUIC_SESSION_MAX_STREAM_DATA_BIDI_REMOTE) |
+ setConfigField(sessionConfig,
+ maxStreamDataUni,
+ IDX_QUIC_SESSION_MAX_STREAM_DATA_UNI) |
+ setConfigField(sessionConfig,
+ maxData,
+ IDX_QUIC_SESSION_MAX_DATA) |
+ setConfigField(sessionConfig,
+ maxStreamsBidi,
+ IDX_QUIC_SESSION_MAX_STREAMS_BIDI) |
+ setConfigField(sessionConfig,
+ maxStreamsUni,
+ IDX_QUIC_SESSION_MAX_STREAMS_UNI) |
+ setConfigField(sessionConfig,
+ idleTimeout,
+ IDX_QUIC_SESSION_MAX_IDLE_TIMEOUT) |
+ setConfigField(sessionConfig,
+ maxAckDelay,
+ IDX_QUIC_SESSION_MAX_ACK_DELAY) |
+ setConfigField(sessionConfig,
+ maxPacketSize,
+ IDX_QUIC_SESSION_MAX_PACKET_SIZE);
+
+ sessionConfig[IDX_QUIC_SESSION_CONFIG_COUNT] = flags;
+
+ const h3flags = setConfigField(http3Config,
+ qpackMaxTableCapacity,
+ IDX_HTTP3_QPACK_MAX_TABLE_CAPACITY) |
+ setConfigField(http3Config,
+ qpackBlockedStreams,
+ IDX_HTTP3_QPACK_BLOCKED_STREAMS) |
+ setConfigField(http3Config,
+ maxHeaderListSize,
+ IDX_HTTP3_MAX_HEADER_LIST_SIZE) |
+ setConfigField(http3Config,
+ maxPushes,
+ IDX_HTTP3_MAX_PUSHES) |
+ setConfigField(http3Config,
+ maxHeaderPairs,
+ IDX_HTTP3_MAX_HEADER_PAIRS) |
+ setConfigField(http3Config,
+ maxHeaderLength,
+ IDX_HTTP3_MAX_HEADER_LENGTH);
+
+ http3Config[IDX_HTTP3_CONFIG_COUNT] = h3flags;
+}
+
+// Some events that are emitted originate from the C++ internals and are
+// fairly expensive and optional. An aliased array buffer is used to
+// communicate that a handler has been added for the optional events
+// so that the C++ internals know there is an actual listener. The event
+// will not be emitted if there is no handler.
+function toggleListeners(handle, event, on) {
+ if (handle === undefined)
+ return;
+ const val = on ? 1 : 0;
+ switch (event) {
+ case 'keylog':
+ handle.state[IDX_QUIC_SESSION_STATE_KEYLOG_ENABLED] = val;
+ break;
+ case 'clientHello':
+ handle.state[IDX_QUIC_SESSION_STATE_CLIENT_HELLO_ENABLED] = val;
+ break;
+ case 'pathValidation':
+ handle.state[IDX_QUIC_SESSION_STATE_PATH_VALIDATED_ENABLED] = val;
+ break;
+ case 'OCSPRequest':
+ handle.state[IDX_QUIC_SESSION_STATE_CERT_ENABLED] = val;
+ break;
+ case 'usePreferredAddress':
+ handle.state[IDX_QUIC_SESSION_STATE_USE_PREFERRED_ADDRESS_ENABLED] = on;
+ break;
+ }
+}
+
+module.exports = {
+ getAllowUnauthorized,
+ getSocketType,
+ lookup4,
+ lookup6,
+ setTransportParams,
+ toggleListeners,
+ validateNumber,
+ validateCloseCode,
+ validateTransportParams,
+ validateQuicClientSessionOptions,
+ validateQuicSocketOptions,
+ validateQuicStreamOptions,
+ validateQuicSocketListenOptions,
+ validateQuicEndpointOptions,
+ validateCreateSecureContextOptions,
+ validateQuicSocketConnectOptions,
+};
diff --git a/lib/internal/stream_base_commons.js b/lib/internal/stream_base_commons.js
index 569a1419b5f287..fdf540a47e08ae 100644
--- a/lib/internal/stream_base_commons.js
+++ b/lib/internal/stream_base_commons.js
@@ -108,7 +108,7 @@ function onWriteComplete(status) {
this.callback(null);
}
-function createWriteWrap(handle) {
+function createWriteWrap(handle, callback) {
const req = new WriteWrap();
req.handle = handle;
@@ -116,12 +116,13 @@ function createWriteWrap(handle) {
req.async = false;
req.bytes = 0;
req.buffer = null;
+ req.callback = callback;
return req;
}
function writevGeneric(self, data, cb) {
- const req = createWriteWrap(self[kHandle]);
+ const req = createWriteWrap(self[kHandle], cb);
const allBuffers = data.allBuffers;
let chunks;
if (allBuffers) {
@@ -146,7 +147,7 @@ function writevGeneric(self, data, cb) {
}
function writeGeneric(self, data, encoding, cb) {
- const req = createWriteWrap(self[kHandle]);
+ const req = createWriteWrap(self[kHandle], cb);
const err = handleWriteReq(req, data, encoding);
afterWriteDispatched(req, err, cb);
@@ -160,10 +161,8 @@ function afterWriteDispatched(req, err, cb) {
if (err !== 0)
return cb(errnoException(err, 'write', req.error));
- if (!req.async) {
- cb();
- } else {
- req.callback = cb;
+ if (!req.async && typeof req.callback === 'function') {
+ req.callback();
}
}
diff --git a/lib/net.js b/lib/net.js
index c92381f56d0178..c040434f3ea050 100644
--- a/lib/net.js
+++ b/lib/net.js
@@ -38,6 +38,9 @@ const { inspect } = require('internal/util/inspect');
let debug = require('internal/util/debuglog').debuglog('net', (fn) => {
debug = fn;
});
+const {
+ assertCrypto,
+} = require('internal/util');
const {
isIP,
isIPv4,
@@ -1726,3 +1729,25 @@ module.exports = {
Socket,
Stream: Socket, // Legacy naming
};
+
+if (process.versions.ngtcp2 !== undefined) {
+ let quic;
+
+ function lazyQuic() {
+ if (quic === undefined) {
+ assertCrypto();
+ quic = require('internal/quic/core');
+ process.emitWarning(
+ 'The QUIC protocol is experimental and not yet ' +
+ 'supported for production use',
+ 'ExperimentalWarning');
+ }
+ return quic;
+ }
+
+ function createQuicSocket(...args) {
+ return lazyQuic().createSocket(...args);
+ }
+
+ module.exports.createQuicSocket = createQuicSocket;
+}
diff --git a/node.gyp b/node.gyp
index a7e198bf968024..ddf59f4a0273cf 100644
--- a/node.gyp
+++ b/node.gyp
@@ -1,5 +1,6 @@
{
'variables': {
+ 'experimental_quic': 'false',
'v8_use_siphash%': 0,
'v8_trace_maps%': 0,
'v8_enable_pointer_compression%': 0,
@@ -19,6 +20,8 @@
'node_shared_cares%': 'false',
'node_shared_libuv%': 'false',
'node_shared_nghttp2%': 'false',
+ 'node_shared_ngtcp2%': 'false',
+ 'node_shared_nghttp3%': 'false',
'node_use_openssl%': 'true',
'node_shared_openssl%': 'false',
'node_v8_options%': '',
@@ -190,6 +193,8 @@
'lib/internal/process/task_queues.js',
'lib/internal/querystring.js',
'lib/internal/readline/utils.js',
+ 'lib/internal/quic/core.js',
+ 'lib/internal/quic/util.js',
'lib/internal/repl.js',
'lib/internal/repl/await.js',
'lib/internal/repl/history.js',
@@ -903,6 +908,38 @@
'node_target_type=="executable"', {
'defines': [ 'NODE_ENABLE_LARGE_CODE_PAGES=1' ],
}],
+ [
+ # We can only use QUIC if using our modified, static linked
+ # OpenSSL because we have patched in the QUIC support.
+ 'node_use_openssl=="true" and node_shared_openssl=="false" and experimental_quic==1', {
+ 'defines': ['NODE_EXPERIMENTAL_QUIC=1'],
+ 'sources': [
+ 'src/node_bob.h',
+ 'src/node_bob-inl.h',
+ 'src/quic/node_quic_buffer.h',
+ 'src/quic/node_quic_buffer-inl.h',
+ 'src/quic/node_quic_crypto.h',
+ 'src/quic/node_quic_session.h',
+ 'src/quic/node_quic_session-inl.h',
+ 'src/quic/node_quic_socket.h',
+ 'src/quic/node_quic_socket-inl.h',
+ 'src/quic/node_quic_stream.h',
+ 'src/quic/node_quic_stream-inl.h',
+ 'src/quic/node_quic_util.h',
+ 'src/quic/node_quic_util-inl.h',
+ 'src/quic/node_quic_state.h',
+ 'src/quic/node_quic_default_application.h',
+ 'src/quic/node_quic_http3_application.h',
+ 'src/quic/node_quic_buffer.cc',
+ 'src/quic/node_quic_crypto.cc',
+ 'src/quic/node_quic_session.cc',
+ 'src/quic/node_quic_socket.cc',
+ 'src/quic/node_quic_stream.cc',
+ 'src/quic/node_quic.cc',
+ 'src/quic/node_quic_default_application.cc',
+ 'src/quic/node_quic_http3_application.cc'
+ ]
+ }],
[ 'use_openssl_def==1', {
# TODO(bnoordhuis) Make all platforms export the same list of symbols.
# Teach mkssldef.py to generate linker maps that UNIX linkers understand.
@@ -1188,6 +1225,16 @@
'HAVE_OPENSSL=1',
],
}],
+ [ 'node_use_openssl=="true" and experimental_quic==1', {
+ 'defines': [
+ 'NODE_EXPERIMENTAL_QUIC=1',
+ ],
+ 'sources': [
+ 'test/cctest/test_quic_buffer.cc',
+ 'test/cctest/test_quic_cid.cc',
+ 'test/cctest/test_quic_verifyhostnameidentity.cc'
+ ]
+ }],
['v8_enable_inspector==1', {
'sources': [
'test/cctest/test_inspector_socket.cc',
diff --git a/node.gypi b/node.gypi
index 43dbda7bbf5302..dbe1b05cf546e2 100644
--- a/node.gypi
+++ b/node.gypi
@@ -187,6 +187,24 @@
'dependencies': [ 'deps/nghttp2/nghttp2.gyp:nghttp2' ],
}],
+ [
+ 'experimental_quic==1', {
+ 'conditions': [
+ [
+ 'node_shared_ngtcp2=="false"', {
+ 'dependencies': [
+ 'deps/ngtcp2/ngtcp2.gyp:ngtcp2',
+ ]}
+ ],
+ [
+ 'node_shared_nghttp3=="false"', {
+ 'dependencies': [
+ 'deps/nghttp3/nghttp3.gyp:nghttp3'
+ ]}
+ ]
+ ]}
+ ],
+
[ 'node_shared_brotli=="false"', {
'dependencies': [ 'deps/brotli/brotli.gyp:brotli' ],
}],
diff --git a/src/async_wrap.h b/src/async_wrap.h
index 4b62b740de3f1a..fa6634b6d3ab4c 100644
--- a/src/async_wrap.h
+++ b/src/async_wrap.h
@@ -59,6 +59,11 @@ namespace node {
V(PROCESSWRAP) \
V(PROMISE) \
V(QUERYWRAP) \
+ V(QUICCLIENTSESSION) \
+ V(QUICSERVERSESSION) \
+ V(QUICSENDWRAP) \
+ V(QUICSOCKET) \
+ V(QUICSTREAM) \
V(SHUTDOWNWRAP) \
V(SIGNALWRAP) \
V(STATWATCHER) \
diff --git a/src/debug_utils.h b/src/debug_utils.h
index ecc53b0c2b0aa0..845a78c53c30fc 100644
--- a/src/debug_utils.h
+++ b/src/debug_utils.h
@@ -45,6 +45,7 @@ void FWrite(FILE* file, const std::string& str);
V(INSPECTOR_SERVER) \
V(INSPECTOR_PROFILER) \
V(CODE_CACHE) \
+ V(NGTCP2_DEBUG) \
V(WASI)
enum class DebugCategory {
diff --git a/src/env.h b/src/env.h
index 60fb2575dc0972..706a2de16a0073 100644
--- a/src/env.h
+++ b/src/env.h
@@ -172,6 +172,7 @@ constexpr size_t kFsStatsBufferLength =
// Strings are per-isolate primitives but Environment proxies them
// for the sake of convenience. Strings should be ASCII-only.
#define PER_ISOLATE_STRING_PROPERTIES(V) \
+ V(ack_string, "ack") \
V(address_string, "address") \
V(aliases_string, "aliases") \
V(args_string, "args") \
@@ -334,6 +335,8 @@ constexpr size_t kFsStatsBufferLength =
V(psk_string, "psk") \
V(pubkey_string, "pubkey") \
V(query_string, "query") \
+ V(quic_alpn_string, "h3-27") \
+ V(rate_string, "rate") \
V(raw_string, "raw") \
V(read_host_object_string, "_readHostObject") \
V(readable_string, "readable") \
@@ -361,6 +364,8 @@ constexpr size_t kFsStatsBufferLength =
V(stack_string, "stack") \
V(standard_name_string, "standardName") \
V(start_time_string, "startTime") \
+ V(state_string, "state") \
+ V(stats_string, "stats") \
V(status_string, "status") \
V(stdio_string, "stdio") \
V(subject_string, "subject") \
@@ -399,6 +404,16 @@ constexpr size_t kFsStatsBufferLength =
V(x_forwarded_string, "x-forwarded-for") \
V(zero_return_string, "ZERO_RETURN")
+#if defined(NODE_EXPERIMENTAL_QUIC) && NODE_EXPERIMENTAL_QUIC
+# define QUIC_ENVIRONMENT_STRONG_PERSISTENT_TEMPLATES(V) \
+ V(quicclientsession_instance_template, v8::ObjectTemplate) \
+ V(quicserversession_instance_template, v8::ObjectTemplate) \
+ V(quicserverstream_instance_template, v8::ObjectTemplate) \
+ V(quicsocketsendwrap_instance_template, v8::ObjectTemplate)
+#else
+# define QUIC_ENVIRONMENT_STRONG_PERSISTENT_TEMPLATES(V)
+#endif
+
#define ENVIRONMENT_STRONG_PERSISTENT_TEMPLATES(V) \
V(async_wrap_ctor_template, v8::FunctionTemplate) \
V(async_wrap_object_ctor_template, v8::FunctionTemplate) \
@@ -428,7 +443,38 @@ constexpr size_t kFsStatsBufferLength =
V(tcp_constructor_template, v8::FunctionTemplate) \
V(tty_constructor_template, v8::FunctionTemplate) \
V(write_wrap_template, v8::ObjectTemplate) \
- V(worker_heap_snapshot_taker_template, v8::ObjectTemplate)
+ V(worker_heap_snapshot_taker_template, v8::ObjectTemplate) \
+ QUIC_ENVIRONMENT_STRONG_PERSISTENT_TEMPLATES(V)
+
+#if defined(NODE_EXPERIMENTAL_QUIC) && NODE_EXPERIMENTAL_QUIC
+# define QUIC_ENVIRONMENT_STRONG_PERSISTENT_VALUES(V) \
+ V(quic_on_socket_close_function, v8::Function) \
+ V(quic_on_socket_error_function, v8::Function) \
+ V(quic_on_socket_server_busy_function, v8::Function) \
+ V(quic_on_session_cert_function, v8::Function) \
+ V(quic_on_session_client_hello_function, v8::Function) \
+ V(quic_on_session_close_function, v8::Function) \
+ V(quic_on_session_destroyed_function, v8::Function) \
+ V(quic_on_session_error_function, v8::Function) \
+ V(quic_on_session_handshake_function, v8::Function) \
+ V(quic_on_session_keylog_function, v8::Function) \
+ V(quic_on_session_path_validation_function, v8::Function) \
+ V(quic_on_session_use_preferred_address_function, v8::Function) \
+ V(quic_on_session_qlog_function, v8::Function) \
+ V(quic_on_session_ready_function, v8::Function) \
+ V(quic_on_session_silent_close_function, v8::Function) \
+ V(quic_on_session_status_function, v8::Function) \
+ V(quic_on_session_ticket_function, v8::Function) \
+ V(quic_on_session_version_negotiation_function, v8::Function) \
+ V(quic_on_stream_close_function, v8::Function) \
+ V(quic_on_stream_error_function, v8::Function) \
+ V(quic_on_stream_ready_function, v8::Function) \
+ V(quic_on_stream_reset_function, v8::Function) \
+ V(quic_on_stream_headers_function, v8::Function) \
+ V(quic_on_stream_blocked_function, v8::Function)
+#else
+# define QUIC_ENVIRONMENT_STRONG_PERSISTENT_VALUES(V)
+#endif
#define ENVIRONMENT_STRONG_PERSISTENT_VALUES(V) \
V(async_hooks_after_function, v8::Function) \
@@ -477,7 +523,8 @@ constexpr size_t kFsStatsBufferLength =
V(tls_wrap_constructor_function, v8::Function) \
V(trace_category_state_function, v8::Function) \
V(udp_constructor_function, v8::Function) \
- V(url_constructor_function, v8::Function)
+ V(url_constructor_function, v8::Function) \
+ QUIC_ENVIRONMENT_STRONG_PERSISTENT_VALUES(V)
class Environment;
struct AllocatedBuffer;
diff --git a/src/handle_wrap.h b/src/handle_wrap.h
index a555da9479de93..4134b28bfc1491 100644
--- a/src/handle_wrap.h
+++ b/src/handle_wrap.h
@@ -93,10 +93,11 @@ class HandleWrap : public AsyncWrap {
return state_ == kClosing || state_ == kClosed;
}
+ static void OnClose(uv_handle_t* handle);
+
private:
friend class Environment;
friend void GetActiveHandles(const v8::FunctionCallbackInfo&);
- static void OnClose(uv_handle_t* handle);
// handle_wrap_queue_ needs to be at a fixed offset from the start of the
// class because it is used by src/node_postmortem_metadata.cc to calculate
diff --git a/src/node_binding.cc b/src/node_binding.cc
index 99fd69819f97d7..e3014657bbe25b 100644
--- a/src/node_binding.cc
+++ b/src/node_binding.cc
@@ -11,6 +11,12 @@
#define NODE_BUILTIN_OPENSSL_MODULES(V)
#endif
+#if defined(NODE_EXPERIMENTAL_QUIC) && NODE_EXPERIMENTAL_QUIC
+#define NODE_BUILTIN_QUIC_MODULES(V) V(quic)
+#else
+#define NODE_BUILTIN_QUIC_MODULES(V)
+#endif
+
#if NODE_HAVE_I18N_SUPPORT
#define NODE_BUILTIN_ICU_MODULES(V) V(icu)
#else
@@ -89,6 +95,7 @@
#define NODE_BUILTIN_MODULES(V) \
NODE_BUILTIN_STANDARD_MODULES(V) \
NODE_BUILTIN_OPENSSL_MODULES(V) \
+ NODE_BUILTIN_QUIC_MODULES(V) \
NODE_BUILTIN_ICU_MODULES(V) \
NODE_BUILTIN_PROFILER_MODULES(V) \
NODE_BUILTIN_DTRACE_MODULES(V)
diff --git a/src/node_bob-inl.h b/src/node_bob-inl.h
new file mode 100644
index 00000000000000..578e45e9ff6149
--- /dev/null
+++ b/src/node_bob-inl.h
@@ -0,0 +1,37 @@
+#ifndef SRC_NODE_BOB_INL_H_
+#define SRC_NODE_BOB_INL_H_
+
+#include "node_bob.h"
+
+#include
+
+namespace node {
+namespace bob {
+
+template
+int SourceImpl::Pull(
+ Next next,
+ int options,
+ T* data,
+ size_t count,
+ size_t max_count_hint) {
+
+ int status;
+ if (eos_) {
+ status = bob::Status::STATUS_EOS;
+ std::move(next)(status, nullptr, 0, [](size_t len) {});
+ return status;
+ }
+
+ status = DoPull(std::move(next), options, data, count, max_count_hint);
+
+ if (status == bob::Status::STATUS_END)
+ eos_ = true;
+
+ return status;
+}
+
+} // namespace bob
+} // namespace node
+
+#endif // SRC_NODE_BOB_INL_H_
diff --git a/src/node_bob.h b/src/node_bob.h
new file mode 100644
index 00000000000000..74571608f3ae56
--- /dev/null
+++ b/src/node_bob.h
@@ -0,0 +1,111 @@
+#ifndef SRC_NODE_BOB_H_
+#define SRC_NODE_BOB_H_
+
+#include
+
+namespace node {
+namespace bob {
+
+constexpr size_t kMaxCountHint = 16;
+
+// Negative status codes indicate error conditions.
+enum Status : int {
+ // Indicates that an attempt was made to pull after end.
+ STATUS_EOS = -1,
+
+ // Indicates the end of the stream. No additional
+ // data will be available and the consumer should stop
+ // pulling.
+ STATUS_END = 0,
+
+ // Indicates that there is additional data available
+ // and the consumer may continue to pull.
+ STATUS_CONTINUE = 1,
+
+ // Indicates that there is no additional data available
+ // but the stream has not ended. The consumer should not
+ // continue to pull but may resume pulling later when
+ // data is available.
+ STATUS_BLOCK = 2,
+
+ // Indicates that there is no additional data available
+ // but the stream has not ended and the source will call
+ // next again later when data is available. STATUS_WAIT
+ // must not be used with the OPTIONS_SYNC option.
+ STATUS_WAIT = 3,
+};
+
+enum Options : int {
+ OPTIONS_NONE = 0,
+
+ // Indicates that the consumer is requesting the end
+ // of the stream.
+ OPTIONS_END = 1,
+
+ // Indicates that the consumer requires the source to
+ // invoke Next synchronously. If the source is
+ // unable to provide data immediately but the
+ // stream has not yet ended, it should call Next
+ // using STATUS_BLOCK. When not set, the source
+ // may call Next asynchronously.
+ OPTIONS_SYNC = 2
+};
+
+// There are Sources and there are Consumers.
+//
+// Consumers get data by calling Source::Pull,
+// optionally passing along a status and allocated
+// buffer space for the source to fill, and a Next
+// function the Source will call when data is
+// available.
+//
+// The source calls Next to deliver the data. It can
+// choose to either use the allocated buffer space
+// provided by the consumer or it can allocate its own
+// buffers and push those instead. If it allocates
+// its own, it can send a Done function that the
+// Sink will call when it is done consuming the data.
+using Done = std::function;
+template
+using Next = std::function;
+
+template
+class Source {
+ public:
+ virtual int Pull(
+ Next next,
+ int options,
+ T* data,
+ size_t count,
+ size_t max_count_hint = kMaxCountHint) = 0;
+};
+
+
+template
+class SourceImpl : public Source {
+ public:
+ int Pull(
+ Next next,
+ int options,
+ T* data,
+ size_t count,
+ size_t max_count_hint = kMaxCountHint) override;
+
+ bool is_eos() const { return eos_; }
+
+ protected:
+ virtual int DoPull(
+ Next next,
+ int options,
+ T* data,
+ size_t count,
+ size_t max_count_hint) = 0;
+
+ private:
+ bool eos_ = false;
+};
+
+} // namespace bob
+} // namespace node
+
+#endif // SRC_NODE_BOB_H_
diff --git a/src/node_errors.h b/src/node_errors.h
index b9f001d777bb07..871071aaa07c0a 100644
--- a/src/node_errors.h
+++ b/src/node_errors.h
@@ -56,7 +56,9 @@ void OnFatalError(const char* location, const char* message);
V(ERR_VM_MODULE_CACHED_DATA_REJECTED, Error) \
V(ERR_WASI_NOT_STARTED, Error) \
V(ERR_WORKER_INIT_FAILED, Error) \
- V(ERR_PROTO_ACCESS, Error)
+ V(ERR_PROTO_ACCESS, Error) \
+ V(ERR_QUIC_CANNOT_SET_GROUPS, Error) \
+ V(ERR_QUIC_FAILURE_SETTING_SNI_CONTEXT, Error)
#define V(code, type) \
inline v8::Local code(v8::Isolate* isolate, \
@@ -111,7 +113,9 @@ void OnFatalError(const char* location, const char* message);
V(ERR_WORKER_INIT_FAILED, "Worker initialization failure") \
V(ERR_PROTO_ACCESS, \
"Accessing Object.prototype.__proto__ has been " \
- "disallowed with --disable-proto=throw")
+ "disallowed with --disable-proto=throw") \
+ V(ERR_QUIC_CANNOT_SET_GROUPS, "Cannot set groups") \
+ V(ERR_QUIC_FAILURE_SETTING_SNI_CONTEXT, "Failure setting SNI context")
#define V(code, message) \
inline v8::Local code(v8::Isolate* isolate) { \
diff --git a/src/node_metadata.cc b/src/node_metadata.cc
index 8d0a725de45421..e8864d35527258 100644
--- a/src/node_metadata.cc
+++ b/src/node_metadata.cc
@@ -13,6 +13,11 @@
#include
#endif // HAVE_OPENSSL
+#if defined(NODE_EXPERIMENTAL_QUIC) && NODE_EXPERIMENTAL_QUIC
+#include
+#include
+#endif
+
#ifdef NODE_HAVE_I18N_SUPPORT
#include
#include
@@ -91,6 +96,11 @@ Metadata::Versions::Versions() {
openssl = GetOpenSSLVersion();
#endif
+#if defined(NODE_EXPERIMENTAL_QUIC) && NODE_EXPERIMENTAL_QUIC
+ ngtcp2 = NGTCP2_VERSION;
+ nghttp3 = NGHTTP3_VERSION;
+#endif
+
#ifdef NODE_HAVE_I18N_SUPPORT
icu = U_ICU_VERSION;
unicode = U_UNICODE_VERSION;
diff --git a/src/node_metadata.h b/src/node_metadata.h
index bf7e5d3ff4e811..2a4571883d8a0d 100644
--- a/src/node_metadata.h
+++ b/src/node_metadata.h
@@ -38,6 +38,12 @@ namespace node {
#define NODE_VERSIONS_KEY_CRYPTO(V)
#endif
+#if defined(NODE_EXPERIMENTAL_QUIC) && NODE_EXPERIMENTAL_QUIC
+#define NODE_VERSIONS_KEY_QUIC(V) V(ngtcp2) V(nghttp3)
+#else
+#define NODE_VERSIONS_KEY_QUIC(V)
+#endif
+
#ifdef NODE_HAVE_I18N_SUPPORT
#define NODE_VERSIONS_KEY_INTL(V) \
V(cldr) \
@@ -51,6 +57,7 @@ namespace node {
#define NODE_VERSIONS_KEYS(V) \
NODE_VERSIONS_KEYS_BASE(V) \
NODE_VERSIONS_KEY_CRYPTO(V) \
+ NODE_VERSIONS_KEY_QUIC(V) \
NODE_VERSIONS_KEY_INTL(V)
class Metadata {
diff --git a/src/node_native_module.cc b/src/node_native_module.cc
index a7675d00d89cd3..8791c99a0657fd 100644
--- a/src/node_native_module.cc
+++ b/src/node_native_module.cc
@@ -101,7 +101,10 @@ void NativeModuleLoader::InitializeModuleCategories() {
"internal/process/policy",
"internal/streams/lazy_transform",
#endif // !HAVE_OPENSSL
-
+#if !NODE_EXPERIMENTAL_QUIC
+ "internal/quic/core",
+ "internal/quic/util",
+#endif
"sys", // Deprecated.
"wasi", // Experimental.
"internal/test/binding",
diff --git a/src/quic/node_quic.cc b/src/quic/node_quic.cc
new file mode 100644
index 00000000000000..9223694643554b
--- /dev/null
+++ b/src/quic/node_quic.cc
@@ -0,0 +1,250 @@
+#include "debug_utils-inl.h"
+#include "node.h"
+#include "env-inl.h"
+#include "node_crypto.h" // SecureContext
+#include "node_crypto_common.h"
+#include "node_errors.h"
+#include "node_process.h"
+#include "node_quic_crypto.h"
+#include "node_quic_session-inl.h"
+#include "node_quic_socket-inl.h"
+#include "node_quic_stream-inl.h"
+#include "node_quic_state.h"
+#include "node_quic_util-inl.h"
+#include "node_sockaddr-inl.h"
+
+#include
+#include
+
+namespace node {
+
+using crypto::SecureContext;
+using v8::Context;
+using v8::Function;
+using v8::FunctionCallbackInfo;
+using v8::HandleScope;
+using v8::Isolate;
+using v8::Local;
+using v8::Object;
+using v8::Value;
+
+namespace quic {
+
+constexpr FastStringKey QuicState::binding_data_name;
+
+void QuicState::MemoryInfo(MemoryTracker* tracker) const {
+ tracker->TrackField("root_buffer", root_buffer);
+}
+
+namespace {
+// Register the JavaScript callbacks the internal binding will use to report
+// status and updates. This is called only once when the quic module is loaded.
+void QuicSetCallbacks(const FunctionCallbackInfo& args) {
+ Environment* env = Environment::GetCurrent(args);
+ CHECK(args[0]->IsObject());
+ Local