From 15cb9dc5456543c89a7a1464ffba20ea638374ff Mon Sep 17 00:00:00 2001 From: James M Snell Date: Sat, 9 Dec 2023 13:27:50 -0800 Subject: [PATCH] quic: add quic internalBinding, refine Endpoint, add types --- node.gyp | 1 + src/node_binding.cc | 3 +- src/node_binding.h | 9 +- src/node_external_reference.h | 9 +- src/quic/application.cc | 19 +- src/quic/bindingdata.cc | 9 +- src/quic/bindingdata.h | 6 +- src/quic/defs.h | 46 ++- src/quic/endpoint.cc | 323 +++++++++++++----- src/quic/endpoint.h | 33 +- src/quic/preferredaddress.cc | 6 +- src/quic/quic.cc | 50 +++ src/quic/session.cc | 56 ++- src/quic/streams.h | 1 - src/quic/tlscontext.cc | 27 +- src/quic/tlscontext.h | 4 +- src/quic/transportparams.cc | 23 +- src/quic/transportparams.h | 4 +- ...-quic-internal-endpoint-listen-defaults.js | 74 ++++ .../test-quic-internal-endpoint-options.js | 195 +++++++++++ ...test-quic-internal-endpoint-stats-state.js | 77 +++++ .../test-quic-internal-setcallbacks.js | 36 ++ typings/internalBinding/quic.d.ts | 101 ++++++ 23 files changed, 938 insertions(+), 174 deletions(-) create mode 100644 src/quic/quic.cc create mode 100644 test/parallel/test-quic-internal-endpoint-listen-defaults.js create mode 100644 test/parallel/test-quic-internal-endpoint-options.js create mode 100644 test/parallel/test-quic-internal-endpoint-stats-state.js create mode 100644 test/parallel/test-quic-internal-setcallbacks.js create mode 100644 typings/internalBinding/quic.d.ts diff --git a/node.gyp b/node.gyp index 811d15b0df9ad3..2e591eea59d413 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 2b69a828a744b6..8013d9a0bbf48b 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 dfcfe5fe9e1bf4..094d14cb2ee6ac 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 b1b6ca31766326..703e287b7602e3 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 cafd6a8941c830..ce630ae35e456f 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 "application.h" +#include #include +#include #include -#include "application.h" #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(); } + 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(); - Application_Options options; #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 5ab0cc4b3f45a6..015265967fd58e 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 65ebe812efa1b7..fc0bc0c81a7b7e 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 bc47cfb26be9e2..dbb1ba3cdd2f9e 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,14 +204,27 @@ 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; } @@ -204,6 +262,26 @@ Maybe Endpoint::Options::From(Environment* env, return Nothing(); } + v8::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 @@ -241,8 +319,7 @@ class Endpoint::UDP::Impl final : public HandleWrap { .ToLocal(&obj)) { return BaseObjectPtr(); } - - return MakeDetachedBaseObject(endpoint, obj); + return MakeBaseObject(endpoint, obj); } static Impl* From(uv_udp_t* handle) { @@ -321,9 +398,9 @@ int Endpoint::UDP::Bind(const Endpoint::Options& options) { if (is_closed() || impl_->IsHandleClosing()) 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 +438,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,17 +446,23 @@ 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_); + // Remember to remove the cleanup hook first... We don't need managed cleanup + // since we are cleaning up explicitly here. + impl_->env()->RemoveCleanupHook(CleanupHook, this); + // Stop receiving packets if necessary. Stop(); + // Close the handle. is_bound_ = false; - impl_->env()->RemoveCleanupHook(CleanupHook, this); impl_->Close(); + // Reset the impl pointer. impl_.reset(); } @@ -390,18 +473,23 @@ bool Endpoint::UDP::is_bound() const { bool Endpoint::UDP::is_closed() const { return !impl_; } + +bool Endpoint::UDP::is_closed_or_closing() const { + return is_closed() || 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, @@ -425,6 +513,9 @@ void Endpoint::UDP::CleanupHook(void* data) { // ============================================================================ +v8::CFunction Endpoint::fast_mark_busy_(v8::CFunction::Make(FastMarkBusy)); +v8::CFunction Endpoint::fast_ref_(v8::CFunction::Make(FastRef)); + bool Endpoint::HasInstance(Environment* env, Local value) { return GetConstructorTemplate(env)->HasInstance(value); } @@ -442,33 +533,68 @@ Local Endpoint::GetConstructorTemplate(Environment* env) { SetProtoMethod(isolate, tmpl, "listen", DoListen); SetProtoMethod(isolate, tmpl, "closeGracefully", DoCloseGracefully); SetProtoMethod(isolate, tmpl, "connect", DoConnect); - SetProtoMethod(isolate, tmpl, "markBusy", MarkBusy); - SetProtoMethod(isolate, tmpl, "ref", Ref); - SetProtoMethod(isolate, tmpl, "unref", Unref); + SetFastMethod(isolate, + tmpl->InstanceTemplate(), + "markBusy", + MarkBusy, + &Endpoint::fast_mark_busy_); + SetFastMethod( + isolate, tmpl->InstanceTemplate(), "ref", Ref, &Endpoint::fast_ref_); 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); - ENDPOINT_STATE(V) +#define V(name, key) NODE_DEFINE_CONSTANT(target, IDX_STATS_ENDPOINT_##name); + ENDPOINT_STATS(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); +#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 + + 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); + + SetMethod(realm->context(), target, "createEndpoint", CreateEndpoint); + SetConstructorFunction(realm->context(), + target, + "Endpoint", + GetConstructorTemplate(realm->env())); } void Endpoint::RegisterExternalReferences(ExternalReferenceRegistry* registry) { @@ -478,7 +604,7 @@ void Endpoint::RegisterExternalReferences(ExternalReferenceRegistry* registry) { registry->Register(DoCloseGracefully); registry->Register(LocalAddress); registry->Register(Ref); - registry->Register(Unref); + registry->Register(MarkBusy); } BaseObjectPtr Endpoint::Create(Environment* env, @@ -491,7 +617,7 @@ BaseObjectPtr Endpoint::Create(Environment* env, return BaseObjectPtr(); } - return MakeDetachedBaseObject(env, obj, options); + return MakeBaseObject(env, obj, options); } Endpoint::Endpoint(Environment* env, @@ -524,7 +650,7 @@ Endpoint::~Endpoint() { } SocketAddress Endpoint::local_address() const { - CHECK(!is_closed()); + DCHECK(!is_closed() && !is_closing()); return udp_.local_address(); } @@ -562,7 +688,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) { @@ -790,7 +916,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,9 +930,9 @@ void Endpoint::Destroy(CloseContext context, int status) { } void Endpoint::CloseGracefully() { - if (!is_closed() && !is_closing() && state_->waiting_for_callbacks == 0) { + if (!is_closed() && !is_closing() && state_->closing == 0) { state_->listening = 0; - state_->waiting_for_callbacks = 1; + state_->closing = 1; } // Maybe we can go ahead and destroy now? @@ -1188,7 +1314,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()); @@ -1268,10 +1394,10 @@ void Endpoint::EmitClose(CloseContext context, int status) { // Endpoint JavaScript API void Endpoint::CreateEndpoint(const FunctionCallbackInfo& args) { - CHECK(!args.IsConstructCall()); + 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; @@ -1332,6 +1458,10 @@ void Endpoint::MarkBusy(const FunctionCallbackInfo& args) { endpoint->MarkAsBusy(args[0]->IsTrue()); } +void Endpoint::FastMarkBusy(Local receiver, bool on) { + FromJSObject(receiver)->MarkAsBusy(on); +} + void Endpoint::DoCloseGracefully(const FunctionCallbackInfo& args) { Endpoint* endpoint; ASSIGN_OR_RETURN_UNWRAP(&endpoint, args.Holder()); @@ -1342,23 +1472,30 @@ 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(); + auto env = Environment::GetCurrent(args); + if (args[0]->BooleanValue(env->isolate())) { + endpoint->udp_.Ref(); + } else { + endpoint->udp_.Unref(); + } } -void Endpoint::Unref(const FunctionCallbackInfo& args) { - Endpoint* endpoint; - ASSIGN_OR_RETURN_UNWRAP(&endpoint, args.Holder()); - endpoint->udp_.Unref(); +void Endpoint::FastRef(Local receiver, bool on) { + auto endpoint = FromJSObject(receiver); + if (on) { + endpoint->udp_.Ref(); + } else { + endpoint->udp_.Unref(); + } } } // namespace quic diff --git a/src/quic/endpoint.h b/src/quic/endpoint.h index 700630c2244420..20c5cb218e143d 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 @@ -169,7 +173,9 @@ 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, @@ -289,6 +295,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(); @@ -373,6 +380,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 +395,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); @@ -428,6 +434,9 @@ class Endpoint final : public AsyncWrap, public Packet::Listener { CloseContext close_context_ = CloseContext::CLOSE; int close_status_ = 0; + static v8::CFunction fast_mark_busy_; + static v8::CFunction fast_ref_; + friend class UDP; friend class Packet; friend class Session; diff --git a/src/quic/preferredaddress.cc b/src/quic/preferredaddress.cc index 138dbf47c46a07..66a44cd5bcf2f4 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 00000000000000..17eacb9b5f4034 --- /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 b4a3ea796f338b..e839aed992ab5d 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 fe53d2d9a0e50b..835dcfa30e8a26 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 171f6e1d259bc7..efb8dd02b11f10 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(); } + 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(); - Options options; #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 2de6c016a267af..82593269d3fe2c 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/transportparams.cc b/src/quic/transportparams.cc index 3ea7a3d6d268ae..07c4ed45c50c5b 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(); } + 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(); - Options options; #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 1269f11fbbbf1c..b8fa7b2aec0984 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/parallel/test-quic-internal-endpoint-listen-defaults.js b/test/parallel/test-quic-internal-endpoint-listen-defaults.js new file mode 100644 index 00000000000000..602be4b76325dc --- /dev/null +++ b/test/parallel/test-quic-internal-endpoint-listen-defaults.js @@ -0,0 +1,74 @@ +// Flags: --expose-internals +'use strict'; + +const common = require('../common'); + +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 = quic.createEndpoint({}); + +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 00000000000000..912f70b1337f2a --- /dev/null +++ b/test/parallel/test-quic-internal-endpoint-options.js @@ -0,0 +1,195 @@ +// Flags: --expose-internals +'use strict'; + +require('../common'); +const { + throws, +} = require('node:assert'); + +const { internalBinding } = require('internal/test/binding'); +const quic = internalBinding('quic'); + +throws(() => quic.createEndpoint(), { + code: 'ERR_INVALID_ARG_TYPE', + message: 'options must be an object' +}); + +throws(() => quic.createEndpoint('a'), { + code: 'ERR_INVALID_ARG_TYPE', + message: 'options must be an object' +}); + +throws(() => quic.createEndpoint(null), { + code: 'ERR_INVALID_ARG_TYPE', + message: 'options must be an object' +}); + +throws(() => quic.createEndpoint(false), { + code: 'ERR_INVALID_ARG_TYPE', + message: 'options must be an object' +}); + +{ + // Just Works... using all defaults + quic.createEndpoint({}); +} + +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; + quic.createEndpoint(options); + } + + for (const value of invalid) { + const options = {}; + options[key] = value; + throws(() => quic.createEndpoint(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 00000000000000..640f6fe351960b --- /dev/null +++ b/test/parallel/test-quic-internal-endpoint-stats-state.js @@ -0,0 +1,77 @@ +// Flags: --expose-internals +'use strict'; + +require('../common'); +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 = quic.createEndpoint({}); + +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 00000000000000..b6db66cc0155fd --- /dev/null +++ b/test/parallel/test-quic-internal-setcallbacks.js @@ -0,0 +1,36 @@ +// Flags: --expose-internals +'use strict'; + +require('../common'); +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 00000000000000..4a270c4df520d3 --- /dev/null +++ b/typings/internalBinding/quic.d.ts @@ -0,0 +1,101 @@ +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; + createEndpoint(options: EndpointOptions): Endpoint; + + 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; +}