summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorJames M Snell <jasnell@gmail.com>2023-03-31 17:58:57 -0700
committerJames M Snell <jasnell@gmail.com>2023-04-17 17:49:16 -0700
commit1c3d741cb00399e23df25e94a767a9a1c1357be2 (patch)
tree8765e13070451ad285cf792398c14912b9c3a90b /src
parent17c9aa2f7635eccbb132cce20cd45c4cf14fd7be (diff)
downloadnode-new-1c3d741cb00399e23df25e94a767a9a1c1357be2.tar.gz
quic: add more QUIC implementation
* add TLSContext * quic: add stat collection utilities * add Packet * add NgTcp2CallbackScope/NgHttp3CallbackScope PR-URL: https://github.com/nodejs/node/pull/47494 Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com> Reviewed-By: Tobias Nießen <tniessen@tnie.de>
Diffstat (limited to 'src')
-rw-r--r--src/async_wrap.h1
-rw-r--r--src/node_errors.h2
-rw-r--r--src/quic/bindingdata.cc46
-rw-r--r--src/quic/bindingdata.h51
-rw-r--r--src/quic/defs.h48
-rw-r--r--src/quic/packet.cc406
-rw-r--r--src/quic/packet.h168
-rw-r--r--src/quic/tlscontext.cc589
-rw-r--r--src/quic/tlscontext.h176
9 files changed, 1479 insertions, 8 deletions
diff --git a/src/async_wrap.h b/src/async_wrap.h
index 42e3413b12..735ae1f7db 100644
--- a/src/async_wrap.h
+++ b/src/async_wrap.h
@@ -61,6 +61,7 @@ namespace node {
V(PROMISE) \
V(QUERYWRAP) \
V(QUIC_LOGSTREAM) \
+ V(QUIC_PACKET) \
V(SHUTDOWNWRAP) \
V(SIGNALWRAP) \
V(STATWATCHER) \
diff --git a/src/node_errors.h b/src/node_errors.h
index 4edb1c20b3..5250b1437e 100644
--- a/src/node_errors.h
+++ b/src/node_errors.h
@@ -63,6 +63,7 @@ void OOMErrorHandler(const char* location, const v8::OOMDetails& details);
V(ERR_DLOPEN_FAILED, Error) \
V(ERR_ENCODING_INVALID_ENCODED_DATA, TypeError) \
V(ERR_EXECUTION_ENVIRONMENT_NOT_AVAILABLE, Error) \
+ V(ERR_ILLEGAL_CONSTRUCTOR, Error) \
V(ERR_INVALID_ADDRESS, Error) \
V(ERR_INVALID_ARG_VALUE, TypeError) \
V(ERR_OSSL_EVP_INVALID_DIGEST, Error) \
@@ -156,6 +157,7 @@ ERRORS_WITH_CODE(V)
V(ERR_DLOPEN_FAILED, "DLOpen failed") \
V(ERR_EXECUTION_ENVIRONMENT_NOT_AVAILABLE, \
"Context not associated with Node.js environment") \
+ V(ERR_ILLEGAL_CONSTRUCTOR, "Illegal constructor") \
V(ERR_INVALID_ADDRESS, "Invalid socket address") \
V(ERR_INVALID_MODULE, "No such module") \
V(ERR_INVALID_STATE, "Invalid state") \
diff --git a/src/quic/bindingdata.cc b/src/quic/bindingdata.cc
index 2a4962b3b5..9690031773 100644
--- a/src/quic/bindingdata.cc
+++ b/src/quic/bindingdata.cc
@@ -58,6 +58,7 @@ void BindingData::DecreaseAllocatedSize(size_t size) {
void BindingData::Initialize(Environment* env, Local<Object> target) {
SetMethod(env->context(), target, "setCallbacks", SetCallbacks);
+ SetMethod(env->context(), target, "flushPacketFreelist", FlushPacketFreelist);
Realm::GetCurrent(env->context())
->AddBindingData<BindingData>(env->context(), target);
}
@@ -65,6 +66,7 @@ void BindingData::Initialize(Environment* env, Local<Object> target) {
void BindingData::RegisterExternalReferences(
ExternalReferenceRegistry* registry) {
registry->Register(SetCallbacks);
+ registry->Register(FlushPacketFreelist);
}
BindingData::BindingData(Realm* realm, Local<Object> object)
@@ -140,7 +142,7 @@ QUIC_JS_CALLBACKS(V)
void BindingData::SetCallbacks(const FunctionCallbackInfo<Value>& args) {
auto env = Environment::GetCurrent(args);
auto isolate = env->isolate();
- BindingData& state = BindingData::Get(env);
+ auto& state = BindingData::Get(env);
CHECK(args[0]->IsObject());
Local<Object> obj = args[0].As<Object>();
@@ -159,6 +161,48 @@ void BindingData::SetCallbacks(const FunctionCallbackInfo<Value>& args) {
#undef V
}
+void BindingData::FlushPacketFreelist(const FunctionCallbackInfo<Value>& args) {
+ auto env = Environment::GetCurrent(args);
+ auto& state = BindingData::Get(env);
+ state.packet_freelist.clear();
+}
+
+NgTcp2CallbackScope::NgTcp2CallbackScope(Environment* env) : env(env) {
+ auto& binding = BindingData::Get(env);
+ CHECK(!binding.in_ngtcp2_callback_scope);
+ binding.in_ngtcp2_callback_scope = true;
+}
+
+NgTcp2CallbackScope::~NgTcp2CallbackScope() {
+ auto& binding = BindingData::Get(env);
+ binding.in_ngtcp2_callback_scope = false;
+}
+
+bool NgTcp2CallbackScope::in_ngtcp2_callback(Environment* env) {
+ auto& binding = BindingData::Get(env);
+ return binding.in_ngtcp2_callback_scope;
+}
+
+NgHttp3CallbackScope::NgHttp3CallbackScope(Environment* env) : env(env) {
+ auto& binding = BindingData::Get(env);
+ CHECK(!binding.in_nghttp3_callback_scope);
+ binding.in_nghttp3_callback_scope = true;
+}
+
+NgHttp3CallbackScope::~NgHttp3CallbackScope() {
+ auto& binding = BindingData::Get(env);
+ binding.in_nghttp3_callback_scope = false;
+}
+
+bool NgHttp3CallbackScope::in_nghttp3_callback(Environment* env) {
+ auto& binding = BindingData::Get(env);
+ return binding.in_nghttp3_callback_scope;
+}
+
+void IllegalConstructor(const FunctionCallbackInfo<Value>& args) {
+ THROW_ERR_ILLEGAL_CONSTRUCTOR(Environment::GetCurrent(args));
+}
+
} // namespace quic
} // namespace node
diff --git a/src/quic/bindingdata.h b/src/quic/bindingdata.h
index d22699ca4f..e9e8e719c4 100644
--- a/src/quic/bindingdata.h
+++ b/src/quic/bindingdata.h
@@ -12,11 +12,13 @@
#include <node.h>
#include <node_mem.h>
#include <v8.h>
+#include <vector>
namespace node {
namespace quic {
class Endpoint;
+class Packet;
enum class Side {
CLIENT = NGTCP2_CRYPTO_SIDE_CLIENT,
@@ -64,9 +66,17 @@ constexpr size_t kDefaultMaxPacketLength = NGTCP2_MAX_UDP_PAYLOAD_SIZE;
#define QUIC_STRINGS(V) \
V(ack_delay_exponent, "ackDelayExponent") \
V(active_connection_id_limit, "activeConnectionIDLimit") \
+ V(alpn, "alpn") \
+ V(ca, "ca") \
+ V(certs, "certs") \
+ V(crl, "crl") \
+ V(ciphers, "ciphers") \
V(disable_active_migration, "disableActiveMigration") \
+ V(enable_tls_trace, "tlsTrace") \
V(endpoint, "Endpoint") \
V(endpoint_udp, "Endpoint::UDP") \
+ V(groups, "groups") \
+ V(hostname, "hostname") \
V(http3_alpn, &NGHTTP3_ALPN_H3[1]) \
V(initial_max_data, "initialMaxData") \
V(initial_max_stream_data_bidi_local, "initialMaxStreamDataBidiLocal") \
@@ -74,13 +84,19 @@ constexpr size_t kDefaultMaxPacketLength = NGTCP2_MAX_UDP_PAYLOAD_SIZE;
V(initial_max_stream_data_uni, "initialMaxStreamDataUni") \
V(initial_max_streams_bidi, "initialMaxStreamsBidi") \
V(initial_max_streams_uni, "initialMaxStreamsUni") \
+ V(keylog, "keylog") \
+ V(keys, "keys") \
V(logstream, "LogStream") \
V(max_ack_delay, "maxAckDelay") \
V(max_datagram_frame_size, "maxDatagramFrameSize") \
V(max_idle_timeout, "maxIdleTimeout") \
V(packetwrap, "PacketWrap") \
+ V(reject_unauthorized, "rejectUnauthorized") \
+ V(request_peer_certificate, "requestPeerCertificate") \
V(session, "Session") \
- V(stream, "Stream")
+ V(session_id_ctx, "sessionIDContext") \
+ V(stream, "Stream") \
+ V(verify_hostname_identity, "verifyHostnameIdentity")
// =============================================================================
// The BindingState object holds state for the internalBinding('quic') binding
@@ -115,12 +131,14 @@ class BindingData final
// bridge out to the JS API.
static void SetCallbacks(const v8::FunctionCallbackInfo<v8::Value>& args);
- // TODO(@jasnell) This will be added when Endpoint is implemented.
- // // A set of listening Endpoints. We maintain this to ensure that the
- // Endpoint
- // // cannot be gc'd while it is still listening and there are active
- // // connections.
- // std::unordered_map<Endpoint*, BaseObjectPtr<Endpoint>> listening_endpoints;
+ std::vector<BaseObjectPtr<BaseObject>> packet_freelist;
+
+ // Purge the packet free list to free up memory.
+ static void FlushPacketFreelist(
+ const v8::FunctionCallbackInfo<v8::Value>& args);
+
+ bool in_ngtcp2_callback_scope = false;
+ bool in_nghttp3_callback_scope = false;
// The following set up various storage and accessors for common strings,
// construction templates, and callbacks stored on the BindingData. These
@@ -166,6 +184,25 @@ class BindingData final
#undef V
};
+void IllegalConstructor(const v8::FunctionCallbackInfo<v8::Value>& args);
+
+// The ngtcp2 and nghttp3 callbacks have certain restrictions
+// that forbid re-entry. We provide the following scopes for
+// use in those to help protect against it.
+struct NgTcp2CallbackScope {
+ Environment* env;
+ explicit NgTcp2CallbackScope(Environment* env);
+ ~NgTcp2CallbackScope();
+ static bool in_ngtcp2_callback(Environment* env);
+};
+
+struct NgHttp3CallbackScope {
+ Environment* env;
+ explicit NgHttp3CallbackScope(Environment* env);
+ ~NgHttp3CallbackScope();
+ static bool in_nghttp3_callback(Environment* env);
+};
+
} // namespace quic
} // namespace node
diff --git a/src/quic/defs.h b/src/quic/defs.h
index 3dbdd7ee25..6b8048d040 100644
--- a/src/quic/defs.h
+++ b/src/quic/defs.h
@@ -1,12 +1,28 @@
#pragma once
+#include <aliased_struct.h>
#include <env.h>
#include <node_errors.h>
+#include <uv.h>
#include <v8.h>
namespace node {
namespace quic {
+template <typename Opt, std::string 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()) {
+ Utf8Value utf8(env->isolate(), value);
+ options->*member = *utf8;
+ }
+ return true;
+}
+
template <typename Opt, bool Opt::*member>
bool SetOption(Environment* env,
Opt* options,
@@ -50,5 +66,37 @@ bool SetOption(Environment* env,
return true;
}
+// Utilities used to update the stats for Endpoint, Session, and Stream
+// objects. The stats themselves are maintained in an AliasedStruct within
+// each of the relevant classes.
+
+template <typename Stats, uint64_t Stats::*member>
+void IncrementStat(Stats* stats, uint64_t amt = 1) {
+ stats->*member += amt;
+}
+
+template <typename Stats, uint64_t Stats::*member>
+void RecordTimestampStat(Stats* stats) {
+ stats->*member = uv_hrtime();
+}
+
+template <typename Stats, uint64_t Stats::*member>
+void SetStat(Stats* stats, uint64_t val) {
+ stats->*member = val;
+}
+
+template <typename Stats, uint64_t Stats::*member>
+uint64_t GetStat(Stats* stats) {
+ return stats->*member;
+}
+
+#define STAT_INCREMENT(Type, name) IncrementStat<Type, &Type::name>(&stats_);
+#define STAT_INCREMENT_N(Type, name, amt) \
+ IncrementStat<Type, &Type::name>(&stats_, amt);
+#define STAT_RECORD_TIMESTAMP(Type, name) \
+ RecordTimestampStat<Type, &Type::name>(&stats_);
+#define STAT_SET(Type, name, val) SetStat<Type, &Type::name>(&stats_, val);
+#define STAT_GET(Type, name) GetStat<Type, &Type::name>(&stats_);
+
} // namespace quic
} // namespace node
diff --git a/src/quic/packet.cc b/src/quic/packet.cc
new file mode 100644
index 0000000000..27ba21d69a
--- /dev/null
+++ b/src/quic/packet.cc
@@ -0,0 +1,406 @@
+#if HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC
+
+#include "packet.h"
+#include <base_object-inl.h>
+#include <crypto/crypto_util.h>
+#include <env-inl.h>
+#include <ngtcp2/ngtcp2.h>
+#include <ngtcp2/ngtcp2_crypto.h>
+#include <node_sockaddr-inl.h>
+#include <req_wrap-inl.h>
+#include <uv.h>
+#include <v8.h>
+#include <string>
+#include "bindingdata.h"
+#include "cid.h"
+#include "tokens.h"
+
+namespace node {
+
+using v8::FunctionTemplate;
+using v8::Local;
+using v8::Object;
+
+namespace quic {
+
+namespace {
+static constexpr size_t kRandlen = NGTCP2_MIN_STATELESS_RESET_RANDLEN * 5;
+static constexpr size_t kMinStatelessResetLen = 41;
+static constexpr size_t kMaxFreeList = 100;
+} // namespace
+
+struct Packet::Data final : public MemoryRetainer {
+ MaybeStackBuffer<uint8_t, kDefaultMaxPacketLength> data_;
+
+ // The diagnostic_label_ is used only as a debugging tool when
+ // logging debug information about the packet. It identifies
+ // the purpose of the packet.
+ const std::string diagnostic_label_;
+
+ void MemoryInfo(MemoryTracker* tracker) const override {
+ tracker->TrackFieldWithSize("data", data_.length());
+ }
+ SET_MEMORY_INFO_NAME(Data)
+ SET_SELF_SIZE(Data)
+
+ Data(size_t length, std::string_view diagnostic_label)
+ : diagnostic_label_(diagnostic_label) {
+ data_.AllocateSufficientStorage(length);
+ }
+
+ size_t length() const { return data_.length(); }
+ operator uv_buf_t() {
+ return uv_buf_init(reinterpret_cast<char*>(data_.out()), data_.length());
+ }
+ operator ngtcp2_vec() { return ngtcp2_vec{data_.out(), data_.length()}; }
+
+ std::string ToString() const {
+ return diagnostic_label_ + ", " + std::to_string(length());
+ }
+};
+
+const SocketAddress& Packet::destination() const {
+ return destination_;
+}
+
+bool Packet::is_sending() const {
+ return !!handle_;
+}
+
+size_t Packet::length() const {
+ return data_ ? data_->length() : 0;
+}
+
+Packet::operator uv_buf_t() const {
+ return !data_ ? uv_buf_init(nullptr, 0) : *data_;
+}
+
+Packet::operator ngtcp2_vec() const {
+ return !data_ ? ngtcp2_vec{nullptr, 0} : *data_;
+}
+
+void Packet::Truncate(size_t len) {
+ DCHECK(data_);
+ DCHECK_LE(len, data_->length());
+ data_->data_.SetLength(len);
+}
+
+Local<FunctionTemplate> Packet::GetConstructorTemplate(Environment* env) {
+ auto& state = BindingData::Get(env);
+ Local<FunctionTemplate> tmpl = state.packet_constructor_template();
+ if (tmpl.IsEmpty()) {
+ tmpl = NewFunctionTemplate(env->isolate(), IllegalConstructor);
+ tmpl->Inherit(ReqWrap<uv_udp_send_t>::GetConstructorTemplate(env));
+ tmpl->InstanceTemplate()->SetInternalFieldCount(
+ Packet::kInternalFieldCount);
+ tmpl->SetClassName(state.packetwrap_string());
+ state.set_packet_constructor_template(tmpl);
+ }
+ return tmpl;
+}
+
+BaseObjectPtr<Packet> Packet::Create(Environment* env,
+ Listener* listener,
+ const SocketAddress& destination,
+ size_t length,
+ const char* diagnostic_label) {
+ auto& binding = BindingData::Get(env);
+ if (binding.packet_freelist.empty()) {
+ Local<Object> obj;
+ if (UNLIKELY(!GetConstructorTemplate(env)
+ ->InstanceTemplate()
+ ->NewInstance(env->context())
+ .ToLocal(&obj))) {
+ return BaseObjectPtr<Packet>();
+ }
+
+ return MakeBaseObject<Packet>(
+ env, listener, obj, destination, length, diagnostic_label);
+ }
+
+ return FromFreeList(env,
+ std::make_shared<Data>(length, diagnostic_label),
+ listener,
+ destination);
+}
+
+BaseObjectPtr<Packet> Packet::Clone() const {
+ auto& binding = BindingData::Get(env());
+ if (binding.packet_freelist.empty()) {
+ Local<Object> obj;
+ if (UNLIKELY(!GetConstructorTemplate(env())
+ ->InstanceTemplate()
+ ->NewInstance(env()->context())
+ .ToLocal(&obj))) {
+ return BaseObjectPtr<Packet>();
+ }
+
+ return MakeBaseObject<Packet>(env(), listener_, obj, destination_, data_);
+ }
+
+ return FromFreeList(env(), data_, listener_, destination_);
+}
+
+BaseObjectPtr<Packet> Packet::FromFreeList(Environment* env,
+ std::shared_ptr<Data> data,
+ Listener* listener,
+ const SocketAddress& destination) {
+ auto& binding = BindingData::Get(env);
+ auto obj = binding.packet_freelist.back();
+ binding.packet_freelist.pop_back();
+ DCHECK_EQ(env, obj->env());
+ auto packet = static_cast<Packet*>(obj.get());
+ packet->data_ = std::move(data);
+ packet->destination_ = destination;
+ packet->listener_ = listener;
+ return BaseObjectPtr<Packet>(packet);
+}
+
+Packet::Packet(Environment* env,
+ Listener* listener,
+ Local<Object> object,
+ const SocketAddress& destination,
+ std::shared_ptr<Data> data)
+ : ReqWrap<uv_udp_send_t>(env, object, AsyncWrap::PROVIDER_QUIC_PACKET),
+ listener_(listener),
+ destination_(destination),
+ data_(std::move(data)) {}
+
+Packet::Packet(Environment* env,
+ Listener* listener,
+ Local<Object> object,
+ const SocketAddress& destination,
+ size_t length,
+ const char* diagnostic_label)
+ : Packet(env,
+ listener,
+ object,
+ destination,
+ std::make_shared<Data>(length, diagnostic_label)) {}
+
+int Packet::Send(uv_udp_t* handle, BaseObjectPtr<BaseObject> ref) {
+ if (is_sending()) return UV_EALREADY;
+ if (data_ == nullptr) return UV_EINVAL;
+ DCHECK(!is_sending());
+ handle_ = std::move(ref);
+ uv_buf_t buf = *this;
+ return Dispatch(
+ uv_udp_send,
+ handle,
+ &buf,
+ 1,
+ destination().data(),
+ uv_udp_send_cb{[](uv_udp_send_t* req, int status) {
+ auto ptr = static_cast<Packet*>(ReqWrap<uv_udp_send_t>::from_req(req));
+ ptr->Done(status);
+ // Do not try accessing ptr after this. We don't know if it
+ // was freelisted or destroyed. Either way, done means done.
+ }});
+}
+
+void Packet::Done(int status) {
+ DCHECK_NOT_NULL(listener_);
+ listener_->PacketDone(status);
+ handle_.reset();
+ data_.reset();
+ listener_ = nullptr;
+ Reset();
+
+ // As a performance optimization, we add this packet to a freelist
+ // rather than deleting it but only if the freelist isn't too
+ // big, we don't want to accumulate these things forever.
+ auto& binding = BindingData::Get(env());
+ if (binding.packet_freelist.size() < kMaxFreeList) {
+ binding.packet_freelist.emplace_back(this);
+ } else {
+ delete this;
+ }
+}
+
+std::string Packet::ToString() const {
+ if (!data_) return "Packet (<empty>)";
+ return "Packet (" + data_->ToString() + ")";
+}
+
+void Packet::MemoryInfo(MemoryTracker* tracker) const {
+ tracker->TrackField("destination", destination_);
+ tracker->TrackField("data", data_);
+ tracker->TrackField("handle", handle_);
+}
+
+BaseObjectPtr<Packet> Packet::CreateRetryPacket(
+ Environment* env,
+ Listener* listener,
+ const PathDescriptor& path_descriptor,
+ const TokenSecret& token_secret) {
+ auto& random = CID::Factory::random();
+ CID cid = random.Generate();
+ RetryToken token(path_descriptor.version,
+ path_descriptor.remote_address,
+ cid,
+ path_descriptor.dcid,
+ token_secret);
+ if (!token) return BaseObjectPtr<Packet>();
+
+ const ngtcp2_vec& vec = token;
+
+ size_t pktlen =
+ vec.len + (2 * NGTCP2_MAX_CIDLEN) + path_descriptor.scid.length() + 8;
+
+ auto packet =
+ Create(env, listener, path_descriptor.remote_address, pktlen, "retry");
+ if (!packet) return BaseObjectPtr<Packet>();
+
+ ngtcp2_vec dest = *packet;
+
+ ssize_t nwrite = ngtcp2_crypto_write_retry(dest.base,
+ pktlen,
+ path_descriptor.version,
+ path_descriptor.scid,
+ cid,
+ path_descriptor.dcid,
+ vec.base,
+ vec.len);
+ if (nwrite <= 0) return BaseObjectPtr<Packet>();
+ packet->Truncate(static_cast<size_t>(nwrite));
+ return packet;
+}
+
+BaseObjectPtr<Packet> Packet::CreateConnectionClosePacket(
+ Environment* env,
+ Listener* listener,
+ const SocketAddress& destination,
+ ngtcp2_conn* conn,
+ const QuicError& error) {
+ auto packet = Packet::Create(
+ env, listener, destination, kDefaultMaxPacketLength, "connection close");
+ ngtcp2_vec vec = *packet;
+
+ ssize_t nwrite = ngtcp2_conn_write_connection_close(
+ conn, nullptr, nullptr, vec.base, vec.len, error, uv_hrtime());
+ if (nwrite < 0) return BaseObjectPtr<Packet>();
+ packet->Truncate(static_cast<size_t>(nwrite));
+ return packet;
+}
+
+BaseObjectPtr<Packet> Packet::CreateImmediateConnectionClosePacket(
+ Environment* env,
+ Listener* listener,
+ const SocketAddress& destination,
+ const PathDescriptor& path_descriptor,
+ const QuicError& reason) {
+ auto packet = Packet::Create(env,
+ listener,
+ path_descriptor.remote_address,
+ kDefaultMaxPacketLength,
+ "immediate connection close (endpoint)");
+ ngtcp2_vec vec = *packet;
+ ssize_t nwrite = ngtcp2_crypto_write_connection_close(
+ vec.base,
+ vec.len,
+ path_descriptor.version,
+ path_descriptor.dcid,
+ path_descriptor.scid,
+ reason.code(),
+ // We do not bother sending a reason string here, even if
+ // there is one in the QuicError
+ nullptr,
+ 0);
+ if (nwrite <= 0) return BaseObjectPtr<Packet>();
+ packet->Truncate(static_cast<size_t>(nwrite));
+ return packet;
+}
+
+BaseObjectPtr<Packet> Packet::CreateStatelessResetPacket(
+ Environment* env,
+ Listener* listener,
+ const PathDescriptor& path_descriptor,
+ const TokenSecret& token_secret,
+ size_t source_len) {
+ // Per the QUIC spec, a stateless reset token must be strictly smaller than
+ // the packet that triggered it. This is one of the mechanisms to prevent
+ // infinite looping exchange of stateless tokens with the peer. An endpoint
+ // should never send a stateless reset token smaller than 41 bytes per the
+ // QUIC spec. The reason is that packets less than 41 bytes may allow an
+ // observer to reliably determine that it's a stateless reset.
+ size_t pktlen = source_len - 1;
+ if (pktlen < kMinStatelessResetLen) return BaseObjectPtr<Packet>();
+
+ StatelessResetToken token(token_secret, path_descriptor.dcid);
+ uint8_t random[kRandlen];
+ CHECK(crypto::CSPRNG(random, kRandlen).is_ok());
+
+ auto packet = Packet::Create(env,
+ listener,
+ path_descriptor.remote_address,
+ kDefaultMaxPacketLength,
+ "stateless reset");
+ ngtcp2_vec vec = *packet;
+
+ ssize_t nwrite = ngtcp2_pkt_write_stateless_reset(
+ vec.base, pktlen, token, random, kRandlen);
+ if (nwrite <= static_cast<ssize_t>(kMinStatelessResetLen)) {
+ return BaseObjectPtr<Packet>();
+ }
+
+ packet->Truncate(static_cast<size_t>(nwrite));
+ return packet;
+}
+
+BaseObjectPtr<Packet> Packet::CreateVersionNegotiationPacket(
+ Environment* env,
+ Listener* listener,
+ const PathDescriptor& path_descriptor) {
+ const auto generateReservedVersion = [&] {
+ socklen_t addrlen = path_descriptor.remote_address.length();
+ uint32_t h = 0x811C9DC5u;
+ uint32_t ver = htonl(path_descriptor.version);
+ const uint8_t* p = path_descriptor.remote_address.raw();
+ const uint8_t* ep = p + addrlen;
+ for (; p != ep; ++p) {
+ h ^= *p;
+ h *= 0x01000193u;
+ }
+ p = reinterpret_cast<const uint8_t*>(&ver);
+ ep = p + sizeof(path_descriptor.version);
+ for (; p != ep; ++p) {
+ h ^= *p;
+ h *= 0x01000193u;
+ }
+ h &= 0xf0f0f0f0u;
+ h |= NGTCP2_RESERVED_VERSION_MASK;
+ return h;
+ };
+
+ uint32_t sv[3] = {
+ generateReservedVersion(), NGTCP2_PROTO_VER_MIN, NGTCP2_PROTO_VER_MAX};
+
+ size_t pktlen = path_descriptor.dcid.length() +
+ path_descriptor.scid.length() + (sizeof(sv)) + 7;
+
+ auto packet = Packet::Create(env,
+ listener,
+ path_descriptor.remote_address,
+ kDefaultMaxPacketLength,
+ "version negotiation");
+ ngtcp2_vec vec = *packet;
+
+ ssize_t nwrite =
+ ngtcp2_pkt_write_version_negotiation(vec.base,
+ pktlen,
+ 0,
+ path_descriptor.dcid,
+ path_descriptor.dcid.length(),
+ path_descriptor.scid,
+ path_descriptor.scid.length(),
+ sv,
+ arraysize(sv));
+ if (nwrite <= 0) return BaseObjectPtr<Packet>();
+ packet->Truncate(static_cast<size_t>(nwrite));
+ return packet;
+}
+
+} // namespace quic
+} // namespace node
+
+#endif // HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC
diff --git a/src/quic/packet.h b/src/quic/packet.h
new file mode 100644
index 0000000000..156174ebac
--- /dev/null
+++ b/src/quic/packet.h
@@ -0,0 +1,168 @@
+#pragma once
+
+#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
+#if HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC
+
+#include <base_object.h>
+#include <env.h>
+#include <ngtcp2/ngtcp2.h>
+#include <node_external_reference.h>
+#include <node_sockaddr.h>
+#include <req_wrap.h>
+#include <uv.h>
+#include <v8.h>
+#include <string>
+#include "bindingdata.h"
+#include "cid.h"
+#include "data.h"
+#include "tokens.h"
+
+namespace node {
+namespace quic {
+
+struct PathDescriptor {
+ uint32_t version;
+ const CID& dcid;
+ const CID& scid;
+ const SocketAddress& local_address;
+ const SocketAddress& remote_address;
+};
+
+// A Packet encapsulates serialized outbound QUIC data.
+// Packets must never be larger than the path MTU. The
+// default QUIC packet maximum length is 1200 bytes,
+// which we assume by default. The packet storage will
+// be stack allocated up to this size.
+//
+// Packets are maintained in a freelist held by the
+// BindingData instance. When using Create() to create
+// a Packet, we'll check to see if there is a free
+// packet in the freelist and use it instead of starting
+// fresh with a new packet. The freelist can store at
+// most kMaxFreeList packets
+//
+// Packets are always encrypted so their content should
+// be considered opaque to us. We leave it entirely up
+// to ngtcp2 how to encode QUIC frames into the packet.
+class Packet final : public ReqWrap<uv_udp_send_t> {
+ private:
+ struct Data;
+
+ public:
+ using Queue = std::deque<BaseObjectPtr<Packet>>;
+
+ static v8::Local<v8::FunctionTemplate> GetConstructorTemplate(
+ Environment* env);
+
+ class Listener {
+ public:
+ virtual void PacketDone(int status) = 0;
+ };
+
+ // Do not use the Packet constructors directly to create
+ // them. These are public only to support MakeBaseObject.
+ // Use the Create, or Create variants to create or
+ // acquire packet instances.
+
+ Packet(Environment* env,
+ Listener* listener,
+ v8::Local<v8::Object> object,
+ const SocketAddress& destination,
+ size_t length,
+ const char* diagnostic_label = "<unknown>");
+
+ Packet(Environment* env,
+ Listener* listener,
+ v8::Local<v8::Object> object,
+ const SocketAddress& destination,
+ std::shared_ptr<Data> data);
+
+ Packet(const Packet&) = delete;
+ Packet(Packet&&) = delete;
+ Packet& operator=(const Packet&) = delete;
+ Packet& operator=(Packet&&) = delete;
+
+ const SocketAddress& destination() const;
+ bool is_sending() const;
+ size_t length() const;
+ operator uv_buf_t() const;
+ operator ngtcp2_vec() const;
+
+ // Modify the size of the packet after ngtcp2 has written
+ // to it. len must be <= length(). We call this after we've
+ // asked ngtcp2 to encode frames into the packet and ngtcp2
+ // tells us how many of the packets bytes were used.
+ void Truncate(size_t len);
+
+ static BaseObjectPtr<Packet> Create(
+ Environment* env,
+ Listener* listener,
+ const SocketAddress& destination,
+ size_t length = kDefaultMaxPacketLength,
+ const char* diagnostic_label = "<unknown>");
+
+ BaseObjectPtr<Packet> Clone() const;
+
+ void MemoryInfo(MemoryTracker* tracker) const override;
+ SET_MEMORY_INFO_NAME(Packet)
+ SET_SELF_SIZE(Packet)
+
+ std::string ToString() const;
+
+ // Transmits the packet. The handle is the bound uv_udp_t
+ // port that we're sending on, the ref is a pointer to the
+ // HandleWrap that owns the handle.
+ int Send(uv_udp_t* handle, BaseObjectPtr<BaseObject> ref);
+
+ static BaseObjectPtr<Packet> CreateRetryPacket(
+ Environment* env,
+ Listener* listener,
+ const PathDescriptor& path_descriptor,
+ const TokenSecret& token_secret);
+
+ static BaseObjectPtr<Packet> CreateConnectionClosePacket(
+ Environment* env,
+ Listener* listener,
+ const SocketAddress& destination,
+ ngtcp2_conn* conn,
+ const QuicError& error);
+
+ static BaseObjectPtr<Packet> CreateImmediateConnectionClosePacket(
+ Environment* env,
+ Listener* listener,
+ const SocketAddress& destination,
+ const PathDescriptor& path_descriptor,
+ const QuicError& reason);
+
+ static BaseObjectPtr<Packet> CreateStatelessResetPacket(
+ Environment* env,
+ Listener* listener,
+ const PathDescriptor& path_descriptor,
+ const TokenSecret& token_secret,
+ size_t source_len);
+
+ static BaseObjectPtr<Packet> CreateVersionNegotiationPacket(
+ Environment* env,
+ Listener* listener,
+ const PathDescriptor& path_descriptor);
+
+ private:
+ static BaseObjectPtr<Packet> FromFreeList(Environment* env,
+ std::shared_ptr<Data> data,
+ Listener* listener,
+ const SocketAddress& destination);
+
+ // Called when the packet is done being sent.
+ void Done(int status);
+
+ Listener* listener_;
+ SocketAddress destination_;
+ std::shared_ptr<Data> data_;
+ BaseObjectPtr<BaseObject> handle_;
+};
+
+} // namespace quic
+} // namespace node
+
+#endif // HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC
+#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
diff --git a/src/quic/tlscontext.cc b/src/quic/tlscontext.cc
new file mode 100644
index 0000000000..b75f5c5631
--- /dev/null
+++ b/src/quic/tlscontext.cc
@@ -0,0 +1,589 @@
+#if HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC
+
+#include "tlscontext.h"
+#include "bindingdata.h"
+#include "defs.h"
+#include "transportparams.h"
+#include <base_object-inl.h>
+#include <env-inl.h>
+#include <memory_tracker-inl.h>
+#include <ngtcp2/ngtcp2.h>
+#include <ngtcp2/ngtcp2_crypto.h>
+#include <ngtcp2/ngtcp2_crypto_openssl.h>
+#include <openssl/ssl.h>
+#include <v8.h>
+
+namespace node {
+
+using v8::ArrayBuffer;
+using v8::BackingStore;
+using v8::Just;
+using v8::Local;
+using v8::Maybe;
+using v8::MaybeLocal;
+using v8::Nothing;
+using v8::Object;
+using v8::Value;
+
+namespace quic {
+
+// TODO(@jasnell): This session class is just a placeholder.
+// The real session impl will be added in a separate commit.
+class Session {
+ public:
+ operator ngtcp2_conn*() { return nullptr; }
+ void EmitKeylog(const char* line) const {}
+ void EmitSessionTicket(Store&& store) {}
+ void SetStreamOpenAllowed() {}
+ bool is_destroyed() const { return false; }
+ bool wants_session_ticket() const { return false; }
+};
+
+namespace {
+constexpr size_t kMaxAlpnLen = 255;
+
+int AllowEarlyDataCallback(SSL* ssl, void* arg) {
+ // Currently, we always allow early data. Later we might make
+ // it configurable.
+ return 1;
+}
+
+int NewSessionCallback(SSL* ssl, SSL_SESSION* session) {
+ // We use this event to trigger generation of the SessionTicket
+ // if the user has requested to receive it.
+ return TLSContext::From(ssl).OnNewSession(session);
+}
+
+void KeylogCallback(const SSL* ssl, const char* line) {
+ TLSContext::From(ssl).Keylog(line);
+}
+
+int AlpnSelectionCallback(SSL* ssl,
+ const unsigned char** out,
+ unsigned char* outlen,
+ const unsigned char* in,
+ unsigned int inlen,
+ void* arg) {
+ auto& context = TLSContext::From(ssl);
+
+ auto requested = context.options().alpn;
+ if (requested.length() > kMaxAlpnLen) return SSL_TLSEXT_ERR_NOACK;
+
+ // The Session supports exactly one ALPN identifier. If that does not match
+ // any of the ALPN identifiers provided in the client request, then we fail
+ // here. Note that this will not fail the TLS handshake, so we have to check
+ // later if the ALPN matches the expected identifier or not.
+ //
+ // We might eventually want to support the ability to negotiate multiple
+ // possible ALPN's on a single endpoint/session but for now, we only support
+ // one.
+ if (SSL_select_next_proto(
+ const_cast<unsigned char**>(out),
+ outlen,
+ reinterpret_cast<const unsigned char*>(requested.c_str()),
+ requested.length(),
+ in,
+ inlen) == OPENSSL_NPN_NO_OVERLAP) {
+ return SSL_TLSEXT_ERR_NOACK;
+ }
+
+ return SSL_TLSEXT_ERR_OK;
+}
+
+BaseObjectPtr<crypto::SecureContext> InitializeSecureContext(
+ Side side, Environment* env, const TLSContext::Options& options) {
+ auto context = crypto::SecureContext::Create(env);
+
+ auto& ctx = context->ctx();
+
+ switch (side) {
+ case Side::SERVER: {
+ ctx.reset(SSL_CTX_new(TLS_server_method()));
+ SSL_CTX_set_app_data(ctx.get(), context);
+
+ if (ngtcp2_crypto_openssl_configure_server_context(ctx.get()) != 0) {
+ return BaseObjectPtr<crypto::SecureContext>();
+ }
+
+ SSL_CTX_set_max_early_data(ctx.get(), UINT32_MAX);
+ SSL_CTX_set_allow_early_data_cb(
+ ctx.get(), AllowEarlyDataCallback, nullptr);
+ SSL_CTX_set_options(ctx.get(),
+ (SSL_OP_ALL & ~SSL_OP_DONT_INSERT_EMPTY_FRAGMENTS) |
+ SSL_OP_SINGLE_ECDH_USE |
+ SSL_OP_CIPHER_SERVER_PREFERENCE |
+ SSL_OP_NO_ANTI_REPLAY);
+ SSL_CTX_set_mode(ctx.get(), SSL_MODE_RELEASE_BUFFERS);
+ SSL_CTX_set_alpn_select_cb(ctx.get(), AlpnSelectionCallback, nullptr);
+ SSL_CTX_set_session_ticket_cb(ctx.get(),
+ SessionTicket::GenerateCallback,
+ SessionTicket::DecryptedCallback,
+ nullptr);
+
+ const unsigned char* sid_ctx = reinterpret_cast<const unsigned char*>(
+ options.session_id_ctx.c_str());
+ SSL_CTX_set_session_id_context(
+ ctx.get(), sid_ctx, options.session_id_ctx.length());
+
+ break;
+ }
+ case Side::CLIENT: {
+ ctx.reset(SSL_CTX_new(TLS_client_method()));
+ SSL_CTX_set_app_data(ctx.get(), context);
+
+ if (ngtcp2_crypto_openssl_configure_client_context(ctx.get()) != 0) {
+ return BaseObjectPtr<crypto::SecureContext>();
+ }
+
+ SSL_CTX_set_session_cache_mode(
+ ctx.get(), SSL_SESS_CACHE_CLIENT | SSL_SESS_CACHE_NO_INTERNAL_STORE);
+ SSL_CTX_sess_set_new_cb(ctx.get(), NewSessionCallback);
+ break;
+ }
+ default:
+ UNREACHABLE();
+ }
+
+ SSL_CTX_set_default_verify_paths(ctx.get());
+
+ if (options.keylog) SSL_CTX_set_keylog_callback(ctx.get(), KeylogCallback);
+
+ if (SSL_CTX_set_ciphersuites(ctx.get(), options.ciphers.c_str()) != 1) {
+ return BaseObjectPtr<crypto::SecureContext>();
+ }
+
+ if (SSL_CTX_set1_groups_list(ctx.get(), options.groups.c_str()) != 1) {
+ return BaseObjectPtr<crypto::SecureContext>();
+ }
+
+ // Handle CA certificates...
+
+ const auto addCACert = [&](uv_buf_t ca) {
+ crypto::ClearErrorOnReturn clear_error_on_return;
+ crypto::BIOPointer bio = crypto::NodeBIO::NewFixed(ca.base, ca.len);
+ if (!bio) return false;
+ context->SetCACert(bio);
+ return true;
+ };
+
+ const auto addRootCerts = [&] {
+ crypto::ClearErrorOnReturn clear_error_on_return;
+ context->SetRootCerts();
+ };
+
+ if (!options.ca.empty()) {
+ for (auto& ca : options.ca) {
+ if (!addCACert(ca)) {
+ return BaseObjectPtr<crypto::SecureContext>();
+ }
+ }
+ } else {
+ addRootCerts();
+ }
+
+ // Handle Certs
+
+ const auto addCert = [&](uv_buf_t cert) {
+ crypto::ClearErrorOnReturn clear_error_on_return;
+ crypto::BIOPointer bio = crypto::NodeBIO::NewFixed(cert.base, cert.len);
+ if (!bio) return Just(false);
+ auto ret = context->AddCert(env, std::move(bio));
+ return ret;
+ };
+
+ for (auto& cert : options.certs) {
+ if (!addCert(cert).IsJust()) {
+ return BaseObjectPtr<crypto::SecureContext>();
+ }
+ }
+
+ // Handle keys
+
+ const auto addKey = [&](auto& key) {
+ crypto::ClearErrorOnReturn clear_error_on_return;
+ return context->UseKey(env, key);
+ // TODO(@jasnell): Maybe SSL_CTX_check_private_key also?
+ };
+
+ for (auto& key : options.keys) {
+ if (!addKey(key).IsJust()) {
+ return BaseObjectPtr<crypto::SecureContext>();
+ }
+ }
+
+ // Handle CRL
+
+ const auto addCRL = [&](uv_buf_t crl) {
+ crypto::ClearErrorOnReturn clear_error_on_return;
+ crypto::BIOPointer bio = crypto::NodeBIO::NewFixed(crl.base, crl.len);
+ if (!bio) return Just(false);
+ return context->SetCRL(env, bio);
+ };
+
+ for (auto& crl : options.crl) {
+ if (!addCRL(crl).IsJust()) {
+ return BaseObjectPtr<crypto::SecureContext>();
+ }
+ }
+
+ // TODO(@jasnell): Possibly handle other bits. Such a pfx, client cert engine,
+ // and session timeout.
+ return BaseObjectPtr<crypto::SecureContext>(context);
+}
+
+void EnableTrace(Environment* env, crypto::BIOPointer* bio, SSL* ssl) {
+#if HAVE_SSL_TRACE
+ static bool warn_trace_tls = true;
+ if (warn_trace_tls) {
+ warn_trace_tls = false;
+ ProcessEmitWarning(env,
+ "Enabling --trace-tls can expose sensitive data in "
+ "the resulting log");
+ }
+ if (!*bio) {
+ bio->reset(BIO_new_fp(stderr, BIO_NOCLOSE | BIO_FP_TEXT));
+ SSL_set_msg_callback(
+ ssl,
+ [](int write_p,
+ int version,
+ int content_type,
+ const void* buf,
+ size_t len,
+ SSL* ssl,
+ void* arg) -> void {
+ crypto::MarkPopErrorOnReturn mark_pop_error_on_return;
+ SSL_trace(write_p, version, content_type, buf, len, ssl, arg);
+ });
+ SSL_set_msg_callback_arg(ssl, bio->get());
+ }
+#endif
+}
+
+template <typename T, typename Opt, std::vector<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;
+
+ // The value can be either a single item or an array of items.
+
+ if (value->IsArray()) {
+ auto context = env->context();
+ auto values = value.As<v8::Array>();
+ uint32_t count = values->Length();
+ for (uint32_t n = 0; n < count; n++) {
+ v8::Local<v8::Value> item;
+ if (!values->Get(context, n).ToLocal(&item)) {
+ return false;
+ }
+ if constexpr (std::is_same<T, std::shared_ptr<crypto::KeyObjectData>>::
+ value) {
+ if (crypto::KeyObjectHandle::HasInstance(env, item)) {
+ crypto::KeyObjectHandle* handle;
+ ASSIGN_OR_RETURN_UNWRAP(&handle, item, false);
+ (options->*member).push_back(handle->Data());
+ } else {
+ return false;
+ }
+ } else if constexpr (std::is_same<T, Store>::value) {
+ if (item->IsArrayBufferView()) {
+ (options->*member).emplace_back(item.As<v8::ArrayBufferView>());
+ } else if (item->IsArrayBuffer()) {
+ (options->*member).emplace_back(item.As<v8::ArrayBuffer>());
+ } else {
+ return false;
+ }
+ }
+ }
+ } else {
+ if constexpr (std::is_same<T,
+ std::shared_ptr<crypto::KeyObjectData>>::value) {
+ if (crypto::KeyObjectHandle::HasInstance(env, value)) {
+ crypto::KeyObjectHandle* handle;
+ ASSIGN_OR_RETURN_UNWRAP(&handle, value, false);
+ (options->*member).push_back(handle->Data());
+ } else {
+ return false;
+ }
+ } else if constexpr (std::is_same<T, Store>::value) {
+ if (value->IsArrayBufferView()) {
+ (options->*member).emplace_back(value.As<v8::ArrayBufferView>());
+ } else if (value->IsArrayBuffer()) {
+ (options->*member).emplace_back(value.As<v8::ArrayBuffer>());
+ } else {
+ return false;
+ }
+ }
+ }
+ return true;
+}
+} // namespace
+
+Side TLSContext::side() const {
+ return side_;
+}
+
+const TLSContext::Options& TLSContext::options() const {
+ return options_;
+}
+
+inline const TLSContext& TLSContext::From(const SSL* ssl) {
+ auto ref = static_cast<ngtcp2_crypto_conn_ref*>(SSL_get_app_data(ssl));
+ TLSContext* context = ContainerOf(&TLSContext::conn_ref_, ref);
+ return *context;
+}
+
+inline TLSContext& TLSContext::From(SSL* ssl) {
+ auto ref = static_cast<ngtcp2_crypto_conn_ref*>(SSL_get_app_data(ssl));
+ TLSContext* context = ContainerOf(&TLSContext::conn_ref_, ref);
+ return *context;
+}
+
+TLSContext::TLSContext(Environment* env,
+ Side side,
+ Session* session,
+ const Options& options)
+ : conn_ref_({getConnection, this}),
+ side_(side),
+ env_(env),
+ session_(session),
+ options_(options),
+ secure_context_(InitializeSecureContext(side, env, options)) {
+ CHECK(secure_context_);
+ ssl_.reset(SSL_new(secure_context_->ctx().get()));
+ CHECK(ssl_ && SSL_is_quic(ssl_.get()));
+
+ SSL_set_app_data(ssl_.get(), &conn_ref_);
+ SSL_set_verify(ssl_.get(), SSL_VERIFY_NONE, crypto::VerifyCallback);
+
+ // Enable tracing if the `--trace-tls` command line flag is used.
+ if (UNLIKELY(env->options()->trace_tls || options.enable_tls_trace))
+ EnableTrace(env, &bio_trace_, ssl_.get());
+
+ switch (side) {
+ case Side::CLIENT: {
+ SSL_set_connect_state(ssl_.get());
+ CHECK_EQ(0,
+ SSL_set_alpn_protos(ssl_.get(),
+ reinterpret_cast<const unsigned char*>(
+ options_.alpn.c_str()),
+ options_.alpn.length()));
+ CHECK_EQ(0,
+ SSL_set_tlsext_host_name(ssl_.get(), options_.hostname.c_str()));
+ break;
+ }
+ case Side::SERVER: {
+ SSL_set_accept_state(ssl_.get());
+ if (options.request_peer_certificate) {
+ int verify_mode = SSL_VERIFY_PEER;
+ if (options.reject_unauthorized)
+ verify_mode |= SSL_VERIFY_FAIL_IF_NO_PEER_CERT;
+ SSL_set_verify(ssl_.get(), verify_mode, crypto::VerifyCallback);
+ }
+ break;
+ }
+ default:
+ UNREACHABLE();
+ }
+}
+
+void TLSContext::Start() {
+ ngtcp2_conn_set_tls_native_handle(*session_, ssl_.get());
+
+ TransportParams tp(TransportParams::Type::ENCRYPTED_EXTENSIONS,
+ ngtcp2_conn_get_local_transport_params(*session_));
+ Store store = tp.Encode(env_);
+ if (store && store.length() > 0) {
+ ngtcp2_vec vec = store;
+ SSL_set_quic_transport_params(ssl_.get(), vec.base, vec.len);
+ }
+}
+
+void TLSContext::Keylog(const char* line) const {
+ session_->EmitKeylog(line);
+}
+
+int TLSContext::Receive(ngtcp2_crypto_level crypto_level,
+ uint64_t offset,
+ const ngtcp2_vec& vec) {
+ // ngtcp2 provides an implementation of this in
+ // ngtcp2_crypto_recv_crypto_data_cb but given that we are using the
+ // implementation specific error codes below, we can't use it.
+
+ if (UNLIKELY(session_->is_destroyed())) return NGTCP2_ERR_CALLBACK_FAILURE;
+
+ // Internally, this passes the handshake data off to openssl for processing.
+ // The handshake may or may not complete.
+ int ret = ngtcp2_crypto_read_write_crypto_data(
+ *session_, crypto_level, vec.base, vec.len);
+
+ switch (ret) {
+ case 0:
+ // Fall-through
+
+ // In either of following cases, the handshake is being paused waiting for
+ // user code to take action (for instance OCSP requests or client hello
+ // modification)
+ case NGTCP2_CRYPTO_OPENSSL_ERR_TLS_WANT_X509_LOOKUP:
+ [[fallthrough]];
+ case NGTCP2_CRYPTO_OPENSSL_ERR_TLS_WANT_CLIENT_HELLO_CB:
+ return 0;
+ }
+ return ret;
+}
+
+int TLSContext::OnNewSession(SSL_SESSION* session) {
+ // Used to generate and emit a SessionTicket for TLS session resumption.
+
+ // If there is nothing listening for the session ticket, don't both emitting.
+ if (!session_->wants_session_ticket()) return 0;
+
+ // Pre-fight to see how much space we need to allocate for the session ticket.
+ size_t size = i2d_SSL_SESSION(session, nullptr);
+
+ if (size > 0 && size < crypto::SecureContext::kMaxSessionSize) {
+ // Generate the actual ticket. If this fails, we'll simply carry on without
+ // emitting the ticket.
+ std::shared_ptr<BackingStore> ticket =
+ ArrayBuffer::NewBackingStore(env_->isolate(), size);
+ unsigned char* data = reinterpret_cast<unsigned char*>(ticket->Data());
+ if (i2d_SSL_SESSION(session, &data) <= 0) return 0;
+ session_->EmitSessionTicket(Store(std::move(ticket), size));
+ }
+ // If size == 0, there's no session ticket data to emit. Let's ignore it
+ // and continue without emitting the sessionticket event.
+
+ return 0;
+}
+
+bool TLSContext::InitiateKeyUpdate() {
+ if (session_->is_destroyed() || in_key_update_) return false;
+ auto leave = OnScopeLeave([this] { in_key_update_ = false; });
+ in_key_update_ = true;
+
+ return ngtcp2_conn_initiate_key_update(*session_, uv_hrtime()) == 0;
+}
+
+int TLSContext::VerifyPeerIdentity() {
+ return crypto::VerifyPeerCertificate(ssl_);
+}
+
+void TLSContext::MaybeSetEarlySession(const SessionTicket& sessionTicket) {
+ TransportParams rtp(TransportParams::Type::ENCRYPTED_EXTENSIONS,
+ sessionTicket.transport_params());
+
+ // Ignore invalid remote transport parameters.
+ if (!rtp) return;
+
+ uv_buf_t buf = sessionTicket.ticket();
+ crypto::SSLSessionPointer ticket = crypto::GetTLSSession(
+ reinterpret_cast<unsigned char*>(buf.base), buf.len);
+
+ // Silently ignore invalid TLS session
+ if (!ticket || !SSL_SESSION_get_max_early_data(ticket.get())) return;
+
+ // The early data will just be ignored if it's invalid.
+ if (crypto::SetTLSSession(ssl_, ticket)) {
+ ngtcp2_conn_set_early_remote_transport_params(*session_, rtp);
+ session_->SetStreamOpenAllowed();
+ }
+}
+
+void TLSContext::MemoryInfo(MemoryTracker* tracker) const {
+ tracker->TrackField("options", options_);
+ tracker->TrackField("secure_context", secure_context_);
+}
+
+MaybeLocal<Object> TLSContext::cert(Environment* env) const {
+ return crypto::X509Certificate::GetCert(env, ssl_);
+}
+
+MaybeLocal<Object> TLSContext::peer_cert(Environment* env) const {
+ crypto::X509Certificate::GetPeerCertificateFlag flag =
+ side_ == Side::SERVER
+ ? crypto::X509Certificate::GetPeerCertificateFlag::SERVER
+ : crypto::X509Certificate::GetPeerCertificateFlag::NONE;
+ return crypto::X509Certificate::GetPeerCert(env, ssl_, flag);
+}
+
+MaybeLocal<Value> TLSContext::cipher_name(Environment* env) const {
+ return crypto::GetCurrentCipherName(env, ssl_);
+}
+
+MaybeLocal<Value> TLSContext::cipher_version(Environment* env) const {
+ return crypto::GetCurrentCipherVersion(env, ssl_);
+}
+
+MaybeLocal<Object> TLSContext::ephemeral_key(Environment* env) const {
+ return crypto::GetEphemeralKey(env, ssl_);
+}
+
+const std::string_view TLSContext::servername() const {
+ const char* servername = crypto::GetServerName(ssl_.get());
+ return servername != nullptr ? std::string_view(servername)
+ : std::string_view();
+}
+
+const std::string_view TLSContext::alpn() const {
+ const unsigned char* alpn_buf = nullptr;
+ unsigned int alpnlen;
+ SSL_get0_alpn_selected(ssl_.get(), &alpn_buf, &alpnlen);
+ return alpnlen ? std::string_view(reinterpret_cast<const char*>(alpn_buf),
+ alpnlen)
+ : std::string_view();
+}
+
+bool TLSContext::early_data_was_accepted() const {
+ return (early_data_ &&
+ SSL_get_early_data_status(ssl_.get()) == SSL_EARLY_DATA_ACCEPTED);
+}
+
+void TLSContext::Options::MemoryInfo(MemoryTracker* tracker) const {
+ tracker->TrackField("keys", keys);
+ tracker->TrackField("certs", certs);
+ tracker->TrackField("ca", ca);
+ tracker->TrackField("crl", crl);
+}
+
+ngtcp2_conn* TLSContext::getConnection(ngtcp2_crypto_conn_ref* ref) {
+ TLSContext* context = ContainerOf(&TLSContext::conn_ref_, ref);
+ return *context->session_;
+}
+
+Maybe<const TLSContext::Options> TLSContext::Options::From(Environment* env,
+ Local<Value> value) {
+ if (value.IsEmpty() || !value->IsObject()) {
+ return Nothing<const Options>();
+ }
+
+ auto& state = BindingData::Get(env);
+ auto params = value.As<Object>();
+ Options options;
+
+#define SET_VECTOR(Type, name) \
+ SetOption<Type, TLSContext::Options, &TLSContext::Options::name>( \
+ env, &options, params, state.name##_string())
+
+#define SET(name) \
+ SetOption<TLSContext::Options, &TLSContext::Options::name>( \
+ env, &options, params, state.name##_string())
+
+ if (!SET(keylog) || !SET(reject_unauthorized) || !SET(enable_tls_trace) ||
+ !SET(request_peer_certificate) || !SET(verify_hostname_identity) ||
+ !SET(alpn) || !SET(hostname) || !SET(session_id_ctx) || !SET(ciphers) ||
+ !SET(groups) ||
+ !SET_VECTOR(std::shared_ptr<crypto::KeyObjectData>, keys) ||
+ !SET_VECTOR(Store, certs) || !SET_VECTOR(Store, ca) ||
+ !SET_VECTOR(Store, crl)) {
+ return Nothing<const Options>();
+ }
+
+ return Just<const Options>(options);
+}
+
+} // namespace quic
+} // namespace node
+
+#endif // HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC
diff --git a/src/quic/tlscontext.h b/src/quic/tlscontext.h
new file mode 100644
index 0000000000..588c3e7f25
--- /dev/null
+++ b/src/quic/tlscontext.h
@@ -0,0 +1,176 @@
+#pragma once
+
+#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
+#if HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC
+
+#include <base_object.h>
+#include <crypto/crypto_context.h>
+#include <crypto/crypto_keys.h>
+#include <memory_tracker.h>
+#include <ngtcp2/ngtcp2_crypto.h>
+#include "bindingdata.h"
+#include "data.h"
+#include "sessionticket.h"
+
+namespace node {
+namespace quic {
+
+class Session;
+
+// Every QUIC Session has exactly one TLSContext that maintains the state
+// of the TLS handshake and negotiated cipher keys after the handshake has
+// been completed. It is separated out from the main Session class only as a
+// convenience to help make the code more maintainable and understandable.
+class TLSContext final : public MemoryRetainer {
+ public:
+ static constexpr auto DEFAULT_CIPHERS = "TLS_AES_128_GCM_SHA256:"
+ "TLS_AES_256_GCM_SHA384:"
+ "TLS_CHACHA20_POLY1305_"
+ "SHA256:TLS_AES_128_CCM_SHA256";
+ static constexpr auto DEFAULT_GROUPS = "X25519:P-256:P-384:P-521";
+
+ static inline const TLSContext& From(const SSL* ssl);
+ static inline TLSContext& From(SSL* ssl);
+
+ struct Options final : public MemoryRetainer {
+ // The protocol identifier to be used by this Session.
+ std::string alpn = NGHTTP3_ALPN_H3;
+
+ // The SNI hostname to be used. This is used only by client Sessions to
+ // identify the SNI host in the TLS client hello message.
+ std::string hostname = "";
+
+ // When true, TLS keylog data will be emitted to the JavaScript session.
+ bool keylog = false;
+
+ // When set, the peer certificate is verified against the list of supplied
+ // CAs. If verification fails, the connection will be refused.
+ bool reject_unauthorized = true;
+
+ // When set, enables TLS tracing for the session. This should only be used
+ // for debugging.
+ bool enable_tls_trace = false;
+
+ // Options only used by server sessions:
+
+ // When set, instructs the server session to request a client authentication
+ // certificate.
+ bool request_peer_certificate = false;
+
+ // Options only used by client sessions:
+
+ // When set, instructs the client session to verify the hostname default.
+ // This is required by QUIC and enabled by default. We allow disabling it
+ // only for debugging.
+ bool verify_hostname_identity = true;
+
+ // The TLS session ID context (only used on the server)
+ std::string session_id_ctx = "Node.js QUIC Server";
+
+ // TLS cipher suite
+ std::string ciphers = DEFAULT_CIPHERS;
+
+ // TLS groups
+ std::string groups = DEFAULT_GROUPS;
+
+ // The TLS private key to use for this session.
+ std::vector<std::shared_ptr<crypto::KeyObjectData>> keys;
+
+ // Collection of certificates to use for this session.
+ std::vector<Store> certs;
+
+ // Optional certificate authority overrides to use.
+ std::vector<Store> ca;
+
+ // Optional certificate revocation lists to use.
+ std::vector<Store> crl;
+
+ void MemoryInfo(MemoryTracker* tracker) const override;
+ SET_MEMORY_INFO_NAME(CryptoContext::Options)
+ SET_SELF_SIZE(Options)
+
+ static v8::Maybe<const Options> From(Environment* env,
+ v8::Local<v8::Value> value);
+ };
+
+ static const Options kDefaultOptions;
+
+ TLSContext(Environment* env,
+ Side side,
+ Session* session,
+ const Options& options);
+ TLSContext(const TLSContext&) = delete;
+ TLSContext(TLSContext&&) = delete;
+ TLSContext& operator=(const TLSContext&) = delete;
+ TLSContext& operator=(TLSContext&&) = delete;
+
+ // Start the TLS handshake.
+ void Start();
+
+ // TLS Keylogging is enabled per-Session by attaching a handler to the
+ // "keylog" event. Each keylog line is emitted to JavaScript where it can be
+ // routed to whatever destination makes sense. Typically, this will be to a
+ // keylog file that can be consumed by tools like Wireshark to intercept and
+ // decrypt QUIC network traffic.
+ void Keylog(const char* line) const;
+
+ // Called when a chunk of peer TLS handshake data is received. For every
+ // chunk, we move the TLS handshake further along until it is complete.
+ int Receive(ngtcp2_crypto_level crypto_level,
+ uint64_t offset,
+ const ngtcp2_vec& vec);
+
+ v8::MaybeLocal<v8::Object> cert(Environment* env) const;
+ v8::MaybeLocal<v8::Object> peer_cert(Environment* env) const;
+ v8::MaybeLocal<v8::Value> cipher_name(Environment* env) const;
+ v8::MaybeLocal<v8::Value> cipher_version(Environment* env) const;
+ v8::MaybeLocal<v8::Object> ephemeral_key(Environment* env) const;
+
+ // The SNI servername negotiated for the session
+ const std::string_view servername() const;
+
+ // The ALPN (protocol name) negotiated for the session
+ const std::string_view alpn() const;
+
+ // Triggers key update to begin. This will fail and return false if either a
+ // previous key update is in progress and has not been confirmed or if the
+ // initial handshake has not yet been confirmed.
+ bool InitiateKeyUpdate();
+
+ int VerifyPeerIdentity();
+
+ Side side() const;
+ const Options& options() const;
+
+ int OnNewSession(SSL_SESSION* session);
+
+ void MaybeSetEarlySession(const SessionTicket& sessionTicket);
+ bool early_data_was_accepted() const;
+
+ void MemoryInfo(MemoryTracker* tracker) const override;
+ SET_MEMORY_INFO_NAME(CryptoContext)
+ SET_SELF_SIZE(TLSContext)
+
+ private:
+ static ngtcp2_conn* getConnection(ngtcp2_crypto_conn_ref* ref);
+ ngtcp2_crypto_conn_ref conn_ref_;
+
+ Side side_;
+ Environment* env_;
+ Session* session_;
+ const Options options_;
+ BaseObjectPtr<crypto::SecureContext> secure_context_;
+ crypto::SSLPointer ssl_;
+ crypto::BIOPointer bio_trace_;
+
+ bool in_key_update_ = false;
+ bool early_data_ = false;
+
+ friend class Session;
+};
+
+} // namespace quic
+} // namespace node
+
+#endif // HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC
+#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS