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/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 3ae6c8f2efef0d..8308f261daf7dd 100644 --- a/src/quic/application.cc +++ b/src/quic/application.cc @@ -1,12 +1,15 @@ #if HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC #include "application.h" +#include +#include #include #include #include #include #include "defs.h" #include "endpoint.h" +#include "http3.h" #include "packet.h" #include "session.h" @@ -21,24 +24,48 @@ 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 = {}; +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, + static_cast(qpack_max_dtable_capacity), + static_cast(qpack_encoder_max_dtable_capacity), + static_cast(qpack_blocked_streams), + enable_connect_protocol, + enable_datagrams, + }; +} + +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()) { + if (value.IsEmpty() || (!value->IsUndefined() && !value->IsObject())) { THROW_ERR_INVALID_ARG_TYPE(env, "options must be an object"); return Nothing(); } @@ -49,11 +76,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 +85,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(); } @@ -78,16 +101,22 @@ 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(); } @@ -96,6 +125,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; } @@ -104,26 +134,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. } @@ -131,6 +166,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 @@ -140,6 +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()); // By default do nothing. } @@ -147,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_, @@ -156,10 +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); 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); } @@ -167,13 +213,18 @@ 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; + Packet* packet = nullptr; uint8_t* pos = nullptr; int err = 0; @@ -182,6 +233,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(); }; @@ -209,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); } @@ -319,12 +371,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; @@ -380,7 +434,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) { @@ -394,6 +451,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; @@ -425,6 +483,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_); @@ -432,6 +491,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(); } @@ -440,13 +500,15 @@ 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..5ecaede68e1c01 100644 --- a/src/quic/application.h +++ b/src/quic/application.h @@ -118,8 +118,9 @@ 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(); + Packet* CreateStreamDataPacket(); struct StreamData; @@ -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/bindingdata.h b/src/quic/bindingdata.h index 53622cf2d36eba..83264e48d3d965 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") \ @@ -219,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/defs.h b/src/quic/defs.h index fc0bc0c81a7b7e..7802a1fa40a22e 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, @@ -34,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; } @@ -145,5 +150,29 @@ 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 93154c1fe29883..a9b401d18fdb6c 100644 --- a/src/quic/endpoint.cc +++ b/src/quic/endpoint.cc @@ -3,6 +3,7 @@ #include "endpoint.h" #include #include +#include #include #include #include @@ -253,6 +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) || #ifdef DEBUG !SET(rx_loss) || !SET(tx_loss) || #endif @@ -292,6 +295,74 @@ 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 @@ -483,20 +554,35 @@ 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, + + // 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 { @@ -600,6 +686,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 @@ -618,35 +707,49 @@ 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 token 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()) { 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) { if (is_closed()) return; + Debug(this, "Removing session for CID %s", cid); auto session = FindSession(cid); if (!session) return; DecrementSocketAddressCounter(session->remote_address()); @@ -673,26 +776,35 @@ 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) { +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 @@ -705,10 +817,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); } @@ -726,6 +840,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 = @@ -742,6 +857,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 @@ -763,6 +879,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); const auto exceeds_limits = [&] { SocketAddressInfoTraits::Type* counts = @@ -789,6 +909,10 @@ 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( @@ -801,7 +925,11 @@ 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; + Debug(this, "Starting"); int err = 0; if (state_->bound == 0) { @@ -831,7 +959,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( @@ -841,20 +972,35 @@ 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); + 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(); + // 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(); } } @@ -862,6 +1008,28 @@ 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; @@ -894,6 +1062,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; @@ -903,28 +1073,36 @@ 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(); + 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); + } }; 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; + + Debug(this, "Trying to create new session for %s", config.dcid); + 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, @@ -934,81 +1112,106 @@ 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); - // 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. + Debug(this, "Failed to accept initial packet from %s", remote_address); + return; } - // This is the second 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 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) { + Debug(this, + "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); + 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 = ([&] { 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) { + 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); // 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); + + 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* // 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) { + 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. @@ -1016,6 +1219,10 @@ 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 %s to start " + "validation", + remote_address); SendRetry(PathDescriptor{ version, dcid, @@ -1025,7 +1232,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 @@ -1033,13 +1241,19 @@ 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); auto ocid = token.Validate( version, remote_address, dcid, options_.token_secret, options_.retry_token_expiration * NGTCP2_SECONDS); - if (ocid == std::nullopt) { + if (!ocid.has_value()) { + Debug( + this, "Retry token from %s is invalid.", remote_address); // Invalid retry token was detected. Close the connection. SendImmediateConnectionClose( PathDescriptor{ @@ -1047,23 +1261,40 @@ 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 // original retry packet sent to the client. We use it for // validation. + Debug(this, + "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); break; } case RegularToken::kTokenMagic: { RegularToken token(hd.token, hd.tokenlen); + 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); + // 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 +1304,21 @@ 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; + Debug(this, "Regular token from %s is valid.", remote_address); + config.set_token(token); break; } default: { + 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 + // is fairly low here. SendRetry(PathDescriptor{ version, dcid, @@ -1087,7 +1326,8 @@ void Endpoint::Receive(const uv_buf_t& buf, local_address, remote_address, }); - return true; + STAT_INCREMENT(Stats, packets_received); + return; } } @@ -1095,17 +1335,24 @@ 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 %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", + 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 // assume the peer is buggy or malicious and drop the packet on the // floor. - return false; + return; } break; case NGTCP2_PKT_0RTT: + 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 @@ -1124,11 +1371,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 +1395,37 @@ 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 +1443,21 @@ 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 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); @@ -1198,16 +1471,17 @@ 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! } - // 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)) { + Debug(this, "Packet had incorrectly sized CIDs, igoring"); 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 @@ -1221,19 +1495,21 @@ 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 // 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(); @@ -1247,41 +1523,40 @@ 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)) + 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. - - 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. + 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? - 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) { @@ -1338,6 +1613,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); } @@ -1348,6 +1624,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 04f8f5ae50f082..470771eb1a1914 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 @@ -168,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); @@ -217,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 @@ -283,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 new file mode 100644 index 00000000000000..eed0b2619327fd --- /dev/null +++ b/src/quic/http3.cc @@ -0,0 +1,833 @@ +#if HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC + +#include "http3.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "application.h" +#include "bindingdata.h" +#include "defs.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), + 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) { + // 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(), 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(), static_cast(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 { + DCHECK_NOT_NULL(conn_.get()); + 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_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_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 + // *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_); + } + + bool started_ = false; + nghttp3_mem allocator_; + 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, static_cast(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 a25bd9e78180bd..dad5b59ad3cbe5 100644 --- a/src/quic/packet.cc +++ b/src/quic/packet.cc @@ -13,6 +13,7 @@ #include #include "bindingdata.h" #include "cid.h" +#include "defs.h" #include "tokens.h" namespace node { @@ -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_; @@ -63,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; } @@ -99,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); } @@ -124,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; @@ -132,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, @@ -164,7 +175,10 @@ Packet::Packet(Environment* env, : ReqWrap(env, object, AsyncWrap::PROVIDER_QUIC_PACKET), listener_(listener), destination_(destination), - data_(std::move(data)) {} + data_(std::move(data)) { + ClearWeak(); + Debug(this, "Created a new packet"); +} Packet::Packet(Environment* env, Listener* listener, @@ -178,41 +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; - 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; } @@ -226,14 +221,12 @@ 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( - 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, @@ -241,7 +234,7 @@ BaseObjectPtr Packet::CreateRetryPacket( cid, path_descriptor.dcid, token_secret); - if (!token) return BaseObjectPtr(); + if (!token) return nullptr; const ngtcp2_vec& vec = token; @@ -250,7 +243,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; @@ -264,33 +257,33 @@ 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( - 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; 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, @@ -300,6 +293,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, @@ -314,13 +308,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, @@ -333,7 +327,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]; @@ -344,20 +338,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) { @@ -393,6 +388,7 @@ BaseObjectPtr Packet::CreateVersionNegotiationPacket( path_descriptor.remote_address, kDefaultMaxPacketLength, "version negotiation"); + if (packet == nullptr) return nullptr; ngtcp2_vec vec = *packet; ssize_t nwrite = @@ -407,7 +403,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 228f67d1e86187..c92f2fd4a60f82 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. @@ -49,8 +50,6 @@ class Packet final : public ReqWrap { struct Data; public: - using Queue = std::deque>; - static v8::Local GetConstructorTemplate( Environment* env); @@ -83,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; @@ -94,14 +92,13 @@ class Packet final : public ReqWrap { // tells us how many of the packets bytes were used. void Truncate(size_t len); - static BaseObjectPtr 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 = ""); - BaseObjectPtr Clone() const; + Packet* Clone() const; void MemoryInfo(MemoryTracker* tracker) const override; SET_MEMORY_INFO_NAME(Packet) @@ -109,38 +106,31 @@ 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( - 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 BaseObjectPtr 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 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); @@ -149,15 +139,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/quic.cc b/src/quic/quic.cc index 17eacb9b5f4034..879e16e353d74d 100644 --- a/src/quic/quic.cc +++ b/src/quic/quic.cc @@ -20,9 +20,12 @@ using v8::Value; namespace quic { +int DebugIndentScope::indent_ = 0; + void CreatePerIsolateProperties(IsolateData* isolate_data, Local target) { Endpoint::InitPerIsolate(isolate_data, target); + Session::InitPerIsolate(isolate_data, target); } void CreatePerContextProperties(Local target, @@ -32,11 +35,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..4c9aad191ad650 100644 --- a/src/quic/session.cc +++ b/src/quic/session.cc @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -71,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) @@ -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) \ @@ -144,6 +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"); session->connection_close_depth_++; } MaybeCloseConnectionScope(const MaybeCloseConnectionScope&) = delete; @@ -171,6 +167,7 @@ struct Session::MaybeCloseConnectionScope final { Session::SendPendingDataScope::SendPendingDataScope(Session* session) : session(session) { + Debug(session, "Entering send pending data scope"); session->send_scope_depth_++; } @@ -187,6 +184,21 @@ 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 @@ -277,7 +289,6 @@ bool SetOption(Environment* env, } // namespace // ============================================================================ - Session::Config::Config(Side side, const Endpoint& endpoint, const Options& options, @@ -300,6 +311,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 +328,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 +368,59 @@ 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); +} + +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, @@ -358,7 +432,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( \ @@ -385,13 +459,38 @@ 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) { 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,24 +500,29 @@ 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), stats_(env()->isolate()), state_(env()->isolate()), + 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(); + + Debug(this, "Session created."); + timer_.Unref(); application().ExtendMaxStreams(EndpointLabel::LOCAL, @@ -462,14 +566,17 @@ Session::Session(BaseObjectPtr 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(); }); } @@ -541,7 +648,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 @@ -552,8 +659,9 @@ 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(), 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 +671,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)); @@ -582,30 +682,42 @@ TransportParams Session::GetRemoteTransportParams() const { } void Session::SetLastError(QuicError&& error) { + Debug(this, "Setting last error to %s", error); last_error_ = std::move(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: { + 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()) return DoClose(); - state_->graceful_close = 1; - STAT_RECORD_TIMESTAMP(Stats, graceful_closing_at); - return; + if (streams_.empty()) { + DoClose(false); + } else { + state_->graceful_close = 1; + STAT_RECORD_TIMESTAMP(Stats, graceful_closing_at); + } + break; + } } - UNREACHABLE(); } void Session::Destroy() { if (is_destroyed()) return; + Debug(this, "Session destroyed"); // The DoClose() method should have already been called. DCHECK(state_->closing); @@ -628,35 +740,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()); @@ -667,24 +784,35 @@ bool Session::Receive(Store&& store, 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; } 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()); + Debug(this, "Server must send a retry packet"); endpoint_->SendRetry(PathDescriptor{ version(), config_.dcid, @@ -697,12 +825,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 (%d)", last_error_, err); Close(); return false; }; @@ -710,6 +840,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(); @@ -718,7 +849,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 @@ -727,17 +858,19 @@ 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)); + endpoint_->Send(packet); return; } + Debug(this, "Session could not send %s", packet->ToString()); 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) { @@ -745,10 +878,12 @@ 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; } - BaseObjectPtr packet; + Debug(this, "Session is sending datagram"); + Packet* packet = nullptr; uint8_t* pos = nullptr; int accepted = 0; ngtcp2_vec vec = data; @@ -761,13 +896,19 @@ uint64_t Session::SendDatagram(Store&& data) { int attempts = 0; for (;;) { - if (!packet) { + // 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 == nullptr) { packet = Packet::Create(env(), endpoint_.get(), remote_address_, ngtcp2_conn_get_max_tx_udp_payload_size(*this), "datagram"); - if (!packet) { + // Typically sending datagrams is best effort, but if we cannot create + // the packet, then we handle it as a fatal error. + if (packet == nullptr) { last_error_ = QuicError::ForNgtcp2Error(NGTCP2_ERR_INTERNAL); Close(CloseMethod::SILENT); return 0; @@ -786,8 +927,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 +956,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,11 +978,11 @@ 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 // 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; @@ -840,6 +992,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; } @@ -851,6 +1004,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_); } BaseObjectPtr Session::FindStream(int64_t id) const { @@ -869,19 +1026,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; @@ -939,6 +1101,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)) { @@ -955,11 +1118,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, @@ -970,11 +1135,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, @@ -1056,8 +1223,13 @@ 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"); // Once Close has been called, we cannot re-enter if (state_->closing == 1) return; state_->closing = 1; @@ -1088,15 +1260,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); @@ -1110,28 +1285,13 @@ 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() { 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) { @@ -1180,6 +1340,7 @@ void Session::OnTimeout() { return; } + Debug(this, "Session timed out"); last_error_ = QuicError::ForNgtcp2Error(ret); Close(CloseMethod::SILENT); } @@ -1188,6 +1349,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); if (expiry <= now) { // The timer has already expired. @@ -1205,6 +1368,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_); @@ -1221,12 +1386,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); } @@ -1239,6 +1408,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); @@ -1249,6 +1419,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); @@ -1259,6 +1430,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()) @@ -1287,6 +1460,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); } @@ -1294,6 +1470,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; } @@ -1302,6 +1479,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; @@ -1314,6 +1492,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; @@ -1348,6 +1527,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); } @@ -1361,6 +1541,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); @@ -1373,7 +1554,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 +1562,11 @@ 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}; + Debug(this, "Notifying JavaScript of datagram status"); MakeCallback(state.session_datagram_status_callback(), arraysize(argv), argv); } @@ -1430,6 +1612,7 @@ void Session::EmitHandshakeComplete() { return; } + Debug(this, "Notifying JavaScript of handshake complete"); MakeCallback(BindingData::Get(env()).session_handshake_callback(), arraysize(argv), argv); @@ -1447,7 +1630,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 +1640,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), @@ -1472,6 +1655,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); } @@ -1492,8 +1676,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) { @@ -1502,6 +1688,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); } @@ -1538,6 +1725,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); @@ -1546,6 +1734,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); }); } @@ -1762,13 +1951,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 @@ -1814,17 +1996,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, @@ -1849,7 +2020,13 @@ 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)) { @@ -1902,7 +2079,14 @@ 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; @@ -1955,14 +2139,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, @@ -2004,7 +2180,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, @@ -2012,7 +2188,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, @@ -2036,7 +2212,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, @@ -2046,7 +2222,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, @@ -2054,7 +2230,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, @@ -2078,7 +2254,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, @@ -2122,10 +2298,12 @@ 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); transport_params.GenerateSessionTokens(this); + switch (config_.side) { case Side::SERVER: { CHECK_EQ(ngtcp2_conn_server_new(&conn, @@ -2161,25 +2339,29 @@ 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..eded22481e8a36 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) @@ -79,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; }; @@ -122,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. @@ -141,10 +152,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,22 +192,32 @@ 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) + + std::string ToString() const; }; 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; @@ -214,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. @@ -221,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) @@ -228,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 @@ -261,21 +284,44 @@ 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, 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); - 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); @@ -283,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; @@ -322,20 +355,18 @@ 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; - 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); - void ExtendStreamOffset(int64_t id, size_t amount); - void ExtendOffset(size_t amount); void UpdateDataStats(); void SendConnectionClose(); void OnTimeout(); @@ -373,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(); @@ -385,19 +413,19 @@ class Session final : public AsyncWrap, private SessionTicket::AppData::Source { AliasedStruct stats_; AliasedStruct state_; ngtcp2_mem allocator_; + BaseObjectWeakPtr endpoint_; Config config_; + SocketAddress local_address_; + SocketAddress remote_address_; QuicConnectionPointer connection_; - BaseObjectPtr endpoint_; 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 f20a160717e803..1a6e05f42f610b 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; } @@ -910,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(); @@ -928,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/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 d60e4a7dbf3cea..04d46f83380774 100644 --- a/src/quic/tlscontext.cc +++ b/src/quic/tlscontext.cc @@ -1,12 +1,15 @@ #if HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC #include "tlscontext.h" +#include #include +#include #include #include #include #include #include +#include #include #include #include @@ -32,6 +35,16 @@ namespace quic { const TLSContext::Options TLSContext::Options::kDefault = {}; namespace { + +// 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; int AllowEarlyDataCallback(SSL* ssl, void* arg) { @@ -83,13 +96,17 @@ 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); @@ -120,6 +137,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); @@ -259,6 +277,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()) { @@ -277,7 +297,9 @@ 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) { @@ -286,7 +308,9 @@ 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; } } @@ -299,7 +323,9 @@ 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) { @@ -308,7 +334,9 @@ 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; } } @@ -346,7 +374,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())); @@ -386,6 +414,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_)); @@ -400,37 +429,8 @@ 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) { - // 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. // If there is nothing listening for the session ticket, don't both emitting. @@ -455,6 +455,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 +464,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); @@ -560,8 +563,17 @@ 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(); @@ -587,9 +599,46 @@ 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); } +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..65d8e2f4e74942 100644 --- a/src/quic/tlscontext.h +++ b/src/quic/tlscontext.h @@ -100,6 +100,8 @@ class TLSContext final : public MemoryRetainer { static v8::Maybe From(Environment* env, v8::Local value); + + std::string ToString() const; }; static const Options kDefaultOptions; @@ -123,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; diff --git a/src/quic/tokens.cc b/src/quic/tokens.cc index dc14be55723e89..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 { @@ -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, dest.length()); + 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, dest.length()); + 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..2e8cd26a0cef9e 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();