From 11e0f39dfb571b649976cdcb5240ac287ea0f19a Mon Sep 17 00:00:00 2001 From: James M Snell Date: Fri, 31 Mar 2023 17:58:57 -0700 Subject: [PATCH 1/9] quic: add TLSContext --- node.gyp | 2 + src/quic/bindingdata.h | 16 +- src/quic/defs.h | 14 + src/quic/tlscontext.cc | 589 +++++++++++++++++++++++++++++++++++++++++ src/quic/tlscontext.h | 176 ++++++++++++ 5 files changed, 796 insertions(+), 1 deletion(-) create mode 100644 src/quic/tlscontext.cc create mode 100644 src/quic/tlscontext.h diff --git a/node.gyp b/node.gyp index f092c332001e1c..bcc0c43d8caafe 100644 --- a/node.gyp +++ b/node.gyp @@ -343,6 +343,7 @@ 'src/quic/logstream.cc', 'src/quic/preferredaddress.cc', 'src/quic/sessionticket.cc', + 'src/quic/tlscontext.cc', 'src/quic/tokens.cc', 'src/quic/transportparams.cc', 'src/quic/bindingdata.h', @@ -351,6 +352,7 @@ 'src/quic/logstream.h', 'src/quic/preferredaddress.h', 'src/quic/sessionticket.h', + 'src/quic/tlscontext.h', 'src/quic/tokens.h', 'src/quic/transportparams.h', ], diff --git a/src/quic/bindingdata.h b/src/quic/bindingdata.h index d22699ca4f3d63..070ff8499c54de 100644 --- a/src/quic/bindingdata.h +++ b/src/quic/bindingdata.h @@ -64,9 +64,17 @@ constexpr size_t kDefaultMaxPacketLength = NGTCP2_MAX_UDP_PAYLOAD_SIZE; #define QUIC_STRINGS(V) \ V(ack_delay_exponent, "ackDelayExponent") \ V(active_connection_id_limit, "activeConnectionIDLimit") \ + V(alpn, "alpn") \ + V(ca, "ca") \ + V(certs, "certs") \ + V(crl, "crl") \ + V(ciphers, "ciphers") \ V(disable_active_migration, "disableActiveMigration") \ + V(enable_tls_trace, "tlsTrace") \ V(endpoint, "Endpoint") \ V(endpoint_udp, "Endpoint::UDP") \ + V(groups, "groups") \ + V(hostname, "hostname") \ V(http3_alpn, &NGHTTP3_ALPN_H3[1]) \ V(initial_max_data, "initialMaxData") \ V(initial_max_stream_data_bidi_local, "initialMaxStreamDataBidiLocal") \ @@ -74,13 +82,19 @@ constexpr size_t kDefaultMaxPacketLength = NGTCP2_MAX_UDP_PAYLOAD_SIZE; V(initial_max_stream_data_uni, "initialMaxStreamDataUni") \ V(initial_max_streams_bidi, "initialMaxStreamsBidi") \ V(initial_max_streams_uni, "initialMaxStreamsUni") \ + V(keylog, "keylog") \ + V(keys, "keys") \ V(logstream, "LogStream") \ V(max_ack_delay, "maxAckDelay") \ V(max_datagram_frame_size, "maxDatagramFrameSize") \ V(max_idle_timeout, "maxIdleTimeout") \ V(packetwrap, "PacketWrap") \ + V(reject_unauthorized, "rejectUnauthorized") \ + V(request_peer_certificate, "requestPeerCertificate") \ V(session, "Session") \ - V(stream, "Stream") + V(session_id_ctx, "sessionIDContext") \ + V(stream, "Stream") \ + V(verify_hostname_identity, "verifyHostnameIdentity") // ============================================================================= // The BindingState object holds state for the internalBinding('quic') binding diff --git a/src/quic/defs.h b/src/quic/defs.h index 3dbdd7ee25eba8..48e80964c0905e 100644 --- a/src/quic/defs.h +++ b/src/quic/defs.h @@ -7,6 +7,20 @@ namespace node { namespace quic { +template +bool SetOption(Environment* env, + Opt* options, + const v8::Local& object, + const v8::Local& name) { + v8::Local value; + if (!object->Get(env->context(), name).ToLocal(&value)) return false; + if (!value->IsUndefined()) { + Utf8Value utf8(env->isolate(), value); + options->*member = *utf8; + } + return true; +} + template bool SetOption(Environment* env, Opt* options, diff --git a/src/quic/tlscontext.cc b/src/quic/tlscontext.cc new file mode 100644 index 00000000000000..500c15fc02c32c --- /dev/null +++ b/src/quic/tlscontext.cc @@ -0,0 +1,589 @@ +#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS + +#include "tlscontext.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include "bindingdata.h" +#include "defs.h" +#include "transportparams.h" + +namespace node { + +using v8::ArrayBuffer; +using v8::BackingStore; +using v8::Just; +using v8::Local; +using v8::Maybe; +using v8::MaybeLocal; +using v8::Nothing; +using v8::Object; +using v8::Value; + +namespace quic { + +// TODO(@jasnell): This session class is just a placeholder. +// The real session impl will be added in a separate commit. +class Session { + public: + operator ngtcp2_conn*() { return nullptr; } + void EmitKeylog(const char* line) const {} + void EmitSessionTicket(Store&& store) {} + void SetStreamOpenAllowed() {} + bool is_destroyed() const { return false; } + bool wants_session_ticket() const { return false; } +}; + +namespace { +constexpr size_t kMaxAlpnLen = 255; + +int AllowEarlyDataCallback(SSL* ssl, void* arg) { + // Currently, we always allow early data. Later we might make + // it configurable. + return 1; +} + +int NewSessionCallback(SSL* ssl, SSL_SESSION* session) { + // We use this event to trigger generation of the SessionTicket + // if the user has requested to receive it. + return TLSContext::From(ssl).OnNewSession(session); +} + +void KeylogCallback(const SSL* ssl, const char* line) { + TLSContext::From(ssl).Keylog(line); +} + +int AlpnSelectionCallback(SSL* ssl, + const unsigned char** out, + unsigned char* outlen, + const unsigned char* in, + unsigned int inlen, + void* arg) { + auto& context = TLSContext::From(ssl); + + auto requested = context.options().alpn; + if (requested.length() > kMaxAlpnLen) return SSL_TLSEXT_ERR_NOACK; + + // The Session supports exactly one ALPN identifier. If that does not match + // any of the ALPN identifiers provided in the client request, then we fail + // here. Note that this will not fail the TLS handshake, so we have to check + // later if the ALPN matches the expected identifier or not. + // + // We might eventually want to support the ability to negotiate multiple + // possible ALPN's on a single endpoint/session but for now, we only support + // one. + if (SSL_select_next_proto( + const_cast(out), + outlen, + reinterpret_cast(requested.c_str()), + requested.length(), + in, + inlen) == OPENSSL_NPN_NO_OVERLAP) { + return SSL_TLSEXT_ERR_NOACK; + } + + return SSL_TLSEXT_ERR_OK; +} + +BaseObjectPtr InitializeSecureContext( + Side side, Environment* env, const TLSContext::Options& options) { + auto context = crypto::SecureContext::Create(env); + + auto& ctx = context->ctx(); + + switch (side) { + case Side::SERVER: { + ctx.reset(SSL_CTX_new(TLS_server_method())); + SSL_CTX_set_app_data(ctx.get(), context); + + if (ngtcp2_crypto_openssl_configure_server_context(ctx.get()) != 0) { + return BaseObjectPtr(); + } + + SSL_CTX_set_max_early_data(ctx.get(), UINT32_MAX); + SSL_CTX_set_allow_early_data_cb( + ctx.get(), AllowEarlyDataCallback, nullptr); + SSL_CTX_set_options(ctx.get(), + (SSL_OP_ALL & ~SSL_OP_DONT_INSERT_EMPTY_FRAGMENTS) | + SSL_OP_SINGLE_ECDH_USE | + SSL_OP_CIPHER_SERVER_PREFERENCE | + SSL_OP_NO_ANTI_REPLAY); + SSL_CTX_set_mode(ctx.get(), SSL_MODE_RELEASE_BUFFERS); + SSL_CTX_set_alpn_select_cb(ctx.get(), AlpnSelectionCallback, nullptr); + SSL_CTX_set_session_ticket_cb(ctx.get(), + SessionTicket::GenerateCallback, + SessionTicket::DecryptedCallback, + nullptr); + + const unsigned char* sid_ctx = reinterpret_cast( + options.session_id_ctx.c_str()); + SSL_CTX_set_session_id_context( + ctx.get(), sid_ctx, options.session_id_ctx.length()); + + break; + } + case Side::CLIENT: { + ctx.reset(SSL_CTX_new(TLS_client_method())); + SSL_CTX_set_app_data(ctx.get(), context); + + if (ngtcp2_crypto_openssl_configure_client_context(ctx.get()) != 0) { + return BaseObjectPtr(); + } + + SSL_CTX_set_session_cache_mode( + ctx.get(), SSL_SESS_CACHE_CLIENT | SSL_SESS_CACHE_NO_INTERNAL_STORE); + SSL_CTX_sess_set_new_cb(ctx.get(), NewSessionCallback); + break; + } + default: + UNREACHABLE(); + } + + SSL_CTX_set_default_verify_paths(ctx.get()); + + if (options.keylog) SSL_CTX_set_keylog_callback(ctx.get(), KeylogCallback); + + if (SSL_CTX_set_ciphersuites(ctx.get(), options.ciphers.c_str()) != 1) { + return BaseObjectPtr(); + } + + if (SSL_CTX_set1_groups_list(ctx.get(), options.groups.c_str()) != 1) { + return BaseObjectPtr(); + } + + // Handle CA certificates... + + const auto addCACert = [&](uv_buf_t ca) { + crypto::ClearErrorOnReturn clear_error_on_return; + crypto::BIOPointer bio = crypto::NodeBIO::NewFixed(ca.base, ca.len); + if (!bio) return false; + context->SetCACert(bio); + return true; + }; + + const auto addRootCerts = [&] { + crypto::ClearErrorOnReturn clear_error_on_return; + context->SetRootCerts(); + }; + + if (!options.ca.empty()) { + for (auto& ca : options.ca) { + if (!addCACert(ca)) { + return BaseObjectPtr(); + } + } + } else { + addRootCerts(); + } + + // Handle Certs + + const auto addCert = [&](uv_buf_t cert) { + crypto::ClearErrorOnReturn clear_error_on_return; + crypto::BIOPointer bio = crypto::NodeBIO::NewFixed(cert.base, cert.len); + if (!bio) return Just(false); + auto ret = context->AddCert(env, std::move(bio)); + return ret; + }; + + for (auto& cert : options.certs) { + if (!addCert(cert).IsJust()) { + return BaseObjectPtr(); + } + } + + // Handle keys + + const auto addKey = [&](auto& key) { + crypto::ClearErrorOnReturn clear_error_on_return; + return context->UseKey(env, key); + // TODO(@jasnell): Maybe SSL_CTX_check_private_key also? + }; + + for (auto& key : options.keys) { + if (!addKey(key).IsJust()) { + return BaseObjectPtr(); + } + } + + // Handle CRL + + const auto addCRL = [&](uv_buf_t crl) { + crypto::ClearErrorOnReturn clear_error_on_return; + crypto::BIOPointer bio = crypto::NodeBIO::NewFixed(crl.base, crl.len); + if (!bio) return Just(false); + return context->SetCRL(env, bio); + }; + + for (auto& crl : options.crl) { + if (!addCRL(crl).IsJust()) { + return BaseObjectPtr(); + } + } + + // TODO(@jasnell): Possibly handle other bits. Such a pfx, client cert engine, + // and session timeout. + return BaseObjectPtr(context); +} + +void EnableTrace(Environment* env, crypto::BIOPointer* bio, SSL* ssl) { +#if HAVE_SSL_TRACE + static bool warn_trace_tls = true; + if (warn_trace_tls) { + warn_trace_tls = false; + ProcessEmitWarning(env, + "Enabling --trace-tls can expose sensitive data in " + "the resulting log"); + } + if (!*bio) { + bio->reset(BIO_new_fp(stderr, BIO_NOCLOSE | BIO_FP_TEXT)); + SSL_set_msg_callback( + ssl, + [](int write_p, + int version, + int content_type, + const void* buf, + size_t len, + SSL* ssl, + void* arg) -> void { + crypto::MarkPopErrorOnReturn mark_pop_error_on_return; + SSL_trace(write_p, version, content_type, buf, len, ssl, arg); + }); + SSL_set_msg_callback_arg(ssl, bio->get()); + } +#endif +} + +template Opt::*member> +bool SetOption(Environment* env, + Opt* options, + const v8::Local& object, + const v8::Local& name) { + v8::Local value; + if (!object->Get(env->context(), name).ToLocal(&value)) return false; + + // The value can be either a single item or an array of items. + + if (value->IsArray()) { + auto context = env->context(); + auto values = value.As(); + uint32_t count = values->Length(); + for (uint32_t n = 0; n < count; n++) { + v8::Local item; + if (!values->Get(context, n).ToLocal(&item)) { + return false; + } + if constexpr (std::is_same>:: + value) { + if (crypto::KeyObjectHandle::HasInstance(env, item)) { + crypto::KeyObjectHandle* handle; + ASSIGN_OR_RETURN_UNWRAP(&handle, item, false); + (options->*member).push_back(handle->Data()); + } else { + return false; + } + } else if constexpr (std::is_same::value) { + if (item->IsArrayBufferView()) { + (options->*member).emplace_back(item.As()); + } else if (item->IsArrayBuffer()) { + (options->*member).emplace_back(item.As()); + } else { + return false; + } + } + } + } else { + if constexpr (std::is_same>::value) { + if (crypto::KeyObjectHandle::HasInstance(env, value)) { + crypto::KeyObjectHandle* handle; + ASSIGN_OR_RETURN_UNWRAP(&handle, value, false); + (options->*member).push_back(handle->Data()); + } else { + return false; + } + } else if constexpr (std::is_same::value) { + if (value->IsArrayBufferView()) { + (options->*member).emplace_back(value.As()); + } else if (value->IsArrayBuffer()) { + (options->*member).emplace_back(value.As()); + } else { + return false; + } + } + } + return true; +} +} // namespace + +Side TLSContext::side() const { + return side_; +} + +const TLSContext::Options& TLSContext::options() const { + return options_; +} + +inline const TLSContext& TLSContext::From(const SSL* ssl) { + auto ref = static_cast(SSL_get_app_data(ssl)); + TLSContext* context = ContainerOf(&TLSContext::conn_ref_, ref); + return *context; +} + +inline TLSContext& TLSContext::From(SSL* ssl) { + auto ref = static_cast(SSL_get_app_data(ssl)); + TLSContext* context = ContainerOf(&TLSContext::conn_ref_, ref); + return *context; +} + +TLSContext::TLSContext(Environment* env, + Side side, + Session* session, + const Options& options) + : conn_ref_({getConnection, this}), + side_(side), + env_(env), + session_(session), + options_(options), + secure_context_(InitializeSecureContext(side, env, options)) { + CHECK(secure_context_); + ssl_.reset(SSL_new(secure_context_->ctx().get())); + CHECK(ssl_ && SSL_is_quic(ssl_.get())); + + SSL_set_app_data(ssl_.get(), &conn_ref_); + SSL_set_verify(ssl_.get(), SSL_VERIFY_NONE, crypto::VerifyCallback); + + // Enable tracing if the `--trace-tls` command line flag is used. + if (UNLIKELY(env->options()->trace_tls || options.enable_tls_trace)) + EnableTrace(env, &bio_trace_, ssl_.get()); + + switch (side) { + case Side::CLIENT: { + SSL_set_connect_state(ssl_.get()); + CHECK_EQ(0, + SSL_set_alpn_protos(ssl_.get(), + reinterpret_cast( + options_.alpn.c_str()), + options_.alpn.length())); + CHECK_EQ(0, + SSL_set_tlsext_host_name(ssl_.get(), options_.hostname.c_str())); + break; + } + case Side::SERVER: { + SSL_set_accept_state(ssl_.get()); + if (options.request_peer_certificate) { + int verify_mode = SSL_VERIFY_PEER; + if (options.reject_unauthorized) + verify_mode |= SSL_VERIFY_FAIL_IF_NO_PEER_CERT; + SSL_set_verify(ssl_.get(), verify_mode, crypto::VerifyCallback); + } + break; + } + default: + UNREACHABLE(); + } +} + +void TLSContext::Start() { + ngtcp2_conn_set_tls_native_handle(*session_, ssl_.get()); + + TransportParams tp(TransportParams::Type::ENCRYPTED_EXTENSIONS, + ngtcp2_conn_get_local_transport_params(*session_)); + Store store = tp.Encode(env_); + if (store && store.length() > 0) { + ngtcp2_vec vec = store; + SSL_set_quic_transport_params(ssl_.get(), vec.base, vec.len); + } +} + +void TLSContext::Keylog(const char* line) const { + session_->EmitKeylog(line); +} + +int TLSContext::Receive(ngtcp2_crypto_level crypto_level, + uint64_t offset, + const ngtcp2_vec& vec) { + // 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_, crypto_level, vec.base, vec.len); + + 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_OPENSSL_ERR_TLS_WANT_X509_LOOKUP: + [[fallthrough]]; + case NGTCP2_CRYPTO_OPENSSL_ERR_TLS_WANT_CLIENT_HELLO_CB: + return 0; + } + return ret; +} + +int TLSContext::OnNewSession(SSL_SESSION* 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. + if (!session_->wants_session_ticket()) return 0; + + // Pre-fight to see how much space we need to allocate for the session ticket. + size_t size = i2d_SSL_SESSION(session, nullptr); + + if (size > 0 && size < crypto::SecureContext::kMaxSessionSize) { + // Generate the actual ticket. If this fails, we'll simply carry on without + // emitting the ticket. + std::shared_ptr ticket = + ArrayBuffer::NewBackingStore(env_->isolate(), size); + unsigned char* data = reinterpret_cast(ticket->Data()); + if (i2d_SSL_SESSION(session, &data) <= 0) return 0; + session_->EmitSessionTicket(Store(std::move(ticket), size)); + } + // If size == 0, there's no session ticket data to emit. Let's ignore it + // and continue without emitting the sessionticket event. + + return 0; +} + +bool TLSContext::InitiateKeyUpdate() { + if (session_->is_destroyed() || in_key_update_) return false; + auto leave = OnScopeLeave([this] { in_key_update_ = false; }); + in_key_update_ = true; + + return ngtcp2_conn_initiate_key_update(*session_, uv_hrtime()) == 0; +} + +int TLSContext::VerifyPeerIdentity() { + return crypto::VerifyPeerCertificate(ssl_); +} + +void TLSContext::MaybeSetEarlySession(const SessionTicket& sessionTicket) { + TransportParams rtp(TransportParams::Type::ENCRYPTED_EXTENSIONS, + sessionTicket.transport_params()); + + // Ignore invalid remote transport parameters. + if (!rtp) return; + + uv_buf_t buf = sessionTicket.ticket(); + crypto::SSLSessionPointer ticket = crypto::GetTLSSession( + reinterpret_cast(buf.base), buf.len); + + // Silently ignore invalid TLS session + if (!ticket || !SSL_SESSION_get_max_early_data(ticket.get())) return; + + // The early data will just be ignored if it's invalid. + if (crypto::SetTLSSession(ssl_, ticket)) { + ngtcp2_conn_set_early_remote_transport_params(*session_, rtp); + session_->SetStreamOpenAllowed(); + } +} + +void TLSContext::MemoryInfo(MemoryTracker* tracker) const { + tracker->TrackField("options", options_); + tracker->TrackField("secure_context", secure_context_); +} + +MaybeLocal TLSContext::cert(Environment* env) const { + return crypto::X509Certificate::GetCert(env, ssl_); +} + +MaybeLocal TLSContext::peer_cert(Environment* env) const { + crypto::X509Certificate::GetPeerCertificateFlag flag = + side_ == Side::SERVER + ? crypto::X509Certificate::GetPeerCertificateFlag::SERVER + : crypto::X509Certificate::GetPeerCertificateFlag::NONE; + return crypto::X509Certificate::GetPeerCert(env, ssl_, flag); +} + +MaybeLocal TLSContext::cipher_name(Environment* env) const { + return crypto::GetCurrentCipherName(env, ssl_); +} + +MaybeLocal TLSContext::cipher_version(Environment* env) const { + return crypto::GetCurrentCipherVersion(env, ssl_); +} + +MaybeLocal TLSContext::ephemeral_key(Environment* env) const { + return crypto::GetEphemeralKey(env, ssl_); +} + +const std::string_view TLSContext::servername() const { + const char* servername = crypto::GetServerName(ssl_.get()); + return servername != nullptr ? std::string_view(servername) + : std::string_view(); +} + +const std::string_view TLSContext::alpn() const { + const unsigned char* alpn_buf = nullptr; + unsigned int alpnlen; + SSL_get0_alpn_selected(ssl_.get(), &alpn_buf, &alpnlen); + return alpnlen ? std::string_view(reinterpret_cast(alpn_buf), + alpnlen) + : std::string_view(); +} + +bool TLSContext::early_data_was_accepted() const { + return (early_data_ && + SSL_get_early_data_status(ssl_.get()) == SSL_EARLY_DATA_ACCEPTED); +} + +void TLSContext::Options::MemoryInfo(MemoryTracker* tracker) const { + tracker->TrackField("keys", keys); + tracker->TrackField("certs", certs); + tracker->TrackField("ca", ca); + tracker->TrackField("crl", crl); +} + +ngtcp2_conn* TLSContext::getConnection(ngtcp2_crypto_conn_ref* ref) { + TLSContext* context = ContainerOf(&TLSContext::conn_ref_, ref); + return *context->session_; +} + +Maybe TLSContext::Options::From(Environment* env, + Local value) { + if (value.IsEmpty() || !value->IsObject()) { + return Nothing(); + } + + auto& state = BindingData::Get(env); + auto params = value.As(); + Options options; + +#define SET_VECTOR(Type, name) \ + SetOption( \ + env, &options, params, state.name##_string()) + +#define SET(name) \ + SetOption( \ + env, &options, params, state.name##_string()) + + if (!SET(keylog) || !SET(reject_unauthorized) || !SET(enable_tls_trace) || + !SET(request_peer_certificate) || !SET(verify_hostname_identity) || + !SET(alpn) || !SET(hostname) || !SET(session_id_ctx) || !SET(ciphers) || + !SET(groups) || + !SET_VECTOR(std::shared_ptr, keys) || + !SET_VECTOR(Store, certs) || !SET_VECTOR(Store, ca) || + !SET_VECTOR(Store, crl)) { + return Nothing(); + } + + return Just(options); +} + +} // namespace quic +} // namespace node + +#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS diff --git a/src/quic/tlscontext.h b/src/quic/tlscontext.h new file mode 100644 index 00000000000000..9dff16e983b5a3 --- /dev/null +++ b/src/quic/tlscontext.h @@ -0,0 +1,176 @@ +#pragma once + +#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS +#if HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC + +#include +#include +#include +#include +#include +#include "bindingdata.h" +#include "data.h" +#include "sessionticket.h" + +namespace node { +namespace quic { + +class Session; + +// Every QUIC Session has exactly one TLSContext that maintains the state +// of the TLS handshake and negotiated cipher keys after the handshake has +// been completed. It is separated out from the main Session class only as a +// convenience to help make the code more maintainable and understandable. +class TLSContext final : public MemoryRetainer { + public: + static constexpr auto DEFAULT_CIPHERS = "TLS_AES_128_GCM_SHA256:" + "TLS_AES_256_GCM_SHA384:" + "TLS_CHACHA20_POLY1305_" + "SHA256:TLS_AES_128_CCM_SHA256"; + static constexpr auto DEFAULT_GROUPS = "X25519:P-256:P-384:P-521"; + + static inline const TLSContext& From(const SSL* ssl); + static inline TLSContext& From(SSL* ssl); + + struct Options final : public MemoryRetainer { + // The protocol identifier to be used by this Session. + std::string alpn = NGHTTP3_ALPN_H3; + + // The SNI hostname to be used. This is used only by client Sessions to + // identify the SNI host in the TLS client hello message. + std::string hostname = ""; + + // When true, TLS keylog data will be emitted to the JavaScript session. + bool keylog = false; + + // When set, the peer certificate is verified against the list of supplied + // CAs. If verification fails, the connection will be refused. + bool reject_unauthorized = true; + + // When set, enables TLS tracing for the session. This should only be used + // for debugging. + bool enable_tls_trace = false; + + // Options only used by server sessions: + + // When set, instructs the server session to request a client authentication + // certificate. + bool request_peer_certificate = false; + + // Options only used by client sessions: + + // When set, instructs the client session to verify the hostname default. + // This is required by QUIC and enabled by default. We allow disabling it + // only for debugging. + bool verify_hostname_identity = true; + + // The TLS session ID context (only used on the server) + std::string session_id_ctx = "Node.js QUIC Server"; + + // TLS cipher suite + std::string ciphers = DEFAULT_CIPHERS; + + // TLS groups + std::string groups = DEFAULT_GROUPS; + + // The TLS private key to use for this session. + std::vector> keys; + + // Collection of certificates to use for this session. + std::vector certs; + + // Optional certificate authority overrides to use. + std::vector ca; + + // Optional certificate revocation lists to use. + std::vector crl; + + void MemoryInfo(MemoryTracker* tracker) const override; + SET_MEMORY_INFO_NAME(CryptoContext::Options) + SET_SELF_SIZE(Options); + + static v8::Maybe From(Environment* env, + v8::Local value); + }; + + static const Options kDefaultOptions; + + TLSContext(Environment* env, + Side side, + Session* session, + const Options& options); + TLSContext(const TLSContext&) = delete; + TLSContext(TLSContext&&) = delete; + TLSContext& operator=(const TLSContext&) = delete; + TLSContext& operator=(TLSContext&&) = delete; + + // Start the TLS handshake. + void Start(); + + // TLS Keylogging is enabled per-Session by attaching an handler to the + // "keylog" event. Each keylog line is emitted to JavaScript where it can be + // routed to whatever destination makes sense. Typically, this will be to a + // keylog file that can be consumed by tools like Wireshark to intercept and + // decrypt QUIC network traffic. + void 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(ngtcp2_crypto_level crypto_level, + uint64_t offset, + const ngtcp2_vec& vec); + + v8::MaybeLocal cert(Environment* env) const; + v8::MaybeLocal peer_cert(Environment* env) const; + v8::MaybeLocal cipher_name(Environment* env) const; + v8::MaybeLocal cipher_version(Environment* env) const; + v8::MaybeLocal ephemeral_key(Environment* env) const; + + // The SNI servername negotiated for the session + const std::string_view servername() const; + + // The ALPN (protocol name) negotiated for the session + const std::string_view alpn() const; + + // Triggers key update to begin. This will fail and return false if either a + // previous key update is in progress and has not been confirmed or if the + // initial handshake has not yet been confirmed. + bool InitiateKeyUpdate(); + + int VerifyPeerIdentity(); + + Side side() const; + const Options& options() const; + + int OnNewSession(SSL_SESSION* session); + + void MaybeSetEarlySession(const SessionTicket& sessionTicket); + bool early_data_was_accepted() const; + + void MemoryInfo(MemoryTracker* tracker) const override; + SET_MEMORY_INFO_NAME(CryptoContext) + SET_SELF_SIZE(TLSContext) + + private: + static ngtcp2_conn* getConnection(ngtcp2_crypto_conn_ref* ref); + ngtcp2_crypto_conn_ref conn_ref_; + + Side side_; + Environment* env_; + Session* session_; + const Options options_; + BaseObjectPtr secure_context_; + crypto::SSLPointer ssl_; + crypto::BIOPointer bio_trace_; + + bool in_key_update_ = false; + bool early_data_ = false; + + friend class Session; +}; + +} // namespace quic +} // namespace node + +#endif // HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC +#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS From 566bb14e82ae12a36ca14e5d734d48aa776cfa4d Mon Sep 17 00:00:00 2001 From: James M Snell Date: Sat, 1 Apr 2023 08:42:21 -0700 Subject: [PATCH 2/9] quic: add stat collection utilities --- src/quic/defs.h | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/src/quic/defs.h b/src/quic/defs.h index 48e80964c0905e..bf9ce785efb7c9 100644 --- a/src/quic/defs.h +++ b/src/quic/defs.h @@ -1,7 +1,9 @@ #pragma once +#include #include #include +#include #include namespace node { @@ -64,5 +66,37 @@ bool SetOption(Environment* env, return true; } +// Utilities used to update that stats for Endpoint, Session, and Stream +// objects. The stats themselves are maintained in an AliasedStruct within +// each of the relevant classes. + +template +void IncrementStat(Stats* stats, uint64_t amt = 1) { + stats->*member += amt; +} + +template +void RecordTimestampStat(Stats* stats) { + stats->*member = uv_hrtime(); +} + +template +void SetStat(Stats* stats, uint64_t val) { + stats->*member = val; +} + +template +uint64_t GetStat(Stats* stats) { + return stats->*member; +} + +#define STAT_INCREMENT(Type, name) IncrementStat(&stats_); +#define STAT_INCREMENT_N(Type, name, amt) \ + IncrementStat(&stats_, amt); +#define STAT_RECORD_TIMESTAMP(Type, name) \ + RecordTimestampStat(&stats_); +#define STAT_SET(Type, name, val) SetStat(&stats_, val); +#define STAT_GET(Type, name) GetStat(&stats_); + } // namespace quic } // namespace node From a3a0624a3b90d3681c4ece7b0c9083a1819236e7 Mon Sep 17 00:00:00 2001 From: James M Snell Date: Sat, 1 Apr 2023 09:18:04 -0700 Subject: [PATCH 3/9] quic: add Packet --- node.gyp | 2 + src/async_wrap.h | 1 + src/node_errors.h | 2 + src/quic/bindingdata.cc | 13 ++ src/quic/bindingdata.h | 15 +- src/quic/packet.cc | 406 ++++++++++++++++++++++++++++++++++++++++ src/quic/packet.h | 168 +++++++++++++++++ 7 files changed, 601 insertions(+), 6 deletions(-) create mode 100644 src/quic/packet.cc create mode 100644 src/quic/packet.h diff --git a/node.gyp b/node.gyp index bcc0c43d8caafe..291f8fa76254ee 100644 --- a/node.gyp +++ b/node.gyp @@ -341,6 +341,7 @@ 'src/quic/cid.cc', 'src/quic/data.cc', 'src/quic/logstream.cc', + 'src/quic/packet.cc', 'src/quic/preferredaddress.cc', 'src/quic/sessionticket.cc', 'src/quic/tlscontext.cc', @@ -350,6 +351,7 @@ 'src/quic/cid.h', 'src/quic/data.h', 'src/quic/logstream.h', + 'src/quic/packet.h', 'src/quic/preferredaddress.h', 'src/quic/sessionticket.h', 'src/quic/tlscontext.h', diff --git a/src/async_wrap.h b/src/async_wrap.h index 42e3413b12ace4..735ae1f7dbe54e 100644 --- a/src/async_wrap.h +++ b/src/async_wrap.h @@ -61,6 +61,7 @@ namespace node { V(PROMISE) \ V(QUERYWRAP) \ V(QUIC_LOGSTREAM) \ + V(QUIC_PACKET) \ V(SHUTDOWNWRAP) \ V(SIGNALWRAP) \ V(STATWATCHER) \ diff --git a/src/node_errors.h b/src/node_errors.h index 4edb1c20b389ad..5250b1437ed770 100644 --- a/src/node_errors.h +++ b/src/node_errors.h @@ -63,6 +63,7 @@ void OOMErrorHandler(const char* location, const v8::OOMDetails& details); V(ERR_DLOPEN_FAILED, Error) \ V(ERR_ENCODING_INVALID_ENCODED_DATA, TypeError) \ V(ERR_EXECUTION_ENVIRONMENT_NOT_AVAILABLE, Error) \ + V(ERR_ILLEGAL_CONSTRUCTOR, Error) \ V(ERR_INVALID_ADDRESS, Error) \ V(ERR_INVALID_ARG_VALUE, TypeError) \ V(ERR_OSSL_EVP_INVALID_DIGEST, Error) \ @@ -156,6 +157,7 @@ ERRORS_WITH_CODE(V) V(ERR_DLOPEN_FAILED, "DLOpen failed") \ V(ERR_EXECUTION_ENVIRONMENT_NOT_AVAILABLE, \ "Context not associated with Node.js environment") \ + V(ERR_ILLEGAL_CONSTRUCTOR, "Illegal constructor") \ V(ERR_INVALID_ADDRESS, "Invalid socket address") \ V(ERR_INVALID_MODULE, "No such module") \ V(ERR_INVALID_STATE, "Invalid state") \ diff --git a/src/quic/bindingdata.cc b/src/quic/bindingdata.cc index 2a4962b3b52c06..4623566d1db79e 100644 --- a/src/quic/bindingdata.cc +++ b/src/quic/bindingdata.cc @@ -58,6 +58,7 @@ void BindingData::DecreaseAllocatedSize(size_t size) { void BindingData::Initialize(Environment* env, Local target) { SetMethod(env->context(), target, "setCallbacks", SetCallbacks); + SetMethod(env->context(), target, "flushPacketFreelist", FlushPacketFreelist); Realm::GetCurrent(env->context()) ->AddBindingData(env->context(), target); } @@ -65,6 +66,7 @@ void BindingData::Initialize(Environment* env, Local target) { void BindingData::RegisterExternalReferences( ExternalReferenceRegistry* registry) { registry->Register(SetCallbacks); + registry->Register(FlushPacketFreelist); } BindingData::BindingData(Realm* realm, Local object) @@ -159,6 +161,17 @@ void BindingData::SetCallbacks(const FunctionCallbackInfo& args) { #undef V } +void BindingData::FlushPacketFreelist( + const v8::FunctionCallbackInfo& args) { + auto env = Environment::GetCurrent(args); + BindingData& state = BindingData::Get(env); + state.packet_freelist.clear(); +} + +void IllegalConstructor(const FunctionCallbackInfo& args) { + THROW_ERR_ILLEGAL_CONSTRUCTOR(Environment::GetCurrent(args)); +} + } // namespace quic } // namespace node diff --git a/src/quic/bindingdata.h b/src/quic/bindingdata.h index 070ff8499c54de..b3b96cfa7dfdc8 100644 --- a/src/quic/bindingdata.h +++ b/src/quic/bindingdata.h @@ -12,11 +12,13 @@ #include #include #include +#include namespace node { namespace quic { class Endpoint; +class Packet; enum class Side { CLIENT = NGTCP2_CRYPTO_SIDE_CLIENT, @@ -129,12 +131,11 @@ class BindingData final // bridge out to the JS API. static void SetCallbacks(const v8::FunctionCallbackInfo& args); - // TODO(@jasnell) This will be added when Endpoint is implemented. - // // A set of listening Endpoints. We maintain this to ensure that the - // Endpoint - // // cannot be gc'd while it is still listening and there are active - // // connections. - // std::unordered_map> listening_endpoints; + std::vector> packet_freelist; + + // Purge the packet free list to free up memory. + static void FlushPacketFreelist( + const v8::FunctionCallbackInfo& args); // The following set up various storage and accessors for common strings, // construction templates, and callbacks stored on the BindingData. These @@ -180,6 +181,8 @@ class BindingData final #undef V }; +void IllegalConstructor(const v8::FunctionCallbackInfo& args); + } // namespace quic } // namespace node diff --git a/src/quic/packet.cc b/src/quic/packet.cc new file mode 100644 index 00000000000000..b33e859fe1cf39 --- /dev/null +++ b/src/quic/packet.cc @@ -0,0 +1,406 @@ +#if HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC + +#include "packet.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "bindingdata.h" +#include "cid.h" +#include "tokens.h" + +namespace node { + +using v8::FunctionTemplate; +using v8::Local; +using v8::Object; + +namespace quic { + +namespace { +static constexpr size_t kRandlen = NGTCP2_MIN_STATELESS_RESET_RANDLEN * 5; +static constexpr size_t kMinStatelessResetLen = 41; +static constexpr size_t kMaxFreeList = 100; +} // namespace + +struct Packet::Data final : public MemoryRetainer { + MaybeStackBuffer data_; + + // The diagnostic_label_ is used only as a debugging tool when + // logging debug information about the packet. It identifies + // the purpose of the packet. + const std::string diagnostic_label_; + + void MemoryInfo(MemoryTracker* tracker) const override { + tracker->TrackFieldWithSize("data", data_.length()); + } + SET_MEMORY_INFO_NAME(Data) + SET_SELF_SIZE(Data) + + Data(size_t length, const char* diagnostic_label) + : diagnostic_label_(diagnostic_label) { + data_.AllocateSufficientStorage(length); + }; + + size_t length() const { return data_.length(); } + operator uv_buf_t() { + return uv_buf_init(reinterpret_cast(data_.out()), data_.length()); + } + operator ngtcp2_vec() { return ngtcp2_vec{data_.out(), data_.length()}; } + + std::string ToString() const { + return diagnostic_label_ + ", " + std::to_string(length()); + } +}; + +const SocketAddress& Packet::destination() const { + return destination_; +} + +bool Packet::is_sending() const { + return !!handle_; +} + +size_t Packet::length() const { + return data_ ? data_->length() : 0; +} + +Packet::operator uv_buf_t() const { + return !data_ ? uv_buf_init(nullptr, 0) : *data_; +} + +Packet::operator ngtcp2_vec() const { + return !data_ ? ngtcp2_vec{nullptr, 0} : *data_; +} + +void Packet::Truncate(size_t len) { + DCHECK(data_); + DCHECK_LE(len, data_->length()); + data_->data_.SetLength(len); +} + +Local Packet::GetConstructorTemplate(Environment* env) { + auto& state = BindingData::Get(env); + Local tmpl = state.packet_constructor_template(); + if (tmpl.IsEmpty()) { + tmpl = NewFunctionTemplate(env->isolate(), IllegalConstructor); + tmpl->Inherit(ReqWrap::GetConstructorTemplate(env)); + tmpl->InstanceTemplate()->SetInternalFieldCount( + Packet::kInternalFieldCount); + tmpl->SetClassName(state.packetwrap_string()); + state.set_packet_constructor_template(tmpl); + } + 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()) { + Local obj; + if (UNLIKELY(!GetConstructorTemplate(env) + ->InstanceTemplate() + ->NewInstance(env->context()) + .ToLocal(&obj))) { + return BaseObjectPtr(); + } + + return MakeBaseObject( + env, listener, obj, destination, length, diagnostic_label); + } + + return FromFreeList(env, + std::make_shared(length, diagnostic_label), + listener, + destination); +} + +BaseObjectPtr Packet::Clone() const { + auto& binding = BindingData::Get(env()); + if (binding.packet_freelist.empty()) { + Local obj; + if (UNLIKELY(!GetConstructorTemplate(env()) + ->InstanceTemplate() + ->NewInstance(env()->context()) + .ToLocal(&obj))) { + return BaseObjectPtr(); + } + + return MakeBaseObject(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) { + auto& binding = BindingData::Get(env); + auto obj = 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); + packet->destination_ = destination; + packet->listener_ = listener; + return BaseObjectPtr(packet); +} + +Packet::Packet(Environment* env, + Listener* listener, + Local object, + const SocketAddress& destination, + std::shared_ptr data) + : ReqWrap(env, object, AsyncWrap::PROVIDER_QUIC_PACKET), + listener_(listener), + destination_(destination), + data_(std::move(data)) {} + +Packet::Packet(Environment* env, + Listener* listener, + Local object, + const SocketAddress& destination, + size_t length, + const char* diagnostic_label) + : Packet(env, + listener, + object, + 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) { + DCHECK_NOT_NULL(listener_); + listener_->PacketDone(status); + handle_.reset(); + data_.reset(); + listener_ = nullptr; + 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); + } else { + delete this; + } +} + +std::string Packet::ToString() const { + if (!data_) return "Packet ()"; + return "Packet (" + data_->ToString() + ")"; +} + +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) { + auto& random = CID::Factory::random(); + CID cid = random.Generate(); + RetryToken token(path_descriptor.version, + path_descriptor.remote_address, + cid, + path_descriptor.dcid, + token_secret); + if (!token) return BaseObjectPtr(); + + const ngtcp2_vec& vec = token; + + size_t pktlen = + vec.len + (2 * NGTCP2_MAX_CIDLEN) + path_descriptor.scid.length() + 8; + + auto packet = + Create(env, listener, path_descriptor.remote_address, pktlen, "retry"); + if (!packet) return BaseObjectPtr(); + + ngtcp2_vec dest = *packet; + + ssize_t nwrite = ngtcp2_crypto_write_retry(dest.base, + pktlen, + path_descriptor.version, + path_descriptor.scid, + cid, + path_descriptor.dcid, + vec.base, + vec.len); + if (nwrite <= 0) return BaseObjectPtr(); + packet->Truncate(nwrite); + return packet; +} + +BaseObjectPtr Packet::CreateConnectionClosePacket( + Environment* env, + Listener* listener, + const SocketAddress& destination, + ngtcp2_conn* conn, + const QuicError& error) { + auto packet = Packet::Create( + env, listener, destination, kDefaultMaxPacketLength, "connection close"); + 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) return BaseObjectPtr(); + packet->Truncate(nwrite); + return packet; +} + +BaseObjectPtr Packet::CreateImmediateConnectionClosePacket( + Environment* env, + Listener* listener, + const SocketAddress& destination, + const PathDescriptor& path_descriptor, + const QuicError& reason) { + auto packet = Packet::Create(env, + listener, + path_descriptor.remote_address, + kDefaultMaxPacketLength, + "immediate connection close (endpoint)"); + ngtcp2_vec vec = *packet; + ssize_t nwrite = ngtcp2_crypto_write_connection_close( + vec.base, + vec.len, + path_descriptor.version, + path_descriptor.dcid, + path_descriptor.scid, + reason.code(), + // We do not bother sending a reason string here, even if + // there is one in the QuicError + nullptr, + 0); + if (nwrite <= 0) return BaseObjectPtr(); + packet->Truncate(static_cast(nwrite)); + return packet; +} + +BaseObjectPtr Packet::CreateStatelessResetPacket( + Environment* env, + Listener* listener, + const PathDescriptor& path_descriptor, + const TokenSecret& token_secret, + size_t source_len) { + // Per the QUIC spec, a stateless reset token must be strictly smaller than + // the packet that triggered it. This is one of the mechanisms to prevent + // infinite looping exchange of stateless tokens with the peer. An endpoint + // should never send a stateless reset token smaller than 41 bytes per the + // QUIC spec. The reason is that packets less than 41 bytes may allow an + // observer to reliably determine that it's a stateless reset. + size_t pktlen = source_len - 1; + if (pktlen < kMinStatelessResetLen) return BaseObjectPtr(); + + StatelessResetToken token(token_secret, path_descriptor.dcid); + uint8_t random[kRandlen]; + CHECK(crypto::CSPRNG(random, kRandlen).is_ok()); + + auto packet = Packet::Create(env, + listener, + path_descriptor.remote_address, + kDefaultMaxPacketLength, + "stateless reset"); + ngtcp2_vec vec = *packet; + + ssize_t nwrite = ngtcp2_pkt_write_stateless_reset( + vec.base, pktlen, token, random, kRandlen); + if (nwrite <= static_cast(kMinStatelessResetLen)) { + return BaseObjectPtr(); + } + + packet->Truncate(nwrite); + return packet; +} + +BaseObjectPtr Packet::CreateVersionNegotiationPacket( + Environment* env, + Listener* listener, + const PathDescriptor& path_descriptor) { + const auto generateReservedVersion = [&] { + socklen_t addrlen = path_descriptor.remote_address.length(); + uint32_t h = 0x811C9DC5u; + uint32_t ver = htonl(path_descriptor.version); + const uint8_t* p = path_descriptor.remote_address.raw(); + const uint8_t* ep = p + addrlen; + for (; p != ep; ++p) { + h ^= *p; + h *= 0x01000193u; + } + p = reinterpret_cast(&ver); + ep = p + sizeof(path_descriptor.version); + for (; p != ep; ++p) { + h ^= *p; + h *= 0x01000193u; + } + h &= 0xf0f0f0f0u; + h |= NGTCP2_RESERVED_VERSION_MASK; + return h; + }; + + uint32_t sv[3] = { + generateReservedVersion(), NGTCP2_PROTO_VER_MIN, NGTCP2_PROTO_VER_MAX}; + + size_t pktlen = path_descriptor.dcid.length() + + path_descriptor.scid.length() + (sizeof(sv)) + 7; + + auto packet = Packet::Create(env, + listener, + path_descriptor.remote_address, + kDefaultMaxPacketLength, + "version negotiation"); + ngtcp2_vec vec = *packet; + + ssize_t nwrite = + ngtcp2_pkt_write_version_negotiation(vec.base, + pktlen, + 0, + path_descriptor.dcid, + path_descriptor.dcid.length(), + path_descriptor.scid, + path_descriptor.scid.length(), + sv, + arraysize(sv)); + if (nwrite <= 0) return BaseObjectPtr(); + packet->Truncate(nwrite); + return packet; +} + +} // namespace quic +} // namespace node + +#endif // HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC diff --git a/src/quic/packet.h b/src/quic/packet.h new file mode 100644 index 00000000000000..156174ebac8379 --- /dev/null +++ b/src/quic/packet.h @@ -0,0 +1,168 @@ +#pragma once + +#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS +#if HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "bindingdata.h" +#include "cid.h" +#include "data.h" +#include "tokens.h" + +namespace node { +namespace quic { + +struct PathDescriptor { + uint32_t version; + const CID& dcid; + const CID& scid; + const SocketAddress& local_address; + const SocketAddress& remote_address; +}; + +// A Packet encapsulates serialized outbound QUIC data. +// Packets must never be larger than the path MTU. The +// default QUIC packet maximum length is 1200 bytes, +// which we assume by default. The packet storage will +// be stack allocated up to this size. +// +// Packets are maintained in a freelist held by the +// BindingData instance. When using Create() to create +// a Packet, we'll check to see if there is a free +// packet in the freelist and use it instead of starting +// fresh with a new packet. The freelist can store at +// most kMaxFreeList packets +// +// Packets are always encrypted so their content should +// be considered opaque to us. We leave it entirely up +// to ngtcp2 how to encode QUIC frames into the packet. +class Packet final : public ReqWrap { + private: + struct Data; + + public: + using Queue = std::deque>; + + static v8::Local GetConstructorTemplate( + Environment* env); + + class Listener { + public: + virtual void PacketDone(int status) = 0; + }; + + // Do not use the Packet constructors directly to create + // them. These are public only to support MakeBaseObject. + // Use the Create, or Create variants to create or + // acquire packet instances. + + Packet(Environment* env, + Listener* listener, + v8::Local object, + const SocketAddress& destination, + size_t length, + const char* diagnostic_label = ""); + + Packet(Environment* env, + Listener* listener, + v8::Local object, + const SocketAddress& destination, + std::shared_ptr data); + + Packet(const Packet&) = delete; + Packet(Packet&&) = delete; + Packet& operator=(const Packet&) = delete; + 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; + + // Modify the size of the packet after ngtcp2 has written + // to it. len must be <= length(). We call this after we've + // asked ngtcp2 to encode frames into the packet and ngtcp2 + // 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 = ""); + + BaseObjectPtr Clone() const; + + void MemoryInfo(MemoryTracker* tracker) const override; + SET_MEMORY_INFO_NAME(Packet) + SET_SELF_SIZE(Packet) + + 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 BaseObjectPtr CreateConnectionClosePacket( + Environment* env, + Listener* listener, + const SocketAddress& destination, + ngtcp2_conn* conn, + const QuicError& error); + + static BaseObjectPtr CreateImmediateConnectionClosePacket( + Environment* env, + Listener* listener, + const SocketAddress& destination, + const PathDescriptor& path_descriptor, + const QuicError& reason); + + static BaseObjectPtr CreateStatelessResetPacket( + Environment* env, + Listener* listener, + const PathDescriptor& path_descriptor, + const TokenSecret& token_secret, + size_t source_len); + + static BaseObjectPtr CreateVersionNegotiationPacket( + Environment* env, + Listener* listener, + const PathDescriptor& path_descriptor); + + private: + static BaseObjectPtr FromFreeList(Environment* env, + std::shared_ptr data, + Listener* listener, + const SocketAddress& destination); + + // Called when the packet is done being sent. + void Done(int status); + + Listener* listener_; + SocketAddress destination_; + std::shared_ptr data_; + BaseObjectPtr handle_; +}; + +} // namespace quic +} // namespace node + +#endif // HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC +#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS From 2fa2cdbfbcca3e727b3463241fc6ad4527b0985d Mon Sep 17 00:00:00 2001 From: James M Snell Date: Sat, 1 Apr 2023 14:28:38 -0700 Subject: [PATCH 4/9] quic: add NgTcp2CallbackScope/NgHttp3CallbackScope --- src/quic/bindingdata.cc | 32 ++++++++++++++++++++++++++++++++ src/quic/bindingdata.h | 20 ++++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/src/quic/bindingdata.cc b/src/quic/bindingdata.cc index 4623566d1db79e..07feb3f91bd25b 100644 --- a/src/quic/bindingdata.cc +++ b/src/quic/bindingdata.cc @@ -168,6 +168,38 @@ void BindingData::FlushPacketFreelist( state.packet_freelist.clear(); } +NgTcp2CallbackScope::NgTcp2CallbackScope(Environment* env) : env(env) { + auto& binding = BindingData::Get(env); + CHECK(!binding.in_ngtcp2_callback_scope); + binding.in_ngtcp2_callback_scope = true; +} + +NgTcp2CallbackScope::~NgTcp2CallbackScope() { + auto& binding = BindingData::Get(env); + binding.in_ngtcp2_callback_scope = false; +} + +bool NgTcp2CallbackScope::in_ngtcp2_callback(Environment* env) { + auto& binding = BindingData::Get(env); + return binding.in_ngtcp2_callback_scope; +} + +NgHttp3CallbackScope::NgHttp3CallbackScope(Environment* env) : env(env) { + auto& binding = BindingData::Get(env); + CHECK(!binding.in_nghttp3_callback_scope); + binding.in_nghttp3_callback_scope = true; +} + +NgHttp3CallbackScope::~NgHttp3CallbackScope() { + auto& binding = BindingData::Get(env); + binding.in_nghttp3_callback_scope = false; +} + +bool NgHttp3CallbackScope::in_nghttp3_callback(Environment* env) { + auto& binding = BindingData::Get(env); + return binding.in_nghttp3_callback_scope; +} + void IllegalConstructor(const FunctionCallbackInfo& args) { THROW_ERR_ILLEGAL_CONSTRUCTOR(Environment::GetCurrent(args)); } diff --git a/src/quic/bindingdata.h b/src/quic/bindingdata.h index b3b96cfa7dfdc8..fcc31c56840166 100644 --- a/src/quic/bindingdata.h +++ b/src/quic/bindingdata.h @@ -137,6 +137,9 @@ class BindingData final static void FlushPacketFreelist( const v8::FunctionCallbackInfo& args); + bool in_ngtcp2_callback_scope = false; + bool in_nghttp3_callback_scope = false; + // The following set up various storage and accessors for common strings, // construction templates, and callbacks stored on the BindingData. These // are all defined in defs.h @@ -183,6 +186,23 @@ class BindingData final void IllegalConstructor(const v8::FunctionCallbackInfo& args); +// The ngtcp2 and nghttp3 callbacks have certain restrictions +// that forbid re-entry. We provide the following scopes for +// use in those to help protect against it. +struct NgTcp2CallbackScope { + Environment* env; + NgTcp2CallbackScope(Environment* env); + ~NgTcp2CallbackScope(); + static bool in_ngtcp2_callback(Environment* env); +}; + +struct NgHttp3CallbackScope { + Environment* env; + NgHttp3CallbackScope(Environment* env); + ~NgHttp3CallbackScope(); + static bool in_nghttp3_callback(Environment* env); +}; + } // namespace quic } // namespace node From c9b092f1e98604bd58de08fb4d8de0b9699d4c9c Mon Sep 17 00:00:00 2001 From: James M Snell Date: Tue, 11 Apr 2023 13:19:33 -0500 Subject: [PATCH 5/9] quic: address review feedback --- src/quic/bindingdata.cc | 7 +++---- src/quic/defs.h | 2 +- src/quic/packet.cc | 2 +- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/quic/bindingdata.cc b/src/quic/bindingdata.cc index 07feb3f91bd25b..9690031773781b 100644 --- a/src/quic/bindingdata.cc +++ b/src/quic/bindingdata.cc @@ -142,7 +142,7 @@ QUIC_JS_CALLBACKS(V) void BindingData::SetCallbacks(const FunctionCallbackInfo& args) { auto env = Environment::GetCurrent(args); auto isolate = env->isolate(); - BindingData& state = BindingData::Get(env); + auto& state = BindingData::Get(env); CHECK(args[0]->IsObject()); Local obj = args[0].As(); @@ -161,10 +161,9 @@ void BindingData::SetCallbacks(const FunctionCallbackInfo& args) { #undef V } -void BindingData::FlushPacketFreelist( - const v8::FunctionCallbackInfo& args) { +void BindingData::FlushPacketFreelist(const FunctionCallbackInfo& args) { auto env = Environment::GetCurrent(args); - BindingData& state = BindingData::Get(env); + auto& state = BindingData::Get(env); state.packet_freelist.clear(); } diff --git a/src/quic/defs.h b/src/quic/defs.h index bf9ce785efb7c9..6b8048d040e991 100644 --- a/src/quic/defs.h +++ b/src/quic/defs.h @@ -66,7 +66,7 @@ bool SetOption(Environment* env, return true; } -// Utilities used to update that stats for Endpoint, Session, and Stream +// Utilities used to update the stats for Endpoint, Session, and Stream // objects. The stats themselves are maintained in an AliasedStruct within // each of the relevant classes. diff --git a/src/quic/packet.cc b/src/quic/packet.cc index b33e859fe1cf39..5932792b2302d6 100644 --- a/src/quic/packet.cc +++ b/src/quic/packet.cc @@ -43,7 +43,7 @@ struct Packet::Data final : public MemoryRetainer { SET_MEMORY_INFO_NAME(Data) SET_SELF_SIZE(Data) - Data(size_t length, const char* diagnostic_label) + Data(size_t length, std::string_view diagnostic_label) : diagnostic_label_(diagnostic_label) { data_.AllocateSufficientStorage(length); }; From a9b5246e5997dd6eb6a3a68753a5c0bf6f818d56 Mon Sep 17 00:00:00 2001 From: James M Snell Date: Sat, 15 Apr 2023 19:07:21 +0200 Subject: [PATCH 6/9] fixup! quic: address review feedback --- src/quic/packet.cc | 8 ++++---- test/sequential/test-async-wrap-getasyncid.js | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/quic/packet.cc b/src/quic/packet.cc index 5932792b2302d6..5b5bc71a1d277f 100644 --- a/src/quic/packet.cc +++ b/src/quic/packet.cc @@ -262,7 +262,7 @@ BaseObjectPtr Packet::CreateRetryPacket( vec.base, vec.len); if (nwrite <= 0) return BaseObjectPtr(); - packet->Truncate(nwrite); + packet->Truncate(static_cast(nwrite)); return packet; } @@ -279,7 +279,7 @@ BaseObjectPtr Packet::CreateConnectionClosePacket( ssize_t nwrite = ngtcp2_conn_write_connection_close( conn, nullptr, nullptr, vec.base, vec.len, error, uv_hrtime()); if (nwrite < 0) return BaseObjectPtr(); - packet->Truncate(nwrite); + packet->Truncate(static_cast(nwrite)); return packet; } @@ -343,7 +343,7 @@ BaseObjectPtr Packet::CreateStatelessResetPacket( return BaseObjectPtr(); } - packet->Truncate(nwrite); + packet->Truncate(static_cast(nwrite)); return packet; } @@ -396,7 +396,7 @@ BaseObjectPtr Packet::CreateVersionNegotiationPacket( sv, arraysize(sv)); if (nwrite <= 0) return BaseObjectPtr(); - packet->Truncate(nwrite); + packet->Truncate(static_cast(nwrite)); return packet; } diff --git a/test/sequential/test-async-wrap-getasyncid.js b/test/sequential/test-async-wrap-getasyncid.js index 097529f8caeae7..f934536982e7bc 100644 --- a/test/sequential/test-async-wrap-getasyncid.js +++ b/test/sequential/test-async-wrap-getasyncid.js @@ -67,6 +67,7 @@ const { getSystemErrorName } = require('util'); delete providers.RANDOMPRIMEREQUEST; delete providers.CHECKPRIMEREQUEST; delete providers.QUIC_LOGSTREAM; + delete providers.QUIC_PACKET; const objKeys = Object.keys(providers); if (objKeys.length > 0) From 6611dc7986ad8b962f32a2826eac52a1dc557a69 Mon Sep 17 00:00:00 2001 From: James M Snell Date: Sat, 15 Apr 2023 19:16:33 +0200 Subject: [PATCH 7/9] fixup! fixup! quic: address review feedback --- src/quic/tlscontext.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/quic/tlscontext.h b/src/quic/tlscontext.h index 9dff16e983b5a3..588c3e7f2517fd 100644 --- a/src/quic/tlscontext.h +++ b/src/quic/tlscontext.h @@ -87,7 +87,7 @@ class TLSContext final : public MemoryRetainer { void MemoryInfo(MemoryTracker* tracker) const override; SET_MEMORY_INFO_NAME(CryptoContext::Options) - SET_SELF_SIZE(Options); + SET_SELF_SIZE(Options) static v8::Maybe From(Environment* env, v8::Local value); @@ -107,7 +107,7 @@ class TLSContext final : public MemoryRetainer { // Start the TLS handshake. void Start(); - // TLS Keylogging is enabled per-Session by attaching an handler to the + // TLS Keylogging is enabled per-Session by attaching a handler to the // "keylog" event. Each keylog line is emitted to JavaScript where it can be // routed to whatever destination makes sense. Typically, this will be to a // keylog file that can be consumed by tools like Wireshark to intercept and From 9ffd87d3a304dacde516d5a591ded0ab342124e2 Mon Sep 17 00:00:00 2001 From: James M Snell Date: Sat, 15 Apr 2023 19:20:42 +0200 Subject: [PATCH 8/9] fixup! fixup! fixup! quic: address review feedback --- src/quic/bindingdata.h | 4 ++-- src/quic/packet.cc | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/quic/bindingdata.h b/src/quic/bindingdata.h index fcc31c56840166..e9e8e719c4d892 100644 --- a/src/quic/bindingdata.h +++ b/src/quic/bindingdata.h @@ -191,14 +191,14 @@ void IllegalConstructor(const v8::FunctionCallbackInfo& args); // use in those to help protect against it. struct NgTcp2CallbackScope { Environment* env; - NgTcp2CallbackScope(Environment* env); + explicit NgTcp2CallbackScope(Environment* env); ~NgTcp2CallbackScope(); static bool in_ngtcp2_callback(Environment* env); }; struct NgHttp3CallbackScope { Environment* env; - NgHttp3CallbackScope(Environment* env); + explicit NgHttp3CallbackScope(Environment* env); ~NgHttp3CallbackScope(); static bool in_nghttp3_callback(Environment* env); }; diff --git a/src/quic/packet.cc b/src/quic/packet.cc index 5b5bc71a1d277f..27ba21d69a5e90 100644 --- a/src/quic/packet.cc +++ b/src/quic/packet.cc @@ -46,7 +46,7 @@ struct Packet::Data final : public MemoryRetainer { Data(size_t length, std::string_view diagnostic_label) : diagnostic_label_(diagnostic_label) { data_.AllocateSufficientStorage(length); - }; + } size_t length() const { return data_.length(); } operator uv_buf_t() { From c7eecd2a8df0f428714c7814c4f8c389a42b5d30 Mon Sep 17 00:00:00 2001 From: James M Snell Date: Mon, 17 Apr 2023 11:18:26 -0700 Subject: [PATCH 9/9] fixup! fixup! fixup! fixup! quic: address review feedback --- src/quic/tlscontext.cc | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/quic/tlscontext.cc b/src/quic/tlscontext.cc index 500c15fc02c32c..b75f5c56312ec0 100644 --- a/src/quic/tlscontext.cc +++ b/src/quic/tlscontext.cc @@ -1,6 +1,9 @@ -#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS +#if HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC #include "tlscontext.h" +#include "bindingdata.h" +#include "defs.h" +#include "transportparams.h" #include #include #include @@ -9,9 +12,6 @@ #include #include #include -#include "bindingdata.h" -#include "defs.h" -#include "transportparams.h" namespace node { @@ -586,4 +586,4 @@ Maybe TLSContext::Options::From(Environment* env, } // namespace quic } // namespace node -#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS +#endif // HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC