diff --git a/node.gyp b/node.gyp index 811d15b0df9..2e591eea59d 100644 --- a/node.gyp +++ b/node.gyp @@ -377,6 +377,7 @@ 'src/quic/tlscontext.h', 'src/quic/tokens.h', 'src/quic/transportparams.h', + 'src/quic/quic.cc', ], 'node_cctest_sources': [ 'src/node_snapshot_stub.cc', diff --git a/src/node_binding.cc b/src/node_binding.cc index 2b69a828a74..8013d9a0bbf 100644 --- a/src/node_binding.cc +++ b/src/node_binding.cc @@ -89,7 +89,8 @@ NODE_BUILTIN_STANDARD_BINDINGS(V) \ NODE_BUILTIN_OPENSSL_BINDINGS(V) \ NODE_BUILTIN_ICU_BINDINGS(V) \ - NODE_BUILTIN_PROFILER_BINDINGS(V) + NODE_BUILTIN_PROFILER_BINDINGS(V) \ + NODE_BUILTIN_QUIC_BINDINGS(V) // This is used to load built-in bindings. Instead of using // __attribute__((constructor)), we call the _register_ diff --git a/src/node_binding.h b/src/node_binding.h index dfcfe5fe9e1..094d14cb2ee 100644 --- a/src/node_binding.h +++ b/src/node_binding.h @@ -30,6 +30,12 @@ static_assert(static_cast(NM_F_LINKED) == #define NODE_BUILTIN_ICU_BINDINGS(V) #endif +#if HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC +#define NODE_BUILTIN_QUIC_BINDINGS(V) V(quic) +#else +#define NODE_BUILTIN_QUIC_BINDINGS(V) +#endif + #define NODE_BINDINGS_WITH_PER_ISOLATE_INIT(V) \ V(async_wrap) \ V(blob) \ @@ -47,7 +53,8 @@ static_assert(static_cast(NM_F_LINKED) == V(timers) \ V(url) \ V(worker) \ - NODE_BUILTIN_ICU_BINDINGS(V) + NODE_BUILTIN_ICU_BINDINGS(V) \ + NODE_BUILTIN_QUIC_BINDINGS(V) #define NODE_BINDING_CONTEXT_AWARE_CPP(modname, regfunc, priv, flags) \ static node::node_module _module = { \ diff --git a/src/node_external_reference.h b/src/node_external_reference.h index b1b6ca31766..703e287b760 100644 --- a/src/node_external_reference.h +++ b/src/node_external_reference.h @@ -154,11 +154,18 @@ class ExternalReferenceRegistry { #define EXTERNAL_REFERENCE_BINDING_LIST_CRYPTO(V) #endif // HAVE_OPENSSL +#if HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC +#define EXTERNAL_REFERENCE_BINDING_LIST_QUIC(V) V(quic) +#else +#define EXTERNAL_REFERENCE_BINDING_LIST_QUIC(V) +#endif + #define EXTERNAL_REFERENCE_BINDING_LIST(V) \ EXTERNAL_REFERENCE_BINDING_LIST_BASE(V) \ EXTERNAL_REFERENCE_BINDING_LIST_INSPECTOR(V) \ EXTERNAL_REFERENCE_BINDING_LIST_I18N(V) \ - EXTERNAL_REFERENCE_BINDING_LIST_CRYPTO(V) + EXTERNAL_REFERENCE_BINDING_LIST_CRYPTO(V) \ + EXTERNAL_REFERENCE_BINDING_LIST_QUIC(V) } // namespace node diff --git a/src/quic/application.cc b/src/quic/application.cc index cafd6a8941c..ce630ae35e4 100644 --- a/src/quic/application.cc +++ b/src/quic/application.cc @@ -1,10 +1,10 @@ -#include "node_bob.h" -#include "uv.h" #if HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC -#include -#include #include "application.h" +#include +#include +#include +#include #include "defs.h" #include "endpoint.h" #include "packet.h" @@ -38,14 +38,23 @@ const Session::Application_Options Session::Application_Options::kDefault = {}; Maybe Session::Application_Options::From( Environment* env, Local value) { - if (value.IsEmpty() || !value->IsObject()) { + if (value.IsEmpty()) { THROW_ERR_INVALID_ARG_TYPE(env, "options must be an object"); return Nothing(); } - auto& state = BindingData::Get(env); - auto params = value.As(); Application_Options options; + auto& state = BindingData::Get(env); + if (value->IsUndefined()) { + return Just(options); + } + + if (!value->IsObject()) { + THROW_ERR_INVALID_ARG_TYPE(env, "options must be an object"); + return Nothing(); + } + + auto params = value.As(); #define SET(name) \ SetOption target) { - SetMethod(env->context(), target, "setCallbacks", SetCallbacks); - SetMethod(env->context(), target, "flushPacketFreelist", FlushPacketFreelist); - Realm::GetCurrent(env->context())->AddBindingData(target); +void BindingData::InitPerContext(Realm* realm, Local target) { + SetMethod(realm->context(), target, "setCallbacks", SetCallbacks); + SetMethod( + realm->context(), target, "flushPacketFreelist", FlushPacketFreelist); + Realm::GetCurrent(realm->context())->AddBindingData(target); } void BindingData::RegisterExternalReferences( diff --git a/src/quic/bindingdata.h b/src/quic/bindingdata.h index 5ab0cc4b3f4..015265967fd 100644 --- a/src/quic/bindingdata.h +++ b/src/quic/bindingdata.h @@ -118,11 +118,14 @@ constexpr size_t kMaxVectorCount = 16; V(address_lru_size, "addressLRUSize") \ V(alpn, "alpn") \ V(application_options, "application") \ + V(bbr, "bbr") \ + V(bbr2, "bbr2") \ V(ca, "ca") \ V(certs, "certs") \ V(cc_algorithm, "cc") \ V(crl, "crl") \ V(ciphers, "ciphers") \ + V(cubic, "cubic") \ V(disable_active_migration, "disableActiveMigration") \ V(disable_stateless_reset, "disableStatelessReset") \ V(enable_tls_trace, "tlsTrace") \ @@ -162,6 +165,7 @@ constexpr size_t kMaxVectorCount = 16; V(qpack_encoder_max_dtable_capacity, "qpackEncoderMaxDTableCapacity") \ V(qpack_max_dtable_capacity, "qpackMaxDTableCapacity") \ V(reject_unauthorized, "rejectUnauthorized") \ + V(reno, "reno") \ V(retry_token_expiration, "retryTokenExpiration") \ V(request_peer_certificate, "requestPeerCertificate") \ V(reset_token_secret, "resetTokenSecret") \ @@ -194,7 +198,7 @@ class BindingData final public mem::NgLibMemoryManager { public: SET_BINDING_ID(quic_binding_data) - static void Initialize(Environment* env, v8::Local target); + static void InitPerContext(Realm* realm, v8::Local target); static void RegisterExternalReferences(ExternalReferenceRegistry* registry); static BindingData& Get(Environment* env); diff --git a/src/quic/defs.h b/src/quic/defs.h index 65ebe812efa..fc0bc0c81a7 100644 --- a/src/quic/defs.h +++ b/src/quic/defs.h @@ -28,6 +28,17 @@ bool SetOption(Environment* env, } 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; + options->*member = value->BooleanValue(env->isolate()); + return true; +} + +template bool SetOption(Environment* env, Opt* options, const v8::Local& object, @@ -35,8 +46,20 @@ bool SetOption(Environment* env, v8::Local value; if (!object->Get(env->context(), name).ToLocal(&value)) return false; if (!value->IsUndefined()) { - CHECK(value->IsBoolean()); - options->*member = value->IsTrue(); + if (!value->IsUint32()) { + Utf8Value nameStr(env->isolate(), name); + THROW_ERR_INVALID_ARG_VALUE( + env, "The %s option must be an uint32", *nameStr); + return false; + } + v8::Local num; + if (!value->ToUint32(env->context()).ToLocal(&num)) { + Utf8Value nameStr(env->isolate(), name); + THROW_ERR_INVALID_ARG_VALUE( + env, "The %s option must be an uint32", *nameStr); + return false; + } + options->*member = num->Value(); } return true; } @@ -50,7 +73,13 @@ bool SetOption(Environment* env, if (!object->Get(env->context(), name).ToLocal(&value)) return false; if (!value->IsUndefined()) { - CHECK_IMPLIES(!value->IsBigInt(), value->IsNumber()); + if (!value->IsBigInt() && !value->IsNumber()) { + Utf8Value nameStr(env->isolate(), name); + THROW_ERR_INVALID_ARG_VALUE( + env, "option %s must be a bigint or number", *nameStr); + return false; + } + DCHECK_IMPLIES(!value->IsBigInt(), value->IsNumber()); uint64_t val = 0; if (value->IsBigInt()) { @@ -58,12 +87,17 @@ bool SetOption(Environment* env, val = value.As()->Uint64Value(&lossless); if (!lossless) { Utf8Value label(env->isolate(), name); - THROW_ERR_OUT_OF_RANGE( - env, ("options." + label.ToString() + " is out of range").c_str()); + THROW_ERR_INVALID_ARG_VALUE(env, "option %s is out of range", *label); return false; } } else { - val = static_cast(value.As()->Value()); + double dbl = value.As()->Value(); + if (dbl < 0) { + Utf8Value label(env->isolate(), name); + THROW_ERR_INVALID_ARG_VALUE(env, "option %s is out of range", *label); + return false; + } + val = static_cast(dbl); } options->*member = val; } diff --git a/src/quic/endpoint.cc b/src/quic/endpoint.cc index bc47cfb26be..c8feed459a2 100644 --- a/src/quic/endpoint.cc +++ b/src/quic/endpoint.cc @@ -7,12 +7,15 @@ #include #include #include +#include #include #include #include #include #include +#include #include "application.h" +#include "bindingdata.h" #include "defs.h" namespace node { @@ -30,8 +33,10 @@ using v8::Maybe; using v8::Nothing; using v8::Number; using v8::Object; +using v8::ObjectTemplate; using v8::PropertyAttribute; using v8::String; +using v8::Uint32; using v8::Value; namespace quic { @@ -43,14 +48,12 @@ namespace quic { V(RECEIVING, receiving, uint8_t) \ /* Listening as a QUIC server */ \ V(LISTENING, listening, uint8_t) \ - /* In the process of closing down */ \ - V(CLOSING, closing, uint8_t) \ /* In the process of closing down, waiting for pending send callbacks */ \ - V(WAITING_FOR_CALLBACKS, waiting_for_callbacks, uint8_t) \ + V(CLOSING, closing, uint8_t) \ /* Temporarily paused serving new initial requests */ \ V(BUSY, busy, uint8_t) \ /* The number of pending send callbacks */ \ - V(PENDING_CALLBACKS, pending_callbacks, size_t) + V(PENDING_CALLBACKS, pending_callbacks, uint64_t) #define ENDPOINT_STATS(V) \ V(CREATED_AT, created_at) \ @@ -67,6 +70,12 @@ namespace quic { V(STATELESS_RESET_COUNT, stateless_reset_count) \ V(IMMEDIATE_CLOSE_COUNT, immediate_close_count) +#define ENDPOINT_CC(V) \ + V(RENO, reno) \ + V(CUBIC, cubic) \ + V(BBR, bbr) \ + V(BBR2, bbr2) + struct Endpoint::State { #define V(_, name, type) type name; ENDPOINT_STATE(V) @@ -76,7 +85,7 @@ struct Endpoint::State { STAT_STRUCT(Endpoint, ENDPOINT) // ============================================================================ - +// Endpoint::Options namespace { #ifdef DEBUG bool is_diagnostic_packet_loss(double probability) { @@ -87,71 +96,107 @@ bool is_diagnostic_packet_loss(double probability) { } #endif // DEBUG +Maybe getAlgoFromString(Environment* env, Local input) { + auto& state = BindingData::Get(env); +#define V(name, str) \ + if (input->StringEquals(state.str##_string())) { \ + return Just(NGTCP2_CC_ALGO_##name); \ + } + + ENDPOINT_CC(V) + +#undef V + return Nothing(); +} + template bool SetOption(Environment* env, Opt* options, - const v8::Local& object, - const v8::Local& name) { - v8::Local value; + const Local& object, + const Local& name) { + Local value; if (!object->Get(env->context(), name).ToLocal(&value)) return false; if (!value->IsUndefined()) { - int num = value.As()->Value(); - switch (num) { - case NGTCP2_CC_ALGO_RENO: - [[fallthrough]]; - case NGTCP2_CC_ALGO_CUBIC: - [[fallthrough]]; - case NGTCP2_CC_ALGO_BBR: - [[fallthrough]]; - case NGTCP2_CC_ALGO_BBR2: - break; - default: - THROW_ERR_INVALID_ARG_VALUE(env, "The cc_algorithm is invalid"); + ngtcp2_cc_algo algo; + if (value->IsString()) { + if (!getAlgoFromString(env, value.As()).To(&algo)) { + THROW_ERR_INVALID_ARG_VALUE(env, "The cc_algorithm option is invalid"); return false; + } + } else { + if (!value->IsInt32()) { + THROW_ERR_INVALID_ARG_VALUE( + env, "The cc_algorithm option must be a string or an integer"); + return false; + } + Local num; + if (!value->ToInt32(env->context()).ToLocal(&num)) { + THROW_ERR_INVALID_ARG_VALUE(env, "The cc_algorithm option is invalid"); + return false; + } + switch (num->Value()) { +#define V(name, _) \ + case NGTCP2_CC_ALGO_##name: \ + break; + ENDPOINT_CC(V) +#undef V + default: + THROW_ERR_INVALID_ARG_VALUE(env, + "The cc_algorithm option is invalid"); + return false; + } + algo = static_cast(num->Value()); } - options->*member = static_cast(num); + options->*member = algo; } return true; } +#if DEBUG template bool SetOption(Environment* env, Opt* options, - const v8::Local& object, - const v8::Local& name) { - v8::Local value; + const Local& object, + const Local& name) { + Local value; if (!object->Get(env->context(), name).ToLocal(&value)) return false; if (!value->IsUndefined()) { - CHECK(value->IsNumber()); - options->*member = value.As()->Value(); - } - 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(value->IsNumber()); - options->*member = value.As()->Value(); + Local num; + if (!value->ToNumber(env->context()).ToLocal(&num)) { + Utf8Value nameStr(env->isolate(), name); + THROW_ERR_INVALID_ARG_VALUE( + env, "The %s option must be a number", *nameStr); + return false; + } + options->*member = num->Value(); } return true; } +#endif // DEBUG template bool SetOption(Environment* env, Opt* options, - const v8::Local& object, - const v8::Local& name) { - v8::Local value; + const Local& object, + const Local& name) { + Local value; if (!object->Get(env->context(), name).ToLocal(&value)) return false; if (!value->IsUndefined()) { - CHECK(value->IsNumber()); - options->*member = value.As()->Value(); + if (!value->IsUint32()) { + Utf8Value nameStr(env->isolate(), name); + THROW_ERR_INVALID_ARG_VALUE( + env, "The %s option must be an uint8", *nameStr); + return false; + } + Local num; + if (!value->ToUint32(env->context()).ToLocal(&num) || + num->Value() > std::numeric_limits::max()) { + Utf8Value nameStr(env->isolate(), name); + THROW_ERR_INVALID_ARG_VALUE( + env, "The %s option must be an uint8", *nameStr); + return false; + } + options->*member = num->Value(); } return true; } @@ -159,16 +204,30 @@ bool SetOption(Environment* env, template bool SetOption(Environment* env, Opt* options, - const v8::Local& object, - const v8::Local& name) { - v8::Local value; + const Local& object, + const Local& name) { + Local value; if (!object->Get(env->context(), name).ToLocal(&value)) return false; if (!value->IsUndefined()) { - CHECK(value->IsArrayBufferView()); + if (!value->IsArrayBufferView()) { + Utf8Value nameStr(env->isolate(), name); + THROW_ERR_INVALID_ARG_VALUE( + env, "The %s option must be an ArrayBufferView", *nameStr); + return false; + } Store store(value.As()); - CHECK_EQ(store.length(), TokenSecret::QUIC_TOKENSECRET_LEN); + if (store.length() != TokenSecret::QUIC_TOKENSECRET_LEN) { + Utf8Value nameStr(env->isolate(), name); + THROW_ERR_INVALID_ARG_VALUE( + env, + "The %s option must be an ArrayBufferView of length %d", + *nameStr, + TokenSecret::QUIC_TOKENSECRET_LEN); + return false; + } ngtcp2_vec buf = store; - options->*member = buf.base; + TokenSecret secret(buf.base); + options->*member = secret; } return true; } @@ -204,6 +263,26 @@ Maybe Endpoint::Options::From(Environment* env, return Nothing(); } + Local address; + if (!params->Get(env->context(), env->address_string()).ToLocal(&address)) { + return Nothing(); + } + if (!address->IsUndefined()) { + if (!SocketAddressBase::HasInstance(env, address)) { + THROW_ERR_INVALID_ARG_TYPE(env, + "The address option must be a SocketAddress"); + return Nothing(); + } + auto addr = FromJSObject(address.As()); + options.local_address = addr->address(); + } else { + options.local_address = std::make_shared(); + if (!SocketAddress::New("127.0.0.1", 0, options.local_address.get())) { + THROW_ERR_INVALID_ADDRESS(env); + return Nothing(); + } + } + return Just(options); #undef SET @@ -233,16 +312,15 @@ class Endpoint::UDP::Impl final : public HandleWrap { return tmpl; } - static BaseObjectPtr Create(Endpoint* endpoint) { + static Impl* Create(Endpoint* endpoint) { Local obj; if (!GetConstructorTemplate(endpoint->env()) ->InstanceTemplate() ->NewInstance(endpoint->env()->context()) .ToLocal(&obj)) { - return BaseObjectPtr(); + return nullptr; } - - return MakeDetachedBaseObject(endpoint, obj); + return new Impl(endpoint, obj); } static Impl* From(uv_udp_t* handle) { @@ -268,10 +346,6 @@ class Endpoint::UDP::Impl final : public HandleWrap { SET_SELF_SIZE(Impl) private: - static void ClosedCb(uv_handle_t* handle) { - std::unique_ptr ptr(From(handle)); - } - static void OnAlloc(uv_handle_t* handle, size_t suggested_size, uv_buf_t* buf) { @@ -309,7 +383,7 @@ class Endpoint::UDP::Impl final : public HandleWrap { }; Endpoint::UDP::UDP(Endpoint* endpoint) : impl_(Impl::Create(endpoint)) { - endpoint->env()->AddCleanupHook(CleanupHook, this); + DCHECK(impl_); } Endpoint::UDP::~UDP() { @@ -318,12 +392,12 @@ Endpoint::UDP::~UDP() { int Endpoint::UDP::Bind(const Endpoint::Options& options) { if (is_bound_) return UV_EALREADY; - if (is_closed() || impl_->IsHandleClosing()) return UV_EBADF; + if (is_closed_or_closing()) return UV_EBADF; int flags = 0; - if (options.local_address.family() == AF_INET6 && options.ipv6_only) + if (options.local_address->family() == AF_INET6 && options.ipv6_only) flags |= UV_UDP_IPV6ONLY; - int err = uv_udp_bind(&impl_->handle_, options.local_address.data(), flags); + int err = uv_udp_bind(&impl_->handle_, options.local_address->data(), flags); int size; if (!err) { @@ -361,7 +435,7 @@ void Endpoint::UDP::Unref() { } int Endpoint::UDP::Start() { - if (is_closed() || impl_->IsHandleClosing()) return UV_EBADF; + if (is_closed_or_closing()) return UV_EBADF; if (is_started_) return 0; int err = uv_udp_recv_start(&impl_->handle_, Impl::OnAlloc, Impl::OnReceive); is_started_ = (err == 0); @@ -369,16 +443,17 @@ int Endpoint::UDP::Start() { } void Endpoint::UDP::Stop() { - if (is_closed() || impl_->IsHandleClosing() || !is_started_) return; + if (is_closed_or_closing() || !is_started_) return; USE(uv_udp_recv_stop(&impl_->handle_)); is_started_ = false; } void Endpoint::UDP::Close() { - if (is_closed() || impl_->IsHandleClosing()) return; + if (is_closed_or_closing()) return; + DCHECK(impl_); Stop(); is_bound_ = false; - impl_->env()->RemoveCleanupHook(CleanupHook, this); + is_closed_ = true; impl_->Close(); impl_.reset(); } @@ -388,20 +463,26 @@ bool Endpoint::UDP::is_bound() const { } bool Endpoint::UDP::is_closed() const { - return !impl_; + return is_closed_; } + +bool Endpoint::UDP::is_closed_or_closing() const { + if (is_closed() || !impl_) return true; + return impl_->IsHandleClosing(); +} + Endpoint::UDP::operator bool() const { - return !impl_; + return impl_; } SocketAddress Endpoint::UDP::local_address() const { - CHECK(!is_closed() && is_bound()); + DCHECK(!is_closed() && is_bound()); return SocketAddress::FromSockName(impl_->handle_); } int Endpoint::UDP::Send(BaseObjectPtr packet) { - if (is_closed() || impl_->IsHandleClosing()) return UV_EBADF; - CHECK(packet && !packet->is_sending()); + if (is_closed_or_closing()) return UV_EBADF; + DCHECK(packet && !packet->is_sending()); uv_buf_t buf = *packet; return packet->Dispatch( uv_udp_send, @@ -419,10 +500,6 @@ void Endpoint::UDP::MemoryInfo(MemoryTracker* tracker) const { if (impl_) tracker->TrackField("impl", impl_); } -void Endpoint::UDP::CleanupHook(void* data) { - static_cast(data)->Close(); -} - // ============================================================================ bool Endpoint::HasInstance(Environment* env, Local value) { @@ -434,7 +511,7 @@ Local Endpoint::GetConstructorTemplate(Environment* env) { auto tmpl = state.endpoint_constructor_template(); if (tmpl.IsEmpty()) { auto isolate = env->isolate(); - tmpl = NewFunctionTemplate(isolate, IllegalConstructor); + tmpl = NewFunctionTemplate(isolate, New); tmpl->Inherit(AsyncWrap::GetConstructorTemplate(env)); tmpl->SetClassName(state.endpoint_string()); tmpl->InstanceTemplate()->SetInternalFieldCount( @@ -444,54 +521,70 @@ Local Endpoint::GetConstructorTemplate(Environment* env) { SetProtoMethod(isolate, tmpl, "connect", DoConnect); SetProtoMethod(isolate, tmpl, "markBusy", MarkBusy); SetProtoMethod(isolate, tmpl, "ref", Ref); - SetProtoMethod(isolate, tmpl, "unref", Unref); SetProtoMethodNoSideEffect(isolate, tmpl, "address", LocalAddress); state.set_endpoint_constructor_template(tmpl); } return tmpl; } -void Endpoint::Initialize(Environment* env, Local target) { - SetMethod(env->context(), target, "createEndpoint", CreateEndpoint); +void Endpoint::InitPerIsolate(IsolateData* data, Local target) { + // TODO(@jasnell): Implement the per-isolate state +} + +void Endpoint::InitPerContext(Realm* realm, Local target) { +#define V(name, str) \ + NODE_DEFINE_CONSTANT(target, QUIC_CC_ALGO_##name); \ + NODE_DEFINE_STRING_CONSTANT(target, "QUIC_CC_ALGO_" #name "_STR", #str); + ENDPOINT_CC(V) +#undef V #define V(name, _) IDX_STATS_ENDPOINT_##name, - enum EndpointStatsIdx { ENDPOINT_STATS(V) IDX_STATS_ENDPOINT_COUNT }; + enum IDX_STATS_ENDPONT { ENDPOINT_STATS(V) IDX_STATS_ENDPOINT_COUNT }; + NODE_DEFINE_CONSTANT(target, IDX_STATS_ENDPOINT_COUNT); #undef V -#define V(name, key, __) \ - auto IDX_STATE_ENDPOINT_##name = offsetof(Endpoint::State, key); +#define V(name, key) NODE_DEFINE_CONSTANT(target, IDX_STATS_ENDPOINT_##name); + ENDPOINT_STATS(V); +#undef V + +#define V(name, key, type) \ + static constexpr auto IDX_STATE_ENDPOINT_##name = \ + offsetof(Endpoint::State, key); \ + static constexpr auto IDX_STATE_ENDPOINT_##name##_SIZE = sizeof(type); \ + NODE_DEFINE_CONSTANT(target, IDX_STATE_ENDPOINT_##name); \ + NODE_DEFINE_CONSTANT(target, IDX_STATE_ENDPOINT_##name##_SIZE); ENDPOINT_STATE(V) #undef V -#define V(name, _) NODE_DEFINE_CONSTANT(target, IDX_STATS_ENDPOINT_##name); - ENDPOINT_STATS(V) -#undef V -#define V(name, _, __) NODE_DEFINE_CONSTANT(target, IDX_STATE_ENDPOINT_##name); - ENDPOINT_STATE(V) -#undef V + NODE_DEFINE_CONSTANT(target, DEFAULT_MAX_CONNECTIONS); + NODE_DEFINE_CONSTANT(target, DEFAULT_MAX_CONNECTIONS_PER_HOST); + NODE_DEFINE_CONSTANT(target, DEFAULT_MAX_SOCKETADDRESS_LRU_SIZE); + NODE_DEFINE_CONSTANT(target, DEFAULT_MAX_STATELESS_RESETS); + NODE_DEFINE_CONSTANT(target, DEFAULT_MAX_RETRY_LIMIT); + + static constexpr auto DEFAULT_RETRYTOKEN_EXPIRATION = + RetryToken::QUIC_DEFAULT_RETRYTOKEN_EXPIRATION / NGTCP2_SECONDS; + static constexpr auto DEFAULT_REGULARTOKEN_EXPIRATION = + RegularToken::QUIC_DEFAULT_REGULARTOKEN_EXPIRATION / NGTCP2_SECONDS; + static constexpr auto DEFAULT_MAX_PACKET_LENGTH = kDefaultMaxPacketLength; + NODE_DEFINE_CONSTANT(target, DEFAULT_RETRYTOKEN_EXPIRATION); + NODE_DEFINE_CONSTANT(target, DEFAULT_REGULARTOKEN_EXPIRATION); + NODE_DEFINE_CONSTANT(target, DEFAULT_MAX_PACKET_LENGTH); + + SetConstructorFunction(realm->context(), + target, + "Endpoint", + GetConstructorTemplate(realm->env())); } void Endpoint::RegisterExternalReferences(ExternalReferenceRegistry* registry) { - registry->Register(CreateEndpoint); + registry->Register(New); registry->Register(DoConnect); registry->Register(DoListen); registry->Register(DoCloseGracefully); registry->Register(LocalAddress); registry->Register(Ref); - registry->Register(Unref); -} - -BaseObjectPtr Endpoint::Create(Environment* env, - const Endpoint::Options& options) { - Local obj; - if (!GetConstructorTemplate(env) - ->InstanceTemplate() - ->NewInstance(env->context()) - .ToLocal(&obj)) { - return BaseObjectPtr(); - } - - return MakeDetachedBaseObject(env, obj, options); + registry->Register(MarkBusy); } Endpoint::Endpoint(Environment* env, @@ -500,7 +593,7 @@ Endpoint::Endpoint(Environment* env, : AsyncWrap(env, object, AsyncWrap::PROVIDER_QUIC_ENDPOINT), stats_(env->isolate()), state_(env->isolate()), - options_(std::move(options)), + options_(options), udp_(this), addrLRU_(options_.address_lru_size) { MakeWeak(); @@ -516,15 +609,8 @@ Endpoint::Endpoint(Environment* env, defineProperty(env->stats_string(), stats_.GetArrayBuffer()); } -Endpoint::~Endpoint() { - udp_.Close(); - DCHECK_EQ(state_->pending_callbacks, 0); - DCHECK(sessions_.empty()); - DCHECK(is_closed()); -} - SocketAddress Endpoint::local_address() const { - CHECK(!is_closed()); + DCHECK(!is_closed() && !is_closing()); return udp_.local_address(); } @@ -562,7 +648,7 @@ void Endpoint::RemoveSession(const CID& cid) { if (!session) return; DecrementSocketAddressCounter(session->remote_address()); sessions_.erase(cid); - if (state_->waiting_for_callbacks == 1) MaybeDestroy(); + if (state_->closing == 1) MaybeDestroy(); } BaseObjectPtr Endpoint::FindSession(const CID& cid) { @@ -764,18 +850,17 @@ BaseObjectPtr Endpoint::Connect( } void Endpoint::MaybeDestroy() { - if (!is_closing() && sessions_.empty() && state_->pending_callbacks == 0 && + if (!is_closed() && sessions_.empty() && state_->pending_callbacks == 0 && state_->listening == 0) { Destroy(); } } void Endpoint::Destroy(CloseContext context, int status) { - if (is_closed() || is_closing()) return; + if (is_closed()) return; STAT_RECORD_TIMESTAMP(Stats, destroyed_at); - state_->closing = 1; state_->listening = 0; close_context_ = context; @@ -790,7 +875,7 @@ void Endpoint::Destroy(CloseContext context, int status) { for (auto& session : sessions) session.second->Close(Session::CloseMethod::SILENT); sessions.clear(); - CHECK(sessions_.empty()); + DCHECK(sessions_.empty()); token_map_.clear(); dcid_to_scid_.clear(); @@ -804,10 +889,10 @@ void Endpoint::Destroy(CloseContext context, int status) { } void Endpoint::CloseGracefully() { - if (!is_closed() && !is_closing() && state_->waiting_for_callbacks == 0) { - state_->listening = 0; - state_->waiting_for_callbacks = 1; - } + if (is_closed() || is_closing()) return; + + state_->listening = 0; + state_->closing = 1; // Maybe we can go ahead and destroy now? MaybeDestroy(); @@ -1188,7 +1273,7 @@ void Endpoint::PacketDone(int status) { if (is_closed()) return; state_->pending_callbacks--; // Can we go ahead and close now? - if (state_->waiting_for_callbacks == 1) { + if (state_->closing == 1) { // MaybeDestroy potentially creates v8 handles so let's make sure // we have a HandleScope on the stack. HandleScope scope(env()->isolate()); @@ -1267,18 +1352,17 @@ void Endpoint::EmitClose(CloseContext context, int status) { // ====================================================================================== // Endpoint JavaScript API -void Endpoint::CreateEndpoint(const FunctionCallbackInfo& args) { - CHECK(!args.IsConstructCall()); +void Endpoint::New(const FunctionCallbackInfo& args) { + DCHECK(args.IsConstructCall()); auto env = Environment::GetCurrent(args); - CHECK(args[0]->IsObject()); Options options; + // Options::From will validate that args[0] is the correct type. if (!Options::From(env, args[0]).To(&options)) { // There was an error. Just exit to propagate. return; } - auto endpoint = Endpoint::Create(env, options); - if (endpoint) args.GetReturnValue().Set(endpoint->object()); + new Endpoint(env, args.This(), options); } void Endpoint::DoConnect(const FunctionCallbackInfo& args) { @@ -1342,23 +1426,21 @@ void Endpoint::LocalAddress(const FunctionCallbackInfo& args) { auto env = Environment::GetCurrent(args); Endpoint* endpoint; ASSIGN_OR_RETURN_UNWRAP(&endpoint, args.Holder()); - if (endpoint->is_closed()) return; - auto local_address = endpoint->local_address(); + if (endpoint->is_closed() || !endpoint->udp_.is_bound()) return; auto addr = SocketAddressBase::Create( - env, std::make_shared(local_address)); + env, std::make_shared(endpoint->local_address())); if (addr) args.GetReturnValue().Set(addr->object()); } void Endpoint::Ref(const FunctionCallbackInfo& args) { Endpoint* endpoint; ASSIGN_OR_RETURN_UNWRAP(&endpoint, args.Holder()); - endpoint->udp_.Ref(); -} - -void Endpoint::Unref(const FunctionCallbackInfo& args) { - Endpoint* endpoint; - ASSIGN_OR_RETURN_UNWRAP(&endpoint, args.Holder()); - endpoint->udp_.Unref(); + auto env = Environment::GetCurrent(args); + if (args[0]->BooleanValue(env->isolate())) { + endpoint->udp_.Ref(); + } else { + endpoint->udp_.Unref(); + } } } // namespace quic diff --git a/src/quic/endpoint.h b/src/quic/endpoint.h index 700630c2244..e1503030fd5 100644 --- a/src/quic/endpoint.h +++ b/src/quic/endpoint.h @@ -12,7 +12,6 @@ #include #include #include "bindingdata.h" -#include "defs.h" #include "packet.h" #include "session.h" #include "sessionticket.h" @@ -26,20 +25,25 @@ namespace quic { // client and server simultaneously. class Endpoint final : public AsyncWrap, public Packet::Listener { public: - static constexpr size_t DEFAULT_MAX_CONNECTIONS = - std::min(kMaxSizeT, static_cast(kMaxSafeJsInteger)); - static constexpr size_t DEFAULT_MAX_CONNECTIONS_PER_HOST = 100; - static constexpr size_t DEFAULT_MAX_SOCKETADDRESS_LRU_SIZE = + static constexpr uint64_t DEFAULT_MAX_CONNECTIONS = + std::min(kMaxSizeT, static_cast(kMaxSafeJsInteger)); + static constexpr uint64_t DEFAULT_MAX_CONNECTIONS_PER_HOST = 100; + static constexpr uint64_t DEFAULT_MAX_SOCKETADDRESS_LRU_SIZE = (DEFAULT_MAX_CONNECTIONS_PER_HOST * 10); - static constexpr size_t DEFAULT_MAX_STATELESS_RESETS = 10; - static constexpr size_t DEFAULT_MAX_RETRY_LIMIT = 10; + static constexpr uint64_t DEFAULT_MAX_STATELESS_RESETS = 10; + static constexpr uint64_t DEFAULT_MAX_RETRY_LIMIT = 10; + + static constexpr auto QUIC_CC_ALGO_RENO = NGTCP2_CC_ALGO_RENO; + static constexpr auto QUIC_CC_ALGO_CUBIC = NGTCP2_CC_ALGO_CUBIC; + static constexpr auto QUIC_CC_ALGO_BBR = NGTCP2_CC_ALGO_BBR; + static constexpr auto QUIC_CC_ALGO_BBR2 = NGTCP2_CC_ALGO_BBR2; // Endpoint configuration options struct Options final : public MemoryRetainer { // The local socket address to which the UDP port will be bound. The port // may be 0 to have Node.js select an available port. IPv6 or IPv4 addresses // may be used. When using IPv6, dual mode will be supported by default. - SocketAddress local_address; + std::shared_ptr local_address; // Retry tokens issued by the Endpoint are time-limited. By default, retry // tokens expire after DEFAULT_RETRYTOKEN_EXPIRATION *seconds*. This is an @@ -134,14 +138,15 @@ class Endpoint final : public AsyncWrap, public Packet::Listener { // is the better of the two for our needs. ngtcp2_cc_algo cc_algorithm = NGTCP2_CC_ALGO_CUBIC; - // By default, when Node.js starts, it will generate a reset_token_secret at - // random. This is a secret used in generating stateless reset tokens. In - // order for stateless reset to be effective, however, it is necessary to - // use a deterministic secret that persists across ngtcp2 endpoints and - // sessions. + // By default, when the endpoint is created, it will generate a + // reset_token_secret at random. This is a secret used in generating + // stateless reset tokens. In order for stateless reset to be effective, + // however, it is necessary to use a deterministic secret that persists + // across ngtcp2 endpoints and sessions. This means that the endpoint + // configuration really should have a reset token secret passed in. TokenSecret reset_token_secret; - // The secret used for generating new tokens. + // The secret used for generating new regular tokens. TokenSecret token_secret; // When the local_address specifies an IPv6 local address to bind to, the @@ -169,16 +174,14 @@ class Endpoint final : public AsyncWrap, public Packet::Listener { bool HasInstance(Environment* env, v8::Local value); static v8::Local GetConstructorTemplate( Environment* env); - static void Initialize(Environment* env, v8::Local target); + static void InitPerIsolate(IsolateData* data, + v8::Local target); + static void InitPerContext(Realm* realm, v8::Local target); static void RegisterExternalReferences(ExternalReferenceRegistry* registry); - static BaseObjectPtr Create(Environment* env, - const Endpoint::Options& config); - Endpoint(Environment* env, v8::Local object, const Endpoint::Options& options); - ~Endpoint() override; inline const Options& options() const { return options_; @@ -289,6 +292,7 @@ class Endpoint final : public AsyncWrap, public Packet::Listener { bool is_bound() const; bool is_closed() const; + bool is_closed_or_closing() const; operator bool() const; void Ref(); @@ -301,11 +305,10 @@ class Endpoint final : public AsyncWrap, public Packet::Listener { private: class Impl; - static void CleanupHook(void* data); - - BaseObjectPtr impl_; + BaseObjectWeakPtr impl_; bool is_bound_ = false; bool is_started_ = false; + bool is_closed_ = false; }; bool is_closed() const; @@ -349,10 +352,9 @@ class Endpoint final : public AsyncWrap, public Packet::Listener { // JavaScript API - // Create a new Endpoint instance. `createEndpoint()` is exposed as a method - // on the internalBinding('quic') object. + // Create a new Endpoint. // @param Endpoint::Options options - Options to configure the Endpoint. - static void CreateEndpoint(const v8::FunctionCallbackInfo& args); + static void New(const v8::FunctionCallbackInfo& args); // Methods on the Endpoint instance: @@ -373,6 +375,7 @@ class Endpoint final : public AsyncWrap, public Packet::Listener { // packets. // @param bool on - If true, mark the Endpoint as busy. static void MarkBusy(const v8::FunctionCallbackInfo& args); + static void FastMarkBusy(v8::Local receiver, bool on); // DoCloseGracefully is the signal that endpoint should close. Any packets // that are already in the queue or in flight will be allowed to finish, but @@ -387,9 +390,7 @@ class Endpoint final : public AsyncWrap, public Packet::Listener { // Ref() causes a listening Endpoint to keep the event loop active. static void Ref(const v8::FunctionCallbackInfo& args); - - // Unref() allows the event loop to close even if the Endpoint is listening. - static void Unref(const v8::FunctionCallbackInfo& args); + static void FastRef(v8::Local receiver, bool on); void Receive(const uv_buf_t& buf, const SocketAddress& from); diff --git a/src/quic/preferredaddress.cc b/src/quic/preferredaddress.cc index 138dbf47c46..66a44cd5bcf 100644 --- a/src/quic/preferredaddress.cc +++ b/src/quic/preferredaddress.cc @@ -156,13 +156,17 @@ void PreferredAddress::Set(ngtcp2_transport_params* params, Maybe PreferredAddress::tryGetPolicy( Environment* env, Local value) { - if (value->IsNumber()) { + if (value->IsUndefined()) { + return Just(PreferredAddress::Policy::USE_PREFERRED_ADDRESS); + } + if (value->IsUint32()) { auto val = value.As()->Value(); if (val == static_cast(Policy::IGNORE_PREFERRED_ADDRESS)) return Just(Policy::IGNORE_PREFERRED_ADDRESS); if (val == static_cast(Policy::USE_PREFERRED_ADDRESS)) return Just(Policy::USE_PREFERRED_ADDRESS); } + THROW_ERR_INVALID_ARG_VALUE(env, "invalid preferred address policy"); return Nothing(); } diff --git a/src/quic/quic.cc b/src/quic/quic.cc new file mode 100644 index 00000000000..17eacb9b5f4 --- /dev/null +++ b/src/quic/quic.cc @@ -0,0 +1,50 @@ +#if HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC + +#include +#include +#include +#include +#include +#include +#include "bindingdata.h" +#include "endpoint.h" +#include "node_external_reference.h" + +namespace node { + +using v8::Context; +using v8::Local; +using v8::Object; +using v8::ObjectTemplate; +using v8::Value; + +namespace quic { + +void CreatePerIsolateProperties(IsolateData* isolate_data, + Local target) { + Endpoint::InitPerIsolate(isolate_data, target); +} + +void CreatePerContextProperties(Local target, + Local unused, + Local context, + void* priv) { + Realm* realm = Realm::GetCurrent(context); + BindingData::InitPerContext(realm, target); + Endpoint::InitPerContext(realm, target); +} + +void RegisterExternalReferences(ExternalReferenceRegistry* registry) { + BindingData::RegisterExternalReferences(registry); + Endpoint::RegisterExternalReferences(registry); +} + +} // namespace quic +} // namespace node + +NODE_BINDING_CONTEXT_AWARE_INTERNAL(quic, + node::quic::CreatePerContextProperties) +NODE_BINDING_PER_ISOLATE_INIT(quic, node::quic::CreatePerIsolateProperties) +NODE_BINDING_EXTERNAL_REFERENCE(quic, node::quic::RegisterExternalReferences) + +#endif // HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC diff --git a/src/quic/session.cc b/src/quic/session.cc index b4a3ea796f3..e839aed992a 100644 --- a/src/quic/session.cc +++ b/src/quic/session.cc @@ -211,31 +211,19 @@ void ngtcp2_debug_log(void* user_data, const char* fmt, ...) { va_end(ap); } -template -bool SetOption(Environment* env, - Opt* options, - const v8::Local& object, - const v8::Local& name) { - Local value; - if (!object->Get(env->context(), name).ToLocal(&value)) return false; - if (!value->IsUndefined()) { - DCHECK(value->IsNumber()); - options->*member = value.As()->Value(); - } - return true; -} - template bool SetOption(Environment* env, Opt* options, const v8::Local& object, const v8::Local& name) { Local value; - if (!object->Get(env->context(), name).ToLocal(&value)) return false; - // If the policy specified is invalid, we will just ignore it. - auto maybePolicy = PreferredAddress::tryGetPolicy(env, value); - if (!maybePolicy.IsJust()) return false; - options->*member = maybePolicy.FromJust(); + PreferredAddress::Policy policy = + PreferredAddress::Policy::USE_PREFERRED_ADDRESS; + if (!object->Get(env->context(), name).ToLocal(&value) || + !PreferredAddress::tryGetPolicy(env, value).To(&policy)) { + return false; + } + options->*member = policy; return true; } @@ -245,10 +233,12 @@ bool SetOption(Environment* env, const v8::Local& object, const v8::Local& name) { Local value; - if (!object->Get(env->context(), name).ToLocal(&value)) return false; - auto maybeOptions = TLSContext::Options::From(env, value); - if (!maybeOptions.IsJust()) return false; - options->*member = maybeOptions.FromJust(); + TLSContext::Options opts; + if (!object->Get(env->context(), name).ToLocal(&value) || + !TLSContext::Options::From(env, value).To(&opts)) { + return false; + } + options->*member = opts; return true; } @@ -258,10 +248,12 @@ bool SetOption(Environment* env, const v8::Local& object, const v8::Local& name) { Local value; - if (!object->Get(env->context(), name).ToLocal(&value)) return false; - auto maybeOptions = Session::Application_Options::From(env, value); - if (!maybeOptions.IsJust()) return false; - options->*member = maybeOptions.FromJust(); + Session::Application_Options opts; + if (!object->Get(env->context(), name).ToLocal(&value) || + !Session::Application_Options::From(env, value).To(&opts)) { + return false; + } + options->*member = opts; return true; } @@ -271,10 +263,12 @@ bool SetOption(Environment* env, const v8::Local& object, const v8::Local& name) { Local value; - if (!object->Get(env->context(), name).ToLocal(&value)) return false; - auto maybeOptions = TransportParams::Options::From(env, value); - if (!maybeOptions.IsJust()) return false; - options->*member = maybeOptions.FromJust(); + TransportParams::Options opts; + if (!object->Get(env->context(), name).ToLocal(&value) || + !TransportParams::Options::From(env, value).To(&opts)) { + return false; + } + options->*member = opts; return true; } diff --git a/src/quic/streams.h b/src/quic/streams.h index fe53d2d9a0e..835dcfa30e8 100644 --- a/src/quic/streams.h +++ b/src/quic/streams.h @@ -1,6 +1,5 @@ #pragma once -#include #if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS #if HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC diff --git a/src/quic/tlscontext.cc b/src/quic/tlscontext.cc index 171f6e1d259..efb8dd02b11 100644 --- a/src/quic/tlscontext.cc +++ b/src/quic/tlscontext.cc @@ -277,6 +277,7 @@ bool SetOption(Environment* env, ASSIGN_OR_RETURN_UNWRAP(&handle, item, false); (options->*member).push_back(handle->Data()); } else { + THROW_ERR_INVALID_ARG_TYPE(env, "options must be an object"); return false; } } else if constexpr (std::is_same::value) { @@ -285,6 +286,7 @@ bool SetOption(Environment* env, } else if (item->IsArrayBuffer()) { (options->*member).emplace_back(item.As()); } else { + THROW_ERR_INVALID_ARG_TYPE(env, "options must be an object"); return false; } } @@ -297,6 +299,7 @@ bool SetOption(Environment* env, ASSIGN_OR_RETURN_UNWRAP(&handle, value, false); (options->*member).push_back(handle->Data()); } else { + THROW_ERR_INVALID_ARG_TYPE(env, "options must be an object"); return false; } } else if constexpr (std::is_same::value) { @@ -305,6 +308,7 @@ bool SetOption(Environment* env, } else if (value->IsArrayBuffer()) { (options->*member).emplace_back(value.As()); } else { + THROW_ERR_INVALID_ARG_TYPE(env, "options must be an object"); return false; } } @@ -545,16 +549,25 @@ ngtcp2_conn* TLSContext::getConnection(ngtcp2_crypto_conn_ref* ref) { return *context->session_; } -Maybe TLSContext::Options::From(Environment* env, - Local value) { - if (value.IsEmpty() || !value->IsObject()) { +Maybe TLSContext::Options::From(Environment* env, + Local value) { + if (value.IsEmpty()) { THROW_ERR_INVALID_ARG_TYPE(env, "options must be an object"); - return Nothing(); + return Nothing(); } - auto& state = BindingData::Get(env); - auto params = value.As(); Options options; + auto& state = BindingData::Get(env); + + if (value->IsUndefined()) { + return Just(options); + } + if (!value->IsObject()) { + THROW_ERR_INVALID_ARG_TYPE(env, "options must be an object"); + return Nothing(); + } + + auto params = value.As(); #define SET_VECTOR(Type, name) \ SetOption( \ @@ -571,10 +584,10 @@ Maybe TLSContext::Options::From(Environment* env, !SET_VECTOR(std::shared_ptr, keys) || !SET_VECTOR(Store, certs) || !SET_VECTOR(Store, ca) || !SET_VECTOR(Store, crl)) { - return Nothing(); + return Nothing(); } - return Just(options); + return Just(options); } } // namespace quic diff --git a/src/quic/tlscontext.h b/src/quic/tlscontext.h index 2de6c016a26..82593269d3f 100644 --- a/src/quic/tlscontext.h +++ b/src/quic/tlscontext.h @@ -91,8 +91,8 @@ class TLSContext final : public MemoryRetainer { static const Options kDefault; - static v8::Maybe From(Environment* env, - v8::Local value); + static v8::Maybe From(Environment* env, + v8::Local value); }; static const Options kDefaultOptions; diff --git a/src/quic/tokens.cc b/src/quic/tokens.cc index 8c9ced0a4aa..dc14be55723 100644 --- a/src/quic/tokens.cc +++ b/src/quic/tokens.cc @@ -15,24 +15,6 @@ namespace quic { // TokenSecret TokenSecret::TokenSecret() : buf_() { - Reset(); -} - -TokenSecret::TokenSecret(const uint8_t* secret) : buf_() { - *this = secret; -} - -TokenSecret& TokenSecret::operator=(const uint8_t* other) { - CHECK_NOT_NULL(other); - memcpy(buf_, other, QUIC_TOKENSECRET_LEN); - return *this; -} - -TokenSecret::operator const uint8_t*() const { - return buf_; -} - -void TokenSecret::Reset() { // As a performance optimization later, we could consider creating an entropy // cache here similar to what we use for random CIDs so that we do not have // to engage CSPRNG on every call. That, however, is suboptimal for secrets. @@ -42,6 +24,25 @@ void TokenSecret::Reset() { CHECK(crypto::CSPRNG(buf_, QUIC_TOKENSECRET_LEN).is_ok()); } +TokenSecret::TokenSecret(const uint8_t* secret) : buf_() { + CHECK_NOT_NULL(secret); + memcpy(buf_, secret, QUIC_TOKENSECRET_LEN); +} + +TokenSecret::~TokenSecret() { + memset(buf_, 0, QUIC_TOKENSECRET_LEN); +} + +TokenSecret::operator const uint8_t*() const { + return buf_; +} + +uint8_t TokenSecret::operator[](int pos) const { + CHECK_GE(pos, 0); + CHECK_LT(pos, QUIC_TOKENSECRET_LEN); + return buf_[pos]; +} + // ============================================================================ // StatelessResetToken diff --git a/src/quic/tokens.h b/src/quic/tokens.h index 21f611b7bbf..d6ebe34a12d 100644 --- a/src/quic/tokens.h +++ b/src/quic/tokens.h @@ -31,17 +31,15 @@ class TokenSecret final : public MemoryRetainer { // the length is not verified so care must be taken // when this constructor is used. explicit TokenSecret(const uint8_t* secret); + ~TokenSecret(); - TokenSecret(const TokenSecret& other) = default; - TokenSecret& operator=(const TokenSecret& other) = default; - TokenSecret& operator=(const uint8_t* other); - - TokenSecret& operator=(TokenSecret&& other) = delete; + TokenSecret(const TokenSecret&) = default; + TokenSecret& operator=(const TokenSecret&) = default; + TokenSecret(TokenSecret&&) = delete; + TokenSecret& operator=(TokenSecret&&) = delete; operator const uint8_t*() const; - - // Resets the secret to a random value. - void Reset(); + uint8_t operator[](int pos) const; SET_NO_MEMORY_INFO() SET_MEMORY_INFO_NAME(TokenSecret) diff --git a/src/quic/transportparams.cc b/src/quic/transportparams.cc index 3ea7a3d6d26..07c4ed45c50 100644 --- a/src/quic/transportparams.cc +++ b/src/quic/transportparams.cc @@ -31,15 +31,26 @@ TransportParams::Config::Config(Side side, const CID& retry_scid) : side(side), ocid(ocid), retry_scid(retry_scid) {} -Maybe TransportParams::Options::From( +Maybe TransportParams::Options::From( Environment* env, Local value) { - if (value.IsEmpty() || !value->IsObject()) { - return Nothing(); + if (value.IsEmpty()) { + THROW_ERR_INVALID_ARG_TYPE(env, "options must be an object"); + return Nothing(); } - auto& state = BindingData::Get(env); - auto params = value.As(); Options options; + auto& state = BindingData::Get(env); + + if (value->IsUndefined()) { + return Just(options); + } + + if (!value->IsObject()) { + THROW_ERR_INVALID_ARG_TYPE(env, "options must be an object"); + return Nothing(); + } + + auto params = value.As(); #define SET(name) \ SetOption( \ @@ -52,12 +63,12 @@ Maybe TransportParams::Options::From( !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(); + return Nothing(); } #undef SET - return Just(options); + return Just(options); } void TransportParams::Options::MemoryInfo(MemoryTracker* tracker) const { diff --git a/src/quic/transportparams.h b/src/quic/transportparams.h index 1269f11fbbb..b8fa7b2aec0 100644 --- a/src/quic/transportparams.h +++ b/src/quic/transportparams.h @@ -116,8 +116,8 @@ class TransportParams final { SET_MEMORY_INFO_NAME(TransportParams::Options) SET_SELF_SIZE(Options) - static v8::Maybe From(Environment* env, - v8::Local value); + static v8::Maybe From(Environment* env, + v8::Local value); }; explicit TransportParams(Type type); diff --git a/test/cctest/test_quic_tokens.cc b/test/cctest/test_quic_tokens.cc index c02bab54646..cf5b12da4d5 100644 --- a/test/cctest/test_quic_tokens.cc +++ b/test/cctest/test_quic_tokens.cc @@ -14,6 +14,21 @@ using node::quic::RetryToken; using node::quic::StatelessResetToken; using node::quic::TokenSecret; +TEST(TokenScret, Basics) { + uint8_t secret[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6}; + TokenSecret fixed_secret(secret); + + for (int n = 0; n < TokenSecret::QUIC_TOKENSECRET_LEN; n++) { + CHECK_EQ(fixed_secret[n], secret[n]); + } + + // Copy assignment works + TokenSecret other = fixed_secret; + for (int n = 0; n < TokenSecret::QUIC_TOKENSECRET_LEN; n++) { + CHECK_EQ(other[n], secret[n]); + } +} + TEST(StatelessResetToken, Basic) { ngtcp2_cid cid_; uint8_t secret[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6}; diff --git a/test/parallel/test-quic-internal-endpoint-listen-defaults.js b/test/parallel/test-quic-internal-endpoint-listen-defaults.js new file mode 100644 index 00000000000..9fb9f9461c5 --- /dev/null +++ b/test/parallel/test-quic-internal-endpoint-listen-defaults.js @@ -0,0 +1,76 @@ +// Flags: --expose-internals +'use strict'; + +const common = require('../common'); +if (!common.hasQuic) + common.skip('missing quic'); + +const { internalBinding } = require('internal/test/binding'); +const { + ok, + strictEqual, + deepStrictEqual, +} = require('node:assert'); + +const { + SocketAddress: _SocketAddress, + AF_INET, +} = internalBinding('block_list'); +const quic = internalBinding('quic'); + +quic.setCallbacks({ + onEndpointClose: common.mustCall((...args) => { + deepStrictEqual(args, [0, 0]); + }), + + // The following are unused in this test + onSessionNew() {}, + onSessionClose() {}, + onSessionDatagram() {}, + onSessionDatagramStatus() {}, + onSessionHandshake() {}, + onSessionPathValidation() {}, + onSessionTicket() {}, + onSessionVersionNegotiation() {}, + onStreamCreated() {}, + onStreamBlocked() {}, + onStreamClose() {}, + onStreamReset() {}, + onStreamHeaders() {}, + onStreamTrailers() {}, +}); + +const endpoint = new quic.Endpoint({}); + +const state = new DataView(endpoint.state); +ok(!state.getUint8(quic.IDX_STATE_ENDPOINT_LISTENING)); +ok(!state.getUint8(quic.IDX_STATE_ENDPOINT_RECEIVING)); +ok(!state.getUint8(quic.IDX_STATE_ENDPOINT_BOUND)); +strictEqual(endpoint.address(), undefined); + +endpoint.listen({}); + +ok(state.getUint8(quic.IDX_STATE_ENDPOINT_LISTENING)); +ok(state.getUint8(quic.IDX_STATE_ENDPOINT_RECEIVING)); +ok(state.getUint8(quic.IDX_STATE_ENDPOINT_BOUND)); +const address = endpoint.address(); +ok(address instanceof _SocketAddress); + +const detail = address.detail({ + address: undefined, + port: undefined, + family: undefined, + flowlabel: undefined, +}); + +strictEqual(detail.address, '127.0.0.1'); +strictEqual(detail.family, AF_INET); +strictEqual(detail.flowlabel, 0); +ok(detail.port !== 0); + +endpoint.closeGracefully(); + +ok(!state.getUint8(quic.IDX_STATE_ENDPOINT_LISTENING)); +ok(!state.getUint8(quic.IDX_STATE_ENDPOINT_RECEIVING)); +ok(!state.getUint8(quic.IDX_STATE_ENDPOINT_BOUND)); +strictEqual(endpoint.address(), undefined); diff --git a/test/parallel/test-quic-internal-endpoint-options.js b/test/parallel/test-quic-internal-endpoint-options.js new file mode 100644 index 00000000000..0d19841e5e8 --- /dev/null +++ b/test/parallel/test-quic-internal-endpoint-options.js @@ -0,0 +1,215 @@ +// Flags: --expose-internals +'use strict'; + +const common = require('../common'); +if (!common.hasQuic) + common.skip('missing quic'); +const { + throws, +} = require('node:assert'); + +const { internalBinding } = require('internal/test/binding'); +const quic = internalBinding('quic'); + +quic.setCallbacks({ + onEndpointClose() {}, + onSessionNew() {}, + onSessionClose() {}, + onSessionDatagram() {}, + onSessionDatagramStatus() {}, + onSessionHandshake() {}, + onSessionPathValidation() {}, + onSessionTicket() {}, + onSessionVersionNegotiation() {}, + onStreamCreated() {}, + onStreamBlocked() {}, + onStreamClose() {}, + onStreamReset() {}, + onStreamHeaders() {}, + onStreamTrailers() {}, +}); + +throws(() => new quic.Endpoint(), { + code: 'ERR_INVALID_ARG_TYPE', + message: 'options must be an object' +}); + +throws(() => new quic.Endpoint('a'), { + code: 'ERR_INVALID_ARG_TYPE', + message: 'options must be an object' +}); + +throws(() => new quic.Endpoint(null), { + code: 'ERR_INVALID_ARG_TYPE', + message: 'options must be an object' +}); + +throws(() => new quic.Endpoint(false), { + code: 'ERR_INVALID_ARG_TYPE', + message: 'options must be an object' +}); + +{ + // Just Works... using all defaults + new quic.Endpoint({}); +} + +const cases = [ + { + key: 'retryTokenExpiration', + valid: [ + 1, 10, 100, 1000, 10000, 10000n, + ], + invalid: [-1, -1n, 'a', null, false, true, {}, [], () => {}] + }, + { + key: 'tokenExpiration', + valid: [ + 1, 10, 100, 1000, 10000, 10000n, + ], + invalid: [-1, -1n, 'a', null, false, true, {}, [], () => {}] + }, + { + key: 'maxConnectionsPerHost', + valid: [ + 1, 10, 100, 1000, 10000, 10000n, + ], + invalid: [-1, -1n, 'a', null, false, true, {}, [], () => {}] + }, + { + key: 'maxConnectionsTotal', + valid: [ + 1, 10, 100, 1000, 10000, 10000n, + ], + invalid: [-1, -1n, 'a', null, false, true, {}, [], () => {}] + }, + { + key: 'maxStatelessResetsPerHost', + valid: [ + 1, 10, 100, 1000, 10000, 10000n, + ], + invalid: [-1, -1n, 'a', null, false, true, {}, [], () => {}] + }, + { + key: 'addressLRUSize', + valid: [ + 1, 10, 100, 1000, 10000, 10000n, + ], + invalid: [-1, -1n, 'a', null, false, true, {}, [], () => {}] + }, + { + key: 'maxRetries', + valid: [ + 1, 10, 100, 1000, 10000, 10000n, + ], + invalid: [-1, -1n, 'a', null, false, true, {}, [], () => {}] + }, + { + key: 'maxPayloadSize', + valid: [ + 1, 10, 100, 1000, 10000, 10000n, + ], + invalid: [-1, -1n, 'a', null, false, true, {}, [], () => {}] + }, + { + key: 'unacknowledgedPacketThreshold', + valid: [ + 1, 10, 100, 1000, 10000, 10000n, + ], + invalid: [-1, -1n, 'a', null, false, true, {}, [], () => {}] + }, + { + key: 'validateAddress', + valid: [true, false, 0, 1, 'a'], + invalid: [], + }, + { + key: 'disableStatelessReset', + valid: [true, false, 0, 1, 'a'], + invalid: [], + }, + { + key: 'ipv6Only', + valid: [true, false, 0, 1, 'a'], + invalid: [], + }, + { + key: 'cc', + valid: [ + quic.QUIC_CC_ALGO_RENO, + quic.QUIC_CC_ALGO_CUBIC, + quic.QUIC_CC_ALGO_BBR, + quic.QUIC_CC_ALGO_BBR2, + quic.QUIC_CC_ALGO_RENO_STR, + quic.QUIC_CC_ALGO_CUBIC_STR, + quic.QUIC_CC_ALGO_BBR_STR, + quic.QUIC_CC_ALGO_BBR2_STR, + ], + invalid: [-1, 4, 1n, 'a', null, false, true, {}, [], () => {}], + }, + { + key: 'udpReceiveBufferSize', + valid: [0, 1, 2, 3, 4, 1000], + invalid: [-1, 'a', null, false, true, {}, [], () => {}], + }, + { + key: 'udpSendBufferSize', + valid: [0, 1, 2, 3, 4, 1000], + invalid: [-1, 'a', null, false, true, {}, [], () => {}], + }, + { + key: 'udpTTL', + valid: [0, 1, 2, 3, 4, 255], + invalid: [-1, 256, 'a', null, false, true, {}, [], () => {}], + }, + { + key: 'resetTokenSecret', + valid: [ + new Uint8Array(16), + new Uint16Array(8), + new Uint32Array(4), + ], + invalid: [ + 'a', null, false, true, {}, [], () => {}, + new Uint8Array(15), + new Uint8Array(17), + new ArrayBuffer(16), + ], + }, + { + key: 'tokenSecret', + valid: [ + new Uint8Array(16), + new Uint16Array(8), + new Uint32Array(4), + ], + invalid: [ + 'a', null, false, true, {}, [], () => {}, + new Uint8Array(15), + new Uint8Array(17), + new ArrayBuffer(16), + ], + }, + { + // Unknown options are ignored entirely for any value type + key: 'ignored', + valid: ['a', null, false, true, {}, [], () => {}], + invalid: [], + }, +]; + +for (const { key, valid, invalid } of cases) { + for (const value of valid) { + const options = {}; + options[key] = value; + new quic.Endpoint(options); + } + + for (const value of invalid) { + const options = {}; + options[key] = value; + throws(() => new quic.Endpoint(options), { + code: 'ERR_INVALID_ARG_VALUE', + }); + } +} diff --git a/test/parallel/test-quic-internal-endpoint-stats-state.js b/test/parallel/test-quic-internal-endpoint-stats-state.js new file mode 100644 index 00000000000..566dd675d73 --- /dev/null +++ b/test/parallel/test-quic-internal-endpoint-stats-state.js @@ -0,0 +1,79 @@ +// Flags: --expose-internals +'use strict'; + +const common = require('../common'); +if (!common.hasQuic) + common.skip('missing quic'); +const { + strictEqual, +} = require('node:assert'); + +const { internalBinding } = require('internal/test/binding'); +const quic = internalBinding('quic'); + +const { + IDX_STATS_ENDPOINT_CREATED_AT, + IDX_STATS_ENDPOINT_DESTROYED_AT, + IDX_STATS_ENDPOINT_BYTES_RECEIVED, + IDX_STATS_ENDPOINT_BYTES_SENT, + IDX_STATS_ENDPOINT_PACKETS_RECEIVED, + IDX_STATS_ENDPOINT_PACKETS_SENT, + IDX_STATS_ENDPOINT_SERVER_SESSIONS, + IDX_STATS_ENDPOINT_CLIENT_SESSIONS, + IDX_STATS_ENDPOINT_SERVER_BUSY_COUNT, + IDX_STATS_ENDPOINT_RETRY_COUNT, + IDX_STATS_ENDPOINT_VERSION_NEGOTIATION_COUNT, + IDX_STATS_ENDPOINT_STATELESS_RESET_COUNT, + IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_COUNT, + IDX_STATS_ENDPOINT_COUNT, + IDX_STATE_ENDPOINT_BOUND, + IDX_STATE_ENDPOINT_BOUND_SIZE, + IDX_STATE_ENDPOINT_RECEIVING, + IDX_STATE_ENDPOINT_RECEIVING_SIZE, + IDX_STATE_ENDPOINT_LISTENING, + IDX_STATE_ENDPOINT_LISTENING_SIZE, + IDX_STATE_ENDPOINT_CLOSING, + IDX_STATE_ENDPOINT_CLOSING_SIZE, + IDX_STATE_ENDPOINT_BUSY, + IDX_STATE_ENDPOINT_BUSY_SIZE, + IDX_STATE_ENDPOINT_PENDING_CALLBACKS, + IDX_STATE_ENDPOINT_PENDING_CALLBACKS_SIZE, +} = quic; + +const endpoint = new quic.Endpoint({}); + +const state = new DataView(endpoint.state); +strictEqual(IDX_STATE_ENDPOINT_BOUND_SIZE, 1); +strictEqual(IDX_STATE_ENDPOINT_RECEIVING_SIZE, 1); +strictEqual(IDX_STATE_ENDPOINT_LISTENING_SIZE, 1); +strictEqual(IDX_STATE_ENDPOINT_CLOSING_SIZE, 1); +strictEqual(IDX_STATE_ENDPOINT_BUSY_SIZE, 1); +strictEqual(IDX_STATE_ENDPOINT_PENDING_CALLBACKS_SIZE, 8); + +strictEqual(state.getUint8(IDX_STATE_ENDPOINT_BOUND), 0); +strictEqual(state.getUint8(IDX_STATE_ENDPOINT_RECEIVING), 0); +strictEqual(state.getUint8(IDX_STATE_ENDPOINT_LISTENING), 0); +strictEqual(state.getUint8(IDX_STATE_ENDPOINT_CLOSING), 0); +strictEqual(state.getUint8(IDX_STATE_ENDPOINT_BUSY), 0); +strictEqual(state.getBigUint64(IDX_STATE_ENDPOINT_PENDING_CALLBACKS), 0n); + +endpoint.markBusy(true); +strictEqual(state.getUint8(IDX_STATE_ENDPOINT_BUSY), 1); +endpoint.markBusy(false); +strictEqual(state.getUint8(IDX_STATE_ENDPOINT_BUSY), 0); + +const stats = new BigUint64Array(endpoint.stats); +strictEqual(stats[IDX_STATS_ENDPOINT_CREATED_AT], 0n); +strictEqual(stats[IDX_STATS_ENDPOINT_DESTROYED_AT], 0n); +strictEqual(stats[IDX_STATS_ENDPOINT_BYTES_RECEIVED], 0n); +strictEqual(stats[IDX_STATS_ENDPOINT_BYTES_SENT], 0n); +strictEqual(stats[IDX_STATS_ENDPOINT_PACKETS_RECEIVED], 0n); +strictEqual(stats[IDX_STATS_ENDPOINT_PACKETS_SENT], 0n); +strictEqual(stats[IDX_STATS_ENDPOINT_SERVER_SESSIONS], 0n); +strictEqual(stats[IDX_STATS_ENDPOINT_CLIENT_SESSIONS], 0n); +strictEqual(stats[IDX_STATS_ENDPOINT_SERVER_BUSY_COUNT], 0n); +strictEqual(stats[IDX_STATS_ENDPOINT_RETRY_COUNT], 0n); +strictEqual(stats[IDX_STATS_ENDPOINT_VERSION_NEGOTIATION_COUNT], 0n); +strictEqual(stats[IDX_STATS_ENDPOINT_STATELESS_RESET_COUNT], 0n); +strictEqual(stats[IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_COUNT], 0n); +strictEqual(IDX_STATS_ENDPOINT_COUNT, 13); diff --git a/test/parallel/test-quic-internal-setcallbacks.js b/test/parallel/test-quic-internal-setcallbacks.js new file mode 100644 index 00000000000..881e9161ca9 --- /dev/null +++ b/test/parallel/test-quic-internal-setcallbacks.js @@ -0,0 +1,38 @@ +// Flags: --expose-internals +'use strict'; + +const common = require('../common'); +if (!common.hasQuic) + common.skip('missing quic'); +const { internalBinding } = require('internal/test/binding'); +const quic = internalBinding('quic'); + +const { throws } = require('assert'); + +const callbacks = { + onEndpointClose() {}, + onSessionNew() {}, + onSessionClose() {}, + onSessionDatagram() {}, + onSessionDatagramStatus() {}, + onSessionHandshake() {}, + onSessionPathValidation() {}, + onSessionTicket() {}, + onSessionVersionNegotiation() {}, + onStreamCreated() {}, + onStreamBlocked() {}, + onStreamClose() {}, + onStreamReset() {}, + onStreamHeaders() {}, + onStreamTrailers() {}, +}; +// Fail if any callback is missing +for (const fn of Object.keys(callbacks)) { + // eslint-disable-next-line no-unused-vars + const { [fn]: _, ...rest } = callbacks; + throws(() => quic.setCallbacks(rest), { + code: 'ERR_MISSING_ARGS', + }); +} +// If all callbacks are present it should work +quic.setCallbacks(callbacks); diff --git a/typings/internalBinding/quic.d.ts b/typings/internalBinding/quic.d.ts new file mode 100644 index 00000000000..d95c34c9da0 --- /dev/null +++ b/typings/internalBinding/quic.d.ts @@ -0,0 +1,100 @@ +interface QuicCallbacks { + onEndpointClose: (context: number, status: number) => void; + onSessionNew: (session: Session) => void; + onSessionClose: (type: number, code: bigint, reason?: string) => void; + onSessionDatagram: (datagram: Uint8Array, early: boolean) => void;); + onSessionDatagramStatus: (id: bigint, status: string) => void; + onSessionHandshake: (sni: string, + alpn: string, + cipher: string, + cipherVersion: string, + validationReason?: string, + validationCode?: string) => void; + onSessionPathValidation: (result: string, + local: SocketAddress, + remote: SocketAddress, + preferred: boolean) => void; + onSessionTicket: (ticket: ArrayBuffer) => void; + onSessionVersionNegotiation: (version: number, + versions: number[], + supports: number[]) => void; + onStreamCreated: (stream: Stream) => void; + onStreamBlocked: () => void; + onStreamClose: (error: [number,bigint,string]) => void; + onStreamReset: (error: [number,bigint,string]) => void; + onStreamHeaders: (headers: string[], kind: number) => void; + onStreamTrailers: () => void; +} + +interface EndpointOptions { + address?: SocketAddress; + retryTokenExpiration?: number|bigint; + tokenExpiration?: number|bigint; + maxConnectionsPerHost?: number|bigint; + maxConnectionsTotal?: number|bigint; + maxStatelessResetsPerHost?: number|bigint; + addressLRUSize?: number|bigint; + maxRetries?: number|bigint; + maxPayloadSize?: number|bigint; + unacknowledgedPacketThreshold?: number|bigint; + validateAddress?: boolean; + disableStatelessReset?: boolean; + ipv6Only?: boolean; + udpReceiveBufferSize?: number; + udpSendBufferSize?: number; + udpTTL?: number; + resetTokenSecret?: ArrayBufferView; + tokenSecret?: ArrayBufferView; + cc?: 'reno'|'cubic'|'pcc'|'bbr'| 0 | 2 | 3 | 4; +} + +interface SessionOptions {} +interface SocketAddress {} + +interface Session {} +interface Stream {} + +interface Endpoint { + listen(options: SessionOptions): void; + connect(address: SocketAddress, options: SessionOptions): Session; + closeGracefully(): void; + markBusy(on?: boolean): void; + ref(on?: boolean): void; + address(): SocketAddress|void; + readonly state: ArrayBuffer; + readonly stats: ArrayBuffer; +} + +export interface QuicBinding { + setCallbacks(callbacks: QuicCallbacks): void; + flushPacketFreeList(): void; + + readonly IDX_STATS_ENDPOINT_CREATED_AT: number; + readonly IDX_STATS_ENDPOINT_DESTROYED_AT: number; + readonly IDX_STATS_ENDPOINT_BYTES_RECEIVED: number; + readonly IDX_STATS_ENDPOINT_BYTES_SENT: number; + readonly IDX_STATS_ENDPOINT_PACKETS_RECEIVED: number; + readonly IDX_STATS_ENDPOINT_PACKETS_SENT: number; + readonly IDX_STATS_ENDPOINT_SERVER_SESSIONS: number; + readonly IDX_STATS_ENDPOINT_CLIENT_SESSIONS: number; + readonly IDX_STATS_ENDPOINT_SERVER_BUSY_COUNT: number; + readonly IDX_STATS_ENDPOINT_RETRY_COUNT: number; + readonly IDX_STATS_ENDPOINT_VERSION_NEGOTIATION_COUNT: number; + readonly IDX_STATS_ENDPOINT_STATELESS_RESET_COUNT: number; + readonly IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_COUNT: number; + readonly IDX_STATS_ENDPOINT_COUNT: number; + readonly IDX_STATE_ENDPOINT_BOUND: number; + readonly IDX_STATE_ENDPOINT_BOUND_SIZE: number; + readonly IDX_STATE_ENDPOINT_RECEIVING: number; + readonly IDX_STATE_ENDPOINT_RECEIVING_SIZE: number; + readonly IDX_STATE_ENDPOINT_LISTENING: number; + readonly IDX_STATE_ENDPOINT_LISTENING_SIZE: number; + readonly IDX_STATE_ENDPOINT_CLOSING: number; + readonly IDX_STATE_ENDPOINT_CLOSING_SIZE: number; + readonly IDX_STATE_ENDPOINT_WAITING_FOR_CALLBACKS: number; + readonly IDX_STATE_ENDPOINT_WAITING_FOR_CALLBACKS_SIZE: number; + readonly IDX_STATE_ENDPOINT_BUSY: number; + readonly IDX_STATE_ENDPOINT_BUSY_SIZE: number; + readonly IDX_STATE_ENDPOINT_PENDING_CALLBACKS: number; + readonly IDX_STATE_ENDPOINT_PENDING_CALLBACKS_SIZE: number; +}