From 2ebd5f35336c6078faad98ecf61c9c49708ed027 Mon Sep 17 00:00:00 2001 From: James M Snell Date: Mon, 12 Apr 2021 17:07:32 -0700 Subject: [PATCH] quic: add quic Signed-off-by: James M Snell --- node.gyp | 18 + src/async_wrap.h | 21 +- src/node_binding.cc | 1 + src/node_errors.h | 11 + src/quic/buffer.cc | 596 +++++++ src/quic/buffer.h | 438 +++++ src/quic/crypto.cc | 919 +++++++++++ src/quic/crypto.h | 214 +++ src/quic/endpoint.cc | 2054 +++++++++++++++++++++++ src/quic/endpoint.h | 1105 +++++++++++++ src/quic/qlog.h | 102 ++ src/quic/quic.cc | 395 +++++ src/quic/quic.h | 815 +++++++++ src/quic/session.cc | 3761 ++++++++++++++++++++++++++++++++++++++++++ src/quic/session.h | 1696 +++++++++++++++++++ src/quic/stats.h | 140 ++ src/quic/stream.cc | 347 ++++ src/quic/stream.h | 299 ++++ 18 files changed, 12931 insertions(+), 1 deletion(-) create mode 100644 src/quic/buffer.cc create mode 100644 src/quic/buffer.h create mode 100644 src/quic/crypto.cc create mode 100644 src/quic/crypto.h create mode 100644 src/quic/endpoint.cc create mode 100644 src/quic/endpoint.h create mode 100644 src/quic/qlog.h create mode 100644 src/quic/quic.cc create mode 100644 src/quic/quic.h create mode 100644 src/quic/session.cc create mode 100644 src/quic/session.h create mode 100644 src/quic/stats.h create mode 100644 src/quic/stream.cc create mode 100644 src/quic/stream.h diff --git a/node.gyp b/node.gyp index 86808cbd643e92..eb458694857095 100644 --- a/node.gyp +++ b/node.gyp @@ -764,6 +764,7 @@ 'src/node_watchdog.h', 'src/node_worker.h', 'src/pipe_wrap.h', + 'src/quic/quic.cc', 'src/req_wrap.h', 'src/req_wrap-inl.h', 'src/spawn_sync.h', @@ -929,6 +930,23 @@ } ] ] } ], + [ 'openssl_quic=="true"', { + 'sources': [ + 'src/quic/buffer.cc', + 'src/quic/crypto.cc', + 'src/quic/endpoint.cc', + 'src/quic/session.cc', + 'src/quic/stream.cc', + 'src/quic/buffer.h', + 'src/quic/crypto.h', + 'src/quic/endpoint.h', + 'src/quic/qlog.h', + 'src/quic/quic.h', + 'src/quic/session.h', + 'src/quic/stats.h', + 'src/quic/stream.h' + ] + }], [ 'node_use_openssl=="true"', { 'sources': [ 'src/crypto/crypto_aes.cc', diff --git a/src/async_wrap.h b/src/async_wrap.h index 522779d57a0448..85e1ae8240d916 100644 --- a/src/async_wrap.h +++ b/src/async_wrap.h @@ -27,6 +27,10 @@ #include "base_object.h" #include "v8.h" +#if HAVE_OPENSSL +# include +#endif + #include namespace node { @@ -103,10 +107,25 @@ namespace node { #define NODE_ASYNC_INSPECTOR_PROVIDER_TYPES(V) #endif // HAVE_INSPECTOR +#ifdef OPENSSL_INFO_QUIC +#define NODE_ASYNC_QUIC_PROVIDER_TYPES(V) \ + V(JSQUICBUFFERCONSUMER) \ + V(STREAMSOURCE) \ + V(STREAMBASESOURCE) \ + V(QLOGSTREAM) \ + V(QUICENDPOINT) \ + V(QUICSENDWRAP) \ + V(QUICSESSION) \ + V(QUICSTREAM) +#else +#define NODE_ASYNC_QUIC_PROVIDER_TYPES(V) +#endif + #define NODE_ASYNC_PROVIDER_TYPES(V) \ NODE_ASYNC_NON_CRYPTO_PROVIDER_TYPES(V) \ NODE_ASYNC_CRYPTO_PROVIDER_TYPES(V) \ - NODE_ASYNC_INSPECTOR_PROVIDER_TYPES(V) + NODE_ASYNC_INSPECTOR_PROVIDER_TYPES(V) \ + NODE_ASYNC_QUIC_PROVIDER_TYPES(V) class Environment; class DestroyParam; diff --git a/src/node_binding.cc b/src/node_binding.cc index b5e42af79510b6..7aa5aa4b457504 100644 --- a/src/node_binding.cc +++ b/src/node_binding.cc @@ -65,6 +65,7 @@ V(pipe_wrap) \ V(process_wrap) \ V(process_methods) \ + V(quic) \ V(report) \ V(serdes) \ V(signal_wrap) \ diff --git a/src/node_errors.h b/src/node_errors.h index 291365fa3b4dc9..d254ae1755d399 100644 --- a/src/node_errors.h +++ b/src/node_errors.h @@ -59,6 +59,7 @@ void OnFatalError(const char* location, const char* message); V(ERR_INVALID_ADDRESS, Error) \ V(ERR_INVALID_ARG_VALUE, TypeError) \ V(ERR_OSSL_EVP_INVALID_DIGEST, Error) \ + V(ERR_ILLEGAL_CONSTRUCTOR, Error) \ V(ERR_INVALID_ARG_TYPE, TypeError) \ V(ERR_INVALID_MODULE, Error) \ V(ERR_INVALID_THIS, TypeError) \ @@ -71,6 +72,10 @@ void OnFatalError(const char* location, const char* message); V(ERR_MISSING_PLATFORM_FOR_WORKER, Error) \ V(ERR_NON_CONTEXT_AWARE_DISABLED, Error) \ V(ERR_OUT_OF_RANGE, RangeError) \ + V(ERR_QUIC_ENDPOINT_INITIAL_PACKET_FAILURE, Error) \ + V(ERR_QUIC_ENDPOINT_SEND_FAILURE, Error) \ + V(ERR_QUIC_FAILURE_SETTING_SNI_CONTEXT, Error) \ + V(ERR_QUIC_INTERNAL_ERROR, Error) \ V(ERR_SCRIPT_EXECUTION_INTERRUPTED, Error) \ V(ERR_SCRIPT_EXECUTION_TIMEOUT, Error) \ V(ERR_STRING_TOO_LONG, Error) \ @@ -144,12 +149,18 @@ ERRORS_WITH_CODE(V) V(ERR_DLOPEN_FAILED, "DLOpen failed") \ V(ERR_EXECUTION_ENVIRONMENT_NOT_AVAILABLE, \ "Context not associated with Node.js environment") \ + V(ERR_ILLEGAL_CONSTRUCTOR, "Illegal constructor") \ V(ERR_INVALID_ADDRESS, "Invalid socket address") \ V(ERR_INVALID_MODULE, "No such module") \ V(ERR_INVALID_THIS, "Value of \"this\" is the wrong type") \ V(ERR_INVALID_TRANSFER_OBJECT, "Found invalid object in transferList") \ V(ERR_MEMORY_ALLOCATION_FAILED, "Failed to allocate memory") \ V(ERR_OSSL_EVP_INVALID_DIGEST, "Invalid digest used") \ + V(ERR_QUIC_ENDPOINT_INITIAL_PACKET_FAILURE, \ + "Failure processing initial packet") \ + V(ERR_QUIC_ENDPOINT_SEND_FAILURE, "Failure to send packet") \ + V(ERR_QUIC_FAILURE_SETTING_SNI_CONTEXT, "Failure setting SNI context") \ + V(ERR_QUIC_INTERNAL_ERROR, "Internal error: %s") \ V(ERR_MESSAGE_TARGET_CONTEXT_UNAVAILABLE, \ "A message object could not be deserialized successfully in the target " \ "vm.Context") \ diff --git a/src/quic/buffer.cc b/src/quic/buffer.cc new file mode 100644 index 00000000000000..4a46d03b7ca91d --- /dev/null +++ b/src/quic/buffer.cc @@ -0,0 +1,596 @@ +#include "quic/buffer.h" // NOLINT(build/include) + +#include "quic/crypto.h" +#include "quic/session.h" +#include "quic/stream.h" +#include "quic/quic.h" +#include "aliased_struct-inl.h" +#include "async_wrap-inl.h" +#include "base_object-inl.h" +#include "env-inl.h" +#include "memory_tracker-inl.h" +#include "node_bob-inl.h" +#include "node_sockaddr-inl.h" +#include "stream_base-inl.h" +#include "util.h" +#include "uv.h" +#include "v8.h" + +#include +#include +#include + +namespace node { + +using v8::Array; +using v8::ArrayBuffer; +using v8::ArrayBufferView; +using v8::BackingStore; +using v8::EscapableHandleScope; +using v8::FunctionCallbackInfo; +using v8::FunctionTemplate; +using v8::Just; +using v8::Local; +using v8::Maybe; +using v8::MaybeLocal; +using v8::Nothing; +using v8::Object; +using v8::Uint8Array; +using v8::Value; + +namespace quic { + +Buffer::Source* Buffer::Source::FromObject(Local object) { + return static_cast( + object->GetAlignedPointerFromInternalField( + Buffer::Source::kSourceField)); +} + +void Buffer::Source::AttachToObject(Local object) { + object->SetAlignedPointerInInternalField( + Buffer::Source::kSourceField, this); +} + +Buffer::Chunk::Chunk( + const std::shared_ptr& data, + size_t length, + size_t offset) + : data_(std::move(data)), + offset_(offset), + length_(length), + unacknowledged_(length) {} + +std::unique_ptr Buffer::Chunk::Create( + Environment* env, + const uint8_t* data, + size_t len) { + std::shared_ptr store = + v8::ArrayBuffer::NewBackingStore(env->isolate(), len); + memcpy(store->Data(), data, len); + return std::unique_ptr( + new Buffer::Chunk(std::move(store), len)); +} + +std::unique_ptr Buffer::Chunk::Create( + const std::shared_ptr& data, + size_t length, + size_t offset) { + return std::unique_ptr( + new Buffer::Chunk(std::move(data), length, offset)); +} + +MaybeLocal Buffer::Chunk::Release(Environment* env) { + EscapableHandleScope scope(env->isolate()); + Local ret = + Uint8Array::New( + ArrayBuffer::New(env->isolate(), std::move(data_)), + offset_, + length_); + CHECK(!data_); + offset_ = 0; + length_ = 0; + read_ = 0; + unacknowledged_ = 0; + return scope.Escape(ret); +} + +size_t Buffer::Chunk::Seek(size_t amount) { + amount = std::min(amount, remaining()); + read_ += amount; + CHECK_LE(read_, length_); + return amount; +} + +size_t Buffer::Chunk::Acknowledge(size_t amount) { + amount = std::min(amount, unacknowledged_); + unacknowledged_ -= amount; + return amount; +} + +ngtcp2_vec Buffer::Chunk::vec() const { + uint8_t* ptr = static_cast(data_->Data()); + ptr += offset_ + read_; + return ngtcp2_vec { ptr, length() }; +} + +void Buffer::Chunk::MemoryInfo(MemoryTracker* tracker) const { + if (data_) + tracker->TrackFieldWithSize("data", data_->ByteLength()); +} + +const uint8_t* Buffer::Chunk::data() const { + uint8_t* ptr = static_cast(data_->Data()); + ptr += offset_ + read_; + return ptr; +} + +void Buffer::Push(Environment* env, const uint8_t* data, size_t len) { + CHECK(!ended_); + queue_.emplace_back(Buffer::Chunk::Create(env, data, len)); + length_ += len; + remaining_ += len; +} + +void Buffer::Push( + std::shared_ptr data, + size_t length, + size_t offset) { + CHECK(!ended_); + queue_.emplace_back(Buffer::Chunk::Create(std::move(data), length, offset)); + length_ += length; + remaining_ += length; +} + +size_t Buffer::Seek(size_t amount) { + if (queue_.empty()) + return 0; + amount = std::min(amount, remaining_); + size_t len = 0; + while (amount > 0) { + size_t actual = queue_[head_]->Seek(amount); + CHECK_LE(actual, amount); + amount -= actual; + remaining_ -= actual; + len += actual; + if (actual) { + head_++; + // head_ should never extend beyond queue size! + CHECK_LE(head_, queue_.size() - 1); + } + } + return len; +} + +size_t Buffer::Acknowledge(size_t amount) { + if (queue_.empty()) + return 0; + amount = std::min(amount, length_); + size_t len = 0; + while (amount > 0) { + CHECK_GT(queue_.size(), 0); + size_t actual = queue_.front()->Acknowledge(amount); + + CHECK_LE(actual, amount); + amount -= actual; + length_ -= actual; + len += actual; + // If we've acknowledged all of the bytes in the current + // chunk, pop it to free the memory and decrement the + // head_ pointer if necessary. + if (queue_.front()->length() == 0) { + queue_.pop_front(); + if (head_ > 0) head_--; + } + } + return len; +} + +void Buffer::MemoryInfo(MemoryTracker* tracker) const { + tracker->TrackField("queue", queue_); +} + +int Buffer::DoPull( + bob::Next next, + int options, + ngtcp2_vec* data, + size_t count, + size_t max_count_hint) { + size_t len = 0; + size_t numbytes = 0; + int status = bob::Status::STATUS_CONTINUE; + + // There's no data to read. + if (queue_.empty() || !remaining_) { + status = ended_ ? + bob::Status::STATUS_END : + bob::Status::STATUS_BLOCK; + std::move(next)(status, nullptr, 0, [](size_t len) {}); + return status; + } + + // Ensure that there's storage space. + MaybeStackBuffer vec; + size_t queue_size = queue_.size() - head_; + max_count_hint = (max_count_hint == 0) + ? queue_size + : std::min(max_count_hint, queue_size); + + CHECK_IMPLIES(data == nullptr, count == 0); + if (data == nullptr) { + vec.AllocateSufficientStorage(max_count_hint); + data = vec.out(); + count = max_count_hint; + } + + // Count should be greater than or equal to the number of + // items we have available. + CHECK_GE(count, queue_size); + + // Build the list of buffers. + for (size_t n = head_; + n < queue_.size() && len < count; + n++, len++) { + data[len] = queue_[n]->vec(); + numbytes += data[len].len; + } + + // If the buffer is ended, and the number of bytes + // matches the total remaining, and OPTIONS_END is + // used, set the status to STATUS_END. + if (is_ended() && + numbytes == remaining() && + options & bob::OPTIONS_END) { + status = bob::Status::STATUS_END; + } + + // Pass the data back out to the caller. + std::move(next)( + status, + data, + len, + [this](size_t len) { + size_t actual = Seek(len); + CHECK_LE(actual, len); + }); + + return status; +} + +Maybe Buffer::Release(Consumer* consumer) { + if (queue_.empty()) + return Just(static_cast(0)); + head_ = 0; + length_ = 0; + remaining_ = 0; + return consumer->Process(std::move(queue_), ended_); +} + +JSQuicBufferConsumer::JSQuicBufferConsumer( + Environment* env, + Local wrap) + : AsyncWrap(env, wrap, AsyncWrap::PROVIDER_JSQUICBUFFERCONSUMER) {} + +Maybe JSQuicBufferConsumer::Process( + Buffer::Chunk::Queue queue, + bool ended) { + EscapableHandleScope scope(env()->isolate()); + std::vector> items; + size_t len = 0; + while (!queue.empty()) { + Local val; + len += queue.front()->length(); + // If this fails, the error is unrecoverable and neither + // is the data. Return nothing to signal error and handle + // upstream. + if (!queue.front()->Release(env()).ToLocal(&val)) + return Nothing(); + queue.pop_front(); + items.emplace_back(val); + } + + Local args[] = { + Array::New(env()->isolate(), items.data(), items.size()), + ended ? v8::True(env()->isolate()) : v8::False(env()->isolate()) + }; + MakeCallback(env()->emit_string(), arraysize(args), args); + return Just(len); +} + +void JSQuicBufferConsumer::Initialize(Environment* env, Local target) { + Local temp = + env->NewFunctionTemplate(JSQuicBufferConsumer::New); + temp->InstanceTemplate()->SetInternalFieldCount( + JSQuicBufferConsumer::kInternalFieldCount); + temp->Inherit(AsyncWrap::GetConstructorTemplate(env)); + env->SetConstructorFunction(target, "JSQuicBufferConsumer", temp); +} + +void ArrayBufferViewSource::Initialize(Environment* env, Local target) { + Local temp = + env->NewFunctionTemplate(ArrayBufferViewSource::New); + temp->Inherit(BaseObject::GetConstructorTemplate(env)); + temp->InstanceTemplate()->SetInternalFieldCount( + Buffer::Source::kInternalFieldCount); + env->SetConstructorFunction(target, "ArrayBufferViewSource", temp); +} + +void StreamSource::Initialize(Environment* env, Local target) { + Local temp = env->NewFunctionTemplate(StreamSource::New); + temp->Inherit(AsyncWrap::GetConstructorTemplate(env)); + StreamBase::AddMethods(env, temp); + temp->InstanceTemplate()->SetInternalFieldCount( + StreamBase::kInternalFieldCount); + temp->InstanceTemplate()->Set(env->owner_symbol(), Null(env->isolate())); + env->SetConstructorFunction(target, "StreamSource", temp); +} + +void StreamBaseSource::Initialize(Environment* env, Local target) { + Local temp = + env->NewFunctionTemplate(StreamBaseSource::New); + temp->Inherit(AsyncWrap::GetConstructorTemplate(env)); + temp->InstanceTemplate()->SetInternalFieldCount( + Buffer::Source::kInternalFieldCount); + env->SetConstructorFunction(target, "StreamBaseSource", temp); +} + +void JSQuicBufferConsumer::New(const FunctionCallbackInfo& args) { + CHECK(args.IsConstructCall()); + Environment* env = Environment::GetCurrent(args); + new JSQuicBufferConsumer(env, args.This()); +} + +int NullSource::DoPull( + bob::Next next, + int options, + ngtcp2_vec* data, + size_t count, + size_t max_count_hint) { + int status = bob::Status::STATUS_END; + std::move(next)(status, nullptr, 0, [](size_t len) {}); + return status; +} + +void ArrayBufferViewSource::New(const FunctionCallbackInfo& args) { + CHECK(args.IsConstructCall()); + CHECK(args[0]->IsArrayBufferView()); + Environment* env = Environment::GetCurrent(args); + Local view = args[0].As(); + new ArrayBufferViewSource( + env, + args.This(), + Buffer::Chunk::Create( + view->Buffer()->GetBackingStore(), + view->ByteLength(), + view->ByteOffset())); +} + +ArrayBufferViewSource::ArrayBufferViewSource( + Environment* env, + Local wrap, + std::unique_ptr chunk) + : Buffer::Source(), + BaseObject(env, wrap), + chunk_(std::move(chunk)) { + MakeWeak(); + AttachToObject(object()); +} + +int ArrayBufferViewSource::DoPull( + bob::Next next, + int options, + ngtcp2_vec* data, + size_t count, + size_t max_count_hint) { + int status = bob::Status::STATUS_CONTINUE; + + if (!chunk_ || !chunk_->remaining()) { + status = bob::Status::STATUS_END; + std::move(next)(status, nullptr, 0, [](size_t len) {}); + return status; + } + + ngtcp2_vec vec; + CHECK_IMPLIES(data == nullptr, count == 0); + if (data == nullptr) { + data = &vec; + count = 1; + } + + *data = chunk_->vec(); + + if (options & bob::OPTIONS_END) + status = bob::Status::STATUS_END; + + // Pass the data back out to the caller. + std::move(next)( + status, + data, + 1, + [this](size_t len) { chunk_->Seek(len); }); + + return status; +} + +size_t ArrayBufferViewSource::Acknowledge( + uint64_t offset, + size_t datalen) { + if (!chunk_) return 0; + size_t actual = chunk_->Acknowledge(datalen); + if (!chunk_->remaining()) + chunk_.reset(); + return actual; +} + +size_t ArrayBufferViewSource::Seek(size_t amount) { + if (!chunk_) return 0; + return chunk_->Seek(amount); +} + +void ArrayBufferViewSource::MemoryInfo(MemoryTracker* tracker) const { + // TODO(@jasnell): Implement + // if (chunk_) + // tracker->TrackField("data", chunk_); +} + +void StreamSource::New(const FunctionCallbackInfo& args) { + CHECK(args.IsConstructCall()); + Environment* env = Environment::GetCurrent(args); + new StreamSource(env, args.This()); +} + +StreamSource::StreamSource(Environment* env, Local wrap) + : AsyncWrap(env, wrap, AsyncWrap::PROVIDER_STREAMSOURCE), + StreamBase(env) { + MakeWeak(); +} + +int StreamSource::DoPull( + bob::Next next, + int options, + ngtcp2_vec* data, + size_t count, + size_t max_count_hint) { + return queue_.DoPull(std::move(next), options, data, count, max_count_hint); +} + +int StreamSource::DoShutdown(ShutdownWrap* wrap) { + if (queue_.is_ended()) + return UV_EPIPE; + queue_.End(); + env()->SetImmediate([ + wrap, + ref = BaseObjectPtr(wrap->GetAsyncWrap())](Environment* env) { + wrap->Done(0); + }); + return 0; +} + +void StreamSource::set_closed() { + queue_.End(); +} + +int StreamSource::DoWrite( + WriteWrap* w, + uv_buf_t* bufs, + size_t count, + uv_stream_t* send_handle) { + CHECK(!queue_.is_ended()); + CHECK_NOT_NULL(owner()); + for (size_t n = 0; n < count; n++) { + std::shared_ptr store; + if (n == count - 1) { + store = ArrayBuffer::NewBackingStore( + bufs[n].base, + bufs[n].len, + [](void* data, size_t len, void* ptr) { + WriteWrap* wrap = static_cast(ptr); + wrap->Done(0); + }, + w); + } else { + store = ArrayBuffer::NewBackingStore( + bufs[n].base, + bufs[n].len, + [](void* data, size_t len, void* ptr) {}, + nullptr); + } + queue_.Push(std::move(store), store->ByteLength()); + } + owner()->Resume(); + return 0; +} + +size_t StreamSource::Acknowledge(uint64_t offset, size_t datalen) { + return queue_.Acknowledge(datalen); +} + +size_t StreamSource::Seek(size_t amount) { + return queue_.Seek(amount); +} + +void StreamSource::MemoryInfo(MemoryTracker* tracker) const { + tracker->TrackField("queue", queue_); +} + +void StreamBaseSource::New(const FunctionCallbackInfo& args) { + CHECK(args.IsConstructCall()); + CHECK(args[0]->IsObject()); + Environment* env = Environment::GetCurrent(args); + StreamBase* wrap = StreamBase::FromObject(args[0].As()); + new StreamBaseSource( + env, + args.This(), + wrap, + BaseObjectPtr(wrap->GetAsyncWrap())); +} + +StreamBaseSource::StreamBaseSource( + Environment* env, + Local obj, + StreamResource* resource, + BaseObjectPtr strong_ptr) + : AsyncWrap(env, obj, AsyncWrap::PROVIDER_STREAMBASESOURCE), + strong_ptr_(std::move(strong_ptr)) { + MakeWeak(); + resource_->PushStreamListener(this); +} + +StreamBaseSource::~StreamBaseSource() { + resource_->RemoveStreamListener(this); +} + +void StreamBaseSource::set_closed() { + resource_->ReadStop(); + resource_->DoShutdown(nullptr); + buffer_.End(); +} + +uv_buf_t StreamBaseSource::OnStreamAlloc(size_t suggested_size) { + uv_buf_t buf; + buf.base = Malloc(suggested_size); + buf.len = suggested_size; + return buf; +} + +void StreamBaseSource::OnStreamRead(ssize_t nread, const uv_buf_t& buf_) { + CHECK(!buffer_.is_ended()); + CHECK_NOT_NULL(owner()); + if (nread < 0) { + buffer_.End(); + } else { + std::shared_ptr store = + ArrayBuffer::NewBackingStore( + static_cast(buf_.base), + buf_.len, + [](void* ptr, size_t len, void* deleter_data) { + std::unique_ptr delete_me(static_cast(ptr)); + }, + nullptr); + buffer_.Push(std::move(store), store->ByteLength()); + } + owner()->Resume(); +} + +int StreamBaseSource::DoPull( + bob::Next next, + int options, + ngtcp2_vec* data, + size_t count, + size_t max_count_hint) { + return buffer_.DoPull(std::move(next), options, data, count, max_count_hint); +} + +size_t StreamBaseSource::Acknowledge(uint64_t offset, size_t datalen) { + return buffer_.Acknowledge(datalen); +} + +size_t StreamBaseSource::Seek(size_t amount) { + return buffer_.Seek(amount); +} + +void StreamBaseSource::MemoryInfo(MemoryTracker* tracker) const { + tracker->TrackField("queue", buffer_); +} + +} // namespace quic +} // namespace node diff --git a/src/quic/buffer.h b/src/quic/buffer.h new file mode 100644 index 00000000000000..e246b90a5c0b3d --- /dev/null +++ b/src/quic/buffer.h @@ -0,0 +1,438 @@ +#ifndef SRC_QUIC_BUFFER_H_ +#define SRC_QUIC_BUFFER_H_ + +#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS + +#include "async_wrap.h" +#include "base_object.h" +#include "memory_tracker.h" +#include "node.h" +#include "node_bob.h" +#include "node_file.h" +#include "node_internals.h" +#include "stream_base.h" +#include "util-inl.h" +#include "uv.h" + +#include "ngtcp2/ngtcp2.h" + +#include + +namespace node { +namespace quic { + +class Buffer; +class Stream; + +constexpr size_t kMaxVectorCount = 16; + +// When data is sent over QUIC, we are required to retain it in memory +// until we receive an acknowledgement that it has been successfully +// received by the peer. The QuicBuffer object is what we use to handle +// that and track until it is acknowledged. To understand the QuicBuffer +// object itself, it is important to understand how ngtcp2 and nghttp3 +// handle data that is given to it to serialize into QUIC packets. +// +// An individual QUIC packet may contain multiple QUIC frames. Whenever +// we create a QUIC packet, we really have no idea what frames are going +// to be encoded or how much buffered handshake or stream data is going +// to be included within that QuicPacket. If there is buffered data +// available for a stream, we provide an array of pointers to that data +// and an indication about how much data is available, then we leave it +// entirely up to ngtcp2 and nghttp3 to determine how much of the data +// to encode into the QUIC packet. It is only *after* the QUIC packet +// is encoded that we can know how much was actually written. +// +// Once written to a QUIC Packet, we have to keep the data in memory +// until an acknowledgement is received. In QUIC, acknowledgements are +// received per range of packets, but (fortunately) ngtcp2 gives us that +// information as byte offsets instead. +// +// Buffer is complicated because it needs to be able to accomplish +// three things: (a) buffering v8::BackingStore instances passed down +// from JavaScript without memcpy, (b) tracking what data has already been +// encoded in a QUIC packet and what data is remaining to be read, and +// (c) tracking which data has been acknowledged and which hasn't. +// +// Buffer contains a deque of Buffer::Chunk instances. +// A single Buffer::Chunk wraps a v8::BackingStore with length and +// offset. When the Buffer::Chunk is created, we capture the total +// length of the buffer and the total number of bytes remaining to be sent. +// Initially, these numbers are identical. +// +// When data is encoded into a Packet, we advance the Buffer::Chunk's +// remaining-to-be-read by the number of bytes actually encoded. If there +// are no more bytes remaining to be encoded, we move to the next chunk +// in the deque (but we do not yet pop it off the deque). +// +// When an acknowledgement is received, we decrement the Buffer::Chunk's +// length by the number of acknowledged bytes. Once the unacknowledged +// length reaches 0 we pop the chunk off the deque. + +class Buffer : public bob::SourceImpl, + public MemoryRetainer { + public: + // Stores chunks of both inbound and outbound data. Each chunk + // stores a shared pointer to a v8::BackingStore with appropriate + // length and offset details. Each Buffer::Chunk is stored in a + // deque in Buffer which manages the aggregate collection of all chunks. + class Chunk : public MemoryRetainer { + public: + static std::unique_ptr Create( + Environment* env, + const uint8_t* data, + size_t len); + + static std::unique_ptr Create( + const std::shared_ptr& data, + size_t offset, + size_t length); + + size_t Acknowledge(size_t amount); + + // Releases the chunk to a v8 Uint8Array. data_ is reset + // and offset_, length_, and consumed_ are all set to 0 + // and the strong_ptr_, if any, is reset. This is used + // only for inbound data and only when queued data is + // being flushed out to the JavaScript side. + v8::MaybeLocal Release(Environment* env); + + // Increments consumed_ by amount bytes. If amount is greater + // than remaining(), remaining() bytes are advanced. Returns + // the actual number of bytes advanced. + size_t Seek(size_t amount); + + // Returns a pointer to the remaining data. This is used only + // for outbound data. + const uint8_t* data() const; + + inline size_t length() const { return unacknowledged_; } + + inline size_t remaining() const { return length_ - read_; } + + ngtcp2_vec vec() const; + + void MemoryInfo(MemoryTracker* tracker) const override; + SET_MEMORY_INFO_NAME(Buffer::Chunk) + SET_SELF_SIZE(Chunk) + + using Queue = std::deque>; + + private: + Chunk( + const std::shared_ptr& data, + size_t length, + size_t offset = 0); + + std::shared_ptr data_; + size_t offset_ = 0; + size_t length_ = 0; + size_t read_ = 0; + size_t unacknowledged_ = 0; + }; + + // Receives the inbound data for a Stream + struct Consumer { + virtual v8::Maybe Process( + Chunk::Queue queue, + bool ended = false) = 0; + }; + + // Provides outbound data for a stream + class Source : public bob::SourceImpl, + public MemoryRetainer { + public: + enum InternalFields { + kSlot = BaseObject::kSlot, + kSourceField = BaseObject::kInternalFieldCount, + kInternalFieldCount + }; + + virtual BaseObjectPtr GetStrongPtr() { + return BaseObjectPtr(); + } + + virtual size_t Acknowledge(uint64_t offset, size_t amount) = 0; + virtual size_t Seek(size_t amount) = 0; + inline void set_owner(Stream* owner) { owner_ = owner; } + + // If the BufferSource is explicitly marked closed, then it + // should not accept any more pending data than what's already + // in it's queue, if any, and it should send EOS as soon as possible. + // The set_closed state will not be relevant to all sources + // (e.g. ArrayBufferViewSource and NullSource) so the default + // implementation is to do nothing. + virtual void set_closed() { } + + static Source* FromObject(v8::Local object); + + protected: + void AttachToObject(v8::Local object); + inline Stream* owner() { return owner_; } + + private: + Stream* owner_; + }; + + Buffer() = default; + Buffer(const Buffer& other) = delete; + Buffer(const Buffer&& src) = delete; + Buffer& operator=(const Buffer& other) = delete; + Buffer& operator=(const Buffer&& src) = delete; + + // Marks the Buffer as having ended, preventing new Buffer::Chunk + // instances from being added and allowing the Pull operation to know when + // to signal that the flow of data is completed. + inline void End() { ended_ = true; } + inline bool is_ended() const { return ended_; } + + // Push inbound data onto the buffer. + void Push(Environment* env, const uint8_t* data, size_t len); + + // Push outbound data onto the buffer. + void Push( + std::shared_ptr data, + size_t length, + size_t offset = 0); + + // Increment the given number of bytes within the buffer. If amount + // is greater than length(), length() bytes are advanced. Returns + // the actual number of bytes advanced. Will not cause bytes to be + // freed. + size_t Seek(size_t amount); + + // Acknowledge the given number of bytes in the buffer. May cause + // bytes to be freed. + size_t Acknowledge(size_t amount); + + // Clears any bytes remaining in the buffer. + inline void Clear() { + queue_.clear(); + head_ = 0; + length_ = 0; + remaining_ = 0; + } + + // The total number of unacknowledged bytes remaining. The length + // is incremented by Push and decremented by Acknowledge. + inline size_t length() const { return length_; } + + // The total number of unread bytes remaining. The remaining + // length is incremental by Push and decremented by Seek. + inline size_t remaining() const { return remaining_; } + + // Flushes the entire inbound queue into a v8::Local + // of Uint8Array instances, returning the total number of bytes + // released to the consumer. + v8::Maybe Release(Consumer* consumer); + + int DoPull( + bob::Next next, + int options, + ngtcp2_vec* data, + size_t count, + size_t max_count_hint) override; + + void MemoryInfo(MemoryTracker* tracker) const override; + SET_MEMORY_INFO_NAME(Buffer); + SET_SELF_SIZE(Buffer); + + private: + Chunk::Queue queue_; + bool ended_ = false; + + // The queue_ index of the current read head. + // This is incremented by Seek() as necessary and + // decremented by Acknowledge() as data is consumed. + size_t head_ = 0; + size_t length_ = 0; + size_t remaining_ = 0; +}; + +// The JSQuicBufferConsumer receives inbound data for a Stream +// and forwards that up as Uint8Array instances to the JavaScript +// API. +class JSQuicBufferConsumer : public Buffer::Consumer, + public AsyncWrap { + public: + static void Initialize(Environment* env, v8::Local target); + static void New(const v8::FunctionCallbackInfo& args); + + SET_NO_MEMORY_INFO() + SET_MEMORY_INFO_NAME(JSQuicBufferConsumer) + SET_SELF_SIZE(JSQuicBufferConsumer) + + JSQuicBufferConsumer( + Environment* env, + v8::Local wrap); + + v8::Maybe Process( + Buffer::Chunk::Queue queue, + bool ended = false) override; +}; + +// The NullSource is used when no payload source is provided +// for a Stream. Whenever DoPull is called, it simply +// immediately responds with no data and EOS set. +class NullSource : public Buffer::Source { + public: + NullSource() = default; + + int DoPull( + bob::Next next, + int options, + ngtcp2_vec* data, + size_t count, + size_t max_count_hint) override; + + BaseObjectPtr GetStrongPtr() override { + return BaseObjectPtr(); + } + + size_t Acknowledge(uint64_t offset, size_t datalen) override { + return 0; + } + + size_t Seek(size_t amount) override { + return 0; + } + + SET_NO_MEMORY_INFO() + SET_MEMORY_INFO_NAME(NullSource) + SET_SELF_SIZE(NullSource) +}; + +// Receives a single ArrayBufferView and uses it's contents as the +// complete source of outbound data for the Stream. +class ArrayBufferViewSource : public Buffer::Source, + public BaseObject { + public: + static void Initialize(Environment* env, v8::Local target); + static void New(const v8::FunctionCallbackInfo& args); + + int DoPull( + bob::Next next, + int options, + ngtcp2_vec* data, + size_t count, + size_t max_count_hint) override; + + BaseObjectPtr GetStrongPtr() override { + return BaseObjectPtr(this); + } + size_t Acknowledge(uint64_t offset, size_t datalen) override; + size_t Seek(size_t amount) override; + + void MemoryInfo(MemoryTracker* tracker) const override; + SET_MEMORY_INFO_NAME(ArrayBufferViewSource); + SET_SELF_SIZE(ArrayBufferViewSource); + + private: + ArrayBufferViewSource( + Environment* env, + v8::Local wrap, + std::unique_ptr chunk); + + std::unique_ptr chunk_; +}; + +// Implements StreamBase to asynchronously accept outbound data from the +// JavaScript side. +class StreamSource : public AsyncWrap, + public StreamBase, + public Buffer::Source { + public: + static void Initialize(Environment* env, v8::Local target); + static void New(const v8::FunctionCallbackInfo& args); + + size_t Acknowledge(uint64_t offset, size_t datalen) override; + size_t Seek(size_t amount) override; + + // This is a write-only stream. These are ignored. + int ReadStart() override { return 0; } + int ReadStop() override { return 0; } + bool IsAlive() override { return !queue_.is_ended(); } + bool IsClosing() override { return queue_.is_ended(); } + + int DoShutdown(ShutdownWrap* wrap) override; + void set_closed() override; + + int DoWrite( + WriteWrap* w, + uv_buf_t* bufs, + size_t count, + uv_stream_t* send_handle) override; + + AsyncWrap* GetAsyncWrap() override { return this; } + + int DoPull( + bob::Next next, + int options, + ngtcp2_vec* data, + size_t count, + size_t max_count_hint) override; + + BaseObjectPtr GetStrongPtr() override { + return BaseObjectPtr(this); + } + + void MemoryInfo(MemoryTracker* tracker) const override; + SET_MEMORY_INFO_NAME(StreamSource); + SET_SELF_SIZE(StreamSource); + + private: + StreamSource(Environment* env, v8::Local wrap); + + Buffer queue_; +}; + +// Implements StreamListener to receive data from any native level +// StreamBase implementation. +class StreamBaseSource : public AsyncWrap, + public Buffer::Source, + public StreamListener { + public: + static void Initialize(Environment* env, v8::Local target); + static void New(const v8::FunctionCallbackInfo& args); + + ~StreamBaseSource() override; + + int DoPull( + bob::Next next, + int options, + ngtcp2_vec* data, + size_t count, + size_t max_count_hint) override; + + size_t Acknowledge(uint64_t offset, size_t datalen) override; + size_t Seek(size_t amount) override; + + uv_buf_t OnStreamAlloc(size_t suggested_size) override; + void OnStreamRead(ssize_t nread, const uv_buf_t& buf) override; + + BaseObjectPtr GetStrongPtr() override { + return BaseObjectPtr(this); + } + + void set_closed() override; + + void MemoryInfo(MemoryTracker* tracker) const override; + SET_MEMORY_INFO_NAME(StreamBaseSource) + SET_SELF_SIZE(StreamBaseSource) + + private: + StreamBaseSource( + Environment* env, + v8::Local wrap, + StreamResource* resource, + BaseObjectPtr strong_ptr = BaseObjectPtr()); + + StreamResource* resource_; + BaseObjectPtr strong_ptr_; + Buffer buffer_; +}; +} // namespace quic +} // namespace node + +#endif // NODE_WANT_INTERNALS +#endif // SRC_QUIC_BUFFER_H_ diff --git a/src/quic/crypto.cc b/src/quic/crypto.cc new file mode 100644 index 00000000000000..fbe5cc24051ae5 --- /dev/null +++ b/src/quic/crypto.cc @@ -0,0 +1,919 @@ +#include "quic/crypto.h" + +#include "quic/endpoint.h" +#include "quic/session.h" +#include "quic/stream.h" +#include "quic/quic.h" +#include "crypto/crypto_util.h" +#include "crypto/crypto_context.h" +#include "crypto/crypto_common.h" +#include "aliased_struct-inl.h" +#include "async_wrap-inl.h" +#include "base_object-inl.h" +#include "env-inl.h" +#include "node_crypto.h" +#include "node_process.h" +#include "node_sockaddr-inl.h" +#include "node_url.h" +#include "string_bytes.h" +#include "util-inl.h" + +#include "v8.h" + +#include +#include +#include // NGHTTP3_ALPN_H3 +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace node { + +using crypto::EntropySource; +using v8::Just; +using v8::Local; +using v8::Maybe; +using v8::Nothing; +using v8::Value; + +namespace quic { + +bool SessionTicketAppData::Set(const uint8_t* data, size_t len) { + if (set_) return false; + set_ = true; + SSL_SESSION_set1_ticket_appdata(session_, data, len); + return set_; +} + +bool SessionTicketAppData::Get(uint8_t** data, size_t* len) const { + return SSL_SESSION_get0_ticket_appdata( + session_, + reinterpret_cast(data), + len) == 1; +} + +namespace { +bool DeriveTokenKey( + uint8_t* token_key, + uint8_t* token_iv, + const uint8_t* rand_data, + size_t rand_datalen, + const ngtcp2_crypto_aead& aead, + const ngtcp2_crypto_md& md, + const uint8_t* token_secret) { + static constexpr int kCryptoTokenSecretlen = 32; + uint8_t secret[kCryptoTokenSecretlen]; + + return + NGTCP2_OK(ngtcp2_crypto_hkdf_extract( + secret, + &md, + token_secret, + kTokenSecretLen, + rand_data, + rand_datalen)) && + NGTCP2_OK(ngtcp2_crypto_derive_packet_protection_key( + token_key, + token_iv, + nullptr, + &aead, + &md, + secret, + kCryptoTokenSecretlen)); +} + +// Retry tokens are generated only by QUIC servers. They +// are opaque to QUIC clients and must not be guessable by +// on- or off-path attackers. A QUIC server sends a RETRY +// token as a way of initiating explicit path validation +// with a client in response to an initial QUIC packet. +// The client, upon receiving a RETRY, must abandon the +// initial connection attempt and try again, including the +// received retry token in the new initial packet sent to +// the server. If the server is performing explicit +// valiation, it will look for the presence of the retry +// token and validate it if found. The internal structure +// of the retry token must be meaningful to the server, +// and the server must be able to validate the token without +// relying on any state left over from the previous connection +// attempt. The implementation here is entirely Node.js +// specific. +// +// The token is generated by: +// 1. Appending the raw bytes of given socket address, the current +// timestamp, and the original CID together into a single byte +// array. +// 2. Generating a block of random data that is used together with +// the token secret to cryptographically derive an encryption key. +// 3. Encrypting the byte array from step 1 using the encryption key +// from step 2. +// 4. Appending random data generated in step 2 to the token. +// +// The token secret must be kept secret on the QUIC server that +// generated the retry. When multiple QUIC servers are used in a +// cluster, it cannot be guaranteed that the same QUIC server +// instance will receive the subsequent new Initial packet. Therefore, +// all QUIC servers in the cluster should either share or be aware +// of the same token secret or a mechanism needs to be implemented +// to ensure that subsequent packets are routed to the same QUIC +// server instance. +// +// A malicious peer could attempt to guess the token secret by +// sending a large number specially crafted RETRY-eliciting packets +// to a server then analyzing the resulting retry tokens. To reduce +// the possibility of such attacks, the current implementation of +// QuicSocket generates the token secret randomly for each instance, +// and the number of RETRY responses sent to a given remote address +// should be limited. Such attacks should be of little actual value +// in most cases. +bool GenerateRetryToken( + uint8_t* token, + size_t* tokenlen, + const std::shared_ptr& addr, + const CID& retry_cid, + const CID& ocid, + const uint8_t* token_secret, + const ngtcp2_crypto_aead& aead, + const ngtcp2_crypto_md& md) { + std::array plaintext; + std::array aad; + uint8_t rand_data[kTokenRandLen]; + uint8_t token_key[kCryptoTokenKeylen]; + uint8_t token_iv[kCryptoTokenIvlen]; + + EntropySource(rand_data, kTokenRandLen); + + if (!DeriveTokenKey( + token_key, + token_iv, + rand_data, + kTokenRandLen, + aead, + md, + token_secret)) { + return false; + } + + AeadContextPointer aead_ctx( + AeadContextPointer::Mode::ENCRYPT, + token_key, + aead); + if (!aead_ctx) return false; + + uint64_t now = uv_hrtime(); + + // Prepare the plaintext (host order timestamp + ocid) + auto p = std::begin(plaintext); + p = std::copy_n(reinterpret_cast(&now), sizeof(uint64_t), p); + p = std::copy_n(ocid->data, ocid->datalen, p); + + size_t plaintextlen = std::distance(std::begin(plaintext), p); + + // Prepare the additional data (raw socket address + retry_cid) + p = std::begin(aad); + p = std::copy_n(addr->raw(), addr->length(), p); + p = std::copy_n(retry_cid.data(), retry_cid.length(), p); + + size_t aadlen = std::distance(std::begin(aad), p); + + token[0] = kRetryTokenMagic; + + if (NGTCP2_ERR(ngtcp2_crypto_encrypt( + token + 1, + &aead, + aead_ctx.get(), + plaintext.data(), + plaintextlen, + token_iv, + kCryptoTokenIvlen, + aad.data(), + aadlen))) { + return false; + } + + *tokenlen = 1 + plaintextlen + aead.max_overhead; + memcpy(token + (*tokenlen), rand_data, kTokenRandLen); + *tokenlen += kTokenRandLen; + return true; +} +} // namespace + +// A stateless reset token is used when a QUIC endpoint receives a +// QUIC packet with a short header but the associated connection ID +// cannot be matched to any known QuicSession. In such cases, the +// receiver may choose to send a subtle opaque indication to the +// sending peer that state for the QuicSession has apparently been +// lost. For any on- or off- path attacker, a stateless reset packet +// resembles any other QUIC packet with a short header. In order to +// be successfully handled as a stateless reset, the peer must have +// already seen a reset token issued to it associated with the given +// CID. The token itself is opaque to the peer that receives is but +// must be possible to statelessly recreate by the peer that +// originally created it. The actual implementation is Node.js +// specific but we currently defer to a utility function provided +// by ngtcp2. +bool GenerateResetToken(uint8_t* token, const uint8_t* secret, const CID& cid) { + ngtcp2_crypto_ctx ctx; + ngtcp2_crypto_ctx_initial(&ctx); + return NGTCP2_OK(ngtcp2_crypto_generate_stateless_reset_token( + token, + &ctx.md, + secret, + NGTCP2_STATELESS_RESET_TOKENLEN, + cid.cid())); +} + +// Validates a retry token included in the given header. This will return +// true if the token cannot be validated, false otherwise. A token is +// valid if it can be successfully decrypted using the key derived from +// random data embedded in the token, the structure of the token matches +// that generated by the GenerateRetryToken function, and the token was +// not generated earlier than now - verification_expiration. If validation +// is successful, ocid will be updated to the original connection ID encoded +// in the encrypted token. +Maybe ValidateRetryToken( + const ngtcp2_vec& token, + const std::shared_ptr& addr, + const CID& dcid, + const uint8_t* token_secret, + uint64_t verification_expiration, + const ngtcp2_crypto_aead& aead, + const ngtcp2_crypto_md& md) { + uint8_t token_key[kCryptoTokenKeylen]; + uint8_t token_iv[kCryptoTokenIvlen]; + uint8_t plaintext[4096]; + std::array aad; + + // Quick checks. If the token is too short, too long, or does not + // contain the right token magic byte, assume invalid and skip further + // checks. + if (token.len < kMinRetryTokenLen || + token.len > kMaxRetryTokenLen || + token.base[0] != kRetryTokenMagic) { + return Nothing(); + } + + AeadContextPointer aead_ctx( + AeadContextPointer::Mode::DECRYPT, + token_key, + aead); + if (!aead_ctx) return Nothing(); + + const uint8_t* rand_data = token.base + token.len - kTokenRandLen; + const uint8_t* ciphertext = token.base + 1; + size_t ciphertextlen = token.len - kTokenRandLen - 1; + + if (!DeriveTokenKey( + token_key, + token_iv, + rand_data, + kTokenRandLen, + aead, + md, + token_secret)) { + return Nothing(); + } + + // Prepare the additional data (raw socket address + retry_cid) + auto p = std::begin(aad); + p = std::copy_n(addr->raw(), addr->length(), p); + p = std::copy_n(dcid.data(), dcid.length(), p); + + size_t aadlen = std::distance(std::begin(aad), p); + + if (NGTCP2_ERR(ngtcp2_crypto_decrypt( + plaintext, + &aead, + aead_ctx.get(), + ciphertext, + ciphertextlen, + token_iv, + kCryptoTokenIvlen, + aad.data(), + aadlen))) { + return Nothing(); + } + + size_t plaintextlen = ciphertextlen - aead.max_overhead; + if (plaintextlen < sizeof(uint64_t)) + return Nothing(); + + size_t cil = plaintextlen - sizeof(uint64_t); + if (cil != 0 && (cil < NGTCP2_MIN_CIDLEN || cil > NGTCP2_MAX_CIDLEN)) + return Nothing(); + + uint64_t t; + memcpy(&t, plaintext, sizeof(uint64_t)); + + // 10-second window by default, but configurable for each + // Endpoint instance with a MIN_RETRYTOKEN_EXPIRATION second + // minimum and a MAX_RETRYTOKEN_EXPIRATION second maximum. + if (t + verification_expiration * NGTCP2_SECONDS < uv_hrtime()) + return Nothing(); + + return Just(CID(plaintext + addr->length() + sizeof(uint64_t), cil)); +} + +// Generates a RETRY packet. See the notes for GenerateRetryToken for details. +std::unique_ptr GenerateRetryPacket( + quic_version version, + const uint8_t* token_secret, + const CID& dcid, + const CID& scid, + const std::shared_ptr& local_addr, + const std::shared_ptr& remote_addr, + const ngtcp2_crypto_aead& aead, + const ngtcp2_crypto_md& md) { + + uint8_t token[256]; + size_t tokenlen = sizeof(token); + + CID cid; + EntropySource(cid.data(), NGTCP2_MAX_CIDLEN); + cid.set_length(NGTCP2_MAX_CIDLEN); + + if (!GenerateRetryToken( + token, + &tokenlen, + remote_addr, + cid, + dcid, + token_secret, + aead, + md)) { + return {}; + } + + size_t pktlen = tokenlen + (2 * NGTCP2_MAX_CIDLEN) + scid.length() + 8; + + std::unique_ptr packet = std::make_unique(pktlen, "retry"); + ssize_t nwrite = + ngtcp2_crypto_write_retry( + packet->data(), + NGTCP2_MAX_PKTLEN_IPV4, + version, + scid.cid(), + cid.cid(), + dcid.cid(), + token, + tokenlen); + if (nwrite <= 0) + return {}; + packet->set_length(nwrite); + return packet; +} + +bool GenerateToken( + uint8_t* token, + size_t* tokenlen, + const std::shared_ptr& addr, + const uint8_t* token_secret, + const ngtcp2_crypto_aead& aead, + const ngtcp2_crypto_md& md) { + std::array plaintext; + std::array aad; + uint8_t rand_data[kTokenRandLen]; + uint8_t token_key[kCryptoTokenKeylen]; + uint8_t token_iv[kCryptoTokenIvlen]; + + EntropySource(rand_data, kTokenRandLen); + + if (!DeriveTokenKey( + token_key, + token_iv, + rand_data, + kTokenRandLen, + aead, + md, + token_secret)) { + return false; + } + + AeadContextPointer aead_ctx( + AeadContextPointer::Mode::ENCRYPT, + token_key, + aead); + if (!aead_ctx) return false; + + uint64_t now = uv_hrtime(); + + // Prepare the plaintext (host order timestamp) + auto p = std::begin(plaintext); + p = std::copy_n(reinterpret_cast(&now), sizeof(uint64_t), p); + + size_t plaintextlen = std::distance(std::begin(plaintext), p); + + // Prepare the additional data (raw socket address) + p = std::begin(aad); + p = std::copy_n(addr->raw(), addr->length(), p); + + size_t aadlen = std::distance(std::begin(aad), p); + + token[0] = kTokenMagic; + + if (NGTCP2_ERR(ngtcp2_crypto_encrypt( + token + 1, + &aead, + aead_ctx.get(), + plaintext.data(), + plaintextlen, + token_iv, + kCryptoTokenIvlen, + aad.data(), + aadlen))) { + return false; + } + + *tokenlen = 1 + plaintextlen + aead.max_overhead; + memcpy(token + (*tokenlen), rand_data, kTokenRandLen); + *tokenlen += kTokenRandLen; + return true; +} + +bool ValidateToken( + const ngtcp2_vec& token, + const std::shared_ptr& addr, + const uint8_t* token_secret, + uint64_t verification_expiration, + const ngtcp2_crypto_aead& aead, + const ngtcp2_crypto_md& md) { + uint8_t token_key[kCryptoTokenKeylen]; + uint8_t token_iv[kCryptoTokenIvlen]; + uint8_t plaintext[kMaxTokenLen]; + std::array aad; + + // Quick checks. If the token is too short, too long, or does not + // contain the right token magic byte, assume invalid and skip further + // checks. + if (token.len < kMinRetryTokenLen || + token.len > kMaxTokenLen || + token.base[0] != kTokenMagic) { + return false; + } + + AeadContextPointer aead_ctx( + AeadContextPointer::Mode::DECRYPT, + token_key, + aead); + if (!aead_ctx) return false; + + const uint8_t* rand_data = token.base + token.len - kTokenRandLen; + const uint8_t* ciphertext = token.base + 1; + size_t ciphertextlen = token.len - kTokenRandLen - 1; + + if (!DeriveTokenKey( + token_key, + token_iv, + rand_data, + kTokenRandLen, + aead, + md, + token_secret)) { + return false; + } + + // Prepare the additional data (raw socket address + retry_cid) + auto p = std::begin(aad); + p = std::copy_n(addr->raw(), addr->length(), p); + + size_t aadlen = std::distance(std::begin(aad), p); + + if (NGTCP2_ERR(ngtcp2_crypto_decrypt( + plaintext, + &aead, + aead_ctx.get(), + ciphertext, + ciphertextlen, + token_iv, + kCryptoTokenIvlen, + aad.data(), + aadlen))) { + return false; + } + + size_t plaintextlen = ciphertextlen - aead.max_overhead; + if (plaintextlen != sizeof(uint64_t)) + return false; + + uint64_t t; + memcpy(&t, plaintext, sizeof(uint64_t)); + + // 1 hour window by default, but configurable for each + // Endpoint instance with a MIN_RETRYTOKEN_EXPIRATION second + // minimum and a MAX_RETRYTOKEN_EXPIRATION second maximum. + return t + verification_expiration * NGTCP2_SECONDS >= uv_hrtime(); +} + +// Get the ALPN protocol identifier that was negotiated for the session +Local GetALPNProtocol(const Session& session) { + Session::CryptoContext* ctx = session.crypto_context(); + Environment* env = session.env(); + BindingState* state = BindingState::Get(env); + std::string alpn = ctx->selected_alpn(); + if (alpn == &NGHTTP3_ALPN_H3[1]) { + return state->http3_alpn_string(); + } else { + return ToV8Value( + env->context(), + alpn, + env->isolate()).FromMaybe(Local()); + } +} + +namespace { +int CertCB(SSL* ssl, void* arg) { + Session* session = static_cast(arg); + int ret; + switch (SSL_get_tlsext_status_type(ssl)) { + case TLSEXT_STATUSTYPE_ocsp: + ret = session->crypto_context()->OnOCSP(); + return UNLIKELY(session->is_destroyed()) ? 0 : ret; + default: + return 1; + } +} + +void Keylog_CB(const SSL* ssl, const char* line) { + Session* session = static_cast(SSL_get_app_data(ssl)); + session->crypto_context()->Keylog(line); +} + +int Client_Hello_CB( + SSL* ssl, + int* tls_alert, + void* arg) { + Session* session = static_cast(SSL_get_app_data(ssl)); + int ret = session->crypto_context()->OnClientHello(); + if (UNLIKELY(session->is_destroyed())) { + *tls_alert = SSL_R_SSL_HANDSHAKE_FAILURE; + return 0; + } + switch (ret) { + case 0: + return 1; + case -1: + return -1; + default: + *tls_alert = ret; + return 0; + } +} + +int AlpnSelection( + SSL* ssl, + const unsigned char** out, + unsigned char* outlen, + const unsigned char* in, + unsigned int inlen, + void* arg) { + Session* session = static_cast(SSL_get_app_data(ssl)); + + unsigned char* tmp; + + // The QuicServerSession 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. + if (SSL_select_next_proto( + &tmp, + outlen, + reinterpret_cast(session->alpn().c_str()), + session->alpn().length(), + in, + inlen) == OPENSSL_NPN_NO_OVERLAP) { + return SSL_TLSEXT_ERR_NOACK; + } + *out = tmp; + return SSL_TLSEXT_ERR_OK; +} + +int AllowEarlyDataCB(SSL* ssl, void* arg) { + Session* session = static_cast(SSL_get_app_data(ssl)); + return session->allow_early_data() ? 1 : 0; +} + +int TLS_Status_Callback(SSL* ssl, void* arg) { + Session* session = static_cast(SSL_get_app_data(ssl)); + return session->crypto_context()->OnTLSStatus(); +} + +int New_Session_Callback(SSL* ssl, SSL_SESSION* session) { + Session* s = static_cast(SSL_get_app_data(ssl)); + return s->set_session(session); +} + +int GenerateSessionTicket(SSL* ssl, void* arg) { + Session* s = static_cast(SSL_get_app_data(ssl)); + SessionTicketAppData app_data(SSL_get_session(ssl)); + s->SetSessionTicketAppData(app_data); + return 1; +} + +SSL_TICKET_RETURN DecryptSessionTicket( + SSL* ssl, + SSL_SESSION* session, + const unsigned char* keyname, + size_t keyname_len, + SSL_TICKET_STATUS status, + void* arg) { + Session* s = static_cast(SSL_get_app_data(ssl)); + SessionTicketAppData::Flag flag = SessionTicketAppData::Flag::STATUS_NONE; + switch (status) { + default: + return SSL_TICKET_RETURN_IGNORE; + case SSL_TICKET_EMPTY: + // Fall through + case SSL_TICKET_NO_DECRYPT: + return SSL_TICKET_RETURN_IGNORE_RENEW; + case SSL_TICKET_SUCCESS_RENEW: + flag = SessionTicketAppData::Flag::STATUS_RENEW; + // Fall through + case SSL_TICKET_SUCCESS: + SessionTicketAppData app_data(session); + switch (s->GetSessionTicketAppData(app_data, flag)) { + default: + return SSL_TICKET_RETURN_IGNORE; + case SessionTicketAppData::Status::TICKET_IGNORE: + return SSL_TICKET_RETURN_IGNORE; + case SessionTicketAppData::Status::TICKET_IGNORE_RENEW: + return SSL_TICKET_RETURN_IGNORE_RENEW; + case SessionTicketAppData::Status::TICKET_USE: + return SSL_TICKET_RETURN_USE; + case SessionTicketAppData::Status::TICKET_USE_RENEW: + return SSL_TICKET_RETURN_USE_RENEW; + } + } +} + +int SetEncryptionSecrets( + SSL* ssl, + OSSL_ENCRYPTION_LEVEL ossl_level, + const uint8_t* read_secret, + const uint8_t* write_secret, + size_t secret_len) { + Session* session = static_cast(SSL_get_app_data(ssl)); + return session->crypto_context()->OnSecrets( + from_ossl_level(ossl_level), + read_secret, + write_secret, + secret_len) ? 1 : 0; +} + +int AddHandshakeData( + SSL* ssl, + OSSL_ENCRYPTION_LEVEL ossl_level, + const uint8_t* data, + size_t len) { + Session* session = static_cast(SSL_get_app_data(ssl)); + session->crypto_context()->WriteHandshake( + from_ossl_level(ossl_level), + data, + len); + return 1; +} + +int FlushFlight(SSL* ssl) { return 1; } + +int SendAlert( + SSL* ssl, + ssl_encryption_level_t level, + uint8_t alert) { + Session* session = static_cast(SSL_get_app_data(ssl)); + session->crypto_context()->set_tls_alert(alert); + return 1; +} + +bool SetTransportParams(Session* session, const crypto::SSLPointer& ssl) { + ngtcp2_transport_params params; + ngtcp2_conn_get_local_transport_params(session->connection(), ¶ms); + uint8_t buf[512]; + ssize_t nwrite = ngtcp2_encode_transport_params( + buf, + arraysize(buf), + NGTCP2_TRANSPORT_PARAMS_TYPE_ENCRYPTED_EXTENSIONS, + ¶ms); + return nwrite >= 0 && + SSL_set_quic_transport_params(ssl.get(), buf, nwrite) == 1; +} + +SSL_QUIC_METHOD quic_method = SSL_QUIC_METHOD{ + SetEncryptionSecrets, + AddHandshakeData, + FlushFlight, + SendAlert +}; + +void SetHostname(const crypto::SSLPointer& ssl, const std::string& hostname) { + // If the hostname is an IP address, use an empty string + // as the hostname instead. + X509_VERIFY_PARAM* param = SSL_get0_param(ssl.get()); + X509_VERIFY_PARAM_set_hostflags(param, 0); + + if (UNLIKELY(SocketAddress::is_numeric_host(hostname.c_str()))) { + SSL_set_tlsext_host_name(ssl.get(), ""); + CHECK_EQ(X509_VERIFY_PARAM_set1_host(param, "", 0), 1); + } else { + SSL_set_tlsext_host_name(ssl.get(), hostname.c_str()); + CHECK_EQ( + X509_VERIFY_PARAM_set1_host(param, hostname.c_str(), hostname.length()), + 1); + } +} + +} // namespace + +void InitializeTLS(Session* session, const crypto::SSLPointer& ssl) { + Session::CryptoContext* ctx = session->crypto_context(); + Environment* env = session->env(); + BindingState* state = env->GetBindingData(env->context()); + + SSL_set_app_data(ssl.get(), session); + SSL_set_cert_cb( + ssl.get(), + CertCB, + const_cast(reinterpret_cast(session))); + SSL_set_verify( + ssl.get(), + SSL_VERIFY_NONE, + crypto::VerifyCallback); + + // Enable tracing if the `--trace-tls` command line flag is used. + if (env->options()->trace_tls || ctx->enable_tls_trace()) { + ctx->EnableTrace(); + if (state->warn_trace_tls) { + state->warn_trace_tls = false; + ProcessEmitWarning(env, + "Enabling --trace-tls can expose sensitive data " + "in the resulting log"); + } + } + + switch (ctx->side()) { + case NGTCP2_CRYPTO_SIDE_CLIENT: { + SSL_set_connect_state(ssl.get()); + crypto::SetALPN(ssl, session->alpn()); + SetHostname(ssl, session->hostname()); + if (ctx->request_ocsp()) + SSL_set_tlsext_status_type(ssl.get(), TLSEXT_STATUSTYPE_ocsp); + break; + } + case NGTCP2_CRYPTO_SIDE_SERVER: { + SSL_set_accept_state(ssl.get()); + if (ctx->request_peer_certificate()) { + int verify_mode = SSL_VERIFY_PEER; + if (ctx->reject_unauthorized()) + verify_mode |= SSL_VERIFY_FAIL_IF_NO_PEER_CERT; + SSL_set_verify(ssl.get(), verify_mode, crypto::VerifyCallback); + } + break; + } + default: + UNREACHABLE(); + } + + ngtcp2_conn_set_tls_native_handle(session->connection(), ssl.get()); + SetTransportParams(session, ssl); +} + +void InitializeSecureContext( + crypto::SecureContext* sc, + ngtcp2_crypto_side side) { + sc->ctx_.reset(SSL_CTX_new(TLS_method())); + SSL_CTX_set_app_data(**sc, sc); + int options = SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3; + + switch (side) { + case NGTCP2_CRYPTO_SIDE_SERVER: + options = + (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(**sc, SSL_MODE_RELEASE_BUFFERS); + SSL_CTX_set_alpn_select_cb(**sc, AlpnSelection, nullptr); + SSL_CTX_set_client_hello_cb(**sc, Client_Hello_CB, nullptr); + SSL_CTX_set_session_ticket_cb( + **sc, + GenerateSessionTicket, + DecryptSessionTicket, + nullptr); + SSL_CTX_set_max_early_data(**sc, 0xffffffff); + SSL_CTX_set_allow_early_data_cb(**sc, AllowEarlyDataCB, nullptr); + break; + case NGTCP2_CRYPTO_SIDE_CLIENT: + SSL_CTX_set_session_cache_mode( + **sc, + SSL_SESS_CACHE_CLIENT | + SSL_SESS_CACHE_NO_INTERNAL_STORE); + SSL_CTX_sess_set_new_cb(**sc, New_Session_Callback); + break; + default: + UNREACHABLE(); + } + SSL_CTX_set_options(**sc, options); + SSL_CTX_clear_mode(**sc, SSL_MODE_NO_AUTO_CHAIN); + SSL_CTX_set_min_proto_version(**sc, TLS1_3_VERSION); + SSL_CTX_set_max_proto_version(**sc, TLS1_3_VERSION); + SSL_CTX_set_default_verify_paths(**sc); + SSL_CTX_set_tlsext_status_cb(**sc, TLS_Status_Callback); + SSL_CTX_set_keylog_callback(**sc, Keylog_CB); + SSL_CTX_set_tlsext_status_arg(**sc, nullptr); + SSL_CTX_set_quic_method(**sc, &quic_method); +} + +ngtcp2_crypto_level from_ossl_level(OSSL_ENCRYPTION_LEVEL ossl_level) { + switch (ossl_level) { + case ssl_encryption_initial: + return NGTCP2_CRYPTO_LEVEL_INITIAL; + case ssl_encryption_early_data: + return NGTCP2_CRYPTO_LEVEL_EARLY; + case ssl_encryption_handshake: + return NGTCP2_CRYPTO_LEVEL_HANDSHAKE; + case ssl_encryption_application: + return NGTCP2_CRYPTO_LEVEL_APPLICATION; + default: + UNREACHABLE(); + } +} + +const char* crypto_level_name(ngtcp2_crypto_level level) { + switch (level) { + case NGTCP2_CRYPTO_LEVEL_INITIAL: + return "initial"; + case NGTCP2_CRYPTO_LEVEL_EARLY: + return "early"; + case NGTCP2_CRYPTO_LEVEL_HANDSHAKE: + return "handshake"; + case NGTCP2_CRYPTO_LEVEL_APPLICATION: + return "app"; + default: + UNREACHABLE(); + } +} + +// When using IPv6, QUIC recommends the use of IPv6 Flow Labels +// as specified in https://tools.ietf.org/html/rfc6437. These +// are used as a means of reliably associating packets exchanged +// as part of a single flow and protecting against certain kinds +// of attacks. +uint32_t GenerateFlowLabel( + const std::shared_ptr& local, + const std::shared_ptr& remote, + const CID& cid, + const uint8_t* secret, + size_t secretlen) { + static constexpr size_t kInfoLen = + (sizeof(sockaddr_in6) * 2) + NGTCP2_MAX_CIDLEN; + + uint32_t label = 0; + + std::array plaintext; + size_t infolen = local->length() + remote->length() + cid.length(); + CHECK_LE(infolen, kInfoLen); + + ngtcp2_crypto_ctx ctx; + ngtcp2_crypto_ctx_initial(&ctx); + + auto p = std::begin(plaintext); + p = std::copy_n(local->raw(), local->length(), p); + p = std::copy_n(remote->raw(), remote->length(), p); + p = std::copy_n(cid->data, cid->datalen, p); + + ngtcp2_crypto_hkdf_expand( + reinterpret_cast(&label), + sizeof(label), + &ctx.md, + secret, + secretlen, + plaintext.data(), + infolen); + + label &= kLabelMask; + DCHECK_LE(label, kLabelMask); + return label; +} + +ngtcp2_crypto_aead CryptoAeadAes128GCM() { + ngtcp2_crypto_aead aead; + ngtcp2_crypto_aead_init(&aead, const_cast(EVP_aes_128_gcm())); + return aead; +} + +ngtcp2_crypto_md CryptoMDSha256() { + ngtcp2_crypto_md md; + ngtcp2_crypto_md_init(&md, const_cast(EVP_sha256())); + return md; +} + +} // namespace quic +} // namespace node diff --git a/src/quic/crypto.h b/src/quic/crypto.h new file mode 100644 index 00000000000000..06b4bb48fc3414 --- /dev/null +++ b/src/quic/crypto.h @@ -0,0 +1,214 @@ +#ifndef SRC_QUIC_CRYPTO_H_ +#define SRC_QUIC_CRYPTO_H_ + +#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS + +#include "quic/quic.h" +#include "crypto/crypto_context.h" +#include "crypto/crypto_util.h" +#include "node_sockaddr.h" +#include "v8.h" + +#include +#include +#include + +namespace node { + +namespace quic { + +constexpr uint8_t kRetryTokenMagic = 0xb6; +constexpr uint8_t kTokenMagic = 0x36; +constexpr int kCryptoTokenKeylen = 32; +constexpr int kCryptoTokenIvlen = 32; +constexpr size_t kTokenRandLen = 16; +// 1 accounts for the magic byte, 16 accounts for aead tag +constexpr size_t kMaxRetryTokenLen = + 1 + sizeof(uint64_t) + NGTCP2_MAX_CIDLEN + 16 + kTokenRandLen; +constexpr size_t kMinRetryTokenLen = 1 + kTokenRandLen; + +// 1 accounts for the magic byte, 16 accounts for aead tag +constexpr size_t kMaxTokenLen = 1 + sizeof(uint64_t) + 16 + kTokenRandLen; + +// Forward declaration +class Session; + +// many ngtcp2 functions return 0 to indicate success +// and non-zero to indicate failure. Most of the time, +// for such functions we don't care about the specific +// return value so we simplify using a macro. + +#define NGTCP2_ERR(V) (V != 0) +#define NGTCP2_OK(V) (V == 0) + +// Called by QuicInitSecureContext to initialize the +// given SecureContext with the defaults for the given +// QUIC side (client or server). +void InitializeSecureContext( + crypto::SecureContext* sc, + ngtcp2_crypto_side side); + +void InitializeTLS(Session* session, const crypto::SSLPointer& ssl); + +// Generates a stateless reset token using HKDF with the +// cid and token secret as input. The token secret is +// either provided by user code when an Endpoint is +// created or is generated randomly. +// +// QUIC leaves the generation of stateless session tokens +// up to the implementation to figure out. The idea, however, +// is that it ought to be possible to generate a stateless +// reset token reliably even when all state for a connection +// has been lost. We use the cid as it's the only reliably +// consistent bit of data we have when a session is destroyed. +bool GenerateResetToken(uint8_t* token, const uint8_t* secret, const CID& cid); + +// The Retry Token is an encrypted token that is sent to the client +// by the server as part of the path validation flow. The plaintext +// format within the token is opaque and only meaningful the server. +// We can structure it any way we want. It needs to: +// * be hard to guess +// * be time limited +// * be specific to the client address +// * be specific to the original cid +// * contain random data. +std::unique_ptr GenerateRetryPacket( + quic_version version, + const uint8_t* token_secret, + const CID& dcid, + const CID& scid, + const std::shared_ptr& local_addr, + const std::shared_ptr& remote_addr, + const ngtcp2_crypto_aead& aead, + const ngtcp2_crypto_md& md); + +// The IPv6 Flow Label is generated and set whenever IPv6 is used. +// The label is derived as a cryptographic function of the CID, +// local and remote addresses, and the given secret, that is then +// truncated to a 20-bit value (per IPv6 requirements). In QUIC, +// the flow label *may* be used as a way of disambiguating IP +// packets that belong to the same flow from a remote peer. +uint32_t GenerateFlowLabel( + const std::shared_ptr& local, + const std::shared_ptr& remote, + const CID& cid, + const uint8_t* secret, + size_t secretlen); + +// Validates a retry token. Returns Nothing() if the +// token is *not valid*, returns the OCID otherwise. +v8::Maybe ValidateRetryToken( + const ngtcp2_vec& token, + const std::shared_ptr& addr, + const CID& dcid, + const uint8_t* token_secret, + uint64_t verification_expiration, + const ngtcp2_crypto_aead& aead, + const ngtcp2_crypto_md& md); + +bool GenerateToken( + uint8_t* token, + size_t* tokenlen, + const std::shared_ptr& addr, + const uint8_t* token_secret, + const ngtcp2_crypto_aead& aead, + const ngtcp2_crypto_md& md); + +bool ValidateToken( + const ngtcp2_vec& token, + const std::shared_ptr& addr, + const uint8_t* token_secret, + uint64_t verification_expiration, + const ngtcp2_crypto_aead& aead, + const ngtcp2_crypto_md& md); + +// Get the ALPN protocol identifier that was negotiated for the session +v8::Local GetALPNProtocol(const Session& session); + +ngtcp2_crypto_level from_ossl_level(OSSL_ENCRYPTION_LEVEL ossl_level); +const char* crypto_level_name(ngtcp2_crypto_level level); + +// SessionTicketAppData is a utility class that is used only during +// the generation or access of TLS stateless sesson tickets. It +// exists solely to provide a easier way for QuicApplication instances +// to set relevant metadata in the session ticket when it is created, +// and the exract and subsequently verify that data when a ticket is +// received and is being validated. The app data is completely opaque +// to anything other than the server-side of the QuicApplication that +// sets it. +class SessionTicketAppData { + public: + enum class Status { + TICKET_USE, + TICKET_USE_RENEW, + TICKET_IGNORE, + TICKET_IGNORE_RENEW + }; + + enum class Flag { + STATUS_NONE, + STATUS_RENEW + }; + + explicit SessionTicketAppData(SSL_SESSION* session) : session_(session) {} + bool Set(const uint8_t* data, size_t len); + bool Get(uint8_t** data, size_t* len) const; + + private: + bool set_ = false; + SSL_SESSION* session_; +}; + +ngtcp2_crypto_aead CryptoAeadAes128GCM(); + +ngtcp2_crypto_md CryptoMDSha256(); + +class AeadContextPointer { + public: + enum class Mode { + ENCRYPT, + DECRYPT + }; + + inline AeadContextPointer( + Mode mode, + const uint8_t* key, + const ngtcp2_crypto_aead& aead) { + switch (mode) { + case Mode::ENCRYPT: + inited_ = NGTCP2_OK(ngtcp2_crypto_aead_ctx_encrypt_init( + &ctx_, + &aead, + key, + kCryptoTokenIvlen)); + break; + case Mode::DECRYPT: + inited_ = NGTCP2_OK(ngtcp2_crypto_aead_ctx_decrypt_init( + &ctx_, + &aead, + key, + kCryptoTokenIvlen)); + break; + default: + UNREACHABLE(); + } + } + + inline ~AeadContextPointer() { + ngtcp2_crypto_aead_ctx_free(&ctx_); + } + + inline ngtcp2_crypto_aead_ctx* get() { return &ctx_; } + + inline operator bool() const noexcept { return inited_; } + + private: + ngtcp2_crypto_aead_ctx ctx_; + bool inited_ = false; +}; + +} // namespace quic +} // namespace node + +#endif // NODE_WANT_INTERNALS +#endif // SRC_QUIC_CRYPTO_H_ diff --git a/src/quic/endpoint.cc b/src/quic/endpoint.cc new file mode 100644 index 00000000000000..679d024f5e7a16 --- /dev/null +++ b/src/quic/endpoint.cc @@ -0,0 +1,2054 @@ +#include "quic/buffer.h" +#include "quic/crypto.h" +#include "quic/endpoint.h" +#include "quic/quic.h" +#include "quic/qlog.h" +#include "quic/session.h" +#include "quic/stream.h" +#include "crypto/crypto_util.h" +#include "aliased_struct-inl.h" +#include "allocated_buffer-inl.h" +#include "async_wrap-inl.h" +#include "base_object-inl.h" +#include "debug_utils-inl.h" +#include "env-inl.h" +#include "memory_tracker-inl.h" +#include "node_mem-inl.h" +#include "node_sockaddr-inl.h" +#include "req_wrap-inl.h" +#include "udp_wrap.h" +#include "v8.h" + +#include +#include + +namespace node { + +using v8::BackingStore; +using v8::BigInt; +using v8::Context; +using v8::FunctionCallbackInfo; +using v8::FunctionTemplate; +using v8::HandleScope; +using v8::Int32; +using v8::Integer; +using v8::Just; +using v8::Local; +using v8::Maybe; +using v8::Nothing; +using v8::Number; +using v8::Object; +using v8::PropertyAttribute; +using v8::String; +using v8::Uint32; +using v8::Value; + +namespace quic { + +namespace { +// The reserved version is a mechanism QUIC endpoints +// can use to ensure correct handling of version +// negotiation. It is defined by the QUIC spec in +// https://tools.ietf.org/html/draft-ietf-quic-transport-24#section-6.3 +// Specifically, any version that follows the pattern +// 0x?a?a?a?a may be used to force version negotiation. +inline quic_version GenerateReservedVersion( + const std::shared_ptr& addr, + const quic_version version) { + socklen_t addrlen = addr->length(); + quic_version h = 0x811C9DC5u; + quic_version ver = htonl(version); + const uint8_t* p = addr->raw(); + const uint8_t* ep = p + addrlen; + for (; p != ep; ++p) { + h ^= *p; + h *= 0x01000193u; + } + p = reinterpret_cast(&ver); + ep = p + sizeof(version); + for (; p != ep; ++p) { + h ^= *p; + h *= 0x01000193u; + } + h &= 0xf0f0f0f0u; + h |= 0x0a0a0a0au; + return h; +} + +inline bool IsShortHeader(quic_version version, const CID& cid) { + return version == NGTCP2_PROTO_VER_MAX && !cid; +} +} // namespace + +Endpoint::Config::Config() { + GenerateResetTokenSecret(); +} + +Endpoint::Config::Config(const Config& other) noexcept + : local_address(other.local_address), + retry_token_expiration(other.retry_token_expiration), + token_expiration(other.token_expiration), + max_window_override(other.max_window_override), + max_stream_window_override(other.max_stream_window_override), + max_connections_per_host(other.max_connections_per_host), + max_connections_total(other.max_connections_total), + max_stateless_resets(other.max_stateless_resets), + address_lru_size(other.address_lru_size), + retry_limit(other.retry_limit), + max_payload_size(other.max_payload_size), + unacknowledged_packet_threshold( + other.unacknowledged_packet_threshold), + qlog(other.qlog), + validate_address(other.validate_address), + disable_stateless_reset(other.disable_stateless_reset), + rx_loss(other.rx_loss), + tx_loss(other.tx_loss), + cc_algorithm(other.cc_algorithm), + ipv6_only(other.ipv6_only), + udp_receive_buffer_size(other.udp_receive_buffer_size), + udp_send_buffer_size(other.udp_send_buffer_size), + udp_ttl(other.udp_ttl) { + memcpy( + reset_token_secret, + other.reset_token_secret, + NGTCP2_STATELESS_RESET_TOKENLEN); +} + +void Endpoint::Config::GenerateResetTokenSecret() { + crypto::EntropySource( + reinterpret_cast(&reset_token_secret), + NGTCP2_STATELESS_RESET_TOKENLEN); +} + +bool Endpoint::SocketAddressInfoTraits::CheckExpired( + const SocketAddress& address, + const Type& type) { + return (uv_hrtime() - type.timestamp) > 1e10; // 10 seconds. +} + +void Endpoint::SocketAddressInfoTraits::Touch( + const SocketAddress& address, + Type* type) { + type->timestamp = uv_hrtime(); +} + +template<> +void StatsTraitsImpl::ToString( + const Endpoint& ptr, + AddStatsField add_field) { +#define V(_n, name, label) add_field(label, ptr.GetStat(&EndpointStats::name)); + ENDPOINT_STATS(V) +#undef V +} + +bool ConfigObject::HasInstance(Environment* env, const Local& value) { + return GetConstructorTemplate(env)->HasInstance(value); +} + +Local ConfigObject::GetConstructorTemplate( + Environment* env) { + BindingState* state = env->GetBindingData(env->context()); + Local tmpl = state->endpoint_config_constructor_template(); + if (tmpl.IsEmpty()) { + tmpl = env->NewFunctionTemplate(New); + tmpl->Inherit(BaseObject::GetConstructorTemplate(env)); + tmpl->InstanceTemplate()->SetInternalFieldCount( + ConfigObject::kInternalFieldCount); + tmpl->SetClassName(FIXED_ONE_BYTE_STRING(env->isolate(), "ConfigObject")); + env->SetProtoMethod( + tmpl, + "generateResetTokenSecret", + GenerateResetTokenSecret); + env->SetProtoMethod( + tmpl, + "setResetTokenSecret", + SetResetTokenSecret); + state->set_endpoint_config_constructor_template(tmpl); + } + return tmpl; +} + +void ConfigObject::Initialize(Environment* env, Local target) { + env->SetConstructorFunction( + target, + "ConfigObject", + GetConstructorTemplate(env)); +} + +template <> Maybe ConfigObject::SetOption( + const Local& object, + const Local& name, + uint64_t Endpoint::Config::*member) { + Local value; + if (UNLIKELY(!object->Get(env()->context(), name).ToLocal(&value))) + return Nothing(); + + if (value->IsUndefined()) + return Just(false); + + CHECK_IMPLIES(!value->IsBigInt(), value->IsNumber()); + + uint64_t val = 0; + if (value->IsBigInt()) { + bool lossless = true; + val = value.As()->Uint64Value(&lossless); + if (!lossless) { + Utf8Value label(env()->isolate(), name); + THROW_ERR_OUT_OF_RANGE( + env(), + (std::string("options.") + (*label) + " is out of range").c_str()); + return Nothing(); + } + } else { + val = static_cast(value.As()->Value()); + } + config_.get()->*member = val; + return Just(true); +} + +template <> Maybe ConfigObject::SetOption( + const Local& object, + const Local& name, + uint32_t Endpoint::Config::*member) { + Local value; + if (UNLIKELY(!object->Get(env()->context(), name).ToLocal(&value))) + return Nothing(); + + if (value->IsUndefined()) + return Just(false); + + CHECK(value->IsUint32()); + + uint32_t val = value.As()->Value(); + config_.get()->*member = val; + return Just(true); +} + +template <> Maybe ConfigObject::SetOption( + const Local& object, + const Local& name, + uint8_t Endpoint::Config::*member) { + Local value; + if (UNLIKELY(!object->Get(env()->context(), name).ToLocal(&value))) + return Nothing(); + + if (value->IsUndefined()) + return Just(false); + + CHECK(value->IsUint32()); + + uint32_t val = value.As()->Value(); + if (val > 255) return Just(false); + config_.get()->*member = static_cast(val); + return Just(true); +} + +template <> Maybe ConfigObject::SetOption( + const Local& object, + const Local& name, + double Endpoint::Config::*member) { + Local value; + if (UNLIKELY(!object->Get(env()->context(), name).ToLocal(&value))) + return Nothing(); + + if (value->IsUndefined()) + return Just(false); + + CHECK(value->IsNumber()); + double val = static_cast(value.As()->Value()); + config_.get()->*member = val; + return Just(true); +} + +template <> Maybe ConfigObject::SetOption( + const Local& object, + const Local& name, + ngtcp2_cc_algo Endpoint::Config::*member) { + Local value; + if (UNLIKELY(!object->Get(env()->context(), name).ToLocal(&value))) + return Nothing(); + + if (value->IsUndefined()) + return Just(false); + + ngtcp2_cc_algo val = static_cast(value.As()->Value()); + switch (val) { + case NGTCP2_CC_ALGO_CUBIC: + // Fall through + case NGTCP2_CC_ALGO_RENO: + config_.get()->*member = val; + break; + default: + UNREACHABLE(); + } + + return Just(true); +} + +template <> Maybe ConfigObject::SetOption( + const Local& object, + const Local& name, + bool Endpoint::Config::*member) { + Local value; + if (UNLIKELY(!object->Get(env()->context(), name).ToLocal(&value))) + return Nothing(); + if (value->IsUndefined()) + return Just(false); + CHECK(value->IsBoolean()); + config_.get()->*member = value->IsTrue(); + return Just(true); +} + +void ConfigObject::New(const FunctionCallbackInfo& args) { + CHECK(args.IsConstructCall()); + Environment* env = Environment::GetCurrent(args); + + ConfigObject* config = new ConfigObject(env, args.This()); + config->data()->GenerateResetTokenSecret(); + + CHECK(SocketAddressBase::HasInstance(env, args[0])); + SocketAddressBase* address; + ASSIGN_OR_RETURN_UNWRAP(&address, args[0]); + + config->data()->local_address = address->address(); + + if (LIKELY(args[1]->IsObject())) { + BindingState* state = env->GetBindingData(env->context()); + Local object = args[0].As(); + if (UNLIKELY(config->SetOption( + object, + state->retry_token_expiration_string(), + &Endpoint::Config::retry_token_expiration).IsNothing()) || + UNLIKELY(config->SetOption( + object, + state->token_expiration_string(), + &Endpoint::Config::token_expiration).IsNothing()) || + UNLIKELY(config->SetOption( + object, + state->max_window_override_string(), + &Endpoint::Config::max_window_override).IsNothing()) || + UNLIKELY(config->SetOption( + object, + state->max_stream_window_override_string(), + &Endpoint::Config::max_stream_window_override).IsNothing()) || + UNLIKELY(config->SetOption( + object, + state->max_connections_per_host_string(), + &Endpoint::Config::max_connections_per_host).IsNothing()) || + UNLIKELY(config->SetOption( + object, + state->max_connections_total_string(), + &Endpoint::Config::max_connections_total).IsNothing()) || + UNLIKELY(config->SetOption( + object, + state->max_stateless_resets_string(), + &Endpoint::Config::max_stateless_resets).IsNothing()) || + UNLIKELY(config->SetOption( + object, + state->address_lru_size_string(), + &Endpoint::Config::address_lru_size).IsNothing()) || + UNLIKELY(config->SetOption( + object, + state->retry_limit_string(), + &Endpoint::Config::retry_limit).IsNothing()) || + UNLIKELY(config->SetOption( + object, + state->max_payload_size_string(), + &Endpoint::Config::max_payload_size).IsNothing()) || + UNLIKELY(config->SetOption( + object, + state->unacknowledged_packet_threshold_string(), + &Endpoint::Config::unacknowledged_packet_threshold).IsNothing()) || + UNLIKELY(config->SetOption( + object, + state->qlog_string(), + &Endpoint::Config::qlog).IsNothing()) || + UNLIKELY(config->SetOption( + object, + state->validate_address_string(), + &Endpoint::Config::validate_address).IsNothing()) || + UNLIKELY(config->SetOption( + object, + state->disable_stateless_reset_string(), + &Endpoint::Config::disable_stateless_reset).IsNothing()) || + UNLIKELY(config->SetOption( + object, + state->rx_packet_loss_string(), + &Endpoint::Config::rx_loss).IsNothing()) || + UNLIKELY(config->SetOption( + object, + state->tx_packet_loss_string(), + &Endpoint::Config::tx_loss).IsNothing()) || + UNLIKELY(config->SetOption( + object, + state->cc_algorithm_string(), + &Endpoint::Config::cc_algorithm).IsNothing()) || + UNLIKELY(config->SetOption( + object, + state->ipv6_only_string(), + &Endpoint::Config::ipv6_only).IsNothing()) || + UNLIKELY(config->SetOption( + object, + state->udp_receive_buffer_size_string(), + &Endpoint::Config::udp_receive_buffer_size).IsNothing()) || + UNLIKELY(config->SetOption( + object, + state->udp_send_buffer_size_string(), + &Endpoint::Config::udp_send_buffer_size).IsNothing()) || + UNLIKELY(config->SetOption( + object, + state->udp_ttl_string(), + &Endpoint::Config::udp_ttl).IsNothing())) { + // The if block intentionally does nothing. The code is structured + // like this to shortcircuit if any of the SetOptions() returns Nothing. + } + } +} + +void ConfigObject::GenerateResetTokenSecret( + const FunctionCallbackInfo& args) { + ConfigObject* config; + ASSIGN_OR_RETURN_UNWRAP(&config, args.Holder()); + config->data()->GenerateResetTokenSecret(); +} + +void ConfigObject::SetResetTokenSecret( + const FunctionCallbackInfo& args) { + ConfigObject* config; + ASSIGN_OR_RETURN_UNWRAP(&config, args.Holder()); + + crypto::ArrayBufferOrViewContents secret(args[0]); + CHECK_EQ(secret.size(), sizeof(config->data()->reset_token_secret)); + memcpy(config->data()->reset_token_secret, secret.data(), secret.size()); +} + +ConfigObject::ConfigObject( + Environment* env, + Local object, + std::shared_ptr config) + : BaseObject(env, object), + config_(config) { + MakeWeak(); +} + +void ConfigObject::MemoryInfo(MemoryTracker* tracker) const { + tracker->TrackField("config", config_); +} + +Local Endpoint::SendWrap::GetConstructorTemplate( + Environment* env) { + BindingState* state = env->GetBindingData(env->context()); + CHECK_NOT_NULL(state); + Local tmpl = state->send_wrap_constructor_template(); + if (tmpl.IsEmpty()) { + tmpl = FunctionTemplate::New(env->isolate()); + tmpl->Inherit(UdpSendWrap::GetConstructorTemplate(env)); + tmpl->InstanceTemplate()->SetInternalFieldCount( + SendWrap::kInternalFieldCount); + tmpl->SetClassName(FIXED_ONE_BYTE_STRING(env->isolate(), "QuicSendWrap")); + state->set_send_wrap_constructor_template(tmpl); + } + return tmpl; +} + +BaseObjectPtr Endpoint::SendWrap::Create( + Environment* env, + const std::shared_ptr& destination, + std::unique_ptr packet, + BaseObjectPtr endpoint) { + Local obj; + if (UNLIKELY(!GetConstructorTemplate(env) + ->InstanceTemplate() + ->NewInstance(env->context()).ToLocal(&obj))) { + return BaseObjectPtr(); + } + + return MakeBaseObject( + env, + obj, + destination, + std::move(packet), + std::move(endpoint)); +} + +Endpoint::SendWrap::SendWrap( + Environment* env, + v8::Local object, + const std::shared_ptr& destination, + std::unique_ptr packet, + BaseObjectPtr endpoint) + : UdpSendWrap(env, object, AsyncWrap::PROVIDER_QUICSENDWRAP), + destination_(destination), + packet_(std::move(packet)), + endpoint_(std::move(endpoint)), + self_ptr_(this) { + MakeWeak(); +} + +void Endpoint::SendWrap::MemoryInfo(MemoryTracker* tracker) const { + tracker->TrackField("destination", destination_); + tracker->TrackField("packet", packet_); + if (endpoint_) + tracker->TrackField("endpoint", endpoint_); +} + +void Endpoint::SendWrap::Done(int status) { + if (endpoint_) + endpoint_->OnSendDone(status); + strong_ptr_.reset(); + self_ptr_.reset(); + endpoint_.reset(); +} + +Endpoint::Endpoint(Environment* env, const Config& config) + : EndpointStatsBase(env), + env_(env), + udp_(env, this), + config_(config), + outbound_signal_(env, [this]() { this->ProcessOutbound(); }), + token_aead_(CryptoAeadAes128GCM()), + token_md_(CryptoMDSha256()), + addrLRU_(config.address_lru_size) { + crypto::EntropySource( + reinterpret_cast(token_secret_), + kTokenSecretLen); + env->AddCleanupHook(OnCleanup, this); +} + +Endpoint::~Endpoint() { + // There should be no more sessions and all queues + // and lists should be empty. + CHECK(sessions_.empty()); + CHECK(outbound_.empty()); + CHECK(listeners_.empty()); + outbound_signal_.Close(); + env()->RemoveCleanupHook(OnCleanup, this); +} + +void Endpoint::OnCleanup(void* data) { + Endpoint* endpoint = static_cast(data); + endpoint->Close(); +} + +void Endpoint::MemoryInfo(MemoryTracker* tracker) const { + tracker->TrackField("udp", udp_); + tracker->TrackField("outbound", outbound_); + tracker->TrackField("addrLRU", addrLRU_); +} + +void Endpoint::ProcessReceiveFailure(int status) { + Close(CloseListener::Context::RECEIVE_FAILURE, status); +} + +void Endpoint::AddCloseListener(CloseListener* listener) { + close_listeners_.insert(listener); +} + +void Endpoint::RemoveCloseListener(CloseListener* listener) { + close_listeners_.erase(listener); +} + +void Endpoint::Close(CloseListener::Context context, int status) { + RecordTimestamp(&EndpointStats::destroyed_at); + + udp_.Close(); + + // Cancel any remaining outbound packets. Ideally there wouldn't + // be any, but at this point there's nothing else we can do. + SendWrap::Queue outbound; + outbound_.swap(outbound); + for (const auto packet : outbound) + packet->Done(UV_ECANCELED); + outbound.clear(); + pending_outbound_ = 0; + + // Notify all of the registered EndpointWrap instances that + // this shared endpoint is closed. + for (const auto listener : close_listeners_) + listener->EndpointClosed(context, status); +} + +bool Endpoint::AcceptInitialPacket( + const quic_version version, + const CID& dcid, + const CID& scid, + std::shared_ptr store, + size_t nread, + const std::shared_ptr& local_address, + const std::shared_ptr& remote_address) { + + ngtcp2_pkt_hd hd; + CID ocid; + + if (listeners_.empty()) return false; + + switch (ngtcp2_accept(&hd, static_cast(store->Data()), nread)) { + case 1: + // Send Version Negotiation + SendVersionNegotiation( + version, + dcid, + scid, + local_address, + remote_address); + // Fall through + case -1: + // Either a version negotiation packet was sent or the packet is + // an invalid initial packet. Either way, there's nothing more we + // can do here and we will consider this an ignored packet. + return false; + } + + // If the server is busy, of the number of connections total for this + // server, and this remote addr, new connections will be shut down + // immediately. + if (UNLIKELY(busy_) || + sessions_.size() >= config_.max_connections_total || + current_socket_address_count(remote_address) >= + config_.max_connections_per_host) { + // Endpoint is busy or the connection count is exceeded + IncrementStat(&EndpointStats::server_busy_count); + ImmediateConnectionClose( + version, + scid, + dcid, + local_address, + remote_address, + NGTCP2_CONNECTION_REFUSED); + return BaseObjectPtr(); + } + + Session::Config config(this, dcid, scid, version); + + // QUIC has address validation built in to the handshake but allows for + // an additional explicit validation request using RETRY frames. If we + // are using explicit validation, we check for the existence of a valid + // retry token in the packet. If one does not exist, we send a retry with + // a new token. If it does exist, and if it's valid, we grab the original + // cid and continue. + if (!is_validated_address(remote_address)) { + switch (hd.type) { + case NGTCP2_PKT_INITIAL: + if (LIKELY(config_.validate_address) || hd.token.len > 0) { + // Perform explicit address validation + if (hd.token.len == 0) { + // No retry token was detected. Generate one. + SendRetry(version, dcid, scid, local_address, remote_address); + return BaseObjectPtr(); + } + + if (hd.token.base[0] != kRetryTokenMagic && + hd.dcid.datalen < NGTCP2_MIN_INITIAL_DCIDLEN) { + ImmediateConnectionClose( + version, + scid, + dcid, + local_address, + remote_address); + return BaseObjectPtr(); + } + + switch (hd.token.base[0]) { + case kRetryTokenMagic: { + if (!ValidateRetryToken( + hd.token, + remote_address, + dcid, + token_secret_, + config_.retry_token_expiration, + token_aead_, + token_md_).To(&ocid)) { + // Invalid retry token was detected. Close the connection. + ImmediateConnectionClose( + version, + scid, + dcid, + local_address, + remote_address); + return BaseObjectPtr(); + } + break; + } + case kTokenMagic: { + if (!ValidateToken( + hd.token, + remote_address, + token_secret_, + config_.token_expiration, + token_aead_, + token_md_)) { + SendRetry(version, dcid, scid, local_address, remote_address); + return BaseObjectPtr(); + } + hd.token.base = nullptr; + hd.token.len = 0; + break; + } + default: { + if (LIKELY(config_.validate_address)) { + SendRetry(version, dcid, scid, local_address, remote_address); + return BaseObjectPtr(); + } + hd.token.base = nullptr; + hd.token.len = 0; + } + } + set_validated_address(remote_address); + config.token = hd.token; + } + break; + case NGTCP2_PKT_0RTT: + SendRetry(version, dcid, scid, local_address, remote_address); + return BaseObjectPtr(); + } + } + + if (ocid && config_.qlog) + config.EnableQLog(ocid); + + // Iterate through the available listeners, if any. If a listener + // accepts the packet, that listener will be moved to the end of + // the list so that another listener has the option of picking + // up the next one. + { + Lock lock(this); + for (auto it = listeners_.begin(); it != listeners_.end(); it++) { + InitialPacketListener* listener = *it; + if (listener->Accept( + config, + store, + nread, + local_address, + remote_address)) { + listeners_.erase(it); + listeners_.emplace_back(listener); + return true; + } + } + } + + return false; +} + +void Endpoint::AssociateCID(const CID& cid, PacketListener* listener) { + sessions_[cid] = listener; + int err = StartReceiving(); + if (err && err != UV_EALREADY) + Close(CloseListener::Context::LISTEN_FAILURE, err); +} + +void Endpoint::DisassociateCID(const CID& cid) { + sessions_.erase(cid); + MaybeStopReceiving(); +} + +void Endpoint::AddInitialPacketListener(InitialPacketListener* listener) { + listeners_.emplace_back(listener); + int err = StartReceiving(); + if (err && err != UV_EALREADY) + Close(CloseListener::Context::LISTEN_FAILURE, err); +} + +void Endpoint::ImmediateConnectionClose( + const quic_version version, + const CID& scid, + const CID& dcid, + const std::shared_ptr& local_address, + const std::shared_ptr& remote_address, + error_code reason) { + std::unique_ptr packet = + std::make_unique("immediate connection close"); + + ssize_t nwrite = ngtcp2_crypto_write_connection_close( + packet->data(), + packet->length(), + version, + scid.cid(), + dcid.cid(), + reason); + if (nwrite <= 0) return; + packet->set_length(static_cast(nwrite)); + SendPacket(remote_address, std::move(packet)); +} + +void Endpoint::RemoveInitialPacketListener( + InitialPacketListener* listener) { + auto it = std::find(listeners_.begin(), listeners_.end(), listener); + if (it != listeners_.end()) + listeners_.erase(it); + MaybeStopReceiving(); +} + +Endpoint::PacketListener* Endpoint::FindSession(const CID& cid) { + auto session_it = sessions_.find(cid); + if (session_it != std::end(sessions_)) + return session_it->second; + return nullptr; +} + +void Endpoint::DisassociateStatelessResetToken( + const StatelessResetToken& token) { + token_map_.erase(token); +} + +void Endpoint::AssociateStatelessResetToken( + const StatelessResetToken& token, + PacketListener* listener) { + token_map_[token] = listener; +} + +int Endpoint::MaybeBind() { + if (bound_) return 0; + bound_ = true; + return udp_.Bind(config_); +} + +bool Endpoint::MaybeStatelessReset( + const CID& dcid, + const CID& scid, + std::shared_ptr store, + size_t nread, + const std::shared_ptr& local_address, + const std::shared_ptr& remote_address) { + if (UNLIKELY(config_.disable_stateless_reset) || + nread < NGTCP2_STATELESS_RESET_TOKENLEN) { + return false; + } + uint8_t* ptr = static_cast(store->Data()); + ptr += nread; + ptr -= NGTCP2_STATELESS_RESET_TOKENLEN; + StatelessResetToken possible_token(ptr); + Lock lock(this); + auto it = token_map_.find(possible_token); + if (it == token_map_.end()) + return false; + return it->second->Receive( + dcid, + scid, + std::move(store), + nread, + local_address, + remote_address, + PacketListener::Flags::STATELESS_RESET); +} + +uv_buf_t Endpoint::OnAlloc(size_t suggested_size) { + return AllocatedBuffer::AllocateManaged(env(), suggested_size).release(); +} + +void Endpoint::OnReceive( + size_t nread, + const uv_buf_t& buf, + const std::shared_ptr& remote_address) { + AllocatedBuffer buffer(env(), buf); + // When diagnostic packet loss is enabled, the packet will be randomly + // dropped based on the rx_loss probability. + if (UNLIKELY(is_diagnostic_packet_loss(config_.rx_loss))) + return; + + // TODO(@jasnell): Implement blocklist support + // if (UNLIKELY(block_list_->Apply(remote_address))) { + // Debug(this, "Ignoring blocked remote address: %s", remote_address); + // return; + // } + + IncrementStat(&EndpointStats::bytes_received, nread); + + // If the bytes read is less than the allocated buffer size, + // we need to compact it back down + std::shared_ptr store = buffer.ReleaseBackingStore(); + + if (UNLIKELY(!store)) { + ProcessReceiveFailure(UV_ENOMEM); + return; + } + + const uint8_t* data = reinterpret_cast(store->Data()); + + CHECK_LE(nread, store->ByteLength()); + + quic_version pversion; + const uint8_t* pdcid; + size_t pdcidlen; + const uint8_t* pscid; + size_t pscidlen; + + // This is our first check to see if the received data can be + // processed as a QUIC packet. If this fails, then the QUIC packet + // header is invalid and cannot be processed; all we can do is ignore + // it. If it succeeds, we have a valid QUIC header but there is still + // no guarantee that the packet can be successfully processed. + if (ngtcp2_pkt_decode_version_cid( + &pversion, + &pdcid, + &pdcidlen, + &pscid, + &pscidlen, + data, + nread, + NGTCP2_MAX_CIDLEN) < 0) { + return; // Ignore the packet! + } + + // QUIC currently requires CID lengths of max NGTCP2_MAX_CIDLEN. The + // ngtcp2 API allows non-standard lengths, and we may want to allow + // non-standard lengths later. But for now, we're going to ignore any + // packet with a non-standard CID length. + if (pdcidlen > NGTCP2_MAX_CIDLEN || pscidlen > NGTCP2_MAX_CIDLEN) + return; // Ignore the packet! + + CID dcid(pdcid, pdcidlen); + CID scid(pscid, pscidlen); + + PacketListener* listener = nullptr; + { + Lock lock(this); + listener = FindSession(dcid); + } + + // If a session is not found, there are four possible reasons: + // 1. The session has not been created yet + // 2. The session existed once but we've lost the local state for it + // 3. The packet is a stateless reset sent by the peer + // 4. This is a malicious or malformed packet. + if (listener == nullptr) { + bool is_short_header = IsShortHeader(pversion, scid); + + // Handle possible reception of a stateless reset token... + // If it is a stateless reset, the packet will be handled with + // no additional action necessary here. We want to return immediately + // without committing any further resources. + if (is_short_header && + MaybeStatelessReset( + dcid, + scid, + store, + nread, + local_address(), + remote_address)) { + return; // Ignore the packet! + } + + if (AcceptInitialPacket( + pversion, + dcid, + scid, + store, + nread, + local_address(), + remote_address)) { + return IncrementStat(&EndpointStats::packets_received); + } + + // There are many reasons why a server session could not be + // created. The most common will be invalid packets or incorrect + // QUIC version. In any of these cases, however, to prevent a + // potential attacker from causing us to consume resources, + // we're just going to ignore the packet. It is possible that + // the AcceptInitialPacket sent a version negotiation packet, + // or a CONNECTION_CLOSE packet. + + // If the packet contained a short header, we might need to send + // a stateless reset. The stateless reset contains a token derived + // from the received destination connection ID. + // + // Stateless resets are generated programmatically using HKDF with + // the sender provided dcid and a locally provided secret as input. + // It is entirely possible that a malicious peer could send multiple + // stateless reset eliciting packets with the specific intent of using + // the returned stateless reset to guess the stateless reset token + // secret used by the server. Once guessed, the malicious peer could use + // that secret as a DOS vector against other peers. We currently + // implement some mitigations for this by limiting the number + // of stateless resets that can be sent to a specific remote + // address but there are other possible mitigations, such as + // including the remote address as input in the generation of + // the stateless token. + if (is_short_header && + SendStatelessReset(dcid, local_address(), remote_address, nread)) { + return IncrementStat(&EndpointStats::stateless_reset_count); + } + return; // Ignore the packet! + } + + if (listener->Receive( + dcid, + scid, + std::move(store), + nread, + local_address(), + remote_address)) { + IncrementStat(&EndpointStats::packets_received); + } +} + +void Endpoint::ProcessOutbound() { + SendWrap::Queue queue; + { + Lock lock(this); + outbound_.swap(queue); + } + + int err = 0; + while (!queue.empty()) { + auto& packet = queue.front(); + queue.pop_front(); + err = udp_.SendPacket(packet); + if (err) { + packet->Done(err); + break; + } + } + + // If there was a fatal error sending, the Endpoint + // will be destroyed along with all associated sessions. + // Go ahead and cancel the remaining pending sends. + if (err) { + while (!queue.empty()) { + auto& packet = queue.front(); + queue.pop_front(); + packet->Done(UV_ECANCELED); + } + ProcessSendFailure(err); + } +} + +void Endpoint::ProcessSendFailure(int status) { + Close(CloseListener::Context::SEND_FAILURE, status); +} + +void Endpoint::Ref() { + ref_count_++; + udp_.Ref(); +} + +bool Endpoint::SendPacket( + const std::shared_ptr& remote_address, + std::unique_ptr packet) { + BaseObjectPtr wrap( + SendWrap::Create( + env(), + remote_address, + std::move(packet))); + if (!wrap) return false; + SendPacket(std::move(wrap)); + return true; +} + +void Endpoint::SendPacket(BaseObjectPtr packet) { + { + Lock lock(this); + outbound_.emplace_back(std::move(packet)); + } + IncrementStat(&EndpointStats::bytes_sent, packet->packet()->length()); + IncrementStat(&EndpointStats::packets_sent); + outbound_signal_.Send(); +} + +bool Endpoint::SendRetry( + const quic_version version, + const CID& dcid, + const CID& scid, + const std::shared_ptr& local_address, + const std::shared_ptr& remote_address) { + CHECK(remote_address); + auto info = addrLRU_.Upsert(*remote_address.get()); + if (++(info->retry_count) > config_.retry_limit) + return true; + std::unique_ptr packet = + GenerateRetryPacket( + version, + token_secret_, + dcid, + scid, + local_address, + remote_address, + token_aead_, + token_md_); + if (UNLIKELY(!packet)) return false; + return SendPacket(remote_address, std::move(packet)); +} + +bool Endpoint::SendStatelessReset( + const CID& cid, + const std::shared_ptr& local_address, + const std::shared_ptr& remote_address, + size_t source_len) { + if (UNLIKELY(config_.disable_stateless_reset)) + return false; + constexpr static size_t kRandlen = NGTCP2_MIN_STATELESS_RESET_RANDLEN * 5; + constexpr static size_t kMinStatelessResetLen = 41; + uint8_t random[kRandlen]; + + // Per the QUIC spec, we need to protect against sending too + // many stateless reset tokens to an endpoint to prevent + // endless looping. + if (current_stateless_reset_count(remote_address) >= + config_.max_stateless_resets) { + return false; + } + // Per the QUIC spec, a stateless reset token must be strictly + // smaller than the packet that triggered it. This is one of the + // mechanisms to prevent infinite looping exchange of stateless + // tokens with the peer. + // An endpoint should never send a stateless reset token smaller than + // 41 bytes per the QUIC spec. The reason is that packets less than + // 41 bytes may allow an observer to determine that it's a stateless + // reset. + size_t pktlen = source_len - 1; + if (pktlen < kMinStatelessResetLen) + return false; + + StatelessResetToken token(config_.reset_token_secret, cid); + crypto::EntropySource(random, kRandlen); + + std::unique_ptr packet = + std::make_unique(pktlen, "stateless reset"); + ssize_t nwrite = + ngtcp2_pkt_write_stateless_reset( + packet->data(), + NGTCP2_MAX_PKTLEN_IPV4, + const_cast(token.data()), + random, + kRandlen); + if (nwrite >= static_cast(kMinStatelessResetLen)) { + packet->set_length(nwrite); + IncrementStatelessResetCounter(remote_address); + return SendPacket(remote_address, std::move(packet)); + } + return false; +} + +Maybe Endpoint::GenerateNewToken( + uint8_t* token, + const std::shared_ptr& remote_address) { + size_t tokenlen = kMaxTokenLen; + if (!GenerateToken( + token, + &tokenlen, + remote_address, + token_secret_, + token_aead_, + token_md_)) { + return Nothing(); + } + return Just(tokenlen); +} + +uint32_t Endpoint::GetFlowLabel( + const std::shared_ptr& local_address, + const std::shared_ptr& remote_address, + const CID& cid) { + return GenerateFlowLabel( + local_address, + remote_address, + cid, + token_secret_, + NGTCP2_STATELESS_RESET_TOKENLEN); +} + +void Endpoint::SendVersionNegotiation( + const quic_version version, + const CID& dcid, + const CID& scid, + const std::shared_ptr& local_address, + const std::shared_ptr& remote_address) { + uint32_t sv[2]; + sv[0] = GenerateReservedVersion(remote_address, version); + sv[1] = NGTCP2_PROTO_VER_MAX; + + uint8_t unused_random; + crypto::EntropySource(&unused_random, 1); + + size_t pktlen = dcid.length() + scid.length() + (sizeof(sv)) + 7; + + std::unique_ptr packet = + std::make_unique(pktlen, "version negotiation"); + ssize_t nwrite = ngtcp2_pkt_write_version_negotiation( + packet->data(), + NGTCP2_MAX_PKTLEN_IPV6, + unused_random, + dcid.data(), + dcid.length(), + scid.data(), + scid.length(), + sv, + arraysize(sv)); + if (nwrite > 0) { + packet->set_length(nwrite); + SendPacket(remote_address, std::move(packet)); + } +} + +int Endpoint::StartReceiving() { + if (receiving_) return UV_EALREADY; + receiving_ = true; + int err = MaybeBind(); + if (err) return err; + return udp_.StartReceiving(); +} + +void Endpoint::MaybeStopReceiving() { + if (!sessions_.empty() || !listeners_.empty()) + return; + receiving_ = false; + udp_.StopReceiving(); +} + +void Endpoint::Unref() { + ref_count_--; + + // Only Unref if the ref_count_ actually falls below + if (!ref_count_) udp_.Unref(); +} + +bool Endpoint::is_diagnostic_packet_loss(double prob) const { + if (LIKELY(prob == 0.0)) return false; + unsigned char c = 255; + crypto::EntropySource(&c, 1); + return (static_cast(c) / 255) < prob; +} + +void Endpoint::set_validated_address( + const std::shared_ptr& addr) { + CHECK(addr); + addrLRU_.Upsert(*(addr.get()))->validated = true; +} + +bool Endpoint::is_validated_address( + const std::shared_ptr& addr) const { + CHECK(addr); + auto info = addrLRU_.Peek(*(addr.get())); + return info != nullptr ? info->validated : false; +} + +void Endpoint::IncrementStatelessResetCounter( + const std::shared_ptr& addr) { + CHECK(addr); + addrLRU_.Upsert(*(addr.get()))->reset_count++; +} + +void Endpoint::IncrementSocketAddressCounter( + const std::shared_ptr& addr) { + CHECK(addr); + addrLRU_.Upsert(*(addr.get()))->active_connections++; +} + +void Endpoint::DecrementSocketAddressCounter( + const std::shared_ptr& addr) { + CHECK(addr); + SocketAddressInfoTraits::Type* counts = addrLRU_.Peek(*(addr.get())); + if (counts != nullptr && counts->active_connections > 0) + counts->active_connections--; +} + +size_t Endpoint::current_socket_address_count( + const std::shared_ptr& addr) const { + CHECK(addr); + SocketAddressInfoTraits::Type* counts = addrLRU_.Peek(*(addr.get())); + return counts != nullptr ? counts->active_connections : 0; +} + +size_t Endpoint::current_stateless_reset_count( + const std::shared_ptr& addr) const { + CHECK(addr); + SocketAddressInfoTraits::Type* counts = addrLRU_.Peek(*(addr.get())); + return counts != nullptr ? counts->reset_count : 0; +} + +std::shared_ptr Endpoint::local_address() const { + return udp_.local_address(); +} + +Local Endpoint::UDP::GetConstructorTemplate( + Environment* env) { + BindingState* state = env->GetBindingData(env->context()); + Local tmpl = state->udp_constructor_template(); + if (tmpl.IsEmpty()) { + tmpl = FunctionTemplate::New(env->isolate()); + tmpl->Inherit(HandleWrap::GetConstructorTemplate(env)); + tmpl->InstanceTemplate()->SetInternalFieldCount( + HandleWrap::kInternalFieldCount); + tmpl->SetClassName( + FIXED_ONE_BYTE_STRING(env->isolate(), "Session::UDP")); + state->set_udp_constructor_template(tmpl); + } + return tmpl; +} + +Endpoint::UDP* Endpoint::UDP::Create( + Environment* env, + Endpoint* endpoint) { + Local obj; + if (UNLIKELY(!GetConstructorTemplate(env) + ->InstanceTemplate() + ->NewInstance(env->context()).ToLocal(&obj))) { + return nullptr; + } + + return new Endpoint::UDP(env, obj, endpoint); +} + +Endpoint::UDP::UDP( + Environment* env, + Local obj, + Endpoint* endpoint) + : HandleWrap( + env, + obj, + reinterpret_cast(&handle_), + AsyncWrap::PROVIDER_QUICENDPOINT), + endpoint_(endpoint) { + CHECK_EQ(uv_udp_init(env->event_loop(), &handle_), 0); + handle_.data = this; +} + +std::shared_ptr Endpoint::UDP::local_address() const { + return SocketAddress::FromSockName(handle_); +} + +int Endpoint::UDP::Bind(const Endpoint::Config& config) { + int flags = 0; + if (config.local_address->family() == AF_INET6 && config.ipv6_only) + flags |= UV_UDP_IPV6ONLY; + + int err = uv_udp_bind( + &handle_, + config.local_address->data(), flags); + int size; + + if (!err) { + size = static_cast(config.udp_receive_buffer_size); + if (size > 0) { + err = uv_recv_buffer_size( + reinterpret_cast(&handle_), + &size); + if (err) return err; + } + + size = static_cast(config.udp_send_buffer_size); + if (size > 0) { + err = uv_send_buffer_size( + reinterpret_cast(&handle_), + &size); + if (err) return err; + } + + size = static_cast(config.udp_ttl); + if (size > 0) { + err = uv_udp_set_ttl(&handle_, size); + if (err) return err; + } + } + + return err; +} + +void Endpoint::UDP::Close() { + if (is_closing()) return; + env()->CloseHandle(reinterpret_cast(&handle_), ClosedCb); +} + +void Endpoint::UDP::ClosedCb(uv_handle_t* handle) { + std::unique_ptr ptr( + ContainerOf(&Endpoint::UDP::handle_, + reinterpret_cast(handle))); +} + +void Endpoint::UDP::Ref() { + uv_ref(reinterpret_cast(&handle_)); +} + +void Endpoint::UDP::Unref() { + uv_unref(reinterpret_cast(&handle_)); +} + +int Endpoint::UDP::StartReceiving() { + if (IsHandleClosing()) return UV_EBADF; + int err = uv_udp_recv_start(&handle_, OnAlloc, OnReceive); + if (err == UV_EALREADY) + err = 0; + return err; +} + +void Endpoint::UDP::StopReceiving() { + if (!IsHandleClosing()) + USE(uv_udp_recv_stop(&handle_)); +} + +int Endpoint::UDP::SendPacket(BaseObjectPtr req) { + CHECK(req); + // Attach a strong pointer to the UDP instance to + // ensure that it is not freed until all of the + // dispatched SendWraps are freed. + req->Attach(BaseObjectPtr(this)); + uv_buf_t buf = req->packet()->buf(); + const sockaddr* dest = req->destination()->data(); + return req->Dispatch( + uv_udp_send, + &handle_, + &buf, 1, + dest, + uv_udp_send_cb{[](uv_udp_send_t* req, int status) { + std::unique_ptr ptr( + static_cast(UdpSendWrap::from_req(req))); + ptr->Done(status); + }}); +} + +void Endpoint::UDP::OnAlloc( + uv_handle_t* handle, + size_t suggested_size, + uv_buf_t* buf) { + UDP* udp = + ContainerOf( + &Endpoint::UDP::handle_, + reinterpret_cast(handle)); + *buf = udp->endpoint_->OnAlloc(suggested_size); +} + +void Endpoint::UDP::OnReceive( + uv_udp_t* handle, + ssize_t nread, + const uv_buf_t* buf, + const sockaddr* addr, + unsigned int flags) { + UDP* udp = ContainerOf(&Endpoint::UDP::handle_, handle); + if (nread < 0) { + udp->endpoint_->ProcessReceiveFailure(static_cast(nread)); + return; + } + + // Nothing to do it in this case. + if (nread == 0) return; + CHECK_NOT_NULL(addr); + + if (UNLIKELY(flags & UV_UDP_PARTIAL)) { + udp->endpoint_->ProcessReceiveFailure(UV_ENOBUFS); + return; + } + + udp->endpoint_->OnReceive( + static_cast(nread), + *buf, + std::make_shared(addr)); +} + +Endpoint::UDPHandle::UDPHandle(Environment* env, Endpoint* endpoint) + : env_(env), + udp_(Endpoint::UDP::Create(env, endpoint)) { + CHECK_NOT_NULL(udp_); + env->AddCleanupHook(CleanupHook, this); +} + +void Endpoint::UDPHandle::Close() { + if (udp_ != nullptr) { + env_->RemoveCleanupHook(CleanupHook, this); + udp_->Close(); + } + udp_ = nullptr; +} + +void Endpoint::UDPHandle::MemoryInfo(MemoryTracker* tracker) const { + if (udp_) + tracker->TrackField("udp", udp_); +} + +void Endpoint::UDPHandle::CleanupHook(void* data) { + static_cast(data)->Close(); +} + +bool EndpointWrap::HasInstance(Environment* env, const Local& value) { + return GetConstructorTemplate(env)->HasInstance(value); +} + +Local EndpointWrap::GetConstructorTemplate( + Environment* env) { + BindingState* state = env->GetBindingData(env->context()); + Local tmpl = state->endpoint_constructor_template(); + if (tmpl.IsEmpty()) { + tmpl = env->NewFunctionTemplate(IllegalConstructor); + tmpl->Inherit(AsyncWrap::GetConstructorTemplate(env)); + tmpl->SetClassName(FIXED_ONE_BYTE_STRING(env->isolate(), "Endpoint")); + tmpl->InstanceTemplate()->SetInternalFieldCount( + EndpointWrap::kInternalFieldCount); + env->SetProtoMethod( + tmpl, + "listen", + StartListen); + env->SetProtoMethod( + tmpl, + "waitForPendingCallbacks", + StartWaitForPendingCallbacks); + env->SetProtoMethod( + tmpl, + "createClientSession", + CreateClientSession); + env->SetProtoMethodNoSideEffect( + tmpl, + "address", + LocalAddress); + env->SetProtoMethod(tmpl, "ref", Ref); + env->SetProtoMethod(tmpl, "unref", Unref); + state->set_endpoint_constructor_template(tmpl); + } + return tmpl; +} + +void EndpointWrap::Initialize(Environment* env, Local target) { + env->SetMethod(target, "createEndpoint", CreateEndpoint); + + ConfigObject::Initialize(env, target); + +#define V(name, _, __) NODE_DEFINE_CONSTANT(target, IDX_STATS_ENDPOINT_##name); + ENDPOINT_STATS(V) + NODE_DEFINE_CONSTANT(target, IDX_STATS_ENDPOINT_COUNT); +#undef V +#define V(name, _, __) NODE_DEFINE_CONSTANT(target, IDX_STATE_ENDPOINT_##name); + ENDPOINT_STATE(V) + NODE_DEFINE_CONSTANT(target, IDX_STATE_ENDPOINT_COUNT); +#undef V +} + +void EndpointWrap::CreateClientSession( + const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + EndpointWrap* endpoint; + ASSIGN_OR_RETURN_UNWRAP(&endpoint, args.Holder()); + + CHECK(SocketAddressBase::HasInstance(env, args[0])); + CHECK(OptionsObject::HasInstance(env, args[1])); + CHECK(crypto::SecureContext::HasInstance(env, args[2])); + + SocketAddressBase* address; + OptionsObject* options; + crypto::SecureContext* context; + + ASSIGN_OR_RETURN_UNWRAP(&address, args[0]); + ASSIGN_OR_RETURN_UNWRAP(&options, args[1]); + ASSIGN_OR_RETURN_UNWRAP(&context, args[2]); + + Session::Config config( + endpoint->inner_.get(), + NGTCP2_PROTO_VER_MAX); + + BaseObjectPtr session = + Session::CreateClient( + endpoint, + endpoint->local_address(), + address->address(), + config, + options->options(), + BaseObjectPtr(context)); + + if (UNLIKELY(!session)) { + return THROW_ERR_QUIC_INTERNAL_ERROR( + env, "Failure to create new client session"); + } + + args.GetReturnValue().Set(session->object()); +} + +void EndpointWrap::CreateEndpoint(const FunctionCallbackInfo& args) { + CHECK(!args.IsConstructCall()); + Environment* env = Environment::GetCurrent(args); + CHECK(ConfigObject::HasInstance(env, args[0])); + ConfigObject* config; + ASSIGN_OR_RETURN_UNWRAP(&config, args[0]); + + BaseObjectPtr endpoint = Create(env, config->config()); + if (LIKELY(endpoint)) + args.GetReturnValue().Set(endpoint->object()); +} + +void EndpointWrap::StartListen(const FunctionCallbackInfo& args) { + EndpointWrap* endpoint; + ASSIGN_OR_RETURN_UNWRAP(&endpoint, args.Holder()); + Environment* env = Environment::GetCurrent(args); + CHECK(OptionsObject::HasInstance(env, args[0])); + OptionsObject* options; + ASSIGN_OR_RETURN_UNWRAP(&options, args[0].As()); + CHECK(crypto::SecureContext::HasInstance(env, args[1])); + crypto::SecureContext* context; + ASSIGN_OR_RETURN_UNWRAP(&context, args[1]); + endpoint->Listen( + options->options(), + BaseObjectPtr(context)); +} + +void EndpointWrap::StartWaitForPendingCallbacks( + const FunctionCallbackInfo& args) { + EndpointWrap* endpoint; + ASSIGN_OR_RETURN_UNWRAP(&endpoint, args.Holder()); + endpoint->WaitForPendingCallbacks(); +} + +Maybe EndpointWrap::GenerateNewToken( + uint8_t* token, + const std::shared_ptr& remote_address) { + return inner_->GenerateNewToken(token, remote_address); +} + +void EndpointWrap::LocalAddress(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + EndpointWrap* endpoint; + ASSIGN_OR_RETURN_UNWRAP(&endpoint, args.Holder()); + BaseObjectPtr addr; + std::shared_ptr address = endpoint->inner_->local_address(); + if (address) + addr = SocketAddressBase::Create(env, address); + if (addr) + args.GetReturnValue().Set(addr->object()); +} + +void EndpointWrap::Ref(const FunctionCallbackInfo& args) { + EndpointWrap* wrap; + ASSIGN_OR_RETURN_UNWRAP(&wrap, args.Holder()); + Endpoint::Lock lock(wrap->inner_); + wrap->inner_->Ref(); +} + +void EndpointWrap::Unref(const FunctionCallbackInfo& args) { + EndpointWrap* wrap; + ASSIGN_OR_RETURN_UNWRAP(&wrap, args.Holder()); + Endpoint::Lock lock(wrap->inner_); + wrap->inner_->Unref(); +} + +BaseObjectPtr EndpointWrap::Create( + Environment* env, + const Endpoint::Config& config) { + Local obj; + if (UNLIKELY(!GetConstructorTemplate(env) + ->InstanceTemplate() + ->NewInstance(env->context()).ToLocal(&obj))) { + return BaseObjectPtr(); + } + + return MakeBaseObject(env, obj, config); +} + +BaseObjectPtr EndpointWrap::Create( + Environment* env, + std::shared_ptr endpoint) { + Local obj; + if (UNLIKELY(!GetConstructorTemplate(env) + ->InstanceTemplate() + ->NewInstance(env->context()).ToLocal(&obj))) { + return BaseObjectPtr(); + } + + return MakeBaseObject(env, obj, std::move(endpoint)); +} + +EndpointWrap::EndpointWrap( + Environment* env, + Local object, + const Endpoint::Config& config) + : EndpointWrap( + env, + object, + std::make_shared(env, config)) {} + +EndpointWrap::EndpointWrap( + Environment* env, + v8::Local object, + std::shared_ptr inner) + : AsyncWrap(env, object, AsyncWrap::PROVIDER_QUICENDPOINT), + state_(env), + inner_(std::move(inner)), + close_signal_(env, [this]() { Close(); }), + inbound_signal_(env, [this]() { ProcessInbound(); }), + initial_signal_(env, [this]() { ProcessInitial(); }) { + MakeWeak(); + + Debug(this, "New QUIC endpoint created"); + + { + Endpoint::Lock lock(inner_); + inner_->AddCloseListener(this); + } + + object->DefineOwnProperty( + env->context(), + env->state_string(), + state_.GetArrayBuffer(), + PropertyAttribute::ReadOnly).Check(); + + object->DefineOwnProperty( + env->context(), + env->stats_string(), + inner_->ToBigUint64Array(env), + PropertyAttribute::ReadOnly).Check(); +} + +EndpointWrap::~EndpointWrap() { + CHECK(sessions_.empty()); + Debug(this, "Destroying"); + if (inner_) { + Endpoint::Lock lock(inner_); + inner_->RemoveCloseListener(this); + inner_->RemoveInitialPacketListener(this); + inner_->DebugStats(this); + } + + close_signal_.Close(); + inbound_signal_.Close(); + initial_signal_.Close(); +} + +void EndpointWrap::EndpointClosed( + Endpoint::CloseListener::Context context, + int status) { + close_context_ = context; + close_status_ = status; + close_signal_.Send(); +} + +void EndpointWrap::Close() { + MakeWeak(); + state_->listening = 0; + HandleScope scope(env()->isolate()); + v8::Context::Scope context_scope(env()->context()); + BindingState* state = env()->GetBindingData(env()->context()); + // If the Environment is being torn down, then there's nothing more we can do. + if (state == nullptr || !env()->can_call_into_js()) + return; + Local argv[] = { + Integer::NewFromUnsigned( + env()->isolate(), + static_cast(close_context_)), + Integer::New(env()->isolate(), close_status_) + }; + BaseObjectPtr ptr(this); + MakeCallback(state->endpoint_close_callback(), arraysize(argv), argv); +} + +void EndpointWrap::MemoryInfo(MemoryTracker* tracker) const { + tracker->TrackField("endpoint", inner_); + tracker->TrackField("sessions", sessions_); +} + +void EndpointWrap::AddSession( + const CID& cid, + const BaseObjectPtr& session) { + sessions_[cid] = session; + Endpoint::Lock lock(inner_); + inner_->AssociateCID(cid, this); + inner_->IncrementSocketAddressCounter(session->remote_address()); + inner_->IncrementStat( + session->is_server() + ? &EndpointStats::server_sessions + : &EndpointStats::client_sessions); + ClearWeak(); +} + +void EndpointWrap::AssociateCID(const CID& cid, const CID& scid) { + if (LIKELY(cid && scid)) { + Debug(this, "Associating cid %s with %s", cid, scid); + dcid_to_scid_[cid] = scid; + Endpoint::Lock lock(inner_); + inner_->AssociateCID(cid, this); + } +} + +void EndpointWrap::AssociateStatelessResetToken( + const StatelessResetToken& token, + const BaseObjectPtr& session) { + Debug(this, "Associating stateless reset token %s", token); + token_map_[token] = session; + Endpoint::Lock lock(inner_); + inner_->AssociateStatelessResetToken(token, this); +} + +void EndpointWrap::DisassociateCID(const CID& cid) { + if (LIKELY(cid)) { + Debug(this, "Removing association for cid %s", cid); + dcid_to_scid_.erase(cid); + Endpoint::Lock lock(inner_); + inner_->DisassociateCID(cid); + } +} + +void EndpointWrap::DisassociateStatelessResetToken( + const StatelessResetToken& token) { + Debug(this, "Removing stateless reset token %s", token); + Endpoint::Lock lock(inner_); + inner_->DisassociateStatelessResetToken(token); +} + +BaseObjectPtr EndpointWrap::FindSession(const CID& cid) { + BaseObjectPtr session; + auto session_it = sessions_.find(cid); + if (session_it == std::end(sessions_)) { + auto scid_it = dcid_to_scid_.find(cid); + if (scid_it != std::end(dcid_to_scid_)) { + session_it = sessions_.find(scid_it->second); + CHECK_NE(session_it, std::end(sessions_)); + session = session_it->second; + } + } else { + session = session_it->second; + } + return session; +} + +uint32_t EndpointWrap::GetFlowLabel( + const std::shared_ptr& local_address, + const std::shared_ptr& remote_address, + const CID& cid) { + return inner_->GetFlowLabel(local_address, remote_address, cid); +} + +void EndpointWrap::ImmediateConnectionClose( + const quic_version version, + const CID& scid, + const CID& dcid, + const std::shared_ptr& local_address, + const std::shared_ptr& remote_address, + const error_code reason) { + Debug(this, "Sending stateless connection close to %s", scid); + inner_->ImmediateConnectionClose( + version, + scid, + dcid, + local_address, + remote_address, + reason); +} + +void EndpointWrap::Listen( + const std::shared_ptr& options, + const BaseObjectPtr& context) { + if (state_->listening == 1) return; + CHECK(context); + Debug(this, "Starting to listen"); + server_options_ = options; + server_context_ = context; + state_->listening = 1; + Endpoint::Lock lock(inner_); + inner_->AddInitialPacketListener(this); + // While listening, this shouldn't be weak + this->ClearWeak(); +} + +void EndpointWrap::OnEndpointDone() { + MakeWeak(); + HandleScope scope(env()->isolate()); + v8::Context::Scope context_scope(env()->context()); + BindingState* state = env()->GetBindingData(env()->context()); + BaseObjectPtr ptr(this); + MakeCallback(state->endpoint_done_callback(), 0, nullptr); +} + +void EndpointWrap::OnError(Local error) { + MakeWeak(); + BindingState* state = env()->GetBindingData(env()->context()); + HandleScope scope(env()->isolate()); + v8::Context::Scope context_scope(env()->context()); + + if (UNLIKELY(error.IsEmpty())) + error = ERR_QUIC_INTERNAL_ERROR(env()->isolate(), "OnError failure"); + + BaseObjectPtr ptr(this); + MakeCallback(state->endpoint_error_callback(), 1, &error); +} + +void EndpointWrap::OnNewSession(const BaseObjectPtr& session) { + BindingState* state = env()->GetBindingData(env()->context()); + Local arg = session->object(); + v8::Context::Scope context_scope(env()->context()); + MakeCallback(state->session_new_callback(), 1, &arg); +} + +void EndpointWrap::OnSendDone(int status) { + DecrementPendingCallbacks(); + if (is_done_waiting_for_callbacks()) + OnEndpointDone(); +} + +bool EndpointWrap::Accept( + const Session::Config& config, + std::shared_ptr store, + size_t nread, + const std::shared_ptr& local_address, + const std::shared_ptr& remote_address) { + + { + Mutex::ScopedLock lock(inbound_mutex_); + initial_.emplace_back(InitialPacket { + config, + std::move(store), + nread, + local_address, + remote_address + }); + } + initial_signal_.Send(); + return true; +} + +void EndpointWrap::ProcessInitial() { + InitialPacket::Queue queue; + { + Mutex::ScopedLock lock(inbound_mutex_); + initial_.swap(queue); + } + + while (!queue.empty()) { + InitialPacket packet = queue.front(); + queue.pop_front(); + + BaseObjectPtr session = + Session::CreateServer( + this, + packet.local_address, + packet.remote_address, + packet.config, + server_options_, + server_context_); + + if (UNLIKELY(!session)) + return ProcessInitialFailure(); + + session->Receive( + packet.nread, + std::move(packet.store), + packet.local_address, + packet.remote_address); + } +} + +void EndpointWrap::ProcessInitialFailure() { + OnError(ERR_QUIC_ENDPOINT_INITIAL_PACKET_FAILURE(env()->isolate())); +} + +bool EndpointWrap::Receive( + const CID& dcid, + const CID& scid, + std::shared_ptr store, + size_t nread, + const std::shared_ptr& local_address, + const std::shared_ptr& remote_address, + const Endpoint::PacketListener::Flags flags) { + { + Mutex::ScopedLock lock(inbound_mutex_); + inbound_.emplace_back(InboundPacket{ + dcid, + scid, + std::move(store), + nread, + local_address, + remote_address, + flags + }); + } + inbound_signal_.Send(); + return true; +} + +void EndpointWrap::ProcessInbound() { + InboundPacket::Queue queue; + { + Mutex::ScopedLock lock(inbound_mutex_); + inbound_.swap(queue); + } + + while (!queue.empty()) { + InboundPacket packet = queue.front(); + queue.pop_front(); + + inner_->IncrementStat(&EndpointStats::bytes_received, packet.nread); + BaseObjectPtr session = FindSession(packet.dcid); + if (session && !session->is_destroyed()) { + session->Receive( + packet.nread, + std::move(packet.store), + packet.local_address, + packet.remote_address); + } + } +} + +void EndpointWrap::RemoveSession( + const CID& cid, + const std::shared_ptr& addr) { + sessions_.erase(cid); + Endpoint::Lock lock(inner_); + inner_->DisassociateCID(cid); + inner_->DecrementSocketAddressCounter(addr); + if (!state_->listening && sessions_.empty()) + MakeWeak(); +} + +void EndpointWrap::SendPacket( + const std::shared_ptr& local_address, + const std::shared_ptr& remote_address, + std::unique_ptr packet, + const BaseObjectPtr& session) { + if (UNLIKELY(packet->length() == 0)) + return; + + // Make certain we're in a handle scope. + HandleScope scope(env()->isolate()); + + Debug(this, "Sending %" PRIu64 " bytes to %s from %s (label: %s)", + packet->length(), + remote_address.get(), + local_address.get(), + packet->diagnostic_label()); + + BaseObjectPtr wrap = + Endpoint::SendWrap::Create( + env(), + remote_address, + std::move(packet), + BaseObjectPtr(this)); + if (UNLIKELY(!wrap)) + return OnError(ERR_QUIC_ENDPOINT_SEND_FAILURE(env()->isolate())); + + IncrementPendingCallbacks(); + inner_->SendPacket(std::move(wrap)); +} + +bool EndpointWrap::SendRetry( + const quic_version version, + const CID& dcid, + const CID& scid, + const std::shared_ptr& local_address, + const std::shared_ptr& remote_address) { + return inner_->SendRetry(version, dcid, scid, local_address, remote_address); +} + +void EndpointWrap::WaitForPendingCallbacks() { + // If this EndpointWrap is listening for incoming initial packets, + // unregister the listener now so that the Endpoint does not try + // to forward on new initial packets while we're waiting for the + // existing writes to clear. + inner_->RemoveInitialPacketListener(this); + state_->listening = 0; + + if (!is_done_waiting_for_callbacks()) { + OnEndpointDone(); + return; + } + state_->waiting_for_callbacks = 1; +} + +std::unique_ptr EndpointWrap::CloneForMessaging() const { + return std::make_unique(inner_); +} + +BaseObjectPtr EndpointWrap::TransferData::Deserialize( + Environment* env, + v8::Local context, + std::unique_ptr self) { + return EndpointWrap::Create(env, std::move(inner_)); +} + +void EndpointWrap::TransferData::MemoryInfo(MemoryTracker* tracker) const { + tracker->TrackField("inner", inner_); +} + +} // namespace quic +} // namespace node diff --git a/src/quic/endpoint.h b/src/quic/endpoint.h new file mode 100644 index 00000000000000..4f306ce78bfb70 --- /dev/null +++ b/src/quic/endpoint.h @@ -0,0 +1,1105 @@ +#ifndef SRC_QUIC_ENDPOINT_H_ +#define SRC_QUIC_ENDPOINT_H_ +#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS + +#include "quic/buffer.h" +#include "quic/crypto.h" +#include "quic/quic.h" +#include "quic/stats.h" +#include "quic/session.h" +#include "crypto/crypto_context.h" +#include "crypto/crypto_util.h" +#include "aliased_struct.h" +#include "async_signal.h" +#include "async_wrap.h" +#include "base_object.h" +#include "env.h" +#include "handle_wrap.h" +#include "node_sockaddr.h" +#include "node_worker.h" +#include "udp_wrap.h" + +#include +#include + +#include +#include +#include + +namespace node { +namespace quic { + +#define ENDPOINT_STATS(V) \ + V(CREATED_AT, created_at, "Created at") \ + V(DESTROYED_AT, destroyed_at, "Destroyed at") \ + V(BYTES_RECEIVED, bytes_received, "Bytes received") \ + V(BYTES_SENT, bytes_sent, "Bytes sent") \ + V(PACKETS_RECEIVED, packets_received, "Packets received") \ + V(PACKETS_SENT, packets_sent, "Packets sent") \ + V(SERVER_SESSIONS, server_sessions, "Server sessions") \ + V(CLIENT_SESSIONS, client_sessions, "Client sessions") \ + V(STATELESS_RESET_COUNT, stateless_reset_count, "Stateless reset count") \ + V(SERVER_BUSY_COUNT, server_busy_count, "Server busy count") + +#define ENDPOINT_STATE(V) \ + V(LISTENING, listening, uint8_t) \ + V(WAITING_FOR_CALLBACKS, waiting_for_callbacks, uint8_t) \ + V(PENDING_CALLBACKS, pending_callbacks, size_t) + +class Endpoint; +class EndpointWrap; + +#define V(name, _, __) IDX_STATS_ENDPOINT_##name, +enum EndpointStatsIdx { + ENDPOINT_STATS(V) + IDX_STATS_ENDPOINT_COUNT +}; +#undef V + +#define V(name, _, __) IDX_STATE_ENDPOINT_##name, +enum EndpointStateIdx { + ENDPOINT_STATE(V) + IDX_STATE_ENDPOINT_COUNT +}; +#undef V + +#define V(_, name, __) uint64_t name; +struct EndpointStats final { + ENDPOINT_STATS(V) +}; +#undef V + +using EndpointStatsBase = StatsBase>; +using UdpSendWrap = ReqWrap; + +// An Endpoint encapsulates a bound UDP port through which QUIC packets +// are sent and received. An Endpoint is created when a new EndpointWrap +// is created, and may be shared by multiple EndpointWrap instances at +// the same time. The Endpoint, and it's bound UDP port, are associated +// with the libuv event loop and Environment of the EndpointWrap that +// originally created it. All network traffic will be processed within +// the context of that owning event loop for as long as it lasts. If that +// event loop exits, the Endpoint will be closed along with all +// EndpointWrap instances holding a reference to it. +// +// For inbound packets, the Endpoint will perform a preliminary check +// to determine if the UDP packet *looks* like a valid QUIC packet, and +// will perform a handful of checks to see if the packet is acceptable. +// Assuming the packet passes those checks, the Endpoint will extract +// the destination CID from the packet header and will determine whether +// there is an EndpointWrap associated with the CID. If there is, the +// packet will be passed on the EndpointWrap to be processed further. +// If the packet is an "Initial" QUIC packet and the CID is not matched +// to an existing EndpointWrap, the Endpoint will dispatch the packet +// to the first available EndpointWrap instance that has registered +// itself as accepting initial packets ("listening"). If there are +// multiple EndpointWrap instances listening, dispatch is round-robin, +// iterating through each until one accepts the packet or all reject. +// The EndpointWrap that accepts the packet will be moved to the end +// of the list for when the next packet arrives. +// +// The Endpoint can be marked as "busy", which will stop it from accepting +// and dispatching new initial packets to any EndpointWrap, even if those +// are currently idle. +// +// For outbound packets, EndpointWrap instances prepare a SendWrap +// that encapsulates the packet to be sent and the destination address. +// Those are pushed into a queue and processed as a batch on each +// event loop turn. The packets are processed in the order they are added +// to the queue. While the outbound packets can originate from any +// worker thread or context, the actual dispatch of the packet data to +// the UDP port takes place within the context of the Endpoint's owning +// event loop. +// +// The Endpoint encapsulates the uv_udp_t handle directly (via the +// Endpoint::UDPHandle and Endpoint::UDP classes) rather than using +// the node::UDPWrap in order to provide greater control over the +// buffer allocation of inbound packets to ensure that we can safely +// implement zero-copy, thread-safe data sharing across worker +// threads. +class Endpoint final : public MemoryRetainer, + public EndpointStatsBase { + public: + // Endpoint::Config provides the fundamental configuration options for + // an Endpoint instance. The configuration property names should be + // relatively self-explanatory but additional notes are provided in + // comments where necessary. + struct Config final : public MemoryRetainer { + // The local socket address to which the UDP port will be bound. + // The port may be 0 to have Node.js select an available port. + // IPv6 or IPv4 addresses may be used. When using IPv6, dual mode + // will be supported by default. + std::shared_ptr local_address; + + // Retry tokens issued by the Endpoint are time-limited. By default, + // retry tokens expire after DEFAULT_RETRYTOKEN_EXPIRATION *seconds*. + // The retry_token_expiration parameter is always expressed in terms + // of seconds. This is an arbitrary choice that is not mandated by + // the QUIC specification; so we can choose any value that makes + // sense here. Retry tokens are sent to the client, which echoes them + // back to the server in a subsequent set of packets, which means the + // expiration must be set high enough to allow a reasonable round-trip + // time for the session TLS handshake to complete. + uint64_t retry_token_expiration = DEFAULT_RETRYTOKEN_EXPIRATION; + + // Tokens issued using NEW_TOKEN are time-limited. By default, + // tokens expire after DEFAULT_TOKEN_EXPIRATION *seconds*. + uint64_t token_expiration = DEFAULT_TOKEN_EXPIRATION; + + // The max_window_override and max_stream_window_override parameters + // determine the maximum flow control window sizes that will be used. + // Setting these at zero causes ngtcp2 to use it's defaults, which is + // ideal. Setting things to any other value will disable the automatic + // flow control management, which is already optimized. Settings these + // should be rare, and should only be done if there's a really good + // reason. + uint64_t max_window_override = 0; + uint64_t max_stream_window_override = 0; + + // Each Endpoint places limits on the number of concurrent connections + // from a single host, and the total number of concurrent connections + // allowed as a whole. These are set to fairly modest, and arbitrary + // defaults. We can set these to whatever we'd like. + uint64_t max_connections_per_host = DEFAULT_MAX_CONNECTIONS_PER_HOST; + uint64_t max_connections_total = DEFAULT_MAX_CONNECTIONS; + + // A stateless reset in QUIC is a discrete mechanism that one endpoint + // can use to communicate to a peer that it has lost whatever state + // it previously held about a session. Because generating a stateless + // reset consumes resources (even very modestly), they can be a DOS + // vector in which a malicious peer intentionally sends a large number + // of stateless reset eliciting packets. To protect against that risk, + // we limit the number of stateless resets that may be generated for + // a given remote host within a window of time. This is not mandated + // by QUIC, and the limit is arbitrary. We can set it to whatever we'd + // like. + uint64_t max_stateless_resets = DEFAULT_MAX_STATELESS_RESETS; + + // For tracking the number of connections per host, the number of + // stateless resets that have been sent, and tracking the path + // verification status of a remote host, we maintain an LRU cache + // of the most recently seen hosts. The address_lru_size parameter + // determines the size of that cache. The default is set modestly + // at 10 times the default max connections per host. + uint64_t address_lru_size = DEFAULT_MAX_SOCKETADDRESS_LRU_SIZE; + + // Similar to stateless resets, we enforce a limit on the number of + // retry packets that can be generated and sent for a remote host. + // Generating retry packets consumes a modest amount of resources + // and it's fairly trivial for a malcious peer to trigger generation + // of a large number of retries, so limiting them helps prevent a + // DOS vector. + uint64_t retry_limit = DEFAULT_MAX_RETRY_LIMIT; + + // The max_payload_size is the maximum size of a serialized QUIC + // packet. It should always be set small enough to fit within a + // single MTU without fragmentation. The default is set by the QUIC + // specification at 1200. This value should not be changed unless + // you know for sure that the entire path supports a given MTU + // without fragmenting at any point in the path. + uint64_t max_payload_size = NGTCP2_DEFAULT_MAX_PKTLEN; + + // The unacknowledged_packet_threshold is the maximum number of + // unacknowledged packets that an ngtcp2 session will accumulate + // before sending an acknowledgement. Setting this to 0 uses the + // ngtcp2 defaults, which is what most will want. The value can + // be changed to fine tune some of the performance characteristics + // of the session. This should only be changed if you have a really + // good reason for doing so. + uint64_t unacknowledged_packet_threshold = 0; + + // The qlog parameter enables the generation of detailed qlog + // debugging details for each ngtcp2 session. + bool qlog = false; + + // The validate_address parameter instructs the Endpoint to perform + // explicit address validation using retry tokens. This is strongly + // recommended and should only be disabled in trusted, closed + // environments as a performance optimization. + bool validate_address = true; + + // The stateless reset mechanism can be disabled. This should rarely + // ever be needed, and should only ever be done in trusted, closed + // environments as a performance optimization. + bool disable_stateless_reset = false; + + // The rx_loss and tx_loss parameters are debugging tools that allow + // the Endpoint to simulate random packet loss. The value for each + // parameter is a value between 0.0 and 1.0 indicating a probability + // of packet loss. Each time a packet is sent or received, the packet + // loss bit is calculated and if true, the packet is silently dropped. + // This should only ever be used for testing and debugging. There is + // never a reason why rx_loss and tx_loss should ever be used in a + // production system. + double rx_loss = 0.0; + double tx_loss = 0.0; + + // There are two common congestion control algorithms that ngtcp2 uses + // to determine how it manages the flow control window: RENO and CUBIC. + // The details of how each works is not relevant here. The choice of + // which to use by default is arbitrary and we can choose whichever we'd + // like. Additional performance profiling will be needed to determine + // which is the better of the two for our needs. + ngtcp2_cc_algo cc_algorithm = NGTCP2_CC_ALGO_CUBIC; + + // By default, when Node.js starts, it will generate a reset_token_secret + // at random. This is a secret used in generating stateless reset tokens. + // In order for stateless reset to be effective, however, it is necessary + // to use a deterministic secret that persists across ngtcp2 endpoints and + // sessions. + // TODO(@jasnell): Given that this is a secret, it may make sense to use + // the secure heap to store it (when the heap is available). Doing so + // will require some refactoring here, however, so we'll defer that to + // a future iteration. + uint8_t reset_token_secret[NGTCP2_STATELESS_RESET_TOKENLEN]; + + // When the local_address specifies an IPv6 local address to bind + // to, the ipv6_only parameter determines whether dual stack mode + // (supporting both IPv6 and IPv4) transparently is supported. + // This sets the UV_UDP_IPV6ONLY flag on the underlying uv_udp_t. + bool ipv6_only = false; + + uint32_t udp_receive_buffer_size = 0; + uint32_t udp_send_buffer_size = 0; + + // The UDP TTL configuration is the number of network hops a packet + // will be forwarded through. The default is 64. The value is in the + // range 1 to 255. Setting to 0 uses the default. + uint8_t udp_ttl = 0; + + Config(); + Config(const Config& other) noexcept; + + Config(Config&& other) = delete; + Config& operator=(Config&& other) = delete; + + inline Config& operator=(const Config& other) noexcept { + if (this == &other) return *this; + this->~Config(); + return *new(this) Config(other); + } + + inline void GenerateResetTokenSecret(); + + SET_NO_MEMORY_INFO() + SET_MEMORY_INFO_NAME(Endpoint::Config) + SET_SELF_SIZE(Config) + }; + + // The SendWrap is a persistent ReqWrap instance that encapsulates a + // QUIC Packet that is to be sent to a remote peer. They are created + // by the EndpointWrap and queued into the shared Endpoint for processing. + class SendWrap final : public UdpSendWrap { + public: + static v8::Local GetConstructorTemplate( + Environment* env); + + static BaseObjectPtr Create( + Environment* env, + const std::shared_ptr& destination, + std::unique_ptr packet, + BaseObjectPtr endpoint = BaseObjectPtr()); + + SendWrap( + Environment* env, + v8::Local object, + const std::shared_ptr& destination, + std::unique_ptr packet, + BaseObjectPtr endpoint); + + inline void Attach(const BaseObjectPtr& strong_ptr) { + strong_ptr_ = strong_ptr; + } + + inline const std::shared_ptr& destination() const { + return destination_; + } + inline EndpointWrap* endpoint() const { return endpoint_.get(); } + inline Packet* packet() const { return packet_.get(); } + + void Done(int status); + + void MemoryInfo(MemoryTracker* tracker) const override; + SET_MEMORY_INFO_NAME(Endpoint::SendWrap) + SET_SELF_SIZE(SendWrap) + + using Queue = std::deque>; + + private: + std::shared_ptr destination_; + std::unique_ptr packet_; + BaseObjectPtr endpoint_; + BaseObjectPtr strong_ptr_; + BaseObjectPtr self_ptr_; + }; + + // The UDP class directly encapsulates the uv_udp_t handle. This is + // very similar to UDPWrap except that it is specific to the QUIC + // data structures (like Endpoint and Packet), and passes received + // packet data on using v8::BackingStore instead of uv_buf_t to + // help eliminate the need for memcpy down the line by making + // transfer of data ownership more explicit. + class UDP final : public HandleWrap { + public: + static v8::Local GetConstructorTemplate( + Environment* env); + + static UDP* Create(Environment* env, Endpoint* endpoint); + + UDP(Environment* env, + v8::Local object, + Endpoint* endpoint); + + UDP(const UDP&) = delete; + UDP(UDP&&) = delete; + UDP& operator=(const UDP&) = delete; + UDP& operator=(UDP&&) = delete; + + int Bind(const Endpoint::Config& config); + + void Ref(); + void Unref(); + void Close(); + int StartReceiving(); + void StopReceiving(); + std::shared_ptr local_address() const; + + int SendPacket(BaseObjectPtr req); + + inline bool is_closing() const { return uv_is_closing(GetHandle()); } + + SET_NO_MEMORY_INFO() + SET_MEMORY_INFO_NAME(Endpoint::UDP) + SET_SELF_SIZE(UDP) + + private: + static void ClosedCb(uv_handle_t* handle); + static void OnAlloc( + uv_handle_t* handle, + size_t suggested_size, + uv_buf_t* buf); + + static void OnReceive( + uv_udp_t* handle, + ssize_t nread, + const uv_buf_t* buf, + const sockaddr* addr, + unsigned int flags); + + void MaybeClose(); + + uv_udp_t handle_; + Endpoint* endpoint_; + }; + + // UDPHandle is a helper class that sits between the Endpoint and + // the UDP the help manage the lifecycle of the UDP class. Essentially, + // UDPHandle allows the Endpoint to be deconstructed immediately while + // allowing the UDP class to go through the typical asynchronous cleanup + // flow with the event loop. + class UDPHandle final : public MemoryRetainer { + public: + UDPHandle(Environment* env, Endpoint* endpoint); + + inline ~UDPHandle() { Close(); } + + inline int Bind(const Endpoint::Config& config) { + return udp_->Bind(config); + } + + inline void Ref() { if (udp_) udp_->Ref(); } + inline void Unref() { if (udp_) udp_->Unref();} + inline int StartReceiving() { + return udp_ ? udp_->StartReceiving() : UV_EBADF; + } + inline void StopReceiving() { + udp_->StopReceiving(); + } + inline std::shared_ptr local_address() const { + return udp_ ? udp_->local_address() : std::make_shared(); + } + void Close(); + + inline int SendPacket(BaseObjectPtr req) { + return udp_ ? udp_->SendPacket(std::move(req)) : UV_EBADF; + } + + inline bool closed() const { return !udp_; } + + void MemoryInfo(node::MemoryTracker* tracker) const override; + SET_MEMORY_INFO_NAME(Endpoint::UDPHandle) + SET_SELF_SIZE(UDPHandle) + + private: + static void CleanupHook(void* data); + Environment* env_; + UDP* udp_; + }; + + // The InitialPacketListener is an interface implemented by EndpointWrap + // and registered with the Endpoint when it is set to listen for new + // incoming initial packets (that is, when it's acting as a server). + // The Endpoint passes both the v8::BackingStore and the nread because + // the nread (the actual number of bytes read and filled in the backing + // store) may be less than the actual size of the v8::BackingStore. + // Specifically, nread will always <= v8::BackStore::ByteLength(). + struct InitialPacketListener { + virtual bool Accept( + const Session::Config& config, + std::shared_ptr store, + size_t nread, + const std::shared_ptr& local_address, + const std::shared_ptr& remote_address) = 0; + + using List = std::deque; + }; + + // The PacketListener is an interface implemented by EndpointWrap + // and registered with the Endpoint to handle received packets + // intended for a specific session. + // The Endpoint passes both the v8::BackingStore and the nread because + // the nread (the actual number of bytes read and filled in the backing + // store) may be less than the actual size of the v8::BackingStore. + // Specifically, nread will always <= v8::BackStore::ByteLength(). + struct PacketListener { + enum class Flags { + NONE, + STATELESS_RESET + }; + + virtual bool Receive( + const CID& dcid, + const CID& scid, + std::shared_ptr store, + size_t nread, + const std::shared_ptr& local_address, + const std::shared_ptr& remote_address, + Flags flags = Flags::NONE) = 0; + }; + + // Every EndpointWrap associated with the Endpoint registers a CloseListener + // that receives notification when the Endpoint is closing, either because + // it's owning event loop is closing or because of an error. + struct CloseListener { + enum class Context { + CLOSE, + RECEIVE_FAILURE, + SEND_FAILURE, + LISTEN_FAILURE, + }; + virtual void EndpointClosed(Context context, int status) = 0; + + using Set = std::unordered_set; + }; + + Endpoint(Environment* env, const Config& config); + + ~Endpoint() override; + + void AddCloseListener(CloseListener* listener); + void RemoveCloseListener(CloseListener* listener); + + void AddInitialPacketListener(InitialPacketListener* listener); + void RemoveInitialPacketListener(InitialPacketListener* listener); + + void AssociateCID(const CID& cid, PacketListener* session); + void DisassociateCID(const CID& cid); + + void IncrementSocketAddressCounter( + const std::shared_ptr& address); + void DecrementSocketAddressCounter( + const std::shared_ptr& address); + + void AssociateStatelessResetToken( + const StatelessResetToken& token, + PacketListener* session); + + void DisassociateStatelessResetToken(const StatelessResetToken& token); + + v8::Maybe GenerateNewToken( + uint8_t* token, + const std::shared_ptr& remote_address); + + // This version of SendPacket is used to send packets that are not + // affiliated with a Session (Retry, Version Negotiation, and Early + // Connection Close packets, for instance). + bool SendPacket( + const std::shared_ptr& remote_address, + std::unique_ptr packet); + + // This version of SendPacket is used to send packets that are + // affiliated with a Session. + void SendPacket(BaseObjectPtr packet); + + // Shutdown a connection prematurely, before a QuicSession is created. + // This should only be called at the start of a session before the crypto + // keys have been established. + void ImmediateConnectionClose( + const quic_version version, + const CID& scid, + const CID& dcid, + const std::shared_ptr& local_address, + const std::shared_ptr& remote_addr, + error_code reason = NGTCP2_INVALID_TOKEN); + + // Generates and sends a retry packet. This is terminal + // for the connection. Retry packets are used to force + // explicit path validation by issuing a token to the + // peer that it must thereafter include in all subsequent + // initial packets. Upon receiving a retry packet, the + // peer must termination it's initial attempt to + // establish a connection and start a new attempt. + // + // Retry packets will only ever be generated by QUIC servers, + // and only if the QuicSocket is configured for explicit path + // validation. There is no way for a client to force a retry + // packet to be created. However, once a client determines that + // explicit path validation is enabled, it could attempt to + // DOS by sending a large number of malicious initial packets + // to intentionally ellicit retry packets (It can do so by + // intentionally sending initial packets that ignore the retry + // token). To help mitigate that risk, we limit the number of + // retries we send to a given remote endpoint. + bool SendRetry( + const quic_version version, + const CID& dcid, + const CID& scid, + const std::shared_ptr& local_address, + const std::shared_ptr& remote_address); + + // Sends a version negotiation packet. This is terminal for + // the connection and is sent only when a QUIC packet is + // received for an unsupported Node.js version. + // It is possible that a malicious packet triggered this + // so we need to be careful not to commit too many resources. + // Currently, we only support one QUIC version at a time. + void SendVersionNegotiation( + const quic_version version, + const CID& dcid, + const CID& scid, + const std::shared_ptr& local_address, + const std::shared_ptr& remote_address); + + void Ref(); + void Unref(); + + // While the busy flag is set, the Endpoint will reject all initial + // packets with a SERVER_BUSY response, even if there are available + // listening EndpointWraps. This allows us to build a circuit breaker + // directly in to the implementation, explicitly signaling that the + // server is blocked when activity is high. + inline void set_busy(bool on = true) { busy_ = on; } + + // QUIC strongly recommends the use of flow labels when using IPv6. + // The GetFlowLabel will deterministically generate a flow label as + // a function of the given local address, remote address, and connection ID. + uint32_t GetFlowLabel( + const std::shared_ptr& local_address, + const std::shared_ptr& remote_address, + const CID& cid); + + inline const Config& config() const { return config_; } + inline Environment* env() const { return env_; } + std::shared_ptr local_address() const; + + void MemoryInfo(MemoryTracker* tracker) const override; + SET_MEMORY_INFO_NAME(Endpoint); + SET_SELF_SIZE(Endpoint); + + struct Lock { + Mutex::ScopedLock lock_; + explicit Lock(Endpoint* endpoint) : lock_(endpoint->mutex_) {} + explicit Lock(const std::shared_ptr& endpoint) + : lock_(endpoint->mutex_) {} + }; + + private: + static void OnCleanup(void* data); + + void Close( + CloseListener::Context context = CloseListener::Context::CLOSE, + int status = 0); + + // Inspects the packet and possibly accepts it as a new + // initial packet creating a new Session instance. + // If the packet is not acceptable, it is very important + // not to commit resources. + bool AcceptInitialPacket( + const quic_version version, + const CID& dcid, + const CID& scid, + std::shared_ptr store, + size_t nread, + const std::shared_ptr& local_address, + const std::shared_ptr& remote_address); + + int MaybeBind(); + int StartReceiving(); + void MaybeStopReceiving(); + + PacketListener* FindSession(const CID& cid); + + // When a received packet contains a QUIC short header but cannot be + // matched to a known Session, it is either (a) garbage, + // (b) a valid packet for a connection we no longer have state + // for, or (c) a stateless reset. Because we do not yet know if + // we are going to process the packet, we need to try to quickly + // determine -- with as little cost as possible -- whether the + // packet contains a reset token. We do so by checking the final + // NGTCP2_STATELESS_RESET_TOKENLEN bytes in the packet to see if + // they match one of the known reset tokens previously given by + // the remote peer. If there's a match, then it's a reset token, + // if not, we move on the to the next check. It is very important + // that this check be as inexpensive as possible to avoid a DOS + // vector. + bool MaybeStatelessReset( + const CID& dcid, + const CID& scid, + std::shared_ptr store, + size_t nread, + const std::shared_ptr& local_address, + const std::shared_ptr& remote_address); + + uv_buf_t OnAlloc(size_t suggested_size); + void OnReceive( + size_t nread, + const uv_buf_t& buf, + const std::shared_ptr& address); + + void ProcessOutbound(); + void ProcessSendFailure(int status); + void ProcessReceiveFailure(int status); + + // Possibly generates and sends a stateless reset packet. + // This is terminal for the connection. It is possible + // that a malicious packet triggered this so we need to + // be careful not to commit too many resources. + bool SendStatelessReset( + const CID& cid, + const std::shared_ptr& local_address, + const std::shared_ptr& remote_address, + size_t source_len); + + void set_validated_address(const std::shared_ptr& addr); + bool is_validated_address(const std::shared_ptr& addr) const; + void IncrementStatelessResetCounter( + const std::shared_ptr& addr); + size_t current_socket_address_count( + const std::shared_ptr& addr) const; + size_t current_stateless_reset_count( + const std::shared_ptr& addr) const; + bool is_diagnostic_packet_loss(double prob) const; + + Environment* env_; + UDPHandle udp_; + const Config config_; + + SendWrap::Queue outbound_; + AsyncSignalHandle outbound_signal_; + size_t pending_outbound_ = 0; + + size_t ref_count_ = 0; + + uint8_t token_secret_[kTokenSecretLen]; + ngtcp2_crypto_aead token_aead_; + ngtcp2_crypto_md token_md_; + + struct SocketAddressInfoTraits final { + struct Type final { + size_t active_connections; + size_t reset_count; + size_t retry_count; + uint64_t timestamp; + bool validated; + }; + + static bool CheckExpired( + const SocketAddress& address, + const Type& type); + static void Touch( + const SocketAddress& address, + Type* type); + }; + + SocketAddressLRU addrLRU_; + StatelessResetToken::Map token_map_; + CID::Map sessions_; + InitialPacketListener::List listeners_; + CloseListener::Set close_listeners_; + + bool busy_ = false; + bool bound_ = false; + bool receiving_ = false; + + Mutex mutex_; +}; + +// The EndpointWrap is the intermediate JavaScript binding object +// that is passed into JavaScript for interacting with the Endpoint. +// Every EndpointWrap wraps a single Endpoint (that may be shared +// with other EndpointWrap instances). +// All EndpointWrap instances are "cloneable" via MessagePort but +// the instances are not true copies. Each "clone" will share a +// reference to the same Endpoint (via std::shared_ptr), +// but will retain their own separate state in every other regard. +// Specifically, QUIC Sessions are only ever associated with a +// single EndpointWrap at a time. +class EndpointWrap final : public AsyncWrap, + public Endpoint::CloseListener, + public Endpoint::InitialPacketListener, + public Endpoint::PacketListener { + public: + struct State final { +#define V(_, name, type) type name; + ENDPOINT_STATE(V) +#undef V + }; + + // The InboundPacket represents a packet received by the + // Endpoint and passed on to the EndpointWrap for processing. + // InboundPackets are stored in a queue and processed in a batch + // once per event loop turn. They are always processed in the + // order they were received. + struct InboundPacket final { + CID dcid; + CID scid; + std::shared_ptr store; + size_t nread; + std::shared_ptr local_address; + std::shared_ptr remote_address; + Endpoint::PacketListener::Flags flags; + + using Queue = std::deque; + }; + + // The InitialPacket represents an initial packet to create + // a new Session received by the Endpoint and passed on to + // the EndpointWrap for processing. InitialPackets are stored + // in a queue and processed in a batch once per event loop turn. + // They are always processed in the order they were received. + struct InitialPacket final { + Session::Config config; + std::shared_ptr store; + size_t nread; + std::shared_ptr local_address; + std::shared_ptr remote_address; + + using Queue = std::deque; + }; + + static bool HasInstance(Environment* env, const v8::Local& value); + + static v8::Local GetConstructorTemplate( + Environment* env); + + static void Initialize(Environment* env, v8::Local target); + + static BaseObjectPtr Create( + Environment* env, + const Endpoint::Config& config); + + static BaseObjectPtr Create( + Environment* env, + std::shared_ptr endpoint); + + static void CreateClientSession( + const v8::FunctionCallbackInfo& args); + static void CreateEndpoint( + const v8::FunctionCallbackInfo& args); + static void StartListen( + const v8::FunctionCallbackInfo& args); + static void StartWaitForPendingCallbacks( + const v8::FunctionCallbackInfo& args); + static void LocalAddress( + const v8::FunctionCallbackInfo& args); + static void Ref(const v8::FunctionCallbackInfo& args); + static void Unref(const v8::FunctionCallbackInfo& args); + + EndpointWrap( + Environment* env, + v8::Local object, + const Endpoint::Config& config); + + EndpointWrap( + Environment* env, + v8::Local object, + std::shared_ptr inner); + + ~EndpointWrap() override; + + explicit EndpointWrap(const EndpointWrap& other) = delete; + explicit EndpointWrap(const EndpointWrap&& other) = delete; + EndpointWrap& operator=(const Endpoint& other) = delete; + EndpointWrap& operator=(const Endpoint&& other) = delete; + + void EndpointClosed(Endpoint::CloseListener::Context context, int status); + + // Returns the default Session::Options used for new server + // sessions accepted by this EndpointWrap. The server_options_ + // is set when EndpointWrap::Listen() is called. Until then it + // will return the defaults. + inline const std::shared_ptr& server_config() const { + return server_options_; + } + + v8::Maybe GenerateNewToken( + uint8_t* token, + const std::shared_ptr& remote_address); + + inline State* state() { return state_.Data(); } + inline const Endpoint::Config& config() const { return inner_->config(); } + + // The local UDP address to which the inner Endpoint is bound. + inline std::shared_ptr local_address() const { + return inner_->local_address(); + } + + // Called by the inner Endpoint when a new initial packet is received. + // Accept() will return true if the EndpointWrap will handle the initial + // packet, false otherwise. + bool Accept( + const Session::Config& config, + std::shared_ptr store, + size_t nread, + const std::shared_ptr& local_address, + const std::shared_ptr& remote_address) override; + + // Adds a new Session to this EndpointWrap, associating the + // session with the given CID. The inner Endpoint is also + // notified to associate the CID with this EndpointWrap. + void AddSession(const CID& cid, const BaseObjectPtr& session); + + // A single session may be associated with multiple CIDs + // The AssociateCID registers the neceesary mapping both in the + // EndpointWrap and the inner Endpoint. + void AssociateCID(const CID& cid, const CID& scid); + + // Associates a given stateless reset token with the session. + // This allows stateless reset tokens to be recognized and + // dispatched to the proper EndpointWrap and Session for + // processing. + void AssociateStatelessResetToken( + const StatelessResetToken& token, + const BaseObjectPtr& session); + + // Removes the associated CID from this EndpointWrap and the + // inner Endpoint. + void DisassociateCID(const CID& cid); + + // Removes the associated stateless reset token from this EndpointWrap + // and the inner Endpoint. + void DisassociateStatelessResetToken(const StatelessResetToken& token); + + // Looks up an existing session by the associated CID. If no matching + // session is found, returns an empty BaseObjectPtr. + BaseObjectPtr FindSession(const CID& cid); + + // Generates an IPv6 flow label for the given local_address, remote_address, + // and CID. Both the local_address and remote_address must be IPv6. + uint32_t GetFlowLabel( + const std::shared_ptr& local_address, + const std::shared_ptr& remote_address, + const CID& cid); + + // Shutdown a connection prematurely, before a Session is created. + // This should only be called at the start of a session before the crypto + // keys have been established. + void ImmediateConnectionClose( + const quic_version version, + const CID& scid, + const CID& dcid, + const std::shared_ptr& local_address, + const std::shared_ptr& remote_address, + const error_code reason = NGTCP2_INVALID_TOKEN); + + // Registers this EndpointWrap as able to accept incoming initial + // packets. Whenever an Endpoint receives an initial packet for which + // there is no associated Session, the Endpoint will iterate through + // it's registered listening EndpointWrap instances to find one willing + // to accept the packet. + void Listen( + const std::shared_ptr& options, + const BaseObjectPtr& context); + + void OnSendDone(int status); + + // Receives a packet intended for a session owned by this EndpointWrap + bool Receive( + const CID& dcid, + const CID& scid, + std::shared_ptr store, + size_t nread, + const std::shared_ptr& local_address, + const std::shared_ptr& remote_address, + const PacketListener::Flags flags) override; + + // Removes the given session from from EndpointWrap and removes the + // registered associations on the inner Endpoint. + void RemoveSession( + const CID& cid, + const std::shared_ptr& address); + + // Sends a serialized QUIC packet to the remote_addr on behalf of the + // given session. + void SendPacket( + const std::shared_ptr& local_address, + const std::shared_ptr& remote_address, + std::unique_ptr packet, + const BaseObjectPtr& session = BaseObjectPtr()); + + // Generates and sends a retry packet. This is terminal + // for the connection. Retry packets are used to force + // explicit path validation by issuing a token to the + // peer that it must thereafter include in all subsequent + // initial packets. Upon receiving a retry packet, the + // peer must termination it's initial attempt to + // establish a connection and start a new attempt. + // + // Retry packets will only ever be generated by QUIC servers, + // and only if the Endpoint is configured for explicit path + // validation. There is no way for a client to force a retry + // packet to be created. However, once a client determines that + // explicit path validation is enabled, it could attempt to + // DOS by sending a large number of malicious initial packets + // to intentionally ellicit retry packets (It can do so by + // intentionally sending initial packets that ignore the retry + // token). To help mitigate that risk, we limit the number of + // retries we send to a given remote endpoint. + bool SendRetry( + const quic_version version, + const CID& dcid, + const CID& scid, + const std::shared_ptr& local_address, + const std::shared_ptr& remote_address); + + inline void set_busy(bool on = true) { inner_->set_busy(on); } + + void MemoryInfo(MemoryTracker* tracker) const override; + SET_MEMORY_INFO_NAME(EndpointWrap); + SET_SELF_SIZE(EndpointWrap); + + // An EndpointWrap instance is cloneable over MessagePort. + // Clones will share the same inner Endpoint instance but + // will maintain their own state and their own collection + // of associated sessions. + class TransferData final : public worker::TransferData { + public: + inline TransferData(std::shared_ptr inner) + : inner_(inner) {} + + BaseObjectPtr Deserialize( + Environment* env, + v8::Local context, + std::unique_ptr self); + + void MemoryInfo(MemoryTracker* tracker) const override; + SET_MEMORY_INFO_NAME(EndpointWrap::TransferData) + SET_SELF_SIZE(TransferData) + + private: + std::shared_ptr inner_; + }; + + TransferMode GetTransferMode() const override { + return TransferMode::kCloneable; + } + std::unique_ptr CloneForMessaging() const override; + + private: + // The underlying endpoint has been closed. Clean everything up and notify. + // No further packets will be sent at this point. This can happen abruptly + // so we have to make sure we cycle out through the JavaScript side to free + // up everything there. + void Close(); + + // Called after the endpoint has been closed and the final + // pending send callback has been received. Signals to the + // JavaScript side that the endpoint is ready to be destroyed. + void OnEndpointDone(); + + // Called when the Endpoint has encountered an error condition + // Signals to the JavaScript side. + void OnError(v8::Local error = v8::Local()); + + // Called when a new Session has been created. Passes the + // reference to the new session on the JavaScript side for + // additional processing. + void OnNewSession(const BaseObjectPtr& session); + + void ProcessInbound(); + void ProcessInitial(); + void ProcessInitialFailure(); + + inline void DecrementPendingCallbacks() { state_->pending_callbacks--; } + inline void IncrementPendingCallbacks() { state_->pending_callbacks++; } + inline bool is_done_waiting_for_callbacks() const { + return state_->waiting_for_callbacks && !state_->pending_callbacks; + } + void WaitForPendingCallbacks(); + + AliasedStruct state_; + std::shared_ptr inner_; + + std::shared_ptr server_options_; + BaseObjectPtr server_context_; + + StatelessResetToken::Map> token_map_; + CID::Map> sessions_; + CID::Map dcid_to_scid_; + + InboundPacket::Queue inbound_; + InitialPacket::Queue initial_; + + Endpoint::CloseListener::Context close_context_ = + Endpoint::CloseListener::Context::CLOSE; + int close_status_ = 0; + + AsyncSignalHandle close_signal_; + AsyncSignalHandle inbound_signal_; + AsyncSignalHandle initial_signal_; + Mutex inbound_mutex_; +}; + +// The ConfigObject is a persistent, cloneable Endpoint::Config. +// It is used to encapsulate all of the fairly complex configuration +// options for an Endpoint. +class ConfigObject final : public BaseObject { + public: + static bool HasInstance(Environment* env, const v8::Local& value); + static v8::Local GetConstructorTemplate( + Environment* env); + static void Initialize(Environment* env, v8::Local target); + static void New(const v8::FunctionCallbackInfo& args); + static void GenerateResetTokenSecret( + const v8::FunctionCallbackInfo& args); + static void SetResetTokenSecret( + const v8::FunctionCallbackInfo& args); + + ConfigObject( + Environment* env, + v8::Local object, + std::shared_ptr config = + std::make_shared()); + + inline Endpoint::Config* data() { return config_.get(); } + inline const Endpoint::Config& config() { return *config_.get(); } + + void MemoryInfo(MemoryTracker* tracker) const override; + SET_MEMORY_INFO_NAME(ConfigObject) + SET_SELF_SIZE(ConfigObject) + + private: + template + v8::Maybe SetOption( + const v8::Local& object, + const v8::Local& name, + T Endpoint::Config::*member); + + std::shared_ptr config_; +}; + +} // namespace quic +} // namespace node + +#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS +#endif // SRC_QUIC_ENDPOINT_H_ diff --git a/src/quic/qlog.h b/src/quic/qlog.h new file mode 100644 index 00000000000000..d93d943c399dce --- /dev/null +++ b/src/quic/qlog.h @@ -0,0 +1,102 @@ +#ifndef SRC_QUIC_QLOG_H_ +#define SRC_QUIC_QLOG_H_ + +#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS + +#include "quic/quic.h" +#include "async_wrap-inl.h" +#include "base_object.h" +#include "env-inl.h" +#include "stream_base-inl.h" +#include + +namespace node { +namespace quic { + +class QLogStream final : public AsyncWrap, + public StreamBase { + public: + inline static v8::Local GetConstructorTemplate( + Environment* env) { + BindingState* state = env->GetBindingData(env->context()); + v8::Local tmpl = + state->qlogstream_constructor_template(); + if (tmpl.IsEmpty()) { + tmpl = v8::FunctionTemplate::New(env->isolate()); + tmpl->Inherit(AsyncWrap::GetConstructorTemplate(env)); + tmpl->InstanceTemplate()->SetInternalFieldCount( + AsyncWrap::kInternalFieldCount); + tmpl->SetClassName(FIXED_ONE_BYTE_STRING(env->isolate(), "QLogStream")); + StreamBase::AddMethods(env, tmpl); + state->set_qlogstream_constructor_template(tmpl); + } + return tmpl; + } + + inline static BaseObjectPtr Create(Environment* env) { + v8::Local obj; + if (!GetConstructorTemplate(env) + ->InstanceTemplate() + ->NewInstance(env->context()).ToLocal(&obj)) { + return BaseObjectPtr(); + } + return MakeDetachedBaseObject(env, obj); + } + + inline QLogStream(Environment* env, v8::Local obj) + : AsyncWrap(env, obj, AsyncWrap::PROVIDER_QLOGSTREAM), + StreamBase(env) { + MakeWeak(); + StreamBase::AttachToObject(GetObject()); + } + + inline void Emit(const uint8_t* data, size_t len, uint32_t flags) { + size_t remaining = len; + while (remaining != 0) { + uv_buf_t buf = EmitAlloc(len); + ssize_t avail = std::min(remaining, buf.len); + memcpy(buf.base, data, avail); + remaining -= avail; + data += avail; + EmitRead(avail, buf); + } + + if (ended_ && flags & NGTCP2_QLOG_WRITE_FLAG_FIN) + EmitRead(UV_EOF); + } + + inline void End() { ended_ = true; } + + inline int ReadStart() override { return 0; } + + inline int ReadStop() override { return 0; } + + inline int DoShutdown(ShutdownWrap* req_wrap) override { + UNREACHABLE(); + } + + inline int DoWrite( + WriteWrap* w, + uv_buf_t* bufs, + size_t count, + uv_stream_t* send_handle) override { + UNREACHABLE(); + } + + inline bool IsAlive() override { return !ended_; } + inline bool IsClosing() override { return ended_; } + inline AsyncWrap* GetAsyncWrap() override { return this; } + + SET_NO_MEMORY_INFO(); + SET_MEMORY_INFO_NAME(QLogStream); + SET_SELF_SIZE(QLogStream); + + private: + bool ended_ = false; +}; + +} // namespace quic +} // namespace node + +#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS +#endif // SRC_QUIC_QLOG_H_ diff --git a/src/quic/quic.cc b/src/quic/quic.cc new file mode 100644 index 00000000000000..2f9706b4f4df30 --- /dev/null +++ b/src/quic/quic.cc @@ -0,0 +1,395 @@ + +#if HAVE_OPENSSL +#include +#endif // HAVE_OPENSSL + +#ifdef OPENSSL_INFO_QUIC +# include "quic/quic.h" +# include "quic/endpoint.h" +# include "quic/session.h" +# include "quic/stream.h" +# include "crypto/crypto_random.h" +# include "crypto/crypto_context.h" +#endif // OPENSSL_INFO_QUIC + +#include "async_wrap-inl.h" +#include "base_object-inl.h" +#include "env-inl.h" +#include "memory_tracker-inl.h" +#include "node.h" +#include "node_errors.h" +#include "node_mem-inl.h" +#include "node_sockaddr-inl.h" +#include "util-inl.h" +#include "v8.h" +#include "uv.h" + +namespace node { + +using v8::Context; +using v8::Function; +using v8::FunctionCallbackInfo; +using v8::FunctionTemplate; +using v8::Local; +using v8::Object; +using v8::String; +using v8::Value; + +namespace quic { +#ifdef OPENSSL_INFO_QUIC + +constexpr FastStringKey BindingState::type_name; + +void IllegalConstructor(const FunctionCallbackInfo& args) { + CHECK(args.IsConstructCall()); + Environment* env = Environment::GetCurrent(args); + THROW_ERR_ILLEGAL_CONSTRUCTOR(env); +} + +BindingState* BindingState::Get(Environment* env) { + return env->GetBindingData(env->context()); +} + +bool BindingState::Initialize(Environment* env, Local target) { + BindingState* const state = + env->AddBindingData(env->context(), target); + return state != nullptr; +} + +BindingState::BindingState(Environment* env, Local object) + : BaseObject(env, object) {} + +ngtcp2_mem BindingState::GetAllocator(Environment* env) { + BindingState* state = env->GetBindingData(env->context()); + return state->MakeAllocator(); +} + +void BindingState::MemoryInfo(MemoryTracker* tracker) const { +#define V(name, _) tracker->TrackField(#name, name ## _callback()); + QUIC_JS_CALLBACKS(V) +#undef V +#define V(name, _) tracker->TrackField(#name, name ## _string()); + QUIC_STRINGS(V) +#undef V +} + +void BindingState::CheckAllocatedSize(size_t previous_size) const { + CHECK_GE(current_ngtcp2_memory_, previous_size); +} + +void BindingState::IncreaseAllocatedSize(size_t size) { + current_ngtcp2_memory_ += size; +} + +void BindingState::DecreaseAllocatedSize(size_t size) { + current_ngtcp2_memory_ -= size; +} + +#define V(name) \ + void BindingState::set_ ## name ## _constructor_template( \ + Local tmpl) { \ + name ## _constructor_template_.Reset(env()->isolate(), tmpl); \ + } \ + Local BindingState::name ## _constructor_template() const {\ + return PersistentToLocal::Default( \ + env()->isolate(), name ## _constructor_template_); \ + } + QUIC_CONSTRUCTORS(V) +#undef V + +#define V(name, _) \ + void BindingState::set_ ## name ## _callback(Local fn) { \ + name ## _callback_.Reset(env()->isolate(), fn); \ + } \ + Local BindingState::name ## _callback() const { \ + return PersistentToLocal::Default(env()->isolate(), name ## _callback_); \ + } + QUIC_JS_CALLBACKS(V) +#undef V + +#define V(name, value) \ + Local BindingState::name ## _string() const { \ + if (name ## _string_.IsEmpty()) \ + name ## _string_.Set( \ + env()->isolate(), \ + OneByteString(env()->isolate(), value)); \ + return name ## _string_.Get(env()->isolate()); \ + } + QUIC_STRINGS(V) +#undef V + +PreferredAddress::Address PreferredAddress::ipv4() const { + Address address; + address.family = AF_INET; + address.port = paddr_->ipv4_port; + + char host[NI_MAXHOST]; + // Return an empty string if unable to convert... + if (uv_inet_ntop(AF_INET, paddr_->ipv4_addr, host, sizeof(host)) == 0) + address.address = std::string(host); + + return address; +} + +PreferredAddress::Address PreferredAddress::ipv6() const { + Address address; + address.family = AF_INET6; + address.port = paddr_->ipv6_port; + + char host[NI_MAXHOST]; + // Return an empty string if unable to convert... + if (uv_inet_ntop(AF_INET6, paddr_->ipv6_addr, host, sizeof(host)) == 0) + address.address = std::string(host); + + return address; +} + +bool PreferredAddress::Use(const Address& address) const { + uv_getaddrinfo_t req; + + if (!Resolve(address, &req)) + return false; + + dest_->addrlen = req.addrinfo->ai_addrlen; + memcpy(dest_->addr, req.addrinfo->ai_addr, req.addrinfo->ai_addrlen); + uv_freeaddrinfo(req.addrinfo); + return true; +} + +void PreferredAddress::CopyToTransportParams( + ngtcp2_transport_params* params, + const sockaddr* addr) { + CHECK_NOT_NULL(params); + CHECK_NOT_NULL(addr); + params->preferred_address_present = 1; + switch (addr->sa_family) { + case AF_INET: { + const sockaddr_in* src = reinterpret_cast(addr); + memcpy( + params->preferred_address.ipv4_addr, + &src->sin_addr, + sizeof(params->preferred_address.ipv4_addr)); + params->preferred_address.ipv4_port = SocketAddress::GetPort(addr); + break; + } + case AF_INET6: { + const sockaddr_in6* src = reinterpret_cast(addr); + memcpy( + params->preferred_address.ipv6_addr, + &src->sin6_addr, + sizeof(params->preferred_address.ipv6_addr)); + params->preferred_address.ipv6_port = SocketAddress::GetPort(addr); + break; + } + default: + UNREACHABLE(); + } +} + +bool PreferredAddress::Resolve( + const Address& address, + uv_getaddrinfo_t* req) const { + addrinfo hints{}; + hints.ai_flags = AI_NUMERICHOST | AI_NUMERICSERV; + hints.ai_family = address.family; + hints.ai_socktype = SOCK_DGRAM; + + // Unfortunately ngtcp2 requires the selection of the + // preferred address to be synchronous, which means we + // have to do a sync resolve using uv_getaddrinfo here. + return + uv_getaddrinfo( + env_->event_loop(), + req, + nullptr, + address.address.c_str(), + std::to_string(address.port).c_str(), + &hints) == 0 && + req->addrinfo != nullptr; +} + +void Packet::MemoryInfo(MemoryTracker* tracker) const { + tracker->TrackFieldWithSize("allocated", ptr_ != data_ ? len_ : 0); +} + +Path::Path( + const std::shared_ptr& local, + const std::shared_ptr& remote) { + CHECK(local); + CHECK(remote); + ngtcp2_addr_init( + &this->local, + local->data(), + local->length(), + nullptr); + ngtcp2_addr_init( + &this->remote, + remote->data(), + remote->length(), + nullptr); +} + +StatelessResetToken::StatelessResetToken( + uint8_t* token, + const uint8_t* secret, + const CID& cid) { + // TODO(@jasnell) + // GenerateResetToken(token, secret, cid); + memcpy(buf_, token, sizeof(buf_)); +} + +StatelessResetToken::StatelessResetToken( + const uint8_t* secret, + const CID& cid) { + // TODO(@jasnell) + // GenerateResetToken(buf_, secret, cid); +} + +void RandomConnectionIDTraits::NewConnectionID( + const Options& options, + State* state, + Session* session, + ngtcp2_cid* cid, + size_t length_hint) { + CHECK_NOT_NULL(cid); + crypto::EntropySource( + reinterpret_cast(cid->data), + length_hint); + cid->data[0] |= 0xc0; + cid->datalen = length_hint; +} + +void RandomConnectionIDTraits::New( + const FunctionCallbackInfo& args) { + CHECK(args.IsConstructCall()); + Environment* env = Environment::GetCurrent(args); + new RandomConnectionIDBase(env, args.This()); +} + +Local RandomConnectionIDTraits::GetConstructorTemplate( + Environment* env) { + BindingState* state = env->GetBindingData(env->context()); + Local tmpl = + state->random_connection_id_strategy_constructor_template(); + if (tmpl.IsEmpty()) { + tmpl = env->NewFunctionTemplate(New); + tmpl->SetClassName(OneByteString(env->isolate(), name)); + tmpl->Inherit(BaseObject::GetConstructorTemplate(env)); + tmpl->InstanceTemplate()->SetInternalFieldCount( + BaseObject::kInternalFieldCount); + state->set_random_connection_id_strategy_constructor_template(tmpl); + } + return tmpl; +} + +bool RandomConnectionIDTraits::HasInstance( + Environment* env, + Local value) { + return GetConstructorTemplate(env)->HasInstance(value); +} + +namespace { +void InitializeCallbacks(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + BindingState* state = env->GetBindingData(env->context()); + CHECK(!state->initialized); + if (!args[0]->IsObject()) + return THROW_ERR_INVALID_ARG_TYPE(env, "Missing Callbacks"); + Local obj = args[0].As(); +#define V(name, key) \ + do { \ + Local val; \ + if (!obj->Get( \ + env->context(), \ + FIXED_ONE_BYTE_STRING( \ + env->isolate(), \ + "on" # key)).ToLocal(&val) || \ + !val->IsFunction()) { \ + return THROW_ERR_MISSING_ARGS( \ + env->isolate(), \ + "Missing Callback: on" # key); \ + } \ + state->set_ ## name ## _callback(val.As()); \ + } while (0); + QUIC_JS_CALLBACKS(V) +#undef V + state->initialized = true; +} + +template +void CreateSecureContext(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + crypto::SecureContext* context = crypto::SecureContext::Create(env); + if (UNLIKELY(context == nullptr)) return; + // TODO(@jasnell) + //InitializeSecureContext(context, side); + args.GetReturnValue().Set(context->object()); +} +} // namespace + +#endif // OPENSSL_INFO_QUIC + +void Initialize( + Local target, + Local unused, + Local context, + void* priv) { +#ifdef OPENSSL_INFO_QUIC + Environment* env = Environment::GetCurrent(context); + + if (UNLIKELY(!BindingState::Initialize(env, target))) + return; + + EndpointWrap::Initialize(env, target); + Session::Initialize(env, target); + Stream::Initialize(env); + RandomConnectionIDBase::Initialize(env, target); + + env->SetMethod(target, "initializeCallbacks", InitializeCallbacks); + env->SetMethod(target, "createClientSecureContext", + CreateSecureContext); + env->SetMethod(target, "createServerSecureContext", + CreateSecureContext); + + constexpr uint32_t NGTCP2_PREFERRED_ADDRESS_USE = + static_cast(PreferredAddress::Policy::USE); + constexpr uint32_t NGTCP2_PREFERRED_ADDRESS_IGNORE = + static_cast(PreferredAddress::Policy::IGNORE); + + NODE_DEFINE_STRING_CONSTANT(target, "HTTP3_ALPN", &NGHTTP3_ALPN_H3[1]); + NODE_DEFINE_CONSTANT(target, AF_INET); + NODE_DEFINE_CONSTANT(target, AF_INET6); + NODE_DEFINE_CONSTANT(target, NGTCP2_CC_ALGO_CUBIC); + NODE_DEFINE_CONSTANT(target, NGTCP2_CC_ALGO_RENO); + NODE_DEFINE_CONSTANT(target, NGTCP2_PREFERRED_ADDRESS_IGNORE); + NODE_DEFINE_CONSTANT(target, NGTCP2_PREFERRED_ADDRESS_USE); + NODE_DEFINE_CONSTANT(target, NGTCP2_MAX_CIDLEN); + NODE_DEFINE_CONSTANT(target, NGTCP2_APP_NOERROR); + NODE_DEFINE_CONSTANT(target, NGTCP2_NO_ERROR); + NODE_DEFINE_CONSTANT(target, NGTCP2_INTERNAL_ERROR); + NODE_DEFINE_CONSTANT(target, NGTCP2_CONNECTION_REFUSED); + NODE_DEFINE_CONSTANT(target, NGTCP2_FLOW_CONTROL_ERROR); + NODE_DEFINE_CONSTANT(target, NGTCP2_STREAM_LIMIT_ERROR); + NODE_DEFINE_CONSTANT(target, NGTCP2_STREAM_STATE_ERROR); + NODE_DEFINE_CONSTANT(target, NGTCP2_FINAL_SIZE_ERROR); + NODE_DEFINE_CONSTANT(target, NGTCP2_FRAME_ENCODING_ERROR); + NODE_DEFINE_CONSTANT(target, NGTCP2_TRANSPORT_PARAMETER_ERROR); + NODE_DEFINE_CONSTANT(target, NGTCP2_CONNECTION_ID_LIMIT_ERROR); + NODE_DEFINE_CONSTANT(target, NGTCP2_PROTOCOL_VIOLATION); + NODE_DEFINE_CONSTANT(target, NGTCP2_INVALID_TOKEN); + NODE_DEFINE_CONSTANT(target, NGTCP2_APPLICATION_ERROR); + NODE_DEFINE_CONSTANT(target, NGTCP2_CRYPTO_BUFFER_EXCEEDED); + NODE_DEFINE_CONSTANT(target, NGTCP2_KEY_UPDATE_ERROR); + NODE_DEFINE_CONSTANT(target, NGTCP2_CRYPTO_ERROR); + NODE_DEFINE_CONSTANT(target, UV_UDP_IPV6ONLY); +#endif // OPENSSL_INFO_QUIC +} + +} // namespace quic +} // namespace node + +// The internalBinding('quic') will be available even if quic +// support is not enabled. This prevents the internalBinding call +// from throwing. However, if quic is not enabled, the binding will +// have no exports. +NODE_MODULE_CONTEXT_AWARE_INTERNAL(quic, node::quic::Initialize) diff --git a/src/quic/quic.h b/src/quic/quic.h new file mode 100644 index 00000000000000..06a8ea729922a5 --- /dev/null +++ b/src/quic/quic.h @@ -0,0 +1,815 @@ +#ifndef SRC_QUIC_QUIC_H_ +#define SRC_QUIC_QUIC_H_ + +#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS + +#include "base_object.h" +#include "env.h" +#include "node_mem.h" +#include "node_sockaddr.h" +#include "string_bytes.h" +#include "util.h" + +#include "nghttp3/nghttp3.h" +#include "ngtcp2/ngtcp2.h" +#include +#include "uv.h" +#include "v8.h" + +#include +#include +#include +#include +#include + +namespace node { +namespace quic { + +class BindingState; +class Session; + +using QuicConnectionPointer = DeleteFnPtr; +using Http3ConnectionPointer = DeleteFnPtr; +using QuicMemoryManager = mem::NgLibMemoryManager; + +using stream_id = int64_t; +using error_code = uint64_t; +using quic_version = uint32_t; + +// The constants prefixed with k are used internally. +// The constants in all caps snake case are exported to the JavaScript binding. + +// NGTCP2_MAX_PKTLEN_IPV4 should always be larger, but we check just in case. +constexpr size_t kDefaultMaxPacketLength = + std::max(NGTCP2_MAX_PKTLEN_IPV4, NGTCP2_MAX_PKTLEN_IPV6); +constexpr size_t kMaxSizeT = std::numeric_limits::max(); +constexpr size_t kTokenSecretLen = 16; +constexpr size_t kMaxDynamicServerIDLength = 7; +constexpr size_t kMinDynamicServerIDLength = 1; + +constexpr uint64_t DEFAULT_ACTIVE_CONNECTION_ID_LIMIT = 2; +constexpr uint64_t DEFAULT_MAX_STREAM_DATA_BIDI_LOCAL = 256 * 1024; +constexpr uint64_t DEFAULT_MAX_STREAM_DATA_BIDI_REMOTE = 256 * 1024; +constexpr uint64_t DEFAULT_MAX_STREAM_DATA_UNI = 256 * 1024; +constexpr uint64_t DEFAULT_MAX_STREAMS_BIDI = 100; +constexpr uint64_t DEFAULT_MAX_STREAMS_UNI = 3; +constexpr uint64_t DEFAULT_MAX_DATA = 1 * 1024 * 1024; +constexpr uint64_t DEFAULT_MAX_IDLE_TIMEOUT = 10; +constexpr size_t DEFAULT_MAX_CONNECTIONS = + std::min( + kMaxSizeT, + static_cast(kMaxSafeJsInteger)); +constexpr size_t DEFAULT_MAX_CONNECTIONS_PER_HOST = 100; +constexpr size_t DEFAULT_MAX_SOCKETADDRESS_LRU_SIZE = + (DEFAULT_MAX_CONNECTIONS_PER_HOST * 10); +constexpr size_t DEFAULT_MAX_STATELESS_RESETS = 10; +constexpr size_t DEFAULT_MAX_RETRY_LIMIT = 10; +constexpr uint64_t DEFAULT_RETRYTOKEN_EXPIRATION = 10; +constexpr uint64_t DEFAULT_TOKEN_EXPIRATION = 3600; +constexpr uint64_t NGTCP2_APP_NOERROR = 0xff00; + +// The constructors are v8::FunctionTemplates that are stored +// persistently in the quic::BindingState class. These are +// used for creating instances of the various objects, as well +// as for performing HasInstance type checks. We choose to +// store these on the BindingData instead of the Environment +// in order to keep like-things together and to reduce the +// additional memory overhead on the Environment when QUIC is +// not being used. +#define QUIC_CONSTRUCTORS(V) \ + V(endpoint) \ + V(endpoint_config) \ + V(qlogstream) \ + V(random_connection_id_strategy) \ + V(send_wrap) \ + V(session) \ + V(session_options) \ + V(stream) \ + V(udp) + +// The callbacks are persistent v8::Function references that +// are set in the quic::BindingState used to communicate data +// and events back out to the JS environment. They are set once +// from the JavaScript side when the internalBinding('quic') is +// first loaded. +#define QUIC_JS_CALLBACKS(V) \ + V(endpoint_close, EndpointClose) \ + V(endpoint_done, EndpointDone) \ + V(endpoint_error, EndpointError) \ + V(session_new, SessionNew) \ + V(session_cert, SessionCert) \ + V(session_client_hello, SessionClientHello) \ + V(session_close, SessionClose) \ + V(session_datagram, SessionDatagram) \ + V(session_handshake, SessionHandshake) \ + V(session_keylog, SessionKeylog) \ + V(session_path_validation, SessionPathValidation) \ + V(session_use_preferred_address, SessionUsePreferredAddress) \ + V(session_qlog, SessionQlog) \ + V(session_ocsp_request, SessionOcspRequest) \ + V(session_ocsp_response, SessionOcspResponse) \ + V(session_ticket, SessionTicket) \ + V(session_version_negotiation, SessionVersionNegotiation) \ + V(stream_close, StreamClose) \ + V(stream_error, StreamError) \ + V(stream_ready, StreamReady) \ + V(stream_reset, StreamReset) \ + V(stream_headers, StreamHeaders) \ + V(stream_blocked, StreamBlocked) + +// The strings are persistent/eternal v8::Strings that are set in +// the quic::BindingState. +#define QUIC_STRINGS(V) \ + V(http3_alpn, &NGHTTP3_ALPN_H3[1]) \ + V(handshake_timeout, "handshakeTimeout") \ + V(initial_max_stream_data_bidi_local, "initialMaxStreamDataBidiLocal") \ + V(initial_max_stream_data_bidi_remote, "initialMaxStreamDataBidiRemote") \ + V(initial_max_stream_data_uni, "initialMaxStreamDataUni") \ + V(initial_max_data, "initialMaxData") \ + V(initial_max_streams_bidi, "initialMaxStreamsBidi") \ + V(initial_max_streams_uni, "initialMaxStreamsUni") \ + V(ipv6_only, "ipv6Only") \ + V(max_idle_timeout, "maxIdleTimeout") \ + V(active_connection_id_limit, "activeConnectionIdLimit") \ + V(ack_delay_exponent, "ackDelayExponent") \ + V(max_ack_delay, "maxAckDelay") \ + V(max_datagram_frame_size, "maxDatagramFrameSize") \ + V(disable_active_migration, "disableActiveMigration") \ + V(reject_unauthorized, "rejectUnauthorized") \ + V(enable_tls_trace, "enableTLSTrace") \ + V(request_peer_certificate, "requestPeerCertificate") \ + V(request_ocsp, "requestOCSP") \ + V(verify_hostname_identity, "verifyHostnameIdentity") \ + V(retry_token_expiration, "retryTokenExpiration") \ + V(token_expiration, "tokenExpiration") \ + V(max_window_override, "maxWindowOverride") \ + V(max_stream_window_override, "maxStreamWindowOverride") \ + V(max_connections_per_host, "maxConnectionsPerHost") \ + V(max_connections_total, "maxConnectionsTotal") \ + V(max_stateless_resets, "maxStatelessResets") \ + V(min_dh_size, "minDHSize") \ + V(address_lru_size, "addressLRUSize") \ + V(pskcallback, "pskCallback") \ + V(retry_limit, "retryLimit") \ + V(max_payload_size, "maxPayloadSize") \ + V(unacknowledged_packet_threshold, "unacknowledgedPacketThreshold") \ + V(qlog, "qlog") \ + V(validate_address, "validateAddress") \ + V(disable_stateless_reset, "disableStatelessReset") \ + V(rx_packet_loss, "rxPacketLoss") \ + V(tx_packet_loss, "txPacketLoss") \ + V(cc_algorithm, "ccAlgorithm") \ + V(udp_receive_buffer_size, "receiveBufferSize") \ + V(udp_send_buffer_size, "sendBufferSize") \ + V(udp_ttl, "ttl") + +class BindingState final : public BaseObject, + public QuicMemoryManager { + public: + static bool Initialize(Environment* env, v8::Local target); + + static BindingState* Get(Environment* env); + + static ngtcp2_mem GetAllocator(Environment* env); + + static constexpr FastStringKey type_name { "quic" }; + BindingState(Environment* env, v8::Local object); + + BindingState(const BindingState&) = delete; + BindingState(const BindingState&&) = delete; + BindingState& operator=(const BindingState&) = delete; + BindingState& operator=(const BindingState&&) = delete; + + void MemoryInfo(MemoryTracker* tracker) const override; + SET_MEMORY_INFO_NAME(BindingState); + SET_SELF_SIZE(BindingState); + + // NgLibMemoryManager (QuicMemoryManager) + void CheckAllocatedSize(size_t previous_size) const; + void IncreaseAllocatedSize(size_t size); + void DecreaseAllocatedSize(size_t size); + + bool warn_trace_tls = true; + bool initialized = false; + +#define V(name) \ + void set_ ## name ## _constructor_template( \ + v8::Local tmpl); \ + v8::Local name ## _constructor_template() const; + QUIC_CONSTRUCTORS(V) +#undef V + +#define V(name, _) \ + void set_ ## name ## _callback(v8::Local fn); \ + v8::Local name ## _callback() const; + QUIC_JS_CALLBACKS(V) +#undef V + +#define V(name, _) v8::Local name ## _string() const; + QUIC_STRINGS(V) +#undef V + + private: + size_t current_ngtcp2_memory_ = 0; + +#define V(name) \ + v8::Global name ## _constructor_template_; + QUIC_CONSTRUCTORS(V) +#undef V + +#define V(name, _) v8::Global name ## _callback_; + QUIC_JS_CALLBACKS(V) +#undef V + +#define V(name, _) mutable v8::Eternal name ## _string_; + QUIC_STRINGS(V) +#undef V +}; + +// CIDs are used to identify endpoints participating in a QUIC session +class CID final : public MemoryRetainer { + public: + inline CID() : ptr_(&cid_) {} + + inline CID(const CID& cid) noexcept : CID(cid->data, cid->datalen) {} + + inline explicit CID(const ngtcp2_cid& cid) : CID(cid.data, cid.datalen) {} + + inline explicit CID(const ngtcp2_cid* cid) : ptr_(cid) {} + + inline CID(const uint8_t* cid, size_t len) : CID() { + ngtcp2_cid* ptr = this->cid(); + ngtcp2_cid_init(ptr, cid, len); + ptr_ = ptr; + } + + CID(CID&&cid) = delete; + + struct Hash final { + inline size_t operator()(const CID& cid) const { + size_t hash = 0; + for (size_t n = 0; n < cid->datalen; n++) { + hash ^= std::hash{}(cid->data[n]) + 0x9e3779b9 + + (hash << 6) + (hash >> 2); + } + return hash; + } + }; + + inline bool operator==(const CID& other) const noexcept { + return memcmp(cid()->data, other.cid()->data, cid()->datalen) == 0; + } + + inline bool operator!=(const CID& other) const noexcept { + return !(*this == other); + } + + inline CID& operator=(const CID& cid) noexcept { + if (this == &cid) return *this; + this->~CID(); + return *new(this) CID(cid); + } + + inline const ngtcp2_cid& operator*() const { return *ptr_; } + + inline const ngtcp2_cid* operator->() const { return ptr_; } + + inline std::string ToString() const { + std::vector dest(ptr_->datalen * 2 + 1); + dest[dest.size() - 1] = '\0'; + size_t written = StringBytes::hex_encode( + reinterpret_cast(ptr_->data), + ptr_->datalen, + dest.data(), + dest.size()); + return std::string(dest.data(), written); + } + + inline const ngtcp2_cid* cid() const { return ptr_; } + + inline const uint8_t* data() const { return ptr_->data; } + + inline operator bool() const { return ptr_->datalen > 0; } + + inline size_t length() const { return ptr_->datalen; } + + inline ngtcp2_cid* cid() { + CHECK_EQ(ptr_, &cid_); + return &cid_; + } + + inline unsigned char* data() { + return reinterpret_cast(cid()->data); + } + + inline void set_length(size_t length) { + cid()->datalen = length; + } + + SET_NO_MEMORY_INFO() + SET_MEMORY_INFO_NAME(CID) + SET_SELF_SIZE(CID) + + template using Map = std::unordered_map; + + private: + ngtcp2_cid cid_{}; + const ngtcp2_cid* ptr_; +}; + +// A serialized QUIC packet. Packets are transient. They are created, filled +// with the contents of a serialized packet, and passed off immediately to the +// Endpoint to be sent. As soon as the packet is sent, it is freed. +class Packet final : public MemoryRetainer { + public: + inline static std::unique_ptr Copy( + const std::unique_ptr& other) { + return std::make_unique(*other.get()); + } + + // The diagnostic_label is a debugging utility that is printed when debug + // output is enabled. It allows differentiated between different types of + // packets sent for different reasons since the packets themselves are + // opaque and often encrypted. + inline explicit Packet(const char* diagnostic_label = nullptr) + : ptr_(data_), + diagnostic_label_(diagnostic_label) { } + + inline Packet(size_t len, const char* diagnostic_label = nullptr) + : ptr_(len <= kDefaultMaxPacketLength ? data_ : Malloc(len)), + len_(len), + diagnostic_label_(diagnostic_label) { + CHECK_GT(len_, 0); + CHECK_NOT_NULL(ptr_); + } + + inline Packet(const Packet& other) noexcept + : Packet(other.len_, other.diagnostic_label_) { + if (UNLIKELY(len_ == 0)) return; + memcpy(ptr_, other.ptr_, len_); + } + + Packet(Packet&& other) = delete; + Packet& operator=(Packet&& other) = delete; + + inline ~Packet() { + if (ptr_ != data_) + std::unique_ptr free_me(ptr_); + } + + inline Packet& operator=(const Packet& other) noexcept { + if (this == &other) return *this; + this->~Packet(); + return *new(this) Packet(other); + } + + inline uint8_t* data() { return ptr_; } + + inline size_t length() const { return len_; } + + inline uv_buf_t buf() const { + return uv_buf_init(reinterpret_cast(ptr_), len_); + } + + inline void set_length(size_t len) { + CHECK_LE(len, len_); + len_ = len; + } + + inline const char* diagnostic_label() const { + return diagnostic_label_; + } + + void MemoryInfo(MemoryTracker* tracker) const override; + SET_MEMORY_INFO_NAME(Packet); + SET_SELF_SIZE(Packet); + + private: + uint8_t data_[kDefaultMaxPacketLength]; + uint8_t* ptr_ = nullptr; + size_t len_ = kDefaultMaxPacketLength; + const char* diagnostic_label_ = nullptr; +}; + +// A utility class that wraps ngtcp2_path to adapt it to work with SocketAddress +struct Path final : public ngtcp2_path { + Path( + const std::shared_ptr& local, + const std::shared_ptr& remote); +}; + +struct PathStorage final : public ngtcp2_path_storage { + inline PathStorage() { ngtcp2_path_storage_zero(this); } +}; + +// PreferredAddress is a helper class used only when a client QuicSession +// receives an advertised preferred address from a server. The helper provides +// information about the servers advertised preferred address. Call Use() +// to let ngtcp2 know which preferred address to use (if any). +class PreferredAddress final { + public: + enum class Policy { + IGNORE, + USE + }; + + struct Address final { + int family; + uint16_t port; + std::string address; + }; + + inline PreferredAddress( + Environment* env, + ngtcp2_addr* dest, + const ngtcp2_preferred_addr* paddr) + : env_(env), + dest_(dest), + paddr_(paddr) {} + + PreferredAddress(const PreferredAddress& other) = delete; + PreferredAddress(PreferredAddress&& other) = delete; + PreferredAddress* operator=(const PreferredAddress& other) = delete; + PreferredAddress* operator=(PreferredAddress&& other) = delete; + + // When a preferred address is advertised by a server, the + // advertisement also includes a new CID and (optionally) + // a stateless reset token. If the preferred address is + // selected, then the client Session will make use of + // these new values. Access to the cid and reset token + // are provided via the PreferredAddress class only as a + // convenience. + inline const ngtcp2_cid* cid() const { + return &paddr_->cid; + } + + // The stateless reset token associated with the preferred address CID + inline const uint8_t* stateless_reset_token() const { + return paddr_->stateless_reset_token; + } + + // A preferred address advertisement may include both an + // IPv4 and IPv6 address. Only one of which will be used. + + Address ipv4() const; + + Address ipv6() const; + + // Instructs the QuicSession to use the advertised + // preferred address matching the given family. If + // the advertisement does not include a matching + // address, the preferred address is ignored. If + // the given address cannot be successfully resolved + // using uv_getaddrinfo it is ignored. + bool Use(const Address& address) const; + + void CopyToTransportParams( + ngtcp2_transport_params* params, + const sockaddr* addr); + + private: + inline bool Resolve(const Address& address, uv_getaddrinfo_t* req) const; + + Environment* env_; + mutable ngtcp2_addr* dest_; + const ngtcp2_preferred_addr* paddr_; +}; + +// Encapsulates a QUIC Error. QUIC makes a distinction between Transport and +// Application error codes but allows the error code values to overlap. That +// is, a Transport error and Application error can both use code 1 to mean +// entirely different things. +struct QuicError final { + enum class Type { + TRANSPORT, + APPLICATION + }; + Type type; + error_code code; + + bool operator==(const QuicError& other) const noexcept { + return type == other.type && code == other.code; + } + + std::string ToString() const { + return std::to_string(code) + "(" + TypeName(*this) + ")"; + } + + static inline QuicError FromNgtcp2( + ngtcp2_connection_close_error_code close_code) { + switch (close_code.type) { + case NGTCP2_CONNECTION_CLOSE_ERROR_CODE_TYPE_TRANSPORT: + return QuicError { Type::TRANSPORT, close_code.error_code }; + case NGTCP2_CONNECTION_CLOSE_ERROR_CODE_TYPE_APPLICATION: + return QuicError { Type::APPLICATION, close_code.error_code }; + default: + UNREACHABLE(); + } + } + + static inline const char* TypeName(QuicError error) { + switch (error.type) { + case Type::TRANSPORT: return "transport"; + case Type::APPLICATION: return "application"; + default: UNREACHABLE(); + } + } +}; + +static constexpr QuicError kQuicNoError = + QuicError { QuicError::Type::TRANSPORT, NGTCP2_NO_ERROR }; + +static constexpr QuicError kQuicInternalError = + QuicError { QuicError::Type::TRANSPORT, NGTCP2_INTERNAL_ERROR }; + +static constexpr QuicError kQuicAppNoError = + QuicError { QuicError::Type::APPLICATION, NGTCP2_APP_NOERROR }; + +// A Stateless Reset Token is a mechanism by which a QUIC +// endpoint can discreetly signal to a peer that it has +// lost all state associated with a connection. This +// helper class is used to both store received tokens and +// provide storage when creating new tokens to send. +class StatelessResetToken final : public MemoryRetainer { + public: + StatelessResetToken( + uint8_t* token, + const uint8_t* secret, + const CID& cid); + + StatelessResetToken(const uint8_t* secret, const CID& cid); + + explicit inline StatelessResetToken(const uint8_t* token) { + memcpy(buf_, token, sizeof(buf_)); + } + + StatelessResetToken(const StatelessResetToken& other) + : StatelessResetToken(other.buf_) {} + + StatelessResetToken(StatelessResetToken&& other) = delete; + StatelessResetToken& operator=(StatelessResetToken&& other) = delete; + + StatelessResetToken& operator=(const StatelessResetToken& other) { + if (this == &other) return *this; + this->~StatelessResetToken(); + return *new(this) StatelessResetToken(other); + } + + inline std::string ToString() const { + std::vector dest(NGTCP2_STATELESS_RESET_TOKENLEN * 2 + 1); + dest[dest.size() - 1] = '\0'; + size_t written = StringBytes::hex_encode( + reinterpret_cast(buf_), + NGTCP2_STATELESS_RESET_TOKENLEN, + dest.data(), + dest.size()); + return std::string(dest.data(), written); + } + + inline const uint8_t* data() const { return buf_; } + + struct Hash { + inline size_t operator()(const StatelessResetToken& token) const { + size_t hash = 0; + for (size_t n = 0; n < NGTCP2_STATELESS_RESET_TOKENLEN; n++) + hash ^= std::hash{}(token.buf_[n]) + 0x9e3779b9 + + (hash << 6) + (hash >> 2); + return hash; + } + }; + + inline bool operator==(const StatelessResetToken& other) const { + return memcmp(data(), other.data(), NGTCP2_STATELESS_RESET_TOKENLEN) == 0; + } + + inline bool operator!=(const StatelessResetToken& other) const { + return !(*this == other); + } + + SET_NO_MEMORY_INFO() + SET_MEMORY_INFO_NAME(StatelessResetToken) + SET_SELF_SIZE(StatelessResetToken) + + template + using Map = + std::unordered_map< + StatelessResetToken, T, + StatelessResetToken::Hash>; + + private: + uint8_t buf_[NGTCP2_STATELESS_RESET_TOKENLEN]{}; +}; + +// The https://tools.ietf.org/html/draft-ietf-quic-load-balancers-06 +// specification defines a model for creation of Connection Identifiers +// (CIDs) that embed information usable by load balancers for intelligently +// routing QUIC packets and sessions. +class RoutableConnectionIDStrategy { + public: + virtual void NewConnectionID( + ngtcp2_cid* cid, + size_t length_hint = NGTCP2_MAX_CIDLEN) = 0; + + inline void NewConnectionID( + CID* cid, + size_t length_hint = NGTCP2_MAX_CIDLEN) { + NewConnectionID(cid->cid(), length_hint); + } + + virtual void UpdateCIDState(const CID& cid) = 0; +}; + +template +class RoutableConnectionIDStrategyImpl final + : public RoutableConnectionIDStrategy, + public MemoryRetainer { + public: + using Options = typename Traits::Options; + using State = typename Traits::State; + + RoutableConnectionIDStrategyImpl( + Session* session, + const Options& options) + : session_(session), + options_(options) {} + + void NewConnectionID( + ngtcp2_cid* cid, + size_t length_hint = NGTCP2_MAX_CIDLEN) override { + Traits::NewConnectionID( + options_, + &state_, + session_, + cid, + length_hint); + } + + void UpdateCIDState(const CID& cid) override { + Traits::UpdateCIDState(options_, &state_, session_, cid); + } + + const Options& options() const { return options_; } + + SET_NO_MEMORY_INFO() + SET_MEMORY_INFO_NAME(RoutableConnectionIDStrategyImpl) + SET_SELF_SIZE(RoutableConnectionIDStrategyImpl) + + private: + Session* session_; + const Options options_; + State state_; +}; + +class RoutableConnectionIDConfig { + public: + virtual std::unique_ptr NewInstance( + Session* session) = 0; +}; + +template +class RoutableConnectionIDConfigImpl final + : public RoutableConnectionIDConfig, + public MemoryRetainer { + public: + using Options = typename Traits::Options; + + std::unique_ptr NewInstance( + Session* session) override { + return std::make_unique>( + session, options()); + } + + Options& operator*() { return options_; } + Options* operator->() { return &options_; } + const Options& options() const { return options_; } + + SET_NO_MEMORY_INFO() + SET_MEMORY_INFO_NAME(RoutableConnectionIDConfig) + SET_SELF_SIZE(RoutableConnectionIDConfigImpl) + + private: + Options options_; +}; + +struct RandomConnectionIDTraits final { + struct Options final {}; + struct State final {}; + + static void NewConnectionID( + const Options& options, + State* state, + Session* session, + ngtcp2_cid* cid, + size_t length_hint); + + static void UpdateCIDState( + const Options& options, + State* state, + Session* session, + const CID& cid) {} + + static constexpr const char* name = "RandomConnectionIDStrategy"; + + static void New(const v8::FunctionCallbackInfo& args); + + static bool HasInstance(Environment* env, v8::Local value); + static v8::Local GetConstructorTemplate( + Environment* env); +}; + +template +class RoutableConnectionIDStrategyBase final : public BaseObject { + public: + static v8::Local GetConstructorTemplate( + Environment* env) { + return Traits::GetConstructorTemplate(env); + } + + static bool HasInstance(Environment* env, const v8::Local& value) { + return GetConstructorTemplate(env)->HasInstance(value); + } + + static void Initialize(Environment* env, v8::Local target) { + env->SetConstructorFunction( + target, + Traits::name, + GetConstructorTemplate(env)); + } + + RoutableConnectionIDStrategyBase( + Environment* env, + v8::Local object, + std::shared_ptr> strategy = + std::make_shared>()) + : BaseObject(env, object), + strategy_(std::move(strategy)) { + MakeWeak(); + } + + RoutableConnectionIDConfig* strategy() const { + return strategy_.get(); + } + + void MemoryInfo(MemoryTracker* tracker) const override { + tracker->TrackField("strategy", strategy_); + } + + SET_MEMORY_INFO_NAME(RoutableConnectionIDStrategyBase) + SET_SELF_SIZE(RoutableConnectionIDStrategyBase) + + class TransferData final : public worker::TransferData { + public: + TransferData( + std::shared_ptr> strategy) + : strategy_(strategy) {} + + BaseObjectPtr Deserialize( + Environment* env, + v8::Local context, + std::unique_ptr self) { + v8::Local obj; + if (!GetConstructorTemplate(env) + ->InstanceTemplate() + ->NewInstance(context).ToLocal(&obj)) { + return BaseObjectPtr(); + } + return MakeBaseObject>( + env, obj, std::move(strategy_)); + } + + void MemoryInfo(MemoryTracker* tracker) const override { + tracker->TrackField("strategy", strategy_); + } + + SET_MEMORY_INFO_NAME(RoutableConnectionIDStrategyBase::TransferData) + SET_SELF_SIZE(TransferData) + + private: + std::shared_ptr> strategy_; + }; + + TransferMode GetTransferMode() const override { + return TransferMode::kCloneable; + } + + std::unique_ptr CloneForMessaging() const override { + return std::make_unique(strategy_); + } + + private: + std::shared_ptr> strategy_; +}; + +using RandomConnectionIDBase = + RoutableConnectionIDStrategyBase; + +using RandomConnectionIDConfig = + RoutableConnectionIDConfigImpl; + +void IllegalConstructor(const v8::FunctionCallbackInfo& args); + +} // namespace quic +} // namespace node + +#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS +#endif // SRC_QUIC_QUIC_H_ diff --git a/src/quic/session.cc b/src/quic/session.cc new file mode 100644 index 00000000000000..c5358566352022 --- /dev/null +++ b/src/quic/session.cc @@ -0,0 +1,3761 @@ +#include "quic/buffer.h" +#include "quic/crypto.h" +#include "quic/endpoint.h" +#include "quic/qlog.h" +#include "quic/quic.h" +#include "quic/session.h" +#include "quic/stream.h" +#include "crypto/crypto_common.h" +#include "crypto/x509.h" +#include "aliased_struct-inl.h" +#include "async_wrap-inl.h" +#include "base_object-inl.h" +#include "debug_utils-inl.h" +#include "env-inl.h" +#include "memory_tracker-inl.h" +#include "node_bob-inl.h" +#include "node_http_common-inl.h" +#include "node_process.h" +#include "node_sockaddr-inl.h" +#include "v8.h" + +#include + +namespace node { + +using v8::Array; +using v8::ArrayBuffer; +using v8::BackingStore; +using v8::BigInt; +using v8::Context; +using v8::FunctionCallbackInfo; +using v8::FunctionTemplate; +using v8::HandleScope; +using v8::Int32; +using v8::Integer; +using v8::Just; +using v8::Local; +using v8::Maybe; +using v8::MaybeLocal; +using v8::Nothing; +using v8::Number; +using v8::Object; +using v8::PropertyAttribute; +using v8::String; +using v8::Uint32; +using v8::Undefined; +using v8::Value; + +namespace quic { + +namespace { +inline size_t get_max_pkt_len(const std::shared_ptr& addr) { + return addr->family() == AF_INET6 ? + NGTCP2_MAX_PKTLEN_IPV6 : + NGTCP2_MAX_PKTLEN_IPV4; +} + +inline bool is_ngtcp2_debug_enabled(Environment* env) { + return env->enabled_debug_list()->enabled(DebugCategory::NGTCP2_DEBUG); +} + +// Forwards detailed(verbose) debugging information from ngtcp2. Enabled using +// the NODE_DEBUG_NATIVE=NGTCP2_DEBUG category. +void Ngtcp2DebugLog(void* user_data, const char* fmt, ...) { + va_list ap; + va_start(ap, fmt); + std::string format(fmt, strlen(fmt) + 1); + format[strlen(fmt)] = '\n'; + // Debug() does not work with the va_list here. So we use vfprintf + // directly instead. Ngtcp2DebugLog is only enabled when the debug + // category is enabled. + vfprintf(stderr, format.c_str(), ap); + va_end(ap); +} + +void OnQlogWrite( + void* user_data, + uint32_t flags, + const void* data, + size_t len) { + Session* session = static_cast(user_data); + Environment* env = session->env(); + + // Fun fact... ngtcp2 does not emit the final qlog statement until the + // ngtcp2_conn object is destroyed. Ideally, destroying is explicit, + // but sometimes the Session object can be garbage collected without + // being explicitly destroyed. During those times, we cannot call out + // to JavaScript. Because we don't know for sure if we're in in a GC + // when this is called, it is safer to just defer writes to immediate. + BaseObjectPtr ptr = session->qlogstream(); + std::vector buffer(len); + memcpy(buffer.data(), data, len); + env->SetImmediate([ptr = std::move(ptr), + buffer = std::move(buffer), + flags](Environment*) { + ptr->Emit(buffer.data(), buffer.size(), flags); + }); +} + +inline ConnectionCloseFn SelectCloseFn(QuicError error) { + switch (error.type) { + case QuicError::Type::TRANSPORT: + return ngtcp2_conn_write_connection_close; + case QuicError::Type::APPLICATION: + return ngtcp2_conn_write_application_close; + default: + UNREACHABLE(); + } +} + +inline void Consume(ngtcp2_vec** pvec, size_t* pcnt, size_t len) { + ngtcp2_vec* v = *pvec; + size_t cnt = *pcnt; + + for (; cnt > 0; --cnt, ++v) { + if (v->len > len) { + v->len -= len; + v->base += len; + break; + } + len -= v->len; + } + + *pvec = v; + *pcnt = cnt; +} + +inline int IsEmpty(const ngtcp2_vec* vec, size_t cnt) { + size_t i; + for (i = 0; i < cnt && vec[i].len == 0; ++i) {} + return i == cnt; +} + +template +size_t get_length(const T* vec, size_t count) { + CHECK_NOT_NULL(vec); + size_t len = 0; + for (size_t n = 0; n < count; n++) + len += vec[n].len; + return len; +} +} // namespace + +Session::Config::Config( + Endpoint* endpoint, + const CID& dcid_, + const CID& scid_, + quic_version version_) + : version(version_), + dcid(dcid_), + scid(scid_) { + ngtcp2_settings_default(this); + initial_ts = uv_hrtime(); + if (UNLIKELY(is_ngtcp2_debug_enabled(endpoint->env()))) + log_printf = Ngtcp2DebugLog; + + Endpoint::Config config = endpoint->config(); + + cc_algo = config.cc_algorithm; + max_udp_payload_size = config.max_payload_size; + + if (config.max_window_override > 0) + max_window = config.max_window_override; + + if (config.max_stream_window_override > 0) + max_stream_window = config.max_stream_window_override; + + if (config.unacknowledged_packet_threshold > 0) + ack_thresh = config.unacknowledged_packet_threshold; +} + +Session::Config::Config(Endpoint* endpoint, quic_version version) + : Config(endpoint, CID(), CID(), version) {} + +void Session::Config::EnableQLog(const CID& ocid) { + qlog = { (*ocid), OnQlogWrite }; + this->ocid = ocid; +} + +Session::Options::Options(const Session::Options& other) + : alpn(other.alpn), + hostname(other.hostname), + cid_strategy(other.cid_strategy), + cid_strategy_strong_ref(other.cid_strategy_strong_ref), + dcid(other.dcid), + preferred_address_strategy(other.preferred_address_strategy), + preferred_address_ipv4(other.preferred_address_ipv4), + preferred_address_ipv6(other.preferred_address_ipv6), + initial_max_stream_data_bidi_local( + other.initial_max_stream_data_bidi_local), + initial_max_stream_data_bidi_remote( + other.initial_max_stream_data_bidi_remote), + initial_max_stream_data_uni(other.initial_max_stream_data_uni), + initial_max_data(other.initial_max_data), + initial_max_streams_bidi(other.initial_max_streams_bidi), + initial_max_streams_uni(other.initial_max_streams_uni), + max_idle_timeout(other.max_idle_timeout), + active_connection_id_limit(other.active_connection_id_limit), + ack_delay_exponent(other.ack_delay_exponent), + max_ack_delay(other.max_ack_delay), + max_datagram_frame_size(other.max_datagram_frame_size), + disable_active_migration(other.disable_active_migration), + reject_unauthorized(other.reject_unauthorized), + enable_tls_trace(other.enable_tls_trace), + request_peer_certificate(other.request_peer_certificate), + request_ocsp(other.request_ocsp), + verify_hostname_identity(other.verify_hostname_identity), + handshake_timeout(other.handshake_timeout), + min_dh_size(other.min_dh_size), + psk_callback_present(other.psk_callback_present), + session_id_ctx(other.session_id_ctx), + resume(other.resume), + early_transport_params(other.early_transport_params), + early_session_ticket(other.early_session_ticket) {} + +Session::TransportParams::TransportParams( + const std::shared_ptr& options, + const CID& scid, + const CID& ocid) { + ngtcp2_transport_params_default(this); + active_connection_id_limit = options->active_connection_id_limit; + initial_max_stream_data_bidi_local = + options->initial_max_stream_data_bidi_local; + initial_max_stream_data_bidi_remote = + options->initial_max_stream_data_bidi_remote; + initial_max_stream_data_uni = options->initial_max_stream_data_uni; + initial_max_streams_bidi = options->initial_max_streams_bidi; + initial_max_streams_uni = options->initial_max_streams_uni; + initial_max_data = options->initial_max_data; + max_idle_timeout = options->max_idle_timeout; + max_ack_delay = options->max_ack_delay; + ack_delay_exponent = options->ack_delay_exponent; + max_datagram_frame_size = options->max_datagram_frame_size; + disable_active_migration = options->disable_active_migration ? 1 : 0; + preferred_address_present = 0; + stateless_reset_token_present = 0; + retry_scid_present = 0; + + if (ocid) { + original_dcid = *ocid; + if (scid) { + retry_scid = *scid; + retry_scid_present = 1; + } + } else { + original_dcid = *scid; + } + + if (options->preferred_address_ipv4) + SetPreferredAddress(options->preferred_address_ipv4); + + if (options->preferred_address_ipv6) + SetPreferredAddress(options->preferred_address_ipv6); +} + +void Session::TransportParams::SetPreferredAddress( + const std::shared_ptr& address) { + preferred_address_present = 1; + switch (address->family()) { + case AF_INET: { + const sockaddr_in* src = + reinterpret_cast(address->data()); + memcpy(preferred_address.ipv4_addr, + &src->sin_addr, + sizeof(preferred_address.ipv4_addr)); + preferred_address.ipv4_port = address->port(); + break; + } + case AF_INET6: { + const sockaddr_in6* src = + reinterpret_cast(address->data()); + memcpy(preferred_address.ipv6_addr, + &src->sin6_addr, + sizeof(preferred_address.ipv6_addr)); + preferred_address.ipv6_port = address->port(); + break; + } + default: + UNREACHABLE(); + } +} + +void Session::TransportParams::GenerateStatelessResetToken( + EndpointWrap* endpoint, + const CID& cid) { + CHECK(cid); + stateless_reset_token_present = 1; + StatelessResetToken token( + stateless_reset_token, + endpoint->config().reset_token_secret, + cid); +} + +void Session::TransportParams::GeneratePreferredAddressToken( + RoutableConnectionIDStrategy* connection_id_strategy, + Session* session, + CID* pscid) { + CHECK(pscid); + connection_id_strategy->NewConnectionID(pscid); + preferred_address.cid = **pscid; + StatelessResetToken( + preferred_address.stateless_reset_token, + session->endpoint()->config().reset_token_secret, + *pscid); +} + +Session::CryptoContext::CryptoContext( + Session* session, + const std::shared_ptr& options, + const BaseObjectPtr& context, + ngtcp2_crypto_side side) : + session_(session), + secure_context_(context), + side_(side), + options_(options) { + CHECK(secure_context_); + ssl_.reset(SSL_new(secure_context_->ctx_.get())); + CHECK(ssl_); + if (side == NGTCP2_CRYPTO_SIDE_CLIENT) + MaybeSetEarlySession(options); +} + +Session::CryptoContext::~CryptoContext() { + USE(Cancel()); +} + +void Session::CryptoContext::MaybeSetEarlySession( + const std::shared_ptr& options) { + crypto::SSLSessionPointer ticket(options->early_session_ticket); + if (options->early_transport_params == nullptr || !ticket) + return; + + early_data_ = + SSL_SESSION_get_max_early_data(ticket.get()) == 0xffffffffUL; + + if (LIKELY(early_data())) { + ngtcp2_conn_set_early_remote_transport_params( + session()->connection(), + options->early_transport_params); + + // We don't care about the return value here. The early + // data will just be ignored if it's invalid. + USE(crypto::SetTLSSession(ssl_, ticket)); + } +} + +void Session::CryptoContext::AcknowledgeCryptoData( + ngtcp2_crypto_level level, + uint64_t datalen) { + // It is possible for the Session to have been destroyed but not yet + // deconstructed. In such cases, we want to ignore the callback as there + // is nothing to do but wait for further cleanup to happen. + if (UNLIKELY(session_->is_destroyed())) + return; + Debug(session(), + "Acknowledging %" PRIu64 " crypto bytes for %s level", + datalen, + crypto_level_name(level)); + + // Consumes (frees) the given number of bytes in the handshake buffer. + handshake_[level].Acknowledge(static_cast(datalen)); +} + +size_t Session::CryptoContext::Cancel() { + size_t len = + handshake_[0].remaining() + + handshake_[1].remaining() + + handshake_[2].remaining(); + handshake_[0].Clear(); + handshake_[1].Clear(); + handshake_[2].Clear(); + return len; +} + +void Session::CryptoContext::Initialize() { + InitializeTLS(session(), ssl_); +} + +void Session::CryptoContext::EnableTrace() { +#if HAVE_SSL_TRACE + if (!bio_trace_) { + bio_trace_.reset(BIO_new_fp(stderr, BIO_NOCLOSE | BIO_FP_TEXT)); + SSL_set_msg_callback( + ssl_.get(), + [](int write_p, + int version, + int content_type, + const void* buf, + size_t len, + SSL* ssl, + void* arg) -> void { + crypto::MarkPopErrorOnReturn mark_pop_error_on_return; + SSL_trace(write_p, version, content_type, buf, len, ssl, arg); + }); + SSL_set_msg_callback_arg(ssl_.get(), bio_trace_.get()); + } +#endif +} + +std::shared_ptr Session::CryptoContext::ocsp_response( + bool release) { + return LIKELY(release) ? std::move(ocsp_response_) : ocsp_response_; +} + +std::string Session::CryptoContext::selected_alpn() const { + const unsigned char* alpn_buf = nullptr; + unsigned int alpnlen; + SSL_get0_alpn_selected(ssl_.get(), &alpn_buf, &alpnlen); + return alpnlen ? + std::string(reinterpret_cast(alpn_buf), alpnlen) : + std::string(); +} + +ngtcp2_crypto_level Session::CryptoContext::read_crypto_level() const { + return from_ossl_level(SSL_quic_read_level(ssl_.get())); +} + +ngtcp2_crypto_level Session::CryptoContext::write_crypto_level() const { + return from_ossl_level(SSL_quic_write_level(ssl_.get())); +} + +void Session::CryptoContext::Keylog(const char* line) { + Environment* env = session_->env(); + BindingState* state = env->GetBindingData(env->context()); + + HandleScope handle_scope(env->isolate()); + Context::Scope context_scope(env->context()); + + Session::CallbackScope cb_scope(session()); + + size_t len = strlen(line); + if (len == 0) return; + + std::shared_ptr buf = + ArrayBuffer::NewBackingStore(env->isolate(), 1 + strlen(line)); + memcpy(buf->Data(), line, len); + (reinterpret_cast(buf->Data()))[len] = '\n'; + + Local ab = ArrayBuffer::New(env->isolate(), buf); + + // Grab a shared pointer to this to prevent the Session + // from being freed while the MakeCallback is running. + BaseObjectPtr ptr(session_); + USE(state->session_keylog_callback()->Call( + env->context(), + session()->object(), + 1, &ab)); +} + +int Session::CryptoContext::OnClientHello() { + if (LIKELY(session_->state_->client_hello_enabled == 0)) + return 0; + + Environment* env = session_->env(); + CallbackScope callback_scope(this); + if (in_client_hello_) + return -1; + in_client_hello_ = true; + + BindingState* state = env->GetBindingData(env->context()); + HandleScope scope(env->isolate()); + Context::Scope context_scope(env->context()); + + // Why this instead of using MakeCallback? We need to catch any + // errors that happen both when preparing the arguments and + // invoking the callback so that we can properly signal a failure + // to the peer. + Session::CallbackScope cb_scope(session()); + + Local argv[3]; + + Session::CryptoContext* crypto_context = session()->crypto_context(); + + if (!crypto_context->hello_alpn(env).ToLocal(&argv[0]) || + !crypto_context->hello_servername(env).ToLocal(&argv[1]) || + !crypto_context->hello_ciphers(env).ToLocal(&argv[2])) { + return 0; + } + + // Grab a shared pointer to this to prevent the Session + // from being freed while the MakeCallback is running. + BaseObjectPtr ptr(session()); + USE(state->session_client_hello_callback()->Call( + env->context(), + session()->object(), + arraysize(argv), + argv)); + + // Returning -1 here will keep the TLS handshake paused until the + // client hello callback is invoked. Returning 0 means that the + // handshake is ready to proceed. When the OnClientHello callback + // is called above, it may be resolved synchronously or asynchronously. + // In case it is resolved synchronously, we need the check below. + return in_client_hello_ ? -1 : 0; +} + +void Session::CryptoContext::OnClientHelloDone( + BaseObjectPtr context) { + Debug(session(), + "ClientHello completed. Context Provided? %s\n", + context ? "Yes" : "No"); + + // Continue the TLS handshake when this function exits + // otherwise it will stall and fail. + HandshakeScope handshake_scope( + this, + [this]() { in_client_hello_ = false; }); + + // Disable the callback at this point so we don't loop continuously + session_->state_->client_hello_enabled = 0; + + if (context) { + int err = crypto::UseSNIContext(ssl_, context); + if (!err) { + unsigned long err = ERR_get_error(); // NOLINT(runtime/int) + return !err ? + THROW_ERR_QUIC_FAILURE_SETTING_SNI_CONTEXT(session_->env()) : + crypto::ThrowCryptoError(session_->env(), err); + } + secure_context_ = context; + } +} + +int Session::CryptoContext::OnOCSP() { + if (LIKELY(session_->state_->ocsp_enabled == 0)) { + Debug(session(), "No OCSPRequest handler registered"); + return 1; + } + + if (!session_->is_server()) + return 1; + + Debug(session(), "Client is requesting an OCSP Response"); + CallbackScope callback_scope(this); + + // As in node_crypto.cc, this is not an error, but does suspend the + // handshake to continue when OnOCSP is complete. + if (in_ocsp_request_) + return -1; + in_ocsp_request_ = true; + + Environment* env = session()->env(); + BindingState* state = env->GetBindingData(env->context()); + HandleScope scope(env->isolate()); + Context::Scope context_scope(env->context()); + Session::CallbackScope cb_scope(session()); + BaseObjectPtr ptr(session()); + USE(state->session_ocsp_request_callback()->Call( + env->context(), + session()->object(), + 0, nullptr)); + + // Returning -1 here means that we are still waiting for the OCSP + // request to be completed. When the OnCert handler is invoked + // above, it can be resolve synchronously or asynchonously. If + // resolved synchronously, we need the check below. + return in_ocsp_request_ ? -1 : 1; +} + +void Session::CryptoContext::OnOCSPDone( + std::shared_ptr ocsp_response) { + Debug(session(), "OCSPRequest completed. Response Provided"); + HandshakeScope handshake_scope( + this, + [this]() { in_ocsp_request_ = false; }); + session_->state_->ocsp_enabled = 0; + ocsp_response_ = std::move(ocsp_response); +} + +bool Session::CryptoContext::OnSecrets( + ngtcp2_crypto_level level, + const uint8_t* rx_secret, + const uint8_t* tx_secret, + size_t secretlen) { + + Debug(session(), + "Received secrets for %s crypto level", + crypto_level_name(level)); + + if (!SetSecrets(level, rx_secret, tx_secret, secretlen)) + return false; + + if (level == NGTCP2_CRYPTO_LEVEL_APPLICATION) { + session_->set_remote_transport_params(); + if (!session()->InitApplication()) + return false; + } + + return true; +} + +int Session::CryptoContext::OnTLSStatus() { + Environment* env = session_->env(); + HandleScope scope(env->isolate()); + Context::Scope context_scope(env->context()); + switch (side_) { + case NGTCP2_CRYPTO_SIDE_SERVER: { + if (!ocsp_response_) { + Debug(session(), "There is no OCSP response"); + return SSL_TLSEXT_ERR_NOACK; + } + + size_t len = ocsp_response_->ByteLength(); + Debug(session(), "There is an OCSP response of %d bytes", len); + + unsigned char* data = crypto::MallocOpenSSL(len); + memcpy(data, ocsp_response_->Data(), len); + + if (!SSL_set_tlsext_status_ocsp_resp(ssl_.get(), data, len)) + OPENSSL_free(data); + + ocsp_response_.reset(); + return SSL_TLSEXT_ERR_OK; + } + case NGTCP2_CRYPTO_SIDE_CLIENT: { + // Only invoke the callback if the ocsp handler is actually set + if (LIKELY(session_->state_->ocsp_enabled == 0) || !ocsp_response_) + return 1; + Local res = ArrayBuffer::New(env->isolate(), ocsp_response_); + + BindingState* state = env->GetBindingData(env->context()); + HandleScope scope(env->isolate()); + Context::Scope context_scope(env->context()); + Session::CallbackScope cb_scope(session()); + BaseObjectPtr ptr(session()); + USE(state->session_ocsp_response_callback()->Call( + env->context(), + session()->object(), + 1, &res)); + return 1; + } + default: + UNREACHABLE(); + } +} + +int Session::CryptoContext::Receive( + ngtcp2_crypto_level crypto_level, + uint64_t offset, + const uint8_t* data, + size_t datalen) { + if (UNLIKELY(session_->is_destroyed())) + return NGTCP2_ERR_CALLBACK_FAILURE; + + Debug(session(), "Receiving %d bytes of crypto data", datalen); + + // Internally, this passes the handshake data off to openssl + // for processing. The handshake may or may not complete. + int ret = ngtcp2_crypto_read_write_crypto_data( + session_->connection(), + crypto_level, + data, + datalen); + switch (ret) { + case 0: + return 0; + // In either of following cases, the handshake is being + // paused waiting for user code to take action (for instance + // OCSP requests or client hello modification) + case NGTCP2_CRYPTO_OPENSSL_ERR_TLS_WANT_X509_LOOKUP: + Debug(session(), "TLS handshake wants X509 Lookup"); + return 0; + case NGTCP2_CRYPTO_OPENSSL_ERR_TLS_WANT_CLIENT_HELLO_CB: + Debug(session(), "TLS handshake wants client hello callback"); + return 0; + default: + return ret; + } +} + +void Session::CryptoContext::ResumeHandshake() { + Receive(read_crypto_level(), 0, nullptr, 0); + session_->SendPendingData(); +} + +MaybeLocal Session::CryptoContext::cert(Environment* env) const { + return crypto::X509Certificate::GetCert(env, ssl_); +} + +MaybeLocal Session::CryptoContext::peer_cert(Environment* env) const { + crypto::X509Certificate::GetPeerCertificateFlag flag = session_->is_server() + ? crypto::X509Certificate::GetPeerCertificateFlag::SERVER + : crypto::X509Certificate::GetPeerCertificateFlag::NONE; + return crypto::X509Certificate::GetPeerCert(env, ssl_, flag); +} + +MaybeLocal Session::CryptoContext::cipher_name(Environment* env) const { + return crypto::GetCipherName(env, ssl_); +} + +MaybeLocal Session::CryptoContext::cipher_version( + Environment* env) const { + return crypto::GetCipherVersion(env, ssl_); +} + +MaybeLocal Session::CryptoContext::ephemeral_key( + Environment* env) const { + return crypto::GetEphemeralKey(env, ssl_); +} + +MaybeLocal Session::CryptoContext::hello_ciphers( + Environment* env) const { + return crypto::GetClientHelloCiphers(env, ssl_); +} + +MaybeLocal Session::CryptoContext::hello_servername( + Environment* env) const { + return OneByteString(env->isolate(), crypto::GetClientHelloServerName(ssl_)); +} + +MaybeLocal Session::CryptoContext::hello_alpn( + Environment* env) const { + return OneByteString(env->isolate(), crypto::GetClientHelloALPN(ssl_)); +} + +std::string Session::CryptoContext::servername() const { + return crypto::GetServerName(ssl_.get()); +} + +void Session::CryptoContext::set_tls_alert(int err) { + Debug(session(), "TLS Alert [%d]: %s", err, SSL_alert_type_string_long(err)); + session_->set_last_error({ + QuicError::Type::TRANSPORT, + static_cast(NGTCP2_CRYPTO_ERROR | err) + }); +} + +void Session::CryptoContext::WriteHandshake( + ngtcp2_crypto_level level, + const uint8_t* data, + size_t datalen) { + Debug(session(), + "Writing %d bytes of %s handshake data.", + datalen, + crypto_level_name(level)); + + std::unique_ptr store = + ArrayBuffer::NewBackingStore( + session()->env()->isolate(), + datalen); + memcpy(store->Data(), data, datalen); + + CHECK_EQ( + ngtcp2_conn_submit_crypto_data( + session_->connection(), + level, + static_cast(store->Data()), + datalen), 0); + + handshake_[level].Push(std::move(store), datalen); +} + +bool Session::CryptoContext::InitiateKeyUpdate() { + if (UNLIKELY(session_->is_destroyed()) || in_key_update_) + return false; + + // There's no user code that should be able to run while UpdateKey + // is running, but we need to gate on it just to be safe. + auto leave = OnScopeLeave([this]() { in_key_update_ = false; }); + in_key_update_ = true; + Debug(session(), "Initiating key update"); + + session_->IncrementStat(&SessionStats::keyupdate_count); + + return ngtcp2_conn_initiate_key_update( + session_->connection(), + uv_hrtime()) == 0; +} + +int Session::CryptoContext::VerifyPeerIdentity() { + return crypto::VerifyPeerCertificate(ssl_); +} + +bool Session::CryptoContext::early_data() const { + return (early_data_ && + SSL_get_early_data_status(ssl_.get()) == SSL_EARLY_DATA_ACCEPTED) || + SSL_get_max_early_data(ssl_.get()) == 0xffffffffUL; +} + +void Session::CryptoContext::MemoryInfo(MemoryTracker* tracker) const { + tracker->TrackField("initial_crypto", handshake_[0]); + tracker->TrackField("handshake_crypto", handshake_[1]); + tracker->TrackField("app_crypto", handshake_[2]); + tracker->TrackFieldWithSize( + "ocsp_response", + ocsp_response_ ? ocsp_response_->ByteLength() : 0); +} + +bool Session::CryptoContext::SetSecrets( + ngtcp2_crypto_level level, + const uint8_t* rx_secret, + const uint8_t* tx_secret, + size_t secretlen) { + + static constexpr int kCryptoKeylen = 64; + static constexpr int kCryptoIvlen = 64; + static constexpr char kQuicClientEarlyTrafficSecret[] = + "QUIC_CLIENT_EARLY_TRAFFIC_SECRET"; + static constexpr char kQuicClientHandshakeTrafficSecret[] = + "QUIC_CLIENT_HANDSHAKE_TRAFFIC_SECRET"; + static constexpr char kQuicClientTrafficSecret0[] = + "QUIC_CLIENT_TRAFFIC_SECRET_0"; + static constexpr char kQuicServerHandshakeTrafficSecret[] = + "QUIC_SERVER_HANDSHAKE_TRAFFIC_SECRET"; + static constexpr char kQuicServerTrafficSecret[] = + "QUIC_SERVER_TRAFFIC_SECRET_0"; + + uint8_t rx_key[kCryptoKeylen]; + uint8_t rx_hp[kCryptoKeylen]; + uint8_t tx_key[kCryptoKeylen]; + uint8_t tx_hp[kCryptoKeylen]; + uint8_t rx_iv[kCryptoIvlen]; + uint8_t tx_iv[kCryptoIvlen]; + + if (NGTCP2_ERR(ngtcp2_crypto_derive_and_install_rx_key( + session()->connection(), + rx_key, + rx_iv, + rx_hp, + level, + rx_secret, + secretlen))) { + return false; + } + + if (NGTCP2_ERR(ngtcp2_crypto_derive_and_install_tx_key( + session()->connection(), + tx_key, + tx_iv, + tx_hp, + level, + tx_secret, + secretlen))) { + return false; + } + + switch (level) { + case NGTCP2_CRYPTO_LEVEL_EARLY: + crypto::LogSecret( + ssl_, + kQuicClientEarlyTrafficSecret, + rx_secret, + secretlen); + break; + case NGTCP2_CRYPTO_LEVEL_HANDSHAKE: + crypto::LogSecret( + ssl_, + kQuicClientHandshakeTrafficSecret, + rx_secret, + secretlen); + crypto::LogSecret( + ssl_, + kQuicServerHandshakeTrafficSecret, + tx_secret, + secretlen); + break; + case NGTCP2_CRYPTO_LEVEL_APPLICATION: + crypto::LogSecret( + ssl_, + kQuicClientTrafficSecret0, + rx_secret, + secretlen); + crypto::LogSecret( + ssl_, + kQuicServerTrafficSecret, + tx_secret, + secretlen); + break; + default: + UNREACHABLE(); + } + + return true; +} + +void Session::IgnorePreferredAddressStrategy( + Session* session, + const PreferredAddress& preferred_address) { + Debug(session, "Ignoring server preferred address"); +} + +void Session::UsePreferredAddressStrategy( + Session* session, + const PreferredAddress& preferred_address) { + int family = session->endpoint()->local_address()->family(); + PreferredAddress::Address address = family == AF_INET + ? preferred_address.ipv4() + : preferred_address.ipv6(); + + if (!preferred_address.Use(address)) { + Debug(session, "Not using server preferred address"); + return; + } + + Debug(session, "Using server preferred address"); + if (UNLIKELY(session->state_->use_preferred_address_enabled == 1)) + session->UsePreferredAddress(address); +} + +bool Session::HasInstance(Environment* env, const Local& value) { + return GetConstructorTemplate(env)->HasInstance(value); +} + +Local Session::GetConstructorTemplate(Environment* env) { + BindingState* state = env->GetBindingData(env->context()); + CHECK_NOT_NULL(state); + Local tmpl = state->session_constructor_template(); + if (tmpl.IsEmpty()) { + tmpl = FunctionTemplate::New(env->isolate()); + tmpl->SetClassName(FIXED_ONE_BYTE_STRING(env->isolate(), "Session")); + tmpl->Inherit(AsyncWrap::GetConstructorTemplate(env)); + tmpl->InstanceTemplate()->SetInternalFieldCount( + Session::kInternalFieldCount); + env->SetProtoMethodNoSideEffect( + tmpl, + "getRemoteAddress", + GetRemoteAddress); + env->SetProtoMethodNoSideEffect( + tmpl, + "getCertificate", + GetCertificate); + env->SetProtoMethodNoSideEffect( + tmpl, + "getPeerCertificate", + GetPeerCertificate); + env->SetProtoMethodNoSideEffect( + tmpl, + "getEphemeralKeyInfo", + GetEphemeralKeyInfo); + env->SetProtoMethod(tmpl, "destroy", DoDestroy); + env->SetProtoMethod(tmpl, "gracefulClose", GracefulClose); + env->SetProtoMethod(tmpl, "silentClose", SilentClose); + env->SetProtoMethod(tmpl, "updateKey", UpdateKey); + env->SetProtoMethod(tmpl, "attachToEndpoint", DoAttachToEndpoint); + env->SetProtoMethod(tmpl, "detachFromEndpoint", DoDetachFromEndpoint); + env->SetProtoMethod(tmpl, "onClientHelloDone", OnClientHelloDone); + env->SetProtoMethod(tmpl, "onOCSPDone", OnOCSPDone); + state->set_session_constructor_template(tmpl); + } + return tmpl; +} + +void Session::Initialize(Environment* env, Local target) { + USE(GetConstructorTemplate(env)); + + OptionsObject::Initialize(env, target); + +#define V(name, _, __) NODE_DEFINE_CONSTANT(target, IDX_STATS_SESSION_##name); + SESSION_STATS(V) + NODE_DEFINE_CONSTANT(target, IDX_STATS_SESSION_COUNT); +#undef V +#define V(name, _, __) NODE_DEFINE_CONSTANT(target, IDX_STATE_SESSION_##name); + SESSION_STATE(V) + NODE_DEFINE_CONSTANT(target, IDX_STATE_SESSION_COUNT); +#undef V +} + +BaseObjectPtr Session::CreateClient( + EndpointWrap* endpoint, + const std::shared_ptr& local_addr, + const std::shared_ptr& remote_addr, + const Config& config, + const std::shared_ptr& options, + const BaseObjectPtr& context) { + Local obj; + Local tmpl = GetConstructorTemplate(endpoint->env()); + CHECK(!tmpl.IsEmpty()); + if (!tmpl->InstanceTemplate()->NewInstance(endpoint->env()->context()) + .ToLocal(&obj)) { + return BaseObjectPtr(); + } + + BaseObjectPtr session = + MakeBaseObject( + endpoint, + obj, + local_addr, + remote_addr, + config, + options, + context, + config.version); + if (session) session->SendPendingData(); + return session; +} + +// Static function to create a new server Session instance +BaseObjectPtr Session::CreateServer( + EndpointWrap* endpoint, + const std::shared_ptr& local_addr, + const std::shared_ptr& remote_addr, + const Config& config, + const std::shared_ptr& options, + const BaseObjectPtr& context) { + Local obj; + Local tmpl = GetConstructorTemplate(endpoint->env()); + CHECK(!tmpl.IsEmpty()); + if (!tmpl->InstanceTemplate()->NewInstance(endpoint->env()->context()) + .ToLocal(&obj)) { + return BaseObjectPtr(); + } + + return MakeDetachedBaseObject( + endpoint, + obj, + local_addr, + remote_addr, + config, + options, + context, + config.dcid, + config.scid, + config.ocid, + config.version); +} + +Session::Session( + EndpointWrap* endpoint, + v8::Local object, + const std::shared_ptr& local_address, + const std::shared_ptr& remote_address, + const std::shared_ptr& options, + const BaseObjectPtr& context, + const CID& dcid, + ngtcp2_crypto_side side) + : AsyncWrap(endpoint->env(), object, AsyncWrap::PROVIDER_QUICSESSION), + SessionStatsBase(endpoint->env()), + allocator_(BindingState::GetAllocator(endpoint->env())), + options_(options), + endpoint_(endpoint), + state_(endpoint->env()), + local_address_(local_address), + remote_address_(remote_address), + application_(SelectApplication(Application::Config())), + crypto_context_(std::make_unique( + this, + options, + std::move(context), + side)), + idle_(endpoint->env(), [this]() { OnIdleTimeout(); }), + retransmit_(endpoint->env(), [this]() { OnRetransmitTimeout(); }), + dcid_(dcid), + max_pkt_len_(get_max_pkt_len(remote_address)), + cid_strategy_(options->cid_strategy->NewInstance(this)) { + MakeWeak(); + cid_strategy_->NewConnectionID(&scid_); + ExtendMaxStreamsBidi(DEFAULT_MAX_STREAMS_BIDI); + ExtendMaxStreamsUni(DEFAULT_MAX_STREAMS_UNI); + + Debug(this, "Initializing session from %s to %s", + local_address_.get(), + remote_address_.get()); + + object->DefineOwnProperty( + env()->context(), + env()->state_string(), + state_.GetArrayBuffer(), + PropertyAttribute::ReadOnly).Check(); + + object->DefineOwnProperty( + env()->context(), + env()->stats_string(), + ToBigUint64Array(env()), + PropertyAttribute::ReadOnly).Check(); + + idle_.Unref(); + retransmit_.Unref(); +} + +Session::Session( + EndpointWrap* endpoint, + Local object, + const std::shared_ptr& local_address, + const std::shared_ptr& remote_address, + const Config& config, + const std::shared_ptr& options, + const BaseObjectPtr& context, + const CID& dcid, + const CID& scid, + const CID& ocid, + quic_version version) + : Session( + endpoint, + object, + local_address, + remote_address, + options, + context, + dcid, + NGTCP2_CRYPTO_SIDE_SERVER) { + TransportParams transport_params(options, scid, ocid); + transport_params.GenerateStatelessResetToken(endpoint, scid_); + if (transport_params.preferred_address_present) { + transport_params.GeneratePreferredAddressToken( + cid_strategy_.get(), + this, + &pscid_); + } + + Path path(local_address, remote_address); + + ngtcp2_conn* conn; + CHECK_EQ( + ngtcp2_conn_server_new( + &conn, + dcid.cid(), + scid_.cid(), + &path, + version, + &callbacks[crypto_context_->side()], + &config, + &transport_params, + &allocator_, + this), 0); + connection_.reset(conn); + crypto_context_->Initialize(); + + AttachToEndpoint(); + + UpdateDataStats(); + UpdateIdleTimer(); +} + +Session::Session( + EndpointWrap* endpoint, + Local object, + const std::shared_ptr& local_address, + const std::shared_ptr& remote_address, + const Config& config, + const std::shared_ptr& options, + const BaseObjectPtr& context, + quic_version version) + : Session( + endpoint, + object, + local_address, + remote_address, + options, + std::move(context)) { + CID dcid; + if (options->dcid) { + dcid = options->dcid; + } else { + cid_strategy_->NewConnectionID(&dcid); + } + CHECK(dcid); + + TransportParams transport_params(options); + Path path(local_address_, remote_address_); + + ngtcp2_conn* conn; + CHECK_EQ( + ngtcp2_conn_client_new( + &conn, + dcid.cid(), + scid_.cid(), + &path, + version, + &callbacks[crypto_context_->side()], + &config, + &transport_params, + &allocator_, + this), 0); + connection_.reset(conn); + + crypto_context_->Initialize(); + + AttachToEndpoint(); + + UpdateIdleTimer(); + UpdateDataStats(); +} + +Session::~Session() { + if (qlogstream_) qlogstream_->End(); + idle_.Stop(); + retransmit_.Stop(); + DebugStats(this); +} + +void Session::AckedStreamDataOffset( + stream_id id, + uint64_t offset, + uint64_t datalen) { + Debug(this, + "Received acknowledgement for %" PRIu64 + " bytes of stream %" PRId64 " data", + datalen, id); + + application_->AcknowledgeStreamData( + id, + offset, + static_cast(datalen)); +} + +void Session::AddStream(const BaseObjectPtr& stream) { + Debug(this, "Adding stream %" PRId64 " to session", stream->id()); + streams_.emplace(stream->id(), stream); + stream->Resume(); + + // Update tracking statistics for the number of streams associated with + // this session. + switch (stream->origin()) { + case Stream::Origin::CLIENT: + if (is_server()) + IncrementStat(&SessionStats::streams_in_count); + else + IncrementStat(&SessionStats::streams_out_count); + break; + case Stream::Origin::SERVER: + if (is_server()) + IncrementStat(&SessionStats::streams_out_count); + else + IncrementStat(&SessionStats::streams_in_count); + } + IncrementStat(&SessionStats::streams_out_count); + switch (stream->direction()) { + case Stream::Direction::BIDIRECTIONAL: + IncrementStat(&SessionStats::bidi_stream_count); + break; + case Stream::Direction::UNIDIRECTIONAL: + IncrementStat(&SessionStats::uni_stream_count); + break; + } +} + +void Session::AttachToEndpoint() { + CHECK(endpoint_); + Debug(this, "Adding session to %s", endpoint_->diagnostic_name()); + endpoint_->AddSession(scid_, BaseObjectPtr(this)); + switch (crypto_context_->side()) { + case NGTCP2_CRYPTO_SIDE_SERVER: { + endpoint_->AssociateCID(dcid_, scid_); + endpoint_->AssociateCID(pscid_, scid_); + break; + } + case NGTCP2_CRYPTO_SIDE_CLIENT: { + std::vector cids(ngtcp2_conn_get_num_scid(connection())); + ngtcp2_conn_get_scid(connection(), cids.data()); + for (const ngtcp2_cid& cid : cids) + endpoint_->AssociateCID(CID(&cid), scid_); + break; + } + default: + UNREACHABLE(); + } + + std::vector tokens( + ngtcp2_conn_get_num_active_dcid(connection())); + ngtcp2_conn_get_active_dcid(connection(), tokens.data()); + for (const ngtcp2_cid_token& token : tokens) { + if (token.token_present) { + endpoint_->AssociateStatelessResetToken( + StatelessResetToken(token.token), + BaseObjectPtr(this)); + } + } +} + +// A client Session can be migrated to a different Endpoint instance. +bool Session::AttachToNewEndpoint(EndpointWrap* endpoint, bool nat_rebinding) { + CHECK(!is_server()); + CHECK(!is_destroyed()); + + // If we're in the process of gracefully closing, attaching the session + // to a new endpoint is not allowed. + if (state_->graceful_closing) + return false; + + if (endpoint == nullptr || endpoint == endpoint_.get()) + return true; + + Debug(this, "Migrating to %s", endpoint_->diagnostic_name()); + + // Ensure that we maintain a reference to keep this from being + // destroyed while we are starting the migration. + BaseObjectPtr ptr(this); + + // Step 1: Remove the session from the current socket + DetachFromEndpoint(); + + endpoint_.reset(endpoint); + // Step 2: Add this Session to the given Socket + AttachToEndpoint(); + + auto local_address = endpoint->local_address(); + + // The nat_rebinding option here should rarely, if ever + // be used in a real application. It is intended to serve + // as a way of simulating a silent local address change, + // such as when the NAT binding changes. Currently, Node.js + // does not really have an effective way of detecting that. + // Manual user code intervention to handle the migration + // to the new Endpoint is required, which should always + // trigger path validation using the ngtcp2_conn_initiate_migration. + if (LIKELY(!nat_rebinding)) { + SendSessionScope send(this); + Path path(local_address, remote_address_); + return ngtcp2_conn_initiate_migration( + connection(), + &path, + uv_hrtime()) == 0; + } else { + ngtcp2_addr addr; + ngtcp2_conn_set_local_addr( + connection(), + ngtcp2_addr_init( + &addr, + local_address->data(), + local_address->length(), + nullptr)); + } + + return true; +} + +void Session::Close(SessionCloseFlags close_flags) { + if (is_destroyed()) + return; + bool silent = close_flags == SessionCloseFlags::SILENT; + bool stateless_reset = silent && state_->stateless_reset; + + // If we're not running within a ngtcp2 callback scope, schedule + // a CONNECTION_CLOSE to be sent when Close exits. If we are + // within a ngtcp2 callback scope, sending the CONNECTION_CLOSE + // will be deferred. + ConnectionCloseScope close_scope(this, silent); + + // Once Close has been called, we cannot re-enter + if (UNLIKELY(state_->closing)) + return; + + state_->closing = 1; + state_->silent_close = silent ? 1 : 0; + + QuicError error = last_error(); + Debug(this, + "Closing with error: %s (silent: %s, stateless reset: %s)", + error, + silent ? "Y" : "N", + stateless_reset ? "Y" : "N"); + + if (!state_->wrapped) + return Destroy(); + + // If the Session has been wrapped by a JS object, we have to + // notify the JavaScript side that the session is being closed. + // If it hasn't yet been wrapped, we can skip the call and and + // go straight to destroy. + BaseObjectPtr ptr(this); + + BindingState* state = BindingState::Get(env()); + HandleScope scope(env() ->isolate()); + Context::Scope context_scope(env()->context()); + + CallbackScope cb_scope(this); + + Local argv[] = { + Number::New(env()->isolate(), static_cast(error.code)), + Integer::New(env()->isolate(), static_cast(error.type)), + silent + ? v8::True(env()->isolate()) + : v8::False(env()->isolate()), + stateless_reset + ? v8::True(env()->isolate()) + : v8::False(env()->isolate()) + }; + + USE(state->session_close_callback()->Call( + env()->context(), + object(), + arraysize(argv), + argv)); +} + +BaseObjectPtr Session::CreateStream(stream_id id) { + CHECK(!is_destroyed()); + CHECK_EQ(state_->graceful_closing, 0); + CHECK_EQ(state_->closing, 0); + + BaseObjectPtr stream = Stream::Create(env(), this, id); + CHECK(stream); + + BindingState* state = BindingState::Get(env()); + HandleScope scope(env()->isolate()); + Context::Scope context_scope(env()->context()); + + CallbackScope cb_scope(this); + + Local argv[] = { + stream->object(), + Number::New(env()->isolate(), static_cast(stream->id())) + }; + + // Grab a shared pointer to this to prevent the Session + // from being freed while the MakeCallback is running. + BaseObjectPtr ptr(this); + + USE(state->stream_ready_callback()->Call( + env()->context(), + object(), + arraysize(argv), + argv)); + + return stream; +} + +void Session::Datagram(uint32_t flags, const uint8_t* data, size_t datalen) { + if (LIKELY(state_->datagram_enabled == 0) || UNLIKELY(datalen == 0)) + return; + + BindingState* state = BindingState::Get(env()); + HandleScope scope(env()->isolate()); + Context::Scope context_scope(env()->context()); + + CallbackScope cb_scope(this); + + std::shared_ptr store = + ArrayBuffer::NewBackingStore(env()->isolate(), datalen); + if (!store) + return; + memcpy(store->Data(), data, datalen); + + Local argv[] = { + ArrayBuffer::New(env()->isolate(), store), + flags & NGTCP2_DATAGRAM_FLAG_EARLY + ? v8::True(env()->isolate()) + : v8::False(env()->isolate()) + }; + + // Grab a shared pointer to this to prevent the Session + // from being freed while the MakeCallback is running. + BaseObjectPtr ptr(this); + + USE(state->session_datagram_callback()->Call( + env()->context(), + object(), + arraysize(argv), + argv)); +} + +void Session::Destroy() { + if (is_destroyed()) + return; + + Debug(this, "Destroying the Session"); + + // Mark the session destroyed. + state_->destroyed = 1; + state_->closing = 0; + state_->graceful_closing = 0; + + // TODO(@jasnell): Allow overriding the close code + + // If we're not already in a ConnectionCloseScope, schedule + // sending a CONNECTION_CLOSE when destroy exits. If we are + // running within an ngtcp2 callback scope, sending the + // CONNECTION_CLOSE will be deferred. + ConnectionCloseScope close_scope(this, state_->silent_close); + + // All existing streams should have already been destroyed + CHECK(streams_.empty()); + + // Stop and free the idle and retransmission timers if they are active. + idle_.Stop(); + retransmit_.Stop(); + + // The Session instances are kept alive usingBaseObjectPtr. The + // only persistent BaseObjectPtr is the map in the associated + // Endpoint. Removing the Session from the Endpoint will free + // that pointer, allowing the Session to be deconstructed once + // the stack unwinds and any remaining BaseObjectPtr + // instances fall out of scope. + DetachFromEndpoint(); +} + +void Session::DetachFromEndpoint() { + CHECK(endpoint_); + Debug(this, "Removing Session from %s", endpoint_->diagnostic_name()); + if (is_server()) { + endpoint_->DisassociateCID(dcid_); + endpoint_->DisassociateCID(pscid_); + } + + std::vector cids(ngtcp2_conn_get_num_scid(connection())); + std::vector tokens( + ngtcp2_conn_get_num_active_dcid(connection())); + ngtcp2_conn_get_scid(connection(), cids.data()); + ngtcp2_conn_get_active_dcid(connection(), tokens.data()); + + for (const ngtcp2_cid& cid : cids) + endpoint_->DisassociateCID(CID(&cid)); + + for (const ngtcp2_cid_token& token : tokens) { + if (token.token_present) { + endpoint_->DisassociateStatelessResetToken( + StatelessResetToken(token.token)); + } + } + + Debug(this, "Removed from the endpoint"); + BaseObjectPtr endpoint = std::move(endpoint_); + endpoint->RemoveSession(scid_, remote_address_); +} + +void Session::ExtendMaxStreamData(stream_id id, uint64_t max_data) { + Debug(this, + "Extending max stream %" PRId64 " data to %" PRIu64, id, max_data); + application_->ExtendMaxStreamData(id, max_data); +} + +void Session::ExtendMaxStreamsBidi(uint64_t max_streams) { + state_->max_streams_bidi = max_streams; +} + +void Session::ExtendMaxStreamsRemoteUni(uint64_t max_streams) { + Debug(this, "Extend remote max unidirectional streams: %" PRIu64, + max_streams); + application_->ExtendMaxStreamsRemoteUni(max_streams); +} + +void Session::ExtendMaxStreamsRemoteBidi(uint64_t max_streams) { + Debug(this, "Extend remote max bidirectional streams: %" PRIu64, max_streams); + application_->ExtendMaxStreamsRemoteBidi(max_streams); +} + +void Session::ExtendMaxStreamsUni(uint64_t max_streams) { + state_->max_streams_uni = max_streams; +} + +void Session::ExtendOffset(size_t amount) { + Debug(this, "Extending session offset by %" PRId64 " bytes", amount); + ngtcp2_conn_extend_max_offset(connection(), amount); +} + +void Session::ExtendStreamOffset(stream_id id, size_t amount) { + Debug(this, "Extending max stream %" PRId64 " offset by %" PRId64 " bytes", + id, amount); + ngtcp2_conn_extend_max_stream_offset(connection(), id, amount); +} + +BaseObjectPtr Session::FindStream(stream_id id) const { + auto it = streams_.find(id); + return it == std::end(streams_) ? BaseObjectPtr() : it->second; +} + +void Session::GetConnectionCloseInfo() { + ngtcp2_connection_close_error_code close_code; + ngtcp2_conn_get_connection_close_error_code(connection(), &close_code); + set_last_error(QuicError::FromNgtcp2(close_code)); +} + +void Session::GetLocalTransportParams(ngtcp2_transport_params* params) { + CHECK(!is_destroyed()); + ngtcp2_conn_get_local_transport_params(connection(), params); +} + +void Session::GetNewConnectionID( + ngtcp2_cid* cid, + uint8_t* token, + size_t cidlen) { + CHECK(cid_strategy_); + cid_strategy_->NewConnectionID(cid, cidlen); + CID cid_(cid); + StatelessResetToken( + token, + endpoint_->config().reset_token_secret, + cid_); + endpoint_->AssociateCID(cid_, scid_); +} + +SessionTicketAppData::Status Session::GetSessionTicketAppData( + const SessionTicketAppData& app_data, + SessionTicketAppData::Flag flag) { + return application_->GetSessionTicketAppData(app_data, flag); +} + +void Session::HandleError() { + if (is_destroyed()) + return; + + // If the Session is a server, send a CONNECTION_CLOSE. In either + // case, the closing timer will be set and the Session will be + // destroyed. + if (is_server()) + SendConnectionClose(); + else + UpdateClosingTimer(); +} + +bool Session::HandshakeCompleted( + const std::shared_ptr& remote_address) { + RemoteTransportParamsDebug transport_params(this); + Debug(this, "Handshake completed with %s. %s", + remote_address.get(), + transport_params); + RecordTimestamp(&SessionStats::handshake_completed_at); + + if (is_server()) { + uint8_t token[kMaxTokenLen]; + size_t tokenlen = 0; + if (!endpoint()->GenerateNewToken(token, remote_address).To(&tokenlen)) { + Debug(this, "Failed to generate new token on handshake complete"); + return false; + } + + if (NGTCP2_ERR(ngtcp2_conn_submit_new_token( + connection_.get(), + token, + tokenlen))) { + Debug(this, "Failed to submit new token on handshake complete"); + return false; + } + + HandshakeConfirmed(); + return true; + } + + BindingState* state = BindingState::Get(env()); + HandleScope scope(env()->isolate()); + Context::Scope context_scope(env()->context()); + + CallbackScope cb_scope(this); + + Local argv[] = { + Undefined(env()->isolate()), // Server name + GetALPNProtocol(*this), // ALPN + Undefined(env()->isolate()), // Cipher name + Undefined(env()->isolate()), // Cipher version + Integer::New(env()->isolate(), max_pkt_len_), // Max packet length + Undefined(env()->isolate()), // Validation error reason + Undefined(env()->isolate()), // Validation error code + crypto_context_->early_data() ? + v8::True(env()->isolate()) : + v8::False(env()->isolate()) + }; + + std::string hostname = crypto_context_->servername(); + if (!ToV8Value(env()->context(), hostname).ToLocal(&argv[0])) + return false; + + if (!crypto_context_->cipher_name(env()).ToLocal(&argv[2]) || + !crypto_context_->cipher_version(env()).ToLocal(&argv[3])) { + return false; + } + + int err = crypto_context_->VerifyPeerIdentity(); + if (err != X509_V_OK && + (!crypto::GetValidationErrorReason(env(), err).ToLocal(&argv[5]) || + !crypto::GetValidationErrorCode(env(), err).ToLocal(&argv[6]))) { + return false; + } + + BaseObjectPtr ptr(this); + + USE(state->session_handshake_callback()->Call( + env()->context(), + object(), + arraysize(argv), + argv)); + + return true; +} + +void Session::HandshakeConfirmed() { + Debug(this, "Handshake is confirmed"); + RecordTimestamp(&SessionStats::handshake_confirmed_at); + state_->handshake_confirmed = 1; +} + +bool Session::HasStream(stream_id id) const { + return streams_.find(id) != std::end(streams_); +} + +bool Session::InitApplication() { + Debug(this, "Initializing application handler for ALPN %s", + options_->alpn.c_str() + 1); + return application_->Initialize(); +} + +void Session::OnIdleTimeout() { + if (!is_destroyed()) { + if (state_->idle_timeout == 1) { + Debug(this, "Idle timeout"); + Close(SessionCloseFlags::SILENT); + return; + } + state_->idle_timeout = 1; + UpdateClosingTimer(); + } +} + +void Session::OnRetransmitTimeout() { + if (is_destroyed()) return; + uint64_t now = uv_hrtime(); + + if (ngtcp2_conn_get_expiry(connection()) <= now) { + Debug(this, "Retransmitting due to loss detection"); + IncrementStat(&SessionStats::loss_retransmit_count); + } + + if (ngtcp2_conn_handle_expiry(connection(), now) != 0) { + Debug(this, "Handling retransmission failed"); + HandleError(); + } + + SendPendingData(); +} + +Maybe Session::OpenStream(Stream::Direction direction) { + DCHECK(!is_destroyed()); + DCHECK(!is_closing()); + DCHECK(!is_graceful_closing()); + stream_id id; + switch (direction) { + case Stream::Direction::BIDIRECTIONAL: + if (ngtcp2_conn_open_bidi_stream(connection(), &id, nullptr) == 0) + return Just(id); + break; + case Stream::Direction::UNIDIRECTIONAL: + if (ngtcp2_conn_open_uni_stream(connection(), &id, nullptr) == 0) + return Just(id); + break; + default: + UNREACHABLE(); + } + return Nothing(); +} + +void Session::PathValidation( + const ngtcp2_path* path, + ngtcp2_path_validation_result res) { + if (LIKELY(state_->path_validated_enabled == 0)) + return; + + // This is a fairly expensive operation because both the local and + // remote addresses have to converted into JavaScript objects. We + // only do this if a pathValidation handler is registered. + BindingState* state = BindingState::Get(env()); + HandleScope scope(env()->isolate()); + Context::Scope context_scope(env()->context()); + + CallbackScope cb_scope(this); + + Local argv[] = { + Integer::New(env()->isolate(), res), + SocketAddressBase::Create( + env(), + std::make_shared(path->local.addr))->object(), + SocketAddressBase::Create( + env(), + std::make_shared(path->remote.addr))->object() + }; + + BaseObjectPtr ptr(this); + + USE(state->session_path_validation_callback()->Call( + env()->context(), + object(), + arraysize(argv), + argv)); +} + +bool Session::Receive( + size_t nread, + std::shared_ptr store, + const std::shared_ptr& local_address, + const std::shared_ptr& remote_address) { + + CHECK(!is_destroyed()); + + Debug(this, "Receiving QUIC packet"); + IncrementStat(&SessionStats::bytes_received, nread); + + if (is_in_closing_period() && is_server()) { + Debug(this, "Packet received while in closing period"); + IncrementConnectionCloseAttempts(); + // For server Session instances, we serialize the connection close + // packet once but may sent it multiple times. If the client keeps + // transmitting, then the connection close may have gotten lost. + // We don't want to send the connection close in response to + // every received packet, however, so we use an exponential + // backoff, increasing the ratio of packets received to connection + // close frame sent with every one we send. + if (UNLIKELY(ShouldAttemptConnectionClose() && + !SendConnectionClose())) { + Debug(this, "Failure sending another connection close"); + return false; + } + } + + { + // These are within a scope to ensure that the InternalCallbackScope + // and HandleScope are both exited before continuing on with the + // function. This allows any nextTicks and queued tasks to be processed + // before we continue. + auto update_stats = OnScopeLeave([&](){ + UpdateDataStats(); + }); + HandleScope handle_scope(env()->isolate()); + InternalCallbackScope callback_scope(this); + remote_address_ = remote_address; + Path path(local_address, remote_address_); + uint8_t* data = static_cast(store->Data()); + if (!ReceivePacket(&path, data, nread)) { + HandleError(); + return false; + } + } + + // Only send pending data if we haven't entered draining mode. + // We enter the draining period when a CONNECTION_CLOSE has been + // received from the remote peer. + if (is_in_draining_period()) { + Debug(this, "In draining period after processing packet"); + // If processing the packet puts us into draining period, there's + // absolutely nothing left for us to do except silently close + // and destroy this Session, which we do by updating the + // closing timer. + GetConnectionCloseInfo(); + UpdateClosingTimer(); + return true; + } + + if (!is_destroyed()) + UpdateIdleTimer(); + SendPendingData(); + Debug(this, "Successfully processed received packet"); + return true; +} + +bool Session::ReceivePacket( + ngtcp2_path* path, + const uint8_t* data, + ssize_t nread) { + CHECK(!is_destroyed()); + + uint64_t now = uv_hrtime(); + SetStat(&SessionStats::received_at, now); + int err = ngtcp2_conn_read_pkt(connection(), path, nullptr, data, nread, now); + if (err < 0) { + switch (err) { + case NGTCP2_ERR_CALLBACK_FAILURE: + case NGTCP2_ERR_DRAINING: + case NGTCP2_ERR_RECV_VERSION_NEGOTIATION: + break; + case NGTCP2_ERR_RETRY: + // This should only ever happen on the server + CHECK(is_server()); + endpoint_->SendRetry( + version(), + scid_, + dcid_, + local_address_, + remote_address_); + // Fall through + case NGTCP2_ERR_DROP_CONN: + Close(SessionCloseFlags::SILENT); + break; + default: + set_last_error({ + QuicError::Type::APPLICATION, + ngtcp2_err_infer_quic_transport_error_code(err) + }); + return false; + } + } + + // If the Session has been destroyed but it is not + // in the closing period, a CONNECTION_CLOSE has not yet + // been sent to the peer. Let's attempt to send one. This + // will have the effect of setting the idle timer to the + // closing/draining period, after which the Session + // will be destroyed. + if (is_destroyed() && !is_in_closing_period()) { + Debug(this, "Session was destroyed while processing the packet"); + return SendConnectionClose(); + } + + return true; +} + +bool Session::ReceiveStreamData( + uint32_t flags, + stream_id id, + const uint8_t* data, + size_t datalen, + uint64_t offset) { + auto leave = OnScopeLeave([&]() { + // Unconditionally extend the flow control window for the entire + // session but not for the individual Stream. + ExtendOffset(datalen); + }); + + return application_->ReceiveStreamData( + flags, + id, + data, + datalen, + offset); +} + +void Session::ResumeStream(stream_id id) { + application()->ResumeStream(id); +} + +void Session::SelectPreferredAddress( + const PreferredAddress& preferred_address) { + CHECK(!is_server()); + options_->preferred_address_strategy(this, preferred_address); +} + +bool Session::SendConnectionClose() { + CHECK(!NgCallbackScope::InNgCallbackScope(this)); + + // Do not send any frames at all if we're in the draining period + // or in the middle of a silent close + if (is_in_draining_period() || state_->silent_close) + return true; + + // The specific handling of connection close varies for client + // and server Session instances. For servers, we will + // serialize the connection close once but may end up transmitting + // it multiple times; whereas for clients, we will serialize it + // once and send once only. + QuicError error = last_error(); + Debug(this, "Sending connection close with error: %s", error); + + UpdateClosingTimer(); + + // If initial keys have not yet been installed, use the alternative + // ImmediateConnectionClose to send a stateless connection close to + // the peer. + if (crypto_context()->write_crypto_level() == + NGTCP2_CRYPTO_LEVEL_INITIAL) { + endpoint_->ImmediateConnectionClose( + version(), + dcid(), + scid_, + local_address_, + remote_address_, + error.code); + return true; + } + + switch (crypto_context_->side()) { + case NGTCP2_CRYPTO_SIDE_SERVER: { + if (!is_in_closing_period() && !StartClosingPeriod()) { + Close(SessionCloseFlags::SILENT); + return false; + } + CHECK_GT(conn_closebuf_->length(), 0); + return SendPacket(Packet::Copy(conn_closebuf_)); + } + case NGTCP2_CRYPTO_SIDE_CLIENT: { + std::unique_ptr packet = + std::make_unique("client connection close"); + ssize_t nwrite = + SelectCloseFn(error)( + connection(), + nullptr, + nullptr, + packet->data(), + max_pkt_len_, + error.code, + uv_hrtime()); + if (UNLIKELY(nwrite < 0)) { + Debug(this, "Error writing connection close: %d", nwrite); + set_last_error(kQuicInternalError); + Close(SessionCloseFlags::SILENT); + return false; + } + packet->set_length(nwrite); + return SendPacket(std::move(packet)); + } + default: + UNREACHABLE(); + } +} + +bool Session::SendPacket(std::unique_ptr packet) { + CHECK(!is_in_draining_period()); + + // There's nothing to send. + if (!packet || packet->length() == 0) + return true; + + IncrementStat(&SessionStats::bytes_sent, packet->length()); + RecordTimestamp(&SessionStats::sent_at); + ScheduleRetransmit(); + + Debug(this, "Sending %" PRIu64 " bytes to %s from %s", + packet->length(), + remote_address_.get(), + local_address_.get()); + + endpoint_->SendPacket( + local_address_, + remote_address_, + std::move(packet), + BaseObjectPtr(this)); + + return true; +} + +bool Session::SendPacket( + std::unique_ptr packet, + const ngtcp2_path_storage& path) { + UpdateEndpoint(path.path); + return SendPacket(std::move(packet)); +} + +void Session::SendPendingData() { + if (is_unable_to_send_packets()) + return; + + Debug(this, "Sending pending data"); + if (!application_->SendPendingData()) { + Debug(this, "Error sending pending application data"); + HandleError(); + } + ScheduleRetransmit(); +} + +void Session::SetSessionTicketAppData(const SessionTicketAppData& app_data) { + application_->SetSessionTicketAppData(app_data); +} + +void Session::StreamDataBlocked(stream_id id) { + IncrementStat(&SessionStats::block_count); + + BindingState* state = BindingState::Get(env()); + HandleScope handle_scope(env()->isolate()); + Context::Scope context_scope(env()->context()); + + CallbackScope cb_scope(this); + + BaseObjectPtr stream = FindStream(id); + USE(state->stream_blocked_callback()->Call( + env()->context(), + object(), + 0, nullptr)); +} + +void Session::IncrementConnectionCloseAttempts() { + if (connection_close_attempts_ < kMaxSizeT) + connection_close_attempts_++; +} + +void Session::RemoveStream(stream_id id) { + Debug(this, "Removing stream %" PRId64, id); + + // ngtcp2 does not extend the max streams count automatically + // except in very specific conditions, none of which apply + // once we've gotten this far. We need to manually extend when + // a remote peer initiated stream is removed. + if (!is_in_draining_period() && + !is_in_closing_period() && + !state_->silent_close && + !ngtcp2_conn_is_local_stream(connection_.get(), id)) { + if (ngtcp2_is_bidi_stream(id)) + ngtcp2_conn_extend_max_streams_bidi(connection_.get(), 1); + else + ngtcp2_conn_extend_max_streams_uni(connection_.get(), 1); + } + + // Frees the persistent reference to the QuicStream object, + // allowing it to be gc'd any time after the JS side releases + // it's own reference. + streams_.erase(id); +} + +void Session::ScheduleRetransmit() { + uint64_t now = uv_hrtime(); + uint64_t expiry = ngtcp2_conn_get_expiry(connection()); + // now and expiry are in nanoseconds, interval is milliseconds + uint64_t interval = (expiry < now) ? 1 : (expiry - now) / 1000000UL; + // If interval ends up being 0, the repeating timer won't be + // scheduled, so set it to 1 instead. + if (interval == 0) interval = 1; + Debug(this, "Scheduling the retransmit timer for %" PRIu64, interval); + UpdateRetransmitTimer(interval); +} + +bool Session::ShouldAttemptConnectionClose() { + if (connection_close_attempts_ == connection_close_limit_) { + if (connection_close_limit_ * 2 <= kMaxSizeT) + connection_close_limit_ *= 2; + else + connection_close_limit_ = kMaxSizeT; + return true; + } + return false; +} + +void Session::ShutdownStream(stream_id id, error_code code) { + if (is_in_closing_period() || + is_in_draining_period() || + state_->silent_close == 1) { + return; // Nothing to do because we can't send any frames. + } + SendSessionScope send_scope(this); + ngtcp2_conn_shutdown_stream(connection(), id, 0); +} + +bool Session::StartClosingPeriod() { + if (is_destroyed()) + return false; + if (is_in_closing_period()) + return true; + + QuicError error = last_error(); + Debug(this, "Closing period has started. Error %s", error); + + conn_closebuf_ = std::make_unique("server connection close"); + + ssize_t nwrite = + SelectCloseFn(error)( + connection(), + nullptr, + nullptr, + conn_closebuf_->data(), + max_pkt_len_, + error.code, + uv_hrtime()); + if (nwrite < 0) { + set_last_error(kQuicInternalError); + return false; + } + conn_closebuf_->set_length(nwrite); + return true; +} + +void Session::StartGracefulClose() { + state_->graceful_closing = 1; + RecordTimestamp(&SessionStats::closing_at); +} + +void Session::StreamClose(stream_id id, error_code app_error_code) { + Debug(this, "Closing stream %" PRId64 " with code %" PRIu64, + id, + app_error_code); + + application_->StreamClose(id, app_error_code); +} + +void Session::StreamReset( + stream_id id, + uint64_t final_size, + error_code app_error_code) { + Debug(this, + "Reset stream %" PRId64 " with code %" PRIu64 + " and final size %" PRIu64, + id, + app_error_code, + final_size); + + BaseObjectPtr stream = FindStream(id); + + if (stream) { + stream->set_final_size(final_size); + application_->StreamReset(id, app_error_code); + } +} + +bool Session::SubmitHeaders( + Stream::HeadersKind kind, + stream_id id, + const v8::Local& headers) { + return application_->SubmitHeaders(id, kind, headers); +} + +void Session::UpdateClosingTimer() { + if (state_->closing_timer_enabled) + return; + state_->closing_timer_enabled = 1; + uint64_t timeout = + is_server() ? (ngtcp2_conn_get_pto(connection()) / 1000000ULL) * 3 : 0; + Debug(this, "Setting closing timeout to %" PRIu64, timeout); + retransmit_.Stop(); + idle_.Update(timeout, 0); + idle_.Ref(); +} + +void Session::UpdateConnectionID( + int type, + const CID& cid, + const StatelessResetToken& token) { + switch (type) { + case NGTCP2_CONNECTION_ID_STATUS_TYPE_ACTIVATE: + endpoint_->AssociateStatelessResetToken( + token, + BaseObjectPtr(this)); + break; + case NGTCP2_CONNECTION_ID_STATUS_TYPE_DEACTIVATE: + endpoint_->DisassociateStatelessResetToken(token); + break; + } +} + +void Session::UpdateDataStats() { + if (state_->destroyed) + return; + state_->max_data_left = ngtcp2_conn_get_max_data_left(connection()); + + ngtcp2_conn_stat stat; + ngtcp2_conn_get_conn_stat(connection(), &stat); + + SetStat( + &SessionStats::bytes_in_flight, + stat.bytes_in_flight); + SetStat( + &SessionStats::congestion_recovery_start_ts, + stat.congestion_recovery_start_ts); + SetStat(&SessionStats::cwnd, stat.cwnd); + SetStat(&SessionStats::delivery_rate_sec, stat.delivery_rate_sec); + SetStat(&SessionStats::first_rtt_sample_ts, stat.first_rtt_sample_ts); + SetStat(&SessionStats::initial_rtt, stat.initial_rtt); + SetStat(&SessionStats::last_tx_pkt_ts, + reinterpret_cast(stat.last_tx_pkt_ts)); + SetStat(&SessionStats::latest_rtt, stat.latest_rtt); + SetStat(&SessionStats::loss_detection_timer, stat.loss_detection_timer); + SetStat(&SessionStats::loss_time, + reinterpret_cast(stat.loss_time)); + SetStat(&SessionStats::max_udp_payload_size, stat.max_udp_payload_size); + SetStat(&SessionStats::min_rtt, stat.min_rtt); + SetStat(&SessionStats::pto_count, stat.pto_count); + SetStat(&SessionStats::rttvar, stat.rttvar); + SetStat(&SessionStats::smoothed_rtt, stat.smoothed_rtt); + SetStat(&SessionStats::ssthresh, stat.ssthresh); + + // The max_bytes_in_flight is a highwater mark that can be used + // in performance analysis operations. + if (stat.bytes_in_flight > GetStat(&SessionStats::max_bytes_in_flight)) + SetStat(&SessionStats::max_bytes_in_flight, stat.bytes_in_flight); +} + +void Session::UpdateEndpoint(const ngtcp2_path& path) { + remote_address_->Update(path.remote.addr, path.remote.addrlen); + local_address_->Update(path.local.addr, path.local.addrlen); + if (remote_address_->family() == AF_INET6) { + remote_address_->set_flow_label( + endpoint_->GetFlowLabel( + local_address_, + remote_address_, + scid_)); + } +} + +void Session::UpdateIdleTimer() { + if (state_->closing_timer_enabled) + return; + uint64_t now = uv_hrtime(); + uint64_t expiry = ngtcp2_conn_get_idle_expiry(connection()); + // nano to millis + uint64_t timeout = expiry > now ? (expiry - now) / 1000000ULL : 1; + if (timeout == 0) timeout = 1; + Debug(this, "Updating idle timeout to %" PRIu64, timeout); + idle_.Update(timeout, timeout); +} + +void Session::UpdateRetransmitTimer(uint64_t timeout) { + retransmit_.Update(timeout, timeout); +} + +void Session::UsePreferredAddress(const PreferredAddress::Address& address) { + BindingState* state = BindingState::Get(env()); + HandleScope scope(env()->isolate()); + Context::Scope context_scope(env()->context()); + + CallbackScope cb_scope(this); + + Local argv[] = { + OneByteString(env()->isolate(), address.address.c_str()), + Integer::NewFromUnsigned(env()->isolate(), address.port), + Integer::New(env()->isolate(), address.family) + }; + + BaseObjectPtr ptr(this); + + USE(state->session_use_preferred_address_callback()->Call( + env()->context(), + object(), + arraysize(argv), + argv)); +} + +void Session::VersionNegotiation(const quic_version* sv, size_t nsv) { + CHECK(!is_server()); + + BindingState* state = BindingState::Get(env()); + HandleScope scope(env()->isolate()); + Context::Scope context_scope(env()->context()); + + CallbackScope cb_scope(this); + + std::vector> versions(nsv); + + for (size_t n = 0; n < nsv; n++) + versions.emplace_back(Integer::New(env()->isolate(), sv[n])); + + // Currently, we only support one version of QUIC but in + // the future that may change. The callback below passes + // an array back to the JavaScript side to future-proof. + Local supported = Integer::New(env()->isolate(), NGTCP2_PROTO_VER_MAX); + + Local argv[] = { + Integer::New(env()->isolate(), NGTCP2_PROTO_VER_MAX), + Array::New(env()->isolate(), versions.data(), nsv), + Array::New(env()->isolate(), &supported, 1) + }; + + // Grab a shared pointer to this to prevent the Session + // from being freed while the MakeCallback is running. + BaseObjectPtr ptr(this); + USE(state->session_version_negotiation_callback()->Call( + env()->context(), + object(), + arraysize(argv), + argv)); +} + +EndpointWrap* Session::endpoint() const { return endpoint_.get(); } + +bool Session::is_handshake_completed() const { + DCHECK(!is_destroyed()); + return ngtcp2_conn_get_handshake_completed(connection()); +} + +bool Session::is_in_closing_period() const { + return ngtcp2_conn_is_in_closing_period(connection()); +} + +bool Session::is_in_draining_period() const { + return ngtcp2_conn_is_in_draining_period(connection()); +} + +bool Session::is_unable_to_send_packets() { + return NgCallbackScope::InNgCallbackScope(this) || + is_destroyed() || + is_in_draining_period() || + (is_server() && is_in_closing_period()) || + !endpoint_; +} + +uint64_t Session::max_data_left() const { + return ngtcp2_conn_get_max_data_left(connection()); +} + +uint64_t Session::max_local_streams_uni() const { + return ngtcp2_conn_get_max_local_streams_uni(connection()); +} + +void Session::set_remote_transport_params() { + DCHECK(!is_destroyed()); + ngtcp2_conn_get_remote_transport_params(connection(), &transport_params_); + transport_params_set_ = true; +} + +int Session::set_session(SSL_SESSION* session) { + CHECK(!is_server()); + CHECK(!is_destroyed()); + int size = i2d_SSL_SESSION(session, nullptr); + if (size > crypto::SecureContext::kMaxSessionSize) + return 0; + + BindingState* state = BindingState::Get(env()); + HandleScope scope(env()->isolate()); + Context::Scope context_scope(env()->context()); + + CallbackScope cb_scope(this); + + Local argv[] = { + v8::Undefined(env()->isolate()), + v8::Undefined(env()->isolate()) + }; + + if (size > 0) { + std::shared_ptr session_ticket = + ArrayBuffer::NewBackingStore(env()->isolate(), size); + unsigned char* session_data = + reinterpret_cast(session_ticket->Data()); + memset(session_data, 0, size); + if (i2d_SSL_SESSION(session, &session_data) <= 0) + return 0; + argv[0] = ArrayBuffer::New(env()->isolate(), session_ticket); + } + + if (transport_params_set_) { + std::shared_ptr transport_params = + ArrayBuffer::NewBackingStore(env()->isolate(), + sizeof(ngtcp2_transport_params)); + memcpy( + transport_params->Data(), + &transport_params_, + sizeof(ngtcp2_transport_params)); + argv[1] = ArrayBuffer::New(env()->isolate(), transport_params); + } + + BaseObjectPtr ptr(this); + + USE(state->session_ticket_callback()->Call( + env()->context(), + object(), + arraysize(argv), + argv)); + + return 0; +} + +BaseObjectPtr Session::qlogstream() { + if (!qlogstream_) + qlogstream_ = QLogStream::Create(env()); + return qlogstream_; +} + +// Gets the QUIC version negotiated for this Session +quic_version Session::version() const { + CHECK(!is_destroyed()); + return ngtcp2_conn_get_negotiated_version(connection()); +} + +const ngtcp2_callbacks Session::callbacks[2] = { + // NGTCP2_CRYPTO_SIDE_CLIENT + { + ngtcp2_crypto_client_initial_cb, + nullptr, + OnReceiveCryptoData, + OnHandshakeCompleted, + OnVersionNegotiation, + ngtcp2_crypto_encrypt_cb, + ngtcp2_crypto_decrypt_cb, + ngtcp2_crypto_hp_mask_cb, + OnReceiveStreamData, + OnAckedCryptoOffset, + OnAckedStreamDataOffset, + OnStreamOpen, + OnStreamClose, + OnStatelessReset, + ngtcp2_crypto_recv_retry_cb, + OnExtendMaxStreamsBidi, + OnExtendMaxStreamsUni, + OnRand, + OnGetNewConnectionID, + OnRemoveConnectionID, + ngtcp2_crypto_update_key_cb, + OnPathValidation, + OnSelectPreferredAddress, + OnStreamReset, + OnExtendMaxStreamsRemoteBidi, + OnExtendMaxStreamsRemoteUni, + OnExtendMaxStreamData, + OnConnectionIDStatus, + OnHandshakeConfirmed, + nullptr, // recv_new_token + ngtcp2_crypto_delete_crypto_aead_ctx_cb, + ngtcp2_crypto_delete_crypto_cipher_ctx_cb, + OnDatagram + }, + // NGTCP2_CRYPTO_SIDE_SERVER + { + nullptr, + ngtcp2_crypto_recv_client_initial_cb, + OnReceiveCryptoData, + OnHandshakeCompleted, + nullptr, // recv_version_negotiation + ngtcp2_crypto_encrypt_cb, + ngtcp2_crypto_decrypt_cb, + ngtcp2_crypto_hp_mask_cb, + OnReceiveStreamData, + OnAckedCryptoOffset, + OnAckedStreamDataOffset, + OnStreamOpen, + OnStreamClose, + OnStatelessReset, + nullptr, // recv_retry + nullptr, // extend_max_streams_bidi + nullptr, // extend_max_streams_uni + OnRand, + OnGetNewConnectionID, + OnRemoveConnectionID, + ngtcp2_crypto_update_key_cb, + OnPathValidation, + nullptr, // select_preferred_addr + OnStreamReset, + OnExtendMaxStreamsRemoteBidi, + OnExtendMaxStreamsRemoteUni, + OnExtendMaxStreamData, + OnConnectionIDStatus, + nullptr, // handshake_confirmed + nullptr, // recv_new_token + ngtcp2_crypto_delete_crypto_aead_ctx_cb, + ngtcp2_crypto_delete_crypto_cipher_ctx_cb, + OnDatagram + } +}; + +int Session::OnReceiveCryptoData( + ngtcp2_conn* conn, + ngtcp2_crypto_level crypto_level, + uint64_t offset, + const uint8_t* data, + size_t datalen, + void* user_data) { + Session* session = static_cast(user_data); + + if (UNLIKELY(session->is_destroyed())) + return NGTCP2_ERR_CALLBACK_FAILURE; + + NgCallbackScope callback_scope(session); + return session->crypto_context()->Receive( + crypto_level, + offset, + data, + datalen) == 0 ? 0 : NGTCP2_ERR_CALLBACK_FAILURE; +} + +int Session::OnExtendMaxStreamsBidi( + ngtcp2_conn* conn, + uint64_t max_streams, + void* user_data) { + Session* session = static_cast(user_data); + + if (UNLIKELY(session->is_destroyed())) + return NGTCP2_ERR_CALLBACK_FAILURE; + + NgCallbackScope callback_scope(session); + session->ExtendMaxStreamsBidi(max_streams); + return 0; +} + +int Session::OnExtendMaxStreamsUni( + ngtcp2_conn* conn, + uint64_t max_streams, + void* user_data) { + Session* session = static_cast(user_data); + + if (UNLIKELY(session->is_destroyed())) + return NGTCP2_ERR_CALLBACK_FAILURE; + + NgCallbackScope callback_scope(session); + session->ExtendMaxStreamsUni(max_streams); + return 0; +} + +int Session::OnExtendMaxStreamsRemoteUni( + ngtcp2_conn* conn, + uint64_t max_streams, + void* user_data) { + Session* session = static_cast(user_data); + + if (UNLIKELY(session->is_destroyed())) + return NGTCP2_ERR_CALLBACK_FAILURE; + + NgCallbackScope callback_scope(session); + session->ExtendMaxStreamsRemoteUni(max_streams); + return 0; +} + +int Session::OnExtendMaxStreamsRemoteBidi( + ngtcp2_conn* conn, + uint64_t max_streams, + void* user_data) { + Session* session = static_cast(user_data); + + if (UNLIKELY(session->is_destroyed())) + return NGTCP2_ERR_CALLBACK_FAILURE; + + NgCallbackScope callback_scope(session); + session->ExtendMaxStreamsRemoteUni(max_streams); + return 0; +} + +int Session::OnExtendMaxStreamData( + ngtcp2_conn* conn, + stream_id id, + uint64_t max_data, + void* user_data, + void* stream_user_data) { + Session* session = static_cast(user_data); + + if (UNLIKELY(session->is_destroyed())) + return NGTCP2_ERR_CALLBACK_FAILURE; + + NgCallbackScope callback_scope(session); + session->ExtendMaxStreamData(id, max_data); + return 0; +} + +int Session::OnConnectionIDStatus( + ngtcp2_conn* conn, + int type, + uint64_t seq, + const ngtcp2_cid* cid, + const uint8_t* token, + void* user_data) { + Session* session = static_cast(user_data); + + if (UNLIKELY(session->is_destroyed())) + return NGTCP2_ERR_CALLBACK_FAILURE; + + if (token != nullptr) { + NgCallbackScope scope(session); + CID qcid(cid); + Debug(session, "Updating connection ID %s with reset token", qcid); + session->UpdateConnectionID(type, qcid, StatelessResetToken(token)); + } + return 0; +} + +int Session::OnHandshakeCompleted(ngtcp2_conn* conn, void* user_data) { + Session* session = static_cast(user_data); + + if (UNLIKELY(session->is_destroyed())) + return NGTCP2_ERR_CALLBACK_FAILURE; + + NgCallbackScope callback_scope(session); + + const ngtcp2_path* path = ngtcp2_conn_get_path(conn); + return session->HandshakeCompleted( + std::make_shared(path->remote.addr)) + ? 0 + : NGTCP2_ERR_CALLBACK_FAILURE; +} + +int Session::OnHandshakeConfirmed(ngtcp2_conn* conn, void* user_data) { + Session* session = static_cast(user_data); + + if (UNLIKELY(session->is_destroyed())) + return NGTCP2_ERR_CALLBACK_FAILURE; + + NgCallbackScope callback_scope(session); + session->HandshakeConfirmed(); + return 0; +} + +int Session::OnReceiveStreamData( + ngtcp2_conn* conn, + uint32_t flags, + stream_id id, + uint64_t offset, + const uint8_t* data, + size_t datalen, + void* user_data, + void* stream_user_data) { + Session* session = static_cast(user_data); + + if (UNLIKELY(session->is_destroyed())) + return NGTCP2_ERR_CALLBACK_FAILURE; + + NgCallbackScope callback_scope(session); + return session->ReceiveStreamData( + flags, + id, + data, + datalen, + offset) ? 0 : NGTCP2_ERR_CALLBACK_FAILURE; +} + +int Session::OnStreamOpen(ngtcp2_conn* conn, stream_id id, void* user_data) { + Session* session = static_cast(user_data); + + if (UNLIKELY(session->is_destroyed())) + return NGTCP2_ERR_CALLBACK_FAILURE; + + // We currently do not do anything with this callback. + // Stream instances are created implicitly only once the + // first chunk of stream data is received. + + return 0; +} + +int Session::OnAckedCryptoOffset( + ngtcp2_conn* conn, + ngtcp2_crypto_level crypto_level, + uint64_t offset, + uint64_t datalen, + void* user_data) { + Session* session = static_cast(user_data); + + if (UNLIKELY(session->is_destroyed())) + return NGTCP2_ERR_CALLBACK_FAILURE; + + NgCallbackScope callback_scope(session); + session->crypto_context()->AcknowledgeCryptoData(crypto_level, datalen); + return 0; +} + +int Session::OnAckedStreamDataOffset( + ngtcp2_conn* conn, + stream_id id, + uint64_t offset, + uint64_t datalen, + void* user_data, + void* stream_user_data) { + Session* session = static_cast(user_data); + + if (UNLIKELY(session->is_destroyed())) + return NGTCP2_ERR_CALLBACK_FAILURE; + + NgCallbackScope callback_scope(session); + session->AckedStreamDataOffset(id, offset, datalen); + return 0; +} + +int Session::OnSelectPreferredAddress( + ngtcp2_conn* conn, + ngtcp2_addr* dest, + const ngtcp2_preferred_addr* paddr, + void* user_data) { + Session* session = static_cast(user_data); + + if (UNLIKELY(session->is_destroyed())) + return NGTCP2_ERR_CALLBACK_FAILURE; + + NgCallbackScope callback_scope(session); + + // The paddr parameter contains the server advertised preferred + // address. The dest parameter contains the address that is + // actually being used. If the preferred address is selected, + // then the contents of paddr are copied over to dest. + session->SelectPreferredAddress( + PreferredAddress(session->env(), dest, paddr)); + return 0; +} + +int Session::OnStreamClose( + ngtcp2_conn* conn, + stream_id id, + uint64_t app_error_code, + void* user_data, + void* stream_user_data) { + Session* session = static_cast(user_data); + + if (UNLIKELY(session->is_destroyed())) + return NGTCP2_ERR_CALLBACK_FAILURE; + + NgCallbackScope callback_scope(session); + session->StreamClose(id, app_error_code); + return 0; +} + +int Session::OnStreamReset( + ngtcp2_conn* conn, + stream_id id, + uint64_t final_size, + uint64_t app_error_code, + void* user_data, + void* stream_user_data) { + Session* session = static_cast(user_data); + + if (UNLIKELY(session->is_destroyed())) + return NGTCP2_ERR_CALLBACK_FAILURE; + + NgCallbackScope callback_scope(session); + session->StreamReset(id, final_size, app_error_code); + return 0; +} + +int Session::OnRand( + uint8_t* dest, + size_t destlen, + const ngtcp2_rand_ctx* rand_ctx, + ngtcp2_rand_usage usage) { + // For now, we ignore both rand_ctx and usage. The rand_ctx allows + // a custom entropy source to be passed in to the ngtcp2 configuration. + // We don't make use of that mechanism. The usage differentiates what + // the random data is for, in case an implementation wishes to apply + // a different mechanism based on purpose. We don't, at least for now. + crypto::EntropySource(dest, destlen); + return 0; +} + +int Session::OnGetNewConnectionID( + ngtcp2_conn* conn, + ngtcp2_cid* cid, + uint8_t* token, + size_t cidlen, + void* user_data) { + Session* session = static_cast(user_data); + + if (UNLIKELY(session->is_destroyed())) + return NGTCP2_ERR_CALLBACK_FAILURE; + + NgCallbackScope scope(session); + session->GetNewConnectionID(cid, token, cidlen); + return 0; +} + +int Session::OnRemoveConnectionID( + ngtcp2_conn* conn, + const ngtcp2_cid* cid, + void* user_data) { + Session* session = static_cast(user_data); + + if (UNLIKELY(session->is_destroyed())) + return NGTCP2_ERR_CALLBACK_FAILURE; + + if (session->is_server()) { + NgCallbackScope callback_scope(session); + session->endpoint()->DisassociateCID(CID(cid)); + } + return 0; +} + +int Session::OnPathValidation( + ngtcp2_conn* conn, + const ngtcp2_path* path, + ngtcp2_path_validation_result res, + void* user_data) { + Session* session = static_cast(user_data); + + if (UNLIKELY(session->is_destroyed())) + return NGTCP2_ERR_CALLBACK_FAILURE; + + NgCallbackScope callback_scope(session); + session->PathValidation(path, res); + return 0; +} + +int Session::OnVersionNegotiation( + ngtcp2_conn* conn, + const ngtcp2_pkt_hd* hd, + const uint32_t* sv, + size_t nsv, + void* user_data) { + Session* session = static_cast(user_data); + + if (UNLIKELY(session->is_destroyed())) + return NGTCP2_ERR_CALLBACK_FAILURE; + + NgCallbackScope callback_scope(session); + session->VersionNegotiation(sv, nsv); + return 0; +} + +int Session::OnStatelessReset( + ngtcp2_conn* conn, + const ngtcp2_pkt_stateless_reset* sr, + void* user_data) { + Session* session = static_cast(user_data); + + if (UNLIKELY(session->is_destroyed())) + return NGTCP2_ERR_CALLBACK_FAILURE; + + session->stateless_reset_ = true; + return 0; +} + +int Session::OnDatagram( + ngtcp2_conn* conn, + uint32_t flags, + const uint8_t* data, + size_t datalen, + void* user_data) { + Session* session = static_cast(user_data); + + if (UNLIKELY(session->is_destroyed())) + return NGTCP2_ERR_CALLBACK_FAILURE; + + session->Datagram(flags, data, datalen); + return 0; +} + +void Session::DoDestroy(const FunctionCallbackInfo& args) { + Session* session; + ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); + session->Destroy(); +} + +void Session::GetRemoteAddress(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + Session* session; + ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); + CHECK(args[0]->IsObject()); + args.GetReturnValue().Set( + session->remote_address()->ToJS(env, args[0].As())); +} + +void Session::GetCertificate(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + Session* session; + ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); + Local ret; + if (session->crypto_context()->cert(env).ToLocal(&ret)) + args.GetReturnValue().Set(ret); +} + +void Session::GetPeerCertificate(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + Session* session; + ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); + Local ret; + if (session->crypto_context()->peer_cert(env).ToLocal(&ret)) + args.GetReturnValue().Set(ret); +} + +void Session::SilentClose(const FunctionCallbackInfo& args) { + Session* session; + ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); + ProcessEmitWarning( + session->env(), + "Forcing silent close of Session for testing purposes only"); + session->Close(Session::SessionCloseFlags::SILENT); +} + +void Session::GracefulClose(const FunctionCallbackInfo& args) { + Session* session; + ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); + session->StartGracefulClose(); +} + +void Session::UpdateKey(const FunctionCallbackInfo& args) { + Session* session; + ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); + // Initiating a key update may fail if it is done too early (either + // before the TLS handshake has been confirmed or while a previous + // key update is being processed). When it fails, InitiateKeyUpdate() + // will return false. + args.GetReturnValue().Set(session->crypto_context()->InitiateKeyUpdate()); +} + +void Session::DoDetachFromEndpoint(const FunctionCallbackInfo& args) { + Session* session; + ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); + session->DetachFromEndpoint(); +} + +void Session::OnClientHelloDone(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + Session* session; + ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); + Local cons = env->secure_context_constructor_template(); + crypto::SecureContext* context = nullptr; + if (args[0]->IsObject() && cons->HasInstance(args[0])) + context = Unwrap(args[0].As()); + session->crypto_context()->OnClientHelloDone( + BaseObjectPtr(context)); +} + +void Session::OnOCSPDone(const FunctionCallbackInfo& args) { + Session* session; + ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); + if (UNLIKELY(args[0]->IsUndefined())) return; + + // TODO(@jasnell): Implement properly + // session->crypto_context()->OnOCSPDone(args[0]); + session->crypto_context()->OnOCSPDone(std::shared_ptr()); +} + +void Session::GetEphemeralKeyInfo(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + Session* session; + ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); + Local ret; + if (session->crypto_context()->ephemeral_key(env).ToLocal(&ret)) + args.GetReturnValue().Set(ret); +} + +void Session::DoAttachToEndpoint(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + Session* session; + ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder()); + CHECK(EndpointWrap::HasInstance(env, args[0])); + EndpointWrap* endpoint; + ASSIGN_OR_RETURN_UNWRAP(&endpoint, args[0]); + args.GetReturnValue().Set( + session->AttachToNewEndpoint(endpoint, args[1]->IsTrue())); +} + +template <> +void StatsTraitsImpl::ToString( + const Session& ptr, + AddStatsField add_field) { +#define V(n, name, label) add_field(label, ptr.GetStat(&SessionStats::name)); + SESSION_STATS(V) +#undef V + } + +// Determines which Application variant the Session will be using +// based on the alpn configured for the application. For now, this is +// determined through configuration when tghe Session is created +// and is not negotiable. In the future, we may allow it to be negotiated. +Session::Application* Session::SelectApplication( + const Application::Config& config) { + // if (alpn == NGHTTP3_ALPN_H3) { + // Debug(this, "Selecting HTTP/3 Application"); + // return new Http3Application(this); + // } + + // In the future, we may end up supporting additional + // QUIC protocols. As they are added, extend the cases + // here to create and return them. + + return new DefaultApplication(this, config); +} + +Session::Application::Application( + Session* session, + const Application::Config& config) + : session_(session), + config_(config) {} + +void Session::Application::Acknowledge( + stream_id id, + uint64_t offset, + size_t datalen) { + BaseObjectPtr stream = session()->FindStream(id); + if (LIKELY(stream)) { + stream->Acknowledge(offset, datalen); + ResumeStream(id); + } +} + +std::unique_ptr Session::Application::CreateStreamDataPacket() { + return std::make_unique( + session()->max_packet_length(), + "stream data"); +} + +bool Session::Application::Initialize() { + if (needs_init_) needs_init_ = false; + return !needs_init_; +} + +void Session::Application::MaybeSetFin(const StreamData& stream_data) { + if (ShouldSetFin(stream_data)) + set_stream_fin(stream_data.id); +} + +bool Session::Application::SendPendingData() { + // The maximum number of packets to send per call + static constexpr size_t kMaxPackets = 16; + PathStorage path; + std::unique_ptr packet; + uint8_t* pos = nullptr; + size_t packets_sent = 0; + int err; + + for (;;) { + ssize_t ndatalen; + StreamData stream_data; + err = GetStreamData(&stream_data); + if (err < 0) { + session()->set_last_error(kQuicInternalError); + return false; + } + + // If stream_data.id is -1, then we're not serializing any data for any + // specific stream. We still need to process QUIC session packets tho. + if (stream_data.id > -1) + Debug(session(), "Serializing packets for stream id %" PRId64, + stream_data.id); + else + Debug(session(), "Serializing session packets"); + + // If the packet was sent previously, then packet will have been reset. + if (!packet) { + packet = CreateStreamDataPacket(); + pos = packet->data(); + } + + ssize_t nwrite = WriteVStream(&path, pos, &ndatalen, stream_data); + + if (nwrite <= 0) { + switch (nwrite) { + case 0: + goto congestion_limited; + case NGTCP2_ERR_PKT_NUM_EXHAUSTED: + // There is a finite number of packets that can be sent + // per connection. Once those are exhausted, there's + // absolutely nothing we can do except immediately + // and silently tear down the Session. This has + // to be silent because we can't even send a + // CONNECTION_CLOSE since even those require a + // packet number. + session()->Close(Session::SessionCloseFlags::SILENT); + return false; + case NGTCP2_ERR_STREAM_DATA_BLOCKED: + session()->StreamDataBlocked(stream_data.id); + if (session()->max_data_left() == 0) + goto congestion_limited; + // Fall through + case NGTCP2_ERR_STREAM_SHUT_WR: + if (UNLIKELY(!BlockStream(stream_data.id))) + return false; + continue; + case NGTCP2_ERR_STREAM_NOT_FOUND: + continue; + case NGTCP2_ERR_WRITE_MORE: + CHECK_GT(ndatalen, 0); + CHECK(StreamCommit(&stream_data, ndatalen)); + pos += ndatalen; + continue; + } + session()->set_last_error(kQuicInternalError); + return false; + } + + pos += nwrite; + + if (ndatalen >= 0) + CHECK(StreamCommit(&stream_data, ndatalen)); + + Debug(session(), "Sending %" PRIu64 " bytes in serialized packet", nwrite); + packet->set_length(nwrite); + if (!session()->SendPacket(std::move(packet), path)) + return false; + packet.reset(); + pos = nullptr; + MaybeSetFin(stream_data); + if (++packets_sent == kMaxPackets) + break; + } + return true; + +congestion_limited: + // We are either congestion limited or done. + if (pos - packet->data()) { + // Some data was serialized into the packet. We need to send it. + packet->set_length(pos - packet->data()); + Debug(session(), "Congestion limited, but %" PRIu64 " bytes pending", + packet->length()); + if (!session()->SendPacket(std::move(packet), path)) + return false; + } + return true; +} + +void Session::Application::StreamClose( + stream_id id, + error_code app_error_code) { + BaseObjectPtr stream = session()->FindStream(id); + if (stream) { + // Calling stream->OnClose() frees up the internal state and + // disconnects the stream from the session. The subsequent + // call to OnStreamClose notifies the JavaScript side (or + // whichever listener is attached) so that any references and + // state on that side can be freed up. + + BindingState* state = BindingState::Get(env()); + HandleScope scope(env()->isolate()); + Context::Scope context_scope(env()->context()); + + CallbackScope cb_scope(session()); + + Local argv[] = { + Number::New(env()->isolate(), static_cast(id)), + Number::New(env()->isolate(), static_cast(app_error_code)) + }; + + // Grab a shared pointer to this to prevent the Session + // from being freed while the MakeCallback is running. + BaseObjectPtr ptr(session()); + + USE(state->stream_close_callback()->Call( + env()->context(), + session()->object(), + arraysize(argv), + argv)); + + stream->OnClose(); + } +} + +void Session::Application::StreamHeaders( + stream_id id, + Stream::HeadersKind kind, + const Stream::HeaderList& headers) { + BindingState* state = BindingState::Get(env()); + HandleScope scope(env()->isolate()); + Context::Scope context_scope(env()->context()); + + CallbackScope cb_scope(session()); + + std::vector> head(headers.size()); + for (const auto& header : headers) { + Local pair[2]; + if (UNLIKELY(!header->GetName(state).ToLocal(&pair[0])) || + UNLIKELY(!header->GetValue(state).ToLocal(&pair[1]))) { + return; + } + + head.emplace_back(Array::New(env()->isolate(), pair, 2)); + } + + Local argv[] = { + Number::New(env()->isolate(), static_cast(id)), + Array::New(env()->isolate(), head.data(), head.size()), + Integer::NewFromUnsigned(env()->isolate(), static_cast(kind)), + }; + + BaseObjectPtr ptr(session()); + + USE(state->stream_headers_callback()->Call( + env()->context(), + session()->object(), + arraysize(argv), + argv)); +} + +void Session::Application::StreamReset( + stream_id id, + error_code app_error_code) { + BindingState* state = BindingState::Get(env()); + HandleScope scope(env()->isolate()); + Context::Scope context_scope(env()->context()); + + CallbackScope cb_scope(session()); + + Local argv[] = { + Number::New(env()->isolate(), static_cast(id)), + Number::New(env()->isolate(), static_cast(app_error_code)) + }; + + // Grab a shared pointer to this to prevent the Session + // from being freed while the MakeCallback is running. + BaseObjectPtr ptr(session()); + + USE(state->stream_reset_callback()->Call( + env()->context(), + session()->object(), + arraysize(argv), + argv)); +} + +ssize_t Session::Application::WriteVStream( + PathStorage* path, + uint8_t* buf, + ssize_t* ndatalen, + const StreamData& stream_data) { + CHECK_LE(stream_data.count, kMaxVectorCount); + + uint32_t flags = NGTCP2_WRITE_STREAM_FLAG_NONE; + if (stream_data.remaining > 0) + flags |= NGTCP2_WRITE_STREAM_FLAG_MORE; + if (stream_data.fin) + flags |= NGTCP2_WRITE_STREAM_FLAG_FIN; + + return ngtcp2_conn_writev_stream( + session()->connection(), + &path->path, + nullptr, + buf, + session()->max_packet_length(), + ndatalen, + flags, + stream_data.id, + stream_data.buf, + stream_data.count, + uv_hrtime()); +} + +void Session::Application::set_stream_fin(stream_id id) { + BaseObjectPtr stream = session()->FindStream(id); + CHECK(stream); + stream->set_fin_sent(); +} + +std::string Session::RemoteTransportParamsDebug::ToString() const { + ngtcp2_transport_params params; + ngtcp2_conn_get_remote_transport_params(session->connection(), ¶ms); + std::string out = "Remote Transport Params:\n"; + out += " Ack Delay Exponent: " + + std::to_string(params.ack_delay_exponent) + "\n"; + out += " Active Connection ID Limit: " + + std::to_string(params.active_connection_id_limit) + "\n"; + out += " Disable Active Migration: " + + std::string(params.disable_active_migration ? "Yes" : "No") + "\n"; + out += " Initial Max Data: " + + std::to_string(params.initial_max_data) + "\n"; + out += " Initial Max Stream Data Bidi Local: " + + std::to_string(params.initial_max_stream_data_bidi_local) + "\n"; + out += " Initial Max Stream Data Bidi Remote: " + + std::to_string(params.initial_max_stream_data_bidi_remote) + "\n"; + out += " Initial Max Stream Data Uni: " + + std::to_string(params.initial_max_stream_data_uni) + "\n"; + out += " Initial Max Streams Bidi: " + + std::to_string(params.initial_max_streams_bidi) + "\n"; + out += " Initial Max Streams Uni: " + + std::to_string(params.initial_max_streams_uni) + "\n"; + out += " Max Ack Delay: " + + std::to_string(params.max_ack_delay) + "\n"; + out += " Max Idle Timeout: " + + std::to_string(params.max_idle_timeout) + "\n"; + out += " Max Packet Size: " + + std::to_string(params.max_udp_payload_size) + "\n"; + + if (!session->is_server()) { + if (params.retry_scid_present) { + CID cid(params.original_dcid); + CID retry(params.retry_scid); + out += " Original Connection ID: " + cid.ToString() + "\n"; + out += " Retry SCID: " + retry.ToString() + "\n"; + } else { + out += " Original Connection ID: N/A \n"; + } + + if (params.preferred_address_present) { + out += " Preferred Address Present: Yes\n"; + // TODO(@jasnell): Serialize the IPv4 and IPv6 address options + } else { + out += " Preferred Address Present: No\n"; + } + + if (params.stateless_reset_token_present) { + StatelessResetToken token(params.stateless_reset_token); + out += " Stateless Reset Token: " + token.ToString() + "\n"; + } else { + out += " Stateless Reset Token: N/A"; + } + } + return out; +} + +DefaultApplication::DefaultApplication( + Session* session, + const Application::Config& config) + : Session::Application(session, config) { + Debug(session, "Using default application"); +} + +void DefaultApplication::ScheduleStream(stream_id id) { + BaseObjectPtr stream = session()->FindStream(id); + if (LIKELY(stream && !stream->is_destroyed())) { + Debug(session(), "Scheduling stream %" PRIu64, id); + stream->Schedule(&stream_queue_); + } +} + +void DefaultApplication::UnscheduleStream(stream_id id) { + BaseObjectPtr stream = session()->FindStream(id); + if (LIKELY(stream)) { + Debug(session(), "Unscheduling stream %" PRIu64, id); + stream->Unschedule(); + } +} + +void DefaultApplication::ResumeStream(stream_id id) { + ScheduleStream(id); +} + +bool DefaultApplication::ReceiveStreamData( + uint32_t flags, + stream_id id, + const uint8_t* data, + size_t datalen, + uint64_t offset) { + + // One potential DOS attack vector is to send a bunch of + // empty stream frames to commit resources. Check that + // here. Essentially, we only want to create a new stream + // if the datalen is greater than 0, otherwise, we ignore + // the packet. ngtcp2 should be handling this for us, + // but we handle it just to be safe. + if (UNLIKELY(datalen == 0)) + return true; + + // Ensure that the QuicStream exists. + Debug(session(), "Receiving stream data for %" PRIu64, id); + BaseObjectPtr stream = session()->FindStream(id); + if (!stream) { + // Because we are closing gracefully, we are not allowing + // new streams to be created. Shut it down immediately + // and commit no further resources. + if (session()->is_graceful_closing()) { + session()->ShutdownStream(id, NGTCP2_ERR_CLOSING); + return true; + } + + stream = session()->CreateStream(id); + } + CHECK(stream); + + // If the stream ended up being destroyed immediately after + // creation, just skip the data processing and return. + if (UNLIKELY(stream->is_destroyed())) + return true; + + stream->ReceiveData(flags, data, datalen, offset); + return true; +} + +int DefaultApplication::GetStreamData(StreamData* stream_data) { + Stream* stream = stream_queue_.PopFront(); + // If stream is nullptr, there are no streams with data pending. + if (stream == nullptr) + return 0; + + stream_data->stream.reset(stream); + stream_data->id = stream->id(); + + auto next = [&]( + int status, + const ngtcp2_vec* data, + size_t count, + bob::Done done) { + switch (status) { + case bob::Status::STATUS_BLOCK: + // Fall through + case bob::Status::STATUS_WAIT: + // Fall through + case bob::Status::STATUS_EOS: + return; + case bob::Status::STATUS_END: + stream_data->fin = 1; + } + + stream_data->count = count; + + if (count > 0) { + stream->Schedule(&stream_queue_); + stream_data->remaining = get_length(data, count); + } else { + stream_data->remaining = 0; + } + }; + + if (LIKELY(!stream->is_eos())) { + CHECK_GE(stream->Pull( + std::move(next), + bob::Options::OPTIONS_SYNC, + stream_data->data, + arraysize(stream_data->data), + kMaxVectorCount), 0); + } + + return 0; +} + +bool DefaultApplication::StreamCommit(StreamData* stream_data, size_t datalen) { + CHECK(stream_data->stream); + stream_data->remaining -= datalen; + Consume(&stream_data->buf, &stream_data->count, datalen); + stream_data->stream->Commit(datalen); + return true; +} + +bool DefaultApplication::ShouldSetFin(const StreamData& stream_data) { + if (!stream_data.stream || !IsEmpty(stream_data.buf, stream_data.count)) + return false; + // TODO(@jasnell): Revisit this? + // return !stream_data.stream->is_writable(); + return true; +} + +bool OptionsObject::HasInstance(Environment* env, const Local& value) { + return GetConstructorTemplate(env)->HasInstance(value); +} + +Local OptionsObject::GetConstructorTemplate( + Environment* env) { + BindingState* state = env->GetBindingData(env->context()); + Local tmpl = + state->session_options_constructor_template(); + if (tmpl.IsEmpty()) { + tmpl = env->NewFunctionTemplate(New); + tmpl->Inherit(BaseObject::GetConstructorTemplate(env)); + tmpl->InstanceTemplate()->SetInternalFieldCount( + OptionsObject::kInternalFieldCount); + tmpl->SetClassName(FIXED_ONE_BYTE_STRING(env->isolate(), "OptionsObject")); + env->SetProtoMethod(tmpl, "setPreferredAddress", SetPreferredAddress); + env->SetProtoMethod(tmpl, "setTransportParams", SetTransportParams); + env->SetProtoMethod(tmpl, "setTLSOptions", SetTLSOptions); + env->SetProtoMethod(tmpl, "setSessionResume", SetSessionResume); + state->set_session_options_constructor_template(tmpl); + } + return tmpl; +} + +void OptionsObject::Initialize(Environment* env, Local target) { + env->SetConstructorFunction( + target, + "OptionsObject", + GetConstructorTemplate(env)); +} + +void OptionsObject::New(const FunctionCallbackInfo& args) { + CHECK(args.IsConstructCall()); + Environment* env = Environment::GetCurrent(args); + + CHECK(args[0]->IsString()); // ALPN + CHECK_IMPLIES( + !args[1]->IsUndefined(), + args[1]->IsString()); // Hostname + CHECK_IMPLIES( // CID + !args[2]->IsUndefined(), + args[2]->IsArrayBuffer() || args[2]->IsArrayBufferView()); + CHECK_IMPLIES( // Preferred address strategy + !args[3]->IsUndefined(), + args[3]->IsInt32()); + + Utf8Value alpn(env->isolate(), args[0]); + + OptionsObject* options = new OptionsObject(env, args.This()); + options->options()->alpn = *alpn; + + // Add support for the other strategies once implemented + if (RandomConnectionIDBase::HasInstance(env, args[4])) { + RandomConnectionIDBase* cid_strategy; + ASSIGN_OR_RETURN_UNWRAP(&cid_strategy, args[4]); + options->options()->cid_strategy = cid_strategy->strategy(); + options->options()->cid_strategy_strong_ref.reset(cid_strategy); + } else { + UNREACHABLE(); + } + + if (!args[1]->IsUndefined()) { + Utf8Value hostname(env->isolate(), args[1]); + options->options()->hostname = *hostname; + } + + if (!args[2]->IsUndefined()) { + crypto::ArrayBufferOrViewContents cid(args[2]); + // CHECK_LE(cid.size(), NGTCP2_MAX_CIDLEN); + if (cid.size() > 0) { + memcpy( + options->options()->dcid.data(), + cid.data(), + cid.size()); + options->options()->dcid.set_length(cid.size()); + } + } + + if (!args[3]->IsUndefined()) { + PreferredAddress::Policy policy = + static_cast(args[3].As()->Value()); + switch (policy) { + case PreferredAddress::Policy::USE: + options->options()->preferred_address_strategy = + Session::UsePreferredAddressStrategy; + break; + case PreferredAddress::Policy::IGNORE: + options->options()->preferred_address_strategy = + Session::IgnorePreferredAddressStrategy; + break; + default: + UNREACHABLE(); + } + } +} + +void OptionsObject::SetPreferredAddress( + const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + OptionsObject* options; + ASSIGN_OR_RETURN_UNWRAP(&options, args.Holder()); + + CHECK(SocketAddressBase::HasInstance(env, args[0])); + SocketAddressBase* preferred_addr; + ASSIGN_OR_RETURN_UNWRAP(&preferred_addr, args[0]); + + switch (preferred_addr->address()->family()) { + case AF_INET: + options->options()->preferred_address_ipv4 = preferred_addr->address(); + break; + case AF_INET6: + options->options()->preferred_address_ipv6 = preferred_addr->address(); + break; + default: + UNREACHABLE(); + } +} + +Maybe OptionsObject::SetOption( + const Local& object, + const Local& name, + uint64_t Session::Options::*member) { + Local value; + if (!object->Get(env()->context(), name).ToLocal(&value)) + return Nothing(); + + if (value->IsUndefined()) + return Just(false); + + CHECK_IMPLIES(!value->IsBigInt(), value->IsNumber()); + + uint64_t val = 0; + if (value->IsBigInt()) { + bool lossless = true; + val = value.As()->Uint64Value(&lossless); + if (!lossless) { + Utf8Value label(env()->isolate(), name); + THROW_ERR_OUT_OF_RANGE( + env(), + (std::string("options.") + (*label) + " is out of range").c_str()); + return Nothing(); + } + } else { + val = static_cast(value.As()->Value()); + } + options_.get()->*member = val; + return Just(true); +} + +Maybe OptionsObject::SetOption( + const Local& object, + const Local& name, + uint32_t Session::Options::*member) { + Local value; + if (!object->Get(env()->context(), name).ToLocal(&value)) + return Nothing(); + + if (value->IsUndefined()) + return Just(false); + + CHECK(value->IsUint32()); + uint32_t val = value.As()->Value(); + options_.get()->*member = val; + return Just(true); +} + +Maybe OptionsObject::SetOption( + const Local& object, + const Local& name, + bool Session::Options::*member) { + Local value; + if (!object->Get(env()->context(), name).ToLocal(&value)) + return Nothing(); + if (value->IsUndefined()) + return Just(false); + CHECK(value->IsBoolean()); + options_.get()->*member = value->IsTrue(); + return Just(true); +} + +void OptionsObject::SetTransportParams( + const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + BindingState* state = env->GetBindingData(env->context()); + OptionsObject* options; + ASSIGN_OR_RETURN_UNWRAP(&options, args.Holder()); + + CHECK(args[0]->IsObject()); + Local obj = args[0].As(); + + if (UNLIKELY(options->SetOption( + obj, + state->initial_max_stream_data_bidi_local_string(), + &Session::Options::initial_max_stream_data_bidi_local).IsNothing()) || + UNLIKELY(options->SetOption( + obj, + state->initial_max_stream_data_bidi_remote_string(), + &Session::Options::initial_max_stream_data_bidi_remote) + .IsNothing()) || + UNLIKELY(options->SetOption( + obj, + state->initial_max_stream_data_uni_string(), + &Session::Options::initial_max_stream_data_uni).IsNothing()) || + UNLIKELY(options->SetOption( + obj, + state->initial_max_data_string(), + &Session::Options::initial_max_data).IsNothing()) || + UNLIKELY(options->SetOption( + obj, + state->initial_max_streams_bidi_string(), + &Session::Options::initial_max_streams_bidi).IsNothing()) || + UNLIKELY(options->SetOption( + obj, + state->initial_max_streams_uni_string(), + &Session::Options::initial_max_streams_uni).IsNothing()) || + UNLIKELY(options->SetOption( + obj, + state->max_idle_timeout_string(), + &Session::Options::max_idle_timeout).IsNothing()) || + UNLIKELY(options->SetOption( + obj, + state->active_connection_id_limit_string(), + &Session::Options::active_connection_id_limit).IsNothing()) || + UNLIKELY(options->SetOption( + obj, + state->ack_delay_exponent_string(), + &Session::Options::ack_delay_exponent).IsNothing()) || + UNLIKELY(options->SetOption( + obj, + state->max_ack_delay_string(), + &Session::Options::max_ack_delay).IsNothing()) || + UNLIKELY(options->SetOption( + obj, + state->max_datagram_frame_size_string(), + &Session::Options::max_datagram_frame_size).IsNothing()) || + UNLIKELY(options->SetOption( + obj, + state->disable_active_migration_string(), + &Session::Options::disable_active_migration).IsNothing())) { + // The if block intentionally does nothing. The code is structured + // like this to shortcircuit if any of the SetOptions() returns Nothing. + } +} + +void OptionsObject::SetTLSOptions(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + BindingState* state = env->GetBindingData(env->context()); + OptionsObject* options; + ASSIGN_OR_RETURN_UNWRAP(&options, args.Holder()); + + CHECK(args[0]->IsObject()); + Local obj = args[0].As(); + + if (UNLIKELY(options->SetOption( + obj, + state->reject_unauthorized_string(), + &Session::Options::reject_unauthorized).IsNothing()) || + UNLIKELY(options->SetOption( + obj, + state->enable_tls_trace_string(), + &Session::Options::enable_tls_trace).IsNothing()) || + UNLIKELY(options->SetOption( + obj, + state->request_peer_certificate_string(), + &Session::Options::request_peer_certificate).IsNothing()) || + UNLIKELY(options->SetOption( + obj, + state->request_ocsp_string(), + &Session::Options::request_ocsp).IsNothing()) || + UNLIKELY(options->SetOption( + obj, + state->verify_hostname_identity_string(), + &Session::Options::verify_hostname_identity).IsNothing()) || + UNLIKELY(options->SetOption( + obj, + state->handshake_timeout_string(), + &Session::Options::handshake_timeout).IsNothing()) || + UNLIKELY(options->SetOption( + obj, + state->min_dh_size_string(), + &Session::Options::min_dh_size).IsNothing())) { + // The if block intentionally does nothing. The code is structured + // like this to shortcircuit if any of the SetOptions() returns Nothing. + } + + Local val; + options->options()->psk_callback_present = + obj->Get(env->context(), state->pskcallback_string()).ToLocal(&val) && + val->IsFunction(); +} + +void OptionsObject::SetSessionResume(const FunctionCallbackInfo& args) { + // TODO(@jasnell): Implement +} + +OptionsObject::OptionsObject( + Environment* env, + Local object, + std::shared_ptr options) + : BaseObject(env, object), + options_(std::move(options)) { + MakeWeak(); +} + +void Session::Options::MemoryInfo(MemoryTracker* tracker) const { + tracker->TrackFieldWithSize("alpn", alpn.length()); + tracker->TrackFieldWithSize("hostname", hostname.length()); +} + +void OptionsObject::MemoryInfo(MemoryTracker* tracker) const { + tracker->TrackField("options", options_); +} + +} // namespace quic +} // namespace node diff --git a/src/quic/session.h b/src/quic/session.h new file mode 100644 index 00000000000000..d02a1d6d914370 --- /dev/null +++ b/src/quic/session.h @@ -0,0 +1,1696 @@ +#ifndef SRC_QUIC_SESSION_H_ +#define SRC_QUIC_SESSION_H_ +#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS + +#include "quic/buffer.h" +#include "quic/crypto.h" +#include "quic/stats.h" +#include "quic/stream.h" +#include "quic/quic.h" +#include "aliased_struct.h" +#include "async_wrap.h" +#include "base_object.h" +#include "crypto/crypto_context.h" +#include "env.h" +#include "node_http_common.h" +#include "node_sockaddr.h" +#include "node_worker.h" +#include "timer_wrap.h" + +#include +#include +#include + +#include +#include +#include +#include + +namespace node { +namespace quic { + +#define SESSION_STATS(V) \ + V(CREATED_AT, created_at, "Created at") \ + V(HANDSHAKE_COMPLETED_AT, handshake_completed_at, "Handshake completed") \ + V(HANDSHAKE_CONFIRMED_AT, handshake_confirmed_at, "Handshake confirmed") \ + V(SENT_AT, sent_at, "Last sent at") \ + V(RECEIVED_AT, received_at, "Last received at") \ + V(CLOSING_AT, closing_at, "Closing") \ + V(DESTROYED_AT, destroyed_at, "Destroyed at") \ + V(BYTES_RECEIVED, bytes_received, "Bytes received") \ + V(BYTES_SENT, bytes_sent, "Bytes sent") \ + V(BIDI_STREAM_COUNT, bidi_stream_count, "Bidi stream count") \ + V(UNI_STREAM_COUNT, uni_stream_count, "Uni stream count") \ + V(STREAMS_IN_COUNT, streams_in_count, "Streams in count") \ + V(STREAMS_OUT_COUNT, streams_out_count, "Streams out count") \ + V(KEYUPDATE_COUNT, keyupdate_count, "Key update count") \ + V(LOSS_RETRANSMIT_COUNT, loss_retransmit_count, "Loss retransmit count") \ + V(MAX_BYTES_IN_FLIGHT, max_bytes_in_flight, "Max bytes in flight") \ + V(BLOCK_COUNT, block_count, "Block count") \ + V(BYTES_IN_FLIGHT, bytes_in_flight, "Bytes in flight") \ + V(CONGESTION_RECOVERY_START_TS, \ + congestion_recovery_start_ts, \ + "Congestion recovery start time") \ + V(CWND, cwnd, "Size of the congestion window") \ + V(DELIVERY_RATE_SEC, delivery_rate_sec, "Delivery bytes/sec") \ + V(FIRST_RTT_SAMPLE_TS, first_rtt_sample_ts, "First RTT sample time") \ + V(INITIAL_RTT, initial_rtt, "Initial RTT") \ + V(LAST_TX_PKT_TS, last_tx_pkt_ts, "Last TX packet time") \ + V(LATEST_RTT, latest_rtt, "Latest RTT") \ + V(LOSS_DETECTION_TIMER, \ + loss_detection_timer, \ + "Loss detection timer deadline") \ + V(LOSS_TIME, loss_time, "Loss time") \ + V(MAX_UDP_PAYLOAD_SIZE, max_udp_payload_size, "Max UDP payload size") \ + V(MIN_RTT, min_rtt, "Minimum RTT so far") \ + V(PTO_COUNT, pto_count, "PTO count") \ + V(RTTVAR, rttvar, "Mean deviation of observed RTT") \ + V(SMOOTHED_RTT, smoothed_rtt, "Smoothed RTT") \ + V(SSTHRESH, ssthresh, "Slow start threshold") \ + V(RECEIVE_RATE, receive_rate, "Receive Rate / Sec") \ + V(SEND_RATE, send_rate, "Send Rate Sec") + +// Every Session instance maintains an AliasedStruct that is used to quickly +// toggle certain settings back and forth or to access various details with +// lower cost. +#define SESSION_STATE(V) \ + V(CLIENT_HELLO_ENABLED, client_hello_enabled, uint8_t) \ + V(DATAGRAM_ENABLED, datagram_enabled, uint8_t) \ + V(KEYLOG_ENABLED, keylog_enabled, uint8_t) \ + V(OCSP_ENABLED, ocsp_enabled, uint8_t) \ + V(PATH_VALIDATED_ENABLED, path_validated_enabled, uint8_t) \ + V(USE_PREFERRED_ADDRESS_ENABLED, use_preferred_address_enabled, uint8_t) \ + V(CLOSING, closing, uint8_t) \ + V(CLOSING_TIMER_ENABLED, closing_timer_enabled, uint8_t) \ + V(CONNECTION_CLOSE_SCOPE, in_connection_close_scope, uint8_t) \ + V(DESTROYED, destroyed, uint8_t) \ + V(GRACEFUL_CLOSING, graceful_closing, uint8_t) \ + V(HANDSHAKE_CONFIRMED, handshake_confirmed, uint8_t) \ + V(IDLE_TIMEOUT, idle_timeout, uint8_t) \ + V(MAX_DATA_LEFT, max_data_left, uint64_t) \ + V(MAX_STREAMS_BIDI, max_streams_bidi, uint64_t) \ + V(MAX_STREAMS_UNI, max_streams_uni, uint64_t) \ + V(NGTCP2_CALLBACK, in_ngtcp2_callback, uint8_t) \ + V(SILENT_CLOSE, silent_close, uint8_t) \ + V(STATELESS_RESET, stateless_reset, uint8_t) \ + V(TRANSPORT_PARAMS_SET, transport_params_set, uint8_t) \ + V(WRAPPED, wrapped, uint8_t) + +class Endpoint; +class EndpointWrap; +class QLogStream; +class Session; + +using StreamsMap = std::unordered_map>; + +using PreferredAddressStrategy = void(*)(Session*, const PreferredAddress&); +using ConnectionCloseFn = + ssize_t(*)( + ngtcp2_conn* conn, + ngtcp2_path* path, + ngtcp2_pkt_info* pi, + uint8_t* dest, + size_t destlen, + uint64_t error_code, + ngtcp2_tstamp ts); + + +static const int kInitialClientBufferLength = 4096; + +#define V(name, _, __) IDX_STATS_SESSION_##name, +enum SessionStatsIdx : int { + SESSION_STATS(V) + IDX_STATS_SESSION_COUNT +}; +#undef V + +#define V(name, _, __) IDX_STATE_SESSION_##name, +enum SessionStateIdx { + SESSION_STATE(V) + IDX_STATE_SESSION_COUNT +}; +#undef V + + +#define V(_, name, __) uint64_t name; +struct SessionStats final { + SESSION_STATS(V) +}; +#undef V + +using SessionStatsBase = StatsBase>; + +// A Session is a persistent connection between two QUIC peers, +// one acting as a server, the other acting as a client. Every +// Session is established first by performing a TLS 1.3 handshake +// in which the client sends an initial packet to the server +// containing a TLS client hello. Once the TLS handshake has +// been completed, the Session can be used to open one or more +// Streams for the actual data flow back and forth. +class Session final : public AsyncWrap, + public SessionStatsBase { + public: + class Application; + + // Used only by client Sessions, this PreferredAddressStrategy + // ignores the server provided preference communicated via the + // transport parameters. + static void IgnorePreferredAddressStrategy( + Session* session, + const PreferredAddress& preferred_address); + + // Used only by client Sessions, this PreferredAddressStrategy + // uses the server provided preference that matches the local + // port type (IPv4 or IPv6) used by the Endpoint. That is, if + // the Endpoint is IPv4, and the server advertises an IPv4 + // preferred address, then that preference will be used. Otherwise, + // the preferred address preference is ignored. + static void UsePreferredAddressStrategy( + Session* session, + const PreferredAddress& preferred_address); + + // A utility that wraps the configuration settings for the + // Session and the underlying ngtcp2_conn. This struct is + // created when a new Client or Server session is created. + struct Config final : public ngtcp2_settings { + // The QUIC protocol version requested for the Session. + quic_version version; + + // The initial destination CID. + CID dcid; + + // The locally selected source CID. + CID scid; + + // The original CID (if any). + CID ocid; + + Config( + Endpoint* endpoint, + const CID& dcid, + const CID& scid, + quic_version version = NGTCP2_PROTO_VER_MAX); + + Config( + Endpoint* endpoint, + quic_version version = NGTCP2_PROTO_VER_MAX); + + void EnableQLog(const CID& ocid); + }; + + // The Options struct contains all of the usercode specified + // options for the session. Most of the options correlate to + // the transport parameters that are communicated to the remote + // peer once the session is created. + struct Options final : public MemoryRetainer { + // The protocol identifier to be used by this Session. + std::string alpn = &NGHTTP3_ALPN_H3[1]; + + // The SNI hostname to be used. This is used only by client + // Sessions to identify the SNI host in the TLS client hello + // message. + std::string hostname = ""; + + RoutableConnectionIDConfig* cid_strategy; + BaseObjectPtr cid_strategy_strong_ref; + + CID dcid {}; + + PreferredAddressStrategy preferred_address_strategy = + UsePreferredAddressStrategy; + + // Set only on server Sessions, the preferred address communicates + // the IP address and port that the server would prefer the client + // to use when communicating with it. See the QUIC specification for + // more detail on how the preferred address mechanism works. + std::shared_ptr preferred_address_ipv4; + std::shared_ptr preferred_address_ipv6; + + // The initial size of the flow control window of locally initiated + // streams. This is the maximum number of bytes that the *remote* + // endpoint can send when the connection is started. + uint64_t initial_max_stream_data_bidi_local = + DEFAULT_MAX_STREAM_DATA_BIDI_LOCAL; + + // The initial size of the flow control window of remotely initiated + // streams. This is the maximum number of bytes that the remote endpoint + // can send when the connection is started. + uint64_t initial_max_stream_data_bidi_remote = + DEFAULT_MAX_STREAM_DATA_BIDI_REMOTE; + + // The initial size of the flow control window of remotely initiated + // unidirectional streams. This is the maximum number of bytes that + // the remote endpoint can send when the connection is started. + uint64_t initial_max_stream_data_uni = DEFAULT_MAX_STREAM_DATA_UNI; + + // The initial size of the session-level flow control window. + uint64_t initial_max_data = DEFAULT_MAX_DATA; + + // The initial maximum number of concurrent bidirectional streams + // the remote endpoint is permitted to open. + uint64_t initial_max_streams_bidi = DEFAULT_MAX_STREAMS_BIDI; + + // The initial maximum number of concurrent unidirectional streams + // the remote endpoint is permitted to open. + uint64_t initial_max_streams_uni = DEFAULT_MAX_STREAMS_UNI; + + // The maximum amount of time that a Session is permitted to remain + // idle before it is silently closed and state is discarded. + uint64_t max_idle_timeout = DEFAULT_MAX_IDLE_TIMEOUT; + + // The maximum number of Connection IDs that the peer can store. + // A single Session may have several connection IDs over it's lifetime. + uint64_t active_connection_id_limit = DEFAULT_ACTIVE_CONNECTION_ID_LIMIT; + + // Establishes the exponent used in ACK Delay field in the ACK frame. + // See the QUIC specification for details. This is an advanced option + // that should rarely be modified and only if there is really good reason. + uint64_t ack_delay_exponent = NGTCP2_DEFAULT_ACK_DELAY_EXPONENT; + + // The maximum amount of time by which the endpoint will delay sending + // acknowledgements. This is an advanced option that should rarely be + // modified and only if there is a really good reason. It is used to + // determine how long a Session will wait to determine that a packet + // has been lost. + uint64_t max_ack_delay = NGTCP2_DEFAULT_MAX_ACK_DELAY; + + // The maximum size of DATAGRAM frames that the endpoint will accept. + // Setting the value to 0 will disable DATAGRAM support. + uint64_t max_datagram_frame_size = NGTCP2_DEFAULT_MAX_PKTLEN; + + // When true, communicates that the Session does not support active + // connection migration. See the QUIC specification for more details + // on connection migration. + bool disable_active_migration = 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 pnly used by client sessions: + + // When set, instructs the client session to include an + // OCSP request in the initial TLS handshake. + bool request_ocsp = false; + + // 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 handshake completion timeout. + // TODO(@jasnell): implement support + uint32_t handshake_timeout = 120000; + + // The minimum diffie-hellman size. + // TODO(@jasnell): implement support + uint32_t min_dh_size = 0; + + // True if a PSK callback was given. + bool psk_callback_present = false; + + // The TLS session ID context (only used on the server) + std::string session_id_ctx = "node.js quic server"; + + // When set, instructs the client session to perform + // additional checks on TLS session resumption. + bool resume = false; + ngtcp2_transport_params* early_transport_params = nullptr; + SSL_SESSION* early_session_ticket = nullptr; + + Options() = default; + Options(const Options& other) noexcept; + + inline Options& operator=(const Options& other) noexcept { + if (this == &other) return *this; + this->~Options(); + return *new(this) Options(other); + } + + void MemoryInfo(MemoryTracker* tracker) const override; + SET_MEMORY_INFO_NAME(Session::Options) + SET_SELF_SIZE(Options) + }; + + #define V(_, name, type) type name; + struct State final { + SESSION_STATE(V) + }; + #undef V + + // A utility struct used to prepare the ngtcp2_transport_params + // when creating a new Session. + struct TransportParams final : public ngtcp2_transport_params { + TransportParams( + const std::shared_ptr& options, + const CID& scid = CID(), + const CID& ocid = CID()); + + void SetPreferredAddress(const std::shared_ptr& address); + void GenerateStatelessResetToken(EndpointWrap* endpoint, const CID& cid); + void GeneratePreferredAddressToken( + RoutableConnectionIDStrategy* connection_id_strategy, + Session* session, + CID* pscid); + }; + + // Every Session has exactly one CryptoContext that maintains the + // state of the TLS handshake and negotiated cipher keys after the + // handshake has been completed. It is separated out from the main + // Session class only as a convenience to help make the code more + // maintainable and understandable. + class CryptoContext final : public MemoryRetainer { + public: + CryptoContext( + Session* session, + const std::shared_ptr& options, + const BaseObjectPtr& context, + ngtcp2_crypto_side side); + ~CryptoContext() override; + + // Outgoing crypto data must be retained in memory until it is + // explicitly acknowledged. AcknowledgeCryptoData will be invoked + // when ngtcp2 determines that it has received an acknowledgement + // for crypto data at the specified level. This is our indication + // that the data for that level can be released. + void AcknowledgeCryptoData(ngtcp2_crypto_level level, size_t datalen); + + // Cancels the TLS handshake and returns the number of unprocessed + // bytes that were still in the queue when canceled. + size_t Cancel(); + + void Initialize(); + + // Returns the server's prepared OCSP response for transmission + // (if any). The shared_ptr will be empty if there was an error + // or if no OCSP response was provided. If release is true, the + // internal std::shared_ptr will be reset. + std::shared_ptr ocsp_response(bool release = true); + + // Returns ngtcp2's understanding of the current inbound crypto level + ngtcp2_crypto_level read_crypto_level() const; + + // Returns ngtcp2's understanding of the current outbound crypto level + ngtcp2_crypto_level write_crypto_level() const; + + // TLS Keylogging is enabled per-Session by attaching an handler to the + // "keylog" event. Each keylog line is emitted to JavaScript where it can + // be routed to whatever destination makes sense. Typically, this will be + // to a keylog file that can be consumed by tools like Wireshark to + // intercept and decrypt QUIC network traffic. + void Keylog(const char* line); + + int OnClientHello(); + + void OnClientHelloDone(BaseObjectPtr context); + + // The OnCert callback provides an opportunity to prompt the server to + // perform on OCSP request on behalf of the client (when the client + // requests it). If there is a listener for the 'OCSPRequest' event + // on the JavaScript side, the IDX_QUIC_SESSION_STATE_CERT_ENABLED + // session state slot will equal 1, which will cause the callback to + // be invoked. The callback will be given a reference to a JavaScript + // function that must be called in order for the TLS handshake to + // continue. + int OnOCSP(); + + // The OnOCSP function is called by the QuicSessionOnOCSPDone + // function when usercode is done handling the OCSP request + void OnOCSPDone(std::shared_ptr ocsp_response); + + // At this point in time, the TLS handshake secrets have been + // generated by openssl for this end of the connection and are + // ready to be used. Within this function, we need to install + // the secrets into the ngtcp2 connection object, store the + // remote transport parameters, and begin initialization of + // the Application that was selected. + bool OnSecrets( + ngtcp2_crypto_level level, + const uint8_t* rx_secret, + const uint8_t* tx_secret, + size_t secretlen); + + // When the client has requested OSCP, this function will be called to + // provide the OSCP response. The OnOSCP() callback should have already + // been called by this point if any data is to be provided. If it hasn't, + // and ocsp_response_ is empty, no OCSP response will be sent. + int OnTLSStatus(); + + // Called by ngtcp2 when a chunk of peer TLS handshake data is received. + // For every chunk, we move the TLS handshake further along until it + // is complete. + int Receive( + ngtcp2_crypto_level crypto_level, + uint64_t offset, + const uint8_t* data, + size_t datalen); + + void ResumeHandshake(); + + v8::MaybeLocal cert(Environment* env) const; + v8::MaybeLocal peer_cert(Environment* env) const; + v8::MaybeLocal cipher_name(Environment* env) const; + v8::MaybeLocal cipher_version(Environment* env) const; + v8::MaybeLocal ephemeral_key(Environment* env) const; + v8::MaybeLocal hello_ciphers(Environment* env) const; + v8::MaybeLocal hello_servername(Environment* env) const; + v8::MaybeLocal hello_alpn(Environment* env) const; + std::string servername() const; + + std::string selected_alpn() const; + + void set_tls_alert(int err); + + // Write outbound TLS handshake data into the ngtcp2 connection + // to prepare it to be serialized. The outbound data must be + // stored in the handshake_ until it is acknowledged by the + // remote peer. It's important to keep in mind that there is + // a potential security risk here -- that is, a malicious peer + // can cause the local session to keep sent handshake data in + // memory by failing to acknowledge it or slowly acknowledging + // it. We currently do not track how much data is being buffered + // here but we do record statistics on how long the handshake + // data is foreced to be kept in memory. + void WriteHandshake( + ngtcp2_crypto_level level, + const uint8_t* data, + size_t datalen); + + // 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(); + void EnableTrace(); + + inline Session* session() const { return session_.get(); } + inline ngtcp2_crypto_side side() const { return side_; } + + bool early_data() const; + + inline bool enable_tls_trace() const { + return options_->enable_tls_trace; + } + + inline bool reject_unauthorized() const { + return options_->reject_unauthorized; + } + + inline bool request_ocsp() const { + return options_->request_ocsp; + } + + inline bool request_peer_certificate() const { + return options_->request_peer_certificate; + } + + inline bool verify_hostname_identity() const { + return options_->verify_hostname_identity; + } + + void MemoryInfo(MemoryTracker* tracker) const override; + SET_MEMORY_INFO_NAME(CryptoContext) + SET_SELF_SIZE(CryptoContext) + + private: + void MaybeSetEarlySession(const std::shared_ptr& options); + bool SetSecrets( + ngtcp2_crypto_level level, + const uint8_t* rx_secret, + const uint8_t* tx_secret, + size_t secretlen); + + BaseObjectPtr session_; + BaseObjectPtr secure_context_; + ngtcp2_crypto_side side_; + std::shared_ptr options_; + crypto::SSLPointer ssl_; + crypto::BIOPointer bio_trace_; + + // There are three distinct levels of crypto data + // involved in the TLS handshake. We use the handshake_ + // buffer to temporarily store the outbound crypto + // data until it is acknowledged. + Buffer handshake_[3]; + + bool in_tls_callback_ = false; + bool in_ocsp_request_ = false; + bool in_client_hello_ = false; + bool in_key_update_ = false; + bool early_data_ = false; + + std::shared_ptr ocsp_response_; + + struct CallbackScope final { + CryptoContext* context; + + inline explicit CallbackScope(CryptoContext* context_) + : context(context_) { + context_->in_tls_callback_ = true; + } + + inline ~CallbackScope() { + context->in_tls_callback_ = false; + } + + inline static bool is_in_callback(CryptoContext* context) { + return context->in_tls_callback_; + } + }; + + struct HandshakeScope final { + using DoneCB = std::function; + CryptoContext* context; + DoneCB done; + + inline HandshakeScope(CryptoContext* context_, DoneCB done_) + : context(context_), + done(done_) {} + + inline ~HandshakeScope() { + if (!is_handshake_suspended()) + return; + + done(); + + if (!CallbackScope::is_in_callback(context)) + context->ResumeHandshake(); + } + + inline bool is_handshake_suspended() const { + return context->in_ocsp_request_ || context->in_client_hello_; + } + }; + + friend class Session; + }; + + // An Application encapsulates the ALPN-identified application + // specific semantics associated with the Session. The Application + // class itself is an abstract base class that must be specialized. + // Every Session has exactly one associated Application that is + // selected using the ALPN identifier when the Session is created. + // Once selected, the Session will defer many actions to be handled + // by the Application. + class Application : public MemoryRetainer { + public: + // A base class for configuring the Application. Specific + // Application subclasses may extend this with additional + // configuration properties. + struct Config { + // The maximum number of header pairs permitted for a Stream. + // Not all Applications support headers so the default is 0. + size_t max_header_pairs = 0; + + // The maximum total number of header bytes (including header + // name and value) permitted for a Stream. + // Not all Applications support headers so the default is 0. + size_t max_header_length = 0; + }; + + Application(Session* session, const Config& config); + + Application(const Application& other) = delete; + Application(Application&& other) = delete; + Application& operator=(const Application& other) = delete; + Application& operator=(Application&& other) = delete; + + virtual ~Application() = default; + + // The session will call initialize as soon as the TLS secrets + // have been set. + virtual bool Initialize(); + + // Session will forward all received stream data immediately + // on to the Application. The only additional processing the + // Session does is to automatically adjust the session-level + // flow control window. It is up to the Application to do + // the same for the Stream-level flow control. + // + // flags are passed on directly from ngtcp2. The most important + // of which here is NGTCP2_STREAM_DATA_FLAG_FIN, which indicates + // that this is the final chunk of data that the peer will send + // for this stream. + // + // It is also possible for the NGTCP2_STREAM_DATA_FLAG_EARLY flag + // to be set, indicating that this chunk of data was received in + // a 0RTT packet before the TLS handshake completed. This would + // indicate that it is not as secure and could be replayed by + // an attacker. We're not currently making use of that flag. + virtual bool ReceiveStreamData( + uint32_t flags, + stream_id id, + const uint8_t* data, + size_t datalen, + uint64_t offset) = 0; + + // Session will forward all data acknowledgements for a stream to + // the Application. + virtual void AcknowledgeStreamData( + stream_id id, + uint64_t offset, + size_t datalen) { + Acknowledge(id, offset, datalen); + } + + // Called to mark the identified stream as being blocked. Not all + // Application types will support blocked streams, and those that + // do will do so differently. The default implementation here is + // to simply acknowledge the notification. + virtual bool BlockStream(stream_id id) { return true; } + + // Called when the Session determines that the maximum number of + // remotely-initiated unidirectional streams has been extended. + // Not all Application types will require this notification so + // the default is to do nothing. + virtual void ExtendMaxStreamsRemoteUni(uint64_t max_streams) {} + + // Called when the Session determines that the maximum number of + // remotely-initiated bidirectional streams has been extended. + // Not all Application types will require this notification so + // the default is to do nothing. + virtual void ExtendMaxStreamsRemoteBidi(uint64_t max_streams) {} + + // Called when the Session determines that the flow control window + // for the given stream has been expanded. Not all Application types + // will require this notification so the default is to do nothing. + virtual void ExtendMaxStreamData(stream_id id, uint64_t max_data) {} + + // Called when the session determines that there is outbound data + // available to send for the given stream. + virtual void ResumeStream(stream_id id) {} + + // Different Applications may wish to set some application data in + // the session ticket (e.g. http/3 would set server settings in the + // application data). By default, there's nothing to set. + virtual void SetSessionTicketAppData( + const SessionTicketAppData& app_data) {} + + // Different Applications may set some application data in + // the session ticket (e.g. http/3 would set server settings in the + // application data). By default, there's nothing to get. + virtual SessionTicketAppData::Status GetSessionTicketAppData( + const SessionTicketAppData& app_data, + SessionTicketAppData::Flag flag) { + return flag == SessionTicketAppData::Flag::STATUS_RENEW ? + SessionTicketAppData::Status::TICKET_USE_RENEW : + SessionTicketAppData::Status::TICKET_USE; + } + + // Notifies the Application about a block of headers that + // have been fully received for the given stream. + virtual void StreamHeaders( + stream_id id, + Stream::HeadersKind kind, + const Stream::HeaderList& headers); + + // Notifies the Application that the identified stream has + // been closed. + virtual void StreamClose(stream_id id, error_code app_error_code); + + // Notifies the Application that the identified stream has + // been reset. + virtual void StreamReset(stream_id id, error_code app_error_code); + + // Submits an outbound block of headers for the given stream. + // Not all Application types will support headers, in which + // case this function should return false. + virtual bool SubmitHeaders( + stream_id id, + Stream::HeadersKind kind, + const v8::Local& headers) { return false; } + + inline Environment* env() const { return session_->env(); } + inline Session* session() const { return session_.get(); } + inline const Config& config() const { return config_; } + + // Signals to the Application that it should serialize and transmit + // any pending session and stream packets it has accumulated. + bool SendPendingData(); + + protected: + void set_stream_fin(stream_id id); + std::unique_ptr CreateStreamDataPacket(); + + struct StreamData final { + size_t count = 0; + size_t remaining = 0; + stream_id id = -1; + int fin = 0; + ngtcp2_vec data[kMaxVectorCount] {}; + ngtcp2_vec* buf = nullptr; + BaseObjectPtr stream; + StreamData() { buf = data; } + }; + + void Acknowledge(stream_id id, uint64_t offset, size_t datalen); + virtual int GetStreamData(StreamData* data) = 0; + virtual bool StreamCommit(StreamData* data, size_t datalen) = 0; + virtual bool ShouldSetFin(const StreamData& data) = 0; + + ssize_t WriteVStream( + PathStorage* path, + uint8_t* buf, + ssize_t* ndatalen, + const StreamData& stream_data); + + private: + void MaybeSetFin(const StreamData& stream_data); + BaseObjectWeakPtr session_; + const Config config_; + bool needs_init_ = true; + }; + + static bool HasInstance(Environment* env, const v8::Local& value); + static v8::Local GetConstructorTemplate( + Environment* env); + static void Initialize(Environment* env, v8::Local target); + + static void DoAttachToEndpoint( + const v8::FunctionCallbackInfo& args); + static void DoDestroy( + const v8::FunctionCallbackInfo& args); + static void GetRemoteAddress( + const v8::FunctionCallbackInfo& args); + static void GetCertificate( + const v8::FunctionCallbackInfo& args); + static void GetEphemeralKeyInfo( + const v8::FunctionCallbackInfo& args); + static void GetPeerCertificate( + const v8::FunctionCallbackInfo& args); + static void GracefulClose( + const v8::FunctionCallbackInfo& args); + static void SilentClose( + const v8::FunctionCallbackInfo& args); + static void UpdateKey( + const v8::FunctionCallbackInfo& args); + static void DoDetachFromEndpoint( + const v8::FunctionCallbackInfo& args); + static void OnClientHelloDone( + const v8::FunctionCallbackInfo& args); + static void OnOCSPDone( + const v8::FunctionCallbackInfo& args); + + static BaseObjectPtr CreateServer( + EndpointWrap* endpoint, + const std::shared_ptr& local_addr, + const std::shared_ptr& remote_addr, + const Config& config, + const std::shared_ptr& options, + const BaseObjectPtr& context); + + static BaseObjectPtr CreateClient( + EndpointWrap* endpoint, + const std::shared_ptr& local_addr, + const std::shared_ptr& remote_addr, + const Config& config, + const std::shared_ptr& options, + const BaseObjectPtr& context); + + Session( + EndpointWrap* endpoint, + v8::Local object, + const std::shared_ptr& local_addr, + const std::shared_ptr& remote_addr, + const Config& config, + const std::shared_ptr& options, + const BaseObjectPtr& context, + const CID& dcid, + const CID& scid, + const CID& ocid, + quic_version version = NGTCP2_PROTO_VER_MAX); + + Session( + EndpointWrap* endpoint, + v8::Local object, + const std::shared_ptr& local_addr, + const std::shared_ptr& remote_addr, + const Config& config, + const std::shared_ptr& options, + const BaseObjectPtr& context, + quic_version version = NGTCP2_PROTO_VER_MAX); + + ~Session() override; + + inline ngtcp2_conn* connection() const { return connection_.get(); } + inline CryptoContext* crypto_context() const { + return crypto_context_.get(); + } + inline const CID& dcid() const { return dcid_; } + inline Application* application() const { return application_.get(); } + inline EndpointWrap* endpoint() const; + inline const std::shared_ptr& remote_address() const { + return remote_address_; + } + inline const std::shared_ptr& local_address() const { + return local_address_; + } + inline const std::string& alpn() const { return options_->alpn; } + inline const std::string& hostname() const { return options_->hostname; } + + BaseObjectPtr qlogstream(); + + inline bool is_closing() const { return state_->closing; } + inline bool is_destroyed() const { return state_->destroyed; } + inline bool is_graceful_closing() const { return state_->graceful_closing; } + inline bool is_server() const { + return crypto_context_->side() == NGTCP2_CRYPTO_SIDE_SERVER; + } + + v8::Maybe OpenStream( + Stream::Direction direction = Stream::Direction::BIDIRECTIONAL); + BaseObjectPtr CreateStream(stream_id id); + BaseObjectPtr FindStream(stream_id id) const; + void AddStream(const BaseObjectPtr& stream); + + // Removes the given stream from the Session. All streams must + // be removed before the Session is destroyed. + void RemoveStream(stream_id id); + void ResumeStream(stream_id id); + bool HasStream(stream_id id) const; + void StreamDataBlocked(stream_id id); + void ShutdownStream(stream_id id, error_code code = NGTCP2_NO_ERROR); + const StreamsMap& streams() const { return streams_; } + + // Submits headers to the QUIC Application If headers are not supported, + // false will be returned. Otherwise, returns true + bool SubmitHeaders( + Stream::HeadersKind kind, + stream_id id, + const v8::Local& headers); + + // Receive and process a QUIC packet received from the peer + bool Receive( + size_t nread, + std::shared_ptr store, + const std::shared_ptr& local_address, + const std::shared_ptr& remote_address); + + // Called by ngtcp2 when a chunk of stream data has been received. If + // the stream does not yet exist, it is created, then the data is + // forwarded on. + bool ReceiveStreamData( + uint32_t flags, + stream_id id, + const uint8_t* data, + size_t datalen, + uint64_t offset); + + // Causes pending ngtcp2 frames to be serialized and sent + void SendPendingData(); + + bool SendPacket( + std::unique_ptr packet, + const ngtcp2_path_storage& path); + + uint64_t max_data_left() const; + + uint64_t max_local_streams_uni() const; + + inline bool allow_early_data() const { + // TODO(@jasnell): For now, we always allow early data. + // Later there will be reasons we do not want to allow + // it, such as lack of available system resources. + return true; + } + + // Returns true if the Session has entered the + // closing period after sending a CONNECTION_CLOSE. + // While true, the QuicSession is only permitted to + // transmit CONNECTION_CLOSE frames until either the + // idle timeout period elapses or until the QuicSession + // is explicitly destroyed. + bool is_in_closing_period() const; + + // Returns true if the Session has received a + // CONNECTION_CLOSE frame from the peer. Once in + // the draining period, the QuicSession is not + // permitted to send any frames to the peer. The + // QuicSession will be silently closed after either + // the idle timeout period elapses or until the + // QuicSession is explicitly destroyed. + bool is_in_draining_period() const; + + // Starting a GracefulClose disables the ability to open or accept + // new streams for this session. Existing streams are allowed to + // close naturally on their own. Once called, the QuicSession will + // be immediately closed once there are no remaining streams. Note + // that no notification is given to the connecting peer that we're + // in a graceful closing state. A CONNECTION_CLOSE will be sent only + // once Close() is called. + void StartGracefulClose(); + + bool AttachToNewEndpoint(EndpointWrap* endpoint, bool nat_rebinding = false); + + // Error handling for the Session. client and server + // instances will do different things here, but ultimately + // an error means that the Session + // should be torn down. + void HandleError(); + + // Transmits either a protocol or application connection + // close to the peer. The choice of which is send is + // based on the current value of last_error_. + bool SendConnectionClose(); + + enum class SessionCloseFlags { + NONE, + SILENT, + STATELESS_RESET + }; + + // Initiate closing of the QuicSession. This will round trip + // through JavaScript, causing all currently opened streams + // to be closed. If the SILENT flag is set, the connected peer + // will not be notified, otherwise an attempt will be made to + // send a CONNECTION_CLOSE frame to the peer. If Close is called + // while within the ngtcp2 callback scope, sending the + // CONNECTION_CLOSE will be deferred until the ngtcp2 callback + // scope exits. + void Close(SessionCloseFlags close_flags = SessionCloseFlags::NONE); + + bool IsResetToken(const CID& cid, const uint8_t* data, size_t datalen); + + // Mark the Session instance destroyed. This will either be invoked + // synchronously within the callstack of the Session::Close() method + // or not. If it is invoked within Session::Close(), the + // QuicSession::Close() will handle sending the CONNECTION_CLOSE + // frame. + void Destroy(); + + // Extends the QUIC stream flow control window. This is + // called after received data has been consumed and we + // want to allow the peer to send more data. + void ExtendStreamOffset(stream_id id, size_t amount); + + // Extends the QUIC session flow control window + void ExtendOffset(size_t amount); + + // Retrieve the local transport parameters established for + // this ngtcp2_conn + void GetLocalTransportParams(ngtcp2_transport_params* params); + + quic_version version() const; + + inline QuicError last_error() const { return last_error_; } + + inline size_t max_packet_length() const { return max_pkt_len_; } + + // When completing the TLS handshake, the TLS session information + // is provided to the Session so that the session ticket and + // the remote transport parameters can be captured to support 0RTT + // session resumption. + int set_session(SSL_SESSION* session); + + // True only if ngtcp2 considers the TLS handshake to be completed + bool is_handshake_completed() const; + + bool is_unable_to_send_packets(); + + inline void set_wrapped() { state_->wrapped = 1; } + + void SetSessionTicketAppData(const SessionTicketAppData& app_data); + + SessionTicketAppData::Status GetSessionTicketAppData( + const SessionTicketAppData& app_data, + SessionTicketAppData::Flag flag); + + // When a server advertises a preferred address in its initial + // transport parameters, ngtcp2 on the client side will trigger + // the OnSelectPreferredAdddress callback which will call this. + // The paddr argument contains the advertised preferred address. + // If the new address is going to be used, it needs to be copied + // over to dest, otherwise dest is left alone. There are two + // possible strategies that we currently support via user + // configuration: use the preferred address or ignore it. + void SelectPreferredAddress(const PreferredAddress& preferred_address); + + // TODO(@jasnell): This is a lie + SET_NO_MEMORY_INFO() + SET_MEMORY_INFO_NAME(Session) + SET_SELF_SIZE(Session) + + struct CallbackScope final { + BaseObjectPtr session; + std::unique_ptr internal; + v8::TryCatch try_catch; + + inline explicit CallbackScope(Session* session_) + : session(session_), + internal(new InternalCallbackScope( + session->env(), + session->object(), + { + session->get_async_id(), + session->get_trigger_async_id() + })), + try_catch(session->env()->isolate()) { + try_catch.SetVerbose(true); + } + + inline ~CallbackScope() { + Environment* env = session->env(); + if (UNLIKELY(try_catch.HasCaught())) { + session->crypto_context()->in_client_hello_ = false; + session->crypto_context()->in_ocsp_request_ = false; + if (!try_catch.HasTerminated() && env->can_call_into_js()) { + session->set_last_error(kQuicInternalError); + session->Close(); + CHECK(session->is_destroyed()); + } + internal->MarkAsFailed(); + } + } + }; + + // ConnectionCloseScope triggers sending a CONNECTION_CLOSE + // when not executing within the context of an ngtcp2 callback + // and the session is in the correct state. + struct ConnectionCloseScope final { + BaseObjectPtr session; + bool silent = false; + + inline ConnectionCloseScope(Session* session_, bool silent_ = false) + : session(session_), + silent(silent_) { + CHECK(session); + // If we are already in a ConnectionCloseScope, ignore. + if (session->in_connection_close_) + silent = true; + else + session->in_connection_close_ = true; + } + + inline ~ConnectionCloseScope() { + if (silent || + NgCallbackScope::InNgCallbackScope(session.get()) || + session->is_in_closing_period() || + session->is_in_draining_period()) { + return; + } + session->in_connection_close_ = false; + session->SendConnectionClose(); + } + }; + + // Used as a guard in the static callback functions + // (e.g. Session::OnStreamClose) to prevent re-entry + // into the ngtcp2 callbacks + struct NgCallbackScope final { + BaseObjectPtr session; + inline explicit NgCallbackScope(Session* session_) + : session(session_) { + CHECK(session); + CHECK(!InNgCallbackScope(session_)); + session->in_ng_callback_ = true; + } + + inline ~NgCallbackScope() { + session->in_ng_callback_ = false; + } + + static inline bool InNgCallbackScope(Session* session) { + return session->in_ng_callback_; + } + }; + + // SendSessionScope triggers SendPendingData() when not executing + // within the context of an ngtcp2 callback. When within an ngtcp2 + // callback, SendPendingData will always be called when the callbacks + // complete. + struct SendSessionScope final { + BaseObjectPtr session; + + inline explicit SendSessionScope(Session* session_) : session(session_) { + CHECK(session); + session->send_scope_depth_++; + } + + inline ~SendSessionScope() { + if (--session->send_scope_depth_ || + NgCallbackScope::InNgCallbackScope(session.get()) || + session->is_in_closing_period() || + session->is_in_draining_period()) { + return; + } + CHECK_EQ(session->send_scope_depth_, 0); + session->SendPendingData(); + } + }; + + private: + Session( + EndpointWrap* endpoint, + v8::Local object, + const std::shared_ptr& local_addr, + const std::shared_ptr& remote_addr, + const std::shared_ptr& options, + const BaseObjectPtr& context, + const CID& dcid = CID(), + ngtcp2_crypto_side side = NGTCP2_CRYPTO_SIDE_CLIENT); + + void UsePreferredAddress(const PreferredAddress::Address& address); + + inline void set_last_error(QuicError error = kQuicNoError) { + last_error_ = error; + } + + void set_remote_transport_params(); + + bool InitApplication(); + void AttachToEndpoint(); + + // Removes the Session from the current socket. This is + // done with when the session is being destroyed or being + // migrated to another Endpoint. It is important to keep in mind + // that the Endpoint uses a BaseObjectPtr for the Session. + // If the session is removed and there are no other references held, + // the session object will be destroyed automatically. + void DetachFromEndpoint(); + void OnIdleTimeout(); + + // The the retransmit libuv timer fires, it will call OnRetransmitTimeout, + // which determines whether or not we need to retransmit data to + // to packet loss or ack delay. + void OnRetransmitTimeout(); + void UpdateDataStats(); + void AckedStreamDataOffset( + stream_id id, + uint64_t offset, + uint64_t datalen); + void ExtendMaxStreamData(stream_id id, uint64_t max_data); + void ExtendMaxStreams(bool bidi, uint64_t max_streams); + void ExtendMaxStreamsUni(uint64_t max_streams); + void ExtendMaxStreamsBidi(uint64_t max_streams); + void ExtendMaxStreamsRemoteUni(uint64_t max_streams); + void ExtendMaxStreamsRemoteBidi(uint64_t max_streams); + + // Generates and associates a new connection ID for this Session. + // ngtcp2 will call this multiple times at the start of a new + // connection // in order to build a pool of available CIDs. + void GetNewConnectionID(ngtcp2_cid* cid, uint8_t* token, size_t cidlen); + + // Captures the error code and family information from a received + // connection close frame. + void GetConnectionCloseInfo(); + + // The HandshakeCompleted function is called by ngtcp2 once it + // determines that the TLS Handshake is done. The only thing we + // need to do at this point is let the javascript side know. + bool HandshakeCompleted(const std::shared_ptr& remote_address); + void HandshakeConfirmed(); + + // When ngtcp2 receives a successful response to a PATH_CHALLENGE, + // it will trigger the OnPathValidation callback which will, in turn + // invoke this. There's really nothing to do here but update stats and + // and optionally notify the javascript side if there is a handler registered. + // Notifying the JavaScript side is purely informational. + void PathValidation( + const ngtcp2_path* path, + ngtcp2_path_validation_result res); + + // Performs intake processing on a received QUIC packet. The received + // data is passed on to ngtcp2 for parsing and processing. ngtcp2 will, + // in turn, invoke a series of callbacks to handle the received packet. + bool ReceivePacket(ngtcp2_path* path, const uint8_t* data, ssize_t nread); + + // The retransmit timer allows us to trigger retransmission + // of packets in case they are considered lost. The exact amount + // of time is determined internally by ngtcp2 according to the + // guidelines established by the QUIC spec but we use a libuv + // timer to actually monitor. + void ScheduleRetransmit(); + bool SendPacket(std::unique_ptr packet); + void StreamClose(stream_id id, error_code app_error_code); + + // Called when the Session has received a RESET_STREAM frame from the + // peer, indicating that it will no longer send additional frames for the + // stream. If the stream is not yet known, reset is ignored. If the stream + // has already received a STREAM frame with fin set, the stream reset is + // ignored (the QUIC spec permits implementations to handle this situation + // however they want.) If the stream has not yet received a STREAM frame + // with the fin set, then the RESET_STREAM causes the readable side of the + // stream to be abruptly closed and any additional stream frames that may + // be received will be discarded if their offset is greater than final_size. + // On the JavaScript side, receiving a C is undistinguishable from + // a normal end-of-stream. No additional data events will be emitted, the + // end event will be emitted, and the readable side of the duplex will be + // closed. + // + // If the stream is still writable, no additional action is taken. If, + // however, the writable side of the stream has been closed (or was never + // open in the first place as in the case of peer-initiated unidirectional + // streams), the reset will cause the stream to be immediately destroyed. + void StreamReset( + stream_id id, + uint64_t final_size, + error_code app_error_code); + + bool WritePackets(const char* diagnostic_label = nullptr); + + void UpdateConnectionID( + int type, + const CID& cid, + const StatelessResetToken& token); + + // Every QUIC session has a remote address and local address. + // Those endpoints can change through the lifetime of a connection, + // so whenever a packet is successfully processed, or when a + // response is to be sent, we have to keep track of the path + // and update as we go. + void UpdateEndpoint(const ngtcp2_path& path); + + // Called by the OnVersionNegotiation callback when a version + // negotiation frame has been received by the client. The sv + // parameter is an array of versions supported by the remote peer. + void VersionNegotiation(const quic_version* sv, size_t nsv); + void UpdateClosingTimer(); + + // The retransmit timer allows us to trigger retransmission + // of packets in case they are considered lost. The exact amount + // of time is determined internally by ngtcp2 according to the + // guidelines established by the QUIC spec but we use a libuv + // timer to actually monitor. Here we take the calculated timeout + // and extend out the libuv timer. + void UpdateRetransmitTimer(uint64_t timeout); + + // Begin connection close by serializing the CONNECTION_CLOSE packet. + // There are two variants: one to serialize an application close, the + // other to serialize a protocol close. The frames are generally + // identical with the exception of a bit in the header. On server + // Sessions, we serialize the frame once and may retransmit it + // multiple times. On client Sessions, we only ever serialize the + // connection close once. + bool StartClosingPeriod(); + + void IncrementConnectionCloseAttempts(); + bool ShouldAttemptConnectionClose(); + void Datagram( + uint32_t flags, + const uint8_t* data, + size_t datalen); + + // Updates the idle timer deadline. If the idle timer fires, the + // connection will be silently closed. It is important to update + // this as activity occurs to keep the idle timer from firing. + void UpdateIdleTimer(); + + Application* SelectApplication(const Application::Config& config); + + ngtcp2_mem allocator_; + std::shared_ptr options_; + QuicConnectionPointer connection_; + BaseObjectPtr endpoint_; + AliasedStruct state_; + StreamsMap streams_; + + std::shared_ptr local_address_; + std::shared_ptr remote_address_; + + std::unique_ptr application_; + std::unique_ptr crypto_context_; + + TimerWrapHandle idle_; + TimerWrapHandle retransmit_; + + CID dcid_; + CID scid_; + CID pscid_; + ngtcp2_transport_params transport_params_; + bool transport_params_set_ = false; + bool in_ng_callback_ = false; + bool in_connection_close_ = false; + bool stateless_reset_ = false; + size_t send_scope_depth_ = 0; + + size_t max_pkt_len_; + QuicError last_error_ = kQuicNoError; + std::unique_ptr conn_closebuf_; + size_t connection_close_attempts_ = 0; + size_t connection_close_limit_ = 1; + + std::unique_ptr cid_strategy_; + BaseObjectPtr qlogstream_; + + struct RemoteTransportParamsDebug final { + Session* session; + inline explicit RemoteTransportParamsDebug(Session* session_) + : session(session_) {} + std::string ToString() const; + }; + + static const ngtcp2_callbacks callbacks[2]; + + // Called by ngtcp2 for both client and server connections when + // TLS handshake data has been received and needs to be processed. + // This will be called multiple times during the TLS handshake + // process and may be called during key updates. + static int OnReceiveCryptoData( + ngtcp2_conn* conn, + ngtcp2_crypto_level crypto_level, + uint64_t offset, + const uint8_t* data, + size_t datalen, + void* user_data); + + // Called by ngtcp2 for both client and server connections + // when ngtcp2 has determined that the TLS handshake has + // been completed. It is important to understand that this + // is only an indication of the local peer's handshake state. + // The remote peer might not yet have completed its part + // of the handshake. + static int OnHandshakeCompleted(ngtcp2_conn* conn, void* user_data); + + // Called by ngtcp2 for clients when the handshake has been + // confirmed. Confirmation occurs *after* handshake completion. + static int OnHandshakeConfirmed(ngtcp2_conn* conn, void* user_data); + + // Called by ngtcp2 when a chunk of stream data has been received. + // Currently, ngtcp2 ensures that this callback is always called + // with an offset parameter strictly larger than the previous call's + // offset + datalen (that is, data will never be delivered out of + // order). That behavior may change in the future but only via a + // configuration option. + static int OnReceiveStreamData( + ngtcp2_conn* conn, + uint32_t flags, + stream_id id, + uint64_t offset, + const uint8_t* data, + size_t datalen, + void* user_data, + void* stream_user_data); + + // Called by ngtcp2 when an acknowledgement for a chunk of + // TLS handshake data has been received by the remote peer. + // This is only an indication that data was received, not that + // it was successfully processed. Acknowledgements are a key + // part of the QUIC reliability mechanism. + static int OnAckedCryptoOffset( + ngtcp2_conn* conn, + ngtcp2_crypto_level crypto_level, + uint64_t offset, + uint64_t datalen, + void* user_data); + + // Called by ngtcp2 when an acknowledgement for a chunk of + // stream data has been received successfully by the remote peer. + // This is only an indication that data was received, not that + // it was successfully processed. Acknowledgements are a key + // part of the QUIC reliability mechanism. + static int OnAckedStreamDataOffset( + ngtcp2_conn* conn, + stream_id id, + uint64_t offset, + uint64_t datalen, + void* user_data, + void* stream_user_data); + + // Called by ngtcp2 for a client connection when the server + // has indicated a preferred address in the transport + // params. + // For now, there are two modes: we can accept the preferred address + // or we can reject it. Later, we may want to implement a callback + // to ask the user if they want to accept the preferred address or + // not. + static int OnSelectPreferredAddress( + ngtcp2_conn* conn, + ngtcp2_addr* dest, + const ngtcp2_preferred_addr* paddr, + void* user_data); + + static int OnStreamClose( + ngtcp2_conn* conn, + stream_id id, + uint64_t app_error_code, + void* user_data, + void* stream_user_data); + + static int OnStreamOpen( + ngtcp2_conn* conn, + stream_id id, + void* user_data); + + // Stream reset means the remote peer will no longer send data + // on the identified stream. It is essentially a premature close. + // The final_size parameter is important here in that it identifies + // exactly how much data the *remote peer* is aware that it sent. + // If there are lost packets, then the local peer's idea of the final + // size might not match. + static int OnStreamReset( + ngtcp2_conn* conn, + stream_id id, + uint64_t final_size, + uint64_t app_error_code, + void* user_data, + void* stream_user_data); + + // Called by ngtcp2 when it needs to generate some random data. + // We currently do not use it, but the ngtcp2_rand_ctx identifies + // why the random data is necessary. When ctx is equal to + // NGTCP2_RAND_CTX_NONE, it typically means the random data + // is being used during the TLS handshake. When ctx is equal to + // NGTCP2_RAND_CTX_PATH_CHALLENGE, the random data is being + // used to construct a PATH_CHALLENGE. These *might* need more + // secure and robust random number generation given the + // sensitivity of PATH_CHALLENGE operations (an attacker + // could use a compromised PATH_CHALLENGE to trick an endpoint + // into redirecting traffic). + // + // The ngtcp2_rand_ctx tells us what the random data is used for. + // Currently, there is only one use. In the future, we'll want to + // explore whether we want to handle the different cases uses. + static int OnRand( + uint8_t* dest, + size_t destlen, + const ngtcp2_rand_ctx* rand_ctx, + ngtcp2_rand_usage usage); + + // When a new client connection is established, ngtcp2 will call + // this multiple times to generate a pool of connection IDs to use. + static int OnGetNewConnectionID( + ngtcp2_conn* conn, + ngtcp2_cid* cid, + uint8_t* token, + size_t cidlen, + void* user_data); + + // When a connection is closed, ngtcp2 will call this multiple + // times to retire connection IDs. It's also possible for this + // to be called at times throughout the lifecycle of the connection + // to remove a CID from the availability pool. + static int OnRemoveConnectionID( + ngtcp2_conn* conn, + const ngtcp2_cid* cid, + void* user_data); + + // Called by ngtcp2 to perform path validation. Path validation + // is necessary to ensure that a packet is originating from the + // expected source. If the res parameter indicates success, it + // means that the path specified has been verified as being + // valid. + // + // Validity here means only that there has been a successful + // exchange of PATH_CHALLENGE information between the peers. + // It's critical to understand that the validity of a path + // can change at any timee so this is only an indication of + // validity at a specific point in time. + static int OnPathValidation( + ngtcp2_conn* conn, + const ngtcp2_path* path, + ngtcp2_path_validation_result res, + void* user_data); + + // Called by ngtcp2 for both client and server connections + // when a request to extend the maximum number of unidirectional + // streams has been received + static int OnExtendMaxStreamsUni( + ngtcp2_conn* conn, + uint64_t max_streams, + void* user_data); + + // Called by ngtcp2 for both client and server connections + // when a request to extend the maximum number of bidirectional + // streams has been received. + static int OnExtendMaxStreamsBidi( + ngtcp2_conn* conn, + uint64_t max_streams, + void* user_data); + + // Triggered by ngtcp2 when the local peer has received a flow + // control signal from the remote peer indicating that additional + // data can be sent. The max_data parameter identifies the maximum + // data offset that may be sent. That is, a value of 99 means that + // out of a stream of 1000 bytes, only the first 100 may be sent. + // (offsets 0 through 99). + static int OnExtendMaxStreamData( + ngtcp2_conn* conn, + stream_id id, + uint64_t max_data, + void* user_data, + void* stream_user_data); + + // Triggered by ngtcp2 when a version negotiation is received. + // What this means is that the remote peer does not support the + // QUIC version requested. The only thing we can do here (per + // the QUIC specification) is silently discard the connection + // and notify the JavaScript side that a different version of + // QUIC should be used. The sv parameter does list the QUIC + // versions advertised as supported by the remote peer. + static int OnVersionNegotiation( + ngtcp2_conn* conn, + const ngtcp2_pkt_hd* hd, + const uint32_t* sv, + size_t nsv, + void* user_data); + + // Triggered by ngtcp2 when a stateless reset is received. What this + // means is that the remote peer might recognize the CID but has lost + // all state necessary to successfully process it. The only thing we + // can do is silently close the connection. For server sessions, this + // means all session state is shut down and discarded, even on the + // JavaScript side. For client sessions, we discard session state at + // the C++ layer but -- at least in the future -- we can retain some + // state at the JavaScript level to allow for automatic session + // resumption. + static int OnStatelessReset( + ngtcp2_conn* conn, + const ngtcp2_pkt_stateless_reset* sr, + void* user_data); + + // Triggered by ngtcp2 when the local peer has received an + // indication from the remote peer indicating that additional + // unidirectional streams may be sent. The max_streams parameter + // identifies the highest unidirectional stream ID that may be + // opened. + static int OnExtendMaxStreamsRemoteUni( + ngtcp2_conn* conn, + uint64_t max_streams, + void* user_data); + + // Triggered by ngtcp2 when the local peer has received an + // indication from the remote peer indicating that additional + // bidirectional streams may be sent. The max_streams parameter + // identifies the highest bidirectional stream ID that may be + // opened. + static int OnExtendMaxStreamsRemoteBidi( + ngtcp2_conn* conn, + uint64_t max_streams, + void* user_data); + + static int OnConnectionIDStatus( + ngtcp2_conn* conn, + int type, + uint64_t seq, + const ngtcp2_cid* cid, + const uint8_t* token, + void* user_data); + + // A QUIC datagram is an independent data packet that is + // unaffiliated with a stream. + static int OnDatagram( + ngtcp2_conn* conn, + uint32_t flags, + const uint8_t* data, + size_t datalen, + void* user_data); + + friend class Session::CallbackScope; + friend class Session::NgCallbackScope; + friend class Session::SendSessionScope; + friend class Session::CryptoContext; +}; + +class DefaultApplication final : public Session::Application { + public: + DefaultApplication(Session* session, const Application::Config& config); + + bool ReceiveStreamData( + uint32_t flags, + stream_id id, + const uint8_t* data, + size_t datalen, + uint64_t offset) override; + + int GetStreamData(StreamData* stream_data) override; + + void ResumeStream(stream_id id) override; + bool ShouldSetFin(const StreamData& stream_data) override; + bool StreamCommit(StreamData* stream_data, size_t datalen) override; + + SET_SELF_SIZE(DefaultApplication) + SET_MEMORY_INFO_NAME(DefaultApplication) + SET_NO_MEMORY_INFO() + + private: + void ScheduleStream(stream_id id); + void UnscheduleStream(stream_id id); + + Stream::Queue stream_queue_; +}; + +class OptionsObject final : public BaseObject { + public: + static bool HasInstance( + Environment* env, + const v8::Local& value); + static v8::Local GetConstructorTemplate( + Environment* env); + static void Initialize(Environment* env, v8::Local target); + + static void New(const v8::FunctionCallbackInfo& args); + static void SetPreferredAddress( + const v8::FunctionCallbackInfo& args); + static void SetTransportParams( + const v8::FunctionCallbackInfo& args); + static void SetTLSOptions(const v8::FunctionCallbackInfo& args); + static void SetSessionResume(const v8::FunctionCallbackInfo& args); + + OptionsObject( + Environment* env, + v8::Local object, + std::shared_ptr options = + std::make_shared()); + + inline std::shared_ptr options() const { return options_; } + + void MemoryInfo(MemoryTracker* tracker) const override; + SET_MEMORY_INFO_NAME(OptionsObject) + SET_SELF_SIZE(OptionsObject) + + private: + v8::Maybe SetOption( + const v8::Local& object, + const v8::Local& name, + uint64_t Session::Options::*member); + + v8::Maybe SetOption( + const v8::Local& object, + const v8::Local& name, + uint32_t Session::Options::*member); + + v8::Maybe SetOption( + const v8::Local& object, + const v8::Local& name, + bool Session::Options::*member); + + std::shared_ptr options_; +}; + +} // namespace quic +} // namespace node + +#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS +#endif // SRC_QUIC_SESSION_H_ diff --git a/src/quic/stats.h b/src/quic/stats.h new file mode 100644 index 00000000000000..dc060f89373d2e --- /dev/null +++ b/src/quic/stats.h @@ -0,0 +1,140 @@ +#ifndef SRC_QUIC_STATS_H_ +#define SRC_QUIC_STATS_H_ + +#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS + +#include "memory_tracker.h" +#include "util.h" +#include +#include + +#include +#include + +namespace node { +namespace quic { + +static constexpr uint64_t kMaxUint64 = std::numeric_limits::max(); + +template class StatsBase; + +template +struct StatsTraits { + using Stats = T; + using Base = Q; + + template + static void ToString(const Q& ptr, Fn&& add_field) { + } +}; + +using AddStatsField = std::function; + +// StatsBase is a utility help for classes (like Session) +// that record performance statistics. The template takes a +// single Traits argument (see StreamStatsTraits in +// stream.h as an example). When the StatsBase +// is deconstructed, collected statistics are output to +// Debug automatically. +template +class StatsBase { + public: + typedef typename T::Stats Stats; + + inline explicit StatsBase(Environment* env) + : stats_store_( + v8::ArrayBuffer::NewBackingStore( + env->isolate(), + sizeof(Stats))), + stats_(new (stats_store_->Data()) Stats) { + DCHECK_NOT_NULL(stats_); + stats_->created_at = uv_hrtime(); + } + + inline v8::Local ToBigUint64Array(Environment* env) { + size_t size = sizeof(Stats); + size_t count = size / sizeof(uint64_t); + v8::Local stats_buffer = + v8::ArrayBuffer::New(env->isolate(), stats_store_); + return v8::BigUint64Array::New(stats_buffer, 0, count); + } + + virtual ~StatsBase() { + if (stats_ != nullptr) stats_->~Stats(); + } + + // The StatsDebug utility is used when StatsBase is destroyed + // to output statistical information to Debug. It is designed + // to only incur a performance cost constructing the debug + // output when Debug output is enabled. + struct StatsDebug { + typename T::Base* ptr; + inline explicit StatsDebug(typename T::Base* ptr_) : ptr(ptr_) {} + inline std::string ToString() const { + std::string out = "Statistics:\n"; + auto add_field = [&out](const char* name, uint64_t val) { + out += " "; + out += std::string(name); + out += ": "; + out += std::to_string(val); + out += "\n"; + }; + add_field("Duration", uv_hrtime() - ptr->GetStat(&Stats::created_at)); + T::ToString(*ptr, add_field); + return out; + } + }; + + // Increments the given stat field by the given amount or 1 if + // no amount is specified. + inline void IncrementStat(uint64_t Stats::*member, uint64_t amount = 1) { + Mutex::ScopedLock lock(mutex_); + stats_->*member += std::min(amount, kMaxUint64 - stats_->*member); + } + + // Sets an entirely new value for the given stat field + inline void SetStat(uint64_t Stats::*member, uint64_t value) { + Mutex::ScopedLock lock(mutex_); + stats_->*member = value; + } + + // Sets the given stat field to the current uv_hrtime() + inline void RecordTimestamp(uint64_t Stats::*member) { + Mutex::ScopedLock lock(mutex_); + stats_->*member = uv_hrtime(); + } + + // Gets the current value of the given stat field + inline uint64_t GetStat(uint64_t Stats::*member) const { + return stats_->*member; + } + + inline void StatsMemoryInfo(MemoryTracker* tracker) const { + tracker->TrackField("stats_store", stats_store_); + } + + inline void DebugStats(AsyncWrap* wrap) { + StatsDebug stats_debug(static_cast(this)); + Debug(wrap, "Destroyed. %s", stats_debug); + } + + private: + std::shared_ptr stats_store_; + Stats* stats_ = nullptr; + + Mutex mutex_; +}; + +template +struct StatsTraitsImpl final { + using Stats = S; + using Base = B; + + static void ToString(const B& ptr, AddStatsField add_field); +}; + +} // namespace quic +} // namespace node + +#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS +#endif // SRC_QUIC_STATS_H_ diff --git a/src/quic/stream.cc b/src/quic/stream.cc new file mode 100644 index 00000000000000..ea751ace7095ea --- /dev/null +++ b/src/quic/stream.cc @@ -0,0 +1,347 @@ +#include "quic/buffer.h" +#include "quic/crypto.h" +#include "quic/endpoint.h" +#include "quic/session.h" +#include "quic/stream.h" +#include "quic/quic.h" +#include "aliased_struct-inl.h" +#include "async_wrap-inl.h" +#include "base_object-inl.h" +#include "debug_utils-inl.h" +#include "env-inl.h" +#include "memory_tracker-inl.h" +#include "node_bob-inl.h" +#include "node_http_common-inl.h" +#include "node_sockaddr-inl.h" +#include "v8.h" + +namespace node { + +using v8::FunctionTemplate; +using v8::Local; +using v8::Maybe; +using v8::Object; +using v8::PropertyAttribute; + +namespace quic { + +Local Stream::GetConstructorTemplate(Environment* env) { + BindingState* state = env->GetBindingData(env->context()); + CHECK_NOT_NULL(state); + Local tmpl = state->stream_constructor_template(); + if (tmpl.IsEmpty()) { + tmpl = FunctionTemplate::New(env->isolate()); + tmpl->SetClassName(FIXED_ONE_BYTE_STRING(env->isolate(), "QuicStream")); + tmpl->Inherit(AsyncWrap::GetConstructorTemplate(env)); + tmpl->InstanceTemplate()->SetInternalFieldCount( + Stream::kInternalFieldCount); + state->set_stream_constructor_template(tmpl); + } + return tmpl; +} + +bool Stream::HasInstance(Environment* env, const Local& obj) { + return GetConstructorTemplate(env)->HasInstance(obj); +} + +void Stream::Initialize(Environment* env) { + USE(GetConstructorTemplate(env)); +} + +BaseObjectPtr Stream::Create( + Environment* env, + Session* session, + stream_id id) { + Local obj; + Local tmpl = GetConstructorTemplate(env); + CHECK(!tmpl.IsEmpty()); + if (!tmpl->InstanceTemplate()->NewInstance(env->context()).ToLocal(&obj)) + return BaseObjectPtr(); + + return MakeBaseObject(session, obj, id); +} + +Stream::Stream( + Session* session, + Local object, + stream_id id, + Buffer::Source* source) + : AsyncWrap(session->env(), object, AsyncWrap::PROVIDER_QUICSTREAM), + StreamStatsBase(session->env()), + session_(session), + state_(session->env()), + id_(id) { + MakeWeak(); + Debug(this, "Created"); + + AttachOutboundSource(source); + + object->DefineOwnProperty( + env()->context(), + env()->state_string(), + state_.GetArrayBuffer(), + PropertyAttribute::ReadOnly).Check(); + + object->DefineOwnProperty( + env()->context(), + env()->stats_string(), + ToBigUint64Array(env()), + PropertyAttribute::ReadOnly).Check(); + + ngtcp2_transport_params params; + ngtcp2_conn_get_local_transport_params(session->connection(), ¶ms); + IncrementStat(&StreamStats::max_offset, params.initial_max_data); +} + +Stream::~Stream() { + DebugStats(this); +} + +void Stream::Acknowledge(uint64_t offset, size_t datalen) { + if (is_destroyed() || outbound_source_ == nullptr) + return; + + // ngtcp2 guarantees that offset must always be greater + // than the previously received offset. + DCHECK_GE(offset, GetStat(&StreamStats::max_offset_ack)); + SetStat(&StreamStats::max_offset_ack, offset); + + Debug(this, "Acknowledging %d bytes", datalen); + + // Consumes the given number of bytes in the buffer. + CHECK_LE(outbound_source_->Acknowledge(offset, datalen), datalen); +} + +bool Stream::AddHeader(std::unique_ptr
header) { + size_t len = header->length(); + Session::Application* app = session()->application(); + // We cannot add the header if we've either reached + // * the max number of header pairs or + // * the max number of header bytes + if (headers_.size() == app->config().max_header_pairs || + current_headers_length_ + len > app->config().max_header_length) { + return false; + } + + current_headers_length_ += header->length(); + Debug(this, "Header - %s", header.get()); + headers_.emplace_back(std::move(header)); + return true; +} + +void Stream::AttachInboundConsumer( + Buffer::Consumer* consumer, + BaseObjectPtr strong_ptr) { + CHECK_IMPLIES(strong_ptr, consumer != nullptr); + Debug(this, "%s data consumer", + consumer != nullptr ? "Attaching" : "Clearing"); + inbound_consumer_ = consumer; + inbound_consumer_strong_ptr_ = std::move(strong_ptr); + ProcessInbound(); +} + +void Stream::AttachOutboundSource(Buffer::Source* source) { + Debug(this, "%s data source", + source != nullptr ? "Attaching" : "Clearing"); + if (source == nullptr) return; + outbound_source_ = source; + if (source != nullptr) { + outbound_source_strong_ptr_ = source->GetStrongPtr(); + source->set_owner(this); + Resume(); + } +} + +void Stream::BeginHeaders(HeadersKind kind) { + Debug(this, "Beginning Headers"); + headers_.clear(); + set_headers_kind(kind); +} + +void Stream::Commit(size_t amount) { + CHECK(!is_destroyed()); + if (outbound_source_ == nullptr) + return; + size_t actual = outbound_source_->Seek(amount); + CHECK_LE(actual, amount); +} + +void Stream::Destroy(const QuicError& error) { + if (destroyed_) + return; + destroyed_ = true; + // Triggers sending a RESET_STREAM and/or STOP_SENDING as + // appropriate. + // TODO(@jasnell): Determine if the shutdown stream triggers the + // stream close flow. If so, then we don't need to call OnClose. + session_->ShutdownStream(id_, error.code); + OnClose(); +} + +int Stream::DoPull( + bob::Next next, + int options, + ngtcp2_vec* data, + size_t count, + size_t max_count_hint) { + Debug(this, "Pulling outbound data for serialization"); + // If an outbound source has not yet been attached, block until + // one is available. When AttachOutboundSource() is called the + // stream will be resumed. + if (outbound_source_ == nullptr) { + int status = bob::Status::STATUS_BLOCK; + std::move(next)(status, nullptr, 0, [](size_t len) {}); + return status; + } + + return outbound_source_->Pull( + std::move(next), + options, + data, + count, + max_count_hint); +} + +void Stream::EndHeaders() { + Debug(this, "End Headers"); + session()->application()->StreamHeaders(id_, headers_kind_, headers_); + headers_.clear(); +} + +void Stream::OnClose() { + if (!destroyed_) + destroyed_ = true; + Unschedule(); + if (outbound_source_ != nullptr) { + outbound_source_ = nullptr; + outbound_source_strong_ptr_.reset(); + } + session()->RemoveStream(id_); + session_.reset(); +} + +void Stream::ProcessInbound() { + // If there is no inbound consumer, do nothing. + if (inbound_consumer_ == nullptr) + return; + + Debug(this, "Releasing the inbound queue to the consumer"); + + Maybe amt = inbound_.Release(inbound_consumer_); + if (amt.IsNothing()) { + Debug(this, "Failed to process the inbound queue"); + return Destroy(); + } + size_t len = amt.FromJust(); + + Debug(this, "Released %" PRIu64 " bytes to consumer", len); + IncrementStat(&StreamStats::max_offset, len); + session_->ExtendStreamOffset(id_, len); +} + +void Stream::MemoryInfo(MemoryTracker* tracker) const { + tracker->TrackField("outbound", outbound_source_); + tracker->TrackField("outbound_strong_ptr", outbound_source_strong_ptr_); + tracker->TrackField("inbound", inbound_); + tracker->TrackField("inbound_consumer_strong_ptr_", + inbound_consumer_strong_ptr_); + tracker->TrackField("headers", headers_); + StatsBase::StatsMemoryInfo(tracker); +} + +void Stream::ReceiveData( + uint32_t flags, + const uint8_t* data, + size_t datalen, + uint64_t offset) { + CHECK(!is_destroyed()); + Debug(this, "Receiving %d bytes. Final? %s", + datalen, + flags & NGTCP2_STREAM_DATA_FLAG_FIN ? "yes" : "no"); + + // ngtcp2 guarantees that datalen will only be 0 if fin is set. + DCHECK_IMPLIES(datalen == 0, flags & NGTCP2_STREAM_DATA_FLAG_FIN); + + // ngtcp2 guarantees that offset is greater than the previously received. + DCHECK_GE(offset, GetStat(&StreamStats::max_offset_received)); + SetStat(&StreamStats::max_offset_received, offset); + + if (datalen > 0) { + // IncrementStats will update the data_rx_rate_ and data_rx_size_ + // histograms. These will provide data necessary to detect and + // prevent Slow Send DOS attacks specifically by allowing us to + // see if a connection is sending very small chunks of data at very + // slow speeds. It is important to emphasize, however, that slow send + // rates may be perfectly legitimate so we cannot simply take blanket + // action when slow rates are detected. Nor can we reliably define what + // a slow rate even is! Will will need to determine some reasonable + // default and allow user code to change the default as well as determine + // what action to take. The current strategy will be to trigger an event + // on the stream when data transfer rates are likely to be considered too + // slow. + UpdateStats(datalen); + inbound_.Push(env(), data, datalen); + } + + if (flags & NGTCP2_STREAM_DATA_FLAG_FIN) { + set_final_size(offset + datalen); + inbound_.End(); + } + + ProcessInbound(); +} + +void Stream::ResetStream(const QuicError& error) { + CHECK_EQ(error.type, QuicError::Type::APPLICATION); + Session::SendSessionScope send_scope(session()); + session()->ShutdownStream(id_, error.code); + state_->read_ended = 1; +} + +void Stream::Resume() { + Session::SendSessionScope send_scope(session()); + Debug(this, "Resuming stream %" PRIu64, id_); + session()->ResumeStream(id_); +} + +void Stream::StopSending(const QuicError& error) { + CHECK_EQ(error.type, QuicError::Type::APPLICATION); + Session::SendSessionScope send_scope(session()); + ngtcp2_conn_shutdown_stream_read( + session()->connection(), + id_, + error.code); + state_->read_ended = 1; +} + +void Stream::UpdateStats(size_t datalen) { + uint64_t len = static_cast(datalen); + IncrementStat(&StreamStats::bytes_received, len); +} + +void Stream::set_final_size(uint64_t final_size) { + CHECK_IMPLIES( + state_->fin_received == 1, + final_size <= GetStat(&StreamStats::final_size)); + state_->fin_received = 1; + SetStat(&StreamStats::final_size, final_size); + Debug(this, "Set final size to %" PRIu64, final_size); +} + +void Stream::Schedule(Queue* queue) { + if (!stream_queue_.IsEmpty()) // Already scheduled? + return; + queue->PushBack(this); +} + +template <> +void StatsTraitsImpl::ToString( + const Stream& ptr, + AddStatsField add_field) { +#define V(_, name, label) add_field(label, ptr.GetStat(&StreamStats::name)); + STREAM_STATS(V) +#undef V +} + +} // namespace quic +} // namespace node diff --git a/src/quic/stream.h b/src/quic/stream.h new file mode 100644 index 00000000000000..9fd21a184b1bcf --- /dev/null +++ b/src/quic/stream.h @@ -0,0 +1,299 @@ +#ifndef SRC_QUIC_STREAM_H_ +#define SRC_QUIC_STREAM_H_ +#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS + +#include "aliased_struct.h" +#include "async_wrap.h" +#include "base_object.h" +#include "env.h" +#include "node_http_common.h" +#include "quic/quic.h" +#include "quic/buffer.h" +#include "quic/stats.h" + +#include + +namespace node { +namespace quic { + +#define STREAM_STATS(V) \ + V(CREATED_AT, created_at, "Created At") \ + V(RECEIVED_AT, received_at, "Last Received At") \ + V(ACKED_AT, acked_at, "Last Acknowledged At") \ + V(CLOSING_AT, closing_at, "Closing At") \ + V(DESTROYED_AT, destroyed_at, "Destroyed At") \ + V(BYTES_RECEIVED, bytes_received, "Bytes Received") \ + V(BYTES_SENT, bytes_sent, "Bytes Sent") \ + V(MAX_OFFSET, max_offset, "Max Offset") \ + V(MAX_OFFSET_ACK, max_offset_ack, "Max Acknowledged Offset") \ + V(MAX_OFFSET_RECV, max_offset_received, "Max Received Offset") \ + V(FINAL_SIZE, final_size, "Final Size") + +#define STREAM_STATE(V) \ + V(FIN_SENT, fin_sent, uint8_t) \ + V(FIN_RECEIVED, fin_received, uint8_t) \ + V(READ_ENDED, read_ended, uint8_t) + +class Stream; + +#define V(name, _, __) IDX_STATS_STREAM_##name, +enum class StreamStatsIdx : int { + STREAM_STATS(V) + IDX_STATS_STREAM_COUNT +}; +#undef V + +#define V(_, name, __) uint64_t name; +struct StreamStats final { + STREAM_STATS(V) +}; +#undef V + +using StreamStatsBase = StatsBase>; + +// QUIC Stream's are simple data flows that may be: +// +// * Bidirectional or Unidirectional +// * Server or Client Initiated +// +// The flow direction and origin of the stream are important in +// determining the write and read state (Open or Closed). Specifically: +// +// A Unidirectional stream originating with the Server is: +// +// * Server Writable (Open) but not Client Writable (Closed) +// * Client Readable (Open) but not Server Readable (Closed) +// +// Likewise, a Unidirectional stream originating with the +// Client is: +// +// * Client Writable (Open) but not Server Writable (Closed) +// * Server Readable (Open) but not Client Readable (Closed) +// +// Bidirectional Stream States +// +------------+--------------+--------------------+---------------------+ +// | | Initiated By | Initial Read State | Initial Write State | +// +------------+--------------+--------------------+---------------------+ +// | On Server | Server | Open | Open | +// +------------+--------------+--------------------+---------------------+ +// | On Server | Client | Open | Open | +// +------------+--------------+--------------------+---------------------+ +// | On Client | Server | Open | Open | +// +------------+--------------+--------------------+---------------------+ +// | On Client | Client | Open | Open | +// +------------+--------------+--------------------+---------------------+ +// +// Unidirectional Stream States +// +------------+--------------+--------------------+---------------------+ +// | | Initiated By | Initial Read State | Initial Write State | +// +------------+--------------+--------------------+---------------------+ +// | On Server | Server | Closed | Open | +// +------------+--------------+--------------------+---------------------+ +// | On Server | Client | Open | Closed | +// +------------+--------------+--------------------+---------------------+ +// | On Client | Server | Open | Closed | +// +------------+--------------+--------------------+---------------------+ +// | On Client | Client | Closed | Open | +// +------------+--------------+--------------------+---------------------+ +// +// All data sent via the Stream is buffered internally until either +// receipt is acknowledged from the peer or attempts to send are abandoned. +// +// A Stream may be in a fully closed state (No longer readable nor +// writable) state but still have unacknowledged data in it's outbound queue. +// +// A Stream is gracefully closed when (a) both Read and Write states +// are Closed, (b) all queued data has been acknowledged. +// +// The Stream may be forcefully closed immediately using destroy(err). +// This causes all queued data and pending JavaScript writes to be +// abandoned, and causes the QuicStream to be immediately closed at the +// ngtcp2 level. + +class Stream final : public AsyncWrap, + public bob::SourceImpl, + public StreamStatsBase { + public: + using Header = NgHeaderBase; + using HeaderList = std::vector>; + + enum class Origin { + SERVER, + CLIENT, + }; + + enum class Direction { + UNIDIRECTIONAL, + BIDIRECTIONAL, + }; + + enum class HeadersKind { + INFO, + INITIAL, + TRAILING, + }; + + struct State final { +#define V(_, name, type) type name; + STREAM_STATE(V) +#undef V + }; + + static bool HasInstance(Environment* env, const v8::Local& obj); + static v8::Local GetConstructorTemplate( + Environment* env); + static void Initialize(Environment* env); + + static BaseObjectPtr Create( + Environment* env, + Session* session, + stream_id id); + + Stream( + Session* session, + v8::Local object, + stream_id id, + Buffer::Source* source = nullptr); + + ~Stream() override; + + // Acknowledge is called when ngtcp2 has received an acknowledgement + // for one or more stream frames for this Stream. + void Acknowledge(uint64_t offset, size_t datalen); + + // Returns false if the header cannot be added. This will + // typically only happen if a maximimum number of headers, + // or the maximum total header length is received. + bool AddHeader(std::unique_ptr
header); + + // Attaches the given Buffer::Consumer to this Stream to consume + // and inbound data. + void AttachInboundConsumer( + Buffer::Consumer* consumer, + BaseObjectPtr strong_ptr = BaseObjectPtr()); + + // Attaches an outbound Buffer::Source + void AttachOutboundSource(Buffer::Source* source); + + // Signals the beginning of a new block of headers. + void BeginHeaders(HeadersKind kind); + + void Commit(size_t ammount); + + void Destroy(const QuicError& error = kQuicNoError); + + int DoPull( + bob::Next next, + int options, + ngtcp2_vec* data, + size_t count, + size_t max_count_hint) override; + + // Signals the ending of the current block of headers. + void EndHeaders(); + + void OnClose(); + + void ReceiveData( + uint32_t flags, + const uint8_t* data, + size_t datalen, + uint64_t offset); + + // ResetStream will cause ngtcp2 to queue a RESET_STREAM and STOP_SENDING + // frame, as appropriate, for the given stream_id. For a locally-initiated + // unidirectional stream, only a RESET_STREAM frame will be scheduled and + // the stream will be immediately closed. For a bidirectional stream, a + // STOP_SENDING frame will be sent. + void ResetStream(const QuicError& error = kQuicAppNoError); + + void Resume(); + + void StopSending(const QuicError& error = kQuicAppNoError); + + inline bool is_destroyed() const { return destroyed_; } + + inline Direction direction() const { + return id_ & 0b10 ? + Direction::UNIDIRECTIONAL : + Direction::BIDIRECTIONAL; + } + + inline stream_id id() const { return id_; } + + inline Origin origin() const { + return id_ & 0b01 ? Origin::SERVER : Origin::CLIENT; + } + + inline Session* session() const { return session_.get(); } + + inline void set_destroyed() { destroyed_ = true; } + + inline void set_fin_sent() { fin_sent_ = true; } + + // Set the final size for the Stream. This only works + // the first time it is called. Subsequent calls will be + // ignored unless the subsequent size is greater than the + // prior set size, in which case we have a bug and we'll + // assert. + void set_final_size(uint64_t final_size); + + // The final size is the maximum amount of data that has been + // acknowleged to have been received for a Stream. + inline uint64_t final_size() const { + return GetStat(&StreamStats::final_size); + } + + inline void set_headers_kind(HeadersKind kind) { headers_kind_ = kind; } + + void MemoryInfo(MemoryTracker* tracker) const override; + SET_MEMORY_INFO_NAME(Stream) + SET_SELF_SIZE(Stream) + + private: + void UpdateStats(size_t datalen); + void ProcessInbound(); + + BaseObjectPtr session_; + AliasedStruct state_; + stream_id id_; + HeadersKind kind_; + bool destroyed_ = false; + bool fin_sent_ = false; + + // The outbound_source_ provides the data that is to be + // sent by this Stream. After the source is read + // the writable side of the QuicStream will be closed + // by sending a fin data frame. + Buffer::Source* outbound_source_ = nullptr; + BaseObjectPtr outbound_source_strong_ptr_; + + // The inbound_ buffer contains the data that has been + // received by this Stream. The received data will + // be buffered in inbound_ until an inbound_consumer_ + // is attached. Only a single inbound_consumer_ may be + // attached at a time. + Buffer inbound_; + Buffer::Consumer* inbound_consumer_ = nullptr; + BaseObjectPtr inbound_consumer_strong_ptr_; + + HeaderList headers_; + HeadersKind headers_kind_; + + // The current total byte length of the headers + size_t current_headers_length_ = 0; + + ListNode stream_queue_; + public: + using Queue = ListHead; + + void Schedule(Queue* queue); + + inline void Unschedule() { stream_queue_.Remove(); } +}; + +} // namespace quic +} // namespace node + +#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS +#endif // SRC_QUIC_STREAM_H_