mirror of https://github.com/oxen-io/lokinet
WIP plainquic tunnels
parent
46c619c49f
commit
aa0f54fa07
@ -0,0 +1,57 @@
|
||||
#include "address.hpp"
|
||||
|
||||
extern "C"
|
||||
{
|
||||
#include <arpa/inet.h>
|
||||
}
|
||||
|
||||
#include <iostream>
|
||||
|
||||
namespace llarp::quic
|
||||
{
|
||||
using namespace std::literals;
|
||||
|
||||
Address::Address(std::array<uint8_t, 4> ip, uint16_t port)
|
||||
{
|
||||
s.in.sin_family = AF_INET;
|
||||
std::memcpy(&s.in.sin_addr.s_addr, ip.data(), ip.size());
|
||||
s.in.sin_port = htons(port);
|
||||
a.addrlen = sizeof(s.in);
|
||||
}
|
||||
|
||||
Address::Address(const sockaddr_any* addr, size_t addrlen)
|
||||
{
|
||||
assert(addrlen == sizeof(sockaddr_in)); // FIXME: IPv6 support
|
||||
std::memmove(&s, addr, addrlen);
|
||||
a.addrlen = addrlen;
|
||||
}
|
||||
Address&
|
||||
Address::operator=(const Address& addr)
|
||||
{
|
||||
std::memmove(&s, &addr.s, sizeof(s));
|
||||
a.addrlen = addr.a.addrlen;
|
||||
return *this;
|
||||
}
|
||||
|
||||
std::string
|
||||
Address::to_string() const
|
||||
{
|
||||
if (a.addrlen != sizeof(sockaddr_in))
|
||||
return "(unknown-addr)";
|
||||
char buf[INET_ADDRSTRLEN] = {0};
|
||||
inet_ntop(AF_INET, &s.in.sin_addr, buf, INET_ADDRSTRLEN);
|
||||
return buf + ":"s + std::to_string(ntohs(s.in.sin_port));
|
||||
}
|
||||
|
||||
std::ostream&
|
||||
operator<<(std::ostream& o, const Address& a)
|
||||
{
|
||||
return o << a.to_string();
|
||||
}
|
||||
std::ostream&
|
||||
operator<<(std::ostream& o, const Path& p)
|
||||
{
|
||||
return o << p.local << "<-" << p.remote;
|
||||
}
|
||||
|
||||
} // namespace llarp::quic
|
@ -0,0 +1,126 @@
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <cassert>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <iosfwd>
|
||||
|
||||
#include <ngtcp2/ngtcp2.h>
|
||||
|
||||
extern "C"
|
||||
{
|
||||
#include <netinet/in.h>
|
||||
#include <sys/socket.h>
|
||||
}
|
||||
|
||||
// FIXME: replace use of this with a llarp::SockAddr
|
||||
|
||||
namespace llarp::quic
|
||||
{
|
||||
union sockaddr_any
|
||||
{
|
||||
sockaddr_storage storage;
|
||||
sockaddr sa;
|
||||
sockaddr_in6 in6;
|
||||
sockaddr_in in;
|
||||
};
|
||||
|
||||
class Address
|
||||
{
|
||||
sockaddr_any s{};
|
||||
ngtcp2_addr a{0, &s.sa, nullptr};
|
||||
|
||||
public:
|
||||
Address() = default;
|
||||
Address(std::array<uint8_t, 4> ip, uint16_t port);
|
||||
Address(const sockaddr_any* addr, size_t addrlen);
|
||||
Address(const Address& addr)
|
||||
{
|
||||
*this = addr;
|
||||
}
|
||||
Address&
|
||||
operator=(const Address& addr);
|
||||
|
||||
// Implicit conversion to sockaddr* and ngtcp2_addr& so that an Address can be passed wherever
|
||||
// one of those is expected.
|
||||
operator sockaddr*()
|
||||
{
|
||||
return a.addr;
|
||||
}
|
||||
operator const sockaddr*() const
|
||||
{
|
||||
return a.addr;
|
||||
}
|
||||
constexpr socklen_t
|
||||
sockaddr_size() const
|
||||
{
|
||||
return a.addrlen;
|
||||
}
|
||||
operator ngtcp2_addr&()
|
||||
{
|
||||
return a;
|
||||
}
|
||||
operator const ngtcp2_addr&() const
|
||||
{
|
||||
return a;
|
||||
}
|
||||
|
||||
std::string
|
||||
to_string() const;
|
||||
};
|
||||
|
||||
// Wraps an ngtcp2_path (which is basically just and address pair) with remote/local components.
|
||||
// Implicitly convertable to a ngtcp2_path* so that this can be passed wherever a ngtcp2_path* is
|
||||
// taken in the ngtcp2 API.
|
||||
struct Path
|
||||
{
|
||||
private:
|
||||
Address local_{}, remote_{};
|
||||
|
||||
public:
|
||||
ngtcp2_path path{
|
||||
{local_.sockaddr_size(), local_, nullptr}, {remote_.sockaddr_size(), remote_, nullptr}};
|
||||
|
||||
// Public accessors are const:
|
||||
const Address& local = local_;
|
||||
const Address& remote = remote_;
|
||||
|
||||
Path() = default;
|
||||
Path(const Address& local, const Address& remote) : local_{local}, remote_{remote}
|
||||
{}
|
||||
Path(const Address& local, const sockaddr_any* remote_addr, size_t remote_len)
|
||||
: local_{local}, remote_{remote_addr, remote_len}
|
||||
{}
|
||||
Path(const Path& p) : local_{p.local_}, remote_{p.remote_}
|
||||
{}
|
||||
|
||||
Path&
|
||||
operator=(const Path& p)
|
||||
{
|
||||
local_ = p.local_;
|
||||
remote_ = p.remote_;
|
||||
return *this;
|
||||
}
|
||||
|
||||
// Equivalent to `&obj.path`, but slightly more convenient for passing into ngtcp2 functions
|
||||
// taking a ngtcp2_path pointer.
|
||||
operator ngtcp2_path*()
|
||||
{
|
||||
return &path;
|
||||
}
|
||||
operator const ngtcp2_path*() const
|
||||
{
|
||||
return &path;
|
||||
}
|
||||
|
||||
std::string
|
||||
to_string() const;
|
||||
};
|
||||
|
||||
std::ostream&
|
||||
operator<<(std::ostream& o, const Address& a);
|
||||
std::ostream&
|
||||
operator<<(std::ostream& o, const Path& p);
|
||||
|
||||
} // namespace llarp::quic
|
@ -0,0 +1,99 @@
|
||||
|
||||
#include "client.hpp"
|
||||
#include "log.hpp"
|
||||
|
||||
#include <oxenmq/variant.h>
|
||||
|
||||
namespace llarp::quic
|
||||
{
|
||||
Client::Client(
|
||||
Address remote,
|
||||
std::shared_ptr<uvw::Loop> loop_,
|
||||
uint16_t tunnel_port,
|
||||
std::optional<Address> local_)
|
||||
: Endpoint{std::move(local_), std::move(loop_)}
|
||||
{
|
||||
// Our UDP socket is now set up, so now we initiate contact with the remote QUIC
|
||||
Path path{local, remote};
|
||||
Debug("Connecting to ", remote);
|
||||
|
||||
if (tunnel_port == 0)
|
||||
throw std::logic_error{"Cannot tunnel to port 0"};
|
||||
|
||||
// TODO: need timers for:
|
||||
//
|
||||
// - timeout (to disconnect if idle for too longer)
|
||||
//
|
||||
// - probably don't need for lokinet tunnel: change local addr -- attempts to re-bind the local
|
||||
// socket
|
||||
//
|
||||
// - key_update_timer
|
||||
//
|
||||
// - delay_stream_timer
|
||||
|
||||
auto connptr =
|
||||
std::make_shared<Connection>(*this, ConnectionID::random(rng), path, tunnel_port);
|
||||
auto& conn = *connptr;
|
||||
conns.emplace(conn.base_cid, connptr);
|
||||
|
||||
/* Debug("set crypto ctx");
|
||||
|
||||
null_crypto.client_initial(conn);
|
||||
|
||||
auto x = ngtcp2_conn_get_max_data_left(conn);
|
||||
Debug("mdl = ", x);
|
||||
*/
|
||||
|
||||
conn.io_ready();
|
||||
|
||||
/*
|
||||
Debug("Opening bidi stream");
|
||||
int64_t stream_id;
|
||||
if (auto rv = ngtcp2_conn_open_bidi_stream(conn, &stream_id, nullptr);
|
||||
rv != 0) {
|
||||
Debug("Opening bidi stream failed: ", ngtcp2_strerror(rv));
|
||||
assert(rv == NGTCP2_ERR_STREAM_ID_BLOCKED);
|
||||
}
|
||||
else { Debug("Opening bidi stream good"); }
|
||||
*/
|
||||
}
|
||||
|
||||
std::shared_ptr<Connection>
|
||||
Client::get_connection()
|
||||
{
|
||||
// A client only has one outgoing connection, so everything in conns should either be a
|
||||
// shared_ptr or weak_ptr to that same outgoing connection so we can just use the first one.
|
||||
auto it = conns.begin();
|
||||
if (it == conns.end())
|
||||
return nullptr;
|
||||
if (auto* wptr = std::get_if<alias_conn_ptr>(&it->second))
|
||||
return wptr->lock();
|
||||
return std::get<primary_conn_ptr>(it->second);
|
||||
}
|
||||
|
||||
void
|
||||
Client::handle_packet(const Packet& p)
|
||||
{
|
||||
Debug("Handling incoming client packet: ", buffer_printer{p.data});
|
||||
auto maybe_dcid = handle_packet_init(p);
|
||||
if (!maybe_dcid)
|
||||
return;
|
||||
auto& dcid = *maybe_dcid;
|
||||
|
||||
Debug("Incoming connection id ", dcid);
|
||||
auto [connptr, alias] = get_conn(dcid);
|
||||
if (!connptr)
|
||||
{
|
||||
Debug("CID is ", alias ? "expired alias" : "unknown/expired", "; dropping");
|
||||
return;
|
||||
}
|
||||
auto& conn = *connptr;
|
||||
if (alias)
|
||||
Debug("CID is alias for primary CID ", conn.base_cid);
|
||||
else
|
||||
Debug("CID is primary CID");
|
||||
|
||||
handle_conn_packet(conn, p);
|
||||
}
|
||||
|
||||
} // namespace llarp::quic
|
@ -0,0 +1,31 @@
|
||||
#pragma once
|
||||
|
||||
#include "endpoint.hpp"
|
||||
|
||||
#include <optional>
|
||||
|
||||
namespace llarp::quic
|
||||
{
|
||||
class Client : public Endpoint
|
||||
{
|
||||
public:
|
||||
// Constructs a client that establishes an outgoing connection to `remote` to tunnel packets to
|
||||
// `tunnel_port` on the remote's lokinet address. `local` can be used to optionally bind to a
|
||||
// local IP and/or port for the connection.
|
||||
Client(
|
||||
Address remote,
|
||||
std::shared_ptr<uvw::Loop> loop,
|
||||
uint16_t tunnel_port,
|
||||
std::optional<Address> local = std::nullopt);
|
||||
|
||||
// Returns a reference to the client's connection to the server. Returns a nullptr if there is
|
||||
// no connection.
|
||||
std::shared_ptr<Connection>
|
||||
get_connection();
|
||||
|
||||
private:
|
||||
void
|
||||
handle_packet(const Packet& p) override;
|
||||
};
|
||||
|
||||
} // namespace llarp::quic
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,311 @@
|
||||
#pragma once
|
||||
|
||||
#include "address.hpp"
|
||||
#include "random.hpp"
|
||||
#include "stream.hpp"
|
||||
#include "io_result.hpp"
|
||||
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string_view>
|
||||
#include <unordered_set>
|
||||
#include <map>
|
||||
|
||||
#include <ngtcp2/ngtcp2.h>
|
||||
#include <uvw/async.h>
|
||||
#include <uvw/poll.h>
|
||||
#include <uvw/timer.h>
|
||||
|
||||
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<uint8_t, 8> handshake_magic_bytes{
|
||||
'l', 'o', 'k', 'i', 'n', 'e', 't', 0x01};
|
||||
constexpr std::basic_string_view<uint8_t> 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 on a connection
|
||||
constexpr uint64_t STREAM_LIMIT = 100;
|
||||
|
||||
using bstring_view = std::basic_string_view<std::byte>;
|
||||
|
||||
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<uint8_t>::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);
|
||||
}
|
||||
|
||||
template <typename RNG>
|
||||
static ConnectionID
|
||||
random(RNG&& rng, size_t size = ConnectionID::max_size())
|
||||
{
|
||||
ConnectionID r;
|
||||
r.datalen = std::min(size, ConnectionID::max_size());
|
||||
random_bytes(r.data, r.datalen, rng);
|
||||
return r;
|
||||
}
|
||||
};
|
||||
std::ostream&
|
||||
operator<<(std::ostream& o, const ConnectionID& c);
|
||||
|
||||
} // namespace llarp::quic
|
||||
namespace std
|
||||
{
|
||||
template <>
|
||||
struct hash<llarp::quic::ConnectionID>
|
||||
{
|
||||
// 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<const size_t*>(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<std::chrono::nanoseconds>(t.time_since_epoch()).count();
|
||||
}
|
||||
|
||||
// Stores an established connection between server/client.
|
||||
class Connection : public std::enable_shared_from_this<Connection>
|
||||
{
|
||||
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<std::byte, NGTCP2_MAX_PKTLEN_IPV4> 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();
|
||||
|
||||
// Poll for writability; activated if we block while trying to send a packet.
|
||||
std::shared_ptr<uvw::PollHandle> wpoll;
|
||||
bool wpoll_active = false;
|
||||
|
||||
// Internal base method called invoked during construction to set up common client/server
|
||||
// settings. dest_cid and path must already be set.
|
||||
std::tuple<ngtcp2_settings, ngtcp2_transport_params, ngtcp2_callbacks>
|
||||
init();
|
||||
|
||||
// Event trigger used to queue packet processing for this connection
|
||||
std::shared_ptr<uvw::AsyncHandle> io_trigger;
|
||||
|
||||
// Schedules a retransmit in the event loop (according to when ngtcp2 tells us we should)
|
||||
void
|
||||
schedule_retransmit();
|
||||
std::shared_ptr<uvw::TimerHandle> 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<ngtcp2_conn, connection_deleter> 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<std::byte> conn_buffer;
|
||||
|
||||
// Stores callbacks of active streams, indexed by our local source connection ID that we assign
|
||||
// when the connection is initiated.
|
||||
std::map<StreamID, std::shared_ptr<Stream>> streams;
|
||||
|
||||
/// Constructs and initializes a new connection received by a Server
|
||||
///
|
||||
/// \param s - the Server object on which the connection was initiated
|
||||
/// \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& s, const ConnectionID& base_cid, ngtcp2_pkt_hd& header, const Path& path);
|
||||
|
||||
/// Establishes a connection from the local Client to a remote Server
|
||||
/// \param c - the Client object from which the connection is being made
|
||||
/// \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& c, const ConnectionID& scid, const Path& path, uint16_t tunnel_port);
|
||||
|
||||
// Non-movable, non-copyable:
|
||||
Connection(Connection&&) = delete;
|
||||
Connection&
|
||||
operator=(Connection&&) = delete;
|
||||
Connection(const 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/reset
|
||||
int
|
||||
stream_reset(StreamID id, uint64_t app_error_code);
|
||||
|
||||
// Called when stream data has been acknoledged 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());
|
||||
|
||||
// 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<Stream>&
|
||||
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<Stream>&
|
||||
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<uint8_t> data);
|
||||
int
|
||||
recv_transport_params(std::basic_string_view<uint8_t> data);
|
||||
int
|
||||
send_magic(ngtcp2_crypto_level level);
|
||||
int
|
||||
send_transport_params(ngtcp2_crypto_level level);
|
||||
void
|
||||
complete_handshake();
|
||||
};
|
||||
|
||||
} // namespace llarp::quic
|
@ -0,0 +1,526 @@
|
||||
#include "endpoint.hpp"
|
||||
#include "client.hpp"
|
||||
#include "log.hpp"
|
||||
#include "server.hpp"
|
||||
|
||||
#include <iostream>
|
||||
#include <variant>
|
||||
|
||||
#include <oxenmq/hex.h>
|
||||
#include <oxenmq/variant.h>
|
||||
|
||||
#include <uvw/timer.h>
|
||||
|
||||
#include <sodium/crypto_generichash.h>
|
||||
|
||||
// DEBUG:
|
||||
extern "C"
|
||||
{
|
||||
#include "../ngtcp2_conn.h"
|
||||
}
|
||||
|
||||
namespace llarp::quic
|
||||
{
|
||||
Endpoint::Endpoint(std::optional<Address> addr, std::shared_ptr<uvw::Loop> loop_)
|
||||
: loop{std::move(loop_)}
|
||||
{
|
||||
random_bytes(static_secret.data(), static_secret.size(), rng);
|
||||
|
||||
// Create and bind the UDP socket. We can't use libuv's UDP socket here because it doesn't
|
||||
// give us the ability to set up the ECN field as QUIC requires.
|
||||
auto fd = socket(AF_INET, SOCK_DGRAM | SOCK_NONBLOCK, 0);
|
||||
if (fd == -1)
|
||||
throw std::runtime_error{"Failed to open socket: "s + strerror(errno)};
|
||||
|
||||
if (addr)
|
||||
{
|
||||
assert(addr->sockaddr_size() == sizeof(sockaddr_in)); // FIXME: IPv4-only for now
|
||||
auto rv = bind(fd, *addr, addr->sockaddr_size());
|
||||
if (rv == -1)
|
||||
throw std::runtime_error{
|
||||
"Failed to bind UDP socket to " + addr->to_string() + ": " + strerror(errno)};
|
||||
}
|
||||
|
||||
// Get our address via the socket in case `addr` is using anyaddr/anyport.
|
||||
sockaddr_any sa;
|
||||
socklen_t salen = sizeof(sa);
|
||||
// FIXME: if I didn't call bind above then do I need to call bind() before this (with
|
||||
// anyaddr/anyport)?
|
||||
getsockname(fd, &sa.sa, &salen);
|
||||
assert(salen == sizeof(sockaddr_in)); // FIXME: IPv4-only for now
|
||||
local = {&sa, salen};
|
||||
Debug("Bound to ", local, addr ? "" : " (auto-selected)");
|
||||
|
||||
// Set up the socket to provide us with incoming ECN (IP_TOS) info
|
||||
// NB: This is for IPv4; on AF_INET6 this would be IPPROTO_IPV6, IPV6_RECVTCLASS
|
||||
if (uint8_t want_tos = 1;
|
||||
- 1
|
||||
== setsockopt(
|
||||
fd, IPPROTO_IP, IP_RECVTOS, &want_tos, static_cast<socklen_t>(sizeof(want_tos))))
|
||||
throw std::runtime_error{"Failed to set ECN on socket: "s + strerror(errno)};
|
||||
|
||||
// Wire up our recv buffer structures into what recvmmsg() wants
|
||||
buf.resize(max_buf_size * msgs.size());
|
||||
for (size_t i = 0; i < msgs.size(); i++)
|
||||
{
|
||||
auto& iov = msgs_iov[i];
|
||||
iov.iov_base = buf.data() + max_buf_size * i;
|
||||
iov.iov_len = max_buf_size;
|
||||
#ifdef LOKINET_HAVE_RECVMMSG
|
||||
auto& mh = msgs[i].msg_hdr;
|
||||
#else
|
||||
auto& mh = msgs[i];
|
||||
#endif
|
||||
mh.msg_name = &msgs_addr[i];
|
||||
mh.msg_namelen = sizeof(msgs_addr[i]);
|
||||
mh.msg_iov = &iov;
|
||||
mh.msg_iovlen = 1;
|
||||
mh.msg_control = msgs_cmsg[i].data();
|
||||
mh.msg_controllen = msgs_cmsg[i].size();
|
||||
}
|
||||
|
||||
// Let uv do its stuff
|
||||
poll = loop->resource<uvw::PollHandle>(fd);
|
||||
poll->on<uvw::PollEvent>([this](const auto&, auto&) { on_readable(); });
|
||||
poll->start(uvw::PollHandle::Event::READABLE);
|
||||
|
||||
// Set up a callback every 250ms to clean up stale sockets, etc.
|
||||
expiry_timer = loop->resource<uvw::TimerHandle>();
|
||||
expiry_timer->on<uvw::TimerEvent>([this](const auto&, auto&) { check_timeouts(); });
|
||||
expiry_timer->start(250ms, 250ms);
|
||||
|
||||
Debug("Created endpoint");
|
||||
}
|
||||
|
||||
Endpoint::~Endpoint()
|
||||
{
|
||||
if (poll)
|
||||
poll->close();
|
||||
if (expiry_timer)
|
||||
expiry_timer->close();
|
||||
}
|
||||
|
||||
int
|
||||
Endpoint::socket_fd() const
|
||||
{
|
||||
return poll->fd();
|
||||
}
|
||||
|
||||
void
|
||||
Endpoint::on_readable()
|
||||
{
|
||||
Debug("poll callback on readable");
|
||||
|
||||
#ifdef LOKINET_HAVE_RECVMMSG
|
||||
// NB: recvmmsg is linux-specific but ought to offer some performance benefits
|
||||
int n_msg = recvmmsg(socket_fd(), msgs.data(), msgs.size(), 0, nullptr);
|
||||
if (n_msg == -1)
|
||||
{
|
||||
if (errno != EAGAIN && errno != ENOTCONN)
|
||||
Warn("Error recv'ing from ", local.to_string(), ": ", strerror(errno));
|
||||
return;
|
||||
}
|
||||
|
||||
Debug("Recv'd ", n_msg, " messages");
|
||||
for (int i = 0; i < n_msg; i++)
|
||||
{
|
||||
auto& [msg_hdr, msg_len] = msgs[i];
|
||||
bstring_view data{buf.data() + i * max_buf_size, msg_len};
|
||||
#else
|
||||
for (size_t i = 0; i < N_msgs; i++)
|
||||
{
|
||||
auto& msg_hdr = msgs[0];
|
||||
auto n_bytes = recvmsg(socket_fd(), &msg_hdr, 0);
|
||||
if (n_bytes == -1 && errno != EAGAIN && errno != ENOTCONN)
|
||||
Warn("Error recv'ing from ", local.to_string(), ": ", strerror(errno));
|
||||
if (n_bytes <= 0)
|
||||
return;
|
||||
auto msg_len = static_cast<unsigned int>(n_bytes);
|
||||
bstring_view data{buf.data(), msg_len};
|
||||
#endif
|
||||
|
||||
Debug(
|
||||
"header [",
|
||||
msg_hdr.msg_namelen,
|
||||
"]: ",
|
||||
buffer_printer{reinterpret_cast<char*>(msg_hdr.msg_name), msg_hdr.msg_namelen});
|
||||
|
||||
if (!msg_hdr.msg_name || msg_hdr.msg_namelen != sizeof(sockaddr_in))
|
||||
{ // FIXME: IPv6 support?
|
||||
Warn("Invalid/unknown source address, dropping packet");
|
||||
continue;
|
||||
}
|
||||
|
||||
Packet pkt{
|
||||
Path{local, reinterpret_cast<const sockaddr_any*>(msg_hdr.msg_name), msg_hdr.msg_namelen},
|
||||
data,
|
||||
ngtcp2_pkt_info{.ecn = 0}};
|
||||
|
||||
// Go look for the ECN header field on the incoming packet
|
||||
for (auto cmsg = CMSG_FIRSTHDR(&msg_hdr); cmsg; cmsg = CMSG_NXTHDR(&msg_hdr, cmsg))
|
||||
{
|
||||
// IPv4; for IPv6 these would be IPPROTO_IPV6 and IPV6_TCLASS
|
||||
if (cmsg->cmsg_level == IPPROTO_IP && cmsg->cmsg_type == IP_TOS && cmsg->cmsg_len)
|
||||
{
|
||||
pkt.info.ecn = *reinterpret_cast<uint8_t*>(CMSG_DATA(cmsg));
|
||||
}
|
||||
}
|
||||
|
||||
Debug(
|
||||
i,
|
||||
"[",
|
||||
pkt.path,
|
||||
",ecn=0x",
|
||||
std::hex,
|
||||
+pkt.info.ecn,
|
||||
std::dec,
|
||||
"]: received ",
|
||||
msg_len,
|
||||
" bytes");
|
||||
|
||||
handle_packet(pkt);
|
||||
|
||||
Debug("Done handling packet");
|
||||
|
||||
#ifdef LOKINET_HAVE_RECVMMSG // Help editor's { } matching:
|
||||
}
|
||||
#else
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
std::optional<ConnectionID>
|
||||
Endpoint::handle_packet_init(const Packet& p)
|
||||
{
|
||||
version_info vi;
|
||||
auto rv = ngtcp2_pkt_decode_version_cid(
|
||||
&vi.version,
|
||||
&vi.dcid,
|
||||
&vi.dcid_len,
|
||||
&vi.scid,
|
||||
&vi.scid_len,
|
||||
u8data(p.data),
|
||||
p.data.size(),
|
||||
NGTCP2_MAX_CIDLEN);
|
||||
if (rv == 1)
|
||||
{ // 1 means Version Negotiation should be sent and otherwise the packet should be ignored
|
||||
send_version_negotiation(vi, p.path.remote);
|
||||
return std::nullopt;
|
||||
}
|
||||
else if (rv != 0)
|
||||
{
|
||||
Warn("QUIC packet header decode failed: ", ngtcp2_strerror(rv));
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
if (vi.dcid_len > ConnectionID::max_size())
|
||||
{
|
||||
Warn("Internal error: destination ID is longer than should be allowed");
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return std::make_optional<ConnectionID>(vi.dcid, vi.dcid_len);
|
||||
}
|
||||
void
|
||||
Endpoint::handle_conn_packet(Connection& conn, const Packet& p)
|
||||
{
|
||||
if (ngtcp2_conn_is_in_closing_period(conn))
|
||||
{
|
||||
Debug("Connection is in closing period, dropping");
|
||||
close_connection(conn);
|
||||
return;
|
||||
}
|
||||
if (conn.draining)
|
||||
{
|
||||
Debug("Connection is draining, dropping");
|
||||
// "draining" state means we received a connection close and we're keeping the
|
||||
// connection alive just to catch (and discard) straggling packets that arrive
|
||||
// out of order w.r.t to connection close.
|
||||
return;
|
||||
}
|
||||
|
||||
if (auto result = read_packet(p, conn); !result)
|
||||
{
|
||||
Warn("Read packet failed! ", ngtcp2_strerror(result.error_code));
|
||||
}
|
||||
|
||||
// FIXME - reset idle timer?
|
||||
Debug("Done with incoming packet");
|
||||
}
|
||||
|
||||
io_result
|
||||
Endpoint::read_packet(const Packet& p, Connection& conn)
|
||||
{
|
||||
Debug("Reading packet from ", p.path);
|
||||
Debug("Conn state before reading: ", conn.conn->state);
|
||||
auto rv =
|
||||
ngtcp2_conn_read_pkt(conn, p.path, &p.info, u8data(p.data), p.data.size(), get_timestamp());
|
||||
Debug("Conn state after reading: ", conn.conn->state);
|
||||
|
||||
if (rv == 0)
|
||||
conn.io_ready();
|
||||
else
|
||||
Warn("read pkt error: ", ngtcp2_strerror(rv));
|
||||
|
||||
if (rv == NGTCP2_ERR_DRAINING)
|
||||
start_draining(conn);
|
||||
else if (rv == NGTCP2_ERR_DROP_CONN)
|
||||
delete_conn(conn.base_cid);
|
||||
|
||||
return {rv};
|
||||
}
|
||||
|
||||
void
|
||||
Endpoint::update_ecn(uint32_t ecn)
|
||||
{
|
||||
assert(ecn <= std::numeric_limits<uint8_t>::max());
|
||||
if (ecn_curr != ecn)
|
||||
{
|
||||
if (-1
|
||||
== setsockopt(socket_fd(), IPPROTO_IP, IP_TOS, &ecn, static_cast<socklen_t>(sizeof(ecn))))
|
||||
Warn("setsockopt failed to set IP_TOS: ", strerror(errno));
|
||||
|
||||
// IPv6 version:
|
||||
// int tclass = this->ecn;
|
||||
// setsockopt(socket_fd(), IPPROTO_IPV6, IPV6_TCLASS, &tclass,
|
||||
// static_cast<socklen_t>(sizeof(tclass)));
|
||||
|
||||
ecn_curr = ecn;
|
||||
}
|
||||
}
|
||||
|
||||
io_result
|
||||
Endpoint::send_packet(const Address& to, bstring_view data, uint32_t ecn)
|
||||
{
|
||||
iovec msg_iov;
|
||||
msg_iov.iov_base = const_cast<std::byte*>(data.data());
|
||||
msg_iov.iov_len = data.size();
|
||||
|
||||
msghdr msg{};
|
||||
msg.msg_name = &const_cast<sockaddr&>(reinterpret_cast<const sockaddr&>(to));
|
||||
msg.msg_namelen = sizeof(sockaddr_in);
|
||||
msg.msg_iov = &msg_iov;
|
||||
msg.msg_iovlen = 1;
|
||||
|
||||
auto fd = socket_fd();
|
||||
|
||||
update_ecn(ecn);
|
||||
ssize_t nwrite = 0;
|
||||
do
|
||||
{
|
||||
nwrite = sendmsg(fd, &msg, 0);
|
||||
} while (nwrite == -1 && errno == EINTR);
|
||||
|
||||
if (nwrite == -1)
|
||||
{
|
||||
Warn("sendmsg failed: ", strerror(errno));
|
||||
return {errno};
|
||||
}
|
||||
|
||||
Debug(
|
||||
"[",
|
||||
to.to_string(),
|
||||
",ecn=0x",
|
||||
std::hex,
|
||||
+ecn_curr,
|
||||
std::dec,
|
||||
"]: sent ",
|
||||
nwrite,
|
||||
" bytes");
|
||||
return {};
|
||||
}
|
||||
|
||||
void
|
||||
Endpoint::send_version_negotiation(const version_info& vi, const Address& source)
|
||||
{
|
||||
std::array<std::byte, NGTCP2_MAX_PKTLEN_IPV4> buf;
|
||||
std::array<uint32_t, NGTCP2_PROTO_VER_MAX - NGTCP2_PROTO_VER_MIN + 2> versions;
|
||||
std::iota(versions.begin() + 1, versions.end(), NGTCP2_PROTO_VER_MIN);
|
||||
// we're supposed to send some 0x?a?a?a?a version to trigger version negotiation
|
||||
versions[0] = 0x1a2a3a4au;
|
||||
|
||||
auto nwrote = ngtcp2_pkt_write_version_negotiation(
|
||||
u8data(buf),
|
||||
buf.size(),
|
||||
std::uniform_int_distribution<uint8_t>{0, 255}(rng),
|
||||
vi.dcid,
|
||||
vi.dcid_len,
|
||||
vi.scid,
|
||||
vi.scid_len,
|
||||
versions.data(),
|
||||
versions.size());
|
||||
if (nwrote < 0)
|
||||
Warn("Failed to construct version negotiation packet: ", ngtcp2_strerror(nwrote));
|
||||
if (nwrote <= 0)
|
||||
return;
|
||||
|
||||
send_packet(source, bstring_view{buf.data(), static_cast<size_t>(nwrote)}, 0);
|
||||
}
|
||||
|
||||
void
|
||||
Endpoint::close_connection(Connection& conn, uint64_t code, bool application)
|
||||
{
|
||||
Debug("Closing connection ", conn.base_cid);
|
||||
if (!conn.closing)
|
||||
{
|
||||
conn.conn_buffer.resize(max_pkt_size_v4);
|
||||
Path path;
|
||||
ngtcp2_pkt_info pi;
|
||||
|
||||
auto write_close_func =
|
||||
application ? ngtcp2_conn_write_application_close : ngtcp2_conn_write_connection_close;
|
||||
auto written = write_close_func(
|
||||
conn,
|
||||
path,
|
||||
&pi,
|
||||
u8data(conn.conn_buffer),
|
||||
conn.conn_buffer.size(),
|
||||
code,
|
||||
get_timestamp());
|
||||
if (written <= 0)
|
||||
{
|
||||
Warn(
|
||||
"Failed to write connection close packet: ",
|
||||
written < 0 ? ngtcp2_strerror(written) : "unknown error: closing is 0 bytes??");
|
||||
return;
|
||||
}
|
||||
assert(written <= (long)conn.conn_buffer.size());
|
||||
conn.conn_buffer.resize(written);
|
||||
conn.closing = true;
|
||||
|
||||
// FIXME: ipv6
|
||||
assert(path.local.sockaddr_size() == sizeof(sockaddr_in));
|
||||
assert(path.remote.sockaddr_size() == sizeof(sockaddr_in));
|
||||
|
||||
conn.path = path;
|
||||
}
|
||||
assert(conn.closing && !conn.conn_buffer.empty());
|
||||
|
||||
if (auto sent = send_packet(conn.path.remote, conn.conn_buffer, 0); !sent)
|
||||
{
|
||||
Warn(
|
||||
"Failed to send packet: ",
|
||||
strerror(sent.error_code),
|
||||
"; removing connection ",
|
||||
conn.base_cid);
|
||||
delete_conn(conn.base_cid);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/// Puts a connection into draining mode (i.e. after getting a connection close). This will
|
||||
/// keep the connection registered for the recommended 3*Probe Timeout, during which we drop
|
||||
/// packets that use the connection id and after which we will forget about it.
|
||||
void
|
||||
Endpoint::start_draining(Connection& conn)
|
||||
{
|
||||
if (conn.draining)
|
||||
return;
|
||||
Debug("Putting ", conn.base_cid, " into draining mode");
|
||||
conn.draining = true;
|
||||
// Recommended draining time is 3*Probe Timeout
|
||||
draining.emplace(conn.base_cid, get_time() + ngtcp2_conn_get_pto(conn) * 3 * 1ns);
|
||||
}
|
||||
|
||||
void
|
||||
Endpoint::check_timeouts()
|
||||
{
|
||||
auto now = get_time();
|
||||
uint64_t now_ts = get_timestamp(now);
|
||||
|
||||
// Destroy any connections that are finished draining
|
||||
bool cleanup = false;
|
||||
while (!draining.empty() && draining.front().second < now)
|
||||
{
|
||||
if (auto it = conns.find(draining.front().first); it != conns.end())
|
||||
{
|
||||
if (std::holds_alternative<primary_conn_ptr>(it->second))
|
||||
cleanup = true;
|
||||
Debug("Deleting connection ", it->first);
|
||||
conns.erase(it);
|
||||
}
|
||||
draining.pop();
|
||||
}
|
||||
if (cleanup)
|
||||
clean_alias_conns();
|
||||
|
||||
for (auto it = conns.begin(); it != conns.end(); ++it)
|
||||
{
|
||||
if (auto* conn_ptr = std::get_if<primary_conn_ptr>(&it->second))
|
||||
{
|
||||
Connection& conn = **conn_ptr;
|
||||
auto exp = ngtcp2_conn_get_idle_expiry(conn);
|
||||
if (exp >= now_ts || conn.draining)
|
||||
continue;
|
||||
start_draining(conn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::pair<std::shared_ptr<Connection>, bool>
|
||||
Endpoint::get_conn(const ConnectionID& cid)
|
||||
{
|
||||
if (auto it = conns.find(cid); it != conns.end())
|
||||
{
|
||||
if (auto* wptr = std::get_if<alias_conn_ptr>(&it->second))
|
||||
return {wptr->lock(), true};
|
||||
return {var::get<primary_conn_ptr>(it->second), false};
|
||||
}
|
||||
return {nullptr, false};
|
||||
}
|
||||
|
||||
bool
|
||||
Endpoint::delete_conn(const ConnectionID& cid)
|
||||
{
|
||||
auto it = conns.find(cid);
|
||||
if (it == conns.end())
|
||||
{
|
||||
Debug("Cannot delete connection ", cid, ": cid not found");
|
||||
return false;
|
||||
}
|
||||
|
||||
bool primary = std::holds_alternative<primary_conn_ptr>(it->second);
|
||||
Debug("Deleting ", primary ? "primary" : "alias", " connection ", cid);
|
||||
conns.erase(it);
|
||||
if (primary)
|
||||
clean_alias_conns();
|
||||
return true;
|
||||
}
|
||||
|
||||
void
|
||||
Endpoint::clean_alias_conns()
|
||||
{
|
||||
for (auto it = conns.begin(); it != conns.end();)
|
||||
{
|
||||
if (auto* conn_wptr = std::get_if<alias_conn_ptr>(&it->second);
|
||||
conn_wptr && conn_wptr->expired())
|
||||
it = conns.erase(it);
|
||||
else
|
||||
++it;
|
||||
}
|
||||
}
|
||||
|
||||
ConnectionID
|
||||
Endpoint::add_connection_id(Connection& conn, size_t cid_length)
|
||||
{
|
||||
ConnectionID cid;
|
||||
for (bool inserted = false; !inserted;)
|
||||
{
|
||||
cid = ConnectionID::random(rng, cid_length);
|
||||
inserted = conns.emplace(cid, conn.weak_from_this()).second;
|
||||
}
|
||||
Debug("Created cid ", cid, " alias for ", conn.base_cid);
|
||||
return cid;
|
||||
}
|
||||
|
||||
void
|
||||
Endpoint::make_stateless_reset_token(const ConnectionID& cid, unsigned char* dest)
|
||||
{
|
||||
crypto_generichash_state state;
|
||||
crypto_generichash_init(&state, nullptr, 0, NGTCP2_STATELESS_RESET_TOKENLEN);
|
||||
crypto_generichash_update(&state, u8data(static_secret), static_secret.size());
|
||||
crypto_generichash_update(&state, cid.data, cid.datalen);
|
||||
crypto_generichash_final(&state, dest, NGTCP2_STATELESS_RESET_TOKENLEN);
|
||||
}
|
||||
|
||||
} // namespace llarp::quic
|
@ -0,0 +1,241 @@
|
||||
#pragma once
|
||||
|
||||
#include "address.hpp"
|
||||
#include "connection.hpp"
|
||||
#include "io_result.hpp"
|
||||
#include "null_crypto.hpp"
|
||||
#include "packet.hpp"
|
||||
#include "stream.hpp"
|
||||
|
||||
#include <chrono>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <queue>
|
||||
#include <random>
|
||||
#include <unordered_map>
|
||||
#include <variant>
|
||||
#include <vector>
|
||||
|
||||
#include <uvw/loop.h>
|
||||
#include <uvw/poll.h>
|
||||
#include <uvw/timer.h>
|
||||
|
||||
// True if we support recvmmsg/sendmmsg
|
||||
#if defined(__linux__) && !defined(LOKINET_NO_RECVMMSG)
|
||||
#define LOKINET_HAVE_RECVMMSG
|
||||
#endif
|
||||
|
||||
namespace llarp::quic
|
||||
{
|
||||
using namespace std::literals;
|
||||
|
||||
inline constexpr auto IDLE_TIMEOUT = 5min;
|
||||
|
||||
class Endpoint
|
||||
{
|
||||
protected:
|
||||
// Address we are listening on
|
||||
Address local;
|
||||
// The current outgoing IP ecn value for the socket
|
||||
uint8_t ecn_curr = 0;
|
||||
|
||||
std::shared_ptr<uvw::PollHandle> poll;
|
||||
std::shared_ptr<uvw::TimerHandle> expiry_timer;
|
||||
std::shared_ptr<uvw::Loop> loop;
|
||||
|
||||
// How many messages (at most) we recv per callback:
|
||||
static constexpr int N_msgs = 8;
|
||||
#ifdef LOKINET_HAVE_RECVMMSG
|
||||
static constexpr int N_mmsg = N_msgs;
|
||||
std::array<mmsghdr, N_mmsg> msgs;
|
||||
#else
|
||||
static constexpr int N_mmsg = 1;
|
||||
std::array<msghdr, N_mmsg> msgs;
|
||||
#endif
|
||||
|
||||
std::array<iovec, N_mmsg> msgs_iov;
|
||||
std::array<sockaddr_any, N_mmsg> msgs_addr;
|
||||
std::array<std::array<uint8_t, CMSG_SPACE(1)>, N_mmsg> msgs_cmsg;
|
||||
std::vector<std::byte> buf;
|
||||
// Max theoretical size of a UDP packet is 2^16-1 minus IP/UDP header overhead
|
||||
static constexpr size_t max_buf_size = 64 * 1024;
|
||||
// Max size of a UDP packet that we'll send
|
||||
static constexpr size_t max_pkt_size_v4 = NGTCP2_MAX_PKTLEN_IPV4;
|
||||
static constexpr size_t max_pkt_size_v6 = NGTCP2_MAX_PKTLEN_IPV6;
|
||||
|
||||
std::mt19937_64 rng = seeded<std::mt19937_64>();
|
||||
|
||||
using primary_conn_ptr = std::shared_ptr<Connection>;
|
||||
using alias_conn_ptr = std::weak_ptr<Connection>;
|
||||
|
||||
// Connections. When a client establishes a new connection it chooses its own source connection
|
||||
// ID and a destination connection ID and sends them to the server.
|
||||
//
|
||||
// This container stores the primary Connection instance as a shared_ptr, and any connection
|
||||
// aliases as weak_ptrs referencing the primary instance (so that we don't have to double a
|
||||
// double-hash lookup on incoming packets, since those frequently use aliases).
|
||||
//
|
||||
// The destination connection ID should be entirely random and can be up to 160 bits, but the
|
||||
// source connection ID does not have to be (i.e. it can encode some information, if desired).
|
||||
//
|
||||
// The server is going to include in the response:
|
||||
// - destination connection ID equal to the client's source connection ID
|
||||
// - a new random source connection ID. (We don't use the client's destination ID but generate
|
||||
// our own). Like the clients source ID, this can contain embedded info.
|
||||
//
|
||||
// The client stores this, and so we end up with client-scid == server-dcid, and client-dcid ==
|
||||
// server-scid, where each side chose its own source connection ID.
|
||||
//
|
||||
// Ultimately, we store here our own {source connection ID -> Connection} pairs (or
|
||||
// equivalently, on incoming packets, the key will be the packet's dest conn ID).
|
||||
std::unordered_map<ConnectionID, std::variant<primary_conn_ptr, alias_conn_ptr>> conns;
|
||||
|
||||
using conns_iterator = decltype(conns)::iterator;
|
||||
|
||||
// Connections that are draining (i.e. we are dropping, but need to keep around for a while
|
||||
// to catch and drop lagged packets). The time point is the scheduled removal time.
|
||||
std::queue<std::pair<ConnectionID, std::chrono::steady_clock::time_point>> draining;
|
||||
|
||||
NullCrypto null_crypto;
|
||||
|
||||
// Random data that we hash together with a CID to make a stateless reset token
|
||||
std::array<std::byte, 32> static_secret;
|
||||
|
||||
friend class Connection;
|
||||
|
||||
// Wires up an endpoint connection.
|
||||
//
|
||||
// `bind` - address we should bind to. Required for a server, optional for a client. If
|
||||
// omitted, no explicit bind is performed (which means the socket will be implicitly bound to
|
||||
// some OS-determined random high bind port).
|
||||
// `loop` - the uv loop pointer managing polling of this endpoint
|
||||
Endpoint(std::optional<Address> bind, std::shared_ptr<uvw::Loop> loop);
|
||||
|
||||
virtual ~Endpoint();
|
||||
|
||||
int
|
||||
socket_fd() const;
|
||||
|
||||
void
|
||||
on_readable();
|
||||
|
||||
// Version & connection id info that we can potentially extract when decoding a packet
|
||||
struct version_info
|
||||
{
|
||||
uint32_t version;
|
||||
const uint8_t* dcid;
|
||||
size_t dcid_len;
|
||||
const uint8_t* scid;
|
||||
size_t scid_len;
|
||||
};
|
||||
|
||||
// Called to handle an incoming packet
|
||||
virtual void
|
||||
handle_packet(const Packet& p) = 0;
|
||||
|
||||
// Internal method: handles initial common packet decoding, returns the connection ID or nullopt
|
||||
// if decoding failed.
|
||||
std::optional<ConnectionID>
|
||||
handle_packet_init(const Packet& p);
|
||||
// Internal method: handles a packet sent to the given connection
|
||||
void
|
||||
handle_conn_packet(Connection& c, const Packet& p);
|
||||
|
||||
// Reads a packet and handles various error conditions. Returns an io_result. Note that it is
|
||||
// possible for the conn_it to be erased from `conns` if the error code is anything other than
|
||||
// success (0) or NGTCP2_ERR_RETRY.
|
||||
io_result
|
||||
read_packet(const Packet& p, Connection& conn);
|
||||
|
||||
// Sets up the ECN IP field (IP_TOS for IPv4) for the next outgoing packet sent via
|
||||
// send_packet(). This does the actual syscall (if ECN is different than currently set), and is
|
||||
// typically called implicitly via send_packet().
|
||||
void
|
||||
update_ecn(uint32_t ecn);
|
||||
|
||||
// Sends a packet to `to` containing `data`. Returns a non-error io_result on success,
|
||||
// an io_result with .error_code set to the errno of the failure on failure.
|
||||
io_result
|
||||
send_packet(const Address& to, bstring_view data, uint32_t ecn);
|
||||
|
||||
// Wrapper around the above that takes a regular std::string_view (i.e. of chars) and recasts
|
||||
// it to an string_view of std::bytes.
|
||||
io_result
|
||||
send_packet(const Address& to, std::string_view data, uint32_t ecn)
|
||||
{
|
||||
return send_packet(
|
||||
to, bstring_view{reinterpret_cast<const std::byte*>(data.data()), data.size()}, ecn);
|
||||
}
|
||||
|
||||
// Another wrapper taking a vector
|
||||
io_result
|
||||
send_packet(const Address& to, const std::vector<std::byte>& data, uint32_t ecn)
|
||||
{
|
||||
return send_packet(to, bstring_view{data.data(), data.size()}, ecn);
|
||||
}
|
||||
|
||||
void
|
||||
send_version_negotiation(const version_info& vi, const Address& source);
|
||||
|
||||
// Looks up a connection. Returns a shared_ptr (either copied for a primary connection, or
|
||||
// locked from an alias's weak pointer) if the connection was found or nullptr if not; and a
|
||||
// bool indicating whether this connection ID was an alias (true) or not (false). [Note: the
|
||||
// alias value can be true even if the shared_ptr is null in the case of an expired alias that
|
||||
// hasn't yet been cleaned up].
|
||||
std::pair<std::shared_ptr<Connection>, bool>
|
||||
get_conn(const ConnectionID& cid);
|
||||
|
||||
// Called to start closing (or continue closing) a connection by sending a connection close
|
||||
// response to any incoming packets.
|
||||
//
|
||||
// Takes the iterator to the connection pair from `conns` and optional error parameters: if
|
||||
// `application` is false (the default) then we do a hard connection close because of transport
|
||||
// error, if true we do a graceful application close. For application closes the code is
|
||||
// application-defined; for hard closes the code should be one of the NGTCP2_*_ERROR values.
|
||||
void
|
||||
close_connection(Connection& conn, uint64_t code = NGTCP2_NO_ERROR, bool application = false);
|
||||
|
||||
/// Puts a connection into draining mode (i.e. after getting a connection close). This will
|
||||
/// keep the connection registered for the recommended 3*Probe Timeout, during which we drop
|
||||
/// packets that use the connection id and after which we will forget about it.
|
||||
void
|
||||
start_draining(Connection& conn);
|
||||
|
||||
void
|
||||
check_timeouts();
|
||||
|
||||
/// Deletes a connection from `conns`; if the connecion is a primary connection shared pointer
|
||||
/// then it is removed and clean_alias_conns() is immediately called to remove any aliases to
|
||||
/// the connection. If the given connection is an alias connection then it is removed but no
|
||||
/// cleanup is performed. Returns true if something was removed, false if the connection was
|
||||
/// not found.
|
||||
bool
|
||||
delete_conn(const ConnectionID& cid);
|
||||
|
||||
/// Removes any connection id aliases that no longer have associated Connections.
|
||||
void
|
||||
clean_alias_conns();
|
||||
|
||||
/// Creates a new, unused connection ID alias for the given connection; adds the alias to
|
||||
/// `conns` and returns the ConnectionID.
|
||||
ConnectionID
|
||||
add_connection_id(Connection& conn, size_t cid_length = ConnectionID::max_size());
|
||||
|
||||
public:
|
||||
// Makes a deterministic stateless reset token for the given connection ID. Writes it to dest
|
||||
// (which must have NGTCP2_STATELESS_RESET_TOKENLEN bytes available).
|
||||
void
|
||||
make_stateless_reset_token(const ConnectionID& cid, unsigned char* dest);
|
||||
|
||||
// Default stream buffer size for streams opened through this endpoint.
|
||||
size_t default_stream_buffer_size = 64 * 1024;
|
||||
|
||||
// Gets a reference to the UV event loop
|
||||
uvw::Loop&
|
||||
get_loop()
|
||||
{
|
||||
return *loop;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace llarp::quic
|
@ -0,0 +1,38 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstring>
|
||||
#include <cerrno>
|
||||
#include <string_view>
|
||||
|
||||
namespace llarp::quic
|
||||
{
|
||||
// Return result from a read or write operation that wraps an errno value. It is implicitly
|
||||
// convertible to bool to test for "is not an error" (which is the inverse of casting a plain
|
||||
// integer error code value to bool).
|
||||
struct io_result
|
||||
{
|
||||
// An error code, typically an errno value
|
||||
int error_code{0};
|
||||
// Returns true if this represent a successful result, i.e. an error_code of 0.
|
||||
operator bool() const
|
||||
{
|
||||
return error_code == 0;
|
||||
}
|
||||
|
||||
// Returns true if this is an error value indicating a failure to write without blocking (only
|
||||
// applied to io_result's capturing an errno).
|
||||
bool
|
||||
blocked() const
|
||||
{
|
||||
return error_code == EAGAIN || error_code == EWOULDBLOCK;
|
||||
}
|
||||
|
||||
// Returns the errno string for the given error code.
|
||||
std::string_view
|
||||
str() const
|
||||
{
|
||||
return strerror(error_code);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace llarp::quic
|
@ -0,0 +1,45 @@
|
||||
#include "log.hpp"
|
||||
|
||||
namespace llarp::quic
|
||||
{
|
||||
std::ostream&
|
||||
operator<<(std::ostream& o, const buffer_printer& bp)
|
||||
{
|
||||
auto& b = bp.buf;
|
||||
auto oldfill = o.fill();
|
||||
o.fill('0');
|
||||
o << "Buffer[" << b.size() << "/0x" << std::hex << b.size() << " bytes]:";
|
||||
for (size_t i = 0; i < b.size(); i += 32)
|
||||
{
|
||||
o << "\n" << std::setw(4) << i << " ";
|
||||
|
||||
size_t stop = std::min(b.size(), i + 32);
|
||||
for (size_t j = 0; j < 32; j++)
|
||||
{
|
||||
auto k = i + j;
|
||||
if (j % 4 == 0)
|
||||
o << ' ';
|
||||
if (k >= stop)
|
||||
o << " ";
|
||||
else
|
||||
o << std::setw(2) << std::to_integer<uint_fast16_t>(b[k]);
|
||||
}
|
||||
o << u8" ┃";
|
||||
for (size_t j = i; j < stop; j++)
|
||||
{
|
||||
auto c = std::to_integer<char>(b[j]);
|
||||
if (c == 0x00)
|
||||
o << u8"∅";
|
||||
else if (c < 0x20 || c > 0x7e)
|
||||
o << u8"·";
|
||||
else
|
||||
o << c;
|
||||
}
|
||||
o << u8"┃";
|
||||
}
|
||||
o << std::dec;
|
||||
o.fill(oldfill);
|
||||
return o;
|
||||
}
|
||||
|
||||
} // namespace llarp::quic
|
@ -0,0 +1,146 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdarg>
|
||||
#include <cstddef>
|
||||
#include <iostream>
|
||||
#include <iomanip>
|
||||
#include <type_traits>
|
||||
|
||||
// Temporary logging code to be replaced with lokinet logging
|
||||
|
||||
#include <oxenmq/hex.h>
|
||||
|
||||
#ifdef __cpp_lib_source_location
|
||||
#include <source_location>
|
||||
namespace slns = std;
|
||||
#else
|
||||
#include <experimental/source_location>
|
||||
namespace slns = std::experimental;
|
||||
#endif
|
||||
|
||||
namespace llarp::quic
|
||||
{
|
||||
struct buffer_printer
|
||||
{
|
||||
std::basic_string_view<std::byte> buf;
|
||||
|
||||
template <typename T, typename = std::enable_if_t<sizeof(T) == 1>>
|
||||
explicit buffer_printer(std::basic_string_view<T> buf)
|
||||
: buf{reinterpret_cast<const std::byte*>(buf.data()), buf.size()}
|
||||
{}
|
||||
|
||||
template <typename T, typename = std::enable_if_t<sizeof(T) == 1>>
|
||||
explicit buffer_printer(const std::basic_string<T>& buf)
|
||||
: buffer_printer(std::basic_string_view<T>{buf})
|
||||
{}
|
||||
|
||||
template <typename T, typename = std::enable_if_t<sizeof(T) == 1>>
|
||||
explicit buffer_printer(std::basic_string<T>&& buf) = delete;
|
||||
|
||||
template <typename T, typename = std::enable_if_t<sizeof(T) == 1>>
|
||||
explicit buffer_printer(const T* data, size_t size)
|
||||
: buffer_printer(std::basic_string_view<T>{data, size})
|
||||
{}
|
||||
};
|
||||
std::ostream&
|
||||
operator<<(std::ostream& o, const buffer_printer& bp);
|
||||
|
||||
namespace detail
|
||||
{
|
||||
template <typename T, typename... V>
|
||||
constexpr bool is_same_any_v = (std::is_same_v<T, V> || ...);
|
||||
|
||||
template <typename T, typename... More>
|
||||
void
|
||||
log_print_vals(T&& val, More&&... more)
|
||||
{
|
||||
using PlainT = std::remove_reference_t<T>;
|
||||
if constexpr (is_same_any_v<PlainT, char, unsigned char, signed char, uint8_t, std::byte>)
|
||||
std::cerr
|
||||
<< +val; // Promote chars to int so that they get printed as numbers, not literal chars
|
||||
else
|
||||
std::cerr << val;
|
||||
if constexpr (sizeof...(More))
|
||||
log_print_vals(std::forward<More>(more)...);
|
||||
}
|
||||
|
||||
template <typename... T>
|
||||
void
|
||||
log_print(const slns::source_location& location, T&&... args)
|
||||
{
|
||||
std::string_view filename{location.file_name()};
|
||||
if (auto pos = filename.rfind('/'); pos != std::string::npos
|
||||
&& (pos = filename.substr(0, pos).rfind('/')) != std::string::npos)
|
||||
{
|
||||
filename.remove_prefix(pos + 1);
|
||||
}
|
||||
std::cerr << "\e[3m[" << filename << ':' << location.line() << "]\e[23m";
|
||||
if constexpr (sizeof...(T))
|
||||
{
|
||||
std::cerr << ": ";
|
||||
detail::log_print_vals(std::forward<T>(args)...);
|
||||
}
|
||||
std::cerr << '\n';
|
||||
}
|
||||
|
||||
} // namespace detail
|
||||
|
||||
#ifndef NDEBUG
|
||||
template <typename... T>
|
||||
struct Debug
|
||||
{
|
||||
Debug(T&&... args, const slns::source_location& location = slns::source_location::current())
|
||||
{
|
||||
std::cerr << "DBG";
|
||||
detail::log_print(location, std::forward<T>(args)...);
|
||||
}
|
||||
};
|
||||
template <typename... T>
|
||||
Debug(T&&...) -> Debug<T...>;
|
||||
#else
|
||||
template <typename... T>
|
||||
void
|
||||
Debug(T&&...)
|
||||
{}
|
||||
#endif
|
||||
|
||||
template <typename... T>
|
||||
struct Info
|
||||
{
|
||||
Info(T&&... args, const slns::source_location& location = slns::source_location::current())
|
||||
{
|
||||
std::cerr << "\e[32mNFO";
|
||||
detail::log_print(location, std::forward<T>(args)...);
|
||||
std::cerr << "\e[0m";
|
||||
}
|
||||
};
|
||||
template <typename... T>
|
||||
Info(T&&...) -> Info<T...>;
|
||||
|
||||
template <typename... T>
|
||||
struct Warn
|
||||
{
|
||||
Warn(T&&... args, const slns::source_location& location = slns::source_location::current())
|
||||
{
|
||||
std::cerr << "\e[33;1mWRN";
|
||||
detail::log_print(location, std::forward<T>(args)...);
|
||||
std::cerr << "\e[0m";
|
||||
}
|
||||
};
|
||||
template <typename... T>
|
||||
Warn(T&&...) -> Warn<T...>;
|
||||
|
||||
template <typename... T>
|
||||
struct Error
|
||||
{
|
||||
Error(T&&... args, const slns::source_location& location = slns::source_location::current())
|
||||
{
|
||||
std::cerr << "\e[31;1mWRN";
|
||||
detail::log_print(location, std::forward<T>(args)...);
|
||||
std::cerr << "\e[0m";
|
||||
}
|
||||
};
|
||||
template <typename... T>
|
||||
Error(T&&...) -> Error<T...>;
|
||||
|
||||
} // namespace llarp::quic
|
@ -0,0 +1,93 @@
|
||||
#include "null_crypto.hpp"
|
||||
#include "log.hpp"
|
||||
|
||||
#include <limits>
|
||||
|
||||
namespace llarp::quic
|
||||
{
|
||||
// Cranks a value to "11", i.e. set it to its maximum
|
||||
template <typename T>
|
||||
void
|
||||
crank_to_eleven(T& val)
|
||||
{
|
||||
val = std::numeric_limits<T>::max();
|
||||
}
|
||||
|
||||
NullCrypto::NullCrypto()
|
||||
{
|
||||
crank_to_eleven(null_ctx.max_encryption);
|
||||
crank_to_eleven(null_ctx.max_decryption_failure);
|
||||
null_ctx.aead.max_overhead = 1; // Fails an assertion if 0
|
||||
null_aead.max_overhead = 1; // FIXME - can this be 0?
|
||||
}
|
||||
|
||||
void
|
||||
NullCrypto::client_initial(Connection& conn)
|
||||
{
|
||||
ngtcp2_conn_set_initial_crypto_ctx(conn, &null_ctx);
|
||||
ngtcp2_conn_install_initial_key(
|
||||
conn,
|
||||
&null_aead_ctx,
|
||||
null_iv.data(),
|
||||
&null_cipher_ctx,
|
||||
&null_aead_ctx,
|
||||
null_iv.data(),
|
||||
&null_cipher_ctx,
|
||||
null_iv.size());
|
||||
ngtcp2_conn_set_retry_aead(conn, &null_aead, &null_aead_ctx);
|
||||
ngtcp2_conn_set_crypto_ctx(conn, &null_ctx);
|
||||
}
|
||||
|
||||
void
|
||||
NullCrypto::server_initial(Connection& conn)
|
||||
{
|
||||
Debug("Server initial null crypto setup");
|
||||
ngtcp2_conn_set_initial_crypto_ctx(conn, &null_ctx);
|
||||
ngtcp2_conn_install_initial_key(
|
||||
conn,
|
||||
&null_aead_ctx,
|
||||
null_iv.data(),
|
||||
&null_cipher_ctx,
|
||||
&null_aead_ctx,
|
||||
null_iv.data(),
|
||||
&null_cipher_ctx,
|
||||
null_iv.size());
|
||||
ngtcp2_conn_set_crypto_ctx(conn, &null_ctx);
|
||||
}
|
||||
|
||||
bool
|
||||
NullCrypto::install_tx_handshake_key(Connection& conn)
|
||||
{
|
||||
return ngtcp2_conn_install_tx_handshake_key(
|
||||
conn, &null_aead_ctx, null_iv.data(), null_iv.size(), &null_cipher_ctx)
|
||||
== 0;
|
||||
}
|
||||
bool
|
||||
NullCrypto::install_rx_handshake_key(Connection& conn)
|
||||
{
|
||||
return ngtcp2_conn_install_rx_handshake_key(
|
||||
conn, &null_aead_ctx, null_iv.data(), null_iv.size(), &null_cipher_ctx)
|
||||
== 0;
|
||||
}
|
||||
bool
|
||||
NullCrypto::install_tx_key(Connection& conn)
|
||||
{
|
||||
return ngtcp2_conn_install_tx_key(
|
||||
conn,
|
||||
null_iv.data(),
|
||||
null_iv.size(),
|
||||
&null_aead_ctx,
|
||||
null_iv.data(),
|
||||
null_iv.size(),
|
||||
&null_cipher_ctx)
|
||||
== 0;
|
||||
}
|
||||
bool
|
||||
NullCrypto::install_rx_key(Connection& conn)
|
||||
{
|
||||
return ngtcp2_conn_install_rx_key(
|
||||
conn, nullptr, 0, &null_aead_ctx, null_iv.data(), null_iv.size(), &null_cipher_ctx)
|
||||
== 0;
|
||||
}
|
||||
|
||||
} // namespace llarp::quic
|
@ -0,0 +1,44 @@
|
||||
#pragma once
|
||||
|
||||
#include "connection.hpp"
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
|
||||
#include <ngtcp2/ngtcp2.h>
|
||||
|
||||
namespace llarp::quic
|
||||
{
|
||||
// Class providing do-nothing stubs for quic crypto operations: everything over lokinet is already
|
||||
// encrypted so we just no-op QUIC's built in crypto operations.
|
||||
struct NullCrypto
|
||||
{
|
||||
NullCrypto();
|
||||
|
||||
void
|
||||
client_initial(Connection& conn);
|
||||
|
||||
void
|
||||
server_initial(Connection& conn);
|
||||
|
||||
bool
|
||||
install_tx_handshake_key(Connection& conn);
|
||||
bool
|
||||
install_tx_key(Connection& conn);
|
||||
|
||||
bool
|
||||
install_rx_handshake_key(Connection& conn);
|
||||
bool
|
||||
install_rx_key(Connection& conn);
|
||||
|
||||
private:
|
||||
std::array<uint8_t, 1> null_iv{};
|
||||
// std::array<uint8_t, 4096> null_data{};
|
||||
|
||||
ngtcp2_crypto_ctx null_ctx{};
|
||||
ngtcp2_crypto_aead null_aead{};
|
||||
ngtcp2_crypto_aead_ctx null_aead_ctx{};
|
||||
ngtcp2_crypto_cipher_ctx null_cipher_ctx{};
|
||||
};
|
||||
|
||||
} // namespace llarp::quic
|
@ -0,0 +1,15 @@
|
||||
#pragma once
|
||||
|
||||
#include "connection.hpp"
|
||||
|
||||
namespace llarp::quic
|
||||
{
|
||||
// Encapsulates a packet, i.e. the remote addr, packet data, plus metadata.
|
||||
struct Packet
|
||||
{
|
||||
Path path;
|
||||
bstring_view data;
|
||||
ngtcp2_pkt_info info;
|
||||
};
|
||||
|
||||
} // namespace llarp::quic
|
@ -0,0 +1,37 @@
|
||||
#pragma once
|
||||
|
||||
// TODO: replace with llarp
|
||||
|
||||
#include <algorithm>
|
||||
#include <random>
|
||||
#include <cstring>
|
||||
#include <cstddef>
|
||||
|
||||
template <typename Gen>
|
||||
void
|
||||
random_bytes(void* dest, size_t length, Gen&& rng)
|
||||
{
|
||||
using RNG = std::remove_reference_t<Gen>;
|
||||
using UInt = typename RNG::result_type;
|
||||
static_assert(std::is_same_v<UInt, uint32_t> || std::is_same_v<UInt, uint64_t>);
|
||||
static_assert(RNG::min() == 0 && RNG::max() == std::numeric_limits<UInt>::max());
|
||||
auto* d = reinterpret_cast<std::byte*>(dest);
|
||||
for (size_t o = 0; o < length; o += sizeof(UInt))
|
||||
{
|
||||
UInt x = rng();
|
||||
std::memcpy(d + o, &x, std::min(sizeof(UInt), length - o));
|
||||
}
|
||||
}
|
||||
|
||||
// Returns an RNG with a fully seeded state from std::random_device
|
||||
template <typename RNG>
|
||||
RNG
|
||||
seeded()
|
||||
{
|
||||
constexpr size_t rd_draws =
|
||||
((RNG::state_size * sizeof(typename RNG::result_type) - 1) / sizeof(unsigned int) + 1);
|
||||
std::array<unsigned int, rd_draws> seed_data;
|
||||
std::generate(seed_data.begin(), seed_data.end(), std::random_device{});
|
||||
std::seed_seq seed(seed_data.begin(), seed_data.end());
|
||||
return RNG{seed};
|
||||
}
|
@ -0,0 +1,117 @@
|
||||
#include "server.hpp"
|
||||
#include "log.hpp"
|
||||
|
||||
#include <oxenmq/hex.h>
|
||||
#include <oxenmq/variant.h>
|
||||
#include <uvw/loop.h>
|
||||
|
||||
#include <stdexcept>
|
||||
#include <tuple>
|
||||
#include <variant>
|
||||
|
||||
namespace llarp::quic
|
||||
{
|
||||
Server::Server(
|
||||
Address listen, std::shared_ptr<uvw::Loop> loop, stream_open_callback_t stream_open)
|
||||
: Endpoint{std::move(listen), std::move(loop)}, stream_open_callback{std::move(stream_open)}
|
||||
{}
|
||||
|
||||
void
|
||||
Server::handle_packet(const Packet& p)
|
||||
{
|
||||
Debug("Handling incoming server packet: ", buffer_printer{p.data});
|
||||
auto maybe_dcid = handle_packet_init(p);
|
||||
if (!maybe_dcid)
|
||||
return;
|
||||
auto& dcid = *maybe_dcid;
|
||||
|
||||
// See if we have an existing connection already established for it
|
||||
Debug("Incoming connection id ", dcid);
|
||||
primary_conn_ptr connptr;
|
||||
if (auto conn_it = conns.find(dcid); conn_it != conns.end())
|
||||
{
|
||||
if (auto* wptr = std::get_if<alias_conn_ptr>(&conn_it->second))
|
||||
{
|
||||
connptr = wptr->lock();
|
||||
if (!connptr)
|
||||
Debug("CID is an expired alias");
|
||||
else
|
||||
Debug("CID is an alias for primary CID ", connptr->base_cid);
|
||||
}
|
||||
else
|
||||
{
|
||||
connptr = var::get<primary_conn_ptr>(conn_it->second);
|
||||
Debug("CID is primary");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
connptr = accept_connection(p);
|
||||
}
|
||||
|
||||
if (!connptr)
|
||||
{
|
||||
Warn("invalid or expired connection, ignoring");
|
||||
return;
|
||||
}
|
||||
|
||||
handle_conn_packet(*connptr, p);
|
||||
}
|
||||
|
||||
std::shared_ptr<Connection>
|
||||
Server::accept_connection(const Packet& p)
|
||||
{
|
||||
Debug("Accepting new connection");
|
||||
// This is a new incoming connection
|
||||
ngtcp2_pkt_hd hd;
|
||||
auto rv = ngtcp2_accept(&hd, u8data(p.data), p.data.size());
|
||||
|
||||
if (rv == -1)
|
||||
{ // Invalid packet
|
||||
Warn("Invalid packet received, length=", p.data.size());
|
||||
#ifndef NDEBUG
|
||||
Debug("packet body:");
|
||||
for (size_t i = 0; i < p.data.size(); i += 50)
|
||||
Debug(" ", oxenmq::to_hex(p.data.substr(i, 50)));
|
||||
#endif
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (rv == 1)
|
||||
{ // Invalid/unexpected version, send a version negotiation
|
||||
Debug("Invalid/unsupported version; sending version negotiation");
|
||||
send_version_negotiation(
|
||||
version_info{hd.version, hd.dcid.data, hd.dcid.datalen, hd.scid.data, hd.scid.datalen},
|
||||
p.path.remote);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
/*
|
||||
ngtcp2_cid ocid;
|
||||
ngtcp2_cid *pocid = nullptr;
|
||||
*/
|
||||
if (hd.type == NGTCP2_PKT_0RTT)
|
||||
{
|
||||
Warn("Received 0-RTT packet, which shouldn't happen in our implementation; dropping");
|
||||
return nullptr;
|
||||
}
|
||||
else if (hd.type == NGTCP2_PKT_INITIAL && hd.token.len)
|
||||
{
|
||||
// This is a normal QUIC thing, but we don't do it:
|
||||
Warn("Unexpected token in initial packet");
|
||||
}
|
||||
|
||||
// create and store Connection
|
||||
for (;;)
|
||||
{
|
||||
if (auto [it, ins] = conns.emplace(ConnectionID::random(rng), primary_conn_ptr{}); ins)
|
||||
{
|
||||
auto connptr = std::make_shared<Connection>(*this, it->first, hd, p.path);
|
||||
it->second = connptr;
|
||||
Debug("Created local Connection ", it->first, " for incoming connection");
|
||||
return connptr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace llarp::quic
|
@ -0,0 +1,40 @@
|
||||
#pragma once
|
||||
|
||||
#include "endpoint.hpp"
|
||||
|
||||
#include <functional>
|
||||
|
||||
namespace llarp::quic
|
||||
{
|
||||
class Server : public Endpoint
|
||||
{
|
||||
public:
|
||||
using stream_open_callback_t =
|
||||
std::function<bool(Server& server, Stream& stream, uint16_t port)>;
|
||||
|
||||
Server(Address listen, std::shared_ptr<uvw::Loop> loop, stream_open_callback_t stream_opened);
|
||||
|
||||
// Stream callback: takes the server, the (just-created) stream, and the connection port.
|
||||
// Returns true if the stream should be allowed or false to reject the stream. The callback
|
||||
// should set up the data_callback and close_callback on the stream: they will default to null
|
||||
// (which means incoming data will simply be dropped).
|
||||
stream_open_callback_t stream_open_callback;
|
||||
|
||||
int
|
||||
setup_null_crypto(ngtcp2_conn* conn);
|
||||
|
||||
private:
|
||||
// Handles an incoming packet by figuring out and handling the connection id; if necessary we
|
||||
// send back a version negotiation or a connection close frame, or drop the packet (if in the
|
||||
// draining state). If we get through all of the above then it's a packet to read, in which
|
||||
// case we pass it on to read_packet().
|
||||
void
|
||||
handle_packet(const Packet& p) override;
|
||||
|
||||
// Creates a new connection from an incoming packet. Returns a nullptr if the connection can't
|
||||
// be created.
|
||||
std::shared_ptr<Connection>
|
||||
accept_connection(const Packet& p);
|
||||
};
|
||||
|
||||
} // namespace llarp::quic
|
@ -0,0 +1,343 @@
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <queue>
|
||||
#include <functional>
|
||||
#include <string_view>
|
||||
#include <type_traits>
|
||||
#include <variant>
|
||||
#include <vector>
|
||||
|
||||
#include <uvw/async.h>
|
||||
|
||||
namespace llarp::quic
|
||||
{
|
||||
class Connection;
|
||||
|
||||
using bstring_view = std::basic_string_view<std::byte>;
|
||||
|
||||
// Shortcut for a const-preserving `reinterpret_cast`ing c.data() from a std::byte to a uint8_t
|
||||
// pointer, because we need it all over the place in the ngtcp2 API and I'd rather deal with
|
||||
// std::byte's out here for type safety.
|
||||
template <
|
||||
typename Container,
|
||||
typename = std::enable_if_t<
|
||||
sizeof(typename std::remove_reference_t<Container>::value_type) == sizeof(uint8_t)>>
|
||||
inline auto*
|
||||
u8data(Container&& c)
|
||||
{
|
||||
using u8_sameconst_t = std::conditional_t<
|
||||
std::is_const_v<std::remove_pointer_t<decltype(c.data())>>,
|
||||
const uint8_t,
|
||||
uint8_t>;
|
||||
return reinterpret_cast<u8_sameconst_t*>(c.data());
|
||||
}
|
||||
|
||||
// Type-safe wrapper around a int64_t stream id. Default construction is ngtcp2's special
|
||||
// "no-stream" id.
|
||||
struct StreamID
|
||||
{
|
||||
int64_t id{-1};
|
||||
bool
|
||||
operator==(const StreamID& s) const
|
||||
{
|
||||
return s.id == id;
|
||||
}
|
||||
bool
|
||||
operator!=(const StreamID& s) const
|
||||
{
|
||||
return s.id != id;
|
||||
}
|
||||
bool
|
||||
operator<(const StreamID& s) const
|
||||
{
|
||||
return s.id < id;
|
||||
}
|
||||
bool
|
||||
operator<=(const StreamID& s) const
|
||||
{
|
||||
return s.id <= id;
|
||||
}
|
||||
bool
|
||||
operator>(const StreamID& s) const
|
||||
{
|
||||
return s.id > id;
|
||||
}
|
||||
bool
|
||||
operator>=(const StreamID& s) const
|
||||
{
|
||||
return s.id >= id;
|
||||
}
|
||||
};
|
||||
|
||||
// Application error code we close with if the data handle throws
|
||||
constexpr uint64_t STREAM_EXCEPTION_ERROR_CODE = (1ULL << 62) - 2;
|
||||
|
||||
std::ostream&
|
||||
operator<<(std::ostream& o, const StreamID& s);
|
||||
} // namespace llarp::quic
|
||||
|
||||
namespace std
|
||||
{
|
||||
template <>
|
||||
struct hash<llarp::quic::StreamID>
|
||||
{
|
||||
size_t
|
||||
operator()(const llarp::quic::StreamID& s) const
|
||||
{
|
||||
return std::hash<decltype(s.id)>{}(s.id);
|
||||
}
|
||||
};
|
||||
} // namespace std
|
||||
|
||||
namespace llarp::quic
|
||||
{
|
||||
// Class for an established stream (a single connection has multiple streams): we have a
|
||||
// fixed-sized ring buffer for holding outgoing data, and a callback to invoke on received data.
|
||||
// To construct a Stream call `conn.open_stream()`.
|
||||
class Stream : public std::enable_shared_from_this<Stream>
|
||||
{
|
||||
public:
|
||||
// Returns the StreamID of this stream
|
||||
const StreamID&
|
||||
id() const
|
||||
{
|
||||
return stream_id;
|
||||
}
|
||||
|
||||
// Sets the size of the outgoing data buffer. This may *only* be used if the buffer is
|
||||
// currently entirely empty; otherwise a runtime_error is thrown. The minimum buffer size is
|
||||
// 2048, the default is 64kiB. A value of 0 puts the Stream into user-provided buffer mode
|
||||
// where only the version of `append` taking ownership of a char* is permitted.
|
||||
void
|
||||
set_buffer_size(size_t size);
|
||||
|
||||
// Returns the size of the buffer (including both pending and free space). If using
|
||||
// user-provided buffer mode then this is the sum of all held buffers.
|
||||
size_t
|
||||
buffer_size() const;
|
||||
|
||||
// Returns the number of free bytes available in the outgoing stream data buffer. Always 0 in
|
||||
// user-provided buffer mode.
|
||||
size_t
|
||||
available() const
|
||||
{
|
||||
return is_closing || buffer.empty() ? 0 : buffer.size() - size;
|
||||
}
|
||||
|
||||
// Returns the number of bytes currently referenced in the buffer (i.e. pending or
|
||||
// sent-but-unacknowledged).
|
||||
size_t
|
||||
used() const
|
||||
{
|
||||
return size;
|
||||
}
|
||||
|
||||
// Returns the number of bytes of the buffer that have been sent but not yet acknowledged and
|
||||
// thus are still required.
|
||||
size_t
|
||||
unacked() const
|
||||
{
|
||||
return unacked_size;
|
||||
}
|
||||
|
||||
// Returns the number of bytes of the buffer that have not yet been sent.
|
||||
size_t
|
||||
unsent() const
|
||||
{
|
||||
return used() - unacked();
|
||||
}
|
||||
|
||||
// Try to append all of the given bytes to the outgoing stream data buffer. Returns true if
|
||||
// successful, false (without appending anything) if there is insufficient space. If you want
|
||||
// to append as much as possible then use `append_any` instead.
|
||||
bool
|
||||
append(bstring_view data);
|
||||
bool
|
||||
append(std::string_view data)
|
||||
{
|
||||
return append(bstring_view{reinterpret_cast<const std::byte*>(data.data()), data.size()});
|
||||
}
|
||||
|
||||
// Append bytes to the outgoing stream data buffer, allowing partial consumption of data if the
|
||||
// entire provided data cannot be appended. Returns the number of appended bytes (which will be
|
||||
// less than the total provided if the provided data is larger than `available()`). If you want
|
||||
// an all-or-nothing append then use `append` instead.
|
||||
size_t
|
||||
append_any(bstring_view data);
|
||||
size_t
|
||||
append_any(std::string_view data)
|
||||
{
|
||||
return append_any(bstring_view{reinterpret_cast<const std::byte*>(data.data()), data.size()});
|
||||
}
|
||||
|
||||
// Takes ownership of the given buffer pointer, queuing it to be sent after any existing buffers
|
||||
// and freed once fully acked. You *must* have called `set_buffer_size(0)` (or set the
|
||||
// endpoints default_stream_buffer_size to 0) in order to use this.
|
||||
void
|
||||
append_buffer(const std::byte* buf, size_t length);
|
||||
|
||||
// Starting closing the stream and prevent any more outgoing data from being appended. If
|
||||
// `error_code` is provided then we close immediately with the given code; if std::nullopt (the
|
||||
// default) we close gracefully by sending a FIN bit.
|
||||
void
|
||||
close(std::optional<uint64_t> error_code = std::nullopt);
|
||||
|
||||
// Returns true if this Stream is closing (or already closed).
|
||||
bool
|
||||
closing() const
|
||||
{
|
||||
return is_closing;
|
||||
}
|
||||
|
||||
// Callback invoked when data is received
|
||||
using data_callback_t = std::function<void(Stream&, bstring_view)>;
|
||||
|
||||
// Callback invoked when the stream is closed
|
||||
using close_callback_t = std::function<void(Stream&, std::optional<uint64_t> error_code)>;
|
||||
|
||||
// Callback invoked when free stream buffer space becomes available. Should return true if the
|
||||
// callback is finished and can be discarded, false if the callback is still needed. If
|
||||
// returning false then it *must* have filled the stream's outgoing buffer (this is asserted in
|
||||
// a debug build).
|
||||
using unblocked_callback_t = std::function<bool(Stream&)>;
|
||||
|
||||
// Callback to invoke when we receive some incoming data; there's no particular guarantee on the
|
||||
// size of the data, just that this will always be called in sequential order.
|
||||
data_callback_t data_callback;
|
||||
|
||||
// Callback to invoke when the connection has closed. If the close was an abrupt stream close
|
||||
// initiated by the remote then `error_code` will be set to whatever code the remote side
|
||||
// provided; for graceful closing or locally initiated closing the error code will be null.
|
||||
close_callback_t close_callback;
|
||||
|
||||
// Queues a callback to be invoked when space becomes available for writing in the buffer. The
|
||||
// callback should true if it completed, false if it still needs more buffer space. If multiple
|
||||
// callbacks are queued they are invoked in order, space permitting. The stored std::function
|
||||
// will not be moved or copied after being invoked (i.e. if invoked multiple times it will
|
||||
// always be invoked on the same instance).
|
||||
//
|
||||
// Available callbacks should only be used when the buffer is full, typically immediately after
|
||||
// an `append_any` call that returns less than the full write. Similarly a false return from an
|
||||
// unblock function (which keeps the callback alive) should satisfy the same condition.
|
||||
//
|
||||
// In user-provided buffer mode the callback will be invoked after any data has been acked: it
|
||||
// is up to the caller to look at used()/buffer_size()/etc. to decide what to do. As described
|
||||
// above, return true to remove this callback, false to keep it and try again after the next
|
||||
// ack.
|
||||
void
|
||||
when_available(unblocked_callback_t unblocked_cb);
|
||||
|
||||
// Calls io_ready() on the stream's connection to scheduling sending outbound data
|
||||
void
|
||||
io_ready();
|
||||
|
||||
// Schedules processing of the "when_available" callbacks
|
||||
void
|
||||
available_ready();
|
||||
|
||||
// Lets you stash some arbitrary data in a shared_ptr; this is not used internally.
|
||||
void
|
||||
data(std::shared_ptr<void> data);
|
||||
|
||||
// Variation of data() that holds the pointer in a weak_ptr instead of a shared_ptr.
|
||||
void
|
||||
weak_data(std::weak_ptr<void> data);
|
||||
|
||||
// Retrieves the stashed data, with a static_cast to the desired type. This is used for
|
||||
// retrieval of both shared or weak data types (if held as a weak_ptr it is lock()ed first).
|
||||
template <typename T>
|
||||
std::shared_ptr<T>
|
||||
data() const
|
||||
{
|
||||
return std::static_pointer_cast<T>(
|
||||
std::holds_alternative<std::shared_ptr<void>>(user_data)
|
||||
? std::get<std::shared_ptr<void>>(user_data)
|
||||
: std::get<std::weak_ptr<void>>(user_data).lock());
|
||||
}
|
||||
|
||||
private:
|
||||
friend class Connection;
|
||||
|
||||
Stream(
|
||||
Connection& conn,
|
||||
data_callback_t data_cb,
|
||||
close_callback_t close_cb,
|
||||
size_t buffer_size,
|
||||
StreamID id = {-1});
|
||||
Stream(Connection& conn, StreamID id, size_t buffer_size);
|
||||
|
||||
// Non-copyable, non-movable; we manage it via a unique_ptr held by its Connection
|
||||
Stream(const Stream&) = delete;
|
||||
const Stream&
|
||||
operator=(const Stream&) = delete;
|
||||
Stream(Stream&&) = delete;
|
||||
Stream&
|
||||
operator=(Stream&&) = delete;
|
||||
|
||||
Connection& conn;
|
||||
|
||||
// Callback(s) to invoke once we have the requested amount of space available in the buffer.
|
||||
std::queue<unblocked_callback_t> unblocked_callbacks;
|
||||
void
|
||||
handle_unblocked(); // Processes the above if space is available
|
||||
|
||||
// Called to advance the number of acknowledged bytes (freeing up that space in the buffer for
|
||||
// appending data).
|
||||
void
|
||||
acknowledge(size_t bytes);
|
||||
|
||||
// Returns a view into unwritten stream data. This returns a vector of string_views of the data
|
||||
// to write, in order. After writing any of the provided data you must call `wrote()` to signal
|
||||
// how much of the given data was consumed (to advance the next pending() call).
|
||||
std::vector<bstring_view>
|
||||
pending();
|
||||
|
||||
// Called to signal that bytes have been written and should now be considered sent (but still
|
||||
// unacknowledged), thereby advancing the initial data position returned by the next `pending()`
|
||||
// call. Should typically be called after `pending()` to signal how much of the pending data
|
||||
// was actually used.
|
||||
void
|
||||
wrote(size_t bytes);
|
||||
|
||||
// ngtcp2 stream_id, assigned during stream creation
|
||||
StreamID stream_id{-1};
|
||||
|
||||
// ring buffer of outgoing stream data that has not yet been acknowledged. This cannot be
|
||||
// resized once used as ngtcp2 will have pointers into the data. If this is empty then we are
|
||||
// in user-provided buffer mode.
|
||||
std::vector<std::byte> buffer{65536};
|
||||
|
||||
// user-provided buffers; only used when `buffer` is empty (via a `set_buffer_size(0)` or a 0
|
||||
// size given in the constructor).
|
||||
std::deque<std::pair<std::unique_ptr<const std::byte[]>, size_t>> user_buffers;
|
||||
|
||||
// Offset of the first used byte in the circular buffer, will always be in [0, buffer.size()).
|
||||
// For user-provided buffers this is the starting offset in the currently sending user-provided
|
||||
// buffer.
|
||||
size_t start{0};
|
||||
|
||||
// Number of sent-but-unacked packets in the buffer (i.e. [start, start+unacked_size) are sent
|
||||
// but not yet acked).
|
||||
size_t unacked_size{0};
|
||||
|
||||
// Number of used bytes in the buffer; thus start+size is the next write location and
|
||||
// [start+unacked_size, start+size) is the range of not-yet-sent bytes. (Note that this
|
||||
// description is ignoring the circularity of the buffer).
|
||||
size_t size{0};
|
||||
|
||||
bool is_new{true};
|
||||
bool is_closing{false};
|
||||
bool sent_fin{false};
|
||||
bool is_shutdown{false};
|
||||
|
||||
// Async trigger we use to schedule when_available callbacks (so that we can make them happen in
|
||||
// batches rather than after each and every packet ack).
|
||||
std::shared_ptr<uvw::AsyncHandle> avail_trigger;
|
||||
|
||||
std::variant<std::shared_ptr<void>, std::weak_ptr<void>> user_data;
|
||||
};
|
||||
|
||||
} // namespace llarp::quic
|
@ -0,0 +1,111 @@
|
||||
#include "tunnel.hpp"
|
||||
#include "log.hpp"
|
||||
#include "stream.hpp"
|
||||
|
||||
namespace llarp::quic::tunnel
|
||||
{
|
||||
// Takes data from the tcp connection and pushes it down the quic tunnel
|
||||
void
|
||||
on_outgoing_data(uvw::DataEvent& event, uvw::TCPHandle& client)
|
||||
{
|
||||
auto stream = client.data<llarp::quic::Stream>();
|
||||
assert(stream);
|
||||
std::string_view data{event.data.get(), event.length};
|
||||
auto peer = client.peer();
|
||||
llarp::quic::Debug(peer.ip, ":", peer.port, " → lokinet ", llarp::quic::buffer_printer{data});
|
||||
// Steal the buffer from the DataEvent's unique_ptr<char[]>:
|
||||
stream->append_buffer(reinterpret_cast<const std::byte*>(event.data.release()), event.length);
|
||||
if (stream->used() >= PAUSE_SIZE)
|
||||
{
|
||||
llarp::quic::Debug(
|
||||
"quic tunnel is congested (have ",
|
||||
stream->used(),
|
||||
" bytes in flight); pausing local tcp connection reads");
|
||||
client.stop();
|
||||
stream->when_available([](llarp::quic::Stream& s) {
|
||||
auto client = s.data<uvw::TCPHandle>();
|
||||
if (s.used() < PAUSE_SIZE)
|
||||
{
|
||||
llarp::quic::Debug("quic tunnel is no longer congested; resuming tcp connection reading");
|
||||
client->read();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
llarp::quic::Debug("Queued ", event.length, " bytes");
|
||||
}
|
||||
}
|
||||
|
||||
// Received data from the quic tunnel and sends it to the TCP connection
|
||||
void
|
||||
on_incoming_data(llarp::quic::Stream& stream, llarp::quic::bstring_view bdata)
|
||||
{
|
||||
auto tcp = stream.data<uvw::TCPHandle>();
|
||||
assert(tcp);
|
||||
std::string_view data{reinterpret_cast<const char*>(bdata.data()), bdata.size()};
|
||||
auto peer = tcp->peer();
|
||||
llarp::quic::Debug(peer.ip, ":", peer.port, " ← lokinet ", llarp::quic::buffer_printer{data});
|
||||
|
||||
if (data.empty())
|
||||
return;
|
||||
|
||||
// Try first to write immediately from the existing buffer to avoid needing an
|
||||
// allocation and copy:
|
||||
auto written = tcp->tryWrite(const_cast<char*>(data.data()), data.size());
|
||||
if (written < (int)data.size())
|
||||
{
|
||||
data.remove_prefix(written);
|
||||
|
||||
auto wdata = std::make_unique<char[]>(data.size());
|
||||
std::copy(data.begin(), data.end(), wdata.get());
|
||||
tcp->write(std::move(wdata), data.size());
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
install_stream_forwarding(uvw::TCPHandle& tcp, llarp::quic::Stream& stream)
|
||||
{
|
||||
tcp.data(stream.shared_from_this());
|
||||
stream.weak_data(tcp.weak_from_this());
|
||||
|
||||
tcp.on<uvw::CloseEvent>([](auto&, uvw::TCPHandle& c) {
|
||||
// This fires sometime after we call `close()` to signal that the close is done.
|
||||
llarp::quic::Error(
|
||||
"Connection with ",
|
||||
c.peer().ip,
|
||||
":",
|
||||
c.peer().port,
|
||||
" closed directly, closing quic stream");
|
||||
c.data<llarp::quic::Stream>()->close();
|
||||
});
|
||||
tcp.on<uvw::EndEvent>([](auto&, uvw::TCPHandle& c) {
|
||||
// This fires on eof, most likely because the other side of the TCP connection closed it.
|
||||
llarp::quic::Error(
|
||||
"EOF on connection with ", c.peer().ip, ":", c.peer().port, ", closing quic stream");
|
||||
c.data<llarp::quic::Stream>()->close();
|
||||
});
|
||||
tcp.on<uvw::ErrorEvent>([](const uvw::ErrorEvent& e, uvw::TCPHandle& tcp) {
|
||||
llarp::quic::Error(
|
||||
"ErrorEvent[",
|
||||
e.name(),
|
||||
": ",
|
||||
e.what(),
|
||||
"] on connection with ",
|
||||
tcp.peer().ip,
|
||||
":",
|
||||
tcp.peer().port,
|
||||
", shutting down quic stream");
|
||||
// Failed to open connection, so close the quic stream
|
||||
auto stream = tcp.data<llarp::quic::Stream>();
|
||||
if (stream)
|
||||
stream->close(ERROR_TCP);
|
||||
tcp.close();
|
||||
});
|
||||
tcp.on<uvw::DataEvent>(tunnel::on_outgoing_data);
|
||||
stream.data_callback = on_incoming_data;
|
||||
}
|
||||
|
||||
} // namespace tunnel
|
@ -0,0 +1,54 @@
|
||||
#pragma once
|
||||
|
||||
#include "stream.hpp"
|
||||
#include "log.hpp"
|
||||
|
||||
#include <charconv>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
#include <uvw/tcp.h>
|
||||
|
||||
namespace llarp::quic::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;
|
||||
|
||||
// Callbacks for network events. The uvw::TCPHandle client must contain a shared pointer to the
|
||||
// associated llarp::quic::Stream in its data, and the llarp::quic::Stream must contain a weak
|
||||
// pointer to the uvw::TCPHandle.
|
||||
|
||||
// Callback when we receive data to go out over lokinet, i.e. read from the local TCP socket
|
||||
void
|
||||
on_outgoing_data(uvw::DataEvent& event, uvw::TCPHandle& client);
|
||||
|
||||
// Callback when we receive data from lokinet to write to the local TCP socket
|
||||
void
|
||||
on_incoming_data(llarp::quic::Stream& stream, llarp::quic::bstring_view bdata);
|
||||
|
||||
// Callback to handle and discard the first incoming 0x00 byte that initiates the stream
|
||||
void
|
||||
on_init_incoming_data(llarp::quic::Stream& stream, llarp::quic::bstring_view bdata);
|
||||
|
||||
// Creates a new tcp handle that forwards incoming data/errors/closes into appropriate actions on
|
||||
// the given quic stream.
|
||||
void
|
||||
install_stream_forwarding(uvw::TCPHandle& tcp, llarp::quic::Stream& stream);
|
||||
|
||||
} // namespace llarp::quic::tunnel
|
@ -0,0 +1,139 @@
|
||||
#include "connection.hpp"
|
||||
#include "client.hpp"
|
||||
#include "log.hpp"
|
||||
#include "stream.hpp"
|
||||
#include "tunnel.hpp"
|
||||
|
||||
#include <util/str.hpp>
|
||||
|
||||
#include <charconv>
|
||||
#include <iterator>
|
||||
|
||||
#include <uvw.hpp>
|
||||
|
||||
using namespace std::literals;
|
||||
|
||||
namespace llarp::quic::tunnel
|
||||
{
|
||||
// When we receive a new incoming connection we immediately initiate a new quic stream. This quic
|
||||
// stream in turn causes the other end to initiate a TCP connection on whatever port we specified
|
||||
// in the connection; if the connection is established, it sends back a single byte 0x00
|
||||
// (CONNECT_INIT); otherwise it shuts down the stream with an error code.
|
||||
void
|
||||
on_new_connection(const uvw::ListenEvent&, uvw::TCPHandle& server)
|
||||
{
|
||||
llarp::quic::Debug("New connection!\n");
|
||||
auto client = server.loop().resource<uvw::TCPHandle>();
|
||||
server.accept(*client);
|
||||
|
||||
auto conn = server.data<llarp::quic::Connection>();
|
||||
std::shared_ptr<llarp::quic::Stream> stream;
|
||||
try
|
||||
{
|
||||
llarp::quic::Debug("open stream");
|
||||
stream = conn->open_stream(
|
||||
[client](llarp::quic::Stream& stream, llarp::quic::bstring_view bdata) {
|
||||
if (bdata.empty())
|
||||
return;
|
||||
if (auto b0 = bdata[0]; b0 == tunnel::CONNECT_INIT)
|
||||
{
|
||||
// Set up callbacks, which replaces both of these initial callbacks
|
||||
client->read();
|
||||
tunnel::install_stream_forwarding(*client, stream);
|
||||
|
||||
if (bdata.size() > 1)
|
||||
{
|
||||
bdata.remove_prefix(1);
|
||||
stream.data_callback(stream, std::move(bdata));
|
||||
}
|
||||
llarp::quic::Debug("starting client reading");
|
||||
}
|
||||
else
|
||||
{
|
||||
llarp::quic::Warn(
|
||||
"Remote connection returned invalid initial byte (0x",
|
||||
oxenmq::to_hex(bdata.begin(), bdata.begin() + 1),
|
||||
"); dropping connection");
|
||||
client->closeReset();
|
||||
stream.close(tunnel::ERROR_BAD_INIT);
|
||||
}
|
||||
stream.io_ready();
|
||||
},
|
||||
[client](llarp::quic::Stream&, std::optional<uint64_t> error_code) mutable {
|
||||
if (error_code && *error_code == tunnel::ERROR_CONNECT)
|
||||
llarp::quic::Debug("Remote TCP connection failed, closing local connection");
|
||||
else
|
||||
llarp::quic::Warn(
|
||||
"Stream connection closed ",
|
||||
error_code ? "with error " + std::to_string(*error_code) : "gracefully",
|
||||
"; closing local TCP connection.");
|
||||
auto peer = client->peer();
|
||||
llarp::quic::Debug("Closing connection to ", peer.ip, ":", peer.port);
|
||||
if (error_code)
|
||||
client->closeReset();
|
||||
else
|
||||
client->close();
|
||||
});
|
||||
stream->io_ready();
|
||||
}
|
||||
catch (const std::exception& e)
|
||||
{
|
||||
llarp::quic::Debug("open stream failed");
|
||||
client->closeReset();
|
||||
return;
|
||||
}
|
||||
|
||||
llarp::quic::Debug("setup stream");
|
||||
conn->io_ready();
|
||||
}
|
||||
|
||||
int
|
||||
usage(std::string_view arg0, std::string_view msg)
|
||||
{
|
||||
std::cerr << msg << "\n\n"
|
||||
<< "Usage: " << arg0
|
||||
<< " [DESTPORT [SERVERPORT [LISTENPORT]]]\n\nDefaults to ports 4444 4242 5555\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
int
|
||||
main(int argc, char* argv[])
|
||||
{
|
||||
auto loop = uvw::Loop::create();
|
||||
|
||||
std::array<uint16_t, 3> ports{{4444, 4242, 5555}};
|
||||
for (size_t i = 0; i < ports.size(); i++)
|
||||
{
|
||||
if (argc < 2 + (int)i)
|
||||
break;
|
||||
if (!parse_int(argv[1 + i], ports[i]))
|
||||
return usage(argv[0], "Invalid port "s + argv[1 + i]);
|
||||
}
|
||||
auto& [dest_port, server_port, listen_port] = ports;
|
||||
std::cout << "Connecting to quic server at localhost:" << server_port
|
||||
<< " to reach tunneled port " << dest_port
|
||||
<< ", listening on localhost:" << listen_port << "\n";
|
||||
|
||||
signal(SIGPIPE, SIG_IGN);
|
||||
|
||||
llarp::quic::Debug("Initializing client");
|
||||
auto tunnel_client = std::make_shared<llarp::quic::Client>(
|
||||
llarp::quic::Address{{127, 0, 0, 1}, server_port}, // server addr
|
||||
loop,
|
||||
dest_port // tunnel destination port
|
||||
);
|
||||
tunnel_client->default_stream_buffer_size = 0; // We steal uvw's provided buffers
|
||||
llarp::quic::Debug("Initialized client");
|
||||
|
||||
// Start listening for TCP connections:
|
||||
auto server = loop->resource<uvw::TCPHandle>();
|
||||
server->data(tunnel_client->get_connection());
|
||||
server->on<uvw::ListenEvent>(llarp::quic::tunnel::on_new_connection);
|
||||
|
||||
server->bind("127.0.0.1", listen_port);
|
||||
server->listen();
|
||||
|
||||
loop->run();
|
||||
}
|
||||
|
||||
} // namespace llarp::quic::tunnel
|
@ -0,0 +1,174 @@
|
||||
#include "tunnel_server.hpp"
|
||||
#include "tunnel.hpp"
|
||||
#include "connection.hpp"
|
||||
#include "server.hpp"
|
||||
#include "log.hpp"
|
||||
|
||||
#include <util/str.hpp>
|
||||
|
||||
#include <uvw/tcp.h>
|
||||
|
||||
using namespace std::literals;
|
||||
|
||||
namespace llarp::quic::tunnel
|
||||
{
|
||||
IncomingTunnel::IncomingTunnel(uint16_t localhost_port)
|
||||
: IncomingTunnel{
|
||||
[localhost_port](
|
||||
[[maybe_unused]] const auto& remote, uint16_t port, SockAddr& connect_to) {
|
||||
if (port != localhost_port)
|
||||
return AcceptResult::DECLINE;
|
||||
connect_to.setIPv4(127, 0, 0, 1);
|
||||
connect_to.setPort(port);
|
||||
return AcceptResult::ACCEPT;
|
||||
}}
|
||||
{}
|
||||
|
||||
int
|
||||
usage(std::string_view arg0, std::string_view msg)
|
||||
{
|
||||
std::cerr << msg << "\n\n"
|
||||
<< "Usage: " << arg0
|
||||
<< " [LISTENPORT [ALLOWED ...]]\n\nDefaults to listening on 4242 and allowing "
|
||||
"22,80,4444,8080\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
int
|
||||
main(int argc, char* argv[])
|
||||
{
|
||||
uint16_t listen_port = 4242;
|
||||
std::set<uint16_t> allowed_ports{{22, 80, 4444, 8080}};
|
||||
|
||||
if (argc >= 2 && !parse_int(argv[1], listen_port))
|
||||
return usage(argv[0], "Invalid port "s + argv[1]);
|
||||
if (argc >= 3)
|
||||
{
|
||||
allowed_ports.clear();
|
||||
for (int i = 2; i < argc; i++)
|
||||
{
|
||||
if (argv[i] == "all"sv)
|
||||
{
|
||||
allowed_ports.clear();
|
||||
break;
|
||||
}
|
||||
uint16_t port;
|
||||
if (!parse_int(argv[i], port))
|
||||
return usage(argv[0], "Invalid port "s + argv[i]);
|
||||
allowed_ports.insert(port);
|
||||
}
|
||||
}
|
||||
|
||||
auto loop = uvw::Loop::create();
|
||||
|
||||
Address listen_addr{{0, 0, 0, 0}, listen_port};
|
||||
|
||||
signal(SIGPIPE, SIG_IGN);
|
||||
|
||||
// The local address we connect to for incoming connections. (localhost for this demo, should
|
||||
// be the localhost.loki address for lokinet).
|
||||
std::string localhost = "127.0.0.1";
|
||||
|
||||
llarp::quic::Debug("Initializing server");
|
||||
llarp::quic::Server s{
|
||||
listen_addr,
|
||||
loop,
|
||||
[loop, localhost, allowed_ports](
|
||||
llarp::quic::Server&, llarp::quic::Stream& stream, uint16_t port) {
|
||||
llarp::quic::Debug(
|
||||
"\e[33mNew incoming quic stream ",
|
||||
stream.id(),
|
||||
" to reach ",
|
||||
localhost,
|
||||
":",
|
||||
port,
|
||||
"\e[0m");
|
||||
if (port == 0 || !(allowed_ports.empty() || allowed_ports.count(port)))
|
||||
{
|
||||
llarp::quic::Warn(
|
||||
"quic stream denied by configuration: ", port, " is not a permitted local port");
|
||||
return false;
|
||||
}
|
||||
/*
|
||||
stream.data_callback = [init_seen=false](llarp::quic::Stream& stream,
|
||||
llarp::quic::bstring_view bdata) mutable { if (init_seen) { llarp::quic::Warn("Invalid
|
||||
remote data: received multiple bytes before connection confirmation");
|
||||
}
|
||||
};
|
||||
*/
|
||||
stream.close_callback = [](llarp::quic::Stream& strm,
|
||||
std::optional<uint64_t> error_code) {
|
||||
llarp::quic::Debug(
|
||||
error_code ? "Remote side" : "We",
|
||||
" closed the quic stream, closing localhost tcp connection");
|
||||
if (error_code && *error_code > 0)
|
||||
llarp::quic::Warn("Remote quic stream was closed with error code ", *error_code);
|
||||
auto tcp = strm.data<uvw::TCPHandle>();
|
||||
if (!tcp)
|
||||
llarp::quic::Debug("Local TCP connection already closed");
|
||||
else
|
||||
tcp->close();
|
||||
};
|
||||
// Try to open a TCP connection to the configured localhost port; if we establish a
|
||||
// connection then we immediately send a CONNECT_INIT back down the stream; if we fail
|
||||
// then we send a fail-to-connect error code. Once we successfully connect both of
|
||||
// these handlers get replaced with the normal tunnel handlers.
|
||||
auto tcp = loop->resource<uvw::TCPHandle>();
|
||||
auto error_handler = tcp->once<uvw::ErrorEvent>(
|
||||
[&stream, localhost, port](const uvw::ErrorEvent&, uvw::TCPHandle&) {
|
||||
llarp::quic::Error(
|
||||
"Failed to connect to ", localhost, ":", port, ", shutting down quic stream");
|
||||
stream.close(tunnel::ERROR_CONNECT);
|
||||
});
|
||||
tcp->once<uvw::ConnectEvent>(
|
||||
[streamw = stream.weak_from_this(), error_handler = std::move(error_handler)](
|
||||
const uvw::ConnectEvent&, uvw::TCPHandle& tcp) {
|
||||
auto peer = tcp.peer();
|
||||
auto stream = streamw.lock();
|
||||
if (!stream)
|
||||
{
|
||||
llarp::quic::Warn(
|
||||
"Connected to ",
|
||||
peer.ip,
|
||||
":",
|
||||
peer.port,
|
||||
" but quic stream has gone away; resetting local connection");
|
||||
tcp.closeReset();
|
||||
return;
|
||||
}
|
||||
llarp::quic::Debug(
|
||||
"\e[32mConnected to ",
|
||||
peer.ip,
|
||||
":",
|
||||
peer.port,
|
||||
" for quic ",
|
||||
stream->id(),
|
||||
"\e[0m");
|
||||
tcp.erase(error_handler);
|
||||
tunnel::install_stream_forwarding(tcp, *stream);
|
||||
assert(stream->used() == 0);
|
||||
|
||||
stream->append_buffer(new std::byte[1]{tunnel::CONNECT_INIT}, 1);
|
||||
tcp.read();
|
||||
});
|
||||
|
||||
tcp->connect("127.0.0.1", port);
|
||||
|
||||
return true;
|
||||
}};
|
||||
s.default_stream_buffer_size = 0; // We steal uvw's provided buffers
|
||||
llarp::quic::Debug("Initialized server");
|
||||
std::cout << "Listening on localhost:" << listen_port
|
||||
<< " with tunnel(s) to localhost port(s):";
|
||||
if (allowed_ports.empty())
|
||||
std::cout << " (any)";
|
||||
for (auto p : allowed_ports)
|
||||
std::cout << ' ' << p;
|
||||
std::cout << '\n';
|
||||
|
||||
loop->run();
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
} // namespace llarp::quic::tunnel
|
@ -0,0 +1,80 @@
|
||||
#pragma once
|
||||
|
||||
#include <net/sock_addr.hpp>
|
||||
#include <service/address.hpp>
|
||||
#include <ev/ev.hpp>
|
||||
|
||||
#include <memory>
|
||||
|
||||
namespace llarp::quic::tunnel
|
||||
{
|
||||
enum class AcceptResult : int
|
||||
{
|
||||
ACCEPT = 0, // Accepts a connection
|
||||
DECLINE = -1, // Declines a connection (try other callbacks, refuse if all decline)
|
||||
REFUSE = -2, // Refuses a connection (don't try any more callbacks)
|
||||
};
|
||||
|
||||
// Class that wraps an incoming connection acceptance callback (to allow for callback removal).
|
||||
// This is not directly constructible: you must construct it via the TunnelServer instance.
|
||||
class IncomingTunnel final
|
||||
{
|
||||
public:
|
||||
using AcceptCallback = std::function<AcceptResult(
|
||||
const llarp::service::Address& remote, uint16_t port, llarp::SockAddr& connect_to)>;
|
||||
|
||||
private:
|
||||
AcceptCallback accept;
|
||||
|
||||
friend class TunnelServer;
|
||||
|
||||
// Constructor with a full callback; invoked via TunnelServer::add_incoming_tunnel
|
||||
explicit IncomingTunnel(AcceptCallback accept) : accept{std::move(accept)}
|
||||
{}
|
||||
|
||||
// Constructor for a simple forwarding to a single localhost port. E.g. IncomingTunnel(22)
|
||||
// allows incoming connections to reach port 22 and forwards them to localhost:22.
|
||||
explicit IncomingTunnel(uint16_t localhost_port);
|
||||
|
||||
// Constructor for forwarding everything to the same port; this is used by full clients by
|
||||
// default.
|
||||
IncomingTunnel();
|
||||
};
|
||||
|
||||
// Class that handles incoming quic connections. This class sets itself up in the llarp event
|
||||
// loop on construction and maintains a list of incoming acceptor callbacks. When a new incoming
|
||||
// quic connections is being established we try the callbacks one by one to determine the local
|
||||
// TCP port the tunnel should be connected to until:
|
||||
// - a callback sets connect_to and returns AcceptResult::ACCEPT - we connect it to the returned
|
||||
// address
|
||||
// - a callback returns AcceptResult::REFUSE - we reject the connection
|
||||
//
|
||||
// If a callback returns AcceptResult::DECLINE then we skip that callback and try the next one; if
|
||||
// all callbacks decline (or we have no callbacks at all) then we reject the connection.
|
||||
//
|
||||
// Note that tunnel operations and initialization are done in the event loop thread and so will
|
||||
// not take effect until the next event loop tick when called from some other thread.
|
||||
class TunnelServer : public std::enable_shared_from_this<TunnelServer>
|
||||
{
|
||||
public:
|
||||
explicit TunnelServer(EventLoop_ptr ev);
|
||||
|
||||
// Appends a new tunnel to the end of the queue; all arguments are forwarded to private
|
||||
// constructor(s) of IncomingTunnel.
|
||||
template <typename... Args>
|
||||
std::shared_ptr<IncomingTunnel>
|
||||
add_incoming_tunnel(Args&&... args)
|
||||
{
|
||||
return std::shared_ptr<IncomingTunnel>{new IncomingTunnel{std::forward<Args>(args)...}};
|
||||
}
|
||||
|
||||
// Removes a tunnel acceptor from the acceptor queue.
|
||||
void
|
||||
remove_incoming_tunnel(std::weak_ptr<IncomingTunnel> tunnel);
|
||||
|
||||
private:
|
||||
EventLoop_ptr ev;
|
||||
std::vector<std::shared_ptr<IncomingTunnel>> tunnels;
|
||||
};
|
||||
|
||||
} // namespace llarp::quic::tunnel
|
Loading…
Reference in New Issue