quic: rework TLSContext, additional cleanups

PR-URL: https://github.com/nodejs/node/pull/51340
Reviewed-By: Yagiz Nizipli <yagiz.nizipli@sentry.io>
Reviewed-By: Franziska Hinkelmann <franziska.hinkelmann@gmail.com>
Reviewed-By: Stephen Belanger <admin@stephenbelanger.com>
This commit is contained in:
James M Snell 2023-12-30 16:45:40 -08:00
parent 3f74b68e39
commit 06a3a2a1fb
28 changed files with 1269 additions and 924 deletions

View file

@ -415,6 +415,7 @@
'test/cctest/test_node_crypto.cc', 'test/cctest/test_node_crypto.cc',
'test/cctest/test_node_crypto_env.cc', 'test/cctest/test_node_crypto_env.cc',
'test/cctest/test_quic_cid.cc', 'test/cctest/test_quic_cid.cc',
'test/cctest/test_quic_error.cc',
'test/cctest/test_quic_tokens.cc', 'test/cctest/test_quic_tokens.cc',
], ],
'node_cctest_inspector_sources': [ 'node_cctest_inspector_sources': [

View file

@ -53,7 +53,7 @@ static const char system_cert_path[] = NODE_OPENSSL_SYSTEM_CERT_PATH;
static bool extra_root_certs_loaded = false; static bool extra_root_certs_loaded = false;
inline X509_STORE* GetOrCreateRootCertStore() { X509_STORE* GetOrCreateRootCertStore() {
// Guaranteed thread-safe by standard, just don't use -fno-threadsafe-statics. // Guaranteed thread-safe by standard, just don't use -fno-threadsafe-statics.
static X509_STORE* store = NewRootCertStore(); static X509_STORE* store = NewRootCertStore();
return store; return store;
@ -140,6 +140,8 @@ int SSL_CTX_use_certificate_chain(SSL_CTX* ctx,
return ret; return ret;
} }
} // namespace
// Read a file that contains our certificate in "PEM" format, // Read a file that contains our certificate in "PEM" format,
// possibly followed by a sequence of CA certificates that should be // possibly followed by a sequence of CA certificates that should be
// sent to the peer in the Certificate message. // sent to the peer in the Certificate message.
@ -194,8 +196,6 @@ int SSL_CTX_use_certificate_chain(SSL_CTX* ctx,
issuer); issuer);
} }
} // namespace
X509_STORE* NewRootCertStore() { X509_STORE* NewRootCertStore() {
static std::vector<X509*> root_certs_vector; static std::vector<X509*> root_certs_vector;
static Mutex root_certs_vector_mutex; static Mutex root_certs_vector_mutex;

View file

@ -24,6 +24,8 @@ void IsExtraRootCertsFileLoaded(
X509_STORE* NewRootCertStore(); X509_STORE* NewRootCertStore();
X509_STORE* GetOrCreateRootCertStore();
BIOPointer LoadBIO(Environment* env, v8::Local<v8::Value> v); BIOPointer LoadBIO(Environment* env, v8::Local<v8::Value> v);
class SecureContext final : public BaseObject { class SecureContext final : public BaseObject {
@ -153,6 +155,11 @@ class SecureContext final : public BaseObject {
unsigned char ticket_key_hmac_[16]; unsigned char ticket_key_hmac_[16];
}; };
int SSL_CTX_use_certificate_chain(SSL_CTX* ctx,
BIOPointer&& in,
X509Pointer* cert,
X509Pointer* issuer);
} // namespace crypto } // namespace crypto
} // namespace node } // namespace node

View file

@ -3,6 +3,7 @@
#include "application.h" #include "application.h"
#include <async_wrap-inl.h> #include <async_wrap-inl.h>
#include <debug_utils-inl.h> #include <debug_utils-inl.h>
#include <ngtcp2/ngtcp2.h>
#include <node_bob.h> #include <node_bob.h>
#include <node_sockaddr-inl.h> #include <node_sockaddr-inl.h>
#include <uv.h> #include <uv.h>
@ -95,6 +96,20 @@ Maybe<Session::Application_Options> Session::Application_Options::From(
return Just<Application_Options>(options); return Just<Application_Options>(options);
} }
// ============================================================================
std::string Session::Application::StreamData::ToString() const {
DebugIndentScope indent;
auto prefix = indent.Prefix();
std::string res("{");
res += prefix + "count: " + std::to_string(count);
res += prefix + "remaining: " + std::to_string(remaining);
res += prefix + "id: " + std::to_string(id);
res += prefix + "fin: " + std::to_string(fin);
res += indent.Close();
return res;
}
Session::Application::Application(Session* session, const Options& options) Session::Application::Application(Session* session, const Options& options)
: session_(session) {} : session_(session) {}
@ -189,7 +204,7 @@ Packet* Session::Application::CreateStreamDataPacket() {
return Packet::Create(env(), return Packet::Create(env(),
session_->endpoint_.get(), session_->endpoint_.get(),
session_->remote_address_, session_->remote_address_,
ngtcp2_conn_get_max_tx_udp_payload_size(*session_), session_->max_packet_size(),
"stream data"); "stream data");
} }
@ -221,141 +236,188 @@ void Session::Application::StreamReset(Stream* stream,
} }
void Session::Application::SendPendingData() { void Session::Application::SendPendingData() {
static constexpr size_t kMaxPackets = 32;
Debug(session_, "Application sending pending data"); Debug(session_, "Application sending pending data");
PathStorage path; PathStorage path;
StreamData stream_data;
// The maximum size of packet to create.
const size_t max_packet_size = session_->max_packet_size();
// The maximum number of packets to send in this call to SendPendingData.
const size_t max_packet_count = std::min(
kMaxPackets, ngtcp2_conn_get_send_quantum(*session_) / max_packet_size);
// The number of packets that have been sent in this call to SendPendingData.
size_t packet_send_count = 0;
Packet* packet = nullptr; Packet* packet = nullptr;
uint8_t* pos = nullptr; uint8_t* pos = nullptr;
int err = 0; uint8_t* begin = nullptr;
size_t maxPacketCount = std::min(static_cast<size_t>(64000), auto ensure_packet = [&] {
ngtcp2_conn_get_send_quantum(*session_)); if (packet == nullptr) {
size_t packetSendCount = 0; packet = CreateStreamDataPacket();
if (packet == nullptr) return false;
const auto updateTimer = [&] { pos = begin = ngtcp2_vec(*packet).base;
Debug(session_, "Application updating the session timer");
ngtcp2_conn_update_pkt_tx_time(*session_, uv_hrtime());
session_->UpdateTimer();
};
const auto congestionLimited = [&](auto packet) {
auto len = pos - ngtcp2_vec(*packet).base;
// We are either congestion limited or done.
if (len) {
// Some data was serialized into the packet. We need to send it.
packet->Truncate(len);
session_->Send(std::move(packet), path);
} }
DCHECK_NOT_NULL(packet);
updateTimer(); DCHECK_NOT_NULL(pos);
DCHECK_NOT_NULL(begin);
return true;
}; };
// We're going to enter a loop here to prepare and send no more than
// max_packet_count packets.
for (;;) { for (;;) {
ssize_t ndatalen; // ndatalen is the amount of stream data that was accepted into the packet.
StreamData stream_data; ssize_t ndatalen = 0;
err = GetStreamData(&stream_data); // Make sure we have a packet to write data into.
if (!ensure_packet()) {
if (err < 0) { Debug(session_, "Failed to create packet for stream data");
// Doh! Could not create a packet. Time to bail.
session_->last_error_ = QuicError::ForNgtcp2Error(NGTCP2_ERR_INTERNAL); session_->last_error_ = QuicError::ForNgtcp2Error(NGTCP2_ERR_INTERNAL);
return session_->Close(Session::CloseMethod::SILENT); return session_->Close(Session::CloseMethod::SILENT);
} }
if (packet == nullptr) { // The stream_data is the next block of data from the application stream.
packet = CreateStreamDataPacket(); if (GetStreamData(&stream_data) < 0) {
if (packet == nullptr) { Debug(session_, "Application failed to get stream data");
session_->last_error_ = QuicError::ForNgtcp2Error(NGTCP2_ERR_INTERNAL); session_->last_error_ = QuicError::ForNgtcp2Error(NGTCP2_ERR_INTERNAL);
return session_->Close(Session::CloseMethod::SILENT); packet->Done(UV_ECANCELED);
} return session_->Close(Session::CloseMethod::SILENT);
pos = ngtcp2_vec(*packet).base;
} }
ssize_t nwrite = WriteVStream(&path, pos, &ndatalen, stream_data); // If we got here, we were at least successful in checking for stream data.
// There might not be any stream data to send.
Debug(session_, "Application using stream data: %s", stream_data);
if (nwrite <= 0) { // Awesome, let's write our packet!
ssize_t nwrite =
WriteVStream(&path, pos, &ndatalen, max_packet_size, stream_data);
Debug(session_, "Application accepted %zu bytes into packet", ndatalen);
// A negative nwrite value indicates either an error or that there is more
// data to write into the packet.
if (nwrite < 0) {
switch (nwrite) { switch (nwrite) {
case 0:
if (stream_data.id >= 0) ResumeStream(stream_data.id);
return congestionLimited(std::move(packet));
case NGTCP2_ERR_STREAM_DATA_BLOCKED: { case NGTCP2_ERR_STREAM_DATA_BLOCKED: {
session().StreamDataBlocked(stream_data.id); // We could not write any data for this stream into the packet because
if (session().max_data_left() == 0) { // the flow control for the stream itself indicates that the stream
if (stream_data.id >= 0) ResumeStream(stream_data.id); // is blocked. We'll skip and move on to the next stream.
return congestionLimited(std::move(packet)); // ndatalen = -1 means that no stream data was accepted into the
} // packet, which is what we want here.
CHECK_LE(ndatalen, 0); DCHECK_EQ(ndatalen, -1);
DCHECK(stream_data.stream);
session_->StreamDataBlocked(stream_data.id);
continue; continue;
} }
case NGTCP2_ERR_STREAM_SHUT_WR: { case NGTCP2_ERR_STREAM_SHUT_WR: {
// Indicates that the writable side of the stream has been closed // Indicates that the writable side of the stream should be closed
// locally or the stream is being reset. In either case, we can't send // locally or the stream is being reset. In either case, we can't send
// any stream data! // any stream data!
CHECK_GE(stream_data.id, 0); Debug(session_,
// We need to notify the stream that the writable side has been closed "Stream %" PRIi64 " should be closed for writing",
// and no more outbound data can be sent. stream_data.id);
CHECK_LE(ndatalen, 0); // ndatalen = -1 means that no stream data was accepted into the
auto stream = session_->FindStream(stream_data.id); // packet, which is what we want here.
if (stream) stream->EndWritable(); DCHECK_EQ(ndatalen, -1);
DCHECK(stream_data.stream);
stream_data.stream->EndWritable();
continue; continue;
} }
case NGTCP2_ERR_WRITE_MORE: { case NGTCP2_ERR_WRITE_MORE: {
CHECK_GT(ndatalen, 0); // This return value indicates that we should call into WriteVStream
if (!StreamCommit(&stream_data, ndatalen)) return session_->Close(); // again to write more data into the same packet.
pos += ndatalen; Debug(session_, "Application should write more to packet");
DCHECK_GE(ndatalen, 0);
if (!StreamCommit(&stream_data, ndatalen)) {
packet->Done(UV_ECANCELED);
return session_->Close(CloseMethod::SILENT);
}
continue; continue;
} }
} }
// Some other type of error happened.
DCHECK_EQ(ndatalen, -1);
Debug(session_,
"Application encountered error while writing packet: %s",
ngtcp2_strerror(nwrite));
session_->SetLastError(QuicError::ForNgtcp2Error(nwrite));
packet->Done(UV_ECANCELED); packet->Done(UV_ECANCELED);
session_->last_error_ = QuicError::ForNgtcp2Error(nwrite);
return session_->Close(Session::CloseMethod::SILENT); return session_->Close(Session::CloseMethod::SILENT);
} else if (ndatalen >= 0) {
// We wrote some data into the packet. We need to update the flow control
// by committing the data.
if (!StreamCommit(&stream_data, ndatalen)) {
packet->Done(UV_ECANCELED);
return session_->Close(CloseMethod::SILENT);
}
} }
// When nwrite is zero, it means we are congestion limited.
// We should stop trying to send additional packets.
if (nwrite == 0) {
Debug(session_, "Congestion limited.");
// There might be a partial packet already prepared. If so, send it.
size_t datalen = pos - begin;
if (datalen) {
Debug(session_, "Packet has %zu bytes to send", datalen);
// At least some data had been written into the packet. We should send
// it.
packet->Truncate(datalen);
session_->Send(packet, path);
} else {
packet->Done(UV_ECANCELED);
}
// If there was stream data selected, we should reschedule it to try
// sending again.
if (stream_data.id >= 0) ResumeStream(stream_data.id);
return session_->UpdatePacketTxTime();
}
// At this point we have a packet prepared to send.
pos += nwrite; pos += nwrite;
if (ndatalen > 0 && !StreamCommit(&stream_data, ndatalen)) { size_t datalen = pos - begin;
// Since we are closing the session here, we don't worry about updating Debug(session_, "Sending packet with %zu bytes", datalen);
// the pkt tx time. The failed StreamCommit should have updated the packet->Truncate(datalen);
// last_error_ appropriately. session_->Send(packet, path);
packet->Done(UV_ECANCELED);
return session_->Close(Session::CloseMethod::SILENT); // If we have sent the maximum number of packets, we're done.
if (++packet_send_count == max_packet_count) {
return session_->UpdatePacketTxTime();
} }
if (stream_data.id >= 0 && ndatalen < 0) ResumeStream(stream_data.id); // Prepare to loop back around to prepare a new packet.
packet = nullptr;
packet->Truncate(nwrite); pos = begin = nullptr;
session_->Send(std::move(packet), path);
pos = nullptr;
if (++packetSendCount == maxPacketCount) {
break;
}
} }
updateTimer();
} }
ssize_t Session::Application::WriteVStream(PathStorage* path, ssize_t Session::Application::WriteVStream(PathStorage* path,
uint8_t* buf, uint8_t* dest,
ssize_t* ndatalen, ssize_t* ndatalen,
size_t max_packet_size,
const StreamData& stream_data) { const StreamData& stream_data) {
CHECK_LE(stream_data.count, kMaxVectorCount); DCHECK_LE(stream_data.count, kMaxVectorCount);
uint32_t flags = NGTCP2_WRITE_STREAM_FLAG_NONE; uint32_t flags = NGTCP2_WRITE_STREAM_FLAG_MORE;
if (stream_data.remaining > 0) flags |= NGTCP2_WRITE_STREAM_FLAG_MORE;
if (stream_data.fin) flags |= NGTCP2_WRITE_STREAM_FLAG_FIN; if (stream_data.fin) flags |= NGTCP2_WRITE_STREAM_FLAG_FIN;
ssize_t ret = ngtcp2_conn_writev_stream( ngtcp2_pkt_info pi;
*session_, return ngtcp2_conn_writev_stream(*session_,
&path->path, &path->path,
nullptr, &pi,
buf, dest,
ngtcp2_conn_get_max_tx_udp_payload_size(*session_), max_packet_size,
ndatalen, ndatalen,
flags, flags,
stream_data.id, stream_data.id,
stream_data.buf, stream_data.buf,
stream_data.count, stream_data.count,
uv_hrtime()); uv_hrtime());
return ret;
} }
// The DefaultApplication is the default implementation of Session::Application // The DefaultApplication is the default implementation of Session::Application

View file

@ -1,9 +1,11 @@
#pragma once #pragma once
#include "quic/defs.h"
#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS #if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
#if HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC #if HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC
#include "bindingdata.h" #include "bindingdata.h"
#include "defs.h"
#include "session.h" #include "session.h"
#include "sessionticket.h" #include "sessionticket.h"
#include "streams.h" #include "streams.h"
@ -18,10 +20,7 @@ class Session::Application : public MemoryRetainer {
using Options = Session::Application_Options; using Options = Session::Application_Options;
Application(Session* session, const Options& options); Application(Session* session, const Options& options);
Application(const Application&) = delete; DISALLOW_COPY_AND_MOVE(Application)
Application(Application&&) = delete;
Application& operator=(const Application&) = delete;
Application& operator=(Application&&) = delete;
virtual bool Start(); virtual bool Start();
@ -115,26 +114,26 @@ class Session::Application : public MemoryRetainer {
// the default stream priority. // the default stream priority.
virtual StreamPriority GetStreamPriority(const Stream& stream); virtual StreamPriority GetStreamPriority(const Stream& stream);
protected:
inline Environment* env() const { return session_->env(); }
inline Session& session() { return *session_; }
inline const Session& session() const { return *session_; }
Packet* CreateStreamDataPacket();
struct StreamData; struct StreamData;
virtual int GetStreamData(StreamData* data) = 0; virtual int GetStreamData(StreamData* data) = 0;
virtual bool StreamCommit(StreamData* data, size_t datalen) = 0; virtual bool StreamCommit(StreamData* data, size_t datalen) = 0;
virtual bool ShouldSetFin(const StreamData& data) = 0; virtual bool ShouldSetFin(const StreamData& data) = 0;
inline Environment* env() const { return session_->env(); }
inline Session& session() { return *session_; }
inline const Session& session() const { return *session_; }
private:
Packet* CreateStreamDataPacket();
// Write the given stream_data into the buffer. // Write the given stream_data into the buffer.
ssize_t WriteVStream(PathStorage* path, ssize_t WriteVStream(PathStorage* path,
uint8_t* buf, uint8_t* buf,
ssize_t* ndatalen, ssize_t* ndatalen,
size_t max_packet_size,
const StreamData& stream_data); const StreamData& stream_data);
private:
Session* session_; Session* session_;
}; };
@ -151,6 +150,8 @@ struct Session::Application::StreamData final {
BaseObjectPtr<Stream> stream; BaseObjectPtr<Stream> stream;
inline operator nghttp3_vec() const { return {data[0].base, data[0].len}; } inline operator nghttp3_vec() const { return {data[0].base, data[0].len}; }
std::string ToString() const;
}; };
} // namespace quic } // namespace quic

View file

@ -12,9 +12,9 @@
#include <node.h> #include <node.h>
#include <node_mem.h> #include <node_mem.h>
#include <v8.h> #include <v8.h>
#include <limits>
#include <unordered_map> #include <unordered_map>
#include <vector> #include <vector>
#include "defs.h"
namespace node { namespace node {
namespace quic { namespace quic {
@ -22,61 +22,6 @@ namespace quic {
class Endpoint; class Endpoint;
class Packet; class Packet;
enum class Side {
CLIENT,
SERVER,
};
enum class EndpointLabel {
LOCAL,
REMOTE,
};
enum class Direction {
BIDIRECTIONAL,
UNIDIRECTIONAL,
};
enum class HeadersKind {
HINTS,
INITIAL,
TRAILING,
};
enum class HeadersFlags {
NONE,
TERMINAL,
};
enum class StreamPriority {
DEFAULT = NGHTTP3_DEFAULT_URGENCY,
LOW = NGHTTP3_URGENCY_LOW,
HIGH = NGHTTP3_URGENCY_HIGH,
};
enum class StreamPriorityFlags {
NONE,
NON_INCREMENTAL,
};
enum class PathValidationResult : uint8_t {
SUCCESS = NGTCP2_PATH_VALIDATION_RESULT_SUCCESS,
FAILURE = NGTCP2_PATH_VALIDATION_RESULT_FAILURE,
ABORTED = NGTCP2_PATH_VALIDATION_RESULT_ABORTED,
};
enum class DatagramStatus {
ACKNOWLEDGED,
LOST,
};
constexpr uint64_t NGTCP2_APP_NOERROR = 65280;
constexpr size_t kDefaultMaxPacketLength = NGTCP2_MAX_UDP_PAYLOAD_SIZE;
constexpr size_t kMaxSizeT = std::numeric_limits<size_t>::max();
constexpr uint64_t kMaxSafeJsInteger = 9007199254740991;
constexpr auto kSocketAddressInfoTimeout = 60 * NGTCP2_SECONDS;
constexpr size_t kMaxVectorCount = 16;
// ============================================================================ // ============================================================================
// The FunctionTemplates the BindingData will store for us. // The FunctionTemplates the BindingData will store for us.
@ -135,7 +80,6 @@ constexpr size_t kMaxVectorCount = 16;
V(failure, "failure") \ V(failure, "failure") \
V(groups, "groups") \ V(groups, "groups") \
V(handshake_timeout, "handshakeTimeout") \ V(handshake_timeout, "handshakeTimeout") \
V(hostname, "hostname") \
V(http3_alpn, &NGHTTP3_ALPN_H3[1]) \ V(http3_alpn, &NGHTTP3_ALPN_H3[1]) \
V(initial_max_data, "initialMaxData") \ V(initial_max_data, "initialMaxData") \
V(initial_max_stream_data_bidi_local, "initialMaxStreamDataBidiLocal") \ V(initial_max_stream_data_bidi_local, "initialMaxStreamDataBidiLocal") \
@ -172,11 +116,10 @@ constexpr size_t kMaxVectorCount = 16;
V(reject_unauthorized, "rejectUnauthorized") \ V(reject_unauthorized, "rejectUnauthorized") \
V(reno, "reno") \ V(reno, "reno") \
V(retry_token_expiration, "retryTokenExpiration") \ V(retry_token_expiration, "retryTokenExpiration") \
V(request_peer_certificate, "requestPeerCertificate") \
V(reset_token_secret, "resetTokenSecret") \ V(reset_token_secret, "resetTokenSecret") \
V(rx_loss, "rxDiagnosticLoss") \ V(rx_loss, "rxDiagnosticLoss") \
V(session, "Session") \ V(session, "Session") \
V(session_id_ctx, "sessionIDContext") \ V(sni, "sni") \
V(stream, "Stream") \ V(stream, "Stream") \
V(success, "success") \ V(success, "success") \
V(tls_options, "tls") \ V(tls_options, "tls") \
@ -189,7 +132,8 @@ constexpr size_t kMaxVectorCount = 16;
V(udp_ttl, "udpTTL") \ V(udp_ttl, "udpTTL") \
V(unacknowledged_packet_threshold, "unacknowledgedPacketThreshold") \ V(unacknowledged_packet_threshold, "unacknowledgedPacketThreshold") \
V(validate_address, "validateAddress") \ V(validate_address, "validateAddress") \
V(verify_hostname_identity, "verifyHostnameIdentity") \ V(verify_client, "verifyClient") \
V(verify_private_key, "verifyPrivateKey") \
V(version, "version") V(version, "version")
// ============================================================================= // =============================================================================
@ -209,6 +153,7 @@ class BindingData final
static BindingData& Get(Environment* env); static BindingData& Get(Environment* env);
BindingData(Realm* realm, v8::Local<v8::Object> object); BindingData(Realm* realm, v8::Local<v8::Object> object);
DISALLOW_COPY_AND_MOVE(BindingData)
void MemoryInfo(MemoryTracker* tracker) const override; void MemoryInfo(MemoryTracker* tracker) const override;
SET_MEMORY_INFO_NAME(BindingData) SET_MEMORY_INFO_NAME(BindingData)
@ -288,6 +233,7 @@ void IllegalConstructor(const v8::FunctionCallbackInfo<v8::Value>& args);
struct NgTcp2CallbackScope { struct NgTcp2CallbackScope {
Environment* env; Environment* env;
explicit NgTcp2CallbackScope(Environment* env); explicit NgTcp2CallbackScope(Environment* env);
DISALLOW_COPY_AND_MOVE(NgTcp2CallbackScope)
~NgTcp2CallbackScope(); ~NgTcp2CallbackScope();
static bool in_ngtcp2_callback(Environment* env); static bool in_ngtcp2_callback(Environment* env);
}; };
@ -295,6 +241,7 @@ struct NgTcp2CallbackScope {
struct NgHttp3CallbackScope { struct NgHttp3CallbackScope {
Environment* env; Environment* env;
explicit NgHttp3CallbackScope(Environment* env); explicit NgHttp3CallbackScope(Environment* env);
DISALLOW_COPY_AND_MOVE(NgHttp3CallbackScope)
~NgHttp3CallbackScope(); ~NgHttp3CallbackScope();
static bool in_nghttp3_callback(Environment* env); static bool in_nghttp3_callback(Environment* env);
}; };
@ -305,10 +252,7 @@ struct CallbackScopeBase {
v8::TryCatch try_catch; v8::TryCatch try_catch;
explicit CallbackScopeBase(Environment* env); explicit CallbackScopeBase(Environment* env);
CallbackScopeBase(const CallbackScopeBase&) = delete; DISALLOW_COPY_AND_MOVE(CallbackScopeBase)
CallbackScopeBase(CallbackScopeBase&&) = delete;
CallbackScopeBase& operator=(const CallbackScopeBase&) = delete;
CallbackScopeBase& operator=(CallbackScopeBase&&) = delete;
~CallbackScopeBase(); ~CallbackScopeBase();
}; };
@ -319,6 +263,7 @@ struct CallbackScope final : public CallbackScopeBase {
BaseObjectPtr<T> ref; BaseObjectPtr<T> ref;
explicit CallbackScope(const T* ptr) explicit CallbackScope(const T* ptr)
: CallbackScopeBase(ptr->env()), ref(ptr) {} : CallbackScopeBase(ptr->env()), ref(ptr) {}
DISALLOW_COPY_AND_MOVE(CallbackScope)
explicit CallbackScope(T* ptr) : CallbackScopeBase(ptr->env()), ref(ptr) {} explicit CallbackScope(T* ptr) : CallbackScopeBase(ptr->env()), ref(ptr) {}
}; };

View file

@ -4,6 +4,7 @@
#include <memory_tracker-inl.h> #include <memory_tracker-inl.h>
#include <node_mutex.h> #include <node_mutex.h>
#include <string_bytes.h> #include <string_bytes.h>
#include "quic/defs.h"
namespace node { namespace node {
namespace quic { namespace quic {
@ -99,10 +100,7 @@ namespace {
class RandomCIDFactory : public CID::Factory { class RandomCIDFactory : public CID::Factory {
public: public:
RandomCIDFactory() = default; RandomCIDFactory() = default;
RandomCIDFactory(const RandomCIDFactory&) = delete; DISALLOW_COPY_AND_MOVE(RandomCIDFactory)
RandomCIDFactory(RandomCIDFactory&&) = delete;
RandomCIDFactory& operator=(const RandomCIDFactory&) = delete;
RandomCIDFactory& operator=(RandomCIDFactory&&) = delete;
CID Generate(size_t length_hint) const override { CID Generate(size_t length_hint) const override {
DCHECK_GE(length_hint, CID::kMinLength); DCHECK_GE(length_hint, CID::kMinLength);
@ -114,8 +112,8 @@ class RandomCIDFactory : public CID::Factory {
return CID(start, length_hint); return CID(start, length_hint);
} }
void GenerateInto(ngtcp2_cid* cid, CID GenerateInto(ngtcp2_cid* cid,
size_t length_hint = CID::kMaxLength) const override { size_t length_hint = CID::kMaxLength) const override {
DCHECK_GE(length_hint, CID::kMinLength); DCHECK_GE(length_hint, CID::kMinLength);
DCHECK_LE(length_hint, CID::kMaxLength); DCHECK_LE(length_hint, CID::kMaxLength);
Mutex::ScopedLock lock(mutex_); Mutex::ScopedLock lock(mutex_);
@ -123,6 +121,7 @@ class RandomCIDFactory : public CID::Factory {
auto start = pool_ + pos_; auto start = pool_ + pos_;
pos_ += length_hint; pos_ += length_hint;
ngtcp2_cid_init(cid, start, length_hint); ngtcp2_cid_init(cid, start, length_hint);
return CID(cid);
} }
private: private:

View file

@ -5,6 +5,7 @@
#include <memory_tracker.h> #include <memory_tracker.h>
#include <ngtcp2/ngtcp2.h> #include <ngtcp2/ngtcp2.h>
#include <string> #include <string>
#include "defs.h"
namespace node { namespace node {
namespace quic { namespace quic {
@ -50,9 +51,8 @@ class CID final : public MemoryRetainer {
explicit CID(const ngtcp2_cid* cid); explicit CID(const ngtcp2_cid* cid);
CID(const CID& other); CID(const CID& other);
CID(CID&& other) = delete;
CID& operator=(const CID& other); CID& operator=(const CID& other);
CID(CID&&) = delete;
struct Hash final { struct Hash final {
size_t operator()(const CID& cid) const; size_t operator()(const CID& cid) const;
@ -111,10 +111,9 @@ class CID::Factory {
virtual CID Generate(size_t length_hint = CID::kMaxLength) const = 0; virtual CID Generate(size_t length_hint = CID::kMaxLength) const = 0;
// Generate a new CID into the given ngtcp2_cid. This variation of // Generate a new CID into the given ngtcp2_cid. This variation of
// Generate should be used far less commonly. It is provided largely // Generate should be used far less commonly.
// for a couple of internal cases. virtual CID GenerateInto(ngtcp2_cid* cid,
virtual void GenerateInto(ngtcp2_cid* cid, size_t length_hint = CID::kMaxLength) const = 0;
size_t length_hint = CID::kMaxLength) const = 0;
// The default random CID generator instance. // The default random CID generator instance.
static const Factory& random(); static const Factory& random();

View file

@ -48,12 +48,28 @@ std::string Path::ToString() const {
} }
PathStorage::PathStorage() { PathStorage::PathStorage() {
ngtcp2_path_storage_zero(this); Reset();
} }
PathStorage::operator ngtcp2_path() { PathStorage::operator ngtcp2_path() {
return path; return path;
} }
void PathStorage::Reset() {
ngtcp2_path_storage_zero(this);
}
void PathStorage::CopyTo(PathStorage* path) const {
ngtcp2_path_copy(&path->path, &this->path);
}
bool PathStorage::operator==(const PathStorage& other) const {
return ngtcp2_path_eq(&path, &other.path) != 0;
}
bool PathStorage::operator!=(const PathStorage& other) const {
return ngtcp2_path_eq(&path, &other.path) == 0;
}
// ============================================================================ // ============================================================================
Store::Store(std::shared_ptr<v8::BackingStore> store, Store::Store(std::shared_ptr<v8::BackingStore> store,
@ -146,7 +162,7 @@ std::string TypeName(QuicError::Type type) {
} }
} // namespace } // namespace
QuicError::QuicError(const std::string_view reason) QuicError::QuicError(const std::string& reason)
: reason_(reason), error_(), ptr_(&error_) { : reason_(reason), error_(), ptr_(&error_) {
ngtcp2_ccerr_default(&error_); ngtcp2_ccerr_default(&error_);
} }
@ -186,7 +202,7 @@ QuicError::Type QuicError::type() const {
return static_cast<Type>(ptr_->type); return static_cast<Type>(ptr_->type);
} }
QuicError::error_code QuicError::code() const { error_code QuicError::code() const {
return ptr_->error_code; return ptr_->error_code;
} }
@ -206,6 +222,39 @@ QuicError::operator const ngtcp2_ccerr*() const {
return ptr_; return ptr_;
} }
std::string QuicError::reason_for_liberr(int liberr) {
return ngtcp2_strerror(liberr);
}
std::string QuicError::reason_for_h3_liberr(int liberr) {
return nghttp3_strerror(liberr);
}
bool QuicError::is_fatal_liberror(int liberr) {
return ngtcp2_err_is_fatal(liberr) != 0;
}
bool QuicError::is_fatal_h3_liberror(int liberr) {
return nghttp3_err_is_fatal(liberr) != 0;
}
error_code QuicError::liberr_to_code(int liberr) {
return ngtcp2_err_infer_quic_transport_error_code(liberr);
}
error_code QuicError::h3_liberr_to_code(int liberr) {
return nghttp3_err_infer_quic_app_error_code(liberr);
}
bool QuicError::is_crypto() const {
return code() & NGTCP2_CRYPTO_ERROR;
}
std::optional<int> QuicError::crypto_error() const {
if (!is_crypto()) return std::nullopt;
return code() & ~NGTCP2_CRYPTO_ERROR;
}
MaybeLocal<Value> QuicError::ToV8Value(Environment* env) const { MaybeLocal<Value> QuicError::ToV8Value(Environment* env) const {
Local<Value> argv[] = { Local<Value> argv[] = {
Integer::New(env->isolate(), static_cast<int>(type())), Integer::New(env->isolate(), static_cast<int>(type())),
@ -217,6 +266,7 @@ MaybeLocal<Value> QuicError::ToV8Value(Environment* env) const {
!node::ToV8Value(env->context(), reason()).ToLocal(&argv[2])) { !node::ToV8Value(env->context(), reason()).ToLocal(&argv[2])) {
return MaybeLocal<Value>(); return MaybeLocal<Value>();
} }
return Array::New(env->isolate(), argv, arraysize(argv)).As<Value>(); return Array::New(env->isolate(), argv, arraysize(argv)).As<Value>();
} }
@ -232,39 +282,37 @@ void QuicError::MemoryInfo(MemoryTracker* tracker) const {
tracker->TrackField("reason", reason_.length()); tracker->TrackField("reason", reason_.length());
} }
QuicError QuicError::ForTransport(error_code code, QuicError QuicError::ForTransport(error_code code, std::string reason) {
const std::string_view reason) { QuicError error(std::move(reason));
QuicError error(reason);
ngtcp2_ccerr_set_transport_error( ngtcp2_ccerr_set_transport_error(
&error.error_, code, error.reason_c_str(), reason.length()); &error.error_, code, error.reason_c_str(), reason.length());
return error; return error;
} }
QuicError QuicError::ForApplication(error_code code, QuicError QuicError::ForApplication(error_code code, std::string reason) {
const std::string_view reason) { QuicError error(std::move(reason));
QuicError error(reason);
ngtcp2_ccerr_set_application_error( ngtcp2_ccerr_set_application_error(
&error.error_, code, error.reason_c_str(), reason.length()); &error.error_, code, error.reason_c_str(), reason.length());
return error; return error;
} }
QuicError QuicError::ForVersionNegotiation(const std::string_view reason) { QuicError QuicError::ForVersionNegotiation(std::string reason) {
return ForNgtcp2Error(NGTCP2_ERR_RECV_VERSION_NEGOTIATION, reason); return ForNgtcp2Error(NGTCP2_ERR_RECV_VERSION_NEGOTIATION, std::move(reason));
} }
QuicError QuicError::ForIdleClose(const std::string_view reason) { QuicError QuicError::ForIdleClose(std::string reason) {
return ForNgtcp2Error(NGTCP2_ERR_IDLE_CLOSE, reason); return ForNgtcp2Error(NGTCP2_ERR_IDLE_CLOSE, std::move(reason));
} }
QuicError QuicError::ForNgtcp2Error(int code, const std::string_view reason) { QuicError QuicError::ForNgtcp2Error(int code, std::string reason) {
QuicError error(reason); QuicError error(std::move(reason));
ngtcp2_ccerr_set_liberr( ngtcp2_ccerr_set_liberr(
&error.error_, code, error.reason_c_str(), reason.length()); &error.error_, code, error.reason_c_str(), reason.length());
return error; return error;
} }
QuicError QuicError::ForTlsAlert(int code, const std::string_view reason) { QuicError QuicError::ForTlsAlert(int code, std::string reason) {
QuicError error(reason); QuicError error(std::move(reason));
ngtcp2_ccerr_set_tls_alert( ngtcp2_ccerr_set_tls_alert(
&error.error_, code, error.reason_c_str(), reason.length()); &error.error_, code, error.reason_c_str(), reason.length());
return error; return error;

View file

@ -1,6 +1,5 @@
#pragma once #pragma once
#include <string>
#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS #if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
#if HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC #if HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC
@ -11,6 +10,8 @@
#include <node_internals.h> #include <node_internals.h>
#include <node_sockaddr.h> #include <node_sockaddr.h>
#include <v8.h> #include <v8.h>
#include <string>
#include "defs.h"
namespace node { namespace node {
namespace quic { namespace quic {
@ -24,6 +25,12 @@ struct Path final : public ngtcp2_path {
struct PathStorage final : public ngtcp2_path_storage { struct PathStorage final : public ngtcp2_path_storage {
PathStorage(); PathStorage();
operator ngtcp2_path(); operator ngtcp2_path();
void Reset();
void CopyTo(PathStorage* path) const;
bool operator==(const PathStorage& other) const;
bool operator!=(const PathStorage& other) const;
}; };
class Store final : public MemoryRetainer { class Store final : public MemoryRetainer {
@ -67,8 +74,6 @@ class Store final : public MemoryRetainer {
class QuicError final : public MemoryRetainer { class QuicError final : public MemoryRetainer {
public: public:
using error_code = uint64_t;
static constexpr error_code QUIC_NO_ERROR = NGTCP2_NO_ERROR; static constexpr error_code QUIC_NO_ERROR = NGTCP2_NO_ERROR;
static constexpr error_code QUIC_APP_NO_ERROR = 65280; static constexpr error_code QUIC_APP_NO_ERROR = 65280;
@ -79,12 +84,7 @@ class QuicError final : public MemoryRetainer {
IDLE_CLOSE = NGTCP2_CCERR_TYPE_IDLE_CLOSE, IDLE_CLOSE = NGTCP2_CCERR_TYPE_IDLE_CLOSE,
}; };
static constexpr error_code QUIC_ERROR_TYPE_TRANSPORT = explicit QuicError(const std::string& reason = "");
NGTCP2_CCERR_TYPE_TRANSPORT;
static constexpr error_code QUIC_ERROR_TYPE_APPLICATION =
NGTCP2_CCERR_TYPE_APPLICATION;
explicit QuicError(const std::string_view reason = "");
explicit QuicError(const ngtcp2_ccerr* ptr); explicit QuicError(const ngtcp2_ccerr* ptr);
explicit QuicError(const ngtcp2_ccerr& error); explicit QuicError(const ngtcp2_ccerr& error);
@ -100,6 +100,9 @@ class QuicError final : public MemoryRetainer {
// transport or application. // transport or application.
operator bool() const; operator bool() const;
bool is_crypto() const;
std::optional<int> crypto_error() const;
bool operator==(const QuicError& other) const; bool operator==(const QuicError& other) const;
bool operator!=(const QuicError& other) const; bool operator!=(const QuicError& other) const;
@ -110,14 +113,19 @@ class QuicError final : public MemoryRetainer {
std::string ToString() const; std::string ToString() const;
v8::MaybeLocal<v8::Value> ToV8Value(Environment* env) const; v8::MaybeLocal<v8::Value> ToV8Value(Environment* env) const;
static QuicError ForTransport(error_code code, static std::string reason_for_liberr(int liberr);
const std::string_view reason = ""); static std::string reason_for_h3_liberr(int liberr);
static QuicError ForApplication(error_code code, static bool is_fatal_liberror(int liberr);
const std::string_view reason = ""); static bool is_fatal_h3_liberror(int liberr);
static QuicError ForVersionNegotiation(const std::string_view reason = ""); static error_code liberr_to_code(int liberr);
static QuicError ForIdleClose(const std::string_view reason = ""); static error_code h3_liberr_to_code(int liberr);
static QuicError ForNgtcp2Error(int code, const std::string_view reason = "");
static QuicError ForTlsAlert(int code, const std::string_view reason = ""); static QuicError ForTransport(error_code code, std::string reason = "");
static QuicError ForApplication(error_code code, std::string reason = "");
static QuicError ForVersionNegotiation(std::string reason = "");
static QuicError ForIdleClose(std::string reason = "");
static QuicError ForNgtcp2Error(int code, std::string reason = "");
static QuicError ForTlsAlert(int code, std::string reason = "");
static QuicError FromConnectionClose(ngtcp2_conn* session); static QuicError FromConnectionClose(ngtcp2_conn* session);

View file

@ -2,9 +2,12 @@
#include <aliased_struct.h> #include <aliased_struct.h>
#include <env.h> #include <env.h>
#include <nghttp3/nghttp3.h>
#include <ngtcp2/ngtcp2.h>
#include <node_errors.h> #include <node_errors.h>
#include <uv.h> #include <uv.h>
#include <v8.h> #include <v8.h>
#include <limits>
namespace node { namespace node {
namespace quic { namespace quic {
@ -16,6 +19,18 @@ namespace quic {
#define IF_QUIC_DEBUG(env) \ #define IF_QUIC_DEBUG(env) \
if (UNLIKELY(env->enabled_debug_list()->enabled(DebugCategory::QUIC))) if (UNLIKELY(env->enabled_debug_list()->enabled(DebugCategory::QUIC)))
#define DISALLOW_COPY(Name) \
Name(const Name&) = delete; \
Name& operator=(const Name&) = delete;
#define DISALLOW_MOVE(Name) \
Name(Name&&) = delete; \
Name& operator=(Name&&) = delete;
#define DISALLOW_COPY_AND_MOVE(Name) \
DISALLOW_COPY(Name) \
DISALLOW_MOVE(Name)
template <typename Opt, std::string Opt::*member> template <typename Opt, std::string Opt::*member>
bool SetOption(Environment* env, bool SetOption(Environment* env,
Opt* options, Opt* options,
@ -150,20 +165,74 @@ uint64_t GetStat(Stats* stats) {
#define JS_METHOD(name) \ #define JS_METHOD(name) \
static void name(const v8::FunctionCallbackInfo<v8::Value>& args) static void name(const v8::FunctionCallbackInfo<v8::Value>& args)
enum class Side : uint8_t {
CLIENT,
SERVER,
};
enum class EndpointLabel : uint8_t {
LOCAL,
REMOTE,
};
enum class Direction : uint8_t {
BIDIRECTIONAL,
UNIDIRECTIONAL,
};
enum class HeadersKind : uint8_t {
HINTS,
INITIAL,
TRAILING,
};
enum class HeadersFlags : uint8_t {
NONE,
TERMINAL,
};
enum class StreamPriority : uint8_t {
DEFAULT = NGHTTP3_DEFAULT_URGENCY,
LOW = NGHTTP3_URGENCY_LOW,
HIGH = NGHTTP3_URGENCY_HIGH,
};
enum class StreamPriorityFlags : uint8_t {
NONE,
NON_INCREMENTAL,
};
enum class PathValidationResult : uint8_t {
SUCCESS = NGTCP2_PATH_VALIDATION_RESULT_SUCCESS,
FAILURE = NGTCP2_PATH_VALIDATION_RESULT_FAILURE,
ABORTED = NGTCP2_PATH_VALIDATION_RESULT_ABORTED,
};
enum class DatagramStatus : uint8_t {
ACKNOWLEDGED,
LOST,
};
constexpr uint64_t NGTCP2_APP_NOERROR = 65280;
constexpr size_t kDefaultMaxPacketLength = NGTCP2_MAX_UDP_PAYLOAD_SIZE;
constexpr size_t kMaxSizeT = std::numeric_limits<size_t>::max();
constexpr uint64_t kMaxSafeJsInteger = 9007199254740991;
constexpr auto kSocketAddressInfoTimeout = 60 * NGTCP2_SECONDS;
constexpr size_t kMaxVectorCount = 16;
using error_code = uint64_t;
class DebugIndentScope { class DebugIndentScope {
public: public:
inline DebugIndentScope() { ++indent_; } inline DebugIndentScope() { ++indent_; }
DebugIndentScope(const DebugIndentScope&) = delete; DISALLOW_COPY_AND_MOVE(DebugIndentScope)
DebugIndentScope(DebugIndentScope&&) = delete;
DebugIndentScope& operator=(const DebugIndentScope&) = delete;
DebugIndentScope& operator=(DebugIndentScope&&) = delete;
inline ~DebugIndentScope() { --indent_; } inline ~DebugIndentScope() { --indent_; }
std::string Prefix() const { inline std::string Prefix() const {
std::string res("\n"); std::string res("\n");
res.append(indent_, '\t'); res.append(indent_, '\t');
return res; return res;
} }
std::string Close() const { inline std::string Close() const {
std::string res("\n"); std::string res("\n");
res.append(indent_ - 1, '\t'); res.append(indent_ - 1, '\t');
res += "}"; res += "}";

View file

@ -9,6 +9,7 @@
#include <ngtcp2/ngtcp2.h> #include <ngtcp2/ngtcp2.h>
#include <node_errors.h> #include <node_errors.h>
#include <node_external_reference.h> #include <node_external_reference.h>
#include <node_process-inl.h>
#include <node_sockaddr-inl.h> #include <node_sockaddr-inl.h>
#include <req_wrap-inl.h> #include <req_wrap-inl.h>
#include <util-inl.h> #include <util-inl.h>
@ -71,11 +72,6 @@ namespace quic {
V(STATELESS_RESET_COUNT, stateless_reset_count) \ V(STATELESS_RESET_COUNT, stateless_reset_count) \
V(IMMEDIATE_CLOSE_COUNT, immediate_close_count) V(IMMEDIATE_CLOSE_COUNT, immediate_close_count)
#define ENDPOINT_CC(V) \
V(RENO, reno) \
V(CUBIC, cubic) \
V(BBR, bbr)
struct Endpoint::State { struct Endpoint::State {
#define V(_, name, type) type name; #define V(_, name, type) type name;
ENDPOINT_STATE(V) ENDPOINT_STATE(V)
@ -340,12 +336,11 @@ std::string Endpoint::Options::ToString() const {
auto ccalg = ([&] { auto ccalg = ([&] {
switch (cc_algorithm) { switch (cc_algorithm) {
case NGTCP2_CC_ALGO_RENO: #define V(name, label) \
return "reno"; case NGTCP2_CC_ALGO_##name: \
case NGTCP2_CC_ALGO_CUBIC: return #label;
return "cubic"; ENDPOINT_CC(V)
case NGTCP2_CC_ALGO_BBR: #undef V
return "bbr";
} }
return "<unknown>"; return "<unknown>";
})(); })();
@ -622,8 +617,8 @@ void Endpoint::InitPerIsolate(IsolateData* data, Local<ObjectTemplate> target) {
void Endpoint::InitPerContext(Realm* realm, Local<Object> target) { void Endpoint::InitPerContext(Realm* realm, Local<Object> target) {
#define V(name, str) \ #define V(name, str) \
NODE_DEFINE_CONSTANT(target, QUIC_CC_ALGO_##name); \ NODE_DEFINE_CONSTANT(target, CC_ALGO_##name); \
NODE_DEFINE_STRING_CONSTANT(target, "QUIC_CC_ALGO_" #name "_STR", #str); NODE_DEFINE_STRING_CONSTANT(target, "CC_ALGO_" #name "_STR", #str);
ENDPOINT_CC(V) ENDPOINT_CC(V)
#undef V #undef V
@ -958,9 +953,32 @@ bool Endpoint::Start() {
void Endpoint::Listen(const Session::Options& options) { void Endpoint::Listen(const Session::Options& options) {
if (is_closed() || is_closing() || state_->listening == 1) return; if (is_closed() || is_closing() || state_->listening == 1) return;
server_options_ = options; DCHECK(!server_state_.has_value());
// We need at least one key and one cert to complete the tls handshake on the
// server. Why not make this an error? We could but it's not strictly
// necessary.
if (options.tls_options.keys.empty() || options.tls_options.certs.empty()) {
env()->EmitProcessEnvWarning();
ProcessEmitWarning(env(),
"The QUIC TLS options did not include a key or cert. "
"This means the TLS handshake will fail. This is likely "
"not what you want.");
}
auto context = TLSContext::CreateServer(options.tls_options);
if (!*context) {
THROW_ERR_INVALID_STATE(
env(), "Failed to create TLS context: %s", context->validation_error());
return;
}
server_state_ = {
options,
std::move(context),
};
if (Start()) { if (Start()) {
Debug(this, "Listening with options %s", server_options_.value()); Debug(this, "Listening with options %s", server_state_->options);
state_->listening = 1; state_->listening = 1;
} }
} }
@ -972,8 +990,7 @@ BaseObjectPtr<Session> Endpoint::Connect(
// If starting fails, the endpoint will be destroyed. // If starting fails, the endpoint will be destroyed.
if (!Start()) return BaseObjectPtr<Session>(); if (!Start()) return BaseObjectPtr<Session>();
Session::Config config( Session::Config config(*this, options, local_address(), remote_address);
*this, options, local_address(), remote_address, session_ticket);
IF_QUIC_DEBUG(env()) { IF_QUIC_DEBUG(env()) {
Debug( Debug(
@ -985,7 +1002,21 @@ BaseObjectPtr<Session> Endpoint::Connect(
session_ticket.has_value() ? "yes" : "no"); session_ticket.has_value() ? "yes" : "no");
} }
auto session = Session::Create(this, config); auto tls_context = TLSContext::CreateClient(options.tls_options);
if (!*tls_context) {
THROW_ERR_INVALID_STATE(env(),
"Failed to create TLS context: %s",
tls_context->validation_error());
return BaseObjectPtr<Session>();
}
auto session =
Session::Create(this, config, tls_context.get(), session_ticket);
if (!session->tls_session()) {
THROW_ERR_INVALID_STATE(env(),
"Failed to create TLS session: %s",
session->tls_session().validation_error());
return BaseObjectPtr<Session>();
}
if (!session) return BaseObjectPtr<Session>(); if (!session) return BaseObjectPtr<Session>();
session->set_wrapped(); session->set_wrapped();
@ -1093,9 +1124,19 @@ void Endpoint::Receive(const uv_buf_t& buf,
// as a server, then we cannot accept the initial packet. // as a server, then we cannot accept the initial packet.
if (is_closed() || is_closing() || !is_listening()) return; if (is_closed() || is_closing() || !is_listening()) return;
Debug(this, "Trying to create new session for %s", config.dcid); Debug(this, "Creating new session for %s", config.dcid);
auto session = Session::Create(this, config);
std::optional<SessionTicket> no_ticket = std::nullopt;
auto session = Session::Create(
this, config, server_state_->tls_context.get(), no_ticket);
if (session) { if (session) {
if (!session->tls_session()) {
Debug(this,
"Failed to create TLS session for %s: %s",
config.dcid,
session->tls_session().validation_error());
return;
}
receive(session.get(), receive(session.get(),
std::move(store), std::move(store),
config.local_address, config.local_address,
@ -1183,7 +1224,7 @@ void Endpoint::Receive(const uv_buf_t& buf,
// because that is the value *this* session will use as the outbound dcid. // because that is the value *this* session will use as the outbound dcid.
Session::Config config(Side::SERVER, Session::Config config(Side::SERVER,
*this, *this,
server_options_.value(), server_state_->options,
version, version,
local_address, local_address,
remote_address, remote_address,
@ -1197,7 +1238,7 @@ void Endpoint::Receive(const uv_buf_t& buf,
// identifies us. config.dcid should equal scid. config.scid should *not* // identifies us. config.dcid should equal scid. config.scid should *not*
// equal dcid. // equal dcid.
DCHECK(config.dcid == scid); DCHECK(config.dcid == scid);
DCHECK(config.scid != dcid); DCHECK(config.scid == dcid);
const auto is_remote_address_validated = ([&] { const auto is_remote_address_validated = ([&] {
auto info = addrLRU_.Peek(remote_address); auto info = addrLRU_.Peek(remote_address);
@ -1582,8 +1623,9 @@ bool Endpoint::is_listening() const {
void Endpoint::MemoryInfo(MemoryTracker* tracker) const { void Endpoint::MemoryInfo(MemoryTracker* tracker) const {
tracker->TrackField("options", options_); tracker->TrackField("options", options_);
tracker->TrackField("udp", udp_); tracker->TrackField("udp", udp_);
if (server_options_.has_value()) { if (server_state_.has_value()) {
tracker->TrackField("server_options", server_options_.value()); tracker->TrackField("server_options", server_state_->options);
tracker->TrackField("server_tls_context", server_state_->tls_context);
} }
tracker->TrackField("token_map", token_map_); tracker->TrackField("token_map", token_map_);
tracker->TrackField("sessions", sessions_); tracker->TrackField("sessions", sessions_);

View file

@ -20,6 +20,11 @@
namespace node { namespace node {
namespace quic { namespace quic {
#define ENDPOINT_CC(V) \
V(RENO, reno) \
V(CUBIC, cubic) \
V(BBR, bbr)
// An Endpoint encapsulates the UDP local port binding and is responsible for // An Endpoint encapsulates the UDP local port binding and is responsible for
// sending and receiving QUIC packets. A single endpoint can act as both a QUIC // sending and receiving QUIC packets. A single endpoint can act as both a QUIC
// client and server simultaneously. // client and server simultaneously.
@ -33,9 +38,9 @@ class Endpoint final : public AsyncWrap, public Packet::Listener {
static constexpr uint64_t DEFAULT_MAX_STATELESS_RESETS = 10; static constexpr uint64_t DEFAULT_MAX_STATELESS_RESETS = 10;
static constexpr uint64_t DEFAULT_MAX_RETRY_LIMIT = 10; static constexpr uint64_t DEFAULT_MAX_RETRY_LIMIT = 10;
static constexpr auto QUIC_CC_ALGO_RENO = NGTCP2_CC_ALGO_RENO; #define V(name, _) static constexpr auto CC_ALGO_##name = NGTCP2_CC_ALGO_##name;
static constexpr auto QUIC_CC_ALGO_CUBIC = NGTCP2_CC_ALGO_CUBIC; ENDPOINT_CC(V)
static constexpr auto QUIC_CC_ALGO_BBR = NGTCP2_CC_ALGO_BBR; #undef V
// Endpoint configuration options // Endpoint configuration options
struct Options final : public MemoryRetainer { struct Options final : public MemoryRetainer {
@ -144,7 +149,7 @@ class Endpoint final : public AsyncWrap, public Packet::Listener {
// which to use by default is arbitrary and we can choose whichever we'd // which to use by default is arbitrary and we can choose whichever we'd
// like. Additional performance profiling will be needed to determine which // like. Additional performance profiling will be needed to determine which
// is the better of the two for our needs. // is the better of the two for our needs.
ngtcp2_cc_algo cc_algorithm = NGTCP2_CC_ALGO_CUBIC; ngtcp2_cc_algo cc_algorithm = CC_ALGO_CUBIC;
// By default, when the endpoint is created, it will generate a // By default, when the endpoint is created, it will generate a
// reset_token_secret at random. This is a secret used in generating // reset_token_secret at random. This is a secret used in generating
@ -409,8 +414,12 @@ class Endpoint final : public AsyncWrap, public Packet::Listener {
const Options options_; const Options options_;
UDP udp_; UDP udp_;
struct ServerState {
Session::Options options;
std::shared_ptr<TLSContext> tls_context;
};
// Set if/when the endpoint is configured to listen. // Set if/when the endpoint is configured to listen.
std::optional<Session::Options> server_options_{}; std::optional<ServerState> server_state_ = std::nullopt;
// A Session is generally identified by one or more CIDs. We use two // A Session is generally identified by one or more CIDs. We use two
// maps for this rather than one to avoid creating a whole bunch of // maps for this rather than one to avoid creating a whole bunch of

View file

@ -362,8 +362,12 @@ class Http3Application final : public Session::Application {
return static_cast<int>(ret); return static_cast<int>(ret);
} else { } else {
data->remaining = data->count = static_cast<size_t>(ret); data->remaining = data->count = static_cast<size_t>(ret);
if (data->id > 0) {
data->stream = session().FindStream(data->id);
}
} }
} }
DCHECK_NOT_NULL(data->buf);
return 0; return 0;
} }

View file

@ -161,7 +161,7 @@ Packet* Packet::FromFreeList(Environment* env,
CHECK_NOT_NULL(packet); CHECK_NOT_NULL(packet);
CHECK_EQ(env, packet->env()); CHECK_EQ(env, packet->env());
Debug(packet, "Reusing packet from freelist"); Debug(packet, "Reusing packet from freelist");
packet->data_ = data; packet->data_ = std::move(data);
packet->destination_ = destination; packet->destination_ = destination;
packet->listener_ = listener; packet->listener_ = listener;
return packet; return packet;

View file

@ -15,6 +15,7 @@
#include "bindingdata.h" #include "bindingdata.h"
#include "cid.h" #include "cid.h"
#include "data.h" #include "data.h"
#include "defs.h"
#include "tokens.h" #include "tokens.h"
namespace node { namespace node {
@ -76,10 +77,7 @@ class Packet final : public ReqWrap<uv_udp_send_t> {
const SocketAddress& destination, const SocketAddress& destination,
std::shared_ptr<Data> data); std::shared_ptr<Data> data);
Packet(const Packet&) = delete; DISALLOW_COPY_AND_MOVE(Packet)
Packet(Packet&&) = delete;
Packet& operator=(const Packet&) = delete;
Packet& operator=(Packet&&) = delete;
const SocketAddress& destination() const; const SocketAddress& destination() const;
size_t length() const; size_t length() const;

View file

@ -97,23 +97,6 @@ bool resolve(const PreferredAddress::AddressInfo& address,
} }
} // namespace } // namespace
Maybe<PreferredAddress::Policy> PreferredAddress::GetPolicy(
Environment* env, Local<Value> value) {
CHECK(value->IsUint32());
uint32_t val = 0;
if (value->Uint32Value(env->context()).To(&val)) {
switch (val) {
case QUIC_PREFERRED_ADDRESS_USE:
return Just(Policy::USE_PREFERRED_ADDRESS);
case QUIC_PREFERRED_ADDRESS_IGNORE:
return Just(Policy::IGNORE_PREFERRED_ADDRESS);
}
}
THROW_ERR_INVALID_ARG_VALUE(
env, "%d is not a valid preferred address policy", val);
return Nothing<Policy>();
}
PreferredAddress::PreferredAddress(ngtcp2_path* dest, PreferredAddress::PreferredAddress(ngtcp2_path* dest,
const ngtcp2_preferred_addr* paddr) const ngtcp2_preferred_addr* paddr)
: dest_(dest), paddr_(paddr) { : dest_(dest), paddr_(paddr) {
@ -160,14 +143,15 @@ void PreferredAddress::Set(ngtcp2_transport_params* params,
Maybe<PreferredAddress::Policy> PreferredAddress::tryGetPolicy( Maybe<PreferredAddress::Policy> PreferredAddress::tryGetPolicy(
Environment* env, Local<Value> value) { Environment* env, Local<Value> value) {
if (value->IsUndefined()) { if (value->IsUndefined()) {
return Just(PreferredAddress::Policy::USE_PREFERRED_ADDRESS); return Just(PreferredAddress::Policy::USE_PREFERRED);
} }
if (value->IsUint32()) { if (value->IsUint32()) {
auto val = value.As<Uint32>()->Value(); switch (value.As<Uint32>()->Value()) {
if (val == static_cast<uint32_t>(Policy::IGNORE_PREFERRED_ADDRESS)) case PREFERRED_ADDRESS_IGNORE:
return Just(Policy::IGNORE_PREFERRED_ADDRESS); return Just(Policy::IGNORE_PREFERRED);
if (val == static_cast<uint32_t>(Policy::USE_PREFERRED_ADDRESS)) case PREFERRED_ADDRESS_USE:
return Just(Policy::USE_PREFERRED_ADDRESS); return Just(Policy::USE_PREFERRED);
}
} }
THROW_ERR_INVALID_ARG_VALUE(env, "invalid preferred address policy"); THROW_ERR_INVALID_ARG_VALUE(env, "invalid preferred address policy");
return Nothing<PreferredAddress::Policy>(); return Nothing<PreferredAddress::Policy>();
@ -175,8 +159,8 @@ Maybe<PreferredAddress::Policy> PreferredAddress::tryGetPolicy(
void PreferredAddress::Initialize(Environment* env, void PreferredAddress::Initialize(Environment* env,
v8::Local<v8::Object> target) { v8::Local<v8::Object> target) {
NODE_DEFINE_CONSTANT(target, QUIC_PREFERRED_ADDRESS_IGNORE); NODE_DEFINE_CONSTANT(target, PREFERRED_ADDRESS_IGNORE);
NODE_DEFINE_CONSTANT(target, QUIC_PREFERRED_ADDRESS_USE); NODE_DEFINE_CONSTANT(target, PREFERRED_ADDRESS_USE);
NODE_DEFINE_CONSTANT(target, DEFAULT_PREFERRED_ADDRESS_POLICY); NODE_DEFINE_CONSTANT(target, DEFAULT_PREFERRED_ADDRESS_POLICY);
} }

View file

@ -8,6 +8,7 @@
#include <node_internals.h> #include <node_internals.h>
#include <v8.h> #include <v8.h>
#include <string> #include <string>
#include "defs.h"
namespace node { namespace node {
namespace quic { namespace quic {
@ -18,11 +19,11 @@ namespace quic {
// the preferred address to be selected. // the preferred address to be selected.
class PreferredAddress final { class PreferredAddress final {
public: public:
enum class Policy { enum class Policy : uint32_t {
// Ignore the server-advertised preferred address. // Ignore the server-advertised preferred address.
IGNORE_PREFERRED_ADDRESS, IGNORE_PREFERRED,
// Use the server-advertised preferred address. // Use the server-advertised preferred address.
USE_PREFERRED_ADDRESS, USE_PREFERRED,
}; };
static v8::Maybe<Policy> tryGetPolicy(Environment* env, static v8::Maybe<Policy> tryGetPolicy(Environment* env,
@ -30,18 +31,15 @@ class PreferredAddress final {
// The QUIC_* constants are expected to be exported out to be used on // The QUIC_* constants are expected to be exported out to be used on
// the JavaScript side of the API. // the JavaScript side of the API.
static constexpr uint32_t QUIC_PREFERRED_ADDRESS_USE = static constexpr auto PREFERRED_ADDRESS_USE =
static_cast<uint32_t>(Policy::USE_PREFERRED_ADDRESS); static_cast<uint32_t>(Policy::USE_PREFERRED);
static constexpr uint32_t QUIC_PREFERRED_ADDRESS_IGNORE = static constexpr auto PREFERRED_ADDRESS_IGNORE =
static_cast<uint32_t>(Policy::IGNORE_PREFERRED_ADDRESS); static_cast<uint32_t>(Policy::IGNORE_PREFERRED);
static constexpr uint32_t DEFAULT_PREFERRED_ADDRESS_POLICY = static constexpr auto DEFAULT_PREFERRED_ADDRESS_POLICY =
static_cast<uint32_t>(Policy::USE_PREFERRED_ADDRESS); static_cast<uint32_t>(Policy::USE_PREFERRED);
static void Initialize(Environment* env, v8::Local<v8::Object> target); static void Initialize(Environment* env, v8::Local<v8::Object> target);
static v8::Maybe<Policy> GetPolicy(Environment* env,
v8::Local<v8::Value> value);
struct AddressInfo final { struct AddressInfo final {
char host[NI_MAXHOST]; char host[NI_MAXHOST];
int family; int family;
@ -51,10 +49,7 @@ class PreferredAddress final {
explicit PreferredAddress(ngtcp2_path* dest, explicit PreferredAddress(ngtcp2_path* dest,
const ngtcp2_preferred_addr* paddr); const ngtcp2_preferred_addr* paddr);
PreferredAddress(const PreferredAddress&) = delete; DISALLOW_COPY_AND_MOVE(PreferredAddress)
PreferredAddress(PreferredAddress&&) = delete;
PreferredAddress& operator=(const PreferredAddress&) = delete;
PreferredAddress& operator=(PreferredAddress&&) = delete;
void Use(const AddressInfo& address); void Use(const AddressInfo& address);

View file

@ -90,7 +90,6 @@ namespace quic {
V(BIDI_OUT_STREAM_COUNT, bidi_out_stream_count) \ V(BIDI_OUT_STREAM_COUNT, bidi_out_stream_count) \
V(UNI_IN_STREAM_COUNT, uni_in_stream_count) \ V(UNI_IN_STREAM_COUNT, uni_in_stream_count) \
V(UNI_OUT_STREAM_COUNT, uni_out_stream_count) \ V(UNI_OUT_STREAM_COUNT, uni_out_stream_count) \
V(KEYUPDATE_COUNT, keyupdate_count) \
V(LOSS_RETRANSMIT_COUNT, loss_retransmit_count) \ V(LOSS_RETRANSMIT_COUNT, loss_retransmit_count) \
V(MAX_BYTES_IN_FLIGHT, max_bytes_in_flight) \ V(MAX_BYTES_IN_FLIGHT, max_bytes_in_flight) \
V(BYTES_IN_FLIGHT, bytes_in_flight) \ V(BYTES_IN_FLIGHT, bytes_in_flight) \
@ -142,11 +141,7 @@ struct Session::MaybeCloseConnectionScope final {
silent ? "yes" : "no"); silent ? "yes" : "no");
session->connection_close_depth_++; session->connection_close_depth_++;
} }
MaybeCloseConnectionScope(const MaybeCloseConnectionScope&) = delete; DISALLOW_COPY_AND_MOVE(MaybeCloseConnectionScope)
MaybeCloseConnectionScope(MaybeCloseConnectionScope&&) = delete;
MaybeCloseConnectionScope& operator=(const MaybeCloseConnectionScope&) =
delete;
MaybeCloseConnectionScope& operator=(MaybeCloseConnectionScope&&) = delete;
~MaybeCloseConnectionScope() { ~MaybeCloseConnectionScope() {
// We only want to trigger the sending the connection close if ... // We only want to trigger the sending the connection close if ...
// a) Silent is not explicitly true at this scope. // a) Silent is not explicitly true at this scope.
@ -185,7 +180,7 @@ Session::SendPendingDataScope::~SendPendingDataScope() {
namespace { namespace {
inline const char* getEncryptionLevelName(ngtcp2_encryption_level level) { inline std::string to_string(ngtcp2_encryption_level level) {
switch (level) { switch (level) {
case NGTCP2_ENCRYPTION_LEVEL_1RTT: case NGTCP2_ENCRYPTION_LEVEL_1RTT:
return "1rtt"; return "1rtt";
@ -231,8 +226,7 @@ bool SetOption(Environment* env,
const v8::Local<Object>& object, const v8::Local<Object>& object,
const v8::Local<String>& name) { const v8::Local<String>& name) {
Local<Value> value; Local<Value> value;
PreferredAddress::Policy policy = PreferredAddress::Policy policy = PreferredAddress::Policy::USE_PREFERRED;
PreferredAddress::Policy::USE_PREFERRED_ADDRESS;
if (!object->Get(env->context(), name).ToLocal(&value) || if (!object->Get(env->context(), name).ToLocal(&value) ||
!PreferredAddress::tryGetPolicy(env, value).To(&policy)) { !PreferredAddress::tryGetPolicy(env, value).To(&policy)) {
return false; return false;
@ -297,7 +291,6 @@ Session::Config::Config(Side side,
const SocketAddress& remote_address, const SocketAddress& remote_address,
const CID& dcid, const CID& dcid,
const CID& scid, const CID& scid,
std::optional<SessionTicket> session_ticket,
const CID& ocid) const CID& ocid)
: side(side), : side(side),
options(options), options(options),
@ -306,8 +299,7 @@ Session::Config::Config(Side side,
remote_address(remote_address), remote_address(remote_address),
dcid(dcid), dcid(dcid),
scid(scid), scid(scid),
ocid(ocid), ocid(ocid) {
session_ticket(session_ticket) {
ngtcp2_settings_default(&settings); ngtcp2_settings_default(&settings);
settings.initial_ts = uv_hrtime(); settings.initial_ts = uv_hrtime();
@ -343,7 +335,6 @@ Session::Config::Config(const Endpoint& endpoint,
const Options& options, const Options& options,
const SocketAddress& local_address, const SocketAddress& local_address,
const SocketAddress& remote_address, const SocketAddress& remote_address,
std::optional<SessionTicket> session_ticket,
const CID& ocid) const CID& ocid)
: Config(Side::CLIENT, : Config(Side::CLIENT,
endpoint, endpoint,
@ -353,7 +344,6 @@ Session::Config::Config(const Endpoint& endpoint,
remote_address, remote_address,
CID::Factory::random().Generate(NGTCP2_MIN_INITIAL_DCIDLEN), CID::Factory::random().Generate(NGTCP2_MIN_INITIAL_DCIDLEN),
options.cid_factory->Generate(), options.cid_factory->Generate(),
session_ticket,
ocid) {} ocid) {}
void Session::Config::MemoryInfo(MemoryTracker* tracker) const { void Session::Config::MemoryInfo(MemoryTracker* tracker) const {
@ -364,8 +354,6 @@ void Session::Config::MemoryInfo(MemoryTracker* tracker) const {
tracker->TrackField("scid", scid); tracker->TrackField("scid", scid);
tracker->TrackField("ocid", ocid); tracker->TrackField("ocid", ocid);
tracker->TrackField("retry_scid", retry_scid); tracker->TrackField("retry_scid", retry_scid);
if (session_ticket.has_value())
tracker->TrackField("session_ticket", session_ticket.value());
} }
void Session::Config::set_token(const uint8_t* token, void Session::Config::set_token(const uint8_t* token,
@ -410,13 +398,6 @@ std::string Session::Config::ToString() const {
res += prefix + "ocid: " + ocid.ToString(); res += prefix + "ocid: " + ocid.ToString();
res += prefix + "retry scid: " + retry_scid.ToString(); res += prefix + "retry scid: " + retry_scid.ToString();
res += prefix + "preferred address cid: " + preferred_address_cid.ToString(); res += prefix + "preferred address cid: " + preferred_address_cid.ToString();
if (session_ticket.has_value()) {
res += prefix + "session ticket: yes";
} else {
res += prefix + "session ticket: <none>";
}
res += indent.Close(); res += indent.Close();
return res; return res;
} }
@ -468,9 +449,9 @@ std::string Session::Options::ToString() const {
auto policy = ([&] { auto policy = ([&] {
switch (preferred_address_strategy) { switch (preferred_address_strategy) {
case PreferredAddress::Policy::USE_PREFERRED_ADDRESS: case PreferredAddress::Policy::USE_PREFERRED:
return "use"; return "use";
case PreferredAddress::Policy::IGNORE_PREFERRED_ADDRESS: case PreferredAddress::Policy::IGNORE_PREFERRED:
return "ignore"; return "ignore";
} }
return "<unknown>"; return "<unknown>";
@ -490,8 +471,11 @@ bool Session::HasInstance(Environment* env, Local<Value> value) {
return GetConstructorTemplate(env)->HasInstance(value); return GetConstructorTemplate(env)->HasInstance(value);
} }
BaseObjectPtr<Session> Session::Create(Endpoint* endpoint, BaseObjectPtr<Session> Session::Create(
const Config& config) { Endpoint* endpoint,
const Config& config,
TLSContext* tls_context,
const std::optional<SessionTicket>& ticket) {
Local<Object> obj; Local<Object> obj;
if (!GetConstructorTemplate(endpoint->env()) if (!GetConstructorTemplate(endpoint->env())
->InstanceTemplate() ->InstanceTemplate()
@ -500,12 +484,15 @@ BaseObjectPtr<Session> Session::Create(Endpoint* endpoint,
return BaseObjectPtr<Session>(); return BaseObjectPtr<Session>();
} }
return MakeDetachedBaseObject<Session>(endpoint, obj, config); return MakeDetachedBaseObject<Session>(
endpoint, obj, config, tls_context, ticket);
} }
Session::Session(Endpoint* endpoint, Session::Session(Endpoint* endpoint,
v8::Local<v8::Object> object, v8::Local<v8::Object> object,
const Config& config) const Config& config,
TLSContext* tls_context,
const std::optional<SessionTicket>& session_ticket)
: AsyncWrap(endpoint->env(), object, AsyncWrap::PROVIDER_QUIC_SESSION), : AsyncWrap(endpoint->env(), object, AsyncWrap::PROVIDER_QUIC_SESSION),
stats_(env()->isolate()), stats_(env()->isolate()),
state_(env()->isolate()), state_(env()->isolate()),
@ -515,7 +502,7 @@ Session::Session(Endpoint* endpoint,
local_address_(config.local_address), local_address_(config.local_address),
remote_address_(config.remote_address), remote_address_(config.remote_address),
connection_(InitConnection()), connection_(InitConnection()),
tls_context_(env(), config_.side, this, config_.options.tls_options), tls_session_(tls_context->NewSession(this, session_ticket)),
application_(select_application()), application_(select_application()),
timer_(env(), timer_(env(),
[this, self = BaseObjectPtr<Session>(this)] { OnTimeout(); }) { [this, self = BaseObjectPtr<Session>(this)] { OnTimeout(); }) {
@ -560,8 +547,6 @@ Session::Session(Endpoint* endpoint,
endpoint_->AddSession(config_.scid, BaseObjectPtr<Session>(this)); endpoint_->AddSession(config_.scid, BaseObjectPtr<Session>(this));
endpoint_->AssociateCID(config_.dcid, config_.scid); endpoint_->AssociateCID(config_.dcid, config_.scid);
tls_context_.Start();
UpdateDataStats(); UpdateDataStats();
} }
@ -583,6 +568,10 @@ Session::~Session() {
DCHECK(streams_.empty()); DCHECK(streams_.empty());
} }
size_t Session::max_packet_size() const {
return ngtcp2_conn_get_max_tx_udp_payload_size(*this);
}
Session::operator ngtcp2_conn*() const { Session::operator ngtcp2_conn*() const {
return connection_.get(); return connection_.get();
} }
@ -595,8 +584,8 @@ Endpoint& Session::endpoint() const {
return *endpoint_; return *endpoint_;
} }
TLSContext& Session::tls_context() { TLSSession& Session::tls_session() {
return tls_context_; return *tls_session_;
} }
Session::Application& Session::application() { Session::Application& Session::application() {
@ -873,6 +862,10 @@ void Session::Send(Packet* packet, const PathStorage& path) {
Send(packet); Send(packet);
} }
void Session::UpdatePacketTxTime() {
ngtcp2_conn_update_pkt_tx_time(*this, uv_hrtime());
}
uint64_t Session::SendDatagram(Store&& data) { uint64_t Session::SendDatagram(Store&& data) {
auto tp = ngtcp2_conn_get_remote_transport_params(*this); auto tp = ngtcp2_conn_get_remote_transport_params(*this);
uint64_t max_datagram_size = tp->max_datagram_frame_size; uint64_t max_datagram_size = tp->max_datagram_frame_size;
@ -1169,7 +1162,7 @@ void Session::MemoryInfo(MemoryTracker* tracker) const {
tracker->TrackField("local_address", local_address_); tracker->TrackField("local_address", local_address_);
tracker->TrackField("remote_address", remote_address_); tracker->TrackField("remote_address", remote_address_);
tracker->TrackField("application", application_); tracker->TrackField("application", application_);
tracker->TrackField("tls_context", tls_context_); tracker->TrackField("tls_session", tls_session_);
tracker->TrackField("timer", timer_); tracker->TrackField("timer", timer_);
tracker->TrackField("conn_closebuf", conn_closebuf_); tracker->TrackField("conn_closebuf", conn_closebuf_);
tracker->TrackField("qlog_stream", qlog_stream_); tracker->TrackField("qlog_stream", qlog_stream_);
@ -1189,7 +1182,6 @@ bool Session::wants_session_ticket() const {
} }
void Session::SetStreamOpenAllowed() { void Session::SetStreamOpenAllowed() {
// TODO(@jasnell): Might remove this. May not be needed
state_->stream_open_allowed = 1; state_->stream_open_allowed = 1;
} }
@ -1334,8 +1326,8 @@ void Session::OnTimeout() {
if (is_destroyed()) return; if (is_destroyed()) return;
int ret = ngtcp2_conn_handle_expiry(*this, uv_hrtime()); int ret = ngtcp2_conn_handle_expiry(*this, uv_hrtime());
if (NGTCP2_OK(ret) && !is_in_closing_period() && !is_in_draining_period() && if (NGTCP2_OK(ret) && !is_in_closing_period() && !is_in_draining_period()) {
env()->can_call_into_js()) { Debug(this, "Sending pending data after timr expiry");
SendPendingDataScope send_scope(this); SendPendingDataScope send_scope(this);
return; return;
} }
@ -1358,6 +1350,7 @@ void Session::UpdateTimer() {
} }
auto timeout = (expiry - now) / NGTCP2_MILLISECONDS; auto timeout = (expiry - now) / NGTCP2_MILLISECONDS;
Debug(this, "Updating timeout to %zu milliseconds", timeout);
// If timeout is zero here, it means our timer is less than a millisecond // If timeout is zero here, it means our timer is less than a millisecond
// off from expiry. Let's bump the timer to 1. // off from expiry. Let's bump the timer to 1.
@ -1418,7 +1411,7 @@ void Session::DatagramReceived(const uint8_t* data,
bool Session::GenerateNewConnectionId(ngtcp2_cid* cid, bool Session::GenerateNewConnectionId(ngtcp2_cid* cid,
size_t len, size_t len,
uint8_t* token) { uint8_t* token) {
CID cid_ = config_.options.cid_factory->Generate(len); CID cid_ = config_.options.cid_factory->GenerateInto(cid, len);
Debug(this, "Generated new connection id %s", cid_); Debug(this, "Generated new connection id %s", cid_);
StatelessResetToken new_token( StatelessResetToken new_token(
token, endpoint_->options().reset_token_secret, cid_); token, endpoint_->options().reset_token_secret, cid_);
@ -1428,13 +1421,14 @@ bool Session::GenerateNewConnectionId(ngtcp2_cid* cid,
} }
bool Session::HandshakeCompleted() { bool Session::HandshakeCompleted() {
if (state_->handshake_completed) return false;
state_->handshake_completed = true;
Debug(this, "Session handshake completed"); Debug(this, "Session handshake completed");
if (state_->handshake_completed) return false;
state_->handshake_completed = 1;
STAT_RECORD_TIMESTAMP(Stats, handshake_completed_at); STAT_RECORD_TIMESTAMP(Stats, handshake_completed_at);
if (!tls_context_.early_data_was_accepted()) if (!tls_session().early_data_was_accepted())
ngtcp2_conn_tls_early_data_rejected(*this); ngtcp2_conn_tls_early_data_rejected(*this);
// When in a server session, handshake completed == handshake confirmed. // When in a server session, handshake completed == handshake confirmed.
@ -1469,7 +1463,7 @@ void Session::HandshakeConfirmed() {
void Session::SelectPreferredAddress(PreferredAddress* preferredAddress) { void Session::SelectPreferredAddress(PreferredAddress* preferredAddress) {
if (config_.options.preferred_address_strategy == if (config_.options.preferred_address_strategy ==
PreferredAddress::Policy::IGNORE_PREFERRED_ADDRESS) { PreferredAddress::Policy::IGNORE_PREFERRED) {
Debug(this, "Ignoring preferred address"); Debug(this, "Ignoring preferred address");
return; return;
} }
@ -1592,23 +1586,21 @@ void Session::EmitHandshakeComplete() {
Undefined(isolate), // Cipher version Undefined(isolate), // Cipher version
Undefined(isolate), // Validation error reason Undefined(isolate), // Validation error reason
Undefined(isolate), // Validation error code Undefined(isolate), // Validation error code
v8::Boolean::New(isolate, tls_context_.early_data_was_accepted())}; v8::Boolean::New(isolate, tls_session().early_data_was_accepted())};
int err = tls_context_.VerifyPeerIdentity(); auto& tls = tls_session();
auto peerVerifyError = tls.VerifyPeerIdentity(env());
if (err != X509_V_OK && (!crypto::GetValidationErrorReason(env(), err) if (peerVerifyError.has_value() &&
.ToLocal(&argv[kValidationErrorReason]) || (!peerVerifyError->reason.ToLocal(&argv[kValidationErrorReason]) ||
!crypto::GetValidationErrorCode(env(), err) !peerVerifyError->code.ToLocal(&argv[kValidationErrorCode]))) {
.ToLocal(&argv[kValidationErrorCode]))) {
return; return;
} }
if (!ToV8Value(env()->context(), tls_context_.servername()) if (!ToV8Value(env()->context(), tls.servername())
.ToLocal(&argv[kServerName]) || .ToLocal(&argv[kServerName]) ||
!ToV8Value(env()->context(), tls_context_.alpn()) !ToV8Value(env()->context(), tls.alpn()).ToLocal(&argv[kSelectedAlpn]) ||
.ToLocal(&argv[kSelectedAlpn]) || !tls.cipher_name(env()).ToLocal(&argv[kCipherName]) ||
tls_context_.cipher_name(env()).ToLocal(&argv[kCipherName]) || !tls.cipher_version(env()).ToLocal(&argv[kCipherVersion])) {
!tls_context_.cipher_version(env()).ToLocal(&argv[kCipherVersion])) {
return; return;
} }
@ -1665,7 +1657,10 @@ void Session::EmitSessionTicket(Store&& ticket) {
// If there is nothing listening for the session ticket, don't bother // If there is nothing listening for the session ticket, don't bother
// emitting. // emitting.
if (LIKELY(state_->session_ticket == 0)) return; if (LIKELY(!wants_session_ticket())) {
Debug(this, "Session ticket was discarded");
return;
}
CallbackScope<Session> cb_scope(this); CallbackScope<Session> cb_scope(this);
@ -1777,7 +1772,7 @@ struct Session::Impl {
Session* session; Session* session;
ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder());
Local<Value> ret; Local<Value> ret;
if (session->tls_context().cert(env).ToLocal(&ret)) if (session->tls_session().cert(env).ToLocal(&ret))
args.GetReturnValue().Set(ret); args.GetReturnValue().Set(ret);
} }
@ -1787,7 +1782,7 @@ struct Session::Impl {
ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder());
Local<Object> ret; Local<Object> ret;
if (!session->is_server() && if (!session->is_server() &&
session->tls_context().ephemeral_key(env).ToLocal(&ret)) session->tls_session().ephemeral_key(env).ToLocal(&ret))
args.GetReturnValue().Set(ret); args.GetReturnValue().Set(ret);
} }
@ -1796,7 +1791,7 @@ struct Session::Impl {
Session* session; Session* session;
ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder());
Local<Value> ret; Local<Value> ret;
if (session->tls_context().peer_cert(env).ToLocal(&ret)) if (session->tls_session().peer_cert(env).ToLocal(&ret))
args.GetReturnValue().Set(ret); args.GetReturnValue().Set(ret);
} }
@ -1820,7 +1815,8 @@ struct Session::Impl {
// before the TLS handshake has been confirmed or while a previous // before the TLS handshake has been confirmed or while a previous
// key update is being processed). When it fails, InitiateKeyUpdate() // key update is being processed). When it fails, InitiateKeyUpdate()
// will return false. // will return false.
args.GetReturnValue().Set(session->tls_context().InitiateKeyUpdate()); Debug(session, "Initiating key update");
args.GetReturnValue().Set(session->tls_session().InitiateKeyUpdate());
} }
static void DoOpenStream(const FunctionCallbackInfo<Value>& args) { static void DoOpenStream(const FunctionCallbackInfo<Value>& args) {
@ -2022,17 +2018,17 @@ struct Session::Impl {
void* user_data) { void* user_data) {
auto session = Impl::From(conn, user_data); auto session = Impl::From(conn, user_data);
if (UNLIKELY(session->is_destroyed())) return NGTCP2_ERR_CALLBACK_FAILURE; if (UNLIKELY(session->is_destroyed())) return NGTCP2_ERR_CALLBACK_FAILURE;
CHECK(!session->is_server());
if (level != NGTCP2_ENCRYPTION_LEVEL_1RTT) return NGTCP2_SUCCESS;
Debug(session, Debug(session,
"Receiving RX key for level %d for dcid %s", "Receiving RX key for level %d for dcid %s",
getEncryptionLevelName(level), to_string(level),
session->config().dcid); session->config().dcid);
if (!session->is_server() && (level == NGTCP2_ENCRYPTION_LEVEL_0RTT || return session->application().Start() ? NGTCP2_SUCCESS
level == NGTCP2_ENCRYPTION_LEVEL_1RTT)) { : NGTCP2_ERR_CALLBACK_FAILURE;
if (!session->application().Start()) return NGTCP2_ERR_CALLBACK_FAILURE;
}
return NGTCP2_SUCCESS;
} }
static int on_receive_stateless_reset(ngtcp2_conn* conn, static int on_receive_stateless_reset(ngtcp2_conn* conn,
@ -2081,17 +2077,16 @@ struct Session::Impl {
void* user_data) { void* user_data) {
auto session = Impl::From(conn, user_data); auto session = Impl::From(conn, user_data);
if (UNLIKELY(session->is_destroyed())) return NGTCP2_ERR_CALLBACK_FAILURE; if (UNLIKELY(session->is_destroyed())) return NGTCP2_ERR_CALLBACK_FAILURE;
CHECK(session->is_server());
if (level != NGTCP2_ENCRYPTION_LEVEL_1RTT) return NGTCP2_SUCCESS;
Debug(session, Debug(session,
"Receiving TX key for level %d for dcid %s", "Receiving TX key for level %d for dcid %s",
getEncryptionLevelName(level), to_string(level),
session->config().dcid); session->config().dcid);
return session->application().Start() ? NGTCP2_SUCCESS
if (session->is_server() && (level == NGTCP2_ENCRYPTION_LEVEL_0RTT || : NGTCP2_ERR_CALLBACK_FAILURE;
level == NGTCP2_ENCRYPTION_LEVEL_1RTT)) {
if (!session->application().Start()) return NGTCP2_ERR_CALLBACK_FAILURE;
}
return NGTCP2_SUCCESS;
} }
static int on_receive_version_negotiation(ngtcp2_conn* conn, static int on_receive_version_negotiation(ngtcp2_conn* conn,
@ -2216,7 +2211,7 @@ struct Session::Impl {
on_stream_stop_sending, on_stream_stop_sending,
ngtcp2_crypto_version_negotiation_cb, ngtcp2_crypto_version_negotiation_cb,
on_receive_rx_key, on_receive_rx_key,
on_receive_tx_key, nullptr,
on_early_data_rejected}; on_early_data_rejected};
static constexpr ngtcp2_callbacks SERVER = { static constexpr ngtcp2_callbacks SERVER = {
@ -2257,7 +2252,7 @@ struct Session::Impl {
ngtcp2_crypto_get_path_challenge_data_cb, ngtcp2_crypto_get_path_challenge_data_cb,
on_stream_stop_sending, on_stream_stop_sending,
ngtcp2_crypto_version_negotiation_cb, ngtcp2_crypto_version_negotiation_cb,
on_receive_rx_key, nullptr,
on_receive_tx_key, on_receive_tx_key,
on_early_data_rejected}; on_early_data_rejected};
}; };
@ -2280,7 +2275,6 @@ Local<FunctionTemplate> Session::GetConstructorTemplate(Environment* env) {
} else { \ } else { \
SetProtoMethod(isolate, tmpl, #key, Impl::name); \ SetProtoMethod(isolate, tmpl, #key, Impl::name); \
} }
SESSION_JS_METHODS(V) SESSION_JS_METHODS(V)
#undef V #undef V
@ -2317,7 +2311,7 @@ Session::QuicConnectionPointer Session::InitConnection() {
&allocator_, &allocator_,
this), this),
0); 0);
return QuicConnectionPointer(conn); break;
} }
case Side::CLIENT: { case Side::CLIENT: {
CHECK_EQ(ngtcp2_conn_client_new(&conn, CHECK_EQ(ngtcp2_conn_client_new(&conn,
@ -2331,12 +2325,10 @@ Session::QuicConnectionPointer Session::InitConnection() {
&allocator_, &allocator_,
this), this),
0); 0);
if (config_.session_ticket.has_value()) break;
tls_context_.MaybeSetEarlySession(config_.session_ticket.value());
return QuicConnectionPointer(conn);
} }
} }
UNREACHABLE(); return QuicConnectionPointer(conn);
} }
void Session::InitPerIsolate(IsolateData* data, void Session::InitPerIsolate(IsolateData* data,

View file

@ -1,7 +1,5 @@
#pragma once #pragma once
#include <sys/types.h>
#include "quic/tokens.h"
#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS #if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
#if HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC #if HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC
@ -108,7 +106,7 @@ class Session final : public AsyncWrap, private SessionTicket::AppData::Source {
// By default a client session will use the preferred address advertised by // By default a client session will use the preferred address advertised by
// the the server. This option is only relevant for client sessions. // the the server. This option is only relevant for client sessions.
PreferredAddress::Policy preferred_address_strategy = PreferredAddress::Policy preferred_address_strategy =
PreferredAddress::Policy::USE_PREFERRED_ADDRESS; PreferredAddress::Policy::USE_PREFERRED;
TransportParams::Options transport_params = TransportParams::Options transport_params =
TransportParams::Options::kDefault; TransportParams::Options::kDefault;
@ -166,10 +164,6 @@ class Session final : public AsyncWrap, private SessionTicket::AppData::Source {
CID retry_scid = CID::kInvalid; CID retry_scid = CID::kInvalid;
CID preferred_address_cid = CID::kInvalid; CID preferred_address_cid = CID::kInvalid;
// If this is a client session, the session_ticket is used to resume
// a TLS session using a previously established session ticket.
std::optional<SessionTicket> session_ticket = std::nullopt;
ngtcp2_settings settings = {}; ngtcp2_settings settings = {};
operator ngtcp2_settings*() { return &settings; } operator ngtcp2_settings*() { return &settings; }
operator const ngtcp2_settings*() const { return &settings; } operator const ngtcp2_settings*() const { return &settings; }
@ -182,14 +176,12 @@ class Session final : public AsyncWrap, private SessionTicket::AppData::Source {
const SocketAddress& remote_address, const SocketAddress& remote_address,
const CID& dcid, const CID& dcid,
const CID& scid, const CID& scid,
std::optional<SessionTicket> session_ticket = std::nullopt,
const CID& ocid = CID::kInvalid); const CID& ocid = CID::kInvalid);
Config(const Endpoint& endpoint, Config(const Endpoint& endpoint,
const Options& options, const Options& options,
const SocketAddress& local_address, const SocketAddress& local_address,
const SocketAddress& remote_address, const SocketAddress& remote_address,
std::optional<SessionTicket> session_ticket = std::nullopt,
const CID& ocid = CID::kInvalid); const CID& ocid = CID::kInvalid);
void set_token(const uint8_t* token, void set_token(const uint8_t* token,
@ -213,18 +205,23 @@ class Session final : public AsyncWrap, private SessionTicket::AppData::Source {
static void InitPerContext(Realm* env, v8::Local<v8::Object> target); static void InitPerContext(Realm* env, v8::Local<v8::Object> target);
static void RegisterExternalReferences(ExternalReferenceRegistry* registry); static void RegisterExternalReferences(ExternalReferenceRegistry* registry);
static BaseObjectPtr<Session> Create(Endpoint* endpoint, static BaseObjectPtr<Session> Create(
const Config& config); Endpoint* endpoint,
const Config& config,
TLSContext* tls_context,
const std::optional<SessionTicket>& ticket);
// Really should be private but MakeDetachedBaseObject needs visibility. // Really should be private but MakeDetachedBaseObject needs visibility.
Session(Endpoint* endpoint, Session(Endpoint* endpoint,
v8::Local<v8::Object> object, v8::Local<v8::Object> object,
const Config& config); const Config& config,
TLSContext* tls_context,
const std::optional<SessionTicket>& ticket);
~Session() override; ~Session() override;
uint32_t version() const; uint32_t version() const;
Endpoint& endpoint() const; Endpoint& endpoint() const;
TLSContext& tls_context(); TLSSession& tls_session();
Application& application(); Application& application();
const Config& config() const; const Config& config() const;
const Options& options() const; const Options& options() const;
@ -237,6 +234,8 @@ class Session final : public AsyncWrap, private SessionTicket::AppData::Source {
bool is_destroyed() const; bool is_destroyed() const;
bool is_server() const; bool is_server() const;
size_t max_packet_size() const;
void set_priority_supported(bool on = true); void set_priority_supported(bool on = true);
std::string diagnostic_name() const override; std::string diagnostic_name() const override;
@ -248,6 +247,7 @@ class Session final : public AsyncWrap, private SessionTicket::AppData::Source {
TransportParams GetLocalTransportParams() const; TransportParams GetLocalTransportParams() const;
TransportParams GetRemoteTransportParams() const; TransportParams GetRemoteTransportParams() const;
void UpdatePacketTxTime();
void MemoryInfo(MemoryTracker* tracker) const override; void MemoryInfo(MemoryTracker* tracker) const override;
SET_MEMORY_INFO_NAME(Session) SET_MEMORY_INFO_NAME(Session)
@ -290,10 +290,7 @@ class Session final : public AsyncWrap, private SessionTicket::AppData::Source {
Session* session; Session* session;
explicit SendPendingDataScope(Session* session); explicit SendPendingDataScope(Session* session);
explicit SendPendingDataScope(const BaseObjectPtr<Session>& session); explicit SendPendingDataScope(const BaseObjectPtr<Session>& session);
SendPendingDataScope(const SendPendingDataScope&) = delete; DISALLOW_COPY_AND_MOVE(SendPendingDataScope)
SendPendingDataScope(SendPendingDataScope&&) = delete;
SendPendingDataScope& operator=(const SendPendingDataScope&) = delete;
SendPendingDataScope& operator=(SendPendingDataScope&&) = delete;
~SendPendingDataScope(); ~SendPendingDataScope();
}; };
@ -418,7 +415,7 @@ class Session final : public AsyncWrap, private SessionTicket::AppData::Source {
SocketAddress local_address_; SocketAddress local_address_;
SocketAddress remote_address_; SocketAddress remote_address_;
QuicConnectionPointer connection_; QuicConnectionPointer connection_;
TLSContext tls_context_; std::unique_ptr<TLSSession> tls_session_;
std::unique_ptr<Application> application_; std::unique_ptr<Application> application_;
StreamsMap streams_; StreamsMap streams_;
TimerWrapHandle timer_; TimerWrapHandle timer_;
@ -437,6 +434,7 @@ class Session final : public AsyncWrap, private SessionTicket::AppData::Source {
friend struct SendPendingDataScope; friend struct SendPendingDataScope;
friend class Stream; friend class Stream;
friend class TLSContext; friend class TLSContext;
friend class TLSSession;
friend class TransportParams; friend class TransportParams;
}; };

View file

@ -9,6 +9,7 @@
#include <uv.h> #include <uv.h>
#include <v8.h> #include <v8.h>
#include "data.h" #include "data.h"
#include "defs.h"
namespace node { namespace node {
namespace quic { namespace quic {
@ -74,10 +75,7 @@ class SessionTicket::AppData final {
}; };
explicit AppData(SSL* session); explicit AppData(SSL* session);
AppData(const AppData&) = delete; DISALLOW_COPY_AND_MOVE(AppData)
AppData(AppData&&) = delete;
AppData& operator=(const AppData&) = delete;
AppData& operator=(AppData&&) = delete;
bool Set(const uv_buf_t& data); bool Set(const uv_buf_t& data);
std::optional<const uv_buf_t> Get() const; std::optional<const uv_buf_t> Get() const;

View file

@ -3,13 +3,13 @@
#include "tlscontext.h" #include "tlscontext.h"
#include <async_wrap-inl.h> #include <async_wrap-inl.h>
#include <base_object-inl.h> #include <base_object-inl.h>
#include <crypto/crypto_util.h>
#include <debug_utils-inl.h> #include <debug_utils-inl.h>
#include <env-inl.h> #include <env-inl.h>
#include <memory_tracker-inl.h> #include <memory_tracker-inl.h>
#include <ngtcp2/ngtcp2.h> #include <ngtcp2/ngtcp2.h>
#include <ngtcp2/ngtcp2_crypto.h> #include <ngtcp2/ngtcp2_crypto.h>
#include <ngtcp2/ngtcp2_crypto_quictls.h> #include <ngtcp2/ngtcp2_crypto_quictls.h>
#include <node_process-inl.h>
#include <node_sockaddr-inl.h> #include <node_sockaddr-inl.h>
#include <openssl/ssl.h> #include <openssl/ssl.h>
#include <v8.h> #include <v8.h>
@ -21,7 +21,6 @@
namespace node { namespace node {
using v8::ArrayBuffer; using v8::ArrayBuffer;
using v8::BackingStore;
using v8::Just; using v8::Just;
using v8::Local; using v8::Local;
using v8::Maybe; using v8::Maybe;
@ -32,10 +31,9 @@ using v8::Value;
namespace quic { namespace quic {
const TLSContext::Options TLSContext::Options::kDefault = {}; // ============================================================================
namespace { namespace {
// TODO(@jasnell): One time initialization. ngtcp2 says this is optional but // TODO(@jasnell): One time initialization. ngtcp2 says this is optional but
// highly recommended to deal with some perf regression. Unfortunately doing // highly recommended to deal with some perf regression. Unfortunately doing
// this breaks some existing tests and we need to understand the potential // this breaks some existing tests and we need to understand the potential
@ -45,202 +43,6 @@ namespace {
// return 0; // return 0;
// }(); // }();
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(
Session* session,
Side side,
Environment* env,
const TLSContext::Options& options) {
auto context = crypto::SecureContext::Create(env);
auto& ctx = context->ctx();
switch (side) {
case Side::SERVER: {
Debug(session, "Initializing secure context for server");
ctx.reset(SSL_CTX_new(TLS_server_method()));
SSL_CTX_set_app_data(ctx.get(), context);
if (ngtcp2_crypto_quictls_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: {
Debug(session, "Initializing secure context for client");
ctx.reset(SSL_CTX_new(TLS_client_method()));
SSL_CTX_set_app_data(ctx.get(), context);
if (ngtcp2_crypto_quictls_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) { void EnableTrace(Environment* env, crypto::BIOPointer* bio, SSL* ssl) {
#if HAVE_SSL_TRACE #if HAVE_SSL_TRACE
static bool warn_trace_tls = true; static bool warn_trace_tls = true;
@ -345,217 +147,270 @@ bool SetOption(Environment* env,
} }
} // namespace } // namespace
Side TLSContext::side() const { std::shared_ptr<TLSContext> TLSContext::CreateClient(const Options& options) {
return side_; return std::make_shared<TLSContext>(Side::CLIENT, options);
} }
const TLSContext::Options& TLSContext::options() const { std::shared_ptr<TLSContext> TLSContext::CreateServer(const Options& options) {
return options_; return std::make_shared<TLSContext>(Side::SERVER, options);
} }
inline const TLSContext& TLSContext::From(const SSL* ssl) { TLSContext::TLSContext(Side side, const Options& options)
auto ref = static_cast<ngtcp2_crypto_conn_ref*>(SSL_get_app_data(ssl)); : side_(side), options_(options), ctx_(Initialize()) {}
TLSContext* context = ContainerOf(&TLSContext::conn_ref_, ref);
return *context; TLSContext::operator SSL_CTX*() const {
DCHECK(ctx_);
return ctx_.get();
} }
inline TLSContext& TLSContext::From(SSL* ssl) { int TLSContext::OnSelectAlpn(SSL* ssl,
auto ref = static_cast<ngtcp2_crypto_conn_ref*>(SSL_get_app_data(ssl)); const unsigned char** out,
TLSContext* context = ContainerOf(&TLSContext::conn_ref_, ref); unsigned char* outlen,
return *context; const unsigned char* in,
unsigned int inlen,
void* arg) {
static constexpr size_t kMaxAlpnLen = 255;
auto& session = TLSSession::From(ssl);
const auto& requested = session.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.data()),
requested.length(),
in,
inlen) == OPENSSL_NPN_NO_OVERLAP) {
Debug(&session.session(), "ALPN negotiation failed");
return SSL_TLSEXT_ERR_NOACK;
}
Debug(&session.session(), "ALPN negotiation succeeded");
return SSL_TLSEXT_ERR_OK;
} }
TLSContext::TLSContext(Environment* env, int TLSContext::OnNewSession(SSL* ssl, SSL_SESSION* sess) {
Side side, auto& session = TLSSession::From(ssl).session();
Session* session,
const Options& options)
: conn_ref_({getConnection, this}),
side_(side),
env_(env),
session_(session),
options_(options),
secure_context_(InitializeSecureContext(session, 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_); // If there is nothing listening for the session ticket, do not bother.
SSL_set_verify(ssl_.get(), SSL_VERIFY_NONE, crypto::VerifyCallback); if (session.wants_session_ticket()) {
Debug(&session, "Preparing TLS session resumption ticket");
// Enable tracing if the `--trace-tls` command line flag is used. // Pre-fight to see how much space we need to allocate for the session
if (UNLIKELY(env->options()->trace_tls || options.enable_tls_trace)) // ticket.
EnableTrace(env, &bio_trace_, ssl_.get()); size_t size = i2d_SSL_SESSION(sess, nullptr);
switch (side) { // If size is 0 or the size is greater than our max, let's ignore it
case Side::CLIENT: { // and continue without emitting the sessionticket event.
SSL_set_connect_state(ssl_.get()); if (size > 0 && size <= crypto::SecureContext::kMaxSessionSize) {
CHECK_EQ(0, auto ticket =
SSL_set_alpn_protos(ssl_.get(), ArrayBuffer::NewBackingStore(session.env()->isolate(), size);
reinterpret_cast<const unsigned char*>( auto data = reinterpret_cast<unsigned char*>(ticket->Data());
options_.alpn.c_str()), if (i2d_SSL_SESSION(sess, &data) > 0) {
options_.alpn.length())); session.EmitSessionTicket(Store(std::move(ticket), size));
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() {
Debug(session_, "Crypto context is starting");
ngtcp2_conn_set_tls_native_handle(*session_, ssl_.get());
TransportParams tp(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::OnNewSession(SSL_SESSION* session) {
Debug(session_, "Crypto context received new crypto 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; return 0;
} }
bool TLSContext::InitiateKeyUpdate() { void TLSContext::OnKeylog(const SSL* ssl, const char* line) {
Debug(session_, "Crypto context initiating key update"); TLSSession::From(ssl).session().EmitKeylog(line);
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() { int TLSContext::OnVerifyClientCertificate(int preverify_ok,
Debug(session_, "Crypto context verifying peer identity"); X509_STORE_CTX* ctx) {
return crypto::VerifyPeerCertificate(ssl_); // TODO(@jasnell): Implement the logic to verify the client certificate
return 1;
} }
void TLSContext::MaybeSetEarlySession(const SessionTicket& sessionTicket) { std::unique_ptr<TLSSession> TLSContext::NewSession(
Debug(session_, "Crypto context setting early session"); Session* session, const std::optional<SessionTicket>& maybeSessionTicket) {
uv_buf_t buf = sessionTicket.ticket(); // Passing a session ticket only makes sense with a client session.
crypto::SSLSessionPointer ticket = crypto::GetTLSSession( CHECK_IMPLIES(session->is_server(), !maybeSessionTicket.has_value());
reinterpret_cast<unsigned char*>(buf.base), buf.len); return std::make_unique<TLSSession>(
session, shared_from_this(), maybeSessionTicket);
}
// Silently ignore invalid TLS session crypto::SSLCtxPointer TLSContext::Initialize() {
if (!ticket || !SSL_SESSION_get_max_early_data(ticket.get())) return; crypto::SSLCtxPointer ctx;
switch (side_) {
case Side::SERVER: {
static constexpr unsigned char kSidCtx[] = "Node.js QUIC Server";
ctx.reset(SSL_CTX_new(TLS_server_method()));
CHECK_EQ(ngtcp2_crypto_quictls_configure_server_context(ctx.get()), 0);
CHECK_EQ(SSL_CTX_set_max_early_data(ctx.get(), UINT32_MAX), 1);
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(), OnSelectAlpn, this);
CHECK_EQ(SSL_CTX_set_session_id_context(
ctx.get(), kSidCtx, sizeof(kSidCtx) - 1),
1);
// The early data will just be ignored if it's invalid. if (options_.verify_client) {
if (!crypto::SetTLSSession(ssl_, ticket)) return; SSL_CTX_set_verify(ctx.get(),
SSL_VERIFY_PEER | SSL_VERIFY_CLIENT_ONCE |
SSL_VERIFY_FAIL_IF_NO_PEER_CERT,
OnVerifyClientCertificate);
}
ngtcp2_vec rtp = sessionTicket.transport_params(); CHECK_EQ(SSL_CTX_set_session_ticket_cb(ctx.get(),
// Decode and attempt to set the early transport parameters configured SessionTicket::GenerateCallback,
// for the early session. If non-zero is returned, decoding or setting SessionTicket::DecryptedCallback,
// failed, in which case we just ignore it. nullptr),
if (ngtcp2_conn_decode_and_set_0rtt_transport_params( 1);
*session_, rtp.base, rtp.len) != 0) break;
return; }
case Side::CLIENT: {
ctx_.reset(SSL_CTX_new(TLS_client_method()));
CHECK_EQ(ngtcp2_crypto_quictls_configure_client_context(ctx.get()), 0);
session_->SetStreamOpenAllowed(); SSL_CTX_set_session_cache_mode(
ctx.get(), SSL_SESS_CACHE_CLIENT | SSL_SESS_CACHE_NO_INTERNAL);
SSL_CTX_sess_set_new_cb(ctx.get(), OnNewSession);
break;
}
}
SSL_CTX_set_default_verify_paths(ctx.get());
SSL_CTX_set_keylog_callback(ctx.get(), OnKeylog);
if (SSL_CTX_set_ciphersuites(ctx.get(), options_.ciphers.c_str()) != 1) {
validation_error_ = "Invalid cipher suite";
return crypto::SSLCtxPointer();
}
if (SSL_CTX_set1_groups_list(ctx.get(), options_.groups.c_str()) != 1) {
validation_error_ = "Invalid cipher groups";
return crypto::SSLCtxPointer();
}
{
crypto::ClearErrorOnReturn clear_error_on_return;
if (options_.ca.empty()) {
auto store = crypto::GetOrCreateRootCertStore();
X509_STORE_up_ref(store);
SSL_CTX_set_cert_store(ctx.get(), store);
} else {
for (const auto& ca : options_.ca) {
uv_buf_t buf = ca;
if (buf.len == 0) {
auto store = crypto::GetOrCreateRootCertStore();
X509_STORE_up_ref(store);
SSL_CTX_set_cert_store(ctx.get(), store);
} else {
crypto::BIOPointer bio = crypto::NodeBIO::NewFixed(buf.base, buf.len);
CHECK(bio);
X509_STORE* cert_store = SSL_CTX_get_cert_store(ctx.get());
while (crypto::X509Pointer x509 = crypto::X509Pointer(
PEM_read_bio_X509_AUX(bio.get(),
nullptr,
crypto::NoPasswordCallback,
nullptr))) {
if (cert_store == crypto::GetOrCreateRootCertStore()) {
cert_store = crypto::NewRootCertStore();
SSL_CTX_set_cert_store(ctx.get(), cert_store);
}
CHECK_EQ(1, X509_STORE_add_cert(cert_store, x509.get()));
CHECK_EQ(1, SSL_CTX_add_client_CA(ctx.get(), x509.get()));
}
}
}
}
}
{
crypto::ClearErrorOnReturn clear_error_on_return;
for (const auto& cert : options_.certs) {
uv_buf_t buf = cert;
if (buf.len > 0) {
crypto::BIOPointer bio = crypto::NodeBIO::NewFixed(buf.base, buf.len);
CHECK(bio);
cert_.reset();
issuer_.reset();
if (crypto::SSL_CTX_use_certificate_chain(
ctx.get(), std::move(bio), &cert_, &issuer_) == 0) {
validation_error_ = "Invalid certificate";
return crypto::SSLCtxPointer();
}
}
}
}
{
crypto::ClearErrorOnReturn clear_error_on_return;
for (const auto& key : options_.keys) {
if (key->GetKeyType() != crypto::KeyType::kKeyTypePrivate) {
validation_error_ = "Invalid key";
return crypto::SSLCtxPointer();
}
if (!SSL_CTX_use_PrivateKey(ctx.get(), key->GetAsymmetricKey().get())) {
validation_error_ = "Invalid key";
return crypto::SSLCtxPointer();
}
}
}
{
crypto::ClearErrorOnReturn clear_error_on_return;
for (const auto& crl : options_.crl) {
uv_buf_t buf = crl;
crypto::BIOPointer bio = crypto::NodeBIO::NewFixed(buf.base, buf.len);
DeleteFnPtr<X509_CRL, X509_CRL_free> crlptr(PEM_read_bio_X509_CRL(
bio.get(), nullptr, crypto::NoPasswordCallback, nullptr));
if (!crlptr) {
validation_error_ = "Invalid CRL";
return crypto::SSLCtxPointer();
}
X509_STORE* cert_store = SSL_CTX_get_cert_store(ctx.get());
if (cert_store == crypto::GetOrCreateRootCertStore()) {
cert_store = crypto::NewRootCertStore();
SSL_CTX_set_cert_store(ctx.get(), cert_store);
}
CHECK_EQ(1, X509_STORE_add_crl(cert_store, crlptr.get()));
CHECK_EQ(
1,
X509_STORE_set_flags(
cert_store, X509_V_FLAG_CRL_CHECK | X509_V_FLAG_CRL_CHECK_ALL));
}
}
{
crypto::ClearErrorOnReturn clear_error_on_return;
if (options_.verify_private_key &&
SSL_CTX_check_private_key(ctx.get()) != 1) {
validation_error_ = "Invalid private key";
return crypto::SSLCtxPointer();
}
}
return ctx;
} }
void TLSContext::MemoryInfo(MemoryTracker* tracker) const { void TLSContext::MemoryInfo(MemoryTracker* tracker) const {
tracker->TrackField("options", options_); 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<TLSContext::Options> TLSContext::Options::From(Environment* env, Maybe<TLSContext::Options> TLSContext::Options::From(Environment* env,
Local<Value> value) { Local<Value> value) {
if (value.IsEmpty()) { if (value.IsEmpty()) {
THROW_ERR_INVALID_ARG_TYPE(env, "options must be an object");
return Nothing<Options>(); return Nothing<Options>();
} }
@ -563,19 +418,11 @@ Maybe<TLSContext::Options> TLSContext::Options::From(Environment* env,
auto& state = BindingData::Get(env); auto& state = BindingData::Get(env);
if (value->IsUndefined()) { if (value->IsUndefined()) {
// We need at least one key and one cert to complete the tls handshake. return Just(TLSContext::Options::kDefault);
// Why not make this an error? We could but it's not strictly necessary.
env->EmitProcessEnvWarning();
ProcessEmitWarning(
env,
"The default QUIC TLS options are being used. "
"This means there is no key or certificate configured and the "
"TLS handshake will fail. This is likely not what you want.");
return Just<Options>(options);
} }
if (!value->IsObject()) { if (!value->IsObject()) {
THROW_ERR_INVALID_ARG_TYPE(env, "options must be an object"); THROW_ERR_INVALID_ARG_TYPE(env, "tls options must be an object");
return Nothing<Options>(); return Nothing<Options>();
} }
@ -589,26 +436,15 @@ Maybe<TLSContext::Options> TLSContext::Options::From(Environment* env,
SetOption<TLSContext::Options, &TLSContext::Options::name>( \ SetOption<TLSContext::Options, &TLSContext::Options::name>( \
env, &options, params, state.name##_string()) env, &options, params, state.name##_string())
if (!SET(keylog) || !SET(reject_unauthorized) || !SET(enable_tls_trace) || if (!SET(verify_client) || !SET(enable_tls_trace) || !SET(alpn) ||
!SET(request_peer_certificate) || !SET(verify_hostname_identity) || !SET(sni) || !SET(ciphers) || !SET(groups) || !SET(verify_private_key) ||
!SET(alpn) || !SET(hostname) || !SET(session_id_ctx) || !SET(ciphers) || !SET(keylog) ||
!SET(groups) ||
!SET_VECTOR(std::shared_ptr<crypto::KeyObjectData>, keys) || !SET_VECTOR(std::shared_ptr<crypto::KeyObjectData>, keys) ||
!SET_VECTOR(Store, certs) || !SET_VECTOR(Store, ca) || !SET_VECTOR(Store, certs) || !SET_VECTOR(Store, ca) ||
!SET_VECTOR(Store, crl)) { !SET_VECTOR(Store, crl)) {
return Nothing<Options>(); return Nothing<Options>();
} }
// We need at least one key and one cert to complete the tls handshake.
// Why not make this an error? We could but it's not strictly necessary.
if (options.keys.empty() || options.certs.empty()) {
env->EmitProcessEnvWarning();
ProcessEmitWarning(env,
"The QUIC TLS options did not include a key or cert. "
"This means the TLS handshake will fail. This is likely "
"not what you want.");
}
return Just<Options>(options); return Just<Options>(options);
} }
@ -617,18 +453,15 @@ std::string TLSContext::Options::ToString() const {
auto prefix = indent.Prefix(); auto prefix = indent.Prefix();
std::string res("{"); std::string res("{");
res += prefix + "alpn: " + alpn; res += prefix + "alpn: " + alpn;
res += prefix + "hostname: " + hostname; res += prefix + "sni: " + sni;
res += res +=
prefix + "keylog: " + (keylog ? std::string("yes") : std::string("no")); prefix + "keylog: " + (keylog ? std::string("yes") : std::string("no"));
res += prefix + "reject_unauthorized: " + res += prefix + "verify client: " +
(reject_unauthorized ? std::string("yes") : std::string("no")); (verify_client ? std::string("yes") : std::string("no"));
res += prefix + "enable_tls_trace: " + res += prefix + "enable_tls_trace: " +
(enable_tls_trace ? std::string("yes") : std::string("no")); (enable_tls_trace ? std::string("yes") : std::string("no"));
res += prefix + "request_peer_certificate: " + res += prefix + "verify private key: " +
(request_peer_certificate ? std::string("yes") : std::string("no")); (verify_private_key ? std::string("yes") : std::string("no"));
res += prefix + "verify_hostname_identity: " +
(verify_hostname_identity ? std::string("yes") : std::string("no"));
res += prefix + "session_id_ctx: " + session_id_ctx;
res += prefix + "ciphers: " + ciphers; res += prefix + "ciphers: " + ciphers;
res += prefix + "groups: " + groups; res += prefix + "groups: " + groups;
res += prefix + "keys: " + std::to_string(keys.size()); res += prefix + "keys: " + std::to_string(keys.size());
@ -639,6 +472,179 @@ std::string TLSContext::Options::ToString() const {
return res; return res;
} }
void TLSContext::Options::MemoryInfo(MemoryTracker* tracker) const {
tracker->TrackField("keys", keys);
tracker->TrackField("certs", certs);
tracker->TrackField("ca", ca);
tracker->TrackField("crl", crl);
}
const TLSContext::Options TLSContext::Options::kDefault = {};
// ============================================================================
const TLSSession& TLSSession::From(const SSL* ssl) {
auto ref = static_cast<ngtcp2_crypto_conn_ref*>(SSL_get_app_data(ssl));
CHECK_NOT_NULL(ref);
return *static_cast<TLSSession*>(ref->user_data);
}
TLSSession::TLSSession(Session* session,
std::shared_ptr<TLSContext> context,
const std::optional<SessionTicket>& maybeSessionTicket)
: ref_({connection, this}),
context_(std::move(context)),
session_(session),
ssl_(Initialize(maybeSessionTicket)) {
Debug(session_, "Created new TLS session for %s", session->config().dcid);
}
TLSSession::operator SSL*() const {
CHECK(ssl_);
return ssl_.get();
}
bool TLSSession::early_data_was_accepted() const {
CHECK_NE(ngtcp2_conn_get_handshake_completed(*session_), 0);
return SSL_get_early_data_status(*this) == SSL_EARLY_DATA_ACCEPTED;
}
crypto::SSLPointer TLSSession::Initialize(
const std::optional<SessionTicket>& maybeSessionTicket) {
auto& ctx = context();
auto& options = ctx.options();
crypto::SSLPointer ssl(SSL_new(ctx));
SSL_set_app_data(ssl.get(), &ref_);
ngtcp2_conn_set_tls_native_handle(*session_, ssl.get());
// Enable tracing if the `--trace-tls` command line flag is used.
if (UNLIKELY(session_->env()->options()->trace_tls ||
options.enable_tls_trace)) {
EnableTrace(session_->env(), &bio_trace_, *this);
}
switch (ctx.side()) {
case Side::SERVER: {
SSL_set_accept_state(ssl.get());
SSL_set_quic_early_data_enabled(ssl.get(), 1);
break;
}
case Side::CLIENT: {
SSL_set_connect_state(ssl.get());
if (SSL_set_alpn_protos(
ssl.get(),
reinterpret_cast<const unsigned char*>(options.alpn.data()),
options.alpn.size()) != 0) {
validation_error_ = "Invalid ALPN";
return crypto::SSLPointer();
}
if (!options.sni.empty()) {
SSL_set_tlsext_host_name(ssl.get(), options.sni.data());
} else {
SSL_set_tlsext_host_name(ssl.get(), "localhost");
}
if (maybeSessionTicket.has_value()) {
auto sessionTicket = maybeSessionTicket.value();
uv_buf_t buf = sessionTicket.ticket();
crypto::SSLSessionPointer ticket = crypto::GetTLSSession(
reinterpret_cast<unsigned char*>(buf.base), buf.len);
// The early data will just be ignored if it's invalid.
if (crypto::SetTLSSession(ssl, ticket) &&
SSL_SESSION_get_max_early_data(ticket.get()) != 0) {
ngtcp2_vec rtp = sessionTicket.transport_params();
if (ngtcp2_conn_decode_and_set_0rtt_transport_params(
*session_, rtp.base, rtp.len) == 0) {
SSL_set_quic_early_data_enabled(ssl.get(), 1);
session_->SetStreamOpenAllowed();
}
}
}
break;
}
}
TransportParams tp(ngtcp2_conn_get_local_transport_params(*session_));
Store store = tp.Encode(session_->env());
if (store && store.length() > 0) {
ngtcp2_vec vec = store;
SSL_set_quic_transport_params(ssl.get(), vec.base, vec.len);
}
return ssl;
}
std::optional<TLSSession::PeerIdentityValidationError>
TLSSession::VerifyPeerIdentity(Environment* env) {
int err = crypto::VerifyPeerCertificate(ssl_);
if (err == X509_V_OK) return std::nullopt;
Local<Value> reason;
Local<Value> code;
if (!crypto::GetValidationErrorReason(env, err).ToLocal(&reason) ||
!crypto::GetValidationErrorCode(env, err).ToLocal(&code)) {
// Getting the validation error details failed. We'll return a value but
// the fields will be empty.
return PeerIdentityValidationError{};
}
return PeerIdentityValidationError{reason, code};
}
MaybeLocal<Object> TLSSession::cert(Environment* env) const {
return crypto::X509Certificate::GetCert(env, ssl_);
}
MaybeLocal<Object> TLSSession::peer_cert(Environment* env) const {
crypto::X509Certificate::GetPeerCertificateFlag flag =
context_->side() == Side::SERVER
? crypto::X509Certificate::GetPeerCertificateFlag::SERVER
: crypto::X509Certificate::GetPeerCertificateFlag::NONE;
return crypto::X509Certificate::GetPeerCert(env, ssl_, flag);
}
MaybeLocal<Object> TLSSession::ephemeral_key(Environment* env) const {
return crypto::GetEphemeralKey(env, ssl_);
}
MaybeLocal<Value> TLSSession::cipher_name(Environment* env) const {
return crypto::GetCurrentCipherName(env, ssl_);
}
MaybeLocal<Value> TLSSession::cipher_version(Environment* env) const {
return crypto::GetCurrentCipherVersion(env, ssl_);
}
const std::string_view TLSSession::servername() const {
const char* servername = crypto::GetServerName(ssl_.get());
return servername != nullptr ? std::string_view(servername)
: std::string_view();
}
const std::string_view TLSSession::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 TLSSession::InitiateKeyUpdate() {
if (session_->is_destroyed() || in_key_update_) return false;
auto leave = OnScopeLeave([this] { in_key_update_ = false; });
in_key_update_ = true;
Debug(session_, "Initiating key update");
return ngtcp2_conn_initiate_key_update(*session_, uv_hrtime()) == 0;
}
ngtcp2_conn* TLSSession::connection(ngtcp2_crypto_conn_ref* ref) {
CHECK_NOT_NULL(ref->user_data);
return static_cast<TLSSession*>(ref->user_data)->session();
}
} // namespace quic } // namespace quic
} // namespace node } // namespace node

View file

@ -10,92 +10,162 @@
#include <ngtcp2/ngtcp2_crypto.h> #include <ngtcp2/ngtcp2_crypto.h>
#include "bindingdata.h" #include "bindingdata.h"
#include "data.h" #include "data.h"
#include "defs.h"
#include "sessionticket.h" #include "sessionticket.h"
namespace node { namespace node {
namespace quic { namespace quic {
class Session; class Session;
class TLSContext;
// Every QUIC Session has exactly one TLSContext that maintains the state // Every QUIC Session has exactly one TLSSession that maintains the state
// of the TLS handshake and negotiated cipher keys after the handshake has // of the TLS handshake and negotiated keys after the handshake has been
// been completed. It is separated out from the main Session class only as a // completed. It is separated out from the main Session class only as a
// convenience to help make the code more maintainable and understandable. // convenience to help make the code more maintainable and understandable.
class TLSContext final : public MemoryRetainer { // A TLSSession is created from a TLSContext and maintains a reference to
// the context.
class TLSSession final : public MemoryRetainer {
public: public:
enum class EncryptionLevel { static const TLSSession& From(const SSL* ssl);
INITIAL = NGTCP2_ENCRYPTION_LEVEL_INITIAL,
HANDSHAKE = NGTCP2_ENCRYPTION_LEVEL_HANDSHAKE, // The constructor is public in order to satisify the call to std::make_unique
ONERTT = NGTCP2_ENCRYPTION_LEVEL_1RTT, // in TLSContext::NewSession. It should not be called directly.
ZERORTT = NGTCP2_ENCRYPTION_LEVEL_0RTT, TLSSession(Session* session,
std::shared_ptr<TLSContext> context,
const std::optional<SessionTicket>& maybeSessionTicket);
DISALLOW_COPY_AND_MOVE(TLSSession)
inline operator bool() const { return ssl_ != nullptr; }
inline Session& session() const { return *session_; }
inline TLSContext& context() const { return *context_; }
// Returns true if the handshake has been completed and early data was
// accepted by the TLS session. This will assert if the handshake has
// not been completed.
bool early_data_was_accepted() const;
v8::MaybeLocal<v8::Object> cert(Environment* env) const;
v8::MaybeLocal<v8::Object> peer_cert(Environment* env) const;
v8::MaybeLocal<v8::Object> ephemeral_key(Environment* env) const;
v8::MaybeLocal<v8::Value> cipher_name(Environment* env) const;
v8::MaybeLocal<v8::Value> cipher_version(Environment* env) const;
// The SNI (server name) 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 or if the initial handshake has not yet
// been confirmed.
bool InitiateKeyUpdate();
struct PeerIdentityValidationError {
v8::MaybeLocal<v8::Value> reason;
v8::MaybeLocal<v8::Value> code;
}; };
// Checks the peer identity against the configured CA and CRL. If the peer
// certificate is valid, std::nullopt is returned. Otherwise a
// PeerIdentityValidationError is returned with the reason and code for the
// failure.
std::optional<PeerIdentityValidationError> VerifyPeerIdentity(
Environment* env);
inline const std::string_view validation_error() const {
return validation_error_;
}
SET_NO_MEMORY_INFO()
SET_MEMORY_INFO_NAME(TLSSession)
SET_SELF_SIZE(TLSSession)
private:
operator SSL*() const;
crypto::SSLPointer Initialize(
const std::optional<SessionTicket>& maybeSessionTicket);
static ngtcp2_conn* connection(ngtcp2_crypto_conn_ref* ref);
ngtcp2_crypto_conn_ref ref_;
std::shared_ptr<TLSContext> context_;
Session* session_;
crypto::SSLPointer ssl_;
crypto::BIOPointer bio_trace_;
std::string validation_error_ = "";
bool in_key_update_ = false;
};
// The TLSContext is used to create a TLSSession. For the client, there is
// typically only a single TLSContext for each TLSSession. For the server,
// there is a single TLSContext for the server and a TLSSession for every
// QUIC session created by that server.
class TLSContext final : public MemoryRetainer,
public std::enable_shared_from_this<TLSContext> {
public:
static constexpr auto DEFAULT_CIPHERS = "TLS_AES_128_GCM_SHA256:" static constexpr auto DEFAULT_CIPHERS = "TLS_AES_128_GCM_SHA256:"
"TLS_AES_256_GCM_SHA384:" "TLS_AES_256_GCM_SHA384:"
"TLS_CHACHA20_POLY1305_" "TLS_CHACHA20_POLY1305_"
"SHA256:TLS_AES_128_CCM_SHA256"; "SHA256:TLS_AES_128_CCM_SHA256";
static constexpr auto DEFAULT_GROUPS = "X25519:P-256:P-384:P-521"; 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 { struct Options final : public MemoryRetainer {
// The protocol identifier to be used by this Session. // The SNI servername to use for this session. This option is only used by
// the client.
std::string sni = "localhost";
// The ALPN (protocol name) to use for this session. This option is only
// used by the client.
std::string alpn = NGHTTP3_ALPN_H3; std::string alpn = NGHTTP3_ALPN_H3;
// The SNI hostname to be used. This is used only by client Sessions to // The list of TLS ciphers to use for this session.
// 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; std::string ciphers = DEFAULT_CIPHERS;
// TLS groups // The list of TLS groups to use for this session.
std::string groups = DEFAULT_GROUPS; std::string groups = DEFAULT_GROUPS;
// The TLS private key to use for this session. // When true, enables keylog output for the session.
bool keylog = false;
// When true, the peer certificate is verified against the list of supplied
// CA. If verification fails, the connection will be refused. When set,
// instructs the server session to request a client auth certificate. This
// option is only used by the server side.
bool verify_client = false;
// When true, enables TLS tracing for the session. This should only be used
// for debugging.
// JavaScript option name "tlsTrace".
bool enable_tls_trace = false;
// When true, causes the private key passed in for the session to be
// verified.
// JavaScript option name "verifyPrivateKey"
bool verify_private_key = false;
// The TLS private key(s) to use for this session.
// JavaScript option name "keys"
std::vector<std::shared_ptr<crypto::KeyObjectData>> keys; std::vector<std::shared_ptr<crypto::KeyObjectData>> keys;
// Collection of certificates to use for this session. // Collection of certificates to use for this session.
// JavaScript option name "certs"
std::vector<Store> certs; std::vector<Store> certs;
// Optional certificate authority overrides to use. // Optional certificate authority overrides to use.
// JavaScript option name "ca"
std::vector<Store> ca; std::vector<Store> ca;
// Optional certificate revocation lists to use. // Optional certificate revocation lists to use.
// JavaScript option name "crl"
std::vector<Store> crl; std::vector<Store> crl;
void MemoryInfo(MemoryTracker* tracker) const override; void MemoryInfo(MemoryTracker* tracker) const override;
SET_MEMORY_INFO_NAME(CryptoContext::Options) SET_MEMORY_INFO_NAME(TLSContext::Options)
SET_SELF_SIZE(Options) SET_SELF_SIZE(Options)
// The default TLS configuration.
static const Options kDefault; static const Options kDefault;
static v8::Maybe<Options> From(Environment* env, static v8::Maybe<Options> From(Environment* env,
@ -104,74 +174,51 @@ class TLSContext final : public MemoryRetainer {
std::string ToString() const; std::string ToString() const;
}; };
static const Options kDefaultOptions; static std::shared_ptr<TLSContext> CreateClient(const Options& options);
static std::shared_ptr<TLSContext> CreateServer(const Options& options);
TLSContext(Environment* env, TLSContext(Side side, const Options& options);
Side side, DISALLOW_COPY_AND_MOVE(TLSContext)
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. // Each QUIC Session has exactly one TLSSession. Each TLSSession maintains
void Start(); // a reference to the TLSContext used to create it.
std::unique_ptr<TLSSession> NewSession(
Session* session, const std::optional<SessionTicket>& maybeSessionTicket);
// TLS Keylogging is enabled per-Session by attaching a handler to the inline Side side() const { return side_; }
// "keylog" event. Each keylog line is emitted to JavaScript where it can be inline const Options& options() const { return options_; }
// routed to whatever destination makes sense. Typically, this will be to a inline operator bool() const { return ctx_ != nullptr; }
// keylog file that can be consumed by tools like Wireshark to intercept and
// decrypt QUIC network traffic.
void Keylog(const char* line) const;
v8::MaybeLocal<v8::Object> cert(Environment* env) const; inline const std::string_view validation_error() const {
v8::MaybeLocal<v8::Object> peer_cert(Environment* env) const; return validation_error_;
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; void MemoryInfo(MemoryTracker* tracker) const override;
SET_MEMORY_INFO_NAME(CryptoContext) SET_MEMORY_INFO_NAME(TLSContext)
SET_SELF_SIZE(TLSContext) SET_SELF_SIZE(TLSContext)
private: private:
static ngtcp2_conn* getConnection(ngtcp2_crypto_conn_ref* ref); crypto::SSLCtxPointer Initialize();
ngtcp2_crypto_conn_ref conn_ref_; operator SSL_CTX*() const;
static void OnKeylog(const SSL* ssl, const char* line);
static int OnNewSession(SSL* ssl, SSL_SESSION* session);
static int OnSelectAlpn(SSL* ssl,
const unsigned char** out,
unsigned char* outlen,
const unsigned char* in,
unsigned int inlen,
void* arg);
static int OnVerifyClientCertificate(int preverify_ok, X509_STORE_CTX* ctx);
Side side_; Side side_;
Environment* env_; Options options_;
Session* session_; crypto::X509Pointer cert_;
const Options options_; crypto::X509Pointer issuer_;
BaseObjectPtr<crypto::SecureContext> secure_context_; crypto::SSLCtxPointer ctx_;
crypto::SSLPointer ssl_; std::string validation_error_ = "";
crypto::BIOPointer bio_trace_;
bool in_key_update_ = false; friend class TLSSession;
bool early_data_ = false;
friend class Session;
}; };
} // namespace quic } // namespace quic

View file

@ -8,6 +8,7 @@
#include <node_internals.h> #include <node_internals.h>
#include <node_sockaddr.h> #include <node_sockaddr.h>
#include "cid.h" #include "cid.h"
#include "defs.h"
namespace node { namespace node {
namespace quic { namespace quic {
@ -35,8 +36,7 @@ class TokenSecret final : public MemoryRetainer {
TokenSecret(const TokenSecret&) = default; TokenSecret(const TokenSecret&) = default;
TokenSecret& operator=(const TokenSecret&) = default; TokenSecret& operator=(const TokenSecret&) = default;
TokenSecret(TokenSecret&&) = delete; DISALLOW_MOVE(TokenSecret)
TokenSecret& operator=(TokenSecret&&) = delete;
operator const uint8_t*() const; operator const uint8_t*() const;
uint8_t operator[](int pos) const; uint8_t operator[](int pos) const;
@ -99,7 +99,7 @@ class StatelessResetToken final : public MemoryRetainer {
explicit StatelessResetToken(const uint8_t* token); explicit StatelessResetToken(const uint8_t* token);
StatelessResetToken(const StatelessResetToken& other); StatelessResetToken(const StatelessResetToken& other);
StatelessResetToken(StatelessResetToken&&) = delete; DISALLOW_MOVE(StatelessResetToken)
std::string ToString() const; std::string ToString() const;

View file

@ -1,4 +1,5 @@
#if HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC #if HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC
#include <env-inl.h>
#include <gtest/gtest.h> #include <gtest/gtest.h>
#include <ngtcp2/ngtcp2.h> #include <ngtcp2/ngtcp2.h>
#include <quic/cid.h> #include <quic/cid.h>

View file

@ -0,0 +1,132 @@
#if HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC
#include <env-inl.h>
#include <gtest/gtest.h>
#include <quic/data.h>
#include <util-inl.h>
#include <string>
using node::quic::QuicError;
TEST(QuicError, NoError) {
QuicError err;
CHECK_EQ(err.code(), QuicError::QUIC_NO_ERROR);
CHECK_EQ(err.type(), QuicError::Type::TRANSPORT);
CHECK_EQ(err.reason(), "");
CHECK_EQ(err, QuicError::TRANSPORT_NO_ERROR);
CHECK(!err);
QuicError err2("a reason");
CHECK_EQ(err2.code(), QuicError::QUIC_NO_ERROR);
CHECK_EQ(err2.type(), QuicError::Type::TRANSPORT);
CHECK_EQ(err2.reason(), "a reason");
// Equality check ignores the reason
CHECK_EQ(err2, QuicError::TRANSPORT_NO_ERROR);
auto err3 = QuicError::ForTransport(QuicError::QUIC_NO_ERROR);
CHECK_EQ(err3.code(), QuicError::QUIC_NO_ERROR);
CHECK_EQ(err3.type(), QuicError::Type::TRANSPORT);
CHECK_EQ(err3.reason(), "");
CHECK_EQ(err3, QuicError::TRANSPORT_NO_ERROR);
// QuicError's are copy assignable
auto err4 = err3;
CHECK_EQ(err4, err3);
// QuicError's are movable
auto err5 = std::move(err4);
CHECK_EQ(err5, err3);
// Equality check ignores the reason
CHECK(err5 == err2);
CHECK(err5 != QuicError::APPLICATION_NO_ERROR);
const ngtcp2_ccerr& ccerr = err5;
CHECK_EQ(ccerr.error_code, NGTCP2_NO_ERROR);
CHECK_EQ(ccerr.frame_type, 0);
CHECK_EQ(ccerr.type, NGTCP2_CCERR_TYPE_TRANSPORT);
CHECK_EQ(ccerr.reasonlen, 0);
const ngtcp2_ccerr* ccerr2 = err5;
CHECK_EQ(ccerr2->error_code, NGTCP2_NO_ERROR);
CHECK_EQ(ccerr2->frame_type, 0);
CHECK_EQ(ccerr2->type, NGTCP2_CCERR_TYPE_TRANSPORT);
CHECK_EQ(ccerr2->reasonlen, 0);
QuicError err6(ccerr);
QuicError err7(ccerr2);
CHECK_EQ(err6, err7);
CHECK_EQ(err.ToString(), "QuicError(TRANSPORT) 0");
CHECK_EQ(err2.ToString(), "QuicError(TRANSPORT) 0: a reason");
ngtcp2_ccerr ccerr3;
ngtcp2_ccerr_default(&ccerr3);
ccerr3.frame_type = 1;
QuicError err8(ccerr3);
CHECK_EQ(err8.frame_type(), 1);
}
TEST(QuicError, ApplicationNoError) {
CHECK_EQ(QuicError::APPLICATION_NO_ERROR.code(),
QuicError::QUIC_APP_NO_ERROR);
CHECK_EQ(QuicError::APPLICATION_NO_ERROR.type(),
QuicError::Type::APPLICATION);
CHECK_EQ(QuicError::APPLICATION_NO_ERROR.reason(), "");
auto err =
QuicError::ForApplication(QuicError::QUIC_APP_NO_ERROR, "a reason");
CHECK_EQ(err.code(), QuicError::QUIC_APP_NO_ERROR);
CHECK_EQ(err.type(), QuicError::Type::APPLICATION);
CHECK_EQ(err.reason(), "a reason");
CHECK_EQ(err.ToString(), "QuicError(APPLICATION) 65280: a reason");
}
TEST(QuicError, VersionNegotiation) {
CHECK_EQ(QuicError::VERSION_NEGOTIATION.code(), 0);
CHECK_EQ(QuicError::VERSION_NEGOTIATION.type(),
QuicError::Type::VERSION_NEGOTIATION);
CHECK_EQ(QuicError::VERSION_NEGOTIATION.reason(), "");
auto err = QuicError::ForVersionNegotiation("a reason");
CHECK_EQ(err.code(), 0);
CHECK_EQ(err.type(), QuicError::Type::VERSION_NEGOTIATION);
CHECK_EQ(err.reason(), "a reason");
CHECK_EQ(err.ToString(), "QuicError(VERSION_NEGOTIATION) 0: a reason");
}
TEST(QuicError, IdleClose) {
CHECK_EQ(QuicError::IDLE_CLOSE.code(), 0);
CHECK_EQ(QuicError::IDLE_CLOSE.type(), QuicError::Type::IDLE_CLOSE);
CHECK_EQ(QuicError::IDLE_CLOSE.reason(), "");
auto err = QuicError::ForIdleClose("a reason");
CHECK_EQ(err.code(), 0);
CHECK_EQ(err.type(), QuicError::Type::IDLE_CLOSE);
CHECK_EQ(err.reason(), "a reason");
CHECK_EQ(err.ToString(), "QuicError(IDLE_CLOSE) 0: a reason");
CHECK_EQ(QuicError::IDLE_CLOSE, err);
}
TEST(QuicError, InternalError) {
auto err = QuicError::ForNgtcp2Error(NGTCP2_ERR_INTERNAL, "a reason");
CHECK_EQ(err.code(), NGTCP2_INTERNAL_ERROR);
CHECK_EQ(err.type(), QuicError::Type::TRANSPORT);
CHECK_EQ(err.reason(), "a reason");
CHECK_EQ(err.ToString(), "QuicError(TRANSPORT) 1: a reason");
printf("%s\n", QuicError::INTERNAL_ERROR.ToString().c_str());
CHECK_EQ(err, QuicError::INTERNAL_ERROR);
}
TEST(QuicError, TlsAlert) {
auto err = QuicError::ForTlsAlert(1, "a reason");
CHECK_EQ(err.code(), 257);
CHECK_EQ(err.type(), QuicError::Type::TRANSPORT);
CHECK_EQ(err.reason(), "a reason");
CHECK(err.is_crypto());
CHECK_EQ(err.crypto_error(), 1);
}
#endif // HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC

View file

@ -14,7 +14,7 @@ using node::quic::RetryToken;
using node::quic::StatelessResetToken; using node::quic::StatelessResetToken;
using node::quic::TokenSecret; using node::quic::TokenSecret;
TEST(TokenScret, Basics) { TEST(TokenSecret, Basics) {
uint8_t secret[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6}; uint8_t secret[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6};
TokenSecret fixed_secret(secret); TokenSecret fixed_secret(secret);

View file

@ -136,14 +136,14 @@ const cases = [
{ {
key: 'cc', key: 'cc',
valid: [ valid: [
quic.QUIC_CC_ALGO_RENO, quic.CC_ALGO_RENO,
quic.QUIC_CC_ALGO_CUBIC, quic.CC_ALGO_CUBIC,
quic.QUIC_CC_ALGO_BBR, quic.CC_ALGO_BBR,
quic.QUIC_CC_ALGO_BBR2, quic.CC_ALGO_BBR2,
quic.QUIC_CC_ALGO_RENO_STR, quic.CC_ALGO_RENO_STR,
quic.QUIC_CC_ALGO_CUBIC_STR, quic.CC_ALGO_CUBIC_STR,
quic.QUIC_CC_ALGO_BBR_STR, quic.CC_ALGO_BBR_STR,
quic.QUIC_CC_ALGO_BBR2_STR, quic.CC_ALGO_BBR2_STR,
], ],
invalid: [-1, 4, 1n, 'a', null, false, true, {}, [], () => {}], invalid: [-1, 4, 1n, 'a', null, false, true, {}, [], () => {}],
}, },