From 7b70938d24624e5d23b2b642260cc8cdd65e885a Mon Sep 17 00:00:00 2001 From: James M Snell Date: Wed, 29 Mar 2023 19:13:48 -0700 Subject: [PATCH 1/4] quic: add BindingData --- node.gyp | 2 + src/base_object_types.h | 3 +- src/quic/bindingdata.cc | 165 ++++++++++++++++++++++++++++++++++++++++ src/quic/bindingdata.h | 153 +++++++++++++++++++++++++++++++++++++ 4 files changed, 322 insertions(+), 1 deletion(-) create mode 100644 src/quic/bindingdata.cc create mode 100644 src/quic/bindingdata.h diff --git a/node.gyp b/node.gyp index 601c458421628a..e69a421362a6ff 100644 --- a/node.gyp +++ b/node.gyp @@ -335,11 +335,13 @@ 'src/node_crypto.h', ], 'node_quic_sources': [ + 'src/quic/bindingdata.cc', 'src/quic/cid.cc', 'src/quic/data.cc', 'src/quic/preferredaddress.cc', 'src/quic/sessionticket.cc', 'src/quic/tokens.cc', + 'src/quic/bindingdata.h', 'src/quic/cid.h', 'src/quic/data.h', 'src/quic/preferredaddress.h', diff --git a/src/base_object_types.h b/src/base_object_types.h index f6e6a696fc6be3..4916a20bbc6421 100644 --- a/src/base_object_types.h +++ b/src/base_object_types.h @@ -20,7 +20,8 @@ namespace node { #define UNSERIALIZABLE_BINDING_TYPES(V) \ V(http2_binding_data, http2::BindingData) \ - V(http_parser_binding_data, http_parser::BindingData) + V(http_parser_binding_data, http_parser::BindingData) \ + V(quic_binding_data, quic::BindingData) // List of (non-binding) BaseObjects that are serializable in the snapshot. // The first argument should match what the type passes to diff --git a/src/quic/bindingdata.cc b/src/quic/bindingdata.cc new file mode 100644 index 00000000000000..2a4962b3b52c06 --- /dev/null +++ b/src/quic/bindingdata.cc @@ -0,0 +1,165 @@ +#if HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC +#include "bindingdata.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace node { + +using v8::Function; +using v8::FunctionCallbackInfo; +using v8::FunctionTemplate; +using v8::Local; +using v8::Object; +using v8::String; +using v8::Value; + +namespace quic { + +BindingData& BindingData::Get(Environment* env) { + return *Realm::GetBindingData(env->context()); +} + +BindingData::operator ngtcp2_mem() { + return MakeAllocator(); +} + +BindingData::operator nghttp3_mem() { + ngtcp2_mem allocator = *this; + nghttp3_mem http3_allocator = { + allocator.user_data, + allocator.malloc, + allocator.free, + allocator.calloc, + allocator.realloc, + }; + return http3_allocator; +} + +void BindingData::CheckAllocatedSize(size_t previous_size) const { + CHECK_GE(current_ngtcp2_memory_, previous_size); +} + +void BindingData::IncreaseAllocatedSize(size_t size) { + current_ngtcp2_memory_ += size; +} + +void BindingData::DecreaseAllocatedSize(size_t size) { + current_ngtcp2_memory_ -= size; +} + +void BindingData::Initialize(Environment* env, Local target) { + SetMethod(env->context(), target, "setCallbacks", SetCallbacks); + Realm::GetCurrent(env->context()) + ->AddBindingData(env->context(), target); +} + +void BindingData::RegisterExternalReferences( + ExternalReferenceRegistry* registry) { + registry->Register(SetCallbacks); +} + +BindingData::BindingData(Realm* realm, Local object) + : BaseObject(realm, object) { + MakeWeak(); +} + +void BindingData::MemoryInfo(MemoryTracker* tracker) const { +#define V(name, _) tracker->TrackField(#name, name##_callback()); + + QUIC_JS_CALLBACKS(V) + +#undef V + +#define V(name, _) tracker->TrackField(#name, name##_string()); + + QUIC_STRINGS(V) + +#undef V +} + +#define V(name) \ + void BindingData::set_##name##_constructor_template( \ + Local tmpl) { \ + name##_constructor_template_.Reset(env()->isolate(), tmpl); \ + } \ + Local BindingData::name##_constructor_template() const { \ + return PersistentToLocal::Default(env()->isolate(), \ + name##_constructor_template_); \ + } + +QUIC_CONSTRUCTORS(V) + +#undef V + +#define V(name, _) \ + void BindingData::set_##name##_callback(Local fn) { \ + name##_callback_.Reset(env()->isolate(), fn); \ + } \ + Local BindingData::name##_callback() const { \ + return PersistentToLocal::Default(env()->isolate(), name##_callback_); \ + } + +QUIC_JS_CALLBACKS(V) + +#undef V + +#define V(name, value) \ + Local BindingData::name##_string() const { \ + if (name##_string_.IsEmpty()) \ + name##_string_.Set(env()->isolate(), \ + OneByteString(env()->isolate(), value)); \ + return name##_string_.Get(env()->isolate()); \ + } + +QUIC_STRINGS(V) + +#undef V + +#define V(name, value) \ + Local BindingData::on_##name##_string() const { \ + if (on_##name##_string_.IsEmpty()) \ + on_##name##_string_.Set( \ + env()->isolate(), \ + FIXED_ONE_BYTE_STRING(env()->isolate(), "on" #value)); \ + return on_##name##_string_.Get(env()->isolate()); \ + } + +QUIC_JS_CALLBACKS(V) + +#undef V + +void BindingData::SetCallbacks(const FunctionCallbackInfo& args) { + auto env = Environment::GetCurrent(args); + auto isolate = env->isolate(); + BindingData& state = BindingData::Get(env); + CHECK(args[0]->IsObject()); + Local obj = args[0].As(); + +#define V(name, key) \ + do { \ + Local val; \ + if (!obj->Get(env->context(), state.on_##name##_string()).ToLocal(&val) || \ + !val->IsFunction()) { \ + return THROW_ERR_MISSING_ARGS(isolate, "Missing Callback: on" #key); \ + } \ + state.set_##name##_callback(val.As()); \ + } while (0); + + QUIC_JS_CALLBACKS(V) + +#undef V +} + +} // namespace quic +} // namespace node + +#endif // HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC diff --git a/src/quic/bindingdata.h b/src/quic/bindingdata.h new file mode 100644 index 00000000000000..689f4319ca667a --- /dev/null +++ b/src/quic/bindingdata.h @@ -0,0 +1,153 @@ +#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 + +namespace node { +namespace quic { + +class Endpoint; + +// ============================================================================ + +// The FunctionTemplates the BindingData will store for us. +#define QUIC_CONSTRUCTORS(V) \ + V(endpoint) \ + V(logstream) \ + V(packet) \ + V(session) \ + V(stream) \ + V(udp) + +// The callbacks are persistent v8::Function references that are set in the +// quic::BindingState used to communicate data and events back out to the JS +// environment. They are set once from the JavaScript side when the +// internalBinding('quic') is first loaded. +#define QUIC_JS_CALLBACKS(V) \ + V(endpoint_close, EndpointClose) \ + V(endpoint_error, EndpointError) \ + V(session_new, SessionNew) \ + V(session_close, SessionClose) \ + V(session_error, SessionError) \ + V(session_datagram, SessionDatagram) \ + V(session_datagram_status, SessionDatagramStatus) \ + V(session_handshake, SessionHandshake) \ + V(session_ticket, SessionTicket) \ + V(session_version_negotiation, SessionVersionNegotiation) \ + V(session_path_validation, SessionPathValidation) \ + V(stream_close, StreamClose) \ + V(stream_error, StreamError) \ + V(stream_created, StreamCreated) \ + V(stream_reset, StreamReset) \ + V(stream_headers, StreamHeaders) \ + V(stream_blocked, StreamBlocked) \ + V(stream_trailers, StreamTrailers) + +// The various JS strings the implementation uses. +#define QUIC_STRINGS(V) \ + V(http3_alpn, &NGHTTP3_ALPN_H3[1]) \ + V(endpoint, "Endpoint") \ + V(endpoint_udp, "Endpoint::UDP") \ + V(logstream, "LogStream") \ + V(packetwrap, "PacketWrap") \ + V(session, "Session") \ + V(stream, "Stream") + +// ============================================================================= +// The BindingState object holds state for the internalBinding('quic') binding +// instance. It is mostly used to hold the persistent constructors, strings, and +// callback references used for the rest of the implementation. +// +// TODO(@jasnell): Make this snapshotable? +class BindingData final + : public BaseObject, + public mem::NgLibMemoryManager { + public: + SET_BINDING_ID(quic_binding_data) + static void Initialize(Environment* env, v8::Local target); + static void RegisterExternalReferences(ExternalReferenceRegistry* registry); + + static BindingData& Get(Environment* env); + + BindingData(Realm* realm, v8::Local object); + + void MemoryInfo(MemoryTracker* tracker) const override; + SET_MEMORY_INFO_NAME(BindingData) + SET_SELF_SIZE(BindingData) + + // NgLibMemoryManager + operator ngtcp2_mem(); + operator nghttp3_mem(); + void CheckAllocatedSize(size_t previous_size) const; + void IncreaseAllocatedSize(size_t size); + void DecreaseAllocatedSize(size_t size); + + // Installs the set of JavaScript callback functions that are used to + // 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; + + // 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 + +#define V(name) \ + void set_##name##_constructor_template( \ + v8::Local tmpl); \ + v8::Local name##_constructor_template() const; + QUIC_CONSTRUCTORS(V) +#undef V + +#define V(name, _) \ + void set_##name##_callback(v8::Local fn); \ + v8::Local name##_callback() const; + QUIC_JS_CALLBACKS(V) +#undef V + +#define V(name, _) v8::Local name##_string() const; + QUIC_STRINGS(V) +#undef V + +#define V(name, _) v8::Local on_##name##_string() const; + QUIC_JS_CALLBACKS(V) +#undef V + + size_t current_ngtcp2_memory_ = 0; + +#define V(name) v8::Global name##_constructor_template_; + QUIC_CONSTRUCTORS(V) +#undef V + +#define V(name, _) v8::Global name##_callback_; + QUIC_JS_CALLBACKS(V) +#undef V + +#define V(name, _) mutable v8::Eternal name##_string_; + QUIC_STRINGS(V) +#undef V + +#define V(name, _) mutable v8::Eternal on_##name##_string_; + QUIC_JS_CALLBACKS(V) +#undef V +}; + +} // namespace quic +} // namespace node + +#endif // HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC +#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS From 0a1703dd6f704b8cf04c23a4718b70e6ad55febb Mon Sep 17 00:00:00 2001 From: James M Snell Date: Wed, 29 Mar 2023 20:01:21 -0700 Subject: [PATCH 2/4] quic: add LogStream --- node.gyp | 2 + src/async_wrap.h | 1 + src/quic/logstream.cc | 152 ++++++++++++++++++++++++++++++++++++++++++ src/quic/logstream.h | 80 ++++++++++++++++++++++ 4 files changed, 235 insertions(+) create mode 100644 src/quic/logstream.cc create mode 100644 src/quic/logstream.h diff --git a/node.gyp b/node.gyp index e69a421362a6ff..d032b91c058e32 100644 --- a/node.gyp +++ b/node.gyp @@ -338,12 +338,14 @@ 'src/quic/bindingdata.cc', 'src/quic/cid.cc', 'src/quic/data.cc', + 'src/quic/logstream.cc', 'src/quic/preferredaddress.cc', 'src/quic/sessionticket.cc', 'src/quic/tokens.cc', 'src/quic/bindingdata.h', 'src/quic/cid.h', 'src/quic/data.h', + 'src/quic/logstream.h', 'src/quic/preferredaddress.h', 'src/quic/sessionticket.h', 'src/quic/tokens.h', diff --git a/src/async_wrap.h b/src/async_wrap.h index 121216579a8ad7..42e3413b12ace4 100644 --- a/src/async_wrap.h +++ b/src/async_wrap.h @@ -60,6 +60,7 @@ namespace node { V(PROCESSWRAP) \ V(PROMISE) \ V(QUERYWRAP) \ + V(QUIC_LOGSTREAM) \ V(SHUTDOWNWRAP) \ V(SIGNALWRAP) \ V(STATWATCHER) \ diff --git a/src/quic/logstream.cc b/src/quic/logstream.cc new file mode 100644 index 00000000000000..5f9517f2edb261 --- /dev/null +++ b/src/quic/logstream.cc @@ -0,0 +1,152 @@ +#if HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC + +#include "logstream.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include "bindingdata.h" + +namespace node { + +using v8::FunctionTemplate; +using v8::Local; +using v8::Object; + +namespace quic { + +Local LogStream::GetConstructorTemplate(Environment* env) { + auto& state = BindingData::Get(env); + auto tmpl = state.logstream_constructor_template(); + if (tmpl.IsEmpty()) { + tmpl = FunctionTemplate::New(env->isolate()); + tmpl->Inherit(AsyncWrap::GetConstructorTemplate(env)); + tmpl->InstanceTemplate()->SetInternalFieldCount( + StreamBase::kInternalFieldCount); + tmpl->SetClassName(state.logstream_string()); + StreamBase::AddMethods(env, tmpl); + state.set_logstream_constructor_template(tmpl); + } + return tmpl; +} + +BaseObjectPtr LogStream::Create(Environment* env) { + v8::Local obj; + if (!GetConstructorTemplate(env) + ->InstanceTemplate() + ->NewInstance(env->context()) + .ToLocal(&obj)) { + return BaseObjectPtr(); + } + return MakeDetachedBaseObject(env, obj); +} + +LogStream::LogStream(Environment* env, Local obj) + : AsyncWrap(env, obj, AsyncWrap::PROVIDER_QUIC_LOGSTREAM), StreamBase(env) { + MakeWeak(); + StreamBase::AttachToObject(GetObject()); +} + +void LogStream::Emit(const uint8_t* data, size_t len, EmitOption option) { + if (fin_seen_) return; + fin_seen_ = option == EmitOption::FIN; + + size_t remaining = len; + // If the len is greater than the size of the buffer returned by + // EmitAlloc then EmitRead will be called multiple times. + while (remaining != 0) { + uv_buf_t buf = EmitAlloc(len); + size_t len = std::min(remaining, buf.len); + memcpy(buf.base, data, len); + remaining -= len; + data += len; + // If we are actively reading from the stream, we'll call emit + // read immediately. Otherwise we buffer the chunk and will push + // the chunks out the next time ReadStart() is called. + if (reading_) { + EmitRead(len, buf); + } else { + // The total measures the total memory used so we always + // increment but buf.len and not chunk len. + ensure_space(buf.len); + total_ += buf.len; + buffer_.push_back(Chunk{len, buf}); + } + } + + if (ended_ && reading_) { + EmitRead(UV_EOF); + } +} + +void LogStream::Emit(const std::string_view line, EmitOption option) { + Emit(reinterpret_cast(line.begin()), line.length(), option); +} + +void LogStream::End() { + ended_ = true; +} + +int LogStream::ReadStart() { + if (reading_) return 0; + // Flush any chunks that have already been buffered. + for (const auto& chunk : buffer_) EmitRead(chunk.len, chunk.buf); + total_ = 0; + buffer_.clear(); + if (fin_seen_) { + // If we've already received the fin, there's nothing else to wait for. + EmitRead(UV_EOF); + return ReadStop(); + } + // Otherwise, we're going to wait for more chunks to be written. + reading_ = true; + return 0; +} + +int LogStream::ReadStop() { + reading_ = false; + return 0; +} + +// We do not use either of these. +int LogStream::DoShutdown(ShutdownWrap* req_wrap) { + UNREACHABLE(); +} +int LogStream::DoWrite(WriteWrap* w, + uv_buf_t* bufs, + size_t count, + uv_stream_t* send_handle) { + UNREACHABLE(); +} + +bool LogStream::IsAlive() { + return !ended_; +} + +bool LogStream::IsClosing() { + return ended_; +} + +AsyncWrap* LogStream::GetAsyncWrap() { + return this; +} + +void LogStream::MemoryInfo(MemoryTracker* tracker) const { + tracker->TrackFieldWithSize("buffer", total_); +} + +// The LogStream buffer enforces a maximum size of kMaxLogStreamBuffer. +void LogStream::ensure_space(size_t amt) { + while (total_ + amt > kMaxLogStreamBuffer) { + total_ -= buffer_.front().buf.len; + buffer_.pop_front(); + } +} +} // namespace quic +} // namespace node + +#endif // HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC diff --git a/src/quic/logstream.h b/src/quic/logstream.h new file mode 100644 index 00000000000000..da0855898c6b6f --- /dev/null +++ b/src/quic/logstream.h @@ -0,0 +1,80 @@ +#pragma once + +#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS +#if HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC + +#include +#include +#include +#include +#include + +namespace node { +namespace quic { + +// The LogStream is a utility that the QUIC impl uses to publish both QLog +// and Keylog diagnostic data (one instance for each). +class LogStream : public AsyncWrap, public StreamBase { + public: + static v8::Local GetConstructorTemplate( + Environment* env); + + static BaseObjectPtr Create(Environment* env); + + LogStream(Environment* env, v8::Local obj); + + enum class EmitOption { + NONE, + FIN, + }; + + void Emit(const uint8_t* data, + size_t len, + EmitOption option = EmitOption::NONE); + + void Emit(const std::string_view line, EmitOption option = EmitOption::NONE); + + void End(); + + int ReadStart() override; + + int ReadStop() override; + + // We do not use either of these. + int DoShutdown(ShutdownWrap* req_wrap) override; + int DoWrite(WriteWrap* w, + uv_buf_t* bufs, + size_t count, + uv_stream_t* send_handle) override; + + bool IsAlive() override; + bool IsClosing() override; + AsyncWrap* GetAsyncWrap() override; + + void MemoryInfo(MemoryTracker* tracker) const override; + SET_MEMORY_INFO_NAME(LogStream) + SET_SELF_SIZE(LogStream) + + private: + struct Chunk { + // len will be <= buf.len + size_t len; + uv_buf_t buf; + }; + size_t total_ = 0; + bool fin_seen_ = false; + bool ended_ = false; + bool reading_ = false; + std::deque buffer_; + + static constexpr size_t kMaxLogStreamBuffer = 1024 * 10; + + // The LogStream buffer enforces a maximum size of kMaxLogStreamBuffer. + void ensure_space(size_t amt); +}; + +} // namespace quic +} // namespace node + +#endif // HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC +#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS From 7b0fc290b46fb233183b2f01be78553d68ae3b73 Mon Sep 17 00:00:00 2001 From: James M Snell Date: Wed, 29 Mar 2023 22:04:01 -0700 Subject: [PATCH 3/4] quic: add TransportParams --- node.gyp | 2 + src/quic/bindingdata.h | 22 +++- src/quic/defs.h | 55 +++++++++ src/quic/transportparams.cc | 219 ++++++++++++++++++++++++++++++++++++ src/quic/transportparams.h | 162 ++++++++++++++++++++++++++ 5 files changed, 459 insertions(+), 1 deletion(-) create mode 100644 src/quic/defs.h create mode 100644 src/quic/transportparams.cc create mode 100644 src/quic/transportparams.h diff --git a/node.gyp b/node.gyp index d032b91c058e32..c17b4d560af33a 100644 --- a/node.gyp +++ b/node.gyp @@ -342,6 +342,7 @@ 'src/quic/preferredaddress.cc', 'src/quic/sessionticket.cc', 'src/quic/tokens.cc', + 'src/quic/transportparams.cc', 'src/quic/bindingdata.h', 'src/quic/cid.h', 'src/quic/data.h', @@ -349,6 +350,7 @@ 'src/quic/preferredaddress.h', 'src/quic/sessionticket.h', 'src/quic/tokens.h', + 'src/quic/transportparams.h', ], 'node_mksnapshot_exec': '<(PRODUCT_DIR)/<(EXECUTABLE_PREFIX)node_mksnapshot<(EXECUTABLE_SUFFIX)', 'conditions': [ diff --git a/src/quic/bindingdata.h b/src/quic/bindingdata.h index 689f4319ca667a..d22699ca4f3d63 100644 --- a/src/quic/bindingdata.h +++ b/src/quic/bindingdata.h @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -17,6 +18,13 @@ namespace quic { class Endpoint; +enum class Side { + CLIENT = NGTCP2_CRYPTO_SIDE_CLIENT, + SERVER = NGTCP2_CRYPTO_SIDE_SERVER, +}; + +constexpr size_t kDefaultMaxPacketLength = NGTCP2_MAX_UDP_PAYLOAD_SIZE; + // ============================================================================ // The FunctionTemplates the BindingData will store for us. @@ -54,10 +62,22 @@ class Endpoint; // The various JS strings the implementation uses. #define QUIC_STRINGS(V) \ - V(http3_alpn, &NGHTTP3_ALPN_H3[1]) \ + V(ack_delay_exponent, "ackDelayExponent") \ + V(active_connection_id_limit, "activeConnectionIDLimit") \ + V(disable_active_migration, "disableActiveMigration") \ V(endpoint, "Endpoint") \ V(endpoint_udp, "Endpoint::UDP") \ + V(http3_alpn, &NGHTTP3_ALPN_H3[1]) \ + V(initial_max_data, "initialMaxData") \ + V(initial_max_stream_data_bidi_local, "initialMaxStreamDataBidiLocal") \ + V(initial_max_stream_data_bidi_remote, "initialMaxStreamDataBidiRemote") \ + V(initial_max_stream_data_uni, "initialMaxStreamDataUni") \ + V(initial_max_streams_bidi, "initialMaxStreamsBidi") \ + V(initial_max_streams_uni, "initialMaxStreamsUni") \ V(logstream, "LogStream") \ + V(max_ack_delay, "maxAckDelay") \ + V(max_datagram_frame_size, "maxDatagramFrameSize") \ + V(max_idle_timeout, "maxIdleTimeout") \ V(packetwrap, "PacketWrap") \ V(session, "Session") \ V(stream, "Stream") diff --git a/src/quic/defs.h b/src/quic/defs.h new file mode 100644 index 00000000000000..48666953cd43a2 --- /dev/null +++ b/src/quic/defs.h @@ -0,0 +1,55 @@ +#pragma once + +#include +#include +#include + +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()) { + CHECK(value->IsBoolean()); + options->*member = value->IsTrue(); + } + return true; +} + +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()) { + CHECK_IMPLIES(!value->IsBigInt(), value->IsNumber()); + + uint64_t val = 0; + if (value->IsBigInt()) { + bool lossless = true; + val = value.As()->Uint64Value(&lossless); + if (!lossless) { + Utf8Value label(env->isolate(), name); + THROW_ERR_OUT_OF_RANGE( + env, + (std::string("options.") + (*label) + " is out of range").c_str()); + return false; + } + } else { + val = static_cast(value.As()->Value()); + } + options->*member = val; + } + return true; +} + +} // namespace quic +} // namespace node diff --git a/src/quic/transportparams.cc b/src/quic/transportparams.cc new file mode 100644 index 00000000000000..011dbeeea86239 --- /dev/null +++ b/src/quic/transportparams.cc @@ -0,0 +1,219 @@ +#if HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC + +#include "transportparams.h" +#include +#include +#include +#include +#include +#include "bindingdata.h" +#include "defs.h" +#include "tokens.h" + +namespace node { + +using v8::ArrayBuffer; +using v8::Just; +using v8::Local; +using v8::Maybe; +using v8::Nothing; +using v8::Object; +using v8::Value; + +namespace quic { +TransportParams::Config::Config(Side side, + const CID& ocid, + const CID& retry_scid) + : side(side), ocid(ocid), retry_scid(retry_scid) {} + +Maybe TransportParams::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(name) \ + SetOption( \ + env, &options, params, state.name##_string()) + + if (!SET(initial_max_stream_data_bidi_local) || + !SET(initial_max_stream_data_bidi_remote) || + !SET(initial_max_stream_data_uni) || !SET(initial_max_data) || + !SET(initial_max_streams_bidi) || !SET(initial_max_streams_uni) || + !SET(max_idle_timeout) || !SET(active_connection_id_limit) || + !SET(ack_delay_exponent) || !SET(max_ack_delay) || + !SET(max_datagram_frame_size) || !SET(disable_active_migration)) { + return Nothing(); + } + +#undef SET + + return Just(options); +} + +TransportParams::TransportParams(Type type) : type_(type), ptr_(¶ms_) {} + +TransportParams::TransportParams(Type type, const ngtcp2_transport_params* ptr) + : type_(type), ptr_(ptr) {} + +TransportParams::TransportParams(const Config& config, const Options& options) + : TransportParams(Type::ENCRYPTED_EXTENSIONS) { + ngtcp2_transport_params_default(¶ms_); + params_.active_connection_id_limit = options.active_connection_id_limit; + params_.initial_max_stream_data_bidi_local = + options.initial_max_stream_data_bidi_local; + params_.initial_max_stream_data_bidi_remote = + options.initial_max_stream_data_bidi_remote; + params_.initial_max_stream_data_uni = options.initial_max_stream_data_uni; + params_.initial_max_streams_bidi = options.initial_max_streams_bidi; + params_.initial_max_streams_uni = options.initial_max_streams_uni; + params_.initial_max_data = options.initial_max_data; + params_.max_idle_timeout = options.max_idle_timeout * NGTCP2_SECONDS; + params_.max_ack_delay = options.max_ack_delay; + params_.ack_delay_exponent = options.ack_delay_exponent; + params_.max_datagram_frame_size = options.max_datagram_frame_size; + params_.disable_active_migration = options.disable_active_migration ? 1 : 0; + params_.preferred_address_present = 0; + params_.stateless_reset_token_present = 0; + params_.retry_scid_present = 0; + + if (config.side == Side::SERVER) { + // For the server side, the original dcid is always set. + CHECK(config.ocid); + params_.original_dcid = config.ocid; + + // The retry_scid is only set if the server validated a retry token. + if (config.retry_scid) { + params_.retry_scid = config.retry_scid; + params_.retry_scid_present = 1; + } + } + + if (options.preferred_address_ipv4.has_value()) + SetPreferredAddress(options.preferred_address_ipv4.value()); + + if (options.preferred_address_ipv6.has_value()) + SetPreferredAddress(options.preferred_address_ipv6.value()); +} + +TransportParams::TransportParams(Type type, const ngtcp2_vec& vec) + : TransportParams(type) { + int ret = ngtcp2_decode_transport_params( + ¶ms_, + static_cast(type), + vec.base, + vec.len); + + if (ret != 0) { + ptr_ = nullptr; + error_ = QuicError::ForNgtcp2Error(ret); + } +} + +Store TransportParams::Encode(Environment* env) { + if (ptr_ == nullptr) { + error_ = QuicError::ForNgtcp2Error(NGTCP2_INTERNAL_ERROR); + return Store(); + } + + // Preflight to see how much storage we'll need. + ssize_t size = ngtcp2_encode_transport_params( + nullptr, 0, static_cast(type_), ¶ms_); + + DCHECK_GT(size, 0); + + auto result = ArrayBuffer::NewBackingStore(env->isolate(), size); + + auto ret = ngtcp2_encode_transport_params( + static_cast(result->Data()), + size, + static_cast(type_), + ¶ms_); + + if (ret != 0) { + error_ = QuicError::ForNgtcp2Error(ret); + return Store(); + } + + return Store(std::move(result), static_cast(size)); +} + +void TransportParams::SetPreferredAddress(const SocketAddress& address) { + DCHECK(ptr_ == ¶ms_); + params_.preferred_address_present = 1; + switch (address.family()) { + case AF_INET: { + const sockaddr_in* src = + reinterpret_cast(address.data()); + memcpy(params_.preferred_address.ipv4_addr, + &src->sin_addr, + sizeof(params_.preferred_address.ipv4_addr)); + params_.preferred_address.ipv4_port = address.port(); + return; + } + case AF_INET6: { + const sockaddr_in6* src = + reinterpret_cast(address.data()); + memcpy(params_.preferred_address.ipv6_addr, + &src->sin6_addr, + sizeof(params_.preferred_address.ipv6_addr)); + params_.preferred_address.ipv6_port = address.port(); + return; + } + } + UNREACHABLE(); +} + +void TransportParams::GenerateStatelessResetToken( + const TokenSecret& token_secret, const CID& cid) { + DCHECK(ptr_ == ¶ms_); + DCHECK(cid); + params_.stateless_reset_token_present = 1; + + StatelessResetToken token(params_.stateless_reset_token, token_secret, cid); +} + +CID TransportParams::GeneratePreferredAddressToken(const Session& session) { + DCHECK_NOT_NULL(session); + DCHECK(ptr_ == ¶ms_); + DCHECK(pscid); + // TODO(@jasnell): To be implemented when Session is implemented + // *pscid = session->cid_factory_.Generate(); + // params_.preferred_address.cid = *pscid; + // session->endpoint_->AssociateStatelessResetToken( + // session->endpoint().GenerateNewStatelessResetToken( + // params_.preferred_address.stateless_reset_token, *pscid), + // session); + return CID::kInvalid; +} + +TransportParams::Type TransportParams::type() const { + return type_; +} + +TransportParams::operator const ngtcp2_transport_params&() const { + DCHECK_NOT_NULL(ptr_); + return *ptr_; +} + +TransportParams::operator const ngtcp2_transport_params*() const { + DCHECK_NOT_NULL(ptr_); + return ptr_; +} + +TransportParams::operator bool() const { + return ptr_ != nullptr; +} + +const QuicError& TransportParams::error() const { + return error_; +} + +} // namespace quic +} // namespace node + +#endif // HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC diff --git a/src/quic/transportparams.h b/src/quic/transportparams.h new file mode 100644 index 00000000000000..bb38a95a8686cb --- /dev/null +++ b/src/quic/transportparams.h @@ -0,0 +1,162 @@ +#pragma once + +#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS +#if HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC + +#include +#include +#include +#include +#include "bindingdata.h" +#include "cid.h" +#include "data.h" +#include "tokens.h" + +namespace node { +namespace quic { + +class Endpoint; +class Session; + +// The Transport Params are the set of configuration options that are sent to +// the remote peer. They communicate the protocol options the other peer +// should use when communicating with this session. +class TransportParams final { + public: + enum class Type { + CLIENT_HELLO = NGTCP2_TRANSPORT_PARAMS_TYPE_CLIENT_HELLO, + ENCRYPTED_EXTENSIONS = NGTCP2_TRANSPORT_PARAMS_TYPE_ENCRYPTED_EXTENSIONS, + }; + + static constexpr uint64_t DEFAULT_MAX_STREAM_DATA_BIDI_LOCAL = 256 * 1024; + static constexpr uint64_t DEFAULT_MAX_STREAM_DATA_BIDI_REMOTE = 256 * 1024; + static constexpr uint64_t DEFAULT_MAX_STREAM_DATA_UNI = 256 * 1024; + static constexpr uint64_t DEFAULT_MAX_DATA = 1 * 1024 * 1024; + static constexpr uint64_t DEFAULT_MAX_IDLE_TIMEOUT = 10; // seconds + static constexpr uint64_t DEFAULT_MAX_STREAMS_BIDI = 100; + static constexpr uint64_t DEFAULT_MAX_STREAMS_UNI = 3; + static constexpr uint64_t DEFAULT_ACTIVE_CONNECTION_ID_LIMIT = 2; + + struct Config { + Side side; + const CID& ocid; + const CID& retry_scid; + Config(Side side, + const CID& ocid = CID::kInvalid, + const CID& retry_scid = CID::kInvalid); + }; + + struct Options { + // Set only on server Sessions, the preferred address communicates the IP + // address and port that the server would prefer the client to use when + // communicating with it. See the QUIC specification for more detail on how + // the preferred address mechanism works. + std::optional preferred_address_ipv4 = std::nullopt; + std::optional preferred_address_ipv6 = std::nullopt; + + // The initial size of the flow control window of locally initiated streams. + // This is the maximum number of bytes that the *remote* endpoint can send + // when the connection is started. + uint64_t initial_max_stream_data_bidi_local = + DEFAULT_MAX_STREAM_DATA_BIDI_LOCAL; + + // The initial size of the flow control window of remotely initiated + // streams. This is the maximum number of bytes that the remote endpoint can + // send when the connection is started. + uint64_t initial_max_stream_data_bidi_remote = + DEFAULT_MAX_STREAM_DATA_BIDI_REMOTE; + + // The initial size of the flow control window of remotely initiated + // unidirectional streams. This is the maximum number of bytes that the + // remote endpoint can send when the connection is started. + uint64_t initial_max_stream_data_uni = DEFAULT_MAX_STREAM_DATA_UNI; + + // The initial size of the session-level flow control window. + uint64_t initial_max_data = DEFAULT_MAX_DATA; + + // The initial maximum number of concurrent bidirectional streams the remote + // endpoint is permitted to open. + uint64_t initial_max_streams_bidi = DEFAULT_MAX_STREAMS_BIDI; + + // The initial maximum number of concurrent unidirectional streams the + // remote endpoint is permitted to open. + uint64_t initial_max_streams_uni = DEFAULT_MAX_STREAMS_UNI; + + // The maximum amount of time that a Session is permitted to remain idle + // before it is silently closed and state is discarded. + uint64_t max_idle_timeout = DEFAULT_MAX_IDLE_TIMEOUT; + + // The maximum number of Connection IDs that the peer can store. A single + // Session may have several connection IDs over it's lifetime. + uint64_t active_connection_id_limit = DEFAULT_ACTIVE_CONNECTION_ID_LIMIT; + + // Establishes the exponent used in ACK Delay field in the ACK frame. See + // the QUIC specification for details. This is an advanced option that + // should rarely be modified and only if there is really good reason. + uint64_t ack_delay_exponent = NGTCP2_DEFAULT_ACK_DELAY_EXPONENT; + + // The maximum amount of time by which the endpoint will delay sending + // acknowledgements. This is an advanced option that should rarely be + // modified and only if there is a really good reason. It is used to + // determine how long a Session will wait to determine that a packet has + // been lost. + uint64_t max_ack_delay = NGTCP2_DEFAULT_MAX_ACK_DELAY; + + // The maximum size of DATAGRAM frames that the endpoint will accept. + // Setting the value to 0 will disable DATAGRAM support. + uint64_t max_datagram_frame_size = kDefaultMaxPacketLength; + + // When true, communicates that the Session does not support active + // connection migration. See the QUIC specification for more details on + // connection migration. + bool disable_active_migration = false; + + static v8::Maybe From(Environment* env, + v8::Local value); + }; + + explicit TransportParams(Type type); + + // Creates an instance of TransportParams wrapping the existing const + // ngtcp2_transport_params pointer. + TransportParams(Type type, const ngtcp2_transport_params* ptr); + + TransportParams(const Config& config, const Options& options); + + // Creates an instance of TransportParams by decoding the given buffer. + // If the parameters cannot be successfully decoded, the error() + // property will be set with an appropriate QuicError and the bool() + // operator will return false. + TransportParams(Type type, const ngtcp2_vec& buf); + + void GenerateStatelessResetToken(const TokenSecret& token_secret, + const CID& cid); + CID GeneratePreferredAddressToken(const Session& session); + void SetPreferredAddress(const SocketAddress& address); + + Type type() const; + + operator const ngtcp2_transport_params&() const; + operator const ngtcp2_transport_params*() const; + + operator bool() const; + + const QuicError& error() const; + + // Returns an ArrayBuffer containing the encoded transport parameters. + // If an error occurs during encoding, an empty shared_ptr will be returned + // and the error() property will be set to an appropriate QuicError. + Store Encode(Environment* env); + + private: + Type type_; + ngtcp2_transport_params params_{}; + const ngtcp2_transport_params* ptr_; + QuicError error_ = QuicError::TRANSPORT_NO_ERROR; +}; + +} // namespace quic +} // namespace node + +#endif // HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC +#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS From 489177545b9f3237d2779c5c976535dee36ea60d Mon Sep 17 00:00:00 2001 From: James M Snell Date: Fri, 31 Mar 2023 11:40:31 -0700 Subject: [PATCH 4/4] quic: address review comments --- src/quic/defs.h | 3 +-- src/quic/logstream.cc | 2 +- src/quic/logstream.h | 3 +++ src/quic/transportparams.cc | 3 +-- src/quic/transportparams.h | 4 ++-- test/sequential/test-async-wrap-getasyncid.js | 1 + 6 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/quic/defs.h b/src/quic/defs.h index 48666953cd43a2..3dbdd7ee25eba8 100644 --- a/src/quic/defs.h +++ b/src/quic/defs.h @@ -39,8 +39,7 @@ bool SetOption(Environment* env, if (!lossless) { Utf8Value label(env->isolate(), name); THROW_ERR_OUT_OF_RANGE( - env, - (std::string("options.") + (*label) + " is out of range").c_str()); + env, ("options." + label.ToString() + " is out of range").c_str()); return false; } } else { diff --git a/src/quic/logstream.cc b/src/quic/logstream.cc index 5f9517f2edb261..cf8fd5fef347a5 100644 --- a/src/quic/logstream.cc +++ b/src/quic/logstream.cc @@ -84,7 +84,7 @@ void LogStream::Emit(const uint8_t* data, size_t len, EmitOption option) { } void LogStream::Emit(const std::string_view line, EmitOption option) { - Emit(reinterpret_cast(line.begin()), line.length(), option); + Emit(reinterpret_cast(line.data()), line.length(), option); } void LogStream::End() { diff --git a/src/quic/logstream.h b/src/quic/logstream.h index da0855898c6b6f..b9d3f8df974477 100644 --- a/src/quic/logstream.h +++ b/src/quic/logstream.h @@ -67,6 +67,9 @@ class LogStream : public AsyncWrap, public StreamBase { bool reading_ = false; std::deque buffer_; + // The value here is fairly arbitrary. Once we get everything + // fully implemented and start working with this, we might + // tune this number further. static constexpr size_t kMaxLogStreamBuffer = 1024 * 10; // The LogStream buffer enforces a maximum size of kMaxLogStreamBuffer. diff --git a/src/quic/transportparams.cc b/src/quic/transportparams.cc index 011dbeeea86239..52f73e5125605e 100644 --- a/src/quic/transportparams.cc +++ b/src/quic/transportparams.cc @@ -178,9 +178,8 @@ void TransportParams::GenerateStatelessResetToken( } CID TransportParams::GeneratePreferredAddressToken(const Session& session) { - DCHECK_NOT_NULL(session); DCHECK(ptr_ == ¶ms_); - DCHECK(pscid); + // DCHECK(pscid); // TODO(@jasnell): To be implemented when Session is implemented // *pscid = session->cid_factory_.Generate(); // params_.preferred_address.cid = *pscid; diff --git a/src/quic/transportparams.h b/src/quic/transportparams.h index bb38a95a8686cb..7808b1b6c189d2 100644 --- a/src/quic/transportparams.h +++ b/src/quic/transportparams.h @@ -51,8 +51,8 @@ class TransportParams final { // address and port that the server would prefer the client to use when // communicating with it. See the QUIC specification for more detail on how // the preferred address mechanism works. - std::optional preferred_address_ipv4 = std::nullopt; - std::optional preferred_address_ipv6 = std::nullopt; + std::optional preferred_address_ipv4{}; + std::optional preferred_address_ipv6{}; // The initial size of the flow control window of locally initiated streams. // This is the maximum number of bytes that the *remote* endpoint can send diff --git a/test/sequential/test-async-wrap-getasyncid.js b/test/sequential/test-async-wrap-getasyncid.js index dcd33a9f2785db..097529f8caeae7 100644 --- a/test/sequential/test-async-wrap-getasyncid.js +++ b/test/sequential/test-async-wrap-getasyncid.js @@ -66,6 +66,7 @@ const { getSystemErrorName } = require('util'); delete providers.BLOBREADER; delete providers.RANDOMPRIMEREQUEST; delete providers.CHECKPRIMEREQUEST; + delete providers.QUIC_LOGSTREAM; const objKeys = Object.keys(providers); if (objKeys.length > 0)