From b7a598a3cd3c2915b0bd4fc8e675c2411912bfce Mon Sep 17 00:00:00 2001 From: James M Snell Date: Wed, 27 Dec 2023 14:03:38 -0800 Subject: [PATCH 1/8] quic: various additional cleanups, fixes in Endpoint --- src/quic/application.cc | 25 ++-- src/quic/bindingdata.h | 6 + src/quic/endpoint.cc | 254 +++++++++++++++++++++++----------------- src/quic/endpoint.h | 9 ++ src/quic/quic.cc | 3 + src/quic/session.cc | 200 +++++++++++++++++-------------- src/quic/session.h | 34 ++++-- 7 files changed, 323 insertions(+), 208 deletions(-) diff --git a/src/quic/application.cc b/src/quic/application.cc index 3ae6c8f2efef0d..eabd34137cdc76 100644 --- a/src/quic/application.cc +++ b/src/quic/application.cc @@ -34,11 +34,26 @@ struct Session::Application::StreamData final { BaseObjectPtr stream; }; +// ============================================================================ +// Session::Application_Options const Session::Application_Options Session::Application_Options::kDefault = {}; +Session::Application_Options::operator const nghttp3_settings() const { + // In theory, Application_Options might contain options for more than just + // HTTP/3. Here we extract only the properties that are relevant to HTTP/3. + return nghttp3_settings { + max_field_section_size, + qpack_max_dtable_capacity, + qpack_encoder_max_dtable_capacity, + qpack_blocked_streams, + enable_connect_protocol, + enable_datagrams, + }; +} + Maybe Session::Application_Options::From( Environment* env, Local value) { - if (value.IsEmpty()) { + if (value.IsEmpty() || (!value->IsUndefined() && !value->IsObject())) { THROW_ERR_INVALID_ARG_TYPE(env, "options must be an object"); return Nothing(); } @@ -49,11 +64,6 @@ Maybe Session::Application_Options::From( return Just(options); } - if (!value->IsObject()) { - THROW_ERR_INVALID_ARG_TYPE(env, "options must be an object"); - return Nothing(); - } - auto params = value.As(); #define SET(name) \ @@ -63,7 +73,8 @@ Maybe Session::Application_Options::From( if (!SET(max_header_pairs) || !SET(max_header_length) || !SET(max_field_section_size) || !SET(qpack_max_dtable_capacity) || - !SET(qpack_encoder_max_dtable_capacity) || !SET(qpack_blocked_streams)) { + !SET(qpack_encoder_max_dtable_capacity) || !SET(qpack_blocked_streams) || + !SET(enable_connect_protocol) || !SET(enable_datagrams)) { return Nothing(); } diff --git a/src/quic/bindingdata.h b/src/quic/bindingdata.h index 53622cf2d36eba..8c16d7df778c67 100644 --- a/src/quic/bindingdata.h +++ b/src/quic/bindingdata.h @@ -127,11 +127,14 @@ constexpr size_t kMaxVectorCount = 16; V(cubic, "cubic") \ V(disable_active_migration, "disableActiveMigration") \ V(disable_stateless_reset, "disableStatelessReset") \ + V(enable_connect_protocol, "enableConnectProtocol") \ + V(enable_datagrams, "enableDatagrams") \ V(enable_tls_trace, "tlsTrace") \ V(endpoint, "Endpoint") \ V(endpoint_udp, "Endpoint::UDP") \ V(failure, "failure") \ V(groups, "groups") \ + V(handshake_timeout, "handshakeTimeout") \ V(hostname, "hostname") \ V(http3_alpn, &NGHTTP3_ALPN_H3[1]) \ V(initial_max_data, "initialMaxData") \ @@ -156,7 +159,10 @@ constexpr size_t kMaxVectorCount = 16; V(max_payload_size, "maxPayloadSize") \ V(max_retries, "maxRetries") \ V(max_stateless_resets, "maxStatelessResetsPerHost") \ + V(max_stream_window, "maxStreamWindow") \ + V(max_window, "maxWindow") \ V(min_version, "minVersion") \ + V(no_udp_payload_size_shaping, "noUdpPayloadSizeShaping") \ V(packetwrap, "PacketWrap") \ V(preferred_address_strategy, "preferredAddressPolicy") \ V(qlog, "qlog") \ diff --git a/src/quic/endpoint.cc b/src/quic/endpoint.cc index 93154c1fe29883..81b4453866f4b5 100644 --- a/src/quic/endpoint.cc +++ b/src/quic/endpoint.cc @@ -253,6 +253,8 @@ Maybe Endpoint::Options::From(Environment* env, !SET(max_retries) || !SET(max_payload_size) || !SET(unacknowledged_packet_threshold) || !SET(validate_address) || !SET(disable_stateless_reset) || !SET(ipv6_only) || + !SET(handshake_timeout) || !SET(max_stream_window) || + !SET(max_window) || !SET(no_udp_payload_size_shaping) || #ifdef DEBUG !SET(rx_loss) || !SET(tx_loss) || #endif @@ -639,10 +641,10 @@ void Endpoint::AddSession(const CID& cid, BaseObjectPtr session) { IncrementSocketAddressCounter(session->remote_address()); if (session->is_server()) { STAT_INCREMENT(Stats, server_sessions); + EmitNewSession(session); } else { STAT_INCREMENT(Stats, client_sessions); } - if (session->is_server()) EmitNewSession(session); } void Endpoint::RemoveSession(const CID& cid) { @@ -801,6 +803,9 @@ void Endpoint::SendImmediateConnectionClose(const PathDescriptor& options, bool Endpoint::Start() { if (is_closed() || is_closing()) return false; + + // state_->receiving indicates that we're accepting inbound packets. It + // could be for server or client side, or both. if (state_->receiving == 1) return true; int err = 0; @@ -841,20 +846,25 @@ BaseObjectPtr Endpoint::Connect( // If starting fails, the endpoint will be destroyed. if (!Start()) return BaseObjectPtr(); - auto config = Session::Config( + Session::Config config( *this, options, local_address(), remote_address, session_ticket); - auto session = Session::Create(BaseObjectPtr(this), config); + auto session = Session::Create(this, config); if (!session) return BaseObjectPtr(); session->set_wrapped(); + // Calling SendPendingData here triggers the session to send the initial + // handshake packets starting the connection. session->application().SendPendingData(); - return BaseObjectPtr(); + return session; } void Endpoint::MaybeDestroy() { if (!is_closed() && sessions_.empty() && state_->pending_callbacks == 0 && state_->listening == 0) { + // Destroy potentially creates v8 handles so let's make sure + // we have a HandleScope on the stack. + HandleScope scope(env()->isolate()); Destroy(); } } @@ -903,28 +913,30 @@ void Endpoint::CloseGracefully() { void Endpoint::Receive(const uv_buf_t& buf, const SocketAddress& remote_address) { - const auto receive = [&](Store&& store, + const auto receive = [&](Session* session, + Store&& store, const SocketAddress& local_address, const SocketAddress& remote_address, const CID& dcid, const CID& scid) { - STAT_INCREMENT_N(Stats, bytes_received, store.length()); - auto session = FindSession(dcid); - return session && !session->is_destroyed() - ? session->Receive( - std::move(store), local_address, remote_address) - : false; + DCHECK_NOT_NULL(session); + size_t len = store.length(); + if (session->Receive(std::move(store), local_address, remote_address)) { + STAT_INCREMENT_N(Stats, bytes_received, len); + STAT_INCREMENT(Stats, packets_received); + } }; const auto accept = [&](const Session::Config& config, Store&& store) { - if (is_closed() || is_closing() || !is_listening()) return false; - - auto session = Session::Create(BaseObjectPtr(this), config); - - return session ? session->Receive(std::move(store), - config.local_address, - config.remote_address) - : false; + // One final check. If the endpoint is closed, closing, or is not listening as + // a server, then we cannot accept the initial packet. + if (is_closed() || is_closing() || !is_listening()) return; + + auto session = Session::Create(this, config); + if (session) { + receive(session.get(), std::move(store), config.local_address, + config.remote_address, config.dcid, config.scid); + } }; const auto acceptInitialPacket = [&](const uint32_t version, @@ -935,80 +947,88 @@ void Endpoint::Receive(const uv_buf_t& buf, const SocketAddress& remote_address) { // Conditionally accept an initial packet to create a new session. - // If we're not listening, do not accept. - if (state_->listening == 0) return false; + // If we're not listening as a server, do not accept an initial packet. + if (state_->listening == 0) return; ngtcp2_pkt_hd hd; // This is our first condition check... A minimal check to see if ngtcp2 can // even recognize this packet as a quic packet with the correct version. ngtcp2_vec vec = store; - switch (ngtcp2_accept(&hd, vec.base, vec.len)) { - case 1: - // The requested QUIC protocol version is not supported - SendVersionNegotiation( - PathDescriptor{version, dcid, scid, local_address, remote_address}); - // The packet was successfully processed, even if we did refuse the - // connection and send a version negotiation in response. - return true; - case -1: - // The packet is invalid and we're just going to ignore it. - return false; + if (ngtcp2_accept(&hd, vec.base, vec.len) != NGTCP2_SUCCESS) { + // Per the ngtcp2 docs, ngtcp2_accept returns 0 if the check was successful, + // or an error code if it was not. Currently there's only one documented error + // code (NGTCP2_ERR_INVALID_ARGUMENT) but we'll handle any error here the same -- + // by ignoring the packet entirely. + return; } - // This is the second condition check... If the server has been marked busy + // If ngtcp2_is_supported_version returns a non-zero value, the version is + // recognized and supported. If it returns 0, we'll go ahead and send a + // version negotiation packet in response. + if (ngtcp2_is_supported_version(hd.version) == 0) { + SendVersionNegotiation( + PathDescriptor{version, dcid, scid, local_address, remote_address}); + STAT_INCREMENT(Stats, packets_received); + return; + } + + // This is the next important condition check... If the server has been marked busy // or the remote peer has exceeded their maximum number of concurrent // connections, any new connections will be shut down immediately. - const auto limits_exceeded = [&] { + const auto limits_exceeded = ([&] { if (sessions_.size() >= options_.max_connections_total) return true; SocketAddressInfoTraits::Type* counts = addrLRU_.Peek(remote_address); auto count = counts != nullptr ? counts->active_connections : 0; return count >= options_.max_connections_per_host; - }; + })(); - if (state_->busy || limits_exceeded()) { + if (state_->busy || limits_exceeded) { // Endpoint is busy or the connection count is exceeded. The connection is - // refused. + // refused. For the purpose of stats collection, we'll count both of these + // the same. if (state_->busy) STAT_INCREMENT(Stats, server_busy_count); SendImmediateConnectionClose( PathDescriptor{version, scid, dcid, local_address, remote_address}, QuicError::ForTransport(NGTCP2_CONNECTION_REFUSED)); // The packet was successfully processed, even if we did refuse the // connection. - return true; + STAT_INCREMENT(Stats, packets_received); + return; } // At this point, we start to set up the configuration for our local - // session. The second argument to the Config constructor here is the dcid. - // We pass the received scid here as the value because that is the value - // *this* session will use as it's outbound dcid. - auto config = Session::Config(Side::SERVER, - *this, - server_options_.value(), - version, - local_address, - remote_address, - scid, - dcid); + // session. We pass the received scid here as the dcid argument value + // because that is the value *this* session will use as the outbound dcid. + Session::Config config(Side::SERVER, + *this, + server_options_.value(), + version, + local_address, + remote_address, + scid, + dcid); // The this point, the config.scid and config.dcid represent *our* views of // the CIDs. Specifically, config.dcid identifies the peer and config.scid // identifies us. config.dcid should equal scid. config.scid should *not* // equal dcid. + DCHECK(config.dcid == scid); + DCHECK(config.scid != dcid); - const auto is_remote_address_validated = [&] { + const auto is_remote_address_validated = ([&] { auto info = addrLRU_.Peek(remote_address); return info != nullptr ? info->validated : false; - }; + })(); // 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 // 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 + // a new token. If it does exist, and if it is valid, we grab the original // cid and continue. - if (!is_remote_address_validated()) { + if (!is_remote_address_validated) { switch (hd.type) { case NGTCP2_PKT_INITIAL: // First, let's see if we need to do anything here. @@ -1025,7 +1045,8 @@ void Endpoint::Receive(const uv_buf_t& buf, }); // We still consider this a successfully handled packet even // if we send a retry. - return true; + STAT_INCREMENT(Stats, packets_received); + return; } // We have two kinds of tokens, each prefixed with a different magic @@ -1039,7 +1060,7 @@ void Endpoint::Receive(const uv_buf_t& buf, dcid, options_.token_secret, options_.retry_token_expiration * NGTCP2_SECONDS); - if (ocid == std::nullopt) { + if (!ocid.has_value()) { // Invalid retry token was detected. Close the connection. SendImmediateConnectionClose( PathDescriptor{ @@ -1047,7 +1068,8 @@ void Endpoint::Receive(const uv_buf_t& buf, QuicError::ForTransport(NGTCP2_CONNECTION_REFUSED)); // We still consider this a successfully handled packet even // if we send a connection close. - return true; + STAT_INCREMENT(Stats, packets_received); + return; } // The ocid is the original dcid that was encoded into the @@ -1055,6 +1077,7 @@ void Endpoint::Receive(const uv_buf_t& buf, // validation. config.ocid = ocid.value(); config.retry_scid = dcid; + config.set_token(token); break; } case RegularToken::kTokenMagic: { @@ -1064,6 +1087,9 @@ void Endpoint::Receive(const uv_buf_t& buf, remote_address, options_.token_secret, options_.token_expiration * NGTCP2_SECONDS)) { + // If the regular token is invalid, let's send a retry to be lenient. + // There's a small risk that a malicious peer is trying to make us do + // some work but the risk is fairly low here. SendRetry(PathDescriptor{ version, dcid, @@ -1073,13 +1099,16 @@ void Endpoint::Receive(const uv_buf_t& buf, }); // We still consider this to be a successfully handled packet // if a retry is sent. - return true; + STAT_INCREMENT(Stats, packets_received); + return; } - hd.token = nullptr; - hd.tokenlen = 0; + config.set_token(token); break; } default: { + // If our prefix bit does not match anything we know about, let's send + // a retry to be lenient. There's a small risk that a malicious peer is + // trying to make us do some work but the risk is fairly low here. SendRetry(PathDescriptor{ version, dcid, @@ -1087,7 +1116,8 @@ void Endpoint::Receive(const uv_buf_t& buf, local_address, remote_address, }); - return true; + STAT_INCREMENT(Stats, packets_received); + return; } } @@ -1102,7 +1132,7 @@ void Endpoint::Receive(const uv_buf_t& buf, // wouldn't have sent it unless validation was turned on. Let's // assume the peer is buggy or malicious and drop the packet on the // floor. - return false; + return; } break; case NGTCP2_PKT_0RTT: @@ -1124,11 +1154,12 @@ void Endpoint::Receive(const uv_buf_t& buf, local_address, remote_address, }); - return true; + STAT_INCREMENT(Stats, packets_received); + return; } } - return accept(config, std::move(store)); + accept(config, std::move(store)); }; // When a received packet contains a QUIC short header but cannot be matched @@ -1147,24 +1178,36 @@ void Endpoint::Receive(const uv_buf_t& buf, Store& store, const SocketAddress& local_address, const SocketAddress& remote_address) { + // Support for stateless resets can be disabled by the application. If that + // case, or if the packet is too short to contain a reset token, then we skip + // the remaining checks. if (options_.disable_stateless_reset || - store.length() < NGTCP2_STATELESS_RESET_TOKENLEN) + store.length() < NGTCP2_STATELESS_RESET_TOKENLEN) { return false; + } + // The stateless reset token itself is the *final* NGTCP2_STATELESS_RESET_TOKENLEN + // bytes in the received packet. If it is a stateless reset then then rest of the + // bytes in the packet are garbage that we'll ignore. ngtcp2_vec vec = store; - vec.base += vec.len; - vec.base -= NGTCP2_STATELESS_RESET_TOKENLEN; + vec.base += (vec.len - NGTCP2_STATELESS_RESET_TOKENLEN); - Session* session = nullptr; + // If a Session has been associated with the token, then it is a valid + // stateless reset token. We need to dispatch it to the session to be + // processed. auto it = token_map_.find(StatelessResetToken(vec.base)); - if (it != token_map_.end()) session = it->second; - - return session != nullptr ? receive(std::move(store), - local_address, - remote_address, - dcid, - scid) - : false; + if (it != token_map_.end()) { + receive(it->second, + std::move(store), + local_address, + remote_address, + dcid, + scid); + return true; + } + + // Otherwise, it's not a valid stateless reset token. + return false; }; #ifdef DEBUG @@ -1182,9 +1225,16 @@ void Endpoint::Receive(const uv_buf_t& buf, // return; // } + // The managed buffer here contains the received packet. We do not yet know + // at this point if it is a valid QUIC packet. We need to do some basic checks. + // It is critical at this point that we do as little work as possible to avoid + // a DOS vector. std::shared_ptr backing = env()->release_managed_buffer(buf); - if (UNLIKELY(!backing)) + if (UNLIKELY(!backing)) { + // At this point something bad happened and we need to treat this as a fatal + // case. There's likely no way to test this specific condition reliably. return Destroy(CloseContext::RECEIVE_FAILURE, UV_ENOMEM); + } Store store(backing, buf.len, 0); @@ -1201,13 +1251,12 @@ void Endpoint::Receive(const uv_buf_t& buf, return; // Ignore the packet! } - // 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 (pversion_cid.dcidlen > NGTCP2_MAX_CIDLEN || - pversion_cid.scidlen > NGTCP2_MAX_CIDLEN) + // QUIC currently requires CID lengths of max NGTCP2_MAX_CIDLEN. Ignore any packet + // with a non-standard CID length. + if (UNLIKELY(pversion_cid.dcidlen > NGTCP2_MAX_CIDLEN || + pversion_cid.scidlen > NGTCP2_MAX_CIDLEN)) { return; // Ignore the packet! + } // Each QUIC peer has two CIDs: The Source Connection ID (or scid), and the // Destination Connection ID (or dcid). For each peer, the dcid is the CID @@ -1227,13 +1276,13 @@ void Endpoint::Receive(const uv_buf_t& buf, // impossible) that this randomly selected dcid will be in our index. If we do // happen to have a collision, as unlikely as it is, ngtcp2 will do the right // thing when it tries to process the packet so we really don't have to worry - // about it here. If the dcid is not known, the listener here will be nullptr. + // about it here. If the dcid is not known, the session here will be nullptr. // - // When the session is established, this peer will create it's own scid and - // will send that back to the remote peer to use as it's new dcid on + // When the session is established, this peer will create its own scid and + // will send that back to the remote peer to use as the new dcid on // subsequent packets. When that session is added, we will index it by the // local scid, so as long as the client sends the subsequent packets with the - // right dcid, everything will just work. + // right dcid, everything should just work. auto session = FindSession(dcid); auto addr = local_address(); @@ -1252,36 +1301,31 @@ void Endpoint::Receive(const uv_buf_t& buf, // stateless reset, the packet will be handled with no additional action // necessary here. We want to return immediately without committing any // further resources. - if (!scid && maybeStatelessReset(dcid, scid, store, addr, remote_address)) + if (!scid && maybeStatelessReset(dcid, scid, store, addr, remote_address)) { return; // Stateless reset! Don't do any further processing. - - if (acceptInitialPacket(pversion_cid.version, - dcid, - scid, - std::move(store), - addr, - remote_address)) { - // Packet was successfully received. - STAT_INCREMENT(Stats, packets_received); } - return; + + // Process the packet as an initial packet... + return acceptInitialPacket(pversion_cid.version, + dcid, + scid, + std::move(store), + addr, + remote_address); } // If we got here, the dcid matched the scid of a known local session. Yay! - if (receive(std::move(store), addr, remote_address, dcid, scid)) - STAT_INCREMENT(Stats, packets_received); + // The session will take over any further processing of the packet. + receive(session.get(), std::move(store), addr, remote_address, dcid, scid); } void Endpoint::PacketDone(int status) { if (is_closed()) return; + // At this point we should be waiting on at least one packet. + DCHECK_GE(state_->pending_callbacks, 1); state_->pending_callbacks--; // Can we go ahead and close now? - if (state_->closing == 1) { - // MaybeDestroy potentially creates v8 handles so let's make sure - // we have a HandleScope on the stack. - HandleScope scope(env()->isolate()); - MaybeDestroy(); - } + if (state_->closing == 1) MaybeDestroy(); } void Endpoint::IncrementSocketAddressCounter(const SocketAddress& addr) { diff --git a/src/quic/endpoint.h b/src/quic/endpoint.h index 04f8f5ae50f082..8694c0eb44a644 100644 --- a/src/quic/endpoint.h +++ b/src/quic/endpoint.h @@ -106,6 +106,15 @@ class Endpoint final : public AsyncWrap, public Packet::Listener { // changed if you have a really good reason for doing so. uint64_t unacknowledged_packet_threshold = 0; + // The amount of time (in milliseconds) that the endpoint will wait for the + // completion of the tls handshake. + uint64_t handshake_timeout = UINT64_MAX; + + uint64_t max_stream_window = 0; + uint64_t max_window = 0; + + bool no_udp_payload_size_shaping = true; + // The validate_address parameter instructs the Endpoint to perform explicit // address validation using retry tokens. This is strongly recommended and // should only be disabled in trusted, closed environments as a performance diff --git a/src/quic/quic.cc b/src/quic/quic.cc index 17eacb9b5f4034..51d0deab151a09 100644 --- a/src/quic/quic.cc +++ b/src/quic/quic.cc @@ -23,6 +23,7 @@ namespace quic { void CreatePerIsolateProperties(IsolateData* isolate_data, Local target) { Endpoint::InitPerIsolate(isolate_data, target); + Session::InitPerIsolate(isolate_data, target); } void CreatePerContextProperties(Local target, @@ -32,11 +33,13 @@ void CreatePerContextProperties(Local target, Realm* realm = Realm::GetCurrent(context); BindingData::InitPerContext(realm, target); Endpoint::InitPerContext(realm, target); + Session::InitPerContext(realm, target); } void RegisterExternalReferences(ExternalReferenceRegistry* registry) { BindingData::RegisterExternalReferences(registry); Endpoint::RegisterExternalReferences(registry); + Session::RegisterExternalReferences(registry); } } // namespace quic diff --git a/src/quic/session.cc b/src/quic/session.cc index 271fdc89152c57..061ef10424a04b 100644 --- a/src/quic/session.cc +++ b/src/quic/session.cc @@ -1,3 +1,5 @@ +#include +#include "quic/tokens.h" #if HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC #include "session.h" @@ -93,18 +95,9 @@ namespace quic { V(MAX_BYTES_IN_FLIGHT, max_bytes_in_flight) \ V(BYTES_IN_FLIGHT, bytes_in_flight) \ V(BLOCK_COUNT, block_count) \ - V(CONGESTION_RECOVERY_START_TS, congestion_recovery_start_ts) \ V(CWND, cwnd) \ - V(DELIVERY_RATE_SEC, delivery_rate_sec) \ - V(FIRST_RTT_SAMPLE_TS, first_rtt_sample_ts) \ - V(INITIAL_RTT, initial_rtt) \ - V(LAST_TX_PKT_TS, last_tx_pkt_ts) \ V(LATEST_RTT, latest_rtt) \ - V(LOSS_DETECTION_TIMER, loss_detection_timer) \ - V(LOSS_TIME, loss_time) \ - V(MAX_UDP_PAYLOAD_SIZE, max_udp_payload_size) \ V(MIN_RTT, min_rtt) \ - V(PTO_COUNT, pto_count) \ V(RTTVAR, rttvar) \ V(SMOOTHED_RTT, smoothed_rtt) \ V(SSTHRESH, ssthresh) \ @@ -277,7 +270,6 @@ bool SetOption(Environment* env, } // namespace // ============================================================================ - Session::Config::Config(Side side, const Endpoint& endpoint, const Options& options, @@ -300,6 +292,12 @@ Session::Config::Config(Side side, ngtcp2_settings_default(&settings); settings.initial_ts = uv_hrtime(); + // We currently do not support Path MTU Discovery. Once we do, unset this. + settings.no_pmtud = 1; + + settings.tokenlen = 0; + settings.token = nullptr; + if (options.qlog) { settings.qlog_write = on_qlog_write; } @@ -311,6 +309,10 @@ Session::Config::Config(Side side, // We pull parts of the settings for the session from the endpoint options. auto& config = endpoint.options(); + settings.no_tx_udp_payload_size_shaping = config.no_udp_payload_size_shaping; + settings.handshake_timeout = config.handshake_timeout; + settings.max_stream_window = config.max_stream_window; + settings.max_window = config.max_window; settings.cc_algo = config.cc_algorithm; settings.max_tx_udp_payload_size = config.max_payload_size; if (config.unacknowledged_packet_threshold > 0) { @@ -347,6 +349,22 @@ void Session::Config::MemoryInfo(MemoryTracker* tracker) const { tracker->TrackField("session_ticket", session_ticket.value()); } +void Session::Config::set_token(const uint8_t* token, size_t len, ngtcp2_token_type type) { + settings.token = token; + settings.tokenlen = len; + settings.token_type = type; +} + +void Session::Config::set_token(const RetryToken& token) { + ngtcp2_vec vec = token; + set_token(vec.base, vec.len, NGTCP2_TOKEN_TYPE_RETRY); +} + +void Session::Config::set_token(const RegularToken& token) { + ngtcp2_vec vec = token; + set_token(vec.base, vec.len, NGTCP2_TOKEN_TYPE_NEW_TOKEN); +} + // ============================================================================ Maybe Session::Options::From(Environment* env, @@ -358,7 +376,7 @@ Maybe Session::Options::From(Environment* env, auto& state = BindingData::Get(env); auto params = value.As(); - Options options = Options(); + Options options; #define SET(name) \ SetOption( \ @@ -391,7 +409,7 @@ bool Session::HasInstance(Environment* env, Local value) { return GetConstructorTemplate(env)->HasInstance(value); } -BaseObjectPtr Session::Create(BaseObjectPtr endpoint, +BaseObjectPtr Session::Create(Endpoint* endpoint, const Config& config) { Local obj; if (!GetConstructorTemplate(endpoint->env()) @@ -401,10 +419,10 @@ BaseObjectPtr Session::Create(BaseObjectPtr endpoint, return BaseObjectPtr(); } - return MakeDetachedBaseObject(std::move(endpoint), obj, config); + return MakeDetachedBaseObject(endpoint, obj, config); } -Session::Session(BaseObjectPtr endpoint, +Session::Session(Endpoint* endpoint, v8::Local object, const Config& config) : AsyncWrap(endpoint->env(), object, AsyncWrap::PROVIDER_QUIC_SESSION), @@ -412,6 +430,7 @@ Session::Session(BaseObjectPtr endpoint, state_(env()->isolate()), config_(config), connection_(InitConnection()), + endpoint_(BaseObjectWeakPtr(endpoint)), tls_context_(env(), config_.side, this, config_.options.tls_options), application_(select_application()), local_address_(config.local_address), @@ -541,7 +560,7 @@ const Session::Options& Session::options() const { } void Session::HandleQlog(uint32_t flags, const void* data, size_t len) { - if (qlog()) { + if (qlog_stream_) { // Fun fact... ngtcp2 does not emit the final qlog statement until the // ngtcp2_conn object is destroyed. Ideally, destroying is explicit, but // sometimes the Session object can be garbage collected without being @@ -553,7 +572,7 @@ void Session::HandleQlog(uint32_t flags, const void* data, size_t len) { std::vector buffer(len); memcpy(buffer.data(), data, len); env()->SetImmediate( - [ptr = qlog(), buffer = std::move(buffer), flags](Environment*) { + [ptr = qlog_stream_, buffer = std::move(buffer), flags](Environment*) { ptr->Emit(buffer.data(), buffer.size(), flags & NGTCP2_QLOG_WRITE_FLAG_FIN @@ -563,14 +582,6 @@ void Session::HandleQlog(uint32_t flags, const void* data, size_t len) { } } -BaseObjectPtr Session::qlog() const { - return qlog_stream_; -} - -BaseObjectPtr Session::keylog() const { - return keylog_stream_; -} - TransportParams Session::GetLocalTransportParams() const { DCHECK(!is_destroyed()); return TransportParams(ngtcp2_conn_get_local_transport_params(*this)); @@ -588,20 +599,27 @@ void Session::SetLastError(QuicError&& error) { void Session::Close(Session::CloseMethod method) { if (is_destroyed()) return; switch (method) { - case CloseMethod::DEFAULT: - return DoClose(); - case CloseMethod::SILENT: - return DoClose(true); - case CloseMethod::GRACEFUL: + case CloseMethod::DEFAULT: { + DoClose(false); + break; + } + case CloseMethod::SILENT: { + DoClose(true); + break; + } + case CloseMethod::GRACEFUL: { if (is_graceful_closing()) return; // If there are no open streams, then we can close just immediately and // not worry about waiting around for the right moment. - if (streams_.empty()) return DoClose(); - state_->graceful_close = 1; - STAT_RECORD_TIMESTAMP(Stats, graceful_closing_at); - return; + if (streams_.empty()) { + DoClose(); + } else { + state_->graceful_close = 1; + STAT_RECORD_TIMESTAMP(Stats, graceful_closing_at); + } + break; + } } - UNREACHABLE(); } void Session::Destroy() { @@ -628,35 +646,40 @@ void Session::Destroy() { // be deconstructed once the stack unwinds and any remaining // BaseObjectPtr instances fall out of scope. - std::vector cids(ngtcp2_conn_get_scid(*this, nullptr)); - ngtcp2_conn_get_scid(*this, cids.data()); + MaybeStackBuffer cids(ngtcp2_conn_get_scid(*this, nullptr)); + ngtcp2_conn_get_scid(*this, cids.out()); - std::vector tokens( + MaybeStackBuffer tokens( ngtcp2_conn_get_active_dcid(*this, nullptr)); - ngtcp2_conn_get_active_dcid(*this, tokens.data()); + ngtcp2_conn_get_active_dcid(*this, tokens.out()); endpoint_->DisassociateCID(config_.dcid); endpoint_->DisassociateCID(config_.preferred_address_cid); - for (const auto& cid : cids) endpoint_->DisassociateCID(CID(&cid)); + for (size_t n = 0; n < cids.length(); n++) { + endpoint_->DisassociateCID(CID(cids[n])); + } - for (const auto& token : tokens) { - if (token.token_present) + for (size_t n = 0; n < tokens.length(); n++) { + if (tokens[n].token_present) { endpoint_->DisassociateStatelessResetToken( - StatelessResetToken(token.token)); + StatelessResetToken(tokens[n].token)); + } } state_->destroyed = 1; + // Removing the session from the endpoint may cause the endpoint to be destroyed + // if it is waiting on the last session to be destroyed. Let's grab a reference + // just to be safe for the rest of the function. BaseObjectPtr endpoint = std::move(endpoint_); - endpoint->RemoveSession(config_.scid); } bool Session::Receive(Store&& store, const SocketAddress& local_address, const SocketAddress& remote_address) { - DCHECK(!is_destroyed()); + if (is_destroyed()) return false; const auto receivePacket = [&](ngtcp2_path* path, ngtcp2_vec vec) { DCHECK(!is_destroyed()); @@ -674,6 +697,12 @@ bool Session::Receive(Store&& store, // sent. This happens when the remote peer has sent a CONNECTION_CLOSE. return false; } + case NGTCP2_ERR_CLOSING: { + // Connection has entered the closing state, no further data should be + // sent. This happens when the local peer has called + // ngtcp2_conn_write_connection_close. + return false; + } case NGTCP2_ERR_CRYPTO: { // Crypto error happened! Set the last error to the tls alert last_error_ = QuicError::ForTlsAlert(ngtcp2_conn_get_tls_alert(*this)); @@ -681,7 +710,7 @@ bool Session::Receive(Store&& store, return false; } case NGTCP2_ERR_RETRY: { - // This should only ever happen on the server. We have to sent a path + // This should only ever happen on the server. We have to send a path // validation challenge in the form of a RETRY packet to the peer and // drop the connection. DCHECK(is_server()); @@ -761,12 +790,18 @@ uint64_t Session::SendDatagram(Store&& data) { int attempts = 0; for (;;) { + // We may have to make several attempts at encoding and sending the + // datagram packet. On each iteration here we'll try to encode the + // datagram. It's entirely up to ngtcp2 whether to include the datagram + // in the packet on each call to ngtcp2_conn_writev_datagram. if (!packet) { packet = Packet::Create(env(), endpoint_.get(), remote_address_, ngtcp2_conn_get_max_tx_udp_payload_size(*this), "datagram"); + // Typically sending datagrams is best effort, but if we cannot create + // the packet, then we handle it as a fatal error. if (!packet) { last_error_ = QuicError::ForNgtcp2Error(NGTCP2_ERR_INTERNAL); Close(CloseMethod::SILENT); @@ -786,8 +821,9 @@ uint64_t Session::SendDatagram(Store&& data) { &vec, 1, uv_hrtime()); + ngtcp2_conn_update_pkt_tx_time(*this, uv_hrtime()); - if (nwrite < 1) { + if (nwrite <= 0) { // Nothing was written to the packet. switch (nwrite) { case 0: { @@ -814,6 +850,16 @@ uint64_t Session::SendDatagram(Store&& data) { packet->Done(UV_ECANCELED); return 0; } + case NGTCP2_ERR_PKT_NUM_EXHAUSTED: { + // We've exhausted the packet number space. Sadly we have to treat it + // as a fatal condition. + break; + } + case NGTCP2_ERR_CALLBACK_FAILURE: { + // There was an internal failure. Sadly we have to treat it as a fatal + // condition. + break; + } } packet->Done(UV_ECANCELED); last_error_ = QuicError::ForNgtcp2Error(nwrite); @@ -826,7 +872,6 @@ uint64_t Session::SendDatagram(Store&& data) { // datagram! We'll check that next by checking the accepted value. packet->Truncate(nwrite); Send(std::move(packet)); - ngtcp2_conn_update_pkt_tx_time(*this, uv_hrtime()); if (accepted != 0) { // Yay! The datagram was accepted into the packet we just sent and we can @@ -1110,22 +1155,6 @@ void Session::UpdateDataStats() { Stats, max_bytes_in_flight, std::max(STAT_GET(Stats, max_bytes_in_flight), info.bytes_in_flight)); - - // TODO(@jasnell): Want to see if ngtcp2 provides an alternative way of - // getting these before removing them. Will handle that in one of the - // follow-up PRs STAT_SET( - // Stats, congestion_recovery_start_ts, - // info.congestion_recovery_start_ts); - // STAT_SET(Stats, delivery_rate_sec, info.delivery_rate_sec); - // STAT_SET(Stats, first_rtt_sample_ts, stat.first_rtt_sample_ts); - // STAT_SET(Stats, initial_rtt, info.initial_rtt); - // STAT_SET( - // Stats, last_tx_pkt_ts, - // reinterpret_cast(stat.last_tx_pkt_ts)); - // STAT_SET(Stats, loss_detection_timer, info.loss_detection_timer); - // STAT_SET(Stats, loss_time, reinterpret_cast(stat.loss_time)); - // STAT_SET(Stats, max_udp_payload_size, stat.max_udp_payload_size); - // STAT_SET(Stats, pto_count, stat.pto_count); } void Session::SendConnectionClose() { @@ -1373,7 +1402,7 @@ void Session::EmitDatagramStatus(uint64_t id, quic::DatagramStatus status) { CallbackScope cb_scope(this); auto& state = BindingData::Get(env()); - const auto status_to_string = [&] { + const auto status_to_string = ([&] { switch (status) { case quic::DatagramStatus::ACKNOWLEDGED: return state.acknowledged_string(); @@ -1381,10 +1410,10 @@ void Session::EmitDatagramStatus(uint64_t id, quic::DatagramStatus status) { return state.lost_string(); } UNREACHABLE(); - }; + })(); Local argv[] = {BigInt::NewFromUnsigned(env()->isolate(), id), - status_to_string()}; + status_to_string}; MakeCallback(state.session_datagram_status_callback(), arraysize(argv), argv); } @@ -1447,7 +1476,7 @@ void Session::EmitPathValidation(PathValidationResult result, CallbackScope cb_scope(this); auto& state = BindingData::Get(env()); - const auto resultToString = [&] { + const auto resultToString = ([&] { switch (result) { case PathValidationResult::ABORTED: return state.aborted_string(); @@ -1457,10 +1486,10 @@ void Session::EmitPathValidation(PathValidationResult result, return state.success_string(); } UNREACHABLE(); - }; + })(); Local argv[] = { - resultToString(), + resultToString, SocketAddressBase::Create(env(), newPath.local)->object(), SocketAddressBase::Create(env(), newPath.remote)->object(), Undefined(isolate), @@ -1955,14 +1984,6 @@ struct Session::Impl { return NGTCP2_SUCCESS; } - static int on_stream_open(ngtcp2_conn* conn, - int64_t stream_id, - void* user_data) { - // We currently do nothing with this callback. That is because we - // implicitly create streams when we receive data on them. - return NGTCP2_SUCCESS; - } - static int on_stream_reset(ngtcp2_conn* conn, int64_t stream_id, uint64_t final_size, @@ -2012,7 +2033,7 @@ struct Session::Impl { ngtcp2_crypto_hp_mask_cb, on_receive_stream_data, on_acknowledge_stream_data_offset, - on_stream_open, + nullptr, on_stream_close, on_receive_stateless_reset, ngtcp2_crypto_recv_retry_cb, @@ -2054,7 +2075,7 @@ struct Session::Impl { ngtcp2_crypto_hp_mask_cb, on_receive_stream_data, on_acknowledge_stream_data_offset, - on_stream_open, + nullptr, on_stream_close, on_receive_stateless_reset, nullptr, @@ -2161,25 +2182,28 @@ Session::QuicConnectionPointer Session::InitConnection() { UNREACHABLE(); } -void Session::Initialize(Environment* env, Local target) { +void Session::InitPerIsolate(IsolateData* data, v8::Local target) { + // TODO(@jasnell): Implement the per-isolate state +} + +void Session::InitPerContext(Realm* realm, Local target) { // Make sure the Session constructor template is initialized. - USE(GetConstructorTemplate(env)); + USE(GetConstructorTemplate(realm->env())); - TransportParams::Initialize(env, target); - PreferredAddress::Initialize(env, target); + TransportParams::Initialize(realm->env(), target); + PreferredAddress::Initialize(realm->env(), target); - static constexpr uint32_t STREAM_DIRECTION_BIDIRECTIONAL = + static constexpr auto STREAM_DIRECTION_BIDIRECTIONAL = static_cast(Direction::BIDIRECTIONAL); - static constexpr uint32_t STREAM_DIRECTION_UNIDIRECTIONAL = + static constexpr auto STREAM_DIRECTION_UNIDIRECTIONAL = static_cast(Direction::UNIDIRECTIONAL); + static constexpr auto QUIC_PROTO_MAX = NGTCP2_PROTO_VER_MAX; + static constexpr auto QUIC_PROTO_MIN = NGTCP2_PROTO_VER_MIN; NODE_DEFINE_CONSTANT(target, STREAM_DIRECTION_BIDIRECTIONAL); NODE_DEFINE_CONSTANT(target, STREAM_DIRECTION_UNIDIRECTIONAL); NODE_DEFINE_CONSTANT(target, DEFAULT_MAX_HEADER_LIST_PAIRS); NODE_DEFINE_CONSTANT(target, DEFAULT_MAX_HEADER_LENGTH); - - constexpr auto QUIC_PROTO_MAX = NGTCP2_PROTO_VER_MAX; - constexpr auto QUIC_PROTO_MIN = NGTCP2_PROTO_VER_MIN; NODE_DEFINE_CONSTANT(target, QUIC_PROTO_MAX); NODE_DEFINE_CONSTANT(target, QUIC_PROTO_MIN); diff --git a/src/quic/session.h b/src/quic/session.h index a844e2a3c2709f..c3d5e2ef77bff3 100644 --- a/src/quic/session.h +++ b/src/quic/session.h @@ -1,5 +1,7 @@ #pragma once +#include +#include "quic/tokens.h" #if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS #if HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC @@ -72,6 +74,11 @@ class Session final : public AsyncWrap, private SessionTicket::AppData::Source { uint64_t qpack_encoder_max_dtable_capacity = 0; uint64_t qpack_blocked_streams = 0; + bool enable_connect_protocol = true; + bool enable_datagrams = true; + + operator const nghttp3_settings() const; + SET_NO_MEMORY_INFO() SET_MEMORY_INFO_NAME(Application::Options) SET_SELF_SIZE(Options) @@ -141,10 +148,12 @@ class Session final : public AsyncWrap, private SessionTicket::AppData::Source { SocketAddress local_address; SocketAddress remote_address; - // The destination CID, identifying the remote peer. + // The destination CID, identifying the remote peer. This value is always + // provided by the remote peer. CID dcid = CID::kInvalid; - // The source CID, identifying this session. + // The source CID, identifying this session. This value is always created + // locally. CID scid = CID::kInvalid; // Used only by client sessions to identify the original DCID @@ -179,6 +188,12 @@ class Session final : public AsyncWrap, private SessionTicket::AppData::Source { std::optional session_ticket = std::nullopt, const CID& ocid = CID::kInvalid); + void set_token(const uint8_t* token, + size_t len, + ngtcp2_token_type type = NGTCP2_TOKEN_TYPE_UNKNOWN); + void set_token(const RetryToken& token); + void set_token(const RegularToken& token); + void MemoryInfo(MemoryTracker* tracker) const override; SET_MEMORY_INFO_NAME(Session::Config) SET_SELF_SIZE(Config) @@ -187,14 +202,16 @@ class Session final : public AsyncWrap, private SessionTicket::AppData::Source { static bool HasInstance(Environment* env, v8::Local value); static v8::Local GetConstructorTemplate( Environment* env); - static void Initialize(Environment* env, v8::Local target); + static void InitPerIsolate(IsolateData* isolate_data, + v8::Local target); + static void InitPerContext(Realm* env, v8::Local target); static void RegisterExternalReferences(ExternalReferenceRegistry* registry); - static BaseObjectPtr Create(BaseObjectPtr endpoint, + static BaseObjectPtr Create(Endpoint* endpoint, const Config& config); // Really should be private but MakeDetachedBaseObject needs visibility. - Session(BaseObjectPtr endpoint, + Session(Endpoint* endpoint, v8::Local object, const Config& config); ~Session() override; @@ -325,12 +342,13 @@ class Session final : public AsyncWrap, private SessionTicket::AppData::Source { uint64_t max_data_left() const; uint64_t max_local_streams_uni() const; uint64_t max_local_streams_bidi() const; - BaseObjectPtr qlog() const; - BaseObjectPtr keylog() const; bool wants_session_ticket() const; void SetStreamOpenAllowed(); + // It's a terrible name but "wrapped" here means that the Session has been + // passed out to JavaScript and should be "wrapped" by whatever handler is + // defined there to manage it. void set_wrapped(); void DoClose(bool silent = false); @@ -387,7 +405,7 @@ class Session final : public AsyncWrap, private SessionTicket::AppData::Source { ngtcp2_mem allocator_; Config config_; QuicConnectionPointer connection_; - BaseObjectPtr endpoint_; + BaseObjectWeakPtr endpoint_; TLSContext tls_context_; std::unique_ptr application_; SocketAddress local_address_; From 1cea9d4f891b11fc3b20f58ddd43e12bf200c2fc Mon Sep 17 00:00:00 2001 From: James M Snell Date: Thu, 28 Dec 2023 10:06:21 -0800 Subject: [PATCH 2/8] quic: add debugging output for endpoint, session, etc --- src/debug_utils.h | 3 +- src/quic/application.cc | 50 ++++++++++- src/quic/defs.h | 26 ++++++ src/quic/endpoint.cc | 164 +++++++++++++++++++++++++++++++++++- src/quic/endpoint.h | 2 + src/quic/packet.cc | 20 ++++- src/quic/packet.h | 1 + src/quic/quic.cc | 2 + src/quic/session.cc | 112 ++++++++++++++++++++++-- src/quic/session.h | 9 ++ src/quic/tlscontext.cc | 36 ++++++++ src/quic/tlscontext.h | 3 + src/quic/tokens.cc | 42 +++++++++ src/quic/tokens.h | 10 +++ src/quic/transportparams.cc | 41 +++++++++ src/quic/transportparams.h | 2 + 16 files changed, 509 insertions(+), 14 deletions(-) diff --git a/src/debug_utils.h b/src/debug_utils.h index 280b4cb39c780a..072545c648236f 100644 --- a/src/debug_utils.h +++ b/src/debug_utils.h @@ -52,7 +52,8 @@ void NODE_EXTERN_PRIVATE FWrite(FILE* file, const std::string& str); V(WASI) \ V(MKSNAPSHOT) \ V(SNAPSHOT_SERDES) \ - V(PERMISSION_MODEL) + V(PERMISSION_MODEL) \ + V(QUIC) enum class DebugCategory : unsigned int { #define V(name) name, diff --git a/src/quic/application.cc b/src/quic/application.cc index eabd34137cdc76..1ed55b786df0b6 100644 --- a/src/quic/application.cc +++ b/src/quic/application.cc @@ -1,6 +1,8 @@ #if HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC #include "application.h" +#include +#include #include #include #include @@ -51,6 +53,27 @@ Session::Application_Options::operator const nghttp3_settings() const { }; } +std::string Session::Application_Options::ToString() const { + DebugIndentScope indent; + auto prefix = indent.Prefix(); + std::string res("{"); + res += prefix + "max header pairs: " + std::to_string(max_header_pairs); + res += prefix + "max header length: " + std::to_string(max_header_length); + res += prefix + "max field section size: " + + std::to_string(max_field_section_size); + res += prefix + "qpack max dtable capacity: " + + std::to_string(qpack_max_dtable_capacity); + res += prefix + "qpack encoder max dtable capacity: " + + std::to_string(qpack_encoder_max_dtable_capacity); + res += prefix + "qpack blocked streams: " + std::to_string(qpack_blocked_streams); + res += prefix + "enable connect protocol: " + + (enable_connect_protocol ? std::string("yes") : std::string("no")); + res += prefix + "enable datagrams: " + + (enable_datagrams ? std::string("yes") : std::string("no")); + res += indent.Close(); + return res; +} + Maybe Session::Application_Options::From( Environment* env, Local value) { if (value.IsEmpty() || (!value->IsUndefined() && !value->IsObject())) { @@ -89,16 +112,19 @@ Session::Application::Application(Session* session, const Options& options) bool Session::Application::Start() { // By default there is nothing to do. Specific implementations may // override to perform more actions. + Debug(session_, "Session application started"); return true; } void Session::Application::AcknowledgeStreamData(Stream* stream, size_t datalen) { + Debug(session_, "Application acknowledging stream %" PRIi64 " data: %zu", stream->id(), datalen); DCHECK_NOT_NULL(stream); stream->Acknowledge(datalen); } void Session::Application::BlockStream(int64_t id) { + Debug(session_, "Application blocking stream %" PRIi64, id); auto stream = session().FindStream(id); if (stream) stream->EmitBlocked(); } @@ -107,6 +133,7 @@ bool Session::Application::CanAddHeader(size_t current_count, size_t current_headers_length, size_t this_header_length) { // By default headers are not supported. + Debug(session_, "Application cannot add header"); return false; } @@ -115,26 +142,31 @@ bool Session::Application::SendHeaders(const Stream& stream, const v8::Local& headers, HeadersFlags flags) { // By default do nothing. + Debug(session_, "Application cannot send headers"); return false; } void Session::Application::ResumeStream(int64_t id) { + Debug(session_, "Application resuming stream %" PRIi64, id); // By default do nothing. } void Session::Application::ExtendMaxStreams(EndpointLabel label, Direction direction, uint64_t max_streams) { + Debug(session_, "Application extending max streams"); // By default do nothing. } void Session::Application::ExtendMaxStreamData(Stream* stream, uint64_t max_data) { + Debug(session_, "Application extending max stream data"); // By default do nothing. } void Session::Application::CollectSessionTicketAppData( SessionTicket::AppData* app_data) const { + Debug(session_, "Application collecting session ticket app data"); // By default do nothing. } @@ -142,6 +174,7 @@ SessionTicket::AppData::Status Session::Application::ExtractSessionTicketAppData( const SessionTicket::AppData& app_data, SessionTicket::AppData::Source::Flag flag) { + Debug(session_, "Application extracting session ticket app data"); // By default we do not have any application data to retrieve. return flag == SessionTicket::AppData::Source::Flag::STATUS_RENEW ? SessionTicket::AppData::Status::TICKET_USE_RENEW @@ -151,6 +184,7 @@ Session::Application::ExtractSessionTicketAppData( void Session::Application::SetStreamPriority(const Stream& stream, StreamPriority priority, StreamPriorityFlags flags) { + Debug(session_, "Application setting stream %" PRIi64 " priority", stream.id()); // By default do nothing. } @@ -167,10 +201,12 @@ BaseObjectPtr Session::Application::CreateStreamDataPacket() { } void Session::Application::StreamClose(Stream* stream, QuicError error) { + Debug(session_, "Application closing stream %" PRIi64 " with error %s", stream->id(), error); stream->Destroy(error); } void Session::Application::StreamStopSending(Stream* stream, QuicError error) { + Debug(session_, "Application stopping sending on stream %" PRIi64 " with error %s", stream->id(), error); DCHECK_NOT_NULL(stream); stream->ReceiveStopSending(error); } @@ -178,10 +214,12 @@ void Session::Application::StreamStopSending(Stream* stream, QuicError error) { void Session::Application::StreamReset(Stream* stream, uint64_t final_size, QuicError error) { + Debug(session_, "Application resetting stream %" PRIi64 " with error %s", stream->id(), error); stream->ReceiveStreamReset(final_size, error); } void Session::Application::SendPendingData() { + Debug(session_, "Application sending pending data"); PathStorage path; BaseObjectPtr packet; @@ -193,6 +231,7 @@ void Session::Application::SendPendingData() { size_t packetSendCount = 0; const auto updateTimer = [&] { + Debug(session_, "Application updating the session timer"); ngtcp2_conn_update_pkt_tx_time(*session_, uv_hrtime()); session_->UpdateTimer(); }; @@ -330,12 +369,14 @@ class DefaultApplication final : public Session::Application { const uint8_t* data, size_t datalen, Stream::ReceiveDataFlags flags) override { + Debug(&session(), "Default application receiving stream data"); DCHECK_NOT_NULL(stream); if (!stream->is_destroyed()) stream->ReceiveData(data, datalen, flags); return true; } int GetStreamData(StreamData* stream_data) override { + Debug(&session(), "Default application getting stream data"); DCHECK_NOT_NULL(stream_data); // If the queue is empty, there aren't any streams with data yet if (stream_queue_.IsEmpty()) return 0; @@ -391,7 +432,10 @@ class DefaultApplication final : public Session::Application { return 0; } - void ResumeStream(int64_t id) override { ScheduleStream(id); } + void ResumeStream(int64_t id) override { + Debug(&session(), "Default application resuming stream %" PRIi64, id); + ScheduleStream(id); + } bool ShouldSetFin(const StreamData& stream_data) override { auto const is_empty = [](auto vec, size_t cnt) { @@ -405,6 +449,7 @@ class DefaultApplication final : public Session::Application { } bool StreamCommit(StreamData* stream_data, size_t datalen) override { + Debug(&session(), "Default application committing stream data"); DCHECK_NOT_NULL(stream_data); const auto consume = [](ngtcp2_vec** pvec, size_t* pcnt, size_t len) { ngtcp2_vec* v = *pvec; @@ -436,6 +481,7 @@ class DefaultApplication final : public Session::Application { private: void ScheduleStream(int64_t id) { + Debug(&session(), "Default application scheduling stream %" PRIi64, id); auto stream = session().FindStream(id); if (stream && !stream->is_destroyed()) { stream->Schedule(&stream_queue_); @@ -443,6 +489,7 @@ class DefaultApplication final : public Session::Application { } void UnscheduleStream(int64_t id) { + Debug(&session(), "Default application unscheduling stream %" PRIi64, id); auto stream = session().FindStream(id); if (stream && !stream->is_destroyed()) stream->Unschedule(); } @@ -458,6 +505,7 @@ std::unique_ptr Session::select_application() { // 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 std::make_unique( this, config_.options.application_options); } diff --git a/src/quic/defs.h b/src/quic/defs.h index fc0bc0c81a7b7e..6eced43f0f7733 100644 --- a/src/quic/defs.h +++ b/src/quic/defs.h @@ -13,6 +13,9 @@ namespace quic { #define NGTCP2_ERR(V) (V != NGTCP2_SUCCESS) #define NGTCP2_OK(V) (V == NGTCP2_SUCCESS) +#define IF_QUIC_DEBUG(env) \ + if (UNLIKELY(env->enabled_debug_list()->enabled(DebugCategory::QUIC))) + template bool SetOption(Environment* env, Opt* options, @@ -145,5 +148,28 @@ uint64_t GetStat(Stats* stats) { #define JS_METHOD(name) \ static void name(const v8::FunctionCallbackInfo& args) +class DebugIndentScope { + public: + inline DebugIndentScope() { ++indent_; } + DebugIndentScope(const DebugIndentScope&) = delete; + DebugIndentScope(DebugIndentScope&&) = delete; + DebugIndentScope& operator=(const DebugIndentScope&) = delete; + DebugIndentScope& operator=(DebugIndentScope&&) = delete; + inline ~DebugIndentScope() { --indent_; } + std::string Prefix() const { + std::string res("\n"); + res.append(indent_, '\t'); + return res; + } + std::string Close() const { + std::string res("\n"); + res.append(indent_ - 1, '\t'); + res += "}"; + return res; + } + private: + static int indent_; +}; + } // namespace quic } // namespace node diff --git a/src/quic/endpoint.cc b/src/quic/endpoint.cc index 81b4453866f4b5..0c9ddf7d8e11be 100644 --- a/src/quic/endpoint.cc +++ b/src/quic/endpoint.cc @@ -1,8 +1,10 @@ +#include "debug_utils.h" #if HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC #include "endpoint.h" #include #include +#include #include #include #include @@ -294,6 +296,68 @@ void Endpoint::Options::MemoryInfo(MemoryTracker* tracker) const { tracker->TrackField("token_secret", token_secret); } +std::string Endpoint::Options::ToString() const { + DebugIndentScope indent; + auto prefix = indent.Prefix(); + auto boolToString = [](uint8_t val) { + return val ? std::string("yes") : std::string("no"); + }; + + std::string res = "{ "; + res += prefix + "local address: " + local_address->ToString(); + res += prefix + "retry token expiration: " + + std::to_string(retry_token_expiration) + " seconds"; + res += prefix + "token expiration: " + std::to_string(token_expiration) + + " seconds"; + res += prefix + "max connections per host: " + + std::to_string(max_connections_per_host); + res += prefix + "max connections total: " + std::to_string(max_connections_total); + res += prefix + "max stateless resets: " + std::to_string(max_stateless_resets); + res += prefix + "address lru size: " + std::to_string(address_lru_size); + res += prefix + "max retries: " + std::to_string(max_retries); + res += prefix + "max payload size: " + std::to_string(max_payload_size); + res += prefix + "unacknowledged packet threshold: " + + std::to_string(unacknowledged_packet_threshold); + if (handshake_timeout == UINT64_MAX) { + res += prefix + "handshake timeout: "; + } else { + res += prefix + "handshake timeout: " + std::to_string(handshake_timeout) + + " nanoseconds"; + } + res += prefix + "max stream window: " + std::to_string(max_stream_window); + res += prefix + "max window: " + std::to_string(max_window); + res += prefix + "no udp payload size shaping: " + boolToString(no_udp_payload_size_shaping); + res += prefix + "validate address: " + boolToString(validate_address); + res += prefix + "disable stateless reset: " + boolToString(disable_stateless_reset); +#ifdef DEBUG + res += prefix + "rx loss: " + std::to_string(rx_loss); + res += prefix + "tx loss: " + std::to_string(tx_loss); +#endif + + auto ccalg = ([&] { + switch (cc_algorithm) { + case NGTCP2_CC_ALGO_RENO: + return "reno"; + case NGTCP2_CC_ALGO_CUBIC: + return "cubic"; + case NGTCP2_CC_ALGO_BBR: + return "bbr"; + } + return ""; + })(); + res += prefix + "cc algorithm: " + std::string(ccalg); + res += prefix + "reset token secret: " + reset_token_secret.ToString(); + res += prefix + "token secret: " + token_secret.ToString(); + res += prefix + "ipv6 only: " + boolToString(ipv6_only); + res += prefix + "udp receive buffer size: " + + std::to_string(udp_receive_buffer_size); + res += prefix + "udp send buffer size: " + std::to_string(udp_send_buffer_size); + res += prefix + "udp ttl: " + std::to_string(udp_ttl); + + res += indent.Close(); + return res; +} + // ====================================================================================== // Endpoint::UDP and Endpoint::UDP::Impl @@ -602,6 +666,9 @@ Endpoint::Endpoint(Environment* env, udp_(this), addrLRU_(options_.address_lru_size) { MakeWeak(); + IF_QUIC_DEBUG(env) { + Debug(this, "Endpoint created. Options %s", options.ToString()); + } const auto defineProperty = [&](auto name, auto value) { object @@ -620,23 +687,33 @@ SocketAddress Endpoint::local_address() const { } void Endpoint::MarkAsBusy(bool on) { + Debug(this, "Marking endpoint as %s", on ? "busy" : "not busy"); state_->busy = on ? 1 : 0; } RegularToken Endpoint::GenerateNewToken(uint32_t version, const SocketAddress& remote_address) { + IF_QUIC_DEBUG(env()) { + Debug(this, "Generating new regular token for version %u and remote address %s", + version, remote_address); + } DCHECK(!is_closed() && !is_closing()); return RegularToken(version, remote_address, options_.token_secret); } StatelessResetToken Endpoint::GenerateNewStatelessResetToken( uint8_t* token, const CID& cid) const { + IF_QUIC_DEBUG(env()) { + Debug(const_cast(this), "Generating new stateless reset tokek for CID %s", + cid); + } DCHECK(!is_closed() && !is_closing()); return StatelessResetToken(token, options_.reset_token_secret, cid); } void Endpoint::AddSession(const CID& cid, BaseObjectPtr session) { if (is_closed() || is_closing()) return; + Debug(this, "Adding session for CID %s", cid); sessions_[cid] = session; IncrementSocketAddressCounter(session->remote_address()); if (session->is_server()) { @@ -649,6 +726,7 @@ void Endpoint::AddSession(const CID& cid, BaseObjectPtr session) { void Endpoint::RemoveSession(const CID& cid) { if (is_closed()) return; + Debug(this, "Removing session for CID %s", cid); auto session = FindSession(cid); if (!session) return; DecrementSocketAddressCounter(session->remote_address()); @@ -675,23 +753,31 @@ BaseObjectPtr Endpoint::FindSession(const CID& cid) { void Endpoint::AssociateCID(const CID& cid, const CID& scid) { if (!is_closed() && !is_closing() && cid && scid && cid != scid && dcid_to_scid_[cid] != scid) { + Debug(this, "Associating CID %s with SCID %s", cid, scid); dcid_to_scid_.emplace(cid, scid); } } void Endpoint::DisassociateCID(const CID& cid) { - if (!is_closed() && cid) dcid_to_scid_.erase(cid); + if (!is_closed() && cid) { + Debug(this, "Disassociating CID %s", cid); + dcid_to_scid_.erase(cid); + } } void Endpoint::AssociateStatelessResetToken(const StatelessResetToken& token, Session* session) { if (is_closed() || is_closing()) return; + Debug(this, "Associating stateless reset token %s with session", token); token_map_[token] = session; } void Endpoint::DisassociateStatelessResetToken( const StatelessResetToken& token) { - if (!is_closed()) token_map_.erase(token); + if (!is_closed()) { + Debug(this, "Disassociating stateless reset token %s", token); + token_map_.erase(token); + } } void Endpoint::Send(BaseObjectPtr packet) { @@ -707,10 +793,12 @@ void Endpoint::Send(BaseObjectPtr packet) { #endif // DEBUG if (is_closed() || is_closing() || packet->length() == 0) return; + Debug(this, "Sending %s", packet->ToString()); state_->pending_callbacks++; int err = udp_.Send(packet); if (err != 0) { + Debug(this, "Sending packet failed with error %d", err); packet->Done(err); Destroy(CloseContext::SEND_FAILURE, err); } @@ -728,6 +816,7 @@ void Endpoint::SendRetry(const PathDescriptor& options) { // to give application code a means of detecting and responding to abuse on // its own. What this count does not give is the rate of retry, so it is still // somewhat limited. + Debug(this, "Sending retry on path %s", options); auto info = addrLRU_.Upsert(options.remote_address); if (++(info->retry_count) <= options_.max_retries) { auto packet = @@ -744,6 +833,7 @@ void Endpoint::SendRetry(const PathDescriptor& options) { } void Endpoint::SendVersionNegotiation(const PathDescriptor& options) { + Debug(this, "Sending version negotiation on path %s", options); // While creating and sending a version negotiation packet does consume a // small amount of system resources, and while it is fairly trivial for a // malicious peer to force a version negotiation to be sent, these are more @@ -765,6 +855,7 @@ void Endpoint::SendVersionNegotiation(const PathDescriptor& options) { bool Endpoint::SendStatelessReset(const PathDescriptor& options, size_t source_len) { if (UNLIKELY(options_.disable_stateless_reset)) return false; + Debug(this, "Sending stateless reset on path %s with len %" PRIu64, options, source_len); const auto exceeds_limits = [&] { SocketAddressInfoTraits::Type* counts = @@ -791,6 +882,8 @@ bool Endpoint::SendStatelessReset(const PathDescriptor& options, void Endpoint::SendImmediateConnectionClose(const PathDescriptor& options, QuicError reason) { + Debug(this, "Sending immediate connection close on path %s with reason %s", options, + reason); // While it is possible for a malicious peer to cause us to create a large // number of these, generating them is fairly trivial. auto packet = Packet::CreateImmediateConnectionClosePacket( @@ -807,6 +900,7 @@ bool Endpoint::Start() { // state_->receiving indicates that we're accepting inbound packets. It // could be for server or client side, or both. if (state_->receiving == 1) return true; + Debug(this, "Starting"); int err = 0; if (state_->bound == 0) { @@ -836,7 +930,10 @@ bool Endpoint::Start() { void Endpoint::Listen(const Session::Options& options) { if (is_closed() || is_closing() || state_->listening == 1) return; server_options_ = options; - if (Start()) state_->listening = 1; + if (Start()) { + Debug(this, "Listening with options %s", server_options_.value()); + state_->listening = 1; + } } BaseObjectPtr Endpoint::Connect( @@ -849,6 +946,11 @@ BaseObjectPtr Endpoint::Connect( Session::Config config( *this, options, local_address(), remote_address, session_ticket); + IF_QUIC_DEBUG(env()) { + Debug(this, "Connecting to %s with options %s and config %s [has 0rtt ticket? %s]", + remote_address, options, config, session_ticket.has_value() ? "yes" : "no"); + } + auto session = Session::Create(this, config); if (!session) return BaseObjectPtr(); session->set_wrapped(); @@ -872,6 +974,27 @@ void Endpoint::MaybeDestroy() { void Endpoint::Destroy(CloseContext context, int status) { if (is_closed()) return; + IF_QUIC_DEBUG(env()) { + auto ctx = ([&] { + switch (context) { + case CloseContext::BIND_FAILURE: + return "bind failure"; + case CloseContext::CLOSE: + return "close"; + case CloseContext::LISTEN_FAILURE: + return "listen failure"; + case CloseContext::RECEIVE_FAILURE: + return "receive failure"; + case CloseContext::SEND_FAILURE: + return "send failure"; + case CloseContext::START_FAILURE: + return "start failure"; + } + return ""; + })(); + Debug(this, "Destroying endpoint due to \"%s\" with status %d", ctx, status); + } + STAT_RECORD_TIMESTAMP(Stats, destroyed_at); state_->listening = 0; @@ -904,6 +1027,8 @@ void Endpoint::Destroy(CloseContext context, int status) { void Endpoint::CloseGracefully() { if (is_closed() || is_closing()) return; + Debug(this, "Closing gracefully"); + state_->listening = 0; state_->closing = 1; @@ -921,6 +1046,7 @@ void Endpoint::Receive(const uv_buf_t& buf, const CID& scid) { DCHECK_NOT_NULL(session); size_t len = store.length(); + Debug(this, "Passing received packet to session for processing"); if (session->Receive(std::move(store), local_address, remote_address)) { STAT_INCREMENT_N(Stats, bytes_received, len); STAT_INCREMENT(Stats, packets_received); @@ -932,6 +1058,7 @@ void Endpoint::Receive(const uv_buf_t& buf, // a server, then we cannot accept the initial packet. if (is_closed() || is_closing() || !is_listening()) return; + Debug(this, "Trying to create new session for initial packet"); auto session = Session::Create(this, config); if (session) { receive(session.get(), std::move(store), config.local_address, @@ -946,6 +1073,7 @@ void Endpoint::Receive(const uv_buf_t& buf, const SocketAddress& local_address, const SocketAddress& remote_address) { // Conditionally accept an initial packet to create a new session. + Debug(this, "Trying to accept initial packet"); // If we're not listening as a server, do not accept an initial packet. if (state_->listening == 0) return; @@ -960,6 +1088,7 @@ void Endpoint::Receive(const uv_buf_t& buf, // or an error code if it was not. Currently there's only one documented error // code (NGTCP2_ERR_INVALID_ARGUMENT) but we'll handle any error here the same -- // by ignoring the packet entirely. + Debug(this, "Failed to accept initial packet"); return; } @@ -967,6 +1096,7 @@ void Endpoint::Receive(const uv_buf_t& buf, // recognized and supported. If it returns 0, we'll go ahead and send a // version negotiation packet in response. if (ngtcp2_is_supported_version(hd.version) == 0) { + Debug(this, "Packet was not accepted because the version is not supported"); SendVersionNegotiation( PathDescriptor{version, dcid, scid, local_address, remote_address}); STAT_INCREMENT(Stats, packets_received); @@ -985,6 +1115,8 @@ void Endpoint::Receive(const uv_buf_t& buf, })(); if (state_->busy || limits_exceeded) { + Debug(this, "Packet was not accepted because the endpoint is busy or the " + "remote peer has exceeded their maximum number of concurrent connections"); // Endpoint is busy or the connection count is exceeded. The connection is // refused. For the purpose of stats collection, we'll count both of these // the same. @@ -1010,6 +1142,8 @@ void Endpoint::Receive(const uv_buf_t& buf, scid, dcid); + Debug(this, "Using session config for initial packet %s", config); + // The this point, the config.scid and config.dcid represent *our* views of // the CIDs. Specifically, config.dcid identifies the peer and config.scid // identifies us. config.dcid should equal scid. config.scid should *not* @@ -1029,6 +1163,7 @@ void Endpoint::Receive(const uv_buf_t& buf, // a new token. If it does exist, and if it is valid, we grab the original // cid and continue. if (!is_remote_address_validated) { + Debug(this, "Remote address is not validated"); switch (hd.type) { case NGTCP2_PKT_INITIAL: // First, let's see if we need to do anything here. @@ -1036,6 +1171,7 @@ void Endpoint::Receive(const uv_buf_t& buf, if (options_.validate_address) { // If there is no token, generate and send one. if (hd.tokenlen == 0) { + Debug(this, "Initial packet has no token. Sending retry to start validation"); SendRetry(PathDescriptor{ version, dcid, @@ -1054,6 +1190,7 @@ void Endpoint::Receive(const uv_buf_t& buf, switch (hd.token[0]) { case RetryToken::kTokenMagic: { RetryToken token(hd.token, hd.tokenlen); + Debug(this, "Initial packet has retry token %s", token); auto ocid = token.Validate( version, remote_address, @@ -1061,6 +1198,7 @@ void Endpoint::Receive(const uv_buf_t& buf, options_.token_secret, options_.retry_token_expiration * NGTCP2_SECONDS); if (!ocid.has_value()) { + Debug(this, "Retry token is invalid."); // Invalid retry token was detected. Close the connection. SendImmediateConnectionClose( PathDescriptor{ @@ -1075,6 +1213,7 @@ void Endpoint::Receive(const uv_buf_t& buf, // The ocid is the original dcid that was encoded into the // original retry packet sent to the client. We use it for // validation. + Debug(this, "Retry token is valid. Original dcid %s", ocid.value()); config.ocid = ocid.value(); config.retry_scid = dcid; config.set_token(token); @@ -1082,11 +1221,13 @@ void Endpoint::Receive(const uv_buf_t& buf, } case RegularToken::kTokenMagic: { RegularToken token(hd.token, hd.tokenlen); + Debug(this, "Initial packet has regular token %s", token); if (!token.Validate( version, remote_address, options_.token_secret, options_.token_expiration * NGTCP2_SECONDS)) { + Debug(this, "Regular token is invalid."); // If the regular token is invalid, let's send a retry to be lenient. // There's a small risk that a malicious peer is trying to make us do // some work but the risk is fairly low here. @@ -1102,10 +1243,12 @@ void Endpoint::Receive(const uv_buf_t& buf, STAT_INCREMENT(Stats, packets_received); return; } + Debug(this, "Regular token is valid."); config.set_token(token); break; } default: { + Debug(this, "Initial packet has unknown token type"); // If our prefix bit does not match anything we know about, let's send // a retry to be lenient. There's a small risk that a malicious peer is // trying to make us do some work but the risk is fairly low here. @@ -1125,8 +1268,10 @@ void Endpoint::Receive(const uv_buf_t& buf, // path to the remote address is valid (for now). Let's record that // so we don't have to do this dance again for this endpoint // instance. + Debug(this, "Remote address is validated"); addrLRU_.Upsert(remote_address)->validated = true; } else if (hd.tokenlen > 0) { + Debug(this, "Ignoring initial packet with unexpected token"); // If validation is turned off and there is a token, that's weird. // The peer should only have a token if we sent it to them and we // wouldn't have sent it unless validation was turned on. Let's @@ -1136,6 +1281,7 @@ void Endpoint::Receive(const uv_buf_t& buf, } break; case NGTCP2_PKT_0RTT: + Debug(this, "Sending retry to initial 0RTT packet"); // If it's a 0RTT packet, we're always going to perform path // validation no matter what. This is a bit unfortunate since // ORTT is supposed to be, you know, 0RTT, but sending a retry @@ -1225,6 +1371,8 @@ void Endpoint::Receive(const uv_buf_t& buf, // return; // } + Debug(this, "Received packet with length %" PRIu64 " from %s", buf.len, remote_address); + // The managed buffer here contains the received packet. We do not yet know // at this point if it is a valid QUIC packet. We need to do some basic checks. // It is critical at this point that we do as little work as possible to avoid @@ -1248,6 +1396,7 @@ void Endpoint::Receive(const uv_buf_t& buf, // successfully processed. if (ngtcp2_pkt_decode_version_cid( &pversion_cid, vec.base, vec.len, NGTCP2_MAX_CIDLEN) < 0) { + Debug(this, "Failed to decode packet header, ignoring"); return; // Ignore the packet! } @@ -1255,6 +1404,7 @@ void Endpoint::Receive(const uv_buf_t& buf, // with a non-standard CID length. if (UNLIKELY(pversion_cid.dcidlen > NGTCP2_MAX_CIDLEN || pversion_cid.scidlen > NGTCP2_MAX_CIDLEN)) { + Debug(this, "Packet had incorrectly sized CIDs, igoring"); return; // Ignore the packet! } @@ -1270,6 +1420,8 @@ void Endpoint::Receive(const uv_buf_t& buf, CID dcid(pversion_cid.dcid, pversion_cid.dcidlen); CID scid(pversion_cid.scid, pversion_cid.scidlen); + Debug(this, "Packet dcid %s, scid %s", dcid, scid); + // We index the current sessions by the dcid of the client. For initial // packets, the dcid is some random value and the scid is omitted from the // header (it uses what quic calls a "short header"). It is unlikely (but not @@ -1296,12 +1448,14 @@ void Endpoint::Receive(const uv_buf_t& buf, // 4. This is a malicious or malformed packet. if (!session) { // No existing session. + Debug(this, "No existing session for dcid %s", dcid); // 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 (!scid && maybeStatelessReset(dcid, scid, store, addr, remote_address)) { + Debug(this, "Packet was a stateless reset"); return; // Stateless reset! Don't do any further processing. } @@ -1316,12 +1470,14 @@ void Endpoint::Receive(const uv_buf_t& buf, // If we got here, the dcid matched the scid of a known local session. Yay! // The session will take over any further processing of the packet. + Debug(this, "Dispatching packet to known session"); receive(session.get(), std::move(store), addr, remote_address, dcid, scid); } void Endpoint::PacketDone(int status) { if (is_closed()) return; // At this point we should be waiting on at least one packet. + Debug(this, "Packet was sent with status %d", status); DCHECK_GE(state_->pending_callbacks, 1); state_->pending_callbacks--; // Can we go ahead and close now? @@ -1382,6 +1538,7 @@ void Endpoint::EmitNewSession(const BaseObjectPtr& session) { session->set_wrapped(); Local arg = session->object(); + Debug(this, "Notifying JavaScript about new session");; MakeCallback(BindingData::Get(env()).session_new_callback(), 1, &arg); } @@ -1392,6 +1549,7 @@ void Endpoint::EmitClose(CloseContext context, int status) { Local argv[] = {Integer::New(isolate, static_cast(context)), Integer::New(isolate, static_cast(status))}; + Debug(this, "Notifying JavaScript about endpoint closing"); MakeCallback( BindingData::Get(env()).endpoint_close_callback(), arraysize(argv), argv); } diff --git a/src/quic/endpoint.h b/src/quic/endpoint.h index 8694c0eb44a644..1517e48c124162 100644 --- a/src/quic/endpoint.h +++ b/src/quic/endpoint.h @@ -177,6 +177,8 @@ class Endpoint final : public AsyncWrap, public Packet::Listener { static v8::Maybe From(Environment* env, v8::Local value); + + std::string ToString() const; }; bool HasInstance(Environment* env, v8::Local value); diff --git a/src/quic/packet.cc b/src/quic/packet.cc index a25bd9e78180bd..92b9e94eb43618 100644 --- a/src/quic/packet.cc +++ b/src/quic/packet.cc @@ -1,6 +1,7 @@ #if HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC #include "packet.h" +#include "defs.h" #include #include #include @@ -29,6 +30,19 @@ static constexpr size_t kMinStatelessResetLen = 41; static constexpr size_t kMaxFreeList = 100; } // namespace +std::string PathDescriptor::ToString() const { + DebugIndentScope indent; + auto prefix = indent.Prefix(); + std::string res = "{"; + res += prefix + "version: " + std::to_string(version); + res += prefix + "dcid: " + dcid.ToString(); + res += prefix + "scid: " + scid.ToString(); + res += prefix + "local address: " + local_address.ToString(); + res += prefix + "remote address: " + remote_address.ToString(); + res += indent.Close(); + return res; +} + struct Packet::Data final : public MemoryRetainer { MaybeStackBuffer data_; @@ -164,7 +178,9 @@ Packet::Packet(Environment* env, : ReqWrap(env, object, AsyncWrap::PROVIDER_QUIC_PACKET), listener_(listener), destination_(destination), - data_(std::move(data)) {} + data_(std::move(data)) { + Debug(this, "Created a new packet"); +} Packet::Packet(Environment* env, Listener* listener, @@ -184,6 +200,7 @@ int Packet::Send(uv_udp_t* handle, BaseObjectPtr ref) { DCHECK(!is_sending()); handle_ = std::move(ref); uv_buf_t buf = *this; + Debug(this, "Packet it sending"); return Dispatch( uv_udp_send, handle, @@ -199,6 +216,7 @@ int Packet::Send(uv_udp_t* handle, BaseObjectPtr ref) { } void Packet::Done(int status) { + Debug(this, "Packet is done with status %d", status); if (listener_ != nullptr) { listener_->PacketDone(status); } diff --git a/src/quic/packet.h b/src/quic/packet.h index 228f67d1e86187..1e4f3d43472f1e 100644 --- a/src/quic/packet.h +++ b/src/quic/packet.h @@ -26,6 +26,7 @@ struct PathDescriptor { const CID& scid; const SocketAddress& local_address; const SocketAddress& remote_address; + std::string ToString() const; }; // A Packet encapsulates serialized outbound QUIC data. diff --git a/src/quic/quic.cc b/src/quic/quic.cc index 51d0deab151a09..879e16e353d74d 100644 --- a/src/quic/quic.cc +++ b/src/quic/quic.cc @@ -20,6 +20,8 @@ using v8::Value; namespace quic { +int DebugIndentScope::indent_ = 0; + void CreatePerIsolateProperties(IsolateData* isolate_data, Local target) { Endpoint::InitPerIsolate(isolate_data, target); diff --git a/src/quic/session.cc b/src/quic/session.cc index 061ef10424a04b..7fd88353e1b10b 100644 --- a/src/quic/session.cc +++ b/src/quic/session.cc @@ -1,11 +1,10 @@ -#include -#include "quic/tokens.h" #if HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC #include "session.h" #include #include #include +#include #include #include #include @@ -137,6 +136,7 @@ struct Session::MaybeCloseConnectionScope final { MaybeCloseConnectionScope(Session* session_, bool silent_) : session(session_), silent(silent_ || session->connection_close_depth_ > 0) { + Debug(session_, "Entering maybe close connection scope. Silent? %s", silent ? "yes" : "no"); session->connection_close_depth_++; } MaybeCloseConnectionScope(const MaybeCloseConnectionScope&) = delete; @@ -164,6 +164,7 @@ struct Session::MaybeCloseConnectionScope final { Session::SendPendingDataScope::SendPendingDataScope(Session* session) : session(session) { + Debug(session, "Entering send pending data scope"); session->send_scope_depth_++; } @@ -403,6 +404,31 @@ void Session::Options::MemoryInfo(MemoryTracker* tracker) const { tracker->TrackField("cid_factory_ref", cid_factory_ref); } +std::string Session::Options::ToString() const { + DebugIndentScope indent; + auto prefix = indent.Prefix(); + std::string res("{"); + res += prefix + "version: " + std::to_string(version); + res += prefix + "min version: " + std::to_string(min_version); + + auto policy = ([&] { + switch (preferred_address_strategy) { + case PreferredAddress::Policy::USE_PREFERRED_ADDRESS: + return "use"; + case PreferredAddress::Policy::IGNORE_PREFERRED_ADDRESS: + return "ignore"; + } + return ""; + })(); + res += prefix + "preferred address policy: " + std::string(policy); + res += prefix + "transport params: " + transport_params.ToString(); + res += prefix + "crypto options: " + tls_options.ToString(); + res += prefix + "application options: " + application_options.ToString(); + res += prefix + "qlog: " + (qlog ? std::string("yes") : std::string("no")); + res += indent.Close(); + return res; +} + // ============================================================================ bool Session::HasInstance(Environment* env, Local value) { @@ -438,6 +464,9 @@ Session::Session(Endpoint* endpoint, timer_(env(), [this, self = BaseObjectPtr(this)] { OnTimeout(); }) { MakeWeak(); + + Debug(this, "Session created."); + timer_.Unref(); application().ExtendMaxStreams(EndpointLabel::LOCAL, @@ -481,14 +510,17 @@ Session::Session(Endpoint* endpoint, } Session::~Session() { + Debug(this, "Session destroyed."); if (conn_closebuf_) { conn_closebuf_->Done(0); } if (qlog_stream_) { + Debug(this, "Closing the qlog stream for this session"); env()->SetImmediate( [ptr = std::move(qlog_stream_)](Environment*) { ptr->End(); }); } if (keylog_stream_) { + Debug(this, "Closing the keylog stream for this session"); env()->SetImmediate( [ptr = std::move(keylog_stream_)](Environment*) { ptr->End(); }); } @@ -571,6 +603,7 @@ void Session::HandleQlog(uint32_t flags, const void* data, size_t len) { // the deferring is fine). std::vector buffer(len); memcpy(buffer.data(), data, len); + Debug(this, "Emitting qlog data to the qlog stream"); env()->SetImmediate( [ptr = qlog_stream_, buffer = std::move(buffer), flags](Environment*) { ptr->Emit(buffer.data(), @@ -593,6 +626,7 @@ TransportParams Session::GetRemoteTransportParams() const { } void Session::SetLastError(QuicError&& error) { + Debug(this, "Setting last error to %s", error); last_error_ = std::move(error); } @@ -600,19 +634,22 @@ void Session::Close(Session::CloseMethod method) { if (is_destroyed()) return; switch (method) { case CloseMethod::DEFAULT: { + Debug(this, "Closing session"); DoClose(false); break; } case CloseMethod::SILENT: { + Debug(this, "Closing session silently"); DoClose(true); break; } case CloseMethod::GRACEFUL: { if (is_graceful_closing()) return; + Debug(this, "Closing session gracefully"); // If there are no open streams, then we can close just immediately and // not worry about waiting around for the right moment. if (streams_.empty()) { - DoClose(); + DoClose(false); } else { state_->graceful_close = 1; STAT_RECORD_TIMESTAMP(Stats, graceful_closing_at); @@ -624,6 +661,7 @@ void Session::Close(Session::CloseMethod method) { void Session::Destroy() { if (is_destroyed()) return; + Debug(this, "Session destroyed"); // The DoClose() method should have already been called. DCHECK(state_->closing); @@ -686,26 +724,31 @@ bool Session::Receive(Store&& store, uint64_t now = uv_hrtime(); ngtcp2_pkt_info pi{}; // Not used but required. + Debug(this, "Session is receiving packet"); int err = ngtcp2_conn_read_pkt(*this, path, &pi, vec.base, vec.len, now); switch (err) { case 0: { // Return true so we send after receiving. + Debug(this, "Session successfully received packet"); return true; } case NGTCP2_ERR_DRAINING: { // Connection has entered the draining state, no further data should be // sent. This happens when the remote peer has sent a CONNECTION_CLOSE. + Debug(this, "Session is draining"); return false; } case NGTCP2_ERR_CLOSING: { // Connection has entered the closing state, no further data should be // sent. This happens when the local peer has called // ngtcp2_conn_write_connection_close. + Debug(this, "Session is closing"); return false; } case NGTCP2_ERR_CRYPTO: { // Crypto error happened! Set the last error to the tls alert last_error_ = QuicError::ForTlsAlert(ngtcp2_conn_get_tls_alert(*this)); + Debug(this, "Crypto error while receiving packet: %s", last_error_); Close(); return false; } @@ -714,6 +757,7 @@ bool Session::Receive(Store&& store, // validation challenge in the form of a RETRY packet to the peer and // drop the connection. DCHECK(is_server()); + Debug(this, "Server must send a retry packet"); endpoint_->SendRetry(PathDescriptor{ version(), config_.dcid, @@ -726,12 +770,14 @@ bool Session::Receive(Store&& store, } case NGTCP2_ERR_DROP_CONN: { // There's nothing else to do but drop the connection state. + Debug(this, "Session must drop the connection"); Close(CloseMethod::SILENT); return false; } } // Shouldn't happen but just in case. last_error_ = QuicError::ForNgtcp2Error(err); + Debug(this, "Error while receiving packet: %s", last_error_); Close(); return false; }; @@ -756,11 +802,13 @@ void Session::Send(BaseObjectPtr packet) { DCHECK(!is_in_draining_period()); if (can_send_packets() && packet->length() > 0) { + Debug(this, "Session is sending %s", packet->ToString()); STAT_INCREMENT_N(Stats, bytes_sent, packet->length()); endpoint_->Send(std::move(packet)); return; } + Debug(this, "Session could not send %s", packet->ToString()); packet->Done(packet->length() > 0 ? UV_ECANCELED : 0); } @@ -774,9 +822,11 @@ uint64_t Session::SendDatagram(Store&& data) { uint64_t max_datagram_size = tp->max_datagram_frame_size; if (max_datagram_size == 0 || data.length() > max_datagram_size) { // Datagram is too large. + Debug(this, "Data is too large to send as a datagram"); return 0; } + Debug(this, "Session is sending datagram"); BaseObjectPtr packet; uint8_t* pos = nullptr; int accepted = 0; @@ -876,6 +926,7 @@ uint64_t Session::SendDatagram(Store&& data) { if (accepted != 0) { // Yay! The datagram was accepted into the packet we just sent and we can // return the datagram ID. + Debug(this, "Session successfully encoded datagram"); STAT_INCREMENT(Stats, datagrams_sent); STAT_INCREMENT_N(Stats, bytes_sent, vec.len); state_->last_datagram_id = did; @@ -885,6 +936,7 @@ uint64_t Session::SendDatagram(Store&& data) { // We sent a packet, but it wasn't the datagram packet. That can happen. // Let's loop around and try again. if (++attempts == kMaxAttempts) { + Debug(this, "Too many attempts to send the datagram"); // Too many attempts to send the datagram. break; } @@ -896,6 +948,7 @@ uint64_t Session::SendDatagram(Store&& data) { void Session::UpdatePath(const PathStorage& storage) { remote_address_.Update(storage.path.remote.addr, storage.path.remote.addrlen); local_address_.Update(storage.path.local.addr, storage.path.local.addrlen); + Debug(this, "path updated. local %s, remote %s", local_address_, remote_address_); } BaseObjectPtr Session::FindStream(int64_t id) const { @@ -914,19 +967,24 @@ BaseObjectPtr Session::OpenStream(Direction direction) { if (!can_create_streams()) return BaseObjectPtr(); int64_t id; switch (direction) { - case Direction::BIDIRECTIONAL: + case Direction::BIDIRECTIONAL: { + Debug(this, "Opening bidirectional stream"); if (ngtcp2_conn_open_bidi_stream(*this, &id, nullptr) == 0) return CreateStream(id); break; - case Direction::UNIDIRECTIONAL: + } + case Direction::UNIDIRECTIONAL: { + Debug(this, "Opening uni-directional stream"); if (ngtcp2_conn_open_uni_stream(*this, &id, nullptr) == 0) return CreateStream(id); break; + } } return BaseObjectPtr(); } void Session::AddStream(const BaseObjectPtr& stream) { + Debug(this, "Adding stream %" PRIi64 " to session", stream->id()); ngtcp2_conn_set_stream_user_data(*this, stream->id(), stream.get()); streams_[stream->id()] = stream; @@ -984,6 +1042,7 @@ void Session::RemoveStream(int64_t id) { // ngtcp2 does not 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. + Debug(this, "Removing stream " PRIi64 " from session", id); if (!is_in_draining_period() && !is_in_closing_period() && !state_->silent_close && !ngtcp2_conn_is_local_stream(connection_.get(), id)) { @@ -1000,11 +1059,13 @@ void Session::RemoveStream(int64_t id) { } void Session::ResumeStream(int64_t id) { + Debug(this, "Resuming stream %" PRIi64, id); SendPendingDataScope send_scope(this); application_->ResumeStream(id); } void Session::ShutdownStream(int64_t id, QuicError error) { + Debug(this, "Shutting down stream %" PRIi64 " with error %s", id, error); SendPendingDataScope send_scope(this); ngtcp2_conn_shutdown_stream(*this, 0, @@ -1015,11 +1076,13 @@ void Session::ShutdownStream(int64_t id, QuicError error) { } void Session::StreamDataBlocked(int64_t id) { + Debug(this, "Stream %" PRIi64 " is blocked", id); STAT_INCREMENT(Stats, block_count); application_->BlockStream(id); } void Session::ShutdownStreamWrite(int64_t id, QuicError code) { + Debug(this, "Shutting down stream %" PRIi64 " write with error %s", id, code); SendPendingDataScope send_scope(this); ngtcp2_conn_shutdown_stream_write(*this, 0, @@ -1103,6 +1166,7 @@ void Session::set_wrapped() { void Session::DoClose(bool silent) { DCHECK(!is_destroyed()); + Debug(this, "Session is closing. Silently %s", silent ? "yes" : "no"); // Once Close has been called, we cannot re-enter if (state_->closing == 1) return; state_->closing = 1; @@ -1133,15 +1197,18 @@ void Session::DoClose(bool silent) { } void Session::ExtendStreamOffset(int64_t id, size_t amount) { + Debug(this, "Extending stream %" PRIi64 " offset by %zu", id, amount); ngtcp2_conn_extend_max_stream_offset(*this, id, amount); } void Session::ExtendOffset(size_t amount) { + Debug(this, "Extending offset by %zu", amount); ngtcp2_conn_extend_max_offset(*this, amount); } void Session::UpdateDataStats() { if (state_->destroyed) return; + Debug(this, "Updating data stats"); ngtcp2_conn_info info; ngtcp2_conn_get_conn_info(*this, &info); STAT_SET(Stats, bytes_in_flight, info.bytes_in_flight); @@ -1161,6 +1228,7 @@ void Session::SendConnectionClose() { DCHECK(!NgTcp2CallbackScope::in_ngtcp2_callback(env())); if (is_destroyed() || is_in_draining_period() || state_->silent_close) return; + Debug(this, "Sending connection close"); auto on_exit = OnScopeLeave([this] { UpdateTimer(); }); switch (config_.side) { @@ -1209,6 +1277,7 @@ void Session::OnTimeout() { return; } + Debug(this, "Session timed out"); last_error_ = QuicError::ForNgtcp2Error(ret); Close(CloseMethod::SILENT); } @@ -1217,6 +1286,7 @@ void Session::UpdateTimer() { // Both uv_hrtime and ngtcp2_conn_get_expiry return nanosecond units. uint64_t expiry = ngtcp2_conn_get_expiry(*this); uint64_t now = uv_hrtime(); + Debug(this, "Updating timer. Expiry: %" PRIu64 ", now: %" PRIu64, expiry, now); if (expiry <= now) { // The timer has already expired. @@ -1234,6 +1304,8 @@ bool Session::StartClosingPeriod() { if (is_in_closing_period()) return true; if (is_destroyed()) return false; + Debug(this, "Session is entering closing period"); + conn_closebuf_ = Packet::CreateConnectionClosePacket( env(), endpoint_.get(), remote_address_, *this, last_error_); @@ -1250,12 +1322,16 @@ bool Session::StartClosingPeriod() { void Session::DatagramStatus(uint64_t datagramId, quic::DatagramStatus status) { switch (status) { - case quic::DatagramStatus::ACKNOWLEDGED: + case quic::DatagramStatus::ACKNOWLEDGED: { + Debug(this, "Datagram %" PRIu64 " was acknowledged", datagramId); STAT_INCREMENT(Stats, datagrams_acknowledged); break; - case quic::DatagramStatus::LOST: + } + case quic::DatagramStatus::LOST: { + Debug(this, "Datagram %" PRIu64 " was lost", datagramId); STAT_INCREMENT(Stats, datagrams_lost); break; + } } EmitDatagramStatus(datagramId, status); } @@ -1268,6 +1344,7 @@ void Session::DatagramReceived(const uint8_t* data, if (state_->datagram == 0 || datalen == 0) return; auto backing = ArrayBuffer::NewBackingStore(env()->isolate(), datalen); + Debug(this, "Session is receiving datagram of size %zu", datalen); memcpy(backing->Data(), data, datalen); STAT_INCREMENT(Stats, datagrams_received); STAT_INCREMENT_N(Stats, bytes_received, datalen); @@ -1278,6 +1355,7 @@ bool Session::GenerateNewConnectionId(ngtcp2_cid* cid, size_t len, uint8_t* token) { CID cid_ = config_.options.cid_factory->Generate(len); + Debug(this, "Generated new connection id %s", cid_); StatelessResetToken new_token( token, endpoint_->options().reset_token_secret, cid_); endpoint_->AssociateCID(cid_, config_.scid); @@ -1288,6 +1366,8 @@ bool Session::GenerateNewConnectionId(ngtcp2_cid* cid, bool Session::HandshakeCompleted() { if (state_->handshake_completed) return false; state_->handshake_completed = true; + + Debug(this, "Session handshake completed"); STAT_RECORD_TIMESTAMP(Stats, handshake_completed_at); if (!tls_context_.early_data_was_accepted()) @@ -1316,6 +1396,9 @@ bool Session::HandshakeCompleted() { void Session::HandshakeConfirmed() { if (state_->handshake_confirmed) return; + + Debug(this, "Session handshake confirmed"); + state_->handshake_confirmed = true; STAT_RECORD_TIMESTAMP(Stats, handshake_confirmed_at); } @@ -1323,6 +1406,7 @@ void Session::HandshakeConfirmed() { void Session::SelectPreferredAddress(PreferredAddress* preferredAddress) { if (config_.options.preferred_address_strategy == PreferredAddress::Policy::IGNORE_PREFERRED_ADDRESS) { + Debug(this, "Ignoring preferred address"); return; } @@ -1331,6 +1415,7 @@ void Session::SelectPreferredAddress(PreferredAddress* preferredAddress) { switch (family) { case AF_INET: { + Debug(this, "Selecting preferred address for AF_INET"); auto ipv4 = preferredAddress->ipv4(); if (ipv4.has_value()) { if (ipv4->address.empty() || ipv4->port == 0) return; @@ -1343,6 +1428,7 @@ void Session::SelectPreferredAddress(PreferredAddress* preferredAddress) { break; } case AF_INET6: { + Debug(this, "Selecting preferred address for AF_INET6"); auto ipv6 = preferredAddress->ipv6(); if (ipv6.has_value()) { if (ipv6->address.empty() || ipv6->port == 0) return; @@ -1377,6 +1463,7 @@ void Session::EmitClose(const QuicError& error) { !ToV8Value(env()->context(), error.reason()).ToLocal(&argv[2])) { return; } + Debug(this, "Notifying JavaScript of session close"); MakeCallback( BindingData::Get(env()).session_close_callback(), arraysize(argv), argv); } @@ -1390,6 +1477,7 @@ void Session::EmitDatagram(Store&& datagram, DatagramReceivedFlags flag) { Local argv[] = {datagram.ToUint8Array(env()), v8::Boolean::New(env()->isolate(), flag.early)}; + Debug(this, "Notifying JavaScript of datagram"); MakeCallback(BindingData::Get(env()).session_datagram_callback(), arraysize(argv), argv); @@ -1414,6 +1502,7 @@ void Session::EmitDatagramStatus(uint64_t id, quic::DatagramStatus status) { Local argv[] = {BigInt::NewFromUnsigned(env()->isolate(), id), status_to_string}; + Debug(this, "Notifying JavaScript of datagram status"); MakeCallback(state.session_datagram_status_callback(), arraysize(argv), argv); } @@ -1459,6 +1548,7 @@ void Session::EmitHandshakeComplete() { return; } + Debug(this, "Notifying JavaScript of handshake complete"); MakeCallback(BindingData::Get(env()).session_handshake_callback(), arraysize(argv), argv); @@ -1501,6 +1591,7 @@ void Session::EmitPathValidation(PathValidationResult result, argv[4] = SocketAddressBase::Create(env(), oldPath->remote)->object(); } + Debug(this, "Notifying JavaScript of path validation"); MakeCallback(state.session_path_validation_callback(), arraysize(argv), argv); } @@ -1521,8 +1612,10 @@ void Session::EmitSessionTicket(Store&& ticket) { SessionTicket session_ticket(std::move(ticket), std::move(transport_params)); Local argv; - if (session_ticket.encode(env()).ToLocal(&argv)) + if (session_ticket.encode(env()).ToLocal(&argv)) { + Debug(this, "Notifying JavaScript of session ticket"); MakeCallback(BindingData::Get(env()).session_ticket_callback(), 1, &argv); + } } void Session::EmitStream(BaseObjectPtr stream) { @@ -1531,6 +1624,7 @@ void Session::EmitStream(BaseObjectPtr stream) { CallbackScope cb_scope(this); Local arg = stream->object(); + Debug(this, "Notifying JavaScript of stream created"); MakeCallback(BindingData::Get(env()).stream_created_callback(), 1, &arg); } @@ -1567,6 +1661,7 @@ void Session::EmitVersionNegotiation(const ngtcp2_pkt_hd& hd, // The versions we actually support. Array::New(isolate, supported, arraysize(supported))}; + Debug(this, "Notifying JavaScript of version negotiation"); MakeCallback(BindingData::Get(env()).session_version_negotiation_callback(), arraysize(argv), argv); @@ -1575,6 +1670,7 @@ void Session::EmitVersionNegotiation(const ngtcp2_pkt_hd& hd, void Session::EmitKeylog(const char* line) { if (!env()->can_call_into_js()) return; if (keylog_stream_) { + Debug(this, "Emitting keylog line"); env()->SetImmediate([ptr = keylog_stream_, data = std::string(line) + "\n"]( Environment* env) { ptr->Emit(data); }); } diff --git a/src/quic/session.h b/src/quic/session.h index c3d5e2ef77bff3..94de91d3bd6a0d 100644 --- a/src/quic/session.h +++ b/src/quic/session.h @@ -86,6 +86,8 @@ class Session final : public AsyncWrap, private SessionTicket::AppData::Source { static v8::Maybe From(Environment* env, v8::Local value); + std::string ToString() const; + static const Application_Options kDefault; }; @@ -129,6 +131,8 @@ class Session final : public AsyncWrap, private SessionTicket::AppData::Source { static v8::Maybe From(Environment* env, v8::Local value); + + std::string ToString() const; }; // The additional configuration settings used to create a specific session. @@ -197,6 +201,11 @@ class Session final : public AsyncWrap, private SessionTicket::AppData::Source { void MemoryInfo(MemoryTracker* tracker) const override; SET_MEMORY_INFO_NAME(Session::Config) SET_SELF_SIZE(Config) + + std::string ToString() const { + // TODO(@jasnell) + return std::string("{}"); + } }; static bool HasInstance(Environment* env, v8::Local value); diff --git a/src/quic/tlscontext.cc b/src/quic/tlscontext.cc index d60e4a7dbf3cea..2dd956c000277d 100644 --- a/src/quic/tlscontext.cc +++ b/src/quic/tlscontext.cc @@ -2,6 +2,8 @@ #include "tlscontext.h" #include +#include +#include #include #include #include @@ -386,6 +388,7 @@ TLSContext::TLSContext(Environment* env, } void TLSContext::Start() { + Debug(session_, "Crypto context is starting"); ngtcp2_conn_set_tls_native_handle(*session_, ssl_.get()); TransportParams tp(ngtcp2_conn_get_local_transport_params(*session_)); @@ -404,6 +407,7 @@ int TLSContext::Receive(TLSContext::EncryptionLevel level, uint64_t offset, const uint8_t* data, size_t datalen) { + Debug(session_, "Crypto context received data"); // ngtcp2 provides an implementation of this in // ngtcp2_crypto_recv_crypto_data_cb but given that we are using the // implementation specific error codes below, we can't use it. @@ -431,6 +435,7 @@ int TLSContext::Receive(TLSContext::EncryptionLevel level, } int TLSContext::OnNewSession(SSL_SESSION* session) { + Debug(session_, "Crypto context received new crypto session"); // Used to generate and emit a SessionTicket for TLS session resumption. // If there is nothing listening for the session ticket, don't both emitting. @@ -455,6 +460,7 @@ int TLSContext::OnNewSession(SSL_SESSION* session) { } bool TLSContext::InitiateKeyUpdate() { + Debug(session_, "Crypto context initiating key update"); if (session_->is_destroyed() || in_key_update_) return false; auto leave = OnScopeLeave([this] { in_key_update_ = false; }); in_key_update_ = true; @@ -463,10 +469,12 @@ bool TLSContext::InitiateKeyUpdate() { } int TLSContext::VerifyPeerIdentity() { + Debug(session_, "Crypto context verifying peer identity"); return crypto::VerifyPeerCertificate(ssl_); } void TLSContext::MaybeSetEarlySession(const SessionTicket& sessionTicket) { + Debug(session_, "Crypto context setting early session"); uv_buf_t buf = sessionTicket.ticket(); crypto::SSLSessionPointer ticket = crypto::GetTLSSession( reinterpret_cast(buf.base), buf.len); @@ -590,6 +598,34 @@ Maybe TLSContext::Options::From(Environment* env, return Just(options); } +std::string TLSContext::Options::ToString() const { + DebugIndentScope indent; + auto prefix = indent.Prefix(); + std::string res("{"); + res += prefix + "alpn: " + alpn; + res += prefix + "hostname: " + hostname; + res += prefix + "keylog: " + (keylog ? std::string("yes") : std::string("no")); + res += prefix + "reject_unauthorized: " + + (reject_unauthorized ? std::string("yes") : std::string("no")); + res += prefix + "enable_tls_trace: " + + (enable_tls_trace ? std::string("yes") : std::string("no")); + res += prefix + "request_peer_certificate: " + (request_peer_certificate + ? std::string("yes") + : std::string("no")); + res += prefix + "verify_hostname_identity: " + (verify_hostname_identity + ? std::string("yes") + : std::string("no")); + res += prefix + "session_id_ctx: " + session_id_ctx; + res += prefix + "ciphers: " + ciphers; + res += prefix + "groups: " + groups; + res += prefix + "keys: " + std::to_string(keys.size()); + res += prefix + "certs: " + std::to_string(certs.size()); + res += prefix + "ca: " + std::to_string(ca.size()); + res += prefix + "crl: " + std::to_string(crl.size()); + res += indent.Close(); + return res; +} + } // namespace quic } // namespace node diff --git a/src/quic/tlscontext.h b/src/quic/tlscontext.h index b163a68cfa3e73..8c638260ce6004 100644 --- a/src/quic/tlscontext.h +++ b/src/quic/tlscontext.h @@ -1,5 +1,6 @@ #pragma once +#include #if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS #if HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC @@ -100,6 +101,8 @@ class TLSContext final : public MemoryRetainer { static v8::Maybe From(Environment* env, v8::Local value); + + std::string ToString() const; }; static const Options kDefaultOptions; diff --git a/src/quic/tokens.cc b/src/quic/tokens.cc index dc14be55723e89..c8a1f23153a0c4 100644 --- a/src/quic/tokens.cc +++ b/src/quic/tokens.cc @@ -43,6 +43,18 @@ uint8_t TokenSecret::operator[](int pos) const { return buf_[pos]; } +TokenSecret::operator const char*() const { + return reinterpret_cast(buf_); +} + +std::string TokenSecret::ToString() const { + char dest[QUIC_TOKENSECRET_LEN * 2]; + size_t written = + StringBytes::hex_encode(*this, QUIC_TOKENSECRET_LEN, dest, arraysize(dest)); + DCHECK_EQ(written, arraysize(dest)); + return std::string(dest, written); +} + // ============================================================================ // StatelessResetToken @@ -214,6 +226,23 @@ RetryToken::operator const ngtcp2_vec*() const { return &ptr_; } +std::string RetryToken::ToString() const { + if (ptr_.base == nullptr) return std::string(); + MaybeStackBuffer dest(ptr_.len * 2); + size_t written = + StringBytes::hex_encode(*this, ptr_.len, dest.out(), dest.length()); + DCHECK_EQ(written, arraysize(dest)); + return std::string(dest.out(), written); +} + +RetryToken::operator const char*() const { + return reinterpret_cast(ptr_.base); +} + +RetryToken::operator bool() const { + return ptr_.base != nullptr && ptr_.len > 0; +} + RegularToken::RegularToken() : buf_(), ptr_(ngtcp2_vec{nullptr, 0}) {} RegularToken::RegularToken(uint32_t version, @@ -256,6 +285,19 @@ RegularToken::operator const ngtcp2_vec*() const { return &ptr_; } +std::string RegularToken::ToString() const { + if (ptr_.base == nullptr) return std::string(); + MaybeStackBuffer dest(ptr_.len * 2); + size_t written = + StringBytes::hex_encode(*this, ptr_.len, dest.out(), dest.length()); + DCHECK_EQ(written, arraysize(dest)); + return std::string(dest.out(), written); +} + +RegularToken::operator const char*() const { + return reinterpret_cast(ptr_.base); +} + } // namespace quic } // namespace node diff --git a/src/quic/tokens.h b/src/quic/tokens.h index d6ebe34a12dc7f..c66f898429d651 100644 --- a/src/quic/tokens.h +++ b/src/quic/tokens.h @@ -41,11 +41,14 @@ class TokenSecret final : public MemoryRetainer { operator const uint8_t*() const; uint8_t operator[](int pos) const; + std::string ToString() const; + SET_NO_MEMORY_INFO() SET_MEMORY_INFO_NAME(TokenSecret) SET_SELF_SIZE(TokenSecret) private: + operator const char*() const; uint8_t buf_[QUIC_TOKENSECRET_LEN]; }; @@ -183,12 +186,16 @@ class RetryToken final : public MemoryRetainer { operator const ngtcp2_vec&() const; operator const ngtcp2_vec*() const; + operator bool() const; + + std::string ToString() const; SET_NO_MEMORY_INFO() SET_MEMORY_INFO_NAME(RetryToken) SET_SELF_SIZE(RetryToken) private: + operator const char*() const; uint8_t buf_[kRetryTokenLen]; const ngtcp2_vec ptr_; }; @@ -232,11 +239,14 @@ class RegularToken final : public MemoryRetainer { operator bool() const; + std::string ToString() const; + SET_NO_MEMORY_INFO() SET_MEMORY_INFO_NAME(RetryToken) SET_SELF_SIZE(RetryToken) private: + operator const char*() const; uint8_t buf_[kRegularTokenLen]; const ngtcp2_vec ptr_; }; diff --git a/src/quic/transportparams.cc b/src/quic/transportparams.cc index 792396df499543..190b5b82a15f4e 100644 --- a/src/quic/transportparams.cc +++ b/src/quic/transportparams.cc @@ -71,6 +71,47 @@ Maybe TransportParams::Options::From( return Just(options); } +std::string TransportParams::Options::ToString() const { + DebugIndentScope indent; + auto prefix = indent.Prefix(); + std::string res("{"); + res += prefix + "version: " + std::to_string(transportParamsVersion); + if (preferred_address_ipv4.has_value()) { + res += prefix + "preferred_address_ipv4: " + + preferred_address_ipv4.value().ToString(); + } else { + res += prefix + "preferred_address_ipv4: "; + } + if (preferred_address_ipv6.has_value()) { + res += prefix + "preferred_address_ipv6: " + + preferred_address_ipv6.value().ToString(); + } else { + res += prefix + "preferred_address_ipv6: "; + } + res += prefix + "initial max stream data bidi local: " + + std::to_string(initial_max_stream_data_bidi_local); + res += prefix + "initial max stream data bidi remote: " + + std::to_string(initial_max_stream_data_bidi_remote); + res += prefix + "initial max stream data uni: " + + std::to_string(initial_max_stream_data_uni); + res += prefix + "tinitial max data: " + std::to_string(initial_max_data); + res += prefix + "initial max streams bidi: " + + std::to_string(initial_max_streams_bidi); + res += prefix + "initial max streams uni: " + + std::to_string(initial_max_streams_uni); + res += prefix + "max idle timeout: " + std::to_string(max_idle_timeout); + res += prefix + "active connection id limit: " + + std::to_string(active_connection_id_limit); + res += prefix + "ack delay exponent: " + std::to_string(ack_delay_exponent); + res += prefix + "max ack delay: " + std::to_string(max_ack_delay); + res += prefix + "max datagram frame size: " + + std::to_string(max_datagram_frame_size); + res += prefix + "disable active migration: " + + (disable_active_migration ? std::string("yes") : std::string("no")); + res += indent.Close(); + return res; +} + void TransportParams::Options::MemoryInfo(MemoryTracker* tracker) const { if (preferred_address_ipv4.has_value()) { tracker->TrackField("preferred_address_ipv4", diff --git a/src/quic/transportparams.h b/src/quic/transportparams.h index 237927e7cd3ed9..349ddd5c948bdc 100644 --- a/src/quic/transportparams.h +++ b/src/quic/transportparams.h @@ -118,6 +118,8 @@ class TransportParams final { static v8::Maybe From(Environment* env, v8::Local value); + + std::string ToString() const; }; explicit TransportParams(); From 2057b2b8040bd8495748a40aaf0129d211ecb223 Mon Sep 17 00:00:00 2001 From: James M Snell Date: Thu, 28 Dec 2023 11:34:11 -0800 Subject: [PATCH 3/8] quic: get more of the quic impl working --- src/quic/defs.h | 4 +++- src/quic/endpoint.cc | 2 +- src/quic/session.cc | 37 ++++++++++++++++++++++++++++++++++++- src/quic/session.h | 7 ++----- 4 files changed, 42 insertions(+), 8 deletions(-) diff --git a/src/quic/defs.h b/src/quic/defs.h index 6eced43f0f7733..9678ecd1f8de44 100644 --- a/src/quic/defs.h +++ b/src/quic/defs.h @@ -37,7 +37,9 @@ bool SetOption(Environment* env, const v8::Local& name) { v8::Local value; if (!object->Get(env->context(), name).ToLocal(&value)) return false; - options->*member = value->BooleanValue(env->isolate()); + if (!value->IsUndefined()) { + options->*member = value->BooleanValue(env->isolate()); + } return true; } diff --git a/src/quic/endpoint.cc b/src/quic/endpoint.cc index 0c9ddf7d8e11be..15a23d4647e340 100644 --- a/src/quic/endpoint.cc +++ b/src/quic/endpoint.cc @@ -704,7 +704,7 @@ RegularToken Endpoint::GenerateNewToken(uint32_t version, StatelessResetToken Endpoint::GenerateNewStatelessResetToken( uint8_t* token, const CID& cid) const { IF_QUIC_DEBUG(env()) { - Debug(const_cast(this), "Generating new stateless reset tokek for CID %s", + Debug(const_cast(this), "Generating new stateless reset token for CID %s", cid); } DCHECK(!is_closed() && !is_closing()); diff --git a/src/quic/session.cc b/src/quic/session.cc index 7fd88353e1b10b..0462acc50fe77b 100644 --- a/src/quic/session.cc +++ b/src/quic/session.cc @@ -366,6 +366,39 @@ void Session::Config::set_token(const RegularToken& token) { set_token(vec.base, vec.len, NGTCP2_TOKEN_TYPE_NEW_TOKEN); } +std::string Session::Config::ToString() const { + DebugIndentScope indent; + auto prefix = indent.Prefix(); + std::string res("{"); + + auto sidestr = ([&]{ + switch (side) { + case Side::CLIENT: return "client"; + case Side::SERVER: return "server"; + } + return ""; + })(); + res += prefix + "side: " + std::string(sidestr); + res += prefix + "options: " + options.ToString(); + res += prefix + "version: " + std::to_string(version); + res += prefix + "local address: " + local_address.ToString(); + res += prefix + "remote address: " + remote_address.ToString(); + res += prefix + "dcid: " + dcid.ToString(); + res += prefix + "scid: " + scid.ToString(); + res += prefix + "ocid: " + ocid.ToString(); + res += prefix + "retry scid: " + retry_scid.ToString(); + res += prefix + "preferred address cid: " + preferred_address_cid.ToString(); + + if (session_ticket.has_value()) { + res += prefix + "session ticket: yes"; + } else { + res += prefix + "session ticket: "; + } + + res += indent.Close(); + return res; +} + // ============================================================================ Maybe Session::Options::From(Environment* env, @@ -454,9 +487,10 @@ Session::Session(Endpoint* endpoint, : AsyncWrap(endpoint->env(), object, AsyncWrap::PROVIDER_QUIC_SESSION), stats_(env()->isolate()), state_(env()->isolate()), + allocator_(BindingData::Get(env())), + endpoint_(BaseObjectWeakPtr(endpoint)), config_(config), connection_(InitConnection()), - endpoint_(BaseObjectWeakPtr(endpoint)), tls_context_(env(), config_.side, this, config_.options.tls_options), application_(select_application()), local_address_(config.local_address), @@ -2243,6 +2277,7 @@ Session::QuicConnectionPointer Session::InitConnection() { config_.side, config_.ocid, config_.retry_scid); TransportParams transport_params(tp_config, config_.options.transport_params); transport_params.GenerateSessionTokens(this); + switch (config_.side) { case Side::SERVER: { CHECK_EQ(ngtcp2_conn_server_new(&conn, diff --git a/src/quic/session.h b/src/quic/session.h index 94de91d3bd6a0d..a6f6fe2b4c3c8a 100644 --- a/src/quic/session.h +++ b/src/quic/session.h @@ -202,10 +202,7 @@ class Session final : public AsyncWrap, private SessionTicket::AppData::Source { SET_MEMORY_INFO_NAME(Session::Config) SET_SELF_SIZE(Config) - std::string ToString() const { - // TODO(@jasnell) - return std::string("{}"); - } + std::string ToString() const; }; static bool HasInstance(Environment* env, v8::Local value); @@ -412,9 +409,9 @@ class Session final : public AsyncWrap, private SessionTicket::AppData::Source { AliasedStruct stats_; AliasedStruct state_; ngtcp2_mem allocator_; + BaseObjectWeakPtr endpoint_; Config config_; QuicConnectionPointer connection_; - BaseObjectWeakPtr endpoint_; TLSContext tls_context_; std::unique_ptr application_; SocketAddress local_address_; From 1ae7f4bdcd69192ced0768e2c48bc4613eb446b1 Mon Sep 17 00:00:00 2001 From: James M Snell Date: Thu, 28 Dec 2023 13:01:05 -0800 Subject: [PATCH 4/8] quic: add scaffolding for http3 internals --- node.gyp | 2 + src/quic/application.cc | 69 +-- src/quic/application.h | 16 + src/quic/defs.h | 3 +- src/quic/endpoint.cc | 145 ++++--- src/quic/endpoint.h | 2 +- src/quic/http3.cc | 824 ++++++++++++++++++++++++++++++++++++ src/quic/http3.h | 18 + src/quic/packet.cc | 2 +- src/quic/session.cc | 38 +- src/quic/session.h | 76 ++-- src/quic/streams.cc | 4 + src/quic/streams.h | 7 +- src/quic/tlscontext.cc | 15 +- src/quic/tokens.cc | 4 +- src/quic/transportparams.cc | 8 +- 16 files changed, 1079 insertions(+), 154 deletions(-) create mode 100644 src/quic/http3.cc create mode 100644 src/quic/http3.h diff --git a/node.gyp b/node.gyp index a89b467e84a96f..456fe8285332d1 100644 --- a/node.gyp +++ b/node.gyp @@ -354,6 +354,7 @@ 'src/quic/cid.cc', 'src/quic/data.cc', 'src/quic/endpoint.cc', + 'src/quic/http3.cc', 'src/quic/logstream.cc', 'src/quic/packet.cc', 'src/quic/preferredaddress.cc', @@ -368,6 +369,7 @@ 'src/quic/cid.h', 'src/quic/data.h', 'src/quic/endpoint.h', + 'src/quic/http3.h', 'src/quic/logstream.h', 'src/quic/packet.h', 'src/quic/preferredaddress.h', diff --git a/src/quic/application.cc b/src/quic/application.cc index 1ed55b786df0b6..34b04cd4e38f3e 100644 --- a/src/quic/application.cc +++ b/src/quic/application.cc @@ -1,14 +1,15 @@ #if HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC #include "application.h" -#include #include +#include #include #include #include #include #include "defs.h" #include "endpoint.h" +#include "http3.h" #include "packet.h" #include "session.h" @@ -23,19 +24,6 @@ using v8::Value; namespace quic { -struct Session::Application::StreamData final { - // The actual number of vectors in the struct, up to kMaxVectorCount. - size_t count = 0; - size_t remaining = 0; - // The stream identifier. If this is a negative value then no stream is - // identified. - int64_t id = -1; - int fin = 0; - ngtcp2_vec data[kMaxVectorCount]{}; - ngtcp2_vec* buf = data; - BaseObjectPtr stream; -}; - // ============================================================================ // Session::Application_Options const Session::Application_Options Session::Application_Options::kDefault = {}; @@ -43,13 +31,13 @@ const Session::Application_Options Session::Application_Options::kDefault = {}; Session::Application_Options::operator const nghttp3_settings() const { // In theory, Application_Options might contain options for more than just // HTTP/3. Here we extract only the properties that are relevant to HTTP/3. - return nghttp3_settings { - max_field_section_size, - qpack_max_dtable_capacity, - qpack_encoder_max_dtable_capacity, - qpack_blocked_streams, - enable_connect_protocol, - enable_datagrams, + return nghttp3_settings{ + max_field_section_size, + qpack_max_dtable_capacity, + qpack_encoder_max_dtable_capacity, + qpack_blocked_streams, + enable_connect_protocol, + enable_datagrams, }; } @@ -59,13 +47,14 @@ std::string Session::Application_Options::ToString() const { std::string res("{"); res += prefix + "max header pairs: " + std::to_string(max_header_pairs); res += prefix + "max header length: " + std::to_string(max_header_length); - res += prefix + "max field section size: " + - std::to_string(max_field_section_size); + res += prefix + + "max field section size: " + std::to_string(max_field_section_size); res += prefix + "qpack max dtable capacity: " + std::to_string(qpack_max_dtable_capacity); res += prefix + "qpack encoder max dtable capacity: " + std::to_string(qpack_encoder_max_dtable_capacity); - res += prefix + "qpack blocked streams: " + std::to_string(qpack_blocked_streams); + res += prefix + + "qpack blocked streams: " + std::to_string(qpack_blocked_streams); res += prefix + "enable connect protocol: " + (enable_connect_protocol ? std::string("yes") : std::string("no")); res += prefix + "enable datagrams: " + @@ -118,7 +107,10 @@ bool Session::Application::Start() { void Session::Application::AcknowledgeStreamData(Stream* stream, size_t datalen) { - Debug(session_, "Application acknowledging stream %" PRIi64 " data: %zu", stream->id(), datalen); + Debug(session_, + "Application acknowledging stream %" PRIi64 " data: %zu", + stream->id(), + datalen); DCHECK_NOT_NULL(stream); stream->Acknowledge(datalen); } @@ -184,7 +176,8 @@ Session::Application::ExtractSessionTicketAppData( void Session::Application::SetStreamPriority(const Stream& stream, StreamPriority priority, StreamPriorityFlags flags) { - Debug(session_, "Application setting stream %" PRIi64 " priority", stream.id()); + Debug( + session_, "Application setting stream %" PRIi64 " priority", stream.id()); // By default do nothing. } @@ -201,12 +194,18 @@ BaseObjectPtr Session::Application::CreateStreamDataPacket() { } void Session::Application::StreamClose(Stream* stream, QuicError error) { - Debug(session_, "Application closing stream %" PRIi64 " with error %s", stream->id(), error); + Debug(session_, + "Application closing stream %" PRIi64 " with error %s", + stream->id(), + error); stream->Destroy(error); } void Session::Application::StreamStopSending(Stream* stream, QuicError error) { - Debug(session_, "Application stopping sending on stream %" PRIi64 " with error %s", stream->id(), error); + Debug(session_, + "Application stopping sending on stream %" PRIi64 " with error %s", + stream->id(), + error); DCHECK_NOT_NULL(stream); stream->ReceiveStopSending(error); } @@ -214,7 +213,10 @@ void Session::Application::StreamStopSending(Stream* stream, QuicError error) { void Session::Application::StreamReset(Stream* stream, uint64_t final_size, QuicError error) { - Debug(session_, "Application resetting stream %" PRIi64 " with error %s", stream->id(), error); + Debug(session_, + "Application resetting stream %" PRIi64 " with error %s", + stream->id(), + error); stream->ReceiveStreamReset(final_size, error); } @@ -498,13 +500,14 @@ class DefaultApplication final : public Session::Application { }; std::unique_ptr Session::select_application() { - // if (config.options.crypto_options.alpn == NGHTTP3_ALPN_H3) - // return std::make_unique(session, - // config.options.application_options); - // In the future, we may end up supporting additional QUIC protocols. As they // are added, extend the cases here to create and return them. + if (config_.options.tls_options.alpn == NGHTTP3_ALPN_H3) { + Debug(this, "Selecting HTTP/3 application"); + return createHttp3Application(this, config_.options.application_options); + } + Debug(this, "Selecting default application"); return std::make_unique( this, config_.options.application_options); diff --git a/src/quic/application.h b/src/quic/application.h index af64d7ffca026e..ce74985a9c03fc 100644 --- a/src/quic/application.h +++ b/src/quic/application.h @@ -118,6 +118,7 @@ class Session::Application : public MemoryRetainer { protected: inline Environment* env() const { return session_->env(); } inline Session& session() { return *session_; } + inline const Session& session() const { return *session_; } BaseObjectPtr CreateStreamDataPacket(); @@ -137,6 +138,21 @@ class Session::Application : public MemoryRetainer { Session* session_; }; +struct Session::Application::StreamData final { + // The actual number of vectors in the struct, up to kMaxVectorCount. + size_t count = 0; + size_t remaining = 0; + // The stream identifier. If this is a negative value then no stream is + // identified. + int64_t id = -1; + int fin = 0; + ngtcp2_vec data[kMaxVectorCount]{}; + ngtcp2_vec* buf = data; + BaseObjectPtr stream; + + inline operator nghttp3_vec() const { return {data[0].base, data[0].len}; } +}; + } // namespace quic } // namespace node diff --git a/src/quic/defs.h b/src/quic/defs.h index 9678ecd1f8de44..7802a1fa40a22e 100644 --- a/src/quic/defs.h +++ b/src/quic/defs.h @@ -13,7 +13,7 @@ namespace quic { #define NGTCP2_ERR(V) (V != NGTCP2_SUCCESS) #define NGTCP2_OK(V) (V == NGTCP2_SUCCESS) -#define IF_QUIC_DEBUG(env) \ +#define IF_QUIC_DEBUG(env) \ if (UNLIKELY(env->enabled_debug_list()->enabled(DebugCategory::QUIC))) template @@ -169,6 +169,7 @@ class DebugIndentScope { res += "}"; return res; } + private: static int indent_; }; diff --git a/src/quic/endpoint.cc b/src/quic/endpoint.cc index 15a23d4647e340..940101baab24f9 100644 --- a/src/quic/endpoint.cc +++ b/src/quic/endpoint.cc @@ -1,4 +1,3 @@ -#include "debug_utils.h" #if HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC #include "endpoint.h" @@ -255,8 +254,8 @@ Maybe Endpoint::Options::From(Environment* env, !SET(max_retries) || !SET(max_payload_size) || !SET(unacknowledged_packet_threshold) || !SET(validate_address) || !SET(disable_stateless_reset) || !SET(ipv6_only) || - !SET(handshake_timeout) || !SET(max_stream_window) || - !SET(max_window) || !SET(no_udp_payload_size_shaping) || + !SET(handshake_timeout) || !SET(max_stream_window) || !SET(max_window) || + !SET(no_udp_payload_size_shaping) || #ifdef DEBUG !SET(rx_loss) || !SET(tx_loss) || #endif @@ -305,14 +304,17 @@ std::string Endpoint::Options::ToString() const { std::string res = "{ "; res += prefix + "local address: " + local_address->ToString(); - res += prefix + "retry token expiration: " + - std::to_string(retry_token_expiration) + " seconds"; + res += prefix + + "retry token expiration: " + std::to_string(retry_token_expiration) + + " seconds"; res += prefix + "token expiration: " + std::to_string(token_expiration) + " seconds"; res += prefix + "max connections per host: " + std::to_string(max_connections_per_host); - res += prefix + "max connections total: " + std::to_string(max_connections_total); - res += prefix + "max stateless resets: " + std::to_string(max_stateless_resets); + res += prefix + + "max connections total: " + std::to_string(max_connections_total); + res += + prefix + "max stateless resets: " + std::to_string(max_stateless_resets); res += prefix + "address lru size: " + std::to_string(address_lru_size); res += prefix + "max retries: " + std::to_string(max_retries); res += prefix + "max payload size: " + std::to_string(max_payload_size); @@ -326,9 +328,11 @@ std::string Endpoint::Options::ToString() const { } res += prefix + "max stream window: " + std::to_string(max_stream_window); res += prefix + "max window: " + std::to_string(max_window); - res += prefix + "no udp payload size shaping: " + boolToString(no_udp_payload_size_shaping); + res += prefix + "no udp payload size shaping: " + + boolToString(no_udp_payload_size_shaping); res += prefix + "validate address: " + boolToString(validate_address); - res += prefix + "disable stateless reset: " + boolToString(disable_stateless_reset); + res += prefix + + "disable stateless reset: " + boolToString(disable_stateless_reset); #ifdef DEBUG res += prefix + "rx loss: " + std::to_string(rx_loss); res += prefix + "tx loss: " + std::to_string(tx_loss); @@ -349,9 +353,10 @@ std::string Endpoint::Options::ToString() const { res += prefix + "reset token secret: " + reset_token_secret.ToString(); res += prefix + "token secret: " + token_secret.ToString(); res += prefix + "ipv6 only: " + boolToString(ipv6_only); - res += prefix + "udp receive buffer size: " + - std::to_string(udp_receive_buffer_size); - res += prefix + "udp send buffer size: " + std::to_string(udp_send_buffer_size); + res += prefix + + "udp receive buffer size: " + std::to_string(udp_receive_buffer_size); + res += + prefix + "udp send buffer size: " + std::to_string(udp_send_buffer_size); res += prefix + "udp ttl: " + std::to_string(udp_ttl); res += indent.Close(); @@ -694,8 +699,10 @@ void Endpoint::MarkAsBusy(bool on) { RegularToken Endpoint::GenerateNewToken(uint32_t version, const SocketAddress& remote_address) { IF_QUIC_DEBUG(env()) { - Debug(this, "Generating new regular token for version %u and remote address %s", - version, remote_address); + Debug(this, + "Generating new regular token for version %u and remote address %s", + version, + remote_address); } DCHECK(!is_closed() && !is_closing()); return RegularToken(version, remote_address, options_.token_secret); @@ -704,7 +711,8 @@ RegularToken Endpoint::GenerateNewToken(uint32_t version, StatelessResetToken Endpoint::GenerateNewStatelessResetToken( uint8_t* token, const CID& cid) const { IF_QUIC_DEBUG(env()) { - Debug(const_cast(this), "Generating new stateless reset token for CID %s", + Debug(const_cast(this), + "Generating new stateless reset token for CID %s", cid); } DCHECK(!is_closed() && !is_closing()); @@ -855,7 +863,10 @@ void Endpoint::SendVersionNegotiation(const PathDescriptor& options) { bool Endpoint::SendStatelessReset(const PathDescriptor& options, size_t source_len) { if (UNLIKELY(options_.disable_stateless_reset)) return false; - Debug(this, "Sending stateless reset on path %s with len %" PRIu64, options, source_len); + Debug(this, + "Sending stateless reset on path %s with len %" PRIu64, + options, + source_len); const auto exceeds_limits = [&] { SocketAddressInfoTraits::Type* counts = @@ -882,7 +893,9 @@ bool Endpoint::SendStatelessReset(const PathDescriptor& options, void Endpoint::SendImmediateConnectionClose(const PathDescriptor& options, QuicError reason) { - Debug(this, "Sending immediate connection close on path %s with reason %s", options, + Debug(this, + "Sending immediate connection close on path %s with reason %s", + options, reason); // While it is possible for a malicious peer to cause us to create a large // number of these, generating them is fairly trivial. @@ -947,8 +960,13 @@ BaseObjectPtr Endpoint::Connect( *this, options, local_address(), remote_address, session_ticket); IF_QUIC_DEBUG(env()) { - Debug(this, "Connecting to %s with options %s and config %s [has 0rtt ticket? %s]", - remote_address, options, config, session_ticket.has_value() ? "yes" : "no"); + Debug( + this, + "Connecting to %s with options %s and config %s [has 0rtt ticket? %s]", + remote_address, + options, + config, + session_ticket.has_value() ? "yes" : "no"); } auto session = Session::Create(this, config); @@ -992,7 +1010,8 @@ void Endpoint::Destroy(CloseContext context, int status) { } return ""; })(); - Debug(this, "Destroying endpoint due to \"%s\" with status %d", ctx, status); + Debug( + this, "Destroying endpoint due to \"%s\" with status %d", ctx, status); } STAT_RECORD_TIMESTAMP(Stats, destroyed_at); @@ -1054,15 +1073,19 @@ void Endpoint::Receive(const uv_buf_t& buf, }; const auto accept = [&](const Session::Config& config, Store&& store) { - // One final check. If the endpoint is closed, closing, or is not listening as - // a server, then we cannot accept the initial packet. + // One final check. If the endpoint is closed, closing, or is not listening + // as a server, then we cannot accept the initial packet. if (is_closed() || is_closing() || !is_listening()) return; Debug(this, "Trying to create new session for initial packet"); auto session = Session::Create(this, config); if (session) { - receive(session.get(), std::move(store), config.local_address, - config.remote_address, config.dcid, config.scid); + receive(session.get(), + std::move(store), + config.local_address, + config.remote_address, + config.dcid, + config.scid); } }; @@ -1084,10 +1107,10 @@ void Endpoint::Receive(const uv_buf_t& buf, // even recognize this packet as a quic packet with the correct version. ngtcp2_vec vec = store; if (ngtcp2_accept(&hd, vec.base, vec.len) != NGTCP2_SUCCESS) { - // Per the ngtcp2 docs, ngtcp2_accept returns 0 if the check was successful, - // or an error code if it was not. Currently there's only one documented error - // code (NGTCP2_ERR_INVALID_ARGUMENT) but we'll handle any error here the same -- - // by ignoring the packet entirely. + // Per the ngtcp2 docs, ngtcp2_accept returns 0 if the check was + // successful, or an error code if it was not. Currently there's only one + // documented error code (NGTCP2_ERR_INVALID_ARGUMENT) but we'll handle + // any error here the same -- by ignoring the packet entirely. Debug(this, "Failed to accept initial packet"); return; } @@ -1096,16 +1119,18 @@ void Endpoint::Receive(const uv_buf_t& buf, // recognized and supported. If it returns 0, we'll go ahead and send a // version negotiation packet in response. if (ngtcp2_is_supported_version(hd.version) == 0) { - Debug(this, "Packet was not accepted because the version is not supported"); + Debug(this, + "Packet was not accepted because the version is not supported"); SendVersionNegotiation( PathDescriptor{version, dcid, scid, local_address, remote_address}); STAT_INCREMENT(Stats, packets_received); return; } - // This is the next important condition check... If the server has been marked busy - // or the remote peer has exceeded their maximum number of concurrent - // connections, any new connections will be shut down immediately. + // This is the next important condition check... If the server has been + // marked busy or the remote peer has exceeded their maximum number of + // concurrent connections, any new connections will be shut down + // immediately. const auto limits_exceeded = ([&] { if (sessions_.size() >= options_.max_connections_total) return true; @@ -1115,8 +1140,10 @@ void Endpoint::Receive(const uv_buf_t& buf, })(); if (state_->busy || limits_exceeded) { - Debug(this, "Packet was not accepted because the endpoint is busy or the " - "remote peer has exceeded their maximum number of concurrent connections"); + Debug(this, + "Packet was not accepted because the endpoint is busy or the " + "remote peer has exceeded their maximum number of concurrent " + "connections"); // Endpoint is busy or the connection count is exceeded. The connection is // refused. For the purpose of stats collection, we'll count both of these // the same. @@ -1171,7 +1198,9 @@ void Endpoint::Receive(const uv_buf_t& buf, if (options_.validate_address) { // If there is no token, generate and send one. if (hd.tokenlen == 0) { - Debug(this, "Initial packet has no token. Sending retry to start validation"); + Debug(this, + "Initial packet has no token. Sending retry to start " + "validation"); SendRetry(PathDescriptor{ version, dcid, @@ -1213,7 +1242,9 @@ void Endpoint::Receive(const uv_buf_t& buf, // The ocid is the original dcid that was encoded into the // original retry packet sent to the client. We use it for // validation. - Debug(this, "Retry token is valid. Original dcid %s", ocid.value()); + Debug(this, + "Retry token is valid. Original dcid %s", + ocid.value()); config.ocid = ocid.value(); config.retry_scid = dcid; config.set_token(token); @@ -1228,9 +1259,10 @@ void Endpoint::Receive(const uv_buf_t& buf, options_.token_secret, options_.token_expiration * NGTCP2_SECONDS)) { Debug(this, "Regular token is invalid."); - // If the regular token is invalid, let's send a retry to be lenient. - // There's a small risk that a malicious peer is trying to make us do - // some work but the risk is fairly low here. + // If the regular token is invalid, let's send a retry to be + // lenient. There's a small risk that a malicious peer is + // trying to make us do some work but the risk is fairly low + // here. SendRetry(PathDescriptor{ version, dcid, @@ -1249,9 +1281,10 @@ void Endpoint::Receive(const uv_buf_t& buf, } default: { Debug(this, "Initial packet has unknown token type"); - // If our prefix bit does not match anything we know about, let's send - // a retry to be lenient. There's a small risk that a malicious peer is - // trying to make us do some work but the risk is fairly low here. + // If our prefix bit does not match anything we know about, + // let's send a retry to be lenient. There's a small risk that a + // malicious peer is trying to make us do some work but the risk + // is fairly low here. SendRetry(PathDescriptor{ version, dcid, @@ -1325,16 +1358,17 @@ void Endpoint::Receive(const uv_buf_t& buf, const SocketAddress& local_address, const SocketAddress& remote_address) { // Support for stateless resets can be disabled by the application. If that - // case, or if the packet is too short to contain a reset token, then we skip - // the remaining checks. + // case, or if the packet is too short to contain a reset token, then we + // skip the remaining checks. if (options_.disable_stateless_reset || store.length() < NGTCP2_STATELESS_RESET_TOKENLEN) { return false; } - // The stateless reset token itself is the *final* NGTCP2_STATELESS_RESET_TOKENLEN - // bytes in the received packet. If it is a stateless reset then then rest of the - // bytes in the packet are garbage that we'll ignore. + // The stateless reset token itself is the *final* + // NGTCP2_STATELESS_RESET_TOKENLEN bytes in the received packet. If it is a + // stateless reset then then rest of the bytes in the packet are garbage + // that we'll ignore. ngtcp2_vec vec = store; vec.base += (vec.len - NGTCP2_STATELESS_RESET_TOKENLEN); @@ -1371,12 +1405,15 @@ void Endpoint::Receive(const uv_buf_t& buf, // return; // } - Debug(this, "Received packet with length %" PRIu64 " from %s", buf.len, remote_address); + Debug(this, + "Received packet with length %" PRIu64 " from %s", + buf.len, + remote_address); // The managed buffer here contains the received packet. We do not yet know - // at this point if it is a valid QUIC packet. We need to do some basic checks. - // It is critical at this point that we do as little work as possible to avoid - // a DOS vector. + // at this point if it is a valid QUIC packet. We need to do some basic + // checks. It is critical at this point that we do as little work as possible + // to avoid a DOS vector. std::shared_ptr backing = env()->release_managed_buffer(buf); if (UNLIKELY(!backing)) { // At this point something bad happened and we need to treat this as a fatal @@ -1400,8 +1437,8 @@ void Endpoint::Receive(const uv_buf_t& buf, return; // Ignore the packet! } - // QUIC currently requires CID lengths of max NGTCP2_MAX_CIDLEN. Ignore any packet - // with a non-standard CID length. + // QUIC currently requires CID lengths of max NGTCP2_MAX_CIDLEN. Ignore any + // packet with a non-standard CID length. if (UNLIKELY(pversion_cid.dcidlen > NGTCP2_MAX_CIDLEN || pversion_cid.scidlen > NGTCP2_MAX_CIDLEN)) { Debug(this, "Packet had incorrectly sized CIDs, igoring"); @@ -1538,7 +1575,7 @@ void Endpoint::EmitNewSession(const BaseObjectPtr& session) { session->set_wrapped(); Local arg = session->object(); - Debug(this, "Notifying JavaScript about new session");; + Debug(this, "Notifying JavaScript about new session"); MakeCallback(BindingData::Get(env()).session_new_callback(), 1, &arg); } diff --git a/src/quic/endpoint.h b/src/quic/endpoint.h index 1517e48c124162..211bea2979d80f 100644 --- a/src/quic/endpoint.h +++ b/src/quic/endpoint.h @@ -111,7 +111,7 @@ class Endpoint final : public AsyncWrap, public Packet::Listener { uint64_t handshake_timeout = UINT64_MAX; uint64_t max_stream_window = 0; - uint64_t max_window = 0; + uint64_t max_window = 0; bool no_udp_payload_size_shaping = true; diff --git a/src/quic/http3.cc b/src/quic/http3.cc new file mode 100644 index 00000000000000..09480afcafea68 --- /dev/null +++ b/src/quic/http3.cc @@ -0,0 +1,824 @@ +#if HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "application.h" +#include "bindingdata.h" +#include "defs.h" +#include "http3.h" +#include "session.h" +#include "sessionticket.h" + +namespace node { +namespace quic { +namespace { + +struct Http3HeadersTraits { + typedef nghttp3_nv nv_t; +}; + +struct Http3RcBufferPointerTraits { + typedef nghttp3_rcbuf rcbuf_t; + typedef nghttp3_vec vector_t; + + static void inc(rcbuf_t* buf) { + CHECK_NOT_NULL(buf); + nghttp3_rcbuf_incref(buf); + } + static void dec(rcbuf_t* buf) { + CHECK_NOT_NULL(buf); + nghttp3_rcbuf_decref(buf); + } + static vector_t get_vec(rcbuf_t* buf) { + CHECK_NOT_NULL(buf); + return nghttp3_rcbuf_get_buf(buf); + } + static bool is_static(const rcbuf_t* buf) { + CHECK_NOT_NULL(buf); + return nghttp3_rcbuf_is_static(buf); + } +}; + +using Http3ConnectionPointer = DeleteFnPtr; +using Http3Headers = NgHeaders; +using Http3RcBufferPointer = NgRcBufPointer; + +struct Http3HeaderTraits { + typedef Http3RcBufferPointer rcbufferpointer_t; + typedef BindingData allocator_t; + + static const char* ToHttpHeaderName(int32_t token) { + switch (token) { + case -1: + return nullptr; +#define V(key, name) \ + case NGHTTP3_QPACK_TOKEN__##key: \ + return name; + HTTP_SPECIAL_HEADERS(V) +#undef V +#define V(key, name) \ + case NGHTTP3_QPACK_TOKEN_##key: \ + return name; + HTTP_REGULAR_HEADERS(V) +#undef V + } + return nullptr; + } +}; + +using Http3Header = NgHeader; + +// Implements the low-level HTTP/3 Application semantics. +class Http3Application final : public Session::Application { + public: + Http3Application(Session* session, + const Session::Application_Options& options) + : Application(session, options), + options_(options), + conn_(InitializeConnection()) { + session->set_priority_supported(); + } + + bool Start() override { + Debug(&session(), "Starting HTTP/3 application."); + auto params = ngtcp2_conn_get_remote_transport_params(session()); + if (params == nullptr) { + // The params are not available yet. Cannot start. + Debug(&session(), + "Cannot start HTTP/3 application yet. No remote transport params"); + return false; + } + + if (params->initial_max_streams_uni < 3) { + // If the initial max unidirectional stream limit is not at least three, + // we cannot actually use it since we need to create the control streams. + Debug(&session(), + "Cannot start HTTP/3 application. Initial max " + "unidirectional streams is too low"); + return false; + } + + if (session().is_server()) { + nghttp3_conn_set_max_client_streams_bidi( + *this, params->initial_max_streams_bidi); + } + + return CreateAndBindControlStreams(); + } + + bool ReceiveStreamData(Stream* stream, + const uint8_t* data, + size_t datalen, + Stream::ReceiveDataFlags flags) override { + Debug(&session(), "HTTP/3 application received %zu bytes of data", datalen); + ssize_t nread = nghttp3_conn_read_stream( + *this, stream->id(), data, datalen, flags.fin ? 1 : 0); + + if (nread < 0) { + Debug(&session(), + "HTTP/3 application failed to read stream data: %s", + nghttp3_strerror(nread)); + return false; + } + + Debug(&session(), + "Extending stream and connection offset by %zd bytes", + nread); + session().ExtendStreamOffset(stream->id(), nread); + session().ExtendOffset(nread); + + return true; + } + + void AcknowledgeStreamData(Stream* stream, size_t datalen) override { + Debug(&session(), + "HTTP/3 application received acknowledgement for %zu bytes of data", + datalen); + CHECK_EQ(nghttp3_conn_add_ack_offset(*this, stream->id(), datalen), 0); + } + + bool CanAddHeader(size_t current_count, + size_t current_headers_length, + size_t this_header_length) override { + // We cannot add the header if we've either reached + // * the max number of header pairs or + // * the max number of header bytes + bool answer = (current_count < options_.max_header_pairs) && + (current_headers_length + this_header_length) <= + options_.max_header_length; + IF_QUIC_DEBUG(env()) { + if (answer) { + Debug(&session(), "HTTP/3 application can add header"); + } else { + Debug(&session(), "HTTP/3 application cannot add header"); + } + } + return answer; + } + + void BlockStream(int64_t id) override { + nghttp3_conn_block_stream(*this, id); + Application::BlockStream(id); + } + + void ResumeStream(int64_t id) override { + nghttp3_conn_resume_stream(*this, id); + Application::ResumeStream(id); + } + + void ExtendMaxStreams(EndpointLabel label, + Direction direction, + uint64_t max_streams) override { + switch (label) { + case EndpointLabel::LOCAL: + return; + case EndpointLabel::REMOTE: { + switch (direction) { + case Direction::BIDIRECTIONAL: { + Debug(&session(), + "HTTP/3 application extending max bidi streams to %" PRIu64, + max_streams); + ngtcp2_conn_extend_max_streams_bidi(session(), max_streams); + break; + } + case Direction::UNIDIRECTIONAL: { + Debug(&session(), + "HTTP/3 application extending max uni streams to %" PRIu64, + max_streams); + ngtcp2_conn_extend_max_streams_uni(session(), max_streams); + break; + } + } + } + } + } + + void ExtendMaxStreamData(Stream* stream, uint64_t max_data) override { + Debug(&session(), + "HTTP/3 application extending max stream data to %" PRIu64, + max_data); + nghttp3_conn_unblock_stream(*this, stream->id()); + } + + void CollectSessionTicketAppData( + SessionTicket::AppData* app_data) const override { + // TODO(@jasnell): There's currently nothing to store but there may be + // later. + } + + SessionTicket::AppData::Status ExtractSessionTicketAppData( + const SessionTicket::AppData& app_data, + SessionTicket::AppData::Source::Flag flag) override { + // There's currently nothing stored here but we might do so later. + return flag == SessionTicket::AppData::Source::Flag::STATUS_RENEW + ? SessionTicket::AppData::Status::TICKET_USE_RENEW + : SessionTicket::AppData::Status::TICKET_USE; + } + + void StreamClose(Stream* stream, QuicError error = QuicError()) override { + Debug( + &session(), "HTTP/3 application closing stream %" PRIi64, stream->id()); + uint64_t code = NGHTTP3_H3_NO_ERROR; + if (error) { + CHECK_EQ(error.type(), QuicError::Type::APPLICATION); + code = error.code(); + } + + int rv = nghttp3_conn_close_stream(*this, stream->id(), code); + // If the call is successful, Http3Application::OnStreamClose callback will + // be invoked when the stream is ready to be closed. We'll handle destroying + // the actual Stream object there. + if (rv == 0) return; + + if (rv == NGHTTP3_ERR_STREAM_NOT_FOUND) { + ExtendMaxStreams(EndpointLabel::REMOTE, stream->direction(), 1); + return; + } + + session().SetLastError( + QuicError::ForApplication(nghttp3_err_infer_quic_app_error_code(rv))); + session().Close(); + } + + void StreamReset(Stream* stream, + uint64_t final_size, + QuicError error) override { + // We are shutting down the readable side of the local stream here. + Debug(&session(), + "HTTP/3 application resetting stream %" PRIi64, + stream->id()); + int rv = nghttp3_conn_shutdown_stream_read(*this, stream->id()); + if (rv == 0) { + stream->ReceiveStreamReset(final_size, error); + return; + } + + session().SetLastError( + QuicError::ForApplication(nghttp3_err_infer_quic_app_error_code(rv))); + session().Close(); + } + + void StreamStopSending(Stream* stream, QuicError error) override { + Application::StreamStopSending(stream, error); + } + + bool SendHeaders(const Stream& stream, + HeadersKind kind, + const v8::Local& headers, + HeadersFlags flags = HeadersFlags::NONE) override { + Session::SendPendingDataScope send_scope(&session()); + Http3Headers nva(env(), headers); + + switch (kind) { + case HeadersKind::HINTS: { + if (!session().is_server()) { + // Client side cannot send hints + return false; + } + Debug(&session(), + "Submitting early hints for stream " PRIi64, + stream.id()); + return nghttp3_conn_submit_info( + *this, stream.id(), nva.data(), nva.length()) == 0; + break; + } + case HeadersKind::INITIAL: { + static constexpr nghttp3_data_reader reader = {on_read_data_callback}; + const nghttp3_data_reader* reader_ptr = nullptr; + + // If the terminal flag is set, that means that we know we're only + // sending headers and no body and the stream writable side should be + // closed immediately because there is no nghttp3_data_reader provided. + if (flags != HeadersFlags::TERMINAL) reader_ptr = &reader; + + if (session().is_server()) { + // If this is a server, we're submitting a response... + Debug(&session(), + "Submitting response headers for stream " PRIi64, + stream.id()); + return nghttp3_conn_submit_response( + *this, stream.id(), nva.data(), nva.length(), reader_ptr); + } else { + // Otherwise we're submitting a request... + Debug(&session(), + "Submitting request headers for stream " PRIi64, + stream.id()); + return nghttp3_conn_submit_request(*this, + stream.id(), + nva.data(), + nva.length(), + reader_ptr, + const_cast(&stream)) == 0; + } + break; + } + case HeadersKind::TRAILING: { + return nghttp3_conn_submit_trailers( + *this, stream.id(), nva.data(), nva.length()) == 0; + break; + } + } + + return false; + } + + StreamPriority GetStreamPriority(const Stream& stream) override { + nghttp3_pri pri; + if (nghttp3_conn_get_stream_priority(*this, &pri, stream.id()) == 0) { + // TODO(@jasnell): Support the incremental flag + switch (pri.urgency) { + case NGHTTP3_URGENCY_HIGH: + return StreamPriority::HIGH; + case NGHTTP3_URGENCY_LOW: + return StreamPriority::LOW; + default: + return StreamPriority::DEFAULT; + } + } + return StreamPriority::DEFAULT; + } + + int GetStreamData(StreamData* data) override { + ssize_t ret = 0; + Debug(&session(), "HTTP/3 application getting stream data"); + if (conn_ && session().max_data_left()) { + nghttp3_vec vec = *data; + ret = nghttp3_conn_writev_stream( + *this, &data->id, &data->fin, &vec, data->count); + if (ret < 0) { + return static_cast(ret); + } else { + data->remaining = data->count = static_cast(ret); + } + } + return 0; + } + + bool StreamCommit(StreamData* data, size_t datalen) override { + Debug(&session(), + "HTTP/3 application committing stream %" PRIi64 " data %zu", + data->id, + datalen); + int err = nghttp3_conn_add_write_offset(*this, data->id, datalen); + if (err != 0) { + session().SetLastError(QuicError::ForApplication( + nghttp3_err_infer_quic_app_error_code(err))); + return false; + } + return true; + } + + bool ShouldSetFin(const StreamData& data) override { + return data.id > -1 && !is_control_stream(data.id) && data.fin == 1; + } + + SET_NO_MEMORY_INFO(); + SET_MEMORY_INFO_NAME(Http3Application); + SET_SELF_SIZE(Http3Application); + + private: + inline operator nghttp3_conn*() const { return conn_.get(); } + + bool CreateAndBindControlStreams() { + Debug(&session(), "Creating and binding HTTP/3 control streams"); + auto stream = session().OpenStream(Direction::UNIDIRECTIONAL); + if (!stream) return false; + if (nghttp3_conn_bind_control_stream(*this, stream->id()) != 0) { + return false; + } + + auto enc_stream = session().OpenStream(Direction::UNIDIRECTIONAL); + if (!enc_stream) return false; + + auto dec_stream = session().OpenStream(Direction::UNIDIRECTIONAL); + if (!dec_stream) return false; + + bool bound = nghttp3_conn_bind_qpack_streams( + *this, enc_stream->id(), dec_stream->id()) == 0; + control_stream_id_ = stream->id(); + qpack_enc_stream_id_ = enc_stream->id(); + qpack_dec_stream_id_ = dec_stream->id(); + return bound; + } + + inline bool is_control_stream(int64_t id) const { + return id == control_stream_id_ || id == qpack_dec_stream_id_ || + id == qpack_enc_stream_id_; + } + + bool is_destroyed() const { return session().is_destroyed(); } + + Http3ConnectionPointer InitializeConnection() { + nghttp3_conn* conn = nullptr; + nghttp3_mem allocator = BindingData::Get(env()); + nghttp3_settings settings = options_; + if (session().is_server()) { + CHECK_EQ(nghttp3_conn_server_new( + &conn, &kCallbacks, &settings, &allocator, this), + 0); + } else { + CHECK_EQ(nghttp3_conn_client_new( + &conn, &kCallbacks, &settings, &allocator, this), + 0); + } + return Http3ConnectionPointer(conn); + } + + void OnStreamClose(Stream* stream, uint64_t app_error_code) { + if (stream->is_destroyed()) return; + Debug(&session(), + "HTTP/3 application received stream close for stream %" PRIi64, + stream->id()); + auto direction = stream->direction(); + stream->Destroy(QuicError::ForApplication(app_error_code)); + ExtendMaxStreams(EndpointLabel::REMOTE, direction, 1); + } + + void OnReceiveData(Stream* stream, const nghttp3_vec& vec) { + if (stream->is_destroyed()) return; + Debug(&session(), "HTTP/3 application received %zu bytes of data", vec.len); + stream->ReceiveData(vec.base, vec.len, Stream::ReceiveDataFlags{}); + } + + void OnDeferredConsume(Stream* stream, size_t consumed) { + auto& sess = session(); + Debug( + &session(), "HTTP/3 application deferred consume %zu bytes", consumed); + if (!stream->is_destroyed()) { + sess.ExtendStreamOffset(stream->id(), consumed); + } + sess.ExtendOffset(consumed); + } + + void OnBeginHeaders(Stream* stream) { + if (stream->is_destroyed()) return; + Debug(&session(), + "HTTP/3 application beginning initial block of headers for stream " + "%" PRIi64, + stream->id()); + stream->BeginHeaders(HeadersKind::INITIAL); + } + + void OnReceiveHeader(Stream* stream, Http3Header&& header) { + if (stream->is_destroyed()) return; + if (header.name() == ":status") { + if (header.value()[0] == '1') { + Debug( + &session(), + "HTTP/3 application switching to hints headers for stream %" PRIi64, + stream->id()); + stream->set_headers_kind(HeadersKind::HINTS); + } + } + stream->AddHeader(std::move(header)); + } + + void OnEndHeaders(Stream* stream, int fin) { + Debug(&session(), + "HTTP/3 application received end of headers for stream %" PRIi64, + stream->id()); + stream->EmitHeaders(); + if (fin != 0) { + // The stream is done. There's no more data to receive! + Debug(&session(), "Headers are final for stream %" PRIi64, stream->id()); + OnEndStream(stream); + } + } + + void OnBeginTrailers(Stream* stream) { + if (stream->is_destroyed()) return; + Debug(&session(), + "HTTP/3 application beginning block of trailers for stream %" PRIi64, + stream->id()); + stream->BeginHeaders(HeadersKind::TRAILING); + } + + void OnReceiveTrailer(Stream* stream, Http3Header&& header) { + stream->AddHeader(header); + } + + void OnEndTrailers(Stream* stream, int fin) { + if (stream->is_destroyed()) return; + Debug(&session(), + "HTTP/3 application received end of trailers for stream %" PRIi64, + stream->id()); + stream->EmitHeaders(); + if (fin != 0) { + Debug(&session(), "Trailers are final for stream %" PRIi64, stream->id()); + // The stream is done. There's no more data to receive! + stream->ReceiveData(nullptr, + 0, + Stream::ReceiveDataFlags{/* .fin = */ true, + /* .early = */ false}); + } + } + + void OnEndStream(Stream* stream) { + if (stream->is_destroyed()) return; + Debug(&session(), + "HTTP/3 application received end of stream for stream %" PRIi64, + stream->id()); + stream->ReceiveData(nullptr, + 0, + Stream::ReceiveDataFlags{/* .fin = */ true, + /* .early = */ false}); + } + + void OnStopSending(Stream* stream, uint64_t app_error_code) { + if (stream->is_destroyed()) return; + Debug(&session(), + "HTTP/3 application received stop sending for stream %" PRIi64, + stream->id()); + stream->ReceiveStopSending(QuicError::ForApplication(app_error_code)); + } + + void OnResetStream(Stream* stream, uint64_t app_error_code) { + if (stream->is_destroyed()) return; + Debug(&session(), + "HTTP/3 application received reset stream for stream %" PRIi64, + stream->id()); + stream->ReceiveStreamReset(0, QuicError::ForApplication(app_error_code)); + } + + void OnShutdown() { + // This callback is invoked when we receive a request to gracefully shutdown + // the http3 connection. For client, the id is the stream id of a client + // initiated stream. For server, the id is the stream id of a server + // initiated stream. Once received, the other side is guaranteed not to + // process any more data. + + // On the client side, if id is equal to NGHTTP3_SHUTDOWN_NOTICE_STREAM_ID, + // or on the server if the id is equal to NGHTTP3_SHUSTDOWN_NOTICE_PUSH_ID, + // then this is a request to begin a graceful shutdown. + + // This can be called multiple times but the id can only stay the same or + // *decrease*. + + // TODO(@jasnell): Need to determine exactly how to handle. + Debug(&session(), "HTTP/3 application received shutdown notice"); + } + + void OnReceiveSettings(const nghttp3_settings* settings) { + options_.enable_connect_protocol = settings->enable_connect_protocol; + options_.enable_datagrams = settings->h3_datagram; + options_.max_field_section_size = settings->max_field_section_size; + options_.qpack_blocked_streams = settings->qpack_blocked_streams; + options_.qpack_encoder_max_dtable_capacity = + settings->qpack_encoder_max_dtable_capacity; + options_.qpack_max_dtable_capacity = settings->qpack_max_dtable_capacity; + Debug( + &session(), "HTTP/3 application received updated settings ", options_); + } + + Session::Application_Options options_; + Http3ConnectionPointer conn_; + int64_t control_stream_id_ = -1; + int64_t qpack_dec_stream_id_ = -1; + int64_t qpack_enc_stream_id_ = -1; + + // ========================================================================== + // Static callbacks + + static Http3Application* From(nghttp3_conn* conn, void* user_data) { + DCHECK_NOT_NULL(user_data); + auto app = static_cast(user_data); + DCHECK_EQ(conn, app->conn_.get()); + return app; + } + + static Stream* From(int64_t stream_id, void* stream_user_data) { + DCHECK_NOT_NULL(stream_user_data); + auto stream = static_cast(stream_user_data); + DCHECK_EQ(stream_id, stream->id()); + return stream; + } + +#define NGHTTP3_CALLBACK_SCOPE(name) \ + auto name = From(conn, conn_user_data); \ + if (UNLIKELY(name->is_destroyed())) return NGHTTP3_ERR_CALLBACK_FAILURE; \ + NgHttp3CallbackScope scope(name->env()); + + static nghttp3_ssize on_read_data_callback(nghttp3_conn* conn, + int64_t stream_id, + nghttp3_vec* vec, + size_t veccnt, + uint32_t* pflags, + void* conn_user_data, + void* stream_user_data) { + return 0; + } + + static int on_acked_stream_data(nghttp3_conn* conn, + int64_t stream_id, + uint64_t datalen, + void* conn_user_data, + void* stream_user_data) { + NGHTTP3_CALLBACK_SCOPE(app); + auto stream = From(stream_id, stream_user_data); + if (stream == nullptr) return NGHTTP3_ERR_CALLBACK_FAILURE; + app->AcknowledgeStreamData(stream, datalen); + return NGTCP2_SUCCESS; + } + + static int on_stream_close(nghttp3_conn* conn, + int64_t stream_id, + uint64_t app_error_code, + void* conn_user_data, + void* stream_user_data) { + NGHTTP3_CALLBACK_SCOPE(app); + auto stream = From(stream_id, stream_user_data); + if (stream == nullptr) return NGHTTP3_ERR_CALLBACK_FAILURE; + app->OnStreamClose(stream, app_error_code); + return NGTCP2_SUCCESS; + } + + static int on_receive_data(nghttp3_conn* conn, + int64_t stream_id, + const uint8_t* data, + size_t datalen, + void* conn_user_data, + void* stream_user_data) { + NGHTTP3_CALLBACK_SCOPE(app); + auto stream = From(stream_id, stream_user_data); + if (stream == nullptr) return NGHTTP3_ERR_CALLBACK_FAILURE; + app->OnReceiveData(stream, + nghttp3_vec{const_cast(data), datalen}); + return NGTCP2_SUCCESS; + } + + static int on_deferred_consume(nghttp3_conn* conn, + int64_t stream_id, + size_t consumed, + void* conn_user_data, + void* stream_user_data) { + NGHTTP3_CALLBACK_SCOPE(app); + auto stream = From(stream_id, stream_user_data); + if (stream == nullptr) return NGHTTP3_ERR_CALLBACK_FAILURE; + app->OnDeferredConsume(stream, consumed); + return NGTCP2_SUCCESS; + } + + static int on_begin_headers(nghttp3_conn* conn, + int64_t stream_id, + void* conn_user_data, + void* stream_user_data) { + NGHTTP3_CALLBACK_SCOPE(app); + auto stream = From(stream_id, stream_user_data); + if (stream == nullptr) return NGHTTP3_ERR_CALLBACK_FAILURE; + app->OnBeginHeaders(stream); + return NGTCP2_SUCCESS; + } + + static int on_receive_header(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) { + NGHTTP3_CALLBACK_SCOPE(app); + auto stream = From(stream_id, stream_user_data); + if (stream == nullptr) return NGHTTP3_ERR_CALLBACK_FAILURE; + if (Http3Header::IsZeroLength(token, name, value)) return NGTCP2_SUCCESS; + app->OnReceiveHeader(stream, + Http3Header(app->env(), token, name, value, flags)); + return NGTCP2_SUCCESS; + } + + static int on_end_headers(nghttp3_conn* conn, + int64_t stream_id, + int fin, + void* conn_user_data, + void* stream_user_data) { + NGHTTP3_CALLBACK_SCOPE(app); + auto stream = From(stream_id, stream_user_data); + if (stream == nullptr) return NGHTTP3_ERR_CALLBACK_FAILURE; + app->OnEndHeaders(stream, fin); + return NGTCP2_SUCCESS; + } + + static int on_begin_trailers(nghttp3_conn* conn, + int64_t stream_id, + void* conn_user_data, + void* stream_user_data) { + NGHTTP3_CALLBACK_SCOPE(app); + auto stream = From(stream_id, stream_user_data); + if (stream == nullptr) return NGHTTP3_ERR_CALLBACK_FAILURE; + app->OnBeginTrailers(stream); + return NGTCP2_SUCCESS; + } + + static int on_receive_trailer(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) { + NGHTTP3_CALLBACK_SCOPE(app); + auto stream = From(stream_id, stream_user_data); + if (stream == nullptr) return NGHTTP3_ERR_CALLBACK_FAILURE; + if (Http3Header::IsZeroLength(token, name, value)) return NGTCP2_SUCCESS; + app->OnReceiveTrailer(stream, + Http3Header(app->env(), token, name, value, flags)); + return NGTCP2_SUCCESS; + } + + static int on_end_trailers(nghttp3_conn* conn, + int64_t stream_id, + int fin, + void* conn_user_data, + void* stream_user_data) { + NGHTTP3_CALLBACK_SCOPE(app); + auto stream = From(stream_id, stream_user_data); + if (stream == nullptr) return NGHTTP3_ERR_CALLBACK_FAILURE; + app->OnEndTrailers(stream, fin); + return NGTCP2_SUCCESS; + } + + static int on_end_stream(nghttp3_conn* conn, + int64_t stream_id, + void* conn_user_data, + void* stream_user_data) { + NGHTTP3_CALLBACK_SCOPE(app); + auto stream = From(stream_id, stream_user_data); + if (stream == nullptr) return NGHTTP3_ERR_CALLBACK_FAILURE; + app->OnEndStream(stream); + return NGTCP2_SUCCESS; + } + + static int on_stop_sending(nghttp3_conn* conn, + int64_t stream_id, + uint64_t app_error_code, + void* conn_user_data, + void* stream_user_data) { + NGHTTP3_CALLBACK_SCOPE(app); + auto stream = From(stream_id, stream_user_data); + if (stream == nullptr) return NGHTTP3_ERR_CALLBACK_FAILURE; + app->OnStopSending(stream, app_error_code); + return NGTCP2_SUCCESS; + } + + static int on_reset_stream(nghttp3_conn* conn, + int64_t stream_id, + uint64_t app_error_code, + void* conn_user_data, + void* stream_user_data) { + NGHTTP3_CALLBACK_SCOPE(app); + auto stream = From(stream_id, stream_user_data); + if (stream == nullptr) return NGHTTP3_ERR_CALLBACK_FAILURE; + app->OnResetStream(stream, app_error_code); + return NGTCP2_SUCCESS; + } + + static int on_shutdown(nghttp3_conn* conn, int64_t id, void* conn_user_data) { + NGHTTP3_CALLBACK_SCOPE(app); + app->OnShutdown(); + return NGTCP2_SUCCESS; + } + + static int on_receive_settings(nghttp3_conn* conn, + const nghttp3_settings* settings, + void* conn_user_data) { + NGHTTP3_CALLBACK_SCOPE(app); + app->OnReceiveSettings(settings); + return NGTCP2_SUCCESS; + } + + static constexpr nghttp3_callbacks kCallbacks = {on_acked_stream_data, + on_stream_close, + on_receive_data, + on_deferred_consume, + on_begin_headers, + on_receive_header, + on_end_headers, + on_begin_trailers, + on_receive_trailer, + on_end_trailers, + on_stop_sending, + on_end_stream, + on_reset_stream, + on_shutdown, + on_receive_settings}; +}; +} // namespace + +std::unique_ptr createHttp3Application( + Session* session, const Session::Application_Options& options) { + return std::make_unique(session, options); +} + +} // namespace quic +} // namespace node + +#endif // HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC diff --git a/src/quic/http3.h b/src/quic/http3.h new file mode 100644 index 00000000000000..b56d28b0dd202d --- /dev/null +++ b/src/quic/http3.h @@ -0,0 +1,18 @@ +#pragma once + +#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS +#if HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC + +#include "session.h" + +namespace node { +namespace quic { + +std::unique_ptr createHttp3Application( + Session* session, const Session::Application_Options& options); + +} // namespace quic +} // namespace node + +#endif // HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC +#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS diff --git a/src/quic/packet.cc b/src/quic/packet.cc index 92b9e94eb43618..72b958c81125e2 100644 --- a/src/quic/packet.cc +++ b/src/quic/packet.cc @@ -1,7 +1,6 @@ #if HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC #include "packet.h" -#include "defs.h" #include #include #include @@ -14,6 +13,7 @@ #include #include "bindingdata.h" #include "cid.h" +#include "defs.h" #include "tokens.h" namespace node { diff --git a/src/quic/session.cc b/src/quic/session.cc index 0462acc50fe77b..2608da66594367 100644 --- a/src/quic/session.cc +++ b/src/quic/session.cc @@ -72,6 +72,7 @@ namespace quic { V(HANDSHAKE_COMPLETED, handshake_completed, uint8_t) \ V(HANDSHAKE_CONFIRMED, handshake_confirmed, uint8_t) \ V(STREAM_OPEN_ALLOWED, stream_open_allowed, uint8_t) \ + V(PRIORITY_SUPPORTED, priority_supported, uint8_t) \ /* A Session is wrapped if it has been passed out to JS */ \ V(WRAPPED, wrapped, uint8_t) \ V(LAST_DATAGRAM_ID, last_datagram_id, uint64_t) @@ -136,7 +137,9 @@ struct Session::MaybeCloseConnectionScope final { MaybeCloseConnectionScope(Session* session_, bool silent_) : session(session_), silent(silent_ || session->connection_close_depth_ > 0) { - Debug(session_, "Entering maybe close connection scope. Silent? %s", silent ? "yes" : "no"); + Debug(session_, + "Entering maybe close connection scope. Silent? %s", + silent ? "yes" : "no"); session->connection_close_depth_++; } MaybeCloseConnectionScope(const MaybeCloseConnectionScope&) = delete; @@ -350,7 +353,9 @@ void Session::Config::MemoryInfo(MemoryTracker* tracker) const { tracker->TrackField("session_ticket", session_ticket.value()); } -void Session::Config::set_token(const uint8_t* token, size_t len, ngtcp2_token_type type) { +void Session::Config::set_token(const uint8_t* token, + size_t len, + ngtcp2_token_type type) { settings.token = token; settings.tokenlen = len; settings.token_type = type; @@ -371,10 +376,12 @@ std::string Session::Config::ToString() const { auto prefix = indent.Prefix(); std::string res("{"); - auto sidestr = ([&]{ + auto sidestr = ([&] { switch (side) { - case Side::CLIENT: return "client"; - case Side::SERVER: return "server"; + case Side::CLIENT: + return "client"; + case Side::SERVER: + return "server"; } return ""; })(); @@ -741,9 +748,9 @@ void Session::Destroy() { state_->destroyed = 1; - // Removing the session from the endpoint may cause the endpoint to be destroyed - // if it is waiting on the last session to be destroyed. Let's grab a reference - // just to be safe for the rest of the function. + // Removing the session from the endpoint may cause the endpoint to be + // destroyed if it is waiting on the last session to be destroyed. Let's grab + // a reference just to be safe for the rest of the function. BaseObjectPtr endpoint = std::move(endpoint_); endpoint->RemoveSession(config_.scid); } @@ -982,7 +989,10 @@ uint64_t Session::SendDatagram(Store&& data) { void Session::UpdatePath(const PathStorage& storage) { remote_address_.Update(storage.path.remote.addr, storage.path.remote.addrlen); local_address_.Update(storage.path.local.addr, storage.path.local.addrlen); - Debug(this, "path updated. local %s, remote %s", local_address_, remote_address_); + Debug(this, + "path updated. local %s, remote %s", + local_address_, + remote_address_); } BaseObjectPtr Session::FindStream(int64_t id) const { @@ -1198,6 +1208,10 @@ void Session::set_wrapped() { state_->wrapped = 1; } +void Session::set_priority_supported(bool on) { + state_->priority_supported = on ? 1 : 0; +} + void Session::DoClose(bool silent) { DCHECK(!is_destroyed()); Debug(this, "Session is closing. Silently %s", silent ? "yes" : "no"); @@ -1320,7 +1334,8 @@ void Session::UpdateTimer() { // Both uv_hrtime and ngtcp2_conn_get_expiry return nanosecond units. uint64_t expiry = ngtcp2_conn_get_expiry(*this); uint64_t now = uv_hrtime(); - Debug(this, "Updating timer. Expiry: %" PRIu64 ", now: %" PRIu64, expiry, now); + Debug( + this, "Updating timer. Expiry: %" PRIu64 ", now: %" PRIu64, expiry, now); if (expiry <= now) { // The timer has already expired. @@ -2313,7 +2328,8 @@ Session::QuicConnectionPointer Session::InitConnection() { UNREACHABLE(); } -void Session::InitPerIsolate(IsolateData* data, v8::Local target) { +void Session::InitPerIsolate(IsolateData* data, + v8::Local target) { // TODO(@jasnell): Implement the per-isolate state } diff --git a/src/quic/session.h b/src/quic/session.h index a6f6fe2b4c3c8a..f99716ba0181b5 100644 --- a/src/quic/session.h +++ b/src/quic/session.h @@ -237,6 +237,8 @@ class Session final : public AsyncWrap, private SessionTicket::AppData::Source { bool is_destroyed() const; bool is_server() const; + void set_priority_supported(bool on = true); + std::string diagnostic_name() const override; // Use the configured CID::Factory to generate a new CID. @@ -244,6 +246,9 @@ class Session final : public AsyncWrap, private SessionTicket::AppData::Source { void HandleQlog(uint32_t flags, const void* data, size_t len); + TransportParams GetLocalTransportParams() const; + TransportParams GetRemoteTransportParams() const; + void MemoryInfo(MemoryTracker* tracker) const override; SET_MEMORY_INFO_NAME(Session) SET_SELF_SIZE(Session) @@ -251,20 +256,15 @@ class Session final : public AsyncWrap, private SessionTicket::AppData::Source { struct State; struct Stats; - private: - struct Impl; - struct MaybeCloseConnectionScope; - - using StreamsMap = std::unordered_map>; - using QuicConnectionPointer = DeleteFnPtr; - - struct PathValidationFlags { - bool preferredAddress = false; - }; + operator ngtcp2_conn*() const; - struct DatagramReceivedFlags { - bool early = false; - }; + BaseObjectPtr FindStream(int64_t id) const; + BaseObjectPtr CreateStream(int64_t id); + BaseObjectPtr OpenStream(Direction direction); + void ExtendStreamOffset(int64_t id, size_t amount); + void ExtendOffset(size_t amount); + void SetLastError(QuicError&& error); + uint64_t max_data_left() const; enum class CloseMethod { // Roundtrip through JavaScript, causing all currently opened streams @@ -284,8 +284,34 @@ class Session final : public AsyncWrap, private SessionTicket::AppData::Source { // Close() is called. GRACEFUL }; - void Close(CloseMethod method = CloseMethod::DEFAULT); + + struct SendPendingDataScope { + Session* session; + explicit SendPendingDataScope(Session* session); + explicit SendPendingDataScope(const BaseObjectPtr& session); + SendPendingDataScope(const SendPendingDataScope&) = delete; + SendPendingDataScope(SendPendingDataScope&&) = delete; + SendPendingDataScope& operator=(const SendPendingDataScope&) = delete; + SendPendingDataScope& operator=(SendPendingDataScope&&) = delete; + ~SendPendingDataScope(); + }; + + private: + struct Impl; + struct MaybeCloseConnectionScope; + + using StreamsMap = std::unordered_map>; + using QuicConnectionPointer = DeleteFnPtr; + + struct PathValidationFlags { + bool preferredAddress = false; + }; + + struct DatagramReceivedFlags { + bool early = false; + }; + void Destroy(); bool Receive(Store&& store, @@ -296,9 +322,6 @@ class Session final : public AsyncWrap, private SessionTicket::AppData::Source { void Send(BaseObjectPtr packet, const PathStorage& path); uint64_t SendDatagram(Store&& data); - BaseObjectPtr FindStream(int64_t id) const; - BaseObjectPtr CreateStream(int64_t id); - BaseObjectPtr OpenStream(Direction direction); void AddStream(const BaseObjectPtr& stream); void RemoveStream(int64_t id); void ResumeStream(int64_t id); @@ -306,19 +329,6 @@ class Session final : public AsyncWrap, private SessionTicket::AppData::Source { void StreamDataBlocked(int64_t id); void ShutdownStreamWrite(int64_t id, QuicError code = QuicError()); - struct SendPendingDataScope { - Session* session; - explicit SendPendingDataScope(Session* session); - explicit SendPendingDataScope(const BaseObjectPtr& session); - SendPendingDataScope(const SendPendingDataScope&) = delete; - SendPendingDataScope(SendPendingDataScope&&) = delete; - SendPendingDataScope& operator=(const SendPendingDataScope&) = delete; - SendPendingDataScope& operator=(SendPendingDataScope&&) = delete; - ~SendPendingDataScope(); - }; - - operator ngtcp2_conn*() const; - // Implementation of SessionTicket::AppData::Source void CollectSessionTicketAppData( SessionTicket::AppData* app_data) const override; @@ -345,7 +355,6 @@ class Session final : public AsyncWrap, private SessionTicket::AppData::Source { // Returns false if the Session is currently in a state where it cannot create // new streams. bool can_create_streams() const; - uint64_t max_data_left() const; uint64_t max_local_streams_uni() const; uint64_t max_local_streams_bidi() const; @@ -358,8 +367,6 @@ class Session final : public AsyncWrap, private SessionTicket::AppData::Source { void set_wrapped(); void DoClose(bool silent = false); - void ExtendStreamOffset(int64_t id, size_t amount); - void ExtendOffset(size_t amount); void UpdateDataStats(); void SendConnectionClose(); void OnTimeout(); @@ -397,9 +404,6 @@ class Session final : public AsyncWrap, private SessionTicket::AppData::Source { bool HandshakeCompleted(); void HandshakeConfirmed(); void SelectPreferredAddress(PreferredAddress* preferredAddress); - TransportParams GetLocalTransportParams() const; - TransportParams GetRemoteTransportParams() const; - void SetLastError(QuicError&& error); void UpdatePath(const PathStorage& path); QuicConnectionPointer InitConnection(); diff --git a/src/quic/streams.cc b/src/quic/streams.cc index f20a160717e803..c8c0ecd94947e9 100644 --- a/src/quic/streams.cc +++ b/src/quic/streams.cc @@ -851,6 +851,10 @@ void Stream::BeginHeaders(HeadersKind kind) { if (is_destroyed()) return; headers_length_ = 0; headers_.clear(); + set_headers_kind(kind); +} + +void Stream::set_headers_kind(HeadersKind kind) { headers_kind_ = kind; } diff --git a/src/quic/streams.h b/src/quic/streams.h index 835dcfa30e8a26..1bab5b245fcc50 100644 --- a/src/quic/streams.h +++ b/src/quic/streams.h @@ -158,6 +158,7 @@ class Stream : public AsyncWrap, // if the application does not support headers, a maximimum number of headers // have already been added, or the maximum total header length is reached. bool AddHeader(const Header& header); + void set_headers_kind(HeadersKind kind); SET_NO_MEMORY_INFO() SET_MEMORY_INFO_NAME(Stream) @@ -170,6 +171,9 @@ class Stream : public AsyncWrap, // blocked because of flow control restriction. void EmitBlocked(); + // Delivers the set of inbound headers that have been collected. + void EmitHeaders(); + private: struct Impl; class Outbound; @@ -185,9 +189,6 @@ class Stream : public AsyncWrap, // Notifies the JavaScript side that the stream has been destroyed. void EmitClose(const QuicError& error); - // Delivers the set of inbound headers that have been collected. - void EmitHeaders(); - // Notifies the JavaScript side that the stream has been reset. void EmitReset(const QuicError& error); diff --git a/src/quic/tlscontext.cc b/src/quic/tlscontext.cc index 2dd956c000277d..6e72fa88d578ba 100644 --- a/src/quic/tlscontext.cc +++ b/src/quic/tlscontext.cc @@ -1,8 +1,8 @@ #if HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC #include "tlscontext.h" -#include #include +#include #include #include #include @@ -604,17 +604,16 @@ std::string TLSContext::Options::ToString() const { std::string res("{"); res += prefix + "alpn: " + alpn; res += prefix + "hostname: " + hostname; - res += prefix + "keylog: " + (keylog ? std::string("yes") : std::string("no")); + res += + prefix + "keylog: " + (keylog ? std::string("yes") : std::string("no")); res += prefix + "reject_unauthorized: " + (reject_unauthorized ? std::string("yes") : std::string("no")); res += prefix + "enable_tls_trace: " + (enable_tls_trace ? std::string("yes") : std::string("no")); - res += prefix + "request_peer_certificate: " + (request_peer_certificate - ? std::string("yes") - : std::string("no")); - res += prefix + "verify_hostname_identity: " + (verify_hostname_identity - ? std::string("yes") - : std::string("no")); + res += prefix + "request_peer_certificate: " + + (request_peer_certificate ? std::string("yes") : std::string("no")); + res += prefix + "verify_hostname_identity: " + + (verify_hostname_identity ? std::string("yes") : std::string("no")); res += prefix + "session_id_ctx: " + session_id_ctx; res += prefix + "ciphers: " + ciphers; res += prefix + "groups: " + groups; diff --git a/src/quic/tokens.cc b/src/quic/tokens.cc index c8a1f23153a0c4..498b9776a2bd18 100644 --- a/src/quic/tokens.cc +++ b/src/quic/tokens.cc @@ -49,8 +49,8 @@ TokenSecret::operator const char*() const { std::string TokenSecret::ToString() const { char dest[QUIC_TOKENSECRET_LEN * 2]; - size_t written = - StringBytes::hex_encode(*this, QUIC_TOKENSECRET_LEN, dest, arraysize(dest)); + size_t written = StringBytes::hex_encode( + *this, QUIC_TOKENSECRET_LEN, dest, arraysize(dest)); DCHECK_EQ(written, arraysize(dest)); return std::string(dest, written); } diff --git a/src/quic/transportparams.cc b/src/quic/transportparams.cc index 190b5b82a15f4e..2e8cd26a0cef9e 100644 --- a/src/quic/transportparams.cc +++ b/src/quic/transportparams.cc @@ -97,15 +97,15 @@ std::string TransportParams::Options::ToString() const { res += prefix + "tinitial max data: " + std::to_string(initial_max_data); res += prefix + "initial max streams bidi: " + std::to_string(initial_max_streams_bidi); - res += prefix + "initial max streams uni: " + - std::to_string(initial_max_streams_uni); + res += prefix + + "initial max streams uni: " + std::to_string(initial_max_streams_uni); res += prefix + "max idle timeout: " + std::to_string(max_idle_timeout); res += prefix + "active connection id limit: " + std::to_string(active_connection_id_limit); res += prefix + "ack delay exponent: " + std::to_string(ack_delay_exponent); res += prefix + "max ack delay: " + std::to_string(max_ack_delay); - res += prefix + "max datagram frame size: " + - std::to_string(max_datagram_frame_size); + res += prefix + + "max datagram frame size: " + std::to_string(max_datagram_frame_size); res += prefix + "disable active migration: " + (disable_active_migration ? std::string("yes") : std::string("no")); res += indent.Close(); From b0f1863e7448b044b743c9e64a55e02ef90cc926 Mon Sep 17 00:00:00 2001 From: James M Snell Date: Fri, 29 Dec 2023 08:52:23 -0800 Subject: [PATCH 5/8] quic: more of the implementation, fixing bits --- src/quic/application.cc | 8 +-- src/quic/application.h | 2 +- src/quic/bindingdata.h | 2 +- src/quic/data.cc | 21 ++++++++ src/quic/data.h | 2 + src/quic/endpoint.cc | 81 +++++++++++++++++------------ src/quic/endpoint.h | 4 +- src/quic/http3.cc | 15 ++++-- src/quic/packet.cc | 110 ++++++++++++++++------------------------ src/quic/packet.h | 31 ++++------- src/quic/session.cc | 75 ++++++++++++++------------- src/quic/session.h | 10 ++-- src/quic/streams.cc | 5 ++ src/quic/tlscontext.cc | 74 +++++++++++++-------------- src/quic/tlscontext.h | 8 --- 15 files changed, 232 insertions(+), 216 deletions(-) diff --git a/src/quic/application.cc b/src/quic/application.cc index 34b04cd4e38f3e..d933cb9d351c7b 100644 --- a/src/quic/application.cc +++ b/src/quic/application.cc @@ -185,7 +185,7 @@ StreamPriority Session::Application::GetStreamPriority(const Stream& stream) { return StreamPriority::DEFAULT; } -BaseObjectPtr Session::Application::CreateStreamDataPacket() { +Packet* Session::Application::CreateStreamDataPacket() { return Packet::Create(env(), session_->endpoint_.get(), session_->remote_address_, @@ -224,7 +224,7 @@ void Session::Application::SendPendingData() { Debug(session_, "Application sending pending data"); PathStorage path; - BaseObjectPtr packet; + Packet* packet = nullptr; uint8_t* pos = nullptr; int err = 0; @@ -261,9 +261,9 @@ void Session::Application::SendPendingData() { return session_->Close(Session::CloseMethod::SILENT); } - if (!packet) { + if (packet == nullptr) { packet = CreateStreamDataPacket(); - if (!packet) { + if (packet == nullptr) { session_->last_error_ = QuicError::ForNgtcp2Error(NGTCP2_ERR_INTERNAL); return session_->Close(Session::CloseMethod::SILENT); } diff --git a/src/quic/application.h b/src/quic/application.h index ce74985a9c03fc..5ecaede68e1c01 100644 --- a/src/quic/application.h +++ b/src/quic/application.h @@ -120,7 +120,7 @@ class Session::Application : public MemoryRetainer { inline Session& session() { return *session_; } inline const Session& session() const { return *session_; } - BaseObjectPtr CreateStreamDataPacket(); + Packet* CreateStreamDataPacket(); struct StreamData; diff --git a/src/quic/bindingdata.h b/src/quic/bindingdata.h index 8c16d7df778c67..83264e48d3d965 100644 --- a/src/quic/bindingdata.h +++ b/src/quic/bindingdata.h @@ -225,7 +225,7 @@ class BindingData final // bridge out to the JS API. static void SetCallbacks(const v8::FunctionCallbackInfo& args); - std::vector> packet_freelist; + std::vector packet_freelist; std::unordered_map> listening_endpoints; diff --git a/src/quic/data.cc b/src/quic/data.cc index 2dd542f24b02ee..c1216189219890 100644 --- a/src/quic/data.cc +++ b/src/quic/data.cc @@ -5,7 +5,9 @@ #include #include #include +#include #include +#include "defs.h" #include "util.h" namespace node { @@ -26,6 +28,25 @@ Path::Path(const SocketAddress& local, const SocketAddress& remote) { ngtcp2_addr_init(&this->remote, remote.data(), remote.length()); } +std::string Path::ToString() const { + DebugIndentScope indent; + auto prefix = indent.Prefix(); + + const sockaddr* local_in = reinterpret_cast(local.addr); + auto local_addr = SocketAddress::GetAddress(local_in); + auto local_port = SocketAddress::GetPort(local_in); + + const sockaddr* remote_in = reinterpret_cast(remote.addr); + auto remote_addr = SocketAddress::GetAddress(remote_in); + auto remote_port = SocketAddress::GetPort(remote_in); + + std::string res("{"); + res += prefix + "local: " + local_addr + ":" + std::to_string(local_port); + res += prefix + "remote: " + remote_addr + ":" + std::to_string(remote_port); + res += indent.Close(); + return res; +} + PathStorage::PathStorage() { ngtcp2_path_storage_zero(this); } diff --git a/src/quic/data.h b/src/quic/data.h index 56a8c8c6d5e869..db715235bd768c 100644 --- a/src/quic/data.h +++ b/src/quic/data.h @@ -1,5 +1,6 @@ #pragma once +#include #if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS #if HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC @@ -17,6 +18,7 @@ namespace quic { struct Path final : public ngtcp2_path { Path(const SocketAddress& local, const SocketAddress& remote); inline operator ngtcp2_path*() { return this; } + std::string ToString() const; }; struct PathStorage final : public ngtcp2_path_storage { diff --git a/src/quic/endpoint.cc b/src/quic/endpoint.cc index 940101baab24f9..b70429f42a884b 100644 --- a/src/quic/endpoint.cc +++ b/src/quic/endpoint.cc @@ -554,20 +554,31 @@ SocketAddress Endpoint::UDP::local_address() const { return SocketAddress::FromSockName(impl_->handle_); } -int Endpoint::UDP::Send(BaseObjectPtr packet) { +int Endpoint::UDP::Send(Packet* packet) { if (is_closed_or_closing()) return UV_EBADF; - DCHECK(packet && !packet->is_sending()); + DCHECK_NOT_NULL(packet); uv_buf_t buf = *packet; - return packet->Dispatch( - uv_udp_send, - &impl_->handle_, - &buf, - 1, - packet->destination().data(), - uv_udp_send_cb{[](uv_udp_send_t* req, int status) { - auto ptr = static_cast(ReqWrap::from_req(req)); - ptr->Done(status); - }}); + + // We don't use the default implementation of Dispatch because the packet + // itself is going to be reset and added to a freelist to be reused. The + // default implementation of Dispatch will cause the packet to be deleted, + // which we don't want. We call ClearWeak here just to be doubly sure. + packet->ClearWeak(); + packet->Dispatched(); + int err = uv_udp_send(packet->req(), &impl_->handle_, &buf, 1, packet->destination().data(), + uv_udp_send_cb{[](uv_udp_send_t* req, int status) { + auto ptr = + static_cast(ReqWrap::from_req(req)); + ptr->env()->DecreaseWaitingRequestCounter(); + ptr->Done(status); + }}); + if (err < 0) { + // The packet failed. + packet->Done(err); + } else { + packet->env()->IncreaseWaitingRequestCounter(); + } + return err; } void Endpoint::UDP::MemoryInfo(MemoryTracker* tracker) const { @@ -788,7 +799,8 @@ void Endpoint::DisassociateStatelessResetToken( } } -void Endpoint::Send(BaseObjectPtr packet) { +void Endpoint::Send(Packet* packet) { + CHECK_NOT_NULL(packet); #ifdef DEBUG // When diagnostic packet loss is enabled, the packet will be randomly // dropped. This can happen to any type of packet. We use this only in @@ -1077,7 +1089,7 @@ void Endpoint::Receive(const uv_buf_t& buf, // as a server, then we cannot accept the initial packet. if (is_closed() || is_closing() || !is_listening()) return; - Debug(this, "Trying to create new session for initial packet"); + Debug(this, "Trying to create new session for %s", config.dcid); auto session = Session::Create(this, config); if (session) { receive(session.get(), @@ -1096,7 +1108,7 @@ void Endpoint::Receive(const uv_buf_t& buf, const SocketAddress& local_address, const SocketAddress& remote_address) { // Conditionally accept an initial packet to create a new session. - Debug(this, "Trying to accept initial packet"); + Debug(this, "Trying to accept initial packet for %s from %s", dcid, remote_address); // If we're not listening as a server, do not accept an initial packet. if (state_->listening == 0) return; @@ -1111,7 +1123,7 @@ void Endpoint::Receive(const uv_buf_t& buf, // successful, or an error code if it was not. Currently there's only one // documented error code (NGTCP2_ERR_INVALID_ARGUMENT) but we'll handle // any error here the same -- by ignoring the packet entirely. - Debug(this, "Failed to accept initial packet"); + Debug(this, "Failed to accept initial packet from %s", remote_address); return; } @@ -1120,7 +1132,8 @@ void Endpoint::Receive(const uv_buf_t& buf, // version negotiation packet in response. if (ngtcp2_is_supported_version(hd.version) == 0) { Debug(this, - "Packet was not accepted because the version is not supported"); + "Packet was not accepted because the version (%d) is not supported", + hd.version); SendVersionNegotiation( PathDescriptor{version, dcid, scid, local_address, remote_address}); STAT_INCREMENT(Stats, packets_received); @@ -1142,8 +1155,8 @@ void Endpoint::Receive(const uv_buf_t& buf, if (state_->busy || limits_exceeded) { Debug(this, "Packet was not accepted because the endpoint is busy or the " - "remote peer has exceeded their maximum number of concurrent " - "connections"); + "remote address %s has exceeded their maximum number of concurrent " + "connections", remote_address); // Endpoint is busy or the connection count is exceeded. The connection is // refused. For the purpose of stats collection, we'll count both of these // the same. @@ -1190,7 +1203,7 @@ void Endpoint::Receive(const uv_buf_t& buf, // a new token. If it does exist, and if it is valid, we grab the original // cid and continue. if (!is_remote_address_validated) { - Debug(this, "Remote address is not validated"); + Debug(this, "Remote address %s is not validated", remote_address); switch (hd.type) { case NGTCP2_PKT_INITIAL: // First, let's see if we need to do anything here. @@ -1199,8 +1212,8 @@ void Endpoint::Receive(const uv_buf_t& buf, // If there is no token, generate and send one. if (hd.tokenlen == 0) { Debug(this, - "Initial packet has no token. Sending retry to start " - "validation"); + "Initial packet has no token. Sending retry to %s to start " + "validation", remote_address); SendRetry(PathDescriptor{ version, dcid, @@ -1219,7 +1232,7 @@ void Endpoint::Receive(const uv_buf_t& buf, switch (hd.token[0]) { case RetryToken::kTokenMagic: { RetryToken token(hd.token, hd.tokenlen); - Debug(this, "Initial packet has retry token %s", token); + Debug(this, "Initial packet from %s has retry token %s", remote_address, token); auto ocid = token.Validate( version, remote_address, @@ -1227,7 +1240,7 @@ void Endpoint::Receive(const uv_buf_t& buf, options_.token_secret, options_.retry_token_expiration * NGTCP2_SECONDS); if (!ocid.has_value()) { - Debug(this, "Retry token is invalid."); + Debug(this, "Retry token from %s is invalid.", remote_address); // Invalid retry token was detected. Close the connection. SendImmediateConnectionClose( PathDescriptor{ @@ -1243,8 +1256,8 @@ void Endpoint::Receive(const uv_buf_t& buf, // original retry packet sent to the client. We use it for // validation. Debug(this, - "Retry token is valid. Original dcid %s", - ocid.value()); + "Retry token from %s is valid. Original dcid %s", + remote_address, ocid.value()); config.ocid = ocid.value(); config.retry_scid = dcid; config.set_token(token); @@ -1252,13 +1265,14 @@ void Endpoint::Receive(const uv_buf_t& buf, } case RegularToken::kTokenMagic: { RegularToken token(hd.token, hd.tokenlen); - Debug(this, "Initial packet has regular token %s", token); + Debug(this, "Initial packet from %s has regular token %s", + remote_address, token); if (!token.Validate( version, remote_address, options_.token_secret, options_.token_expiration * NGTCP2_SECONDS)) { - Debug(this, "Regular token is invalid."); + Debug(this, "Regular token from %s is invalid.", remote_address); // If the regular token is invalid, let's send a retry to be // lenient. There's a small risk that a malicious peer is // trying to make us do some work but the risk is fairly low @@ -1275,12 +1289,12 @@ void Endpoint::Receive(const uv_buf_t& buf, STAT_INCREMENT(Stats, packets_received); return; } - Debug(this, "Regular token is valid."); + Debug(this, "Regular token from %s is valid.", remote_address); config.set_token(token); break; } default: { - Debug(this, "Initial packet has unknown token type"); + Debug(this, "Initial packet from %s has unknown token type", remote_address); // If our prefix bit does not match anything we know about, // let's send a retry to be lenient. There's a small risk that a // malicious peer is trying to make us do some work but the risk @@ -1301,10 +1315,11 @@ void Endpoint::Receive(const uv_buf_t& buf, // path to the remote address is valid (for now). Let's record that // so we don't have to do this dance again for this endpoint // instance. - Debug(this, "Remote address is validated"); + Debug(this, "Remote address %s is validated", remote_address); addrLRU_.Upsert(remote_address)->validated = true; } else if (hd.tokenlen > 0) { - Debug(this, "Ignoring initial packet with unexpected token"); + Debug(this, "Ignoring initial packet from %s with unexpected token", + remote_address); // If validation is turned off and there is a token, that's weird. // The peer should only have a token if we sent it to them and we // wouldn't have sent it unless validation was turned on. Let's @@ -1314,7 +1329,7 @@ void Endpoint::Receive(const uv_buf_t& buf, } break; case NGTCP2_PKT_0RTT: - Debug(this, "Sending retry to initial 0RTT packet"); + Debug(this, "Sending retry to %s due to initial 0RTT packet", remote_address); // If it's a 0RTT packet, we're always going to perform path // validation no matter what. This is a bit unfortunate since // ORTT is supposed to be, you know, 0RTT, but sending a retry diff --git a/src/quic/endpoint.h b/src/quic/endpoint.h index 211bea2979d80f..470771eb1a1914 100644 --- a/src/quic/endpoint.h +++ b/src/quic/endpoint.h @@ -228,7 +228,7 @@ class Endpoint final : public AsyncWrap, public Packet::Listener { Session* session); void DisassociateStatelessResetToken(const StatelessResetToken& token); - void Send(BaseObjectPtr packet); + void Send(Packet* packet); // 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 @@ -294,7 +294,7 @@ class Endpoint final : public AsyncWrap, public Packet::Listener { int Start(); void Stop(); void Close(); - int Send(BaseObjectPtr packet); + int Send(Packet* packet); // Returns the local UDP socket address to which we are bound, // or fail with an assert if we are not bound. diff --git a/src/quic/http3.cc b/src/quic/http3.cc index 09480afcafea68..439bb666ef6ba5 100644 --- a/src/quic/http3.cc +++ b/src/quic/http3.cc @@ -82,12 +82,15 @@ class Http3Application final : public Session::Application { Http3Application(Session* session, const Session::Application_Options& options) : Application(session, options), + allocator_(BindingData::Get(env())), options_(options), conn_(InitializeConnection()) { session->set_priority_supported(); } bool Start() override { + CHECK(!started_); + started_ = true; Debug(&session(), "Starting HTTP/3 application."); auto params = ngtcp2_conn_get_remote_transport_params(session()); if (params == nullptr) { @@ -385,7 +388,10 @@ class Http3Application final : public Session::Application { SET_SELF_SIZE(Http3Application); private: - inline operator nghttp3_conn*() const { return conn_.get(); } + inline operator nghttp3_conn*() const { + DCHECK_NOT_NULL(conn_.get()); + return conn_.get(); + } bool CreateAndBindControlStreams() { Debug(&session(), "Creating and binding HTTP/3 control streams"); @@ -418,15 +424,14 @@ class Http3Application final : public Session::Application { Http3ConnectionPointer InitializeConnection() { nghttp3_conn* conn = nullptr; - nghttp3_mem allocator = BindingData::Get(env()); nghttp3_settings settings = options_; if (session().is_server()) { CHECK_EQ(nghttp3_conn_server_new( - &conn, &kCallbacks, &settings, &allocator, this), + &conn, &kCallbacks, &settings, &allocator_, this), 0); } else { CHECK_EQ(nghttp3_conn_client_new( - &conn, &kCallbacks, &settings, &allocator, this), + &conn, &kCallbacks, &settings, &allocator_, this), 0); } return Http3ConnectionPointer(conn); @@ -578,6 +583,8 @@ class Http3Application final : public Session::Application { &session(), "HTTP/3 application received updated settings ", options_); } + bool started_ = false; + nghttp3_mem allocator_; Session::Application_Options options_; Http3ConnectionPointer conn_; int64_t control_stream_id_ = -1; diff --git a/src/quic/packet.cc b/src/quic/packet.cc index 72b958c81125e2..ad5efac70fb909 100644 --- a/src/quic/packet.cc +++ b/src/quic/packet.cc @@ -77,10 +77,6 @@ const SocketAddress& Packet::destination() const { return destination_; } -bool Packet::is_sending() const { - return !!handle_; -} - size_t Packet::length() const { return data_ ? data_->length() : 0; } @@ -113,22 +109,21 @@ Local Packet::GetConstructorTemplate(Environment* env) { return tmpl; } -BaseObjectPtr Packet::Create(Environment* env, - Listener* listener, - const SocketAddress& destination, - size_t length, - const char* diagnostic_label) { - auto& binding = BindingData::Get(env); - if (binding.packet_freelist.empty()) { +Packet* Packet::Create(Environment* env, + Listener* listener, + const SocketAddress& destination, + size_t length, + const char* diagnostic_label) { + if (BindingData::Get(env).packet_freelist.empty()) { Local obj; if (UNLIKELY(!GetConstructorTemplate(env) ->InstanceTemplate() ->NewInstance(env->context()) .ToLocal(&obj))) { - return BaseObjectPtr(); + return nullptr; } - return MakeBaseObject( + return new Packet( env, listener, obj, destination, length, diagnostic_label); } @@ -138,7 +133,7 @@ BaseObjectPtr Packet::Create(Environment* env, destination); } -BaseObjectPtr Packet::Clone() const { +Packet* Packet::Clone() const { auto& binding = BindingData::Get(env()); if (binding.packet_freelist.empty()) { Local obj; @@ -146,28 +141,30 @@ BaseObjectPtr Packet::Clone() const { ->InstanceTemplate() ->NewInstance(env()->context()) .ToLocal(&obj))) { - return BaseObjectPtr(); + return nullptr; } - return MakeBaseObject(env(), listener_, obj, destination_, data_); + return new Packet(env(), listener_, obj, destination_, data_); } return FromFreeList(env(), data_, listener_, destination_); } -BaseObjectPtr Packet::FromFreeList(Environment* env, - std::shared_ptr data, - Listener* listener, - const SocketAddress& destination) { +Packet* Packet::FromFreeList(Environment* env, + std::shared_ptr data, + Listener* listener, + const SocketAddress& destination) { auto& binding = BindingData::Get(env); - auto obj = binding.packet_freelist.back(); + if (binding.packet_freelist.empty()) return nullptr; + Packet* packet = binding.packet_freelist.back(); binding.packet_freelist.pop_back(); - DCHECK_EQ(env, obj->env()); - auto packet = static_cast(obj.get()); - packet->data_ = std::move(data); + CHECK_NOT_NULL(packet); + CHECK_EQ(env, packet->env()); + Debug(packet, "Reusing packet from freelist"); + packet->data_ = data; packet->destination_ = destination; packet->listener_ = listener; - return BaseObjectPtr(packet); + return packet; } Packet::Packet(Environment* env, @@ -179,6 +176,7 @@ Packet::Packet(Environment* env, listener_(listener), destination_(destination), data_(std::move(data)) { + ClearWeak(); Debug(this, "Created a new packet"); } @@ -194,43 +192,22 @@ Packet::Packet(Environment* env, destination, std::make_shared(length, diagnostic_label)) {} -int Packet::Send(uv_udp_t* handle, BaseObjectPtr ref) { - if (is_sending()) return UV_EALREADY; - if (data_ == nullptr) return UV_EINVAL; - DCHECK(!is_sending()); - handle_ = std::move(ref); - uv_buf_t buf = *this; - Debug(this, "Packet it sending"); - return Dispatch( - uv_udp_send, - handle, - &buf, - 1, - destination().data(), - uv_udp_send_cb{[](uv_udp_send_t* req, int status) { - auto ptr = static_cast(ReqWrap::from_req(req)); - ptr->Done(status); - // Do not try accessing ptr after this. We don't know if it - // was freelisted or destroyed. Either way, done means done. - }}); -} - void Packet::Done(int status) { Debug(this, "Packet is done with status %d", status); if (listener_ != nullptr) { listener_->PacketDone(status); } - listener_ = nullptr; - handle_.reset(); - data_.reset(); - Reset(); // As a performance optimization, we add this packet to a freelist // rather than deleting it but only if the freelist isn't too // big, we don't want to accumulate these things forever. auto& binding = BindingData::Get(env()); if (binding.packet_freelist.size() < kMaxFreeList) { - binding.packet_freelist.emplace_back(this); + Debug(this, "Returning packet to freelist"); + listener_ = nullptr; + data_.reset(); + Reset(); + binding.packet_freelist.push_back(this); } else { delete this; } @@ -244,10 +221,9 @@ std::string Packet::ToString() const { void Packet::MemoryInfo(MemoryTracker* tracker) const { tracker->TrackField("destination", destination_); tracker->TrackField("data", data_); - tracker->TrackField("handle", handle_); } -BaseObjectPtr Packet::CreateRetryPacket( +Packet* Packet::CreateRetryPacket( Environment* env, Listener* listener, const PathDescriptor& path_descriptor, @@ -259,7 +235,7 @@ BaseObjectPtr Packet::CreateRetryPacket( cid, path_descriptor.dcid, token_secret); - if (!token) return BaseObjectPtr(); + if (!token) return nullptr; const ngtcp2_vec& vec = token; @@ -268,7 +244,7 @@ BaseObjectPtr Packet::CreateRetryPacket( auto packet = Create(env, listener, path_descriptor.remote_address, pktlen, "retry"); - if (!packet) return BaseObjectPtr(); + if (packet == nullptr) return nullptr; ngtcp2_vec dest = *packet; @@ -282,13 +258,13 @@ BaseObjectPtr Packet::CreateRetryPacket( vec.len); if (nwrite <= 0) { packet->Done(UV_ECANCELED); - return BaseObjectPtr(); + return nullptr; } packet->Truncate(static_cast(nwrite)); return packet; } -BaseObjectPtr Packet::CreateConnectionClosePacket( +Packet* Packet::CreateConnectionClosePacket( Environment* env, Listener* listener, const SocketAddress& destination, @@ -296,19 +272,20 @@ BaseObjectPtr Packet::CreateConnectionClosePacket( const QuicError& error) { auto packet = Create( env, listener, destination, kDefaultMaxPacketLength, "connection close"); + if (packet == nullptr) return nullptr; ngtcp2_vec vec = *packet; ssize_t nwrite = ngtcp2_conn_write_connection_close( conn, nullptr, nullptr, vec.base, vec.len, error, uv_hrtime()); if (nwrite < 0) { packet->Done(UV_ECANCELED); - return BaseObjectPtr(); + return nullptr; } packet->Truncate(static_cast(nwrite)); return packet; } -BaseObjectPtr Packet::CreateImmediateConnectionClosePacket( +Packet* Packet::CreateImmediateConnectionClosePacket( Environment* env, Listener* listener, const PathDescriptor& path_descriptor, @@ -318,6 +295,7 @@ BaseObjectPtr Packet::CreateImmediateConnectionClosePacket( path_descriptor.remote_address, kDefaultMaxPacketLength, "immediate connection close (endpoint)"); + if (packet == nullptr) return nullptr; ngtcp2_vec vec = *packet; ssize_t nwrite = ngtcp2_crypto_write_connection_close( vec.base, @@ -332,13 +310,13 @@ BaseObjectPtr Packet::CreateImmediateConnectionClosePacket( 0); if (nwrite <= 0) { packet->Done(UV_ECANCELED); - return BaseObjectPtr(); + return nullptr; } packet->Truncate(static_cast(nwrite)); return packet; } -BaseObjectPtr Packet::CreateStatelessResetPacket( +Packet* Packet::CreateStatelessResetPacket( Environment* env, Listener* listener, const PathDescriptor& path_descriptor, @@ -351,7 +329,7 @@ BaseObjectPtr Packet::CreateStatelessResetPacket( // QUIC spec. The reason is that packets less than 41 bytes may allow an // observer to reliably determine that it's a stateless reset. size_t pktlen = source_len - 1; - if (pktlen < kMinStatelessResetLen) return BaseObjectPtr(); + if (pktlen < kMinStatelessResetLen) return nullptr; StatelessResetToken token(token_secret, path_descriptor.dcid); uint8_t random[kRandlen]; @@ -362,20 +340,21 @@ BaseObjectPtr Packet::CreateStatelessResetPacket( path_descriptor.remote_address, kDefaultMaxPacketLength, "stateless reset"); + if (packet == nullptr) return nullptr; ngtcp2_vec vec = *packet; ssize_t nwrite = ngtcp2_pkt_write_stateless_reset( vec.base, pktlen, token, random, kRandlen); if (nwrite <= static_cast(kMinStatelessResetLen)) { packet->Done(UV_ECANCELED); - return BaseObjectPtr(); + return nullptr; } packet->Truncate(static_cast(nwrite)); return packet; } -BaseObjectPtr Packet::CreateVersionNegotiationPacket( +Packet* Packet::CreateVersionNegotiationPacket( Environment* env, Listener* listener, const PathDescriptor& path_descriptor) { @@ -411,6 +390,7 @@ BaseObjectPtr Packet::CreateVersionNegotiationPacket( path_descriptor.remote_address, kDefaultMaxPacketLength, "version negotiation"); + if (packet == nullptr) return nullptr; ngtcp2_vec vec = *packet; ssize_t nwrite = @@ -425,7 +405,7 @@ BaseObjectPtr Packet::CreateVersionNegotiationPacket( arraysize(sv)); if (nwrite <= 0) { packet->Done(UV_ECANCELED); - return BaseObjectPtr(); + return nullptr; } packet->Truncate(static_cast(nwrite)); return packet; diff --git a/src/quic/packet.h b/src/quic/packet.h index 1e4f3d43472f1e..da30fca133c2ce 100644 --- a/src/quic/packet.h +++ b/src/quic/packet.h @@ -50,8 +50,6 @@ class Packet final : public ReqWrap { struct Data; public: - using Queue = std::deque>; - static v8::Local GetConstructorTemplate( Environment* env); @@ -84,7 +82,6 @@ class Packet final : public ReqWrap { Packet& operator=(Packet&&) = delete; const SocketAddress& destination() const; - bool is_sending() const; size_t length() const; operator uv_buf_t() const; operator ngtcp2_vec() const; @@ -95,14 +92,14 @@ class Packet final : public ReqWrap { // tells us how many of the packets bytes were used. void Truncate(size_t len); - static BaseObjectPtr Create( + static Packet* Create( Environment* env, Listener* listener, const SocketAddress& destination, size_t length = kDefaultMaxPacketLength, const char* diagnostic_label = ""); - BaseObjectPtr Clone() const; + Packet* Clone() const; void MemoryInfo(MemoryTracker* tracker) const override; SET_MEMORY_INFO_NAME(Packet) @@ -110,38 +107,33 @@ class Packet final : public ReqWrap { std::string ToString() const; - // Transmits the packet. The handle is the bound uv_udp_t - // port that we're sending on, the ref is a pointer to the - // HandleWrap that owns the handle. - int Send(uv_udp_t* handle, BaseObjectPtr ref); - - static BaseObjectPtr CreateRetryPacket( + static Packet* CreateRetryPacket( Environment* env, Listener* listener, const PathDescriptor& path_descriptor, const TokenSecret& token_secret); - static BaseObjectPtr CreateConnectionClosePacket( + static Packet* CreateConnectionClosePacket( Environment* env, Listener* listener, const SocketAddress& destination, ngtcp2_conn* conn, const QuicError& error); - static BaseObjectPtr CreateImmediateConnectionClosePacket( + static Packet* CreateImmediateConnectionClosePacket( Environment* env, Listener* listener, const PathDescriptor& path_descriptor, const QuicError& reason); - static BaseObjectPtr CreateStatelessResetPacket( + static Packet* CreateStatelessResetPacket( Environment* env, Listener* listener, const PathDescriptor& path_descriptor, const TokenSecret& token_secret, size_t source_len); - static BaseObjectPtr CreateVersionNegotiationPacket( + static Packet* CreateVersionNegotiationPacket( Environment* env, Listener* listener, const PathDescriptor& path_descriptor); @@ -150,15 +142,14 @@ class Packet final : public ReqWrap { void Done(int status); private: - static BaseObjectPtr FromFreeList(Environment* env, - std::shared_ptr data, - Listener* listener, - const SocketAddress& destination); + static Packet* FromFreeList(Environment* env, + std::shared_ptr data, + Listener* listener, + const SocketAddress& destination); Listener* listener_; SocketAddress destination_; std::shared_ptr data_; - BaseObjectPtr handle_; }; } // namespace quic diff --git a/src/quic/session.cc b/src/quic/session.cc index 2608da66594367..56d88896755ed0 100644 --- a/src/quic/session.cc +++ b/src/quic/session.cc @@ -184,6 +184,17 @@ Session::SendPendingDataScope::~SendPendingDataScope() { // ============================================================================ namespace { + +inline const char* getEncryptionLevelName(ngtcp2_encryption_level level) { + switch (level) { + case NGTCP2_ENCRYPTION_LEVEL_1RTT: return "1rtt"; + case NGTCP2_ENCRYPTION_LEVEL_0RTT: return "0rtt"; + case NGTCP2_ENCRYPTION_LEVEL_HANDSHAKE: return "handshake"; + case NGTCP2_ENCRYPTION_LEVEL_INITIAL: return "initial"; + } + return ""; +} + // Qlog is a JSON-based logging format that is being standardized for low-level // debug logging of QUIC connections and dataflows. The qlog output is generated // optionally by ngtcp2 for us. The on_qlog_write callback is registered with @@ -497,11 +508,11 @@ Session::Session(Endpoint* endpoint, allocator_(BindingData::Get(env())), endpoint_(BaseObjectWeakPtr(endpoint)), config_(config), + local_address_(config.local_address), + remote_address_(config.remote_address), connection_(InitConnection()), tls_context_(env(), config_.side, this, config_.options.tls_options), application_(select_application()), - local_address_(config.local_address), - remote_address_(config.remote_address), timer_(env(), [this, self = BaseObjectPtr(this)] { OnTimeout(); }) { MakeWeak(); @@ -765,7 +776,6 @@ bool Session::Receive(Store&& store, uint64_t now = uv_hrtime(); ngtcp2_pkt_info pi{}; // Not used but required. - Debug(this, "Session is receiving packet"); int err = ngtcp2_conn_read_pkt(*this, path, &pi, vec.base, vec.len, now); switch (err) { case 0: { @@ -818,7 +828,7 @@ bool Session::Receive(Store&& store, } // Shouldn't happen but just in case. last_error_ = QuicError::ForNgtcp2Error(err); - Debug(this, "Error while receiving packet: %s", last_error_); + Debug(this, "Error while receiving packet: %s (%d)", last_error_, err); Close(); return false; }; @@ -826,6 +836,7 @@ bool Session::Receive(Store&& store, auto update_stats = OnScopeLeave([&] { UpdateDataStats(); }); remote_address_ = remote_address; Path path(local_address, remote_address_); + Debug(this, "Session is receiving packet received along path %s", path); STAT_INCREMENT_N(Stats, bytes_received, store.length()); if (receivePacket(&path, store)) application().SendPendingData(); @@ -834,7 +845,7 @@ bool Session::Receive(Store&& store, return true; } -void Session::Send(BaseObjectPtr packet) { +void Session::Send(Packet* packet) { // Sending a Packet is generally best effort. If we're not in a state // where we can send a packet, it's ok to drop it on the floor. The // packet loss mechanisms will cause the packet data to be resent later @@ -845,7 +856,7 @@ void Session::Send(BaseObjectPtr packet) { if (can_send_packets() && packet->length() > 0) { Debug(this, "Session is sending %s", packet->ToString()); STAT_INCREMENT_N(Stats, bytes_sent, packet->length()); - endpoint_->Send(std::move(packet)); + endpoint_->Send(packet); return; } @@ -853,9 +864,9 @@ void Session::Send(BaseObjectPtr packet) { packet->Done(packet->length() > 0 ? UV_ECANCELED : 0); } -void Session::Send(BaseObjectPtr packet, const PathStorage& path) { +void Session::Send(Packet* packet, const PathStorage& path) { UpdatePath(path); - Send(std::move(packet)); + Send(packet); } uint64_t Session::SendDatagram(Store&& data) { @@ -868,7 +879,7 @@ uint64_t Session::SendDatagram(Store&& data) { } Debug(this, "Session is sending datagram"); - BaseObjectPtr packet; + Packet* packet = nullptr; uint8_t* pos = nullptr; int accepted = 0; ngtcp2_vec vec = data; @@ -885,7 +896,7 @@ uint64_t Session::SendDatagram(Store&& data) { // datagram packet. On each iteration here we'll try to encode the // datagram. It's entirely up to ngtcp2 whether to include the datagram // in the packet on each call to ngtcp2_conn_writev_datagram. - if (!packet) { + if (packet == nullptr) { packet = Packet::Create(env(), endpoint_.get(), remote_address_, @@ -893,7 +904,7 @@ uint64_t Session::SendDatagram(Store&& data) { "datagram"); // Typically sending datagrams is best effort, but if we cannot create // the packet, then we handle it as a fatal error. - if (!packet) { + if (packet == nullptr) { last_error_ = QuicError::ForNgtcp2Error(NGTCP2_ERR_INTERNAL); Close(CloseMethod::SILENT); return 0; @@ -1086,7 +1097,7 @@ void Session::RemoveStream(int64_t id) { // ngtcp2 does not 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. - Debug(this, "Removing stream " PRIi64 " from session", id); + Debug(this, "Removing stream %" PRIi64 " from session", id); if (!is_in_draining_period() && !is_in_closing_period() && !state_->silent_close && !ngtcp2_conn_is_local_stream(connection_.get(), id)) { @@ -1936,13 +1947,6 @@ struct Session::Impl { : NGTCP2_ERR_CALLBACK_FAILURE; } - static int on_get_path_challenge_data(ngtcp2_conn* conn, - uint8_t* data, - void* user_data) { - CHECK(crypto::CSPRNG(data, NGTCP2_PATH_CHALLENGE_DATALEN).is_ok()); - return NGTCP2_SUCCESS; - } - static int on_handshake_completed(ngtcp2_conn* conn, void* user_data) { NGTCP2_CALLBACK_SCOPE(session) return session->HandshakeCompleted() ? NGTCP2_SUCCESS @@ -1988,17 +1992,6 @@ struct Session::Impl { return NGTCP2_SUCCESS; } - static int on_receive_crypto_data(ngtcp2_conn* conn, - ngtcp2_encryption_level level, - uint64_t offset, - const uint8_t* data, - size_t datalen, - void* user_data) { - NGTCP2_CALLBACK_SCOPE(session) - return session->tls_context().Receive( - static_cast(level), offset, data, datalen); - } - static int on_receive_datagram(ngtcp2_conn* conn, uint32_t flags, const uint8_t* data, @@ -2023,7 +2016,11 @@ struct Session::Impl { static int on_receive_rx_key(ngtcp2_conn* conn, ngtcp2_encryption_level level, void* user_data) { - NGTCP2_CALLBACK_SCOPE(session) + auto session = Impl::From(conn, user_data); + if (UNLIKELY(session->is_destroyed())) return NGTCP2_ERR_CALLBACK_FAILURE; + + Debug(session, "Receiving RX key for level %d for dcid %s", + getEncryptionLevelName(level), session->config().dcid); if (!session->is_server() && (level == NGTCP2_ENCRYPTION_LEVEL_0RTT || level == NGTCP2_ENCRYPTION_LEVEL_1RTT)) { @@ -2076,7 +2073,12 @@ struct Session::Impl { static int on_receive_tx_key(ngtcp2_conn* conn, ngtcp2_encryption_level level, void* user_data) { - NGTCP2_CALLBACK_SCOPE(session) + auto session = Impl::From(conn, user_data); + if (UNLIKELY(session->is_destroyed())) return NGTCP2_ERR_CALLBACK_FAILURE; + + Debug(session, "Receiving TX key for level %d for dcid %s", + getEncryptionLevelName(level), session->config().dcid); + if (session->is_server() && (level == NGTCP2_ENCRYPTION_LEVEL_0RTT || level == NGTCP2_ENCRYPTION_LEVEL_1RTT)) { if (!session->application().Start()) return NGTCP2_ERR_CALLBACK_FAILURE; @@ -2170,7 +2172,7 @@ struct Session::Impl { static constexpr ngtcp2_callbacks CLIENT = { ngtcp2_crypto_client_initial_cb, nullptr, - on_receive_crypto_data, + ngtcp2_crypto_recv_crypto_data_cb, on_handshake_completed, on_receive_version_negotiation, ngtcp2_crypto_encrypt_cb, @@ -2202,7 +2204,7 @@ struct Session::Impl { on_receive_datagram, on_acknowledge_datagram, on_lost_datagram, - on_get_path_challenge_data, + ngtcp2_crypto_get_path_challenge_data_cb, on_stream_stop_sending, ngtcp2_crypto_version_negotiation_cb, on_receive_rx_key, @@ -2212,7 +2214,7 @@ struct Session::Impl { static constexpr ngtcp2_callbacks SERVER = { nullptr, ngtcp2_crypto_recv_client_initial_cb, - on_receive_crypto_data, + ngtcp2_crypto_recv_crypto_data_cb, on_handshake_completed, nullptr, ngtcp2_crypto_encrypt_cb, @@ -2244,7 +2246,7 @@ struct Session::Impl { on_receive_datagram, on_acknowledge_datagram, on_lost_datagram, - on_get_path_challenge_data, + ngtcp2_crypto_get_path_challenge_data_cb, on_stream_stop_sending, ngtcp2_crypto_version_negotiation_cb, on_receive_rx_key, @@ -2288,6 +2290,7 @@ void Session::RegisterExternalReferences(ExternalReferenceRegistry* registry) { Session::QuicConnectionPointer Session::InitConnection() { ngtcp2_conn* conn; Path path(local_address_, remote_address_); + Debug(this, "Initializing session for path %s", path); TransportParams::Config tp_config( config_.side, config_.ocid, config_.retry_scid); TransportParams transport_params(tp_config, config_.options.transport_params); diff --git a/src/quic/session.h b/src/quic/session.h index f99716ba0181b5..eded22481e8a36 100644 --- a/src/quic/session.h +++ b/src/quic/session.h @@ -318,8 +318,8 @@ class Session final : public AsyncWrap, private SessionTicket::AppData::Source { const SocketAddress& local_address, const SocketAddress& remote_address); - void Send(BaseObjectPtr packet); - void Send(BaseObjectPtr packet, const PathStorage& path); + void Send(Packet* packet); + void Send(Packet* packet, const PathStorage& path); uint64_t SendDatagram(Store&& data); void AddStream(const BaseObjectPtr& stream); @@ -415,17 +415,17 @@ class Session final : public AsyncWrap, private SessionTicket::AppData::Source { ngtcp2_mem allocator_; BaseObjectWeakPtr endpoint_; Config config_; + SocketAddress local_address_; + SocketAddress remote_address_; QuicConnectionPointer connection_; TLSContext tls_context_; std::unique_ptr application_; - SocketAddress local_address_; - SocketAddress remote_address_; StreamsMap streams_; TimerWrapHandle timer_; size_t send_scope_depth_ = 0; size_t connection_close_depth_ = 0; QuicError last_error_; - BaseObjectPtr conn_closebuf_; + Packet* conn_closebuf_; BaseObjectPtr qlog_stream_; BaseObjectPtr keylog_stream_; diff --git a/src/quic/streams.cc b/src/quic/streams.cc index c8c0ecd94947e9..1a6e05f42f610b 100644 --- a/src/quic/streams.cc +++ b/src/quic/streams.cc @@ -914,6 +914,8 @@ void Stream::EndReadable(std::optional maybe_final_size) { void Stream::Destroy(QuicError error) { if (is_destroyed()) return; + DCHECK_NOT_NULL(session_.get()); + Debug(this, "Stream %" PRIi64 " being destroyed with error %s", id(), error); // End the writable before marking as destroyed. EndWritable(); @@ -932,8 +934,11 @@ void Stream::Destroy(QuicError error) { // the JavaScript side could still have a reader on the inbound DataQueue, // which may keep that data alive a bit longer. inbound_->removeBackpressureListener(this); + inbound_.reset(); + CHECK_NOT_NULL(session_.get()); + // Finally, remove the stream from the session and clear our reference // to the session. session_->RemoveStream(id()); diff --git a/src/quic/tlscontext.cc b/src/quic/tlscontext.cc index 6e72fa88d578ba..6f08a91c98d8df 100644 --- a/src/quic/tlscontext.cc +++ b/src/quic/tlscontext.cc @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -34,6 +35,13 @@ namespace quic { const TLSContext::Options TLSContext::Options::kDefault = {}; namespace { + +// One time initialization +auto _ = []() { + CHECK_EQ(ngtcp2_crypto_quictls_init(), 0); + return 0; +}(); + constexpr size_t kMaxAlpnLen = 255; int AllowEarlyDataCallback(SSL* ssl, void* arg) { @@ -85,13 +93,14 @@ int AlpnSelectionCallback(SSL* ssl, } BaseObjectPtr InitializeSecureContext( - Side side, Environment* env, const TLSContext::Options& options) { + Session* session, Side side, Environment* env, const TLSContext::Options& options) { auto context = crypto::SecureContext::Create(env); auto& ctx = context->ctx(); switch (side) { case Side::SERVER: { + Debug(session, "Initializing secure context for server"); ctx.reset(SSL_CTX_new(TLS_server_method())); SSL_CTX_set_app_data(ctx.get(), context); @@ -122,6 +131,7 @@ BaseObjectPtr InitializeSecureContext( break; } case Side::CLIENT: { + Debug(session, "Initializing secure context for client"); ctx.reset(SSL_CTX_new(TLS_client_method())); SSL_CTX_set_app_data(ctx.get(), context); @@ -261,6 +271,8 @@ bool SetOption(Environment* env, v8::Local value; if (!object->Get(env->context(), name).ToLocal(&value)) return false; + if (value->IsUndefined()) return true; + // The value can be either a single item or an array of items. if (value->IsArray()) { @@ -279,7 +291,8 @@ bool SetOption(Environment* env, ASSIGN_OR_RETURN_UNWRAP(&handle, item, false); (options->*member).push_back(handle->Data()); } else { - THROW_ERR_INVALID_ARG_TYPE(env, "options must be an object"); + Utf8Value namestr(env->isolate(), name); + THROW_ERR_INVALID_ARG_TYPE(env, "%s value must be a key object", *namestr); return false; } } else if constexpr (std::is_same::value) { @@ -288,7 +301,8 @@ bool SetOption(Environment* env, } else if (item->IsArrayBuffer()) { (options->*member).emplace_back(item.As()); } else { - THROW_ERR_INVALID_ARG_TYPE(env, "options must be an object"); + Utf8Value namestr(env->isolate(), name); + THROW_ERR_INVALID_ARG_TYPE(env, "%s value must be an array buffer", *namestr); return false; } } @@ -301,7 +315,8 @@ bool SetOption(Environment* env, ASSIGN_OR_RETURN_UNWRAP(&handle, value, false); (options->*member).push_back(handle->Data()); } else { - THROW_ERR_INVALID_ARG_TYPE(env, "options must be an object"); + Utf8Value namestr(env->isolate(), name); + THROW_ERR_INVALID_ARG_TYPE(env, "%s value must be a key object", *namestr); return false; } } else if constexpr (std::is_same::value) { @@ -310,7 +325,8 @@ bool SetOption(Environment* env, } else if (value->IsArrayBuffer()) { (options->*member).emplace_back(value.As()); } else { - THROW_ERR_INVALID_ARG_TYPE(env, "options must be an object"); + Utf8Value namestr(env->isolate(), name); + THROW_ERR_INVALID_ARG_TYPE(env, "%s value must be an array buffer", *namestr); return false; } } @@ -348,7 +364,7 @@ TLSContext::TLSContext(Environment* env, env_(env), session_(session), options_(options), - secure_context_(InitializeSecureContext(side, env, options)) { + secure_context_(InitializeSecureContext(session, side, env, options)) { CHECK(secure_context_); ssl_.reset(SSL_new(secure_context_->ctx().get())); CHECK(ssl_ && SSL_is_quic(ssl_.get())); @@ -403,37 +419,6 @@ void TLSContext::Keylog(const char* line) const { session_->EmitKeylog(line); } -int TLSContext::Receive(TLSContext::EncryptionLevel level, - uint64_t offset, - const uint8_t* data, - size_t datalen) { - Debug(session_, "Crypto context received data"); - // ngtcp2 provides an implementation of this in - // ngtcp2_crypto_recv_crypto_data_cb but given that we are using the - // implementation specific error codes below, we can't use it. - - if (UNLIKELY(session_->is_destroyed())) return NGTCP2_ERR_CALLBACK_FAILURE; - - // 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_, static_cast(level), data, datalen); - - switch (ret) { - case 0: - // Fall-through - - // 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_QUICTLS_ERR_TLS_WANT_X509_LOOKUP: - [[fallthrough]]; - case NGTCP2_CRYPTO_QUICTLS_ERR_TLS_WANT_CLIENT_HELLO_CB: - return 0; - } - return ret; -} - int TLSContext::OnNewSession(SSL_SESSION* session) { Debug(session_, "Crypto context received new crypto session"); // Used to generate and emit a SessionTicket for TLS session resumption. @@ -568,8 +553,15 @@ Maybe TLSContext::Options::From(Environment* env, auto& state = BindingData::Get(env); if (value->IsUndefined()) { + // We need at least one key and one cert to complete the tls handshake. + // Why not make this an error? We could but it's not strictly necessary. + env->EmitProcessEnvWarning(); + ProcessEmitWarning(env, "The default QUIC TLS options are being used. " + "This means there is no key or certificate configured and the " + "TLS handshake will fail. This is likely not what you want."); return Just(options); } + if (!value->IsObject()) { THROW_ERR_INVALID_ARG_TYPE(env, "options must be an object"); return Nothing(); @@ -595,6 +587,14 @@ Maybe TLSContext::Options::From(Environment* env, return Nothing(); } + // We need at least one key and one cert to complete the tls handshake. + // Why not make this an error? We could but it's not strictly necessary. + if (options.keys.empty() || options.certs.empty()) { + env->EmitProcessEnvWarning(); + ProcessEmitWarning(env, "The QUIC TLS options did not include a key or cert. " + "This means the TLS handshake will fail. This is likely not what you want."); + } + return Just(options); } diff --git a/src/quic/tlscontext.h b/src/quic/tlscontext.h index 8c638260ce6004..65d8e2f4e74942 100644 --- a/src/quic/tlscontext.h +++ b/src/quic/tlscontext.h @@ -1,6 +1,5 @@ #pragma once -#include #if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS #if HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC @@ -126,13 +125,6 @@ class TLSContext final : public MemoryRetainer { // decrypt QUIC network traffic. void Keylog(const char* line) const; - // Called 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 Receive(TLSContext::EncryptionLevel level, - uint64_t offset, - const uint8_t* data, - size_t datalen); - v8::MaybeLocal cert(Environment* env) const; v8::MaybeLocal peer_cert(Environment* env) const; v8::MaybeLocal cipher_name(Environment* env) const; From 84dbe6ac484b3beb7d75583155f2845f26882fe3 Mon Sep 17 00:00:00 2001 From: James M Snell Date: Fri, 29 Dec 2023 14:29:16 -0800 Subject: [PATCH 6/8] quic: fixup linting after multiple updates --- src/quic/endpoint.cc | 61 +++++++++++++++++++++++++++++------------- src/quic/http3.cc | 8 +++--- src/quic/packet.cc | 20 +++++++------- src/quic/packet.h | 31 ++++++++++----------- src/quic/session.cc | 24 +++++++++++------ src/quic/tlscontext.cc | 29 +++++++++++++------- src/quic/tokens.cc | 6 ++--- 7 files changed, 108 insertions(+), 71 deletions(-) diff --git a/src/quic/endpoint.cc b/src/quic/endpoint.cc index b70429f42a884b..a9b401d18fdb6c 100644 --- a/src/quic/endpoint.cc +++ b/src/quic/endpoint.cc @@ -565,13 +565,17 @@ int Endpoint::UDP::Send(Packet* packet) { // which we don't want. We call ClearWeak here just to be doubly sure. packet->ClearWeak(); packet->Dispatched(); - int err = uv_udp_send(packet->req(), &impl_->handle_, &buf, 1, packet->destination().data(), - uv_udp_send_cb{[](uv_udp_send_t* req, int status) { - auto ptr = - static_cast(ReqWrap::from_req(req)); - ptr->env()->DecreaseWaitingRequestCounter(); - ptr->Done(status); - }}); + int err = uv_udp_send( + packet->req(), + &impl_->handle_, + &buf, + 1, + packet->destination().data(), + uv_udp_send_cb{[](uv_udp_send_t* req, int status) { + auto ptr = static_cast(ReqWrap::from_req(req)); + ptr->env()->DecreaseWaitingRequestCounter(); + ptr->Done(status); + }}); if (err < 0) { // The packet failed. packet->Done(err); @@ -1108,7 +1112,10 @@ void Endpoint::Receive(const uv_buf_t& buf, const SocketAddress& local_address, const SocketAddress& remote_address) { // Conditionally accept an initial packet to create a new session. - Debug(this, "Trying to accept initial packet for %s from %s", dcid, remote_address); + Debug(this, + "Trying to accept initial packet for %s from %s", + dcid, + remote_address); // If we're not listening as a server, do not accept an initial packet. if (state_->listening == 0) return; @@ -1156,7 +1163,8 @@ void Endpoint::Receive(const uv_buf_t& buf, Debug(this, "Packet was not accepted because the endpoint is busy or the " "remote address %s has exceeded their maximum number of concurrent " - "connections", remote_address); + "connections", + remote_address); // Endpoint is busy or the connection count is exceeded. The connection is // refused. For the purpose of stats collection, we'll count both of these // the same. @@ -1213,7 +1221,8 @@ void Endpoint::Receive(const uv_buf_t& buf, if (hd.tokenlen == 0) { Debug(this, "Initial packet has no token. Sending retry to %s to start " - "validation", remote_address); + "validation", + remote_address); SendRetry(PathDescriptor{ version, dcid, @@ -1232,7 +1241,10 @@ void Endpoint::Receive(const uv_buf_t& buf, switch (hd.token[0]) { case RetryToken::kTokenMagic: { RetryToken token(hd.token, hd.tokenlen); - Debug(this, "Initial packet from %s has retry token %s", remote_address, token); + Debug(this, + "Initial packet from %s has retry token %s", + remote_address, + token); auto ocid = token.Validate( version, remote_address, @@ -1240,7 +1252,8 @@ void Endpoint::Receive(const uv_buf_t& buf, options_.token_secret, options_.retry_token_expiration * NGTCP2_SECONDS); if (!ocid.has_value()) { - Debug(this, "Retry token from %s is invalid.", remote_address); + Debug( + this, "Retry token from %s is invalid.", remote_address); // Invalid retry token was detected. Close the connection. SendImmediateConnectionClose( PathDescriptor{ @@ -1257,7 +1270,8 @@ void Endpoint::Receive(const uv_buf_t& buf, // validation. Debug(this, "Retry token from %s is valid. Original dcid %s", - remote_address, ocid.value()); + remote_address, + ocid.value()); config.ocid = ocid.value(); config.retry_scid = dcid; config.set_token(token); @@ -1265,14 +1279,18 @@ void Endpoint::Receive(const uv_buf_t& buf, } case RegularToken::kTokenMagic: { RegularToken token(hd.token, hd.tokenlen); - Debug(this, "Initial packet from %s has regular token %s", - remote_address, token); + Debug(this, + "Initial packet from %s has regular token %s", + remote_address, + token); if (!token.Validate( version, remote_address, options_.token_secret, options_.token_expiration * NGTCP2_SECONDS)) { - Debug(this, "Regular token from %s is invalid.", remote_address); + Debug(this, + "Regular token from %s is invalid.", + remote_address); // If the regular token is invalid, let's send a retry to be // lenient. There's a small risk that a malicious peer is // trying to make us do some work but the risk is fairly low @@ -1294,7 +1312,9 @@ void Endpoint::Receive(const uv_buf_t& buf, break; } default: { - Debug(this, "Initial packet from %s has unknown token type", remote_address); + Debug(this, + "Initial packet from %s has unknown token type", + remote_address); // If our prefix bit does not match anything we know about, // let's send a retry to be lenient. There's a small risk that a // malicious peer is trying to make us do some work but the risk @@ -1318,7 +1338,8 @@ void Endpoint::Receive(const uv_buf_t& buf, Debug(this, "Remote address %s is validated", remote_address); addrLRU_.Upsert(remote_address)->validated = true; } else if (hd.tokenlen > 0) { - Debug(this, "Ignoring initial packet from %s with unexpected token", + Debug(this, + "Ignoring initial packet from %s with unexpected token", remote_address); // If validation is turned off and there is a token, that's weird. // The peer should only have a token if we sent it to them and we @@ -1329,7 +1350,9 @@ void Endpoint::Receive(const uv_buf_t& buf, } break; case NGTCP2_PKT_0RTT: - Debug(this, "Sending retry to %s due to initial 0RTT packet", remote_address); + Debug(this, + "Sending retry to %s due to initial 0RTT packet", + remote_address); // If it's a 0RTT packet, we're always going to perform path // validation no matter what. This is a bit unfortunate since // ORTT is supposed to be, you know, 0RTT, but sending a retry diff --git a/src/quic/http3.cc b/src/quic/http3.cc index 439bb666ef6ba5..fb6c9bc4aef1f4 100644 --- a/src/quic/http3.cc +++ b/src/quic/http3.cc @@ -1,5 +1,6 @@ #if HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC +#include "http3.h" #include #include #include @@ -13,7 +14,6 @@ #include "application.h" #include "bindingdata.h" #include "defs.h" -#include "http3.h" #include "session.h" #include "sessionticket.h" @@ -383,9 +383,9 @@ class Http3Application final : public Session::Application { return data.id > -1 && !is_control_stream(data.id) && data.fin == 1; } - SET_NO_MEMORY_INFO(); - SET_MEMORY_INFO_NAME(Http3Application); - SET_SELF_SIZE(Http3Application); + SET_NO_MEMORY_INFO() + SET_MEMORY_INFO_NAME(Http3Application) + SET_SELF_SIZE(Http3Application) private: inline operator nghttp3_conn*() const { diff --git a/src/quic/packet.cc b/src/quic/packet.cc index ad5efac70fb909..dad5b59ad3cbe5 100644 --- a/src/quic/packet.cc +++ b/src/quic/packet.cc @@ -223,11 +223,10 @@ void Packet::MemoryInfo(MemoryTracker* tracker) const { tracker->TrackField("data", data_); } -Packet* Packet::CreateRetryPacket( - Environment* env, - Listener* listener, - const PathDescriptor& path_descriptor, - const TokenSecret& token_secret) { +Packet* Packet::CreateRetryPacket(Environment* env, + Listener* listener, + const PathDescriptor& path_descriptor, + const TokenSecret& token_secret) { auto& random = CID::Factory::random(); CID cid = random.Generate(); RetryToken token(path_descriptor.version, @@ -264,12 +263,11 @@ Packet* Packet::CreateRetryPacket( return packet; } -Packet* Packet::CreateConnectionClosePacket( - Environment* env, - Listener* listener, - const SocketAddress& destination, - ngtcp2_conn* conn, - const QuicError& error) { +Packet* Packet::CreateConnectionClosePacket(Environment* env, + Listener* listener, + const SocketAddress& destination, + ngtcp2_conn* conn, + const QuicError& error) { auto packet = Create( env, listener, destination, kDefaultMaxPacketLength, "connection close"); if (packet == nullptr) return nullptr; diff --git a/src/quic/packet.h b/src/quic/packet.h index da30fca133c2ce..c92f2fd4a60f82 100644 --- a/src/quic/packet.h +++ b/src/quic/packet.h @@ -92,12 +92,11 @@ class Packet final : public ReqWrap { // tells us how many of the packets bytes were used. void Truncate(size_t len); - static Packet* Create( - Environment* env, - Listener* listener, - const SocketAddress& destination, - size_t length = kDefaultMaxPacketLength, - const char* diagnostic_label = ""); + static Packet* Create(Environment* env, + Listener* listener, + const SocketAddress& destination, + size_t length = kDefaultMaxPacketLength, + const char* diagnostic_label = ""); Packet* Clone() const; @@ -107,18 +106,16 @@ class Packet final : public ReqWrap { std::string ToString() const; - static Packet* CreateRetryPacket( - Environment* env, - Listener* listener, - const PathDescriptor& path_descriptor, - const TokenSecret& token_secret); + static Packet* CreateRetryPacket(Environment* env, + Listener* listener, + const PathDescriptor& path_descriptor, + const TokenSecret& token_secret); - static Packet* CreateConnectionClosePacket( - Environment* env, - Listener* listener, - const SocketAddress& destination, - ngtcp2_conn* conn, - const QuicError& error); + static Packet* CreateConnectionClosePacket(Environment* env, + Listener* listener, + const SocketAddress& destination, + ngtcp2_conn* conn, + const QuicError& error); static Packet* CreateImmediateConnectionClosePacket( Environment* env, diff --git a/src/quic/session.cc b/src/quic/session.cc index 56d88896755ed0..4c9aad191ad650 100644 --- a/src/quic/session.cc +++ b/src/quic/session.cc @@ -187,10 +187,14 @@ namespace { inline const char* getEncryptionLevelName(ngtcp2_encryption_level level) { switch (level) { - case NGTCP2_ENCRYPTION_LEVEL_1RTT: return "1rtt"; - case NGTCP2_ENCRYPTION_LEVEL_0RTT: return "0rtt"; - case NGTCP2_ENCRYPTION_LEVEL_HANDSHAKE: return "handshake"; - case NGTCP2_ENCRYPTION_LEVEL_INITIAL: return "initial"; + case NGTCP2_ENCRYPTION_LEVEL_1RTT: + return "1rtt"; + case NGTCP2_ENCRYPTION_LEVEL_0RTT: + return "0rtt"; + case NGTCP2_ENCRYPTION_LEVEL_HANDSHAKE: + return "handshake"; + case NGTCP2_ENCRYPTION_LEVEL_INITIAL: + return "initial"; } return ""; } @@ -2019,8 +2023,10 @@ struct Session::Impl { auto session = Impl::From(conn, user_data); if (UNLIKELY(session->is_destroyed())) return NGTCP2_ERR_CALLBACK_FAILURE; - Debug(session, "Receiving RX key for level %d for dcid %s", - getEncryptionLevelName(level), session->config().dcid); + Debug(session, + "Receiving RX key for level %d for dcid %s", + getEncryptionLevelName(level), + session->config().dcid); if (!session->is_server() && (level == NGTCP2_ENCRYPTION_LEVEL_0RTT || level == NGTCP2_ENCRYPTION_LEVEL_1RTT)) { @@ -2076,8 +2082,10 @@ struct Session::Impl { auto session = Impl::From(conn, user_data); if (UNLIKELY(session->is_destroyed())) return NGTCP2_ERR_CALLBACK_FAILURE; - Debug(session, "Receiving TX key for level %d for dcid %s", - getEncryptionLevelName(level), session->config().dcid); + Debug(session, + "Receiving TX key for level %d for dcid %s", + getEncryptionLevelName(level), + session->config().dcid); if (session->is_server() && (level == NGTCP2_ENCRYPTION_LEVEL_0RTT || level == NGTCP2_ENCRYPTION_LEVEL_1RTT)) { diff --git a/src/quic/tlscontext.cc b/src/quic/tlscontext.cc index 6f08a91c98d8df..33fffbd585db14 100644 --- a/src/quic/tlscontext.cc +++ b/src/quic/tlscontext.cc @@ -6,10 +6,10 @@ #include #include #include -#include #include #include #include +#include #include #include #include @@ -93,7 +93,10 @@ int AlpnSelectionCallback(SSL* ssl, } BaseObjectPtr InitializeSecureContext( - Session* session, Side side, Environment* env, const TLSContext::Options& options) { + Session* session, + Side side, + Environment* env, + const TLSContext::Options& options) { auto context = crypto::SecureContext::Create(env); auto& ctx = context->ctx(); @@ -292,7 +295,8 @@ bool SetOption(Environment* env, (options->*member).push_back(handle->Data()); } else { Utf8Value namestr(env->isolate(), name); - THROW_ERR_INVALID_ARG_TYPE(env, "%s value must be a key object", *namestr); + THROW_ERR_INVALID_ARG_TYPE( + env, "%s value must be a key object", *namestr); return false; } } else if constexpr (std::is_same::value) { @@ -302,7 +306,8 @@ bool SetOption(Environment* env, (options->*member).emplace_back(item.As()); } else { Utf8Value namestr(env->isolate(), name); - THROW_ERR_INVALID_ARG_TYPE(env, "%s value must be an array buffer", *namestr); + THROW_ERR_INVALID_ARG_TYPE( + env, "%s value must be an array buffer", *namestr); return false; } } @@ -316,7 +321,8 @@ bool SetOption(Environment* env, (options->*member).push_back(handle->Data()); } else { Utf8Value namestr(env->isolate(), name); - THROW_ERR_INVALID_ARG_TYPE(env, "%s value must be a key object", *namestr); + THROW_ERR_INVALID_ARG_TYPE( + env, "%s value must be a key object", *namestr); return false; } } else if constexpr (std::is_same::value) { @@ -326,7 +332,8 @@ bool SetOption(Environment* env, (options->*member).emplace_back(value.As()); } else { Utf8Value namestr(env->isolate(), name); - THROW_ERR_INVALID_ARG_TYPE(env, "%s value must be an array buffer", *namestr); + THROW_ERR_INVALID_ARG_TYPE( + env, "%s value must be an array buffer", *namestr); return false; } } @@ -556,7 +563,9 @@ Maybe TLSContext::Options::From(Environment* env, // We need at least one key and one cert to complete the tls handshake. // Why not make this an error? We could but it's not strictly necessary. env->EmitProcessEnvWarning(); - ProcessEmitWarning(env, "The default QUIC TLS options are being used. " + ProcessEmitWarning( + env, + "The default QUIC TLS options are being used. " "This means there is no key or certificate configured and the " "TLS handshake will fail. This is likely not what you want."); return Just(options); @@ -591,8 +600,10 @@ Maybe TLSContext::Options::From(Environment* env, // Why not make this an error? We could but it's not strictly necessary. if (options.keys.empty() || options.certs.empty()) { env->EmitProcessEnvWarning(); - ProcessEmitWarning(env, "The QUIC TLS options did not include a key or cert. " - "This means the TLS handshake will fail. This is likely not what you want."); + ProcessEmitWarning(env, + "The QUIC TLS options did not include a key or cert. " + "This means the TLS handshake will fail. This is likely " + "not what you want."); } return Just(options); diff --git a/src/quic/tokens.cc b/src/quic/tokens.cc index 498b9776a2bd18..9ffdab2575cb42 100644 --- a/src/quic/tokens.cc +++ b/src/quic/tokens.cc @@ -5,8 +5,8 @@ #include #include #include +#include #include -#include "util.h" namespace node { namespace quic { @@ -231,7 +231,7 @@ std::string RetryToken::ToString() const { MaybeStackBuffer dest(ptr_.len * 2); size_t written = StringBytes::hex_encode(*this, ptr_.len, dest.out(), dest.length()); - DCHECK_EQ(written, arraysize(dest)); + DCHECK_EQ(written, dest.length()); return std::string(dest.out(), written); } @@ -290,7 +290,7 @@ std::string RegularToken::ToString() const { MaybeStackBuffer dest(ptr_.len * 2); size_t written = StringBytes::hex_encode(*this, ptr_.len, dest.out(), dest.length()); - DCHECK_EQ(written, arraysize(dest)); + DCHECK_EQ(written, dest.length()); return std::string(dest.out(), written); } From 615c63e40f034602a61722c5c6b4e769be40a4c9 Mon Sep 17 00:00:00 2001 From: James M Snell Date: Fri, 29 Dec 2023 16:31:16 -0800 Subject: [PATCH 7/8] quic: avoid using recommended tls init on startup --- src/quic/tlscontext.cc | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/quic/tlscontext.cc b/src/quic/tlscontext.cc index 33fffbd585db14..04d46f83380774 100644 --- a/src/quic/tlscontext.cc +++ b/src/quic/tlscontext.cc @@ -36,11 +36,14 @@ const TLSContext::Options TLSContext::Options::kDefault = {}; namespace { -// One time initialization -auto _ = []() { - CHECK_EQ(ngtcp2_crypto_quictls_init(), 0); - return 0; -}(); +// TODO(@jasnell): One time initialization. ngtcp2 says this is optional but +// highly recommended to deal with some perf regression. Unfortunately doing +// this breaks some existing tests and we need to understand the potential +// impact of calling this. +// auto _ = []() { +// CHECK_EQ(ngtcp2_crypto_quictls_init(), 0); +// return 0; +// }(); constexpr size_t kMaxAlpnLen = 255; From 6d0b4c7284e2d3e62511791dcb0e59719f5c11a1 Mon Sep 17 00:00:00 2001 From: James M Snell Date: Sat, 30 Dec 2023 10:33:47 -0800 Subject: [PATCH 8/8] quic: fixup build errors --- src/quic/application.cc | 6 +++--- src/quic/http3.cc | 10 ++++++---- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/quic/application.cc b/src/quic/application.cc index d933cb9d351c7b..8308f261daf7dd 100644 --- a/src/quic/application.cc +++ b/src/quic/application.cc @@ -33,9 +33,9 @@ Session::Application_Options::operator const nghttp3_settings() const { // HTTP/3. Here we extract only the properties that are relevant to HTTP/3. return nghttp3_settings{ max_field_section_size, - qpack_max_dtable_capacity, - qpack_encoder_max_dtable_capacity, - qpack_blocked_streams, + static_cast(qpack_max_dtable_capacity), + static_cast(qpack_encoder_max_dtable_capacity), + static_cast(qpack_blocked_streams), enable_connect_protocol, enable_datagrams, }; diff --git a/src/quic/http3.cc b/src/quic/http3.cc index fb6c9bc4aef1f4..eed0b2619327fd 100644 --- a/src/quic/http3.cc +++ b/src/quic/http3.cc @@ -189,14 +189,16 @@ class Http3Application final : public Session::Application { Debug(&session(), "HTTP/3 application extending max bidi streams to %" PRIu64, max_streams); - ngtcp2_conn_extend_max_streams_bidi(session(), max_streams); + ngtcp2_conn_extend_max_streams_bidi( + session(), static_cast(max_streams)); break; } case Direction::UNIDIRECTIONAL: { Debug(&session(), "HTTP/3 application extending max uni streams to %" PRIu64, max_streams); - ngtcp2_conn_extend_max_streams_uni(session(), max_streams); + ngtcp2_conn_extend_max_streams_uni( + session(), static_cast(max_streams)); break; } } @@ -561,7 +563,7 @@ class Http3Application final : public Session::Application { // process any more data. // On the client side, if id is equal to NGHTTP3_SHUTDOWN_NOTICE_STREAM_ID, - // or on the server if the id is equal to NGHTTP3_SHUSTDOWN_NOTICE_PUSH_ID, + // or on the server if the id is equal to NGHTTP3_SHUTDOWN_NOTICE_PUSH_ID, // then this is a request to begin a graceful shutdown. // This can be called multiple times but the id can only stay the same or @@ -631,7 +633,7 @@ class Http3Application final : public Session::Application { NGHTTP3_CALLBACK_SCOPE(app); auto stream = From(stream_id, stream_user_data); if (stream == nullptr) return NGHTTP3_ERR_CALLBACK_FAILURE; - app->AcknowledgeStreamData(stream, datalen); + app->AcknowledgeStreamData(stream, static_cast(datalen)); return NGTCP2_SUCCESS; }