From 55360443ced34515c775c5beb29f907de671dffe Mon Sep 17 00:00:00 2001 From: James M Snell Date: Thu, 7 May 2020 15:49:14 -0700 Subject: [PATCH] quic: initial QUIC implementation Co-authored-by: Anna Henningsen Co-authored-by: Daniel Bevenius Co-authored-by: gengjiawen Co-authored-by: James M Snell Co-authored-by: Lucas Pardue Co-authored-by: Ouyang Yadong Co-authored-by: Juan Jos Arboleda Co-authored-by: Trivikram Kamat Co-authored-by: Denys Otrishko PR-URL: https://github.com/nodejs/node/pull/32379 Reviewed-By: Anna Henningsen --- LICENSE | 52 + configure.py | 57 + deps/openssl/openssl.gyp | 7 + doc/api/errors.md | 119 + doc/api/index.md | 1 + doc/api/net.md | 9 + doc/api/quic.md | 2277 ++++++++++ lib/internal/errors.js | 55 + lib/internal/quic/core.js | 3352 +++++++++++++++ lib/internal/quic/util.js | 773 ++++ lib/internal/stream_base_commons.js | 13 +- lib/net.js | 25 + node.gyp | 47 + node.gypi | 18 + src/async_wrap.h | 5 + src/debug_utils.h | 1 + src/env.h | 51 +- src/handle_wrap.h | 3 +- src/node_binding.cc | 7 + src/node_bob-inl.h | 37 + src/node_bob.h | 111 + src/node_errors.h | 8 +- src/node_metadata.cc | 10 + src/node_metadata.h | 7 + src/node_native_module.cc | 5 +- src/quic/node_quic.cc | 250 ++ src/quic/node_quic_buffer-inl.h | 99 + src/quic/node_quic_buffer.cc | 159 + src/quic/node_quic_buffer.h | 239 ++ src/quic/node_quic_crypto.cc | 961 +++++ src/quic/node_quic_crypto.h | 157 + src/quic/node_quic_default_application.cc | 181 + src/quic/node_quic_default_application.h | 61 + src/quic/node_quic_http3_application.cc | 935 ++++ src/quic/node_quic_http3_application.h | 333 ++ src/quic/node_quic_session-inl.h | 614 +++ src/quic/node_quic_session.cc | 3757 +++++++++++++++++ src/quic/node_quic_session.h | 1563 +++++++ src/quic/node_quic_socket-inl.h | 232 + src/quic/node_quic_socket.cc | 1183 ++++++ src/quic/node_quic_socket.h | 618 +++ src/quic/node_quic_state.h | 81 + src/quic/node_quic_stream-inl.h | 158 + src/quic/node_quic_stream.cc | 513 +++ src/quic/node_quic_stream.h | 407 ++ src/quic/node_quic_util-inl.h | 473 +++ src/quic/node_quic_util.h | 428 ++ src/udp_wrap.cc | 1 + src/util.h | 10 + test/cctest/test_quic_buffer.cc | 206 + test/cctest/test_quic_cid.cc | 31 + .../test_quic_verifyhostnameidentity.cc | 349 ++ test/common/README.md | 1 + test/common/fixtures.js | 7 +- test/common/index.js | 2 + test/common/quic.js | 38 + test/parallel/test-process-versions.js | 5 + test/parallel/test-quic-binding.js | 39 + ...t-quic-client-connect-multiple-parallel.js | 57 + ...quic-client-connect-multiple-sequential.js | 56 + ...est-quic-client-empty-preferred-address.js | 57 + test/parallel/test-quic-client-server.js | 372 ++ ...test-quic-errors-quicsession-openstream.js | 84 + .../test-quic-errors-quicsocket-connect.js | 243 ++ .../test-quic-errors-quicsocket-create.js | 187 + .../test-quic-errors-quicsocket-listen.js | 179 + .../parallel/test-quic-http3-client-server.js | 157 + test/parallel/test-quic-http3-push.js | 157 + test/parallel/test-quic-http3-trailers.js | 109 + test/parallel/test-quic-idle-timeout.js | 73 + test/parallel/test-quic-ipv6only.js | 140 + test/parallel/test-quic-keylog.js | 67 + .../test-quic-maxconnectionsperhost.js | 86 + test/parallel/test-quic-process-cleanup.js | 57 + test/parallel/test-quic-qlog.js | 66 + .../test-quic-quicendpoint-address.js | 87 + ...ic-quicession-server-openstream-pending.js | 59 + ...est-quic-quicsession-openstream-pending.js | 64 + test/parallel/test-quic-quicsession-resume.js | 100 + .../parallel/test-quic-quicsession-send-fd.js | 86 + ...quicsession-send-file-close-before-open.js | 46 + ...uicsession-send-file-open-error-handled.js | 49 + ...t-quic-quicsession-send-file-open-error.js | 49 + ...t-quic-quicsession-server-destroy-early.js | 81 + test/parallel/test-quic-quicsocket-close.js | 19 + ...st-quic-quicsocket-packetloss-stream-rx.js | 109 + ...st-quic-quicsocket-packetloss-stream-tx.js | 109 + .../test-quic-quicsocket-serverbusy.js | 62 + test/parallel/test-quic-quicsocket.js | 154 + .../test-quic-quicstream-close-early.js | 122 + test/parallel/test-quic-quicstream-destroy.js | 75 + .../test-quic-quicstream-identifiers.js | 156 + .../test-quic-simple-client-migrate.js | 108 + test/parallel/test-quic-statelessreset.js | 76 + test/parallel/test-quic-with-fake-udp.js | 61 + test/pummel/test-heapdump-quic.js | 152 + test/sequential/test-async-wrap-getasyncid.js | 6 + .../test-quic-preferred-address-ipv6.js | 94 + .../sequential/test-quic-preferred-address.js | 93 + tools/doc/type-parser.js | 4 + tools/license-builder.sh | 6 + vcbuild.bat | 5 +- 102 files changed, 25305 insertions(+), 15 deletions(-) create mode 100644 doc/api/quic.md create mode 100644 lib/internal/quic/core.js create mode 100644 lib/internal/quic/util.js create mode 100644 src/node_bob-inl.h create mode 100644 src/node_bob.h create mode 100644 src/quic/node_quic.cc create mode 100644 src/quic/node_quic_buffer-inl.h create mode 100644 src/quic/node_quic_buffer.cc create mode 100644 src/quic/node_quic_buffer.h create mode 100644 src/quic/node_quic_crypto.cc create mode 100644 src/quic/node_quic_crypto.h create mode 100644 src/quic/node_quic_default_application.cc create mode 100644 src/quic/node_quic_default_application.h create mode 100644 src/quic/node_quic_http3_application.cc create mode 100644 src/quic/node_quic_http3_application.h create mode 100644 src/quic/node_quic_session-inl.h create mode 100644 src/quic/node_quic_session.cc create mode 100644 src/quic/node_quic_session.h create mode 100644 src/quic/node_quic_socket-inl.h create mode 100644 src/quic/node_quic_socket.cc create mode 100644 src/quic/node_quic_socket.h create mode 100644 src/quic/node_quic_state.h create mode 100644 src/quic/node_quic_stream-inl.h create mode 100644 src/quic/node_quic_stream.cc create mode 100644 src/quic/node_quic_stream.h create mode 100644 src/quic/node_quic_util-inl.h create mode 100644 src/quic/node_quic_util.h create mode 100644 test/cctest/test_quic_buffer.cc create mode 100644 test/cctest/test_quic_cid.cc create mode 100644 test/cctest/test_quic_verifyhostnameidentity.cc create mode 100644 test/common/quic.js create mode 100644 test/parallel/test-quic-binding.js create mode 100644 test/parallel/test-quic-client-connect-multiple-parallel.js create mode 100644 test/parallel/test-quic-client-connect-multiple-sequential.js create mode 100644 test/parallel/test-quic-client-empty-preferred-address.js create mode 100644 test/parallel/test-quic-client-server.js create mode 100644 test/parallel/test-quic-errors-quicsession-openstream.js create mode 100644 test/parallel/test-quic-errors-quicsocket-connect.js create mode 100644 test/parallel/test-quic-errors-quicsocket-create.js create mode 100644 test/parallel/test-quic-errors-quicsocket-listen.js create mode 100644 test/parallel/test-quic-http3-client-server.js create mode 100644 test/parallel/test-quic-http3-push.js create mode 100644 test/parallel/test-quic-http3-trailers.js create mode 100644 test/parallel/test-quic-idle-timeout.js create mode 100644 test/parallel/test-quic-ipv6only.js create mode 100644 test/parallel/test-quic-keylog.js create mode 100644 test/parallel/test-quic-maxconnectionsperhost.js create mode 100644 test/parallel/test-quic-process-cleanup.js create mode 100644 test/parallel/test-quic-qlog.js create mode 100644 test/parallel/test-quic-quicendpoint-address.js create mode 100644 test/parallel/test-quic-quicession-server-openstream-pending.js create mode 100644 test/parallel/test-quic-quicsession-openstream-pending.js create mode 100644 test/parallel/test-quic-quicsession-resume.js create mode 100644 test/parallel/test-quic-quicsession-send-fd.js create mode 100644 test/parallel/test-quic-quicsession-send-file-close-before-open.js create mode 100644 test/parallel/test-quic-quicsession-send-file-open-error-handled.js create mode 100644 test/parallel/test-quic-quicsession-send-file-open-error.js create mode 100644 test/parallel/test-quic-quicsession-server-destroy-early.js create mode 100644 test/parallel/test-quic-quicsocket-close.js create mode 100644 test/parallel/test-quic-quicsocket-packetloss-stream-rx.js create mode 100644 test/parallel/test-quic-quicsocket-packetloss-stream-tx.js create mode 100644 test/parallel/test-quic-quicsocket-serverbusy.js create mode 100644 test/parallel/test-quic-quicsocket.js create mode 100644 test/parallel/test-quic-quicstream-close-early.js create mode 100644 test/parallel/test-quic-quicstream-destroy.js create mode 100644 test/parallel/test-quic-quicstream-identifiers.js create mode 100644 test/parallel/test-quic-simple-client-migrate.js create mode 100644 test/parallel/test-quic-statelessreset.js create mode 100644 test/parallel/test-quic-with-fake-udp.js create mode 100644 test/pummel/test-heapdump-quic.js create mode 100644 test/sequential/test-quic-preferred-address-ipv6.js create mode 100644 test/sequential/test-quic-preferred-address.js 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 obj = args[0].As(); + +#define SETFUNCTION(name, callback) \ + do { \ + Local fn; \ + CHECK(obj->Get(env->context(), \ + FIXED_ONE_BYTE_STRING(env->isolate(), name)).ToLocal(&fn));\ + CHECK(fn->IsFunction()); \ + env->set_quic_on_##callback##_function(fn.As()); \ + } while (0) + + SETFUNCTION("onSocketClose", socket_close); + SETFUNCTION("onSocketError", socket_error); + SETFUNCTION("onSessionReady", session_ready); + SETFUNCTION("onSessionCert", session_cert); + SETFUNCTION("onSessionClientHello", session_client_hello); + SETFUNCTION("onSessionClose", session_close); + SETFUNCTION("onSessionDestroyed", session_destroyed); + SETFUNCTION("onSessionError", session_error); + SETFUNCTION("onSessionHandshake", session_handshake); + SETFUNCTION("onSessionKeylog", session_keylog); + SETFUNCTION("onSessionUsePreferredAddress", session_use_preferred_address); + SETFUNCTION("onSessionPathValidation", session_path_validation); + SETFUNCTION("onSessionQlog", session_qlog); + SETFUNCTION("onSessionSilentClose", session_silent_close); + SETFUNCTION("onSessionStatus", session_status); + SETFUNCTION("onSessionTicket", session_ticket); + SETFUNCTION("onSessionVersionNegotiation", session_version_negotiation); + SETFUNCTION("onStreamReady", stream_ready); + SETFUNCTION("onStreamClose", stream_close); + SETFUNCTION("onStreamError", stream_error); + SETFUNCTION("onStreamReset", stream_reset); + SETFUNCTION("onSocketServerBusy", socket_server_busy); + SETFUNCTION("onStreamHeaders", stream_headers); + SETFUNCTION("onStreamBlocked", stream_blocked); + +#undef SETFUNCTION +} + +// Sets QUIC specific configuration options for the SecureContext. +// It's entirely likely that there's a better way to do this, but +// for now this works. +template +void QuicInitSecureContext(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + CHECK(args[0]->IsObject()); // Secure Context + CHECK(args[1]->IsString()); // groups + CHECK(args[2]->IsBoolean()); // early data + + SecureContext* sc; + ASSIGN_OR_RETURN_UNWRAP(&sc, args[0].As(), + args.GetReturnValue().Set(UV_EBADF)); + const node::Utf8Value groups(env->isolate(), args[1]); + + bool early_data = args[2]->BooleanValue(env->isolate()); + + InitializeSecureContext( + BaseObjectPtr(sc), + early_data, + side); + + if (!crypto::SetGroups(sc, *groups)) + THROW_ERR_QUIC_CANNOT_SET_GROUPS(env); +} +} // namespace + +void Initialize(Local target, + Local unused, + Local context, + void* priv) { + Environment* env = Environment::GetCurrent(context); + Isolate* isolate = env->isolate(); + HandleScope handle_scope(isolate); + + HistogramBase::Initialize(env); + + QuicState* const state = + env->AddBindingData(context, target); + if (state == nullptr) return; + +#define SET_STATE_TYPEDARRAY(name, field) \ + target->Set(context, \ + FIXED_ONE_BYTE_STRING(isolate, (name)), \ + (field.GetJSArray())).FromJust() + SET_STATE_TYPEDARRAY("sessionConfig", state->quicsessionconfig_buffer); + SET_STATE_TYPEDARRAY("http3Config", state->http3config_buffer); +#undef SET_STATE_TYPEDARRAY + + QuicSocket::Initialize(env, target, context); + QuicEndpoint::Initialize(env, target, context); + QuicSession::Initialize(env, target, context); + QuicStream::Initialize(env, target, context); + + env->SetMethod(target, + "setCallbacks", + QuicSetCallbacks); + env->SetMethod(target, + "initSecureContext", + QuicInitSecureContext); + env->SetMethod(target, + "initSecureContextClient", + QuicInitSecureContext); + + Local constants = Object::New(env->isolate()); + +// TODO(@jasnell): Audit which constants are actually being used in JS +#define QUIC_CONSTANTS(V) \ + V(DEFAULT_MAX_STREAM_DATA_BIDI_LOCAL) \ + V(DEFAULT_RETRYTOKEN_EXPIRATION) \ + V(DEFAULT_MAX_CONNECTIONS) \ + V(DEFAULT_MAX_CONNECTIONS_PER_HOST) \ + V(DEFAULT_MAX_STATELESS_RESETS_PER_HOST) \ + V(IDX_HTTP3_QPACK_MAX_TABLE_CAPACITY) \ + V(IDX_HTTP3_QPACK_BLOCKED_STREAMS) \ + V(IDX_HTTP3_MAX_HEADER_LIST_SIZE) \ + V(IDX_HTTP3_MAX_PUSHES) \ + V(IDX_HTTP3_MAX_HEADER_PAIRS) \ + V(IDX_HTTP3_MAX_HEADER_LENGTH) \ + V(IDX_HTTP3_CONFIG_COUNT) \ + V(IDX_QUIC_SESSION_ACTIVE_CONNECTION_ID_LIMIT) \ + V(IDX_QUIC_SESSION_MAX_IDLE_TIMEOUT) \ + V(IDX_QUIC_SESSION_MAX_DATA) \ + V(IDX_QUIC_SESSION_MAX_STREAM_DATA_BIDI_LOCAL) \ + V(IDX_QUIC_SESSION_MAX_STREAM_DATA_BIDI_REMOTE) \ + V(IDX_QUIC_SESSION_MAX_STREAM_DATA_UNI) \ + V(IDX_QUIC_SESSION_MAX_STREAMS_BIDI) \ + V(IDX_QUIC_SESSION_MAX_STREAMS_UNI) \ + V(IDX_QUIC_SESSION_MAX_PACKET_SIZE) \ + V(IDX_QUIC_SESSION_ACK_DELAY_EXPONENT) \ + V(IDX_QUIC_SESSION_DISABLE_MIGRATION) \ + V(IDX_QUIC_SESSION_MAX_ACK_DELAY) \ + V(IDX_QUIC_SESSION_CONFIG_COUNT) \ + V(IDX_QUIC_SESSION_STATE_CERT_ENABLED) \ + V(IDX_QUIC_SESSION_STATE_CLIENT_HELLO_ENABLED) \ + V(IDX_QUIC_SESSION_STATE_USE_PREFERRED_ADDRESS_ENABLED) \ + V(IDX_QUIC_SESSION_STATE_PATH_VALIDATED_ENABLED) \ + V(IDX_QUIC_SESSION_STATE_KEYLOG_ENABLED) \ + V(IDX_QUIC_SESSION_STATE_MAX_STREAMS_BIDI) \ + V(IDX_QUIC_SESSION_STATE_MAX_STREAMS_UNI) \ + V(IDX_QUIC_SESSION_STATE_MAX_DATA_LEFT) \ + V(IDX_QUIC_SESSION_STATE_BYTES_IN_FLIGHT) \ + V(IDX_QUIC_SESSION_STATE_HANDSHAKE_CONFIRMED) \ + V(IDX_QUIC_SESSION_STATE_IDLE_TIMEOUT) \ + V(MAX_RETRYTOKEN_EXPIRATION) \ + V(MIN_RETRYTOKEN_EXPIRATION) \ + V(NGTCP2_APP_NOERROR) \ + V(NGTCP2_PATH_VALIDATION_RESULT_FAILURE) \ + V(NGTCP2_PATH_VALIDATION_RESULT_SUCCESS) \ + V(QUIC_ERROR_APPLICATION) \ + V(QUIC_ERROR_CRYPTO) \ + V(QUIC_ERROR_SESSION) \ + V(QUIC_PREFERRED_ADDRESS_USE) \ + V(QUIC_PREFERRED_ADDRESS_IGNORE) \ + V(QUICCLIENTSESSION_OPTION_REQUEST_OCSP) \ + V(QUICCLIENTSESSION_OPTION_VERIFY_HOSTNAME_IDENTITY) \ + V(QUICSERVERSESSION_OPTION_REJECT_UNAUTHORIZED) \ + V(QUICSERVERSESSION_OPTION_REQUEST_CERT) \ + V(QUICSOCKET_OPTIONS_VALIDATE_ADDRESS) \ + V(QUICSOCKET_OPTIONS_VALIDATE_ADDRESS_LRU) \ + V(QUICSTREAM_HEADER_FLAGS_NONE) \ + V(QUICSTREAM_HEADER_FLAGS_TERMINAL) \ + V(QUICSTREAM_HEADERS_KIND_NONE) \ + V(QUICSTREAM_HEADERS_KIND_INFORMATIONAL) \ + V(QUICSTREAM_HEADERS_KIND_PUSH) \ + V(QUICSTREAM_HEADERS_KIND_INITIAL) \ + V(QUICSTREAM_HEADERS_KIND_TRAILING) \ + V(ERR_FAILED_TO_CREATE_SESSION) \ + V(UV_EBADF) + +#define V(name, _, __) \ + NODE_DEFINE_CONSTANT(constants, IDX_QUIC_SESSION_STATS_##name); + SESSION_STATS(V) +#undef V + +#define V(name, _, __) \ + NODE_DEFINE_CONSTANT(constants, IDX_QUIC_SOCKET_STATS_##name); + SOCKET_STATS(V) +#undef V + +#define V(name, _, __) \ + NODE_DEFINE_CONSTANT(constants, IDX_QUIC_STREAM_STATS_##name); + STREAM_STATS(V) +#undef V + +#define V(name) NODE_DEFINE_CONSTANT(constants, name); + QUIC_CONSTANTS(V) +#undef V + + NODE_DEFINE_CONSTANT(constants, NGTCP2_PROTO_VER); + NODE_DEFINE_CONSTANT(constants, NGTCP2_DEFAULT_MAX_ACK_DELAY); + NODE_DEFINE_CONSTANT(constants, NGTCP2_MAX_CIDLEN); + NODE_DEFINE_CONSTANT(constants, NGTCP2_MIN_CIDLEN); + NODE_DEFINE_CONSTANT(constants, NGTCP2_NO_ERROR); + NODE_DEFINE_CONSTANT(constants, AF_INET); + NODE_DEFINE_CONSTANT(constants, AF_INET6); + NODE_DEFINE_STRING_CONSTANT(constants, + NODE_STRINGIFY_HELPER(NGTCP2_ALPN_H3), + NGTCP2_ALPN_H3); + + target->Set(context, env->constants_string(), constants).FromJust(); +} + +} // namespace quic +} // namespace node + +NODE_MODULE_CONTEXT_AWARE_INTERNAL(quic, node::quic::Initialize) diff --git a/src/quic/node_quic_buffer-inl.h b/src/quic/node_quic_buffer-inl.h new file mode 100644 index 00000000000000..e03378a331154b --- /dev/null +++ b/src/quic/node_quic_buffer-inl.h @@ -0,0 +1,99 @@ +#ifndef SRC_QUIC_NODE_QUIC_BUFFER_INL_H_ +#define SRC_QUIC_NODE_QUIC_BUFFER_INL_H_ + +#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS + +#include "node_quic_buffer.h" +#include "node_bob-inl.h" +#include "util-inl.h" +#include "uv.h" + +#include + +namespace node { + +namespace quic { + +QuicBufferChunk::QuicBufferChunk(size_t len) + : data_buf_(len, 0), + buf_(uv_buf_init(reinterpret_cast(data_buf_.data()), len)), + length_(len), + done_called_(true) {} + +QuicBufferChunk::QuicBufferChunk(uv_buf_t buf, DoneCB done) + : buf_(buf), + length_(buf.len) { + if (done != nullptr) + done_ = std::move(done); +} + +QuicBufferChunk::~QuicBufferChunk() { + CHECK(done_called_); +} + +size_t QuicBufferChunk::Seek(size_t amount) { + amount = std::min(amount, remaining()); + buf_.base += amount; + buf_.len -= amount; + return amount; +} + +size_t QuicBufferChunk::Consume(size_t amount) { + amount = std::min(amount, length_); + length_ -= amount; + return amount; +} + +void QuicBufferChunk::Done(int status) { + if (done_called_) return; + done_called_ = true; + if (done_ != nullptr) + std::move(done_)(status); +} + +QuicBuffer::QuicBuffer(QuicBuffer&& src) noexcept + : head_(src.head_), + tail_(src.tail_), + ended_(src.ended_), + length_(src.length_) { + root_ = std::move(src.root_); + src.head_ = nullptr; + src.tail_ = nullptr; + src.length_ = 0; + src.remaining_ = 0; + src.ended_ = false; +} + + +QuicBuffer& QuicBuffer::operator=(QuicBuffer&& src) noexcept { + if (this == &src) return *this; + this->~QuicBuffer(); + return *new(this) QuicBuffer(std::move(src)); +} + +bool QuicBuffer::is_empty(uv_buf_t buf) { + DCHECK_IMPLIES(buf.base == nullptr, buf.len == 0); + return buf.len == 0; +} + +size_t QuicBuffer::Consume(size_t amount) { + return Consume(0, amount); +} + +size_t QuicBuffer::Cancel(int status) { + if (canceled_) return 0; + canceled_ = true; + size_t t = Consume(status, length()); + return t; +} + +void QuicBuffer::Push(uv_buf_t buf, DoneCB done) { + std::unique_ptr chunk = + std::make_unique(buf, done); + Push(std::move(chunk)); +} +} // namespace quic +} // namespace node + +#endif // NODE_WANT_INTERNALS +#endif // SRC_QUIC_NODE_QUIC_BUFFER_INL_H_ diff --git a/src/quic/node_quic_buffer.cc b/src/quic/node_quic_buffer.cc new file mode 100644 index 00000000000000..2cb9211994138d --- /dev/null +++ b/src/quic/node_quic_buffer.cc @@ -0,0 +1,159 @@ +#include "node_quic_buffer-inl.h" // NOLINT(build/include) +#include "node_bob-inl.h" +#include "util.h" +#include "uv.h" + +#include +#include +#include + +namespace node { +namespace quic { + +void QuicBufferChunk::MemoryInfo(MemoryTracker* tracker) const { + tracker->TrackField("buf", data_buf_); + tracker->TrackField("next", next_); +} + +size_t QuicBuffer::Push(uv_buf_t* bufs, size_t nbufs, DoneCB done) { + size_t len = 0; + if (nbufs == 0 || bufs == nullptr || is_empty(bufs[0])) { + done(0); + return 0; + } + size_t n = 0; + while (nbufs > 1) { + if (!is_empty(bufs[n])) { + Push(bufs[n]); + len += bufs[n].len; + } + n++; + nbufs--; + } + len += bufs[n].len; + Push(bufs[n], done); + return len; +} + +void QuicBuffer::Push(std::unique_ptr chunk) { + CHECK(!ended_); + length_ += chunk->remaining(); + remaining_ += chunk->remaining(); + if (!tail_) { + root_ = std::move(chunk); + head_ = tail_ = root_.get(); + } else { + tail_->next_ = std::move(chunk); + tail_ = tail_->next_.get(); + if (!head_) + head_ = tail_; + } +} + +size_t QuicBuffer::Seek(size_t amount) { + size_t len = 0; + while (head_ && amount > 0) { + size_t amt = head_->Seek(amount); + amount -= amt; + len += amt; + remaining_ -= amt; + if (head_->remaining()) + break; + head_ = head_->next_.get(); + } + return len; +} + +bool QuicBuffer::Pop(int status) { + if (!root_) + return false; + std::unique_ptr root(std::move(root_)); + root_ = std::move(root.get()->next_); + + if (head_ == root.get()) + head_ = root_.get(); + if (tail_ == root.get()) + tail_ = root_.get(); + + root->Done(status); + return true; +} + +size_t QuicBuffer::Consume(int status, size_t amount) { + size_t amt = std::min(amount, length_); + size_t len = 0; + while (root_ && amt > 0) { + auto root = root_.get(); + size_t consumed = root->Consume(amt); + len += consumed; + length_ -= consumed; + amt -= consumed; + if (root->length() > 0) + break; + Pop(status); + } + return len; +} + +void QuicBuffer::MemoryInfo(MemoryTracker* tracker) const { + tracker->TrackField("root", root_); +} + +int QuicBuffer::DoPull( + bob::Next next, + int options, + ngtcp2_vec* data, + size_t count, + size_t max_count_hint) { + size_t len = 0; + size_t numbytes = 0; + int status = bob::Status::STATUS_CONTINUE; + + // There's no data to read. + if (!remaining() || head_ == nullptr) { + status = is_ended() ? + bob::Status::STATUS_END : + bob::Status::STATUS_BLOCK; + std::move(next)(status, nullptr, 0, [](size_t len) {}); + return status; + } + + // Ensure that there's storage space. + MaybeStackBuffer vec; + if (data == nullptr || count == 0) { + vec.AllocateSufficientStorage(max_count_hint); + data = vec.out(); + } else { + max_count_hint = std::min(count, max_count_hint); + } + + // Build the list of buffers. + QuicBufferChunk* pos = head_; + while (pos != nullptr && len < max_count_hint) { + data[len].base = reinterpret_cast(pos->buf().base); + data[len].len = pos->buf().len; + numbytes += data[len].len; + len++; + pos = pos->next_.get(); + } + + // If the buffer is ended, and the number of bytes + // matches the total remaining and OPTIONS_END is + // used, set the status to STATUS_END. + if (is_ended() && + numbytes == remaining() && + options & bob::OPTIONS_END) + status = bob::Status::STATUS_END; + + // Pass the data back out to the caller. + std::move(next)( + status, + data, + len, + std::move([this](size_t len) { Seek(len); })); + + return status; +} + +} // namespace quic +} // namespace node diff --git a/src/quic/node_quic_buffer.h b/src/quic/node_quic_buffer.h new file mode 100644 index 00000000000000..17f59a7e759161 --- /dev/null +++ b/src/quic/node_quic_buffer.h @@ -0,0 +1,239 @@ +#ifndef SRC_QUIC_NODE_QUIC_BUFFER_H_ +#define SRC_QUIC_NODE_QUIC_BUFFER_H_ + +#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS + +#include "memory_tracker-inl.h" +#include "ngtcp2/ngtcp2.h" +#include "node.h" +#include "node_bob.h" +#include "node_internals.h" +#include "util.h" +#include "uv.h" + +#include + +namespace node { +namespace quic { + +class QuicBuffer; + +constexpr size_t kMaxVectorCount = 16; + +using DoneCB = std::function; + +// When data is sent over QUIC, we are required to retain it in memory +// until we receive an acknowledgement that it has been successfully +// acknowledged. The QuicBuffer object is what we use to handle that +// and track until it is acknowledged. To understand the QuicBuffer +// object itself, it is important to understand how ngtcp2 and nghttp3 +// handle data that is given to it to serialize into QUIC packets. +// +// An individual QUIC packet may contain multiple QUIC frames. Whenever +// we create a QUIC packet, we really have no idea what frames are going +// to be encoded or how much buffered handshake or stream data is going +// to be included within that QuicPacket. If there is buffered data +// available for a stream, we provide an array of pointers to that data +// and an indication about how much data is available, then we leave it +// entirely up to ngtcp2 and nghttp3 to determine how much of the data +// to encode into the QUIC packet. It is only *after* the QUIC packet +// is encoded that we can know how much was actually written. +// +// Once written to a QUIC Packet, we have to keep the data in memory +// until an acknowledgement is received. In QUIC, acknowledgements are +// received per range of packets. +// +// QuicBuffer is complicated because it needs to be able to accomplish +// three things: (a) buffering uv_buf_t instances passed down from +// JavaScript without memcpy and keeping track of the Write callback +// associated with each, (b) tracking what data has already been +// encoded in a QUIC packet and what data is remaining to be read, and +// (c) tracking which data has been acknowledged and which hasn't. +// QuicBuffer is further complicated by design quirks and limitations +// of the StreamBase API and how it interacts with the JavaScript side. +// +// QuicBuffer is essentially a linked list of QuicBufferChunk instances. +// A single QuicBufferChunk wraps a single non-zero-length uv_buf_t. +// When the QuicBufferChunk is created, we capture the total length +// of the buffer and the total number of bytes remaining to be sent. +// Initially, these numbers are identical. +// +// When data is encoded into a QuicPacket, we advance the QuicBufferChunk's +// remaining-to-be-read by the number of bytes actually encoded. If there +// are no more bytes remaining to be encoded, we move to the next chunk +// in the linked list. +// +// When an acknowledgement is received, we decrement the QuicBufferChunk's +// length by the number of acknowledged bytes. Once the unacknowledged +// length reaches 0, we invoke the callback function associated with the +// QuicBufferChunk (if any). +// +// QuicStream is a StreamBase implementation. For every DoWrite call, +// it receives one or more uv_buf_t instances in a single batch associated +// with a single write callback. For each uv_buf_t DoWrite gets, a +// corresponding QuicBufferChunk is added to the QuicBuffer, with the +// callback associated with the final chunk added to the list. + + +// A QuicBufferChunk contains the actual buffered data +// along with a callback to be called when the data has +// been consumed. +// +// Any given chunk as a remaining-to-be-acknowledged length (length()) and a +// remaining-to-be-read-length (remaining()). The former tracks the number +// of bytes that have yet to be acknowledged by the QUIC peer. Once the +// remaining-to-be-acknowledged length reaches zero, the done callback +// associated with the QuicBufferChunk can be called and the QuicBufferChunk +// can be discarded. The remaining-to-be-read length is adjusted as data is +// serialized into QUIC packets and sent. +// The remaining-to-be-acknowledged length is adjusted using consume(), +// while the remaining-to-be-ead length is adjusted using seek(). +class QuicBufferChunk : public MemoryRetainer { + public: + // Default non-op done handler. + static void default_done(int status) {} + + // In this variant, the QuicBufferChunk owns the underlying + // data storage within a vector. The data will be + // freed when the QuicBufferChunk is destroyed. + inline explicit QuicBufferChunk(size_t len); + + // In this variant, the QuicBufferChunk only maintains a + // pointer to the underlying data buffer. The QuicBufferChunk + // does not take ownership of the buffer. The done callback + // is invoked to let the caller know when the chunk is no + // longer being used. + inline QuicBufferChunk(uv_buf_t buf_, DoneCB done_); + + inline ~QuicBufferChunk() override; + + // Invokes the done callback associated with the QuicBufferChunk. + inline void Done(int status); + + // length() provides the remaining-to-be-acknowledged length. + // The QuicBufferChunk must be retained in memory while this + // count is greater than zero. The length is adjusted by + // calling Consume(); + inline size_t length() const { return length_; } + + // remaining() provides the remaining-to-be-read length number of bytes. + // The length is adjusted by calling Seek() + inline size_t remaining() const { return buf_.len; } + + // Consumes (acknowledges) the given number of bytes. If amount + // is greater than length(), only length() bytes are consumed. + // Returns the actual number of bytes consumed. + inline size_t Consume(size_t amount); + + // Seeks (reads) the given number of bytes. If amount is greater + // than remaining(), only remaining() bytes are read. Returns + // the actual number of bytes read. + inline size_t Seek(size_t amount); + + uint8_t* out() { return reinterpret_cast(buf_.base); } + uv_buf_t buf() { return buf_; } + const uv_buf_t buf() const { return buf_; } + + void MemoryInfo(MemoryTracker* tracker) const override; + SET_MEMORY_INFO_NAME(QuicBufferChunk) + SET_SELF_SIZE(QuicBufferChunk) + + private: + std::vector data_buf_; + uv_buf_t buf_; + DoneCB done_ = default_done; + size_t length_ = 0; + bool done_called_ = false; + std::unique_ptr next_; + + friend class QuicBuffer; +}; + +class QuicBuffer : public bob::SourceImpl, + public MemoryRetainer { + public: + QuicBuffer() = default; + + inline QuicBuffer(QuicBuffer&& src) noexcept; + inline QuicBuffer& operator=(QuicBuffer&& src) noexcept; + + ~QuicBuffer() override { + Cancel(); // Cancel the remaining data + CHECK_EQ(length_, 0); + } + + // Marks the QuicBuffer as having ended, preventing new QuicBufferChunk + // instances from being appended to the linked list and allowing the + // Pull operation to know when to signal that the flow of data is + // completed. + void End() { ended_ = true; } + bool is_ended() const { return ended_; } + + // Push one or more uv_buf_t instances into the buffer. + // the DoneCB callback will be invoked when the last + // uv_buf_t in the bufs array is consumed and popped out + // of the internal linked list. Ownership of the uv_buf_t + // remains with the caller. + size_t Push( + uv_buf_t* bufs, + size_t nbufs, + DoneCB done = QuicBufferChunk::default_done); + + // Pushes a single QuicBufferChunk into the linked list + void Push(std::unique_ptr chunk); + + // Consume the given number of bytes within the buffer. If amount + // is greater than length(), length() bytes are consumed. Returns + // the actual number of bytes consumed. + inline size_t Consume(size_t amount); + + // Cancels the remaining bytes within the buffer. + inline size_t Cancel(int status = UV_ECANCELED); + + // Seeks (reads) the given number of bytes. If amount is greater + // than remaining(), seeks remaining() bytes. Returns the actual + // number of bytes read. + size_t Seek(size_t amount); + + // The total number of unacknowledged bytes remaining. + size_t length() const { return length_; } + + // The total number of unread bytes remaining. + size_t remaining() const { return remaining_; } + + void MemoryInfo(MemoryTracker* tracker) const override; + SET_MEMORY_INFO_NAME(QuicBuffer); + SET_SELF_SIZE(QuicBuffer); + + protected: + int DoPull( + bob::Next next, + int options, + ngtcp2_vec* data, + size_t count, + size_t max_count_hint) override; + + private: + inline static bool is_empty(uv_buf_t buf); + size_t Consume(int status, size_t amount); + bool Pop(int status = 0); + inline void Push(uv_buf_t buf, DoneCB done = nullptr); + + std::unique_ptr root_; + QuicBufferChunk* head_ = nullptr; // Current Read Position + QuicBufferChunk* tail_ = nullptr; // Current Write Position + + bool canceled_ = false; + bool ended_ = false; + size_t length_ = 0; + size_t remaining_ = 0; + + friend class QuicBufferChunk; +}; + +} // namespace quic +} // namespace node + +#endif // NODE_WANT_INTERNALS + +#endif // SRC_QUIC_NODE_QUIC_BUFFER_H_ diff --git a/src/quic/node_quic_crypto.cc b/src/quic/node_quic_crypto.cc new file mode 100644 index 00000000000000..4b4ec901001f40 --- /dev/null +++ b/src/quic/node_quic_crypto.cc @@ -0,0 +1,961 @@ +#include "node_quic_crypto.h" +#include "env-inl.h" +#include "node_crypto.h" +#include "node_crypto_common.h" +#include "node_process.h" +#include "node_quic_session-inl.h" +#include "node_quic_util-inl.h" +#include "node_sockaddr-inl.h" +#include "node_url.h" +#include "string_bytes.h" +#include "v8.h" +#include "util-inl.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace node { + +using crypto::EntropySource; +using v8::Local; +using v8::String; +using v8::Value; + +namespace quic { + +bool SessionTicketAppData::Set(const uint8_t* data, size_t len) { + if (set_) + return false; + set_ = true; + SSL_SESSION_set1_ticket_appdata(session_, data, len); + return set_; +} + +bool SessionTicketAppData::Get(uint8_t** data, size_t* len) const { + return SSL_SESSION_get0_ticket_appdata( + session_, + reinterpret_cast(data), + len) == 1; +} + +namespace { +constexpr int kCryptoTokenKeylen = 32; +constexpr int kCryptoTokenIvlen = 32; + +// Used solely to derive the keys used to generate and +// validate retry tokens. The implementation of this is +// Node.js specific. We use the current implementation +// because it is simple. +bool DeriveTokenKey( + uint8_t* token_key, + uint8_t* token_iv, + const uint8_t* rand_data, + size_t rand_datalen, + const ngtcp2_crypto_ctx& ctx, + const uint8_t* token_secret) { + static constexpr int kCryptoTokenSecretlen = 32; + uint8_t secret[kCryptoTokenSecretlen]; + + return + NGTCP2_OK(ngtcp2_crypto_hkdf_extract( + secret, + &ctx.md, + token_secret, + kTokenSecretLen, + rand_data, + rand_datalen)) && + NGTCP2_OK(ngtcp2_crypto_derive_packet_protection_key( + token_key, + token_iv, + nullptr, + &ctx.aead, + &ctx.md, + secret, + kCryptoTokenSecretlen)); +} + +// Retry tokens are generated only by QUIC servers. They +// are opaque to QUIC clients and must not be guessable by +// on- or off-path attackers. A QUIC server sends a RETRY +// token as a way of initiating explicit path validation +// with a client in response to an initial QUIC packet. +// The client, upon receiving a RETRY, must abandon the +// initial connection attempt and try again, including the +// received retry token in the new initial packet sent to +// the server. If the server is performing explicit +// valiation, it will look for the presence of the retry +// token and validate it if found. The internal structure +// of the retry token must be meaningful to the server, +// and the server must be able to validate the token without +// relying on any state left over from the previous connection +// attempt. The implementation here is entirely Node.js +// specific. +// +// The token is generated by: +// 1. Appending the raw bytes of given socket address, the current +// timestamp, and the original CID together into a single byte +// array. +// 2. Generating a block of random data that is used together with +// the token secret to cryptographically derive an encryption key. +// 3. Encrypting the byte array from step 1 using the encryption key +// from step 2. +// 4. Appending random data generated in step 2 to the token. +// +// The token secret must be kept secret on the QUIC server that +// generated the retry. When multiple QUIC servers are used in a +// cluster, it cannot be guaranteed that the same QUIC server +// instance will receive the subsequent new Initial packet. Therefore, +// all QUIC servers in the cluster should either share or be aware +// of the same token secret or a mechanism needs to be implemented +// to ensure that subsequent packets are routed to the same QUIC +// server instance. +// +// A malicious peer could attempt to guess the token secret by +// sending a large number specially crafted RETRY-eliciting packets +// to a server then analyzing the resulting retry tokens. To reduce +// the possibility of such attacks, the current implementation of +// QuicSocket generates the token secret randomly for each instance, +// and the number of RETRY responses sent to a given remote address +// should be limited. Such attacks should be of little actual value +// in most cases. +bool GenerateRetryToken( + uint8_t* token, + size_t* tokenlen, + const SocketAddress& addr, + const QuicCID& ocid, + const uint8_t* token_secret) { + std::array plaintext; + uint8_t rand_data[kTokenRandLen]; + uint8_t token_key[kCryptoTokenKeylen]; + uint8_t token_iv[kCryptoTokenIvlen]; + + ngtcp2_crypto_ctx ctx; + ngtcp2_crypto_ctx_initial(&ctx); + size_t ivlen = ngtcp2_crypto_packet_protection_ivlen(&ctx.aead); + uint64_t now = uv_hrtime(); + + auto p = std::begin(plaintext); + p = std::copy_n(addr.raw(), addr.length(), p); + p = std::copy_n(reinterpret_cast(&now), sizeof(uint64_t), p); + p = std::copy_n(ocid->data, ocid->datalen, p); + + EntropySource(rand_data, kTokenRandLen); + + if (!DeriveTokenKey( + token_key, + token_iv, + rand_data, + kTokenRandLen, + ctx, + token_secret)) { + return false; + } + + size_t plaintextlen = std::distance(std::begin(plaintext), p); + if (NGTCP2_ERR(ngtcp2_crypto_encrypt( + token, + &ctx.aead, + plaintext.data(), + plaintextlen, + token_key, + token_iv, + ivlen, + addr.raw(), + addr.length()))) { + return false; + } + + *tokenlen = plaintextlen + ngtcp2_crypto_aead_taglen(&ctx.aead); + memcpy(token + (*tokenlen), rand_data, kTokenRandLen); + *tokenlen += kTokenRandLen; + return true; +} +} // namespace + +// A stateless reset token is used when a QUIC endpoint receives a +// QUIC packet with a short header but the associated connection ID +// cannot be matched to any known QuicSession. In such cases, the +// receiver may choose to send a subtle opaque indication to the +// sending peer that state for the QuicSession has apparently been +// lost. For any on- or off- path attacker, a stateless reset packet +// resembles any other QUIC packet with a short header. In order to +// be successfully handled as a stateless reset, the peer must have +// already seen a reset token issued to it associated with the given +// CID. The token itself is opaque to the peer that receives is but +// must be possible to statelessly recreate by the peer that +// originally created it. The actual implementation is Node.js +// specific but we currently defer to a utility function provided +// by ngtcp2. +bool GenerateResetToken( + uint8_t* token, + const uint8_t* secret, + const QuicCID& cid) { + ngtcp2_crypto_ctx ctx; + ngtcp2_crypto_ctx_initial(&ctx); + return NGTCP2_OK(ngtcp2_crypto_generate_stateless_reset_token( + token, + &ctx.md, + secret, + NGTCP2_STATELESS_RESET_TOKENLEN, + cid.cid())); +} + +// Generates a RETRY packet. See the notes for GenerateRetryToken for details. +std::unique_ptr GenerateRetryPacket( + const uint8_t* token_secret, + const QuicCID& dcid, + const QuicCID& scid, + const SocketAddress& local_addr, + const SocketAddress& remote_addr) { + + uint8_t token[256]; + size_t tokenlen = sizeof(token); + + if (!GenerateRetryToken(token, &tokenlen, remote_addr, dcid, token_secret)) + return {}; + + QuicCID cid; + EntropySource(cid.data(), kScidLen); + cid.set_length(kScidLen); + + size_t pktlen = tokenlen + (2 * NGTCP2_MAX_CIDLEN) + scid.length() + 8; + CHECK_LE(pktlen, NGTCP2_MAX_PKT_SIZE); + + auto packet = QuicPacket::Create("retry", pktlen); + ssize_t nwrite = + ngtcp2_crypto_write_retry( + packet->data(), + NGTCP2_MAX_PKTLEN_IPV4, + scid.cid(), + cid.cid(), + dcid.cid(), + token, + tokenlen); + if (nwrite <= 0) + return {}; + packet->set_length(nwrite); + return packet; +} + +// Validates a retry token included in the given header. This will return +// true if the token cannot be validated, false otherwise. A token is +// valid if it can be successfully decrypted using the key derived from +// random data embedded in the token, the structure of the token matches +// that generated by the GenerateRetryToken function, and the token was +// not generated earlier than now - verification_expiration. If validation +// is successful, ocid will be updated to the original connection ID encoded +// in the encrypted token. +bool InvalidRetryToken( + const ngtcp2_pkt_hd& hd, + const SocketAddress& addr, + QuicCID* ocid, + const uint8_t* token_secret, + uint64_t verification_expiration) { + + if (hd.tokenlen < kTokenRandLen) + return true; + + ngtcp2_crypto_ctx ctx; + ngtcp2_crypto_ctx_initial(&ctx); + + size_t ivlen = ngtcp2_crypto_packet_protection_ivlen(&ctx.aead); + + size_t ciphertextlen = hd.tokenlen - kTokenRandLen; + const uint8_t* ciphertext = hd.token; + const uint8_t* rand_data = hd.token + ciphertextlen; + + uint8_t token_key[kCryptoTokenKeylen]; + uint8_t token_iv[kCryptoTokenIvlen]; + + if (!DeriveTokenKey( + token_key, + token_iv, + rand_data, + kTokenRandLen, + ctx, + token_secret)) { + return true; + } + + uint8_t plaintext[4096]; + + if (NGTCP2_ERR(ngtcp2_crypto_decrypt( + plaintext, + &ctx.aead, + ciphertext, + ciphertextlen, + token_key, + token_iv, + ivlen, + addr.raw(), + addr.length()))) { + return true; + } + + size_t plaintextlen = ciphertextlen - ngtcp2_crypto_aead_taglen(&ctx.aead); + if (plaintextlen < addr.length() + sizeof(uint64_t)) + return true; + + ssize_t cil = plaintextlen - addr.length() - sizeof(uint64_t); + if ((cil != 0 && (cil < NGTCP2_MIN_CIDLEN || cil > NGTCP2_MAX_CIDLEN)) || + memcmp(plaintext, addr.raw(), addr.length()) != 0) { + return true; + } + + uint64_t t; + memcpy(&t, plaintext + addr.length(), sizeof(uint64_t)); + + // 10-second window by default, but configurable for each + // QuicSocket instance with a MIN_RETRYTOKEN_EXPIRATION second + // minimum and a MAX_RETRYTOKEN_EXPIRATION second maximum. + if (t + verification_expiration * NGTCP2_SECONDS < uv_hrtime()) + return true; + + ngtcp2_cid_init( + ocid->cid(), + plaintext + addr.length() + sizeof(uint64_t), + cil); + + return false; +} + +namespace { + +bool SplitHostname( + const char* hostname, + std::vector* parts, + const char delim = '.') { + static std::string check_str = + "\x21\x22\x23\x24\x25\x26\x27\x28\x29\x2A\x2B\x2C\x2D\x2E\x2F\x30" + "\x31\x32\x33\x34\x35\x36\x37\x38\x39\x3A\x3B\x3C\x3D\x3E\x3F\x40" + "\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4A\x4B\x4C\x4D\x4E\x4F\x50" + "\x51\x52\x53\x54\x55\x56\x57\x58\x59\x5A\x5B\x5C\x5D\x5E\x5F\x60" + "\x61\x62\x63\x64\x65\x66\x67\x68\x69\x6A\x6B\x6C\x6D\x6E\x6F\x70" + "\x71\x72\x73\x74\x75\x76\x77\x78\x79\x7A\x7B\x7C\x7D\x7E\x7F"; + + std::stringstream str(hostname); + std::string part; + while (getline(str, part, delim)) { + // if (part.length() == 0 || + // part.find_first_not_of(check_str) != std::string::npos) { + // return false; + // } + for (size_t n = 0; n < part.length(); n++) { + if (part[n] >= 'A' && part[n] <= 'Z') + part[n] = (part[n] | 0x20); // Lower case the letter + if (check_str.find(part[n]) == std::string::npos) + return false; + } + parts->push_back(part); + } + return true; +} + +bool CheckCertNames( + const std::vector& host_parts, + const std::string& name, + bool use_wildcard = true) { + + if (name.length() == 0) + return false; + + std::vector name_parts; + if (!SplitHostname(name.c_str(), &name_parts)) + return false; + + if (name_parts.size() != host_parts.size()) + return false; + + for (size_t n = host_parts.size() - 1; n > 0; --n) { + if (host_parts[n] != name_parts[n]) + return false; + } + + if (name_parts[0].find("*") == std::string::npos || + name_parts[0].find("xn--") != std::string::npos) { + return host_parts[0] == name_parts[0]; + } + + if (!use_wildcard) + return false; + + std::vector sub_parts; + SplitHostname(name_parts[0].c_str(), &sub_parts, '*'); + + if (sub_parts.size() > 2) + return false; + + if (name_parts.size() <= 2) + return false; + + std::string prefix; + std::string suffix; + if (sub_parts.size() == 2) { + prefix = sub_parts[0]; + suffix = sub_parts[1]; + } else { + prefix = ""; + suffix = sub_parts[0]; + } + + if (prefix.length() + suffix.length() > host_parts[0].length()) + return false; + + if (host_parts[0].compare(0, prefix.length(), prefix)) + return false; + + if (host_parts[0].compare( + host_parts[0].length() - suffix.length(), + suffix.length(), suffix)) { + return false; + } + + return true; +} + +} // namespace + +int VerifyHostnameIdentity( + const crypto::SSLPointer& ssl, + const char* hostname) { + int err = X509_V_ERR_HOSTNAME_MISMATCH; + crypto::X509Pointer cert(SSL_get_peer_certificate(ssl.get())); + if (!cert) + return err; + + // There are several pieces of information we need from the cert at this point + // 1. The Subject (if it exists) + // 2. The collection of Alt Names (if it exists) + // + // The certificate may have many Alt Names. We only care about the ones that + // are prefixed with 'DNS:', 'URI:', or 'IP Address:'. We might check + // additional ones later but we'll start with these. + // + // Ideally, we'd be able to *just* use OpenSSL's built in name checking for + // this (SSL_set1_host and X509_check_host) but it does not appear to do + // checking on URI or IP Address Alt names, which is unfortunate. We need + // both of those to retain compatibility with the peer identity verification + // Node.js already does elsewhere. At the very least, we'll use + // X509_check_host here first as a first step. If it is successful, awesome, + // there's nothing else for us to do. Return and be happy! + if (X509_check_host( + cert.get(), + hostname, + strlen(hostname), + X509_CHECK_FLAG_ALWAYS_CHECK_SUBJECT | + X509_CHECK_FLAG_MULTI_LABEL_WILDCARDS | + X509_CHECK_FLAG_SINGLE_LABEL_SUBDOMAINS, + nullptr) > 0) { + return 0; + } + + if (X509_check_ip_asc( + cert.get(), + hostname, + X509_CHECK_FLAG_ALWAYS_CHECK_SUBJECT) > 0) { + return 0; + } + + // If we've made it this far, then we have to perform a more check + return VerifyHostnameIdentity( + hostname, + crypto::GetCertificateCN(cert.get()), + crypto::GetCertificateAltNames(cert.get())); +} + +int VerifyHostnameIdentity( + const char* hostname, + const std::string& cert_cn, + const std::unordered_multimap& altnames) { + + int err = X509_V_ERR_HOSTNAME_MISMATCH; + + // 1. If the hostname is an IP address (v4 or v6), the certificate is valid + // if and only if there is an 'IP Address:' alt name specifying the same + // IP address. The IP address must be canonicalized to ensure a proper + // check. It's possible that the X509_check_ip_asc covers this. If so, + // we can remove this check. + + if (SocketAddress::is_numeric_host(hostname)) { + auto ips = altnames.equal_range("ip"); + for (auto ip = ips.first; ip != ips.second; ++ip) { + if (ip->second.compare(hostname) == 0) { + // Success! + return 0; + } + } + // No match, and since the hostname is an IP address, skip any + // further checks + return err; + } + + auto dns_names = altnames.equal_range("dns"); + auto uri_names = altnames.equal_range("uri"); + + size_t dns_count = std::distance(dns_names.first, dns_names.second); + size_t uri_count = std::distance(uri_names.first, uri_names.second); + + std::vector host_parts; + SplitHostname(hostname, &host_parts); + + // 2. If there no 'DNS:' or 'URI:' Alt names, if the certificate has a + // Subject, then we need to extract the CN field from the Subject. and + // check that the hostname matches the CN, taking into consideration + // the possibility of a wildcard in the CN. If there is a match, congrats, + // we have a valid certificate. Return and be happy. + + if (dns_count == 0 && uri_count == 0) { + if (cert_cn.length() > 0 && CheckCertNames(host_parts, cert_cn)) + return 0; + // No match, and since there are no dns or uri entries, return + return err; + } + + // 3. If, however, there are 'DNS:' and 'URI:' Alt names, things become more + // complicated. Essentially, we need to iterate through each 'DNS:' and + // 'URI:' Alt name to find one that matches. The 'DNS:' Alt names are + // relatively simple but may include wildcards. The 'URI:' Alt names + // require the name to be parsed as a URL, then extract the hostname from + // the URL, which is then checked against the hostname. If you find a + // match, yay! Return and be happy. (Note, it's possible that the 'DNS:' + // check in this step is redundant to the X509_check_host check. If so, + // we can simplify by removing those checks here.) + + // First, let's check dns names + for (auto name = dns_names.first; name != dns_names.second; ++name) { + if (name->first.length() > 0 && + CheckCertNames(host_parts, name->second)) { + return 0; + } + } + + // Then, check uri names + for (auto name = uri_names.first; name != uri_names.second; ++name) { + if (name->first.length() > 0 && + CheckCertNames(host_parts, name->second, false)) { + return 0; + } + } + + // 4. Failing all of the previous checks, we assume the certificate is + // invalid for an unspecified reason. + return err; +} + +// Get the ALPN protocol identifier that was negotiated for the session +Local GetALPNProtocol(const QuicSession& session) { + QuicCryptoContext* ctx = session.crypto_context(); + Environment* env = session.env(); + std::string alpn = ctx->selected_alpn(); + if (alpn == NGTCP2_ALPN_H3 + 1) { + return env->quic_alpn_string(); + } else { + return ToV8Value( + env->context(), + alpn, + env->isolate()).FromMaybe(Local()); + } +} + +namespace { +int CertCB(SSL* ssl, void* arg) { + QuicSession* session = static_cast(arg); + return SSL_get_tlsext_status_type(ssl) == TLSEXT_STATUSTYPE_ocsp ? + session->crypto_context()->OnOCSP() : 1; +} + +void Keylog_CB(const SSL* ssl, const char* line) { + QuicSession* session = static_cast(SSL_get_app_data(ssl)); + session->crypto_context()->Keylog(line); +} + +int Client_Hello_CB( + SSL* ssl, + int* tls_alert, + void* arg) { + QuicSession* session = static_cast(SSL_get_app_data(ssl)); + int ret = session->crypto_context()->OnClientHello(); + switch (ret) { + case 0: + return 1; + case -1: + return -1; + default: + *tls_alert = ret; + return 0; + } +} + +int AlpnSelection( + SSL* ssl, + const unsigned char** out, + unsigned char* outlen, + const unsigned char* in, + unsigned int inlen, + void* arg) { + QuicSession* session = static_cast(SSL_get_app_data(ssl)); + + unsigned char* tmp; + + // The QuicServerSession supports exactly one ALPN identifier. If that does + // not match any of the ALPN identifiers provided in the client request, + // then we fail here. Note that this will not fail the TLS handshake, so + // we have to check later if the ALPN matches the expected identifier or not. + if (SSL_select_next_proto( + &tmp, + outlen, + reinterpret_cast(session->alpn().c_str()), + session->alpn().length(), + in, + inlen) == OPENSSL_NPN_NO_OVERLAP) { + return SSL_TLSEXT_ERR_NOACK; + } + *out = tmp; + return SSL_TLSEXT_ERR_OK; +} + +int AllowEarlyDataCB(SSL* ssl, void* arg) { + QuicSession* session = static_cast(SSL_get_app_data(ssl)); + return session->allow_early_data() ? 1 : 0; +} + +int TLS_Status_Callback(SSL* ssl, void* arg) { + QuicSession* session = static_cast(SSL_get_app_data(ssl)); + return session->crypto_context()->OnTLSStatus(); +} + +int New_Session_Callback(SSL* ssl, SSL_SESSION* session) { + QuicSession* s = static_cast(SSL_get_app_data(ssl)); + return s->set_session(session); +} + +int GenerateSessionTicket(SSL* ssl, void* arg) { + QuicSession* s = static_cast(SSL_get_app_data(ssl)); + SessionTicketAppData app_data(SSL_get_session(ssl)); + s->SetSessionTicketAppData(app_data); + return 1; +} + +SSL_TICKET_RETURN DecryptSessionTicket( + SSL* ssl, + SSL_SESSION* session, + const unsigned char* keyname, + size_t keyname_len, + SSL_TICKET_STATUS status, + void* arg) { + QuicSession* s = static_cast(SSL_get_app_data(ssl)); + SessionTicketAppData::Flag flag = SessionTicketAppData::Flag::STATUS_NONE; + switch (status) { + default: + return SSL_TICKET_RETURN_IGNORE; + case SSL_TICKET_EMPTY: + // Fall through + case SSL_TICKET_NO_DECRYPT: + return SSL_TICKET_RETURN_IGNORE_RENEW; + case SSL_TICKET_SUCCESS_RENEW: + flag = SessionTicketAppData::Flag::STATUS_RENEW; + // Fall through + case SSL_TICKET_SUCCESS: + SessionTicketAppData app_data(session); + switch (s->GetSessionTicketAppData(app_data, flag)) { + default: + return SSL_TICKET_RETURN_IGNORE; + case SessionTicketAppData::Status::TICKET_IGNORE: + return SSL_TICKET_RETURN_IGNORE; + case SessionTicketAppData::Status::TICKET_IGNORE_RENEW: + return SSL_TICKET_RETURN_IGNORE_RENEW; + case SessionTicketAppData::Status::TICKET_USE: + return SSL_TICKET_RETURN_USE; + case SessionTicketAppData::Status::TICKET_USE_RENEW: + return SSL_TICKET_RETURN_USE_RENEW; + } + } +} + +int SetEncryptionSecrets( + SSL* ssl, + OSSL_ENCRYPTION_LEVEL ossl_level, + const uint8_t* read_secret, + const uint8_t* write_secret, + size_t secret_len) { + QuicSession* session = static_cast(SSL_get_app_data(ssl)); + return session->crypto_context()->OnSecrets( + from_ossl_level(ossl_level), + read_secret, + write_secret, + secret_len) ? 1 : 0; +} + +int AddHandshakeData( + SSL* ssl, + OSSL_ENCRYPTION_LEVEL ossl_level, + const uint8_t* data, + size_t len) { + QuicSession* session = static_cast(SSL_get_app_data(ssl)); + session->crypto_context()->WriteHandshake( + from_ossl_level(ossl_level), + data, + len); + return 1; +} + +int FlushFlight(SSL* ssl) { return 1; } + +int SendAlert( + SSL* ssl, + enum ssl_encryption_level_t level, + uint8_t alert) { + QuicSession* session = static_cast(SSL_get_app_data(ssl)); + session->crypto_context()->set_tls_alert(alert); + return 1; +} + +bool SetTransportParams(QuicSession* session, const crypto::SSLPointer& ssl) { + ngtcp2_transport_params params; + ngtcp2_conn_get_local_transport_params(session->connection(), ¶ms); + uint8_t buf[512]; + ssize_t nwrite = ngtcp2_encode_transport_params( + buf, + arraysize(buf), + NGTCP2_TRANSPORT_PARAMS_TYPE_ENCRYPTED_EXTENSIONS, + ¶ms); + return nwrite >= 0 && + SSL_set_quic_transport_params(ssl.get(), buf, nwrite) == 1; +} + +SSL_QUIC_METHOD quic_method = SSL_QUIC_METHOD{ + SetEncryptionSecrets, + AddHandshakeData, + FlushFlight, + SendAlert +}; + +void SetHostname(const crypto::SSLPointer& ssl, const std::string& hostname) { + // If the hostname is an IP address, use an empty string + // as the hostname instead. + if (SocketAddress::is_numeric_host(hostname.c_str())) { + SSL_set_tlsext_host_name(ssl.get(), ""); + } else { + SSL_set_tlsext_host_name(ssl.get(), hostname.c_str()); + } +} + +} // namespace + +void InitializeTLS(QuicSession* session, const crypto::SSLPointer& ssl) { + QuicCryptoContext* ctx = session->crypto_context(); + Environment* env = session->env(); + QuicState* quic_state = session->quic_state(); + + SSL_set_app_data(ssl.get(), session); + SSL_set_cert_cb(ssl.get(), CertCB, + const_cast(reinterpret_cast(session))); + SSL_set_verify(ssl.get(), SSL_VERIFY_NONE, crypto::VerifyCallback); + + // Enable tracing if the `--trace-tls` command line flag is used. + if (env->options()->trace_tls) { + ctx->EnableTrace(); + if (quic_state->warn_trace_tls) { + quic_state->warn_trace_tls = false; + ProcessEmitWarning(env, + "Enabling --trace-tls can expose sensitive data " + "in the resulting log"); + } + } + + switch (ctx->side()) { + case NGTCP2_CRYPTO_SIDE_CLIENT: { + SSL_set_connect_state(ssl.get()); + crypto::SetALPN(ssl, session->alpn()); + SetHostname(ssl, session->hostname()); + if (ctx->is_option_set(QUICCLIENTSESSION_OPTION_REQUEST_OCSP)) + SSL_set_tlsext_status_type(ssl.get(), TLSEXT_STATUSTYPE_ocsp); + break; + } + case NGTCP2_CRYPTO_SIDE_SERVER: { + SSL_set_accept_state(ssl.get()); + if (ctx->is_option_set(QUICSERVERSESSION_OPTION_REQUEST_CERT)) { + int verify_mode = SSL_VERIFY_PEER; + if (ctx->is_option_set(QUICSERVERSESSION_OPTION_REJECT_UNAUTHORIZED)) + verify_mode |= SSL_VERIFY_FAIL_IF_NO_PEER_CERT; + SSL_set_verify(ssl.get(), verify_mode, crypto::VerifyCallback); + } + break; + } + default: + UNREACHABLE(); + } + + SetTransportParams(session, ssl); +} + +void InitializeSecureContext( + BaseObjectPtr sc, + bool early_data, + ngtcp2_crypto_side side) { + // TODO(@jasnell): Using a static value for this at the moment but + // we need to determine if a non-static or per-session value is better. + constexpr static unsigned char session_id_ctx[] = "node.js quic server"; + switch (side) { + case NGTCP2_CRYPTO_SIDE_SERVER: + SSL_CTX_set_options( + **sc, + (SSL_OP_ALL & ~SSL_OP_DONT_INSERT_EMPTY_FRAGMENTS) | + SSL_OP_SINGLE_ECDH_USE | + SSL_OP_CIPHER_SERVER_PREFERENCE | + SSL_OP_NO_ANTI_REPLAY); + + SSL_CTX_set_mode(**sc, SSL_MODE_RELEASE_BUFFERS); + + SSL_CTX_set_alpn_select_cb(**sc, AlpnSelection, nullptr); + SSL_CTX_set_client_hello_cb(**sc, Client_Hello_CB, nullptr); + + SSL_CTX_set_session_ticket_cb( + **sc, + GenerateSessionTicket, + DecryptSessionTicket, + nullptr); + + if (early_data) { + SSL_CTX_set_max_early_data(**sc, 0xffffffff); + SSL_CTX_set_allow_early_data_cb(**sc, AllowEarlyDataCB, nullptr); + } + + SSL_CTX_set_session_id_context( + **sc, + session_id_ctx, + sizeof(session_id_ctx) - 1); + break; + case NGTCP2_CRYPTO_SIDE_CLIENT: + SSL_CTX_set_session_cache_mode( + **sc, + SSL_SESS_CACHE_CLIENT | + SSL_SESS_CACHE_NO_INTERNAL_STORE); + SSL_CTX_sess_set_new_cb(**sc, New_Session_Callback); + break; + default: + UNREACHABLE(); + } + SSL_CTX_set_min_proto_version(**sc, TLS1_3_VERSION); + SSL_CTX_set_max_proto_version(**sc, TLS1_3_VERSION); + SSL_CTX_set_default_verify_paths(**sc); + SSL_CTX_set_tlsext_status_cb(**sc, TLS_Status_Callback); + SSL_CTX_set_keylog_callback(**sc, Keylog_CB); + SSL_CTX_set_tlsext_status_arg(**sc, nullptr); + SSL_CTX_set_quic_method(**sc, &quic_method); +} + +bool DeriveAndInstallInitialKey( + const QuicSession& session, + const QuicCID& dcid) { + uint8_t initial_secret[NGTCP2_CRYPTO_INITIAL_SECRETLEN]; + uint8_t rx_secret[NGTCP2_CRYPTO_INITIAL_SECRETLEN]; + uint8_t tx_secret[NGTCP2_CRYPTO_INITIAL_SECRETLEN]; + uint8_t rx_key[NGTCP2_CRYPTO_INITIAL_KEYLEN]; + uint8_t tx_key[NGTCP2_CRYPTO_INITIAL_KEYLEN]; + uint8_t rx_hp[NGTCP2_CRYPTO_INITIAL_KEYLEN]; + uint8_t tx_hp[NGTCP2_CRYPTO_INITIAL_KEYLEN]; + uint8_t rx_iv[NGTCP2_CRYPTO_INITIAL_IVLEN]; + uint8_t tx_iv[NGTCP2_CRYPTO_INITIAL_IVLEN]; + return NGTCP2_OK(ngtcp2_crypto_derive_and_install_initial_key( + session.connection(), + rx_secret, + tx_secret, + initial_secret, + rx_key, + rx_iv, + rx_hp, + tx_key, + tx_iv, + tx_hp, + dcid.cid(), + session.crypto_context()->side())); +} + +ngtcp2_crypto_level from_ossl_level(OSSL_ENCRYPTION_LEVEL ossl_level) { + switch (ossl_level) { + case ssl_encryption_initial: + return NGTCP2_CRYPTO_LEVEL_INITIAL; + case ssl_encryption_early_data: + return NGTCP2_CRYPTO_LEVEL_EARLY; + case ssl_encryption_handshake: + return NGTCP2_CRYPTO_LEVEL_HANDSHAKE; + case ssl_encryption_application: + return NGTCP2_CRYPTO_LEVEL_APP; + default: + UNREACHABLE(); + } +} + +const char* crypto_level_name(ngtcp2_crypto_level level) { + switch (level) { + case NGTCP2_CRYPTO_LEVEL_INITIAL: + return "initial"; + case NGTCP2_CRYPTO_LEVEL_EARLY: + return "early"; + case NGTCP2_CRYPTO_LEVEL_HANDSHAKE: + return "handshake"; + case NGTCP2_CRYPTO_LEVEL_APP: + return "app"; + default: + UNREACHABLE(); + } +} + +// When using IPv6, QUIC recommends the use of IPv6 Flow Labels +// as specified in https://tools.ietf.org/html/rfc6437. These +// are used as a means of reliably associating packets exchanged +// as part of a single flow and protecting against certain kinds +// of attacks. +uint32_t GenerateFlowLabel( + const SocketAddress& local, + const SocketAddress& remote, + const QuicCID& cid, + const uint8_t* secret, + size_t secretlen) { + static constexpr size_t kInfoLen = + (sizeof(sockaddr_in6) * 2) + NGTCP2_MAX_CIDLEN; + + uint32_t label = 0; + + std::array plaintext; + size_t infolen = local.length() + remote.length() + cid.length(); + CHECK_LE(infolen, kInfoLen); + + ngtcp2_crypto_ctx ctx; + ngtcp2_crypto_ctx_initial(&ctx); + + auto p = std::begin(plaintext); + p = std::copy_n(local.raw(), local.length(), p); + p = std::copy_n(remote.raw(), remote.length(), p); + p = std::copy_n(cid->data, cid->datalen, p); + + ngtcp2_crypto_hkdf_expand( + reinterpret_cast(&label), + sizeof(label), + &ctx.md, + secret, + secretlen, + plaintext.data(), + infolen); + + label &= kLabelMask; + DCHECK_LE(label, kLabelMask); + return label; +} + +} // namespace quic +} // namespace node diff --git a/src/quic/node_quic_crypto.h b/src/quic/node_quic_crypto.h new file mode 100644 index 00000000000000..8ec0851ba3affe --- /dev/null +++ b/src/quic/node_quic_crypto.h @@ -0,0 +1,157 @@ +#ifndef SRC_QUIC_NODE_QUIC_CRYPTO_H_ +#define SRC_QUIC_NODE_QUIC_CRYPTO_H_ + +#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS + +#include "node_crypto.h" +#include "node_quic_util.h" +#include "v8.h" + +#include +#include +#include + +namespace node { + +namespace quic { + +// Crypto and OpenSSL related utility functions used in +// various places throughout the QUIC implementation. + +// Forward declaration +class QuicSession; +class QuicPacket; + +// many ngtcp2 functions return 0 to indicate success +// and non-zero to indicate failure. Most of the time, +// for such functions we don't care about the specific +// return value so we simplify using a macro. + +#define NGTCP2_ERR(V) (V != 0) +#define NGTCP2_OK(V) (V == 0) + +// Called by QuicInitSecureContext to initialize the +// given SecureContext with the defaults for the given +// QUIC side (client or server). +void InitializeSecureContext( + BaseObjectPtr sc, + bool early_data, + ngtcp2_crypto_side side); + +// Called in the QuicSession::InitServer and +// QuicSession::InitClient to configure the +// appropriate settings for the SSL* associated +// with the session. +void InitializeTLS(QuicSession* session, const crypto::SSLPointer& ssl); + +// Called when the client QuicSession is created and +// when the server QuicSession first receives the +// client hello. +bool DeriveAndInstallInitialKey( + const QuicSession& session, + const QuicCID& dcid); + +// Generates a stateless reset token using HKDF with the +// cid and token secret as input. The token secret is +// either provided by user code when a QuicSocket is +// created or is generated randomly. +// +// QUIC leaves the generation of stateless session tokens +// up to the implementation to figure out. The idea, however, +// is that it ought to be possible to generate a stateless +// reset token reliably even when all state for a connection +// has been lost. We use the cid as it's the only reliably +// consistent bit of data we have when a session is destroyed. +bool GenerateResetToken( + uint8_t* token, + const uint8_t* secret, + const QuicCID& cid); + +// The Retry Token is an encrypted token that is sent to the client +// by the server as part of the path validation flow. The plaintext +// format within the token is opaque and only meaningful the server. +// We can structure it any way we want. It needs to: +// * be hard to guess +// * be time limited +// * be specific to the client address +// * be specific to the original cid +// * contain random data. +std::unique_ptr GenerateRetryPacket( + const uint8_t* token_secret, + const QuicCID& dcid, + const QuicCID& scid, + const SocketAddress& local_addr, + const SocketAddress& remote_addr); + +// The IPv6 Flow Label is generated and set whenever IPv6 is used. +// The label is derived as a cryptographic function of the CID, +// local and remote addresses, and the given secret, that is then +// truncated to a 20-bit value (per IPv6 requirements). In QUIC, +// the flow label *may* be used as a way of disambiguating IP +// packets that belong to the same flow from a remote peer. +uint32_t GenerateFlowLabel( + const SocketAddress& local, + const SocketAddress& remote, + const QuicCID& cid, + const uint8_t* secret, + size_t secretlen); + +// Verifies the validity of a retry token. Returns true if the +// token is *not valid*, false otherwise. If the token is valid, +// the ocid will be updated to the original CID value encoded +// within the successfully validated, encrypted token. +bool InvalidRetryToken( + const ngtcp2_pkt_hd& hd, + const SocketAddress& addr, + QuicCID* ocid, + const uint8_t* token_secret, + uint64_t verification_expiration); + +int VerifyHostnameIdentity(const crypto::SSLPointer& ssl, const char* hostname); +int VerifyHostnameIdentity( + const char* hostname, + const std::string& cert_cn, + const std::unordered_multimap& altnames); + +// Get the ALPN protocol identifier that was negotiated for the session +v8::Local GetALPNProtocol(const QuicSession& session); + +ngtcp2_crypto_level from_ossl_level(OSSL_ENCRYPTION_LEVEL ossl_level); +const char* crypto_level_name(ngtcp2_crypto_level level); + +// SessionTicketAppData is a utility class that is used only during +// the generation or access of TLS stateless sesson tickets. It +// exists solely to provide a easier way for QuicApplication instances +// to set relevant metadata in the session ticket when it is created, +// and the exract and subsequently verify that data when a ticket is +// received and is being validated. The app data is completely opaque +// to anything other than the server-side of the QuicApplication that +// sets it. +class SessionTicketAppData { + public: + enum class Status { + TICKET_USE, + TICKET_USE_RENEW, + TICKET_IGNORE, + TICKET_IGNORE_RENEW + }; + + enum class Flag { + STATUS_NONE, + STATUS_RENEW + }; + + explicit SessionTicketAppData(SSL_SESSION* session) : session_(session) {} + bool Set(const uint8_t* data, size_t len); + bool Get(uint8_t** data, size_t* len) const; + + private: + bool set_ = false; + SSL_SESSION* session_; +}; + +} // namespace quic +} // namespace node + +#endif // NODE_WANT_INTERNALS +#endif // SRC_QUIC_NODE_QUIC_CRYPTO_H_ diff --git a/src/quic/node_quic_default_application.cc b/src/quic/node_quic_default_application.cc new file mode 100644 index 00000000000000..8f3f0349cf9673 --- /dev/null +++ b/src/quic/node_quic_default_application.cc @@ -0,0 +1,181 @@ +#include "debug_utils-inl.h" +#include "node_quic_buffer-inl.h" +#include "node_quic_default_application.h" +#include "node_quic_session-inl.h" +#include "node_quic_socket-inl.h" +#include "node_quic_stream-inl.h" +#include "node_quic_util-inl.h" +#include "node_sockaddr-inl.h" +#include + +#include + +namespace node { +namespace quic { + +namespace { +void Consume(ngtcp2_vec** pvec, size_t* pcnt, size_t len) { + ngtcp2_vec* v = *pvec; + size_t cnt = *pcnt; + + for (; cnt > 0; --cnt, ++v) { + if (v->len > len) { + v->len -= len; + v->base += len; + break; + } + len -= v->len; + } + + *pvec = v; + *pcnt = cnt; +} + +int IsEmpty(const ngtcp2_vec* vec, size_t cnt) { + size_t i; + for (i = 0; i < cnt && vec[i].len == 0; ++i) {} + return i == cnt; +} +} // anonymous namespace + +DefaultApplication::DefaultApplication( + QuicSession* session) : + QuicApplication(session) {} + +bool DefaultApplication::Initialize() { + if (needs_init()) { + Debug(session(), "Default QUIC Application Initialized"); + set_init_done(); + } + return needs_init(); +} + +void DefaultApplication::ScheduleStream(int64_t stream_id) { + BaseObjectPtr stream = session()->FindStream(stream_id); + Debug(session(), "Scheduling stream %" PRIu64, stream_id); + if (LIKELY(stream)) + stream->Schedule(&stream_queue_); +} + +void DefaultApplication::UnscheduleStream(int64_t stream_id) { + BaseObjectPtr stream = session()->FindStream(stream_id); + Debug(session(), "Unscheduling stream %" PRIu64, stream_id); + if (LIKELY(stream)) + stream->Unschedule(); +} + +void DefaultApplication::StreamClose( + int64_t stream_id, + uint64_t app_error_code) { + if (!session()->HasStream(stream_id)) + return; + if (app_error_code == 0) + app_error_code = NGTCP2_APP_NOERROR; + UnscheduleStream(stream_id); + QuicApplication::StreamClose(stream_id, app_error_code); +} + +void DefaultApplication::ResumeStream(int64_t stream_id) { + Debug(session(), "Stream %" PRId64 " has data to send", stream_id); + ScheduleStream(stream_id); +} + +bool DefaultApplication::ReceiveStreamData( + int64_t stream_id, + int fin, + const uint8_t* data, + size_t datalen, + uint64_t offset) { + // Ensure that the QuicStream exists before deferring to + // QuicApplication specific processing logic. + Debug(session(), "Default QUIC Application receiving stream data"); + BaseObjectPtr stream = session()->FindStream(stream_id); + if (!stream) { + // Shutdown the stream explicitly if the session is being closed. + if (session()->is_gracefully_closing()) { + session()->ResetStream(stream_id, NGTCP2_ERR_CLOSING); + return true; + } + + // One potential DOS attack vector is to send a bunch of + // empty stream frames to commit resources. Check that + // here. Essentially, we only want to create a new stream + // if the datalen is greater than 0, otherwise, we ignore + // the packet. ngtcp2 should be handling this for us, + // but we handle it just to be safe. + if (UNLIKELY(datalen == 0)) + return true; + + stream = session()->CreateStream(stream_id); + } + CHECK(stream); + + stream->ReceiveData(fin, data, datalen, offset); + return true; +} + +int DefaultApplication::GetStreamData(StreamData* stream_data) { + QuicStream* stream = stream_queue_.PopFront(); + // If stream is nullptr, there are no streams with data pending. + if (stream == nullptr) + return 0; + + stream_data->stream.reset(stream); + stream_data->id = stream->id(); + + auto next = [&]( + int status, + const ngtcp2_vec* data, + size_t count, + bob::Done done) { + switch (status) { + case bob::Status::STATUS_BLOCK: + // Fall through + case bob::Status::STATUS_WAIT: + // Fall through + case bob::Status::STATUS_EOS: + return; + case bob::Status::STATUS_END: + stream_data->fin = 1; + } + stream_data->count = count; + + if (count > 0) { + stream->Schedule(&stream_queue_); + stream_data->remaining = get_length(data, count); + } else { + stream_data->remaining = 0; + } + }; + + if (LIKELY(!stream->is_eos())) { + CHECK_GE(stream->Pull( + std::move(next), + bob::Options::OPTIONS_SYNC, + stream_data->data, + arraysize(stream_data->data), + kMaxVectorCount), 0); + } + + return 0; +} + +bool DefaultApplication::StreamCommit( + StreamData* stream_data, + size_t datalen) { + CHECK(stream_data->stream); + stream_data->remaining -= datalen; + Consume(&stream_data->buf, &stream_data->count, datalen); + stream_data->stream->Commit(datalen); + return true; +} + +bool DefaultApplication::ShouldSetFin(const StreamData& stream_data) { + if (!stream_data.stream || + !IsEmpty(stream_data.buf, stream_data.count)) + return false; + return !stream_data.stream->is_writable(); +} + +} // namespace quic +} // namespace node diff --git a/src/quic/node_quic_default_application.h b/src/quic/node_quic_default_application.h new file mode 100644 index 00000000000000..9fb5e050aa021f --- /dev/null +++ b/src/quic/node_quic_default_application.h @@ -0,0 +1,61 @@ +#ifndef SRC_QUIC_NODE_QUIC_DEFAULT_APPLICATION_H_ +#define SRC_QUIC_NODE_QUIC_DEFAULT_APPLICATION_H_ + +#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS + +#include "node_quic_stream.h" +#include "node_quic_session.h" +#include "node_quic_util.h" +#include "util.h" +#include "v8.h" + +namespace node { + +namespace quic { + +// The DefaultApplication is used whenever an unknown/unrecognized +// alpn identifier is used. It switches the QUIC implementation into +// a minimal/generic mode that defers all application level processing +// to the user-code level. Headers are not supported by QuicStream +// instances created under the default application. +class DefaultApplication final : public QuicApplication { + public: + explicit DefaultApplication(QuicSession* session); + + bool Initialize() override; + + void StopTrackingMemory(void* ptr) override { + // Do nothing. Not used. + } + + bool ReceiveStreamData( + int64_t stream_id, + int fin, + const uint8_t* data, + size_t datalen, + uint64_t offset) override; + + int GetStreamData(StreamData* stream_data) override; + + void ResumeStream(int64_t stream_id) override; + void StreamClose(int64_t stream_id, uint64_t app_error_code) override; + bool ShouldSetFin(const StreamData& stream_data) override; + bool StreamCommit(StreamData* stream_data, size_t datalen) override; + + SET_SELF_SIZE(DefaultApplication) + SET_MEMORY_INFO_NAME(DefaultApplication) + SET_NO_MEMORY_INFO() + + private: + void ScheduleStream(int64_t stream_id); + void UnscheduleStream(int64_t stream_id); + + QuicStream::Queue stream_queue_; +}; + +} // namespace quic + +} // namespace node + +#endif // NODE_WANT_INTERNALS +#endif // SRC_QUIC_NODE_QUIC_DEFAULT_APPLICATION_H_ diff --git a/src/quic/node_quic_http3_application.cc b/src/quic/node_quic_http3_application.cc new file mode 100644 index 00000000000000..8c64e8c6163334 --- /dev/null +++ b/src/quic/node_quic_http3_application.cc @@ -0,0 +1,935 @@ +#include "node.h" +#include "debug_utils-inl.h" +#include "node_mem-inl.h" +#include "node_quic_buffer-inl.h" +#include "node_quic_http3_application.h" +#include "node_quic_session-inl.h" +#include "node_quic_socket-inl.h" +#include "node_quic_stream-inl.h" +#include "node_quic_util-inl.h" +#include "node_sockaddr-inl.h" +#include "node_http_common-inl.h" + +#include +#include +#include +#include + +namespace node { + +using v8::Array; +using v8::Local; + +namespace quic { + +// nghttp3 uses a numeric identifier for a large number +// of known HTTP header names. These allow us to use +// static strings for those rather than allocating new +// strings all of the time. The list of strings supported +// is included in node_http_common.h +#define V1(name, value) case NGHTTP3_QPACK_TOKEN__##name: return value; +#define V2(name, value) case NGHTTP3_QPACK_TOKEN_##name: return value; +const char* Http3HeaderTraits::ToHttpHeaderName(int32_t token) { + switch (token) { + default: + // Fall through + case -1: return nullptr; + HTTP_SPECIAL_HEADERS(V1) + HTTP_REGULAR_HEADERS(V2) + } +} +#undef V1 +#undef V2 + +template +void Http3Application::SetConfig( + int idx, + M T::*member) { + AliasedFloat64Array& buffer = session()->quic_state()->http3config_buffer; + uint64_t flags = static_cast(buffer[IDX_HTTP3_CONFIG_COUNT]); + if (flags & (1ULL << idx)) + config_.*member = static_cast(buffer[idx]); +} + +Http3Application::Http3Application( + QuicSession* session) + : QuicApplication(session), + alloc_info_(MakeAllocator()) { + // Collect Configuration Details. + SetConfig(IDX_HTTP3_QPACK_MAX_TABLE_CAPACITY, + &Http3ApplicationConfig::qpack_max_table_capacity); + SetConfig(IDX_HTTP3_QPACK_BLOCKED_STREAMS, + &Http3ApplicationConfig::qpack_blocked_streams); + SetConfig(IDX_HTTP3_MAX_HEADER_LIST_SIZE, + &Http3ApplicationConfig::max_header_list_size); + SetConfig(IDX_HTTP3_MAX_PUSHES, + &Http3ApplicationConfig::max_pushes); + SetConfig(IDX_HTTP3_MAX_HEADER_PAIRS, + &Http3ApplicationConfig::max_header_pairs); + SetConfig(IDX_HTTP3_MAX_HEADER_LENGTH, + &Http3ApplicationConfig::max_header_length); + set_max_header_pairs( + session->is_server() + ? GetServerMaxHeaderPairs(config_.max_header_pairs) + : GetClientMaxHeaderPairs(config_.max_header_pairs)); + set_max_header_length(config_.max_header_length); + + session->quic_state()->http3config_buffer[IDX_HTTP3_CONFIG_COUNT] = 0; +} + +// Push streams in HTTP/3 are a bit complicated. +// First, it's important to know that only an HTTP/3 server can +// create a push stream. +// Second, it's important to recognize that a push stream is +// essentially an *assumed* request. For instance, if a client +// requests a webpage that has links to css and js files, and +// the server expects the client to send subsequent requests +// for those css and js files, the server can shortcut the +// process by opening a push stream for each additional resource +// it assumes the client to make. +// Third, a push stream can only be opened within the context +// of an HTTP/3 request/response. Essentially, a server receives +// a request and while processing the response, the server can +// open one or more push streams. +// +// Now... a push stream consists of two components: a push promise +// and a push fulfillment. The push promise is sent *as part of +// the response on the original stream* and is assigned a push id +// and a block of headers containing the *assumed request headers*. +// The push promise is sent on the request/response bidirectional +// stream. +// The push fulfillment is a unidirectional stream opened by the +// server that contains the push id, the response header block, and +// the response payload. +// Here's where it can get a bit complicated: the server sends the +// push promise and the push fulfillment on two different, and +// independent QUIC streams. The push id is used to correlate +// those on the client side, but, it's entirely possible for the +// client to receive the push fulfillment before it actually receives +// the push promise. It's *unlikely*, but it's possible. Fortunately, +// nghttp3 handles the complexity of that for us internally but +// makes for some weird timing and could lead to some amount of +// buffering to occur. +// +// The *logical* order of events from the client side *should* +// be: (a) receive the push promise containing assumed request +// headers, (b) receive the push fulfillment containing the +// response headers followed immediately by the response payload. +// +// On the server side, the steps are: (a) first create the push +// promise creating the push_id then (b) open the unidirectional +// stream that will be used to fullfil the push promise. Once that +// unidirectional stream is created, the push id and unidirectional +// stream ID must be bound. The CreateAndBindPushStream handles (b) +int64_t Http3Application::CreateAndBindPushStream(int64_t push_id) { + CHECK(session()->is_server()); + int64_t stream_id; + if (!session()->OpenUnidirectionalStream(&stream_id)) + return 0; + return nghttp3_conn_bind_push_stream( + connection(), + push_id, + stream_id) == 0 ? stream_id : 0; +} + +bool Http3Application::SubmitPushPromise( + int64_t id, + int64_t* push_id, + int64_t* stream_id, + const Http3Headers& headers) { + // Successfully creating the push promise and opening the + // fulfillment stream will queue nghttp3 up to send data. + // Creating the SendSessionScope here ensures that when + // SubmitPush exits, SendPendingData will be called if + // we are not within the context of an ngtcp2 callback. + QuicSession::SendSessionScope send_scope(session()); + + Debug( + session(), + "Submitting %d push promise headers", + headers.length()); + if (nghttp3_conn_submit_push_promise( + connection(), + push_id, + id, + headers.data(), + headers.length()) != 0) { + return false; + } + // Once we've successfully submitting the push promise and have + // a push id assigned, we create the push fulfillment stream. + *stream_id = CreateAndBindPushStream(*push_id); + return *stream_id != 0; // push stream can never use stream id 0 +} + +bool Http3Application::SubmitInformation( + int64_t id, + const Http3Headers& headers) { + QuicSession::SendSessionScope send_scope(session()); + Debug( + session(), + "Submitting %d informational headers for stream %" PRId64, + headers.length(), + id); + return nghttp3_conn_submit_info( + connection(), + id, + headers.data(), + headers.length()) == 0; +} + +bool Http3Application::SubmitTrailers( + int64_t id, + const Http3Headers& headers) { + QuicSession::SendSessionScope send_scope(session()); + Debug( + session(), + "Submitting %d trailing headers for stream %" PRId64, + headers.length(), + id); + return nghttp3_conn_submit_trailers( + connection(), + id, + headers.data(), + headers.length()) == 0; +} + +bool Http3Application::SubmitHeaders( + int64_t id, + const Http3Headers& headers, + int32_t flags) { + QuicSession::SendSessionScope send_scope(session()); + static constexpr nghttp3_data_reader reader = { + Http3Application::OnReadData }; + const nghttp3_data_reader* reader_ptr = nullptr; + if (!(flags & QUICSTREAM_HEADER_FLAGS_TERMINAL)) + reader_ptr = &reader; + + switch (session()->crypto_context()->side()) { + case NGTCP2_CRYPTO_SIDE_CLIENT: + return nghttp3_conn_submit_request( + connection(), + id, + headers.data(), + headers.length(), + reader_ptr, + nullptr) == 0; + case NGTCP2_CRYPTO_SIDE_SERVER: + return nghttp3_conn_submit_response( + connection(), + id, + headers.data(), + headers.length(), + reader_ptr) == 0; + default: + UNREACHABLE(); + } +} + +// SubmitPush initiates a push stream by first creating a push promise +// with an associated push id, then opening the unidirectional stream +// that is used to fullfill it. Assuming both operations are successful, +// the QuicStream instance is created and added to the server QuicSession. +// +// The headers block passed to the submit push contains the assumed +// *request* headers. The response headers are provided using the +// SubmitHeaders() function on the created QuicStream. +BaseObjectPtr Http3Application::SubmitPush( + int64_t id, + Local headers) { + // If the QuicSession is not a server session, return false + // immediately. Push streams cannot be sent by an HTTP/3 client. + if (!session()->is_server()) + return {}; + + Http3Headers nva(env(), headers); + int64_t push_id; + int64_t stream_id; + + // There are several reasons why push may fail. We currently handle + // them all the same. Later we might want to differentiate when the + // return value is NGHTTP3_ERR_PUSH_ID_BLOCKED. + return SubmitPushPromise(id, &push_id, &stream_id, nva) ? + QuicStream::New(session(), stream_id, push_id) : + BaseObjectPtr(); +} + +// Submit informational headers (response headers that use a 1xx +// status code). If the QuicSession is not a server session, return +// false immediately because info headers cannot be sent by a +// client +bool Http3Application::SubmitInformation( + int64_t stream_id, + Local headers) { + if (!session()->is_server()) + return false; + Http3Headers nva(session()->env(), headers); + return SubmitInformation(stream_id, nva); +} + +// For client sessions, submits request headers. For server sessions, +// submits response headers. +bool Http3Application::SubmitHeaders( + int64_t stream_id, + Local headers, + uint32_t flags) { + Http3Headers nva(session()->env(), headers); + return SubmitHeaders(stream_id, nva, flags); +} + +// Submits trailing headers for the HTTP/3 request or response. +bool Http3Application::SubmitTrailers( + int64_t stream_id, + Local headers) { + Http3Headers nva(session()->env(), headers); + return SubmitTrailers(stream_id, nva); +} + +void Http3Application::CheckAllocatedSize(size_t previous_size) const { + CHECK_GE(current_nghttp3_memory_, previous_size); +} + +void Http3Application::IncreaseAllocatedSize(size_t size) { + current_nghttp3_memory_ += size; +} + +void Http3Application::DecreaseAllocatedSize(size_t size) { + current_nghttp3_memory_ -= size; +} + +void Http3Application::MemoryInfo(MemoryTracker* tracker) const { + tracker->TrackFieldWithSize("current_nghttp3_memory", + current_nghttp3_memory_); +} + +// Creates the underlying nghttp3 connection state for the session. +void Http3Application::CreateConnection() { + // nghttp3_conn_server_new and nghttp3_conn_client_new share + // identical definitions, so new_fn will work for both. + using new_fn = decltype(&nghttp3_conn_server_new); + static new_fn fns[] = { + nghttp3_conn_client_new, // NGTCP2_CRYPTO_SIDE_CLIENT + nghttp3_conn_server_new, // NGTCP2_CRYPTO_SIDE_SERVER + }; + + ngtcp2_crypto_side side = session()->crypto_context()->side(); + nghttp3_conn* conn; + + CHECK_EQ(fns[side]( + &conn, + &callbacks_[side], + &config_, + &alloc_info_, + this), 0); + CHECK_NOT_NULL(conn); + connection_.reset(conn); +} + +// The HTTP/3 QUIC binding uses a single unidirectional control +// stream in each direction to exchange frames impacting the entire +// connection. +bool Http3Application::CreateAndBindControlStream() { + if (!session()->OpenUnidirectionalStream(&control_stream_id_)) + return false; + Debug( + session(), + "Open stream %" PRId64 " and bind as control stream", + control_stream_id_); + return nghttp3_conn_bind_control_stream( + connection(), + control_stream_id_) == 0; +} + +// The HTTP/3 QUIC binding creates two unidirectional streams in +// each direction to exchange header compression details. +bool Http3Application::CreateAndBindQPackStreams() { + if (!session()->OpenUnidirectionalStream(&qpack_enc_stream_id_) || + !session()->OpenUnidirectionalStream(&qpack_dec_stream_id_)) { + return false; + } + Debug( + session(), + "Open streams %" PRId64 " and %" PRId64 " and bind as qpack streams", + qpack_enc_stream_id_, + qpack_dec_stream_id_); + return nghttp3_conn_bind_qpack_streams( + connection(), + qpack_enc_stream_id_, + qpack_dec_stream_id_) == 0; +} + +bool Http3Application::Initialize() { + if (!needs_init()) + return false; + + // The QuicSession must allow for at least three local unidirectional streams. + // This number is fixed by the http3 specification and represent the + // control stream and two qpack management streams. + if (session()->max_local_streams_uni() < 3) + return false; + + Debug(session(), "QPack Max Table Capacity: %" PRIu64, + config_.qpack_max_table_capacity); + Debug(session(), "QPack Blocked Streams: %" PRIu64, + config_.qpack_blocked_streams); + Debug(session(), "Max Header List Size: %" PRIu64, + config_.max_header_list_size); + Debug(session(), "Max Pushes: %" PRIu64, + config_.max_pushes); + + CreateConnection(); + Debug(session(), "HTTP/3 connection created"); + + ngtcp2_transport_params params; + session()->GetLocalTransportParams(¶ms); + + if (session()->is_server()) { + nghttp3_conn_set_max_client_streams_bidi( + connection(), + params.initial_max_streams_bidi); + } + + if (!CreateAndBindControlStream() || + !CreateAndBindQPackStreams()) { + return false; + } + + set_init_done(); + return true; +} + +// All HTTP/3 control, header, and stream data arrives as QUIC stream data. +// Here we pass the received data off to nghttp3 for processing. This will +// trigger the invocation of the various nghttp3 callbacks. +bool Http3Application::ReceiveStreamData( + int64_t stream_id, + int fin, + const uint8_t* data, + size_t datalen, + uint64_t offset) { + Debug(session(), "Receiving %" PRIu64 " bytes for stream %" PRIu64 "%s", + datalen, stream_id, fin == 1 ? " (fin)" : ""); + ssize_t nread = + nghttp3_conn_read_stream( + connection(), stream_id, data, datalen, fin); + if (nread < 0) { + Debug(session(), "Failure to read HTTP/3 Stream Data [%" PRId64 "]", nread); + return false; + } + + return true; +} + +// This is the QUIC-level stream data acknowledgement. It is called for +// all streams, including unidirectional streams. This has to forward on +// to nghttp3 for processing. The Http3Application::AckedStreamData might +// be called as a result to acknowledge (and free) QuicStream data. +void Http3Application::AcknowledgeStreamData( + int64_t stream_id, + uint64_t offset, + size_t datalen) { + if (nghttp3_conn_add_ack_offset(connection(), stream_id, datalen) != 0) + Debug(session(), "Failure to acknowledge HTTP/3 Stream Data"); +} + +void Http3Application::StreamClose( + int64_t stream_id, + uint64_t app_error_code) { + if (app_error_code == 0) + app_error_code = NGTCP2_APP_NOERROR; + nghttp3_conn_close_stream(connection(), stream_id, app_error_code); + QuicApplication::StreamClose(stream_id, app_error_code); +} + +void Http3Application::StreamReset( + int64_t stream_id, + uint64_t app_error_code) { + nghttp3_conn_reset_stream(connection(), stream_id); + QuicApplication::StreamReset(stream_id, app_error_code); +} + +// When SendPendingData tries to send data for a given stream and there +// is no data to send but the QuicStream is still writable, it will +// be paused. When there's data available, the stream is resumed. +void Http3Application::ResumeStream(int64_t stream_id) { + nghttp3_conn_resume_stream(connection(), stream_id); +} + +// When stream data cannot be sent because of flow control, it is marked +// as being blocked. When the flow control windows expands, nghttp3 has +// to be told to unblock the stream so it knows to try sending data again. +void Http3Application::ExtendMaxStreamData( + int64_t stream_id, + uint64_t max_data) { + nghttp3_conn_unblock_stream(connection(), stream_id); +} + +// When stream data cannot be sent because of flow control, it is marked +// as being blocked. +bool Http3Application::BlockStream(int64_t stream_id) { + int err = nghttp3_conn_block_stream(connection(), stream_id); + if (err != 0) { + session()->set_last_error(QUIC_ERROR_APPLICATION, err); + return false; + } + return true; +} + +// nghttp3 keeps track of how much QuicStream data it has available and +// has sent. StreamCommit is called when a QuicPacket is serialized +// and updates nghttp3's internal state. +bool Http3Application::StreamCommit(StreamData* stream_data, size_t datalen) { + int err = nghttp3_conn_add_write_offset( + connection(), + stream_data->id, + datalen); + if (err != 0) { + session()->set_last_error(QUIC_ERROR_APPLICATION, err); + return false; + } + return true; +} + +// GetStreamData is called by SendPendingData to collect the QuicStream data +// that is to be packaged into a serialized QuicPacket. There may or may not +// be any stream data to send. The call to nghttp3_conn_writev_stream will +// provide any available stream data (if any). If nghttp3 is not sure if +// there is data to send, it will subsequently call Http3Application::ReadData +// to collect available data from the QuicStream. +int Http3Application::GetStreamData(StreamData* stream_data) { + ssize_t ret = 0; + if (connection() && session()->max_data_left()) { + ret = nghttp3_conn_writev_stream( + connection(), + &stream_data->id, + &stream_data->fin, + reinterpret_cast(stream_data->data), + sizeof(stream_data->data)); + if (ret < 0) + return static_cast(ret); + else + stream_data->remaining = stream_data->count = static_cast(ret); + } + if (stream_data->id > -1) { + Debug(session(), "Selected %" PRId64 " buffers for stream %" PRId64 "%s", + stream_data->count, + stream_data->id, + stream_data->fin == 1 ? " (fin)" : ""); + } + return 0; +} + +// Determines whether SendPendingData should set fin on the QuicStream +bool Http3Application::ShouldSetFin(const StreamData& stream_data) { + return stream_data.id > -1 && + !is_control_stream(stream_data.id) && + stream_data.fin == 1; +} + +// This is where nghttp3 pulls the data from the outgoing +// buffer to prepare it to be sent on the QUIC stream. +ssize_t Http3Application::ReadData( + int64_t stream_id, + nghttp3_vec* vec, + size_t veccnt, + uint32_t* pflags) { + BaseObjectPtr stream = session()->FindStream(stream_id); + CHECK(stream); + + ssize_t ret = NGHTTP3_ERR_WOULDBLOCK; + + auto next = [&]( + int status, + const ngtcp2_vec* data, + size_t count, + bob::Done done) { + CHECK_LE(count, veccnt); + + switch (status) { + case bob::Status::STATUS_BLOCK: + // Fall through + case bob::Status::STATUS_WAIT: + // Fall through + case bob::Status::STATUS_EOS: + return; + case bob::Status::STATUS_END: + *pflags |= NGHTTP3_DATA_FLAG_EOF; + break; + } + + ret = count; + size_t numbytes = + nghttp3_vec_len( + reinterpret_cast(data), + count); + std::move(done)(numbytes); + + Debug(session(), "Sending %" PRIu64 " bytes for stream %" PRId64, + numbytes, stream_id); + }; + + CHECK_GE(stream->Pull( + std::move(next), + // Set OPTIONS_END here because nghttp3 takes over responsibility + // for ensuring the data all gets written out. + bob::Options::OPTIONS_END | bob::Options::OPTIONS_SYNC, + reinterpret_cast(vec), + veccnt, + kMaxVectorCount), 0); + + return ret; +} + +// Outgoing data is retained in memory until it is acknowledged. +void Http3Application::AckedStreamData(int64_t stream_id, size_t datalen) { + Acknowledge(stream_id, 0, datalen); +} + +void Http3Application::StreamClosed( + int64_t stream_id, + uint64_t app_error_code) { + BaseObjectPtr stream = session()->FindStream(stream_id); + CHECK(stream); + stream->ReceiveData(1, nullptr, 0, 0); + session()->listener()->OnStreamClose(stream_id, app_error_code); +} + +BaseObjectPtr Http3Application::FindOrCreateStream( + int64_t stream_id) { + BaseObjectPtr stream = session()->FindStream(stream_id); + if (!stream) { + if (session()->is_gracefully_closing()) { + nghttp3_conn_close_stream(connection(), stream_id, NGTCP2_ERR_CLOSING); + return {}; + } + stream = session()->CreateStream(stream_id); + nghttp3_conn_set_stream_user_data(connection(), stream_id, stream.get()); + } + CHECK(stream); + return stream; +} + +void Http3Application::ReceiveData( + int64_t stream_id, + const uint8_t* data, + size_t datalen) { + FindOrCreateStream(stream_id)->ReceiveData(0, data, datalen, 0); +} + +void Http3Application::DeferredConsume( + int64_t stream_id, + size_t consumed) { + // Do nothing here for now. nghttp3 uses the on_deferred_consume + // callback to notify when stream data that had previously been + // deferred has been delivered to the application so that the + // stream data offset can be extended. However, we extend the + // data offset from within QuicStream when the data is delivered + // so we don't have to do it here. +} + +// Called when a nghttp3 detects that a new block of headers +// has been received. Http3Application::ReceiveHeader will +// be called for each name+value pair received, then +// Http3Application::EndHeaders will be called to finalize +// the header block. +void Http3Application::BeginHeaders( + int64_t stream_id, + QuicStreamHeadersKind kind) { + Debug(session(), "Starting header block for stream %" PRId64, stream_id); + FindOrCreateStream(stream_id)->BeginHeaders(kind); +} + +// As each header name+value pair is received, it is stored internally +// by the QuicStream until stream->EndHeaders() is called, during which +// the collected headers are converted to an array and passed off to +// the javascript side. +bool Http3Application::ReceiveHeader( + int64_t stream_id, + int32_t token, + nghttp3_rcbuf* name, + nghttp3_rcbuf* value, + uint8_t flags) { + // Protect against zero-length headers (zero-length if either the + // name or value are zero-length). Such headers are simply ignored. + if (!Http3Header::IsZeroLength(name, value)) { + Debug(session(), "Receiving header for stream %" PRId64, stream_id); + BaseObjectPtr stream = session()->FindStream(stream_id); + CHECK(stream); + if (token == NGHTTP3_QPACK_TOKEN__STATUS) { + nghttp3_vec vec = nghttp3_rcbuf_get_buf(value); + if (vec.base[0] == '1') + stream->set_headers_kind(QUICSTREAM_HEADERS_KIND_INFORMATIONAL); + else + stream->set_headers_kind(QUICSTREAM_HEADERS_KIND_INITIAL); + } + auto header = std::make_unique( + session()->env(), + token, + name, + value, + flags); + return stream->AddHeader(std::move(header)); + } + return true; +} + +// Marks the completion of a headers block. +void Http3Application::EndHeaders(int64_t stream_id, int64_t push_id) { + Debug(session(), "Ending header block for stream %" PRId64, stream_id); + BaseObjectPtr stream = session()->FindStream(stream_id); + CHECK(stream); + stream->EndHeaders(); +} + +void Http3Application::CancelPush( + int64_t push_id, + int64_t stream_id) { + Debug(session(), "push stream canceled"); +} + +void Http3Application::PushStream( + int64_t push_id, + int64_t stream_id) { + Debug(session(), "Received push stream %" PRIu64 " (%" PRIu64 ")", + stream_id, push_id); +} + +void Http3Application::SendStopSending( + int64_t stream_id, + uint64_t app_error_code) { + session()->ResetStream(stream_id, app_error_code); +} + +void Http3Application::EndStream(int64_t stream_id) { + BaseObjectPtr stream = session()->FindStream(stream_id); + CHECK(stream); + stream->ReceiveData(1, nullptr, 0, 0); +} + +const nghttp3_conn_callbacks Http3Application::callbacks_[2] = { + // NGTCP2_CRYPTO_SIDE_CLIENT + { + OnAckedStreamData, + OnStreamClose, + OnReceiveData, + OnDeferredConsume, + OnBeginHeaders, + OnReceiveHeader, + OnEndHeaders, + OnBeginTrailers, // Begin Trailers + OnReceiveHeader, // Receive Trailer + OnEndHeaders, // End Trailers + OnBeginPushPromise, + OnReceivePushPromise, + OnEndPushPromise, + OnCancelPush, + OnSendStopSending, + OnPushStream, + OnEndStream + }, + // NGTCP2_CRYPTO_SIDE_SERVER + { + OnAckedStreamData, + OnStreamClose, + OnReceiveData, + OnDeferredConsume, + OnBeginHeaders, + OnReceiveHeader, + OnEndHeaders, + OnBeginTrailers, // Begin Trailers + OnReceiveHeader, // Receive Trailer + OnEndHeaders, // End Trailers + OnBeginPushPromise, + OnReceivePushPromise, + OnEndPushPromise, + OnCancelPush, + OnSendStopSending, + OnPushStream, + OnEndStream + } +}; + +int Http3Application::OnAckedStreamData( + nghttp3_conn* conn, + int64_t stream_id, + size_t datalen, + void* conn_user_data, + void* stream_user_data) { + Http3Application* app = static_cast(conn_user_data); + app->AckedStreamData(stream_id, datalen); + return 0; +} + +int Http3Application::OnStreamClose( + nghttp3_conn* conn, + int64_t stream_id, + uint64_t app_error_code, + void* conn_user_data, + void* stream_user_data) { + Http3Application* app = static_cast(conn_user_data); + app->StreamClosed(stream_id, app_error_code); + return 0; +} + +int Http3Application::OnReceiveData( + nghttp3_conn* conn, + int64_t stream_id, + const uint8_t* data, + size_t datalen, + void* conn_user_data, + void* stream_user_data) { + Http3Application* app = static_cast(conn_user_data); + app->ReceiveData(stream_id, data, datalen); + return 0; +} + +int Http3Application::OnDeferredConsume( + nghttp3_conn* conn, + int64_t stream_id, + size_t consumed, + void* conn_user_data, + void* stream_user_data) { + Http3Application* app = static_cast(conn_user_data); + app->DeferredConsume(stream_id, consumed); + return 0; +} + +int Http3Application::OnBeginHeaders( + nghttp3_conn* conn, + int64_t stream_id, + void* conn_user_data, + void* stream_user_data) { + Http3Application* app = static_cast(conn_user_data); + app->BeginHeaders(stream_id); + return 0; +} + +int Http3Application::OnBeginTrailers( + nghttp3_conn* conn, + int64_t stream_id, + void* conn_user_data, + void* stream_user_data) { + Http3Application* app = static_cast(conn_user_data); + app->BeginHeaders(stream_id, QUICSTREAM_HEADERS_KIND_TRAILING); + return 0; +} + +int Http3Application::OnReceiveHeader( + nghttp3_conn* conn, + int64_t stream_id, + int32_t token, + nghttp3_rcbuf* name, + nghttp3_rcbuf* value, + uint8_t flags, + void* conn_user_data, + void* stream_user_data) { + Http3Application* app = static_cast(conn_user_data); + // TODO(@jasnell): Need to determine the appropriate response code here + // for when the header is not going to be accepted. + return app->ReceiveHeader(stream_id, token, name, value, flags) ? + 0 : NGHTTP3_ERR_CALLBACK_FAILURE; +} + +int Http3Application::OnEndHeaders( + nghttp3_conn* conn, + int64_t stream_id, + void* conn_user_data, + void* stream_user_data) { + Http3Application* app = static_cast(conn_user_data); + app->EndHeaders(stream_id); + return 0; +} + +int Http3Application::OnBeginPushPromise( + nghttp3_conn* conn, + int64_t stream_id, + int64_t push_id, + void* conn_user_data, + void* stream_user_data) { + Http3Application* app = static_cast(conn_user_data); + app->BeginHeaders(stream_id, QUICSTREAM_HEADERS_KIND_PUSH); + return 0; +} + +int Http3Application::OnReceivePushPromise( + nghttp3_conn* conn, + int64_t stream_id, + int64_t push_id, + int32_t token, + nghttp3_rcbuf* name, + nghttp3_rcbuf* value, + uint8_t flags, + void* conn_user_data, + void* stream_user_data) { + Http3Application* app = static_cast(conn_user_data); + if (!app->ReceiveHeader(stream_id, token, name, value, flags)) + return NGHTTP3_ERR_CALLBACK_FAILURE; + return 0; +} + +int Http3Application::OnEndPushPromise( + nghttp3_conn* conn, + int64_t stream_id, + int64_t push_id, + void* conn_user_data, + void* stream_user_data) { + Http3Application* app = static_cast(conn_user_data); + app->EndHeaders(stream_id, push_id); + return 0; +} + +int Http3Application::OnCancelPush( + nghttp3_conn* conn, + int64_t push_id, + int64_t stream_id, + void* conn_user_data, + void* stream_user_data) { + Http3Application* app = static_cast(conn_user_data); + app->CancelPush(push_id, stream_id); + return 0; +} + +int Http3Application::OnSendStopSending( + nghttp3_conn* conn, + int64_t stream_id, + uint64_t app_error_code, + void* conn_user_data, + void* stream_user_data) { + Http3Application* app = static_cast(conn_user_data); + app->SendStopSending(stream_id, app_error_code); + return 0; +} + +int Http3Application::OnPushStream( + nghttp3_conn* conn, + int64_t push_id, + int64_t stream_id, + void* conn_user_data) { + Http3Application* app = static_cast(conn_user_data); + app->PushStream(push_id, stream_id); + return 0; +} + +int Http3Application::OnEndStream( + nghttp3_conn* conn, + int64_t stream_id, + void* conn_user_data, + void* stream_user_data) { + Http3Application* app = static_cast(conn_user_data); + app->EndStream(stream_id); + return 0; +} + +ssize_t Http3Application::OnReadData( + nghttp3_conn* conn, + int64_t stream_id, + nghttp3_vec* vec, + size_t veccnt, + uint32_t* pflags, + void* conn_user_data, + void* stream_user_data) { + Http3Application* app = static_cast(conn_user_data); + return app->ReadData(stream_id, vec, veccnt, pflags); +} +} // namespace quic +} // namespace node diff --git a/src/quic/node_quic_http3_application.h b/src/quic/node_quic_http3_application.h new file mode 100644 index 00000000000000..1430b5f4f77054 --- /dev/null +++ b/src/quic/node_quic_http3_application.h @@ -0,0 +1,333 @@ +#ifndef SRC_QUIC_NODE_QUIC_HTTP3_APPLICATION_H_ +#define SRC_QUIC_NODE_QUIC_HTTP3_APPLICATION_H_ + +#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS + +#include "node.h" +#include "node_http_common.h" +#include "node_mem.h" +#include "node_quic_session.h" +#include "node_quic_stream-inl.h" +#include "node_quic_util.h" +#include "v8.h" +#include +#include + +#include +namespace node { + +namespace quic { + +constexpr uint64_t DEFAULT_QPACK_MAX_TABLE_CAPACITY = 4096; +constexpr uint64_t DEFAULT_QPACK_BLOCKED_STREAMS = 100; +constexpr size_t DEFAULT_MAX_HEADER_LIST_SIZE = 65535; +constexpr size_t DEFAULT_MAX_PUSHES = 65535; + +struct Http3RcBufferPointerTraits { + typedef nghttp3_rcbuf rcbuf_t; + typedef nghttp3_vec vector_t; + + static void inc(rcbuf_t* buf) { + nghttp3_rcbuf_incref(buf); + } + static void dec(rcbuf_t* buf) { + nghttp3_rcbuf_decref(buf); + } + static vector_t get_vec(const rcbuf_t* buf) { + return nghttp3_rcbuf_get_buf(buf); + } + static bool is_static(const rcbuf_t* buf) { + return nghttp3_rcbuf_is_static(buf); + } +}; + +struct Http3HeadersTraits { + typedef nghttp3_nv nv_t; + static const uint8_t kNoneFlag = NGHTTP3_NV_FLAG_NONE; +}; + +using Http3ConnectionPointer = DeleteFnPtr; +using Http3RcBufferPointer = NgRcBufPointer; +using Http3Headers = NgHeaders; + +struct Http3HeaderTraits { + typedef Http3RcBufferPointer rcbufferpointer_t; + typedef QuicApplication allocator_t; + + static const char* ToHttpHeaderName(int32_t token); +}; + +using Http3Header = NgHeader; + +struct Http3ApplicationConfig : public nghttp3_conn_settings { + Http3ApplicationConfig() { + nghttp3_conn_settings_default(this); + qpack_max_table_capacity = DEFAULT_QPACK_MAX_TABLE_CAPACITY; + qpack_blocked_streams = DEFAULT_QPACK_BLOCKED_STREAMS; + max_header_list_size = DEFAULT_MAX_HEADER_LIST_SIZE; + max_pushes = DEFAULT_MAX_PUSHES; + } + uint64_t max_header_pairs = DEFAULT_MAX_HEADER_LIST_PAIRS; + uint64_t max_header_length = DEFAULT_MAX_HEADER_LENGTH; +}; + +class Http3Application; +using Http3MemoryManager = + mem::NgLibMemoryManager; + +// Http3Application is used whenever the h3 alpn identifier is used. +// It causes the QuicSession to apply HTTP/3 semantics to the connection, +// including handling of headers and other HTTP/3 specific processing. +class Http3Application final : + public QuicApplication, + public Http3MemoryManager { + public: + explicit Http3Application(QuicSession* session); + + bool Initialize() override; + + void StopTrackingMemory(void* ptr) override { + Http3MemoryManager::StopTrackingMemory(ptr); + } + + bool ReceiveStreamData( + int64_t stream_id, + int fin, + const uint8_t* data, + size_t datalen, + uint64_t offset) override; + + void AcknowledgeStreamData( + int64_t stream_id, + uint64_t offset, + size_t datalen) override; + + void StreamClose(int64_t stream_id, uint64_t app_error_code) override; + + void StreamReset( + int64_t stream_id, + uint64_t app_error_code) override; + + void ResumeStream(int64_t stream_id) override; + + void ExtendMaxStreamData(int64_t stream_id, uint64_t max_data) override; + + bool SubmitInformation( + int64_t stream_id, + v8::Local headers) override; + + bool SubmitHeaders( + int64_t stream_id, + v8::Local headers, + uint32_t flags) override; + + bool SubmitTrailers( + int64_t stream_id, + v8::Local headers) override; + + BaseObjectPtr SubmitPush( + int64_t id, + v8::Local headers) override; + + // Implementation for mem::NgLibMemoryManager + void CheckAllocatedSize(size_t previous_size) const; + void IncreaseAllocatedSize(size_t size); + void DecreaseAllocatedSize(size_t size); + + SET_SELF_SIZE(Http3Application) + SET_MEMORY_INFO_NAME(Http3Application) + void MemoryInfo(MemoryTracker* tracker) const override; + + private: + template + void SetConfig(int idx, M T::*member); + + nghttp3_conn* connection() const { return connection_.get(); } + BaseObjectPtr FindOrCreateStream(int64_t stream_id); + + bool CreateAndBindControlStream(); + bool CreateAndBindQPackStreams(); + int64_t CreateAndBindPushStream(int64_t push_id); + + int GetStreamData(StreamData* stream_data) override; + + bool BlockStream(int64_t stream_id) override; + bool StreamCommit(StreamData* stream_data, size_t datalen) override; + bool ShouldSetFin(const StreamData& data) override; + bool SubmitPushPromise( + int64_t id, + int64_t* push_id, + int64_t* stream_id, + const Http3Headers& headers); + bool SubmitInformation(int64_t id, const Http3Headers& headers); + bool SubmitTrailers(int64_t id, const Http3Headers& headers); + bool SubmitHeaders(int64_t id, const Http3Headers& headers, int32_t flags); + + ssize_t ReadData( + int64_t stream_id, + nghttp3_vec* vec, + size_t veccnt, + uint32_t* pflags); + + void AckedStreamData(int64_t stream_id, size_t datalen); + void StreamClosed(int64_t stream_id, uint64_t app_error_code); + void ReceiveData(int64_t stream_id, const uint8_t* data, size_t datalen); + void DeferredConsume(int64_t stream_id, size_t consumed); + void BeginHeaders( + int64_t stream_id, + QuicStreamHeadersKind kind = QUICSTREAM_HEADERS_KIND_NONE); + bool ReceiveHeader( + int64_t stream_id, + int32_t token, + nghttp3_rcbuf* name, + nghttp3_rcbuf* value, + uint8_t flags); + void EndHeaders(int64_t stream_id, int64_t push_id = 0); + void CancelPush(int64_t push_id, int64_t stream_id); + void SendStopSending(int64_t stream_id, uint64_t app_error_code); + void PushStream(int64_t push_id, int64_t stream_id); + void EndStream(int64_t stream_id); + + bool is_control_stream(int64_t stream_id) const { + return stream_id == control_stream_id_ || + stream_id == qpack_dec_stream_id_ || + stream_id == qpack_enc_stream_id_; + } + + nghttp3_mem alloc_info_; + Http3ConnectionPointer connection_; + int64_t control_stream_id_; + int64_t qpack_enc_stream_id_; + int64_t qpack_dec_stream_id_; + size_t current_nghttp3_memory_ = 0; + + Http3ApplicationConfig config_; + + void CreateConnection(); + + static const nghttp3_conn_callbacks callbacks_[2]; + + static int OnAckedStreamData( + nghttp3_conn* conn, + int64_t stream_id, + size_t datalen, + void* conn_user_data, + void* stream_user_data); + + static int OnStreamClose( + nghttp3_conn* conn, + int64_t stream_id, + uint64_t app_error_code, + void* conn_user_data, + void* stream_user_data); + + static int OnReceiveData( + nghttp3_conn* conn, + int64_t stream_id, + const uint8_t* data, + size_t datalen, + void* conn_user_data, + void* stream_user_data); + + static int OnDeferredConsume( + nghttp3_conn* conn, + int64_t stream_id, + size_t consumed, + void* conn_user_data, + void* stream_user_data); + + static int OnBeginHeaders( + nghttp3_conn* conn, + int64_t stream_id, + void* conn_user_data, + void* stream_user_data); + + static int OnBeginTrailers( + nghttp3_conn* conn, + int64_t stream_id, + void* conn_user_data, + void* stream_user_data); + + static int OnReceiveHeader( + nghttp3_conn* conn, + int64_t stream_id, + int32_t token, + nghttp3_rcbuf* name, + nghttp3_rcbuf* value, + uint8_t flags, + void* conn_user_data, + void* stream_user_data); + + static int OnEndHeaders( + nghttp3_conn* conn, + int64_t stream_id, + void* conn_user_data, + void* stream_user_data); + + static int OnBeginPushPromise( + nghttp3_conn* conn, + int64_t stream_id, + int64_t push_id, + void* conn_user_data, + void* stream_user_data); + + static int OnReceivePushPromise( + nghttp3_conn* conn, + int64_t stream_id, + int64_t push_id, + int32_t token, + nghttp3_rcbuf* name, + nghttp3_rcbuf* value, + uint8_t flags, + void* conn_user_data, + void* stream_user_data); + + static int OnEndPushPromise( + nghttp3_conn* conn, + int64_t stream_id, + int64_t push_id, + void* conn_user_data, + void* stream_user_data); + + static int OnCancelPush( + nghttp3_conn* conn, + int64_t push_id, + int64_t stream_id, + void* conn_user_data, + void* stream_user_data); + + static int OnSendStopSending( + nghttp3_conn* conn, + int64_t stream_id, + uint64_t app_error_code, + void* conn_user_data, + void* stream_user_data); + + static int OnPushStream( + nghttp3_conn* conn, + int64_t push_id, + int64_t stream_id, + void* conn_user_data); + + static int OnEndStream( + nghttp3_conn* conn, + int64_t stream_id, + void* conn_user_data, + void* stream_user_data); + + static ssize_t OnReadData( + nghttp3_conn* conn, + int64_t stream_id, + nghttp3_vec* vec, + size_t veccnt, + uint32_t* pflags, + void* conn_user_data, + void* stream_user_data); +}; + +} // namespace quic + +} // namespace node + +#endif // NODE_WANT_INTERNALS +#endif // SRC_QUIC_NODE_QUIC_HTTP3_APPLICATION_H_ diff --git a/src/quic/node_quic_session-inl.h b/src/quic/node_quic_session-inl.h new file mode 100644 index 00000000000000..d1546205f1bf63 --- /dev/null +++ b/src/quic/node_quic_session-inl.h @@ -0,0 +1,614 @@ +#ifndef SRC_QUIC_NODE_QUIC_SESSION_INL_H_ +#define SRC_QUIC_NODE_QUIC_SESSION_INL_H_ + +#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS + +#include "debug_utils-inl.h" +#include "node_crypto.h" +#include "node_crypto_common.h" +#include "node_quic_crypto.h" +#include "node_quic_session.h" +#include "node_quic_socket-inl.h" +#include "node_quic_stream-inl.h" + +#include +#include +#include + +namespace node { + +namespace quic { + +void QuicSessionConfig::GenerateStatelessResetToken( + QuicSession* session, + const QuicCID& cid) { + transport_params.stateless_reset_token_present = 1; + StatelessResetToken token( + transport_params.stateless_reset_token, + session->socket()->session_reset_secret(), + cid); + Debug(session, "Generated stateless reset token %s for CID %s", token, cid); +} + +void QuicSessionConfig::GeneratePreferredAddressToken( + ConnectionIDStrategy connection_id_strategy, + QuicSession* session, + QuicCID* pscid) { + connection_id_strategy(session, pscid->cid(), kScidLen); + transport_params.preferred_address.cid = **pscid; + + StatelessResetToken( + transport_params.preferred_address.stateless_reset_token, + session->socket()->session_reset_secret(), + *pscid); +} + +void QuicSessionConfig::set_original_connection_id(const QuicCID& ocid) { + if (ocid) { + transport_params.original_connection_id = *ocid; + transport_params.original_connection_id_present = 1; + } +} + +void QuicSessionConfig::set_qlog(const ngtcp2_qlog_settings& qlog_) { + qlog = qlog_; +} + +QuicCryptoContext::QuicCryptoContext( + QuicSession* session, + BaseObjectPtr secure_context, + ngtcp2_crypto_side side, + uint32_t options) : + session_(session), + secure_context_(secure_context), + side_(side), + options_(options) { + ssl_.reset(SSL_new(secure_context_->ctx_.get())); + CHECK(ssl_); +} + +void QuicCryptoContext::Initialize() { + InitializeTLS(session(), ssl_); +} + +// Cancels and frees any remaining outbound handshake data +// at each crypto level. +uint64_t QuicCryptoContext::Cancel() { + uint64_t len = handshake_[0].Cancel(); + len += handshake_[1].Cancel(); + len += handshake_[2].Cancel(); + return len; +} + +v8::MaybeLocal QuicCryptoContext::ocsp_response() const { + Environment* env = session()->env(); + return crypto::GetSSLOCSPResponse( + env, + ssl_.get(), + v8::Undefined(env->isolate())); +} + +ngtcp2_crypto_level QuicCryptoContext::read_crypto_level() const { + return from_ossl_level(SSL_quic_read_level(ssl_.get())); +} + +ngtcp2_crypto_level QuicCryptoContext::write_crypto_level() const { + return from_ossl_level(SSL_quic_write_level(ssl_.get())); +} + +// TLS Keylogging is enabled per-QuicSession by attaching an handler to the +// "keylog" event. Each keylog line is emitted to JavaScript where it can +// be routed to whatever destination makes sense. Typically, this will be +// to a keylog file that can be consumed by tools like Wireshark to intercept +// and decrypt QUIC network traffic. +void QuicCryptoContext::Keylog(const char* line) { + if (UNLIKELY(session_->state_[IDX_QUIC_SESSION_STATE_KEYLOG_ENABLED] == 1)) + session_->listener()->OnKeylog(line, strlen(line)); +} + +void QuicCryptoContext::OnClientHelloDone() { + // Continue the TLS handshake when this function exits + // otherwise it will stall and fail. + TLSHandshakeScope handshake(this, &in_client_hello_); + // Disable the callback at this point so we don't loop continuously + session_->state_[IDX_QUIC_SESSION_STATE_CLIENT_HELLO_ENABLED] = 0; +} + +// Following a pause in the handshake for OCSP or client hello, we kickstart +// the handshake again here by triggering ngtcp2 to serialize data. +void QuicCryptoContext::ResumeHandshake() { + // We haven't received any actual new handshake data but calling + // this will trigger the handshake to continue. + Receive(read_crypto_level(), 0, nullptr, 0); + session_->SendPendingData(); +} + +// For 0RTT, this sets the TLS session data from the given buffer. +bool QuicCryptoContext::set_session(crypto::SSLSessionPointer session) { + if (side_ == NGTCP2_CRYPTO_SIDE_CLIENT && session != nullptr) { + early_data_ = + SSL_SESSION_get_max_early_data(session.get()) == 0xffffffffUL; + } + return crypto::SetTLSSession(ssl_, std::move(session)); +} + +v8::MaybeLocal QuicCryptoContext::hello_ciphers() const { + return crypto::GetClientHelloCiphers(session()->env(), ssl_); +} + +v8::MaybeLocal QuicCryptoContext::cipher_name() const { + return crypto::GetCipherName(session()->env(), ssl_); +} + +v8::MaybeLocal QuicCryptoContext::cipher_version() const { + return crypto::GetCipherVersion(session()->env(), ssl_); +} + +const char* QuicCryptoContext::servername() const { + return crypto::GetServerName(ssl_.get()); +} + +const char* QuicCryptoContext::hello_alpn() const { + return crypto::GetClientHelloALPN(ssl_); +} + +const char* QuicCryptoContext::hello_servername() const { + return crypto::GetClientHelloServerName(ssl_); +} + +v8::MaybeLocal QuicCryptoContext::ephemeral_key() const { + return crypto::GetEphemeralKey(session()->env(), ssl_); +} + +v8::MaybeLocal QuicCryptoContext::peer_cert(bool abbreviated) const { + return crypto::GetPeerCert( + session()->env(), + ssl_, + abbreviated, + session()->is_server()); +} + +v8::MaybeLocal QuicCryptoContext::cert() const { + return crypto::GetCert(session()->env(), ssl_); +} + +std::string QuicCryptoContext::selected_alpn() const { + const unsigned char* alpn_buf = nullptr; + unsigned int alpnlen; + SSL_get0_alpn_selected(ssl_.get(), &alpn_buf, &alpnlen); + return alpnlen ? + std::string(reinterpret_cast(alpn_buf), alpnlen) : + std::string(); +} + +bool QuicCryptoContext::early_data() const { + return + (early_data_ && + SSL_get_early_data_status(ssl_.get()) == SSL_EARLY_DATA_ACCEPTED) || + SSL_get_max_early_data(ssl_.get()) == 0xffffffffUL; +} + +void QuicCryptoContext::set_tls_alert(int err) { + Debug(session(), "TLS Alert [%d]: %s", err, SSL_alert_type_string_long(err)); + session_->set_last_error(QuicError(QUIC_ERROR_CRYPTO, err)); +} + +// Derives and installs the initial keying material for a newly +// created session. +bool QuicCryptoContext::SetupInitialKey(const QuicCID& dcid) { + Debug(session(), "Deriving and installing initial keys"); + return DeriveAndInstallInitialKey(*session(), dcid); +} + +QuicApplication::QuicApplication(QuicSession* session) : session_(session) {} + +void QuicApplication::set_stream_fin(int64_t stream_id) { + BaseObjectPtr stream = session()->FindStream(stream_id); + CHECK(stream); + stream->set_fin_sent(); +} + +ssize_t QuicApplication::WriteVStream( + QuicPathStorage* path, + uint8_t* buf, + ssize_t* ndatalen, + const StreamData& stream_data) { + CHECK_LE(stream_data.count, kMaxVectorCount); + return ngtcp2_conn_writev_stream( + session()->connection(), + &path->path, + buf, + session()->max_packet_length(), + ndatalen, + stream_data.remaining > 0 ? + NGTCP2_WRITE_STREAM_FLAG_MORE : + NGTCP2_WRITE_STREAM_FLAG_NONE, + stream_data.id, + stream_data.fin, + stream_data.buf, + stream_data.count, + uv_hrtime()); +} + +std::unique_ptr QuicApplication::CreateStreamDataPacket() { + return QuicPacket::Create( + "stream data", + session()->max_packet_length()); +} + +Environment* QuicApplication::env() const { + return session()->env(); +} + +// Every QUIC session will have multiple CIDs associated with it. +void QuicSession::AssociateCID(const QuicCID& cid) { + socket()->AssociateCID(cid, scid_); +} + +void QuicSession::DisassociateCID(const QuicCID& cid) { + if (is_server()) + socket()->DisassociateCID(cid); +} + +void QuicSession::StartHandshake() { + if (crypto_context_->is_handshake_started() || is_server()) + return; + crypto_context_->handshake_started(); + SendPendingData(); +} + +void QuicSession::ExtendMaxStreamData(int64_t stream_id, uint64_t max_data) { + Debug(this, + "Extending max stream %" PRId64 " data to %" PRIu64, + stream_id, max_data); + application_->ExtendMaxStreamData(stream_id, max_data); +} + +void QuicSession::ExtendMaxStreamsRemoteUni(uint64_t max_streams) { + Debug(this, "Extend remote max unidirectional streams: %" PRIu64, + max_streams); + application_->ExtendMaxStreamsRemoteUni(max_streams); +} + +void QuicSession::ExtendMaxStreamsRemoteBidi(uint64_t max_streams) { + Debug(this, "Extend remote max bidirectional streams: %" PRIu64, + max_streams); + application_->ExtendMaxStreamsRemoteBidi(max_streams); +} + +void QuicSession::ExtendMaxStreamsUni(uint64_t max_streams) { + Debug(this, "Setting max unidirectional streams to %" PRIu64, max_streams); + state_[IDX_QUIC_SESSION_STATE_MAX_STREAMS_UNI] = + static_cast(max_streams); +} + +void QuicSession::ExtendMaxStreamsBidi(uint64_t max_streams) { + Debug(this, "Setting max bidirectional streams to %" PRIu64, max_streams); + state_[IDX_QUIC_SESSION_STATE_MAX_STREAMS_BIDI] = + static_cast(max_streams); +} + +// Extends the stream-level flow control by the given number of bytes. +void QuicSession::ExtendStreamOffset(int64_t stream_id, size_t amount) { + Debug(this, "Extending max stream %" PRId64 " offset by %" PRId64 " bytes", + stream_id, amount); + ngtcp2_conn_extend_max_stream_offset( + connection(), + stream_id, + amount); +} + +// Extends the connection-level flow control for the entire session by +// the given number of bytes. +void QuicSession::ExtendOffset(size_t amount) { + Debug(this, "Extending session offset by %" PRId64 " bytes", amount); + ngtcp2_conn_extend_max_offset(connection(), amount); +} + +// Copies the local transport params into the given struct for serialization. +void QuicSession::GetLocalTransportParams(ngtcp2_transport_params* params) { + CHECK(!is_flag_set(QUICSESSION_FLAG_DESTROYED)); + ngtcp2_conn_get_local_transport_params(connection(), params); +} + +// Gets the QUIC version negotiated for this QuicSession +uint32_t QuicSession::negotiated_version() const { + CHECK(!is_flag_set(QUICSESSION_FLAG_DESTROYED)); + return ngtcp2_conn_get_negotiated_version(connection()); +} + +// The HandshakeCompleted function is called by ngtcp2 once it +// determines that the TLS Handshake is done. The only thing we +// need to do at this point is let the javascript side know. +void QuicSession::HandshakeCompleted() { + RemoteTransportParamsDebug transport_params(this); + Debug(this, "Handshake is completed. %s", transport_params); + RecordTimestamp(&QuicSessionStats::handshake_completed_at); + if (is_server()) HandshakeConfirmed(); + listener()->OnHandshakeCompleted(); +} + +void QuicSession::HandshakeConfirmed() { + Debug(this, "Handshake is confirmed"); + RecordTimestamp(&QuicSessionStats::handshake_confirmed_at); + state_[IDX_QUIC_SESSION_STATE_HANDSHAKE_CONFIRMED] = 1; +} + +bool QuicSession::is_handshake_completed() const { + DCHECK(!is_flag_set(QUICSESSION_FLAG_DESTROYED)); + return ngtcp2_conn_get_handshake_completed(connection()); +} + +void QuicSession::InitApplication() { + Debug(this, "Initializing application handler for ALPN %s", + alpn().c_str() + 1); + application_->Initialize(); +} + +// When a QuicSession hits the idle timeout, it is to be silently and +// immediately closed without attempting to send any additional data to +// the peer. All existing streams are abandoned and closed. +void QuicSession::OnIdleTimeout() { + if (!is_flag_set(QUICSESSION_FLAG_DESTROYED)) { + state_[IDX_QUIC_SESSION_STATE_IDLE_TIMEOUT] = 1; + Debug(this, "Idle timeout"); + SilentClose(); + } +} + +// Captures the error code and family information from a received +// connection close frame. +void QuicSession::GetConnectionCloseInfo() { + ngtcp2_connection_close_error_code close_code; + ngtcp2_conn_get_connection_close_error_code(connection(), &close_code); + set_last_error(QuicError(close_code)); +} + +// Removes the given connection id from the QuicSession. +void QuicSession::RemoveConnectionID(const QuicCID& cid) { + if (!is_flag_set(QUICSESSION_FLAG_DESTROYED)) + DisassociateCID(cid); +} + +QuicCID QuicSession::dcid() const { + return QuicCID(ngtcp2_conn_get_dcid(connection())); +} + +// The retransmit timer allows us to trigger retransmission +// of packets in case they are considered lost. The exact amount +// of time is determined internally by ngtcp2 according to the +// guidelines established by the QUIC spec but we use a libuv +// timer to actually monitor. Here we take the calculated timeout +// and extend out the libuv timer. +void QuicSession::UpdateRetransmitTimer(uint64_t timeout) { + DCHECK_NOT_NULL(retransmit_); + retransmit_->Update(timeout); +} + +void QuicSession::CheckAllocatedSize(size_t previous_size) const { + CHECK_GE(current_ngtcp2_memory_, previous_size); +} + +void QuicSession::IncreaseAllocatedSize(size_t size) { + current_ngtcp2_memory_ += size; +} + +void QuicSession::DecreaseAllocatedSize(size_t size) { + current_ngtcp2_memory_ -= size; +} + +uint64_t QuicSession::max_data_left() const { + return ngtcp2_conn_get_max_data_left(connection()); +} + +uint64_t QuicSession::max_local_streams_uni() const { + return ngtcp2_conn_get_max_local_streams_uni(connection()); +} + +void QuicSession::set_last_error(QuicError error) { + last_error_ = error; +} + +void QuicSession::set_last_error(int32_t family, uint64_t code) { + set_last_error({ family, code }); +} + +void QuicSession::set_last_error(int32_t family, int code) { + set_last_error({ family, code }); +} + +bool QuicSession::is_in_closing_period() const { + return ngtcp2_conn_is_in_closing_period(connection()); +} + +bool QuicSession::is_in_draining_period() const { + return ngtcp2_conn_is_in_draining_period(connection()); +} + +bool QuicSession::HasStream(int64_t id) const { + return streams_.find(id) != std::end(streams_); +} + +bool QuicSession::allow_early_data() const { + // TODO(@jasnell): For now, we always allow early data. + // Later there will be reasons we do not want to allow + // it, such as lack of available system resources. + return true; +} + +void QuicSession::SetSessionTicketAppData( + const SessionTicketAppData& app_data) { + application_->SetSessionTicketAppData(app_data); +} + +SessionTicketAppData::Status QuicSession::GetSessionTicketAppData( + const SessionTicketAppData& app_data, + SessionTicketAppData::Flag flag) { + return application_->GetSessionTicketAppData(app_data, flag); +} + +bool QuicSession::is_gracefully_closing() const { + return is_flag_set(QUICSESSION_FLAG_GRACEFUL_CLOSING); +} + +bool QuicSession::is_destroyed() const { + return is_flag_set(QUICSESSION_FLAG_DESTROYED); +} + +bool QuicSession::is_stateless_reset() const { + return is_flag_set(QUICSESSION_FLAG_STATELESS_RESET); +} + +bool QuicSession::is_server() const { + return crypto_context_->side() == NGTCP2_CRYPTO_SIDE_SERVER; +} + +void QuicSession::StartGracefulClose() { + set_flag(QUICSESSION_FLAG_GRACEFUL_CLOSING); + RecordTimestamp(&QuicSessionStats::closing_at); +} + +// The connection ID Strategy is a function that generates +// connection ID values. By default these are generated randomly. +void QuicSession::set_connection_id_strategy(ConnectionIDStrategy strategy) { + CHECK_NOT_NULL(strategy); + connection_id_strategy_ = strategy; +} + +void QuicSession::set_preferred_address_strategy( + PreferredAddressStrategy strategy) { + preferred_address_strategy_ = strategy; +} + +QuicSocket* QuicSession::socket() const { + return socket_.get(); +} + +// Indicates that the stream is blocked from transmitting any +// data. The specific handling of this is application specific. +// By default, we keep track of statistics but leave it up to +// the application to perform specific handling. +void QuicSession::StreamDataBlocked(int64_t stream_id) { + IncrementStat(&QuicSessionStats::block_count); + listener_->OnStreamBlocked(stream_id); +} + +// When a server advertises a preferred address in its initial +// transport parameters, ngtcp2 on the client side will trigger +// the OnSelectPreferredAdddress callback which will call this. +// The paddr argument contains the advertised preferred address. +// If the new address is going to be used, it needs to be copied +// over to dest, otherwise dest is left alone. There are two +// possible strategies that we currently support via user +// configuration: use the preferred address or ignore it. +void QuicSession::SelectPreferredAddress( + const PreferredAddress& preferred_address) { + CHECK(!is_server()); + preferred_address_strategy_(this, preferred_address); +} + +// This variant of SendPacket is used by QuicApplication +// instances to transmit a packet and update the network +// path used at the same time. +bool QuicSession::SendPacket( + std::unique_ptr packet, + const ngtcp2_path_storage& path) { + UpdateEndpoint(path.path); + return SendPacket(std::move(packet)); +} + +void QuicSession::set_local_address(const ngtcp2_addr* addr) { + DCHECK(!is_flag_set(QUICSESSION_FLAG_DESTROYED)); + ngtcp2_conn_set_local_addr(connection(), addr); +} + +// Set the transport parameters received from the remote peer +void QuicSession::set_remote_transport_params() { + DCHECK(!is_flag_set(QUICSESSION_FLAG_DESTROYED)); + ngtcp2_conn_get_remote_transport_params(connection(), &transport_params_); + set_flag(QUICSESSION_FLAG_HAS_TRANSPORT_PARAMS); +} + +void QuicSession::StopIdleTimer() { + CHECK_NOT_NULL(idle_); + idle_->Stop(); +} + +void QuicSession::StopRetransmitTimer() { + CHECK_NOT_NULL(retransmit_); + retransmit_->Stop(); +} + +// Called by the OnVersionNegotiation callback when a version +// negotiation frame has been received by the client. The sv +// parameter is an array of versions supported by the remote peer. +void QuicSession::VersionNegotiation(const uint32_t* sv, size_t nsv) { + CHECK(!is_server()); + if (!is_flag_set(QUICSESSION_FLAG_DESTROYED)) + listener()->OnVersionNegotiation(NGTCP2_PROTO_VER, sv, nsv); +} + +// Every QUIC session has a remote address and local address. +// Those endpoints can change through the lifetime of a connection, +// so whenever a packet is successfully processed, or when a +// response is to be sent, we have to keep track of the path +// and update as we go. +void QuicSession::UpdateEndpoint(const ngtcp2_path& path) { + remote_address_.Update(path.remote.addr, path.remote.addrlen); + local_address_.Update(path.local.addr, path.local.addrlen); + + // If the updated remote address is IPv6, set the flow label + if (remote_address_.family() == AF_INET6) { + // TODO(@jasnell): Currently, this reuses the session reset secret. + // That may or may not be a good idea, we need to verify and may + // need to have a distinct secret for flow labels. + uint32_t flow_label = + GenerateFlowLabel( + local_address_, + remote_address_, + scid_, + socket()->session_reset_secret(), + NGTCP2_STATELESS_RESET_TOKENLEN); + remote_address_.set_flow_label(flow_label); + } +} + +// Submits information headers only if the selected application +// supports headers. +bool QuicSession::SubmitInformation( + int64_t stream_id, + v8::Local headers) { + return application_->SubmitInformation(stream_id, headers); +} + +// Submits initial headers only if the selected application +// supports headers. For http3, for instance, this is the +// method used to submit both request and response headers. +bool QuicSession::SubmitHeaders( + int64_t stream_id, + v8::Local headers, + uint32_t flags) { + return application_->SubmitHeaders(stream_id, headers, flags); +} + +// Submits trailing headers only if the selected application +// supports headers. +bool QuicSession::SubmitTrailers( + int64_t stream_id, + v8::Local headers) { + return application_->SubmitTrailers(stream_id, headers); +} + +// Submits a new push stream +BaseObjectPtr QuicSession::SubmitPush( + int64_t stream_id, + v8::Local headers) { + return application_->SubmitPush(stream_id, headers); +} + +} // namespace quic +} // namespace node + +#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS + +#endif // SRC_QUIC_NODE_QUIC_SESSION_INL_H_ diff --git a/src/quic/node_quic_session.cc b/src/quic/node_quic_session.cc new file mode 100644 index 00000000000000..66addb3133ab36 --- /dev/null +++ b/src/quic/node_quic_session.cc @@ -0,0 +1,3757 @@ +#include "node_quic_session-inl.h" // NOLINT(build/include) +#include "aliased_buffer.h" +#include "allocated_buffer-inl.h" +#include "debug_utils-inl.h" +#include "env-inl.h" +#include "node_crypto_common.h" +#include "ngtcp2/ngtcp2.h" +#include "ngtcp2/ngtcp2_crypto.h" +#include "ngtcp2/ngtcp2_crypto_openssl.h" +#include "node.h" +#include "node_buffer.h" +#include "node_crypto.h" +#include "node_errors.h" +#include "node_internals.h" +#include "node_http_common-inl.h" +#include "node_mem-inl.h" +#include "node_process.h" +#include "node_quic_buffer-inl.h" +#include "node_quic_crypto.h" +#include "node_quic_socket-inl.h" +#include "node_quic_stream-inl.h" +#include "node_quic_util-inl.h" +#include "node_quic_default_application.h" +#include "node_quic_http3_application.h" +#include "node_sockaddr-inl.h" +#include "v8.h" +#include "uv.h" + +#include +#include +#include + +namespace node { + +using crypto::EntropySource; +using crypto::SecureContext; + +using v8::Array; +using v8::ArrayBufferView; +using v8::Context; +using v8::Function; +using v8::FunctionCallbackInfo; +using v8::FunctionTemplate; +using v8::HandleScope; +using v8::Integer; +using v8::Local; +using v8::Number; +using v8::Object; +using v8::ObjectTemplate; +using v8::PropertyAttribute; +using v8::String; +using v8::Undefined; +using v8::Value; + +namespace quic { + +namespace { +void SetConfig(QuicState* quic_state, int idx, uint64_t* val) { + AliasedFloat64Array& buffer = quic_state->quicsessionconfig_buffer; + uint64_t flags = static_cast(buffer[IDX_QUIC_SESSION_CONFIG_COUNT]); + if (flags & (1ULL << idx)) + *val = static_cast(buffer[idx]); +} + +// Forwards detailed(verbose) debugging information from ngtcp2. Enabled using +// the NODE_DEBUG_NATIVE=NGTCP2_DEBUG category. +void Ngtcp2DebugLog(void* user_data, const char* fmt, ...) { + va_list ap; + va_start(ap, fmt); + std::string format(fmt, strlen(fmt) + 1); + format[strlen(fmt)] = '\n'; + // Debug() does not work with the va_list here. So we use vfprintf + // directly instead. Ngtcp2DebugLog is only enabled when the debug + // category is enabled. + vfprintf(stderr, format.c_str(), ap); + va_end(ap); +} + +void CopyPreferredAddress( + uint8_t* dest, + size_t destlen, + uint16_t* port, + const sockaddr* addr) { + const sockaddr_in* src = reinterpret_cast(addr); + memcpy(dest, &src->sin_addr, destlen); + *port = SocketAddress::GetPort(addr); +} + +} // namespace + +std::string QuicSession::RemoteTransportParamsDebug::ToString() const { + ngtcp2_transport_params params; + ngtcp2_conn_get_remote_transport_params(session->connection(), ¶ms); + std::string out = "Remote Transport Params:\n"; + out += " Ack Delay Exponent: " + + std::to_string(params.ack_delay_exponent) + "\n"; + out += " Active Connection ID Limit: " + + std::to_string(params.active_connection_id_limit) + "\n"; + out += " Disable Active Migration: " + + std::string(params.disable_active_migration ? "Yes" : "No") + "\n"; + out += " Initial Max Data: " + + std::to_string(params.initial_max_data) + "\n"; + out += " Initial Max Stream Data Bidi Local: " + + std::to_string(params.initial_max_stream_data_bidi_local) + "\n"; + out += " Initial Max Stream Data Bidi Remote: " + + std::to_string(params.initial_max_stream_data_bidi_remote) + "\n"; + out += " Initial Max Stream Data Uni: " + + std::to_string(params.initial_max_stream_data_uni) + "\n"; + out += " Initial Max Streams Bidi: " + + std::to_string(params.initial_max_streams_bidi) + "\n"; + out += " Initial Max Streams Uni: " + + std::to_string(params.initial_max_streams_uni) + "\n"; + out += " Max Ack Delay: " + + std::to_string(params.max_ack_delay) + "\n"; + out += " Max Idle Timeout: " + + std::to_string(params.max_idle_timeout) + "\n"; + out += " Max Packet Size: " + + std::to_string(params.max_packet_size) + "\n"; + + if (!session->is_server()) { + if (params.original_connection_id_present) { + QuicCID cid(params.original_connection_id); + out += " Original Connection ID: " + cid.ToString() + "\n"; + } else { + out += " Original Connection ID: N/A \n"; + } + + if (params.preferred_address_present) { + out += " Preferred Address Present: Yes\n"; + // TODO(@jasnell): Serialize the IPv4 and IPv6 address options + } else { + out += " Preferred Address Present: No\n"; + } + + if (params.stateless_reset_token_present) { + StatelessResetToken token(params.stateless_reset_token); + out += " Stateless Reset Token: " + token.ToString() + "\n"; + } else { + out += " Stateless Reset Token: N/A"; + } + } + return out; +} + +void QuicSessionConfig::ResetToDefaults(QuicState* quic_state) { + ngtcp2_settings_default(this); + initial_ts = uv_hrtime(); + // Detailed(verbose) logging provided by ngtcp2 is only enabled + // when the NODE_DEBUG_NATIVE=NGTCP2_DEBUG category is used. + if (UNLIKELY(quic_state->env()->enabled_debug_list()->enabled( + DebugCategory::NGTCP2_DEBUG))) { + log_printf = Ngtcp2DebugLog; + } + transport_params.active_connection_id_limit = + DEFAULT_ACTIVE_CONNECTION_ID_LIMIT; + transport_params.initial_max_stream_data_bidi_local = + DEFAULT_MAX_STREAM_DATA_BIDI_LOCAL; + transport_params.initial_max_stream_data_bidi_remote = + DEFAULT_MAX_STREAM_DATA_BIDI_REMOTE; + transport_params.initial_max_stream_data_uni = + DEFAULT_MAX_STREAM_DATA_UNI; + transport_params.initial_max_streams_bidi = + DEFAULT_MAX_STREAMS_BIDI; + transport_params.initial_max_streams_uni = + DEFAULT_MAX_STREAMS_UNI; + transport_params.initial_max_data = DEFAULT_MAX_DATA; + transport_params.max_idle_timeout = DEFAULT_MAX_IDLE_TIMEOUT; + transport_params.max_packet_size = + NGTCP2_MAX_PKT_SIZE; + transport_params.max_ack_delay = + NGTCP2_DEFAULT_MAX_ACK_DELAY; + transport_params.disable_active_migration = 0; + transport_params.preferred_address_present = 0; + transport_params.stateless_reset_token_present = 0; +} + +// Sets the QuicSessionConfig using an AliasedBuffer for efficiency. +void QuicSessionConfig::Set( + QuicState* quic_state, + const sockaddr* preferred_addr) { + ResetToDefaults(quic_state); + SetConfig(quic_state, IDX_QUIC_SESSION_ACTIVE_CONNECTION_ID_LIMIT, + &transport_params.active_connection_id_limit); + SetConfig(quic_state, IDX_QUIC_SESSION_MAX_STREAM_DATA_BIDI_LOCAL, + &transport_params.initial_max_stream_data_bidi_local); + SetConfig(quic_state, IDX_QUIC_SESSION_MAX_STREAM_DATA_BIDI_REMOTE, + &transport_params.initial_max_stream_data_bidi_remote); + SetConfig(quic_state, IDX_QUIC_SESSION_MAX_STREAM_DATA_UNI, + &transport_params.initial_max_stream_data_uni); + SetConfig(quic_state, IDX_QUIC_SESSION_MAX_DATA, + &transport_params.initial_max_data); + SetConfig(quic_state, IDX_QUIC_SESSION_MAX_STREAMS_BIDI, + &transport_params.initial_max_streams_bidi); + SetConfig(quic_state, IDX_QUIC_SESSION_MAX_STREAMS_UNI, + &transport_params.initial_max_streams_uni); + SetConfig(quic_state, IDX_QUIC_SESSION_MAX_IDLE_TIMEOUT, + &transport_params.max_idle_timeout); + SetConfig(quic_state, IDX_QUIC_SESSION_MAX_PACKET_SIZE, + &transport_params.max_packet_size); + SetConfig(quic_state, IDX_QUIC_SESSION_MAX_ACK_DELAY, + &transport_params.max_ack_delay); + + transport_params.max_idle_timeout = + transport_params.max_idle_timeout * 1000000000; + + // TODO(@jasnell): QUIC allows both IPv4 and IPv6 addresses to be + // specified. Here we're specifying one or the other. Need to + // determine if that's what we want or should we support both. + if (preferred_addr != nullptr) { + transport_params.preferred_address_present = 1; + switch (preferred_addr->sa_family) { + case AF_INET: { + CopyPreferredAddress( + transport_params.preferred_address.ipv4_addr, + sizeof(transport_params.preferred_address.ipv4_addr), + &transport_params.preferred_address.ipv4_port, + preferred_addr); + break; + } + case AF_INET6: { + CopyPreferredAddress( + transport_params.preferred_address.ipv6_addr, + sizeof(transport_params.preferred_address.ipv6_addr), + &transport_params.preferred_address.ipv6_port, + preferred_addr); + break; + } + default: + UNREACHABLE(); + } + } +} + +void QuicSessionListener::OnKeylog(const char* line, size_t len) { + if (previous_listener_ != nullptr) + previous_listener_->OnKeylog(line, len); +} + +void QuicSessionListener::OnClientHello( + const char* alpn, + const char* server_name) { + if (previous_listener_ != nullptr) + previous_listener_->OnClientHello(alpn, server_name); +} + +QuicSessionListener::~QuicSessionListener() { + if (session_) + session_->RemoveListener(this); +} + +void QuicSessionListener::OnCert(const char* server_name) { + if (previous_listener_ != nullptr) + previous_listener_->OnCert(server_name); +} + +void QuicSessionListener::OnOCSP(Local ocsp) { + if (previous_listener_ != nullptr) + previous_listener_->OnOCSP(ocsp); +} + +void QuicSessionListener::OnStreamHeaders( + int64_t stream_id, + int kind, + const std::vector>& headers, + int64_t push_id) { + if (previous_listener_ != nullptr) + previous_listener_->OnStreamHeaders(stream_id, kind, headers, push_id); +} + +void QuicSessionListener::OnStreamClose( + int64_t stream_id, + uint64_t app_error_code) { + if (previous_listener_ != nullptr) + previous_listener_->OnStreamClose(stream_id, app_error_code); +} + +void QuicSessionListener::OnStreamReset( + int64_t stream_id, + uint64_t app_error_code) { + if (previous_listener_ != nullptr) + previous_listener_->OnStreamReset(stream_id, app_error_code); +} + +void QuicSessionListener::OnSessionDestroyed() { + if (previous_listener_ != nullptr) + previous_listener_->OnSessionDestroyed(); +} + +void QuicSessionListener::OnSessionClose(QuicError error) { + if (previous_listener_ != nullptr) + previous_listener_->OnSessionClose(error); +} + +void QuicSessionListener::OnStreamReady(BaseObjectPtr stream) { + if (previous_listener_ != nullptr) + previous_listener_->OnStreamReady(stream); +} + +void QuicSessionListener::OnHandshakeCompleted() { + if (previous_listener_ != nullptr) + previous_listener_->OnHandshakeCompleted(); +} + +void QuicSessionListener::OnPathValidation( + ngtcp2_path_validation_result res, + const sockaddr* local, + const sockaddr* remote) { + if (previous_listener_ != nullptr) + previous_listener_->OnPathValidation(res, local, remote); +} + +void QuicSessionListener::OnSessionTicket(int size, SSL_SESSION* session) { + if (previous_listener_ != nullptr) { + previous_listener_->OnSessionTicket(size, session); + } +} + +void QuicSessionListener::OnStreamBlocked(int64_t stream_id) { + if (previous_listener_ != nullptr) { + previous_listener_->OnStreamBlocked(stream_id); + } +} + +void QuicSessionListener::OnSessionSilentClose( + bool stateless_reset, + QuicError error) { + if (previous_listener_ != nullptr) + previous_listener_->OnSessionSilentClose(stateless_reset, error); +} + +void QuicSessionListener::OnUsePreferredAddress( + int family, + const PreferredAddress& preferred_address) { + if (previous_listener_ != nullptr) + previous_listener_->OnUsePreferredAddress(family, preferred_address); +} + +void QuicSessionListener::OnVersionNegotiation( + uint32_t supported_version, + const uint32_t* versions, + size_t vcnt) { + if (previous_listener_ != nullptr) + previous_listener_->OnVersionNegotiation(supported_version, versions, vcnt); +} + +void QuicSessionListener::OnQLog(const uint8_t* data, size_t len) { + if (previous_listener_ != nullptr) + previous_listener_->OnQLog(data, len); +} + +void JSQuicSessionListener::OnKeylog(const char* line, size_t len) { + Environment* env = session()->env(); + + HandleScope handle_scope(env->isolate()); + Context::Scope context_scope(env->context()); + Local line_bf = Buffer::Copy(env, line, 1 + len).ToLocalChecked(); + char* data = Buffer::Data(line_bf); + data[len] = '\n'; + + // Grab a shared pointer to this to prevent the QuicSession + // from being freed while the MakeCallback is running. + BaseObjectPtr ptr(session()); + session()->MakeCallback(env->quic_on_session_keylog_function(), 1, &line_bf); +} + +void JSQuicSessionListener::OnStreamBlocked(int64_t stream_id) { + Environment* env = session()->env(); + + HandleScope handle_scope(env->isolate()); + Context::Scope context_scope(env->context()); + BaseObjectPtr stream = session()->FindStream(stream_id); + stream->MakeCallback(env->quic_on_stream_blocked_function(), 0, nullptr); +} + +void JSQuicSessionListener::OnClientHello( + const char* alpn, + const char* server_name) { + + Environment* env = session()->env(); + HandleScope scope(env->isolate()); + Context::Scope context_scope(env->context()); + + Local argv[] = { + Undefined(env->isolate()), + Undefined(env->isolate()), + session()->crypto_context()->hello_ciphers().ToLocalChecked() + }; + + if (alpn != nullptr) { + argv[0] = String::NewFromUtf8( + env->isolate(), + alpn, + v8::NewStringType::kNormal).ToLocalChecked(); + } + if (server_name != nullptr) { + argv[1] = String::NewFromUtf8( + env->isolate(), + server_name, + v8::NewStringType::kNormal).ToLocalChecked(); + } + + // Grab a shared pointer to this to prevent the QuicSession + // from being freed while the MakeCallback is running. + BaseObjectPtr ptr(session()); + session()->MakeCallback( + env->quic_on_session_client_hello_function(), + arraysize(argv), argv); +} + +void JSQuicSessionListener::OnCert(const char* server_name) { + Environment* env = session()->env(); + HandleScope handle_scope(env->isolate()); + Context::Scope context_scope(env->context()); + + Local servername = Undefined(env->isolate()); + if (server_name != nullptr) { + servername = OneByteString( + env->isolate(), + server_name, + strlen(server_name)); + } + + // Grab a shared pointer to this to prevent the QuicSession + // from being freed while the MakeCallback is running. + BaseObjectPtr ptr(session()); + session()->MakeCallback(env->quic_on_session_cert_function(), 1, &servername); +} + +void JSQuicSessionListener::OnStreamHeaders( + int64_t stream_id, + int kind, + const std::vector>& headers, + int64_t push_id) { + Environment* env = session()->env(); + HandleScope scope(env->isolate()); + Context::Scope context_scope(env->context()); + MaybeStackBuffer, 16> head(headers.size()); + size_t n = 0; + for (const auto& header : headers) { + // name and value should never be empty here, and if + // they are, there's an actual bug so go ahead and crash + Local pair[] = { + header->GetName(session()->application()).ToLocalChecked(), + header->GetValue(session()->application()).ToLocalChecked() + }; + head[n++] = Array::New(env->isolate(), pair, arraysize(pair)); + } + Local argv[] = { + Number::New(env->isolate(), static_cast(stream_id)), + Array::New(env->isolate(), head.out(), n), + Integer::New(env->isolate(), kind), + Undefined(env->isolate()) + }; + if (kind == QUICSTREAM_HEADERS_KIND_PUSH) + argv[3] = Number::New(env->isolate(), static_cast(push_id)); + BaseObjectPtr ptr(session()); + session()->MakeCallback( + env->quic_on_stream_headers_function(), + arraysize(argv), argv); +} + +void JSQuicSessionListener::OnOCSP(Local ocsp) { + Environment* env = session()->env(); + HandleScope scope(env->isolate()); + Context::Scope context_scope(env->context()); + BaseObjectPtr ptr(session()); + session()->MakeCallback(env->quic_on_session_status_function(), 1, &ocsp); +} + +void JSQuicSessionListener::OnStreamClose( + int64_t stream_id, + uint64_t app_error_code) { + Environment* env = session()->env(); + HandleScope scope(env->isolate()); + Context::Scope context_scope(env->context()); + + Local argv[] = { + Number::New(env->isolate(), static_cast(stream_id)), + Number::New(env->isolate(), static_cast(app_error_code)) + }; + + // Grab a shared pointer to this to prevent the QuicSession + // from being freed while the MakeCallback is running. + BaseObjectPtr ptr(session()); + session()->MakeCallback( + env->quic_on_stream_close_function(), + arraysize(argv), + argv); +} + +void JSQuicSessionListener::OnStreamReset( + int64_t stream_id, + uint64_t app_error_code) { + Environment* env = session()->env(); + HandleScope scope(env->isolate()); + Context::Scope context_scope(env->context()); + + Local argv[] = { + Number::New(env->isolate(), static_cast(stream_id)), + Number::New(env->isolate(), static_cast(app_error_code)) + }; + // Grab a shared pointer to this to prevent the QuicSession + // from being freed while the MakeCallback is running. + BaseObjectPtr ptr(session()); + session()->MakeCallback( + env->quic_on_stream_reset_function(), + arraysize(argv), + argv); +} + +void JSQuicSessionListener::OnSessionDestroyed() { + Environment* env = session()->env(); + HandleScope scope(env->isolate()); + Context::Scope context_scope(env->context()); + // Emit the 'close' event in JS. This needs to happen after destroying the + // connection, because doing so also releases the last qlog data. + session()->MakeCallback( + env->quic_on_session_destroyed_function(), 0, nullptr); +} + +void JSQuicSessionListener::OnSessionClose(QuicError error) { + Environment* env = session()->env(); + HandleScope scope(env->isolate()); + Context::Scope context_scope(env->context()); + + Local argv[] = { + Number::New(env->isolate(), static_cast(error.code)), + Integer::New(env->isolate(), error.family) + }; + + // Grab a shared pointer to this to prevent the QuicSession + // from being freed while the MakeCallback is running. + BaseObjectPtr ptr(session()); + session()->MakeCallback( + env->quic_on_session_close_function(), + arraysize(argv), argv); +} + +void JSQuicSessionListener::OnStreamReady(BaseObjectPtr stream) { + Environment* env = session()->env(); + HandleScope scope(env->isolate()); + Context::Scope context_scope(env->context()); + Local argv[] = { + stream->object(), + Number::New(env->isolate(), static_cast(stream->id())), + Number::New(env->isolate(), static_cast(stream->push_id())) + }; + + // Grab a shared pointer to this to prevent the QuicSession + // from being freed while the MakeCallback is running. + BaseObjectPtr ptr(session()); + session()->MakeCallback( + env->quic_on_stream_ready_function(), + arraysize(argv), argv); +} + +void JSQuicSessionListener::OnHandshakeCompleted() { + Environment* env = session()->env(); + HandleScope scope(env->isolate()); + Context::Scope context_scope(env->context()); + + QuicCryptoContext* ctx = session()->crypto_context(); + Local servername = Undefined(env->isolate()); + const char* hostname = ctx->servername(); + if (hostname != nullptr) { + servername = + String::NewFromUtf8( + env->isolate(), + hostname, + v8::NewStringType::kNormal).ToLocalChecked(); + } + + int err = ctx->VerifyPeerIdentity( + hostname != nullptr ? + hostname : + session()->hostname().c_str()); + + Local argv[] = { + servername, + GetALPNProtocol(*session()), + ctx->cipher_name().ToLocalChecked(), + ctx->cipher_version().ToLocalChecked(), + Integer::New(env->isolate(), session()->max_pktlen_), + crypto::GetValidationErrorReason(env, err).ToLocalChecked(), + crypto::GetValidationErrorCode(env, err).ToLocalChecked(), + session()->crypto_context()->early_data() ? + v8::True(env->isolate()) : + v8::False(env->isolate()) + }; + + // Grab a shared pointer to this to prevent the QuicSession + // from being freed while the MakeCallback is running. + BaseObjectPtr ptr(session()); + session()->MakeCallback( + env->quic_on_session_handshake_function(), + arraysize(argv), + argv); +} + +void JSQuicSessionListener::OnPathValidation( + ngtcp2_path_validation_result res, + const sockaddr* local, + const sockaddr* remote) { + // This is a fairly expensive operation because both the local and + // remote addresses have to converted into JavaScript objects. We + // only do this if a pathValidation handler is registered. + Environment* env = session()->env(); + HandleScope scope(env->isolate()); + Local context = env->context(); + Context::Scope context_scope(context); + Local argv[] = { + Integer::New(env->isolate(), res), + AddressToJS(env, local), + AddressToJS(env, remote) + }; + // Grab a shared pointer to this to prevent the QuicSession + // from being freed while the MakeCallback is running. + BaseObjectPtr ptr(session()); + session()->MakeCallback( + env->quic_on_session_path_validation_function(), + arraysize(argv), + argv); +} + +void JSQuicSessionListener::OnSessionTicket(int size, SSL_SESSION* sess) { + Environment* env = session()->env(); + HandleScope scope(env->isolate()); + Context::Scope context_scope(env->context()); + + Local argv[] = { + v8::Undefined(env->isolate()), + v8::Undefined(env->isolate()) + }; + + if (size > 0) { + AllocatedBuffer session_ticket = + AllocatedBuffer::AllocateManaged(env, size); + unsigned char* session_data = + reinterpret_cast(session_ticket.data()); + memset(session_data, 0, size); + if (i2d_SSL_SESSION(sess, &session_data) > 0) + argv[0] = session_ticket.ToBuffer().ToLocalChecked(); + } + + if (session()->is_flag_set( + QuicSession::QUICSESSION_FLAG_HAS_TRANSPORT_PARAMS)) { + argv[1] = Buffer::Copy( + env, + reinterpret_cast(&session()->transport_params_), + sizeof(session()->transport_params_)).ToLocalChecked(); + } + // Grab a shared pointer to this to prevent the QuicSession + // from being freed while the MakeCallback is running. + BaseObjectPtr ptr(session()); + session()->MakeCallback( + env->quic_on_session_ticket_function(), + arraysize(argv), argv); +} + +void JSQuicSessionListener::OnSessionSilentClose( + bool stateless_reset, + QuicError error) { + Environment* env = session()->env(); + HandleScope scope(env->isolate()); + Context::Scope context_scope(env->context()); + + Local argv[] = { + stateless_reset ? v8::True(env->isolate()) : v8::False(env->isolate()), + Number::New(env->isolate(), static_cast(error.code)), + Integer::New(env->isolate(), error.family) + }; + + // Grab a shared pointer to this to prevent the QuicSession + // from being freed while the MakeCallback is running. + BaseObjectPtr ptr(session()); + session()->MakeCallback( + env->quic_on_session_silent_close_function(), arraysize(argv), argv); +} + +void JSQuicSessionListener::OnUsePreferredAddress( + int family, + const PreferredAddress& preferred_address) { + Environment* env = session()->env(); + HandleScope scope(env->isolate()); + Local context = env->context(); + Context::Scope context_scope(context); + + std::string hostname = family == AF_INET ? + preferred_address.ipv4_address(): + preferred_address.ipv6_address(); + uint16_t port = + family == AF_INET ? + preferred_address.ipv4_port() : + preferred_address.ipv6_port(); + + Local argv[] = { + OneByteString(env->isolate(), hostname.c_str()), + Integer::NewFromUnsigned(env->isolate(), port), + Integer::New(env->isolate(), family) + }; + + BaseObjectPtr ptr(session()); + session()->MakeCallback( + env->quic_on_session_use_preferred_address_function(), + arraysize(argv), argv); +} + +void JSQuicSessionListener::OnVersionNegotiation( + uint32_t supported_version, + const uint32_t* vers, + size_t vcnt) { + Environment* env = session()->env(); + HandleScope scope(env->isolate()); + Local context = env->context(); + Context::Scope context_scope(context); + + MaybeStackBuffer, 4> versions(vcnt); + for (size_t n = 0; n < vcnt; n++) + versions[n] = Integer::New(env->isolate(), vers[n]); + + // Currently, we only support one version of QUIC but in + // the future that may change. The callback below passes + // an array back to the JavaScript side to future-proof. + Local supported = + Integer::New(env->isolate(), supported_version); + + Local argv[] = { + Integer::New(env->isolate(), NGTCP2_PROTO_VER), + Array::New(env->isolate(), versions.out(), vcnt), + Array::New(env->isolate(), &supported, 1) + }; + + // Grab a shared pointer to this to prevent the QuicSession + // from being freed while the MakeCallback is running. + BaseObjectPtr ptr(session()); + session()->MakeCallback( + env->quic_on_session_version_negotiation_function(), + arraysize(argv), argv); +} + +void JSQuicSessionListener::OnQLog(const uint8_t* data, size_t len) { + Environment* env = session()->env(); + HandleScope handle_scope(env->isolate()); + Context::Scope context_scope(env->context()); + + Local str = OneByteString(env->isolate(), data, len); + session()->MakeCallback(env->quic_on_session_qlog_function(), 1, &str); +} + +// Generates a new random connection ID. +void QuicSession::RandomConnectionIDStrategy( + QuicSession* session, + ngtcp2_cid* cid, + size_t cidlen) { + // CID min and max length is determined by the QUIC specification. + CHECK_LE(cidlen, NGTCP2_MAX_CIDLEN); + CHECK_GE(cidlen, NGTCP2_MIN_CIDLEN); + cid->datalen = cidlen; + // cidlen shouldn't ever be zero here but just in case that + // behavior changes in ngtcp2 in the future... + if (LIKELY(cidlen > 0)) + EntropySource(cid->data, cidlen); +} + +// Check required capabilities were not excluded from the OpenSSL build: +// - OPENSSL_NO_SSL_TRACE excludes SSL_trace() +// - OPENSSL_NO_STDIO excludes BIO_new_fp() +// HAVE_SSL_TRACE is available on the internal tcp_wrap binding for the tests. +#if defined(OPENSSL_NO_SSL_TRACE) || defined(OPENSSL_NO_STDIO) +# define HAVE_SSL_TRACE 0 +#else +# define HAVE_SSL_TRACE 1 +#endif + + +void QuicCryptoContext::MemoryInfo(MemoryTracker* tracker) const { + tracker->TrackField("initial_crypto", handshake_[0]); + tracker->TrackField("handshake_crypto", handshake_[1]); + tracker->TrackField("app_crypto", handshake_[2]); + tracker->TrackField("ocsp_response", ocsp_response_); +} + +bool QuicCryptoContext::SetSecrets( + ngtcp2_crypto_level level, + const uint8_t* rx_secret, + const uint8_t* tx_secret, + size_t secretlen) { + + static constexpr int kCryptoKeylen = 64; + static constexpr int kCryptoIvlen = 64; + static constexpr char kQuicClientEarlyTrafficSecret[] = + "QUIC_CLIENT_EARLY_TRAFFIC_SECRET"; + static constexpr char kQuicClientHandshakeTrafficSecret[] = + "QUIC_CLIENT_HANDSHAKE_TRAFFIC_SECRET"; + static constexpr char kQuicClientTrafficSecret0[] = + "QUIC_CLIENT_TRAFFIC_SECRET_0"; + static constexpr char kQuicServerHandshakeTrafficSecret[] = + "QUIC_SERVER_HANDSHAKE_TRAFFIC_SECRET"; + static constexpr char kQuicServerTrafficSecret[] = + "QUIC_SERVER_TRAFFIC_SECRET_0"; + + uint8_t rx_key[kCryptoKeylen]; + uint8_t rx_hp[kCryptoKeylen]; + uint8_t tx_key[kCryptoKeylen]; + uint8_t tx_hp[kCryptoKeylen]; + uint8_t rx_iv[kCryptoIvlen]; + uint8_t tx_iv[kCryptoIvlen]; + + if (NGTCP2_ERR(ngtcp2_crypto_derive_and_install_key( + session()->connection(), + ssl_.get(), + rx_key, + rx_iv, + rx_hp, + tx_key, + tx_iv, + tx_hp, + level, + rx_secret, + tx_secret, + secretlen, + side_))) { + return false; + } + + switch (level) { + case NGTCP2_CRYPTO_LEVEL_EARLY: + crypto::LogSecret( + ssl_, + kQuicClientEarlyTrafficSecret, + rx_secret, + secretlen); + break; + case NGTCP2_CRYPTO_LEVEL_HANDSHAKE: + crypto::LogSecret( + ssl_, + kQuicClientHandshakeTrafficSecret, + rx_secret, + secretlen); + crypto::LogSecret( + ssl_, + kQuicServerHandshakeTrafficSecret, + tx_secret, + secretlen); + break; + case NGTCP2_CRYPTO_LEVEL_APP: + crypto::LogSecret( + ssl_, + kQuicClientTrafficSecret0, + rx_secret, + secretlen); + crypto::LogSecret( + ssl_, + kQuicServerTrafficSecret, + tx_secret, + secretlen); + break; + default: + UNREACHABLE(); + } + + return true; +} + +void QuicCryptoContext::AcknowledgeCryptoData( + ngtcp2_crypto_level level, + size_t datalen) { + // It is possible for the QuicSession to have been destroyed but not yet + // deconstructed. In such cases, we want to ignore the callback as there + // is nothing to do but wait for further cleanup to happen. + if (UNLIKELY(session_->is_destroyed())) + return; + Debug(session(), + "Acknowledging %d crypto bytes for %s level", + datalen, + crypto_level_name(level)); + + // Consumes (frees) the given number of bytes in the handshake buffer. + handshake_[level].Consume(datalen); + + // Update the statistics for the handshake, allowing us to track + // how long the handshake is taking to be acknowledged. A malicious + // peer could potentially force the QuicSession to hold on to + // crypto data for a long time by not sending an acknowledgement. + // The histogram will allow us to track the time periods between + // acknowlegements. + session()->RecordAck(&QuicSessionStats::handshake_acked_at); +} + +void QuicCryptoContext::EnableTrace() { +#if HAVE_SSL_TRACE + if (!bio_trace_) { + bio_trace_.reset(BIO_new_fp(stderr, BIO_NOCLOSE | BIO_FP_TEXT)); + SSL_set_msg_callback( + ssl_.get(), + [](int write_p, + int version, + int content_type, + const void* buf, + size_t len, + SSL* ssl, + void* arg) -> void { + crypto::MarkPopErrorOnReturn mark_pop_error_on_return; + SSL_trace(write_p, version, content_type, buf, len, ssl, arg); + }); + SSL_set_msg_callback_arg(ssl_.get(), bio_trace_.get()); + } +#endif +} + +// If a 'clientHello' event listener is registered on the JavaScript +// QuicServerSession object, the STATE_CLIENT_HELLO_ENABLED state +// will be set and the OnClientHello will cause the 'clientHello' +// event to be emitted. +// +// The 'clientHello' callback will be given it's own callback function +// that must be called when the client has completed handling the event. +// The handshake will not continue until it is called. +// +// The intent here is to allow user code the ability to modify or +// replace the SecurityContext based on the server name, ALPN, or +// other handshake characteristics. +// +// The user can also set a 'cert' event handler that will be called +// when the peer certificate is received, allowing additional tweaks +// and verifications to be performed. +int QuicCryptoContext::OnClientHello() { + if (LIKELY(session_->state_[ + IDX_QUIC_SESSION_STATE_CLIENT_HELLO_ENABLED] == 0)) { + return 0; + } + + TLSCallbackScope callback_scope(this); + + // Not an error but does suspend the handshake until we're ready to go. + // A callback function is passed to the JavaScript function below that + // must be called in order to turn QUICSESSION_FLAG_CLIENT_HELLO_CB_RUNNING + // off. Once that callback is invoked, the TLS Handshake will resume. + // It is recommended that the user not take a long time to invoke the + // callback in order to avoid stalling out the QUIC connection. + if (in_client_hello_) + return -1; + in_client_hello_ = true; + + QuicCryptoContext* ctx = session_->crypto_context(); + session_->listener()->OnClientHello( + ctx->hello_alpn(), + ctx->hello_servername()); + + // Returning -1 here will keep the TLS handshake paused until the + // client hello callback is invoked. Returning 0 means that the + // handshake is ready to proceed. When the OnClientHello callback + // is called above, it may be resolved synchronously or asynchronously. + // In case it is resolved synchronously, we need the check below. + return in_client_hello_ ? -1 : 0; +} + +// The OnCert callback provides an opportunity to prompt the server to +// perform on OCSP request on behalf of the client (when the client +// requests it). If there is a listener for the 'OCSPRequest' event +// on the JavaScript side, the IDX_QUIC_SESSION_STATE_CERT_ENABLED +// session state slot will equal 1, which will cause the callback to +// be invoked. The callback will be given a reference to a JavaScript +// function that must be called in order for the TLS handshake to +// continue. +int QuicCryptoContext::OnOCSP() { + if (LIKELY(session_->state_[IDX_QUIC_SESSION_STATE_CERT_ENABLED] == 0)) { + Debug(session(), "No OCSPRequest handler registered"); + return 1; + } + + Debug(session(), "Client is requesting an OCSP Response"); + TLSCallbackScope callback_scope(this); + + // As in node_crypto.cc, this is not an error, but does suspend the + // handshake to continue when OnOCSP is complete. + if (in_ocsp_request_) + return -1; + in_ocsp_request_ = true; + + session_->listener()->OnCert(session_->crypto_context()->servername()); + + // Returning -1 here means that we are still waiting for the OCSP + // request to be completed. When the OnCert handler is invoked + // above, it can be resolve synchronously or asynchonously. If + // resolved synchronously, we need the check below. + return in_ocsp_request_ ? -1 : 1; +} + +// The OnCertDone function is called by the QuicSessionOnCertDone +// function when usercode is done handling the OCSPRequest event. +void QuicCryptoContext::OnOCSPDone( + BaseObjectPtr context, + Local ocsp_response) { + Debug(session(), + "OCSPRequest completed. Context Provided? %s, OCSP Provided? %s", + context ? "Yes" : "No", + ocsp_response->IsArrayBufferView() ? "Yes" : "No"); + // Continue the TLS handshake when this function exits + // otherwise it will stall and fail. + TLSHandshakeScope handshake_scope(this, &in_ocsp_request_); + + // Disable the callback at this point so we don't loop continuously + session_->state_[IDX_QUIC_SESSION_STATE_CERT_ENABLED] = 0; + + if (context) { + int err = crypto::UseSNIContext(ssl_, context); + if (!err) { + unsigned long err = ERR_get_error(); // NOLINT(runtime/int) + return !err ? + THROW_ERR_QUIC_FAILURE_SETTING_SNI_CONTEXT(session_->env()) : + crypto::ThrowCryptoError(session_->env(), err); + } + } + + if (ocsp_response->IsArrayBufferView()) { + ocsp_response_.Reset( + session_->env()->isolate(), + ocsp_response.As()); + } +} + +// At this point in time, the TLS handshake secrets have been +// generated by openssl for this end of the connection and are +// ready to be used. Within this function, we need to install +// the secrets into the ngtcp2 connection object, store the +// remote transport parameters, and begin initialization of +// the QuicApplication that was selected. +bool QuicCryptoContext::OnSecrets( + ngtcp2_crypto_level level, + const uint8_t* rx_secret, + const uint8_t* tx_secret, + size_t secretlen) { + + auto maybe_init_app = OnScopeLeave([&]() { + if (level == NGTCP2_CRYPTO_LEVEL_APP) + session()->InitApplication(); + }); + + Debug(session(), + "Received secrets for %s crypto level", + crypto_level_name(level)); + + if (!SetSecrets(level, rx_secret, tx_secret, secretlen)) + return false; + + if (level == NGTCP2_CRYPTO_LEVEL_APP) + session_->set_remote_transport_params(); + + return true; +} + +// When the client has requested OSCP, this function will be called to provide +// the OSCP response. The OnCert() callback should have already been called +// by this point if any data is to be provided. If it hasn't, and ocsp_response_ +// is empty, no OCSP response will be sent. +int QuicCryptoContext::OnTLSStatus() { + Environment* env = session_->env(); + HandleScope scope(env->isolate()); + Context::Scope context_scope(env->context()); + switch (side_) { + case NGTCP2_CRYPTO_SIDE_SERVER: { + if (ocsp_response_.IsEmpty()) { + Debug(session(), "There is no OCSP response"); + return SSL_TLSEXT_ERR_NOACK; + } + + Local obj = + PersistentToLocal::Default( + env->isolate(), + ocsp_response_); + size_t len = obj->ByteLength(); + + unsigned char* data = crypto::MallocOpenSSL(len); + obj->CopyContents(data, len); + + Debug(session(), "There is an OCSP response of %d bytes", len); + + if (!SSL_set_tlsext_status_ocsp_resp(ssl_.get(), data, len)) + OPENSSL_free(data); + + ocsp_response_.Reset(); + + return SSL_TLSEXT_ERR_OK; + } + case NGTCP2_CRYPTO_SIDE_CLIENT: { + Local res; + if (ocsp_response().ToLocal(&res)) + session_->listener()->OnOCSP(res); + return 1; + } + default: + UNREACHABLE(); + } +} + +// Called by ngtcp2 when a chunk of peer TLS handshake data is received. +// For every chunk, we move the TLS handshake further along until it +// is complete. +int QuicCryptoContext::Receive( + ngtcp2_crypto_level crypto_level, + uint64_t offset, + const uint8_t* data, + size_t datalen) { + if (UNLIKELY(session_->is_destroyed())) + return NGTCP2_ERR_CALLBACK_FAILURE; + + // Statistics are collected so we can monitor how long the + // handshake is taking to operate and complete. + if (session_->GetStat(&QuicSessionStats::handshake_start_at) == 0) + session_->RecordTimestamp(&QuicSessionStats::handshake_start_at); + session_->RecordTimestamp(&QuicSessionStats::handshake_continue_at); + + Debug(session(), "Receiving %d bytes of crypto data", datalen); + + // Internally, this passes the handshake data off to openssl + // for processing. The handshake may or may not complete. + int ret = ngtcp2_crypto_read_write_crypto_data( + session_->connection(), + ssl_.get(), + crypto_level, + data, + datalen); + switch (ret) { + case 0: + return 0; + // In either of following cases, the handshake is being + // paused waiting for user code to take action (for instance + // OCSP requests or client hello modification) + case NGTCP2_CRYPTO_ERR_TLS_WANT_X509_LOOKUP: + Debug(session(), "TLS handshake wants X509 Lookup"); + return 0; + case NGTCP2_CRYPTO_ERR_TLS_WANT_CLIENT_HELLO_CB: + Debug(session(), "TLS handshake wants client hello callback"); + return 0; + default: + return ret; + } +} + +// Triggers key update to begin. This will fail and return false +// if either a previous key update is in progress and has not been +// confirmed or if the initial handshake has not yet been confirmed. +bool QuicCryptoContext::InitiateKeyUpdate() { + if (UNLIKELY(session_->is_destroyed())) + return false; + + // There's no user code that should be able to run while UpdateKey + // is running, but we need to gate on it just to be safe. + auto leave = OnScopeLeave([&]() { in_key_update_ = false; }); + CHECK(!in_key_update_); + in_key_update_ = true; + Debug(session(), "Initiating Key Update"); + + session_->IncrementStat(&QuicSessionStats::keyupdate_count); + + return ngtcp2_conn_initiate_key_update( + session_->connection(), + uv_hrtime()) == 0; +} + +int QuicCryptoContext::VerifyPeerIdentity(const char* hostname) { + int err = crypto::VerifyPeerCertificate(ssl_); + if (err) + return err; + + // QUIC clients are required to verify the peer identity, servers are not. + switch (side_) { + case NGTCP2_CRYPTO_SIDE_CLIENT: + if (LIKELY(is_option_set( + QUICCLIENTSESSION_OPTION_VERIFY_HOSTNAME_IDENTITY))) { + return VerifyHostnameIdentity(ssl_, hostname); + } + break; + case NGTCP2_CRYPTO_SIDE_SERVER: + // TODO(@jasnell): In the future, we may want to implement this but + // for now we keep things simple and skip peer identity verification. + break; + } + + return 0; +} + +// Write outbound TLS handshake data into the ngtcp2 connection +// to prepare it to be serialized. The outbound data must be +// stored in the handshake_ until it is acknowledged by the +// remote peer. It's important to keep in mind that there is +// a potential security risk here -- that is, a malicious peer +// can cause the local session to keep sent handshake data in +// memory by failing to acknowledge it or slowly acknowledging +// it. We currently do not track how much data is being buffered +// here but we do record statistics on how long the handshake +// data is foreced to be kept in memory. +void QuicCryptoContext::WriteHandshake( + ngtcp2_crypto_level level, + const uint8_t* data, + size_t datalen) { + Debug(session(), + "Writing %d bytes of %s handshake data.", + datalen, + crypto_level_name(level)); + std::unique_ptr buffer = + std::make_unique(datalen); + memcpy(buffer->out(), data, datalen); + session_->RecordTimestamp(&QuicSessionStats::handshake_send_at); + CHECK_EQ( + ngtcp2_conn_submit_crypto_data( + session_->connection(), + level, + buffer->out(), + datalen), 0); + handshake_[level].Push(std::move(buffer)); +} + +void QuicApplication::Acknowledge( + int64_t stream_id, + uint64_t offset, + size_t datalen) { + BaseObjectPtr stream = session()->FindStream(stream_id); + if (LIKELY(stream)) { + stream->Acknowledge(offset, datalen); + ResumeStream(stream_id); + } +} + +bool QuicApplication::SendPendingData() { + // The maximum number of packets to send per call + static constexpr size_t kMaxPackets = 16; + QuicPathStorage path; + std::unique_ptr packet; + uint8_t* pos = nullptr; + size_t packets_sent = 0; + int err; + + for (;;) { + ssize_t ndatalen; + StreamData stream_data; + err = GetStreamData(&stream_data); + if (err < 0) { + session()->set_last_error(QUIC_ERROR_APPLICATION, err); + return false; + } + + // If stream_data.id is -1, then we're not serializing any data for any + // specific stream. We still need to process QUIC session packets tho. + if (stream_data.id > -1) + Debug(session(), "Serializing packets for stream id %" PRId64, + stream_data.id); + else + Debug(session(), "Serializing session packets"); + + // If the packet was sent previously, then packet will have been reset. + if (!packet) { + packet = CreateStreamDataPacket(); + pos = packet->data(); + } + + ssize_t nwrite = WriteVStream(&path, pos, &ndatalen, stream_data); + + if (nwrite <= 0) { + switch (nwrite) { + case 0: + goto congestion_limited; + case NGTCP2_ERR_PKT_NUM_EXHAUSTED: + // There is a finite number of packets that can be sent + // per connection. Once those are exhausted, there's + // absolutely nothing we can do except immediately + // and silently tear down the QuicSession. This has + // to be silent because we can't even send a + // CONNECTION_CLOSE since even those require a + // packet number. + session()->SilentClose(); + return false; + case NGTCP2_ERR_STREAM_DATA_BLOCKED: + session()->StreamDataBlocked(stream_data.id); + if (session()->max_data_left() == 0) + goto congestion_limited; + // Fall through + case NGTCP2_ERR_STREAM_SHUT_WR: + if (UNLIKELY(!BlockStream(stream_data.id))) + return false; + continue; + case NGTCP2_ERR_STREAM_NOT_FOUND: + continue; + case NGTCP2_ERR_WRITE_STREAM_MORE: + CHECK_GT(ndatalen, 0); + CHECK(StreamCommit(&stream_data, ndatalen)); + pos += ndatalen; + continue; + } + session()->set_last_error(QUIC_ERROR_SESSION, static_cast(nwrite)); + return false; + } + + pos += nwrite; + + if (ndatalen >= 0) + CHECK(StreamCommit(&stream_data, ndatalen)); + + Debug(session(), "Sending %" PRIu64 " bytes in serialized packet", nwrite); + packet->set_length(nwrite); + if (!session()->SendPacket(std::move(packet), path)) + return false; + packet.reset(); + pos = nullptr; + MaybeSetFin(stream_data); + if (++packets_sent == kMaxPackets) + break; + } + return true; + + congestion_limited: + // We are either congestion limited or done. + if (pos - packet->data()) { + // Some data was serialized into the packet. We need to send it. + packet->set_length(pos - packet->data()); + Debug(session(), "Congestion limited, but %" PRIu64 " bytes pending", + packet->length()); + if (!session()->SendPacket(std::move(packet), path)) + return false; + } + return true; +} + +void QuicApplication::MaybeSetFin(const StreamData& stream_data) { + if (ShouldSetFin(stream_data)) + set_stream_fin(stream_data.id); +} + +void QuicApplication::StreamOpen(int64_t stream_id) { + Debug(session(), "QUIC Stream %" PRId64 " is open", stream_id); +} + +void QuicApplication::StreamHeaders( + int64_t stream_id, + int kind, + const std::vector>& headers, + int64_t push_id) { + session()->listener()->OnStreamHeaders(stream_id, kind, headers, push_id); +} + +void QuicApplication::StreamClose( + int64_t stream_id, + uint64_t app_error_code) { + session()->listener()->OnStreamClose(stream_id, app_error_code); +} + +void QuicApplication::StreamReset( + int64_t stream_id, + uint64_t app_error_code) { + session()->listener()->OnStreamReset(stream_id, app_error_code); +} + +// Determines which QuicApplication variant the QuicSession will be using +// based on the alpn configured for the application. For now, this is +// determined through configuration when tghe QuicSession is created +// and is not negotiable. In the future, we may allow it to be negotiated. +QuicApplication* QuicSession::SelectApplication(QuicSession* session) { + std::string alpn = session->alpn(); + if (alpn == NGTCP2_ALPN_H3) { + Debug(this, "Selecting HTTP/3 Application"); + return new Http3Application(session); + } + // In the future, we may end up supporting additional + // QUIC protocols. As they are added, extend the cases + // here to create and return them. + + Debug(this, "Selecting Default Application"); + return new DefaultApplication(session); +} + +// Server QuicSession Constructor +QuicSession::QuicSession( + QuicSocket* socket, + const QuicSessionConfig& config, + Local wrap, + const QuicCID& rcid, + const SocketAddress& local_addr, + const SocketAddress& remote_addr, + const QuicCID& dcid, + const QuicCID& ocid, + uint32_t version, + const std::string& alpn, + uint32_t options, + QlogMode qlog) + : QuicSession( + NGTCP2_CRYPTO_SIDE_SERVER, + socket, + wrap, + socket->server_secure_context(), + AsyncWrap::PROVIDER_QUICSERVERSESSION, + alpn, + std::string(""), // empty hostname. not used on server side + rcid, + options, + nullptr) { + // The config is copied by assignment in the call below. + InitServer(config, local_addr, remote_addr, dcid, ocid, version, qlog); +} + +// Client QuicSession Constructor +QuicSession::QuicSession( + QuicSocket* socket, + v8::Local wrap, + const SocketAddress& local_addr, + const SocketAddress& remote_addr, + BaseObjectPtr secure_context, + ngtcp2_transport_params* early_transport_params, + crypto::SSLSessionPointer early_session_ticket, + Local dcid, + PreferredAddressStrategy preferred_address_strategy, + const std::string& alpn, + const std::string& hostname, + uint32_t options, + QlogMode qlog) + : QuicSession( + NGTCP2_CRYPTO_SIDE_CLIENT, + socket, + wrap, + secure_context, + AsyncWrap::PROVIDER_QUICCLIENTSESSION, + alpn, + hostname, + QuicCID(), + options, + preferred_address_strategy) { + InitClient( + local_addr, + remote_addr, + early_transport_params, + std::move(early_session_ticket), + dcid, + qlog); +} + +// QuicSession is an abstract base class that defines the code used by both +// server and client sessions. +QuicSession::QuicSession( + ngtcp2_crypto_side side, + QuicSocket* socket, + Local wrap, + BaseObjectPtr secure_context, + AsyncWrap::ProviderType provider_type, + const std::string& alpn, + const std::string& hostname, + const QuicCID& rcid, + uint32_t options, + PreferredAddressStrategy preferred_address_strategy) + : AsyncWrap(socket->env(), wrap, provider_type), + StatsBase(socket->env(), wrap, + HistogramOptions::ACK | + HistogramOptions::RATE), + alloc_info_(MakeAllocator()), + socket_(socket), + alpn_(alpn), + hostname_(hostname), + idle_(new Timer(socket->env(), [this]() { OnIdleTimeout(); })), + retransmit_(new Timer(socket->env(), [this]() { MaybeTimeout(); })), + rcid_(rcid), + state_(env()->isolate(), IDX_QUIC_SESSION_STATE_COUNT), + quic_state_(socket->quic_state()) { + PushListener(&default_listener_); + set_connection_id_strategy(RandomConnectionIDStrategy); + set_preferred_address_strategy(preferred_address_strategy); + crypto_context_.reset( + new QuicCryptoContext( + this, + secure_context, + side, + options)); + application_.reset(SelectApplication(this)); + + // TODO(@jasnell): For now, the following is a check rather than properly + // handled. Before this code moves out of experimental, this should be + // properly handled. + wrap->DefineOwnProperty( + env()->context(), + env()->state_string(), + state_.GetJSArray(), + PropertyAttribute::ReadOnly).Check(); + + // TODO(@jasnell): memory accounting + // env_->isolate()->AdjustAmountOfExternalAllocatedMemory(kExternalSize); +} + +QuicSession::~QuicSession() { + CHECK(!Ngtcp2CallbackScope::InNgtcp2CallbackScope(this)); + crypto_context_->Cancel(); + connection_.reset(); + + QuicSessionListener* listener_ = listener(); + listener_->OnSessionDestroyed(); + if (listener_ == listener()) + RemoveListener(listener_); + + DebugStats(); +} + +template +void QuicSessionStatsTraits::ToString(const QuicSession& ptr, Fn&& add_field) { +#define V(_n, name, label) \ + add_field(label, ptr.GetStat(&QuicSessionStats::name)); + SESSION_STATS(V) +#undef V +} + +void QuicSession::PushListener(QuicSessionListener* listener) { + CHECK_NOT_NULL(listener); + CHECK(!listener->session_); + + listener->previous_listener_ = listener_; + listener->session_.reset(this); + + listener_ = listener; +} + +void QuicSession::RemoveListener(QuicSessionListener* listener) { + CHECK_NOT_NULL(listener); + + QuicSessionListener* previous; + QuicSessionListener* current; + + for (current = listener_, previous = nullptr; + /* No loop condition because we want a crash if listener is not found */ + ; previous = current, current = current->previous_listener_) { + CHECK_NOT_NULL(current); + if (current == listener) { + if (previous != nullptr) + previous->previous_listener_ = current->previous_listener_; + else + listener_ = listener->previous_listener_; + break; + } + } + + listener->session_.reset(); + listener->previous_listener_ = nullptr; +} + +// The diagnostic_name is used in Debug output. +std::string QuicSession::diagnostic_name() const { + return std::string("QuicSession ") + + (is_server() ? "Server" : "Client") + + " (" + alpn().substr(1) + ", " + + std::to_string(static_cast(get_async_id())) + ")"; +} + +// Locate the QuicStream with the given id or return nullptr +BaseObjectPtr QuicSession::FindStream(int64_t id) const { + auto it = streams_.find(id); + return it == std::end(streams_) ? BaseObjectPtr() : it->second; +} + +// Invoked when ngtcp2 receives an acknowledgement for stream data. +void QuicSession::AckedStreamDataOffset( + int64_t stream_id, + uint64_t offset, + size_t datalen) { + // It is possible for the QuicSession to have been destroyed but not yet + // deconstructed. In such cases, we want to ignore the callback as there + // is nothing to do but wait for further cleanup to happen. + if (LIKELY(!is_flag_set(QUICSESSION_FLAG_DESTROYED))) { + Debug(this, + "Received acknowledgement for %" PRIu64 + " bytes of stream %" PRId64 " data", + datalen, stream_id); + + application_->AcknowledgeStreamData(stream_id, offset, datalen); + } +} + +// Attaches the session to the given QuicSocket. The complexity +// here is that any CID's associated with the session have to +// be associated with the new QuicSocket. +void QuicSession::AddToSocket(QuicSocket* socket) { + CHECK_NOT_NULL(socket); + Debug(this, "Adding QuicSession to %s", socket->diagnostic_name()); + socket->AddSession(scid_, BaseObjectPtr(this)); + switch (crypto_context_->side()) { + case NGTCP2_CRYPTO_SIDE_SERVER: { + socket->AssociateCID(rcid_, scid_); + socket->AssociateCID(pscid_, scid_); + break; + } + case NGTCP2_CRYPTO_SIDE_CLIENT: { + std::vector cids(ngtcp2_conn_get_num_scid(connection())); + ngtcp2_conn_get_scid(connection(), cids.data()); + for (const ngtcp2_cid& cid : cids) { + socket->AssociateCID(QuicCID(&cid), scid_); + } + break; + } + default: + UNREACHABLE(); + } + + std::vector tokens( + ngtcp2_conn_get_num_active_dcid(connection())); + ngtcp2_conn_get_active_dcid(connection(), tokens.data()); + for (const ngtcp2_cid_token& token : tokens) { + if (token.token_present) { + socket->AssociateStatelessResetToken( + StatelessResetToken(token.token), + BaseObjectPtr(this)); + } + } +} + +// Add the given QuicStream to this QuicSession's collection of streams. All +// streams added must be removed before the QuicSession instance is freed. +void QuicSession::AddStream(BaseObjectPtr stream) { + DCHECK(!is_flag_set(QUICSESSION_FLAG_GRACEFUL_CLOSING)); + Debug(this, "Adding stream %" PRId64 " to session", stream->id()); + streams_.emplace(stream->id(), stream); + + // Update tracking statistics for the number of streams associated with + // this session. + switch (stream->origin()) { + case QuicStreamOrigin::QUIC_STREAM_CLIENT: + if (is_server()) + IncrementStat(&QuicSessionStats::streams_in_count); + else + IncrementStat(&QuicSessionStats::streams_out_count); + break; + case QuicStreamOrigin::QUIC_STREAM_SERVER: + if (is_server()) + IncrementStat(&QuicSessionStats::streams_out_count); + else + IncrementStat(&QuicSessionStats::streams_in_count); + } + IncrementStat(&QuicSessionStats::streams_out_count); + switch (stream->direction()) { + case QuicStreamDirection::QUIC_STREAM_BIRECTIONAL: + IncrementStat(&QuicSessionStats::bidi_stream_count); + break; + case QuicStreamDirection::QUIC_STREAM_UNIDIRECTIONAL: + IncrementStat(&QuicSessionStats::uni_stream_count); + break; + } +} + +// Like the silent close, the immediate close must start with +// the JavaScript side, first shutting down any existing +// streams before entering the closing period. Unlike silent +// close, however, all streams are closed using proper +// STOP_SENDING and RESET_STREAM frames and a CONNECTION_CLOSE +// frame is ultimately sent to the peer. This makes the +// naming a bit of a misnomer in that the connection is +// not immediately torn down, but is allowed to drain +// properly per the QUIC spec description of "immediate close". +void QuicSession::ImmediateClose() { + // Calling either ImmediateClose or SilentClose will cause + // the QUICSESSION_FLAG_CLOSING to be set. In either case, + // we should never re-enter ImmediateClose or SilentClose. + CHECK(!is_flag_set(QUICSESSION_FLAG_CLOSING)); + set_flag(QUICSESSION_FLAG_CLOSING); + + QuicError err = last_error(); + Debug(this, "Immediate close with code %" PRIu64 " (%s)", + err.code, + err.family_name()); + + listener()->OnSessionClose(err); +} + +// Creates a new stream object and passes it off to the javascript side. +// This has to be called from within a handlescope/contextscope. +BaseObjectPtr QuicSession::CreateStream(int64_t stream_id) { + CHECK(!is_flag_set(QUICSESSION_FLAG_DESTROYED)); + CHECK(!is_flag_set(QUICSESSION_FLAG_GRACEFUL_CLOSING)); + CHECK(!is_flag_set(QUICSESSION_FLAG_CLOSING)); + + BaseObjectPtr stream = QuicStream::New(this, stream_id); + CHECK(stream); + listener()->OnStreamReady(stream); + return stream; +} + +// Mark the QuicSession instance destroyed. After this is called, +// the QuicSession instance will be generally unusable but most +// likely will not be immediately freed. +void QuicSession::Destroy() { + if (is_flag_set(QUICSESSION_FLAG_DESTROYED)) + return; + + // If we're not in the closing or draining periods, + // then we should at least attempt to send a connection + // close to the peer. + if (!Ngtcp2CallbackScope::InNgtcp2CallbackScope(this) && + !is_in_closing_period() && + !is_in_draining_period()) { + Debug(this, "Making attempt to send a connection close"); + set_last_error(); + SendConnectionClose(); + } + + // Streams should have already been destroyed by this point. + CHECK(streams_.empty()); + + // Mark the session destroyed. + set_flag(QUICSESSION_FLAG_DESTROYED); + set_flag(QUICSESSION_FLAG_CLOSING, false); + set_flag(QUICSESSION_FLAG_GRACEFUL_CLOSING, false); + + // Stop and free the idle and retransmission timers if they are active. + StopIdleTimer(); + StopRetransmitTimer(); + + // The QuicSession instances are kept alive using + // BaseObjectPtr. The only persistent BaseObjectPtr + // is the map in the associated QuicSocket. Removing + // the QuicSession from the QuicSocket will free + // that pointer, allowing the QuicSession to be + // deconstructed once the stack unwinds and any + // remaining shared_ptr instances fall out of scope. + RemoveFromSocket(); +} + +// Generates and associates a new connection ID for this QuicSession. +// ngtcp2 will call this multiple times at the start of a new connection +// in order to build a pool of available CIDs. +bool QuicSession::GetNewConnectionID( + ngtcp2_cid* cid, + uint8_t* token, + size_t cidlen) { + if (is_flag_set(QUICSESSION_FLAG_DESTROYED)) + return false; + CHECK(!is_flag_set(QUICSESSION_FLAG_DESTROYED)); + CHECK_NOT_NULL(connection_id_strategy_); + connection_id_strategy_(this, cid, cidlen); + QuicCID cid_(cid); + StatelessResetToken(token, socket()->session_reset_secret(), cid_); + AssociateCID(cid_); + return true; +} + +void QuicSession::HandleError() { + if (is_destroyed() || (is_in_closing_period() && !is_server())) + return; + + if (!SendConnectionClose()) { + set_last_error(QUIC_ERROR_SESSION, NGTCP2_ERR_INTERNAL); + ImmediateClose(); + } +} + +// The the retransmit libuv timer fires, it will call MaybeTimeout, +// which determines whether or not we need to retransmit data to +// to packet loss or ack delay. +void QuicSession::MaybeTimeout() { + if (is_flag_set(QUICSESSION_FLAG_DESTROYED)) + return; + uint64_t now = uv_hrtime(); + bool transmit = false; + if (ngtcp2_conn_loss_detection_expiry(connection()) <= now) { + Debug(this, "Retransmitting due to loss detection"); + CHECK_EQ(ngtcp2_conn_on_loss_detection_timer(connection(), now), 0); + IncrementStat(&QuicSessionStats::loss_retransmit_count); + transmit = true; + } else if (ngtcp2_conn_ack_delay_expiry(connection()) <= now) { + Debug(this, "Retransmitting due to ack delay"); + ngtcp2_conn_cancel_expired_ack_delay_timer(connection(), now); + IncrementStat(&QuicSessionStats::ack_delay_retransmit_count); + transmit = true; + } + if (transmit) + SendPendingData(); +} + +bool QuicSession::OpenBidirectionalStream(int64_t* stream_id) { + DCHECK(!is_flag_set(QUICSESSION_FLAG_DESTROYED)); + DCHECK(!is_flag_set(QUICSESSION_FLAG_CLOSING)); + DCHECK(!is_flag_set(QUICSESSION_FLAG_GRACEFUL_CLOSING)); + return ngtcp2_conn_open_bidi_stream(connection(), stream_id, nullptr) == 0; +} + +bool QuicSession::OpenUnidirectionalStream(int64_t* stream_id) { + DCHECK(!is_flag_set(QUICSESSION_FLAG_DESTROYED)); + DCHECK(!is_flag_set(QUICSESSION_FLAG_CLOSING)); + DCHECK(!is_flag_set(QUICSESSION_FLAG_GRACEFUL_CLOSING)); + if (ngtcp2_conn_open_uni_stream(connection(), stream_id, nullptr)) + return false; + ngtcp2_conn_shutdown_stream_read(connection(), *stream_id, 0); + return true; +} + +// When ngtcp2 receives a successfull response to a PATH_CHALLENGE, +// it will trigger the OnPathValidation callback which will, in turn +// invoke this. There's really nothing to do here but update stats and +// and optionally notify the javascript side if there is a handler registered. +// Notifying the JavaScript side is purely informational. +void QuicSession::PathValidation( + const ngtcp2_path* path, + ngtcp2_path_validation_result res) { + if (res == NGTCP2_PATH_VALIDATION_RESULT_SUCCESS) { + IncrementStat(&QuicSessionStats::path_validation_success_count); + } else { + IncrementStat(&QuicSessionStats::path_validation_failure_count); + } + + // Only emit the callback if there is a handler for the pathValidation + // event on the JavaScript QuicSession object. + if (UNLIKELY(state_[IDX_QUIC_SESSION_STATE_PATH_VALIDATED_ENABLED] == 1)) { + listener_->OnPathValidation( + res, + reinterpret_cast(path->local.addr), + reinterpret_cast(path->remote.addr)); + } +} + +// Calling Ping will trigger the ngtcp2_conn to serialize any +// packets it currently has pending along with a probe frame +// that should keep the connection alive. This is a fire and +// forget and any errors that may occur will be ignored. The +// idle_timeout and retransmit timers will be updated. If Ping +// is called while processing an ngtcp2 callback, or if the +// closing or draining period has started, this is a non-op. +void QuicSession::Ping() { + if (Ngtcp2CallbackScope::InNgtcp2CallbackScope(this) || + is_flag_set(QUICSESSION_FLAG_DESTROYED) || + is_flag_set(QUICSESSION_FLAG_CLOSING) || + is_in_closing_period() || + is_in_draining_period()) { + return; + } + // TODO(@jasnell): We might want to revisit whether to handle + // errors right here. For now, we're ignoring them with the + // intent of capturing them elsewhere. + WritePackets("ping"); + UpdateIdleTimer(); + ScheduleRetransmit(); +} + +// A Retry will effectively restart the TLS handshake process +// by generating new initial crypto material. This should only ever +// be called on client sessions +bool QuicSession::ReceiveRetry() { + CHECK(!is_server()); + if (is_flag_set(QUICSESSION_FLAG_DESTROYED)) + return false; + Debug(this, "A retry packet was received. Restarting the handshake"); + IncrementStat(&QuicSessionStats::retry_count); + return DeriveAndInstallInitialKey(*this, dcid()); +} + +// When the QuicSocket receives a QUIC packet, it is forwarded on to here +// for processing. +bool QuicSession::Receive( + ssize_t nread, + const uint8_t* data, + const SocketAddress& local_addr, + const SocketAddress& remote_addr, + unsigned int flags) { + if (is_flag_set(QUICSESSION_FLAG_DESTROYED)) { + Debug(this, "Ignoring packet because session is destroyed"); + return false; + } + + Debug(this, "Receiving QUIC packet"); + IncrementStat(&QuicSessionStats::bytes_received, nread); + + // Closing period starts once ngtcp2 has detected that the session + // is being shutdown locally. Note that this is different that the + // is_flag_set(QUICSESSION_FLAG_GRACEFUL_CLOSING) function, which + // indicates a graceful shutdown that allows the session and streams + // to finish naturally. When is_in_closing_period is true, ngtcp2 is + // actively in the process of shutting down the connection and a + // CONNECTION_CLOSE has already been sent. The only thing we can do + // at this point is either ignore the packet or send another + // CONNECTION_CLOSE. + if (is_in_closing_period()) { + Debug(this, "QUIC packet received while in closing period"); + IncrementConnectionCloseAttempts(); + // For server QuicSession instances, we serialize the connection close + // packet once but may sent it multiple times. If the client keeps + // transmitting, then the connection close may have gotten lost. + // We don't want to send the connection close in response to + // every received packet, however, so we use an exponential + // backoff, increasing the ratio of packets received to connection + // close frame sent with every one we send. + if (!ShouldAttemptConnectionClose()) { + Debug(this, "Not sending connection close"); + return false; + } + Debug(this, "Sending connection close"); + return SendConnectionClose(); + } + + // When is_in_draining_period is true, ngtcp2 has received a + // connection close and we are simply discarding received packets. + // No outbound packets may be sent. Return true here because + // the packet was correctly processed, even tho it is being + // ignored. + if (is_in_draining_period()) { + Debug(this, "QUIC packet received while in draining period"); + return true; + } + + // It's possible for the remote address to change from one + // packet to the next so we have to look at the addr on + // every packet. + remote_address_ = remote_addr; + QuicPath path(local_addr, remote_address_); + + { + // These are within a scope to ensure that the InternalCallbackScope + // and HandleScope are both exited before continuing on with the + // function. This allows any nextTicks and queued tasks to be processed + // before we continue. + auto update_stats = OnScopeLeave([&](){ + UpdateDataStats(); + }); + Debug(this, "Processing received packet"); + HandleScope handle_scope(env()->isolate()); + InternalCallbackScope callback_scope(this); + if (!ReceivePacket(&path, data, nread)) { + Debug(this, "Failure processing received packet (code %" PRIu64 ")", + last_error().code); + HandleError(); + return false; + } + } + + if (is_destroyed()) { + Debug(this, "Session was destroyed while processing the received packet"); + // If the QuicSession has been destroyed but it is not + // in the closing period, a CONNECTION_CLOSE has not yet + // been sent to the peer. Let's attempt to send one. + if (!is_in_closing_period() && !is_in_draining_period()) { + Debug(this, "Attempting to send connection close"); + set_last_error(); + SendConnectionClose(); + } + return true; + } + + // Only send pending data if we haven't entered draining mode. + // We enter the draining period when a CONNECTION_CLOSE has been + // received from the remote peer. + if (is_in_draining_period()) { + Debug(this, "In draining period after processing packet"); + // If processing the packet puts us into draining period, there's + // absolutely nothing left for us to do except silently close + // and destroy this QuicSession. + GetConnectionCloseInfo(); + SilentClose(); + return true; + } + Debug(this, "Sending pending data after processing packet"); + SendPendingData(); + UpdateIdleTimer(); + UpdateRecoveryStats(); + Debug(this, "Successfully processed received packet"); + return true; +} + +// The ReceiveClientInitial function is called by ngtcp2 when +// a new connection has been initiated. The very first step to +// establishing a communication channel is to setup the keys +// that will be used to secure the communication. +bool QuicSession::ReceiveClientInitial(const QuicCID& dcid) { + if (UNLIKELY(is_flag_set(QUICSESSION_FLAG_DESTROYED))) + return false; + Debug(this, "Receiving client initial parameters"); + crypto_context_->handshake_started(); + return DeriveAndInstallInitialKey(*this, dcid); +} + +// Performs intake processing on a received QUIC packet. The received +// data is passed on to ngtcp2 for parsing and processing. ngtcp2 will, +// in turn, invoke a series of callbacks to handle the received packet. +bool QuicSession::ReceivePacket( + ngtcp2_path* path, + const uint8_t* data, + ssize_t nread) { + DCHECK(!Ngtcp2CallbackScope::InNgtcp2CallbackScope(this)); + + // If the QuicSession has been destroyed, we're not going + // to process any more packets for it. + if (is_flag_set(QUICSESSION_FLAG_DESTROYED)) + return true; + + uint64_t now = uv_hrtime(); + SetStat(&QuicSessionStats::received_at, now); + int err = ngtcp2_conn_read_pkt(connection(), path, data, nread, now); + if (err < 0) { + switch (err) { + // In either of the following two cases, the caller will + // handle the next steps... + case NGTCP2_ERR_DRAINING: + case NGTCP2_ERR_RECV_VERSION_NEGOTIATION: + break; + default: + // Per ngtcp2: If NGTCP2_ERR_RETRY is returned, + // QuicSession must be a server and must perform + // address validation by sending a Retry packet + // then immediately close the connection. + if (err == NGTCP2_ERR_RETRY && is_server()) { + socket()->SendRetry(scid_, rcid_, local_address_, remote_address_); + ImmediateClose(); + break; + } + set_last_error(QUIC_ERROR_SESSION, err); + return false; + } + } + return true; +} + +// Called by ngtcp2 when a chunk of stream data has been received. If +// the stream does not yet exist, it is created, then the data is +// forwarded on. +bool QuicSession::ReceiveStreamData( + int64_t stream_id, + int fin, + const uint8_t* data, + size_t datalen, + uint64_t offset) { + auto leave = OnScopeLeave([&]() { + // This extends the flow control window for the entire session + // but not for the individual Stream. Stream flow control is + // only expanded as data is read on the JavaScript side. + // TODO(jasnell): The strategy for extending the flow control + // window is going to be revisited soon. We don't need to + // extend on every chunk of data. + ExtendOffset(datalen); + }); + + // QUIC does not permit zero-length stream packets if + // fin is not set. ngtcp2 prevents these from coming + // through but just in case of regression in that impl, + // let's double check and simply ignore such packets + // so we do not commit any resources. + if (UNLIKELY(fin == 0 && datalen == 0)) + return true; + + if (is_flag_set(QUICSESSION_FLAG_DESTROYED)) + return false; + + HandleScope scope(env()->isolate()); + Context::Scope context_scope(env()->context()); + + // From here, we defer to the QuicApplication specific processing logic + return application_->ReceiveStreamData(stream_id, fin, data, datalen, offset); +} + +// Removes the QuicSession from the current socket. This is +// done with when the session is being destroyed or being +// migrated to another QuicSocket. It is important to keep in mind +// that the QuicSocket uses a BaseObjectPtr for the QuicSession. +// If the session is removed and there are no other references held, +// the session object will be destroyed automatically. +void QuicSession::RemoveFromSocket() { + CHECK(socket_); + Debug(this, "Removing QuicSession from %s", socket_->diagnostic_name()); + if (is_server()) { + socket_->DisassociateCID(rcid_); + socket_->DisassociateCID(pscid_); + } + + std::vector cids(ngtcp2_conn_get_num_scid(connection())); + std::vector tokens( + ngtcp2_conn_get_num_active_dcid(connection())); + ngtcp2_conn_get_scid(connection(), cids.data()); + ngtcp2_conn_get_active_dcid(connection(), tokens.data()); + + for (const ngtcp2_cid& cid : cids) + socket_->DisassociateCID(QuicCID(&cid)); + + for (const ngtcp2_cid_token& token : tokens) { + if (token.token_present) { + socket_->DisassociateStatelessResetToken( + StatelessResetToken(token.token)); + } + } + + Debug(this, "Removed from the QuicSocket"); + BaseObjectPtr socket = std::move(socket_); + socket->RemoveSession(scid_, remote_address_); +} + +// Removes the given stream from the QuicSession. All streams must +// be removed before the QuicSession is destroyed. +void QuicSession::RemoveStream(int64_t stream_id) { + Debug(this, "Removing stream %" PRId64, stream_id); + + // ngtcp2 does no extend the max streams count automatically + // except in very specific conditions, none of which apply + // once we've gotten this far. We need to manually extend when + // a remote peer initiated stream is removed. + if (!ngtcp2_conn_is_local_stream(connection_.get(), stream_id)) { + if (ngtcp2_is_bidi_stream(stream_id)) + ngtcp2_conn_extend_max_streams_bidi(connection_.get(), 1); + else + ngtcp2_conn_extend_max_streams_uni(connection_.get(), 1); + } + + // This will have the side effect of destroying the QuicStream + // instance. + streams_.erase(stream_id); + // Ensure that the stream state is closed and discarded by ngtcp2 + // Be sure to call this after removing the stream from the map + // above so that when ngtcp2 closes the stream, the callback does + // not attempt to loop back around and destroy the already removed + // QuicStream instance. Typically, the stream is already going to + // be closed by this point. + ngtcp2_conn_shutdown_stream(connection(), stream_id, NGTCP2_NO_ERROR); +} + +// The retransmit timer allows us to trigger retransmission +// of packets in case they are considered lost. The exact amount +// of time is determined internally by ngtcp2 according to the +// guidelines established by the QUIC spec but we use a libuv +// timer to actually monitor. +void QuicSession::ScheduleRetransmit() { + uint64_t now = uv_hrtime(); + uint64_t expiry = ngtcp2_conn_get_expiry(connection()); + uint64_t interval = (expiry - now) / 1000000UL; + if (expiry < now || interval == 0) interval = 1; + Debug(this, "Scheduling the retransmit timer for %" PRIu64, interval); + UpdateRetransmitTimer(interval); +} + +// Transmits either a protocol or application connection +// close to the peer. The choice of which is send is +// based on the current value of last_error_. +bool QuicSession::SendConnectionClose() { + CHECK(!Ngtcp2CallbackScope::InNgtcp2CallbackScope(this)); + + // Do not send any frames at all if we're in the draining period + // or in the middle of a silent close + if (is_in_draining_period() || is_flag_set(QUICSESSION_FLAG_SILENT_CLOSE)) + return true; + + // The specific handling of connection close varies for client + // and server QuicSession instances. For servers, we will + // serialize the connection close once but may end up transmitting + // it multiple times; whereas for clients, we will serialize it + // once and send once only. + QuicError error = last_error(); + + // If initial keys have not yet been installed, use the alternative + // ImmediateConnectionClose to send a stateless connection close to + // the peer. + if (crypto_context()->write_crypto_level() == + NGTCP2_CRYPTO_LEVEL_INITIAL) { + socket()->ImmediateConnectionClose( + dcid(), + scid_, + local_address_, + remote_address_, + error.code); + return true; + } + + switch (crypto_context_->side()) { + case NGTCP2_CRYPTO_SIDE_SERVER: { + // If we're not already in the closing period, + // first attempt to write any pending packets, then + // start the closing period. If closing period has + // already started, skip this. + if (!is_in_closing_period() && + (!WritePackets("server connection close - write packets") || + !StartClosingPeriod())) { + return false; + } + + UpdateIdleTimer(); + CHECK_GT(conn_closebuf_->length(), 0); + + return SendPacket(QuicPacket::Copy(conn_closebuf_)); + } + case NGTCP2_CRYPTO_SIDE_CLIENT: { + UpdateIdleTimer(); + auto packet = QuicPacket::Create("client connection close"); + + // If we're not already in the closing period, + // first attempt to write any pending packets, then + // start the closing period. Note that the behavior + // here is different than the server + if (!is_in_closing_period() && + !WritePackets("client connection close - write packets")) { + return false; + } + ssize_t nwrite = + SelectCloseFn(error.family)( + connection(), + nullptr, + packet->data(), + max_pktlen_, + error.code, + uv_hrtime()); + if (nwrite < 0) { + Debug(this, "Error writing connection close: %d", nwrite); + set_last_error(QUIC_ERROR_SESSION, static_cast(nwrite)); + return false; + } + packet->set_length(nwrite); + return SendPacket(std::move(packet)); + } + default: + UNREACHABLE(); + } +} + +void QuicSession::IgnorePreferredAddressStrategy( + QuicSession* session, + const PreferredAddress& preferred_address) { + Debug(session, "Ignoring server preferred address"); +} + +void QuicSession::UsePreferredAddressStrategy( + QuicSession* session, + const PreferredAddress& preferred_address) { + static constexpr int idx = + IDX_QUIC_SESSION_STATE_USE_PREFERRED_ADDRESS_ENABLED; + int family = session->socket()->local_address().family(); + if (preferred_address.Use(family)) { + Debug(session, "Using server preferred address"); + // Emit only if the QuicSession has a usePreferredAddress handler + // on the JavaScript side. + if (UNLIKELY(session->state_[idx] == 1)) { + session->listener()->OnUsePreferredAddress(family, preferred_address); + } + } else { + // If Use returns false, the advertised preferred address does not + // match the current local preferred endpoint IP version. + Debug(session, + "Not using server preferred address due to IP version mismatch"); + } +} + +// Passes a serialized packet to the associated QuicSocket. +bool QuicSession::SendPacket(std::unique_ptr packet) { + CHECK(!is_flag_set(QUICSESSION_FLAG_DESTROYED)); + CHECK(!is_in_draining_period()); + + // There's nothing to send. + if (packet->length() == 0) + return true; + + IncrementStat(&QuicSessionStats::bytes_sent, packet->length()); + RecordTimestamp(&QuicSessionStats::sent_at); + ScheduleRetransmit(); + + Debug(this, "Sending %" PRIu64 " bytes to %s from %s", + packet->length(), + remote_address_, + local_address_); + + int err = socket()->SendPacket( + local_address_, + remote_address_, + std::move(packet), + BaseObjectPtr(this)); + + if (err != 0) { + set_last_error(QUIC_ERROR_SESSION, err); + return false; + } + + return true; +} + +// Sends any pending handshake or session packet data. +void QuicSession::SendPendingData() { + // Do not proceed if: + // * We are in the ngtcp2 callback scope + // * The QuicSession has been destroyed + // * The QuicSession is in the draining period + // * The QuicSession is a server in the closing period + // * The QuicSession is not currently associated with a QuicSocket + if (Ngtcp2CallbackScope::InNgtcp2CallbackScope(this) || + is_destroyed() || + is_in_draining_period() || + (is_server() && is_in_closing_period()) || + socket() == nullptr) { + return; + } + + if (!application_->SendPendingData()) { + Debug(this, "Error sending QUIC application data"); + HandleError(); + } +} + +// When completing the TLS handshake, the TLS session information +// is provided to the QuicSession so that the session ticket and +// the remote transport parameters can be captured to support 0RTT +// session resumption. +int QuicSession::set_session(SSL_SESSION* session) { + CHECK(!is_server()); + CHECK(!is_flag_set(QUICSESSION_FLAG_DESTROYED)); + int size = i2d_SSL_SESSION(session, nullptr); + if (size > SecureContext::kMaxSessionSize) + return 0; + listener_->OnSessionTicket(size, session); + return 1; +} + +// A client QuicSession can be migrated to a different QuicSocket instance. +// TODO(@jasnell): This will be revisited. +bool QuicSession::set_socket(QuicSocket* socket, bool nat_rebinding) { + CHECK(!is_server()); + CHECK(!is_flag_set(QUICSESSION_FLAG_DESTROYED)); + + if (is_flag_set(QUICSESSION_FLAG_GRACEFUL_CLOSING)) + return false; + + if (socket == nullptr || socket == socket_.get()) + return true; + + Debug(this, "Migrating to %s", socket->diagnostic_name()); + + SendSessionScope send(this); + + // Ensure that we maintain a reference to keep this from being + // destroyed while we are starting the migration. + BaseObjectPtr ptr(this); + + // Step 1: Remove the session from the current socket + RemoveFromSocket(); + + // Step 2: Add this Session to the given Socket + AddToSocket(socket); + + // Step 3: Update the internal references and make sure + // we are listening. + socket_.reset(socket); + socket->ReceiveStart(); + + // Step 4: Update ngtcp2 + auto& local_address = socket->local_address(); + if (nat_rebinding) { + ngtcp2_addr addr; + ngtcp2_addr_init( + &addr, + local_address.data(), + local_address.length(), + nullptr); + ngtcp2_conn_set_local_addr(connection(), &addr); + } else { + QuicPath path(local_address, remote_address_); + if (ngtcp2_conn_initiate_migration( + connection(), + &path, + uv_hrtime()) != 0) { + return false; + } + } + + return true; +} + +void QuicSession::ResumeStream(int64_t stream_id) { + application()->ResumeStream(stream_id); +} + +void QuicSession::ResetStream(int64_t stream_id, uint64_t code) { + SendSessionScope scope(this); + CHECK_EQ(ngtcp2_conn_shutdown_stream(connection(), stream_id, code), 0); +} + +// Silent Close must start with the JavaScript side, which must +// clean up state, abort any still existing QuicSessions, then +// destroy the handle when done. The most important characteristic +// of the SilentClose is that no frames are sent to the peer. +// +// When a valid stateless reset is received, the connection is +// immediately and unrecoverably closed at the ngtcp2 level. +// Specifically, it will be put into the draining_period so +// absolutely no frames can be sent. What we need to do is +// notify the JavaScript side and destroy the connection with +// a flag set that indicates stateless reset. +void QuicSession::SilentClose() { + // Calling either ImmediateClose or SilentClose will cause + // the QUICSESSION_FLAG_CLOSING to be set. In either case, + // we should never re-enter ImmediateClose or SilentClose. + CHECK(!is_flag_set(QUICSESSION_FLAG_CLOSING)); + set_flag(QUICSESSION_FLAG_SILENT_CLOSE); + set_flag(QUICSESSION_FLAG_CLOSING); + + QuicError err = last_error(); + Debug(this, + "Silent close with %s code %" PRIu64 " (stateless reset? %s)", + err.family_name(), + err.code, + is_stateless_reset() ? "yes" : "no"); + + listener()->OnSessionSilentClose(is_stateless_reset(), err); +} +// Begin connection close by serializing the CONNECTION_CLOSE packet. +// There are two variants: one to serialize an application close, the +// other to serialize a protocol close. The frames are generally +// identical with the exception of a bit in the header. On server +// QuicSessions, we serialize the frame once and may retransmit it +// multiple times. On client QuicSession instances, we only ever +// serialize the connection close once. +bool QuicSession::StartClosingPeriod() { + if (is_flag_set(QUICSESSION_FLAG_DESTROYED)) + return false; + if (is_in_closing_period()) + return true; + + StopRetransmitTimer(); + UpdateIdleTimer(); + + QuicError error = last_error(); + Debug(this, "Closing period has started. Error %d", error.code); + + // Once the CONNECTION_CLOSE packet is written, + // is_in_closing_period will return true. + conn_closebuf_ = QuicPacket::Create( + "server connection close"); + ssize_t nwrite = + SelectCloseFn(error.family)( + connection(), + nullptr, + conn_closebuf_->data(), + max_pktlen_, + error.code, + uv_hrtime()); + if (nwrite < 0) { + if (nwrite == NGTCP2_ERR_PKT_NUM_EXHAUSTED) { + set_last_error(QUIC_ERROR_SESSION, NGTCP2_ERR_PKT_NUM_EXHAUSTED); + SilentClose(); + } else { + set_last_error(QUIC_ERROR_SESSION, static_cast(nwrite)); + } + return false; + } + conn_closebuf_->set_length(nwrite); + return true; +} + +// Called by ngtcp2 when a stream has been closed. If the stream does +// not exist, the close is ignored. +void QuicSession::StreamClose(int64_t stream_id, uint64_t app_error_code) { + if (is_flag_set(QUICSESSION_FLAG_DESTROYED)) + return; + + Debug(this, "Closing stream %" PRId64 " with code %" PRIu64, + stream_id, + app_error_code); + + application_->StreamClose(stream_id, app_error_code); +} + +// Called by ngtcp2 when a stream has been opened. All we do is log +// the activity here. We do not want to actually commit any resources +// until data is received for the stream. This allows us to prevent +// a stream commitment attack. The only exception is shutting the +// stream down explicitly if we are in a graceful close period. +void QuicSession::StreamOpen(int64_t stream_id) { + if (is_flag_set(QUICSESSION_FLAG_GRACEFUL_CLOSING)) { + ngtcp2_conn_shutdown_stream( + connection(), + stream_id, + NGTCP2_ERR_CLOSING); + } + Debug(this, "Stream %" PRId64 " opened", stream_id); + return application_->StreamOpen(stream_id); +} + +// Called when the QuicSession has received a RESET_STREAM frame from the +// peer, indicating that it will no longer send additional frames for the +// stream. If the stream is not yet known, reset is ignored. If the stream +// has already received a STREAM frame with fin set, the stream reset is +// ignored (the QUIC spec permits implementations to handle this situation +// however they want.) If the stream has not yet received a STREAM frame +// with the fin set, then the RESET_STREAM causes the readable side of the +// stream to be abruptly closed and any additional stream frames that may +// be received will be discarded if their offset is greater than final_size. +// On the JavaScript side, receiving a C is undistinguishable from +// a normal end-of-stream. No additional data events will be emitted, the +// end event will be emitted, and the readable side of the duplex will be +// closed. +// +// If the stream is still writable, no additional action is taken. If, +// however, the writable side of the stream has been closed (or was never +// open in the first place as in the case of peer-initiated unidirectional +// streams), the reset will cause the stream to be immediately destroyed. +void QuicSession::StreamReset( + int64_t stream_id, + uint64_t final_size, + uint64_t app_error_code) { + if (is_flag_set(QUICSESSION_FLAG_DESTROYED)) + return; + + Debug(this, + "Reset stream %" PRId64 " with code %" PRIu64 + " and final size %" PRIu64, + stream_id, + app_error_code, + final_size); + + BaseObjectPtr stream = FindStream(stream_id); + + if (stream) { + stream->set_final_size(final_size); + application_->StreamReset(stream_id, app_error_code); + } +} + +void QuicSession::UpdateConnectionID( + int type, + const QuicCID& cid, + const StatelessResetToken& token) { + switch (type) { + case NGTCP2_CONNECTION_ID_STATUS_TYPE_ACTIVATE: + socket_->AssociateStatelessResetToken( + token, + BaseObjectPtr(this)); + break; + case NGTCP2_CONNECTION_ID_STATUS_TYPE_DEACTIVATE: + socket_->DisassociateStatelessResetToken(token); + break; + } +} + +// Updates the idle timer timeout. If the idle timer fires, the connection +// will be silently closed. It is important to update this as activity +// occurs to keep the idle timer from firing. +void QuicSession::UpdateIdleTimer() { + CHECK_NOT_NULL(idle_); + uint64_t now = uv_hrtime(); + uint64_t expiry = ngtcp2_conn_get_idle_expiry(connection()); + // nano to millis + uint64_t timeout = expiry > now ? (expiry - now) / 1000000ULL : 1; + if (timeout == 0) timeout = 1; + Debug(this, "Updating idle timeout to %" PRIu64, timeout); + idle_->Update(timeout); +} + + +// Write any packets current pending for the ngtcp2 connection based on +// the current state of the QuicSession. If the QuicSession is in the +// closing period, only CONNECTION_CLOSE packets may be written. If the +// QuicSession is in the draining period, no packets may be written. +// +// Packets are flushed to the underlying QuicSocket uv_udp_t as soon +// as they are written. The WritePackets method may cause zero or more +// packets to be serialized. +// +// If there are any acks or retransmissions pending, those will be +// serialized at this point as well. However, WritePackets does not +// serialize stream data that is being sent initially. +bool QuicSession::WritePackets(const char* diagnostic_label) { + CHECK(!Ngtcp2CallbackScope::InNgtcp2CallbackScope(this)); + CHECK(!is_flag_set(QUICSESSION_FLAG_DESTROYED)); + + // During the draining period, we must not send any frames at all. + if (is_in_draining_period()) + return true; + + // During the closing period, we are only permitted to send + // CONNECTION_CLOSE frames. + if (is_in_closing_period()) { + return SendConnectionClose(); + } + + // Otherwise, serialize and send pending frames + QuicPathStorage path; + for (;;) { + auto packet = QuicPacket::Create(diagnostic_label, max_pktlen_); + // ngtcp2_conn_write_pkt will fill the created QuicPacket up + // as much as possible, and then should be called repeatedly + // until it returns 0 or fatally errors. On each call, it + // will return the number of bytes encoded into the packet. + ssize_t nwrite = + ngtcp2_conn_write_pkt( + connection(), + &path.path, + packet->data(), + max_pktlen_, + uv_hrtime()); + if (nwrite <= 0) { + switch (nwrite) { + case 0: + return true; + case NGTCP2_ERR_PKT_NUM_EXHAUSTED: + // There is a finite number of packets that can be sent + // per connection. Once those are exhausted, there's + // absolutely nothing we can do except immediately + // and silently tear down the QuicSession. This has + // to be silent because we can't even send a + // CONNECTION_CLOSE since even those require a + // packet number. + SilentClose(); + return false; + default: + set_last_error(QUIC_ERROR_SESSION, static_cast(nwrite)); + return false; + } + } + + packet->set_length(nwrite); + UpdateEndpoint(path.path); + UpdateDataStats(); + + if (!SendPacket(std::move(packet))) + return false; + } +} + +void QuicSession::MemoryInfo(MemoryTracker* tracker) const { + tracker->TrackField("crypto_context", crypto_context_.get()); + tracker->TrackField("alpn", alpn_); + tracker->TrackField("hostname", hostname_); + tracker->TrackField("idle", idle_); + tracker->TrackField("retransmit", retransmit_); + tracker->TrackField("streams", streams_); + tracker->TrackField("state", state_); + tracker->TrackFieldWithSize("current_ngtcp2_memory", current_ngtcp2_memory_); + tracker->TrackField("conn_closebuf", conn_closebuf_); + tracker->TrackField("application", application_); + StatsBase::StatsMemoryInfo(tracker); +} + +// Static function to create a new server QuicSession instance +BaseObjectPtr QuicSession::CreateServer( + QuicSocket* socket, + const QuicSessionConfig& config, + const QuicCID& rcid, + const SocketAddress& local_addr, + const SocketAddress& remote_addr, + const QuicCID& dcid, + const QuicCID& ocid, + uint32_t version, + const std::string& alpn, + uint32_t options, + QlogMode qlog) { + Local obj; + if (!socket->env() + ->quicserversession_instance_template() + ->NewInstance(socket->env()->context()).ToLocal(&obj)) { + return {}; + } + BaseObjectPtr session = + MakeDetachedBaseObject( + socket, + config, + obj, + rcid, + local_addr, + remote_addr, + dcid, + ocid, + version, + alpn, + options, + qlog); + + session->AddToSocket(socket); + return session; +} + +// Initializes a newly created server QuicSession. +void QuicSession::InitServer( + QuicSessionConfig config, + const SocketAddress& local_addr, + const SocketAddress& remote_addr, + const QuicCID& dcid, + const QuicCID& ocid, + uint32_t version, + QlogMode qlog) { + + CHECK_NULL(connection_); + + ExtendMaxStreamsBidi(DEFAULT_MAX_STREAMS_BIDI); + ExtendMaxStreamsUni(DEFAULT_MAX_STREAMS_UNI); + + local_address_ = local_addr; + remote_address_ = remote_addr; + max_pktlen_ = GetMaxPktLen(remote_addr); + + config.set_original_connection_id(ocid); + + connection_id_strategy_(this, scid_.cid(), kScidLen); + + config.GenerateStatelessResetToken(this, scid_); + config.GeneratePreferredAddressToken(connection_id_strategy_, this, &pscid_); + + QuicPath path(local_addr, remote_address_); + + // NOLINTNEXTLINE(readability/pointer_notation) + if (qlog == QlogMode::kEnabled) config.set_qlog({ *ocid, OnQlogWrite }); + + ngtcp2_conn* conn; + CHECK_EQ( + ngtcp2_conn_server_new( + &conn, + dcid.cid(), + scid_.cid(), + &path, + version, + &callbacks[crypto_context_->side()], + &config, + &alloc_info_, + static_cast(this)), 0); + + connection_.reset(conn); + + crypto_context_->Initialize(); + UpdateDataStats(); + UpdateIdleTimer(); +} + +namespace { +// A pointer to this function is passed to the JavaScript side during +// the client hello and is called by user code when the TLS handshake +// should resume. +void QuicSessionOnClientHelloDone(const FunctionCallbackInfo& args) { + QuicSession* session; + ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); + session->crypto_context()->OnClientHelloDone(); +} + +// This callback is invoked by user code after completing handling +// of the 'OCSPRequest' event. The callback is invoked with two +// possible arguments, both of which are optional +// 1. A replacement SecureContext +// 2. An OCSP response +void QuicSessionOnCertDone(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + QuicSession* session; + ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); + + Local cons = env->secure_context_constructor_template(); + SecureContext* context = nullptr; + if (args[0]->IsObject() && cons->HasInstance(args[0])) + context = Unwrap(args[0].As()); + session->crypto_context()->OnOCSPDone( + BaseObjectPtr(context), + args[1]); +} +} // namespace + +// Recovery stats are used to allow user code to keep track of +// important round-trip timing statistics that are updated through +// the lifetime of a connection. Effectively, these communicate how +// much time (from the perspective of the local peer) is being taken +// to exchange data reliably with the remote peer. +void QuicSession::UpdateRecoveryStats() { + const ngtcp2_rcvry_stat* stat = + ngtcp2_conn_get_rcvry_stat(connection()); + SetStat(&QuicSessionStats::min_rtt, stat->min_rtt); + SetStat(&QuicSessionStats::latest_rtt, stat->latest_rtt); + SetStat(&QuicSessionStats::smoothed_rtt, stat->smoothed_rtt); +} + +// Data stats are used to allow user code to keep track of important +// statistics such as amount of data in flight through the lifetime +// of a connection. +void QuicSession::UpdateDataStats() { + if (is_destroyed()) + return; + state_[IDX_QUIC_SESSION_STATE_MAX_DATA_LEFT] = + static_cast(ngtcp2_conn_get_max_data_left(connection())); + size_t bytes_in_flight = ngtcp2_conn_get_bytes_in_flight(connection()); + state_[IDX_QUIC_SESSION_STATE_BYTES_IN_FLIGHT] = + static_cast(bytes_in_flight); + // The max_bytes_in_flight is a highwater mark that can be used + // in performance analysis operations. + if (bytes_in_flight > GetStat(&QuicSessionStats::max_bytes_in_flight)) + SetStat(&QuicSessionStats::max_bytes_in_flight, bytes_in_flight); +} + +// Static method for creating a new client QuicSession instance. +BaseObjectPtr QuicSession::CreateClient( + QuicSocket* socket, + const SocketAddress& local_addr, + const SocketAddress& remote_addr, + BaseObjectPtr secure_context, + ngtcp2_transport_params* early_transport_params, + crypto::SSLSessionPointer early_session_ticket, + Local dcid, + PreferredAddressStrategy preferred_address_strategy, + const std::string& alpn, + const std::string& hostname, + uint32_t options, + QlogMode qlog) { + Local obj; + if (!socket->env() + ->quicclientsession_instance_template() + ->NewInstance(socket->env()->context()).ToLocal(&obj)) { + return {}; + } + + BaseObjectPtr session = + MakeDetachedBaseObject( + socket, + obj, + local_addr, + remote_addr, + secure_context, + early_transport_params, + std::move(early_session_ticket), + dcid, + preferred_address_strategy, + alpn, + hostname, + options, + qlog); + + session->AddToSocket(socket); + + return session; +} + +// Initialize a newly created client QuicSession. +// The early_transport_params and session_ticket are optional to +// perform a 0RTT resumption of a prior session. +// The dcid_value parameter is optional to allow user code the +// ability to provide an explicit dcid (this should be rare) +void QuicSession::InitClient( + const SocketAddress& local_addr, + const SocketAddress& remote_addr, + ngtcp2_transport_params* early_transport_params, + crypto::SSLSessionPointer early_session_ticket, + Local dcid_value, + QlogMode qlog) { + CHECK_NULL(connection_); + + local_address_ = local_addr; + remote_address_ = remote_addr; + Debug(this, "Initializing connection from %s to %s", + local_address_, + remote_address_); + + // The maximum packet length is determined largely + // by the IP version (IPv4 vs IPv6). Packet sizes + // should be limited to the maximum MTU necessary to + // prevent IP fragmentation. + max_pktlen_ = GetMaxPktLen(remote_address_); + + QuicSessionConfig config(quic_state()); + ExtendMaxStreamsBidi(DEFAULT_MAX_STREAMS_BIDI); + ExtendMaxStreamsUni(DEFAULT_MAX_STREAMS_UNI); + + connection_id_strategy_(this, scid_.cid(), NGTCP2_MAX_CIDLEN); + + ngtcp2_cid dcid; + if (dcid_value->IsArrayBufferView()) { + ArrayBufferViewContents sbuf( + dcid_value.As()); + CHECK_LE(sbuf.length(), NGTCP2_MAX_CIDLEN); + CHECK_GE(sbuf.length(), NGTCP2_MIN_CIDLEN); + memcpy(dcid.data, sbuf.data(), sbuf.length()); + dcid.datalen = sbuf.length(); + } else { + connection_id_strategy_(this, &dcid, NGTCP2_MAX_CIDLEN); + } + + QuicPath path(local_address_, remote_address_); + + if (qlog == QlogMode::kEnabled) config.set_qlog({ dcid, OnQlogWrite }); + + ngtcp2_conn* conn; + CHECK_EQ( + ngtcp2_conn_client_new( + &conn, + &dcid, + scid_.cid(), + &path, + NGTCP2_PROTO_VER, + &callbacks[crypto_context_->side()], + &config, + &alloc_info_, + static_cast(this)), 0); + + + connection_.reset(conn); + + crypto_context_->Initialize(); + + CHECK(DeriveAndInstallInitialKey(*this, this->dcid())); + + if (early_transport_params != nullptr) + ngtcp2_conn_set_early_remote_transport_params(conn, early_transport_params); + crypto_context_->set_session(std::move(early_session_ticket)); + + UpdateIdleTimer(); + UpdateDataStats(); +} + +// Static ngtcp2 callbacks are registered when ngtcp2 when a new ngtcp2_conn is +// created. These are static functions that, for the most part, simply defer to +// a QuicSession instance that is passed through as user_data. + +// Called by ngtcp2 upon creation of a new client connection +// to initiate the TLS handshake. This is only emitted on the client side. +int QuicSession::OnClientInitial( + ngtcp2_conn* conn, + void* user_data) { + QuicSession* session = static_cast(user_data); + if (UNLIKELY(session->is_destroyed())) + return NGTCP2_ERR_CALLBACK_FAILURE; + QuicSession::Ngtcp2CallbackScope callback_scope(session); + return NGTCP2_OK(session->crypto_context()->Receive( + NGTCP2_CRYPTO_LEVEL_INITIAL, + 0, nullptr, 0)) ? 0 : NGTCP2_ERR_CALLBACK_FAILURE; +} + +// Triggered by ngtcp2 when an initial handshake packet has been +// received. This is only invoked on server sessions and it is +// the absolute beginning of the communication between a client +// and a server. +int QuicSession::OnReceiveClientInitial( + ngtcp2_conn* conn, + const ngtcp2_cid* dcid, + void* user_data) { + QuicSession* session = static_cast(user_data); + if (UNLIKELY(session->is_destroyed())) + return NGTCP2_ERR_CALLBACK_FAILURE; + QuicSession::Ngtcp2CallbackScope callback_scope(session); + if (!session->ReceiveClientInitial(QuicCID(dcid))) { + Debug(session, "Receiving initial client handshake failed"); + return NGTCP2_ERR_CALLBACK_FAILURE; + } + return 0; +} + +// Called by ngtcp2 for both client and server connections when +// TLS handshake data has been received and needs to be processed. +// This will be called multiple times during the TLS handshake +// process and may be called during key updates. +int QuicSession::OnReceiveCryptoData( + ngtcp2_conn* conn, + ngtcp2_crypto_level crypto_level, + uint64_t offset, + const uint8_t* data, + size_t datalen, + void* user_data) { + QuicSession* session = static_cast(user_data); + if (UNLIKELY(session->is_destroyed())) + return NGTCP2_ERR_CALLBACK_FAILURE; + QuicSession::Ngtcp2CallbackScope callback_scope(session); + return static_cast( + session->crypto_context()->Receive(crypto_level, offset, data, datalen)); +} + +// Triggered by ngtcp2 when a RETRY packet has been received. This is +// only emitted on the client side (only a server can send a RETRY). +// +// Per the QUIC specification, a RETRY is essentially a mechanism for +// the server to force path validation at the very start of a connection +// at the cost of a single round trip. The RETRY includes a token that +// the client must use in subsequent requests. When received, the client +// MUST restart the TLS handshake and must include the RETRY token in +// all initial packets. If the initial packets contain a valid RETRY +// token, then the server assumes the path to be validated. Fortunately +// ngtcp2 handles the retry token for us, so all we have to do is +// regenerate the initial keying material and restart the handshake +// and we can ignore the retry parameter. +int QuicSession::OnReceiveRetry( + ngtcp2_conn* conn, + const ngtcp2_pkt_hd* hd, + const ngtcp2_pkt_retry* retry, + void* user_data) { + QuicSession* session = static_cast(user_data); + if (UNLIKELY(session->is_destroyed())) + return NGTCP2_ERR_CALLBACK_FAILURE; + QuicSession::Ngtcp2CallbackScope callback_scope(session); + if (!session->ReceiveRetry()) { + Debug(session, "Receiving retry token failed"); + return NGTCP2_ERR_CALLBACK_FAILURE; + } + return 0; +} + +// Called by ngtcp2 for both client and server connections +// when a request to extend the maximum number of bidirectional +// streams has been received. +int QuicSession::OnExtendMaxStreamsBidi( + ngtcp2_conn* conn, + uint64_t max_streams, + void* user_data) { + QuicSession* session = static_cast(user_data); + if (UNLIKELY(session->is_destroyed())) + return NGTCP2_ERR_CALLBACK_FAILURE; + QuicSession::Ngtcp2CallbackScope callback_scope(session); + session->ExtendMaxStreamsBidi(max_streams); + return 0; +} + +// Called by ngtcp2 for both client and server connections +// when a request to extend the maximum number of unidirectional +// streams has been received +int QuicSession::OnExtendMaxStreamsUni( + ngtcp2_conn* conn, + uint64_t max_streams, + void* user_data) { + QuicSession* session = static_cast(user_data); + if (UNLIKELY(session->is_destroyed())) + return NGTCP2_ERR_CALLBACK_FAILURE; + QuicSession::Ngtcp2CallbackScope callback_scope(session); + session->ExtendMaxStreamsUni(max_streams); + return 0; +} + +// Triggered by ngtcp2 when the local peer has received an +// indication from the remote peer indicating that additional +// unidirectional streams may be sent. The max_streams parameter +// identifies the highest unidirectional stream ID that may be +// opened. +int QuicSession::OnExtendMaxStreamsRemoteUni( + ngtcp2_conn* conn, + uint64_t max_streams, + void* user_data) { + QuicSession* session = static_cast(user_data); + if (UNLIKELY(session->is_destroyed())) + return NGTCP2_ERR_CALLBACK_FAILURE; + QuicSession::Ngtcp2CallbackScope callback_scope(session); + session->ExtendMaxStreamsRemoteUni(max_streams); + return 0; +} + +// Triggered by ngtcp2 when the local peer has received an +// indication from the remote peer indicating that additional +// bidirectional streams may be sent. The max_streams parameter +// identifies the highest bidirectional stream ID that may be +// opened. +int QuicSession::OnExtendMaxStreamsRemoteBidi( + ngtcp2_conn* conn, + uint64_t max_streams, + void* user_data) { + QuicSession* session = static_cast(user_data); + if (UNLIKELY(session->is_destroyed())) + return NGTCP2_ERR_CALLBACK_FAILURE; + QuicSession::Ngtcp2CallbackScope callback_scope(session); + session->ExtendMaxStreamsRemoteUni(max_streams); + return 0; +} + +// Triggered by ngtcp2 when the local peer has received a flow +// control signal from the remote peer indicating that additional +// data can be sent. The max_data parameter identifies the maximum +// data offset that may be sent. That is, a value of 99 means that +// out of a stream of 1000 bytes, only the first 100 may be sent. +// (offsets 0 through 99). +int QuicSession::OnExtendMaxStreamData( + ngtcp2_conn* conn, + int64_t stream_id, + uint64_t max_data, + void* user_data, + void* stream_user_data) { + QuicSession* session = static_cast(user_data); + if (UNLIKELY(session->is_destroyed())) + return NGTCP2_ERR_CALLBACK_FAILURE; + QuicSession::Ngtcp2CallbackScope callback_scope(session); + session->ExtendMaxStreamData(stream_id, max_data); + return 0; +} + +int QuicSession::OnConnectionIDStatus( + ngtcp2_conn* conn, + int type, + uint64_t seq, + const ngtcp2_cid* cid, + const uint8_t* token, + void* user_data) { + QuicSession* session = static_cast(user_data); + if (UNLIKELY(session->is_destroyed())) + return NGTCP2_ERR_CALLBACK_FAILURE; + + QuicCID qcid(cid); + Debug(session, "Updating connection ID %s (has reset token? %s)", + qcid, + token == nullptr ? "No" : "Yes"); + if (token != nullptr) + session->UpdateConnectionID(type, qcid, StatelessResetToken(token)); + return 0; +} + +// Called by ngtcp2 for both client and server connections +// when ngtcp2 has determined that the TLS handshake has +// been completed. It is important to understand that this +// is only an indication of the local peer's handshake state. +// The remote peer might not yet have completed its part +// of the handshake. +int QuicSession::OnHandshakeCompleted( + ngtcp2_conn* conn, + void* user_data) { + QuicSession* session = static_cast(user_data); + if (UNLIKELY(session->is_destroyed())) + return NGTCP2_ERR_CALLBACK_FAILURE; + QuicSession::Ngtcp2CallbackScope callback_scope(session); + session->HandshakeCompleted(); + return 0; +} + +// Called by ngtcp2 for clients when the handshake has been +// confirmed. Confirmation occurs *after* handshake completion. +int QuicSession::OnHandshakeConfirmed( + ngtcp2_conn* conn, + void* user_data) { + QuicSession* session = static_cast(user_data); + if (UNLIKELY(session->is_destroyed())) + return NGTCP2_ERR_CALLBACK_FAILURE; + QuicSession::Ngtcp2CallbackScope callback_scope(session); + session->HandshakeConfirmed(); + return 0; +} + +// Called by ngtcp2 when a chunk of stream data has been received. +// Currently, ngtcp2 ensures that this callback is always called +// with an offset parameter strictly larger than the previous call's +// offset + datalen (that is, data will never be delivered out of +// order). That behavior may change in the future but only via a +// configuration option. +int QuicSession::OnReceiveStreamData( + ngtcp2_conn* conn, + int64_t stream_id, + int fin, + uint64_t offset, + const uint8_t* data, + size_t datalen, + void* user_data, + void* stream_user_data) { + QuicSession* session = static_cast(user_data); + if (UNLIKELY(session->is_destroyed())) + return NGTCP2_ERR_CALLBACK_FAILURE; + QuicSession::Ngtcp2CallbackScope callback_scope(session); + return session->ReceiveStreamData(stream_id, fin, data, datalen, offset) ? + 0 : NGTCP2_ERR_CALLBACK_FAILURE; +} + +// Called by ngtcp2 when a new stream has been opened +int QuicSession::OnStreamOpen( + ngtcp2_conn* conn, + int64_t stream_id, + void* user_data) { + QuicSession* session = static_cast(user_data); + if (UNLIKELY(session->is_destroyed())) + return NGTCP2_ERR_CALLBACK_FAILURE; + session->StreamOpen(stream_id); + return 0; +} + +// Called by ngtcp2 when an acknowledgement for a chunk of +// TLS handshake data has been received by the remote peer. +// This is only an indication that data was received, not that +// it was successfully processed. Acknowledgements are a key +// part of the QUIC reliability mechanism. +int QuicSession::OnAckedCryptoOffset( + ngtcp2_conn* conn, + ngtcp2_crypto_level crypto_level, + uint64_t offset, + size_t datalen, + void* user_data) { + QuicSession* session = static_cast(user_data); + if (UNLIKELY(session->is_destroyed())) + return NGTCP2_ERR_CALLBACK_FAILURE; + QuicSession::Ngtcp2CallbackScope callback_scope(session); + session->crypto_context()->AcknowledgeCryptoData(crypto_level, datalen); + return 0; +} + +// Called by ngtcp2 when an acknowledgement for a chunk of +// stream data has been received successfully by the remote peer. +// This is only an indication that data was received, not that +// it was successfully processed. Acknowledgements are a key +// part of the QUIC reliability mechanism. +int QuicSession::OnAckedStreamDataOffset( + ngtcp2_conn* conn, + int64_t stream_id, + uint64_t offset, + size_t datalen, + void* user_data, + void* stream_user_data) { + QuicSession* session = static_cast(user_data); + if (UNLIKELY(session->is_destroyed())) + return NGTCP2_ERR_CALLBACK_FAILURE; + QuicSession::Ngtcp2CallbackScope callback_scope(session); + session->AckedStreamDataOffset(stream_id, offset, datalen); + return 0; +} + +// Called by ngtcp2 for a client connection when the server +// has indicated a preferred address in the transport +// params. +// For now, there are two modes: we can accept the preferred address +// or we can reject it. Later, we may want to implement a callback +// to ask the user if they want to accept the preferred address or +// not. +int QuicSession::OnSelectPreferredAddress( + ngtcp2_conn* conn, + ngtcp2_addr* dest, + const ngtcp2_preferred_addr* paddr, + void* user_data) { + QuicSession* session = static_cast(user_data); + if (UNLIKELY(session->is_destroyed())) + return NGTCP2_ERR_CALLBACK_FAILURE; + QuicSession::Ngtcp2CallbackScope callback_scope(session); + + // The paddr parameter contains the server advertised preferred + // address. The dest parameter contains the address that is + // actually being used. If the preferred address is selected, + // then the contents of paddr are copied over to dest. It is + // important to remember that SelectPreferredAddress should + // return true regardless of whether the preferred address was + // selected or not. It should only return false if there was + // an actual failure processing things. Note, however, that + // even in such a failure, we debug log and ignore it. + // If the preferred address is not selected, dest remains + // unchanged. + PreferredAddress preferred_address(session->env(), dest, paddr); + session->SelectPreferredAddress(preferred_address); + return 0; +} + +// Called by ngtcp2 when a stream has been closed. +int QuicSession::OnStreamClose( + ngtcp2_conn* conn, + int64_t stream_id, + uint64_t app_error_code, + void* user_data, + void* stream_user_data) { + QuicSession* session = static_cast(user_data); + if (UNLIKELY(session->is_destroyed())) + return NGTCP2_ERR_CALLBACK_FAILURE; + QuicSession::Ngtcp2CallbackScope callback_scope(session); + session->StreamClose(stream_id, app_error_code); + return 0; +} + +// Stream reset means the remote peer will no longer send data +// on the identified stream. It is essentially a premature close. +// The final_size parameter is important here in that it identifies +// exactly how much data the *remote peer* is aware that it sent. +// If there are lost packets, then the local peer's idea of the final +// size might not match. +int QuicSession::OnStreamReset( + ngtcp2_conn* conn, + int64_t stream_id, + uint64_t final_size, + uint64_t app_error_code, + void* user_data, + void* stream_user_data) { + QuicSession* session = static_cast(user_data); + if (UNLIKELY(session->is_destroyed())) + return NGTCP2_ERR_CALLBACK_FAILURE; + QuicSession::Ngtcp2CallbackScope callback_scope(session); + session->StreamReset(stream_id, final_size, app_error_code); + return 0; +} + +// Called by ngtcp2 when it needs to generate some random data. +// We currently do not use it, but the ngtcp2_rand_ctx identifies +// why the random data is necessary. When ctx is equal to +// NGTCP2_RAND_CTX_NONE, it typically means the random data +// is being used during the TLS handshake. When ctx is equal to +// NGTCP2_RAND_CTX_PATH_CHALLENGE, the random data is being +// used to construct a PATH_CHALLENGE. These *might* need more +// secure and robust random number generation given the +// sensitivity of PATH_CHALLENGE operations (an attacker +// could use a compromised PATH_CHALLENGE to trick an endpoint +// into redirecting traffic). +// TODO(@jasnell): In the future, we'll want to explore whether +// we want to handle the different cases of ngtcp2_rand_ctx +int QuicSession::OnRand( + ngtcp2_conn* conn, + uint8_t* dest, + size_t destlen, + ngtcp2_rand_ctx ctx, + void* user_data) { + EntropySource(dest, destlen); + return 0; +} + +// When a new client connection is established, ngtcp2 will call +// this multiple times to generate a pool of connection IDs to use. +int QuicSession::OnGetNewConnectionID( + ngtcp2_conn* conn, + ngtcp2_cid* cid, + uint8_t* token, + size_t cidlen, + void* user_data) { + QuicSession* session = static_cast(user_data); + if (UNLIKELY(session->is_destroyed())) + return NGTCP2_ERR_CALLBACK_FAILURE; + CHECK(!Ngtcp2CallbackScope::InNgtcp2CallbackScope(session)); + return session->GetNewConnectionID(cid, token, cidlen) ? + 0 : NGTCP2_ERR_CALLBACK_FAILURE; +} + +// When a connection is closed, ngtcp2 will call this multiple +// times to retire connection IDs. It's also possible for this +// to be called at times throughout the lifecycle of the connection +// to remove a CID from the availability pool. +int QuicSession::OnRemoveConnectionID( + ngtcp2_conn* conn, + const ngtcp2_cid* cid, + void* user_data) { + QuicSession* session = static_cast(user_data); + if (UNLIKELY(session->is_destroyed())) + return NGTCP2_ERR_CALLBACK_FAILURE; + session->RemoveConnectionID(QuicCID(cid)); + return 0; +} + +// Called by ngtcp2 to perform path validation. Path validation +// is necessary to ensure that a packet is originating from the +// expected source. If the res parameter indicates success, it +// means that the path specified has been verified as being +// valid. +// +// Validity here means only that there has been a successful +// exchange of PATH_CHALLENGE information between the peers. +// It's critical to understand that the validity of a path +// can change at any timee so this is only an indication of +// validity at a specific point in time. +int QuicSession::OnPathValidation( + ngtcp2_conn* conn, + const ngtcp2_path* path, + ngtcp2_path_validation_result res, + void* user_data) { + QuicSession* session = static_cast(user_data); + if (UNLIKELY(session->is_destroyed())) + return NGTCP2_ERR_CALLBACK_FAILURE; + QuicSession::Ngtcp2CallbackScope callback_scope(session); + session->PathValidation(path, res); + return 0; +} + +// Triggered by ngtcp2 when a version negotiation is received. +// What this means is that the remote peer does not support the +// QUIC version requested. The only thing we can do here (per +// the QUIC specification) is silently discard the connection +// and notify the JavaScript side that a different version of +// QUIC should be used. The sv parameter does list the QUIC +// versions advertised as supported by the remote peer. +int QuicSession::OnVersionNegotiation( + ngtcp2_conn* conn, + const ngtcp2_pkt_hd* hd, + const uint32_t* sv, + size_t nsv, + void* user_data) { + QuicSession* session = static_cast(user_data); + if (UNLIKELY(session->is_destroyed())) + return NGTCP2_ERR_CALLBACK_FAILURE; + QuicSession::Ngtcp2CallbackScope callback_scope(session); + session->VersionNegotiation(sv, nsv); + return 0; +} + +// Triggered by ngtcp2 when a stateless reset is received. What this +// means is that the remote peer might recognize the CID but has lost +// all state necessary to successfully process it. The only thing we +// can do is silently close the connection. For server sessions, this +// means all session state is shut down and discarded, even on the +// JavaScript side. For client sessions, we discard session state at +// the C++ layer but -- at least in the future -- we can retain some +// state at the JavaScript level to allow for automatic session +// resumption. +int QuicSession::OnStatelessReset( + ngtcp2_conn* conn, + const ngtcp2_pkt_stateless_reset* sr, + void* user_data) { + QuicSession* session = static_cast(user_data); + if (UNLIKELY(session->is_destroyed())) + return NGTCP2_ERR_CALLBACK_FAILURE; + session->set_flag(QUICSESSION_FLAG_STATELESS_RESET); + return 0; +} + +void QuicSession::OnQlogWrite(void* user_data, const void* data, size_t len) { + QuicSession* session = static_cast(user_data); + session->listener()->OnQLog(reinterpret_cast(data), len); +} + +const ngtcp2_conn_callbacks QuicSession::callbacks[2] = { + // NGTCP2_CRYPTO_SIDE_CLIENT + { + OnClientInitial, + nullptr, + OnReceiveCryptoData, + OnHandshakeCompleted, + OnVersionNegotiation, + ngtcp2_crypto_encrypt_cb, + ngtcp2_crypto_decrypt_cb, + ngtcp2_crypto_hp_mask_cb, + OnReceiveStreamData, + OnAckedCryptoOffset, + OnAckedStreamDataOffset, + OnStreamOpen, + OnStreamClose, + OnStatelessReset, + OnReceiveRetry, + OnExtendMaxStreamsBidi, + OnExtendMaxStreamsUni, + OnRand, + OnGetNewConnectionID, + OnRemoveConnectionID, + ngtcp2_crypto_update_key_cb, + OnPathValidation, + OnSelectPreferredAddress, + OnStreamReset, + OnExtendMaxStreamsRemoteBidi, + OnExtendMaxStreamsRemoteUni, + OnExtendMaxStreamData, + OnConnectionIDStatus, + OnHandshakeConfirmed + }, + // NGTCP2_CRYPTO_SIDE_SERVER + { + nullptr, + OnReceiveClientInitial, + OnReceiveCryptoData, + OnHandshakeCompleted, + nullptr, // recv_version_negotiation + ngtcp2_crypto_encrypt_cb, + ngtcp2_crypto_decrypt_cb, + ngtcp2_crypto_hp_mask_cb, + OnReceiveStreamData, + OnAckedCryptoOffset, + OnAckedStreamDataOffset, + OnStreamOpen, + OnStreamClose, + OnStatelessReset, + nullptr, // recv_retry + nullptr, // extend_max_streams_bidi + nullptr, // extend_max_streams_uni + OnRand, + OnGetNewConnectionID, + OnRemoveConnectionID, + ngtcp2_crypto_update_key_cb, + OnPathValidation, + nullptr, // select_preferred_addr + OnStreamReset, + OnExtendMaxStreamsRemoteBidi, + OnExtendMaxStreamsRemoteUni, + OnExtendMaxStreamData, + OnConnectionIDStatus, + nullptr, // handshake_confirmed + } +}; + +// JavaScript API + +namespace { +void QuicSessionSetSocket(const FunctionCallbackInfo& args) { + QuicSession* session; + QuicSocket* socket; + ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); + CHECK(args[0]->IsObject()); + ASSIGN_OR_RETURN_UNWRAP(&socket, args[0].As()); + args.GetReturnValue().Set(session->set_socket(socket)); +} + +// Perform an immediate close on the QuicSession, causing a +// CONNECTION_CLOSE frame to be scheduled and sent and starting +// the closing period for this session. The name "ImmediateClose" +// is a bit of an unfortunate misnomer as the session will not +// be immediately shutdown. The naming is pulled from the QUIC +// spec to indicate a state where the session immediately enters +// the closing period, but the session will not be destroyed +// until either the idle timeout fires or destroy is explicitly +// called. +void QuicSessionClose(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + QuicSession* session; + ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); + session->set_last_error(QuicError(env, args[0], args[1])); + session->SendConnectionClose(); +} + +// GracefulClose flips a flag that prevents new local streams +// from being opened and new remote streams from being received. It is +// important to note that this does *NOT* send a CONNECTION_CLOSE packet +// to the peer. Existing streams are permitted to close gracefully. +void QuicSessionGracefulClose(const FunctionCallbackInfo& args) { + QuicSession* session; + ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); + session->StartGracefulClose(); +} + +// Destroying the QuicSession will trigger sending of a CONNECTION_CLOSE +// packet, after which the QuicSession will be immediately torn down. +void QuicSessionDestroy(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + QuicSession* session; + ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); + session->set_last_error(QuicError(env, args[0], args[1])); + session->Destroy(); +} + +void QuicSessionGetEphemeralKeyInfo(const FunctionCallbackInfo& args) { + QuicSession* session; + ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); + Local ret; + if (session->crypto_context()->ephemeral_key().ToLocal(&ret)) + args.GetReturnValue().Set(ret); +} + +void QuicSessionGetPeerCertificate(const FunctionCallbackInfo& args) { + QuicSession* session; + ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); + Local ret; + if (session->crypto_context()->peer_cert(!args[0]->IsTrue()).ToLocal(&ret)) + args.GetReturnValue().Set(ret); +} + +void QuicSessionGetRemoteAddress( + const FunctionCallbackInfo& args) { + QuicSession* session; + ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); + Environment* env = session->env(); + CHECK(args[0]->IsObject()); + args.GetReturnValue().Set( + session->remote_address().ToJS(env, args[0].As())); +} + +void QuicSessionGetCertificate( + const FunctionCallbackInfo& args) { + QuicSession* session; + ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); + Local ret; + if (session->crypto_context()->cert().ToLocal(&ret)) + args.GetReturnValue().Set(ret); +} + +void QuicSessionPing(const FunctionCallbackInfo& args) { + QuicSession* session; + ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); + session->Ping(); +} + +// Triggers a silent close of a QuicSession. This is currently only used +// (and should ever only be used) for testing purposes... +void QuicSessionSilentClose(const FunctionCallbackInfo& args) { + QuicSession* session; + ASSIGN_OR_RETURN_UNWRAP(&session, args[0].As()); + ProcessEmitWarning( + session->env(), + "Forcing silent close of QuicSession for testing purposes only"); + session->SilentClose(); +} + +// TODO(addaleax): This is a temporary solution for testing and should be +// removed later. +void QuicSessionRemoveFromSocket(const FunctionCallbackInfo& args) { + QuicSession* session; + ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); + session->RemoveFromSocket(); +} + +void QuicSessionUpdateKey(const FunctionCallbackInfo& args) { + QuicSession* session; + ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); + // Initiating a key update may fail if it is done too early (either + // before the TLS handshake has been confirmed or while a previous + // key update is being processed). When it fails, InitiateKeyUpdate() + // will return false. + args.GetReturnValue().Set(session->crypto_context()->InitiateKeyUpdate()); +} + +// When a client wishes to resume a prior TLS session, it must specify both +// the remember transport parameters and remembered TLS session ticket. Those +// will each be provided as a TypedArray. The DecodeTransportParams and +// DecodeSessionTicket functions handle those. If the argument is undefined, +// then resumption is not used. + +bool DecodeTransportParams( + Local value, + ngtcp2_transport_params* params) { + if (value->IsUndefined()) + return false; + CHECK(value->IsArrayBufferView()); + ArrayBufferViewContents sbuf(value.As()); + if (sbuf.length() != sizeof(ngtcp2_transport_params)) + return false; + memcpy(params, sbuf.data(), sizeof(ngtcp2_transport_params)); + return true; +} + +crypto::SSLSessionPointer DecodeSessionTicket(Local value) { + if (value->IsUndefined()) + return {}; + CHECK(value->IsArrayBufferView()); + ArrayBufferViewContents sbuf(value.As()); + return crypto::GetTLSSession(sbuf.data(), sbuf.length()); +} + +void QuicSessionStartHandshake(const FunctionCallbackInfo& args) { + QuicSession* session; + ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); + session->StartHandshake(); +} + +void NewQuicClientSession(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + + QuicSocket* socket; + int32_t family; + uint32_t port; + SecureContext* sc; + SocketAddress remote_addr; + int32_t preferred_address_policy; + PreferredAddressStrategy preferred_address_strategy; + uint32_t options = QUICCLIENTSESSION_OPTION_VERIFY_HOSTNAME_IDENTITY; + std::string alpn(NGTCP2_ALPN_H3); + + enum ARG_IDX : int { + SOCKET, + TYPE, + IP, + PORT, + SECURE_CONTEXT, + SNI, + REMOTE_TRANSPORT_PARAMS, + SESSION_TICKET, + DCID, + PREFERRED_ADDRESS_POLICY, + ALPN, + OPTIONS, + QLOG, + AUTO_START + }; + + CHECK(args[ARG_IDX::SOCKET]->IsObject()); + CHECK(args[ARG_IDX::SECURE_CONTEXT]->IsObject()); + CHECK(args[ARG_IDX::IP]->IsString()); + CHECK(args[ARG_IDX::ALPN]->IsString()); + CHECK(args[ARG_IDX::TYPE]->Int32Value(env->context()).To(&family)); + CHECK(args[ARG_IDX::PORT]->Uint32Value(env->context()).To(&port)); + CHECK(args[ARG_IDX::OPTIONS]->Uint32Value(env->context()).To(&options)); + CHECK(args[ARG_IDX::AUTO_START]->IsBoolean()); + if (!args[ARG_IDX::SNI]->IsUndefined()) + CHECK(args[ARG_IDX::SNI]->IsString()); + + ASSIGN_OR_RETURN_UNWRAP(&socket, args[ARG_IDX::SOCKET].As()); + ASSIGN_OR_RETURN_UNWRAP(&sc, args[ARG_IDX::SECURE_CONTEXT].As()); + + CHECK(args[ARG_IDX::PREFERRED_ADDRESS_POLICY]->Int32Value( + env->context()).To(&preferred_address_policy)); + switch (preferred_address_policy) { + case QUIC_PREFERRED_ADDRESS_USE: + preferred_address_strategy = QuicSession::UsePreferredAddressStrategy; + break; + default: + preferred_address_strategy = QuicSession::IgnorePreferredAddressStrategy; + } + + node::Utf8Value address(env->isolate(), args[ARG_IDX::IP]); + node::Utf8Value servername(env->isolate(), args[ARG_IDX::SNI]); + + if (!SocketAddress::New(family, *address, port, &remote_addr)) + return args.GetReturnValue().Set(ERR_FAILED_TO_CREATE_SESSION); + + // ALPN is a string prefixed by the length, followed by values + Utf8Value val(env->isolate(), args[ARG_IDX::ALPN]); + alpn = val.length(); + alpn += *val; + + crypto::SSLSessionPointer early_session_ticket = + DecodeSessionTicket(args[ARG_IDX::SESSION_TICKET]); + ngtcp2_transport_params early_transport_params; + bool has_early_transport_params = + DecodeTransportParams( + args[ARG_IDX::REMOTE_TRANSPORT_PARAMS], + &early_transport_params); + + socket->ReceiveStart(); + + BaseObjectPtr session = + QuicSession::CreateClient( + socket, + socket->local_address(), + remote_addr, + BaseObjectPtr(sc), + has_early_transport_params ? &early_transport_params : nullptr, + std::move(early_session_ticket), + args[ARG_IDX::DCID], + preferred_address_strategy, + alpn, + std::string(*servername), + options, + args[ARG_IDX::QLOG]->IsTrue() ? + QlogMode::kEnabled : + QlogMode::kDisabled); + + // Start the TLS handshake if the autoStart option is true + // (which it is by default). + if (args[ARG_IDX::AUTO_START]->BooleanValue(env->isolate())) { + session->StartHandshake(); + // Session was created but was unable to bootstrap properly during + // the start of the TLS handshake. + if (session->is_destroyed()) + return args.GetReturnValue().Set(ERR_FAILED_TO_CREATE_SESSION); + } + + args.GetReturnValue().Set(session->object()); +} + +// Add methods that are shared by both client and server QuicSessions +void AddMethods(Environment* env, Local session) { + env->SetProtoMethod(session, "close", QuicSessionClose); + env->SetProtoMethod(session, "destroy", QuicSessionDestroy); + env->SetProtoMethod(session, "getRemoteAddress", QuicSessionGetRemoteAddress); + env->SetProtoMethod(session, "getCertificate", QuicSessionGetCertificate); + env->SetProtoMethod(session, "getPeerCertificate", + QuicSessionGetPeerCertificate); + env->SetProtoMethod(session, "gracefulClose", QuicSessionGracefulClose); + env->SetProtoMethod(session, "updateKey", QuicSessionUpdateKey); + env->SetProtoMethod(session, "ping", QuicSessionPing); + env->SetProtoMethod(session, "removeFromSocket", QuicSessionRemoveFromSocket); + env->SetProtoMethod(session, "onClientHelloDone", + QuicSessionOnClientHelloDone); + env->SetProtoMethod(session, "onCertDone", QuicSessionOnCertDone); +} +} // namespace + +void QuicSession::Initialize( + Environment* env, + Local target, + Local context) { + { + Local class_name = + FIXED_ONE_BYTE_STRING(env->isolate(), "QuicServerSession"); + Local session = FunctionTemplate::New(env->isolate()); + session->SetClassName(class_name); + session->Inherit(AsyncWrap::GetConstructorTemplate(env)); + Local sessiont = session->InstanceTemplate(); + sessiont->SetInternalFieldCount(1); + sessiont->Set(env->owner_symbol(), Null(env->isolate())); + AddMethods(env, session); + env->set_quicserversession_instance_template(sessiont); + } + + { + Local class_name = + FIXED_ONE_BYTE_STRING(env->isolate(), "QuicClientSession"); + Local session = FunctionTemplate::New(env->isolate()); + session->SetClassName(class_name); + session->Inherit(AsyncWrap::GetConstructorTemplate(env)); + Local sessiont = session->InstanceTemplate(); + sessiont->SetInternalFieldCount(1); + sessiont->Set(env->owner_symbol(), Null(env->isolate())); + AddMethods(env, session); + env->SetProtoMethod(session, + "getEphemeralKeyInfo", + QuicSessionGetEphemeralKeyInfo); + env->SetProtoMethod(session, + "setSocket", + QuicSessionSetSocket); + env->SetProtoMethod(session, "startHandshake", QuicSessionStartHandshake); + env->set_quicclientsession_instance_template(sessiont); + + env->SetMethod(target, "createClientSession", NewQuicClientSession); + env->SetMethod(target, "silentCloseSession", QuicSessionSilentClose); + } +} + +} // namespace quic +} // namespace node diff --git a/src/quic/node_quic_session.h b/src/quic/node_quic_session.h new file mode 100644 index 00000000000000..543d554c4d1731 --- /dev/null +++ b/src/quic/node_quic_session.h @@ -0,0 +1,1563 @@ +#ifndef SRC_QUIC_NODE_QUIC_SESSION_H_ +#define SRC_QUIC_NODE_QUIC_SESSION_H_ + +#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS + +#include "aliased_buffer.h" +#include "async_wrap.h" +#include "env.h" +#include "handle_wrap.h" +#include "node.h" +#include "node_crypto.h" +#include "node_http_common.h" +#include "node_mem.h" +#include "node_quic_state.h" +#include "node_quic_buffer-inl.h" +#include "node_quic_crypto.h" +#include "node_quic_util.h" +#include "node_sockaddr.h" +#include "v8.h" +#include "uv.h" + +#include +#include +#include + +#include +#include +#include + +namespace node { +namespace quic { + +using ConnectionPointer = DeleteFnPtr; + +class QuicApplication; +class QuicPacket; +class QuicSocket; +class QuicStream; + +using QuicHeader = NgHeaderBase; + +using StreamsMap = std::unordered_map>; + +enum class QlogMode { + kDisabled, + kEnabled +}; + +typedef void(*ConnectionIDStrategy)( + QuicSession* session, + ngtcp2_cid* cid, + size_t cidlen); + +typedef void(*PreferredAddressStrategy)( + QuicSession* session, + const PreferredAddress& preferred_address); + +// The QuicSessionConfig class holds the initial transport parameters and +// configuration options set by the JavaScript side when either a +// client or server QuicSession is created. Instances are +// stack created and use a combination of an AliasedBuffer to pass +// the numeric settings quickly (see node_quic_state.h) and passed +// in non-numeric settings (e.g. preferred_addr). +class QuicSessionConfig : public ngtcp2_settings { + public: + QuicSessionConfig() {} + + explicit QuicSessionConfig(QuicState* quic_state) { + Set(quic_state); + } + + QuicSessionConfig(const QuicSessionConfig& config) { + initial_ts = uv_hrtime(); + transport_params = config.transport_params; + qlog = config.qlog; + log_printf = config.log_printf; + token = config.token; + } + + void ResetToDefaults(QuicState* quic_state); + + // QuicSessionConfig::Set() pulls values out of the AliasedBuffer + // defined in node_quic_state.h and stores the values in settings_. + // If preferred_addr is not nullptr, it is copied into the + // settings_.preferred_addr field + void Set(QuicState* quic_state, + const struct sockaddr* preferred_addr = nullptr); + + inline void set_original_connection_id(const QuicCID& ocid); + + // Generates the stateless reset token for the settings_ + inline void GenerateStatelessResetToken( + QuicSession* session, + const QuicCID& cid); + + // If the preferred address is set, generates the associated tokens + inline void GeneratePreferredAddressToken( + ConnectionIDStrategy connection_id_strategy, + QuicSession* session, + QuicCID* pscid); + + inline void set_qlog(const ngtcp2_qlog_settings& qlog); +}; + +// Options to alter the behavior of various functions on the +// server QuicSession. These are set on the QuicSocket when +// the listen() function is called and are passed to the +// constructor of the server QuicSession. +enum QuicServerSessionOptions : uint32_t { + // When set, instructs the server QuicSession to reject + // client authentication certs that cannot be verified. + QUICSERVERSESSION_OPTION_REJECT_UNAUTHORIZED = 0x1, + + // When set, instructs the server QuicSession to request + // a client authentication cert + QUICSERVERSESSION_OPTION_REQUEST_CERT = 0x2 +}; + +// Options to alter the behavior of various functions on the +// client QuicSession. These are set on the client QuicSession +// constructor. +enum QuicClientSessionOptions : uint32_t { + // When set, instructs the client QuicSession to include an + // OCSP request in the initial TLS handshake + QUICCLIENTSESSION_OPTION_REQUEST_OCSP = 0x1, + + // When set, instructs the client QuicSession to verify the + // hostname identity. This is required by QUIC and enabled + // by default. We allow disabling it only for debugging + // purposes. + QUICCLIENTSESSION_OPTION_VERIFY_HOSTNAME_IDENTITY = 0x2, + + // When set, instructs the client QuicSession to perform + // additional checks on TLS session resumption. + QUICCLIENTSESSION_OPTION_RESUME = 0x4 +}; + + +// The QuicSessionState enums are used with the QuicSession's +// private state_ array. This is exposed to JavaScript via an +// aliased buffer and is used to communicate various types of +// state efficiently across the native/JS boundary. +enum QuicSessionState : int { + // Communicates whether a 'keylog' event listener has been + // registered on the JavaScript QuicSession object. The + // value will be either 1 or 0. When set to 1, the native + // code will emit TLS keylog entries to the JavaScript + // side triggering the 'keylog' event once for each line. + IDX_QUIC_SESSION_STATE_KEYLOG_ENABLED, + + // Communicates whether a 'clientHello' event listener has + // been registered on the JavaScript QuicServerSession. + // The value will be either 1 or 0. When set to 1, the + // native code will callout to the JavaScript side causing + // the 'clientHello' event to be emitted. This is only + // used on server QuicSession instances. + IDX_QUIC_SESSION_STATE_CLIENT_HELLO_ENABLED, + + // Communicates whether a 'cert' event listener has been + // registered on the JavaScript QuicSession. The value will + // be either 1 or 0. When set to 1, the native code will + // callout to the JavaScript side causing the 'cert' event + // to be emitted. + IDX_QUIC_SESSION_STATE_CERT_ENABLED, + + // Communicates whether a 'pathValidation' event listener + // has been registered on the JavaScript QuicSession. The + // value will be either 1 or 0. When set to 1, the native + // code will callout to the JavaScript side causing the + // 'pathValidation' event to be emitted + IDX_QUIC_SESSION_STATE_PATH_VALIDATED_ENABLED, + + // Communicates the current max cumulative number of + // bidi and uni streams that may be opened on the session + IDX_QUIC_SESSION_STATE_MAX_STREAMS_BIDI, + IDX_QUIC_SESSION_STATE_MAX_STREAMS_UNI, + + // Communicates the current maxinum number of bytes that + // the local endpoint can send in this connection + // (updated immediately after processing sent/received packets) + IDX_QUIC_SESSION_STATE_MAX_DATA_LEFT, + + // Communicates the current total number of bytes in flight + IDX_QUIC_SESSION_STATE_BYTES_IN_FLIGHT, + + // Communicates whether a 'usePreferredAddress' event listener + // has been registered. + IDX_QUIC_SESSION_STATE_USE_PREFERRED_ADDRESS_ENABLED, + + IDX_QUIC_SESSION_STATE_HANDSHAKE_CONFIRMED, + + // Communicates whether a session was closed due to idle timeout + IDX_QUIC_SESSION_STATE_IDLE_TIMEOUT, + + // Just the number of session state enums for use when + // creating the AliasedBuffer. + IDX_QUIC_SESSION_STATE_COUNT +}; + +#define SESSION_STATS(V) \ + V(CREATED_AT, created_at, "Created At") \ + V(HANDSHAKE_START_AT, handshake_start_at, "Handshake Started") \ + V(HANDSHAKE_SEND_AT, handshake_send_at, "Handshke Last Sent") \ + V(HANDSHAKE_CONTINUE_AT, handshake_continue_at, "Handshke Continued") \ + V(HANDSHAKE_COMPLETED_AT, handshake_completed_at, "Handshake Completed") \ + V(HANDSHAKE_CONFIRMED_AT, handshake_confirmed_at, "Handshake Confirmed") \ + V(HANDSHAKE_ACKED_AT, handshake_acked_at, "Handshake Last Acknowledged") \ + V(SENT_AT, sent_at, "Last Sent At") \ + V(RECEIVED_AT, received_at, "Last Received At") \ + V(CLOSING_AT, closing_at, "Closing") \ + V(BYTES_RECEIVED, bytes_received, "Bytes Received") \ + V(BYTES_SENT, bytes_sent, "Bytes Sent") \ + V(BIDI_STREAM_COUNT, bidi_stream_count, "Bidi Stream Count") \ + V(UNI_STREAM_COUNT, uni_stream_count, "Uni Stream Count") \ + V(STREAMS_IN_COUNT, streams_in_count, "Streams In Count") \ + V(STREAMS_OUT_COUNT, streams_out_count, "Streams Out Count") \ + V(KEYUPDATE_COUNT, keyupdate_count, "Key Update Count") \ + V(RETRY_COUNT, retry_count, "Retry Count") \ + V(LOSS_RETRANSMIT_COUNT, loss_retransmit_count, "Loss Retransmit Count") \ + V(ACK_DELAY_RETRANSMIT_COUNT, \ + ack_delay_retransmit_count, \ + "Ack Delay Retransmit Count") \ + V(PATH_VALIDATION_SUCCESS_COUNT, \ + path_validation_success_count, \ + "Path Validation Success Count") \ + V(PATH_VALIDATION_FAILURE_COUNT, \ + path_validation_failure_count, \ + "Path Validation Failure Count") \ + V(MAX_BYTES_IN_FLIGHT, max_bytes_in_flight, "Max Bytes In Flight") \ + V(BLOCK_COUNT, block_count, "Block Count") \ + V(MIN_RTT, min_rtt, "Minimum RTT") \ + V(LATEST_RTT, latest_rtt, "Latest RTT") \ + V(SMOOTHED_RTT, smoothed_rtt, "Smoothed RTT") + +#define V(name, _, __) IDX_QUIC_SESSION_STATS_##name, +enum QuicSessionStatsIdx : int { + SESSION_STATS(V) + IDX_QUIC_SESSION_STATS_COUNT +}; +#undef V + +#define V(_, name, __) uint64_t name; +struct QuicSessionStats { + SESSION_STATS(V) +}; +#undef V + +struct QuicSessionStatsTraits { + using Stats = QuicSessionStats; + using Base = QuicSession; + + template + static void ToString(const Base& ptr, Fn&& add_field); +}; + +class QuicSessionListener { + public: + virtual ~QuicSessionListener(); + + virtual void OnKeylog(const char* str, size_t size); + virtual void OnClientHello( + const char* alpn, + const char* server_name); + virtual void OnCert(const char* server_name); + virtual void OnOCSP(v8::Local ocsp); + virtual void OnStreamHeaders( + int64_t stream_id, + int kind, + const std::vector>& headers, + int64_t push_id); + virtual void OnStreamClose( + int64_t stream_id, + uint64_t app_error_code); + virtual void OnStreamReset( + int64_t stream_id, + uint64_t app_error_code); + virtual void OnSessionDestroyed(); + virtual void OnSessionClose(QuicError error); + virtual void OnStreamReady(BaseObjectPtr stream); + virtual void OnHandshakeCompleted(); + virtual void OnPathValidation( + ngtcp2_path_validation_result res, + const sockaddr* local, + const sockaddr* remote); + virtual void OnUsePreferredAddress( + int family, + const PreferredAddress& preferred_address); + virtual void OnSessionTicket(int size, SSL_SESSION* session); + virtual void OnSessionSilentClose( + bool stateless_reset, + QuicError error); + virtual void OnStreamBlocked(int64_t stream_id); + virtual void OnVersionNegotiation( + uint32_t supported_version, + const uint32_t* versions, + size_t vcnt); + virtual void OnQLog(const uint8_t* data, size_t len); + + QuicSession* session() const { return session_.get(); } + + private: + BaseObjectWeakPtr session_; + QuicSessionListener* previous_listener_ = nullptr; + friend class QuicSession; +}; + +class JSQuicSessionListener : public QuicSessionListener { + public: + void OnKeylog(const char* str, size_t size) override; + void OnClientHello( + const char* alpn, + const char* server_name) override; + void OnCert(const char* server_name) override; + void OnOCSP(v8::Local ocsp) override; + void OnStreamHeaders( + int64_t stream_id, + int kind, + const std::vector>& headers, + int64_t push_id) override; + void OnStreamClose( + int64_t stream_id, + uint64_t app_error_code) override; + void OnStreamReset( + int64_t stream_id, + uint64_t app_error_code) override; + void OnSessionDestroyed() override; + void OnSessionClose(QuicError error) override; + void OnStreamReady(BaseObjectPtr stream) override; + void OnHandshakeCompleted() override; + void OnPathValidation( + ngtcp2_path_validation_result res, + const sockaddr* local, + const sockaddr* remote) override; + void OnSessionTicket(int size, SSL_SESSION* session) override; + void OnSessionSilentClose(bool stateless_reset, QuicError error) override; + void OnUsePreferredAddress( + int family, + const PreferredAddress& preferred_address) override; + void OnStreamBlocked(int64_t stream_id) override; + void OnVersionNegotiation( + uint32_t supported_version, + const uint32_t* versions, + size_t vcnt) override; + void OnQLog(const uint8_t* data, size_t len) override; + + private: + friend class QuicSession; +}; + +// The QuicCryptoContext class encapsulates all of the crypto/TLS +// handshake details on behalf of a QuicSession. +class QuicCryptoContext : public MemoryRetainer { + public: + inline uint64_t Cancel(); + + // Outgoing crypto data must be retained in memory until it is + // explicitly acknowledged. AcknowledgeCryptoData will be invoked + // when ngtcp2 determines that it has received an acknowledgement + // for crypto data at the specified level. This is our indication + // that the data for that level can be released. + void AcknowledgeCryptoData(ngtcp2_crypto_level level, size_t datalen); + + inline void Initialize(); + + // Enables openssl's TLS tracing mechanism for this session only. + void EnableTrace(); + + // Returns the server's prepared OCSP response for transmission. This + // is not used by client QuicSession instances. + inline v8::MaybeLocal ocsp_response() const; + + // Returns ngtcp2's understanding of the current inbound crypto level + inline ngtcp2_crypto_level read_crypto_level() const; + + // Returns ngtcp2's understanding of the current outbound crypto level + inline ngtcp2_crypto_level write_crypto_level() const; + + inline bool early_data() const; + + bool is_option_set(uint32_t option) const { return options_ & option; } + + // Emits a single keylog line to the JavaScript layer + inline void Keylog(const char* line); + + int OnClientHello(); + + inline void OnClientHelloDone(); + + int OnOCSP(); + + void OnOCSPDone( + BaseObjectPtr secure_context, + v8::Local ocsp_response); + + bool OnSecrets( + ngtcp2_crypto_level level, + const uint8_t* rx_secret, + const uint8_t* tx_secret, + size_t secretlen); + + int OnTLSStatus(); + + // Receives and processes TLS handshake details + int Receive( + ngtcp2_crypto_level crypto_level, + uint64_t offset, + const uint8_t* data, + size_t datalen); + + // Resumes the TLS handshake following a client hello or + // OCSP callback + inline void ResumeHandshake(); + + inline v8::MaybeLocal cert() const; + inline v8::MaybeLocal cipher_name() const; + inline v8::MaybeLocal cipher_version() const; + inline v8::MaybeLocal ephemeral_key() const; + inline const char* hello_alpn() const; + inline v8::MaybeLocal hello_ciphers() const; + inline const char* hello_servername() const; + inline v8::MaybeLocal peer_cert(bool abbreviated) const; + inline std::string selected_alpn() const; + inline const char* servername() const; + + void set_option(uint32_t option, bool on = true) { + if (on) + options_ |= option; + else + options_ &= ~option; + } + + inline bool set_session(crypto::SSLSessionPointer session); + + inline void set_tls_alert(int err); + + inline bool SetupInitialKey(const QuicCID& dcid); + + ngtcp2_crypto_side side() const { return side_; } + + void WriteHandshake( + ngtcp2_crypto_level level, + const uint8_t* data, + size_t datalen); + + bool InitiateKeyUpdate(); + + int VerifyPeerIdentity(const char* hostname); + + QuicSession* session() const { return session_.get(); } + + void MemoryInfo(MemoryTracker* tracker) const override; + + void handshake_started() { + is_handshake_started_ = true; + } + + bool is_handshake_started() const { return is_handshake_started_; } + + SET_MEMORY_INFO_NAME(QuicCryptoContext) + SET_SELF_SIZE(QuicCryptoContext) + + private: + inline QuicCryptoContext( + QuicSession* session, + BaseObjectPtr secure_context, + ngtcp2_crypto_side side, + uint32_t options); + + bool SetSecrets( + ngtcp2_crypto_level level, + const uint8_t* rx_secret, + const uint8_t* tx_secret, + size_t secretlen); + + BaseObjectWeakPtr session_; + BaseObjectPtr secure_context_; + ngtcp2_crypto_side side_; + crypto::SSLPointer ssl_; + QuicBuffer handshake_[3]; + bool is_handshake_started_ = false; + bool in_tls_callback_ = false; + bool in_key_update_ = false; + bool in_ocsp_request_ = false; + bool in_client_hello_ = false; + bool early_data_ = false; + uint32_t options_; + + v8::Global ocsp_response_; + crypto::BIOPointer bio_trace_; + + class TLSCallbackScope { + public: + explicit TLSCallbackScope(QuicCryptoContext* context) : + context_(context) { + context_->in_tls_callback_ = true; + } + + ~TLSCallbackScope() { + context_->in_tls_callback_ = false; + } + + static bool is_in_callback(QuicCryptoContext* context) { + return context->in_tls_callback_; + } + + private: + QuicCryptoContext* context_; + }; + + class TLSHandshakeScope { + public: + TLSHandshakeScope( + QuicCryptoContext* context, + bool* monitor) : + context_(context), + monitor_(monitor) {} + + ~TLSHandshakeScope() { + if (!is_handshake_suspended()) + return; + + *monitor_ = false; + // Only continue the TLS handshake if we are not currently running + // synchronously within the TLS handshake function. This can happen + // when the callback function passed to the clientHello and cert + // event handlers is called synchronously. If the function is called + // asynchronously, then we have to manually continue the handshake. + if (!TLSCallbackScope::is_in_callback(context_)) + context_->ResumeHandshake(); + } + + private: + bool is_handshake_suspended() const { + return context_->in_ocsp_request_ || context_->in_client_hello_; + } + + + QuicCryptoContext* context_; + bool* monitor_; + }; + + friend class QuicSession; +}; + +// A QuicApplication encapsulates the specific details of +// working with a specific QUIC application (e.g. http/3). +class QuicApplication : public MemoryRetainer, + public mem::NgLibMemoryManagerBase { + public: + inline explicit QuicApplication(QuicSession* session); + virtual ~QuicApplication() = default; + + virtual bool Initialize() = 0; + virtual bool ReceiveStreamData( + int64_t stream_id, + int fin, + const uint8_t* data, + size_t datalen, + uint64_t offset) = 0; + virtual void AcknowledgeStreamData( + int64_t stream_id, + uint64_t offset, + size_t datalen) { Acknowledge(stream_id, offset, datalen); } + virtual bool BlockStream(int64_t id) { return true; } + virtual void ExtendMaxStreamsRemoteUni(uint64_t max_streams) {} + virtual void ExtendMaxStreamsRemoteBidi(uint64_t max_streams) {} + virtual void ExtendMaxStreamData(int64_t stream_id, uint64_t max_data) {} + virtual void ResumeStream(int64_t stream_id) {} + virtual void SetSessionTicketAppData(const SessionTicketAppData& app_data) { + // TODO(@jasnell): Different QUIC applications may wish to set some + // application data in the session ticket (e.g. http/3 would set + // server settings in the application data). For now, doing nothing + // as I'm just adding the basic mechanism. + } + virtual SessionTicketAppData::Status GetSessionTicketAppData( + const SessionTicketAppData& app_data, + SessionTicketAppData::Flag flag) { + // TODO(@jasnell): Different QUIC application may wish to set some + // application data in the session ticket (e.g. http/3 would set + // server settings in the application data). For now, doing nothing + // as I'm just adding the basic mechanism. + return flag == SessionTicketAppData::Flag::STATUS_RENEW ? + SessionTicketAppData::Status::TICKET_USE_RENEW : + SessionTicketAppData::Status::TICKET_USE; + } + virtual void StreamHeaders( + int64_t stream_id, + int kind, + const std::vector>& headers, + int64_t push_id = 0); + virtual void StreamClose( + int64_t stream_id, + uint64_t app_error_code); + virtual void StreamOpen(int64_t stream_id); + virtual void StreamReset( + int64_t stream_id, + uint64_t app_error_code); + virtual bool SubmitInformation( + int64_t stream_id, + v8::Local headers) { return false; } + virtual bool SubmitHeaders( + int64_t stream_id, + v8::Local headers, + uint32_t flags) { return false; } + virtual bool SubmitTrailers( + int64_t stream_id, + v8::Local headers) { return false; } + virtual BaseObjectPtr SubmitPush( + int64_t stream_id, + v8::Local headers) { + // By default, push streams are not supported + // by an application. + return {}; + } + + inline Environment* env() const; + + bool SendPendingData(); + size_t max_header_pairs() const { return max_header_pairs_; } + size_t max_header_length() const { return max_header_length_; } + + protected: + QuicSession* session() const { return session_.get(); } + bool needs_init() const { return needs_init_; } + void set_init_done() { needs_init_ = false; } + inline void set_stream_fin(int64_t stream_id); + void set_max_header_pairs(size_t max) { max_header_pairs_ = max; } + void set_max_header_length(size_t max) { max_header_length_ = max; } + inline std::unique_ptr CreateStreamDataPacket(); + + struct StreamData { + size_t count = 0; + size_t remaining = 0; + int64_t id = -1; + int fin = 0; + ngtcp2_vec data[kMaxVectorCount] {}; + ngtcp2_vec* buf = nullptr; + BaseObjectPtr stream; + StreamData() { buf = data; } + }; + + void Acknowledge( + int64_t stream_id, + uint64_t offset, + size_t datalen); + virtual int GetStreamData(StreamData* data) = 0; + virtual bool StreamCommit(StreamData* data, size_t datalen) = 0; + virtual bool ShouldSetFin(const StreamData& data) = 0; + + inline ssize_t WriteVStream( + QuicPathStorage* path, + uint8_t* buf, + ssize_t* ndatalen, + const StreamData& stream_data); + + private: + void MaybeSetFin(const StreamData& stream_data); + BaseObjectWeakPtr session_; + bool needs_init_ = true; + size_t max_header_pairs_ = 0; + size_t max_header_length_ = 0; +}; + +// The QuicSession class is an virtual class that serves as +// the basis for both client and server QuicSession. +// It implements the functionality that is shared for both +// QUIC clients and servers. +// +// QUIC sessions are virtual connections that exchange data +// back and forth between peer endpoints via UDP. Every QuicSession +// has an associated TLS context and all data transfered between +// the peers is always encrypted. Unlike TLS over TCP, however, +// The QuicSession uses a session identifier that is independent +// of both the local *and* peer IP address, allowing a QuicSession +// to persist across changes in the network (one of the key features +// of QUIC). QUIC sessions also support 0RTT, implement error +// correction mechanisms to recover from lost packets, and flow +// control. In other words, there's quite a bit going on within +// a QuicSession object. +class QuicSession : public AsyncWrap, + public mem::NgLibMemoryManager, + public StatsBase { + public: + // The default preferred address strategy is to ignore it + static void IgnorePreferredAddressStrategy( + QuicSession* session, + const PreferredAddress& preferred_address); + + static void UsePreferredAddressStrategy( + QuicSession* session, + const PreferredAddress& preferred_address); + + static void Initialize( + Environment* env, + v8::Local target, + v8::Local context); + + static BaseObjectPtr CreateServer( + QuicSocket* socket, + const QuicSessionConfig& config, + const QuicCID& rcid, + const SocketAddress& local_addr, + const SocketAddress& remote_addr, + const QuicCID& dcid, + const QuicCID& ocid, + uint32_t version, + const std::string& alpn = NGTCP2_ALPN_H3, + uint32_t options = 0, + QlogMode qlog = QlogMode::kDisabled); + + static BaseObjectPtr CreateClient( + QuicSocket* socket, + const SocketAddress& local_addr, + const SocketAddress& remote_addr, + BaseObjectPtr secure_context, + ngtcp2_transport_params* early_transport_params, + crypto::SSLSessionPointer early_session_ticket, + v8::Local dcid, + PreferredAddressStrategy preferred_address_strategy = + IgnorePreferredAddressStrategy, + const std::string& alpn = NGTCP2_ALPN_H3, + const std::string& hostname = "", + uint32_t options = 0, + QlogMode qlog = QlogMode::kDisabled); + + static const int kInitialClientBufferLength = 4096; + + // The QuicSession::CryptoContext encapsulates all details of the + // TLS context on behalf of the QuicSession. + QuicSession( + ngtcp2_crypto_side side, + // The QuicSocket that created this session. Note that + // it is possible to replace this socket later, after + // the TLS handshake has completed. The QuicSession + // should never assume that the socket will always + // remain the same. + QuicSocket* socket, + v8::Local wrap, + BaseObjectPtr secure_context, + AsyncWrap::ProviderType provider_type, + // QUIC is generally just a transport. The ALPN identifier + // is used to specify the application protocol that is + // layered on top. If not specified, this will default + // to the HTTP/3 identifier. For QUIC, the alpn identifier + // is always required. + const std::string& alpn, + const std::string& hostname, + const QuicCID& rcid, + uint32_t options = 0, + PreferredAddressStrategy preferred_address_strategy = + IgnorePreferredAddressStrategy); + + // Server Constructor + QuicSession( + QuicSocket* socket, + const QuicSessionConfig& config, + v8::Local wrap, + const QuicCID& rcid, + const SocketAddress& local_addr, + const SocketAddress& remote_addr, + const QuicCID& dcid, + const QuicCID& ocid, + uint32_t version, + const std::string& alpn, + uint32_t options, + QlogMode qlog); + + // Client Constructor + QuicSession( + QuicSocket* socket, + v8::Local wrap, + const SocketAddress& local_addr, + const SocketAddress& remote_addr, + BaseObjectPtr secure_context, + ngtcp2_transport_params* early_transport_params, + crypto::SSLSessionPointer early_session_ticket, + v8::Local dcid, + PreferredAddressStrategy preferred_address_strategy, + const std::string& alpn, + const std::string& hostname, + uint32_t options, + QlogMode qlog); + + ~QuicSession() override; + + std::string diagnostic_name() const override; + + inline QuicCID dcid() const; + + // When a client QuicSession is created, if the autoStart + // option is true, the handshake will be immediately started. + // If autoStart is false, the start of the handshake will be + // deferred until the start handshake method is called; + inline void StartHandshake(); + + QuicApplication* application() const { return application_.get(); } + + QuicCryptoContext* crypto_context() const { return crypto_context_.get(); } + + QuicSessionListener* listener() const { return listener_; } + + BaseObjectPtr CreateStream(int64_t id); + + BaseObjectPtr FindStream(int64_t id) const; + + inline bool HasStream(int64_t id) const; + + inline bool allow_early_data() const; + + // Returns true if StartGracefulClose() has been called and the + // QuicSession is currently in the process of a graceful close. + inline bool is_gracefully_closing() const; + + // Returns true if Destroy() has been called and the + // QuicSession is no longer usable. + inline bool is_destroyed() const; + + inline bool is_stateless_reset() const; + + // Returns true if the QuicSession has entered the + // closing period following a call to ImmediateClose. + // While true, the QuicSession is only permitted to + // transmit CONNECTION_CLOSE frames until either the + // idle timeout period elapses or until the QuicSession + // is explicitly destroyed. + inline bool is_in_closing_period() const; + + // Returns true if the QuicSession has received a + // CONNECTION_CLOSE frame from the peer. Once in + // the draining period, the QuicSession is not + // permitted to send any frames to the peer. The + // QuicSession will be silently closed after either + // the idle timeout period elapses or until the + // QuicSession is explicitly destroyed. + inline bool is_in_draining_period() const; + + inline bool is_server() const; + + // Starting a GracefulClose disables the ability to open or accept + // new streams for this session. Existing streams are allowed to + // close naturally on their own. Once called, the QuicSession will + // be immediately closed once there are no remaining streams. Note + // that no notification is given to the connecting peer that we're + // in a graceful closing state. A CONNECTION_CLOSE will be sent only + // once ImmediateClose() is called. + inline void StartGracefulClose(); + + QuicError last_error() const { return last_error_; } + + size_t max_packet_length() const { return max_pktlen_; } + + // Get the ALPN protocol identifier configured for this QuicSession. + // For server sessions, this will be compared against the client requested + // ALPN identifier to determine if there is a protocol match. + const std::string& alpn() const { return alpn_; } + + // Get the hostname configured for this QuicSession. This is generally + // only used by client sessions. + const std::string& hostname() const { return hostname_; } + + // Returns the associated peer's address. Note that this + // value can change over the lifetime of the QuicSession. + // The fact that the session is not tied intrinsically to + // a single address is one of the benefits of QUIC. + const SocketAddress& remote_address() const { return remote_address_; } + + inline QuicSocket* socket() const; + + ngtcp2_conn* connection() const { return connection_.get(); } + + void AddStream(BaseObjectPtr stream); + + void AddToSocket(QuicSocket* socket); + + // Immediately discards the state of the QuicSession + // and renders the QuicSession instance completely + // unusable. + void Destroy(); + + // Extends the QUIC stream flow control window. This is + // called after received data has been consumed and we + // want to allow the peer to send more data. + inline void ExtendStreamOffset(int64_t stream_id, size_t amount); + + // Extends the QUIC session flow control window + inline void ExtendOffset(size_t amount); + + // Retrieve the local transport parameters established for + // this ngtcp2_conn + inline void GetLocalTransportParams(ngtcp2_transport_params* params); + + // The QUIC version that has been negotiated for this session + inline uint32_t negotiated_version() const; + + // True only if ngtcp2 considers the TLS handshake to be completed + inline bool is_handshake_completed() const; + + // Checks to see if data needs to be retransmitted + void MaybeTimeout(); + + // Called when the session has been determined to have been + // idle for too long and needs to be torn down. + inline void OnIdleTimeout(); + + bool OpenBidirectionalStream(int64_t* stream_id); + + bool OpenUnidirectionalStream(int64_t* stream_id); + + // Ping causes the QuicSession to serialize any currently + // pending frames in it's queue, including any necessary + // PROBE packets. This is a best attempt, fire-and-forget + // type of operation. There is no way to listen for a ping + // response. The main intent of using Ping is to either keep + // the connection from becoming idle or to update RTT stats. + void Ping(); + + // Receive and process a QUIC packet received from the peer + bool Receive( + ssize_t nread, + const uint8_t* data, + const SocketAddress& local_addr, + const SocketAddress& remote_addr, + unsigned int flags); + + // Receive a chunk of QUIC stream data received from the peer + bool ReceiveStreamData( + int64_t stream_id, + int fin, + const uint8_t* data, + size_t datalen, + uint64_t offset); + + void RemoveStream(int64_t stream_id); + + void RemoveFromSocket(); + + // Causes pending ngtcp2 frames to be serialized and sent + void SendPendingData(); + + inline bool SendPacket( + std::unique_ptr packet, + const ngtcp2_path_storage& path); + + inline uint64_t max_data_left() const; + + inline uint64_t max_local_streams_uni() const; + + inline void set_last_error( + QuicError error = { + uint32_t{QUIC_ERROR_SESSION}, + uint64_t{NGTCP2_NO_ERROR} + }); + + inline void set_last_error(int32_t family, uint64_t error_code); + + inline void set_last_error(int32_t family, int error_code); + + inline void set_remote_transport_params(); + + bool set_socket(QuicSocket* socket, bool nat_rebinding = false); + + int set_session(SSL_SESSION* session); + + const StreamsMap& streams() const { return streams_; } + + // ResetStream will cause ngtcp2 to queue a + // RESET_STREAM and STOP_SENDING frame, as appropriate, + // for the given stream_id. For a locally-initiated + // unidirectional stream, only a RESET_STREAM frame + // will be scheduled and the stream will be immediately + // closed. For a bi-directional stream, a STOP_SENDING + // frame will be sent. + // + // It is important to note that the QuicStream is + // not destroyed immediately following ShutdownStream. + // The sending QuicSession will not close the stream + // until the RESET_STREAM is acknowledged. + // + // Once the RESET_STREAM is sent, the QuicSession + // should not send any new frames for the stream, + // and all inbound stream frames should be discarded. + // Once ngtcp2 receives the appropriate notification + // that the RESET_STREAM has been acknowledged, the + // stream will be closed. + // + // Once the stream has been closed, it will be + // destroyed and memory will be freed. User code + // can request that a stream be immediately and + // abruptly destroyed without calling ShutdownStream. + // Likewise, an idle timeout may cause the stream + // to be silently destroyed without calling + // ShutdownStream. + void ResetStream( + int64_t stream_id, + uint64_t error_code = NGTCP2_APP_NOERROR); + + void ResumeStream(int64_t stream_id); + + // Submits informational headers to the QUIC Application + // implementation. If headers are not supported, false + // will be returned. Otherwise, returns true + inline bool SubmitInformation( + int64_t stream_id, + v8::Local headers); + + // Submits initial headers to the QUIC Application + // implementation. If headers are not supported, false + // will be returned. Otherwise, returns true + inline bool SubmitHeaders( + int64_t stream_id, + v8::Local headers, + uint32_t flags); + + // Submits trailing headers to the QUIC Application + // implementation. If headers are not supported, false + // will be returned. Otherwise, returns true + inline bool SubmitTrailers( + int64_t stream_id, + v8::Local headers); + + inline BaseObjectPtr SubmitPush( + int64_t stream_id, + v8::Local headers); + + // Error handling for the QuicSession. client and server + // instances will do different things here, but ultimately + // an error means that the QuicSession + // should be torn down. + void HandleError(); + + bool SendConnectionClose(); + + bool IsResetToken( + const QuicCID& cid, + const uint8_t* data, + size_t datalen); + + // Implementation for mem::NgLibMemoryManager + inline void CheckAllocatedSize(size_t previous_size) const; + + inline void IncreaseAllocatedSize(size_t size); + + inline void DecreaseAllocatedSize(size_t size); + + // Immediately close the QuicSession. All currently open + // streams are implicitly reset and closed with RESET_STREAM + // and STOP_SENDING frames transmitted as necessary. A + // CONNECTION_CLOSE frame will be sent and the session + // will enter the closing period until either the idle + // timeout period elapses or until the QuicSession is + // explicitly destroyed. During the closing period, + // the only frames that may be transmitted to the peer + // are repeats of the already sent CONNECTION_CLOSE. + // + // The CONNECTION_CLOSE will use the error code set using + // the most recent call to set_last_error() + void ImmediateClose(); + + // Silently, and immediately close the QuicSession. This is + // generally only done during an idle timeout. That is, per + // the QUIC specification, if the session remains idle for + // longer than both the advertised idle timeout and three + // times the current probe timeout (PTO). In such cases, all + // currently open streams are implicitly reset and closed + // without sending corresponding RESET_STREAM and + // STOP_SENDING frames, the connection state is + // discarded, and the QuicSession is destroyed without + // sending a CONNECTION_CLOSE frame. + // + // Silent close may also be used to explicitly destroy + // a QuicSession that has either already entered the + // closing or draining periods; or in response to user + // code requests to forcefully terminate a QuicSession + // without transmitting any additional frames to the + // peer. + void SilentClose(); + + void PushListener(QuicSessionListener* listener); + + void RemoveListener(QuicSessionListener* listener); + + inline void set_connection_id_strategy( + ConnectionIDStrategy strategy); + + inline void set_preferred_address_strategy( + PreferredAddressStrategy strategy); + + inline void SetSessionTicketAppData( + const SessionTicketAppData& app_data); + + inline SessionTicketAppData::Status GetSessionTicketAppData( + const SessionTicketAppData& app_data, + SessionTicketAppData::Flag flag); + + inline void SelectPreferredAddress( + const PreferredAddress& preferred_address); + + // Report that the stream data is flow control blocked + inline void StreamDataBlocked(int64_t stream_id); + + // SendSessionScope triggers SendPendingData() when not executing + // within the context of an ngtcp2 callback. When within an ngtcp2 + // callback, SendPendingData will always be called when the callbacks + // complete. + class SendSessionScope { + public: + explicit SendSessionScope( + QuicSession* session, + bool wait_for_handshake = false) + : session_(session), + wait_for_handshake_(wait_for_handshake) { + CHECK(session_); + } + + ~SendSessionScope() { + if (!Ngtcp2CallbackScope::InNgtcp2CallbackScope(session_.get()) && + (!wait_for_handshake_ || + session_->crypto_context()->is_handshake_started())) + session_->SendPendingData(); + } + + private: + BaseObjectPtr session_; + bool wait_for_handshake_ = false; + }; + + // Tracks whether or not we are currently within an ngtcp2 callback + // function. Certain ngtcp2 APIs are not supposed to be called when + // within a callback. We use this as a gate to check. + class Ngtcp2CallbackScope { + public: + explicit Ngtcp2CallbackScope(QuicSession* session) : session_(session) { + CHECK(session_); + CHECK(!InNgtcp2CallbackScope(session)); + session_->set_flag(QUICSESSION_FLAG_NGTCP2_CALLBACK); + } + + ~Ngtcp2CallbackScope() { + session_->set_flag(QUICSESSION_FLAG_NGTCP2_CALLBACK, false); + } + + static bool InNgtcp2CallbackScope(QuicSession* session) { + return session->is_flag_set(QUICSESSION_FLAG_NGTCP2_CALLBACK); + } + + private: + BaseObjectPtr session_; + }; + + QuicState* quic_state() { return quic_state_.get(); } + + void MemoryInfo(MemoryTracker* tracker) const override; + SET_MEMORY_INFO_NAME(QuicSession) + SET_SELF_SIZE(QuicSession) + + private: + static void RandomConnectionIDStrategy( + QuicSession* session, + ngtcp2_cid* cid, + size_t cidlen); + + // Initialize the QuicSession as a server + void InitServer( + QuicSessionConfig config, + const SocketAddress& local_addr, + const SocketAddress& remote_addr, + const QuicCID& dcid, + const QuicCID& ocid, + uint32_t version, + QlogMode qlog); + + // Initialize the QuicSession as a client + void InitClient( + const SocketAddress& local_addr, + const SocketAddress& remote_addr, + ngtcp2_transport_params* early_transport_params, + crypto::SSLSessionPointer early_session_ticket, + v8::Local dcid, + QlogMode qlog); + + inline void InitApplication(); + + void AckedStreamDataOffset( + int64_t stream_id, + uint64_t offset, + size_t datalen); + + inline void AssociateCID(const QuicCID& cid); + + inline void DisassociateCID(const QuicCID& cid); + + inline void ExtendMaxStreamData(int64_t stream_id, uint64_t max_data); + + void ExtendMaxStreams(bool bidi, uint64_t max_streams); + + inline void ExtendMaxStreamsUni(uint64_t max_streams); + + inline void ExtendMaxStreamsBidi(uint64_t max_streams); + + inline void ExtendMaxStreamsRemoteUni(uint64_t max_streams); + + inline void ExtendMaxStreamsRemoteBidi(uint64_t max_streams); + + bool GetNewConnectionID(ngtcp2_cid* cid, uint8_t* token, size_t cidlen); + + inline void GetConnectionCloseInfo(); + + inline void HandshakeCompleted(); + + inline void HandshakeConfirmed(); + + void PathValidation( + const ngtcp2_path* path, + ngtcp2_path_validation_result res); + + bool ReceiveClientInitial(const QuicCID& dcid); + + bool ReceivePacket(ngtcp2_path* path, const uint8_t* data, ssize_t nread); + + bool ReceiveRetry(); + + inline void RemoveConnectionID(const QuicCID& cid); + + void ScheduleRetransmit(); + + bool SendPacket(std::unique_ptr packet); + + inline void set_local_address(const ngtcp2_addr* addr); + + void StreamClose(int64_t stream_id, uint64_t app_error_code); + + void StreamOpen(int64_t stream_id); + + void StreamReset( + int64_t stream_id, + uint64_t final_size, + uint64_t app_error_code); + + bool WritePackets(const char* diagnostic_label = nullptr); + + void UpdateRecoveryStats(); + + void UpdateConnectionID( + int type, + const QuicCID& cid, + const StatelessResetToken& token); + + void UpdateDataStats(); + + inline void UpdateEndpoint(const ngtcp2_path& path); + + inline void VersionNegotiation(const uint32_t* sv, size_t nsv); + + // static ngtcp2 callbacks + static int OnClientInitial( + ngtcp2_conn* conn, + void* user_data); + + static int OnReceiveClientInitial( + ngtcp2_conn* conn, + const ngtcp2_cid* dcid, + void* user_data); + + static int OnReceiveCryptoData( + ngtcp2_conn* conn, + ngtcp2_crypto_level crypto_level, + uint64_t offset, + const uint8_t* data, + size_t datalen, + void* user_data); + + static int OnHandshakeCompleted( + ngtcp2_conn* conn, + void* user_data); + + static int OnHandshakeConfirmed( + ngtcp2_conn* conn, + void* user_data); + + static int OnReceiveStreamData( + ngtcp2_conn* conn, + int64_t stream_id, + int fin, + uint64_t offset, + const uint8_t* data, + size_t datalen, + void* user_data, + void* stream_user_data); + + static int OnReceiveRetry( + ngtcp2_conn* conn, + const ngtcp2_pkt_hd* hd, + const ngtcp2_pkt_retry* retry, + void* user_data); + + static int OnAckedCryptoOffset( + ngtcp2_conn* conn, + ngtcp2_crypto_level crypto_level, + uint64_t offset, + size_t datalen, + void* user_data); + + static int OnAckedStreamDataOffset( + ngtcp2_conn* conn, + int64_t stream_id, + uint64_t offset, + size_t datalen, + void* user_data, + void* stream_user_data); + + static int OnSelectPreferredAddress( + ngtcp2_conn* conn, + ngtcp2_addr* dest, + const ngtcp2_preferred_addr* paddr, + void* user_data); + + static int OnStreamClose( + ngtcp2_conn* conn, + int64_t stream_id, + uint64_t app_error_code, + void* user_data, + void* stream_user_data); + + static int OnStreamOpen( + ngtcp2_conn* conn, + int64_t stream_id, + void* user_data); + + static int OnStreamReset( + ngtcp2_conn* conn, + int64_t stream_id, + uint64_t final_size, + uint64_t app_error_code, + void* user_data, + void* stream_user_data); + + static int OnRand( + ngtcp2_conn* conn, + uint8_t* dest, + size_t destlen, + ngtcp2_rand_ctx ctx, + void* user_data); + + static int OnGetNewConnectionID( + ngtcp2_conn* conn, + ngtcp2_cid* cid, + uint8_t* token, + size_t cidlen, + void* user_data); + + static int OnRemoveConnectionID( + ngtcp2_conn* conn, + const ngtcp2_cid* cid, + void* user_data); + + static int OnPathValidation( + ngtcp2_conn* conn, + const ngtcp2_path* path, + ngtcp2_path_validation_result res, + void* user_data); + + static int OnExtendMaxStreamsUni( + ngtcp2_conn* conn, + uint64_t max_streams, + void* user_data); + + static int OnExtendMaxStreamsBidi( + ngtcp2_conn* conn, + uint64_t max_streams, + void* user_data); + + static int OnExtendMaxStreamData( + ngtcp2_conn* conn, + int64_t stream_id, + uint64_t max_data, + void* user_data, + void* stream_user_data); + + static int OnVersionNegotiation( + ngtcp2_conn* conn, + const ngtcp2_pkt_hd* hd, + const uint32_t* sv, + size_t nsv, + void* user_data); + + static int OnStatelessReset( + ngtcp2_conn* conn, + const ngtcp2_pkt_stateless_reset* sr, + void* user_data); + + static int OnExtendMaxStreamsRemoteUni( + ngtcp2_conn* conn, + uint64_t max_streams, + void* user_data); + + static int OnExtendMaxStreamsRemoteBidi( + ngtcp2_conn* conn, + uint64_t max_streams, + void* user_data); + + static int OnConnectionIDStatus( + ngtcp2_conn* conn, + int type, + uint64_t seq, + const ngtcp2_cid* cid, + const uint8_t* token, + void* user_data); + + static void OnQlogWrite(void* user_data, const void* data, size_t len); + + void UpdateIdleTimer(); + + inline void UpdateRetransmitTimer(uint64_t timeout); + + inline void StopRetransmitTimer(); + + inline void StopIdleTimer(); + + bool StartClosingPeriod(); + + enum QuicSessionFlags : uint32_t { + // Initial state when a QuicSession is created but nothing yet done. + QUICSESSION_FLAG_INITIAL = 0x1, + + // Set while the QuicSession is in the process of an Immediate + // or silent close. + QUICSESSION_FLAG_CLOSING = 0x2, + + // Set while the QuicSession is in the process of a graceful close. + QUICSESSION_FLAG_GRACEFUL_CLOSING = 0x4, + + // Set when the QuicSession has been destroyed (but not + // yet freed) + QUICSESSION_FLAG_DESTROYED = 0x8, + + QUICSESSION_FLAG_HAS_TRANSPORT_PARAMS = 0x10, + + // Set while the QuicSession is executing an ngtcp2 callback + QUICSESSION_FLAG_NGTCP2_CALLBACK = 0x100, + + // Set if the QuicSession is in the middle of a silent close + // (that is, a CONNECTION_CLOSE should not be sent) + QUICSESSION_FLAG_SILENT_CLOSE = 0x200, + + QUICSESSION_FLAG_HANDSHAKE_RX = 0x400, + QUICSESSION_FLAG_HANDSHAKE_TX = 0x800, + QUICSESSION_FLAG_HANDSHAKE_KEYS = + QUICSESSION_FLAG_HANDSHAKE_RX | + QUICSESSION_FLAG_HANDSHAKE_TX, + QUICSESSION_FLAG_SESSION_RX = 0x1000, + QUICSESSION_FLAG_SESSION_TX = 0x2000, + QUICSESSION_FLAG_SESSION_KEYS = + QUICSESSION_FLAG_SESSION_RX | + QUICSESSION_FLAG_SESSION_TX, + + // Set if the QuicSession was closed due to stateless reset + QUICSESSION_FLAG_STATELESS_RESET = 0x4000 + }; + + void set_flag(QuicSessionFlags flag, bool on = true) { + if (on) + flags_ |= flag; + else + flags_ &= ~flag; + } + + bool is_flag_set(QuicSessionFlags flag) const { + return (flags_ & flag) == flag; + } + + void IncrementConnectionCloseAttempts() { + if (connection_close_attempts_ < kMaxSizeT) + connection_close_attempts_++; + } + + bool ShouldAttemptConnectionClose() { + if (connection_close_attempts_ == connection_close_limit_) { + if (connection_close_limit_ * 2 <= kMaxSizeT) + connection_close_limit_ *= 2; + else + connection_close_limit_ = kMaxSizeT; + return true; + } + return false; + } + + typedef ssize_t(*ngtcp2_close_fn)( + ngtcp2_conn* conn, + ngtcp2_path* path, + uint8_t* dest, + size_t destlen, + uint64_t error_code, + ngtcp2_tstamp ts); + + static inline ngtcp2_close_fn SelectCloseFn(uint32_t family) { + return family == QUIC_ERROR_APPLICATION ? + ngtcp2_conn_write_application_close : + ngtcp2_conn_write_connection_close; + } + + // Select the QUIC Application based on the configured ALPN identifier + QuicApplication* SelectApplication(QuicSession* session); + + ngtcp2_mem alloc_info_; + std::unique_ptr crypto_context_; + std::unique_ptr application_; + BaseObjectWeakPtr socket_; + std::string alpn_; + std::string hostname_; + QuicError last_error_ = { + uint32_t{QUIC_ERROR_SESSION}, + uint64_t{NGTCP2_NO_ERROR} + }; + ConnectionPointer connection_; + SocketAddress local_address_{}; + SocketAddress remote_address_{}; + uint32_t flags_ = 0; + size_t max_pktlen_ = 0; + size_t current_ngtcp2_memory_ = 0; + size_t connection_close_attempts_ = 0; + size_t connection_close_limit_ = 1; + + ConnectionIDStrategy connection_id_strategy_ = nullptr; + PreferredAddressStrategy preferred_address_strategy_ = nullptr; + + QuicSessionListener* listener_ = nullptr; + JSQuicSessionListener default_listener_; + + TimerPointer idle_; + TimerPointer retransmit_; + + QuicCID scid_; + QuicCID rcid_; + QuicCID pscid_; + ngtcp2_transport_params transport_params_; + + std::unique_ptr conn_closebuf_; + + StreamsMap streams_; + + AliasedFloat64Array state_; + + struct RemoteTransportParamsDebug { + QuicSession* session; + explicit RemoteTransportParamsDebug(QuicSession* session_) + : session(session_) {} + std::string ToString() const; + }; + + static const ngtcp2_conn_callbacks callbacks[2]; + + BaseObjectPtr quic_state_; + + friend class QuicCryptoContext; + friend class QuicSessionListener; + friend class JSQuicSessionListener; +}; + +} // namespace quic +} // namespace node + +#endif // NODE_WANT_INTERNALS +#endif // SRC_QUIC_NODE_QUIC_SESSION_H_ diff --git a/src/quic/node_quic_socket-inl.h b/src/quic/node_quic_socket-inl.h new file mode 100644 index 00000000000000..3e9adab6f263c2 --- /dev/null +++ b/src/quic/node_quic_socket-inl.h @@ -0,0 +1,232 @@ +#ifndef SRC_QUIC_NODE_QUIC_SOCKET_INL_H_ +#define SRC_QUIC_NODE_QUIC_SOCKET_INL_H_ + +#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS + +#include "node_quic_socket.h" +#include "node_sockaddr.h" +#include "node_quic_session.h" +#include "node_crypto.h" +#include "debug_utils-inl.h" + +namespace node { + +using crypto::EntropySource; + +namespace quic { + +std::unique_ptr QuicPacket::Create( + const char* diagnostic_label, + size_t len) { + return std::make_unique(diagnostic_label, len); +} + +std::unique_ptr QuicPacket::Copy( + const std::unique_ptr& other) { + return std::make_unique(*other.get()); +} + +void QuicPacket::set_length(size_t len) { + CHECK_LE(len, data_.size()); + data_.resize(len); +} + +int QuicEndpoint::Send( + uv_buf_t* buf, + size_t len, + const sockaddr* addr) { + int ret = static_cast(udp_->Send(buf, len, addr)); + if (ret == 0) + IncrementPendingCallbacks(); + return ret; +} + +int QuicEndpoint::ReceiveStart() { + return udp_->RecvStart(); +} + +int QuicEndpoint::ReceiveStop() { + return udp_->RecvStop(); +} + +void QuicEndpoint::WaitForPendingCallbacks() { + if (!has_pending_callbacks()) { + listener_->OnEndpointDone(this); + return; + } + waiting_for_callbacks_ = true; +} + +void QuicSocket::AssociateCID( + const QuicCID& cid, + const QuicCID& scid) { + if (cid && scid) + dcid_to_scid_[cid] = scid; +} + +void QuicSocket::DisassociateCID(const QuicCID& cid) { + if (cid) { + Debug(this, "Removing association for cid %s", cid); + dcid_to_scid_.erase(cid); + } +} + +void QuicSocket::AssociateStatelessResetToken( + const StatelessResetToken& token, + BaseObjectPtr session) { + Debug(this, "Associating stateless reset token %s", token); + token_map_[token] = session; +} + +const SocketAddress& QuicSocket::local_address() { + CHECK(preferred_endpoint_); + return preferred_endpoint_->local_address(); +} + +void QuicSocket::DisassociateStatelessResetToken( + const StatelessResetToken& token) { + Debug(this, "Removing stateless reset token %s", token); + token_map_.erase(token); +} + +// StopListening is called when the QuicSocket is no longer +// accepting new server connections. Typically, this is called +// when the QuicSocket enters a graceful closing state where +// existing sessions are allowed to close naturally but new +// sessions are rejected. +void QuicSocket::StopListening() { + if (is_flag_set(QUICSOCKET_FLAGS_SERVER_LISTENING)) { + Debug(this, "Stop listening"); + set_flag(QUICSOCKET_FLAGS_SERVER_LISTENING, false); + // It is important to not call ReceiveStop here as there + // is ongoing traffic being exchanged by the peers. + } +} + +void QuicSocket::ReceiveStart() { + for (const auto& endpoint : endpoints_) + CHECK_EQ(endpoint->ReceiveStart(), 0); +} + +void QuicSocket::ReceiveStop() { + for (const auto& endpoint : endpoints_) + CHECK_EQ(endpoint->ReceiveStop(), 0); +} + +void QuicSocket::RemoveSession( + const QuicCID& cid, + const SocketAddress& addr) { + DecrementSocketAddressCounter(addr); + sessions_.erase(cid); +} + +void QuicSocket::ReportSendError(int error) { + listener_->OnError(error); +} + +void QuicSocket::IncrementStatelessResetCounter(const SocketAddress& addr) { + reset_counts_[addr]++; +} + +void QuicSocket::IncrementSocketAddressCounter(const SocketAddress& addr) { + addr_counts_[addr]++; +} + +void QuicSocket::DecrementSocketAddressCounter(const SocketAddress& addr) { + auto it = addr_counts_.find(addr); + if (it == std::end(addr_counts_)) + return; + it->second--; + // Remove the address if the counter reaches zero again. + if (it->second == 0) { + addr_counts_.erase(addr); + reset_counts_.erase(addr); + } +} + +size_t QuicSocket::GetCurrentSocketAddressCounter(const SocketAddress& addr) { + auto it = addr_counts_.find(addr); + return it == std::end(addr_counts_) ? 0 : it->second; +} + +size_t QuicSocket::GetCurrentStatelessResetCounter(const SocketAddress& addr) { + auto it = reset_counts_.find(addr); + return it == std::end(reset_counts_) ? 0 : it->second; +} + +void QuicSocket::set_server_busy(bool on) { + Debug(this, "Turning Server Busy Response %s", on ? "on" : "off"); + set_flag(QUICSOCKET_FLAGS_SERVER_BUSY, on); + listener_->OnServerBusy(on); +} + +bool QuicSocket::is_diagnostic_packet_loss(double prob) const { + if (LIKELY(prob == 0.0)) return false; + unsigned char c = 255; + EntropySource(&c, 1); + return (static_cast(c) / 255) < prob; +} + +void QuicSocket::set_diagnostic_packet_loss(double rx, double tx) { + rx_loss_ = rx; + tx_loss_ = tx; +} + +bool QuicSocket::ToggleStatelessReset() { + set_flag( + QUICSOCKET_FLAGS_DISABLE_STATELESS_RESET, + !is_flag_set(QUICSOCKET_FLAGS_DISABLE_STATELESS_RESET)); + return !is_flag_set(QUICSOCKET_FLAGS_DISABLE_STATELESS_RESET); +} + +void QuicSocket::set_validated_address(const SocketAddress& addr) { + if (is_option_set(QUICSOCKET_OPTIONS_VALIDATE_ADDRESS_LRU)) { + // Remove the oldest item if we've hit the LRU limit + validated_addrs_.push_back(SocketAddress::Hash()(addr)); + if (validated_addrs_.size() > kMaxValidateAddressLru) + validated_addrs_.pop_front(); + } +} + +bool QuicSocket::is_validated_address(const SocketAddress& addr) const { + if (is_option_set(QUICSOCKET_OPTIONS_VALIDATE_ADDRESS_LRU)) { + auto res = std::find(std::begin(validated_addrs_), + std::end(validated_addrs_), + SocketAddress::Hash()(addr)); + return res != std::end(validated_addrs_); + } + return false; +} + +void QuicSocket::AddSession( + const QuicCID& cid, + BaseObjectPtr session) { + sessions_[cid] = session; + IncrementSocketAddressCounter(session->remote_address()); + IncrementStat( + session->is_server() ? + &QuicSocketStats::server_sessions : + &QuicSocketStats::client_sessions); +} + +void QuicSocket::AddEndpoint( + BaseObjectPtr endpoint_, + bool preferred) { + Debug(this, "Adding %sendpoint", preferred ? "preferred " : ""); + if (preferred || endpoints_.empty()) + preferred_endpoint_ = endpoint_; + endpoints_.emplace_back(endpoint_); + if (is_flag_set(QUICSOCKET_FLAGS_SERVER_LISTENING)) + endpoint_->ReceiveStart(); +} + +void QuicSocket::SessionReady(BaseObjectPtr session) { + listener_->OnSessionReady(session); +} + +} // namespace quic +} // namespace node + +#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS + +#endif // SRC_QUIC_NODE_QUIC_SOCKET_INL_H_ diff --git a/src/quic/node_quic_socket.cc b/src/quic/node_quic_socket.cc new file mode 100644 index 00000000000000..5e12df5992b7c6 --- /dev/null +++ b/src/quic/node_quic_socket.cc @@ -0,0 +1,1183 @@ +#include "node_quic_socket-inl.h" // NOLINT(build/include) +#include "allocated_buffer-inl.h" +#include "async_wrap-inl.h" +#include "debug_utils-inl.h" +#include "env-inl.h" +#include "memory_tracker-inl.h" +#include "nghttp2/nghttp2.h" +#include "node.h" +#include "node_buffer.h" +#include "node_crypto.h" +#include "node_internals.h" +#include "node_mem-inl.h" +#include "node_quic_crypto.h" +#include "node_quic_session-inl.h" +#include "node_quic_util-inl.h" +#include "node_sockaddr-inl.h" +#include "req_wrap-inl.h" +#include "util.h" +#include "uv.h" +#include "v8.h" + +#include + +namespace node { + +using crypto::EntropySource; +using crypto::SecureContext; + +using v8::ArrayBufferView; +using v8::Boolean; +using v8::Context; +using v8::FunctionCallbackInfo; +using v8::FunctionTemplate; +using v8::HandleScope; +using v8::Integer; +using v8::Isolate; +using v8::Local; +using v8::Number; +using v8::Object; +using v8::ObjectTemplate; +using v8::PropertyAttribute; +using v8::String; +using v8::Value; + +namespace quic { + +namespace { +// The reserved version is a mechanism QUIC endpoints +// can use to ensure correct handling of version +// negotiation. It is defined by the QUIC spec in +// https://tools.ietf.org/html/draft-ietf-quic-transport-24#section-6.3 +// Specifically, any version that follows the pattern +// 0x?a?a?a?a may be used to force version negotiation. +inline uint32_t GenerateReservedVersion( + const SocketAddress& addr, + uint32_t version) { + socklen_t addrlen = addr.length(); + uint32_t h = 0x811C9DC5u; + const uint8_t* p = addr.raw(); + const uint8_t* ep = p + addrlen; + for (; p != ep; ++p) { + h ^= *p; + h *= 0x01000193u; + } + version = htonl(version); + p = reinterpret_cast(&version); + ep = p + sizeof(version); + for (; p != ep; ++p) { + h ^= *p; + h *= 0x01000193u; + } + h &= 0xf0f0f0f0u; + h |= 0x0a0a0a0au; + return h; +} + +bool IsShortHeader( + uint32_t version, + const uint8_t* pscid, + size_t pscidlen) { + return version == NGTCP2_PROTO_VER && + pscid == nullptr && + pscidlen == 0; +} +} // namespace + +QuicPacket::QuicPacket(const char* diagnostic_label, size_t len) : + data_(len), + diagnostic_label_(diagnostic_label) { + CHECK_LE(len, NGTCP2_MAX_PKT_SIZE); +} + +QuicPacket::QuicPacket(const QuicPacket& other) : + QuicPacket(other.diagnostic_label_, other.data_.size()) { + memcpy(data_.data(), other.data_.data(), other.data_.size()); +} + +const char* QuicPacket::diagnostic_label() const { + return diagnostic_label_ != nullptr ? + diagnostic_label_ : "unspecified"; +} + +void QuicPacket::MemoryInfo(MemoryTracker* tracker) const { + tracker->TrackField("data", data_); +} + +QuicSocketListener::~QuicSocketListener() { + if (socket_) + socket_->RemoveListener(this); +} + +void QuicSocketListener::OnError(ssize_t code) { + if (previous_listener_ != nullptr) + previous_listener_->OnError(code); +} + +void QuicSocketListener::OnSessionReady(BaseObjectPtr session) { + if (previous_listener_ != nullptr) + previous_listener_->OnSessionReady(session); +} + +void QuicSocketListener::OnServerBusy(bool busy) { + if (previous_listener_ != nullptr) + previous_listener_->OnServerBusy(busy); +} + +void QuicSocketListener::OnEndpointDone(QuicEndpoint* endpoint) { + if (previous_listener_ != nullptr) + previous_listener_->OnEndpointDone(endpoint); +} + +void QuicSocketListener::OnDestroy() { + if (previous_listener_ != nullptr) + previous_listener_->OnDestroy(); +} + +void JSQuicSocketListener::OnError(ssize_t code) { + Environment* env = socket()->env(); + HandleScope scope(env->isolate()); + Context::Scope context_scope(env->context()); + Local arg = Number::New(env->isolate(), static_cast(code)); + socket()->MakeCallback(env->quic_on_socket_error_function(), 1, &arg); +} + +void JSQuicSocketListener::OnSessionReady(BaseObjectPtr session) { + Environment* env = socket()->env(); + Local arg = session->object(); + Context::Scope context_scope(env->context()); + socket()->MakeCallback(env->quic_on_session_ready_function(), 1, &arg); +} + +void JSQuicSocketListener::OnServerBusy(bool busy) { + Environment* env = socket()->env(); + HandleScope handle_scope(env->isolate()); + Context::Scope context_scope(env->context()); + Local arg = Boolean::New(env->isolate(), busy); + socket()->MakeCallback(env->quic_on_socket_server_busy_function(), 1, &arg); +} + +void JSQuicSocketListener::OnEndpointDone(QuicEndpoint* endpoint) { + Environment* env = socket()->env(); + HandleScope scope(env->isolate()); + Context::Scope context_scope(env->context()); + MakeCallback( + env->isolate(), + endpoint->object(), + env->ondone_string(), + 0, nullptr); +} + +void JSQuicSocketListener::OnDestroy() { + // Do nothing here. +} + +QuicEndpoint::QuicEndpoint( + QuicState* quic_state, + Local wrap, + QuicSocket* listener, + Local udp_wrap) : + BaseObject(quic_state->env(), wrap), + listener_(listener), + quic_state_(quic_state) { + MakeWeak(); + udp_ = static_cast( + udp_wrap->GetAlignedPointerFromInternalField( + UDPWrapBase::kUDPWrapBaseField)); + CHECK_NOT_NULL(udp_); + udp_->set_listener(this); + strong_ptr_.reset(udp_->GetAsyncWrap()); +} + +void QuicEndpoint::MemoryInfo(MemoryTracker* tracker) const {} + +uv_buf_t QuicEndpoint::OnAlloc(size_t suggested_size) { + return AllocatedBuffer::AllocateManaged(env(), suggested_size).release(); +} + +void QuicEndpoint::OnRecv( + ssize_t nread, + const uv_buf_t& buf_, + const sockaddr* addr, + unsigned int flags) { + AllocatedBuffer buf(env(), buf_); + + if (nread <= 0) { + if (nread < 0) + listener_->OnError(this, nread); + return; + } + + listener_->OnReceive( + nread, + std::move(buf), + local_address(), + SocketAddress(addr), + flags); +} + +ReqWrap* QuicEndpoint::CreateSendWrap(size_t msg_size) { + return listener_->OnCreateSendWrap(msg_size); +} + +void QuicEndpoint::OnSendDone(ReqWrap* wrap, int status) { + DecrementPendingCallbacks(); + listener_->OnSendDone(wrap, status); + if (!has_pending_callbacks() && waiting_for_callbacks_) + listener_->OnEndpointDone(this); +} + +void QuicEndpoint::OnAfterBind() { + listener_->OnBind(this); +} + +QuicSocket::QuicSocket( + QuicState* quic_state, + Local wrap, + uint64_t retry_token_expiration, + size_t max_connections, + size_t max_connections_per_host, + size_t max_stateless_resets_per_host, + uint32_t options, + QlogMode qlog, + const uint8_t* session_reset_secret, + bool disable_stateless_reset) + : AsyncWrap(quic_state->env(), wrap, AsyncWrap::PROVIDER_QUICSOCKET), + StatsBase(quic_state->env(), wrap), + alloc_info_(MakeAllocator()), + options_(options), + max_connections_(max_connections), + max_connections_per_host_(max_connections_per_host), + max_stateless_resets_per_host_(max_stateless_resets_per_host), + retry_token_expiration_(retry_token_expiration), + qlog_(qlog), + server_alpn_(NGTCP2_ALPN_H3), + quic_state_(quic_state) { + MakeWeak(); + PushListener(&default_listener_); + + Debug(this, "New QuicSocket created"); + + EntropySource(token_secret_, kTokenSecretLen); + + if (disable_stateless_reset) + set_flag(QUICSOCKET_FLAGS_DISABLE_STATELESS_RESET); + + // Set the session reset secret to the one provided or random. + // Note that a random secret is going to make it exceedingly + // difficult for the session reset token to be useful. + if (session_reset_secret != nullptr) { + memcpy(reset_token_secret_, + session_reset_secret, + NGTCP2_STATELESS_RESET_TOKENLEN); + } else { + EntropySource(reset_token_secret_, NGTCP2_STATELESS_RESET_TOKENLEN); + } +} + +QuicSocket::~QuicSocket() { + QuicSocketListener* listener = listener_; + listener_->OnDestroy(); + if (listener == listener_) + RemoveListener(listener_); + + DebugStats(); +} + +template +void QuicSocketStatsTraits::ToString(const QuicSocket& ptr, Fn&& add_field) { +#define V(_n, name, label) \ + add_field(label, ptr.GetStat(&QuicSocketStats::name)); + SOCKET_STATS(V) +#undef V +} + +void QuicSocket::MemoryInfo(MemoryTracker* tracker) const { + tracker->TrackField("endpoints", endpoints_); + tracker->TrackField("sessions", sessions_); + tracker->TrackField("dcid_to_scid", dcid_to_scid_); + tracker->TrackField("addr_counts", addr_counts_); + tracker->TrackField("reset_counts", reset_counts_); + tracker->TrackField("token_map", token_map_); + tracker->TrackField("validated_addrs", validated_addrs_); + StatsBase::StatsMemoryInfo(tracker); + tracker->TrackFieldWithSize( + "current_ngtcp2_memory", + current_ngtcp2_memory_); +} + +void QuicSocket::Listen( + BaseObjectPtr sc, + const sockaddr* preferred_address, + const std::string& alpn, + uint32_t options) { + CHECK(sc); + CHECK(!server_secure_context_); + CHECK(!is_flag_set(QUICSOCKET_FLAGS_SERVER_LISTENING)); + Debug(this, "Starting to listen"); + server_session_config_.Set(quic_state(), preferred_address); + server_secure_context_ = sc; + server_alpn_ = alpn; + server_options_ = options; + set_flag(QUICSOCKET_FLAGS_SERVER_LISTENING); + RecordTimestamp(&QuicSocketStats::listen_at); + ReceiveStart(); +} + +void QuicSocket::OnError(QuicEndpoint* endpoint, ssize_t error) { + Debug(this, "Reading data from UDP socket failed. Error %" PRId64, error); + listener_->OnError(error); +} + +ReqWrap* QuicSocket::OnCreateSendWrap(size_t msg_size) { + HandleScope handle_scope(env()->isolate()); + Local obj; + if (!env()->quicsocketsendwrap_instance_template() + ->NewInstance(env()->context()).ToLocal(&obj)) return nullptr; + return last_created_send_wrap_ = new SendWrap(quic_state(), obj, msg_size); +} + +void QuicSocket::OnEndpointDone(QuicEndpoint* endpoint) { + Debug(this, "Endpoint has no pending callbacks"); + listener_->OnEndpointDone(endpoint); +} + +void QuicSocket::OnBind(QuicEndpoint* endpoint) { + const SocketAddress& local_address = endpoint->local_address(); + bound_endpoints_[local_address] = + BaseObjectWeakPtr(endpoint); + Debug(this, "Endpoint %s bound", local_address); + RecordTimestamp(&QuicSocketStats::bound_at); +} + +BaseObjectPtr QuicSocket::FindSession(const QuicCID& cid) { + BaseObjectPtr session; + auto session_it = sessions_.find(cid); + if (session_it == std::end(sessions_)) { + auto scid_it = dcid_to_scid_.find(cid); + if (scid_it != std::end(dcid_to_scid_)) { + session_it = sessions_.find(scid_it->second); + CHECK_NE(session_it, std::end(sessions_)); + session = session_it->second; + } + } else { + session = session_it->second; + } + return session; +} + +// When a received packet contains a QUIC short header but cannot be +// matched to a known QuicSession, it is either (a) garbage, +// (b) a valid packet for a connection we no longer have state +// for, or (c) a stateless reset. Because we do not yet know if +// we are going to process the packet, we need to try to quickly +// determine -- with as little cost as possible -- whether the +// packet contains a reset token. We do so by checking the final +// NGTCP2_STATELESS_RESET_TOKENLEN bytes in the packet to see if +// they match one of the known reset tokens previously given by +// the remote peer. If there's a match, then it's a reset token, +// if not, we move on the to the next check. It is very important +// that this check be as inexpensive as possible to avoid a DOS +// vector. +bool QuicSocket::MaybeStatelessReset( + const QuicCID& dcid, + const QuicCID& scid, + ssize_t nread, + const uint8_t* data, + const SocketAddress& local_addr, + const SocketAddress& remote_addr, + unsigned int flags) { + if (UNLIKELY(is_stateless_reset_disabled() || nread < 16)) + return false; + StatelessResetToken possible_token( + data + nread - NGTCP2_STATELESS_RESET_TOKENLEN); + Debug(this, "Possible stateless reset token: %s", possible_token); + auto it = token_map_.find(possible_token); + if (it == token_map_.end()) + return false; + Debug(this, "Received a stateless reset token %s", possible_token); + return it->second->Receive(nread, data, local_addr, remote_addr, flags); +} + +// When a packet is received here, we do not yet know if we can +// process it successfully as a QUIC packet or not. Given the +// nature of UDP, we may receive a great deal of garbage here +// so it is extremely important not to commit resources until +// we're certain we can process the data we received as QUIC +// packet. +// Any packet we choose not to process must be ignored. +void QuicSocket::OnReceive( + ssize_t nread, + AllocatedBuffer buf, + const SocketAddress& local_addr, + const SocketAddress& remote_addr, + unsigned int flags) { + Debug(this, "Receiving %d bytes from the UDP socket", nread); + + // When diagnostic packet loss is enabled, the packet will be randomly + // dropped based on the rx_loss_ probability. + if (UNLIKELY(is_diagnostic_packet_loss(rx_loss_))) { + Debug(this, "Simulating received packet loss"); + return; + } + + IncrementStat(&QuicSocketStats::bytes_received, nread); + + const uint8_t* data = reinterpret_cast(buf.data()); + + uint32_t pversion; + const uint8_t* pdcid; + size_t pdcidlen; + const uint8_t* pscid; + size_t pscidlen; + + // This is our first check to see if the received data can be + // processed as a QUIC packet. If this fails, then the QUIC packet + // header is invalid and cannot be processed; all we can do is ignore + // it. It's questionable whether we should even increment the + // packets_ignored statistic here but for now we do. If it succeeds, + // we have a valid QUIC header but there's still no guarantee that + // the packet can be successfully processed. + if (ngtcp2_pkt_decode_version_cid( + &pversion, + &pdcid, + &pdcidlen, + &pscid, + &pscidlen, + data, nread, kScidLen) < 0) { + IncrementStat(&QuicSocketStats::packets_ignored); + return; + } + + // QUIC currently requires CID lengths of max NGTCP2_MAX_CIDLEN. The + // ngtcp2 API allows non-standard lengths, and we may want to allow + // non-standard lengths later. But for now, we're going to ignore any + // packet with a non-standard CID length. + if (pdcidlen > NGTCP2_MAX_CIDLEN || pscidlen > NGTCP2_MAX_CIDLEN) { + IncrementStat(&QuicSocketStats::packets_ignored); + return; + } + + QuicCID dcid(pdcid, pdcidlen); + QuicCID scid(pscid, pscidlen); + + // TODO(@jasnell): It would be fantastic if Debug() could be + // modified to accept objects with a ToString-like capability + // similar to what we can do with TraceEvents... that would + // allow us to pass the QuicCID directly to Debug and have it + // converted to hex only if the category is enabled so we can + // skip committing resources here. + std::string dcid_hex = dcid.ToString(); + Debug(this, "Received a QUIC packet for dcid %s", dcid_hex.c_str()); + + BaseObjectPtr session = FindSession(dcid); + + // If a session is not found, there are four possible reasons: + // 1. The session has not been created yet + // 2. The session existed once but we've lost the local state for it + // 3. The packet is a stateless reset sent by the peer + // 4. This is a malicious or malformed packet. + if (!session) { + Debug(this, "There is no existing session for dcid %s", dcid_hex.c_str()); + bool is_short_header = IsShortHeader(pversion, pscid, pscidlen); + + // Handle possible reception of a stateless reset token... + // If it is a stateless reset, the packet will be handled with + // no additional action necessary here. We want to return immediately + // without committing any further resources. + if (is_short_header && + MaybeStatelessReset( + dcid, + scid, + nread, + data, + local_addr, + remote_addr, + flags)) { + Debug(this, "Handled stateless reset"); + return; + } + + // AcceptInitialPacket will first validate that the packet can be + // accepted, then create a new server QuicSession instance if able + // to do so. If a new instance cannot be created (for any reason), + // the session BaseObjectPtr will be empty on return. + session = AcceptInitialPacket( + pversion, + dcid, + scid, + nread, + data, + local_addr, + remote_addr, + flags); + + // There are many reasons why a server QuicSession could not be + // created. The most common will be invalid packets or incorrect + // QUIC version. In any of these cases, however, to prevent a + // potential attacker from causing us to consume resources, + // we're just going to ignore the packet. It is possible that + // the AcceptInitialPacket sent a version negotiation packet, + // or a CONNECTION_CLOSE packet. + if (!session) { + Debug(this, "Unable to create a new server QuicSession"); + // If the packet contained a short header, we might need to send + // a stateless reset. The stateless reset contains a token derived + // from the received destination connection ID. + // + // TODO(@jasnell): Stateless resets are generated programmatically + // using HKDF with the sender provided dcid and a locally provided + // secret as input. It is entirely possible that a malicious + // peer could send multiple stateless reset eliciting packets + // with the specific intent of using the returned stateless + // reset to guess the stateless reset token secret used by + // the server. Once guessed, the malicious peer could use + // that secret as a DOS vector against other peers. We currently + // implement some mitigations for this by limiting the number + // of stateless resets that can be sent to a specific remote + // address but there are other possible mitigations, such as + // including the remote address as input in the generation of + // the stateless token. + if (is_short_header && + SendStatelessReset(dcid, local_addr, remote_addr, nread)) { + Debug(this, "Sent stateless reset"); + IncrementStat(&QuicSocketStats::stateless_reset_count); + return; + } + IncrementStat(&QuicSocketStats::packets_ignored); + return; + } + } + + CHECK(session); + + // If the packet could not successfully processed for any reason (possibly + // due to being malformed or malicious in some way) we mark it ignored. + if (!session->Receive(nread, data, local_addr, remote_addr, flags)) { + IncrementStat(&QuicSocketStats::packets_ignored); + return; + } + + IncrementStat(&QuicSocketStats::packets_received); +} + +// Generates and sends a version negotiation packet. This is +// terminal for the connection and is sent only when a QUIC +// packet is received for an unsupported Node.js version. +// It is possible that a malicious packet triggered this +// so we need to be careful not to commit too many resources. +// Currently, we only support one QUIC version at a time. +void QuicSocket::SendVersionNegotiation( + uint32_t version, + const QuicCID& dcid, + const QuicCID& scid, + const SocketAddress& local_addr, + const SocketAddress& remote_addr) { + uint32_t sv[2]; + sv[0] = GenerateReservedVersion(remote_addr, version); + sv[1] = NGTCP2_PROTO_VER; + + uint8_t unused_random; + EntropySource(&unused_random, 1); + + size_t pktlen = dcid.length() + scid.length() + (sizeof(sv)) + 7; + + auto packet = QuicPacket::Create("version negotiation", pktlen); + ssize_t nwrite = ngtcp2_pkt_write_version_negotiation( + packet->data(), + NGTCP2_MAX_PKTLEN_IPV6, + unused_random, + dcid.data(), + dcid.length(), + scid.data(), + scid.length(), + sv, + arraysize(sv)); + if (nwrite <= 0) + return; + packet->set_length(nwrite); + SocketAddress remote_address(remote_addr); + SendPacket(local_addr, remote_address, std::move(packet)); +} + +// Possible generates and sends a stateless reset packet. +// This is terminal for the connection. It is possible +// that a malicious packet triggered this so we need to +// be careful not to commit too many resources. +bool QuicSocket::SendStatelessReset( + const QuicCID& cid, + const SocketAddress& local_addr, + const SocketAddress& remote_addr, + size_t source_len) { + if (UNLIKELY(is_stateless_reset_disabled())) + return false; + constexpr static size_t kRandlen = NGTCP2_MIN_STATELESS_RESET_RANDLEN * 5; + constexpr static size_t kMinStatelessResetLen = 41; + uint8_t random[kRandlen]; + + // Per the QUIC spec, we need to protect against sending too + // many stateless reset tokens to an endpoint to prevent + // endless looping. + if (GetCurrentStatelessResetCounter(remote_addr) >= + max_stateless_resets_per_host_) { + return false; + } + // Per the QUIC spec, a stateless reset token must be strictly + // smaller than the packet that triggered it. This is one of the + // mechanisms to prevent infinite looping exchange of stateless + // tokens with the peer. + // An endpoint should never send a stateless reset token smaller than + // 41 bytes per the QUIC spec. The reason is that packets less than + // 41 bytes may allow an observer to determine that it's a stateless + // reset. + size_t pktlen = source_len - 1; + if (pktlen < kMinStatelessResetLen) + return false; + + StatelessResetToken token(reset_token_secret_, cid); + EntropySource(random, kRandlen); + + auto packet = QuicPacket::Create("stateless reset", pktlen); + ssize_t nwrite = + ngtcp2_pkt_write_stateless_reset( + packet->data(), + NGTCP2_MAX_PKTLEN_IPV4, + const_cast(token.data()), + random, + kRandlen); + if (nwrite < static_cast(kMinStatelessResetLen)) + return false; + packet->set_length(nwrite); + SocketAddress remote_address(remote_addr); + IncrementStatelessResetCounter(remote_address); + return SendPacket(local_addr, remote_address, std::move(packet)) == 0; +} + +// Generates and sends a retry packet. This is terminal +// for the connection. Retry packets are used to force +// explicit path validation by issuing a token to the +// peer that it must thereafter include in all subsequent +// initial packets. Upon receiving a retry packet, the +// peer must termination it's initial attempt to +// establish a connection and start a new attempt. +// +// TODO(@jasnell): Retry packets will only ever be +// generated by QUIC servers, and only if the QuicSocket +// is configured for explicit path validation. There is +// no way for a client to force a retry packet to be created. +// However, once a client determines that explicit +// path validation is enabled, it could attempt to +// DOS by sending a large number of malicious +// initial packets to intentionally ellicit retry +// packets (It can do so by intentionally sending +// initial packets that ignore the retry token). +// To help mitigate that risk, we should limit the number +// of retries we send to a given remote endpoint. +bool QuicSocket::SendRetry( + const QuicCID& dcid, + const QuicCID& scid, + const SocketAddress& local_addr, + const SocketAddress& remote_addr) { + std::unique_ptr packet = + GenerateRetryPacket(token_secret_, dcid, scid, local_addr, remote_addr); + return packet ? + SendPacket(local_addr, remote_addr, std::move(packet)) == 0 : false; +} + +// Shutdown a connection prematurely, before a QuicSession is created. +void QuicSocket::ImmediateConnectionClose( + const QuicCID& scid, + const QuicCID& dcid, + const SocketAddress& local_addr, + const SocketAddress& remote_addr, + int64_t reason) { + Debug(this, "Sending stateless connection close to %s", scid); + auto packet = QuicPacket::Create("immediate connection close"); + ssize_t nwrite = ngtcp2_crypto_write_connection_close( + packet->data(), + packet->length(), + scid.cid(), + dcid.cid(), + reason); + if (nwrite > 0) { + packet->set_length(nwrite); + SendPacket(local_addr, remote_addr, std::move(packet)); + } +} + +// Inspects the packet and possibly accepts it as a new +// initial packet creating a new QuicSession instance. +// If the packet is not acceptable, it is very important +// not to commit resources. +BaseObjectPtr QuicSocket::AcceptInitialPacket( + uint32_t version, + const QuicCID& dcid, + const QuicCID& scid, + ssize_t nread, + const uint8_t* data, + const SocketAddress& local_addr, + const SocketAddress& remote_addr, + unsigned int flags) { + HandleScope handle_scope(env()->isolate()); + Context::Scope context_scope(env()->context()); + ngtcp2_pkt_hd hd; + QuicCID ocid; + + // If the QuicSocket is not listening, the paket will be ignored. + if (!is_flag_set(QUICSOCKET_FLAGS_SERVER_LISTENING)) { + Debug(this, "QuicSocket is not listening"); + return {}; + } + + switch (ngtcp2_accept(&hd, data, static_cast(nread))) { + case 1: + // Send Version Negotiation + SendVersionNegotiation(version, dcid, scid, local_addr, remote_addr); + // Fall through + case -1: + // Either a version negotiation packet was sent or the packet is + // an invalid initial packet. Either way, there's nothing more we + // can do here. + return {}; + } + + // If the server is busy, new connections will be shut down immediately + // after the initial keys are installed. The busy state is controlled + // entirely by local user code. It is important to understand that + // a QuicSession is created and resources are committed even though + // the QuicSession will be torn down as quickly as possible. + // Else, check to see if the number of connections total for this QuicSocket + // has been exceeded. If the count has been exceeded, shutdown the connection + // immediately after the initial keys are installed. + if (UNLIKELY(is_flag_set(QUICSOCKET_FLAGS_SERVER_BUSY)) || + sessions_.size() >= max_connections_ || + GetCurrentSocketAddressCounter(remote_addr) >= + max_connections_per_host_) { + Debug(this, "QuicSocket is busy or connection count exceeded"); + IncrementStat(&QuicSocketStats::server_busy_count); + ImmediateConnectionClose( + QuicCID(hd.scid), + QuicCID(hd.dcid), + local_addr, + remote_addr, + NGTCP2_SERVER_BUSY); + return {}; + } + + // QUIC has address validation built in to the handshake but allows for + // an additional explicit validation request using RETRY frames. If we + // are using explicit validation, we check for the existence of a valid + // retry token in the packet. If one does not exist, we send a retry with + // a new token. If it does exist, and if it's valid, we grab the original + // cid and continue. + if (!is_validated_address(remote_addr)) { + switch (hd.type) { + case NGTCP2_PKT_INITIAL: + if (is_option_set(QUICSOCKET_OPTIONS_VALIDATE_ADDRESS) || + hd.tokenlen > 0) { + Debug(this, "Performing explicit address validation"); + if (hd.tokenlen == 0) { + Debug(this, "No retry token was detected. Generating one"); + SendRetry(dcid, scid, local_addr, remote_addr); + // Sending a retry token terminates this connection attempt. + return {}; + } + if (InvalidRetryToken( + hd, + remote_addr, + &ocid, + token_secret_, + retry_token_expiration_)) { + Debug(this, "Invalid retry token was detected. Failing"); + ImmediateConnectionClose( + QuicCID(hd.scid), + QuicCID(hd.dcid), + local_addr, + remote_addr); + return {}; + } + } + break; + case NGTCP2_PKT_0RTT: + SendRetry(dcid, scid, local_addr, remote_addr); + return {}; + } + } + + BaseObjectPtr session = + QuicSession::CreateServer( + this, + server_session_config_, + dcid, + local_addr, + remote_addr, + scid, + ocid, + version, + server_alpn_, + server_options_, + qlog_); + CHECK(session); + + listener_->OnSessionReady(session); + + return session; +} + +QuicSocket::SendWrap::SendWrap( + QuicState* quic_state, + Local req_wrap_obj, + size_t total_length) + : ReqWrap(quic_state->env(), req_wrap_obj, PROVIDER_QUICSOCKET), + total_length_(total_length), + quic_state_(quic_state) { +} + +std::string QuicSocket::SendWrap::MemoryInfoName() const { + return "QuicSendWrap"; +} + +void QuicSocket::SendWrap::MemoryInfo(MemoryTracker* tracker) const { + tracker->TrackField("session", session_); + tracker->TrackField("packet", packet_); +} + +int QuicSocket::SendPacket( + const SocketAddress& local_addr, + const SocketAddress& remote_addr, + std::unique_ptr packet, + BaseObjectPtr session) { + // If the packet is empty, there's nothing to do + if (packet->length() == 0) + return 0; + + Debug(this, "Sending %" PRIu64 " bytes to %s from %s (label: %s)", + packet->length(), + remote_addr, + local_addr, + packet->diagnostic_label()); + + // If DiagnosticPacketLoss returns true, it will call Done() internally + if (UNLIKELY(is_diagnostic_packet_loss(tx_loss_))) { + Debug(this, "Simulating transmitted packet loss"); + return 0; + } + + last_created_send_wrap_ = nullptr; + uv_buf_t buf = packet->buf(); + + auto endpoint = bound_endpoints_.find(local_addr); + CHECK_NE(endpoint, bound_endpoints_.end()); + int err = endpoint->second->Send(&buf, 1, remote_addr.data()); + + if (err != 0) { + if (err > 0) err = 0; + OnSend(err, packet.get()); + } else { + CHECK_NOT_NULL(last_created_send_wrap_); + last_created_send_wrap_->set_packet(std::move(packet)); + if (session) + last_created_send_wrap_->set_session(session); + } + return err; +} + +void QuicSocket::OnSend(int status, QuicPacket* packet) { + if (status == 0) { + Debug(this, "Sent %" PRIu64 " bytes (label: %s)", + packet->length(), + packet->diagnostic_label()); + IncrementStat(&QuicSocketStats::bytes_sent, packet->length()); + IncrementStat(&QuicSocketStats::packets_sent); + } else { + Debug(this, "Failed to send %" PRIu64 " bytes (status: %d, label: %s)", + packet->length(), + status, + packet->diagnostic_label()); + } +} + +void QuicSocket::OnSendDone(ReqWrap* wrap, int status) { + std::unique_ptr req_wrap(static_cast(wrap)); + OnSend(status, req_wrap->packet()); +} + +void QuicSocket::CheckAllocatedSize(size_t previous_size) const { + CHECK_GE(current_ngtcp2_memory_, previous_size); +} + +void QuicSocket::IncreaseAllocatedSize(size_t size) { + current_ngtcp2_memory_ += size; +} + +void QuicSocket::DecreaseAllocatedSize(size_t size) { + current_ngtcp2_memory_ -= size; +} + +void QuicSocket::PushListener(QuicSocketListener* listener) { + CHECK_NOT_NULL(listener); + CHECK(!listener->socket_); + + listener->previous_listener_ = listener_; + listener->socket_.reset(this); + + listener_ = listener; +} + +void QuicSocket::RemoveListener(QuicSocketListener* listener) { + CHECK_NOT_NULL(listener); + + QuicSocketListener* previous; + QuicSocketListener* current; + + for (current = listener_, previous = nullptr; + /* No loop condition because we want a crash if listener is not found */ + ; previous = current, current = current->previous_listener_) { + CHECK_NOT_NULL(current); + if (current == listener) { + if (previous != nullptr) + previous->previous_listener_ = current->previous_listener_; + else + listener_ = listener->previous_listener_; + break; + } + } + + listener->socket_.reset(); + listener->previous_listener_ = nullptr; +} + +// JavaScript API +namespace { +void NewQuicEndpoint(const FunctionCallbackInfo& args) { + QuicState* state = Environment::GetBindingData(args); + CHECK(args.IsConstructCall()); + CHECK(args[0]->IsObject()); + QuicSocket* socket; + ASSIGN_OR_RETURN_UNWRAP(&socket, args[0].As()); + CHECK(args[1]->IsObject()); + CHECK_GE(args[1].As()->InternalFieldCount(), + UDPWrapBase::kUDPWrapBaseField); + new QuicEndpoint(state, args.This(), socket, args[1].As()); +} + +void NewQuicSocket(const FunctionCallbackInfo& args) { + QuicState* state = Environment::GetBindingData(args); + Environment* env = state->env(); + CHECK(args.IsConstructCall()); + + uint32_t options; + uint32_t retry_token_expiration; + uint32_t max_connections; + uint32_t max_connections_per_host; + uint32_t max_stateless_resets_per_host; + + if (!args[0]->Uint32Value(env->context()).To(&options) || + !args[1]->Uint32Value(env->context()).To(&retry_token_expiration) || + !args[2]->Uint32Value(env->context()).To(&max_connections) || + !args[3]->Uint32Value(env->context()).To(&max_connections_per_host) || + !args[4]->Uint32Value(env->context()) + .To(&max_stateless_resets_per_host)) { + return; + } + CHECK_GE(retry_token_expiration, MIN_RETRYTOKEN_EXPIRATION); + CHECK_LE(retry_token_expiration, MAX_RETRYTOKEN_EXPIRATION); + + const uint8_t* session_reset_secret = nullptr; + if (args[6]->IsArrayBufferView()) { + ArrayBufferViewContents buf(args[6].As()); + CHECK_EQ(buf.length(), kTokenSecretLen); + session_reset_secret = buf.data(); + } + + new QuicSocket( + state, + args.This(), + retry_token_expiration, + max_connections, + max_connections_per_host, + max_stateless_resets_per_host, + options, + args[5]->IsTrue() ? QlogMode::kEnabled : QlogMode::kDisabled, + session_reset_secret, + args[7]->IsTrue()); +} + +void QuicSocketAddEndpoint(const FunctionCallbackInfo& args) { + QuicSocket* socket; + ASSIGN_OR_RETURN_UNWRAP(&socket, args.Holder()); + CHECK(args[0]->IsObject()); + QuicEndpoint* endpoint; + ASSIGN_OR_RETURN_UNWRAP(&endpoint, args[0].As()); + socket->AddEndpoint( + BaseObjectPtr(endpoint), + args[1]->IsTrue()); +} + +// Enabling diagnostic packet loss enables a mode where the QuicSocket +// instance will randomly ignore received packets in order to simulate +// packet loss. This is not an API that should be enabled in production +// but is useful when debugging and diagnosing performance issues. +// Diagnostic packet loss is enabled by setting either the tx or rx +// arguments to a value between 0.0 and 1.0. Setting both values to 0.0 +// disables the mechanism. +void QuicSocketSetDiagnosticPacketLoss( + const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + QuicSocket* socket; + ASSIGN_OR_RETURN_UNWRAP(&socket, args.Holder()); + double rx, tx; + if (!args[0]->NumberValue(env->context()).To(&rx) || + !args[1]->NumberValue(env->context()).To(&tx)) return; + CHECK_GE(rx, 0.0f); + CHECK_GE(tx, 0.0f); + CHECK_LE(rx, 1.0f); + CHECK_LE(tx, 1.0f); + socket->set_diagnostic_packet_loss(rx, tx); +} + +void QuicSocketDestroy(const FunctionCallbackInfo& args) { + QuicSocket* socket; + ASSIGN_OR_RETURN_UNWRAP(&socket, args.Holder()); + socket->ReceiveStop(); +} + +void QuicSocketListen(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + QuicSocket* socket; + ASSIGN_OR_RETURN_UNWRAP(&socket, args.Holder(), + args.GetReturnValue().Set(UV_EBADF)); + CHECK(args[0]->IsObject() && + env->secure_context_constructor_template()->HasInstance(args[0])); + SecureContext* sc; + ASSIGN_OR_RETURN_UNWRAP(&sc, args[0].As(), + args.GetReturnValue().Set(UV_EBADF)); + + sockaddr_storage preferred_address_storage; + const sockaddr* preferred_address = nullptr; + if (args[1]->IsString()) { + node::Utf8Value preferred_address_host(args.GetIsolate(), args[1]); + int32_t preferred_address_family; + uint32_t preferred_address_port; + if (!args[2]->Int32Value(env->context()).To(&preferred_address_family) || + !args[3]->Uint32Value(env->context()).To(&preferred_address_port)) + return; + if (SocketAddress::ToSockAddr( + preferred_address_family, + *preferred_address_host, + preferred_address_port, + &preferred_address_storage)) { + preferred_address = + reinterpret_cast(&preferred_address_storage); + } + } + + std::string alpn(NGTCP2_ALPN_H3); + if (args[4]->IsString()) { + Utf8Value val(env->isolate(), args[4]); + alpn = val.length(); + alpn += *val; + } + + uint32_t options = 0; + if (!args[5]->Uint32Value(env->context()).To(&options)) return; + + socket->Listen( + BaseObjectPtr(sc), + preferred_address, + alpn, + options); +} + +void QuicSocketStopListening(const FunctionCallbackInfo& args) { + QuicSocket* socket; + ASSIGN_OR_RETURN_UNWRAP(&socket, args.Holder()); + socket->StopListening(); +} + +void QuicSocketset_server_busy(const FunctionCallbackInfo& args) { + QuicSocket* socket; + ASSIGN_OR_RETURN_UNWRAP(&socket, args.Holder()); + CHECK_EQ(args.Length(), 1); + socket->set_server_busy(args[0]->IsTrue()); +} + +void QuicSocketToggleStatelessReset(const FunctionCallbackInfo& args) { + QuicSocket* socket; + ASSIGN_OR_RETURN_UNWRAP(&socket, args.Holder()); + args.GetReturnValue().Set(socket->ToggleStatelessReset()); +} + +void QuicEndpointWaitForPendingCallbacks( + const FunctionCallbackInfo& args) { + QuicEndpoint* endpoint; + ASSIGN_OR_RETURN_UNWRAP(&endpoint, args.Holder()); + endpoint->WaitForPendingCallbacks(); +} + +} // namespace + +void QuicEndpoint::Initialize( + Environment* env, + Local target, + Local context) { + Isolate* isolate = env->isolate(); + Local class_name = FIXED_ONE_BYTE_STRING(isolate, "QuicEndpoint"); + Local endpoint = env->NewFunctionTemplate(NewQuicEndpoint); + endpoint->SetClassName(class_name); + endpoint->InstanceTemplate()->SetInternalFieldCount(1); + env->SetProtoMethod(endpoint, + "waitForPendingCallbacks", + QuicEndpointWaitForPendingCallbacks); + endpoint->InstanceTemplate()->Set(env->owner_symbol(), Null(isolate)); + + target->Set( + context, + class_name, + endpoint->GetFunction(context).ToLocalChecked()) + .FromJust(); +} + +void QuicSocket::Initialize( + Environment* env, + Local target, + Local context) { + Isolate* isolate = env->isolate(); + Local class_name = FIXED_ONE_BYTE_STRING(isolate, "QuicSocket"); + Local socket = env->NewFunctionTemplate(NewQuicSocket); + socket->SetClassName(class_name); + socket->InstanceTemplate()->SetInternalFieldCount(1); + socket->InstanceTemplate()->Set(env->owner_symbol(), Null(isolate)); + env->SetProtoMethod(socket, + "addEndpoint", + QuicSocketAddEndpoint); + env->SetProtoMethod(socket, + "destroy", + QuicSocketDestroy); + env->SetProtoMethod(socket, + "listen", + QuicSocketListen); + env->SetProtoMethod(socket, + "setDiagnosticPacketLoss", + QuicSocketSetDiagnosticPacketLoss); + env->SetProtoMethod(socket, + "setServerBusy", + QuicSocketset_server_busy); + env->SetProtoMethod(socket, + "stopListening", + QuicSocketStopListening); + env->SetProtoMethod(socket, + "toggleStatelessReset", + QuicSocketToggleStatelessReset); + socket->Inherit(HandleWrap::GetConstructorTemplate(env)); + target->Set(context, class_name, + socket->GetFunction(env->context()).ToLocalChecked()).FromJust(); + + // TODO(addaleax): None of these templates actually are constructor templates. + Local sendwrap_template = ObjectTemplate::New(isolate); + sendwrap_template->SetInternalFieldCount(1); + env->set_quicsocketsendwrap_instance_template(sendwrap_template); +} + +} // namespace quic +} // namespace node diff --git a/src/quic/node_quic_socket.h b/src/quic/node_quic_socket.h new file mode 100644 index 00000000000000..01f6e276563abb --- /dev/null +++ b/src/quic/node_quic_socket.h @@ -0,0 +1,618 @@ +#ifndef SRC_QUIC_NODE_QUIC_SOCKET_H_ +#define SRC_QUIC_NODE_QUIC_SOCKET_H_ + +#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS + +#include "base_object.h" +#include "node.h" +#include "node_crypto.h" +#include "node_internals.h" +#include "ngtcp2/ngtcp2.h" +#include "node_quic_state.h" +#include "node_quic_session.h" +#include "node_quic_util.h" +#include "node_sockaddr.h" +#include "env.h" +#include "udp_wrap.h" +#include "v8.h" +#include "uv.h" + +#include +#include +#include +#include + +namespace node { + +using v8::Context; +using v8::FunctionCallbackInfo; +using v8::Local; +using v8::Object; +using v8::Value; + +namespace quic { + +enum QuicSocketOptions : uint32_t { + // When enabled the QuicSocket will validate the address + // using a RETRY packet to the peer. + QUICSOCKET_OPTIONS_VALIDATE_ADDRESS = 0x1, + + // When enabled, and the VALIDATE_ADDRESS option is also + // set, the QuicSocket will use an LRU cache to track + // validated addresses. Address validation will be skipped + // if the address is currently in the cache. + QUICSOCKET_OPTIONS_VALIDATE_ADDRESS_LRU = 0x2, +}; + +#define SOCKET_STATS(V) \ + V(CREATED_AT, created_at, "Created At") \ + V(BOUND_AT, bound_at, "Bound At") \ + V(LISTEN_AT, listen_at, "Listen At") \ + V(BYTES_RECEIVED, bytes_received, "Bytes Received") \ + V(BYTES_SENT, bytes_sent, "Bytes Sent") \ + V(PACKETS_RECEIVED, packets_received, "Packets Received") \ + V(PACKETS_IGNORED, packets_ignored, "Packets Ignored") \ + V(PACKETS_SENT, packets_sent, "Packets Sent") \ + V(SERVER_SESSIONS, server_sessions, "Server Sessions") \ + V(CLIENT_SESSIONS, client_sessions, "Client Sessions") \ + V(STATELESS_RESET_COUNT, stateless_reset_count, "Stateless Reset Count") \ + V(SERVER_BUSY_COUNT, server_busy_count, "Server Busy Count") + +#define V(name, _, __) IDX_QUIC_SOCKET_STATS_##name, +enum QuicSocketStatsIdx : int { + SOCKET_STATS(V) + IDX_QUIC_SOCKET_STATS_COUNT +}; +#undef V + +#define V(_, name, __) uint64_t name; +struct QuicSocketStats { + SOCKET_STATS(V) +}; +#undef V + +struct QuicSocketStatsTraits { + using Stats = QuicSocketStats; + using Base = QuicSocket; + + template + static void ToString(const Base& ptr, Fn&& add_field); +}; + +class QuicSocket; +class QuicEndpoint; + +// This is the generic interface for objects that control QuicSocket +// instances. The default `JSQuicSocketListener` emits events to +// JavaScript +class QuicSocketListener { + public: + virtual ~QuicSocketListener(); + + virtual void OnError(ssize_t code); + virtual void OnSessionReady(BaseObjectPtr session); + virtual void OnServerBusy(bool busy); + virtual void OnEndpointDone(QuicEndpoint* endpoint); + virtual void OnDestroy(); + + QuicSocket* socket() { return socket_.get(); } + + private: + BaseObjectWeakPtr socket_; + QuicSocketListener* previous_listener_ = nullptr; + friend class QuicSocket; +}; + +class JSQuicSocketListener : public QuicSocketListener { + public: + void OnError(ssize_t code) override; + void OnSessionReady(BaseObjectPtr session) override; + void OnServerBusy(bool busy) override; + void OnEndpointDone(QuicEndpoint* endpoint) override; + void OnDestroy() override; +}; + +// A serialized QuicPacket to be sent by a QuicSocket instance. +class QuicPacket : public MemoryRetainer { + public: + // Creates a new QuicPacket. By default the packet will be + // stack allocated with a max size of NGTCP2_MAX_PKTLEN_IPV4. + // If a larger packet size is specified, it will be heap + // allocated. Generally speaking, a QUIC packet should never + // be larger than the current MTU to avoid IP fragmentation. + // + // The content of a QuicPacket is provided by ngtcp2. The + // typical use pattern is to create a QuicPacket instance + // and then pass a pointer to it's internal buffer and max + // size in to an ngtcp2 function that serializes the data. + // ngtcp2 will fill the buffer as much as possible then return + // the number of bytes serialized. User code is then responsible + // for calling set_length() to set the final length of the + // QuicPacket prior to sending it off to the QuicSocket. + // + // The diagnostic label is used in NODE_DEBUG_NATIVE output + // to differentiate send operations. This should always be + // a statically allocated string or nullptr (in which case + // the value "unspecified" is used in the debug output). + // + // Instances of std::unique_ptr are moved through + // QuicSocket and ultimately become the responsibility of the + // SendWrap instance. When the SendWrap is cleaned up, the + // QuicPacket instance will be freed. + static inline std::unique_ptr Create( + const char* diagnostic_label = nullptr, + size_t len = NGTCP2_MAX_PKTLEN_IPV4); + + // Copy the data of the QuicPacket to a new one. Currently, + // this is only used when retransmitting close connection + // packets from a QuicServer. + static inline std::unique_ptr Copy( + const std::unique_ptr& other); + + QuicPacket(const char* diagnostic_label, size_t len); + QuicPacket(const QuicPacket& other); + uint8_t* data() { return data_.data(); } + size_t length() const { return data_.size(); } + uv_buf_t buf() const { + return uv_buf_init( + const_cast(reinterpret_cast(data_.data())), + length()); + } + inline void set_length(size_t len); + const char* diagnostic_label() const; + + void MemoryInfo(MemoryTracker* tracker) const override; + SET_MEMORY_INFO_NAME(QuicPacket); + SET_SELF_SIZE(QuicPacket); + + private: + std::vector data_; + const char* diagnostic_label_ = nullptr; +}; + +// QuicEndpointListener listens to events generated by a QuicEndpoint. +class QuicEndpointListener { + public: + virtual void OnError(QuicEndpoint* endpoint, ssize_t error) = 0; + virtual void OnReceive( + ssize_t nread, + AllocatedBuffer buf, + const SocketAddress& local_addr, + const SocketAddress& remote_addr, + unsigned int flags) = 0; + virtual ReqWrap* OnCreateSendWrap(size_t msg_size) = 0; + virtual void OnSendDone(ReqWrap* wrap, int status) = 0; + virtual void OnBind(QuicEndpoint* endpoint) = 0; + virtual void OnEndpointDone(QuicEndpoint* endpoint) = 0; +}; + +// A QuicEndpoint wraps a UDPBaseWrap. A single QuicSocket may +// have multiple QuicEndpoints, the lifecycles of which are +// attached to the QuicSocket. +class QuicEndpoint : public BaseObject, + public UDPListener { + public: + static void Initialize( + Environment* env, + Local target, + Local context); + + QuicEndpoint( + QuicState* quic_state, + Local wrap, + QuicSocket* listener, + Local udp_wrap); + + const SocketAddress& local_address() const { + local_address_ = udp_->GetSockName(); + return local_address_; + } + + // Implementation for UDPListener + uv_buf_t OnAlloc(size_t suggested_size) override; + + void OnRecv(ssize_t nread, + const uv_buf_t& buf, + const sockaddr* addr, + unsigned int flags) override; + + ReqWrap* CreateSendWrap(size_t msg_size) override; + + void OnSendDone(ReqWrap* wrap, int status) override; + + void OnAfterBind() override; + + inline int ReceiveStart(); + + inline int ReceiveStop(); + + inline int Send( + uv_buf_t* buf, + size_t len, + const sockaddr* addr); + + void IncrementPendingCallbacks() { pending_callbacks_++; } + void DecrementPendingCallbacks() { pending_callbacks_--; } + bool has_pending_callbacks() { return pending_callbacks_ > 0; } + inline void WaitForPendingCallbacks(); + + QuicState* quic_state() const { return quic_state_.get(); } + + void MemoryInfo(MemoryTracker* tracker) const override; + SET_MEMORY_INFO_NAME(QuicEndpoint) + SET_SELF_SIZE(QuicEndpoint) + + private: + mutable SocketAddress local_address_; + BaseObjectWeakPtr listener_; + UDPWrapBase* udp_; + BaseObjectPtr strong_ptr_; + size_t pending_callbacks_ = 0; + bool waiting_for_callbacks_ = false; + BaseObjectPtr quic_state_; +}; + +// QuicSocket manages the flow of data from the UDP socket to the +// QuicSession. It is responsible for managing the lifecycle of the +// UDP sockets, listening for new server QuicSession instances, and +// passing data two and from the remote peer. +class QuicSocket : public AsyncWrap, + public QuicEndpointListener, + public mem::NgLibMemoryManager, + public StatsBase { + public: + static void Initialize( + Environment* env, + Local target, + Local context); + + QuicSocket( + QuicState* quic_state, + Local wrap, + // A retry token should only be valid for a small window of time. + // The retry_token_expiration specifies the number of seconds a + // retry token is permitted to be valid. + uint64_t retry_token_expiration, + // To prevent malicious clients from opening too many concurrent + // connections, we limit the maximum number per remote sockaddr. + size_t max_connections, + size_t max_connections_per_host, + size_t max_stateless_resets_per_host + = DEFAULT_MAX_STATELESS_RESETS_PER_HOST, + uint32_t options = 0, + QlogMode qlog = QlogMode::kDisabled, + const uint8_t* session_reset_secret = nullptr, + bool disable_session_reset = false); + + ~QuicSocket() override; + + // Returns the default/preferred local address. Additional + // QuicEndpoint instances may be associated with the + // QuicSocket bound to other local addresses. + inline const SocketAddress& local_address(); + + void MaybeClose(); + + inline void AddSession( + const QuicCID& cid, + BaseObjectPtr session); + + inline void AssociateCID( + const QuicCID& cid, + const QuicCID& scid); + + inline void DisassociateCID( + const QuicCID& cid); + + inline void AssociateStatelessResetToken( + const StatelessResetToken& token, + BaseObjectPtr session); + + inline void DisassociateStatelessResetToken( + const StatelessResetToken& token); + + void Listen( + BaseObjectPtr context, + const sockaddr* preferred_address = nullptr, + const std::string& alpn = NGTCP2_ALPN_H3, + uint32_t options = 0); + + inline void ReceiveStart(); + + inline void ReceiveStop(); + + inline void RemoveSession( + const QuicCID& cid, + const SocketAddress& addr); + + inline void ReportSendError(int error); + + int SendPacket( + const SocketAddress& local_addr, + const SocketAddress& remote_addr, + std::unique_ptr packet, + BaseObjectPtr session = BaseObjectPtr()); + + inline void SessionReady(BaseObjectPtr session); + + inline void set_server_busy(bool on); + + inline void set_diagnostic_packet_loss(double rx = 0.0, double tx = 0.0); + + inline void StopListening(); + + // Toggles whether or not stateless reset is enabled or not. + // Returns true if stateless reset is enabled, false if it + // is not. + inline bool ToggleStatelessReset(); + + BaseObjectPtr server_secure_context() const { + return server_secure_context_; + } + + QuicState* quic_state() { return quic_state_.get(); } + + void MemoryInfo(MemoryTracker* tracker) const override; + SET_MEMORY_INFO_NAME(QuicSocket) + SET_SELF_SIZE(QuicSocket) + + // Implementation for mem::NgLibMemoryManager + void CheckAllocatedSize(size_t previous_size) const; + + void IncreaseAllocatedSize(size_t size); + + void DecreaseAllocatedSize(size_t size); + + const uint8_t* session_reset_secret() { return reset_token_secret_; } + + // Implementation for QuicListener + ReqWrap* OnCreateSendWrap(size_t msg_size) override; + + // Implementation for QuicListener + void OnSendDone(ReqWrap* wrap, int status) override; + + // Implementation for QuicListener + void OnBind(QuicEndpoint* endpoint) override; + + // Implementation for QuicListener + void OnReceive( + ssize_t nread, + AllocatedBuffer buf, + const SocketAddress& local_addr, + const SocketAddress& remote_addr, + unsigned int flags) override; + + // Implementation for QuicListener + void OnError(QuicEndpoint* endpoint, ssize_t error) override; + + // Implementation for QuicListener + void OnEndpointDone(QuicEndpoint* endpoint) override; + + // Serializes and transmits a RETRY packet to the connected peer. + bool SendRetry( + const QuicCID& dcid, + const QuicCID& scid, + const SocketAddress& local_addr, + const SocketAddress& remote_addr); + + // Serializes and transmits a Stateless Reset to the connected peer. + bool SendStatelessReset( + const QuicCID& cid, + const SocketAddress& local_addr, + const SocketAddress& remote_addr, + size_t source_len); + + // Serializes and transmits a Version Negotiation packet to the + // connected peer. + void SendVersionNegotiation( + uint32_t version, + const QuicCID& dcid, + const QuicCID& scid, + const SocketAddress& local_addr, + const SocketAddress& remote_addr); + + void PushListener(QuicSocketListener* listener); + + void RemoveListener(QuicSocketListener* listener); + + inline void AddEndpoint( + BaseObjectPtr endpoint, + bool preferred = false); + + void ImmediateConnectionClose( + const QuicCID& scid, + const QuicCID& dcid, + const SocketAddress& local_addr, + const SocketAddress& remote_addr, + int64_t reason = NGTCP2_INVALID_TOKEN); + + private: + static void OnAlloc( + uv_handle_t* handle, + size_t suggested_size, + uv_buf_t* buf); + + void OnSend(int status, QuicPacket* packet); + + inline void set_validated_address(const SocketAddress& addr); + + inline bool is_validated_address(const SocketAddress& addr) const; + + bool MaybeStatelessReset( + const QuicCID& dcid, + const QuicCID& scid, + ssize_t nread, + const uint8_t* data, + const SocketAddress& local_addr, + const SocketAddress& remote_addr, + unsigned int flags); + + BaseObjectPtr AcceptInitialPacket( + uint32_t version, + const QuicCID& dcid, + const QuicCID& scid, + ssize_t nread, + const uint8_t* data, + const SocketAddress& local_addr, + const SocketAddress& remote_addr, + unsigned int flags); + + BaseObjectPtr FindSession(const QuicCID& cid); + + inline void IncrementSocketAddressCounter(const SocketAddress& addr); + + inline void DecrementSocketAddressCounter(const SocketAddress& addr); + + inline void IncrementStatelessResetCounter(const SocketAddress& addr); + + inline size_t GetCurrentSocketAddressCounter(const SocketAddress& addr); + + inline size_t GetCurrentStatelessResetCounter(const SocketAddress& addr); + + // Returns true if, and only if, diagnostic packet loss is enabled + // and the current packet should be artificially considered lost. + inline bool is_diagnostic_packet_loss(double prob) const; + + bool is_stateless_reset_disabled() { + return is_flag_set(QUICSOCKET_FLAGS_DISABLE_STATELESS_RESET); + } + + enum QuicSocketFlags : uint32_t { + QUICSOCKET_FLAGS_NONE = 0x0, + + // Indicates that the QuicSocket has entered a graceful + // closing phase, indicating that no additional + QUICSOCKET_FLAGS_GRACEFUL_CLOSE = 0x1, + QUICSOCKET_FLAGS_WAITING_FOR_CALLBACKS = 0x2, + QUICSOCKET_FLAGS_SERVER_LISTENING = 0x4, + QUICSOCKET_FLAGS_SERVER_BUSY = 0x8, + QUICSOCKET_FLAGS_DISABLE_STATELESS_RESET = 0x10 + }; + + void set_flag(QuicSocketFlags flag, bool on = true) { + if (on) + flags_ |= flag; + else + flags_ &= ~flag; + } + + bool is_flag_set(QuicSocketFlags flag) const { + return flags_ & flag; + } + + void set_option(QuicSocketOptions option, bool on = true) { + if (on) + options_ |= option; + else + options_ &= ~option; + } + + bool is_option_set(QuicSocketOptions option) const { + return options_ & option; + } + + ngtcp2_mem alloc_info_; + + std::vector> endpoints_; + SocketAddress::Map> bound_endpoints_; + BaseObjectWeakPtr preferred_endpoint_; + + uint32_t flags_ = QUICSOCKET_FLAGS_NONE; + uint32_t options_; + uint32_t server_options_; + + size_t max_connections_ = DEFAULT_MAX_CONNECTIONS; + size_t max_connections_per_host_ = DEFAULT_MAX_CONNECTIONS_PER_HOST; + size_t current_ngtcp2_memory_ = 0; + size_t max_stateless_resets_per_host_ = DEFAULT_MAX_STATELESS_RESETS_PER_HOST; + + uint64_t retry_token_expiration_; + + // Used to specify diagnostic packet loss probabilities + double rx_loss_ = 0.0; + double tx_loss_ = 0.0; + + QuicSocketListener* listener_; + JSQuicSocketListener default_listener_; + QuicSessionConfig server_session_config_; + QlogMode qlog_ = QlogMode::kDisabled; + BaseObjectPtr server_secure_context_; + std::string server_alpn_; + QuicCID::Map> sessions_; + QuicCID::Map dcid_to_scid_; + + uint8_t token_secret_[kTokenSecretLen]; + uint8_t reset_token_secret_[NGTCP2_STATELESS_RESET_TOKENLEN]; + + // Counts the number of active connections per remote + // address. A custom std::hash specialization for + // sockaddr instances is used. Values are incremented + // when a QuicSession is added to the socket, and + // decremented when the QuicSession is removed. If the + // value reaches the value of max_connections_per_host_, + // attempts to create new connections will be ignored + // until the value falls back below the limit. + SocketAddress::Map addr_counts_; + + // Counts the number of stateless resets sent per + // remote address. + // TODO(@jasnell): this counter persists through the + // lifetime of the QuicSocket, and therefore can become + // a possible risk. Specifically, a malicious peer could + // attempt the local peer to count an increasingly large + // number of remote addresses. Need to mitigate the + // potential risk. + SocketAddress::Map reset_counts_; + + // Counts the number of retry attempts sent per + // remote address. + + StatelessResetToken::Map token_map_; + + // The validated_addrs_ vector is used as an LRU cache for + // validated addresses only when the VALIDATE_ADDRESS_LRU + // option is set. + typedef size_t SocketAddressHash; + std::deque validated_addrs_; + + class SendWrap : public ReqWrap { + public: + SendWrap(QuicState* quic_state, + v8::Local req_wrap_obj, + size_t total_length_); + + void set_packet(std::unique_ptr packet) { + packet_ = std::move(packet); + } + + QuicPacket* packet() { return packet_.get(); } + + void set_session(BaseObjectPtr session) { session_ = session; } + + size_t total_length() const { return total_length_; } + + QuicState* quic_state() { return quic_state_.get(); } + + SET_SELF_SIZE(SendWrap); + std::string MemoryInfoName() const override; + void MemoryInfo(MemoryTracker* tracker) const override; + + private: + BaseObjectPtr session_; + std::unique_ptr packet_; + size_t total_length_; + BaseObjectPtr quic_state_; + }; + + SendWrap* last_created_send_wrap_ = nullptr; + BaseObjectPtr quic_state_; + + friend class QuicSocketListener; +}; + +} // namespace quic +} // namespace node + +#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS + +#endif // SRC_QUIC_NODE_QUIC_SOCKET_H_ diff --git a/src/quic/node_quic_state.h b/src/quic/node_quic_state.h new file mode 100644 index 00000000000000..31f2f42a729bb9 --- /dev/null +++ b/src/quic/node_quic_state.h @@ -0,0 +1,81 @@ +#ifndef SRC_QUIC_NODE_QUIC_STATE_H_ +#define SRC_QUIC_NODE_QUIC_STATE_H_ + +#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS + +#include "aliased_buffer.h" + +namespace node { +namespace quic { + +enum QuicSessionConfigIndex : int { + 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_PACKET_SIZE, + IDX_QUIC_SESSION_ACK_DELAY_EXPONENT, + IDX_QUIC_SESSION_DISABLE_MIGRATION, + IDX_QUIC_SESSION_MAX_ACK_DELAY, + IDX_QUIC_SESSION_CONFIG_COUNT +}; + +enum Http3ConfigIndex : int { + 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 +}; + +class QuicState : public BaseObject { + public: + explicit QuicState(Environment* env, v8::Local obj) + : BaseObject(env, obj), + root_buffer( + env->isolate(), + sizeof(quic_state_internal)), + quicsessionconfig_buffer( + env->isolate(), + offsetof(quic_state_internal, quicsessionconfig_buffer), + IDX_QUIC_SESSION_CONFIG_COUNT + 1, + root_buffer), + http3config_buffer( + env->isolate(), + offsetof(quic_state_internal, http3config_buffer), + IDX_HTTP3_CONFIG_COUNT + 1, + root_buffer) { + } + + AliasedUint8Array root_buffer; + AliasedFloat64Array quicsessionconfig_buffer; + AliasedFloat64Array http3config_buffer; + + bool warn_trace_tls = true; + + static constexpr FastStringKey binding_data_name { "quic" }; + + void MemoryInfo(MemoryTracker* tracker) const override; + SET_SELF_SIZE(QuicState) + SET_MEMORY_INFO_NAME(QuicState) + + private: + struct quic_state_internal { + // doubles first so that they are always sizeof(double)-aligned + double quicsessionconfig_buffer[IDX_QUIC_SESSION_CONFIG_COUNT + 1]; + double http3config_buffer[IDX_HTTP3_CONFIG_COUNT + 1]; + }; +}; + +} // namespace quic +} // namespace node + +#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS + +#endif // SRC_QUIC_NODE_QUIC_STATE_H_ diff --git a/src/quic/node_quic_stream-inl.h b/src/quic/node_quic_stream-inl.h new file mode 100644 index 00000000000000..3da0f5fb3b57cf --- /dev/null +++ b/src/quic/node_quic_stream-inl.h @@ -0,0 +1,158 @@ +#ifndef SRC_QUIC_NODE_QUIC_STREAM_INL_H_ +#define SRC_QUIC_NODE_QUIC_STREAM_INL_H_ + +#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS + +#include "debug_utils-inl.h" +#include "node_quic_session.h" +#include "node_quic_stream.h" +#include "node_quic_buffer-inl.h" + +namespace node { +namespace quic { + +QuicStreamDirection QuicStream::direction() const { + return stream_id_ & 0b10 ? + QUIC_STREAM_UNIDIRECTIONAL : + QUIC_STREAM_BIRECTIONAL; +} + +QuicStreamOrigin QuicStream::origin() const { + return stream_id_ & 0b01 ? + QUIC_STREAM_SERVER : + QUIC_STREAM_CLIENT; +} + +bool QuicStream::is_flag_set(int32_t flag) const { + return flags_ & (1 << flag); +} + +void QuicStream::set_flag(int32_t flag, bool on) { + if (on) + flags_ |= (1 << flag); + else + flags_ &= ~(1 << flag); +} + +void QuicStream::set_final_size(uint64_t final_size) { + CHECK_EQ(GetStat(&QuicStreamStats::final_size), 0); + SetStat(&QuicStreamStats::final_size, final_size); +} + +bool QuicStream::is_destroyed() const { + return is_flag_set(QUICSTREAM_FLAG_DESTROYED); +} + +bool QuicStream::was_ever_writable() const { + if (direction() == QUIC_STREAM_UNIDIRECTIONAL) { + return session_->is_server() ? + origin() == QUIC_STREAM_SERVER : + origin() == QUIC_STREAM_CLIENT; + } + return true; +} + +bool QuicStream::is_writable() const { + return was_ever_writable() && !streambuf_.is_ended(); +} + +bool QuicStream::was_ever_readable() const { + if (direction() == QUIC_STREAM_UNIDIRECTIONAL) { + return session_->is_server() ? + origin() == QUIC_STREAM_CLIENT : + origin() == QUIC_STREAM_SERVER; + } + + return true; +} + +bool QuicStream::is_readable() const { + return was_ever_readable() && !is_flag_set(QUICSTREAM_FLAG_READ_CLOSED); +} + +void QuicStream::set_fin_sent() { + CHECK(!is_writable()); + set_flag(QUICSTREAM_FLAG_FIN_SENT); +} + +bool QuicStream::is_write_finished() const { + return is_flag_set(QUICSTREAM_FLAG_FIN_SENT) && + streambuf_.length() == 0; +} + +bool QuicStream::SubmitInformation(v8::Local headers) { + return session_->SubmitInformation(stream_id_, headers); +} + +bool QuicStream::SubmitHeaders(v8::Local headers, uint32_t flags) { + return session_->SubmitHeaders(stream_id_, headers, flags); +} + +bool QuicStream::SubmitTrailers(v8::Local headers) { + return session_->SubmitTrailers(stream_id_, headers); +} + +BaseObjectPtr QuicStream::SubmitPush( + v8::Local headers) { + return session_->SubmitPush(stream_id_, headers); +} + +void QuicStream::EndHeaders(int64_t push_id) { + Debug(this, "End Headers"); + // Upon completion of a block of headers, convert the + // vector of Header objects into an array of name+value + // pairs, then call the on_stream_headers function. + session()->application()->StreamHeaders( + stream_id_, + headers_kind_, + headers_, + push_id); + headers_.clear(); +} + +void QuicStream::set_headers_kind(QuicStreamHeadersKind kind) { + headers_kind_ = kind; +} + +void QuicStream::BeginHeaders(QuicStreamHeadersKind kind) { + Debug(this, "Beginning Headers"); + // Upon start of a new block of headers, ensure that any + // previously collected ones are cleaned up. + headers_.clear(); + set_headers_kind(kind); +} + +void QuicStream::Commit(size_t amount) { + CHECK(!is_destroyed()); + streambuf_.Seek(amount); +} + +void QuicStream::ResetStream(uint64_t app_error_code) { + // On calling shutdown, the stream will no longer be + // readable or writable, all any pending data in the + // streambuf_ will be canceled, and all data pending + // to be acknowledged at the ngtcp2 level will be + // abandoned. + BaseObjectPtr ptr(session_); + set_flag(QUICSTREAM_FLAG_READ_CLOSED); + session_->ResetStream(stream_id_, app_error_code); + streambuf_.Cancel(); + streambuf_.End(); +} + +void QuicStream::Schedule(Queue* queue) { + if (!stream_queue_.IsEmpty()) // Already scheduled? + return; + queue->PushBack(this); +} + +void QuicStream::Unschedule() { + stream_queue_.Remove(); +} + +} // namespace quic +} // namespace node + +#endif // NODE_WANT_INTERNALS + +#endif // SRC_QUIC_NODE_QUIC_STREAM_INL_H_ diff --git a/src/quic/node_quic_stream.cc b/src/quic/node_quic_stream.cc new file mode 100644 index 00000000000000..28aec15dfd58e5 --- /dev/null +++ b/src/quic/node_quic_stream.cc @@ -0,0 +1,513 @@ +#include "node_quic_stream-inl.h" // NOLINT(build/include) +#include "async_wrap-inl.h" +#include "debug_utils-inl.h" +#include "env-inl.h" +#include "node.h" +#include "node_buffer.h" +#include "node_internals.h" +#include "stream_base-inl.h" +#include "node_sockaddr-inl.h" +#include "node_http_common-inl.h" +#include "node_quic_session-inl.h" +#include "node_quic_socket-inl.h" +#include "node_quic_util-inl.h" +#include "v8.h" +#include "uv.h" + +#include +#include +#include +#include + +namespace node { + +using v8::Array; +using v8::Context; +using v8::FunctionCallbackInfo; +using v8::FunctionTemplate; +using v8::Isolate; +using v8::Local; +using v8::Object; +using v8::ObjectTemplate; +using v8::String; +using v8::Value; + +namespace quic { + +QuicStream::QuicStream( + QuicSession* sess, + Local wrap, + int64_t stream_id, + int64_t push_id) + : AsyncWrap(sess->env(), wrap, AsyncWrap::PROVIDER_QUICSTREAM), + StreamBase(sess->env()), + StatsBase(sess->env(), wrap, + HistogramOptions::ACK | + HistogramOptions::RATE | + HistogramOptions::SIZE), + session_(sess), + stream_id_(stream_id), + push_id_(push_id), + quic_state_(sess->quic_state()) { + CHECK_NOT_NULL(sess); + Debug(this, "Created"); + StreamBase::AttachToObject(GetObject()); + ngtcp2_transport_params params; + ngtcp2_conn_get_local_transport_params(session()->connection(), ¶ms); + IncrementStat(&QuicStreamStats::max_offset, params.initial_max_data); +} + +QuicStream::~QuicStream() { + DebugStats(); +} + +template +void QuicStreamStatsTraits::ToString(const QuicStream& ptr, Fn&& add_field) { +#define V(_n, name, label) \ + add_field(label, ptr.GetStat(&QuicStreamStats::name)); + STREAM_STATS(V) +#undef V +} + +// Acknowledge is called when ngtcp2 has received an acknowledgement +// for one or more stream frames for this QuicStream. This will cause +// data stored in the streambuf_ outbound queue to be consumed and may +// result in the JavaScript callback for the write to be invoked. +void QuicStream::Acknowledge(uint64_t offset, size_t datalen) { + if (is_destroyed()) + return; + + // ngtcp2 guarantees that offset must always be greater + // than the previously received offset, but let's just + // make sure that holds. + CHECK_GE(offset, GetStat(&QuicStreamStats::max_offset_ack)); + SetStat(&QuicStreamStats::max_offset_ack, offset); + + Debug(this, "Acknowledging %d bytes", datalen); + + // Consumes the given number of bytes in the buffer. This may + // have the side-effect of causing the onwrite callback to be + // invoked if a complete chunk of buffered data has been acknowledged. + streambuf_.Consume(datalen); + + RecordAck(&QuicStreamStats::acked_at); +} + +// While not all QUIC applications will support headers, QuicStream +// includes basic, generic support for storing them. +bool QuicStream::AddHeader(std::unique_ptr header) { + size_t len = header->length(); + QuicApplication* app = session()->application(); + // We cannot add the header if we've either reached + // * the max number of header pairs or + // * the max number of header bytes + if (headers_.size() == app->max_header_pairs() || + current_headers_length_ + len > app->max_header_length()) { + return false; + } + + current_headers_length_ += header->length(); + Debug(this, "Header - %s", header.get()); + headers_.emplace_back(std::move(header)); + return true; +} + +std::string QuicStream::diagnostic_name() const { + return std::string("QuicStream ") + std::to_string(stream_id_) + + " (" + std::to_string(static_cast(get_async_id())) + + ", " + session_->diagnostic_name() + ")"; +} + +void QuicStream::Destroy() { + if (is_destroyed()) + return; + set_flag(QUICSTREAM_FLAG_DESTROYED); + set_flag(QUICSTREAM_FLAG_READ_CLOSED); + streambuf_.End(); + + // If there is data currently buffered in the streambuf_, + // then cancel will call out to invoke an arbitrary + // JavaScript callback (the on write callback). Within + // that callback, however, the QuicStream will no longer + // be usable to send or receive data. + streambuf_.Cancel(); + CHECK_EQ(streambuf_.length(), 0); + + // The QuicSession maintains a map of std::unique_ptrs to + // QuicStream instances. Removing this here will cause + // this QuicStream object to be deconstructed, so the + // QuicStream object will no longer exist after this point. + session_->RemoveStream(stream_id_); +} + +// Do shutdown is called when the JS stream writable side is closed. +// If we're not within an ngtcp2 callback, this will trigger the +// QuicSession to send any pending data. Any time after this is +// called, a final stream frame will be sent for this QuicStream, +// but it may not be sent right away. +int QuicStream::DoShutdown(ShutdownWrap* req_wrap) { + if (is_destroyed()) + return UV_EPIPE; + + QuicSession::SendSessionScope send_scope(session(), true); + + if (is_writable()) { + Debug(this, "Shutdown writable side"); + RecordTimestamp(&QuicStreamStats::closing_at); + streambuf_.End(); + session()->ResumeStream(stream_id_); + } + return 1; +} + +int QuicStream::DoWrite( + WriteWrap* req_wrap, + uv_buf_t* bufs, + size_t nbufs, + uv_stream_t* send_handle) { + CHECK_NULL(send_handle); + + // A write should not have happened if we've been destroyed or + // the QuicStream is no longer (or was never) writable. + if (is_destroyed() || !is_writable()) { + req_wrap->Done(UV_EPIPE); + return 0; + } + + // Nothing to write. + size_t length = get_length(bufs, nbufs); + if (length == 0) { + req_wrap->Done(0); + return 0; + } + + QuicSession::SendSessionScope send_scope(session(), true); + + Debug(this, "Queuing %" PRIu64 " bytes of data from %d buffers", + length, nbufs); + IncrementStat(&QuicStreamStats::bytes_sent, static_cast(length)); + + BaseObjectPtr strong_ref{req_wrap->GetAsyncWrap()}; + // The list of buffers will be appended onto streambuf_ without + // copying. Those will remain in the buffer until the serialized + // stream frames are acknowledged. + // This callback function will be invoked once this + // complete batch of buffers has been acknowledged + // by the peer. This will have the side effect of + // blocking additional pending writes from the + // javascript side, so writing data to the stream + // will be throttled by how quickly the peer is + // able to acknowledge stream packets. This is good + // in the sense of providing back-pressure, but + // also means that writes will be significantly + // less performant unless written in batches. + streambuf_.Push( + bufs, + nbufs, + [req_wrap, strong_ref](int status) { + req_wrap->Done(status); + }); + + session()->ResumeStream(stream_id_); + + return 0; +} + +bool QuicStream::IsAlive() { + return !is_destroyed() && !IsClosing(); +} + +bool QuicStream::IsClosing() { + return !is_writable() && !is_readable(); +} + +int QuicStream::ReadStart() { + CHECK(!is_destroyed()); + CHECK(is_readable()); + set_flag(QUICSTREAM_FLAG_READ_STARTED); + set_flag(QUICSTREAM_FLAG_READ_PAUSED, false); + IncrementStat( + &QuicStreamStats::max_offset, + inbound_consumed_data_while_paused_); + session_->ExtendStreamOffset(id(), inbound_consumed_data_while_paused_); + return 0; +} + +int QuicStream::ReadStop() { + CHECK(!is_destroyed()); + CHECK(is_readable()); + set_flag(QUICSTREAM_FLAG_READ_PAUSED); + return 0; +} + +void QuicStream::IncrementStats(size_t datalen) { + uint64_t len = static_cast(datalen); + IncrementStat(&QuicStreamStats::bytes_received, len); + RecordRate(&QuicStreamStats::received_at); + RecordSize(len); +} + +void QuicStream::MemoryInfo(MemoryTracker* tracker) const { + tracker->TrackField("buffer", &streambuf_); + StatsBase::StatsMemoryInfo(tracker); + tracker->TrackField("headers", headers_); +} + +BaseObjectPtr QuicStream::New( + QuicSession* session, + int64_t stream_id, + int64_t push_id) { + Local obj; + if (!session->env() + ->quicserverstream_instance_template() + ->NewInstance(session->env()->context()).ToLocal(&obj)) { + return {}; + } + BaseObjectPtr stream = + MakeDetachedBaseObject( + session, + obj, + stream_id, + push_id); + CHECK(stream); + session->AddStream(stream); + return stream; +} + +// Passes chunks of data on to the JavaScript side as soon as they are +// received but only if we're still readable. The caller of this must have a +// HandleScope. +// +// Note that this is pushing data to the JS side regardless of whether +// anything is listening. For flow-control, we only send window updates +// to the sending peer if the stream is in flowing mode, so the sender +// should not be sending too much data. +void QuicStream::ReceiveData( + int fin, + const uint8_t* data, + size_t datalen, + uint64_t offset) { + CHECK(!is_destroyed()); + Debug(this, "Receiving %d bytes. Final? %s. Readable? %s", + datalen, + fin ? "yes" : "no", + is_readable() ? "yes" : "no"); + + // If the QuicStream is not (or was never) readable, just ignore the chunk. + if (!is_readable()) + return; + + // ngtcp2 guarantees that datalen will only be 0 if fin is set. + // Let's just make sure. + CHECK(datalen > 0 || fin == 1); + + // ngtcp2 guarantees that offset is always greater than the previously + // received offset. Let's just make sure. + CHECK_GE(offset, GetStat(&QuicStreamStats::max_offset_received)); + SetStat(&QuicStreamStats::max_offset_received, offset); + + if (datalen > 0) { + // IncrementStats will update the data_rx_rate_ and data_rx_size_ + // histograms. These will provide data necessary to detect and + // prevent Slow Send DOS attacks specifically by allowing us to + // see if a connection is sending very small chunks of data at very + // slow speeds. It is important to emphasize, however, that slow send + // rates may be perfectly legitimate so we cannot simply take blanket + // action when slow rates are detected. Nor can we reliably define what + // a slow rate even is! Will will need to determine some reasonable + // default and allow user code to change the default as well as determine + // what action to take. The current strategy will be to trigger an event + // on the stream when data transfer rates are likely to be considered too + // slow. + IncrementStats(datalen); + + while (datalen > 0) { + uv_buf_t buf = EmitAlloc(datalen); + size_t avail = std::min(static_cast(buf.len), datalen); + + // For now, we're allocating and copying. Once we determine if we can + // safely switch to a non-allocated mode like we do with http2 streams, + // we can make this branch more efficient by using the LIKELY + // optimization. The way ngtcp2 currently works, however, we have + // to memcpy here. + if (UNLIKELY(buf.base == nullptr)) + buf.base = reinterpret_cast(const_cast(data)); + else + memcpy(buf.base, data, avail); + data += avail; + datalen -= avail; + // Capture read_paused before EmitRead in case user code callbacks + // alter the state when EmitRead is called. + bool read_paused = is_flag_set(QUICSTREAM_FLAG_READ_PAUSED); + EmitRead(avail, buf); + // Reading can be paused while we are processing. If that's + // the case, we still want to acknowledge the current bytes + // so that pausing does not throw off our flow control. + if (read_paused) { + inbound_consumed_data_while_paused_ += avail; + } else { + IncrementStat(&QuicStreamStats::max_offset, avail); + session_->ExtendStreamOffset(id(), avail); + } + } + } + + // When fin != 0, we've received that last chunk of data for this + // stream, indicating that the stream will no longer be readable. + if (fin) { + set_flag(QUICSTREAM_FLAG_FIN); + set_final_size(offset + datalen); + EmitRead(UV_EOF); + } +} + +int QuicStream::DoPull( + bob::Next next, + int options, + ngtcp2_vec* data, + size_t count, + size_t max_count_hint) { + return streambuf_.Pull( + std::move(next), + options, + data, + count, + max_count_hint); +} + +// JavaScript API +namespace { +void QuicStreamGetID(const FunctionCallbackInfo& args) { + QuicStream* stream; + ASSIGN_OR_RETURN_UNWRAP(&stream, args.Holder()); + args.GetReturnValue().Set(static_cast(stream->id())); +} + +void OpenUnidirectionalStream(const FunctionCallbackInfo& args) { + CHECK(!args.IsConstructCall()); + CHECK(args[0]->IsObject()); + QuicSession* session; + ASSIGN_OR_RETURN_UNWRAP(&session, args[0].As()); + + int64_t stream_id; + if (!session->OpenUnidirectionalStream(&stream_id)) + return; + + BaseObjectPtr stream = QuicStream::New(session, stream_id); + args.GetReturnValue().Set(stream->object()); +} + +void OpenBidirectionalStream(const FunctionCallbackInfo& args) { + CHECK(!args.IsConstructCall()); + CHECK(args[0]->IsObject()); + QuicSession* session; + ASSIGN_OR_RETURN_UNWRAP(&session, args[0].As()); + + int64_t stream_id; + if (!session->OpenBidirectionalStream(&stream_id)) + return; + + BaseObjectPtr stream = QuicStream::New(session, stream_id); + args.GetReturnValue().Set(stream->object()); +} + +void QuicStreamDestroy(const FunctionCallbackInfo& args) { + QuicStream* stream; + ASSIGN_OR_RETURN_UNWRAP(&stream, args.Holder()); + stream->Destroy(); +} + +void QuicStreamReset(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + QuicStream* stream; + ASSIGN_OR_RETURN_UNWRAP(&stream, args.Holder()); + + QuicError error(env, args[0], args[1], QUIC_ERROR_APPLICATION); + + stream->ResetStream( + error.family == QUIC_ERROR_APPLICATION ? + error.code : static_cast(NGTCP2_NO_ERROR)); +} + +// Requests transmission of a block of informational headers. Not all +// QUIC Applications will support headers. If headers are not supported, +// This will set the return value to false, otherwise the return value +// is set to true +void QuicStreamSubmitInformation(const FunctionCallbackInfo& args) { + QuicStream* stream; + ASSIGN_OR_RETURN_UNWRAP(&stream, args.Holder()); + CHECK(args[0]->IsArray()); + args.GetReturnValue().Set(stream->SubmitInformation(args[0].As())); +} + +// Requests transmission of a block of initial headers. Not all +// QUIC Applications will support headers. If headers are not supported, +// this will set the return value to false, otherwise the return value +// is set to true. For http/3, these may be request or response headers. +void QuicStreamSubmitHeaders(const FunctionCallbackInfo& args) { + QuicStream* stream; + ASSIGN_OR_RETURN_UNWRAP(&stream, args.Holder()); + CHECK(args[0]->IsArray()); + uint32_t flags = QUICSTREAM_HEADER_FLAGS_NONE; + CHECK(args[1]->Uint32Value(stream->env()->context()).To(&flags)); + args.GetReturnValue().Set(stream->SubmitHeaders(args[0].As(), flags)); +} + +// Requests transmission of a block of trailing headers. Not all +// QUIC Applications will support headers. If headers are not supported, +// this will set the return value to false, otherwise the return value +// is set to true. +void QuicStreamSubmitTrailers(const FunctionCallbackInfo& args) { + QuicStream* stream; + ASSIGN_OR_RETURN_UNWRAP(&stream, args.Holder()); + CHECK(args[0]->IsArray()); + args.GetReturnValue().Set(stream->SubmitTrailers(args[0].As())); +} + +// Requests creation of a push stream. Not all QUIC Applications will +// support push streams. If pushes are not supported, the return value +// will be undefined, otherwise the return value will be the created +// QuicStream representing the push. +void QuicStreamSubmitPush(const FunctionCallbackInfo& args) { + QuicStream* stream; + ASSIGN_OR_RETURN_UNWRAP(&stream, args.Holder()); + CHECK(args[0]->IsArray()); + BaseObjectPtr push_stream = + stream->SubmitPush(args[0].As()); + if (push_stream) + args.GetReturnValue().Set(push_stream->object()); +} + +} // namespace + +void QuicStream::Initialize( + Environment* env, + Local target, + Local context) { + Isolate* isolate = env->isolate(); + Local class_name = FIXED_ONE_BYTE_STRING(isolate, "QuicStream"); + Local stream = FunctionTemplate::New(env->isolate()); + stream->SetClassName(class_name); + stream->Inherit(AsyncWrap::GetConstructorTemplate(env)); + StreamBase::AddMethods(env, stream); + Local streamt = stream->InstanceTemplate(); + streamt->SetInternalFieldCount(StreamBase::kInternalFieldCount); + streamt->Set(env->owner_symbol(), Null(env->isolate())); + env->SetProtoMethod(stream, "destroy", QuicStreamDestroy); + env->SetProtoMethod(stream, "resetStream", QuicStreamReset); + env->SetProtoMethod(stream, "id", QuicStreamGetID); + env->SetProtoMethod(stream, "submitInformation", QuicStreamSubmitInformation); + env->SetProtoMethod(stream, "submitHeaders", QuicStreamSubmitHeaders); + env->SetProtoMethod(stream, "submitTrailers", QuicStreamSubmitTrailers); + env->SetProtoMethod(stream, "submitPush", QuicStreamSubmitPush); + env->set_quicserverstream_instance_template(streamt); + target->Set(env->context(), + class_name, + stream->GetFunction(env->context()).ToLocalChecked()).FromJust(); + + env->SetMethod(target, "openBidirectionalStream", OpenBidirectionalStream); + env->SetMethod(target, "openUnidirectionalStream", OpenUnidirectionalStream); +} + +} // namespace quic +} // namespace node diff --git a/src/quic/node_quic_stream.h b/src/quic/node_quic_stream.h new file mode 100644 index 00000000000000..6e968292b00936 --- /dev/null +++ b/src/quic/node_quic_stream.h @@ -0,0 +1,407 @@ +#ifndef SRC_QUIC_NODE_QUIC_STREAM_H_ +#define SRC_QUIC_NODE_QUIC_STREAM_H_ + +#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS + +#include "memory_tracker.h" +#include "async_wrap.h" +#include "env.h" +#include "node_http_common.h" +#include "node_quic_state.h" +#include "node_quic_util.h" +#include "stream_base-inl.h" +#include "util-inl.h" +#include "v8.h" + +#include +#include + +namespace node { +namespace quic { + +class QuicSession; +class QuicStream; +class QuicApplication; + +using QuicHeader = NgHeaderBase; + +enum QuicStreamHeaderFlags : uint32_t { + // No flags + QUICSTREAM_HEADER_FLAGS_NONE = 0, + + // Set if the initial headers are considered + // terminal (that is, the stream should be closed + // after transmitting the headers). If headers are + // not supported by the QUIC Application, flag is + // ignored. + QUICSTREAM_HEADER_FLAGS_TERMINAL = 1 +}; + +enum QuicStreamHeadersKind : int { + QUICSTREAM_HEADERS_KIND_NONE = 0, + QUICSTREAM_HEADERS_KIND_INFORMATIONAL, + QUICSTREAM_HEADERS_KIND_INITIAL, + QUICSTREAM_HEADERS_KIND_TRAILING, + QUICSTREAM_HEADERS_KIND_PUSH +}; + +#define STREAM_STATS(V) \ + V(CREATED_AT, created_at, "Created At") \ + V(RECEIVED_AT, received_at, "Last Received At") \ + V(ACKED_AT, acked_at, "Last Acknowledged At") \ + V(CLOSING_AT, closing_at, "Closing At") \ + V(BYTES_RECEIVED, bytes_received, "Bytes Received") \ + V(BYTES_SENT, bytes_sent, "Bytes Sent") \ + V(MAX_OFFSET, max_offset, "Max Offset") \ + V(MAX_OFFSET_ACK, max_offset_ack, "Max Acknowledged Offset") \ + V(MAX_OFFSET_RECV, max_offset_received, "Max Received Offset") \ + V(FINAL_SIZE, final_size, "Final Size") + +#define V(name, _, __) IDX_QUIC_STREAM_STATS_##name, +enum QuicStreamStatsIdx : int { + STREAM_STATS(V) + IDX_QUIC_STREAM_STATS_COUNT +}; +#undef V + +#define V(_, name, __) uint64_t name; +struct QuicStreamStats { + STREAM_STATS(V) +}; +#undef V + +struct QuicStreamStatsTraits { + using Stats = QuicStreamStats; + using Base = QuicStream; + + template + static void ToString(const Base& ptr, Fn&& add_field); +}; + +enum QuicStreamStates : uint32_t { + // QuicStream is fully open. Readable and Writable + QUICSTREAM_FLAG_INITIAL = 0, + + // QuicStream Read State is closed because a final stream frame + // has been received from the peer or the QuicStream is unidirectional + // outbound only (i.e. it was never readable) + QUICSTREAM_FLAG_READ_CLOSED, + + // JavaScript side has switched into flowing mode (Readable side) + QUICSTREAM_FLAG_READ_STARTED, + + // JavaScript side has paused the flow of data (Readable side) + QUICSTREAM_FLAG_READ_PAUSED, + + // QuicStream has received a final stream frame (Readable side) + QUICSTREAM_FLAG_FIN, + + // QuicStream has sent a final stream frame (Writable side) + QUICSTREAM_FLAG_FIN_SENT, + + // QuicStream has been destroyed + QUICSTREAM_FLAG_DESTROYED +}; + +enum QuicStreamDirection { + // The QuicStream is readable and writable in both directions + QUIC_STREAM_BIRECTIONAL, + + // The QuicStream is writable and readable in only one direction. + // The direction depends on the QuicStreamOrigin. + QUIC_STREAM_UNIDIRECTIONAL +}; + +enum QuicStreamOrigin { + // The QuicStream was created by the server. + QUIC_STREAM_SERVER, + + // The QuicStream was created by the client. + QUIC_STREAM_CLIENT +}; + +// QuicStream's are simple data flows that, fortunately, do not +// require much. They may be: +// +// * Bidirectional or Unidirectional +// * Server or Client Initiated +// +// The flow direction and origin of the stream are important in +// determining the write and read state (Open or Closed). Specifically: +// +// A Unidirectional stream originating with the Server is: +// +// * Server Writable (Open) but not Client Writable (Closed) +// * Client Readable (Open) but not Server Readable (Closed) +// +// Likewise, a Unidirectional stream originating with the +// Client is: +// +// * Client Writable (Open) but not Server Writable (Closed) +// * Server Readable (Open) but not Client Readable (Closed) +// +// Bidirectional Stream States +// +------------+--------------+--------------------+---------------------+ +// | | Initiated By | Initial Read State | Initial Write State | +// +------------+--------------+--------------------+---------------------+ +// | On Server | Server | Open | Open | +// +------------+--------------+--------------------+---------------------+ +// | On Server | Client | Open | Open | +// +------------+--------------+--------------------+---------------------+ +// | On Client | Server | Open | Open | +// +------------+--------------+--------------------+---------------------+ +// | On Client | Client | Open | Open | +// +------------+--------------+--------------------+---------------------+ +// +// Unidirectional Stream States +// +------------+--------------+--------------------+---------------------+ +// | | Initiated By | Initial Read State | Initial Write State | +// +------------+--------------+--------------------+---------------------+ +// | On Server | Server | Closed | Open | +// +------------+--------------+--------------------+---------------------+ +// | On Server | Client | Open | Closed | +// +------------+--------------+--------------------+---------------------+ +// | On Client | Server | Open | Closed | +// +------------+--------------+--------------------+---------------------+ +// | On Client | Client | Closed | Open | +// +------------+--------------+--------------------+---------------------+ +// +// All data sent via the QuicStream is buffered internally until either +// receipt is acknowledged from the peer or attempts to send are abandoned. +// +// A QuicStream may be in a fully Closed (Read and Write) state but still +// have unacknowledged data in it's outbound queue. +// +// A QuicStream is gracefully closed when (a) both Read and Write states +// are Closed, (b) all queued data has been acknowledged. +// +// The JavaScript Writable side of the QuicStream may be shutdown before +// all pending queued data has been serialized to frames. During this state, +// no additional data may be queued to send. +// +// The Write state of a QuicStream will not be closed while there is still +// pending writes on the JavaScript side. +// +// The QuicStream may be forcefully closed immediately using destroy(err). +// This causes all queued data and pending JavaScript writes to be +// abandoned, and causes the QuicStream to be immediately closed at the +// ngtcp2 level. +class QuicStream : public AsyncWrap, + public bob::SourceImpl, + public StreamBase, + public StatsBase { + public: + static void Initialize( + Environment* env, + v8::Local target, + v8::Local context); + + static BaseObjectPtr New( + QuicSession* session, + int64_t stream_id, + int64_t push_id = 0); + + QuicStream( + QuicSession* session, + v8::Local target, + int64_t stream_id, + int64_t push_id = 0); + + ~QuicStream() override; + + std::string diagnostic_name() const override; + + // The numeric identifier of the QuicStream. + int64_t id() const { return stream_id_; } + + // If the QuicStream is associated with a push promise, + // the numeric identifier of the promise. Currently only + // used by HTTP/3. + int64_t push_id() const { return push_id_; } + + QuicSession* session() const { return session_.get(); } + + // A QuicStream can be either uni- or bi-directional. + inline QuicStreamDirection direction() const; + + // A QuicStream can be initiated by either the client + // or the server. + inline QuicStreamOrigin origin() const; + + // The QuicStream has been destroyed and is no longer usable. + inline bool is_destroyed() const; + + // A QuicStream will not be writable if: + // - The streambuf_ is ended + // - It is a Unidirectional stream originating from the peer + inline bool is_writable() const; + + // A QuicStream will not be readable if: + // - The QUICSTREAM_FLAG_READ_CLOSED flag is set or + // - It is a Unidirectional stream originating from the local peer. + inline bool is_readable() const; + + // Records the fact that a final stream frame has been + // serialized and sent to the peer. There still may be + // unacknowledged data in the outbound queue, but no + // additional frames may be sent for the stream other + // than reset stream. + inline void set_fin_sent(); + + // IsWriteFinished will return true if a final stream frame + // has been sent and all data has been acknowledged (the + // send buffer is empty). + inline bool is_write_finished() const; + + // Specifies the kind of headers currently being processed. + inline void set_headers_kind(QuicStreamHeadersKind kind); + + // Set the final size for the QuicStream + inline void set_final_size(uint64_t final_size); + + // The final size is the maximum amount of data that has been + // acknowleged to have been received for a QuicStream. + uint64_t final_size() const { + return GetStat(&QuicStreamStats::final_size); + } + + // Marks the given data range as having been acknowledged. + // This means that the data range may be released from + // memory. + void Acknowledge(uint64_t offset, size_t datalen); + + // Destroy the QuicStream and render it no longer usable. + void Destroy(); + + // Buffers chunks of data to be written to the QUIC connection. + int DoWrite( + WriteWrap* req_wrap, + uv_buf_t* bufs, + size_t nbufs, + uv_stream_t* send_handle) override; + + // Returns false if the header cannot be added. This will + // typically only happen if a maximimum number of headers + // has been reached. + bool AddHeader(std::unique_ptr header); + + // Some QUIC applications support headers, others do not. + // The following methods allow consistent handling of + // headers at the QuicStream level regardless of the + // protocol. For applications that do not support headers, + // these are simply not used. + inline void BeginHeaders( + QuicStreamHeadersKind kind = QUICSTREAM_HEADERS_KIND_NONE); + + // Indicates an amount of unacknowledged data that has been + // submitted to the QUIC connection. + inline void Commit(size_t amount); + + inline void EndHeaders(int64_t push_id = 0); + + // Passes a chunk of data on to the QuicStream listener. + void ReceiveData( + int fin, + const uint8_t* data, + size_t datalen, + uint64_t offset); + + // Resets the QUIC stream, sending a signal to the peer that + // no additional data will be transmitted for this stream. + inline void ResetStream(uint64_t app_error_code = 0); + + // Submits informational headers. Returns false if headers are not + // supported on the underlying QuicApplication. + inline bool SubmitInformation(v8::Local headers); + + // Submits initial headers. Returns false if headers are not + // supported on the underlying QuicApplication. + inline bool SubmitHeaders(v8::Local headers, uint32_t flags); + + // Submits trailing headers. Returns false if headers are not + // supported on the underlying QuicApplication. + inline bool SubmitTrailers(v8::Local headers); + + inline BaseObjectPtr SubmitPush(v8::Local headers); + + // Required for StreamBase + bool IsAlive() override; + + // Required for StreamBase + bool IsClosing() override; + + // Required for StreamBase + int ReadStart() override; + + // Required for StreamBase + int ReadStop() override; + + // Required for StreamBase + int DoShutdown(ShutdownWrap* req_wrap) override; + + AsyncWrap* GetAsyncWrap() override { return this; } + + QuicState* quic_state() { return quic_state_.get(); } + + // Required for MemoryRetainer + void MemoryInfo(MemoryTracker* tracker) const override; + SET_MEMORY_INFO_NAME(QuicStream) + SET_SELF_SIZE(QuicStream) + + protected: + int DoPull( + bob::Next next, + int options, + ngtcp2_vec* data, + size_t count, + size_t max_count_hint) override; + + private: + inline bool is_flag_set(int32_t flag) const; + + inline void set_flag(int32_t flag, bool on = true); + + // WasEverWritable returns true if it is a bidirectional stream, + // or a Unidirectional stream originating from the local peer. + // If was_ever_writable() is false, then no stream frames should + // ever be sent from the local peer, including final stream frames. + inline bool was_ever_writable() const; + + // WasEverReadable returns true if it is a bidirectional stream, + // or a Unidirectional stream originating from the remote + // peer. + inline bool was_ever_readable() const; + + void IncrementStats(size_t datalen); + + BaseObjectWeakPtr session_; + QuicBuffer streambuf_; + + int64_t stream_id_ = 0; + int64_t push_id_ = 0; + uint32_t flags_ = QUICSTREAM_FLAG_INITIAL; + size_t inbound_consumed_data_while_paused_ = 0; + + std::vector> headers_; + QuicStreamHeadersKind headers_kind_; + size_t current_headers_length_ = 0; + + ListNode stream_queue_; + + BaseObjectPtr quic_state_; + + public: + // Linked List of QuicStream objects + using Queue = ListHead; + + inline void Schedule(Queue* queue); + + inline void Unschedule(); +}; + +} // namespace quic +} // namespace node + +#endif // NODE_WANT_INTERNALS + +#endif // SRC_QUIC_NODE_QUIC_STREAM_H_ diff --git a/src/quic/node_quic_util-inl.h b/src/quic/node_quic_util-inl.h new file mode 100644 index 00000000000000..dbd5648aecd3a5 --- /dev/null +++ b/src/quic/node_quic_util-inl.h @@ -0,0 +1,473 @@ +#ifndef SRC_QUIC_NODE_QUIC_UTIL_INL_H_ +#define SRC_QUIC_NODE_QUIC_UTIL_INL_H_ + +#include "debug_utils-inl.h" +#include "node_internals.h" +#include "node_quic_crypto.h" +#include "node_quic_util.h" +#include "memory_tracker-inl.h" +#include "env-inl.h" +#include "histogram-inl.h" +#include "string_bytes.h" +#include "util-inl.h" +#include "uv.h" + +#include + +namespace node { + +namespace quic { + +QuicPath::QuicPath( + const SocketAddress& local, + const SocketAddress& remote) { + ngtcp2_addr_init( + &this->local, + local.data(), + local.length(), + const_cast(&local)); + ngtcp2_addr_init( + &this->remote, + remote.data(), + remote.length(), + const_cast(&remote)); +} + +size_t QuicCID::Hash::operator()(const QuicCID& token) const { + size_t hash = 0; + for (size_t n = 0; n < token->datalen; n++) { + hash ^= std::hash{}(token->data[n]) + 0x9e3779b9 + + (hash << 6) + (hash >> 2); + } + return hash; +} + +QuicCID& QuicCID::operator=(const QuicCID& cid) { + if (this == &cid) return *this; + this->~QuicCID(); + return *new(this) QuicCID(std::move(cid)); +} + +bool QuicCID::operator==(const QuicCID& other) const { + return memcmp(cid()->data, other.cid()->data, cid()->datalen) == 0; +} + +bool QuicCID::operator!=(const QuicCID& other) const { + return !(*this == other); +} + +std::string QuicCID::ToString() const { + std::vector dest(ptr_->datalen * 2 + 1); + dest[dest.size() - 1] = '\0'; + size_t written = StringBytes::hex_encode( + reinterpret_cast(ptr_->data), + ptr_->datalen, + dest.data(), + dest.size()); + return std::string(dest.data(), written); +} + +size_t GetMaxPktLen(const SocketAddress& addr) { + return addr.family() == AF_INET6 ? + NGTCP2_MAX_PKTLEN_IPV6 : + NGTCP2_MAX_PKTLEN_IPV4; +} + +Timer::Timer(Environment* env, std::function fn) + : env_(env), + fn_(fn) { + uv_timer_init(env_->event_loop(), &timer_); + timer_.data = this; +} + +void Timer::Stop() { + if (stopped_) + return; + stopped_ = true; + + if (timer_.data == this) { + uv_timer_stop(&timer_); + timer_.data = nullptr; + } +} + +// If the timer is not currently active, interval must be either 0 or greater. +// If the timer is already active, interval is ignored. +void Timer::Update(uint64_t interval) { + if (stopped_) + return; + uv_timer_start(&timer_, OnTimeout, interval, interval); + uv_unref(reinterpret_cast(&timer_)); +} + +void Timer::Free(Timer* timer) { + timer->env_->CloseHandle( + reinterpret_cast(&timer->timer_), + [&](uv_handle_t* timer) { + Timer* t = ContainerOf( + &Timer::timer_, + reinterpret_cast(timer)); + delete t; + }); +} + +void Timer::OnTimeout(uv_timer_t* timer) { + Timer* t = ContainerOf(&Timer::timer_, timer); + t->fn_(); +} + +QuicError::QuicError( + int32_t family_, + uint64_t code_) : + family(family_), + code(code_) {} + +QuicError::QuicError( + int32_t family_, + int code_) : + family(family_) { + switch (family) { + case QUIC_ERROR_CRYPTO: + code_ |= NGTCP2_CRYPTO_ERROR; + // Fall-through... + case QUIC_ERROR_SESSION: + code = ngtcp2_err_infer_quic_transport_error_code(code_); + break; + case QUIC_ERROR_APPLICATION: + code = code_; + break; + default: + UNREACHABLE(); + } +} + +QuicError::QuicError(ngtcp2_connection_close_error_code ccec) : + family(QUIC_ERROR_SESSION), + code(ccec.error_code) { + switch (ccec.type) { + case NGTCP2_CONNECTION_CLOSE_ERROR_CODE_TYPE_APPLICATION: + family = QUIC_ERROR_APPLICATION; + break; + case NGTCP2_CONNECTION_CLOSE_ERROR_CODE_TYPE_TRANSPORT: + if (code & NGTCP2_CRYPTO_ERROR) + family = QUIC_ERROR_CRYPTO; + break; + default: + UNREACHABLE(); + } +} + +QuicError::QuicError( + Environment* env, + v8::Local codeArg, + v8::Local familyArg, + int32_t family_) : + family(family_), + code(NGTCP2_NO_ERROR) { + if (codeArg->IsBigInt()) { + code = codeArg.As()->Int64Value(); + } else if (codeArg->IsNumber()) { + double num = 0; + CHECK(codeArg->NumberValue(env->context()).To(&num)); + code = static_cast(num); + } + if (familyArg->IsNumber()) { + CHECK(familyArg->Int32Value(env->context()).To(&family)); + } +} + +const char* QuicError::family_name() { + switch (family) { + case QUIC_ERROR_SESSION: + return "Session"; + case QUIC_ERROR_APPLICATION: + return "Application"; + case QUIC_ERROR_CRYPTO: + return "Crypto"; + default: + UNREACHABLE(); + } +} + +const ngtcp2_cid* PreferredAddress::cid() const { + return &paddr_->cid; +} + +const uint8_t* PreferredAddress::stateless_reset_token() const { + return paddr_->stateless_reset_token; +} + +std::string PreferredAddress::ipv6_address() const { + char host[NI_MAXHOST]; + // Return an empty string if unable to convert... + if (uv_inet_ntop(AF_INET6, paddr_->ipv6_addr, host, sizeof(host)) != 0) + return std::string(); + + return std::string(host); +} +std::string PreferredAddress::ipv4_address() const { + char host[NI_MAXHOST]; + // Return an empty string if unable to convert... + if (uv_inet_ntop(AF_INET, paddr_->ipv4_addr, host, sizeof(host)) != 0) + return std::string(); + + return std::string(host); +} + +uint16_t PreferredAddress::ipv6_port() const { + return paddr_->ipv6_port; +} + +uint16_t PreferredAddress::ipv4_port() const { + return paddr_->ipv4_port; +} + +bool PreferredAddress::Use(int family) const { + uv_getaddrinfo_t req; + + if (!ResolvePreferredAddress(family, &req)) + return false; + + dest_->addrlen = req.addrinfo->ai_addrlen; + memcpy(dest_->addr, req.addrinfo->ai_addr, req.addrinfo->ai_addrlen); + uv_freeaddrinfo(req.addrinfo); + return true; +} + +bool PreferredAddress::ResolvePreferredAddress( + int local_address_family, + uv_getaddrinfo_t* req) const { + int af; + const uint8_t* binaddr; + uint16_t port; + switch (local_address_family) { + case AF_INET: + if (paddr_->ipv4_port > 0) { + af = AF_INET; + binaddr = paddr_->ipv4_addr; + port = paddr_->ipv4_port; + break; + } + return false; + case AF_INET6: + if (paddr_->ipv6_port > 0) { + af = AF_INET6; + binaddr = paddr_->ipv6_addr; + port = paddr_->ipv6_port; + break; + } + return false; + default: + UNREACHABLE(); + } + + char host[NI_MAXHOST]; + if (uv_inet_ntop(af, binaddr, host, sizeof(host)) != 0) + return false; + + addrinfo hints{}; + hints.ai_flags = AI_NUMERICHOST | AI_NUMERICSERV; + hints.ai_family = af; + hints.ai_socktype = SOCK_DGRAM; + + // Unfortunately ngtcp2 requires the selection of the + // preferred address to be synchronous, which means we + // have to do a sync resolve using uv_getaddrinfo here. + return + uv_getaddrinfo( + env_->event_loop(), + req, + nullptr, + host, + std::to_string(port).c_str(), + &hints) == 0 && + req->addrinfo != nullptr; +} + +StatelessResetToken::StatelessResetToken( + uint8_t* token, + const uint8_t* secret, + const QuicCID& cid) : token_(token) { + GenerateResetToken(token, secret, cid); +} + +StatelessResetToken::StatelessResetToken( + const uint8_t* secret, + const QuicCID& cid) + : token_(buf_) { + GenerateResetToken(buf_, secret, cid); +} + +std::string StatelessResetToken::ToString() const { + std::vector dest(NGTCP2_STATELESS_RESET_TOKENLEN * 2 + 1); + dest[dest.size() - 1] = '\0'; + size_t written = StringBytes::hex_encode( + reinterpret_cast(token_), + NGTCP2_STATELESS_RESET_TOKENLEN, + dest.data(), + dest.size()); + return std::string(dest.data(), written); +} + +size_t StatelessResetToken::Hash::operator()( + const StatelessResetToken& token) const { + size_t hash = 0; + for (size_t n = 0; n < NGTCP2_STATELESS_RESET_TOKENLEN; n++) + hash ^= std::hash{}(token.token_[n]) + 0x9e3779b9 + + (hash << 6) + (hash >> 2); + return hash; +} + +bool StatelessResetToken::operator==(const StatelessResetToken& other) const { + return memcmp(data(), other.data(), NGTCP2_STATELESS_RESET_TOKENLEN) == 0; +} + +bool StatelessResetToken::operator!=(const StatelessResetToken& other) const { + return !(*this == other); +} + +template +StatsBase::StatsBase( + Environment* env, + v8::Local wrap, + int options) { + static constexpr uint64_t kMax = std::numeric_limits::max(); + + // Create the backing store for the statistics + size_t size = sizeof(Stats); + size_t count = size / sizeof(uint64_t); + stats_store_ = v8::ArrayBuffer::NewBackingStore(env->isolate(), size); + stats_ = new (stats_store_->Data()) Stats; + + DCHECK_NOT_NULL(stats_); + stats_->created_at = uv_hrtime(); + + // The stats buffer is exposed as a BigUint64Array on + // the JavaScript side to allow statistics to be monitored. + v8::Local stats_buffer = + v8::ArrayBuffer::New(env->isolate(), stats_store_); + v8::Local stats_array = + v8::BigUint64Array::New(stats_buffer, 0, count); + USE(wrap->DefineOwnProperty( + env->context(), + env->stats_string(), + stats_array, + v8::PropertyAttribute::ReadOnly)); + + if (options & HistogramOptions::ACK) { + ack_ = HistogramBase::New(env, 1, kMax); + wrap->DefineOwnProperty( + env->context(), + env->ack_string(), + ack_->object(), + v8::PropertyAttribute::ReadOnly).Check(); + } + + if (options & HistogramOptions::RATE) { + rate_ = HistogramBase::New(env, 1, kMax); + wrap->DefineOwnProperty( + env->context(), + env->rate_string(), + rate_->object(), + v8::PropertyAttribute::ReadOnly).Check(); + } + + if (options & HistogramOptions::SIZE) { + size_ = HistogramBase::New(env, 1, kMax); + wrap->DefineOwnProperty( + env->context(), + env->size_string(), + size_->object(), + v8::PropertyAttribute::ReadOnly).Check(); + } +} + +template +void StatsBase::IncrementStat(uint64_t Stats::*member, uint64_t amount) { + static constexpr uint64_t kMax = std::numeric_limits::max(); + stats_->*member += std::min(amount, kMax - stats_->*member); +} + +template +void StatsBase::SetStat(uint64_t Stats::*member, uint64_t value) { + stats_->*member = value; +} + +template +void StatsBase::RecordTimestamp(uint64_t Stats::*member) { + stats_->*member = uv_hrtime(); +} + +template +uint64_t StatsBase::GetStat(uint64_t Stats::*member) const { + return stats_->*member; +} + +template +inline void StatsBase::RecordRate(uint64_t Stats::*member) { + CHECK(rate_); + uint64_t received_at = GetStat(member); + uint64_t now = uv_hrtime(); + if (received_at > 0) + rate_->Record(now - received_at); + SetStat(member, now); +} + +template +inline void StatsBase::RecordSize(uint64_t val) { + CHECK(size_); + size_->Record(val); +} + +template +inline void StatsBase::RecordAck(uint64_t Stats::*member) { + CHECK(ack_); + uint64_t acked_at = GetStat(member); + uint64_t now = uv_hrtime(); + if (acked_at > 0) + ack_->Record(now - acked_at); + SetStat(member, now); +} + +template +void StatsBase::StatsMemoryInfo(MemoryTracker* tracker) const { + tracker->TrackField("stats_store", stats_store_); + tracker->TrackField("rate_histogram", rate_); + tracker->TrackField("size_histogram", size_); + tracker->TrackField("ack_histogram", ack_); +} + +template +void StatsBase::DebugStats() { + StatsDebug stats_debug(static_cast(this)); + Debug(static_cast(this), "Destroyed. %s", stats_debug); +} + +template +std::string StatsBase::StatsDebug::ToString() const { + std::string out = "Statistics:\n"; + auto add_field = [&out](const char* name, uint64_t val) { + out += " "; + out += std::string(name); + out += ": "; + out += std::to_string(val); + out += "\n"; + }; + add_field("Duration", uv_hrtime() - ptr->GetStat(&Stats::created_at)); + T::ToString(*ptr, add_field); + return out; +} + +template +size_t get_length(const T* vec, size_t count) { + CHECK_NOT_NULL(vec); + size_t len = 0; + for (size_t n = 0; n < count; n++) + len += vec[n].len; + return len; +} + +} // namespace quic +} // namespace node + +#endif // SRC_QUIC_NODE_QUIC_UTIL_INL_H_ diff --git a/src/quic/node_quic_util.h b/src/quic/node_quic_util.h new file mode 100644 index 00000000000000..6fdfb99cc688a3 --- /dev/null +++ b/src/quic/node_quic_util.h @@ -0,0 +1,428 @@ +#ifndef SRC_QUIC_NODE_QUIC_UTIL_H_ +#define SRC_QUIC_NODE_QUIC_UTIL_H_ + +#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS + +#include "node.h" +#include "node_sockaddr.h" +#include "uv.h" +#include "v8.h" +#include "histogram.h" +#include "memory_tracker.h" + +#include +#include + +#include +#include +#include +#include +#include + +namespace node { +namespace quic { + +// k-constants are used internally, all-caps constants +// are exposed to javascript as constants (see node_quic.cc) + +constexpr size_t kMaxSizeT = std::numeric_limits::max(); +constexpr size_t kMaxValidateAddressLru = 10; +constexpr size_t kMinInitialQuicPktSize = 1200; +constexpr size_t kScidLen = NGTCP2_MAX_CIDLEN; +constexpr size_t kTokenRandLen = 16; +constexpr size_t kTokenSecretLen = 16; + +constexpr uint64_t DEFAULT_ACTIVE_CONNECTION_ID_LIMIT = 2; +constexpr uint64_t DEFAULT_MAX_CONNECTIONS = + std::min(kMaxSizeT, kMaxSafeJsInteger); +constexpr uint64_t DEFAULT_MAX_CONNECTIONS_PER_HOST = 100; +constexpr uint64_t DEFAULT_MAX_STREAM_DATA_BIDI_LOCAL = 256 * 1024; +constexpr uint64_t DEFAULT_MAX_STREAM_DATA_BIDI_REMOTE = 256 * 1024; +constexpr uint64_t DEFAULT_MAX_STREAM_DATA_UNI = 256 * 1024; +constexpr uint64_t DEFAULT_MAX_DATA = 1 * 1024 * 1024; +constexpr uint64_t DEFAULT_MAX_STATELESS_RESETS_PER_HOST = 10; +constexpr uint64_t DEFAULT_MAX_STREAMS_BIDI = 100; +constexpr uint64_t DEFAULT_MAX_STREAMS_UNI = 3; +constexpr uint64_t DEFAULT_MAX_IDLE_TIMEOUT = 10; +constexpr uint64_t DEFAULT_RETRYTOKEN_EXPIRATION = 10; +constexpr uint64_t MIN_RETRYTOKEN_EXPIRATION = 1; +constexpr uint64_t MAX_RETRYTOKEN_EXPIRATION = 60; +constexpr uint64_t NGTCP2_APP_NOERROR = 0xff00; + +constexpr int ERR_FAILED_TO_CREATE_SESSION = -1; + +// The preferred address policy determines how a client QuicSession +// handles a server-advertised preferred address. As suggested, the +// preferred address is the address the server would prefer the +// client to use for subsequent communication for a QuicSession. +// The client may choose to ignore the preference but really shouldn't +// without good reason. We currently only support two options but +// additional options may be added later. +enum SelectPreferredAddressPolicy : int { + // Ignore the server-provided preferred address + QUIC_PREFERRED_ADDRESS_IGNORE, + + // Use the server-provided preferred address. + // With this policy in effect, when a client + // receives a preferred address from the server, + // the client QuicSession will be automatically + // switched to use the selected address if it + // matches the current local address family. + QUIC_PREFERRED_ADDRESS_USE +}; + +// QUIC error codes generally fall into two distinct namespaces: +// Connection Errors and Application Errors. Connection errors +// are further subdivided into Crypto and non-Crypto. Application +// errors are entirely specific to the QUIC application being +// used. An easy rule of thumb is that Application errors are +// semantically associated with the ALPN identifier negotiated +// for the QuicSession. So, if a connection is closed with +// family: QUIC_ERROR_APPLICATION and code: 123, you have to +// look at the ALPN identifier to determine exactly what it +// means. Connection (Session) and Crypto errors, on the other +// hand, share the same meaning regardless of the ALPN. +enum QuicErrorFamily : int32_t { + QUIC_ERROR_SESSION, + QUIC_ERROR_CRYPTO, + QUIC_ERROR_APPLICATION +}; + + +template class StatsBase; + +template +struct StatsTraits { + using Stats = T; + using Base = Q; + + template + static void ToString(const Q& ptr, Fn&& add_field); +}; + +// StatsBase is a utility help for classes (like QuicSession) +// that record performance statistics. The template takes a +// single Traits argument (see QuicStreamStatsTraits in +// node_quic_stream.h as an example). When the StatsBase +// is deconstructed, collected statistics are output to +// Debug automatically. +template +class StatsBase { + public: + typedef typename T::Stats Stats; + + // A StatsBase instance may have one of three histogram + // instances. One that records rate of data flow, one + // that records size of data chunk, and one that records + // rate of data ackwowledgement. These may be used in + // slightly different ways of different StatsBase + // instances or may be turned off entirely. + enum HistogramOptions { + NONE = 0, + RATE = 1, + SIZE = 2, + ACK = 4 + }; + + inline StatsBase( + Environment* env, + v8::Local wrap, + int options = HistogramOptions::NONE); + + inline ~StatsBase() { if (stats_ != nullptr) stats_->~Stats(); } + + // The StatsDebug utility is used when StatsBase is destroyed + // to output statistical information to Debug. It is designed + // to only incur a performance cost constructing the debug + // output when Debug output is enabled. + struct StatsDebug { + typename T::Base* ptr; + explicit StatsDebug(typename T::Base* ptr_) : ptr(ptr_) {} + std::string ToString() const; + }; + + // Increments the given stat field by the given amount or 1 if + // no amount is specified. + inline void IncrementStat(uint64_t Stats::*member, uint64_t amount = 1); + + // Sets an entirely new value for the given stat field + inline void SetStat(uint64_t Stats::*member, uint64_t value); + + // Sets the given stat field to the current uv_hrtime() + inline void RecordTimestamp(uint64_t Stats::*member); + + // Gets the current value of the given stat field + inline uint64_t GetStat(uint64_t Stats::*member) const; + + // If the rate histogram is used, records the time elapsed + // between now and the timestamp specified by the member + // field. + inline void RecordRate(uint64_t Stats::*member); + + // If the size histogram is used, records the given size. + inline void RecordSize(uint64_t val); + + // If the ack rate histogram is used, records the time + // elapsed between now and the timestamp specified by + // the member field. + inline void RecordAck(uint64_t Stats::*member); + + inline void StatsMemoryInfo(MemoryTracker* tracker) const; + + inline void DebugStats(); + + private: + BaseObjectPtr rate_; + BaseObjectPtr size_; + BaseObjectPtr ack_; + std::shared_ptr stats_store_; + Stats* stats_ = nullptr; +}; + +// PreferredAddress is a helper class used only when a client QuicSession +// receives an advertised preferred address from a server. The helper provides +// information about the preferred address. The Use() function is used to let +// ngtcp2 know to use the preferred address for the given family. +class PreferredAddress { + public: + PreferredAddress( + Environment* env, + ngtcp2_addr* dest, + const ngtcp2_preferred_addr* paddr) : + env_(env), + dest_(dest), + paddr_(paddr) {} + + // When a preferred address is advertised by a server, the + // advertisement also includes a new CID and (optionally) + // a stateless reset token. If the preferred address is + // selected, then the client QuicSession will make use of + // these new values. Access to the cid and reset token + // are provided via the PreferredAddress class only as a + // convenience. + inline const ngtcp2_cid* cid() const; + + // The stateless reset token associated with the preferred + // address CID + inline const uint8_t* stateless_reset_token() const; + + // A preferred address advertisement may include both an + // IPv4 and IPv6 address. Only one of which will be used. + + inline std::string ipv4_address() const; + + inline uint16_t ipv4_port() const; + + inline std::string ipv6_address() const; + + inline uint16_t ipv6_port() const; + + // Instructs the QuicSession to use the advertised + // preferred address matching the given family. If + // the advertisement does not include a matching + // address, the preferred address is ignored. + inline bool Use(int family = AF_INET) const; + + private: + inline bool ResolvePreferredAddress( + int local_address_family, + uv_getaddrinfo_t* req) const; + + Environment* env_; + mutable ngtcp2_addr* dest_; + const ngtcp2_preferred_addr* paddr_; +}; + +// QuicError is a helper class used to encapsulate basic +// details about a QUIC protocol error. There are three +// basic types of errors (see QuicErrorFamily) +struct QuicError { + int32_t family; + uint64_t code; + inline QuicError( + int32_t family_ = QUIC_ERROR_SESSION, + int code_ = NGTCP2_NO_ERROR); + inline QuicError( + int32_t family_ = QUIC_ERROR_SESSION, + uint64_t code_ = NGTCP2_NO_ERROR); + explicit inline QuicError(ngtcp2_connection_close_error_code code); + inline QuicError( + Environment* env, + v8::Local codeArg, + v8::Local familyArg = v8::Local(), + int32_t family_ = QUIC_ERROR_SESSION); + inline const char* family_name(); +}; + +// Helper function that returns the maximum QUIC packet size for +// the given socket address. +inline size_t GetMaxPktLen(const SocketAddress& addr); + +// QuicPath is a utility class that wraps ngtcp2_path to adapt +// it to work with SocketAddress +struct QuicPath : public ngtcp2_path { + inline QuicPath(const SocketAddress& local, const SocketAddress& remote); +}; + +struct QuicPathStorage : public ngtcp2_path_storage { + QuicPathStorage() { + ngtcp2_path_storage_zero(this); + } +}; + +// Simple wrapper for ngtcp2_cid that handles hex encoding +// CIDs are used to identify QuicSession instances and may +// be between 0 and 20 bytes in length. +class QuicCID : public MemoryRetainer { + public: + // Empty constructor + QuicCID() : ptr_(&cid_) {} + + // Copy constructor + QuicCID(const QuicCID& cid) : QuicCID(cid->data, cid->datalen) {} + + // Copy constructor + explicit QuicCID(const ngtcp2_cid& cid) : QuicCID(cid.data, cid.datalen) {} + + // Wrap constructor + explicit QuicCID(const ngtcp2_cid* cid) : ptr_(cid) {} + + QuicCID(const uint8_t* cid, size_t len) : QuicCID() { + ngtcp2_cid* ptr = this->cid(); + ngtcp2_cid_init(ptr, cid, len); + ptr_ = ptr; + } + + struct Hash { + inline size_t operator()(const QuicCID& cid) const; + }; + + inline bool operator==(const QuicCID& other) const; + inline bool operator!=(const QuicCID& other) const; + inline QuicCID& operator=(const QuicCID& cid); + const ngtcp2_cid& operator*() const { return *ptr_; } + const ngtcp2_cid* operator->() const { return ptr_; } + + inline std::string ToString() const; + + const ngtcp2_cid* cid() const { return ptr_; } + + const uint8_t* data() const { return ptr_->data; } + + operator bool() const { return ptr_->datalen > 0; } + + size_t length() const { return ptr_->datalen; } + + ngtcp2_cid* cid() { + CHECK_EQ(ptr_, &cid_); + return &cid_; + } + + unsigned char* data() { + return reinterpret_cast(cid()->data); + } + + void set_length(size_t length) { + cid()->datalen = length; + } + + SET_NO_MEMORY_INFO() + SET_MEMORY_INFO_NAME(QuicCID) + SET_SELF_SIZE(QuicCID) + + template + using Map = std::unordered_map; + + private: + ngtcp2_cid cid_{}; + const ngtcp2_cid* ptr_; +}; + +// Simple timer wrapper that is used to implement the internals +// for idle and retransmission timeouts. Call Update to start or +// reset the timer; Stop to halt the timer. +class Timer final : public MemoryRetainer { + public: + inline explicit Timer(Environment* env, std::function fn); + + // Stops the timer with the side effect of the timer no longer being usable. + // It will be cleaned up and the Timer object will be destroyed. + inline void Stop(); + + // If the timer is not currently active, interval must be either 0 or greater. + // If the timer is already active, interval is ignored. + inline void Update(uint64_t interval); + + static inline void Free(Timer* timer); + + SET_NO_MEMORY_INFO() + SET_MEMORY_INFO_NAME(Timer) + SET_SELF_SIZE(Timer) + + private: + static inline void OnTimeout(uv_timer_t* timer); + + bool stopped_ = false; + Environment* env_; + std::function fn_; + uv_timer_t timer_; +}; + +using TimerPointer = DeleteFnPtr; + +// A Stateless Reset Token is a mechanism by which a QUIC +// endpoint can discreetly signal to a peer that it has +// lost all state associated with a connection. This +// helper class is used to both store received tokens and +// provide storage when creating new tokens to send. +class StatelessResetToken : public MemoryRetainer { + public: + inline StatelessResetToken( + uint8_t* token, + const uint8_t* secret, + const QuicCID& cid); + + inline StatelessResetToken( + const uint8_t* secret, + const QuicCID& cid); + + explicit StatelessResetToken( + const uint8_t* token) + : token_(token) {} + + inline std::string ToString() const; + + const uint8_t* data() const { return token_; } + + struct Hash { + inline size_t operator()(const StatelessResetToken& token) const; + }; + + inline bool operator==(const StatelessResetToken& other) const; + inline bool operator!=(const StatelessResetToken& other) const; + + SET_NO_MEMORY_INFO() + SET_MEMORY_INFO_NAME(StatelessResetToken) + SET_SELF_SIZE(StatelessResetToken) + + template + using Map = + std::unordered_map< + StatelessResetToken, + BaseObjectPtr, + StatelessResetToken::Hash>; + + private: + uint8_t buf_[NGTCP2_STATELESS_RESET_TOKENLEN]{}; + const uint8_t* token_; +}; + +template +inline size_t get_length(const T*, size_t len); + +} // namespace quic +} // namespace node + +#endif // NOE_WANT_INTERNALS + +#endif // SRC_QUIC_NODE_QUIC_UTIL_H_ diff --git a/src/udp_wrap.cc b/src/udp_wrap.cc index eb02db0a2a45a7..24df0acba46041 100644 --- a/src/udp_wrap.cc +++ b/src/udp_wrap.cc @@ -201,6 +201,7 @@ void UDPWrap::Initialize(Local target, Local constants = Object::New(env->isolate()); NODE_DEFINE_CONSTANT(constants, UV_UDP_IPV6ONLY); + NODE_DEFINE_CONSTANT(constants, UV_UDP_REUSEADDR); target->Set(context, env->constants_string(), constants).Check(); diff --git a/src/util.h b/src/util.h index 8bdeb35184e99d..8cec9a8aab956b 100644 --- a/src/util.h +++ b/src/util.h @@ -331,6 +331,11 @@ constexpr size_t arraysize(const T (&)[N]) { return N; } +template +constexpr size_t strsize(const T (&)[N]) { + return N - 1; +} + // Allocates an array of member type T. For up to kStackStorageSize items, // the stack is used, otherwise malloc(). template @@ -564,6 +569,11 @@ struct MallocedBuffer { size = new_size; } + void Realloc(size_t new_size) { + Truncate(new_size); + data = UncheckedRealloc(data, new_size); + } + inline bool is_empty() const { return data == nullptr; } MallocedBuffer() : data(nullptr), size(0) {} diff --git a/test/cctest/test_quic_buffer.cc b/test/cctest/test_quic_buffer.cc new file mode 100644 index 00000000000000..0aa18216334f8c --- /dev/null +++ b/test/cctest/test_quic_buffer.cc @@ -0,0 +1,206 @@ +#include "quic/node_quic_buffer-inl.h" +#include "node_bob-inl.h" +#include "util-inl.h" +#include "uv.h" + +#include "gtest/gtest.h" +#include +#include + +using node::quic::QuicBuffer; +using node::quic::QuicBufferChunk; +using node::bob::Status; +using node::bob::Options; +using node::bob::Done; +using ::testing::AssertionSuccess; +using ::testing::AssertionFailure; + +::testing::AssertionResult IsEqual(size_t actual, int expected) { + return (static_cast(expected) == actual) ? AssertionSuccess() : + AssertionFailure() << actual << " is not equal to " << expected; +} + +TEST(QuicBuffer, Simple) { + char data[100]; + memset(&data, 0, node::arraysize(data)); + uv_buf_t buf = uv_buf_init(data, node::arraysize(data)); + + bool done = false; + + QuicBuffer buffer; + buffer.Push(&buf, 1, [&](int status) { + EXPECT_EQ(0, status); + done = true; + }); + + buffer.Consume(100); + ASSERT_TRUE(IsEqual(buffer.length(), 0)); + + // We have to move the read head forward in order to consume + buffer.Seek(1); + buffer.Consume(100); + ASSERT_TRUE(done); + ASSERT_TRUE(IsEqual(buffer.length(), 0)); +} + +TEST(QuicBuffer, ConsumeMore) { + char data[100]; + memset(&data, 0, node::arraysize(data)); + uv_buf_t buf = uv_buf_init(data, node::arraysize(data)); + + bool done = false; + + QuicBuffer buffer; + buffer.Push(&buf, 1, [&](int status) { + EXPECT_EQ(0, status); + done = true; + }); + + buffer.Seek(1); + buffer.Consume(150); // Consume more than what was buffered + ASSERT_TRUE(done); + ASSERT_TRUE(IsEqual(buffer.length(), 0)); +} + +TEST(QuicBuffer, Multiple) { + uv_buf_t bufs[] { + uv_buf_init(const_cast("abcdefghijklmnopqrstuvwxyz"), 26), + uv_buf_init(const_cast("zyxwvutsrqponmlkjihgfedcba"), 26) + }; + + QuicBuffer buf; + bool done = false; + buf.Push(bufs, 2, [&](int status) { done = true; }); + + buf.Seek(2); + ASSERT_TRUE(IsEqual(buf.remaining(), 50)); + ASSERT_TRUE(IsEqual(buf.length(), 52)); + + buf.Consume(25); + ASSERT_TRUE(IsEqual(buf.length(), 27)); + + buf.Consume(25); + ASSERT_TRUE(IsEqual(buf.length(), 2)); + + buf.Consume(2); + ASSERT_TRUE(IsEqual(buf.length(), 0)); +} + +TEST(QuicBuffer, Multiple2) { + char* ptr = new char[100]; + memset(ptr, 0, 50); + memset(ptr + 50, 1, 50); + + uv_buf_t bufs[] = { + uv_buf_init(ptr, 50), + uv_buf_init(ptr + 50, 50) + }; + + int count = 0; + + QuicBuffer buffer; + buffer.Push( + bufs, node::arraysize(bufs), + [&](int status) { + count++; + ASSERT_EQ(0, status); + delete[] ptr; + }); + buffer.Seek(node::arraysize(bufs)); + + buffer.Consume(25); + ASSERT_TRUE(IsEqual(buffer.length(), 75)); + buffer.Consume(25); + ASSERT_TRUE(IsEqual(buffer.length(), 50)); + buffer.Consume(25); + ASSERT_TRUE(IsEqual(buffer.length(), 25)); + buffer.Consume(25); + ASSERT_TRUE(IsEqual(buffer.length(), 0)); + + // The callback was only called once tho + ASSERT_EQ(1, count); +} + +TEST(QuicBuffer, Cancel) { + char* ptr = new char[100]; + memset(ptr, 0, 50); + memset(ptr + 50, 1, 50); + + uv_buf_t bufs[] = { + uv_buf_init(ptr, 50), + uv_buf_init(ptr + 50, 50) + }; + + int count = 0; + + QuicBuffer buffer; + buffer.Push( + bufs, node::arraysize(bufs), + [&](int status) { + count++; + ASSERT_EQ(UV_ECANCELED, status); + delete[] ptr; + }); + + buffer.Seek(1); + buffer.Consume(25); + ASSERT_TRUE(IsEqual(buffer.length(), 75)); + buffer.Cancel(); + ASSERT_TRUE(IsEqual(buffer.length(), 0)); + + // The callback was only called once tho + ASSERT_EQ(1, count); +} + +TEST(QuicBuffer, Move) { + QuicBuffer buffer1; + QuicBuffer buffer2; + + char data[100]; + memset(&data, 0, node::arraysize(data)); + uv_buf_t buf = uv_buf_init(data, node::arraysize(data)); + + buffer1.Push(&buf, 1); + + ASSERT_TRUE(IsEqual(buffer1.length(), 100)); + + buffer2 = std::move(buffer1); + ASSERT_TRUE(IsEqual(buffer1.length(), 0)); + ASSERT_TRUE(IsEqual(buffer2.length(), 100)); +} + +TEST(QuicBuffer, QuicBufferChunk) { + std::unique_ptr chunk = + std::make_unique(100); + memset(chunk->out(), 1, 100); + + QuicBuffer buffer; + buffer.Push(std::move(chunk)); + buffer.End(); + ASSERT_TRUE(IsEqual(buffer.length(), 100)); + + auto next = [&]( + int status, + const ngtcp2_vec* data, + size_t count, + Done done) { + ASSERT_EQ(status, Status::STATUS_END); + ASSERT_TRUE(IsEqual(count, 1)); + ASSERT_NE(data, nullptr); + done(100); + }; + + ASSERT_TRUE(IsEqual(buffer.remaining(), 100)); + + ngtcp2_vec data[2]; + size_t len = sizeof(data) / sizeof(ngtcp2_vec); + buffer.Pull(next, Options::OPTIONS_SYNC | Options::OPTIONS_END, data, len); + + ASSERT_TRUE(IsEqual(buffer.remaining(), 0)); + + buffer.Consume(50); + ASSERT_TRUE(IsEqual(buffer.length(), 50)); + + buffer.Consume(50); + ASSERT_TRUE(IsEqual(buffer.length(), 0)); +} diff --git a/test/cctest/test_quic_cid.cc b/test/cctest/test_quic_cid.cc new file mode 100644 index 00000000000000..eb1a9c53319580 --- /dev/null +++ b/test/cctest/test_quic_cid.cc @@ -0,0 +1,31 @@ +#include "quic/node_quic_util-inl.h" +#include "node_sockaddr-inl.h" +#include "util-inl.h" +#include "ngtcp2/ngtcp2.h" +#include "gtest/gtest.h" +#include +#include + +using node::quic::QuicCID; + +TEST(QuicCID, Simple) { + ngtcp2_cid cid1; + ngtcp2_cid cid2; + uint8_t data1[3] = { 'a', 'b', 'c' }; + uint8_t data2[4] = { 1, 2, 3, 4 }; + ngtcp2_cid_init(&cid1, data1, 3); + ngtcp2_cid_init(&cid2, data2, 4); + + QuicCID qcid1(cid1); + CHECK(qcid1); + CHECK_EQ(qcid1.length(), 3); + CHECK_EQ(qcid1.ToString(), "616263"); + + QuicCID qcid2(cid2); + qcid1 = qcid2; + CHECK_EQ(qcid1.ToString(), qcid2.ToString()); + + qcid1.set_length(5); + memset(qcid1.data(), 1, 5); + CHECK_EQ(qcid1.ToString(), "0101010101"); +} diff --git a/test/cctest/test_quic_verifyhostnameidentity.cc b/test/cctest/test_quic_verifyhostnameidentity.cc new file mode 100644 index 00000000000000..f611239ac72e4a --- /dev/null +++ b/test/cctest/test_quic_verifyhostnameidentity.cc @@ -0,0 +1,349 @@ + +#include "base_object-inl.h" +#include "quic/node_quic_crypto.h" +#include "quic/node_quic_util-inl.h" +#include "node_sockaddr-inl.h" +#include "util.h" +#include "gtest/gtest.h" + +#include + +#include +#include +#include + +using node::quic::VerifyHostnameIdentity; + +enum altname_type { + TYPE_DNS, + TYPE_IP, + TYPE_URI +}; + +struct altname { + altname_type type; + const char* name; +}; + +void ToAltNamesMap( + const altname* names, + size_t names_len, + std::unordered_multimap* map) { + for (size_t n = 0; n < names_len; n++) { + switch (names[n].type) { + case TYPE_DNS: + map->emplace("dns", names[n].name); + continue; + case TYPE_IP: + map->emplace("ip", names[n].name); + continue; + case TYPE_URI: + map->emplace("uri", names[n].name); + continue; + } + } +} + +TEST(QuicCrypto, BasicCN_1) { + std::unordered_multimap altnames; + CHECK_EQ(VerifyHostnameIdentity("a.com", std::string("a.com"), altnames), 0); +} + +TEST(QuicCrypto, BasicCN_2) { + std::unordered_multimap altnames; + CHECK_EQ(VerifyHostnameIdentity("a.com", std::string("A.com"), altnames), 0); +} + +TEST(QuicCrypto, BasicCN_3_Fail) { + std::unordered_multimap altnames; + CHECK_EQ( + VerifyHostnameIdentity("a.com", std::string("b.com"), altnames), + X509_V_ERR_HOSTNAME_MISMATCH); +} + +TEST(QuicCrypto, BasicCN_4) { + std::unordered_multimap altnames; + CHECK_EQ(VerifyHostnameIdentity("a.com", std::string("a.com."), altnames), 0); +} + +TEST(QuicCrypto, BasicCN_5_Fail) { + std::unordered_multimap altnames; + CHECK_EQ( + VerifyHostnameIdentity("a.com", std::string(".a.com"), altnames), + X509_V_ERR_HOSTNAME_MISMATCH); +} + +TEST(QuicCrypto, BasicCN_6_Fail) { + std::unordered_multimap altnames; + CHECK_EQ( + VerifyHostnameIdentity("8.8.8.8", std::string("8.8.8.8"), altnames), + X509_V_ERR_HOSTNAME_MISMATCH); +} + +TEST(QuicCrypto, BasicCN_7_Fail) { + std::unordered_multimap altnames; + altnames.emplace("dns", "8.8.8.8"); + CHECK_EQ( + VerifyHostnameIdentity("8.8.8.8", std::string("8.8.8.8"), altnames), + X509_V_ERR_HOSTNAME_MISMATCH); +} + +TEST(QuicCrypto, BasicCN_8_Fail) { + std::unordered_multimap altnames; + altnames.emplace("uri", "8.8.8.8"); + CHECK_EQ( + VerifyHostnameIdentity("8.8.8.8", std::string("8.8.8.8"), altnames), + X509_V_ERR_HOSTNAME_MISMATCH); +} + +TEST(QuicCrypto, BasicCN_9) { + std::unordered_multimap altnames; + altnames.emplace("ip", "8.8.8.8"); + CHECK_EQ( + VerifyHostnameIdentity("8.8.8.8", std::string("8.8.8.8"), altnames), 0); +} + +TEST(QuicCrypto, BasicCN_10_Fail) { + std::unordered_multimap altnames; + altnames.emplace("ip", "8.8.8.8/24"); + CHECK_EQ( + VerifyHostnameIdentity("8.8.8.8", std::string("8.8.8.8"), altnames), + X509_V_ERR_HOSTNAME_MISMATCH); +} + +TEST(QuicCrypto, BasicCN_11) { + std::unordered_multimap altnames; + CHECK_EQ( + VerifyHostnameIdentity("b.a.com", std::string("*.a.com"), altnames), 0); +} + +TEST(QuicCrypto, BasicCN_12_Fail) { + std::unordered_multimap altnames; + CHECK_EQ( + VerifyHostnameIdentity("ba.com", std::string("*.a.com"), altnames), + X509_V_ERR_HOSTNAME_MISMATCH); +} + +TEST(QuicCrypto, BasicCN_13_Fail) { + std::unordered_multimap altnames; + CHECK_EQ( + VerifyHostnameIdentity("\n.a.com", std::string("*.a.com"), altnames), + X509_V_ERR_HOSTNAME_MISMATCH); +} + +TEST(QuicCrypto, BasicCN_14_Fail) { + std::unordered_multimap altnames; + altnames.emplace("dns", "omg.com"); + CHECK_EQ( + VerifyHostnameIdentity("b.a.com", std::string("*.a.com"), altnames), + X509_V_ERR_HOSTNAME_MISMATCH); +} + +TEST(QuicCrypto, BasicCN_15_Fail) { + std::unordered_multimap altnames; + CHECK_EQ( + VerifyHostnameIdentity("b.a.com", std::string("b*b.a.com"), altnames), + X509_V_ERR_HOSTNAME_MISMATCH); +} + +TEST(QuicCrypto, BasicCN_16_Fail) { + std::unordered_multimap altnames; + CHECK_EQ( + VerifyHostnameIdentity("b.a.com", std::string(), altnames), + X509_V_ERR_HOSTNAME_MISMATCH); +} + +TEST(QuicCrypto, BasicCN_17) { + // TODO(@jasnell): This should test multiple CN's. The code is only + // implemented to support one. Need to fix + std::unordered_multimap altnames; + CHECK_EQ( + VerifyHostnameIdentity("foo.com", std::string("foo.com"), altnames), 0); +} + +TEST(QuicCrypto, BasicCN_18_Fail) { + std::unordered_multimap altnames; + altnames.emplace("dns", "*"); + CHECK_EQ( + VerifyHostnameIdentity("a.com", std::string("b.com"), altnames), + X509_V_ERR_HOSTNAME_MISMATCH); +} + +TEST(QuicCrypto, BasicCN_19_Fail) { + std::unordered_multimap altnames; + altnames.emplace("dns", "*.com"); + CHECK_EQ( + VerifyHostnameIdentity("a.com", std::string("b.com"), altnames), + X509_V_ERR_HOSTNAME_MISMATCH); +} + +TEST(QuicCrypto, BasicCN_20) { + std::unordered_multimap altnames; + altnames.emplace("dns", "*.co.uk"); + CHECK_EQ( + VerifyHostnameIdentity("a.co.uk", std::string("b.com"), altnames), 0); +} + +TEST(QuicCrypto, BasicCN_21_Fail) { + std::unordered_multimap altnames; + altnames.emplace("dns", "*.a.com"); + CHECK_EQ( + VerifyHostnameIdentity("a.com", std::string("a.com"), altnames), + X509_V_ERR_HOSTNAME_MISMATCH); +} + +TEST(QuicCrypto, BasicCN_22_Fail) { + std::unordered_multimap altnames; + altnames.emplace("dns", "*.a.com"); + CHECK_EQ( + VerifyHostnameIdentity("a.com", std::string("b.com"), altnames), + X509_V_ERR_HOSTNAME_MISMATCH); +} + +TEST(QuicCrypto, BasicCN_23) { + std::unordered_multimap altnames; + altnames.emplace("dns", "a.com"); + CHECK_EQ( + VerifyHostnameIdentity("a.com", std::string("b.com"), altnames), 0); +} + +TEST(QuicCrypto, BasicCN_24) { + std::unordered_multimap altnames; + altnames.emplace("dns", "A.COM"); + CHECK_EQ( + VerifyHostnameIdentity("a.com", std::string("b.com"), altnames), 0); +} + +TEST(QuicCrypto, BasicCN_25_Fail) { + std::unordered_multimap altnames; + altnames.emplace("dns", "*.a.com"); + CHECK_EQ( + VerifyHostnameIdentity("a.com", std::string(), altnames), + X509_V_ERR_HOSTNAME_MISMATCH); +} + +TEST(QuicCrypto, BasicCN_26) { + std::unordered_multimap altnames; + altnames.emplace("dns", "*.a.com"); + CHECK_EQ( + VerifyHostnameIdentity("b.a.com", std::string(), altnames), 0); +} + +TEST(QuicCrypto, BasicCN_27_Fail) { + std::unordered_multimap altnames; + altnames.emplace("dns", "*.a.com"); + CHECK_EQ( + VerifyHostnameIdentity("c.b.a.com", std::string(), altnames), + X509_V_ERR_HOSTNAME_MISMATCH); +} + +TEST(QuicCrypto, BasicCN_28) { + std::unordered_multimap altnames; + altnames.emplace("dns", "*b.a.com"); + CHECK_EQ( + VerifyHostnameIdentity("b.a.com", std::string(), altnames), 0); +} + +TEST(QuicCrypto, BasicCN_29) { + std::unordered_multimap altnames; + altnames.emplace("dns", "*b.a.com"); + CHECK_EQ( + VerifyHostnameIdentity("a-cb.a.com", std::string(), altnames), 0); +} + +TEST(QuicCrypto, BasicCN_30_Fail) { + std::unordered_multimap altnames; + altnames.emplace("dns", "*b.a.com"); + CHECK_EQ( + VerifyHostnameIdentity("a.b.a.com", std::string(), altnames), + X509_V_ERR_HOSTNAME_MISMATCH); +} + + +TEST(QuicCrypto, BasicCN_31) { + std::unordered_multimap altnames; + altnames.emplace("dns", "*b.a.com"); + altnames.emplace("dns", "a.b.a.com"); + CHECK_EQ( + VerifyHostnameIdentity("a.b.a.com", std::string(), altnames), 0); +} + + +TEST(QuicCrypto, BasicCN_32) { + std::unordered_multimap altnames; + altnames.emplace("uri", "a.b.a.com"); + CHECK_EQ( + VerifyHostnameIdentity("a.b.a.com", std::string(), altnames), 0); +} + +TEST(QuicCrypto, BasicCN_33_Fail) { + std::unordered_multimap altnames; + altnames.emplace("uri", "*.b.a.com"); + CHECK_EQ( + VerifyHostnameIdentity("a.b.a.com", std::string(), altnames), + X509_V_ERR_HOSTNAME_MISMATCH); +} + +// // Invalid URI +// { +// host: 'a.b.a.com', cert: { +// subjectaltname: 'URI:http://[a.b.a.com]/', +// subject: {} +// } +// }, + +TEST(QuicCrypto, BasicCN_35_Fail) { + std::unordered_multimap altnames; + altnames.emplace("ip", "127.0.0.1"); + CHECK_EQ( + VerifyHostnameIdentity("a.b.a.com", std::string(), altnames), + X509_V_ERR_HOSTNAME_MISMATCH); +} + +TEST(QuicCrypto, BasicCN_36) { + std::unordered_multimap altnames; + altnames.emplace("ip", "127.0.0.1"); + CHECK_EQ( + VerifyHostnameIdentity("127.0.0.1", std::string(), altnames), 0); +} + +TEST(QuicCrypto, BasicCN_37_Fail) { + std::unordered_multimap altnames; + altnames.emplace("ip", "127.0.0.1"); + CHECK_EQ( + VerifyHostnameIdentity("127.0.0.2", std::string(), altnames), + X509_V_ERR_HOSTNAME_MISMATCH); +} + +TEST(QuicCrypto, BasicCN_38_Fail) { + std::unordered_multimap altnames; + altnames.emplace("dns", "a.com"); + CHECK_EQ( + VerifyHostnameIdentity("127.0.0.1", std::string(), altnames), + X509_V_ERR_HOSTNAME_MISMATCH); +} + +TEST(QuicCrypto, BasicCN_39_Fail) { + std::unordered_multimap altnames; + altnames.emplace("dns", "a.com"); + CHECK_EQ( + VerifyHostnameIdentity("localhost", std::string("localhost"), altnames), + X509_V_ERR_HOSTNAME_MISMATCH); +} + +TEST(QuicCrypto, BasicCN_40) { + std::unordered_multimap altnames; + CHECK_EQ( + VerifyHostnameIdentity( + "xn--bcher-kva.example.com", + std::string("*.example.com"), altnames), 0); +} + +TEST(QuicCrypto, BasicCN_41_Fail) { + std::unordered_multimap altnames; + CHECK_EQ( + VerifyHostnameIdentity( + "xn--bcher-kva.example.com", + std::string("xn--*.example.com"), altnames), + X509_V_ERR_HOSTNAME_MISMATCH); +} diff --git a/test/common/README.md b/test/common/README.md index b27083e5759c3b..64f19269a01d03 100644 --- a/test/common/README.md +++ b/test/common/README.md @@ -21,6 +21,7 @@ This directory contains modules used to test the Node.js implementation. * [Report module](#report-module) * [tick module](#tick-module) * [tmpdir module](#tmpdir-module) +* [UDP pair helper](#udp-pair-helper) * [WPT module](#wpt-module) ## Benchmark Module diff --git a/test/common/fixtures.js b/test/common/fixtures.js index 2390ee8284e421..e33ab1d3e1b3e6 100644 --- a/test/common/fixtures.js +++ b/test/common/fixtures.js @@ -20,9 +20,14 @@ function readFixtureKey(name, enc) { return fs.readFileSync(fixturesPath('keys', name), enc); } +function readFixtureKeys(enc, ...names) { + return names.map((name) => readFixtureKey(name, enc)); +} + module.exports = { fixturesDir, path: fixturesPath, readSync: readFixtureSync, - readKey: readFixtureKey + readKey: readFixtureKey, + readKeys: readFixtureKeys, }; diff --git a/test/common/index.js b/test/common/index.js index d3645629a7efff..9de81b426e6c7e 100644 --- a/test/common/index.js +++ b/test/common/index.js @@ -50,6 +50,7 @@ const noop = () => {}; const hasCrypto = Boolean(process.versions.openssl) && !process.env.NODE_SKIP_CRYPTO; +const hasQuic = hasCrypto && Boolean(process.versions.ngtcp2); // Check for flags. Skip this for workers (both, the `cluster` module and // `worker_threads`) and child processes. @@ -675,6 +676,7 @@ const common = { getTTYfd, hasIntl, hasCrypto, + hasQuic, hasMultiLocalhost, invalidArgTypeHelper, isAIX, diff --git a/test/common/quic.js b/test/common/quic.js new file mode 100644 index 00000000000000..6fe121886ad0a2 --- /dev/null +++ b/test/common/quic.js @@ -0,0 +1,38 @@ +/* eslint-disable node-core/require-common-first, node-core/required-modules */ +'use strict'; + +// Common bits for all QUIC-related tests +const { debuglog } = require('util'); +const { readKeys } = require('./fixtures'); +const { createWriteStream } = require('fs'); +const kHttp3Alpn = 'h3-27'; + +const [ key, cert, ca ] = + readKeys( + 'binary', + 'agent1-key.pem', + 'agent1-cert.pem', + 'ca1-cert.pem'); + +const debug = debuglog('test'); + +const kServerPort = process.env.NODE_DEBUG_KEYLOG ? 5678 : 0; +const kClientPort = process.env.NODE_DEBUG_KEYLOG ? 5679 : 0; + +function setupKeylog(session) { + if (process.env.NODE_DEBUG_KEYLOG) { + const kl = createWriteStream(process.env.NODE_DEBUG_KEYLOG); + session.on('keylog', kl.write.bind(kl)); + } +} + +module.exports = { + key, + cert, + ca, + debug, + kServerPort, + kClientPort, + setupKeylog, + kHttp3Alpn, +}; diff --git a/test/parallel/test-process-versions.js b/test/parallel/test-process-versions.js index 14484293dc4621..78c33fcf1b6be5 100644 --- a/test/parallel/test-process-versions.js +++ b/test/parallel/test-process-versions.js @@ -16,6 +16,11 @@ if (common.hasIntl) { expected_keys.push('unicode'); } +if (common.hasQuic) { + expected_keys.push('ngtcp2'); + expected_keys.push('nghttp3'); +} + expected_keys.sort(); const actual_keys = Object.keys(process.versions).sort(); diff --git a/test/parallel/test-quic-binding.js b/test/parallel/test-quic-binding.js new file mode 100644 index 00000000000000..3d5a5b581fbc24 --- /dev/null +++ b/test/parallel/test-quic-binding.js @@ -0,0 +1,39 @@ +// Flags: --expose-internals --no-warnings +'use strict'; + +// Tests the availability and correctness of internalBinding(quic) + +const common = require('../common'); +if (!common.hasQuic) + common.skip('missing quic'); + +const assert = require('assert'); +const { internalBinding } = require('internal/test/binding'); + +const quic = internalBinding('quic'); +assert(quic); + +assert(quic.constants); + +// Version numbers used to identify IETF drafts are created by +// adding the draft number to 0xff0000, in this case 19 (25). +assert.strictEqual(quic.constants.NGTCP2_PROTO_VER.toString(16), 'ff00001b'); +assert.strictEqual(quic.constants.NGTCP2_ALPN_H3, '\u0005h3-27'); + +// The following just tests for the presence of things we absolutely need. +// They don't test the functionality of those things. + +assert(quic.sessionConfig instanceof Float64Array); +assert(quic.http3Config instanceof Float64Array); + +assert(quic.QuicSocket); +assert(quic.QuicEndpoint); +assert(quic.QuicStream); + +assert.strictEqual(typeof quic.createClientSession, 'function'); +assert.strictEqual(typeof quic.openBidirectionalStream, 'function'); +assert.strictEqual(typeof quic.openUnidirectionalStream, 'function'); +assert.strictEqual(typeof quic.setCallbacks, 'function'); +assert.strictEqual(typeof quic.initSecureContext, 'function'); +assert.strictEqual(typeof quic.initSecureContextClient, 'function'); +assert.strictEqual(typeof quic.silentCloseSession, 'function'); diff --git a/test/parallel/test-quic-client-connect-multiple-parallel.js b/test/parallel/test-quic-client-connect-multiple-parallel.js new file mode 100644 index 00000000000000..131886f0c06520 --- /dev/null +++ b/test/parallel/test-quic-client-connect-multiple-parallel.js @@ -0,0 +1,57 @@ +// Flags: --no-warnings +'use strict'; +const common = require('../common'); +if (!common.hasQuic) + common.skip('missing quic'); + +// Test that .connect() can be called multiple times with different servers. + +const assert = require('assert'); +const { createQuicSocket } = require('net'); + +const { key, cert, ca } = require('../common/quic'); +const { once } = require('events'); + +(async function() { + const servers = []; + for (let i = 0; i < 3; i++) { + const server = createQuicSocket(); + + server.listen({ key, cert, ca, alpn: 'meow' }); + + server.on('session', common.mustCall((session) => { + session.on('secure', common.mustCall(() => { + const stream = session.openStream({ halfOpen: true }); + stream.end('Hi!'); + })); + })); + + server.on('close', common.mustCall()); + + servers.push(server); + } + + await Promise.all(servers.map((server) => once(server, 'ready'))); + + const client = createQuicSocket({ client: { key, cert, ca, alpn: 'meow' } }); + + let done = 0; + for (const server of servers) { + const req = client.connect({ + address: 'localhost', + port: server.endpoints[0].address.port + }); + + req.on('stream', common.mustCall((stream) => { + stream.on('data', common.mustCall( + (chk) => assert.strictEqual(chk.toString(), 'Hi!'))); + stream.on('end', common.mustCall(() => { + server.close(); + req.close(); + if (++done === servers.length) client.close(); + })); + })); + + req.on('close', common.mustCall()); + } +})().then(common.mustCall()); diff --git a/test/parallel/test-quic-client-connect-multiple-sequential.js b/test/parallel/test-quic-client-connect-multiple-sequential.js new file mode 100644 index 00000000000000..7cced86ba9ec03 --- /dev/null +++ b/test/parallel/test-quic-client-connect-multiple-sequential.js @@ -0,0 +1,56 @@ +// Flags: --no-warnings +'use strict'; +const common = require('../common'); +if (!common.hasQuic) + common.skip('missing quic'); + +// Test that .connect() can be called multiple times with different servers. + +const { createQuicSocket } = require('net'); + +const { key, cert, ca } = require('../common/quic'); + +const { once } = require('events'); + +(async function() { + const servers = []; + for (let i = 0; i < 3; i++) { + const server = createQuicSocket(); + + server.listen({ key, cert, ca, alpn: 'meow' }); + + server.on('session', common.mustCall((session) => { + session.on('secure', common.mustCall(() => { + const stream = session.openStream({ halfOpen: true }); + stream.end('Hi!'); + })); + })); + + server.on('close', common.mustCall()); + + servers.push(server); + } + + await Promise.all(servers.map((server) => once(server, 'ready'))); + + const client = createQuicSocket({ client: { key, cert, ca, alpn: 'meow' } }); + + for (const server of servers) { + const req = client.connect({ + address: 'localhost', + port: server.endpoints[0].address.port + }); + + const [ stream ] = await once(req, 'stream'); + stream.resume(); + await once(stream, 'end'); + + server.close(); + req.close(); + await once(req, 'close'); + } + + client.close(); + + await once(client, 'close'); +})().then(common.mustCall()); diff --git a/test/parallel/test-quic-client-empty-preferred-address.js b/test/parallel/test-quic-client-empty-preferred-address.js new file mode 100644 index 00000000000000..fa5a0b65503af4 --- /dev/null +++ b/test/parallel/test-quic-client-empty-preferred-address.js @@ -0,0 +1,57 @@ +// Flags: --no-warnings +'use strict'; + +// This test ensures that when we don't define `preferredAddress` +// on the server while the `preferredAddressPolicy` on the client +// is `accpet`, it works as expected. + +const common = require('../common'); +if (!common.hasQuic) + common.skip('missing quic'); + +const assert = require('assert'); +const { key, cert, ca } = require('../common/quic'); +const { createQuicSocket } = require('net'); +const { once } = require('events'); + +(async () => { + const server = createQuicSocket(); + + let client; + const options = { key, cert, ca, alpn: 'zzz' }; + server.listen(options); + + server.on('session', common.mustCall((serverSession) => { + serverSession.on('stream', common.mustCall(async (stream) => { + stream.on('data', common.mustCall((data) => { + assert.strictEqual(data.toString('utf8'), 'hello'); + })); + + await once(stream, 'end'); + + stream.close(); + client.close(); + server.close(); + })); + })); + + await once(server, 'ready'); + + client = createQuicSocket({ client: options }); + + const clientSession = client.connect({ + address: common.localhostIPv4, + port: server.endpoints[0].address.port, + preferredAddressPolicy: 'accept', + }); + + await once(clientSession, 'secure'); + + const stream = clientSession.openStream(); + stream.end('hello'); + + await Promise.all([ + once(stream, 'close'), + once(client, 'close'), + once(server, 'close')]); +})().then(common.mustCall()); diff --git a/test/parallel/test-quic-client-server.js b/test/parallel/test-quic-client-server.js new file mode 100644 index 00000000000000..90df11bbb77314 --- /dev/null +++ b/test/parallel/test-quic-client-server.js @@ -0,0 +1,372 @@ +// Flags: --expose-internals --no-warnings +'use strict'; + +// Tests a simple QUIC client/server round-trip + +const common = require('../common'); +if (!common.hasQuic) + common.skip('missing quic'); + +const { internalBinding } = require('internal/test/binding'); +const { + constants: { + NGTCP2_NO_ERROR, + QUIC_ERROR_APPLICATION, + } +} = internalBinding('quic'); + +const { Buffer } = require('buffer'); +const Countdown = require('../common/countdown'); +const assert = require('assert'); +const fs = require('fs'); +const { + key, + cert, + ca, + debug, +} = require('../common/quic'); + +const filedata = fs.readFileSync(__filename, { encoding: 'utf8' }); + +const { createQuicSocket } = require('net'); + +const kStatelessResetToken = + Buffer.from('000102030405060708090A0B0C0D0E0F', 'hex'); + +let client; + +const server = createQuicSocket({ + validateAddress: true, + statelessResetSecret: kStatelessResetToken +}); + +const unidata = ['I wonder if it worked.', 'test']; +const kServerName = 'agent2'; // Intentionally the wrong servername +const kALPN = 'zzz'; // ALPN can be overriden to whatever we want + +const countdown = new Countdown(2, () => { + debug('Countdown expired. Destroying sockets'); + server.close(); + client.close(); +}); + +server.listen({ + key, + cert, + ca, + requestCert: true, + rejectUnauthorized: false, + alpn: kALPN, +}); +server.on('session', common.mustCall((session) => { + debug('QuicServerSession Created'); + + assert.strictEqual(session.maxStreams.bidi, 100); + assert.strictEqual(session.maxStreams.uni, 3); + + { + const { + address, + family, + port + } = session.remoteAddress; + const endpoint = client.endpoints[0].address; + assert.strictEqual(port, endpoint.port); + assert.strictEqual(family, endpoint.family); + debug(`QuicServerSession Client ${family} address ${address}:${port}`); + } + + session.on('usePreferredAddress', common.mustNotCall()); + + session.on('clientHello', common.mustCall( + (alpn, servername, ciphers, cb) => { + assert.strictEqual(alpn, kALPN); + assert.strictEqual(servername, kServerName); + assert.strictEqual(ciphers.length, 4); + cb(); + })); + + session.on('OCSPRequest', common.mustCall( + (servername, context, cb) => { + debug('QuicServerSession received a OCSP request'); + assert.strictEqual(servername, kServerName); + + // This will be a SecureContext. By default it will + // be the SecureContext used to create the QuicSession. + // If the user wishes to do something with it, it can, + // but if it wishes to pass in a new SecureContext, + // it can pass it in as the second argument to the + // callback below. + assert(context); + debug('QuicServerSession Certificate: ', context.getCertificate()); + debug('QuicServerSession Issuer: ', context.getIssuer()); + + // The callback can be invoked asynchronously + setImmediate(() => { + // The first argument is a potential error, + // in which case the session will be destroyed + // immediately. + // The second is an optional new SecureContext + // The third is the ocsp response. + // All arguments are optional + cb(null, null, Buffer.from('hello')); + }); + })); + + session.on('secure', common.mustCall((servername, alpn, cipher) => { + debug('QuicServerSession TLS Handshake Complete'); + debug(' Server name: %s', servername); + debug(' ALPN: %s', alpn); + debug(' Cipher: %s, %s', cipher.name, cipher.version); + assert.strictEqual(session.servername, servername); + assert.strictEqual(servername, kServerName); + assert.strictEqual(session.alpnProtocol, alpn); + + assert.strictEqual(session.getPeerCertificate().subject.CN, 'agent1'); + + assert(session.authenticated); + assert.strictEqual(session.authenticationError, undefined); + + const uni = session.openStream({ halfOpen: true }); + assert(uni.unidirectional); + assert(!uni.bidirectional); + assert(uni.serverInitiated); + assert(!uni.clientInitiated); + assert(!uni.pending); + uni.write(unidata[0], common.mustCall()); + uni.end(unidata[1], common.mustCall()); + uni.on('finish', common.mustCall()); + uni.on('end', common.mustCall()); + uni.on('data', common.mustNotCall()); + uni.on('close', common.mustCall(() => { + assert.strictEqual(uni.finalSize, 0n); + })); + debug('Unidirectional, Server-initiated stream %d opened', uni.id); + })); + + session.on('stream', common.mustCall((stream) => { + debug('Bidirectional, Client-initiated stream %d received', stream.id); + assert.strictEqual(stream.id, 0); + assert.strictEqual(stream.session, session); + assert(stream.bidirectional); + assert(!stream.unidirectional); + assert(stream.clientInitiated); + assert(!stream.serverInitiated); + assert(!stream.pending); + + const file = fs.createReadStream(__filename); + let data = ''; + file.pipe(stream); + stream.setEncoding('utf8'); + stream.on('blocked', common.mustNotCall()); + stream.on('data', (chunk) => { + data += chunk; + + debug('Server: min data rate: %f', stream.dataRateHistogram.min); + debug('Server: max data rate: %f', stream.dataRateHistogram.max); + debug('Server: data rate 50%: %f', + stream.dataRateHistogram.percentile(50)); + debug('Server: data rate 99%: %f', + stream.dataRateHistogram.percentile(99)); + + debug('Server: min data size: %f', stream.dataSizeHistogram.min); + debug('Server: max data size: %f', stream.dataSizeHistogram.max); + debug('Server: data size 50%: %f', + stream.dataSizeHistogram.percentile(50)); + debug('Server: data size 99%: %f', + stream.dataSizeHistogram.percentile(99)); + }); + stream.on('end', common.mustCall(() => { + assert.strictEqual(data, filedata); + debug('Server received expected data for stream %d', stream.id); + })); + stream.on('finish', common.mustCall()); + stream.on('close', common.mustCall(() => { + assert.strictEqual(typeof stream.duration, 'bigint'); + assert.strictEqual(typeof stream.bytesReceived, 'bigint'); + assert.strictEqual(typeof stream.bytesSent, 'bigint'); + assert.strictEqual(typeof stream.maxExtendedOffset, 'bigint'); + assert.strictEqual(stream.finalSize, BigInt(filedata.length)); + })); + })); + + session.on('close', common.mustCall(() => { + const { + code, + family + } = session.closeCode; + debug(`Server session closed with code ${code} (family: ${family})`); + assert.strictEqual(code, NGTCP2_NO_ERROR); + + const err = { + code: 'ERR_QUICSESSION_DESTROYED', + name: 'Error' + }; + assert.throws(() => session.ping(), { ...err }); + assert.throws(() => session.openStream(), { + ...err, + message: 'Cannot call openStream after a QuicSession has been destroyed' + }); + assert.throws(() => session.updateKey(), { + ...err, + message: 'Cannot call updateKey after a QuicSession has been destroyed' + }); + })); +})); + +server.on('ready', common.mustCall(() => { + const endpoints = server.endpoints; + for (const endpoint of endpoints) { + const address = endpoint.address; + debug('Server is listening on address %s:%d', + address.address, + address.port); + } + const endpoint = endpoints[0]; + + client = createQuicSocket({ client: { key, cert, ca, alpn: kALPN } + }); + + client.on('close', common.mustCall(() => { + debug('Client closing. Duration', client.duration); + debug(' Bound duration', + client.boundDuration); + debug(' Bytes Sent/Received: %d/%d', + client.bytesSent, + client.bytesReceived); + debug(' Packets Sent/Received: %d/%d', + client.packetsSent, + client.packetsReceived); + debug(' Sessions:', client.clientSessions); + })); + + const req = client.connect({ + address: 'localhost', + port: endpoint.address.port, + servername: kServerName, + requestOCSP: true, + }); + + assert.strictEqual(req.servername, kServerName); + + req.on('usePreferredAddress', common.mustNotCall()); + + req.on('OCSPResponse', common.mustCall((response) => { + debug(`QuicClientSession OCSP response: "${response.toString()}"`); + assert.strictEqual(response.toString(), 'hello'); + })); + + req.on('sessionTicket', common.mustCall((ticket, params) => { + debug('Session ticket received'); + assert(ticket instanceof Buffer); + assert(params instanceof Buffer); + debug(' Ticket: %s', ticket.toString('hex')); + debug(' Params: %s', params.toString('hex')); + }, 2)); + + req.on('secure', common.mustCall((servername, alpn, cipher) => { + debug('QuicClientSession TLS Handshake Complete'); + debug(' Server name: %s', servername); + debug(' ALPN: %s', alpn); + debug(' Cipher: %s, %s', cipher.name, cipher.version); + assert.strictEqual(servername, kServerName); + assert.strictEqual(req.servername, kServerName); + assert.strictEqual(alpn, kALPN); + assert.strictEqual(req.alpnProtocol, kALPN); + assert(req.ephemeralKeyInfo); + assert.strictEqual(req.getPeerCertificate().subject.CN, 'agent1'); + + debug('Client, min handshake ack: %f', + req.handshakeAckHistogram.min); + debug('Client, max handshake ack: %f', + req.handshakeAckHistogram.max); + debug('Client, min handshake rate: %f', + req.handshakeContinuationHistogram.min); + debug('Client, max handshake rate: %f', + req.handshakeContinuationHistogram.max); + + // The server's identity won't be valid because the requested + // SNI hostname does not match the certificate used. + debug('QuicClientSession server is %sauthenticated', + req.authenticated ? '' : 'not '); + assert(!req.authenticated); + assert.throws(() => { throw req.authenticationError; }, { + code: 'ERR_QUIC_VERIFY_HOSTNAME_MISMATCH', + message: 'Hostname mismatch' + }); + + { + const { + address, + family, + port + } = req.remoteAddress; + const endpoint = server.endpoints[0].address; + assert.strictEqual(port, endpoint.port); + assert.strictEqual(family, endpoint.family); + debug(`QuicClientSession Server ${family} address ${address}:${port}`); + } + + const file = fs.createReadStream(__filename); + const stream = req.openStream(); + file.pipe(stream); + let data = ''; + stream.resume(); + stream.setEncoding('utf8'); + stream.on('blocked', common.mustNotCall()); + stream.on('data', (chunk) => data += chunk); + stream.on('finish', common.mustCall()); + stream.on('end', common.mustCall(() => { + assert.strictEqual(data, filedata); + debug('Client received expected data for stream %d', stream.id); + })); + stream.on('close', common.mustCall(() => { + debug('Bidirectional, Client-initiated stream %d closed', stream.id); + assert.strictEqual(stream.finalSize, BigInt(filedata.length)); + countdown.dec(); + })); + debug('Bidirectional, Client-initiated stream %d opened', stream.id); + })); + + req.on('stream', common.mustCall((stream) => { + debug('Unidirectional, Server-initiated stream %d received', stream.id); + let data = ''; + stream.setEncoding('utf8'); + stream.on('data', (chunk) => data += chunk); + stream.on('end', common.mustCall(() => { + assert.strictEqual(data, unidata.join('')); + debug('Client received expected data for stream %d', stream.id); + })); + stream.on('close', common.mustCall(() => { + debug('Unidirectional, Server-initiated stream %d closed', stream.id); + assert.strictEqual(stream.finalSize, 26n); + countdown.dec(); + })); + })); + + req.on('close', common.mustCall(() => { + const { + code, + family + } = req.closeCode; + debug(`Client session closed with code ${code} (family: ${family})`); + assert.strictEqual(code, NGTCP2_NO_ERROR); + assert.strictEqual(family, QUIC_ERROR_APPLICATION); + })); +})); + +server.on('listening', common.mustCall()); +server.on('close', () => { + debug('Server closing. Duration', server.duration); + debug(' Bound duration:', + server.boundDuration); + debug(' Listen duration:', + server.listenDuration); + debug(' Bytes Sent/Received: %d/%d', + server.bytesSent, + server.bytesReceived); + debug(' Packets Sent/Received: %d/%d', + server.packetsSent, + server.packetsReceived); + debug(' Sessions:', server.serverSessions); +}); diff --git a/test/parallel/test-quic-errors-quicsession-openstream.js b/test/parallel/test-quic-errors-quicsession-openstream.js new file mode 100644 index 00000000000000..ef06ee364c17da --- /dev/null +++ b/test/parallel/test-quic-errors-quicsession-openstream.js @@ -0,0 +1,84 @@ +// Flags: --no-warnings +'use strict'; +const common = require('../common'); +if (!common.hasQuic) + common.skip('missing quic'); + +// Test errors thrown when openStream is called incorrectly +// or is not permitted + +const { createHook } = require('async_hooks'); +const assert = require('assert'); +const { createQuicSocket } = require('net'); + +// Ensure that no QUICSTREAM instances are created during the test +createHook({ + init(id, type) { + assert.notStrictEqual(type, 'QUICSTREAM'); + } +}).enable(); + +const Countdown = require('../common/countdown'); +const { key, cert, ca } = require('../common/quic'); + +const options = { key, cert, ca, alpn: 'zzz', maxStreamsUni: 0 }; +const server = createQuicSocket({ server: options }); +const client = createQuicSocket({ client: options }); + +const countdown = new Countdown(1, () => { + server.close(); + client.close(); +}); + +server.listen(); +server.on('session', common.mustCall((session) => { + session.on('stream', common.mustNotCall()); +})); + +server.on('ready', common.mustCall(() => { + const req = client.connect({ + address: common.localhostIPv4, + port: server.endpoints[0].address.port + }); + + ['z', 1, {}, [], null, Infinity, 1n].forEach((i) => { + assert.throws( + () => req.openStream({ halfOpen: i }), + { code: 'ERR_INVALID_ARG_TYPE' } + ); + }); + + ['', 1n, {}, [], false, 'zebra'].forEach((defaultEncoding) => { + assert.throws(() => req.openStream({ defaultEncoding }), { + code: 'ERR_INVALID_ARG_VALUE' + }); + }); + + [-1, Number.MAX_SAFE_INTEGER + 1].forEach((highWaterMark) => { + assert.throws(() => req.openStream({ highWaterMark }), { + code: 'ERR_OUT_OF_RANGE' + }); + }); + + ['a', 1n, [], {}, false].forEach((highWaterMark) => { + assert.throws(() => req.openStream({ highWaterMark }), { + code: 'ERR_INVALID_ARG_TYPE' + }); + }); + + req.on('ready', common.mustCall()); + req.on('secure', common.mustCall()); + + // Unidirectional streams are not allowed. openStream will succeeed + // but the stream will be destroyed immediately. The underlying + // QuicStream C++ handle will not be created. + req.openStream({ + halfOpen: true, + highWaterMark: 10, + defaultEncoding: 'utf16le' + }).on('error', common.expectsError({ + code: 'ERR_QUICSTREAM_OPEN_FAILED' + })).on('error', common.mustCall(() => countdown.dec())); +})); + +server.on('close', common.mustCall()); diff --git a/test/parallel/test-quic-errors-quicsocket-connect.js b/test/parallel/test-quic-errors-quicsocket-connect.js new file mode 100644 index 00000000000000..49926851f8f899 --- /dev/null +++ b/test/parallel/test-quic-errors-quicsocket-connect.js @@ -0,0 +1,243 @@ +// Flags: --no-warnings +'use strict'; + +// Tests error and input validation checks for QuicSocket.connect() + +const common = require('../common'); + +if (!common.hasQuic) + common.skip('missing quic'); + +const { createHook } = require('async_hooks'); +const assert = require('assert'); +const { createQuicSocket } = require('net'); + +// Ensure that a QuicClientSession handle is never created during the +// error condition tests (ensures that argument and error validation) +// is occurring before the underlying handle is created. +createHook({ + init(id, type) { + assert.notStrictEqual(type, 'QUICCLIENTSESSION'); + } +}).enable(); + +const client = createQuicSocket(); + +// Test invalid minDHSize options argument +['test', 1n, {}, [], false].forEach((minDHSize) => { + assert.throws(() => client.connect({ minDHSize }), { + code: 'ERR_INVALID_ARG_TYPE' + }); +}); + +// Test invalid port argument option +[-1, 'test', 1n, {}, [], NaN, false, 65536].forEach((port) => { + assert.throws(() => client.connect({ port }), { + code: 'ERR_SOCKET_BAD_PORT' + }); +}); + +// Test invalid address argument option +[-1, 10, 1n, {}, [], true].forEach((address) => { + assert.throws(() => client.connect({ address }), { + code: 'ERR_INVALID_ARG_TYPE' + }); +}); + +// Test servername can't be IP address argument option +[ + '0.0.0.0', + '8.8.8.8', + '127.0.0.1', + '192.168.0.1', + '::', + '1::', + '::1', + '1::8', + '1::7:8', + '1:2:3:4:5:6:7:8', + '1:2:3:4:5:6::8', + '2001:0000:1234:0000:0000:C1C0:ABCD:0876', + '3ffe:0b00:0000:0000:0001:0000:0000:000a', + 'a:0:0:0:0:0:0:0', + 'fe80::7:8%eth0', + 'fe80::7:8%1' +].forEach((servername) => { + assert.throws(() => client.connect({ servername }), { + code: 'ERR_INVALID_ARG_VALUE' + }); +}); + +[-1, 10, 1n, {}, [], true].forEach((servername) => { + assert.throws(() => client.connect({ servername }), { + code: 'ERR_INVALID_ARG_TYPE' + }); +}); + +// Test invalid remoteTransportParams argument option +[-1, 'test', 1n, {}, []].forEach((remoteTransportParams) => { + assert.throws(() => client.connect({ remoteTransportParams }), { + code: 'ERR_INVALID_ARG_TYPE' + }); +}); + +// Test invalid sessionTicket argument option +[-1, 'test', 1n, {}, []].forEach((sessionTicket) => { + assert.throws(() => client.connect({ sessionTicket }), { + code: 'ERR_INVALID_ARG_TYPE' + }); +}); + +// Test invalid alpn argument option +[-1, 10, 1n, {}, [], true].forEach((alpn) => { + assert.throws(() => client.connect({ alpn }), { + code: 'ERR_INVALID_ARG_TYPE' + }); +}); + +[ + 'idleTimeout', + 'activeConnectionIdLimit', + 'maxAckDelay', + 'maxData', + 'maxPacketSize', + 'maxStreamDataBidiLocal', + 'maxStreamDataBidiRemote', + 'maxStreamDataUni', + 'maxStreamsBidi', + 'maxStreamsUni', + 'highWaterMark', +].forEach((prop) => { + assert.throws(() => client.connect({ [prop]: -1 }), { + code: 'ERR_OUT_OF_RANGE' + }); + + assert.throws( + () => client.connect({ [prop]: Number.MAX_SAFE_INTEGER + 1 }), { + code: 'ERR_OUT_OF_RANGE' + }); + + ['a', 1n, [], {}, false].forEach((val) => { + assert.throws(() => client.connect({ [prop]: val }), { + code: 'ERR_INVALID_ARG_TYPE' + }); + }); +}); + +// activeConnectionIdLimit must be between 2 and 8, inclusive +[1, 9].forEach((activeConnectionIdLimit) => { + assert.throws(() => client.connect({ activeConnectionIdLimit }), { + code: 'ERR_OUT_OF_RANGE' + }); +}); + +['a', 1n, 1, [], {}].forEach((ipv6Only) => { + assert.throws(() => client.connect({ ipv6Only }), { + code: 'ERR_INVALID_ARG_TYPE' + }); +}); + +[1, 1n, false, [], {}].forEach((preferredAddressPolicy) => { + assert.throws(() => client.connect({ preferredAddressPolicy }), { + code: 'ERR_INVALID_ARG_TYPE' + }); +}); + +[1, 1n, 'test', [], {}].forEach((qlog) => { + assert.throws(() => client.connect({ qlog }), { + code: 'ERR_INVALID_ARG_TYPE' + }); +}); + +[1, 1n, 'test', [], {}].forEach((requestOCSP) => { + assert.throws(() => client.connect({ requestOCSP }), { + code: 'ERR_INVALID_ARG_TYPE' + }); +}); + +[1, 1n, false, [], {}, 'aaa'].forEach((type) => { + assert.throws(() => client.connect({ type }), { + code: 'ERR_INVALID_ARG_VALUE' + }); +}); + + +[ + 'qpackMaxTableCapacity', + 'qpackBlockedStreams', + 'maxHeaderListSize', + 'maxPushes', +].forEach((prop) => { + assert.throws(() => client.connect({ h3: { [prop]: -1 } }), { + code: 'ERR_OUT_OF_RANGE' + }); + + assert.throws( + () => client.connect({ h3: { [prop]: Number.MAX_SAFE_INTEGER + 1 } }), { + code: 'ERR_OUT_OF_RANGE' + }); + + ['a', 1n, [], {}, false].forEach((val) => { + assert.throws(() => client.connect({ h3: { [prop]: val } }), { + code: 'ERR_INVALID_ARG_TYPE' + }); + }); +}); + +['', 1n, {}, [], false, 'zebra'].forEach((defaultEncoding) => { + assert.throws(() => client.connect({ defaultEncoding }), { + code: 'ERR_INVALID_ARG_VALUE' + }); +}); + + +// Test that connect cannot be called after QuicSocket is closed. +client.close(); +assert.throws(() => client.connect(), { + code: 'ERR_QUICSOCKET_DESTROYED' +}); + +// TODO(@jasnell): Test additional options: +// +// Client QuicSession Related: +// +// [x] idleTimeout - must be a number greater than zero +// [x] ipv6Only - must be a boolean +// [x] activeConnectionIdLimit - must be a number between 2 and 8 +// [x] maxAckDelay - must be a number greater than zero +// [x] maxData - must be a number greater than zero +// [x] maxPacketSize - must be a number greater than zero +// [x] maxStreamDataBidiLocal - must be a number greater than zero +// [x] maxStreamDataBidiRemote - must be a number greater than zero +// [x] maxStreamDataUni - must be a number greater than zero +// [x] maxStreamsBidi - must be a number greater than zero +// [x] maxStreamsUni - must be a number greater than zero +// [x] preferredAddressPolicy - must be eiher 'accept' or 'reject' +// [x] qlog - must be a boolean +// [x] requestOCSP - must be a boolean +// [x] type - must be a string, either 'udp4' or 'udp6' +// +// HTTP/3 Related: +// +// [x] h3.qpackMaxTableCapacity - must be a number greater than zero +// [x] h3.qpackBlockedStreams - must be a number greater than zero +// [x] h3.maxHeaderListSize - must be a number greater than zero +// [x] h3.maxPushes - must be a number greater than zero +// +// Secure Context Related: +// +// [ ] ca (certificate authority) - must be a string, string array, +// Buffer, or Buffer array. +// [ ] cert (cert chain) - must be a string, string array, Buffer, or +// Buffer array. +// [ ] ciphers - must be a string +// [ ] clientCertEngine - must be a string +// [ ] crl - must be a string, string array, Buffer, or Buffer array +// [ ] dhparam - must be a string or Buffer +// [ ] ecdhCurve - must be a string +// [ ] honorCipherOrder - must be a boolean +// [ ] key - must be a string, string array, Buffer, or Buffer array +// [ ] passphrase - must be a string +// [ ] pfx - must be a string, string array, Buffer, or Buffer array +// [ ] secureOptions - must be a number +// [x] minDHSize - must be a number diff --git a/test/parallel/test-quic-errors-quicsocket-create.js b/test/parallel/test-quic-errors-quicsocket-create.js new file mode 100644 index 00000000000000..57d72e5e6dbd75 --- /dev/null +++ b/test/parallel/test-quic-errors-quicsocket-create.js @@ -0,0 +1,187 @@ +// Flags: --no-warnings +'use strict'; + +// Test QuicSocket constructor option errors + +const common = require('../common'); +const async_hooks = require('async_hooks'); +if (!common.hasQuic) + common.skip('missing quic'); + +const assert = require('assert'); + +// Hook verifies that no QuicSocket handles are actually created. +async_hooks.createHook({ + init: common.mustCallAtLeast((_, type) => { + assert.notStrictEqual(type, 'QUICSOCKET'); + }) +}).enable(); + +const { createQuicSocket } = require('net'); + +// Test invalid QuicSocket options argument +[1, 'test', false, 1n, null].forEach((i) => { + assert.throws(() => createQuicSocket(i), { + code: 'ERR_INVALID_ARG_TYPE' + }); +}); + +// Test invalid QuicSocket port argument option +[-1, 'test', 1n, {}, [], NaN, false].forEach((port) => { + assert.throws(() => createQuicSocket({ endpoint: { port } }), { + code: 'ERR_SOCKET_BAD_PORT' + }); +}); + +// Test invalid QuicSocket addressargument option +[-1, 10, 1n, {}, [], NaN, false].forEach((address) => { + assert.throws(() => createQuicSocket({ endpoint: { address } }), { + code: 'ERR_INVALID_ARG_TYPE' + }); +}); + +// Test invalid QuicSocket type argument option +[1, false, 1n, {}, null, NaN].forEach((type) => { + assert.throws(() => createQuicSocket({ endpoint: { type } }), { + code: 'ERR_INVALID_ARG_TYPE' + }); +}); + +// Test invalid QuicSocket ipv6Only argument option +[1, NaN, 1n, null, {}, []].forEach((ipv6Only) => { + assert.throws(() => createQuicSocket({ endpoint: { ipv6Only } }), { + code: 'ERR_INVALID_ARG_TYPE' + }); +}); + +// Test invalid QuicSocket reuseAddr argument option +[1, NaN, 1n, null, {}, []].forEach((reuseAddr) => { + assert.throws(() => createQuicSocket({ endpoint: { reuseAddr } }), { + code: 'ERR_INVALID_ARG_TYPE' + }); +}); + +// Test invalid QuicSocket lookup argument option +[1, 1n, {}, [], 'test', true].forEach((lookup) => { + assert.throws(() => createQuicSocket({ lookup }), { + code: 'ERR_INVALID_ARG_TYPE' + }); +}); + +// Test invalid QuicSocket validateAddress argument option +[1, NaN, 1n, null, {}, []].forEach((validateAddress) => { + assert.throws(() => createQuicSocket({ validateAddress }), { + code: 'ERR_INVALID_ARG_TYPE' + }); +}); + +// Test invalid QuicSocket validateAddressLRU argument option +[1, NaN, 1n, null, {}, []].forEach((validateAddressLRU) => { + assert.throws(() => createQuicSocket({ validateAddressLRU }), { + code: 'ERR_INVALID_ARG_TYPE' + }); +}); + +// Test invalid QuicSocket autoClose argument option +[1, NaN, 1n, null, {}, []].forEach((autoClose) => { + assert.throws(() => createQuicSocket({ autoClose }), { + code: 'ERR_INVALID_ARG_TYPE' + }); +}); + +// Test invalid QuicSocket qlog argument option +[1, NaN, 1n, null, {}, []].forEach((qlog) => { + assert.throws(() => createQuicSocket({ qlog }), { + code: 'ERR_INVALID_ARG_TYPE' + }); +}); + + +// Test invalid QuicSocket retryTokenTimeout option +[0, 61, NaN].forEach((retryTokenTimeout) => { + assert.throws(() => createQuicSocket({ retryTokenTimeout }), { + code: 'ERR_OUT_OF_RANGE' + }); +}); + +// Test invalid QuicSocket retryTokenTimeout option +['test', null, 1n, {}, [], false].forEach((retryTokenTimeout) => { + assert.throws(() => createQuicSocket({ retryTokenTimeout }), { + code: 'ERR_INVALID_ARG_TYPE' + }); +}); + +// Test invalid QuicSocket maxConnectionsPerHost option +[0, Number.MAX_SAFE_INTEGER + 1, NaN].forEach((maxConnectionsPerHost) => { + assert.throws(() => createQuicSocket({ maxConnectionsPerHost }), { + code: 'ERR_OUT_OF_RANGE' + }); +}); + +// Test invalid QuicSocket maxConnectionsPerHost option +[ + 'test', + null, + 1n, + {}, + [], + false +].forEach((maxConnectionsPerHost) => { + assert.throws(() => createQuicSocket({ maxConnectionsPerHost }), { + code: 'ERR_INVALID_ARG_TYPE' + }); +}); + +// Test invalid QuicSocket maxConnections option +[0, Number.MAX_SAFE_INTEGER + 1, NaN].forEach((maxConnections) => { + assert.throws(() => createQuicSocket({ maxConnections }), { + code: 'ERR_OUT_OF_RANGE' + }); +}); + +// Test invalid QuicSocket maxConnectionsPerHost option +[ + 'test', + null, + 1n, + {}, + [], + false +].forEach((maxConnections) => { + assert.throws(() => createQuicSocket({ maxConnections }), { + code: 'ERR_INVALID_ARG_TYPE' + }); +}); + +// Test invalid QuicSocket maxStatelessResetsPerHost option +[0, Number.MAX_SAFE_INTEGER + 1, NaN].forEach((maxStatelessResetsPerHost) => { + assert.throws(() => createQuicSocket({ maxStatelessResetsPerHost }), { + code: 'ERR_OUT_OF_RANGE' + }); +}); + +// Test invalid QuicSocket maxStatelessResetsPerHost option +[ + 'test', + null, + 1n, + {}, + [], + false +].forEach((maxStatelessResetsPerHost) => { + assert.throws(() => createQuicSocket({ maxStatelessResetsPerHost }), { + code: 'ERR_INVALID_ARG_TYPE' + }); +}); + +[1, 1n, false, 'test'].forEach((options) => { + assert.throws(() => createQuicSocket({ endpoint: options }), { + code: 'ERR_INVALID_ARG_TYPE' + }); + assert.throws(() => createQuicSocket({ client: options }), { + code: 'ERR_INVALID_ARG_TYPE' + }); + assert.throws(() => createQuicSocket({ server: options }), { + code: 'ERR_INVALID_ARG_TYPE' + }); +}); diff --git a/test/parallel/test-quic-errors-quicsocket-listen.js b/test/parallel/test-quic-errors-quicsocket-listen.js new file mode 100644 index 00000000000000..432322441476c3 --- /dev/null +++ b/test/parallel/test-quic-errors-quicsocket-listen.js @@ -0,0 +1,179 @@ +// Flags: --no-warnings +'use strict'; + +// Tests error and input validation checks for QuicSocket.connect() + +const common = require('../common'); + +if (!common.hasQuic) + common.skip('missing quic'); + +const assert = require('assert'); +const { createQuicSocket } = require('net'); + +// Test invalid callback function +{ + const server = createQuicSocket(); + [1, 1n].forEach((cb) => { + assert.throws(() => server.listen({}, cb), { + code: 'ERR_INVALID_CALLBACK' + }); + }); +} + +// Test QuicSocket is already listening +{ + const server = createQuicSocket(); + server.listen(); + assert.throws(() => server.listen(), { + code: 'ERR_QUICSOCKET_LISTENING' + }); + server.close(); +} + +// Test QuicSocket listen after destroy error +{ + const server = createQuicSocket(); + server.close(); + assert.throws(() => server.listen(), { + code: 'ERR_QUICSOCKET_DESTROYED' + }); +} + +{ + // Test incorrect ALPN + const server = createQuicSocket(); + [1, 1n, true, {}, [], null].forEach((alpn) => { + assert.throws(() => server.listen({ alpn }), { + code: 'ERR_INVALID_ARG_TYPE' + }); + }); + + // Test invalid idle timeout + [ + 'idleTimeout', + 'activeConnectionIdLimit', + 'maxAckDelay', + 'maxData', + 'maxPacketSize', + 'maxStreamDataBidiLocal', + 'maxStreamDataBidiRemote', + 'maxStreamDataUni', + 'maxStreamsBidi', + 'maxStreamsUni', + 'highWaterMark', + ].forEach((prop) => { + assert.throws(() => server.listen({ [prop]: -1 }), { + code: 'ERR_OUT_OF_RANGE' + }); + + assert.throws( + () => server.listen({ [prop]: Number.MAX_SAFE_INTEGER + 1 }), { + code: 'ERR_OUT_OF_RANGE' + }); + + ['a', 1n, [], {}, false].forEach((val) => { + assert.throws(() => server.listen({ [prop]: val }), { + code: 'ERR_INVALID_ARG_TYPE' + }); + }); + }); + + [1, 1n, 'test', {}, []].forEach((rejectUnauthorized) => { + assert.throws(() => server.listen({ rejectUnauthorized }), { + code: 'ERR_INVALID_ARG_TYPE' + }); + }); + + [1, 1n, 'test', {}, []].forEach((requestCert) => { + assert.throws(() => server.listen({ requestCert }), { + code: 'ERR_INVALID_ARG_TYPE' + }); + }); + + [1, 1n, 'test', {}, []].forEach((requestCert) => { + assert.throws(() => server.listen({ requestCert }), { + code: 'ERR_INVALID_ARG_TYPE' + }); + }); + + [1, 1n, 'test', false].forEach((preferredAddress) => { + assert.throws(() => server.listen({ preferredAddress }), { + code: 'ERR_INVALID_ARG_TYPE' + }); + + [1, 1n, null, false, {}, []].forEach((address) => { + assert.throws(() => server.listen({ preferredAddress: { address } }), { + code: 'ERR_INVALID_ARG_TYPE' + }); + }); + + [-1].forEach((port) => { + assert.throws(() => server.listen({ preferredAddress: { port } }), { + code: 'ERR_INVALID_ARG_TYPE' + }); + }); + + [1, 'test', false, null, {}, []].forEach((type) => { + assert.throws(() => server.listen({ preferredAddress: { type } }), { + code: 'ERR_INVALID_ARG_TYPE' + }); + }); + }); + + [1, 1n, false, [], {}, null].forEach((ciphers) => { + assert.throws(() => server.listen({ ciphers }), { + code: 'ERR_INVALID_ARG_TYPE' + }); + }); + + [1, 1n, false, [], {}, null].forEach((groups) => { + assert.throws(() => server.listen({ groups }), { + code: 'ERR_INVALID_ARG_TYPE' + }); + }); + + ['', 1n, {}, [], false, 'zebra'].forEach((defaultEncoding) => { + assert.throws(() => server.listen({ defaultEncoding }), { + code: 'ERR_INVALID_ARG_VALUE' + }); + }); + + // Make sure that after all of the validation checks, the socket + // is not actually marked as listening at all. + assert.strictEqual(typeof server.listening, 'boolean'); + assert(!server.listening); +} + + +// Options to check +// * [x] alpn +// * [x] idleTimeout +// * [x] activeConnectionIdLimit +// * [x] maxAckDelay +// * [x] maxData +// * [x] maxPacketSize +// * [x] maxStreamsBidi +// * [x] maxStreamsUni +// * [x] maxStreamDataBidiLocal +// * [x] maxStreamDataBidiRemote +// * [x] maxStreamDataUni +// * [x] preferredAddress +// * [x] requestCert +// * [x] rejectUnauthorized + +// SecureContext Options +// * [ ] ca +// * [ ] cert +// * [x] ciphers +// * [ ] clientCertEngine +// * [ ] crl +// * [ ] dhparam +// * [ ] groups +// * [ ] ecdhCurve +// * [ ] honorCipherOrder +// * [ ] key +// * [ ] passphrase +// * [ ] pfx +// * [ ] secureOptions +// * [ ] sessionIdContext diff --git a/test/parallel/test-quic-http3-client-server.js b/test/parallel/test-quic-http3-client-server.js new file mode 100644 index 00000000000000..86a98937146e3e --- /dev/null +++ b/test/parallel/test-quic-http3-client-server.js @@ -0,0 +1,157 @@ +// Flags: --expose-internals --no-warnings +'use strict'; + +// Tests a simple QUIC HTTP/3 client/server round-trip + +const common = require('../common'); +if (!common.hasQuic) + common.skip('missing quic'); + +const Countdown = require('../common/countdown'); +const assert = require('assert'); +const fs = require('fs'); + +const { + key, + cert, + ca, + debug, + kHttp3Alpn, + kServerPort, + kClientPort, + setupKeylog, +} = require('../common/quic'); + +const filedata = fs.readFileSync(__filename, { encoding: 'utf8' }); + +const { createQuicSocket } = require('net'); + +let client; +const server = createQuicSocket({ endpoint: { port: kServerPort } }); + +const kServerName = 'agent2'; // Intentionally the wrong servername + +const countdown = new Countdown(1, () => { + debug('Countdown expired. Destroying sockets'); + server.close(); + client.close(); +}); + +server.listen({ + key, + cert, + ca, + alpn: kHttp3Alpn, +}); +server.on('session', common.mustCall((session) => { + debug('QuicServerSession Created'); + + assert.strictEqual(session.maxStreams.bidi, 100); + assert.strictEqual(session.maxStreams.uni, 3); + + setupKeylog(session); + + session.on('secure', common.mustCall((_, alpn) => { + debug('QuicServerSession handshake completed'); + assert.strictEqual(session.alpnProtocol, alpn); + })); + + session.on('stream', common.mustCall((stream) => { + debug('Bidirectional, Client-initiated stream %d received', stream.id); + const file = fs.createReadStream(__filename); + let data = ''; + + assert(stream.submitInitialHeaders({ ':status': '200' })); + + file.pipe(stream); + stream.setEncoding('utf8'); + + stream.on('initialHeaders', common.mustCall((headers) => { + const expected = [ + [ ':path', '/' ], + [ ':authority', 'localhost' ], + [ ':scheme', 'https' ], + [ ':method', 'POST' ] + ]; + assert.deepStrictEqual(expected, headers); + debug('Received expected request headers'); + })); + stream.on('informationalHeaders', common.mustNotCall()); + stream.on('trailingHeaders', common.mustNotCall()); + + stream.on('data', (chunk) => { + data += chunk; + }); + stream.on('end', common.mustCall(() => { + assert.strictEqual(data, filedata); + debug('Server received expected data for stream %d', stream.id); + })); + stream.on('close', common.mustCall()); + stream.on('finish', common.mustCall()); + })); + + session.on('close', common.mustCall()); +})); + +server.on('ready', common.mustCall(() => { + debug('Server is listening on port %d', server.endpoints[0].address.port); + client = createQuicSocket({ + endpoint: { port: kClientPort }, + client: { key, cert, ca, alpn: kHttp3Alpn } + }); + + client.on('close', common.mustCall()); + + const req = client.connect({ + address: 'localhost', + port: server.endpoints[0].address.port, + servername: kServerName, + h3: { maxPushes: 10 } + }); + debug('QuicClientSession Created'); + + req.on('secure', common.mustCall((servername, alpn, cipher) => { + debug('QuicClientSession handshake completed'); + + const file = fs.createReadStream(__filename); + const stream = req.openStream(); + + assert(stream.submitInitialHeaders({ + ':method': 'POST', + ':scheme': 'https', + ':authority': 'localhost', + ':path': '/', + })); + file.pipe(stream); + let data = ''; + stream.resume(); + stream.setEncoding('utf8'); + + stream.on('initialHeaders', common.mustCall((headers) => { + const expected = [ + [ ':status', '200' ] + ]; + assert.deepStrictEqual(expected, headers); + debug('Received expected response headers'); + })); + stream.on('informationalHeaders', common.mustNotCall()); + stream.on('trailingHeaders', common.mustNotCall()); + + stream.on('data', (chunk) => data += chunk); + stream.on('finish', common.mustCall()); + stream.on('end', common.mustCall(() => { + assert.strictEqual(data, filedata); + debug('Client received expected data for stream %d', stream.id); + })); + stream.on('close', common.mustCall(() => { + debug('Bidirectional, Client-initiated stream %d closed', stream.id); + countdown.dec(); + })); + debug('Bidirectional, Client-initiated stream %d opened', stream.id); + })); + + req.on('close', common.mustCall()); +})); + +server.on('listening', common.mustCall()); +server.on('close', common.mustCall()); diff --git a/test/parallel/test-quic-http3-push.js b/test/parallel/test-quic-http3-push.js new file mode 100644 index 00000000000000..f95bdb4f43c276 --- /dev/null +++ b/test/parallel/test-quic-http3-push.js @@ -0,0 +1,157 @@ +// Flags: --expose-internals --no-warnings +'use strict'; + +// Tests a simple QUIC HTTP/3 client/server round-trip + +const common = require('../common'); +if (!common.hasQuic) + common.skip('missing quic'); + +const Countdown = require('../common/countdown'); +const assert = require('assert'); +const { key, cert, ca, kHttp3Alpn } = require('../common/quic'); + +const { createQuicSocket } = require('net'); + +let client; +const server = createQuicSocket(); + +const countdown = new Countdown(2, () => { + server.close(); + client.close(); +}); + +const options = { key, cert, ca, alpn: kHttp3Alpn }; + +server.listen(options); + +server.on('session', common.mustCall((session) => { + + session.on('stream', common.mustCall((stream) => { + assert(stream.submitInitialHeaders({ ':status': '200' })); + + [-1, Number.MAX_SAFE_INTEGER + 1].forEach((highWaterMark) => { + assert.throws(() => stream.pushStream({}, { highWaterMark }), { + code: 'ERR_OUT_OF_RANGE' + }); + }); + ['', 1n, {}, [], false].forEach((highWaterMark) => { + assert.throws(() => stream.pushStream({}, { highWaterMark }), { + code: 'ERR_INVALID_ARG_TYPE' + }); + }); + ['', 1, 1n, true, [], {}, 'zebra'].forEach((defaultEncoding) => { + assert.throws(() => stream.pushStream({}, { defaultEncoding }), { + code: 'ERR_INVALID_ARG_VALUE' + }); + }); + + const push = stream.pushStream({ + ':method': 'GET', + ':scheme': 'https', + ':authority': 'localhost', + ':path': '/foo' + }); + assert(push); + push.submitInitialHeaders({ ':status': '200' }); + push.end('testing'); + push.on('close', common.mustCall()); + push.on('finish', common.mustCall()); + + stream.end('hello world'); + stream.resume(); + stream.on('end', common.mustCall()); + stream.on('close', common.mustCall()); + stream.on('finish', common.mustCall()); + + stream.on('initialHeaders', common.mustCall((headers) => { + const expected = [ + [ ':path', '/' ], + [ ':authority', 'localhost' ], + [ ':scheme', 'https' ], + [ ':method', 'POST' ] + ]; + assert.deepStrictEqual(expected, headers); + })); + stream.on('informationalHeaders', common.mustNotCall()); + stream.on('trailingHeaders', common.mustNotCall()); + })); + + session.on('close', common.mustCall()); +})); + +server.on('ready', common.mustCall(() => { + client = createQuicSocket({ client: options }); + client.on('close', common.mustCall()); + + const req = client.connect({ + address: 'localhost', + port: server.endpoints[0].address.port, + maxStreamsUni: 10, + h3: { maxPushes: 10 } + }); + + req.on('stream', common.mustCall((stream) => { + let data = ''; + + stream.on('initialHeaders', common.mustCall((headers) => { + const expected = [ + [':status', '200'] + ]; + assert.deepStrictEqual(expected, headers); + })); + + stream.setEncoding('utf8'); + stream.on('data', (chunk) => data += chunk); + stream.on('end', common.mustCall(() => { + assert.strictEqual(data, 'testing'); + })); + stream.on('close', common.mustCall(() => { + countdown.dec(); + })); + })); + + req.on('close', common.mustCall()); + req.on('secure', common.mustCall((servername, alpn, cipher) => { + const stream = req.openStream(); + + stream.on('pushHeaders', common.mustCall((headers, push_id) => { + const expected = [ + [ ':path', '/foo' ], + [ ':authority', 'localhost' ], + [ ':scheme', 'https' ], + [ ':method', 'GET' ] + ]; + assert.deepStrictEqual(expected, headers); + assert.strictEqual(push_id, 0); + })); + + assert(stream.submitInitialHeaders({ + ':method': 'POST', + ':scheme': 'https', + ':authority': 'localhost', + ':path': '/', + })); + + stream.end('hello world'); + stream.resume(); + stream.on('finish', common.mustCall()); + stream.on('end', common.mustCall()); + + stream.on('initialHeaders', common.mustCall((headers) => { + const expected = [ + [ ':status', '200' ] + ]; + assert.deepStrictEqual(expected, headers); + })); + stream.on('informationalHeaders', common.mustNotCall()); + stream.on('trailingHeaders', common.mustNotCall()); + + stream.on('close', common.mustCall(() => { + countdown.dec(); + })); + })); +})); + +server.on('listening', common.mustCall()); +server.on('close', common.mustCall()); diff --git a/test/parallel/test-quic-http3-trailers.js b/test/parallel/test-quic-http3-trailers.js new file mode 100644 index 00000000000000..9535f45532c789 --- /dev/null +++ b/test/parallel/test-quic-http3-trailers.js @@ -0,0 +1,109 @@ +// Flags: --expose-internals --no-warnings +'use strict'; + +// Tests a simple QUIC HTTP/3 client/server round-trip + +const common = require('../common'); +if (!common.hasQuic) + common.skip('missing quic'); + +const Countdown = require('../common/countdown'); +const assert = require('assert'); +const { key, cert, ca, kHttp3Alpn } = require('../common/quic'); + +const { createQuicSocket } = require('net'); + +let client; +const server = createQuicSocket(); + +const countdown = new Countdown(1, () => { + server.close(); + client.close(); +}); + +const options = { key, cert, ca, alpn: kHttp3Alpn }; + +server.listen(options); + +server.on('session', common.mustCall((session) => { + + session.on('stream', common.mustCall((stream) => { + assert(stream.submitInitialHeaders({ ':status': '200' })); + + stream.submitTrailingHeaders({ 'a': 1 }); + stream.end('hello world'); + stream.resume(); + stream.on('end', common.mustCall()); + stream.on('close', common.mustCall()); + stream.on('finish', common.mustCall()); + + stream.on('initialHeaders', common.mustCall((headers) => { + const expected = [ + [ ':path', '/' ], + [ ':authority', 'localhost' ], + [ ':scheme', 'https' ], + [ ':method', 'POST' ] + ]; + assert.deepStrictEqual(expected, headers); + })); + + stream.on('trailingHeaders', common.mustCall((headers) => { + const expected = [ [ 'b', '2' ] ]; + assert.deepStrictEqual(expected, headers); + })); + + stream.on('informationalHeaders', common.mustNotCall()); + })); + + session.on('close', common.mustCall()); +})); + +server.on('ready', common.mustCall(() => { + client = createQuicSocket({ client: options }); + client.on('close', common.mustCall()); + + const req = client.connect({ + address: 'localhost', + port: server.endpoints[0].address.port, + maxStreamsUni: 10, + h3: { maxPushes: 10 } + }); + + req.on('close', common.mustCall()); + req.on('secure', common.mustCall((servername, alpn, cipher) => { + const stream = req.openStream(); + + stream.on('trailingHeaders', common.mustCall((headers) => { + const expected = [ [ 'a', '1' ] ]; + assert.deepStrictEqual(expected, headers); + })); + + assert(stream.submitInitialHeaders({ + ':method': 'POST', + ':scheme': 'https', + ':authority': 'localhost', + ':path': '/', + })); + + stream.submitTrailingHeaders({ 'b': 2 }); + stream.end('hello world'); + stream.resume(); + stream.on('finish', common.mustCall()); + stream.on('end', common.mustCall()); + + stream.on('initialHeaders', common.mustCall((headers) => { + const expected = [ + [ ':status', '200' ] + ]; + assert.deepStrictEqual(expected, headers); + })); + stream.on('informationalHeaders', common.mustNotCall()); + + stream.on('close', common.mustCall(() => { + countdown.dec(); + })); + })); +})); + +server.on('listening', common.mustCall()); +server.on('close', common.mustCall()); diff --git a/test/parallel/test-quic-idle-timeout.js b/test/parallel/test-quic-idle-timeout.js new file mode 100644 index 00000000000000..b07830bcb18bdb --- /dev/null +++ b/test/parallel/test-quic-idle-timeout.js @@ -0,0 +1,73 @@ +// Flags: --no-warnings +'use strict'; + +const common = require('../common'); +if (!common.hasQuic) + common.skip('missing quic'); + +const assert = require('assert'); +const { createQuicSocket } = require('net'); +const { key, cert, ca } = require('../common/quic'); +const { once } = require('events'); + +const kALPN = 'zzz'; +const idleTimeout = common.platformTimeout(1); +const options = { key, cert, ca, alpn: kALPN }; + +// Test idleTimeout. The test will hang and fail with a timeout +// if the idleTimeout is not working correctly. + +(async () => { + const server = createQuicSocket({ server: options }); + const client = createQuicSocket({ client: options }); + + server.listen(); + server.on('session', common.mustCall()); + + await once(server, 'ready'); + + const session = client.connect({ + address: common.localhostIPv4, + port: server.endpoints[0].address.port, + idleTimeout, + }); + + await once(session, 'close'); + + assert(session.idleTimeout); + client.close(); + server.close(); + + await Promise.all([ + once(client, 'close'), + once(server, 'close') + ]); +})().then(common.mustCall()); + + +(async () => { + const server = createQuicSocket({ server: options }); + const client = createQuicSocket({ client: options }); + + server.listen({ idleTimeout }); + + server.on('session', common.mustCall(async (session) => { + await once(session, 'close'); + assert(session.idleTimeout); + client.close(); + server.close(); + await Promise.all([ + once(client, 'close'), + once(server, 'close') + ]); + })); + + await once(server, 'ready'); + + const session = client.connect({ + address: common.localhostIPv4, + port: server.endpoints[0].address.port, + }); + + await once(session, 'close'); +})().then(common.mustCall()); diff --git a/test/parallel/test-quic-ipv6only.js b/test/parallel/test-quic-ipv6only.js new file mode 100644 index 00000000000000..e9f4d44963178a --- /dev/null +++ b/test/parallel/test-quic-ipv6only.js @@ -0,0 +1,140 @@ +// Flags: --no-warnings +'use strict'; + +const common = require('../common'); + +if (!common.hasIPv6) + common.skip('missing ipv6'); + +if (!common.hasQuic) + common.skip('missing quic'); + +const assert = require('assert'); +const { createQuicSocket } = require('net'); +const { key, cert, ca } = require('../common/quic'); +const { once } = require('events'); + +const kALPN = 'zzz'; + +// Setting `type` to `udp4` while setting `ipv6Only` to `true` is possible +// and it will throw an error. +{ + const server = createQuicSocket({ + endpoint: { + type: 'udp4', + ipv6Only: true + } }); + + server.on('error', common.mustCall((err) => { + assert.strictEqual(err.code, 'EINVAL'); + assert.strictEqual(err.message, 'bind EINVAL 0.0.0.0'); + })); + + server.listen({ key, cert, ca, alpn: kALPN }); +} + +// Connecting ipv6 server by "127.0.0.1" should work when `ipv6Only` +// is set to `false`. +(async () => { + const server = createQuicSocket({ + endpoint: { + type: 'udp6', + ipv6Only: false + } }); + const client = createQuicSocket({ client: { key, cert, ca, alpn: kALPN } }); + + server.listen({ key, cert, ca, alpn: kALPN }); + + server.on('session', common.mustCall((serverSession) => { + serverSession.on('stream', common.mustCall()); + })); + + await once(server, 'ready'); + + const session = client.connect({ + address: common.localhostIPv4, + port: server.endpoints[0].address.port, + }); + + await once(session, 'secure'); + + const stream = session.openStream({ halfOpen: true }); + stream.end('hello'); + + await once(stream, 'close'); + + client.close(); + server.close(); + + await Promise.allSettled([ + once(client, 'close'), + once(server, 'close') + ]); +})().then(common.mustCall()); + +// When the `ipv6Only` set to `true`, a client cann't connect to it +// through "127.0.0.1". +(async () => { + const server = createQuicSocket({ + endpoint: { + type: 'udp6', + ipv6Only: true + } }); + const client = createQuicSocket({ client: { key, cert, ca, alpn: kALPN } }); + + server.listen({ key, cert, ca, alpn: kALPN }); + server.on('session', common.mustNotCall()); + + await once(server, 'ready'); + + const session = client.connect({ + address: common.localhostIPv4, + port: server.endpoints[0].address.port, + idleTimeout: common.platformTimeout(1), + }); + + session.on('secure', common.mustNotCall()); + + await once(session, 'close'); + + client.close(); + server.close(); + + await Promise.allSettled([ + once(client, 'close'), + once(server, 'close') + ]); +})(); + +// Creating the QuicSession fails when connect type does not match the +// the connect IP address... +(async () => { + const server = createQuicSocket({ endpoint: { type: 'udp6' } }); + const client = createQuicSocket({ client: { key, cert, ca, alpn: kALPN } }); + + server.listen({ key, cert, ca, alpn: kALPN }); + server.on('session', common.mustNotCall()); + + await once(server, 'ready'); + + const session = client.connect({ + address: common.localhostIPv4, + port: server.endpoints[0].address.port, + type: 'udp6', + idleTimeout: common.platformTimeout(1), + }); + + session.on('error', common.mustCall((err) => { + assert.strictEqual(err.code, 'ERR_QUICCLIENTSESSION_FAILED'); + client.close(); + server.close(); + })); + + session.on('secure', common.mustNotCall()); + session.on('close', common.mustCall()); + + await Promise.allSettled([ + once(client, 'close'), + once(server, 'close') + ]); +})().then(common.mustCall()); diff --git a/test/parallel/test-quic-keylog.js b/test/parallel/test-quic-keylog.js new file mode 100644 index 00000000000000..78354d42f65e80 --- /dev/null +++ b/test/parallel/test-quic-keylog.js @@ -0,0 +1,67 @@ +// Flags: --expose-internals --no-warnings +'use strict'; + +// Tests QUIC keylogging + +const common = require('../common'); +if (!common.hasQuic) + common.skip('missing quic'); + +const assert = require('assert'); +const { key, cert, ca } = require('../common/quic'); +const { once } = require('events'); + +const { createQuicSocket } = require('net'); + +const kKeylogs = [ + /^CLIENT_HANDSHAKE_TRAFFIC_SECRET .*/, + /^SERVER_HANDSHAKE_TRAFFIC_SECRET .*/, + /^QUIC_CLIENT_HANDSHAKE_TRAFFIC_SECRET .*/, + /^QUIC_SERVER_HANDSHAKE_TRAFFIC_SECRET .*/, + /^CLIENT_TRAFFIC_SECRET_0 .*/, + /^SERVER_TRAFFIC_SECRET_0 .*/, + /^QUIC_CLIENT_TRAFFIC_SECRET_0 .*/, + /^QUIC_SERVER_TRAFFIC_SECRET_0 .*/, +]; + +const options = { key, cert, ca, alpn: 'zzz' }; + +(async () => { + const server = createQuicSocket({ server: options }); + const client = createQuicSocket({ client: options }); + + const kServerKeylogs = Array.from(kKeylogs); + const kClientKeylogs = Array.from(kKeylogs); + + server.listen(); + + server.on('session', common.mustCall((session) => { + session.on('keylog', common.mustCall((line) => { + assert.match(line.toString(), kServerKeylogs.shift()); + }, kServerKeylogs.length)); + })); + + await once(server, 'ready'); + + const req = client.connect({ + address: common.localhostIPv4, + port: server.endpoints[0].address.port, + }); + + req.on('keylog', common.mustCall((line) => { + assert.match(line.toString(), kClientKeylogs.shift()); + }, kClientKeylogs.length)); + + await once(req, 'secure'); + + server.close(); + client.close(); + + await Promise.allSettled([ + once(server, 'close'), + once(client, 'close') + ]); + + assert.strictEqual(kServerKeylogs.length, 0); + assert.strictEqual(kClientKeylogs.length, 0); +})().then(common.mustCall()); diff --git a/test/parallel/test-quic-maxconnectionsperhost.js b/test/parallel/test-quic-maxconnectionsperhost.js new file mode 100644 index 00000000000000..b94849b88272c3 --- /dev/null +++ b/test/parallel/test-quic-maxconnectionsperhost.js @@ -0,0 +1,86 @@ +// Flags: --no-warnings +'use strict'; + +const common = require('../common'); +if (!common.hasQuic) + common.skip('missing quic'); + +const { createQuicSocket } = require('net'); +const assert = require('assert'); +const Countdown = require('../common/countdown'); +const { key, cert, ca } = require('../common/quic'); +const kServerName = 'agent2'; +const kALPN = 'zzz'; + +// QuicSockets must throw errors when maxConnectionsPerHost is not a +// safe integer or is out of range. +{ + [-1, 0, Number.MAX_SAFE_INTEGER + 1, 1.1].forEach((maxConnectionsPerHost) => { + assert.throws(() => createQuicSocket({ maxConnectionsPerHost }), { + code: 'ERR_OUT_OF_RANGE' + }); + }); +} + +// Test that new client sessions will be closed when it exceeds +// maxConnectionsPerHost. +{ + const kMaxConnectionsPerHost = 5; + const kIdleTimeout = 0; + + let client; + let server; + + const countdown = new Countdown(kMaxConnectionsPerHost + 1, () => { + client.close(); + server.close(); + }); + + function connect() { + return client.connect({ + key, + cert, + ca, + address: common.localhostIPv4, + port: server.endpoints[0].address.port, + servername: kServerName, + alpn: kALPN, + idleTimeout: kIdleTimeout, + }); + } + + server = createQuicSocket({ maxConnectionsPerHost: kMaxConnectionsPerHost }); + + server.listen({ key, cert, ca, alpn: kALPN, idleTimeout: kIdleTimeout }); + + server.on('session', common.mustCall(() => {}, kMaxConnectionsPerHost)); + + server.on('close', common.mustCall(() => { + assert.strictEqual(server.serverBusyCount, 1n); + })); + + server.on('ready', common.mustCall(() => { + client = createQuicSocket(); + + const sessions = []; + + for (let i = 0; i < kMaxConnectionsPerHost; i += 1) { + const req = connect(); + req.on('error', common.mustNotCall()); + req.on('close', common.mustCall(() => countdown.dec())); + sessions.push(req); + } + + const extra = connect(); + extra.on('error', console.log); + extra.on('close', common.mustCall(() => { + countdown.dec(); + // Shutdown the remaining open sessions. + setImmediate(common.mustCall(() => { + for (const req of sessions) + req.close(); + })); + })); + + })); +} diff --git a/test/parallel/test-quic-process-cleanup.js b/test/parallel/test-quic-process-cleanup.js new file mode 100644 index 00000000000000..856c6706f88c73 --- /dev/null +++ b/test/parallel/test-quic-process-cleanup.js @@ -0,0 +1,57 @@ +// Flags: --no-warnings +'use strict'; +const common = require('../common'); +if (!common.hasQuic) + common.skip('missing quic'); + +// Test that shutting down a process containing an active QUIC server behaves +// well. We use Workers because they have a more clearly defined shutdown +// sequence and we can stop execution at any point. + +const { createQuicSocket } = require('net'); +const { Worker, workerData } = require('worker_threads'); + +if (workerData == null) { + new Worker(__filename, { workerData: { removeFromSocket: true } }); + new Worker(__filename, { workerData: { removeFromSocket: false } }); + return; +} + +const { key, cert, ca } = require('../common/quic'); +const options = { key, cert, ca, alpn: 'meow' }; + +const server = createQuicSocket({ server: options }); + +server.listen(); + +server.on('session', common.mustCall((session) => { + session.on('secure', common.mustCall((servername, alpn, cipher) => { + const stream = session.openStream({ halfOpen: false }); + stream.write('Hi!'); + stream.on('data', common.mustNotCall()); + stream.on('finish', common.mustNotCall()); + stream.on('close', common.mustNotCall()); + stream.on('end', common.mustNotCall()); + })); + + session.on('close', common.mustNotCall()); +})); + +server.on('ready', common.mustCall(() => { + const client = createQuicSocket({ client: options }); + + const req = client.connect({ + address: common.localhostIPv4, + port: server.endpoints[0].address.port + }); + + req.on('stream', common.mustCall(() => { + if (workerData.removeFromSocket) + req.removeFromSocket(); + process.exit(); // Exits the worker thread + })); + + req.on('close', common.mustNotCall()); +})); + +server.on('close', common.mustNotCall()); diff --git a/test/parallel/test-quic-qlog.js b/test/parallel/test-quic-qlog.js new file mode 100644 index 00000000000000..3306a539379e8f --- /dev/null +++ b/test/parallel/test-quic-qlog.js @@ -0,0 +1,66 @@ +// Flags: --expose-internals --no-warnings +'use strict'; +const common = require('../common'); +if (!common.hasQuic) + common.skip('missing quic'); + +const { makeUDPPair } = require('../common/udppair'); +const assert = require('assert'); +const { createQuicSocket } = require('net'); +const { kUDPHandleForTesting } = require('internal/quic/core'); + +const { key, cert, ca } = require('../common/quic'); + +const { serverSide, clientSide } = makeUDPPair(); + +const server = createQuicSocket({ + validateAddress: true, + endpoint: { [kUDPHandleForTesting]: serverSide._handle }, + qlog: true +}); + +serverSide.afterBind(); +server.listen({ key, cert, ca, alpn: 'meow' }); + +server.on('session', common.mustCall((session) => { + gatherQlog(session, 'server'); + + session.on('secure', common.mustCall((servername, alpn, cipher) => { + const stream = session.openStream({ halfOpen: true }); + stream.end('Hi!'); + })); +})); + +server.on('ready', common.mustCall(() => { + const client = createQuicSocket({ + endpoint: { [kUDPHandleForTesting]: clientSide._handle }, + client: { key, cert, ca, alpn: 'meow' }, + qlog: true + }); + clientSide.afterBind(); + + const req = client.connect({ + address: 'localhost', + port: server.endpoints[0].address.port, + qlog: true + }); + + gatherQlog(req, 'client'); + + req.on('stream', common.mustCall((stream) => { + stream.resume(); + stream.on('end', common.mustCall(() => { + req.close(); + })); + })); +})); + +function gatherQlog(session, id) { + let log = ''; + session.on('qlog', (chunk) => log += chunk); + session.on('close', common.mustCall(() => { + const { qlog_version, traces } = JSON.parse(log); + assert.strictEqual(typeof qlog_version, 'string'); + assert.strictEqual(typeof traces[0].events, 'object'); + })); +} diff --git a/test/parallel/test-quic-quicendpoint-address.js b/test/parallel/test-quic-quicendpoint-address.js new file mode 100644 index 00000000000000..0554104748e777 --- /dev/null +++ b/test/parallel/test-quic-quicendpoint-address.js @@ -0,0 +1,87 @@ +// Flags: --no-warnings +'use strict'; + +// Tests multiple aspects of QuicSocket multiple endpoint support + +const common = require('../common'); +const { once } = require('events'); +if (!common.hasQuic) + common.skip('missing quic'); + +const assert = require('assert'); + +const { key, cert, ca } = require('../common/quic'); + +const { createQuicSocket } = require('net'); + +async function Test1(options, address) { + const server = createQuicSocket(options); + assert.strictEqual(server.endpoints.length, 1); + assert.strictEqual(server.endpoints[0].bound, false); + assert.deepStrictEqual({}, server.endpoints[0].address); + + server.listen({ key, cert, ca, alpn: 'zzz' }); + + await once(server, 'ready'); + assert.strictEqual(server.endpoints.length, 1); + const endpoint = server.endpoints[0]; + assert.strictEqual(endpoint.bound, true); + assert.strictEqual(endpoint.destroyed, false); + assert.strictEqual(typeof endpoint.address.port, 'number'); + assert.strictEqual(endpoint.address.address, address); + server.close(); + assert.strictEqual(endpoint.destroyed, true); +} + +async function Test2() { + // Creates a server with multiple endpoints (one on udp4 and udp6) + const server = createQuicSocket({ endpoint: { type: 'udp6' } }); + server.addEndpoint(); + assert.strictEqual(server.endpoints.length, 2); + assert.strictEqual(server.endpoints[0].bound, false); + assert.deepStrictEqual({}, server.endpoints[0].address); + + server.listen({ key, cert, ca, alpn: 'zzz' }); + + await once(server, 'ready'); + + assert.strictEqual(server.endpoints.length, 2); + + { + const endpoint = server.endpoints[0]; + assert.strictEqual(endpoint.bound, true); + assert.strictEqual(endpoint.destroyed, false); + assert.strictEqual(endpoint.address.family, 'IPv6'); + assert.strictEqual(typeof endpoint.address.port, 'number'); + assert.strictEqual(endpoint.address.address, '::'); + } + + { + const endpoint = server.endpoints[1]; + assert.strictEqual(endpoint.bound, true); + assert.strictEqual(endpoint.destroyed, false); + assert.strictEqual(endpoint.address.family, 'IPv4'); + assert.strictEqual(typeof endpoint.address.port, 'number'); + assert.strictEqual(endpoint.address.address, '0.0.0.0'); + } + + server.close(); + for (const endpoint of server.endpoints) + assert.strictEqual(endpoint.destroyed, true); +} + +const tests = [ + Test1({}, '0.0.0.0'), + Test1({ endpoint: { port: 0 } }, '0.0.0.0'), + Test1({ endpoint: { address: '127.0.0.1', port: 0 } }, '127.0.0.1'), + Test1({ endpoint: { address: 'localhost', port: 0 } }, '127.0.0.1') +]; + +if (common.hasIPv6) { + tests.push( + Test1({ endpoint: { type: 'udp6' } }, '::'), + Test1({ endpoint: { type: 'udp6', address: 'localhost' } }, '::1'), + Test2()); +} + +Promise.all(tests); diff --git a/test/parallel/test-quic-quicession-server-openstream-pending.js b/test/parallel/test-quic-quicession-server-openstream-pending.js new file mode 100644 index 00000000000000..b0138dd7b5d7a3 --- /dev/null +++ b/test/parallel/test-quic-quicession-server-openstream-pending.js @@ -0,0 +1,59 @@ +// Flags: --no-warnings +'use strict'; +const common = require('../common'); +if (!common.hasQuic) + common.skip('missing quic'); + +// Test that opening a stream works even if the session isn’t ready yet. + +const assert = require('assert'); +const { createQuicSocket } = require('net'); +const { key, cert, ca } = require('../common/quic'); +const { once } = require('events'); +const options = { key, cert, ca, alpn: 'meow' }; + +(async () => { + const server = createQuicSocket({ server: options }); + const client = createQuicSocket({ client: options }); + + server.listen(); + + server.on('session', common.mustCall((session) => { + // The server can create a stream immediately without waiting + // for the secure event... however, the data will not actually + // be transmitted until the handshake is completed. + const stream = session.openStream({ halfOpen: true }); + stream.on('close', common.mustCall()); + stream.on('error', console.log); + stream.end('hello'); + + session.on('stream', common.mustNotCall()); + })); + + await once(server, 'ready'); + + const req = client.connect({ + address: common.localhostIPv4, + port: server.endpoints[0].address.port, + }); + + const [ stream ] = await once(req, 'stream'); + + let data = ''; + stream.setEncoding('utf8'); + stream.on('data', (chunk) => data += chunk); + stream.on('end', common.mustCall()); + + await once(stream, 'close'); + + assert.strictEqual(data, 'hello'); + + server.close(); + client.close(); + + await Promise.all([ + once(server, 'close'), + once(client, 'close') + ]); + +})().then(common.mustCall()); diff --git a/test/parallel/test-quic-quicsession-openstream-pending.js b/test/parallel/test-quic-quicsession-openstream-pending.js new file mode 100644 index 00000000000000..5ea05107eb9b49 --- /dev/null +++ b/test/parallel/test-quic-quicsession-openstream-pending.js @@ -0,0 +1,64 @@ +// Flags: --no-warnings +'use strict'; +const common = require('../common'); +if (!common.hasQuic) + common.skip('missing quic'); + +// Test that opening a stream works even if the session isn’t ready yet. + +const assert = require('assert'); +const { createQuicSocket } = require('net'); +const { key, cert, ca } = require('../common/quic'); +const { once } = require('events'); +const options = { key, cert, ca, alpn: 'meow' }; + +(async () => { + const server = createQuicSocket({ server: options }); + const client = createQuicSocket({ client: options }); + + server.listen(); + + server.on('session', common.mustCall((session) => { + session.on('stream', common.mustCall(async (stream) => { + let data = ''; + stream.setEncoding('utf8'); + stream.on('data', (chunk) => data += chunk); + await once(stream, 'end'); + assert.strictEqual(data, 'Hello!'); + })); + })); + + await once(server, 'ready'); + + const req = client.connect({ + address: common.localhostIPv4, + port: server.endpoints[0].address.port + }); + + // In this case, the QuicStream is usable but corked + // until the underlying internal QuicStream handle + // has been created, which will not happen until + // after the TLS handshake has been completed. + const stream = req.openStream({ halfOpen: true }); + stream.end('Hello!'); + stream.on('error', common.mustNotCall()); + stream.resume(); + assert(!req.allowEarlyData); + assert(!req.handshakeComplete); + assert(stream.pending); + + await once(stream, 'ready'); + + assert(req.handshakeComplete); + assert(!stream.pending); + + await once(stream, 'close'); + + server.close(); + client.close(); + + await Promise.all([ + once(server, 'close'), + once(client, 'close') + ]); +})().then(common.mustCall()); diff --git a/test/parallel/test-quic-quicsession-resume.js b/test/parallel/test-quic-quicsession-resume.js new file mode 100644 index 00000000000000..4d4a00abcb175c --- /dev/null +++ b/test/parallel/test-quic-quicsession-resume.js @@ -0,0 +1,100 @@ +'use strict'; + +// Tests a simple QUIC client/server round-trip + +const common = require('../common'); +if (!common.hasQuic) + common.skip('missing quic'); + +const { Buffer } = require('buffer'); +const Countdown = require('../common/countdown'); +const assert = require('assert'); +const { + key, + cert, + ca, + debug, +} = require('../common/quic'); + +const { createQuicSocket } = require('net'); + +const options = { key, cert, ca, alpn: 'zzz' }; + +const server = createQuicSocket({ server: options }); +const client = createQuicSocket({ client: options }); + +const countdown = new Countdown(2, () => { + server.close(); + client.close(); +}); + +server.listen(); +server.on('session', common.mustCall((session) => { + session.on('secure', common.mustCall(() => { + assert(session.usingEarlyData); + })); + + session.on('stream', common.mustCall((stream) => { + stream.resume(); + })); +}, 2)); + +server.on('ready', common.mustCall(() => { + const req = client.connect({ + address: common.localhostIPv4, + port: server.endpoints[0].address.port, + }); + + const stream = req.openStream({ halfOpen: true }); + stream.end('hello'); + stream.resume(); + stream.on('close', () => countdown.dec()); + + req.on('sessionTicket', common.mustCall((ticket, params) => { + assert(ticket instanceof Buffer); + assert(params instanceof Buffer); + debug(' Ticket: %s', ticket.toString('hex')); + debug(' Params: %s', params.toString('hex')); + + // Destroy this initial client session... + req.destroy(); + + // Wait a tick then start a new one. + setImmediate(newSession, ticket, params); + }, 1)); + + function newSession(sessionTicket, remoteTransportParams) { + const req = client.connect({ + address: common.localhostIPv4, + port: server.endpoints[0].address.port, + sessionTicket, + remoteTransportParams, + autoStart: false, + }); + + assert(req.allowEarlyData); + + const stream = req.openStream({ halfOpen: true }); + stream.end('hello'); + stream.on('error', common.mustNotCall()); + stream.on('close', common.mustCall(() => countdown.dec())); + + req.startHandshake(); + + // TODO(@jasnell): There's a slight bug in here in that + // calling end() will uncork the stream, causing data to + // be flushed to the C++ layer, which will trigger a + // SendPendingData that will start the handshake. That + // has the effect of short circuiting the intent of + // manual startHandshake(), which makes it not use 0RTT + // for the stream data. + + req.on('secure', common.mustCall(() => { + // TODO(@jasnell): This will be false for now because no + // early data was sent. Once we actually start making + // use of early data on the client side, this should be + // true when the early data was accepted. + assert(!req.usingEarlyData); + })); + } +})); diff --git a/test/parallel/test-quic-quicsession-send-fd.js b/test/parallel/test-quic-quicsession-send-fd.js new file mode 100644 index 00000000000000..0e1a8a10d56b68 --- /dev/null +++ b/test/parallel/test-quic-quicsession-send-fd.js @@ -0,0 +1,86 @@ +// Flags: --no-warnings +'use strict'; +const common = require('../common'); +if (!common.hasQuic) + common.skip('missing quic'); + +const assert = require('assert'); +const { createQuicSocket } = require('net'); +const fs = require('fs'); + +const { key, cert, ca } = require('../common/quic'); + +const variants = []; +for (const variant of ['sendFD', 'sendFile', 'sendFD+fileHandle']) { + for (const offset of [-1, 0, 100]) { + for (const length of [-1, 100]) { + variants.push({ variant, offset, length }); + } + } +} + +for (const { variant, offset, length } of variants) { + const server = createQuicSocket(); + let fd; + + server.listen({ key, cert, ca, alpn: 'meow' }); + + server.on('session', common.mustCall((session) => { + session.on('secure', common.mustCall((servername, alpn, cipher) => { + const stream = session.openStream({ halfOpen: false }); + + stream.on('data', common.mustNotCall()); + stream.on('finish', common.mustCall()); + stream.on('close', common.mustCall()); + stream.on('end', common.mustCall()); + + if (variant === 'sendFD') { + fd = fs.openSync(__filename, 'r'); + stream.sendFD(fd, { offset, length }); + } else if (variant === 'sendFD+fileHandle') { + fs.promises.open(__filename, 'r').then(common.mustCall((handle) => { + fd = handle; + stream.sendFD(handle, { offset, length }); + })); + } else { + assert.strictEqual(variant, 'sendFile'); + stream.sendFile(__filename, { offset, length }); + } + })); + + session.on('close', common.mustCall()); + })); + + server.on('ready', common.mustCall(() => { + const client = createQuicSocket({ + client: { key, cert, ca, alpn: 'meow' } }); + + const req = client.connect({ + address: 'localhost', + port: server.endpoints[0].address.port + }); + + req.on('stream', common.mustCall((stream) => { + const data = []; + stream.on('data', (chunk) => data.push(chunk)); + stream.on('end', common.mustCall(() => { + let expectedContent = fs.readFileSync(__filename); + if (offset !== -1) expectedContent = expectedContent.slice(offset); + if (length !== -1) expectedContent = expectedContent.slice(0, length); + assert.deepStrictEqual(Buffer.concat(data), expectedContent); + + stream.end(); + client.close(); + server.close(); + if (fd !== undefined) { + if (fd.close) fd.close().then(common.mustCall()); + else fs.closeSync(fd); + } + })); + })); + + req.on('close', common.mustCall()); + })); + + server.on('close', common.mustCall()); +} diff --git a/test/parallel/test-quic-quicsession-send-file-close-before-open.js b/test/parallel/test-quic-quicsession-send-file-close-before-open.js new file mode 100644 index 00000000000000..d5d0c2c8e04ef4 --- /dev/null +++ b/test/parallel/test-quic-quicsession-send-file-close-before-open.js @@ -0,0 +1,46 @@ +// Flags: --no-warnings +'use strict'; +const common = require('../common'); +if (!common.hasQuic) + common.skip('missing quic'); + +const { createQuicSocket } = require('net'); +const fs = require('fs'); + +const { key, cert, ca } = require('../common/quic'); + +const server = createQuicSocket(); + +server.listen({ key, cert, ca, alpn: 'meow' }); + +server.on('session', common.mustCall((session) => { + session.on('secure', common.mustCall((servername, alpn, cipher) => { + const stream = session.openStream({ halfOpen: false }); + + fs.open = common.mustCall(fs.open); + fs.close = common.mustCall(fs.close); + + stream.sendFile(__filename); + stream.destroy(); // Destroy the stream before opening the fd finishes. + + session.close(); + server.close(); + })); + + session.on('close', common.mustCall()); +})); + +server.on('ready', common.mustCall(() => { + const client = createQuicSocket({ client: { key, cert, ca, alpn: 'meow' } }); + + const req = client.connect({ + address: 'localhost', + port: server.endpoints[0].address.port + }); + + req.on('stream', common.mustNotCall()); + + req.on('close', common.mustCall(() => client.close())); +})); + +server.on('close', common.mustCall()); diff --git a/test/parallel/test-quic-quicsession-send-file-open-error-handled.js b/test/parallel/test-quic-quicsession-send-file-open-error-handled.js new file mode 100644 index 00000000000000..d632b59b68d3ec --- /dev/null +++ b/test/parallel/test-quic-quicsession-send-file-open-error-handled.js @@ -0,0 +1,49 @@ +// Flags: --no-warnings +'use strict'; +const common = require('../common'); +if (!common.hasQuic) + common.skip('missing quic'); + +const path = require('path'); +const { createQuicSocket } = require('net'); + +const { key, cert, ca } = require('../common/quic'); + +const server = createQuicSocket(); + +server.listen({ key, cert, ca, alpn: 'meow' }); + +server.on('session', common.mustCall((session) => { + session.on('secure', common.mustCall((servername, alpn, cipher) => { + const stream = session.openStream({ halfOpen: true }); + const nonexistentPath = path.resolve(__dirname, 'nonexistent.file'); + + stream.sendFile(nonexistentPath, { + onError: common.expectsError({ + code: 'ENOENT', + syscall: 'open', + path: nonexistentPath + }) + }); + + session.close(); + server.close(); + })); + + session.on('close', common.mustCall()); +})); + +server.on('ready', common.mustCall(() => { + const client = createQuicSocket({ client: { key, cert, ca, alpn: 'meow' } }); + + const req = client.connect({ + address: 'localhost', + port: server.endpoints[0].address.port + }); + + req.on('stream', common.mustNotCall()); + + req.on('close', common.mustCall(() => client.close())); +})); + +server.on('close', common.mustCall()); diff --git a/test/parallel/test-quic-quicsession-send-file-open-error.js b/test/parallel/test-quic-quicsession-send-file-open-error.js new file mode 100644 index 00000000000000..62acbaaf9c4557 --- /dev/null +++ b/test/parallel/test-quic-quicsession-send-file-open-error.js @@ -0,0 +1,49 @@ +// Flags: --no-warnings +'use strict'; +const common = require('../common'); +if (!common.hasQuic) + common.skip('missing quic'); + +const path = require('path'); +const { createQuicSocket } = require('net'); + +const { key, cert, ca } = require('../common/quic'); + +const server = createQuicSocket(); + +server.listen({ key, cert, ca, alpn: 'meow' }); + +server.on('session', common.mustCall((session) => { + session.on('secure', common.mustCall((servername, alpn, cipher) => { + const stream = session.openStream({ halfOpen: false }); + const nonexistentPath = path.resolve(__dirname, 'nonexistent.file'); + + stream.on('error', common.expectsError({ + code: 'ENOENT', + syscall: 'open', + path: nonexistentPath + })); + + stream.sendFile(nonexistentPath); + + session.close(); + server.close(); + })); + + session.on('close', common.mustCall()); +})); + +server.on('ready', common.mustCall(() => { + const client = createQuicSocket({ client: { key, cert, ca, alpn: 'meow' } }); + + const req = client.connect({ + address: 'localhost', + port: server.endpoints[0].address.port + }); + + req.on('stream', common.mustNotCall()); + + req.on('close', common.mustCall(() => client.close())); +})); + +server.on('close', common.mustCall()); diff --git a/test/parallel/test-quic-quicsession-server-destroy-early.js b/test/parallel/test-quic-quicsession-server-destroy-early.js new file mode 100644 index 00000000000000..1471790a27de9e --- /dev/null +++ b/test/parallel/test-quic-quicsession-server-destroy-early.js @@ -0,0 +1,81 @@ +// Flags: --no-warnings +'use strict'; + +// Test that destroying a QuicStream immediately and synchronously +// after creation does not crash the process and closes the streams +// abruptly on both ends of the connection. + +const common = require('../common'); +if (!common.hasQuic) + common.skip('missing quic'); + +const assert = require('assert'); +const fs = require('fs'); +const fixtures = require('../common/fixtures'); +const key = fixtures.readKey('agent1-key.pem', 'binary'); +const cert = fixtures.readKey('agent1-cert.pem', 'binary'); +const ca = fixtures.readKey('ca1-cert.pem', 'binary'); +const { debuglog } = require('util'); +const debug = debuglog('test'); + +const { createQuicSocket } = require('net'); + +const kServerPort = process.env.NODE_DEBUG_KEYLOG ? 5678 : 0; +const kClientPort = process.env.NODE_DEBUG_KEYLOG ? 5679 : 0; + +const kServerName = 'agent2'; // Intentionally the wrong servername +const kALPN = 'zzz'; // ALPN can be overriden to whatever we want + +let client; +const server = createQuicSocket({ endpoint: { port: kServerPort } }); + +server.listen({ key, cert, ca, alpn: kALPN }); + +server.on('session', common.mustCall((session) => { + debug('QuicServerSession Created'); + + if (process.env.NODE_DEBUG_KEYLOG) { + const kl = fs.createWriteStream(process.env.NODE_DEBUG_KEYLOG); + session.on('keylog', kl.write.bind(kl)); + } + + session.on('close', common.mustCall(() => { + client.close(); + server.close(); + + assert.throws(() => server.close(), { + code: 'ERR_QUICSOCKET_DESTROYED', + name: 'Error', + message: 'Cannot call close after a QuicSocket has been destroyed' + }); + })); + session.on('stream', common.mustNotCall()); + + // Prematurely destroy the session without waiting for the + // handshake to complete. + session.destroy(); +})); + +server.on('ready', common.mustCall(() => { + debug('Server is listening on port %d', server.endpoints[0].address.port); + + client = createQuicSocket({ + endpoint: { port: kClientPort }, + client: { key, cert, ca, alpn: kALPN } + }); + + client.on('close', common.mustCall(() => { + debug('Client closing. Duration', client.duration); + })); + + const req = client.connect({ + address: 'localhost', + port: server.endpoints[0].address.port, + servername: kServerName, + }); + + req.on('secure', common.mustNotCall()); + req.on('close', common.mustCall()); +})); + +server.on('listening', common.mustCall()); diff --git a/test/parallel/test-quic-quicsocket-close.js b/test/parallel/test-quic-quicsocket-close.js new file mode 100644 index 00000000000000..a0bca264ba7d90 --- /dev/null +++ b/test/parallel/test-quic-quicsocket-close.js @@ -0,0 +1,19 @@ +// Flags: --no-warnings +'use strict'; + +const common = require('../common'); +if (!common.hasQuic) + common.skip('missing quic'); + +const assert = require('assert'); +const { createQuicSocket } = require('net'); + +{ + const socket = createQuicSocket(); + socket.close(common.mustCall()); + socket.on('close', common.mustCall()); + assert.throws(() => socket.close(), { + code: 'ERR_QUICSOCKET_DESTROYED', + message: 'Cannot call close after a QuicSocket has been destroyed' + }); +} diff --git a/test/parallel/test-quic-quicsocket-packetloss-stream-rx.js b/test/parallel/test-quic-quicsocket-packetloss-stream-rx.js new file mode 100644 index 00000000000000..356aa845443bca --- /dev/null +++ b/test/parallel/test-quic-quicsocket-packetloss-stream-rx.js @@ -0,0 +1,109 @@ +// Flags: --no-warnings +'use strict'; + +// Tests that stream data is successfully transmitted under +// packet loss conditions on the receiving end. + +// TODO(@jasnell): We need an equivalent test that checks +// transmission end random packet loss. + +const common = require('../common'); +if (!common.hasQuic) + common.skip('missing quic'); + +const Countdown = require('../common/countdown'); +const assert = require('assert'); +const { + key, + cert, + ca, + debug +} = require('../common/quic'); +// TODO(@jasnell): There's currently a bug in pipeline when piping +// a duplex back into to itself. +// const { pipeline } = require('stream'); + +const { createQuicSocket } = require('net'); + +const kData = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; +const options = { key, cert, ca, alpn: 'echo' }; + +const client = createQuicSocket({ client: options }); +const server = createQuicSocket({ server: options }); + +// Both client and server will drop received packets about 20% of the time +// It is important to keep in mind that this will make the runtime of the +// test non-deterministic. If we encounter flaky timeouts with this test, +// the randomized packet loss will be the reason, but random packet loss +// is exactly what is being tested. So if flaky timeouts do occur, it will +// be best to extend the failure timeout for this test. +server.setDiagnosticPacketLoss({ rx: 0.2 }); +client.setDiagnosticPacketLoss({ rx: 0.2 }); + +const countdown = new Countdown(1, () => { + debug('Countdown expired. Destroying sockets'); + server.close(); + client.close(); +}); + +server.listen(); +server.on('session', common.mustCall((session) => { + debug('QuicServerSession Created'); + + session.on('stream', common.mustCall((stream) => { + debug('Bidirectional, Client-initiated stream %d received', stream.id); + stream.on('data', (chunk) => stream.write(chunk)); + stream.on('end', () => stream.end()); + // TODO(@jasnell): There's currently a bug in pipeline when piping + // a duplex back into to itself. + // pipeline(stream, stream, common.mustCall((err) => { + // assert(!err); + // })); + })); + +})); + +server.on('ready', common.mustCall(() => { + debug('Server is listening on port %d', server.endpoints[0].address.port); + + const req = client.connect({ + address: common.localhostIPv4, + port: server.endpoints[0].address.port, + }); + + req.on('secure', common.mustCall((servername, alpn, cipher) => { + debug('QuicClientSession TLS Handshake Complete'); + + const stream = req.openStream(); + + let n = 0; + // This forces multiple stream packets to be sent out + // rather than all the data being written in a single + // packet. + function sendChunk() { + if (n < kData.length) { + stream.write(kData[n++], common.mustCall()); + setImmediate(sendChunk); + } else { + stream.end(); + } + } + sendChunk(); + + let data = ''; + stream.resume(); + stream.setEncoding('utf8'); + stream.on('data', (chunk) => data += chunk); + stream.on('end', common.mustCall(() => { + debug('Received data: %s', kData); + assert.strictEqual(data, kData); + })); + + stream.on('close', common.mustCall(() => { + debug('Bidirectional, Client-initiated stream %d closed', stream.id); + countdown.dec(); + })); + + debug('Bidirectional, Client-initiated stream %d opened', stream.id); + })); +})); diff --git a/test/parallel/test-quic-quicsocket-packetloss-stream-tx.js b/test/parallel/test-quic-quicsocket-packetloss-stream-tx.js new file mode 100644 index 00000000000000..1f16f265b10c00 --- /dev/null +++ b/test/parallel/test-quic-quicsocket-packetloss-stream-tx.js @@ -0,0 +1,109 @@ +// Flags: --no-warnings +'use strict'; + +// Tests that stream data is successfully transmitted under +// packet loss conditions on the receiving end. + +// TODO(@jasnell): We need an equivalent test that checks +// transmission end random packet loss. + +const common = require('../common'); +if (!common.hasQuic) + common.skip('missing quic'); + +const Countdown = require('../common/countdown'); +const assert = require('assert'); +const { + key, + cert, + ca, + debug +} = require('../common/quic'); +// TODO(@jasnell): There's currently a bug in pipeline when piping +// a duplex back into to itself. +// const { pipeline } = require('stream'); + +const { createQuicSocket } = require('net'); + +const kData = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; +const options = { key, cert, ca, alpn: 'echo' }; + +const client = createQuicSocket({ client: options }); +const server = createQuicSocket({ server: options }); + +// Both client and server will drop transmitted packets about 20% of the time +// It is important to keep in mind that this will make the runtime of the +// test non-deterministic. If we encounter flaky timeouts with this test, +// the randomized packet loss will be the reason, but random packet loss +// is exactly what is being tested. So if flaky timeouts do occur, it will +// be best to extend the failure timeout for this test. +server.setDiagnosticPacketLoss({ tx: 0.2 }); +client.setDiagnosticPacketLoss({ tx: 0.2 }); + +const countdown = new Countdown(1, () => { + debug('Countdown expired. Destroying sockets'); + server.close(); + client.close(); +}); + +server.listen(); +server.on('session', common.mustCall((session) => { + debug('QuicServerSession Created'); + + session.on('stream', common.mustCall((stream) => { + debug('Bidirectional, Client-initiated stream %d received', stream.id); + stream.on('data', (chunk) => stream.write(chunk)); + stream.on('end', () => stream.end()); + // TODO(@jasnell): There's currently a bug in pipeline when piping + // a duplex back into to itself. + // pipeline(stream, stream, common.mustCall((err) => { + // assert(!err); + // })); + })); + +})); + +server.on('ready', common.mustCall(() => { + debug('Server is listening on port %d', server.endpoints[0].address.port); + + const req = client.connect({ + address: common.localhostIPv4, + port: server.endpoints[0].address.port, + }); + + req.on('secure', common.mustCall((servername, alpn, cipher) => { + debug('QuicClientSession TLS Handshake Complete'); + + const stream = req.openStream(); + + let n = 0; + // This forces multiple stream packets to be sent out + // rather than all the data being written in a single + // packet. + function sendChunk() { + if (n < kData.length) { + stream.write(kData[n++], common.mustCall()); + setImmediate(sendChunk); + } else { + stream.end(); + } + } + sendChunk(); + + let data = ''; + stream.resume(); + stream.setEncoding('utf8'); + stream.on('data', (chunk) => data += chunk); + stream.on('end', common.mustCall(() => { + debug('Received data: %s', kData); + assert.strictEqual(data, kData); + })); + + stream.on('close', common.mustCall(() => { + debug('Bidirectional, Client-initiated stream %d closed', stream.id); + countdown.dec(); + })); + + debug('Bidirectional, Client-initiated stream %d opened', stream.id); + })); +})); diff --git a/test/parallel/test-quic-quicsocket-serverbusy.js b/test/parallel/test-quic-quicsocket-serverbusy.js new file mode 100644 index 00000000000000..35cc29d8f098b0 --- /dev/null +++ b/test/parallel/test-quic-quicsocket-serverbusy.js @@ -0,0 +1,62 @@ +// Flags: --expose-internals --no-warnings +'use strict'; + +// Tests QUIC server busy support + +const common = require('../common'); +if (!common.hasQuic) + common.skip('missing quic'); + +const assert = require('assert'); +const { + key, + cert, + ca, + debug, + kServerPort, + kClientPort +} = require('../common/quic'); + +const { createQuicSocket } = require('net'); +const options = { key, cert, ca, alpn: 'zzz' }; + +let client; +const server = createQuicSocket({ + endpoint: { port: kServerPort }, + server: options +}); + +server.on('busy', common.mustCall((busy) => { + assert.strictEqual(busy, true); +})); + +// When the server is set as busy, all connections +// will be rejected with a SERVER_BUSY response. +server.setServerBusy(); +server.listen(); + +server.on('close', common.mustCall()); +server.on('listening', common.mustCall()); +server.on('session', common.mustNotCall()); + +server.on('ready', common.mustCall(() => { + debug('Server is listening on port %d', server.endpoints[0].address.port); + client = createQuicSocket({ + endpoint: { port: kClientPort }, + client: options + }); + + client.on('close', common.mustCall()); + + const req = client.connect({ + address: common.localhostIPv4, + port: server.endpoints[0].address.port, + }); + + req.on('secure', common.mustNotCall()); + + req.on('close', common.mustCall(() => { + server.close(); + client.close(); + })); +})); diff --git a/test/parallel/test-quic-quicsocket.js b/test/parallel/test-quic-quicsocket.js new file mode 100644 index 00000000000000..b7c11661205c87 --- /dev/null +++ b/test/parallel/test-quic-quicsocket.js @@ -0,0 +1,154 @@ +// Flags: --no-warnings +'use strict'; + +// Test QuicSocket constructor option errors. + +const common = require('../common'); +if (!common.hasQuic) + common.skip('missing quic'); + +const assert = require('assert'); + +const { createQuicSocket } = require('net'); + +const socket = createQuicSocket(); +assert(socket); + +// Before listen is called, serverSecureContext is always undefined. +assert.strictEqual(socket.serverSecureContext, undefined); + +assert.deepStrictEqual(socket.endpoints.length, 1); + +// Socket is not bound, so address should be empty +assert.deepStrictEqual(socket.endpoints[0].address, {}); + +// Socket is not bound +assert(!socket.bound); + +// Socket is not pending +assert(!socket.pending); + +// Socket is not destroyed +assert(!socket.destroyed); + +assert.strictEqual(typeof socket.duration, 'bigint'); +assert.strictEqual(typeof socket.boundDuration, 'bigint'); +assert.strictEqual(typeof socket.listenDuration, 'bigint'); +assert.strictEqual(typeof socket.bytesReceived, 'bigint'); +assert.strictEqual(socket.bytesReceived, 0n); +assert.strictEqual(socket.bytesSent, 0n); +assert.strictEqual(socket.packetsReceived, 0n); +assert.strictEqual(socket.packetsSent, 0n); +assert.strictEqual(socket.serverSessions, 0n); +assert.strictEqual(socket.clientSessions, 0n); + +const endpoint = socket.endpoints[0]; +assert(endpoint); + +// Will throw because the QuicSocket is not bound +{ + const err = { code: 'EBADF' }; + assert.throws(() => endpoint.setTTL(1), err); + assert.throws(() => endpoint.setMulticastTTL(1), err); + assert.throws(() => endpoint.setBroadcast(), err); + assert.throws(() => endpoint.setMulticastLoopback(), err); + assert.throws(() => endpoint.setMulticastInterface('0.0.0.0'), err); + // TODO(@jasnell): Verify behavior of add/drop membership then test + // assert.throws(() => endpoint.addMembership( + // '127.0.0.1', '127.0.0.1'), err); + // assert.throws(() => endpoint.dropMembership( + // '127.0.0.1', '127.0.0.1'), err); +} + +['test', null, {}, [], 1n, false].forEach((rx) => { + assert.throws(() => socket.setDiagnosticPacketLoss({ rx }), { + code: 'ERR_INVALID_ARG_TYPE' + }); +}); + +['test', null, {}, [], 1n, false].forEach((tx) => { + assert.throws(() => socket.setDiagnosticPacketLoss({ tx }), { + code: 'ERR_INVALID_ARG_TYPE' + }); +}); + +[ + { rx: -1 }, + { rx: 1.1 }, + { tx: -1 }, + { tx: 1.1 } +].forEach((options) => { + assert.throws(() => socket.setDiagnosticPacketLoss(options), { + code: 'ERR_OUT_OF_RANGE' + }); +}); + +[1, 1n, [], {}, null].forEach((args) => { + assert.throws(() => socket.setServerBusy(args), { + code: 'ERR_INVALID_ARG_TYPE' + }); +}); + +socket.listen({ alpn: 'zzz' }); +assert(socket.pending); + +socket.on('ready', common.mustCall(() => { + assert(endpoint.bound); + + // QuicSocket is already listening. + assert.throws(() => socket.listen(), { + code: 'ERR_QUICSOCKET_LISTENING' + }); + + assert.strictEqual(typeof endpoint.address.address, 'string'); + assert.strictEqual(typeof endpoint.address.port, 'number'); + assert.strictEqual(typeof endpoint.address.family, 'string'); + + if (!common.isWindows) + assert.strictEqual(typeof endpoint.fd, 'number'); + + endpoint.setTTL(1); + endpoint.setMulticastTTL(1); + endpoint.setBroadcast(); + endpoint.setBroadcast(true); + endpoint.setBroadcast(false); + + endpoint.setMulticastLoopback(); + endpoint.setMulticastLoopback(true); + endpoint.setMulticastLoopback(false); + + endpoint.setMulticastInterface('0.0.0.0'); + + socket.setDiagnosticPacketLoss({ rx: 0.5, tx: 0.5 }); + + socket.destroy(); + assert(socket.destroyed); +})); + +socket.on('close', common.mustCall(() => { + [ + 'ref', + 'unref', + 'setTTL', + 'setMulticastTTL', + 'setBroadcast', + 'setMulticastLoopback', + 'setMulticastInterface', + 'addMembership', + 'dropMembership' + ].forEach((op) => { + assert.throws(() => endpoint[op](), { + code: 'ERR_QUICSOCKET_DESTROYED', + message: `Cannot call ${op} after a QuicSocket has been destroyed` + }); + }); + + [ + 'setServerBusy', + ].forEach((op) => { + assert.throws(() => socket[op](), { + code: 'ERR_QUICSOCKET_DESTROYED', + message: `Cannot call ${op} after a QuicSocket has been destroyed` + }); + }); +})); diff --git a/test/parallel/test-quic-quicstream-close-early.js b/test/parallel/test-quic-quicstream-close-early.js new file mode 100644 index 00000000000000..226aa9f56ff1e4 --- /dev/null +++ b/test/parallel/test-quic-quicstream-close-early.js @@ -0,0 +1,122 @@ +// Flags: --expose-internals --no-warnings +'use strict'; + +const common = require('../common'); +if (!common.hasQuic) + common.skip('missing quic'); + +const Countdown = require('../common/countdown'); +const assert = require('assert'); +const { + key, + cert, + ca, + debug, + kServerPort, + kClientPort, + setupKeylog +} = require('../common/quic'); + +const { createQuicSocket } = require('net'); + +let client; +const server = createQuicSocket({ endpoint: { port: kServerPort } }); + +const kServerName = 'agent1'; +const kALPN = 'zzz'; + +const countdown = new Countdown(2, () => { + debug('Countdown expired. Destroying sockets'); + server.close(); + client.close(); +}); + +server.listen({ key, cert, ca, alpn: kALPN }); + +server.on('session', common.mustCall((session) => { + debug('QuicServerSession Created'); + + setupKeylog(session); + + session.on('secure', common.mustCall((servername, alpn, cipher) => { + const uni = session.openStream({ halfOpen: true }); + + uni.write('hi', common.expectsError()); + + + uni.on('error', common.mustCall(() => { + assert.strictEqual(uni.aborted, true); + })); + + uni.on('data', common.mustNotCall()); + uni.on('close', common.mustCall(() => { + debug('Unidirectional, Server-initiated stream %d closed on server', + uni.id); + })); + + uni.close(3); + debug('Unidirectional, Server-initiated stream %d opened', uni.id); + })); + + session.on('stream', common.mustNotCall()); + session.on('close', common.mustCall()); +})); + +server.on('ready', common.mustCall(() => { + debug('Server is listening on port %d', server.endpoints[0].address.port); + client = createQuicSocket({ + endpoint: { port: kClientPort }, + client: { key, cert, ca, alpn: kALPN } + }); + + const req = client.connect({ + address: 'localhost', + port: server.endpoints[0].address.port, + servername: kServerName, + }); + + req.on('secure', common.mustCall((servername, alpn, cipher) => { + debug('QuicClientSession TLS Handshake Complete'); + + const stream = req.openStream(); + + stream.write('hello', common.expectsError()); + stream.write('there', common.expectsError()); + + stream.on('error', common.mustCall(() => { + assert.strictEqual(stream.aborted, true); + })); + + stream.on('end', common.mustNotCall()); + + stream.on('close', common.mustCall(() => { + countdown.dec(); + })); + + stream.close(1); + + debug('Bidirectional, Client-initiated stream %d opened', stream.id); + })); + + req.on('stream', common.mustCall((stream) => { + debug('Unidirectional, Server-initiated stream %d received', stream.id); + stream.on('abort', common.mustNotCall()); + stream.on('data', common.mustCall((chunk) => { + assert.strictEqual(chunk.toString(), 'hi'); + })); + stream.on('end', common.mustCall(() => { + debug('Unidirectional, Server-initiated stream %d ended on client', + stream.id); + })); + stream.on('close', common.mustCall(() => { + debug('Unidirectional, Server-initiated stream %d closed on client', + stream.id); + countdown.dec(); + })); + })); + + req.on('close', common.mustCall()); +})); + +server.on('listening', common.mustCall()); +server.on('close', common.mustCall()); diff --git a/test/parallel/test-quic-quicstream-destroy.js b/test/parallel/test-quic-quicstream-destroy.js new file mode 100644 index 00000000000000..16dffaa2edbe66 --- /dev/null +++ b/test/parallel/test-quic-quicstream-destroy.js @@ -0,0 +1,75 @@ +// Flags: --no-warnings +'use strict'; + +// Test that destroying a QuicStream immediately and synchronously +// after creation does not crash the process and closes the streams +// abruptly on both ends of the connection. + +const common = require('../common'); +if (!common.hasQuic) + common.skip('missing quic'); + +const assert = require('assert'); +const { + debug, + key, + cert, + ca +} = require('../common/quic'); + +const { createQuicSocket } = require('net'); + +const options = { key, cert, ca, alpn: 'zzz' }; + +const server = createQuicSocket({ server: options }); + +server.listen(); + +server.on('session', common.mustCall((session) => { + debug('QuicServerSession Created'); + + session.on('stream', common.mustCall((stream) => { + stream.destroy(); + stream.on('close', common.mustCall()); + stream.on('error', common.mustNotCall()); + assert(stream.destroyed); + })); +})); + +server.on('ready', common.mustCall(() => { + debug('Server is listening on port %d', server.endpoints[0].address.port); + + const client = createQuicSocket({ client: options }); + + client.on('close', common.mustCall(() => { + debug('Client closing. Duration', client.duration); + })); + + const req = client.connect({ + address: common.localhostIPv4, + port: server.endpoints[0].address.port + }); + + req.on('secure', common.mustCall(() => { + debug('QuicClientSession TLS Handshake Complete'); + + const stream = req.openStream(); + stream.write('foo'); + // Do not explicitly end the stream here. + + stream.on('finish', common.mustNotCall()); + stream.on('data', common.mustNotCall()); + stream.on('end', common.mustCall()); + + stream.on('close', common.mustCall(() => { + debug('Stream closed on client side'); + assert(stream.destroyed); + client.close(); + server.close(); + })); + })); + + req.on('close', common.mustCall()); +})); + +server.on('listening', common.mustCall()); diff --git a/test/parallel/test-quic-quicstream-identifiers.js b/test/parallel/test-quic-quicstream-identifiers.js new file mode 100644 index 00000000000000..cb67475312bbd3 --- /dev/null +++ b/test/parallel/test-quic-quicstream-identifiers.js @@ -0,0 +1,156 @@ +// Flags: --no-warnings +'use strict'; + +// Tests that both client and server can open +// bidirectional and unidirectional streams, +// and that the properties for each are set +// accordingly. +// +// +------+----------------------------------+ +// | ID | Stream Type | +// +------+----------------------------------+ +// | 0 | Client-Initiated, Bidirectional | +// | | | +// | 1 | Server-Initiated, Bidirectional | +// | | | +// | 2 | Client-Initiated, Unidirectional | +// | | | +// | 3 | Server-Initiated, Unidirectional | +// +------+----------------------------------+ + +const common = require('../common'); +if (!common.hasQuic) + common.skip('missing quic'); + +const Countdown = require('../common/countdown'); +const assert = require('assert'); +const { debug, key, cert } = require('../common/quic'); + +const { createQuicSocket } = require('net'); +const options = { key, cert, alpn: 'zzz' }; + +let client; +const server = createQuicSocket({ server: options }); + +const countdown = new Countdown(4, () => { + debug('Countdown expired. Closing sockets'); + server.close(); + client.close(); +}); + +const closeHandler = common.mustCall(() => countdown.dec(), 4); + +server.listen(); +server.on('session', common.mustCall((session) => { + debug('QuicServerSession created'); + session.on('secure', common.mustCall(() => { + debug('QuicServerSession TLS Handshake Completed.'); + + ([3, 1n, [], {}, null, 'meow']).forEach((halfOpen) => { + assert.throws(() => session.openStream({ halfOpen }), { + code: 'ERR_INVALID_ARG_TYPE', + }); + }); + + const uni = session.openStream({ halfOpen: true }); + uni.end('test'); + debug('Unidirectional, Server-initiated stream %d opened', uni.id); + + const bidi = session.openStream(); + bidi.end('test'); + bidi.resume(); + bidi.on('end', common.mustCall()); + debug('Bidirectional, Server-initiated stream %d opened', bidi.id); + + assert.strictEqual(uni.id, 3); + assert(uni.unidirectional); + assert(uni.serverInitiated); + assert(!uni.bidirectional); + assert(!uni.clientInitiated); + + assert.strictEqual(bidi.id, 1); + assert(bidi.bidirectional); + assert(bidi.serverInitiated); + assert(!bidi.unidirectional); + assert(!bidi.clientInitiated); + })); + + session.on('stream', common.mustCall((stream) => { + assert(stream.clientInitiated); + assert(!stream.serverInitiated); + switch (stream.id) { + case 0: + debug('Bidirectional, Client-initiated stream %d received', stream.id); + assert(stream.bidirectional); + assert(!stream.unidirectional); + stream.end('test'); + break; + case 2: + debug('Unidirectional, Client-initiated stream %d receieved', + stream.id); + assert(stream.unidirectional); + assert(!stream.bidirectional); + break; + } + stream.resume(); + stream.on('end', common.mustCall()); + }, 2)); +})); + +server.on('ready', common.mustCall(() => { + debug('Server listening on port %d', server.endpoints[0].address.port); + client = createQuicSocket({ client: options }); + const req = client.connect({ + address: common.localhostIPv4, + port: server.endpoints[0].address.port, + }); + + req.on('secure', common.mustCall(() => { + debug('QuicClientSession TLS Handshake Completed'); + const bidi = req.openStream(); + bidi.end('test'); + bidi.resume(); + bidi.on('close', closeHandler); + assert.strictEqual(bidi.id, 0); + debug('Bidirectional, Client-initiated stream %d opened', bidi.id); + + assert(bidi.clientInitiated); + assert(bidi.bidirectional); + assert(!bidi.serverInitiated); + assert(!bidi.unidirectional); + + const uni = req.openStream({ halfOpen: true }); + uni.end('test'); + uni.on('close', closeHandler); + assert.strictEqual(uni.id, 2); + debug('Unidirectional, Client-initiated stream %d opened', uni.id); + + assert(uni.clientInitiated); + assert(!uni.bidirectional); + assert(!uni.serverInitiated); + assert(uni.unidirectional); + })); + + req.on('stream', common.mustCall((stream) => { + assert(stream.serverInitiated); + assert(!stream.clientInitiated); + switch (stream.id) { + case 1: + debug('Bidirectional, Server-initiated stream %d received', stream.id); + assert(!stream.unidirectional); + assert(stream.bidirectional); + stream.end(); + break; + case 3: + debug('Unidirectional, Server-initiated stream %d received', stream.id); + assert(stream.unidirectional); + assert(!stream.bidirectional); + } + stream.resume(); + stream.on('end', common.mustCall()); + stream.on('close', closeHandler); + }, 2)); + +})); + +server.on('listening', common.mustCall()); diff --git a/test/parallel/test-quic-simple-client-migrate.js b/test/parallel/test-quic-simple-client-migrate.js new file mode 100644 index 00000000000000..b738b2f81f9d0f --- /dev/null +++ b/test/parallel/test-quic-simple-client-migrate.js @@ -0,0 +1,108 @@ +// Flags: --expose-internals --no-warnings +'use strict'; + +const common = require('../common'); +if (!common.hasQuic) + common.skip('missing quic'); + +const Countdown = require('../common/countdown'); +const assert = require('assert'); +const { + key, + cert, + ca, + debug, +} = require('../common/quic'); + +const { createQuicSocket } = require('net'); +const { pipeline } = require('stream'); + +let req; +let client; +let client2; +const server = createQuicSocket(); + +const options = { key, cert, ca, alpn: 'zzz' }; +const countdown = new Countdown(2, () => { + debug('Countdown expired. Destroying sockets'); + req.close(); + server.close(); + client2.close(); +}); + +server.listen(options); + +server.on('session', common.mustCall((session) => { + debug('QuicServerSession Created'); + + session.on('stream', common.mustCall((stream) => { + debug('Bidirectional, Client-initiated stream %d received', stream.id); + pipeline(stream, stream, common.mustCall()); + + session.openStream({ halfOpen: true }).end('Hello from the server'); + })); + +})); + +server.on('ready', common.mustCall(() => { + debug('Server is listening on port %d', server.endpoints[0].address.port); + + client = createQuicSocket({ client: options }); + client2 = createQuicSocket({ client: options }); + + req = client.connect({ + address: common.localhostIPv4, + port: server.endpoints[0].address.port, + }); + + client.on('close', common.mustCall()); + + req.on('secure', common.mustCall(() => { + debug('QuicClientSession TLS Handshake Complete'); + + let data = ''; + + const stream = req.openStream(); + debug('Bidirectional, Client-initiated stream %d opened', stream.id); + stream.setEncoding('utf8'); + stream.on('data', (chunk) => data += chunk); + stream.on('end', common.mustCall(() => { + assert.strictEqual(data, 'Hello from the client'); + debug('Client received expected data for stream %d', stream.id); + })); + stream.on('close', common.mustCall(() => { + debug('Bidirectional, Client-initiated stream %d closed', stream.id); + countdown.dec(); + })); + // Send some data on one connection... + stream.write('Hello '); + + // Wait just a bit, then migrate to a different + // QuicSocket and continue sending. + setTimeout(common.mustCall(() => { + req.setSocket(client2, (err) => { + assert(!err); + client.close(); + stream.end('from the client'); + }); + }), common.platformTimeout(100)); + })); + + req.on('stream', common.mustCall((stream) => { + debug('Unidirectional, Server-initiated stream %d received', stream.id); + let data = ''; + stream.setEncoding('utf8'); + stream.on('data', (chunk) => data += chunk); + stream.on('end', common.mustCall(() => { + assert.strictEqual(data, 'Hello from the server'); + debug('Client received expected data for stream %d', stream.id); + })); + stream.on('close', common.mustCall(() => { + debug('Unidirectional, Server-initiated stream %d closed', stream.id); + countdown.dec(); + })); + })); +})); + +server.on('listening', common.mustCall()); +server.on('close', common.mustCall()); diff --git a/test/parallel/test-quic-statelessreset.js b/test/parallel/test-quic-statelessreset.js new file mode 100644 index 00000000000000..b8d0279660c3a5 --- /dev/null +++ b/test/parallel/test-quic-statelessreset.js @@ -0,0 +1,76 @@ +// Flags: --expose-internals --no-warnings +'use strict'; + +// Testing stateless reset + +const common = require('../common'); +if (!common.hasQuic) + common.skip('missing quic'); + +const { internalBinding } = require('internal/test/binding'); +const assert = require('assert'); + +const { key, cert, ca } = require('../common/quic'); + +const { + kHandle, +} = require('internal/stream_base_commons'); +const { silentCloseSession } = internalBinding('quic'); + +const { createQuicSocket } = require('net'); + +const kStatelessResetToken = + Buffer.from('000102030405060708090A0B0C0D0E0F', 'hex'); + +let client; + +const server = createQuicSocket({ statelessResetSecret: kStatelessResetToken }); + +server.listen({ key, cert, ca, alpn: 'zzz' }); + +server.on('session', common.mustCall((session) => { + session.on('stream', common.mustCall((stream) => { + // silentCloseSession is an internal-only testing tool + // that allows us to prematurely destroy a QuicSession + // without the proper communication flow with the connected + // peer. We call this to simulate a local crash that loses + // state, which should trigger the server to send a + // stateless reset token to the client. + silentCloseSession(session[kHandle]); + })); + + session.on('close', common.mustCall()); +})); + +server.on('close', common.mustCall(() => { + // Verify stats recording + assert.strictEqual(server.statelessResetCount, 1n); +})); + +server.on('ready', common.mustCall(() => { + const endpoint = server.endpoints[0]; + + client = createQuicSocket({ client: { key, cert, ca, alpn: 'zzz' } }); + + client.on('close', common.mustCall()); + + const req = client.connect({ + address: 'localhost', + port: endpoint.address.port, + servername: 'localhost', + }); + + req.on('secure', common.mustCall(() => { + const stream = req.openStream(); + stream.end('hello'); + stream.resume(); + stream.on('close', common.mustCall()); + })); + + req.on('close', common.mustCall(() => { + assert.strictEqual(req.statelessReset, true); + server.close(); + client.close(); + })); + +})); diff --git a/test/parallel/test-quic-with-fake-udp.js b/test/parallel/test-quic-with-fake-udp.js new file mode 100644 index 00000000000000..d957488ec6d311 --- /dev/null +++ b/test/parallel/test-quic-with-fake-udp.js @@ -0,0 +1,61 @@ +// Flags: --expose-internals --no-warnings +'use strict'; +const common = require('../common'); +if (!common.hasQuic) + common.skip('missing quic'); + +// Tests that QUIC works properly when using a pair of mocked UDP ports. + +const { makeUDPPair } = require('../common/udppair'); +const assert = require('assert'); +const { createQuicSocket } = require('net'); +const { kUDPHandleForTesting } = require('internal/quic/core'); + +const { key, cert, ca } = require('../common/quic'); + +const { serverSide, clientSide } = makeUDPPair(); + +const server = createQuicSocket({ + endpoint: { [kUDPHandleForTesting]: serverSide._handle } +}); + +serverSide.afterBind(); +server.listen({ key, cert, ca, alpn: 'meow' }); + +server.on('session', common.mustCall((session) => { + session.on('secure', common.mustCall(() => { + const stream = session.openStream({ halfOpen: false }); + stream.end('Hi!'); + stream.on('data', common.mustNotCall()); + stream.on('finish', common.mustCall()); + stream.on('close', common.mustNotCall()); + stream.on('end', common.mustNotCall()); + })); + + session.on('close', common.mustNotCall()); +})); + +server.on('ready', common.mustCall(() => { + const client = createQuicSocket({ + endpoint: { [kUDPHandleForTesting]: clientSide._handle }, + client: { key, cert, ca, alpn: 'meow' } + }); + clientSide.afterBind(); + + const req = client.connect({ + address: 'localhost', + port: server.endpoints[0].address.port + }); + + req.on('stream', common.mustCall((stream) => { + stream.on('data', common.mustCall((data) => { + assert.strictEqual(data.toString(), 'Hi!'); + })); + + stream.on('end', common.mustCall()); + })); + + req.on('close', common.mustNotCall()); +})); + +server.on('close', common.mustNotCall()); diff --git a/test/pummel/test-heapdump-quic.js b/test/pummel/test-heapdump-quic.js new file mode 100644 index 00000000000000..a3e5f95166ad05 --- /dev/null +++ b/test/pummel/test-heapdump-quic.js @@ -0,0 +1,152 @@ +// Flags: --expose-internals +'use strict'; +const common = require('../common'); +if (!common.hasQuic) + common.skip('missing quic'); + +const { createQuicSocket } = require('net'); + +const { recordState } = require('../common/heap'); +const fixtures = require('../common/fixtures'); +const key = fixtures.readKey('agent1-key.pem', 'binary'); +const cert = fixtures.readKey('agent1-cert.pem', 'binary'); +const ca = fixtures.readKey('ca1-cert.pem', 'binary'); + +{ + const state = recordState(); + state.validateSnapshotNodes('Node / QuicStream', []); + state.validateSnapshotNodes('Node / QuicSession', []); + state.validateSnapshotNodes('Node / QuicSocket', []); +} + +const server = createQuicSocket({ port: 0, validateAddress: true }); + +server.listen({ + key, + cert, + ca, + rejectUnauthorized: false, + maxCryptoBuffer: 4096, + alpn: 'meow' +}); + +server.on('session', common.mustCall((session) => { + session.on('secure', common.mustCall((servername, alpn, cipher) => { + // eslint-disable-next-line no-unused-vars + const stream = session.openStream({ halfOpen: false }); + + const state = recordState(); + + state.validateSnapshotNodes('Node / QuicSocket', [ + { + children: [ + { node_name: 'QuicSocket', edge_name: 'wrapped' }, + { node_name: 'BigUint64Array', edge_name: 'stats_buffer' }, + { node_name: 'Node / sessions', edge_name: 'sessions' }, + { node_name: 'Node / dcid_to_scid', edge_name: 'dcid_to_scid' }, + ] + } + ], { loose: true }); + + state.validateSnapshotNodes('Node / QuicStream', [ + { + children: [ + { node_name: 'QuicStream', edge_name: 'wrapped' }, + { node_name: 'BigUint64Array', edge_name: 'stats_buffer' }, + { node_name: 'Node / QuicBuffer', edge_name: 'buffer' }, + { node_name: 'Node / HistogramBase', edge_name: 'data_rx_rate' }, + { node_name: 'Node / HistogramBase', edge_name: 'data_rx_size' }, + { node_name: 'Node / HistogramBase', edge_name: 'data_rx_ack' } + ] + } + ], { loose: true }); + + state.validateSnapshotNodes('Node / QuicBuffer', [ + { + children: [ + { node_name: 'Node / length', edge_name: 'length' } + ] + } + ], { loose: true }); + + state.validateSnapshotNodes('Node / QuicSession', [ + { + children: [ + { node_name: 'QuicServerSession', edge_name: 'wrapped' }, + { node_name: 'Node / QuicCryptoContext', + edge_name: 'crypto_context' }, + { node_name: 'Node / HistogramBase', edge_name: 'crypto_rx_ack' }, + { node_name: 'Node / HistogramBase', + edge_name: 'crypto_handshake_rate' }, + { node_name: 'Node / Timer', edge_name: 'retransmit' }, + { node_name: 'Node / Timer', edge_name: 'idle' }, + { node_name: 'Node / QuicBuffer', edge_name: 'sendbuf' }, + { node_name: 'Node / QuicBuffer', edge_name: 'txbuf' }, + { node_name: 'Float64Array', edge_name: 'recovery_stats_buffer' }, + { node_name: 'BigUint64Array', edge_name: 'stats_buffer' }, + { node_name: 'Node / current_ngtcp2_memory', + edge_name: 'current_ngtcp2_memory' }, + { node_name: 'Node / streams', edge_name: 'streams' }, + { node_name: 'Node / std::basic_string', edge_name: 'alpn' }, + { node_name: 'Node / std::basic_string', edge_name: 'hostname' }, + { node_name: 'Float64Array', edge_name: 'state' }, + ] + }, + { + children: [ + { node_name: 'QuicClientSession', edge_name: 'wrapped' }, + { node_name: 'Node / QuicCryptoContext', + edge_name: 'crypto_context' }, + { node_name: 'Node / HistogramBase', edge_name: 'crypto_rx_ack' }, + { node_name: 'Node / HistogramBase', + edge_name: 'crypto_handshake_rate' }, + { node_name: 'Node / Timer', edge_name: 'retransmit' }, + { node_name: 'Node / Timer', edge_name: 'idle' }, + { node_name: 'Node / QuicBuffer', edge_name: 'sendbuf' }, + { node_name: 'Node / QuicBuffer', edge_name: 'txbuf' }, + { node_name: 'Float64Array', edge_name: 'recovery_stats_buffer' }, + { node_name: 'BigUint64Array', edge_name: 'stats_buffer' }, + { node_name: 'Node / current_ngtcp2_memory', + edge_name: 'current_ngtcp2_memory' }, + { node_name: 'Node / streams', edge_name: 'streams' }, + { node_name: 'Node / std::basic_string', edge_name: 'alpn' }, + { node_name: 'Node / std::basic_string', edge_name: 'hostname' }, + { node_name: 'Float64Array', edge_name: 'state' }, + ] + } + ], { loose: true }); + + state.validateSnapshotNodes('Node / QuicCryptoContext', [ + { + children: [ + { node_name: 'Node / rx_secret', edge_name: 'rx_secret' }, + { node_name: 'Node / tx_secret', edge_name: 'tx_secret' }, + { node_name: 'Node / QuicBuffer', edge_name: 'initial_crypto' }, + { node_name: 'Node / QuicBuffer', + edge_name: 'handshake_crypto' }, + { node_name: 'Node / QuicBuffer', edge_name: 'app_crypto' }, + ] + } + ], { loose: true }); + + session.destroy(); + server.close(); + })); +})); + +server.on('ready', common.mustCall(() => { + const client = createQuicSocket({ + port: 0, + client: { + key, + cert, + ca, + alpn: 'meow' + } + }); + + client.connect({ + address: 'localhost', + port: server.address.port + }).on('close', common.mustCall(() => client.close())); +})); diff --git a/test/sequential/test-async-wrap-getasyncid.js b/test/sequential/test-async-wrap-getasyncid.js index 957c7f7440a4bc..17eb781e5d4b97 100644 --- a/test/sequential/test-async-wrap-getasyncid.js +++ b/test/sequential/test-async-wrap-getasyncid.js @@ -45,6 +45,12 @@ const { getSystemErrorName } = require('util'); delete providers.STREAMPIPE; delete providers.MESSAGEPORT; delete providers.WORKER; + // TODO(danbev): Test for these + delete providers.QUICCLIENTSESSION; + delete providers.QUICSERVERSESSION; + delete providers.QUICSENDWRAP; + delete providers.QUICSOCKET; + delete providers.QUICSTREAM; delete providers.JSUDPWRAP; if (!common.isMainThread) delete providers.INSPECTORJSBINDING; diff --git a/test/sequential/test-quic-preferred-address-ipv6.js b/test/sequential/test-quic-preferred-address-ipv6.js new file mode 100644 index 00000000000000..8d761af210bacd --- /dev/null +++ b/test/sequential/test-quic-preferred-address-ipv6.js @@ -0,0 +1,94 @@ +// Flags: --expose-internals --no-warnings +'use strict'; + +const common = require('../common'); +if (!common.hasQuic) + common.skip('missing quic'); + +const Countdown = require('../common/countdown'); +const assert = require('assert'); +const fixtures = require('../common/fixtures'); +const key = fixtures.readKey('agent1-key.pem', 'binary'); +const cert = fixtures.readKey('agent1-cert.pem', 'binary'); +const ca = fixtures.readKey('ca1-cert.pem', 'binary'); +const { debuglog } = require('util'); +const debug = debuglog('test'); + +const { createQuicSocket } = require('net'); + +let client; + +const server = createQuicSocket({ endpoint: { type: 'udp6' } }); + +const kALPN = 'zzz'; // ALPN can be overriden to whatever we want + +const countdown = new Countdown(1, () => { + debug('Countdown expired. Destroying sockets'); + server.close(); + client.close(); +}); + +server.listen({ key, cert, ca, alpn: kALPN, preferredAddress: { + port: common.PORT, + address: '::', + type: 'udp6', +} }); + +server.on('session', common.mustCall((session) => { + debug('QuicServerSession Created'); + session.on('stream', common.mustCall((stream) => { + stream.end('hello world'); + stream.resume(); + stream.on('close', common.mustCall()); + stream.on('finish', common.mustCall()); + })); +})); + +server.on('ready', common.mustCall(() => { + const endpoints = server.endpoints; + for (const endpoint of endpoints) { + const address = endpoint.address; + debug('Server is listening on address %s:%d', + address.address, + address.port); + } + const endpoint = endpoints[0]; + + client = createQuicSocket({ endpoint: { type: 'udp6' }, client: { + key, + cert, + ca, + alpn: kALPN, + preferredAddressPolicy: 'accept' } }); + + client.on('close', common.mustCall()); + + const req = client.connect({ + address: 'localhost', + port: endpoint.address.port, + servername: 'localhost', + type: 'udp6', + }); + + req.on('ready', common.mustCall(() => { + req.on('usePreferredAddress', common.mustCall(({ address, port, type }) => { + assert.strictEqual(address, '::'); + assert.strictEqual(port, common.PORT); + assert.strictEqual(type, 'udp6'); + })); + })); + + req.on('secure', common.mustCall((servername, alpn, cipher) => { + const stream = req.openStream(); + stream.end('hello world'); + stream.resume(); + + stream.on('close', common.mustCall(() => { + countdown.dec(); + })); + })); + + req.on('close', common.mustCall()); +})); + +server.on('listening', common.mustCall()); diff --git a/test/sequential/test-quic-preferred-address.js b/test/sequential/test-quic-preferred-address.js new file mode 100644 index 00000000000000..a75aa6284c304c --- /dev/null +++ b/test/sequential/test-quic-preferred-address.js @@ -0,0 +1,93 @@ +// Flags: --expose-internals --no-warnings +'use strict'; + +const common = require('../common'); +if (!common.hasQuic) + common.skip('missing quic'); + +const Countdown = require('../common/countdown'); +const assert = require('assert'); +const fixtures = require('../common/fixtures'); +const key = fixtures.readKey('agent1-key.pem', 'binary'); +const cert = fixtures.readKey('agent1-cert.pem', 'binary'); +const ca = fixtures.readKey('ca1-cert.pem', 'binary'); +const { debuglog } = require('util'); +const debug = debuglog('test'); + +const { createQuicSocket } = require('net'); + +let client; + +const server = createQuicSocket(); + +const kALPN = 'zzz'; // ALPN can be overriden to whatever we want + +const countdown = new Countdown(1, () => { + debug('Countdown expired. Destroying sockets'); + server.close(); + client.close(); +}); + +server.listen({ key, cert, ca, alpn: kALPN, preferredAddress: { + port: common.PORT, + address: '0.0.0.0', + type: 'udp4', +} }); + +server.on('session', common.mustCall((session) => { + debug('QuicServerSession Created'); + session.on('stream', common.mustCall((stream) => { + stream.end('hello world'); + stream.resume(); + stream.on('close', common.mustCall()); + stream.on('finish', common.mustCall()); + })); +})); + +server.on('ready', common.mustCall(() => { + const endpoints = server.endpoints; + for (const endpoint of endpoints) { + const address = endpoint.address; + debug('Server is listening on address %s:%d', + address.address, + address.port); + } + const endpoint = endpoints[0]; + + client = createQuicSocket({ client: { + key, + cert, + ca, + alpn: kALPN, + preferredAddressPolicy: 'accept' } }); + + client.on('close', common.mustCall()); + + const req = client.connect({ + address: 'localhost', + port: endpoint.address.port, + servername: 'localhost', + }); + + req.on('ready', common.mustCall(() => { + req.on('usePreferredAddress', common.mustCall(({ address, port, type }) => { + assert.strictEqual(address, '0.0.0.0'); + assert.strictEqual(port, common.PORT); + assert.strictEqual(type, 'udp4'); + })); + })); + + req.on('secure', common.mustCall((servername, alpn, cipher) => { + const stream = req.openStream(); + stream.end('hello world'); + stream.resume(); + + stream.on('close', common.mustCall(() => { + countdown.dec(); + })); + })); + + req.on('close', common.mustCall()); +})); + +server.on('listening', common.mustCall()); diff --git a/tools/doc/type-parser.js b/tools/doc/type-parser.js index 04659c715d3196..6441b5eef70fda 100644 --- a/tools/doc/type-parser.js +++ b/tools/doc/type-parser.js @@ -138,6 +138,10 @@ const customTypesMap = { 'perf_hooks.html#perf_hooks_class_perf_hooks_performanceobserver', 'PerformanceObserverEntryList': 'perf_hooks.html#perf_hooks_class_performanceobserverentrylist', + 'QuicEndpoint': 'quic.html#quic_class_quicendpoint', + 'QuicSession': 'quic.html#quic_class_quicserversession_extends_quicsession', + 'QuicSocket': 'quic.html#quic_net_createquicsocket_options', + 'QuicStream': 'quic.html#quic_class_quicstream_extends_stream_duplex', 'readline.Interface': 'readline.html#readline_class_interface', diff --git a/tools/license-builder.sh b/tools/license-builder.sh index 2da5a9954df0cb..2ee78b69f46f5e 100755 --- a/tools/license-builder.sh +++ b/tools/license-builder.sh @@ -88,6 +88,12 @@ addlicense "gtest" "test/cctest/gtest" "$(cat ${rootdir}/test/cctest/gtest/LICEN # nghttp2 addlicense "nghttp2" "deps/nghttp2" "$(cat ${rootdir}/deps/nghttp2/COPYING)" +# ngtcp2 +addlicense "ngtcp2" "deps/ngtcp2" "$(cat ${rootdir}/deps/ngtcp2/COPYING)" + +# nghttp3 +addlicense "nghttp3" "deps/nghttp3" "$(cat ${rootdir}/deps/nghttp3/COPYING)" + # node-inspect addlicense "node-inspect" "deps/node-inspect" "$(cat ${rootdir}/deps/node-inspect/LICENSE)" diff --git a/vcbuild.bat b/vcbuild.bat index 0cec4409d4e690..66fc132f793c97 100644 --- a/vcbuild.bat +++ b/vcbuild.bat @@ -68,6 +68,7 @@ set openssl_no_asm= set doc= set extra_msbuild_args= set exit_code=0 +set experimental_quic= :next-arg if "%1"=="" goto args-done @@ -143,6 +144,7 @@ if /i "%1"=="cctest" set cctest=1&goto arg-ok if /i "%1"=="openssl-no-asm" set openssl_no_asm=1&goto arg-ok if /i "%1"=="doc" set doc=1&goto arg-ok if /i "%1"=="binlog" set extra_msbuild_args=/binaryLogger:%config%\node.binlog&goto arg-ok +if /i "%1"=="experimental-quic" set experimental_quic=1&goto arg-ok echo Error: invalid command line option `%1`. exit /b 1 @@ -194,6 +196,7 @@ if defined config_flags set configure_flags=%configure_flags% %config_flags% if defined target_arch set configure_flags=%configure_flags% --dest-cpu=%target_arch% if defined openssl_no_asm set configure_flags=%configure_flags% --openssl-no-asm if defined DEBUG_HELPER set configure_flags=%configure_flags% --verbose +if defined experimental_quic set configure_flags=%configure_flags% --experimental-quic if "%target_arch%"=="x86" if "%PROCESSOR_ARCHITECTURE%"=="AMD64" set configure_flags=%configure_flags% --no-cross-compiling if "%target_arch%"=="arm64" set configure_flags=%configure_flags% --cross-compiling @@ -667,7 +670,7 @@ set exit_code=1 goto exit :help -echo vcbuild.bat [debug/release] [msi] [doc] [test/test-all/test-addons/test-js-native-api/test-node-api/test-benchmark/test-internet/test-pummel/test-simple/test-message/test-tick-processor/test-known-issues/test-node-inspect/test-check-deopts/test-npm/test-async-hooks/test-v8/test-v8-intl/test-v8-benchmarks/test-v8-all] [ignore-flaky] [static/dll] [noprojgen] [projgen] [small-icu/full-icu/without-intl] [nobuild] [nosnapshot] [noetw] [ltcg] [licensetf] [sign] [ia32/x86/x64/arm64] [vs2019] [download-all] [lint/lint-ci/lint-js/lint-js-ci/lint-md] [lint-md-build] [package] [build-release] [upload] [no-NODE-OPTIONS] [link-module path-to-module] [debug-http2] [debug-nghttp2] [clean] [cctest] [no-cctest] [openssl-no-asm] +echo vcbuild.bat [debug/release] [msi] [doc] [test/test-all/test-addons/test-js-native-api/test-node-api/test-benchmark/test-internet/test-pummel/test-simple/test-message/test-tick-processor/test-known-issues/test-node-inspect/test-check-deopts/test-npm/test-async-hooks/test-v8/test-v8-intl/test-v8-benchmarks/test-v8-all] [ignore-flaky] [static/dll] [noprojgen] [projgen] [small-icu/full-icu/without-intl] [nobuild] [nosnapshot] [noetw] [ltcg] [licensetf] [sign] [ia32/x86/x64/arm64] [vs2019] [download-all] [lint/lint-ci/lint-js/lint-js-ci/lint-md] [lint-md-build] [package] [build-release] [upload] [no-NODE-OPTIONS] [link-module path-to-module] [debug-http2] [debug-nghttp2] [clean] [cctest] [no-cctest] [openssl-no-asm] [experimental-quic] echo Examples: echo vcbuild.bat : builds release build echo vcbuild.bat debug : builds debug build