quic: add quic internalBinding, refine Endpoint, add types

PR-URL: https://github.com/nodejs/node/pull/51112
Reviewed-By: Stephen Belanger <admin@stephenbelanger.com>
Reviewed-By: Yagiz Nizipli <yagiz.nizipli@sentry.io>
Reviewed-By: Tobias Nießen <tniessen@tnie.de>
This commit is contained in:
James M Snell 2023-12-09 13:27:50 -08:00
parent 20c63134fc
commit c3664227a8
26 changed files with 1005 additions and 265 deletions

View File

@ -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',

View File

@ -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_<modname>

View File

@ -30,6 +30,12 @@ static_assert(static_cast<int>(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<int>(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 = { \

View File

@ -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

View File

@ -1,10 +1,10 @@
#include "node_bob.h"
#include "uv.h"
#if HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC
#include <node_sockaddr-inl.h>
#include <v8.h>
#include "application.h"
#include <node_bob.h>
#include <node_sockaddr-inl.h>
#include <uv.h>
#include <v8.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> Session::Application_Options::From(
Environment* env, Local<Value> value) {
if (value.IsEmpty() || !value->IsObject()) {
if (value.IsEmpty()) {
THROW_ERR_INVALID_ARG_TYPE(env, "options must be an object");
return Nothing<Application_Options>();
}
auto& state = BindingData::Get(env);
auto params = value.As<Object>();
Application_Options options;
auto& state = BindingData::Get(env);
if (value->IsUndefined()) {
return Just<Application_Options>(options);
}
if (!value->IsObject()) {
THROW_ERR_INVALID_ARG_TYPE(env, "options must be an object");
return Nothing<Application_Options>();
}
auto params = value.As<Object>();
#define SET(name) \
SetOption<Session::Application_Options, \

View File

@ -56,10 +56,11 @@ void BindingData::DecreaseAllocatedSize(size_t size) {
current_ngtcp2_memory_ -= size;
}
void BindingData::Initialize(Environment* env, Local<Object> target) {
SetMethod(env->context(), target, "setCallbacks", SetCallbacks);
SetMethod(env->context(), target, "flushPacketFreelist", FlushPacketFreelist);
Realm::GetCurrent(env->context())->AddBindingData<BindingData>(target);
void BindingData::InitPerContext(Realm* realm, Local<Object> target) {
SetMethod(realm->context(), target, "setCallbacks", SetCallbacks);
SetMethod(
realm->context(), target, "flushPacketFreelist", FlushPacketFreelist);
Realm::GetCurrent(realm->context())->AddBindingData<BindingData>(target);
}
void BindingData::RegisterExternalReferences(

View File

@ -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<BindingData, ngtcp2_mem> {
public:
SET_BINDING_ID(quic_binding_data)
static void Initialize(Environment* env, v8::Local<v8::Object> target);
static void InitPerContext(Realm* realm, v8::Local<v8::Object> target);
static void RegisterExternalReferences(ExternalReferenceRegistry* registry);
static BindingData& Get(Environment* env);

View File

@ -28,6 +28,17 @@ bool SetOption(Environment* env,
}
template <typename Opt, bool Opt::*member>
bool SetOption(Environment* env,
Opt* options,
const v8::Local<v8::Object>& object,
const v8::Local<v8::String>& name) {
v8::Local<v8::Value> value;
if (!object->Get(env->context(), name).ToLocal(&value)) return false;
options->*member = value->BooleanValue(env->isolate());
return true;
}
template <typename Opt, uint32_t Opt::*member>
bool SetOption(Environment* env,
Opt* options,
const v8::Local<v8::Object>& object,
@ -35,8 +46,20 @@ bool SetOption(Environment* env,
v8::Local<v8::Value> 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<v8::Uint32> 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<v8::BigInt>()->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<int64_t>(value.As<v8::Number>()->Value());
double dbl = value.As<v8::Number>()->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<uint64_t>(dbl);
}
options->*member = val;
}

View File

@ -7,12 +7,15 @@
#include <memory_tracker-inl.h>
#include <ngtcp2/ngtcp2.h>
#include <node_errors.h>
#include <node_external_reference.h>
#include <node_sockaddr-inl.h>
#include <req_wrap-inl.h>
#include <util-inl.h>
#include <uv.h>
#include <v8.h>
#include <limits>
#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<ngtcp2_cc_algo> getAlgoFromString(Environment* env, Local<String> 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<ngtcp2_cc_algo>();
}
template <typename Opt, ngtcp2_cc_algo Opt::*member>
bool SetOption(Environment* env,
Opt* options,
const v8::Local<v8::Object>& object,
const v8::Local<v8::String>& name) {
v8::Local<v8::Value> value;
const Local<Object>& object,
const Local<String>& name) {
Local<Value> value;
if (!object->Get(env->context(), name).ToLocal(&value)) return false;
if (!value->IsUndefined()) {
int num = value.As<Int32>()->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<String>()).To(&algo)) {
THROW_ERR_INVALID_ARG_VALUE(env, "The cc_algorithm option is invalid");
return false;
}
options->*member = static_cast<ngtcp2_cc_algo>(num);
} else {
if (!value->IsInt32()) {
THROW_ERR_INVALID_ARG_VALUE(
env, "The cc_algorithm option must be a string or an integer");
return false;
}
Local<Int32> 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<ngtcp2_cc_algo>(num->Value());
}
options->*member = algo;
}
return true;
}
#if DEBUG
template <typename Opt, double Opt::*member>
bool SetOption(Environment* env,
Opt* options,
const v8::Local<v8::Object>& object,
const v8::Local<v8::String>& name) {
v8::Local<v8::Value> value;
const Local<Object>& object,
const Local<String>& name) {
Local<Value> value;
if (!object->Get(env->context(), name).ToLocal(&value)) return false;
if (!value->IsUndefined()) {
CHECK(value->IsNumber());
options->*member = value.As<Number>()->Value();
}
return true;
}
template <typename Opt, uint32_t Opt::*member>
bool SetOption(Environment* env,
Opt* options,
const v8::Local<v8::Object>& object,
const v8::Local<v8::String>& name) {
v8::Local<v8::Value> value;
if (!object->Get(env->context(), name).ToLocal(&value)) return false;
if (!value->IsUndefined()) {
CHECK(value->IsNumber());
options->*member = value.As<Int32>()->Value();
Local<Number> 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 <typename Opt, uint8_t Opt::*member>
bool SetOption(Environment* env,
Opt* options,
const v8::Local<v8::Object>& object,
const v8::Local<v8::String>& name) {
v8::Local<v8::Value> value;
const Local<Object>& object,
const Local<String>& name) {
Local<Value> value;
if (!object->Get(env->context(), name).ToLocal(&value)) return false;
if (!value->IsUndefined()) {
CHECK(value->IsNumber());
options->*member = value.As<Int32>()->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<Uint32> num;
if (!value->ToUint32(env->context()).ToLocal(&num) ||
num->Value() > std::numeric_limits<uint8_t>::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 <typename Opt, TokenSecret Opt::*member>
bool SetOption(Environment* env,
Opt* options,
const v8::Local<v8::Object>& object,
const v8::Local<v8::String>& name) {
v8::Local<v8::Value> value;
const Local<Object>& object,
const Local<String>& name) {
Local<Value> 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<ArrayBufferView>());
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> Endpoint::Options::From(Environment* env,
return Nothing<Options>();
}
Local<Value> address;
if (!params->Get(env->context(), env->address_string()).ToLocal(&address)) {
return Nothing<Options>();
}
if (!address->IsUndefined()) {
if (!SocketAddressBase::HasInstance(env, address)) {
THROW_ERR_INVALID_ARG_TYPE(env,
"The address option must be a SocketAddress");
return Nothing<Options>();
}
auto addr = FromJSObject<SocketAddressBase>(address.As<v8::Object>());
options.local_address = addr->address();
} else {
options.local_address = std::make_shared<SocketAddress>();
if (!SocketAddress::New("127.0.0.1", 0, options.local_address.get())) {
THROW_ERR_INVALID_ADDRESS(env);
return Nothing<Options>();
}
}
return Just<Options>(options);
#undef SET
@ -233,16 +312,15 @@ class Endpoint::UDP::Impl final : public HandleWrap {
return tmpl;
}
static BaseObjectPtr<Impl> Create(Endpoint* endpoint) {
static Impl* Create(Endpoint* endpoint) {
Local<Object> obj;
if (!GetConstructorTemplate(endpoint->env())
->InstanceTemplate()
->NewInstance(endpoint->env()->context())
.ToLocal(&obj)) {
return BaseObjectPtr<Impl>();
return nullptr;
}
return MakeDetachedBaseObject<Impl>(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<Impl> 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> 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<UDP*>(data)->Close();
}
// ============================================================================
bool Endpoint::HasInstance(Environment* env, Local<Value> value) {
@ -434,7 +511,7 @@ Local<FunctionTemplate> 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<FunctionTemplate> 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<Object> target) {
SetMethod(env->context(), target, "createEndpoint", CreateEndpoint);
void Endpoint::InitPerIsolate(IsolateData* data, Local<ObjectTemplate> target) {
// TODO(@jasnell): Implement the per-isolate state
}
void Endpoint::InitPerContext(Realm* realm, Local<Object> 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> Endpoint::Create(Environment* env,
const Endpoint::Options& options) {
Local<Object> obj;
if (!GetConstructorTemplate(env)
->InstanceTemplate()
->NewInstance(env->context())
.ToLocal(&obj)) {
return BaseObjectPtr<Endpoint>();
}
return MakeDetachedBaseObject<Endpoint>(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<Session> Endpoint::FindSession(const CID& cid) {
@ -764,18 +850,17 @@ BaseObjectPtr<Session> 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) {
if (is_closed() || is_closing()) return;
state_->listening = 0;
state_->waiting_for_callbacks = 1;
}
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<Value>& args) {
CHECK(!args.IsConstructCall());
void Endpoint::New(const FunctionCallbackInfo<Value>& 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<Value>& args) {
@ -1342,23 +1426,21 @@ void Endpoint::LocalAddress(const FunctionCallbackInfo<Value>& 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<SocketAddress>(local_address));
env, std::make_shared<SocketAddress>(endpoint->local_address()));
if (addr) args.GetReturnValue().Set(addr->object());
}
void Endpoint::Ref(const FunctionCallbackInfo<Value>& args) {
Endpoint* endpoint;
ASSIGN_OR_RETURN_UNWRAP(&endpoint, args.Holder());
auto env = Environment::GetCurrent(args);
if (args[0]->BooleanValue(env->isolate())) {
endpoint->udp_.Ref();
}
void Endpoint::Unref(const FunctionCallbackInfo<Value>& args) {
Endpoint* endpoint;
ASSIGN_OR_RETURN_UNWRAP(&endpoint, args.Holder());
} else {
endpoint->udp_.Unref();
}
}
} // namespace quic

View File

@ -12,7 +12,6 @@
#include <algorithm>
#include <optional>
#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<size_t>(kMaxSizeT, static_cast<size_t>(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<uint64_t>(kMaxSizeT, static_cast<uint64_t>(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<SocketAddress> 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<v8::Value> value);
static v8::Local<v8::FunctionTemplate> GetConstructorTemplate(
Environment* env);
static void Initialize(Environment* env, v8::Local<v8::Object> target);
static void InitPerIsolate(IsolateData* data,
v8::Local<v8::ObjectTemplate> target);
static void InitPerContext(Realm* realm, v8::Local<v8::Object> target);
static void RegisterExternalReferences(ExternalReferenceRegistry* registry);
static BaseObjectPtr<Endpoint> Create(Environment* env,
const Endpoint::Options& config);
Endpoint(Environment* env,
v8::Local<v8::Object> 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> impl_;
BaseObjectWeakPtr<Impl> 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<v8::Value>& args);
static void New(const v8::FunctionCallbackInfo<v8::Value>& 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<v8::Value>& args);
static void FastMarkBusy(v8::Local<v8::Object> 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<v8::Value>& args);
// Unref() allows the event loop to close even if the Endpoint is listening.
static void Unref(const v8::FunctionCallbackInfo<v8::Value>& args);
static void FastRef(v8::Local<v8::Object> receiver, bool on);
void Receive(const uv_buf_t& buf, const SocketAddress& from);

View File

@ -156,13 +156,17 @@ void PreferredAddress::Set(ngtcp2_transport_params* params,
Maybe<PreferredAddress::Policy> PreferredAddress::tryGetPolicy(
Environment* env, Local<Value> value) {
if (value->IsNumber()) {
if (value->IsUndefined()) {
return Just(PreferredAddress::Policy::USE_PREFERRED_ADDRESS);
}
if (value->IsUint32()) {
auto val = value.As<Uint32>()->Value();
if (val == static_cast<uint32_t>(Policy::IGNORE_PREFERRED_ADDRESS))
return Just(Policy::IGNORE_PREFERRED_ADDRESS);
if (val == static_cast<uint32_t>(Policy::USE_PREFERRED_ADDRESS))
return Just(Policy::USE_PREFERRED_ADDRESS);
}
THROW_ERR_INVALID_ARG_VALUE(env, "invalid preferred address policy");
return Nothing<PreferredAddress::Policy>();
}

50
src/quic/quic.cc Normal file
View File

@ -0,0 +1,50 @@
#if HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC
#include <base_object-inl.h>
#include <env-inl.h>
#include <memory_tracker-inl.h>
#include <node_realm-inl.h>
#include <node_sockaddr-inl.h>
#include <v8.h>
#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<ObjectTemplate> target) {
Endpoint::InitPerIsolate(isolate_data, target);
}
void CreatePerContextProperties(Local<Object> target,
Local<Value> unused,
Local<Context> 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

View File

@ -211,31 +211,19 @@ void ngtcp2_debug_log(void* user_data, const char* fmt, ...) {
va_end(ap);
}
template <typename Opt, uint32_t Opt::*member>
bool SetOption(Environment* env,
Opt* options,
const v8::Local<Object>& object,
const v8::Local<String>& name) {
Local<Value> value;
if (!object->Get(env->context(), name).ToLocal(&value)) return false;
if (!value->IsUndefined()) {
DCHECK(value->IsNumber());
options->*member = value.As<Uint32>()->Value();
}
return true;
}
template <typename Opt, PreferredAddress::Policy Opt::*member>
bool SetOption(Environment* env,
Opt* options,
const v8::Local<Object>& object,
const v8::Local<String>& name) {
Local<Value> 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>& object,
const v8::Local<String>& name) {
Local<Value> 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>& object,
const v8::Local<String>& name) {
Local<Value> 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>& object,
const v8::Local<String>& name) {
Local<Value> 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;
}

View File

@ -1,6 +1,5 @@
#pragma once
#include <cstdint>
#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
#if HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC

View File

@ -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<T, Store>::value) {
@ -285,6 +286,7 @@ bool SetOption(Environment* env,
} else if (item->IsArrayBuffer()) {
(options->*member).emplace_back(item.As<v8::ArrayBuffer>());
} 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<T, Store>::value) {
@ -305,6 +308,7 @@ bool SetOption(Environment* env,
} else if (value->IsArrayBuffer()) {
(options->*member).emplace_back(value.As<v8::ArrayBuffer>());
} 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<const TLSContext::Options> TLSContext::Options::From(Environment* env,
Maybe<TLSContext::Options> TLSContext::Options::From(Environment* env,
Local<Value> value) {
if (value.IsEmpty() || !value->IsObject()) {
if (value.IsEmpty()) {
THROW_ERR_INVALID_ARG_TYPE(env, "options must be an object");
return Nothing<const Options>();
return Nothing<Options>();
}
auto& state = BindingData::Get(env);
auto params = value.As<Object>();
Options options;
auto& state = BindingData::Get(env);
if (value->IsUndefined()) {
return Just<Options>(options);
}
if (!value->IsObject()) {
THROW_ERR_INVALID_ARG_TYPE(env, "options must be an object");
return Nothing<Options>();
}
auto params = value.As<Object>();
#define SET_VECTOR(Type, name) \
SetOption<Type, TLSContext::Options, &TLSContext::Options::name>( \
@ -571,10 +584,10 @@ Maybe<const TLSContext::Options> TLSContext::Options::From(Environment* env,
!SET_VECTOR(std::shared_ptr<crypto::KeyObjectData>, keys) ||
!SET_VECTOR(Store, certs) || !SET_VECTOR(Store, ca) ||
!SET_VECTOR(Store, crl)) {
return Nothing<const Options>();
return Nothing<Options>();
}
return Just<const Options>(options);
return Just<Options>(options);
}
} // namespace quic

View File

@ -91,7 +91,7 @@ class TLSContext final : public MemoryRetainer {
static const Options kDefault;
static v8::Maybe<const Options> From(Environment* env,
static v8::Maybe<Options> From(Environment* env,
v8::Local<v8::Value> value);
};

View File

@ -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

View File

@ -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)

View File

@ -31,15 +31,26 @@ TransportParams::Config::Config(Side side,
const CID& retry_scid)
: side(side), ocid(ocid), retry_scid(retry_scid) {}
Maybe<const TransportParams::Options> TransportParams::Options::From(
Maybe<TransportParams::Options> TransportParams::Options::From(
Environment* env, Local<Value> value) {
if (value.IsEmpty() || !value->IsObject()) {
return Nothing<const Options>();
if (value.IsEmpty()) {
THROW_ERR_INVALID_ARG_TYPE(env, "options must be an object");
return Nothing<Options>();
}
auto& state = BindingData::Get(env);
auto params = value.As<Object>();
Options options;
auto& state = BindingData::Get(env);
if (value->IsUndefined()) {
return Just<Options>(options);
}
if (!value->IsObject()) {
THROW_ERR_INVALID_ARG_TYPE(env, "options must be an object");
return Nothing<Options>();
}
auto params = value.As<Object>();
#define SET(name) \
SetOption<TransportParams::Options, &TransportParams::Options::name>( \
@ -52,12 +63,12 @@ Maybe<const TransportParams::Options> 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<const Options>();
return Nothing<Options>();
}
#undef SET
return Just<const Options>(options);
return Just<Options>(options);
}
void TransportParams::Options::MemoryInfo(MemoryTracker* tracker) const {

View File

@ -116,7 +116,7 @@ class TransportParams final {
SET_MEMORY_INFO_NAME(TransportParams::Options)
SET_SELF_SIZE(Options)
static v8::Maybe<const Options> From(Environment* env,
static v8::Maybe<Options> From(Environment* env,
v8::Local<v8::Value> value);
};

View File

@ -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};

View File

@ -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);

View File

@ -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',
});
}
}

View File

@ -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);

View File

@ -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);

100
typings/internalBinding/quic.d.ts vendored Normal file
View File

@ -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;
}