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>
This commit is contained in:
James M Snell 2023-03-31 17:58:57 -07:00
parent 17c9aa2f76
commit 1c3d741cb0
11 changed files with 1484 additions and 8 deletions

View File

@ -341,16 +341,20 @@
'src/quic/cid.cc',
'src/quic/data.cc',
'src/quic/logstream.cc',
'src/quic/packet.cc',
'src/quic/preferredaddress.cc',
'src/quic/sessionticket.cc',
'src/quic/tlscontext.cc',
'src/quic/tokens.cc',
'src/quic/transportparams.cc',
'src/quic/bindingdata.h',
'src/quic/cid.h',
'src/quic/data.h',
'src/quic/logstream.h',
'src/quic/packet.h',
'src/quic/preferredaddress.h',
'src/quic/sessionticket.h',
'src/quic/tlscontext.h',
'src/quic/tokens.h',
'src/quic/transportparams.h',
],

View File

@ -61,6 +61,7 @@ namespace node {
V(PROMISE) \
V(QUERYWRAP) \
V(QUIC_LOGSTREAM) \
V(QUIC_PACKET) \
V(SHUTDOWNWRAP) \
V(SIGNALWRAP) \
V(STATWATCHER) \

View File

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

View File

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

View File

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

View File

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

406
src/quic/packet.cc Normal file
View File

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

168
src/quic/packet.h Normal file
View File

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

589
src/quic/tlscontext.cc Normal file
View File

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

176
src/quic/tlscontext.h Normal file
View File

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

View File

@ -67,6 +67,7 @@ const { getSystemErrorName } = require('util');
delete providers.RANDOMPRIMEREQUEST;
delete providers.CHECKPRIMEREQUEST;
delete providers.QUIC_LOGSTREAM;
delete providers.QUIC_PACKET;
const objKeys = Object.keys(providers);
if (objKeys.length > 0)