#pragma once #if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS #if HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC #include #include #include #include #include #include #include #include #include #include #include "bindingdata.h" #include "cid.h" #include "data.h" #include "defs.h" #include "logstream.h" #include "packet.h" #include "preferredaddress.h" #include "sessionticket.h" #include "streams.h" #include "tlscontext.h" #include "transportparams.h" namespace node::quic { class Endpoint; // A Session represents one half of a persistent connection between two QUIC // peers. Every Session is established first by performing a TLS handshake in // which the client sends an initial packet to the server containing a TLS // client hello. Once the TLS handshake has been completed, the Session can be // used to open one or more Streams for the actual data flow back and forth. // // While client and server Sessions are created in slightly different ways, // their lifecycles are generally identical: // // A Session is either acting as a Client or as a Server. // // Client Sessions are always created using Endpoint::Connect() // // Server Sessions are always created by an Endpoint receiving a valid initial // request received from a remote client. // // As soon as Sessions of either type are created, they will immediately start // working through the TLS handshake to establish the crypographic keys used to // secure the communication. Once those keys are established, the Session can be // used to open Streams. Based on how the Session is configured, any number of // Streams can exist concurrently on a single Session. // // The Session wraps an ngtcp2_conn that is initialized when the session object // is created. This ngtcp2_conn is destroyed when the session object is freed. // However, the session can be in a closed/destroyed state and still have a // valid ngtcp2_conn pointer. This is important because the ngtcp2 still might // be processing data within the scope of an ngtcp2_conn after the session // object itself is closed/destroyed by user code. class Session final : public AsyncWrap, private SessionTicket::AppData::Source { public: // For simplicity, we use the same Application::Options struct for all // Application types. This may change in the future. Not all of the options // are going to be relevant for all Application types. struct Application_Options final : public MemoryRetainer { // The maximum number of header pairs permitted for a Stream. // Only relevant if the selected application supports headers. uint64_t max_header_pairs = DEFAULT_MAX_HEADER_LIST_PAIRS; // The maximum total number of header bytes (including header // name and value) permitted for a Stream. // Only relevant if the selected application supports headers. uint64_t max_header_length = DEFAULT_MAX_HEADER_LENGTH; // HTTP/3 specific options. uint64_t max_field_section_size = 0; uint64_t qpack_max_dtable_capacity = 0; uint64_t qpack_encoder_max_dtable_capacity = 0; uint64_t qpack_blocked_streams = 0; bool enable_connect_protocol = true; bool enable_datagrams = true; operator const nghttp3_settings() const; SET_NO_MEMORY_INFO() SET_MEMORY_INFO_NAME(Application::Options) SET_SELF_SIZE(Options) static v8::Maybe From(Environment* env, v8::Local value); std::string ToString() const; static const Application_Options kDefault; }; // An Application implements the ALPN-protocol specific semantics on behalf // of a QUIC Session. class Application; // The ApplicationProvider optionally supplies the underlying application // protocol handler used by a session. The ApplicationProvider is supplied // in the *internal* options (that is, it is not exposed as a public, user // facing API. If the ApplicationProvider is not specified, then the // DefaultApplication is used (see application.cc). class ApplicationProvider : public BaseObject { public: using BaseObject::BaseObject; virtual std::unique_ptr Create(Session* session) = 0; }; // The options used to configure a session. Most of these deal directly with // the transport parameters that are exchanged with the remote peer during // handshake. struct Options final : public MemoryRetainer { // The QUIC protocol version requested for the session. uint32_t version = NGTCP2_PROTO_VER_MAX; // Te minimum QUIC protocol version supported by this session. uint32_t min_version = NGTCP2_PROTO_VER_MIN; // By default a client session will ignore the preferred address // advertised by the the server. This option is only relevant for // client sessions. PreferredAddress::Policy preferred_address_strategy = PreferredAddress::Policy::IGNORE_PREFERRED; TransportParams::Options transport_params = TransportParams::Options::kDefault; TLSContext::Options tls_options = TLSContext::Options::kDefault; // A reference to the CID::Factory used to generate CID instances // for this session. const CID::Factory* cid_factory = &CID::Factory::random(); // If the CID::Factory is a base object, we keep a reference to it // so that it cannot be garbage collected. BaseObjectPtr cid_factory_ref; // If the application provider is specified, it will be used to create // the underlying Application instance for the session. BaseObjectPtr application_provider; // When true, QLog output will be enabled for the session. bool qlog = false; // The amount of time (in milliseconds) that the endpoint will wait for the // completion of the tls handshake. uint64_t handshake_timeout = UINT64_MAX; // Maximum initial flow control window size for a stream. uint64_t max_stream_window = 0; // Maximum initial flow control window size for the connection. uint64_t max_window = 0; // 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; // There are several common congestion control algorithms that ngtcp2 uses // to determine how it manages the flow control window: RENO, CUBIC, and // BBR. 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 = CC_ALGO_CUBIC; void MemoryInfo(MemoryTracker* tracker) const override; SET_MEMORY_INFO_NAME(Session::Options) SET_SELF_SIZE(Options) static v8::Maybe From(Environment* env, v8::Local value); std::string ToString() const; }; // The additional configuration settings used to create a specific session. // while the Options above can be used to configure multiple sessions, a // single Config is used to create a single session, which is why they are // kept separate. struct Config final : MemoryRetainer { // Is the Session acting as a client or a server? Side side; // The options to use for this session. Options options; // The actual QUIC version identified for this session. uint32_t version; SocketAddress local_address; SocketAddress remote_address; // The destination CID, identifying the remote peer. This value is always // provided by the remote peer. CID dcid = CID::kInvalid; // The source CID, identifying this session. This value is always created // locally. CID scid = CID::kInvalid; // Used only by client sessions to identify the original DCID // used to initiate the connection. CID ocid = CID::kInvalid; CID retry_scid = CID::kInvalid; CID preferred_address_cid = CID::kInvalid; ngtcp2_settings settings = {}; operator ngtcp2_settings*() { return &settings; } operator const ngtcp2_settings*() const { return &settings; } Config(Environment* env, Side side, const Options& options, uint32_t version, const SocketAddress& local_address, const SocketAddress& remote_address, const CID& dcid, const CID& scid, const CID& ocid = CID::kInvalid); Config(Environment* env, const Options& options, const SocketAddress& local_address, const SocketAddress& remote_address, const CID& ocid = CID::kInvalid); void set_token(const uint8_t* token, size_t len, ngtcp2_token_type type = NGTCP2_TOKEN_TYPE_UNKNOWN); void set_token(const RetryToken& token); void set_token(const RegularToken& token); void MemoryInfo(MemoryTracker* tracker) const override; SET_MEMORY_INFO_NAME(Session::Config) SET_SELF_SIZE(Config) std::string ToString() const; }; static bool HasInstance(Environment* env, v8::Local value); static v8::Local GetConstructorTemplate( Environment* env); static void InitPerIsolate(IsolateData* isolate_data, v8::Local target); static void InitPerContext(Realm* env, v8::Local target); static void RegisterExternalReferences(ExternalReferenceRegistry* registry); static BaseObjectPtr Create( Endpoint* endpoint, const Config& config, TLSContext* tls_context, const std::optional& ticket); // Really should be private but MakeDetachedBaseObject needs visibility. Session(Endpoint* endpoint, v8::Local object, const Config& config, TLSContext* tls_context, const std::optional& ticket); DISALLOW_COPY_AND_MOVE(Session) ~Session() override; bool is_destroyed() const; bool is_server() const; uint32_t version() const; Endpoint& endpoint() const; TLSSession& tls_session() const; Application& application() const; const Config& config() const; const Options& options() const; const SocketAddress& remote_address() const; const SocketAddress& local_address() const; std::string diagnostic_name() const override; void MemoryInfo(MemoryTracker* tracker) const override; SET_MEMORY_INFO_NAME(Session) SET_SELF_SIZE(Session) operator ngtcp2_conn*() const; // Ensures that the session/application sends pending data when the scope // exits. Scopes can be nested. When nested, pending data will be sent // only when the outermost scope is exited. struct SendPendingDataScope final { Session* session; explicit SendPendingDataScope(Session* session); explicit SendPendingDataScope(const BaseObjectPtr& session); ~SendPendingDataScope(); DISALLOW_COPY_AND_MOVE(SendPendingDataScope) }; struct State; struct Stats; void HandleQlog(uint32_t flags, const void* data, size_t len); private: struct Impl; using StreamsMap = std::unordered_map>; using QuicConnectionPointer = DeleteFnPtr; struct PathValidationFlags final { bool preferredAddress = false; }; struct DatagramReceivedFlags final { bool early = false; }; bool Receive(Store&& store, const SocketAddress& local_address, const SocketAddress& remote_address); void Send(const BaseObjectPtr& packet); void Send(const BaseObjectPtr& packet, const PathStorage& path); uint64_t SendDatagram(Store&& data); // A non-const variation to allow certain modifications. Config& config(); enum class CreateStreamOption { NOTIFY, DO_NOT_NOTIFY, }; BaseObjectPtr FindStream(int64_t id) const; BaseObjectPtr CreateStream( int64_t id, CreateStreamOption option = CreateStreamOption::NOTIFY, std::shared_ptr data_source = nullptr); void AddStream(BaseObjectPtr stream, CreateStreamOption option = CreateStreamOption::NOTIFY); void RemoveStream(int64_t id); void ResumeStream(int64_t id); void StreamDataBlocked(int64_t id); void ShutdownStream(int64_t id, QuicError error = QuicError()); void ShutdownStreamWrite(int64_t id, QuicError code = QuicError()); // Use the configured CID::Factory to generate a new CID. CID new_cid(size_t len = CID::kMaxLength) const; const TransportParams local_transport_params() const; const TransportParams remote_transport_params() const; bool is_destroyed_or_closing() const; size_t max_packet_size() const; void set_priority_supported(bool on = true); // Open a new locally-initialized stream with the specified directionality. // If the session is not yet in a state where the stream can be openen -- // such as when the handshake is not yet sufficiently far along and ORTT // session resumption is not being used -- then the stream will be created // in a pending state where actually opening the stream will be deferred. v8::MaybeLocal OpenStream( Direction direction, std::shared_ptr data_source = nullptr); void ExtendStreamOffset(int64_t id, size_t amount); void ExtendOffset(size_t amount); void SetLastError(QuicError&& error); uint64_t max_data_left() const; PendingStream::PendingStreamQueue& pending_bidi_stream_queue() const; PendingStream::PendingStreamQueue& pending_uni_stream_queue() const; // Implementation of SessionTicket::AppData::Source void CollectSessionTicketAppData( SessionTicket::AppData* app_data) const override; SessionTicket::AppData::Status ExtractSessionTicketAppData( const SessionTicket::AppData& app_data, SessionTicket::AppData::Source::Flag flag) override; // Returns true if the Session has entered the closing period after sending a // CONNECTION_CLOSE. While true, the Session is only permitted to transmit // CONNECTION_CLOSE frames until either the idle timeout period elapses or // until the Session is explicitly destroyed. bool is_in_closing_period() const; // Returns true if the Session has received a CONNECTION_CLOSE frame from the // peer. Once in the draining period, the Session is not permitted to send any // frames to the peer. The Session will be silently closed after either the // idle timeout period elapses or until the Session is explicitly destroyed. bool is_in_draining_period() const; // Returns false if the Session is currently in a state where it is unable to // transmit any packets. bool can_send_packets() const; // Returns false if the Session is currently in a state where it cannot create // new streams. Specifically, a stream is not in a state to create streams if // it has been destroyed or is closing. bool can_create_streams() const; // Returns false if the Session is currently in a state where it cannot open // a new locally-initiated stream. When using 0RTT session resumption, this // will become true immediately after the session ticket and transport params // have been configured. Otherwise, it becomes true after the remote transport // params and tx keys have been installed. bool can_open_streams() const; uint64_t max_local_streams_uni() const; uint64_t max_local_streams_bidi() const; bool wants_session_ticket() const; void SetStreamOpenAllowed(); // It's a terrible name but "wrapped" here means that the Session has been // passed out to JavaScript and should be "wrapped" by whatever handler is // defined there to manage it. void set_wrapped(); enum class CloseMethod { // Immediate close with a 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, // Same as DEFAULT except that no attempt to notify the peer will be // made. 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 FinishClose() is called. GRACEFUL }; // Initiate closing of the session. void Close(CloseMethod method = CloseMethod::DEFAULT); void FinishClose(); void Destroy(); // Close the session and send a connection close packet to the peer. // If creating the packet fails the session will be silently closed. // The connection close packet will use the value of last_error_ as // the error code transmitted to the peer. void SendConnectionClose(); void OnTimeout(); void UpdateTimer(); // Has to be called after certain operations that generate packets. void UpdatePacketTxTime(); void UpdateDataStats(); void UpdatePath(const PathStorage& path); void ProcessPendingBidiStreams(); void ProcessPendingUniStreams(); // JavaScript callouts void EmitClose(const QuicError& error = QuicError()); void EmitDatagram(Store&& datagram, DatagramReceivedFlags flag); void EmitDatagramStatus(uint64_t id, DatagramStatus status); void EmitHandshakeComplete(); void EmitKeylog(const char* line); struct ValidatedPath { std::shared_ptr local; std::shared_ptr remote; }; void EmitPathValidation(PathValidationResult result, PathValidationFlags flags, const ValidatedPath& newPath, const std::optional& oldPath); void EmitSessionTicket(Store&& ticket); void EmitStream(const BaseObjectWeakPtr& stream); void EmitVersionNegotiation(const ngtcp2_pkt_hd& hd, const uint32_t* sv, size_t nsv); void DatagramStatus(uint64_t datagramId, DatagramStatus status); void DatagramReceived(const uint8_t* data, size_t datalen, DatagramReceivedFlags flag); void GenerateNewConnectionId(ngtcp2_cid* cid, size_t len, uint8_t* token); bool HandshakeCompleted(); void HandshakeConfirmed(); void SelectPreferredAddress(PreferredAddress* preferredAddress); static std::unique_ptr SelectApplication(Session* session, const Config& config); QuicConnectionPointer InitConnection(); Side side_; ngtcp2_mem allocator_; std::unique_ptr impl_; QuicConnectionPointer connection_; std::unique_ptr tls_session_; BaseObjectPtr qlog_stream_; BaseObjectPtr keylog_stream_; friend class Application; friend class DefaultApplication; friend class Http3ApplicationImpl; friend class Endpoint; friend class Stream; friend class PendingStream; friend class TLSContext; friend class TLSSession; friend class TransportParams; friend struct Impl; friend struct SendPendingDataScope; }; } // namespace node::quic #endif // HAVE_OPENSSL && NODE_OPENSSL_HAS_QUIC #endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS