#pragma once #include "address.hpp" #include "stream.hpp" #include "io_result.hpp" #include #include #include #include #include #include #include extern "C" { #include #include } #include #include #include namespace llarp::quic { // We send and verify this in the initial connection and handshake; this is designed to allow // future changes (by either breaking or handling backwards compat). constexpr const std::array handshake_magic_bytes{ 'l', 'o', 'k', 'i', 'n', 'e', 't', 0x01}; constexpr std::basic_string_view handshake_magic{ handshake_magic_bytes.data(), handshake_magic_bytes.size()}; // Flow control window sizes for a buffer and individual streams: constexpr uint64_t CONNECTION_BUFFER = 1024 * 1024; constexpr uint64_t STREAM_BUFFER = 64 * 1024; // Max number of simultaneous streams we support over one connection constexpr uint64_t STREAM_LIMIT = 32; using bstring_view = std::basic_string_view; class Endpoint; class Server; class Client; struct alignas(size_t) ConnectionID : ngtcp2_cid { ConnectionID() = default; ConnectionID(const uint8_t* cid, size_t length); ConnectionID(const ConnectionID& c) = default; ConnectionID(ngtcp2_cid c) : ConnectionID(c.data, c.datalen) {} ConnectionID& operator=(const ConnectionID& c) = default; static constexpr size_t max_size() { return NGTCP2_MAX_CIDLEN; } static_assert(NGTCP2_MAX_CIDLEN <= std::numeric_limits::max()); bool operator==(const ConnectionID& other) const { return datalen == other.datalen && std::memcmp(data, other.data, datalen) == 0; } bool operator!=(const ConnectionID& other) const { return !(*this == other); } static ConnectionID random(size_t size = ConnectionID::max_size()); std::string ToString() const; }; } // namespace llarp::quic template <> constexpr inline bool llarp::IsToStringFormattable = true; namespace std { template <> struct hash { // We pick our own source_cid randomly, so it's a perfectly good hash already. size_t operator()(const llarp::quic::ConnectionID& c) const { static_assert( alignof(llarp::quic::ConnectionID) >= alignof(size_t) && offsetof(llarp::quic::ConnectionID, data) % sizeof(size_t) == 0); return *reinterpret_cast(c.data); } }; } // namespace std namespace llarp::quic { /// Returns the current (monotonic) time as a time_point inline auto get_time() { return std::chrono::steady_clock::now(); } /// Converts a time_point as returned by get_time to a nanosecond timestamp (as ngtcp2 expects). inline uint64_t get_timestamp(const std::chrono::steady_clock::time_point& t = get_time()) { return std::chrono::duration_cast(t.time_since_epoch()).count(); } // Stores an established connection between server/client. class Connection : public std::enable_shared_from_this { private: struct connection_deleter { void operator()(ngtcp2_conn* c) const { ngtcp2_conn_del(c); } }; // Packet data storage for a packet we are currently sending std::array send_buffer{}; size_t send_buffer_size = 0; ngtcp2_pkt_info send_pkt_info{}; // Attempts to send the packet in `send_buffer`. If sending blocks then we set up a write poll // on the socket to wait for it to become available, and return an io_result with `.blocked()` // set to true. On other I/O errors we return the errno, and on successful sending we return a // "true" (i.e. no error code) io_result. io_result send(); // Internal base method called invoked during construction to set up common client/server // settings. dest_cid and path must already be set. std::tuple init(); // Event trigger used to queue packet processing for this connection std::shared_ptr io_trigger; // Schedules a retransmit in the event loop (according to when ngtcp2 tells us we should) void schedule_retransmit(); std::shared_ptr retransmit_timer; // The port the client wants to connect to on the server uint16_t tunnel_port = 0; public: // The endpoint that owns this connection Endpoint& endpoint; /// The primary connection id of this Connection. This is the key of endpoint.conns that stores /// the actual shared_ptr (everything else in `conns` is a weak_ptr alias). const ConnectionID base_cid; /// The destination connection id we use to send to the other end; the remote end sets this as /// the source cid in the header. ConnectionID dest_cid; /// The underlying ngtcp2 connection object std::unique_ptr conn; /// The most recent Path we have to/from the remote Path path; /// True if we are draining (that is, we recently received a connection close from the other end /// and should discard everything that comes in on this connection). Do not set this directly: /// instead call Endpoint::start_draining(conn). bool draining = false; /// True when we are closing; conn_buffer will contain the closing stanza. bool closing = false; /// Buffer where we store non-stream connection data, e.g. for initial transport params during /// connection and the closing stanza when disconnecting. std::basic_string conn_buffer; // Stores callbacks of active streams, indexed by our local source connection ID that we assign // when the connection is initiated. std::map> streams; /// Constructs and initializes a new incoming connection /// /// \param server - the Server object that owns this connection /// \param base_cid - the local "primary" ConnectionID we use for this connection, typically /// random /// \param header - packet header that initiated the connection \param path - the network path /// to reach the remote Connection( Server& server, const ConnectionID& base_cid, ngtcp2_pkt_hd& header, const Path& path); /// Establishes a connection from the local Client to a remote Server /// \param client - the Endpoint object that owns this connection /// \param base_cid - the client's source (i.e. local) connection ID, typically random /// \param path - the network path to reach the remote /// \param tunnel_port - the port that this connection should tunnel to on the remote end Connection(Client& client, const ConnectionID& scid, const Path& path, uint16_t tunnel_port); // Non-movable, non-copyable: Connection(Connection&&) = delete; Connection(const Connection&) = delete; Connection& operator=(Connection&&) = delete; Connection& operator=(const Connection&) = delete; ~Connection(); operator const ngtcp2_conn*() const { return conn.get(); } operator ngtcp2_conn*() { return conn.get(); } // If this connection's endpoint is a server, returns a pointer to it. Otherwise returns // nullptr. Server* server(); // If this connection's endpoint is a client, returns a pointer to it. Otherwise returs // nullptr. Client* client(); // Called to signal libuv that this connection has stuff to do void io_ready(); // Called (via libuv) when it wants us to do our stuff. Call io_ready() to schedule this. void on_io_ready(); int setup_server_crypto_initial(); // Flush any streams with pending data. Note that, depending on available ngtcp2 state, we may // not fully flush all streams -- some streams can individually block while waiting for // confirmation. void flush_streams(); // Called when a new stream is opened int stream_opened(StreamID id); // Called when data is received for a stream int stream_receive(StreamID id, bstring_view data, bool fin); // Called when a stream is closed void stream_closed(StreamID id, uint64_t app_error_code); // Called when stream data has been acknowledged and can be freed int stream_ack(StreamID id, size_t size); // Asks the endpoint for a new connection ID alias to use for this connection. cidlen can be // used to specify the size of the cid (default is full size). ConnectionID make_alias_id(size_t cidlen = ConnectionID::max_size()); // A callback to invoke when the connection handshake completes. Will be cleared after being // called. std::function on_handshake_complete; // Returns true iff this connection has completed a handshake with the remote end. bool get_handshake_completed(); // Callback that is invoked whenever new streams become available: i.e. after handshaking, or // after existing streams are closed. Note that this callback is invoked whenever the number of // available streams increases, even if it was initially non-zero before the increase. To see // how many streams are currently available call `get_streams_available()` (it will always be at // least 1 when this callback is invoked). std::function on_stream_available; // Returns the number of available streams that can currently be opened on the connection int get_streams_available(); // Opens a stream over this connection; when the server receives this it attempts to establish a // TCP connection to the tunnel configured in the connection. The data callback is invoked as // data is received on this stream. The close callback is called if the stream is closed // (either by the remote, or locally after a stream->close() call). // // \param data_cb -- callback to invoke when data is received // \param close_cb -- callback to invoke when the connection is closed // // Throws a `std::runtime_error` if the stream creation fails (e.g. because the connection has // no free stream capacity). // // Returns a const reference to the stored Stream shared_ptr (so that the caller can decide // whether they want a copy or not). const std::shared_ptr& open_stream(Stream::data_callback_t data_cb, Stream::close_callback_t close_cb); // Accesses the stream via its StreamID; throws std::out_of_range if the stream doesn't exist. const std::shared_ptr& get_stream(StreamID s) const; // Internal methods that need to be publicly callable because we call them from C functions: int init_client(); int recv_initial_crypto(std::basic_string_view data); int recv_transport_params(std::basic_string_view data); int send_magic(ngtcp2_crypto_level level); int send_transport_params(ngtcp2_crypto_level level); void complete_handshake(); }; } // namespace llarp::quic