/
githubmirror
/
node
Обзор
Документация
Войти
/
githubmirror
/
node
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
src/quic/endpoint.cc
2 192 строки
79 KB
Tim Perry
quic: defer server session emit until TLS ClientHello is processed
23 июл 2026, 16:44
Не верифицирован
23 июл 2026, 16:44
f9715fc
Код
Авторство
О чём код?
#if HAVE_OPENSSL && HAVE_QUIC #include "guard.h" #ifndef OPENSSL_NO_QUIC #include <aliased_struct-inl.h> #include <async_wrap-inl.h> #include <debug_utils-inl.h> #include <env-inl.h> #include <memory_tracker-inl.h> #include <ngtcp2/ngtcp2.h> #include <node_errors.h> #include <node_external_reference.h> #include <node_process-inl.h> #include <node_sockaddr-inl.h> #include <permission/permission.h> #include <timer_wrap-inl.h> #include <util-inl.h> #include <uv.h> #include <v8.h> #include <limits> #include "application.h" #include "bindingdata.h" #include "defs.h" #include "endpoint.h" #include "http3.h" #include "ncrypto.h" #include "session_manager.h" namespace node { using v8::Array; using v8::ArrayBufferView; using v8::HandleScope; using v8::Integer; using v8::Just; using v8::Local; using v8::Maybe; using v8::Nothing; using v8::Number; using v8::Object; using v8::ObjectTemplate; using v8::String; using v8::Uint32; using v8::Value; namespace quic { #define ENDPOINT_STATE(V) \ /* Bound to the UDP port */ \ V(BOUND, bound, uint8_t) \ /* Receiving packets on the UDP port */ \ V(RECEIVING, receiving, uint8_t) \ /* Listening as a QUIC server */ \ V(LISTENING, listening, uint8_t) \ /* In the process of closing down, waiting for pending send callbacks */ \ V(CLOSING, closing, uint8_t) \ /* Temporarily paused serving new initial requests */ \ V(BUSY, busy, uint8_t) \ /* Max concurrent connections per IP (0 = unlimited) */ \ V(MAX_CONNECTIONS_PER_HOST, max_connections_per_host, uint16_t) \ /* Max total concurrent connections (0 = unlimited) */ \ V(MAX_CONNECTIONS_TOTAL, max_connections_total, uint16_t) \ /* The number of pending send callbacks */ \ V(PENDING_CALLBACKS, pending_callbacks, uint64_t) #define ENDPOINT_STATS(V) \ V(CREATED_AT, created_at) \ V(DESTROYED_AT, destroyed_at) \ V(BYTES_RECEIVED, bytes_received) \ V(BYTES_SENT, bytes_sent) \ V(PACKETS_RECEIVED, packets_received) \ V(PACKETS_SENT, packets_sent) \ V(SERVER_SESSIONS, server_sessions) \ V(CLIENT_SESSIONS, client_sessions) \ V(SERVER_BUSY_COUNT, server_busy_count) \ V(RETRY_COUNT, retry_count) \ V(RETRY_RATE_LIMITED, retry_rate_limited) \ V(VERSION_NEGOTIATION_COUNT, version_negotiation_count) \ V(VERSION_NEGOTIATION_RATE_LIMITED, version_negotiation_rate_limited) \ V(STATELESS_RESET_COUNT, stateless_reset_count) \ V(STATELESS_RESET_RATE_LIMITED, stateless_reset_rate_limited) \ V(IMMEDIATE_CLOSE_COUNT, immediate_close_count) \ V(IMMEDIATE_CLOSE_RATE_LIMITED, immediate_close_rate_limited) \ V(SESSION_CREATION_RATE_LIMITED, session_creation_rate_limited) \ V(PACKETS_BLOCKED, packets_blocked) struct Endpoint::State { #define V(_, name, type) type name; ENDPOINT_STATE(V) #undef V }; STAT_STRUCT(Endpoint, ENDPOINT) TokenBucket::TokenBucket(double rate, double burst) : rate(rate), burst(burst), tokens(burst), last_ts(uv_hrtime()) {} void TokenBucket::InitOnce(double r, double b, uint64_t now) { if (last_ts == 0) { rate = r; burst = b; tokens = b; last_ts = now; } } // Try to consume one token. Refills based on elapsed time, then // attempts to consume. Returns true if the request is allowed. bool TokenBucket::consume(uint64_t now) { double elapsed = static_cast<double>(now - last_ts) / 1e9; // seconds last_ts = now; tokens = std::min(burst, tokens + elapsed * rate); if (tokens >= 1.0) { tokens -= 1.0; return true; } return false; } // ============================================================================ // Endpoint::Options namespace { #ifdef DEBUG bool is_diagnostic_packet_loss(double probability) { if (probability == 0.0) [[unlikely]] { return false; } unsigned char c = 255; CHECK(ncrypto::CSPRNG(&c, 1)); return (static_cast<double>(c) / 255) < probability; } #endif // DEBUG template <typename Opt, double Opt::*member> bool SetOption(Environment* env, Opt* options, const Local<Object>& object, const Local<String>& name) { Local<Value> value; if (!object->Get(env->context(), name).ToLocal(&value)) return false; if (!value->IsUndefined()) { if (!value->IsNumber()) { Utf8Value nameStr(env->isolate(), name); THROW_ERR_INVALID_ARG_VALUE( env, "The %s option must be a number", nameStr); return false; } Local<Number> num = value.As<Number>(); double dbl = num->Value(); if (dbl < 0) { Utf8Value nameStr(env->isolate(), name); THROW_ERR_INVALID_ARG_VALUE( env, "The %s option must be a non-negative number", nameStr); return false; } options->*member = dbl; } return true; } template <typename Opt, uint8_t Opt::*member> bool SetOption(Environment* env, Opt* options, const Local<Object>& object, const Local<String>& name) { Local<Value> value; if (!object->Get(env->context(), name).ToLocal(&value)) return false; if (!value->IsUndefined()) { if (!value->IsUint32()) { Utf8Value nameStr(env->isolate(), name); THROW_ERR_INVALID_ARG_VALUE( env, "The %s option must be an uint8", nameStr); return false; } Local<Uint32> num = value.As<Uint32>(); uint32_t val = num->Value(); if (val > std::numeric_limits<uint8_t>::max()) { Utf8Value nameStr(env->isolate(), name); THROW_ERR_INVALID_ARG_VALUE( env, "The %s option must be an uint8", nameStr); return false; } options->*member = val; } return true; } template <typename Opt, TokenSecret Opt::*member> bool SetOption(Environment* env, Opt* options, const Local<Object>& object, const Local<String>& name) { Local<Value> value; if (!object->Get(env->context(), name).ToLocal(&value)) return false; if (!value->IsUndefined()) { if (!value->IsArrayBufferView()) { Utf8Value nameStr(env->isolate(), name); THROW_ERR_INVALID_ARG_VALUE( env, "The %s option must be an ArrayBufferView", nameStr); return false; } Store store = Store::CopyFrom(value.As<ArrayBufferView>()); if (store.length() != TokenSecret::QUIC_TOKENSECRET_LEN) { Utf8Value nameStr(env->isolate(), name); THROW_ERR_INVALID_ARG_VALUE( env, "The %s option must be an ArrayBufferView of length %d", nameStr, TokenSecret::QUIC_TOKENSECRET_LEN); return false; } ngtcp2_vec buf = store; TokenSecret secret(buf.base); options->*member = secret; } return true; } } // namespace Maybe<Endpoint::Options> Endpoint::Options::From(Environment* env, Local<Value> value) { if (value.IsEmpty() || !value->IsObject()) { if (value->IsUndefined()) return Just(Options()); THROW_ERR_INVALID_ARG_TYPE(env, "options must be an object"); return Nothing<Options>(); } auto& state = BindingData::Get(env); auto params = value.As<Object>(); Options options; #define SET(name) \ SetOption<Endpoint::Options, &Endpoint::Options::name>( \ env, &options, params, state.name##_string()) if (!SET(retry_token_expiration) || !SET(token_expiration) || !SET(address_lru_size) || !SET(validate_address) || !SET(disable_stateless_reset) || !SET(ipv6_only) || !SET(reuse_port) || !SET(retry_rate) || !SET(retry_burst) || !SET(stateless_reset_rate) || !SET(stateless_reset_burst) || !SET(version_negotiation_rate) || !SET(version_negotiation_burst) || !SET(immediate_close_rate) || !SET(immediate_close_burst) || !SET(session_creation_rate) || !SET(session_creation_burst) || #ifdef DEBUG !SET(rx_loss) || !SET(tx_loss) || #endif !SET(udp_receive_buffer_size) || !SET(udp_send_buffer_size) || !SET(udp_ttl) || !SET(idle_timeout) || !SET(reset_token_secret) || !SET(token_secret)) { return Nothing<Options>(); } Local<Value> address; if (!params->Get(env->context(), env->address_string()).ToLocal(&address)) { return Nothing<Options>(); } if (!address->IsUndefined()) { if (!SocketAddressBase::HasInstance(env, address)) { THROW_ERR_INVALID_ARG_TYPE(env, "The address option must be a SocketAddress"); return Nothing<Options>(); } auto addr = FromJSObject<SocketAddressBase>(address.As<Object>()); options.local_address = addr->address(); } else { options.local_address = std::make_shared<SocketAddress>(); if (!SocketAddress::New("127.0.0.1", 0, options.local_address.get())) { THROW_ERR_INVALID_ADDRESS(env); return Nothing<Options>(); } } // Parse block list option. Expects the C++ SocketAddressBlockListWrap handle // (the JS side extracts [kHandle] before passing it through). Local<Value> block_list_val; if (!params->Get(env->context(), state.block_list_string()) .ToLocal(&block_list_val)) { return Nothing<Options>(); } if (!block_list_val->IsUndefined()) { if (!SocketAddressBlockListWrap::HasInstance(env, block_list_val)) { THROW_ERR_INVALID_ARG_TYPE( env, "The blockList option must be a BlockList handle"); return Nothing<Options>(); } auto* wrap = FromJSObject<SocketAddressBlockListWrap>(block_list_val.As<Object>()); options.block_list = wrap->blocklist(); } // Parse block list policy. Local<Value> policy_val; if (!params->Get(env->context(), state.block_list_policy_string()) .ToLocal(&policy_val)) { return Nothing<Options>(); } if (!policy_val->IsUndefined()) { if (policy_val->StrictEquals(state.allow_string())) { options.block_list_policy = Options::BlockListPolicy::ALLOW; } else if (policy_val->StrictEquals(state.deny_string())) { options.block_list_policy = Options::BlockListPolicy::DENY; } else { THROW_ERR_INVALID_ARG_VALUE( env, "The blockListPolicy option must be 'deny' or 'allow'"); return Nothing<Options>(); } } return Just<Options>(options); #undef SET } void Endpoint::Options::MemoryInfo(MemoryTracker* tracker) const { tracker->TrackField("reset_token_secret", reset_token_secret); tracker->TrackField("token_secret", token_secret); } std::string Endpoint::Options::ToString() const { DebugIndentScope indent; auto prefix = indent.Prefix(); auto boolToString = [](uint8_t val) { return val ? std::string("yes") : std::string("no"); }; std::string res = "{ "; res += prefix + "local address: " + local_address->ToString(); res += prefix + "retry token expiration: " + std::to_string(retry_token_expiration) + " seconds"; res += prefix + "token expiration: " + std::to_string(token_expiration) + " seconds"; res += prefix + "address lru size: " + std::to_string(address_lru_size); res += prefix + "retry rate: " + std::to_string(retry_rate) + "/s"; res += prefix + "retry burst: " + std::to_string(retry_burst); res += prefix + "stateless reset rate: " + std::to_string(stateless_reset_rate) + "/s"; res += prefix + "stateless reset burst: " + std::to_string(stateless_reset_burst); res += prefix + "version negotiation rate: " + std::to_string(version_negotiation_rate) + "/s"; res += prefix + "version negotiation burst: " + std::to_string(version_negotiation_burst); res += prefix + "immediate close rate: " + std::to_string(immediate_close_rate) + "/s"; res += prefix + "immediate close burst: " + std::to_string(immediate_close_burst); res += prefix + "session creation rate: " + std::to_string(session_creation_rate) + "/s"; res += prefix + "session creation burst: " + std::to_string(session_creation_burst); res += prefix + "validate address: " + boolToString(validate_address); res += prefix + "disable stateless reset: " + boolToString(disable_stateless_reset); #ifdef DEBUG res += prefix + "rx loss: " + std::to_string(rx_loss); res += prefix + "tx loss: " + std::to_string(tx_loss); #endif res += prefix + "reset token secret: " + reset_token_secret.ToString(); res += prefix + "token secret: " + token_secret.ToString(); res += prefix + "ipv6 only: " + boolToString(ipv6_only); res += prefix + "reuse port: " + boolToString(reuse_port); res += prefix + "udp receive buffer size: " + std::to_string(udp_receive_buffer_size); res += prefix + "udp send buffer size: " + std::to_string(udp_send_buffer_size); res += prefix + "udp ttl: " + std::to_string(udp_ttl); res += prefix + "idle timeout: " + std::to_string(idle_timeout) + " seconds"; res += indent.Close(); return res; } // ====================================================================================== // Endpoint::UDP and Endpoint::UDP::Impl class Endpoint::UDP::Impl final : public HandleWrap { public: JS_CONSTRUCTOR(Impl); static Impl* Create(Endpoint* endpoint) { JS_NEW_INSTANCE_OR_RETURN(endpoint->env(), obj, nullptr); return new Impl(endpoint, obj); } static Impl* From(uv_udp_t* handle) { return ContainerOf(&Impl::handle_, handle); } static Impl* From(uv_handle_t* handle) { return From(reinterpret_cast<uv_udp_t*>(handle)); } Impl(Endpoint* endpoint, Local<Object> object) : HandleWrap(endpoint->env(), object, reinterpret_cast<uv_handle_t*>(&handle_), PROVIDER_QUIC_UDP), endpoint_(endpoint) { CHECK_EQ(uv_udp_init_ex(endpoint->env()->event_loop(), &handle_, AF_UNSPEC | UV_UDP_RECVMMSG), 0); handle_.data = this; } SET_NO_MEMORY_INFO() SET_MEMORY_INFO_NAME(Endpoint::UDP::Impl) SET_SELF_SIZE(Impl) private: // Pre-allocated receive buffer sized for recvmmsg batching. libuv's // recvmmsg path partitions the alloc buffer into 64KB chunks (one per // datagram). With kRecvBatchSize chunks we can receive up to that many // packets in a single recvmmsg syscall. ngtcp2_conn_read_pkt is // synchronous — it copies what it needs — so the buffer is safely // reused across batches. // libuv's recvmmsg partitions the buffer into UV__UDP_DGRAM_MAXSIZE (64KB) // chunks regardless of actual packet size. QUIC packets are ~1200 bytes, // so most of each 64KB chunk is wasted. We use a modest batch size to // balance syscall reduction against memory usage. static constexpr size_t kDgramMaxSize = 65536; // UV__UDP_DGRAM_MAXSIZE static constexpr size_t kRecvBatchSize = 5; static constexpr size_t kRecvBufferSize = kDgramMaxSize * kRecvBatchSize; std::array<char, kRecvBufferSize> recv_buf_ = {}; static void OnAlloc(uv_handle_t* handle, size_t suggested_size, uv_buf_t* buf) { auto* impl = From(handle); *buf = uv_buf_init(impl->recv_buf_.data(), kRecvBufferSize); } static void OnReceive(uv_udp_t* handle, ssize_t nread, const uv_buf_t* buf, const sockaddr* addr, unsigned int flags) { auto impl = From(handle); DCHECK_NOT_NULL(impl); DCHECK_NOT_NULL(impl->endpoint_); // UV_UDP_MMSG_FREE signals the end of a recvmmsg batch — the // buffer can be reused. Since our buffer is pre-allocated and // persistent, there is nothing to free. if (flags & UV_UDP_MMSG_FREE) { return; } // Nothing to do in these cases. Specifically, if the nread // is zero or we have received a partial packet, we are just // going to ignore it. No buffer release needed — recv_buf_ // is pre-allocated and reused. if (nread == 0 || flags & UV_UDP_PARTIAL) { return; } if (nread < 0) { impl->endpoint_->Destroy(CloseContext::RECEIVE_FAILURE, static_cast<int>(nread)); return; } // UV_UDP_MMSG_CHUNK is set for each packet in a recvmmsg batch. // Processing is the same as for a single-message receive — ngtcp2 // copies what it needs synchronously from the buf slice. impl->endpoint_->Receive(reinterpret_cast<const uint8_t*>(buf->base), static_cast<size_t>(nread), SocketAddress(addr)); } uv_udp_t handle_; Endpoint* endpoint_; friend class UDP; }; JS_CONSTRUCTOR_IMPL(Endpoint::UDP::Impl, udp_constructor_template, { JS_ILLEGAL_CONSTRUCTOR(); JS_INHERIT(HandleWrap); JS_CLASS(endpoint_udp); }) Endpoint::UDP::UDP(Endpoint* endpoint) : impl_(Impl::Create(endpoint)) { DCHECK(impl_); // The endpoint starts in an inactive, unref'd state. It will be ref'd when // the endpoint is either configured to listen as a server or when then are // active client sessions. Unref(); } Endpoint::UDP::~UDP() { Close(); } int Endpoint::UDP::Bind(const Options& options) { if (flags_.is_bound) return UV_EALREADY; if (is_closed_or_closing()) return UV_EBADF; int flags = 0; if (options.local_address->family() == AF_INET6 && options.ipv6_only) flags |= UV_UDP_IPV6ONLY; if (options.reuse_port) flags |= UV_UDP_REUSEPORT; int err = uv_udp_bind(&impl_->handle_, options.local_address->data(), flags); int size; if (!err) { flags_.is_bound = true; size = static_cast<int>(options.udp_receive_buffer_size); if (size > 0) { err = uv_recv_buffer_size(reinterpret_cast<uv_handle_t*>(&impl_->handle_), &size); if (err) return err; } size = static_cast<int>(options.udp_send_buffer_size); if (size > 0) { err = uv_send_buffer_size(reinterpret_cast<uv_handle_t*>(&impl_->handle_), &size); if (err) return err; } size = static_cast<int>(options.udp_ttl); if (size > 0) { err = uv_udp_set_ttl(&impl_->handle_, size); if (err) return err; } } return err; } void Endpoint::UDP::Ref() { if (!is_closed_or_closing()) { uv_ref(reinterpret_cast<uv_handle_t*>(&impl_->handle_)); } } void Endpoint::UDP::Unref() { if (!is_closed_or_closing()) { uv_unref(reinterpret_cast<uv_handle_t*>(&impl_->handle_)); } } int Endpoint::UDP::Start() { if (is_closed_or_closing()) return UV_EBADF; if (flags_.is_started) return 0; int err = uv_udp_recv_start(&impl_->handle_, Impl::OnAlloc, Impl::OnReceive); flags_.is_started = (err == 0); return err; } void Endpoint::UDP::Stop() { if (is_closed_or_closing() || !flags_.is_started) return; USE(uv_udp_recv_stop(&impl_->handle_)); flags_.is_started = false; } void Endpoint::UDP::Close() { if (is_closed_or_closing()) return; DCHECK(impl_); Stop(); flags_.is_bound = false; flags_.is_closed = true; impl_->Close(); impl_.reset(); } bool Endpoint::UDP::is_bound() const { return flags_.is_bound; } bool Endpoint::UDP::is_closed() const { return flags_.is_closed; } bool Endpoint::UDP::is_closed_or_closing() const { if (is_closed() || !impl_) return true; return impl_->IsHandleClosing(); } Endpoint::UDP::operator bool() const { return impl_; } SocketAddress Endpoint::UDP::local_address() const { DCHECK(!is_closed_or_closing() && is_bound()); return SocketAddress::FromSockName(impl_->handle_); } int Endpoint::UDP::Send(Packet::Ptr packet) { DCHECK(packet); if (is_closed_or_closing()) return UV_EBADF; // Detach from the Ptr — libuv takes ownership until the callback fires. Packet* raw = packet.release(); uv_buf_t buf = *raw; int err = uv_udp_send(raw->req(), &impl_->handle_, &buf, 1, raw->destination().data(), [](uv_udp_send_t* req, int status) { Packet* p = Packet::FromReq(req); p->listener()->PacketDone(status); ArenaPool<Packet>::Release(p); }); if (err < 0) { // Send failed — release the packet back to the pool immediately. ArenaPool<Packet>::Release(raw); } return err; } int Endpoint::UDP::TrySend(const Packet::Ptr& packet) { DCHECK(packet); if (is_closed_or_closing()) return UV_EBADF; uv_buf_t buf = *packet; return uv_udp_try_send( &impl_->handle_, &buf, 1, packet->destination().data()); } int Endpoint::UDP::TrySendBatch(uv_buf_t* bufs[], unsigned int nbufs[], struct sockaddr* addrs[], size_t count) { DCHECK_GT(count, 0); if (is_closed_or_closing()) return UV_EBADF; return uv_udp_try_send2( &impl_->handle_, static_cast<unsigned int>(count), bufs, nbufs, addrs, 0); } void Endpoint::UDP::MemoryInfo(MemoryTracker* tracker) const { if (impl_) tracker->TrackField("impl", impl_); } // ============================================================================ JS_CONSTRUCTOR_IMPL(Endpoint, endpoint_constructor_template, { auto isolate = env->isolate(); JS_NEW_CONSTRUCTOR(); JS_INHERIT(AsyncWrap); JS_CLASS(endpoint); SetProtoMethod(isolate, tmpl, "listen", DoListen); SetProtoMethod(isolate, tmpl, "closeGracefully", DoCloseGracefully); SetProtoMethod(isolate, tmpl, "connect", DoConnect); SetProtoMethod(isolate, tmpl, "markBusy", MarkBusy); SetProtoMethod(isolate, tmpl, "ref", Ref); SetProtoMethod(isolate, tmpl, "setSNIContexts", DoSetSNIContexts); SetProtoMethodNoSideEffect(isolate, tmpl, "address", LocalAddress); }) void Endpoint::InitPerIsolate(IsolateData* data, Local<ObjectTemplate> target) { // TODO(@jasnell): Implement the per-isolate state } void Endpoint::InitPerContext(Realm* realm, Local<Object> target) { #define V(name, _) IDX_STATS_ENDPOINT_##name, enum IDX_STATS_ENDPOINT { ENDPOINT_STATS(V) IDX_STATS_ENDPOINT_COUNT }; NODE_DEFINE_CONSTANT(target, IDX_STATS_ENDPOINT_COUNT); #undef V #define V(name, key) NODE_DEFINE_CONSTANT(target, IDX_STATS_ENDPOINT_##name); ENDPOINT_STATS(V); #undef V #define V(name, key, type) \ static constexpr auto IDX_STATE_ENDPOINT_##name = \ offsetof(Endpoint::State, key); \ static constexpr auto IDX_STATE_ENDPOINT_##name##_SIZE = sizeof(type); \ NODE_DEFINE_CONSTANT(target, IDX_STATE_ENDPOINT_##name); \ NODE_DEFINE_CONSTANT(target, IDX_STATE_ENDPOINT_##name##_SIZE); ENDPOINT_STATE(V) #undef V NODE_DEFINE_CONSTANT(target, DEFAULT_MAX_SOCKETADDRESS_LRU_SIZE); static constexpr auto DEFAULT_RETRYTOKEN_EXPIRATION = RetryToken::QUIC_DEFAULT_RETRYTOKEN_EXPIRATION / NGTCP2_SECONDS; static constexpr auto DEFAULT_REGULARTOKEN_EXPIRATION = RegularToken::QUIC_DEFAULT_REGULARTOKEN_EXPIRATION / NGTCP2_SECONDS; static constexpr auto DEFAULT_MAX_PACKET_LENGTH = kDefaultMaxPacketLength; NODE_DEFINE_CONSTANT(target, DEFAULT_RETRYTOKEN_EXPIRATION); NODE_DEFINE_CONSTANT(target, DEFAULT_REGULARTOKEN_EXPIRATION); NODE_DEFINE_CONSTANT(target, DEFAULT_MAX_PACKET_LENGTH); static constexpr auto CLOSECONTEXT_CLOSE = static_cast<uint8_t>(CloseContext::CLOSE); static constexpr auto CLOSECONTEXT_BIND_FAILURE = static_cast<uint8_t>(CloseContext::BIND_FAILURE); static constexpr auto CLOSECONTEXT_LISTEN_FAILURE = static_cast<uint8_t>(CloseContext::LISTEN_FAILURE); static constexpr auto CLOSECONTEXT_RECEIVE_FAILURE = static_cast<uint8_t>(CloseContext::RECEIVE_FAILURE); static constexpr auto CLOSECONTEXT_SEND_FAILURE = static_cast<uint8_t>(CloseContext::SEND_FAILURE); static constexpr auto CLOSECONTEXT_START_FAILURE = static_cast<uint8_t>(CloseContext::START_FAILURE); NODE_DEFINE_CONSTANT(target, CLOSECONTEXT_CLOSE); NODE_DEFINE_CONSTANT(target, CLOSECONTEXT_BIND_FAILURE); NODE_DEFINE_CONSTANT(target, CLOSECONTEXT_LISTEN_FAILURE); NODE_DEFINE_CONSTANT(target, CLOSECONTEXT_RECEIVE_FAILURE); NODE_DEFINE_CONSTANT(target, CLOSECONTEXT_SEND_FAILURE); NODE_DEFINE_CONSTANT(target, CLOSECONTEXT_START_FAILURE); SetConstructorFunction(realm->context(), target, "Endpoint", GetConstructorTemplate(realm->env())); } void Endpoint::RegisterExternalReferences(ExternalReferenceRegistry* registry) { registry->Register(New); registry->Register(DoConnect); registry->Register(DoListen); registry->Register(DoCloseGracefully); registry->Register(DoSetSNIContexts); registry->Register(LocalAddress); registry->Register(Ref); registry->Register(MarkBusy); } Endpoint::Endpoint(Environment* env, Local<Object> object, const Options& options) : AsyncWrap(env, object, PROVIDER_QUIC_ENDPOINT), stats_(env->isolate()), state_(env->isolate()), options_(options), packet_pool_(kDefaultMaxPacketLength, ArenaPool<Packet>::kDefaultSlotsPerBlock), udp_(this), idle_timer_(env, [this] { HandleScope scope(this->env()->isolate()); Destroy(); }), addr_validation_lru_(options_.address_lru_size), retry_bucket_(options_.retry_rate, options_.retry_burst), stateless_reset_bucket_(options_.stateless_reset_rate, options_.stateless_reset_burst), version_negotiation_bucket_(options_.version_negotiation_rate, options_.version_negotiation_burst), immediate_close_bucket_(options_.immediate_close_rate, options_.immediate_close_burst) { MakeWeak(); udp_.Unref(); idle_timer_.Unref(); STAT_RECORD_TIMESTAMP(Stats, created_at); IF_QUIC_DEBUG(env) { Debug(this, "Endpoint created. Options %s", options.ToString()); } JS_DEFINE_READONLY_PROPERTY( env, object, env->state_string(), state_.GetArrayBuffer()); JS_DEFINE_READONLY_PROPERTY( env, object, env->stats_string(), stats_.GetArrayBuffer()); } Packet::Ptr Endpoint::CreatePacket(const SocketAddress& destination, size_t length, const char* diagnostic_label) { auto ptr = packet_pool_.AcquireExtra(static_cast<Packet::Listener*>(this), destination); if (ptr) { ptr->Truncate(std::min(length, ptr->capacity())); ptr->set_diagnostic_label(diagnostic_label); } return ptr; } SocketAddress Endpoint::local_address() const { DCHECK(!is_closed() && !is_closing()); return udp_.local_address(); } void Endpoint::MarkAsBusy(bool on) { Debug(this, "Marking endpoint as %s", on ? "busy" : "not busy"); if (on) STAT_INCREMENT(Stats, server_busy_count); state_->busy = on ? 1 : 0; } RegularToken Endpoint::GenerateNewToken(uint32_t version, const SocketAddress& remote_address) { Debug(this, "Generating new regular token for version %u and remote address %s", version, remote_address); DCHECK(!is_closed() && !is_closing()); return RegularToken(version, remote_address, options_.token_secret); } SessionManager& Endpoint::session_manager() const { return BindingData::Get(env()).session_manager(); } StatelessResetToken Endpoint::GenerateNewStatelessResetToken( uint8_t* token, const CID& cid) const { DCHECK(!is_closed() && !is_closing()); return StatelessResetToken(token, options_.reset_token_secret, cid); } void Endpoint::AddSession(const CID& cid, BaseObjectPtr<Session> session) { DCHECK(!is_closed() && !is_closing()); Debug(this, "Adding session for CID %s", cid); if (state_->max_connections_per_host > 0) { conn_counts_per_host_[session->remote_address()]++; } auto& mgr = session_manager(); // Associate peer-chosen CIDs in the local dcid_to_scid_ map. AssociateCID(session->config().dcid, session->config().scid); mgr.AddSession(cid, session); mgr.SetPrimaryEndpoint(session.get(), this); // For server sessions, associate the client's original DCID (ocid) so // that 0-RTT packets arriving in a separate UDP datagram can be routed // to this session. This must happen after the session is added (so // FindSession can resolve the mapping) and before any JS runs (which // may yield to libuv, allowing the 0-RTT packet to arrive). if (session->is_server() && session->config().ocid) { AssociateCID(session->config().ocid, session->config().scid); } // After Retry, the client continues to use the Retry SCID as its DCID // until the handshake completes. Register it so retransmitted Initials // and subsequent handshake packets can be routed to this session. if (session->is_server() && session->config().retry_scid) { AssociateCID(session->config().retry_scid, session->config().scid); } // Increment the primary session count and ref the handle BEFORE any // JS can run for this session (the deferred EmitNewSession, or packet // processing callbacks). JS may close/destroy the session // synchronously; the session's ~Impl calls RemoveSession which // decrements the count. If we incremented after, RemoveSession would // see count=0 and the count would be permanently off by one. if (primary_session_count_++ == 0) { idle_timer_.Stop(); udp_.Ref(); } if (session->is_server()) { STAT_INCREMENT(Stats, server_sessions); // Note that we don't emit new sessions here - that's deferred until the // ClientHello has been processed (see Session::ReadPacket), so the // session is exposed to JS only once its SNI/ALPN are known and invalid // handshakes never surface. } else { STAT_INCREMENT(Stats, client_sessions); } } void Endpoint::RemoveSession(const CID& cid, const SocketAddress& remote_address) { if (is_closed()) return; Debug(this, "Removing session for CID %s", cid); auto it = conn_counts_per_host_.find(remote_address); if (it != conn_counts_per_host_.end()) { if (--it->second == 0) { conn_counts_per_host_.erase(it); } } if (primary_session_count_ > 0 && --primary_session_count_ == 0) { if (!is_listening()) { udp_.Unref(); } session_manager().RemoveSession(cid); // The endpoint may be idle (no sessions, not listening). MaybeDestroy // handles both closing (immediate destroy) and idle timeout (start // timer or destroy based on idle_timeout setting). MaybeDestroy(); return; } session_manager().RemoveSession(cid); if (state_->closing == 1) MaybeDestroy(); } BaseObjectPtr<Session> Endpoint::FindSession(const CID& cid) { // First, try the SessionManager's primary sessions_ map directly. // This handles the common case where the CID is a locally-generated SCID. auto session = session_manager().FindSession(cid); if (session) return session; // If not found, check this endpoint's local dcid_to_scid_ map for a // secondary CID mapping. This map contains peer-chosen CID values that // are only meaningful in the context of this endpoint's sessions. auto scid_it = dcid_to_scid_.find(cid); if (scid_it != dcid_to_scid_.end()) { session = session_manager().FindSession(scid_it->second); if (session) return session; // Stale mapping — clean up. dcid_to_scid_.erase(scid_it); } return {}; } void Endpoint::AssociateCID(const CID& cid, const CID& scid) { if (!is_closed() && !is_closing() && cid && scid && cid != scid) { auto it = dcid_to_scid_.find(cid); if (it == dcid_to_scid_.end() || it->second != scid) { Debug(this, "Associating CID %s with SCID %s", cid, scid); dcid_to_scid_[cid] = scid; } } } void Endpoint::DisassociateCID(const CID& cid) { if (!is_closed() && cid) { Debug(this, "Disassociating CID %s", cid); dcid_to_scid_.erase(cid); } } void Endpoint::AssociateStatelessResetToken(const StatelessResetToken& token, Session* session) { if (is_closed() || is_closing()) return; Debug(this, "Associating stateless reset token %s with session", token); session_manager().AssociateStatelessResetToken(token, session); } void Endpoint::DisassociateStatelessResetToken( const StatelessResetToken& token) { if (!is_closed()) { Debug(this, "Disassociating stateless reset token %s", token); session_manager().DisassociateStatelessResetToken(token); } } void Endpoint::Send(Packet::Ptr packet) { #ifdef DEBUG // When diagnostic packet loss is enabled, the packet will be randomly // dropped. This can happen to any type of packet. We use this only in // testing to test various reliability issues. if (is_diagnostic_packet_loss(options_.tx_loss)) [[unlikely]] { // Simulating tx packet loss. Ptr destructor releases the packet. return; } #endif // DEBUG if (is_closed() || is_closing() || packet->length() == 0) { // Ptr destructor releases the packet back to the pool. return; } Debug(this, "Sending %s", packet->ToString()); size_t packet_length = packet->length(); state_->pending_callbacks++; env()->IncreaseWaitingRequestCounter(); int err = udp_.Send(std::move(packet)); if (err != 0) { Debug(this, "Sending packet failed with error %d", err); // The packet was already released in UDP::Send on error. state_->pending_callbacks--; env()->DecreaseWaitingRequestCounter(); Destroy(CloseContext::SEND_FAILURE, err); return; } STAT_INCREMENT_N(Stats, bytes_sent, packet_length); STAT_INCREMENT(Stats, packets_sent); } void Endpoint::SendOrTrySend(Packet::Ptr packet) { #ifdef DEBUG if (is_diagnostic_packet_loss(options_.tx_loss)) [[unlikely]] { return; } #endif if (is_closed() || is_closing() || !packet || packet->length() == 0) { return; } Debug(this, "TrySend %s", packet->ToString()); // Attempt synchronous send. On success (returns number of bytes sent), // the packet is delivered immediately — no callback overhead, no // waiting for the next poll cycle. int err = udp_.TrySend(packet); if (err >= 0) { // Synchronous send succeeded. STAT_INCREMENT_N(Stats, bytes_sent, packet->length()); STAT_INCREMENT(Stats, packets_sent); // Ptr destructor releases back to arena pool. return; } if (err == UV_EAGAIN) { // Socket not writable or async sends are queued. Fall back to the // async path — the packet will be queued and flushed on the next // POLLOUT cycle. Debug(this, "TrySend got EAGAIN, falling back to async Send"); return Send(std::move(packet)); } // Other errors are fatal. Debug(this, "TrySend failed with error %d", err); Destroy(CloseContext::SEND_FAILURE, err); } void Endpoint::SendBatch(Packet::Ptr* packets, size_t count) { if (count == 0) return; #ifdef DEBUG if (is_diagnostic_packet_loss(options_.tx_loss)) [[unlikely]] { for (size_t i = 0; i < count; i++) packets[i].reset(); return; } #endif if (is_closed() || is_closing()) { for (size_t i = 0; i < count; i++) packets[i].reset(); return; } static constexpr size_t kMaxBatch = 64; DCHECK_LE(count, kMaxBatch); // Build libuv argument arrays directly from the Ptr array. // Packets with zero length are released and skipped. uv_buf_t bufs[kMaxBatch]; uv_buf_t* buf_ptrs[kMaxBatch]; unsigned int nbufs[kMaxBatch]; struct sockaddr* addrs[kMaxBatch]; // Map from valid-index back to the original packets[] index. size_t index_map[kMaxBatch]; size_t valid_count = 0; for (size_t i = 0; i < count; i++) { if (!packets[i] || packets[i]->length() == 0) { packets[i].reset(); continue; } bufs[valid_count] = *packets[i]; buf_ptrs[valid_count] = &bufs[valid_count]; nbufs[valid_count] = 1; addrs[valid_count] = const_cast<struct sockaddr*>(packets[i]->destination().data()); index_map[valid_count] = i; valid_count++; } if (valid_count == 0) return; // Attempt synchronous batched send via sendmmsg. int sent = udp_.TrySendBatch(buf_ptrs, nbufs, addrs, valid_count); if (sent > 0) { // Packets [0, sent) were delivered synchronously. // Release them immediately — no async callback needed. for (size_t i = 0; i < static_cast<size_t>(sent); i++) { size_t idx = index_map[i]; STAT_INCREMENT_N(Stats, bytes_sent, packets[idx]->length()); STAT_INCREMENT(Stats, packets_sent); packets[idx].reset(); } } // Any unsent packets (EAGAIN, partial send, or total failure) fall // back to async uv_udp_send. size_t start = (sent > 0) ? static_cast<size_t>(sent) : 0; for (size_t i = start; i < valid_count; i++) { size_t idx = index_map[i]; Send(std::move(packets[idx])); } } void Endpoint::SendRetry(const PathDescriptor& options, uint64_t now) { Debug(this, "Sending retry on path %s", options); if (!retry_bucket_.consume(now)) { Debug(this, "Retry rate limit exceeded (global)"); STAT_INCREMENT(Stats, retry_rate_limited); return; } auto packet = Packet::CreateRetryPacket(*this, options, options_.token_secret); if (packet) { STAT_INCREMENT(Stats, retry_count); Send(std::move(packet)); } } void Endpoint::SendVersionNegotiation(const PathDescriptor& options, uint64_t now) { Debug(this, "Sending version negotiation on path %s", options); if (!version_negotiation_bucket_.consume(now)) { Debug(this, "Version negotiation rate limit exceeded (global)"); STAT_INCREMENT(Stats, version_negotiation_rate_limited); return; } auto packet = Packet::CreateVersionNegotiationPacket(*this, options); if (packet) { STAT_INCREMENT(Stats, version_negotiation_count); Send(std::move(packet)); } } bool Endpoint::SendStatelessReset(const PathDescriptor& options, size_t source_len, uint64_t now) { if (options_.disable_stateless_reset) [[unlikely]] { return false; } Debug(this, "Sending stateless reset on path %s with len %" PRIu64, options, source_len); if (!stateless_reset_bucket_.consume(now)) { Debug(this, "Stateless reset rate limit exceeded (global)"); STAT_INCREMENT(Stats, stateless_reset_rate_limited); return false; } auto packet = Packet::CreateStatelessResetPacket( *this, options, options_.reset_token_secret, source_len); if (packet) { Debug(this, "Sending stateless reset packet (%zu bytes)", packet->length()); STAT_INCREMENT(Stats, stateless_reset_count); Send(std::move(packet)); return true; } Debug(this, "Failed to create stateless reset packet"); return false; } void Endpoint::SendImmediateConnectionClose(const PathDescriptor& options, QuicError reason, uint64_t now) { Debug(this, "Sending immediate connection close on path %s with reason %s", options, reason); if (!immediate_close_bucket_.consume(now)) { Debug(this, "Immediate connection close rate limit exceeded (global)"); STAT_INCREMENT(Stats, immediate_close_rate_limited); return; } auto packet = Packet::CreateImmediateConnectionClosePacket(*this, options, reason); if (packet) { STAT_INCREMENT(Stats, immediate_close_count); Send(std::move(packet)); } } bool Endpoint::Start() { if (is_closed() || is_closing()) return false; // state_->receiving indicates that we're accepting inbound packets. It // could be for server or client side, or both. if (state_->receiving == 1) return true; Debug(this, "Starting"); int err = 0; if (state_->bound == 0) { err = udp_.Bind(options_); if (err != 0) { // If we failed to bind, destroy the endpoint. There's nothing we can do. Destroy(CloseContext::BIND_FAILURE, err); return false; } state_->bound = 1; } err = udp_.Start(); udp_.Ref(); if (err != 0) { // If we failed to start listening, destroy the endpoint. There's nothing we // can do. Destroy(CloseContext::START_FAILURE, err); return false; } auto& binding = BindingData::Get(env()); binding.listening_endpoints[this] = BaseObjectPtr<Endpoint>(this); binding.session_manager().RegisterEndpoint(this, udp_.local_address()); state_->receiving = 1; return true; } void Endpoint::Listen(const Session::Options& options) { if (is_closed() || is_closing() || state_->listening == 1) return; DCHECK(!server_state_.has_value()); // We need at least one key and one cert to complete the tls handshake on the // server. Why not make this an error? We could but it's not strictly // necessary. if (options.tls_options.keys.empty() || options.tls_options.certs.empty()) { env()->EmitProcessEnvWarning(); ProcessEmitWarning(env(), "The QUIC TLS options did not include a key or cert. " "This means the TLS handshake will fail. This is likely " "not what you want."); } auto context = TLSContext::CreateServer(env(), options.tls_options); if (!*context) { THROW_ERR_INVALID_STATE( env(), "Failed to create TLS context: %s", context->validation_error()); return; } // Create additional TLS contexts for SNI entries (virtual hosts). for (const auto& [hostname, sni_options] : options.sni) { if (!context->AddSNIContext(env(), hostname, sni_options)) { THROW_ERR_INVALID_STATE( env(), "Failed to create TLS context for SNI host '%s'", hostname); return; } } server_state_ = { options, std::move(context), }; if (Start()) { Debug(this, "Listening with options %s", server_state_->options); idle_timer_.Stop(); state_->listening = 1; } } BaseObjectPtr<Session> Endpoint::Connect( const SocketAddress& remote_address, const Session::Options& options, std::optional<SessionTicket> session_ticket) { // If starting fails, the endpoint will be destroyed. if (!Start()) return {}; Session::Config config(env(), options, local_address(), remote_address); Debug(this, "Connecting to %s with options %s and config %s [has 0rtt ticket? %s]", remote_address, options, config, session_ticket.has_value() ? "yes" : "no"); auto tls_context = TLSContext::CreateClient(env(), options.tls_options); if (!*tls_context) { THROW_ERR_INVALID_STATE(env(), "Failed to create TLS context: %s", tls_context->validation_error()); return {}; } auto session = Session::Create(this, config, tls_context.get(), session_ticket); if (!session) { THROW_ERR_INVALID_STATE(env(), "Failed to create session"); return {}; } if (!session->tls_session()) { THROW_ERR_INVALID_STATE(env(), "Failed to create TLS session: %s", session->tls_session().validation_error()); return {}; } // Marking a session as "wrapped" means that the reference has been // (or will be) passed out to JavaScript. Session::SendPendingDataScope send_scope(session); session->set_wrapped(); AddSession(config.scid, session); return session; } void Endpoint::MaybeDestroy() { if (is_closed() || primary_session_count_ > 0 || state_->pending_callbacks > 0 || state_->listening == 1) { return; } if (options_.idle_timeout > 0) { // Start the idle timer. If it fires before a new session or listen // call reactivates this endpoint, the endpoint will be destroyed. idle_timer_.Update(options_.idle_timeout * 1000); return; } // With idle_timeout == 0, only destroy if the endpoint is actively // closing (via close() or CloseGracefully). An idle endpoint that // is not closing stays alive with an unref'd handle so the process // can still exit. if (state_->closing != 1) return; // Destroy potentially creates v8 handles so let's make sure // we have a HandleScope on the stack. HandleScope scope(env()->isolate()); Destroy(); } void Endpoint::Destroy(CloseContext context, int status) { if (is_closed()) return; IF_QUIC_DEBUG(env()) { auto ctx = ([&] { switch (context) { case CloseContext::BIND_FAILURE: return "bind failure"; case CloseContext::CLOSE: return "close"; case CloseContext::LISTEN_FAILURE: return "listen failure"; case CloseContext::RECEIVE_FAILURE: return "receive failure"; case CloseContext::SEND_FAILURE: return "send failure"; case CloseContext::START_FAILURE: return "start failure"; } return "<unknown>"; })(); Debug( this, "Destroying endpoint due to \"%s\" with status %d", ctx, status); } state_->listening = 0; close_context_ = context; close_status_ = status; // If there are open sessions still, shut them down. As those clean themselves // up, they will remove themselves. The cleanup here will be synchronous and // no attempt will be made to communicate further with the peer. idle_timer_.Close(); session_manager().CloseAllSessionsFor(this); DCHECK_EQ(primary_session_count_, 0); dcid_to_scid_.clear(); server_state_.reset(); udp_.Close(); state_->closing = 0; state_->bound = 0; state_->receiving = 0; auto& binding = BindingData::Get(env()); binding.listening_endpoints.erase(this); binding.session_manager().UnregisterEndpoint(this); STAT_RECORD_TIMESTAMP(Stats, destroyed_at); EmitClose(close_context_, close_status_); } void Endpoint::CloseGracefully() { if (is_closed() || is_closing()) return; Debug(this, "Closing gracefully"); state_->listening = 0; state_->closing = 1; // Maybe we can go ahead and destroy now? MaybeDestroy(); } void Endpoint::Receive(const uint8_t* data, size_t len, const SocketAddress& remote_address) { const uint64_t now = uv_hrtime(); // Block list filtering — applied before any packet processing to // minimize resource expenditure on blocked sources. if (options_.block_list) { bool matched = options_.block_list->Apply(remote_address); bool drop = (options_.block_list_policy == Options::BlockListPolicy::DENY) ? matched // deny list: drop if address matches : !matched; // allow list: drop if address doesn't match if (drop) { Debug(this, "Packet from %s blocked by block list", remote_address); STAT_INCREMENT(Stats, packets_blocked); return; } } const auto receive = [&](Session* session, const uint8_t* pkt_data, size_t pkt_len, const SocketAddress& local_address, const SocketAddress& remote_address, const CID& dcid, const CID& scid) { DCHECK_NOT_NULL(session); if (session->is_destroyed()) return; // Use ReadPacket (no SendPendingDataScope) so that multiple packets // received in the same I/O burst are processed before any responses // are generated. The deferred flush via BindingData's uv_check // callback calls SendPendingData once per dirty session after all // packets in the burst have been read. if (session->ReadPacket(pkt_data, pkt_len, local_address, remote_address, PacketInfo(), now)) { STAT_INCREMENT_N(Stats, bytes_received, pkt_len); STAT_INCREMENT(Stats, packets_received); } // Schedule the session for deferred SendPendingData if it hasn't // been scheduled already in this burst. if (!session->is_destroyed() && !session->flags_.pending_flush) { session->flags_.pending_flush = true; BindingData::Get(env()).ScheduleSessionFlush( BaseObjectPtr<Session>(session)); } }; const auto accept = [&](const Session::Config& config, const uint8_t* pkt_data, size_t pkt_len) { // One final check. If the endpoint is closed, closing, or is not listening // as a server, then we cannot accept the initial packet. if (is_closed() || is_closing() || !is_listening()) return; // Per-host session creation rate limit. The bucket is initialized // on first access with the configured rate/burst from options. auto info = addr_validation_lru_.Upsert(config.remote_address, now); info->session_creation_bucket.InitOnce( options_.session_creation_rate, options_.session_creation_burst, now); if (!info->session_creation_bucket.consume(now)) { Debug(this, "Session creation rate limit exceeded for %s", config.remote_address); STAT_INCREMENT(Stats, session_creation_rate_limited); return; } Debug(this, "Creating new session for %s", config.dcid); std::optional<SessionTicket> no_ticket = std::nullopt; auto session = Session::Create( this, config, server_state_->tls_context.get(), no_ticket); if (!session) { Debug(this, "Failed to create session for %s", config.dcid); return; } if (!session->tls_session()) { Debug(this, "Failed to create TLS session for %s: %s", config.dcid, session->tls_session().validation_error()); return; } AddSession(config.scid, session); // It is possible that the session was created then immediately destroyed // during the call to AddSession. If that's the case, we'll just return // early. if (session->is_destroyed()) [[unlikely]] return; receive(session.get(), pkt_data, pkt_len, config.local_address, config.remote_address, config.dcid, config.scid); }; const auto acceptInitialPacket = [&](const uint32_t version, const CID& dcid, const CID& scid, const uint8_t* pkt_data, size_t pkt_len, const SocketAddress& local_address, const SocketAddress& remote_address) { // If we're not listening as a server, do not accept an initial packet. if (!is_listening()) return; ngtcp2_pkt_hd hd; // This is our first condition check... A minimal check to see if ngtcp2 can // even recognize this packet as a quic packet. if (ngtcp2_accept(&hd, pkt_data, pkt_len) != NGTCP2_SUCCESS) { // Per the ngtcp2 docs, ngtcp2_accept returns 0 if the check was // successful, or an error code if it was not. Currently there's only one // documented error code (NGTCP2_ERR_INVALID_ARGUMENT) but we'll handle // any error here the same -- by ignoring the packet entirely. return; } // Unsupported versions are handled earlier in Receive() via the // NGTCP2_ERR_VERSION_NEGOTIATION return from ngtcp2_pkt_decode_version_cid. // If we reach here, the version must be supported. CHECK_NE(ngtcp2_is_supported_version(hd.version), 0); // This is the next important condition check... If the server has been // marked busy or the remote peer has exceeded their maximum number of // concurrent connections, any new connections will be shut down // immediately. const auto limits_exceeded = ([&] { if (state_->max_connections_total > 0 && primary_session_count_ >= state_->max_connections_total) { return true; } if (state_->max_connections_per_host > 0) { auto it = conn_counts_per_host_.find(remote_address); if (it != conn_counts_per_host_.end() && it->second >= state_->max_connections_per_host) { return true; } } return false; })(); if (state_->busy || limits_exceeded) { Debug(this, "Packet was not accepted because the endpoint is busy or the " "remote address %s has exceeded their maximum number of concurrent " "connections", remote_address); // Endpoint is busy or the connection count is exceeded. The connection is // refused. For the purpose of stats collection, we'll count both of these // the same. if (state_->busy) STAT_INCREMENT(Stats, server_busy_count); SendImmediateConnectionClose( PathDescriptor{version, dcid, scid, local_address, remote_address}, QuicError::ForTransport(NGTCP2_CONNECTION_REFUSED), now); // The packet was successfully processed, even if we did refuse the // connection. STAT_INCREMENT(Stats, packets_received); return; } Debug( this, "Accepting initial packet for %s from %s", dcid, remote_address); // Generate a fresh server SCID rather than reusing the client's original // DCID. The client's original DCID is typically short (8 bytes) and we // need a 20-byte SCID to properly match short_dcidlen passed to // ngtcp2_pkt_decode_version_cid. auto server_scid = server_state_->options.cid_factory->Generate(); // At this point, we start to set up the configuration for our local // session. We pass the received scid here as the dcid argument value // because that is the value *this* session will use as the outbound dcid. Session::Config config(env(), Side::SERVER, server_state_->options, version, local_address, remote_address, scid, server_scid, dcid); Debug(this, "Using session config %s", config); // The this point, the config.scid and config.dcid represent *our* views of // the CIDs. Specifically, config.dcid identifies the peer and config.scid // identifies us. config.dcid should equal scid (peer's SCID is our DCID), // and config.ocid should equal dcid (peer's original DCID). DCHECK(config.dcid == scid); DCHECK(config.ocid == dcid); const auto is_remote_address_validated = ([&] { auto info = addr_validation_lru_.Peek(remote_address); return info != nullptr ? info->validated : false; })(); // Retry token processing and address validation are two separate // concerns. A retry token MUST always be parsed when present because // it carries the original_destination_connection_id (ODCID) that the // server must echo in its transport parameters. Without it, the peer // will reject the connection with PROTOCOL_VIOLATION. // // The address validation LRU cache determines whether we need to // *send* a Retry, but must NOT skip *processing* an incoming retry // token — a concurrent connection may have already validated the // address (populating the LRU) while this connection's Retry was // still in flight. // Step 1: Always process a retry token if present, to extract the // ODCID regardless of address validation state. if (hd.type == NGTCP2_PKT_INITIAL && hd.tokenlen > 0 && hd.token[0] == RetryToken::kTokenMagic) { RetryToken token(hd.token, hd.tokenlen); Debug(this, "Initial packet from %s has retry token %s", remote_address, token); auto ocid = token.Validate(version, remote_address, dcid, options_.token_secret, options_.retry_token_expiration * NGTCP2_SECONDS); if (!ocid.has_value()) { Debug(this, "Retry token from %s is invalid.", remote_address); SendImmediateConnectionClose( PathDescriptor{version, scid, dcid, local_address, remote_address}, QuicError::ForTransport(NGTCP2_CONNECTION_REFUSED), now); STAT_INCREMENT(Stats, packets_received); return; } Debug(this, "Retry token from %s is valid. Original dcid %s", remote_address, ocid.value()); config.ocid = ocid.value(); config.retry_scid = dcid; config.set_token(token); // Mark the address as validated since the retry round-trip proves // reachability. Debug(this, "Remote address %s is validated", remote_address); addr_validation_lru_.Upsert(remote_address, now)->validated = true; } // Step 2: Address validation — decide whether to send a Retry or // accept the packet. This only applies when the address has not // been validated yet (no LRU hit and no retry token above). if (!is_remote_address_validated && !config.retry_scid) { Debug(this, "Remote address %s is not validated", remote_address); switch (hd.type) { case NGTCP2_PKT_INITIAL: if (options_.validate_address) { if (hd.tokenlen == 0) { Debug(this, "Initial packet has no token. Sending retry to %s to start " "validation", remote_address); SendRetry( PathDescriptor{ version, dcid, scid, local_address, remote_address, }, now); STAT_INCREMENT(Stats, packets_received); return; } // Non-retry tokens (regular tokens). switch (hd.token[0]) { case RegularToken::kTokenMagic: { RegularToken token(hd.token, hd.tokenlen); Debug(this, "Initial packet from %s has regular token %s", remote_address, token); if (!token.Validate( version, remote_address, options_.token_secret, options_.token_expiration * NGTCP2_SECONDS)) { Debug(this, "Regular token from %s is invalid.", remote_address); SendRetry( PathDescriptor{ version, dcid, scid, local_address, remote_address, }, now); STAT_INCREMENT(Stats, packets_received); return; } Debug(this, "Regular token from %s is valid.", remote_address); config.set_token(token); break; } default: { Debug(this, "Initial packet from %s has unknown token type", remote_address); SendRetry( PathDescriptor{ version, dcid, scid, local_address, remote_address, }, now); STAT_INCREMENT(Stats, packets_received); return; } } Debug(this, "Remote address %s is validated", remote_address); addr_validation_lru_.Upsert(remote_address, now)->validated = true; } else if (hd.tokenlen > 0) { Debug(this, "Ignoring initial packet from %s with unexpected token", remote_address); return; } break; case NGTCP2_PKT_0RTT: if (options_.validate_address) { Debug( this, "Sending retry to %s due to 0RTT packet", remote_address); SendRetry( PathDescriptor{ version, dcid, scid, local_address, remote_address, }, now); STAT_INCREMENT(Stats, packets_received); return; } Debug(this, "Accepting 0RTT packet from %s without " "address validation", remote_address); break; } } accept(config, pkt_data, pkt_len); }; // 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. const auto maybeStatelessReset = [&](const CID& dcid, const CID& scid, const uint8_t* pkt_data, size_t pkt_len, const SocketAddress& local_address, const SocketAddress& remote_address) { // Support for stateless resets can be disabled by the application. If that // case, or if the packet is too short to contain a reset token, then we // skip the remaining checks. if (options_.disable_stateless_reset || pkt_len < NGTCP2_STATELESS_RESET_TOKENLEN) { return false; } // The stateless reset token itself is the *final* // NGTCP2_STATELESS_RESET_TOKENLEN bytes in the received packet. If it is a // stateless reset then then rest of the bytes in the packet are garbage // that we'll ignore. const uint8_t* token_pos = pkt_data + (pkt_len - NGTCP2_STATELESS_RESET_TOKENLEN); // If a Session has been associated with the token, then it is a valid // stateless reset token. We need to dispatch it to the session to be // processed. auto* session = session_manager().FindSessionByStatelessResetToken( StatelessResetToken(token_pos)); if (session != nullptr) { // If the session happens to have been destroyed already, we'll // just ignore the packet. if (!session->is_destroyed()) [[likely]] { receive(session, pkt_data, pkt_len, local_address, remote_address, dcid, scid); } return true; } // Otherwise, it's not a valid stateless reset token. return false; }; #ifdef DEBUG // When diagnostic packet loss is enabled, the packet will be randomly // dropped. if (is_diagnostic_packet_loss(options_.rx_loss)) [[unlikely]] { // Simulating rx packet loss return; } #endif // DEBUG Debug(this, "Received %zu-byte packet from %s", len, remote_address); ngtcp2_version_cid pversion_cid; // 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. switch (ngtcp2_pkt_decode_version_cid( &pversion_cid, data, len, NGTCP2_MAX_CIDLEN)) { case 0: break; // Supported version, continue processing. case NGTCP2_ERR_VERSION_NEGOTIATION: { // The packet has an unsupported version but the CIDs were // successfully decoded. Send a Version Negotiation response // per RFC 9000 Section 6. The VN packet's DCID is the client's // SCID and vice versa (mirrored back to the client). // // ngtcp2_pkt_decode_version_cid() only enforces the // NGTCP2_MAX_CIDLEN limit for *supported* versions; for an // unsupported version it returns the raw connection ID lengths // taken from the single-byte length fields on the wire, which can // be up to 255. Constructing a CID -- backed by a fixed // NGTCP2_MAX_CIDLEN-byte buffer -- from such a length writes past // the buffer (an assertion abort in release builds). A single // unauthenticated UDP datagram could therefore crash the endpoint // before any handshake. Drop these packets, mirroring the // CID-length policy applied below for supported versions. if (pversion_cid.dcidlen > NGTCP2_MAX_CIDLEN || pversion_cid.scidlen > NGTCP2_MAX_CIDLEN) { Debug(this, "Version negotiation packet had incorrectly sized CIDs, " "ignoring"); return; } Debug(this, "Packet version %d is not supported, sending version negotiation", pversion_cid.version); CID dcid(pversion_cid.dcid, pversion_cid.dcidlen); CID scid(pversion_cid.scid, pversion_cid.scidlen); SendVersionNegotiation(PathDescriptor{pversion_cid.version, dcid, scid, local_address(), remote_address}, now); STAT_INCREMENT(Stats, packets_received); return; } default: // Truly invalid packet — cannot be decoded at all. Debug(this, "Failed to decode packet header, ignoring"); return; } // QUIC currently requires CID lengths of max NGTCP2_MAX_CIDLEN. Ignore any // packet with a non-standard CID length. if (pversion_cid.dcidlen > NGTCP2_MAX_CIDLEN || pversion_cid.scidlen > NGTCP2_MAX_CIDLEN) { Debug(this, "Packet had incorrectly sized CIDs, ignoring"); return; // Ignore the packet! } // Each QUIC peer has two CIDs: The Source Connection ID (or scid), and the // Destination Connection ID (or dcid). For each peer, the dcid is the CID // identifying the other peer, and the scid is the CID identifying itself. // That is, the client's scid is the server dcid; likewise the server's scid // is the client's dcid. // // The dcid and scid below are the values sent from the peer received in the // current packet, so in this case, dcid represents who the peer sent the // packet too (this endpoint) and the scid represents who sent the packet. CID dcid(pversion_cid.dcid, pversion_cid.dcidlen); CID scid(pversion_cid.scid, pversion_cid.scidlen); Debug(this, "Packet dcid %s, scid %s", dcid, scid); // We index the current sessions by the dcid of the client. For initial // packets, the dcid is some random value and the scid is omitted from the // header (it uses what quic calls a "short header"). It is unlikely (but not // impossible) that this randomly selected dcid will be in our index. If we do // happen to have a collision, as unlikely as it is, ngtcp2 will do the right // thing when it tries to process the packet so we really don't have to worry // about it here. If the dcid is not known, the session here will be nullptr. // // When the session is established, this peer will create its own scid and // will send that back to the remote peer to use as the new dcid on // subsequent packets. When that session is added, we will index it by the // local scid, so as long as the client sends the subsequent packets with the // right dcid, everything should just work. auto session = FindSession(dcid); auto addr = local_address(); HandleScope handle_scope(env()->isolate()); // 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 (!session) { // No existing session. Debug(this, "No existing session for dcid %s", dcid); // 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 (pversion_cid.version == 0 && maybeStatelessReset(dcid, scid, data, len, addr, remote_address)) { Debug(this, "Packet was a stateless reset"); return; // Stateless reset! Don't do any further processing. } // If this is a short header packet for an unknown DCID, send a // stateless reset so the peer knows the session is gone. Short header // packets are identified by version == 0 (set by ngtcp2_pkt_decode_ // version_cid). We must NOT use !scid here because long header Initial // packets can have a 0-length SCID (valid per RFC 9000 Section 7.2). if (pversion_cid.version == 0) { Debug(this, "Sending stateless reset for unknown short header packet"); SendStatelessReset( PathDescriptor{ pversion_cid.version, dcid, scid, addr, remote_address}, len, now); return; } // Process the packet as an initial packet... return acceptInitialPacket( pversion_cid.version, dcid, scid, data, len, addr, remote_address); } if (session->is_destroyed()) [[unlikely]] { // The session has been destroyed. Well that's not good. Debug(this, "Session for dcid %s has been destroyed", dcid); return; } // If we got here, the dcid matched the scid of a known local session. Yay! // The session will take over any further processing of the packet. Debug(this, "Dispatching packet to known session"); receive(session.get(), data, len, addr, remote_address, dcid, scid); // It is important to note that the session may have been destroyed during // the call to receive(...). If that's the case, the session object still // exists but it is in a destroyed state. Care should be taken accessing // session after this point. } void Endpoint::PacketDone(int status) { if (is_closed()) return; // At this point we should be waiting on at least one packet. DCHECK_GE(state_->pending_callbacks, 1); state_->pending_callbacks--; env()->DecreaseWaitingRequestCounter(); // Check if we can close or start the idle timer now that this // pending callback has completed. if (state_->closing == 1 || primary_session_count_ == 0) MaybeDestroy(); } bool Endpoint::is_closed() const { return !udp_; } bool Endpoint::is_closing() const { return state_->closing; } bool Endpoint::is_listening() const { return state_->listening; } void Endpoint::MemoryInfo(MemoryTracker* tracker) const { tracker->TrackField("options", options_); tracker->TrackField("packet_pool", packet_pool_); tracker->TrackField("udp", udp_); if (server_state_.has_value()) { tracker->TrackField("server_options", server_state_->options); tracker->TrackField("server_tls_context", server_state_->tls_context); } tracker->TrackField("address LRU", addr_validation_lru_); } // ====================================================================================== // Endpoint::SocketAddressInfoTraits bool Endpoint::SocketAddressInfoTraits::CheckExpired( const SocketAddress& address, const Type& type, uint64_t now) { return (now - type.timestamp) > kSocketAddressInfoTimeout; } void Endpoint::SocketAddressInfoTraits::Touch(const SocketAddress& address, Type* type, uint64_t now) { type->timestamp = now; } // ====================================================================================== // JavaScript call outs void Endpoint::EmitNewSession(const BaseObjectPtr<Session>& session) { if (!env()->can_call_into_js()) return; CallbackScope<Endpoint> scope(this); session->set_wrapped(); Local<Value> arg = session->object(); Debug(this, "Notifying JavaScript about new session"); MakeCallback(BindingData::Get(env()).session_new_callback(), 1, &arg); // It is important to note that the session may have been destroyed during // the call to MakeCallback. If that's the case, the session object still // exists but it is in a destroyed state. Care should be taken accessing // session after this point. // Deliver any stream events that were held until the stream was setup, // e.g. 0-RTT streams from the first flight. if (!session->is_destroyed()) { session->ReplayDeferredEmits(); } } void Endpoint::EmitClose(CloseContext context, int status) { if (!env()->can_call_into_js()) return; CallbackScope<Endpoint> scope(this); auto isolate = env()->isolate(); Local<Value> argv[] = {Integer::New(isolate, static_cast<int>(context)), Integer::New(isolate, static_cast<int>(status))}; Debug(this, "Notifying JavaScript about endpoint closing"); MakeCallback( BindingData::Get(env()).endpoint_close_callback(), arraysize(argv), argv); } // ====================================================================================== // Endpoint JavaScript API JS_METHOD_IMPL(Endpoint::New) { DCHECK(args.IsConstructCall()); auto env = Environment::GetCurrent(args); Options options; // Options::From will validate that args[0] is the correct type. if (!Options::From(env, args[0]).To(&options)) { // There was an error. Just exit to propagate. return; } new Endpoint(env, args.This(), options); } JS_METHOD_IMPL(Endpoint::DoConnect) { auto env = Environment::GetCurrent(args); Endpoint* endpoint; ASSIGN_OR_RETURN_UNWRAP(&endpoint, args.This()); // args[0] is a SocketAddress // args[1] is a Session OptionsObject (see session.cc) // args[2] is an optional SessionTicket DCHECK(SocketAddressBase::HasInstance(env, args[0])); SocketAddressBase* address; ASSIGN_OR_RETURN_UNWRAP(&address, args[0]); THROW_IF_INSUFFICIENT_PERMISSIONS( env, permission::PermissionScope::kNet, address->address()->ToString()); DCHECK(args[1]->IsObject()); Session::Options options; if (!Session::Options::From(env, args[1]).To(&options)) { // There was an error. Return to propagate return; } BaseObjectWeakPtr<Session> session; if (!args[2]->IsUndefined()) { SessionTicket ticket; if (SessionTicket::FromV8Value(env, args[2]).To(&ticket)) { session = endpoint->Connect(*address->address(), options, ticket); } } else { session = endpoint->Connect(*address->address(), options); } if (session) args.GetReturnValue().Set(session->object()); } JS_METHOD_IMPL(Endpoint::DoListen) { Endpoint* endpoint; ASSIGN_OR_RETURN_UNWRAP(&endpoint, args.This()); auto env = Environment::GetCurrent(args); THROW_IF_INSUFFICIENT_PERMISSIONS(env, permission::PermissionScope::kNet, ""); Session::Options options; if (Session::Options::From(env, args[0]).To(&options)) { endpoint->Listen(options); } } JS_METHOD_IMPL(Endpoint::MarkBusy) { Endpoint* endpoint; ASSIGN_OR_RETURN_UNWRAP(&endpoint, args.This()); endpoint->MarkAsBusy(args[0]->IsTrue()); } JS_METHOD_IMPL(Endpoint::DoCloseGracefully) { Endpoint* endpoint; ASSIGN_OR_RETURN_UNWRAP(&endpoint, args.This()); endpoint->CloseGracefully(); } JS_METHOD_IMPL(Endpoint::LocalAddress) { auto env = Environment::GetCurrent(args); Endpoint* endpoint; ASSIGN_OR_RETURN_UNWRAP(&endpoint, args.This()); if (endpoint->is_closed() || !endpoint->udp_.is_bound()) return; auto addr = SocketAddressBase::Create( env, std::make_shared<SocketAddress>(endpoint->local_address())); if (addr) args.GetReturnValue().Set(addr->object()); } JS_METHOD_IMPL(Endpoint::Ref) { Endpoint* endpoint; ASSIGN_OR_RETURN_UNWRAP(&endpoint, args.This()); auto env = Environment::GetCurrent(args); if (args[0]->BooleanValue(env->isolate())) { endpoint->udp_.Ref(); } else { endpoint->udp_.Unref(); } } JS_METHOD_IMPL(Endpoint::DoSetSNIContexts) { auto env = Environment::GetCurrent(args); Endpoint* endpoint; ASSIGN_OR_RETURN_UNWRAP(&endpoint, args.This()); if (!endpoint->server_state_.has_value()) { THROW_ERR_INVALID_STATE(env, "Endpoint is not listening"); return; } if (args.Length() < 1 || !args[0]->IsObject()) { THROW_ERR_INVALID_ARG_TYPE(env, "entries must be an object"); return; } bool replace = args.Length() > 1 && args[1]->IsTrue(); auto entries_obj = args[0].As<Object>(); Local<Array> hostnames; if (!entries_obj->GetOwnPropertyNames(env->context()).ToLocal(&hostnames)) { return; } if (replace) { std::unordered_map<std::string, TLSContext::Options> entries; for (uint32_t i = 0; i < hostnames->Length(); i++) { Local<Value> key; Local<Value> entry_val; if (!hostnames->Get(env->context(), i).ToLocal(&key) || !key->IsString() || !entries_obj->Get(env->context(), key).ToLocal(&entry_val)) { return; } Utf8Value hostname(env->isolate(), key); auto entry_options = TLSContext::Options::From(env, entry_val); if (entry_options.IsNothing()) return; entries[std::string(*hostname, hostname.length())] = entry_options.FromJust(); } if (!endpoint->server_state_->tls_context->SetSNIContexts(env, entries)) { THROW_ERR_INVALID_STATE(env, "Failed to set SNI contexts"); return; } } else { for (uint32_t i = 0; i < hostnames->Length(); i++) { Local<Value> key; Local<Value> entry_val; if (!hostnames->Get(env->context(), i).ToLocal(&key) || !key->IsString() || !entries_obj->Get(env->context(), key).ToLocal(&entry_val)) { return; } Utf8Value hostname(env->isolate(), key); auto entry_options = TLSContext::Options::From(env, entry_val); if (entry_options.IsNothing()) return; if (!endpoint->server_state_->tls_context->AddSNIContext( env, std::string(*hostname, hostname.length()), entry_options.FromJust())) { THROW_ERR_INVALID_STATE( env, "Failed to add SNI context for '%s'", *hostname); return; } } } } } // namespace quic } // namespace node #endif // OPENSSL_NO_QUIC #endif // HAVE_OPENSSL && HAVE_QUIC