/
githubmirror
/
node
Обзор
Документация
Войти
/
githubmirror
/
node
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
src/quic/endpoint.h
519 строк
21 KB
James M Snell
quic: add block list support for endpoints
25 май 2026, 05:14
25 май 2026, 05:14
59a4ec1
Код
Авторство
О чём код?
#pragma once #if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS #include <aliased_struct.h> #include <async_wrap.h> #include <env.h> #include <node_sockaddr.h> #include <timer_wrap.h> #include <uv.h> #include <v8.h> #include <algorithm> #include <optional> #include "arena.h" #include "bindingdata.h" #include "packet.h" #include "session.h" #include "session_manager.h" #include "sessionticket.h" #include "tokens.h" namespace node::quic { // An Endpoint encapsulates the UDP local port binding and is responsible for // sending and receiving QUIC packets. A single endpoint can act as both a QUIC // client and server simultaneously. class Endpoint final : public AsyncWrap, public Packet::Listener { public: // The socket address LRU is used for tracking validated remote addresses. static constexpr uint64_t DEFAULT_MAX_SOCKETADDRESS_LRU_SIZE = 1024; // Default rate limits for stateless responses. These are global token // bucket limits that cap the total rate of each response type regardless // of source address. This prevents spoofed-source floods from bypassing // per-host limits (which are keyed by source IP and trivially defeated // by rotating spoofed addresses). The rate is in responses per second // and the burst is the maximum tokens the bucket can hold. static constexpr double DEFAULT_RETRY_RATE = 100; static constexpr double DEFAULT_RETRY_BURST = 200; static constexpr double DEFAULT_STATELESS_RESET_RATE = 100; static constexpr double DEFAULT_STATELESS_RESET_BURST = 200; static constexpr double DEFAULT_VERSION_NEGOTIATION_RATE = 100; static constexpr double DEFAULT_VERSION_NEGOTIATION_BURST = 200; static constexpr double DEFAULT_IMMEDIATE_CLOSE_RATE = 100; static constexpr double DEFAULT_IMMEDIATE_CLOSE_BURST = 200; // Per-host session creation rate limit. This is tracked per validated // remote address in the address LRU, preventing a single source from // churning through sessions faster than the server can handle. Unlike // the global stateless response buckets, this only applies after address // validation (spoofed sources can't reach this path). static constexpr double DEFAULT_SESSION_CREATION_RATE = 50; static constexpr double DEFAULT_SESSION_CREATION_BURST = 100; // Endpoint configuration options struct Options 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<SocketAddress> local_address; // Retry tokens issued by the Endpoint are time-limited. By default, retry // tokens expire after DEFAULT_RETRYTOKEN_EXPIRATION *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 = RetryToken::QUIC_DEFAULT_RETRYTOKEN_EXPIRATION / NGTCP2_SECONDS; // Tokens issued using NEW_TOKEN are time-limited. By default, tokens expire // after QUIC_DEFAULT_REGULARTOKEN_EXPIRATION *seconds*. uint64_t token_expiration = RegularToken::QUIC_DEFAULT_REGULARTOKEN_EXPIRATION / NGTCP2_SECONDS; // For tracking the path verification status of remote hosts, we maintain // an LRU cache of the most recently seen hosts. uint64_t address_lru_size = DEFAULT_MAX_SOCKETADDRESS_LRU_SIZE; // Global token bucket rate limits for stateless responses. These cap // the total rate of each response type regardless of source address, // preventing spoofed-source floods. Rate is in responses per second, // burst is the maximum number of responses that can be sent in a burst. double retry_rate = DEFAULT_RETRY_RATE; double retry_burst = DEFAULT_RETRY_BURST; double stateless_reset_rate = DEFAULT_STATELESS_RESET_RATE; double stateless_reset_burst = DEFAULT_STATELESS_RESET_BURST; double version_negotiation_rate = DEFAULT_VERSION_NEGOTIATION_RATE; double version_negotiation_burst = DEFAULT_VERSION_NEGOTIATION_BURST; double immediate_close_rate = DEFAULT_IMMEDIATE_CLOSE_RATE; double immediate_close_burst = DEFAULT_IMMEDIATE_CLOSE_BURST; // Per-host session creation rate limit. Tracked per validated remote // address in the address LRU. Set to high values for benchmarking // where traffic comes from a single source. double session_creation_rate = DEFAULT_SESSION_CREATION_RATE; double session_creation_burst = DEFAULT_SESSION_CREATION_BURST; // 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; #ifdef DEBUG // 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; #endif // DEBUG // By default, when the endpoint is created, 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. This means that the endpoint // configuration really should have a reset token secret passed in. TokenSecret reset_token_secret; // The secret used for generating new regular tokens. TokenSecret token_secret; // 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; // When true, multiple endpoints (across separate processes) can bind to // the same address:port and the kernel will load-balance incoming UDP // datagrams across them. This sets the UV_UDP_REUSEPORT flag on the // underlying uv_udp_t. Supported on Linux 3.9+ and DragonFlyBSD 3.6+. bool reuse_port = 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; // When an endpoint becomes idle (not listening and no primary sessions), // it will be destroyed after this many seconds. A value of 0 means // destroy immediately when idle (default, preserves pre-SessionManager // behavior). A positive value keeps the endpoint alive for potential // reuse by future connect() or listen() calls. static constexpr uint64_t DEFAULT_IDLE_TIMEOUT = 0; uint64_t idle_timeout = DEFAULT_IDLE_TIMEOUT; // Optional block list for filtering incoming packets by source address. // When block_list_policy is DENY, packets from addresses matching the // block list are dropped. When ALLOW, only packets from addresses // matching the block list are accepted (all others dropped). enum class BlockListPolicy : uint8_t { DENY, // Drop packets from matching addresses (blocklist) ALLOW, // Drop packets from non-matching addresses (allowlist) }; std::shared_ptr<SocketAddressBlockList> block_list; BlockListPolicy block_list_policy = BlockListPolicy::DENY; void MemoryInfo(MemoryTracker* tracker) const override; SET_MEMORY_INFO_NAME(Endpoint::Config) SET_SELF_SIZE(Options) static v8::Maybe<Options> From(Environment* env, v8::Local<v8::Value> value); std::string ToString() const; }; JS_CONSTRUCTOR(Endpoint); JS_BINDING_INIT_BOILERPLATE(); Endpoint(Environment* env, v8::Local<v8::Object> object, const Endpoint::Options& options); inline operator Packet::Listener*() { return this; } inline const Options& options() const { return options_; } // While the busy flag is set, the Endpoint will reject all initial packets // with a SERVER_BUSY response. This allows us to build a circuit breaker // directly into the implementation, explicitly signaling that the server is // blocked when activity is too high. void MarkAsBusy(bool on = true); // Use the endpoint's token secret to generate a new token. RegularToken GenerateNewToken(uint32_t version, const SocketAddress& remote_address); // Use the endpoint's reset token secret to generate a new stateless reset. StatelessResetToken GenerateNewStatelessResetToken(uint8_t* token, const CID& cid) const; void AddSession(const CID& cid, BaseObjectPtr<Session> session); void RemoveSession(const CID& cid, const SocketAddress& remote_address); BaseObjectPtr<Session> FindSession(const CID& cid); // A single session may be associated with multiple CIDs. // AssociateCID registers the mapping both in the Endpoint and the inner // Endpoint. void AssociateCID(const CID& cid, const CID& scid); void DisassociateCID(const CID& cid); // Associates a given stateless reset token with the session. This allows // stateless reset tokens to be recognized and dispatched to the proper // Endpoint and Session for processing. void AssociateStatelessResetToken(const StatelessResetToken& token, Session* session); void DisassociateStatelessResetToken(const StatelessResetToken& token); void Send(Packet::Ptr packet); // Attempt synchronous send via uv_udp_try_send. If the socket is // writable, the packet is sent immediately and the Ptr is released. // If the socket is not writable (UV_EAGAIN), falls back to the // async Send path. Used by the deferred flush callback to avoid // the one-tick latency of async uv_udp_send. void SendOrTrySend(Packet::Ptr packet); // Send a batch of packets using uv_udp_try_send2 (sendmmsg) for // synchronous batched delivery. Packets successfully sent are released // immediately. On EAGAIN or partial send, remaining packets fall back // to async uv_udp_send. The Packet::Ptr array is consumed: all entries // will be empty (released or moved) on return. void SendBatch(Packet::Ptr* packets, size_t count); // Acquire a Packet from the pool. length sets the initial working // size (must be <= pool capacity). The slot is always allocated at // full capacity to avoid fragmentation. Packet::Ptr CreatePacket(const SocketAddress& destination, size_t length = kDefaultMaxPacketLength, const char* diagnostic_label = nullptr); // 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. void SendRetry(const PathDescriptor& options, uint64_t now); // Sends a version negotiation packet. This is terminal for the connection and // is sent only when a QUIC packet is received for an unsupported QUIC // version. It is possible that a malicious packet triggered this so we need // to be careful not to commit too many resources. void SendVersionNegotiation(const PathDescriptor& options, uint64_t now); // 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 PathDescriptor& options, size_t source_len, uint64_t now); // 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 SendImmediateConnectionClose(const PathDescriptor& options, QuicError error, uint64_t now); // Listen for connections (act as a server). void Listen(const Session::Options& options); // Create a new client-side Session. BaseObjectPtr<Session> Connect( const SocketAddress& remote_address, const Session::Options& options, std::optional<SessionTicket> sessionTicket = std::nullopt); // Returns the local address only if the endpoint has been bound. Before // the endpoint is bound, or after it is closed, this will abort due to // a failed check so it is important to check `is_closed()` before calling. SocketAddress local_address() const; void MemoryInfo(MemoryTracker* tracker) const override; SET_MEMORY_INFO_NAME(Endpoint) SET_SELF_SIZE(Endpoint) struct Stats; struct State; private: class UDP final : public MemoryRetainer { public: explicit UDP(Endpoint* endpoint); ~UDP() override; int Bind(const Endpoint::Options& config); int Start(); void Stop(); void Close(); int Send(Packet::Ptr packet); // Synchronous send using uv_udp_try_send. Returns the number of // bytes sent on success, UV_EAGAIN if the socket is not writable // or the send queue is non-empty, or another negative error code. // The Ptr is not consumed — the caller manages the lifecycle. int TrySend(const Packet::Ptr& packet); // Synchronous batched send using uv_udp_try_send2 (sendmmsg). // Takes pre-built libuv argument arrays. Returns the number of // messages successfully sent (>= 0), or a negative error code. int TrySendBatch(uv_buf_t* bufs[], unsigned int nbufs[], struct sockaddr* addrs[], size_t count); // Returns the local UDP socket address to which we are bound, // or fail with an assert if we are not bound. SocketAddress local_address() const; bool is_bound() const; bool is_closed() const; bool is_closed_or_closing() const; operator bool() const; void Ref(); void Unref(); void MemoryInfo(node::MemoryTracker* tracker) const override; SET_MEMORY_INFO_NAME(Endpoint::UDP) SET_SELF_SIZE(UDP) private: class Impl; BaseObjectWeakPtr<Impl> impl_; struct Flags { uint8_t is_bound : 1 = 0; uint8_t is_started : 1 = 0; uint8_t is_closed : 1 = 0; }; Flags flags_; }; bool is_closed() const; bool is_closing() const; bool is_listening() const; bool Start(); // Destroy the endpoint if... // * There are no sessions, // * There are no sent packets with pending done callbacks, and // * We're not listening for new initial packets. void MaybeDestroy(); // Specifies the general reason the endpoint is being destroyed. enum class CloseContext : uint8_t { CLOSE, BIND_FAILURE, START_FAILURE, RECEIVE_FAILURE, SEND_FAILURE, LISTEN_FAILURE, }; void Destroy(CloseContext context = CloseContext::CLOSE, int status = 0); // A graceful close will destroy the endpoint once all existing sessions // have ended normally. Creating new sessions (inbound or outbound) will // be prevented. void CloseGracefully(); void PacketDone(int status) override; void EmitNewSession(const BaseObjectPtr<Session>& session); void EmitClose(CloseContext context, int status); // JavaScript API // Create a new Endpoint. // @param Endpoint::Options options - Options to configure the Endpoint. JS_METHOD(New); // Methods on the Endpoint instance: // Create a new client Session on this endpoint. // @param node::SocketAddress local_address - The local address to bind to. // @param Session::Options options - Options to configure the Session. // @param v8::ArrayBufferView session_ticket - The session ticket to use for // the Session. // @param v8::ArrayBufferView remote_transport_params - The remote transport // params. JS_METHOD(DoConnect); // Start listening as a QUIC server // @param Session::Options options - Options to configure the Session. JS_METHOD(DoListen); // Mark the Endpoint as busy, temporarily pausing handling of new initial // packets. // @param bool on - If true, mark the Endpoint as busy. JS_METHOD(MarkBusy); // DoCloseGracefully is the signal that endpoint should close. Any packets // that are already in the queue or in flight will be allowed to finish, but // the EndpoingWrap will be otherwise no longer able to receive or send // packets. JS_METHOD(DoCloseGracefully); JS_METHOD(DoSetSNIContexts); // Get the local address of the Endpoint. // @return node::SocketAddress - The local address of the Endpoint. JS_METHOD(LocalAddress); // Ref() causes a listening Endpoint to keep the event loop active. JS_METHOD(Ref); void Receive(const uint8_t* data, size_t len, const SocketAddress& from); AliasedStruct<Stats> stats_; AliasedStruct<State> state_; const Options options_; ArenaPool<Packet> packet_pool_; UDP udp_; // Idle timer: started when the endpoint becomes idle (not listening, // no primary sessions). When it fires, the endpoint is destroyed. // Stopped when a new session is added or listening begins. TimerWrapHandle idle_timer_; struct ServerState { Session::Options options; std::shared_ptr<TLSContext> tls_context; }; // Set if/when the endpoint is configured to listen. std::optional<ServerState> server_state_ = std::nullopt; // Count of sessions for which this endpoint is the primary endpoint. // Drives ref/unref and idle timer logic. The actual session-to-endpoint // mapping is maintained by the SessionManager. size_t primary_session_count_ = 0; // Per-endpoint CID -> SCID mapping for peer-chosen CIDs from connection // establishment (config.dcid, config.ocid). These are kept per-endpoint // because peer-chosen values can collide across endpoints (e.g., a // client's random outgoing DCID matching an incoming DCID on the server // endpoint). Locally-generated CIDs that need cross-endpoint routing // (preferred address, multipath) go in SessionManager::dcid_to_scid_. // // Endpoint::FindSession does a three-tier lookup: // 1. SessionManager::sessions_[cid] (direct SCID match) // 2. SessionManager::dcid_to_scid_[cid] (cross-endpoint CID) // 3. Endpoint::dcid_to_scid_[cid] (peer-chosen CID) // Each tier resolves to an SCID and looks up SessionManager::sessions_. CID::Map<CID> dcid_to_scid_; SessionManager& session_manager() const; struct SocketAddressInfoTraits final { struct Type final { uint64_t timestamp; bool validated; TokenBucket session_creation_bucket; }; static bool CheckExpired(const SocketAddress& address, const Type& type, uint64_t now); static void Touch(const SocketAddress& address, Type* type, uint64_t now); }; SocketAddressLRU<SocketAddressInfoTraits> addr_validation_lru_; // Global token buckets for stateless response rate limiting. // These cap the total server-wide rate of each response type, // regardless of source address. TokenBucket retry_bucket_; TokenBucket stateless_reset_bucket_; TokenBucket version_negotiation_bucket_; TokenBucket immediate_close_bucket_; // Per-IP connection counts for maxConnectionsPerHost enforcement. // Only populated when max_connections_per_host > 0. Entries are // added in AddSession and removed when the count reaches 0 in // RemoveSession. The map size is bounded by the number of active // sessions (each entry has count >= 1). SocketAddress::IpMap<uint16_t> conn_counts_per_host_; CloseContext close_context_ = CloseContext::CLOSE; int close_status_ = 0; friend class UDP; friend class Packet; friend class Session; }; } // namespace node::quic #endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS