mirror of
https://github.com/zebrajr/node.git
synced 2026-01-15 12:15:26 +00:00
quic: add additional QUIC implementation
Adds most of the Endpoint implementation with a few tweaks to other bits. PR-URL: https://github.com/nodejs/node/pull/47603 Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com> Reviewed-By: Tobias Nießen <tniessen@tnie.de>
This commit is contained in:
@@ -60,8 +60,10 @@ namespace node {
|
||||
V(PROCESSWRAP) \
|
||||
V(PROMISE) \
|
||||
V(QUERYWRAP) \
|
||||
V(QUIC_ENDPOINT) \
|
||||
V(QUIC_LOGSTREAM) \
|
||||
V(QUIC_PACKET) \
|
||||
V(QUIC_UDP) \
|
||||
V(SHUTDOWNWRAP) \
|
||||
V(SIGNALWRAP) \
|
||||
V(STATWATCHER) \
|
||||
|
||||
@@ -65,6 +65,7 @@ void BindingData::Initialize(Environment* env, Local<Object> target) {
|
||||
|
||||
void BindingData::RegisterExternalReferences(
|
||||
ExternalReferenceRegistry* registry) {
|
||||
registry->Register(IllegalConstructor);
|
||||
registry->Register(SetCallbacks);
|
||||
registry->Register(FlushPacketFreelist);
|
||||
}
|
||||
@@ -199,6 +200,15 @@ bool NgHttp3CallbackScope::in_nghttp3_callback(Environment* env) {
|
||||
return binding.in_nghttp3_callback_scope;
|
||||
}
|
||||
|
||||
CallbackScopeBase::CallbackScopeBase(Environment* env)
|
||||
: env(env), context_scope(env->context()), try_catch(env->isolate()) {}
|
||||
|
||||
CallbackScopeBase::~CallbackScopeBase() {
|
||||
if (try_catch.HasCaught() && !try_catch.HasTerminated()) {
|
||||
errors::TriggerUncaughtException(env->isolate(), try_catch);
|
||||
}
|
||||
}
|
||||
|
||||
void IllegalConstructor(const FunctionCallbackInfo<Value>& args) {
|
||||
THROW_ERR_ILLEGAL_CONSTRUCTOR(Environment::GetCurrent(args));
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
#include <node.h>
|
||||
#include <node_mem.h>
|
||||
#include <v8.h>
|
||||
#include <limits>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace node {
|
||||
@@ -26,6 +28,9 @@ enum class Side {
|
||||
};
|
||||
|
||||
constexpr size_t kDefaultMaxPacketLength = NGTCP2_MAX_UDP_PAYLOAD_SIZE;
|
||||
constexpr size_t kMaxSizeT = std::numeric_limits<size_t>::max();
|
||||
constexpr uint64_t kMaxSafeJsInteger = 9007199254740991;
|
||||
constexpr auto kSocketAddressInfoTimeout = 60 * NGTCP2_SECONDS;
|
||||
|
||||
// ============================================================================
|
||||
|
||||
@@ -44,7 +49,6 @@ constexpr size_t kDefaultMaxPacketLength = NGTCP2_MAX_UDP_PAYLOAD_SIZE;
|
||||
// internalBinding('quic') is first loaded.
|
||||
#define QUIC_JS_CALLBACKS(V) \
|
||||
V(endpoint_close, EndpointClose) \
|
||||
V(endpoint_error, EndpointError) \
|
||||
V(session_new, SessionNew) \
|
||||
V(session_close, SessionClose) \
|
||||
V(session_error, SessionError) \
|
||||
@@ -66,12 +70,15 @@ constexpr size_t kDefaultMaxPacketLength = NGTCP2_MAX_UDP_PAYLOAD_SIZE;
|
||||
#define QUIC_STRINGS(V) \
|
||||
V(ack_delay_exponent, "ackDelayExponent") \
|
||||
V(active_connection_id_limit, "activeConnectionIDLimit") \
|
||||
V(address_lru_size, "addressLRUSize") \
|
||||
V(alpn, "alpn") \
|
||||
V(ca, "ca") \
|
||||
V(certs, "certs") \
|
||||
V(cc_algorithm, "cc") \
|
||||
V(crl, "crl") \
|
||||
V(ciphers, "ciphers") \
|
||||
V(disable_active_migration, "disableActiveMigration") \
|
||||
V(disable_stateless_reset, "disableStatelessReset") \
|
||||
V(enable_tls_trace, "tlsTrace") \
|
||||
V(endpoint, "Endpoint") \
|
||||
V(endpoint_udp, "Endpoint::UDP") \
|
||||
@@ -84,18 +91,35 @@ constexpr size_t kDefaultMaxPacketLength = NGTCP2_MAX_UDP_PAYLOAD_SIZE;
|
||||
V(initial_max_stream_data_uni, "initialMaxStreamDataUni") \
|
||||
V(initial_max_streams_bidi, "initialMaxStreamsBidi") \
|
||||
V(initial_max_streams_uni, "initialMaxStreamsUni") \
|
||||
V(ipv6_only, "ipv6Only") \
|
||||
V(keylog, "keylog") \
|
||||
V(keys, "keys") \
|
||||
V(logstream, "LogStream") \
|
||||
V(max_ack_delay, "maxAckDelay") \
|
||||
V(max_connections_per_host, "maxConnectionsPerHost") \
|
||||
V(max_connections_total, "maxConnectionsTotal") \
|
||||
V(max_datagram_frame_size, "maxDatagramFrameSize") \
|
||||
V(max_idle_timeout, "maxIdleTimeout") \
|
||||
V(max_payload_size, "maxPayloadSize") \
|
||||
V(max_retries, "maxRetries") \
|
||||
V(max_stateless_resets, "maxStatelessResetsPerHost") \
|
||||
V(packetwrap, "PacketWrap") \
|
||||
V(reject_unauthorized, "rejectUnauthorized") \
|
||||
V(retry_token_expiration, "retryTokenExpiration") \
|
||||
V(request_peer_certificate, "requestPeerCertificate") \
|
||||
V(reset_token_secret, "resetTokenSecret") \
|
||||
V(rx_loss, "rxDiagnosticLoss") \
|
||||
V(session, "Session") \
|
||||
V(session_id_ctx, "sessionIDContext") \
|
||||
V(stream, "Stream") \
|
||||
V(token_expiration, "tokenExpiration") \
|
||||
V(token_secret, "tokenSecret") \
|
||||
V(tx_loss, "txDiagnosticLoss") \
|
||||
V(udp_receive_buffer_size, "udpReceiveBufferSize") \
|
||||
V(udp_send_buffer_size, "udpSendBufferSize") \
|
||||
V(udp_ttl, "udpTTL") \
|
||||
V(unacknowledged_packet_threshold, "unacknowledgedPacketThreshold") \
|
||||
V(validate_address, "validateAddress") \
|
||||
V(verify_hostname_identity, "verifyHostnameIdentity")
|
||||
|
||||
// =============================================================================
|
||||
@@ -133,6 +157,8 @@ class BindingData final
|
||||
|
||||
std::vector<BaseObjectPtr<BaseObject>> packet_freelist;
|
||||
|
||||
std::unordered_map<Endpoint*, BaseObjectPtr<BaseObject>> listening_endpoints;
|
||||
|
||||
// Purge the packet free list to free up memory.
|
||||
static void FlushPacketFreelist(
|
||||
const v8::FunctionCallbackInfo<v8::Value>& args);
|
||||
@@ -203,6 +229,26 @@ struct NgHttp3CallbackScope {
|
||||
static bool in_nghttp3_callback(Environment* env);
|
||||
};
|
||||
|
||||
struct CallbackScopeBase {
|
||||
Environment* env;
|
||||
v8::Context::Scope context_scope;
|
||||
v8::TryCatch try_catch;
|
||||
|
||||
explicit CallbackScopeBase(Environment* env);
|
||||
CallbackScopeBase(const CallbackScopeBase&) = delete;
|
||||
CallbackScopeBase(CallbackScopeBase&&) = delete;
|
||||
CallbackScopeBase& operator=(const CallbackScopeBase&) = delete;
|
||||
CallbackScopeBase& operator=(CallbackScopeBase&&) = delete;
|
||||
~CallbackScopeBase();
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
struct CallbackScope final : public CallbackScopeBase {
|
||||
BaseObjectPtr<T> ref;
|
||||
explicit CallbackScope(const T* ptr)
|
||||
: CallbackScopeBase(ptr->env()), ref(ptr) {}
|
||||
};
|
||||
|
||||
} // namespace quic
|
||||
} // namespace node
|
||||
|
||||
|
||||
@@ -90,13 +90,20 @@ uint64_t GetStat(Stats* stats) {
|
||||
return stats->*member;
|
||||
}
|
||||
|
||||
#define STAT_INCREMENT(Type, name) IncrementStat<Type, &Type::name>(&stats_);
|
||||
#define STAT_INCREMENT(Type, name) \
|
||||
IncrementStat<Type, &Type::name>(stats_.Data());
|
||||
#define STAT_INCREMENT_N(Type, name, amt) \
|
||||
IncrementStat<Type, &Type::name>(&stats_, amt);
|
||||
IncrementStat<Type, &Type::name>(stats_.Data(), amt);
|
||||
#define STAT_RECORD_TIMESTAMP(Type, name) \
|
||||
RecordTimestampStat<Type, &Type::name>(&stats_);
|
||||
#define STAT_SET(Type, name, val) SetStat<Type, &Type::name>(&stats_, val);
|
||||
#define STAT_GET(Type, name) GetStat<Type, &Type::name>(&stats_);
|
||||
RecordTimestampStat<Type, &Type::name>(stats_.Data());
|
||||
#define STAT_SET(Type, name, val) \
|
||||
SetStat<Type, &Type::name>(stats_.Data(), val);
|
||||
#define STAT_GET(Type, name) GetStat<Type, &Type::name>(stats_.Data());
|
||||
#define STAT_FIELD(_, name) uint64_t name;
|
||||
#define STAT_STRUCT(name) \
|
||||
struct Stats final { \
|
||||
name##_STATS(STAT_FIELD) \
|
||||
};
|
||||
|
||||
} // namespace quic
|
||||
} // namespace node
|
||||
|
||||
1343
src/quic/endpoint.cc
Normal file
1343
src/quic/endpoint.cc
Normal file
File diff suppressed because it is too large
Load Diff
487
src/quic/endpoint.h
Normal file
487
src/quic/endpoint.h
Normal file
@@ -0,0 +1,487 @@
|
||||
#pragma once
|
||||
|
||||
#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
|
||||
#if HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC
|
||||
|
||||
#include <aliased_struct.h>
|
||||
#include <async_wrap.h>
|
||||
#include <env.h>
|
||||
#include <node_sockaddr.h>
|
||||
#include <uv.h>
|
||||
#include <v8.h>
|
||||
#include <algorithm>
|
||||
#include <optional>
|
||||
#include "bindingdata.h"
|
||||
#include "defs.h"
|
||||
#include "packet.h"
|
||||
#include "session.h"
|
||||
#include "sessionticket.h"
|
||||
#include "tokens.h"
|
||||
|
||||
namespace node {
|
||||
namespace quic {
|
||||
|
||||
#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(VERSION_NEGOTIATION_COUNT, version_negotiation_count) \
|
||||
V(STATELESS_RESET_COUNT, stateless_reset_count) \
|
||||
V(IMMEDIATE_CLOSE_COUNT, immediate_close_count)
|
||||
|
||||
#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 */ \
|
||||
V(CLOSING, closing, uint8_t) \
|
||||
/* In the process of closing down, waiting for pending send callbacks */ \
|
||||
V(WAITING_FOR_CALLBACKS, waiting_for_callbacks, uint8_t) \
|
||||
/* Temporarily paused serving new initial requests */ \
|
||||
V(BUSY, busy, uint8_t) \
|
||||
/* The number of pending send callbacks */ \
|
||||
V(PENDING_CALLBACKS, pending_callbacks, size_t)
|
||||
|
||||
// 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:
|
||||
STAT_STRUCT(ENDPOINT)
|
||||
|
||||
struct State final {
|
||||
#define V(_, name, type) type name;
|
||||
ENDPOINT_STATE(V)
|
||||
#undef V
|
||||
};
|
||||
|
||||
static constexpr size_t DEFAULT_MAX_CONNECTIONS =
|
||||
std::min<size_t>(kMaxSizeT, static_cast<size_t>(kMaxSafeJsInteger));
|
||||
static constexpr size_t DEFAULT_MAX_CONNECTIONS_PER_HOST = 100;
|
||||
static constexpr size_t DEFAULT_MAX_SOCKETADDRESS_LRU_SIZE =
|
||||
(DEFAULT_MAX_CONNECTIONS_PER_HOST * 10);
|
||||
static constexpr size_t DEFAULT_MAX_STATELESS_RESETS = 10;
|
||||
static constexpr size_t DEFAULT_MAX_RETRY_LIMIT = 10;
|
||||
|
||||
// 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.
|
||||
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 DEFAULT_TOKEN_EXPIRATION *seconds*.
|
||||
uint64_t token_expiration =
|
||||
RegularToken::QUIC_DEFAULT_REGULARTOKEN_EXPIRATION / NGTCP2_SECONDS;
|
||||
|
||||
// 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 max_retries = 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 = kDefaultMaxPacketLength;
|
||||
|
||||
// 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 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
|
||||
|
||||
// There are several common congestion control algorithms that ngtcp2 uses
|
||||
// to determine how it manages the flow control window: RENO, CUBIC, BBR,
|
||||
// and BBR2. 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.
|
||||
TokenSecret reset_token_secret;
|
||||
|
||||
// The secret used for generating new 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;
|
||||
|
||||
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;
|
||||
|
||||
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);
|
||||
};
|
||||
|
||||
bool HasInstance(Environment* env, v8::Local<v8::Value> value);
|
||||
static v8::Local<v8::FunctionTemplate> GetConstructorTemplate(
|
||||
Environment* env);
|
||||
static void Initialize(Environment* env, v8::Local<v8::Object> target);
|
||||
static void RegisterExternalReferences(ExternalReferenceRegistry* registry);
|
||||
|
||||
static BaseObjectPtr<Endpoint> Create(Environment* env,
|
||||
const Endpoint::Options& config);
|
||||
|
||||
Endpoint(Environment* env,
|
||||
v8::Local<v8::Object> object,
|
||||
const Endpoint::Options& options);
|
||||
~Endpoint() override;
|
||||
|
||||
inline const Options& options() const {
|
||||
return options_;
|
||||
}
|
||||
inline const Stats& stats() const {
|
||||
return *stats_.Data();
|
||||
}
|
||||
|
||||
// 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.
|
||||
v8::Maybe<RegularToken> GenerateNewToken(uint32_t version,
|
||||
const SocketAddress& remote_address);
|
||||
|
||||
// Use the endpoint's reset token secret to generate a new stateless reset.
|
||||
v8::Maybe<StatelessResetToken> GenerateNewStatelessResetToken(
|
||||
uint8_t* token, const CID& cid) const;
|
||||
|
||||
void AddSession(const CID& cid, BaseObjectPtr<Session> session);
|
||||
void RemoveSession(const CID& cid);
|
||||
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(BaseObjectPtr<Packet> packet);
|
||||
|
||||
// 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);
|
||||
|
||||
// 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);
|
||||
|
||||
// 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);
|
||||
|
||||
// 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);
|
||||
|
||||
// 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)
|
||||
|
||||
private:
|
||||
#define V(name, _) IDX_STATS_ENDPOINT_##name,
|
||||
enum EndpointStatsIdx { ENDPOINT_STATS(V) IDX_STATS_ENDPOINT_COUNT };
|
||||
#undef V
|
||||
|
||||
#define V(name, key, __) \
|
||||
IDX_STATE_ENDPOINT_##name = OffsetOf(&Endpoint::State::key),
|
||||
enum EndpointStateIdx { ENDPOINT_STATE(V) };
|
||||
#undef V
|
||||
|
||||
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(BaseObjectPtr<Packet> packet);
|
||||
|
||||
// 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;
|
||||
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;
|
||||
|
||||
static void CleanupHook(void* data);
|
||||
|
||||
BaseObjectPtr<Impl> impl_;
|
||||
bool is_bound_ = false;
|
||||
bool is_started_ = false;
|
||||
};
|
||||
|
||||
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 {
|
||||
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 Release();
|
||||
|
||||
void PacketDone(int status) override;
|
||||
|
||||
void EmitNewSession(const BaseObjectPtr<Session>& session);
|
||||
void EmitClose(CloseContext context, int status);
|
||||
|
||||
void IncrementSocketAddressCounter(const SocketAddress& address);
|
||||
void DecrementSocketAddressCounter(const SocketAddress& address);
|
||||
|
||||
// JavaScript API
|
||||
|
||||
// Create a new Endpoint instance. `createEndpoint()` is exposed as a method
|
||||
// on the internalBinding('quic') object.
|
||||
// @param Endpoint::Options options - Options to configure the Endpoint.
|
||||
static void CreateEndpoint(const v8::FunctionCallbackInfo<v8::Value>& args);
|
||||
|
||||
// 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.
|
||||
static void DoConnect(const v8::FunctionCallbackInfo<v8::Value>& args);
|
||||
|
||||
// Start listening as a QUIC server
|
||||
// @param Session::Options options - Options to configure the Session.
|
||||
static void DoListen(const v8::FunctionCallbackInfo<v8::Value>& args);
|
||||
|
||||
// Mark the Endpoint as busy, temporarily pausing handling of new initial
|
||||
// packets.
|
||||
// @param bool on - If true, mark the Endpoint as busy.
|
||||
static void MarkBusy(const v8::FunctionCallbackInfo<v8::Value>& args);
|
||||
|
||||
// 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.
|
||||
static void DoCloseGracefully(
|
||||
const v8::FunctionCallbackInfo<v8::Value>& args);
|
||||
|
||||
// Get the local address of the Endpoint.
|
||||
// @return node::SocketAddress - The local address of the Endpoint.
|
||||
static void LocalAddress(const v8::FunctionCallbackInfo<v8::Value>& args);
|
||||
|
||||
// Ref() causes a listening Endpoint to keep the event loop active.
|
||||
static void Ref(const v8::FunctionCallbackInfo<v8::Value>& args);
|
||||
|
||||
// Unref() allows the event loop to close even if the Endpoint is listening.
|
||||
static void Unref(const v8::FunctionCallbackInfo<v8::Value>& args);
|
||||
|
||||
void Receive(const uv_buf_t& buf, const SocketAddress& from);
|
||||
|
||||
AliasedStruct<Stats> stats_;
|
||||
AliasedStruct<State> state_;
|
||||
const Options options_;
|
||||
UDP udp_;
|
||||
|
||||
// Set if/when the endpoint is configured to listen.
|
||||
std::optional<Session::Options> server_options_{};
|
||||
|
||||
// A Session is generally identified by one or more CIDs. We use two
|
||||
// maps for this rather than one to avoid creating a whole bunch of
|
||||
// BaseObjectPtr references. The primary map (sessions_) just maps
|
||||
// the original CID to the Session, the second map (dcid_to_scid_)
|
||||
// maps the additional CIDs to the the primary.
|
||||
CID::Map<BaseObjectPtr<Session>> sessions_;
|
||||
CID::Map<CID> dcid_to_scid_;
|
||||
StatelessResetToken::Map<Session*> token_map_;
|
||||
|
||||
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<SocketAddressInfoTraits> addrLRU_;
|
||||
|
||||
CloseContext close_context_ = CloseContext::CLOSE;
|
||||
int close_status_ = 0;
|
||||
|
||||
friend class UDP;
|
||||
friend class Packet;
|
||||
};
|
||||
|
||||
} // namespace quic
|
||||
} // namespace node
|
||||
|
||||
#endif // HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC
|
||||
#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
|
||||
@@ -286,7 +286,6 @@ BaseObjectPtr<Packet> Packet::CreateConnectionClosePacket(
|
||||
BaseObjectPtr<Packet> Packet::CreateImmediateConnectionClosePacket(
|
||||
Environment* env,
|
||||
Listener* listener,
|
||||
const SocketAddress& destination,
|
||||
const PathDescriptor& path_descriptor,
|
||||
const QuicError& reason) {
|
||||
auto packet = Packet::Create(env,
|
||||
|
||||
@@ -130,7 +130,6 @@ class Packet final : public ReqWrap<uv_udp_send_t> {
|
||||
static BaseObjectPtr<Packet> CreateImmediateConnectionClosePacket(
|
||||
Environment* env,
|
||||
Listener* listener,
|
||||
const SocketAddress& destination,
|
||||
const PathDescriptor& path_descriptor,
|
||||
const QuicError& reason);
|
||||
|
||||
@@ -146,15 +145,15 @@ class Packet final : public ReqWrap<uv_udp_send_t> {
|
||||
Listener* listener,
|
||||
const PathDescriptor& path_descriptor);
|
||||
|
||||
// Called when the packet is done being sent.
|
||||
void Done(int status);
|
||||
|
||||
private:
|
||||
static BaseObjectPtr<Packet> FromFreeList(Environment* env,
|
||||
std::shared_ptr<Data> data,
|
||||
Listener* listener,
|
||||
const SocketAddress& destination);
|
||||
|
||||
// Called when the packet is done being sent.
|
||||
void Done(int status);
|
||||
|
||||
Listener* listener_;
|
||||
SocketAddress destination_;
|
||||
std::shared_ptr<Data> data_;
|
||||
|
||||
94
src/quic/session.h
Normal file
94
src/quic/session.h
Normal file
@@ -0,0 +1,94 @@
|
||||
#pragma once
|
||||
|
||||
#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
|
||||
#if HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC
|
||||
|
||||
#include <async_wrap.h>
|
||||
#include <memory_tracker.h>
|
||||
#include <node_sockaddr.h>
|
||||
#include <optional>
|
||||
#include "bindingdata.h"
|
||||
#include "cid.h"
|
||||
#include "data.h"
|
||||
|
||||
namespace node {
|
||||
namespace quic {
|
||||
|
||||
class Endpoint;
|
||||
|
||||
// TODO(@jasnell): This is a placeholder definition of Session that
|
||||
// includes only the pieces needed by Endpoint right now. The full
|
||||
// Session definition will be provided separately.
|
||||
class Session final : public AsyncWrap {
|
||||
public:
|
||||
struct Config {
|
||||
SocketAddress local_address;
|
||||
SocketAddress remote_address;
|
||||
std::optional<CID> ocid = std::nullopt;
|
||||
std::optional<CID> retry_scid = std::nullopt;
|
||||
|
||||
Config(Side side,
|
||||
const Endpoint& endpoint,
|
||||
const CID& scid,
|
||||
const SocketAddress& local_address,
|
||||
const SocketAddress& remote_address,
|
||||
uint32_t min_quic_version,
|
||||
uint32_t max_quic_version,
|
||||
const CID& ocid = CID::kInvalid);
|
||||
};
|
||||
struct Options : public MemoryRetainer {
|
||||
SET_NO_MEMORY_INFO()
|
||||
SET_MEMORY_INFO_NAME(Session::Options)
|
||||
SET_SELF_SIZE(Options)
|
||||
|
||||
static v8::Maybe<Options> From(Environment* env,
|
||||
v8::Local<v8::Value> value);
|
||||
};
|
||||
|
||||
static BaseObjectPtr<Session> Create(BaseObjectPtr<Endpoint> endpoint,
|
||||
const Config& config,
|
||||
const Options& options);
|
||||
|
||||
enum class CloseMethod {
|
||||
// Roundtrip through JavaScript, causing all currently opened streams
|
||||
// to be closed. An attempt will be made to send a CONNECTION_CLOSE
|
||||
// frame to the peer. If closing while within the ngtcp2 callback scope,
|
||||
// sending the CONNECTION_CLOSE will be deferred until the scope exits.
|
||||
DEFAULT,
|
||||
// The connected peer will not be notified.
|
||||
SILENT,
|
||||
// Closing gracefully 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 Session will be immediately closed once there are no
|
||||
// remaining streams. No notification is given to the connected peer that we
|
||||
// are in a graceful closing state. A CONNECTION_CLOSE will be sent only
|
||||
// once
|
||||
// Close() is called.
|
||||
GRACEFUL
|
||||
};
|
||||
|
||||
Session(Environment* env, v8::Local<v8::Object> object);
|
||||
|
||||
void Close(CloseMethod method = CloseMethod::DEFAULT);
|
||||
bool Receive(Store&& store,
|
||||
const SocketAddress& local_address,
|
||||
const SocketAddress& remote_address);
|
||||
|
||||
bool is_destroyed() const;
|
||||
bool is_server() const;
|
||||
// The session is "wrapped" if it has been passed out to JavaScript
|
||||
// via the New Session event or returned by the connect method.
|
||||
void set_wrapped();
|
||||
SocketAddress remote_address() const;
|
||||
|
||||
SET_NO_MEMORY_INFO()
|
||||
SET_MEMORY_INFO_NAME(Session)
|
||||
SET_SELF_SIZE(Session)
|
||||
};
|
||||
|
||||
} // namespace quic
|
||||
} // namespace node
|
||||
|
||||
#endif // HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC
|
||||
#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
|
||||
@@ -1,9 +1,6 @@
|
||||
#if HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC
|
||||
|
||||
#include "tlscontext.h"
|
||||
#include "bindingdata.h"
|
||||
#include "defs.h"
|
||||
#include "transportparams.h"
|
||||
#include <base_object-inl.h>
|
||||
#include <env-inl.h>
|
||||
#include <memory_tracker-inl.h>
|
||||
@@ -12,6 +9,9 @@
|
||||
#include <ngtcp2/ngtcp2_crypto_openssl.h>
|
||||
#include <openssl/ssl.h>
|
||||
#include <v8.h>
|
||||
#include "bindingdata.h"
|
||||
#include "defs.h"
|
||||
#include "transportparams.h"
|
||||
|
||||
namespace node {
|
||||
|
||||
@@ -555,6 +555,7 @@ ngtcp2_conn* TLSContext::getConnection(ngtcp2_crypto_conn_ref* ref) {
|
||||
Maybe<const TLSContext::Options> TLSContext::Options::From(Environment* env,
|
||||
Local<Value> value) {
|
||||
if (value.IsEmpty() || !value->IsObject()) {
|
||||
THROW_ERR_INVALID_ARG_TYPE(env, "options must be an object");
|
||||
return Nothing<const Options>();
|
||||
}
|
||||
|
||||
|
||||
@@ -68,6 +68,8 @@ const { getSystemErrorName } = require('util');
|
||||
delete providers.CHECKPRIMEREQUEST;
|
||||
delete providers.QUIC_LOGSTREAM;
|
||||
delete providers.QUIC_PACKET;
|
||||
delete providers.QUIC_UDP;
|
||||
delete providers.QUIC_ENDPOINT;
|
||||
|
||||
const objKeys = Object.keys(providers);
|
||||
if (objKeys.length > 0)
|
||||
|
||||
Reference in New Issue
Block a user