From c6ce65640d2cd8d083bc504ba79389324333c2dc Mon Sep 17 00:00:00 2001 From: James M Snell Date: Tue, 7 Jul 2020 11:30:04 -0700 Subject: [PATCH 01/16] quic: additional minor cleanups in node_quic_session.h --- src/quic/node_quic_session.h | 39 +++++++++++++++++++----------------- 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/src/quic/node_quic_session.h b/src/quic/node_quic_session.h index bac8bcd1b9a1a9..daa189d40d291c 100644 --- a/src/quic/node_quic_session.h +++ b/src/quic/node_quic_session.h @@ -65,9 +65,9 @@ typedef void(*PreferredAddressStrategy)( // 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 { +class QuicSessionConfig final : public ngtcp2_settings { public: - QuicSessionConfig() {} + QuicSessionConfig() = default; explicit QuicSessionConfig(QuicState* quic_state) { Set(quic_state); @@ -226,8 +226,8 @@ struct QuicSessionStatsTraits { static void ToString(const Base& ptr, Fn&& add_field); }; -class QLogStream : public AsyncWrap, - public StreamBase { +class QLogStream final : public AsyncWrap, + public StreamBase { public: static BaseObjectPtr Create(Environment* env); @@ -311,7 +311,7 @@ class QuicSessionListener { friend class QuicSession; }; -class JSQuicSessionListener : public QuicSessionListener { +class JSQuicSessionListener final : public QuicSessionListener { public: void OnKeylog(const char* str, size_t size) override; void OnClientHello( @@ -363,7 +363,7 @@ class JSQuicSessionListener : public QuicSessionListener { // The QuicCryptoContext class encapsulates all of the crypto/TLS // handshake details on behalf of a QuicSession. -class QuicCryptoContext : public MemoryRetainer { +class QuicCryptoContext final : public MemoryRetainer { public: inline QuicCryptoContext( QuicSession* session, @@ -697,12 +697,7 @@ class QuicApplication : public MemoryRetainer, V(SILENT_CLOSE, silent_closing) \ V(STATELESS_RESET, stateless_reset) -// 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 +// QUIC sessions are logical 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, @@ -713,9 +708,11 @@ class QuicApplication : public MemoryRetainer, // 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 { +class QuicSession final : public AsyncWrap, + public mem::NgLibMemoryManager< + QuicSession, + ngtcp2_mem>, + public StatsBase { public: // The default preferred address strategy is to ignore it static void IgnorePreferredAddressStrategy( @@ -1080,13 +1077,15 @@ class QuicSession : public AsyncWrap, // within the context of an ngtcp2 callback. When within an ngtcp2 // callback, SendPendingData will always be called when the callbacks // complete. - class SendSessionScope { + class SendSessionScope final { public: explicit SendSessionScope(QuicSession* session) : session_(session) { CHECK(session_); } + SendSessionScope(const SendSessionScope& other) = delete; + ~SendSessionScope() { if (Ngtcp2CallbackScope::InNgtcp2CallbackScope(session_.get()) || session_->is_in_closing_period() || @@ -1103,7 +1102,7 @@ class QuicSession : public AsyncWrap, // ConnectionCloseScope triggers sending a CONNECTION_CLOSE // when not executing within the context of an ngtcp2 callback // and the session is in the correct state. - class ConnectionCloseScope { + class ConnectionCloseScope final { public: ConnectionCloseScope(QuicSession* session, bool silent = false) : session_(session), @@ -1116,6 +1115,8 @@ class QuicSession : public AsyncWrap, session_->set_in_connection_close_scope(); } + ConnectionCloseScope(const ConnectionCloseScope& other) = delete; + ~ConnectionCloseScope() { if (silent_ || Ngtcp2CallbackScope::InNgtcp2CallbackScope(session_.get()) || @@ -1135,7 +1136,7 @@ class QuicSession : public AsyncWrap, // 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 { + class Ngtcp2CallbackScope final { public: explicit Ngtcp2CallbackScope(QuicSession* session) : session_(session) { CHECK(session_); @@ -1143,6 +1144,8 @@ class QuicSession : public AsyncWrap, session_->set_in_ngtcp2_callback(); } + Ngtcp2CallbackScope(const Ngtcp2CallbackScope& other) = delete; + ~Ngtcp2CallbackScope() { session_->set_in_ngtcp2_callback(false); } From 51b7e642f76b9396e7b452aae76a8635ba8ab2cd Mon Sep 17 00:00:00 2001 From: James M Snell Date: Tue, 7 Jul 2020 11:35:24 -0700 Subject: [PATCH 02/16] quic: remove unnecessary bool conversion The argument will always be a boolean already --- lib/internal/quic/core.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/internal/quic/core.js b/lib/internal/quic/core.js index f559e040febd92..8c06d69494279d 100644 --- a/lib/internal/quic/core.js +++ b/lib/internal/quic/core.js @@ -267,7 +267,7 @@ function onSocketClose(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); + this[owner_symbol][kServerBusy](on); } // Called by the C++ internals when a new server QuicSession has been created. From ebc8f5e5825922fe67b9fdc2b0467858276f6446 Mon Sep 17 00:00:00 2001 From: James M Snell Date: Tue, 7 Jul 2020 12:47:58 -0700 Subject: [PATCH 03/16] quic: handle errors thrown / rejections in the session event Errors thrown within the session event handler will be handled by destroying the session (allowing a proper connection close to be sent to the client peer). They will not crash the parent QuicSocket by default. Instead, a `'sessionError'` event will be emitted, allowing the error to be logged or handled. --- doc/api/quic.md | 52 ++++++++++++++++- lib/internal/quic/core.js | 23 +++++++- test/parallel/test-quic-client-server.js | 1 + ...t-quic-server-session-event-error-async.js | 58 +++++++++++++++++++ .../test-quic-server-session-event-error.js | 58 +++++++++++++++++++ 5 files changed, 190 insertions(+), 2 deletions(-) create mode 100644 test/parallel/test-quic-server-session-event-error-async.js create mode 100644 test/parallel/test-quic-server-session-event-error.js diff --git a/doc/api/quic.md b/doc/api/quic.md index 3667a05b394956..6b0f9ae41bb12e 100644 --- a/doc/api/quic.md +++ b/doc/api/quic.md @@ -1385,10 +1385,60 @@ The `'ready'` event will not be emitted multiple times. added: REPLACEME --> -Emitted when a new `QuicServerSession` has been created. +Emitted when a new `QuicServerSession` has been created. The callback is +invoked with a single argument providing the newly created `QuicServerSession` +object. + +```js +const { createQuicSocket } = require('net'); + +const options = getOptionsSomehow(); +const server = createQuicSocket({ server: options }); +server.listen(); + +server.on('session', (session) => { + // Attach session event listeners. +}); +``` The `'session'` event will be emitted multiple times. +The `'session'` event handler *may* be an async function. + +If the `'session'` event handler throws an error, or if it returns a `Promise` +that is rejected, the error will be handled by destroying the `QuicServerSession` +automatically and emitting a `'sessionError'` event on the `QuicSocket`. + +#### Event: `'sessionError'` + + +Emitted when an error occurs processing an event related to a specific +`QuicSession` instance. The callback is invoked with two arguments: + +* `error` {Error} The error that was either thrown or rejected. +* `session` {QuicSession} The `QuicSession` instance that was destroyed. + +The `QuicSession` instance will have been destroyed by the time the +`'sessionError'` event is emitted. + +```js +const { createQuicSocket } = require('net'); + +const options = getOptionsSomehow(); +const server = createQuicSocket({ server: options }); +server.listen(); + +server.on('session', (session) => { + throw new Error('boom'); +}); + +server.on('sessionError', (error, session) => { + console.log('error:', error.message); +}); +``` + #### quicsocket.addEndpoint(options) 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, +`quicSocket.serverBusy = true | false`. The callback is invoked with a +single boolean argument indicating `true` if busy status is enabled, `false` otherwise. This event is strictly informational. ```js @@ -1346,8 +1346,8 @@ socket.on('busy', (busy) => { console.log('Server is not busy'); }); -socket.setServerBusy(true); -socket.setServerBusy(false); +socket.serverBusy = true; +socket.serverBusy = false; ``` This `'busy'` event may be emitted multiple times. @@ -1874,6 +1874,18 @@ Set to `true` if the socket is not yet bound to the local UDP port. added: REPLACEME --> +#### quicsocket.serverBusy + + +* Type: {boolean} When `true`, the `QuicSocket` will reject new connections. + +Setting `quicsocket.serverBusy` to `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 setting +`quicsocket.serverBusy = false`. + #### quicsocket.serverBusyCount - -* `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 + +Emitted after `quicsocket.listen()` is called and the `QuicSocket` has started +listening for incoming `QuicServerSession`s. The callback is invoked with +no arguments. + +The `'listening'` event will not be emitted multiple times. + #### Event: `'ready'` 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. +method, and can be used as both a client and a server. #### Event: `'busy'` -* Type: {bigint} +* Type: {number} -A `BigInt` representing the length of time this `QuicSocket` has been bound -to a local port. +The length of time this `QuicSocket` has been bound to a local port. + +Read-only. #### quicsocket.bytesReceived -* Type: {bigint} +* Type: {number} -A `BigInt` representing the number of bytes received by this `QuicSocket`. +The number of bytes received by this `QuicSocket`. + +Read-only. #### quicsocket.bytesSent -* Type: {bigint} +* Type: {number} + +The number of bytes sent by this `QuicSocket`. -A `BigInt` representing the number of bytes sent by this `QuicSocket`. +Read-only. #### quicsocket.clientSessions -* Type: {bigint} +* Type: {number} + +The number of client `QuicSession` instances that have been associated +with this `QuicSocket`. -A `BigInt` representing the number of client `QuicSession` instances that -have been associated with this `QuicSocket`. +Read-only. #### quicsocket.close(\[callback\]) -* Type: {bigint} +* Type: {number} + +The length of time this `QuicSocket` has been active, -A `BigInt` representing the length of time this `QuicSocket` has been active, +Read-only. #### quicsocket.endpoints -* Type: {bigint} +* Type: {number} -A `BigInt` representing the length of time this `QuicSocket` has been listening -for connections. +The length of time this `QuicSocket` has been listening for connections. + +Read-only #### quicsocket.listening -* Type: {bigint} +* Type: {number} -A `BigInt` representing the number of packets received by this `QuicSocket` that -have been ignored. +The number of packets received by this `QuicSocket` that have been ignored. + +Read-only. #### quicsocket.packetsReceived -* Type: {bigint} +* Type: {number} + +The number of packets successfully received by this `QuicSocket`. -A `BigInt` representing the number of packets successfully received by this -`QuicSocket`. +Read-only #### quicsocket.packetsSent -* Type: {bigint} +* Type: {number} + +The number of packets sent by this `QuicSocket`. -A `BigInt` representing the number of packets sent by this `QuicSocket`. +Read-only #### quicsocket.pending -* Type: {bigint} +* Type: {number} + +The number of `QuicSession` instances rejected due to server busy status. -A `BigInt` representing the number of `QuicSession` instances rejected -due to server busy status. +Read-only. #### quicsocket.serverSessions -* Type: {bigint} +* Type: {number} -A `BigInt` representing the number of server `QuicSession` instances that -have been associated with this `QuicSocket`. +The number of server `QuicSession` instances that have been associated with +this `QuicSocket`. + +Read-only. #### quicsocket.setDiagnosticPacketLoss(options) -* Type: {bigint} +* Type: {number} + +The number of stateless resets that have been sent. -A `BigInt` that represents the number of stateless resets that have been sent. +Read-only. #### quicsocket.toggleStatelessReset() Emitted when the server busy state has been toggled using -`quicSocket.serverBusy = true | false`. The callback is invoked with a -single boolean argument indicating `true` if busy status is enabled, -`false` otherwise. This event is strictly informational. +`quicSocket.serverBusy = true | false`. The callback is invoked with no +arguments. Use the `quicsocket.serverBusy` property to determine the +current status. This event is strictly informational. ```js const { createQuicSocket } = require('net'); const socket = createQuicSocket(); -socket.on('busy', (busy) => { - if (busy) +socket.on('busy', () => { + if (socket.serverBusy) console.log('Server is busy'); else console.log('Server is not busy'); diff --git a/lib/internal/quic/core.js b/lib/internal/quic/core.js index a80fed262db1e2..9a1322a06db9d9 100644 --- a/lib/internal/quic/core.js +++ b/lib/internal/quic/core.js @@ -12,6 +12,7 @@ assertCrypto(); const { Array, BigInt64Array, + Boolean, Error, Map, Number, @@ -38,6 +39,7 @@ const { validateQuicEndpointOptions, validateCreateSecureContextOptions, validateQuicSocketConnectOptions, + QuicSocketSharedState, QuicSessionSharedState, QLogStream, } = require('internal/quic/util'); @@ -269,8 +271,8 @@ function onSocketClose(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); +function onSocketServerBusy() { + this[owner_symbol][kServerBusy](); } // Called by the C++ internals when a new server QuicSession has been created. @@ -845,19 +847,17 @@ class QuicEndpoint { class QuicSocket extends EventEmitter { [kInternalState] = { alpn: undefined, - autoClose: undefined, client: undefined, defaultEncoding: undefined, endpoints: new Set(), highWaterMark: undefined, + listenPending: false, lookup: undefined, server: undefined, - serverBusy: false, - serverListening: false, serverSecureContext: undefined, sessions: new Set(), state: kSocketUnbound, - statelessResetEnabled: true, + sharedState: undefined, stats: undefined, }; @@ -865,11 +865,6 @@ class QuicSocket extends EventEmitter { 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, @@ -913,7 +908,6 @@ class QuicSocket extends EventEmitter { const state = this[kInternalState]; - state.autoClose = autoClose; state.client = client; state.lookup = lookup || (type === AF_INET6 ? lookup6 : lookup4); state.server = server; @@ -976,6 +970,10 @@ class QuicSocket extends EventEmitter { if (handle !== undefined) { handle[owner_symbol] = this; this[async_id_symbol] = handle.getAsyncId(); + this[kInternalState].sharedState = + new QuicSocketSharedState(handle.state); + } else { + this[kInternalState].sharedState = undefined; } } @@ -1081,16 +1079,13 @@ class QuicSocket extends EventEmitter { } // Called by the C++ internals to notify when server busy status is toggled. - [kServerBusy](on) { - this[kInternalState].serverBusy = on; - // In a nextTick because the event ends up being - // emitted synchronously when quicSocket.serverBusy - // is called. + [kServerBusy]() { + const busy = this.serverBusy; process.nextTick(() => { try { - this.emit('busy', on); + this.emit('busy', busy); } catch (error) { - this[kRejections](error, 'busy', on); + this[kRejections](error, 'busy', busy); } }); } @@ -1161,6 +1156,7 @@ class QuicSocket extends EventEmitter { // server and will emit session events whenever a new QuicServerSession // is created. const state = this[kInternalState]; + state.listenPending = false; this[kHandle].listen( state.serverSecureContext.context, address, @@ -1225,14 +1221,12 @@ class QuicSocket extends EventEmitter { // function. listen(options, callback) { const state = this[kInternalState]; - if (state.serverListening) - throw new ERR_QUICSOCKET_LISTENING(); - if (state.state === kSocketDestroyed || state.state === kSocketClosing) { throw new ERR_QUICSOCKET_DESTROYED('listen'); } - + if (this.listening || state.listenPending) + throw new ERR_QUICSOCKET_LISTENING(); if (typeof options === 'function') { callback = options; options = {}; @@ -1265,8 +1259,8 @@ class QuicSocket extends EventEmitter { state.highWaterMark = highWaterMark; state.defaultEncoding = defaultEncoding; - state.serverListening = true; state.alpn = alpn; + state.listenPending = true; // If the callback function is provided, it is registered as a // handler for the on('session') event and will be called whenever @@ -1403,10 +1397,8 @@ class QuicSocket extends EventEmitter { // listening for new QuicServerSession connections. // New initial connection packets for currently unknown // DCID's will be ignored. - if (this[kHandle]) { - this[kHandle].stopListening(); - } - state.serverListening = false; + if (this[kHandle]) + this[kInternalState].sharedState.serverListening = false; // If there are no sessions, calling maybeDestroy // will immediately and synchronously destroy the @@ -1502,7 +1494,7 @@ class QuicSocket extends EventEmitter { // True if listen() has been called successfully get listening() { - return this[kInternalState].serverListening; + return Boolean(this[kInternalState].sharedState?.serverListening); } // True if the QuicSocket is currently waiting on at least one @@ -1518,12 +1510,27 @@ class QuicSocket extends EventEmitter { if (state.state === kSocketDestroyed) throw new ERR_QUICSOCKET_DESTROYED('serverBusy'); validateBoolean(on, 'on'); - if (state.serverBusy !== on) - this[kHandle].setServerBusy(on); + if (state.sharedState.serverBusy !== on) { + state.sharedState.serverBusy = on; + this[kServerBusy](); + } } get serverBusy() { - return this[kInternalState].serverBusy; + return Boolean(this[kInternalState].sharedState?.serverBusy); + } + + set statelessResetDisabled(on) { + const state = this[kInternalState]; + if (state.state === kSocketDestroyed) + throw new ERR_QUICSOCKET_DESTROYED('statelessResetDisabled'); + validateBoolean(on, 'on'); + if (state.sharedState.statelessResetDisabled !== on) + state.sharedState.statelessResetDisabled = on; + } + + get statelessResetDisabled() { + return Boolean(this[kInternalState].sharedState?.statelessResetDisabled); } get duration() { @@ -1613,21 +1620,6 @@ class QuicSocket extends EventEmitter { } this[kHandle].setDiagnosticPacketLoss(rx, tx); } - - get statelessResetEnabled() { - return this[kInternalState].statelessResetEnabled; - } - - set statelessResetEnabled(on) { - const state = this[kInternalState]; - if (state.state === kSocketDestroyed) - throw new ERR_QUICSOCKET_DESTROYED('serverBusy'); - validateBoolean(on, 'on'); - if (state.statelessResetEnabled !== on) { - this[kHandle].enableStatelessReset(on); - state.statelessResetEnabled = on; - } - } } class QuicSession extends EventEmitter { diff --git a/lib/internal/quic/util.js b/lib/internal/quic/util.js index 143ee4dd8cef80..a7763c9bf3c15a 100644 --- a/lib/internal/quic/util.js +++ b/lib/internal/quic/util.js @@ -82,6 +82,10 @@ const { IDX_QUICSESSION_STATE_MAX_DATA_LEFT, IDX_QUICSESSION_STATE_BYTES_IN_FLIGHT, + IDX_QUICSOCKET_STATE_SERVER_LISTENING, + IDX_QUICSOCKET_STATE_SERVER_BUSY, + IDX_QUICSOCKET_STATE_STATELESS_RESET_DISABLED, + IDX_HTTP3_QPACK_MAX_TABLE_CAPACITY, IDX_HTTP3_QPACK_BLOCKED_STREAMS, IDX_HTTP3_MAX_HEADER_LIST_SIZE, @@ -514,7 +518,6 @@ function validateQuicSocketOptions(options = {}) { validateObject(options, 'options'); const { - autoClose = false, client = {}, disableStatelessReset = false, endpoint = { port: 0, type: 'udp4' }, @@ -538,7 +541,6 @@ function validateQuicSocketOptions(options = {}) { validateLookup(lookup); validateBoolean(validateAddress, 'options.validateAddress'); validateBoolean(validateAddressLRU, 'options.validateAddressLRU'); - validateBoolean(autoClose, 'options.autoClose'); validateBoolean(qlog, 'options.qlog'); validateBoolean(disableStatelessReset, 'options.disableStatelessReset'); @@ -576,7 +578,6 @@ function validateQuicSocketOptions(options = {}) { return { endpoint, - autoClose, client, lookup, maxConnections, @@ -803,6 +804,39 @@ function toggleListeners(state, event, on) { } } +class QuicSocketSharedState { + constructor(state) { + this[kHandle] = Buffer.from(state); + } + + get serverListening() { + return Boolean(this[kHandle] + .readUInt8(IDX_QUICSOCKET_STATE_SERVER_LISTENING)); + } + + set serverListening(on) { + this[kHandle].writeUInt8(on ? 1 : 0, IDX_QUICSOCKET_STATE_SERVER_LISTENING); + } + + get serverBusy() { + return Boolean(this[kHandle].readUInt8(IDX_QUICSOCKET_STATE_SERVER_BUSY)); + } + + set serverBusy(on) { + this[kHandle].writeUInt8(on ? 1 : 0, IDX_QUICSOCKET_STATE_SERVER_BUSY); + } + + get statelessResetDisabled() { + return Boolean(this[kHandle] + .readUInt8(IDX_QUICSOCKET_STATE_STATELESS_RESET_DISABLED)); + } + + set statelessResetDisabled(on) { + this[kHandle].writeUInt8(on ? 1 : 0, + IDX_QUICSOCKET_STATE_STATELESS_RESET_DISABLED); + } +} + // A utility class used to handle reading / modifying shared JS/C++ // state associated with a QuicSession class QuicSessionSharedState { @@ -938,6 +972,7 @@ module.exports = { validateQuicEndpointOptions, validateCreateSecureContextOptions, validateQuicSocketConnectOptions, + QuicSocketSharedState, QuicSessionSharedState, QLogStream, }; diff --git a/src/quic/node_quic.cc b/src/quic/node_quic.cc index a651c2f951ffdf..a7baf1aa6df559 100644 --- a/src/quic/node_quic.cc +++ b/src/quic/node_quic.cc @@ -205,6 +205,11 @@ void Initialize(Local target, QUICSESSION_SHARED_STATE(V) #undef V +#define V(id, _, __) \ + NODE_DEFINE_CONSTANT(constants, IDX_QUICSOCKET_STATE_##id); + QUICSOCKET_SHARED_STATE(V) +#undef V + #define V(name, _, __) \ NODE_DEFINE_CONSTANT(constants, IDX_QUIC_SESSION_STATS_##name); SESSION_STATS(V) diff --git a/src/quic/node_quic_socket-inl.h b/src/quic/node_quic_socket-inl.h index 2d269ef87fe248..4d334a19f5b665 100644 --- a/src/quic/node_quic_socket-inl.h +++ b/src/quic/node_quic_socket-inl.h @@ -90,20 +90,6 @@ void QuicSocket::DisassociateStatelessResetToken( 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_server_listening()) { - Debug(this, "Stop listening"); - set_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); @@ -157,8 +143,8 @@ size_t QuicSocket::GetCurrentStatelessResetCounter(const SocketAddress& addr) { void QuicSocket::ServerBusy(bool on) { Debug(this, "Turning Server Busy Response %s", on ? "on" : "off"); - set_server_busy(on); - listener_->OnServerBusy(on); + state_->server_busy = on ? 1 : 0; + listener_->OnServerBusy(); } bool QuicSocket::is_diagnostic_packet_loss(double prob) const { @@ -173,11 +159,6 @@ void QuicSocket::set_diagnostic_packet_loss(double rx, double tx) { tx_loss_ = tx; } -bool QuicSocket::EnableStatelessReset(bool on) { - set_stateless_reset_disabled(!on); - return !is_stateless_reset_disabled(); -} - void QuicSocket::set_validated_address(const SocketAddress& addr) { if (has_option_validate_address_lru()) { // Remove the oldest item if we've hit the LRU limit @@ -215,7 +196,7 @@ void QuicSocket::AddEndpoint( if (preferred || endpoints_.empty()) preferred_endpoint_ = endpoint_; endpoints_.emplace_back(endpoint_); - if (is_server_listening()) + if (state_->server_listening) endpoint_->ReceiveStart(); } diff --git a/src/quic/node_quic_socket.cc b/src/quic/node_quic_socket.cc index 49743ebf9a9d7e..8c6fc253dad6dc 100644 --- a/src/quic/node_quic_socket.cc +++ b/src/quic/node_quic_socket.cc @@ -1,4 +1,5 @@ #include "node_quic_socket-inl.h" // NOLINT(build/include) +#include "aliased_struct-inl.h" #include "allocated_buffer-inl.h" #include "async_wrap-inl.h" #include "debug_utils-inl.h" @@ -38,6 +39,7 @@ using v8::Local; using v8::Number; using v8::Object; using v8::ObjectTemplate; +using v8::PropertyAttribute; using v8::String; using v8::Value; @@ -115,9 +117,9 @@ void QuicSocketListener::OnSessionReady(BaseObjectPtr session) { previous_listener_->OnSessionReady(session); } -void QuicSocketListener::OnServerBusy(bool busy) { +void QuicSocketListener::OnServerBusy() { if (previous_listener_ != nullptr) - previous_listener_->OnServerBusy(busy); + previous_listener_->OnServerBusy(); } void QuicSocketListener::OnEndpointDone(QuicEndpoint* endpoint) { @@ -145,12 +147,12 @@ void JSQuicSocketListener::OnSessionReady(BaseObjectPtr session) { socket()->MakeCallback(env->quic_on_session_ready_function(), 1, &arg); } -void JSQuicSocketListener::OnServerBusy(bool busy) { +void JSQuicSocketListener::OnServerBusy() { 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); + socket()->MakeCallback( + env->quic_on_socket_server_busy_function(), 0, nullptr); } void JSQuicSocketListener::OnEndpointDone(QuicEndpoint* endpoint) { @@ -252,6 +254,7 @@ QuicSocket::QuicSocket( StatsBase(quic_state->env(), wrap), alloc_info_(MakeAllocator()), options_(options), + state_(quic_state->env()->isolate()), max_connections_(max_connections), max_connections_per_host_(max_connections_per_host), max_stateless_resets_per_host_(max_stateless_resets_per_host), @@ -266,8 +269,14 @@ QuicSocket::QuicSocket( EntropySource(token_secret_, kTokenSecretLen); + wrap->DefineOwnProperty( + env()->context(), + env()->state_string(), + state_.GetArrayBuffer(), + PropertyAttribute::ReadOnly).Check(); + if (disable_stateless_reset) - set_stateless_reset_disabled(); + state_->stateless_reset_disabled = 1; // Set the session reset secret to the one provided or random. // Note that a random secret is going to make it exceedingly @@ -316,13 +325,13 @@ void QuicSocket::Listen( const std::string& alpn, uint32_t options) { CHECK(sc); - CHECK(!is_server_listening()); + CHECK_NE(state_->server_listening, 1); Debug(this, "Starting to listen"); server_session_config_.Set(quic_state(), preferred_address); server_secure_context_ = sc; server_alpn_ = alpn; server_options_ = options; - set_server_listening(); + state_->server_listening = 1; RecordTimestamp(&QuicSocketStats::listen_at); ReceiveStart(); } @@ -391,7 +400,7 @@ bool QuicSocket::MaybeStatelessReset( const SocketAddress& local_addr, const SocketAddress& remote_addr, unsigned int flags) { - if (UNLIKELY(is_stateless_reset_disabled() || nread < 16)) + if (UNLIKELY(state_->stateless_reset_disabled || nread < 16)) return false; StatelessResetToken possible_token( data + nread - NGTCP2_STATELESS_RESET_TOKENLEN); @@ -620,7 +629,7 @@ bool QuicSocket::SendStatelessReset( const SocketAddress& local_addr, const SocketAddress& remote_addr, size_t source_len) { - if (UNLIKELY(is_stateless_reset_disabled())) + if (UNLIKELY(state_->stateless_reset_disabled)) return false; constexpr static size_t kRandlen = NGTCP2_MIN_STATELESS_RESET_RANDLEN * 5; constexpr static size_t kMinStatelessResetLen = 41; @@ -737,7 +746,7 @@ BaseObjectPtr QuicSocket::AcceptInitialPacket( QuicCID ocid; // If the QuicSocket is not listening, the paket will be ignored. - if (!is_server_listening()) { + if (!state_->server_listening) { Debug(this, "QuicSocket is not listening"); return {}; } @@ -762,7 +771,7 @@ BaseObjectPtr QuicSocket::AcceptInitialPacket( // 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_server_busy()) || + if (UNLIKELY(state_->server_busy == 1) || sessions_.size() >= max_connections_ || GetCurrentSocketAddressCounter(remote_addr) >= max_connections_per_host_) { @@ -1115,26 +1124,6 @@ void QuicSocketListen(const FunctionCallbackInfo& args) { options); } -void QuicSocketStopListening(const FunctionCallbackInfo& args) { - QuicSocket* socket; - ASSIGN_OR_RETURN_UNWRAP(&socket, args.Holder()); - socket->StopListening(); -} - -void QuicSocketSetServerBusy(const FunctionCallbackInfo& args) { - QuicSocket* socket; - ASSIGN_OR_RETURN_UNWRAP(&socket, args.Holder()); - CHECK_EQ(args.Length(), 1); - socket->ServerBusy(args[0]->IsTrue()); -} - -void QuicSocketEnableStatelessReset(const FunctionCallbackInfo& args) { - QuicSocket* socket; - ASSIGN_OR_RETURN_UNWRAP(&socket, args.Holder()); - CHECK_EQ(args.Length(), 1); - args.GetReturnValue().Set(socket->EnableStatelessReset(args[0]->IsTrue())); -} - void QuicEndpointWaitForPendingCallbacks( const FunctionCallbackInfo& args) { QuicEndpoint* endpoint; @@ -1191,15 +1180,6 @@ void QuicSocket::Initialize( env->SetProtoMethod(socket, "setDiagnosticPacketLoss", QuicSocketSetDiagnosticPacketLoss); - env->SetProtoMethod(socket, - "setServerBusy", - QuicSocketSetServerBusy); - env->SetProtoMethod(socket, - "stopListening", - QuicSocketStopListening); - env->SetProtoMethod(socket, - "enableStatelessReset", - QuicSocketEnableStatelessReset); socket->Inherit(HandleWrap::GetConstructorTemplate(env)); target->Set(context, class_name, socket->GetFunction(env->context()).ToLocalChecked()).FromJust(); diff --git a/src/quic/node_quic_socket.h b/src/quic/node_quic_socket.h index 1fe9a04da0584b..41092c344c8f16 100644 --- a/src/quic/node_quic_socket.h +++ b/src/quic/node_quic_socket.h @@ -3,6 +3,7 @@ #if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS +#include "aliased_struct.h" #include "base_object.h" #include "node.h" #include "node_crypto.h" @@ -47,12 +48,24 @@ enum QuicSocketOptions : uint32_t { }; #undef V -#define QUICSOCKET_FLAGS(V) \ - V(GRACEFUL_CLOSE, graceful_closing) \ - V(WAITING_FOR_CALLBACKS, waiting_for_callbacks) \ - V(SERVER_LISTENING, server_listening) \ - V(SERVER_BUSY, server_busy) \ - V(DISABLE_STATELESS_RESET, stateless_reset_disabled) +#define QUICSOCKET_SHARED_STATE(V) \ + V(SERVER_LISTENING, server_listening, uint8_t) \ + V(SERVER_BUSY, server_busy, uint8_t) \ + V(STATELESS_RESET_DISABLED, stateless_reset_disabled, uint8_t) + +#define V(_, name, type) type name; +struct QuicSocketState { + QUICSOCKET_SHARED_STATE(V) +}; +#undef V + +#define V(id, name, _) \ + IDX_QUICSOCKET_STATE_##id = offsetof(QuicSocketState, name), +enum QuicSocketStateFields { + QUICSOCKET_SHARED_STATE(V) + IDX_QUICSOCKET_STATE_END +}; +#undef V #define SOCKET_STATS(V) \ V(CREATED_AT, created_at, "Created At") \ @@ -98,7 +111,7 @@ class QuicSocketListener { virtual void OnError(ssize_t code); virtual void OnSessionReady(BaseObjectPtr session); - virtual void OnServerBusy(bool busy); + virtual void OnServerBusy(); virtual void OnEndpointDone(QuicEndpoint* endpoint); virtual void OnDestroy(); @@ -114,7 +127,7 @@ class JSQuicSocketListener final : public QuicSocketListener { public: void OnError(ssize_t code) override; void OnSessionReady(BaseObjectPtr session) override; - void OnServerBusy(bool busy) override; + void OnServerBusy() override; void OnEndpointDone(QuicEndpoint* endpoint) override; void OnDestroy() override; }; @@ -357,35 +370,19 @@ class QuicSocket : public AsyncWrap, std::unique_ptr packet, BaseObjectPtr session = BaseObjectPtr()); -#define V(id, name) \ - inline bool is_##name() const { \ - return flags_ & (1 << QUICSOCKET_FLAG_##id); } \ - inline void set_##name(bool on = true) { \ - if (on) \ - flags_ |= (1 << QUICSOCKET_FLAG_##id); \ - else \ - flags_ &= ~(1 << QUICSOCKET_FLAG_##id); \ - } - QUICSOCKET_FLAGS(V) -#undef V - #define V(id, name) \ bool has_option_##name() const { \ return options_ & (1 << QUICSOCKET_OPTIONS_##id); } QUICSOCKET_OPTIONS(V) #undef V + // Allows the server busy status to be enabled from C++. A notification + // will be sent to the JavaScript side informing that the status has + // changed. inline void ServerBusy(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 EnableStatelessReset(bool on = true); - BaseObjectPtr server_secure_context() const { return server_secure_context_; } @@ -513,13 +510,6 @@ class QuicSocket : public AsyncWrap, // and the current packet should be artificially considered lost. inline bool is_diagnostic_packet_loss(double prob) const; -#define V(id, _) QUICSOCKET_FLAG_##id, - enum QuicSocketFlags : uint32_t { - QUICSOCKET_FLAGS(V) - QUICSOCKET_FLAG_COUNT - }; -#undef V - ngtcp2_mem alloc_info_; std::vector> endpoints_; @@ -530,6 +520,8 @@ class QuicSocket : public AsyncWrap, uint32_t options_ = 0; uint32_t server_options_; + AliasedStruct state_; + size_t max_connections_ = DEFAULT_MAX_CONNECTIONS; size_t max_connections_per_host_ = DEFAULT_MAX_CONNECTIONS_PER_HOST; size_t current_ngtcp2_memory_ = 0; diff --git a/test/parallel/test-quic-errors-quicsocket-create.js b/test/parallel/test-quic-errors-quicsocket-create.js index 57d72e5e6dbd75..64c27a50f2701a 100644 --- a/test/parallel/test-quic-errors-quicsocket-create.js +++ b/test/parallel/test-quic-errors-quicsocket-create.js @@ -82,13 +82,6 @@ const { createQuicSocket } = require('net'); }); }); -// 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 }), { From 14250d7886cbad250945573bb1158c24f4e158b7 Mon Sep 17 00:00:00 2001 From: James M Snell Date: Wed, 8 Jul 2020 09:40:23 -0700 Subject: [PATCH 12/16] quic: proper custom inspect for QuicEndpoint --- lib/internal/quic/core.js | 14 +++++++------- lib/internal/quic/util.js | 15 +++++++++++++++ 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/lib/internal/quic/core.js b/lib/internal/quic/core.js index 9a1322a06db9d9..0b0629d37745e0 100644 --- a/lib/internal/quic/core.js +++ b/lib/internal/quic/core.js @@ -24,6 +24,7 @@ const { const { Buffer } = require('buffer'); const { isArrayBufferView } = require('internal/util/types'); const { + customInspect, getAllowUnauthorized, getSocketType, lookup4, @@ -638,14 +639,12 @@ class QuicEndpoint { socket[kHandle].addEndpoint(handle, preferred); } - [kInspect]() { - // TODO(@jasnell): Proper custom inspect implementation - const obj = { + [kInspect](depth, options) { + return customInspect(this, { address: this.address, - fd: this[kInternalState].fd, + fd: this.fd, type: this[kInternalState].type === AF_INET6 ? 'udp6' : 'udp4' - }; - return `QuicEndpoint ${util.format(obj)}`; + }, depth, options); } // afterLookup is invoked when binding a QuicEndpoint. The first @@ -741,7 +740,8 @@ class QuicEndpoint { } get fd() { - return this[kInternalState].fd; + return this[kInternalState].fd >= 0 ? + this[kInternalState].fd : undefined; } // True if the QuicEndpoint has been destroyed and is diff --git a/lib/internal/quic/util.js b/lib/internal/quic/util.js index a7763c9bf3c15a..aff4f3cbc86afb 100644 --- a/lib/internal/quic/util.js +++ b/lib/internal/quic/util.js @@ -23,6 +23,8 @@ const { }, } = require('internal/async_hooks'); +const { inspect } = require('internal/util/inspect'); + const { Readable } = require('stream'); const { kHandle, @@ -955,7 +957,20 @@ class QLogStream extends Readable { } } +function customInspect(self, obj, depth, options) { + if (depth < 0) + return self; + + const opts = { + ...options, + depth: options.depth == null ? null : options.depth - 1 + }; + + return `${self.constructor.name} ${inspect(obj, opts)}`; +} + module.exports = { + customInspect, getAllowUnauthorized, getSocketType, lookup4, From 89132be0ae2fd835fb50ecd2592d93f8ae29c526 Mon Sep 17 00:00:00 2001 From: James M Snell Date: Wed, 8 Jul 2020 09:50:57 -0700 Subject: [PATCH 13/16] quic: proper custom inspect for QuicSocket --- lib/internal/quic/core.js | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/lib/internal/quic/core.js b/lib/internal/quic/core.js index 0b0629d37745e0..2c3ab4b888ef41 100644 --- a/lib/internal/quic/core.js +++ b/lib/internal/quic/core.js @@ -977,14 +977,18 @@ class QuicSocket extends EventEmitter { } } - [kInspect]() { - // TODO(@jasnell): Proper custom inspect implementation - const state = this[kInternalState]; - const obj = { + [kInspect](depth, options) { + return customInspect(this, { endpoints: this.endpoints, - sessions: state.sessions, - }; - return `QuicSocket ${util.format(obj)}`; + sessions: this.sessions, + bound: this.bound, + pending: this.pending, + closing: this.closing, + destroyed: this.destroyed, + listening: this.listening, + serverBusy: this.serverBusy, + statelessResetDisabled: this.statelessResetDisabled, + }, depth, options); } [kAddSession](session) { From 748deb5eb1f53863b1a429785ac4bf43a2b21a07 Mon Sep 17 00:00:00 2001 From: James M Snell Date: Wed, 8 Jul 2020 09:56:21 -0700 Subject: [PATCH 14/16] quic: proper custom inspect for QuicSession --- lib/internal/quic/core.js | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/lib/internal/quic/core.js b/lib/internal/quic/core.js index 2c3ab4b888ef41..f23894b7ec60ab 100644 --- a/lib/internal/quic/core.js +++ b/lib/internal/quic/core.js @@ -1777,10 +1777,9 @@ class QuicSession extends EventEmitter { stream[kStreamReset](code); } - [kInspect]() { - // TODO(@jasnell): A proper custom inspect implementation + [kInspect](depth, options) { const state = this[kInternalState]; - const obj = { + return customInspect(this, { alpn: state.alpn, cipher: this.cipher, closing: this.closing, @@ -1790,12 +1789,7 @@ class QuicSession extends EventEmitter { maxStreams: this.maxStreams, servername: this.servername, streams: state.streams.size, - stats: { - handshakeAck: this.handshakeAckHistogram, - handshakeContinuation: this.handshakeContinuationHistogram, - } - }; - return `${this.constructor.name} ${util.format(obj)}`; + }, depth, options); } [kSetSocket](socket) { From 34da6ddc8669d8b292994954a4d313b79f8f90de Mon Sep 17 00:00:00 2001 From: James M Snell Date: Wed, 8 Jul 2020 09:58:47 -0700 Subject: [PATCH 15/16] quic: proper custom inspect for QuicStream --- lib/internal/quic/core.js | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/lib/internal/quic/core.js b/lib/internal/quic/core.js index f23894b7ec60ab..9b3d0f85da4817 100644 --- a/lib/internal/quic/core.js +++ b/lib/internal/quic/core.js @@ -44,7 +44,6 @@ const { QuicSessionSharedState, QLogStream, } = require('internal/quic/util'); -const util = require('util'); const assert = require('internal/assert'); const EventEmitter = require('events'); const fs = require('fs'); @@ -2713,23 +2712,17 @@ class QuicStream extends Duplex { // TODO(@jasnell): Implement this } - [kInspect]() { + [kInspect](depth, options) { // TODO(@jasnell): Proper custom inspect implementation const direction = this.bidirectional ? 'bidirectional' : 'unidirectional'; const initiated = this.serverInitiated ? 'server' : 'client'; - const obj = { + return customInspect(this, { id: this[kInternalState].id, direction, initiated, writableState: this._writableState, - readableState: this._readableState, - stats: { - dataRate: this.dataRateHistogram, - dataSize: this.dataSizeHistogram, - dataAck: this.dataAckHistogram, - } - }; - return `QuicStream ${util.format(obj)}`; + readableState: this._readableState + }, depth, options); } [kTrackWriteState](stream, bytes) { From 90fd223dba8ed6469b320332fbee6d52d70013c1 Mon Sep 17 00:00:00 2001 From: James M Snell Date: Wed, 8 Jul 2020 11:41:19 -0700 Subject: [PATCH 16/16] quic: remove no longer valid CHECK --- src/quic/node_quic_session.cc | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/quic/node_quic_session.cc b/src/quic/node_quic_session.cc index 09f563c42b275c..b47321e222e9c1 100644 --- a/src/quic/node_quic_session.cc +++ b/src/quic/node_quic_session.cc @@ -1682,11 +1682,6 @@ void QuicSession::Close(int close_flags) { listener()->OnSessionClose(error, close_flags); else Destroy(); - - // At this point, the QuicSession should have been destroyed, indicating - // that all cleanup on the JavaScript side has completed and the - // QuicSession::Destroy() method has been called. - CHECK(is_destroyed()); } // Mark the QuicSession instance destroyed. This will either be invoked