You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
lokinet/llarp/quic/tunnel.hpp

206 lines
8.3 KiB
C++

#pragma once
#include <llarp/endpoint_base.hpp>
#include "stream.hpp"
#include "address.hpp"
#include "client.hpp"
#include "server.hpp"
#include <charconv>
#include <cstdint>
#include <string>
#include <string_view>
#include <uvw/tcp.h>
namespace llarp::quic
{
namespace tunnel
{
// The server sends back a 0x00 to signal that the remote TCP connection was established and
// that it is now accepting stream data; the client is not allowed to send any other data down
// the stream until this comes back (any data sent down the stream before then is discarded.)
inline constexpr std::byte CONNECT_INIT{0x00};
// QUIC application error codes we sent on failures:
// Failure to establish an initial connection:
inline constexpr uint64_t ERROR_CONNECT{0x5471907};
// Error if we receive something other than CONNECT_INIT as the initial stream data from the
// server
inline constexpr uint64_t ERROR_BAD_INIT{0x5471908};
// Close error code sent if we get an error on the TCP socket (other than an initial connect
// failure)
inline constexpr uint64_t ERROR_TCP{0x5471909};
// We pause reading from the local TCP socket if we have more than this amount of outstanding
// unacked data in the quic tunnel, then resume once it drops below this.
inline constexpr size_t PAUSE_SIZE = 64 * 1024;
} // namespace tunnel
/// Manager class for incoming and outgoing QUIC tunnels.
class TunnelManager
{
public:
using ListenHandler = std::function<std::optional<SockAddr>(
std::string_view lokinet_addr, // The remote's full lokinet address
uint16_t port // The requested port the tunnel wants to reach
)>;
// Timeout for the next `open()`. Note that when `open()` is given a ONS name to resolve this
// includes the resolution time.
std::chrono::milliseconds open_timeout = 4s;
TunnelManager(EndpointBase& endpoint);
/// Adds an incoming listener callback. When a new incoming quic connection is initiated to us
/// by some remote we invoke these callback(s) in order of registration. Each one has three
/// options:
/// - return a concrete llarp::SockAddr giving the TCP address/port to which we should connect
/// new incoming streams over the connection.
/// - returns std::nullopt to decline handling the connection (we will try the next listen
/// handler, in order of registration).
/// - throws an exception (derived from std::exception) in which case we refuse the connection
/// without trying any additional handlers.
///
/// If `listen()` is not called at all then new incoming connections will be immediately
/// dropped.
///
/// For plain-C wrappers around this see [FIXME].
int
listen(ListenHandler handler);
/// Simple wrapper around `listen(...)` that adds a handler that accepts all incoming
/// connections trying to tunnel to port `port` and maps them to `localhost:port`.
int
listen(SockAddr port);
/// Removes an incoming connection handler; takes the ID returned by `listen()`.
void
forget(int id);
/// Called when open succeeds or times out.
using OpenCallback = std::function<void(bool success)>;
/// Opens a quic tunnel to some remote lokinet address. (Should only be called from the event
/// loop thread.)
///
/// \param remote_addr is the lokinet address or ONS name (e.g. `azfojblahblahblah.loki` or
/// `blocks.loki`) that the tunnel should connect to.
/// \param port is the tunneled port on the remote that the client wants to reach. (This is
/// *not* the quic pseudo-port, which is always 0).
/// \param callback callback invoked when the quic connection has been established, or has timed
/// out.
/// \param bind_addr is the bind address and port that we should use for the localhost TCP
/// connection. Use port 0 to let the OS choose a random high port. Defaults to `127.0.0.1:0`.
///
/// This call immediately opens the local TCP socket, and initiates the lokinet connection and
/// QUIC tunnel to the remote. If the connection fails, the TCP socket will be closed. Note,
/// however, that this TCP socket will block until the underlying quic connection is
/// established.
///
/// Each connection to the local TCP socket establishes a new stream over the QUIC connection.
///
/// \return a pair:
/// - SockAddr containing the just-opened localhost socket that tunnels to the remote. This is
/// typically the same IP as `bind_addr`, with the port filled in (if bind_addr had a 0 port).
/// Note that, while you can connect to this socket immediately, it will block until the actual
/// connection and streams are established (and will be closed if they fail).
/// - unique integer that can be passed to close() to stop listening for new connections. This
/// also serves as a unique internal "pseudo-port" number to route returned quic packets to the
/// right connection.
///
/// TODO: add a callback to invoke when QUIC connection succeeds or fails.
/// TODO: add a plain C wrapper around this
std::pair<SockAddr, uint16_t>
open(
std::string_view remote_addr,
uint16_t port,
OpenCallback on_open = {},
SockAddr bind_addr = {127, 0, 0, 1});
/// Start closing an outgoing tunnel; takes the ID returned by `open()`. Note that an existing
/// established tunneled connections will not be forcibly closed; this simply stops accepting
/// new tunnel connections.
void
close(int id);
/// Called from tun code to deliver a quic packet.
///
/// \param dest - the convotag for which the packet arrived
/// \param buf - the raw arriving packet
///
void
receive_packet(const service::ConvoTag& tag, const llarp_buffer_t& buf);
/// return true if we have any listeners added
inline bool
hasListeners() const
{
return not incoming_handlers_.empty();
}
private:
EndpointBase& service_endpoint_;
struct ClientTunnel
{
// quic endpoint
std::unique_ptr<Client> client;
// Callback to invoke on quic connection established (true argument) or failed (false arg)
OpenCallback open_cb;
// TCP listening socket
std::shared_ptr<uvw::TCPHandle> tcp;
// Accepted TCP connections
std::unordered_set<std::shared_ptr<uvw::TCPHandle>> conns;
// Queue of incoming connections that are waiting for a stream to become available (either
// because we are still handshaking, or we reached the stream limit).
std::queue<std::weak_ptr<uvw::TCPHandle>> pending_incoming;
~ClientTunnel();
};
// pseudo-port -> Client instance (the "port" is used to route incoming quic packets to the
// right quic endpoint); pseudo-ports start at 1.
std::map<uint16_t, ClientTunnel> client_tunnels_;
uint16_t next_pseudo_port_ = 0;
// bool pport_wrapped_ = false;
bool
continue_connecting(
uint16_t pseudo_port, bool step_success, std::string_view step_name, std::string_view addr);
void
make_client(const SockAddr& remote, std::pair<const uint16_t, ClientTunnel>& row);
void
flush_pending_incoming(ClientTunnel& ct);
// Server instance; this listens on pseudo-port 0 (if it listens). This is automatically
// instantiated the first time `listen()` is called; if not instantiated we simply drop any
// inbound client-to-server quic packets.
std::unique_ptr<Server> server_;
void
make_server();
// Called when a new during connection handshaking once we have the established transport
// parameters (which include the port) if this is an incoming connection (and this endpoint is a
// server). This checks handlers to see whether the stream is allowed and, if so, returns a
// SockAddr containing the IP/port the tunnel should map to. Returns nullopt if the connection
// should be rejected.
std::optional<SockAddr>
allow_connection(std::string_view lokinet_addr, uint16_t port);
// Incoming stream handlers
std::map<int, ListenHandler> incoming_handlers_;
int next_handler_id_ = 1;
std::shared_ptr<uvw::Loop>
get_loop();
// Cleanup member
std::shared_ptr<int> timer_keepalive_ = std::make_shared<int>(0);
};
} // namespace llarp::quic