Use QUIC as transport protocoll

Fixes: #7001
This commit is contained in:
phosit 2026-05-20 21:12:39 +02:00
parent 61bcc38458
commit 996a5482d3
No known key found for this signature in database
GPG key ID: C9430B600671C268
19 changed files with 1545 additions and 371 deletions

View file

@ -72,7 +72,7 @@ CNetClient::CNetClient(CGame* game, std::string serverAddressOrHostname, std::ui
CNetClient{PrivateTag{}, game, std::move(serverAddressOrHostname), serverPort, username, hostJID,
std::move(hashedPassword), std::move(controllerSecret)}
{
SetupConnection(nullptr);
SetupConnection();
}
CNetClient::CNetClient(PrivateTag, CGame* game, std::string serverAddressOrHostname,
@ -169,10 +169,10 @@ CNetClient::~CNetClient()
}
void CNetClient::SetupConnection(ENetHost* enetClient)
void CNetClient::SetupConnection()
{
CNetClientSession* session = new CNetClientSession(*this);
bool ok = session->Connect(m_ServerAddressOrHostname, m_ServerPort, enetClient);
bool ok = session->Connect(m_ServerAddressOrHostname, m_ServerPort);
SetAndOwnSession(session);
if (ok)
m_PollingThread = std::thread(Threading::HandleExceptions<CNetClientSession::RunNetLoop>::Wrapper, m_Session);
@ -284,7 +284,7 @@ bool CNetClient::TryToConnectWithSTUN(std::string serverAddressOrHostname, std::
try
{
g_NetClient->SetupConnection(enetClient);
g_NetClient->SetupConnection();
}
catch (...)
{
@ -463,6 +463,7 @@ bool CNetClient::SendMessage(const CNetMessage* message)
void CNetClient::HandleConnect()
{
LOGMESSAGE("Net client: Connected", m_ServerAddressOrHostname, m_ServerPort);
Update((uint)NMT_CONNECT_COMPLETE, NULL);
}

View file

@ -289,7 +289,7 @@ private:
* Set up a connection to the remote networked server.
* @return true on success, false on connection failure
*/
void SetupConnection(ENetHost* enetClient);
void SetupConnection();
/**
* Take ownership of a session object, and use it for all network communication.

View file

@ -22,8 +22,8 @@
#include "lib/code_generation.h"
#include "lib/debug.h"
#include "network/NetClient.h"
#include "network/NetEnet.h"
#include "network/NetMessage.h"
#include "network/NetProtocol.h"
#include "network/NetStats.h"
#include "ps/CLogger.h"
#include "ps/ProfileViewer.h"
@ -32,43 +32,405 @@
constexpr int NETCLIENT_POLL_TIMEOUT = 50;
constexpr int CHANNEL_COUNT = 1;
#include <time.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
#include <arpa/inet.h>
#include <string.h>
#include <stdio.h>
#include <errno.h>
#include <poll.h>
#include <ngtcp2/ngtcp2.h>
#include <ngtcp2/ngtcp2_crypto.h>
#include <ngtcp2/ngtcp2_crypto_gnutls.h>
#include <gnutls/crypto.h>
#include <gnutls/gnutls.h>
struct CNetClientSession::Quic
{
AddressStorage localAddress;
std::unique_ptr<gnutls_certificate_credentials_st, CredentialsDeleter> credentials;
std::unique_ptr<gnutls_session_int, SessionDeleter> session;
std::unique_ptr<ngtcp2_conn, ConnectionDeleter> quicConnection;
ngtcp2_crypto_conn_ref connectionReference;
int fd;
std::optional<Stream> streams;
};
namespace
{
struct CreateSocketResult
{
int descriptor;
AddressStorage address;
};
CreateSocketResult CreateSocket(const char* host, const std::uint16_t port)
{
addrinfo hints{};
hints.ai_flags = AF_UNSPEC;
hints.ai_socktype = SOCK_DGRAM;
addrinfo* res;
const int rv{getaddrinfo(host, fmt::format("{}", port).c_str(), &hints, &res)};
if (rv)
throw std::runtime_error{fmt::format("getaddrinfo: {}", gai_strerror(rv))};
std::unique_ptr<addrinfo, decltype(&freeaddrinfo)> infoList{res, &freeaddrinfo};
addrinfo* rp;
int fd{-1};
for (rp = res; rp; rp = rp->ai_next)
{
fd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol);
if (fd != -1)
break;
}
if (fd == -1)
throw std::runtime_error{"unable to create a socket"};
CreateSocketResult result{
.descriptor{fd},
.address{.length{rp->ai_addrlen}}
};
if (rp->ai_family == AF_INET6)
result.address.address.in6 = reinterpret_cast<ngtcp2_sockaddr_in6&>(*rp->ai_addr);
else
result.address.address.in = reinterpret_cast<ngtcp2_sockaddr_in&>(*rp->ai_addr);
return result;
}
AddressStorage ConnectSocket(const int fd, const AddressStorage& remoteAddress)
{
if (connect(fd, &remoteAddress.address.sa, remoteAddress.length))
throw std::runtime_error{fmt::format("connect: {}", strerror(errno))};
ngtcp2_sockaddr_union localAddress;
ngtcp2_socklen localAddressLength{sizeof(localAddress)};
if (getsockname(fd, &localAddress.sa, &localAddressLength) == -1)
throw std::runtime_error{fmt::format("getsockname: {}", strerror(errno))};
return {localAddress, localAddressLength};
}
void ClientGnutlsInit(CNetClientSession::Quic* c)
{
gnutls_certificate_credentials_t tempCred;
const int allocRet{gnutls_certificate_allocate_credentials(&tempCred)};
if (allocRet)
{
throw std::runtime_error{fmt::format("cred init failed: {}: {}", allocRet,
gnutls_strerror(allocRet))};
}
c->credentials.reset(tempCred);
gnutls_session_t tempSession;
if (const int initRet{gnutls_init(&tempSession, GNUTLS_CLIENT | GNUTLS_ENABLE_EARLY_DATA |
GNUTLS_NO_END_OF_EARLY_DATA)})
{
throw std::runtime_error{fmt::format("gnutls_init: {}", gnutls_strerror(initRet))};
}
c->session.reset(tempSession);
if (ngtcp2_crypto_gnutls_configure_client_session(c->session.get()))
throw std::runtime_error{"ngtcp2_crypto_gnutls_configure_client_session failed"};
if (const int priorityRet{gnutls_priority_set_direct(c->session.get(), TLS_PRIORITY, nullptr)})
{
throw std::runtime_error{fmt::format("gnutls_priority_set_direct: {}",
gnutls_strerror(priorityRet))};
}
gnutls_session_set_ptr(c->session.get(), &c->connectionReference);
if (const int setRet{gnutls_credentials_set(c->session.get(), GNUTLS_CRD_CERTIFICATE,
c->credentials.get())})
{
throw std::runtime_error{fmt::format("gnutls_credentials_set: {}", gnutls_strerror(setRet))};
}
}
int OpenStream(ngtcp2_conn*, const std::int64_t streamId, void* userData)
{
CNetClientSession& session{*static_cast<CNetClientSession*>(userData)};
session.m_Connected = true;
session.m_WasConnected = true;
session.m_Quic->streams.emplace(streamId);
session.m_IncomingMessages.push(CNetClientSession::ConnectionEstablished{});
return 0;
}
int OnStreamDataReceive(ngtcp2_conn* conn, const std::uint32_t /*flags*/, const std::int64_t streamId,
const std::size_t /*offset*/, const std::uint8_t* data, std::size_t dataSize, void* userData, void*)
{
auto& session = *static_cast<CNetClientSession*>(userData);
auto message = session.m_Quic->streams.value().Receive({data, dataSize});
if (message.has_value())
session.m_IncomingMessages.push(new std::vector<std::uint8_t>{std::move(message).value()});
increaseWindow(conn, streamId, dataSize);
return 0;
}
void ClientQuicInit(CNetClientSession& session, const AddressStorage& remote, const AddressStorage& local)
{
const ngtcp2_path path{
.local{
.addr{const_cast<ngtcp2_sockaddr*>(&local.address.sa)},
.addrlen{local.length},
},
.remote{
.addr{const_cast<ngtcp2_sockaddr*>(&remote.address.sa)},
.addrlen{remote.length},
}
};
constexpr ngtcp2_callbacks callbacks{
.client_initial{&ngtcp2_crypto_client_initial_cb},
.recv_crypto_data{&ngtcp2_crypto_recv_crypto_data_cb},
.encrypt{&ngtcp2_crypto_encrypt_cb},
.decrypt{&ngtcp2_crypto_decrypt_cb},
.hp_mask{&ngtcp2_crypto_hp_mask_cb},
.recv_stream_data{&OnStreamDataReceive},
.stream_open{&OpenStream},
.recv_retry{&ngtcp2_crypto_recv_retry_cb},
.rand{&OnRandomRequest},
.get_new_connection_id{&OnNewConnectionIdRequest},
.update_key{&ngtcp2_crypto_update_key_cb},
.delete_crypto_aead_ctx{&ngtcp2_crypto_delete_crypto_aead_ctx_cb},
.delete_crypto_cipher_ctx{&ngtcp2_crypto_delete_crypto_cipher_ctx_cb},
.get_path_challenge_data{&ngtcp2_crypto_get_path_challenge_data_cb},
.version_negotiation{&ngtcp2_crypto_version_negotiation_cb}
};
ngtcp2_cid dcid;
dcid.datalen = NGTCP2_MIN_INITIAL_DCIDLEN;
if (gnutls_rnd(GNUTLS_RND_RANDOM, dcid.data, dcid.datalen))
throw std::runtime_error{"gnutls_rnd failed"};
ngtcp2_cid scid;
scid.datalen = 8;
if (gnutls_rnd(GNUTLS_RND_RANDOM, scid.data, scid.datalen))
throw std::runtime_error{"gnutls_rnd failed"};
ngtcp2_settings settings;
ngtcp2_settings_default(&settings);
settings.initial_ts = timestamp();
ngtcp2_transport_params params;
ngtcp2_transport_params_default(&params);
params.initial_max_stream_data_bidi_remote = 128 * KiB;
params.initial_max_data = 1 * MiB;
params.initial_max_streams_bidi = 1;
params.max_udp_payload_size = MAX_UDP_PAYLOAD_SIZE;
params.grease_quic_bit = 1;
ngtcp2_conn* tempConn;
if (const int rv{ngtcp2_conn_client_new(&tempConn, &dcid, &scid, &path,
NGTCP2_PROTO_VER_V1, &callbacks, &settings, &params, nullptr, &session)})
{
throw std::runtime_error{fmt::format("ngtcp2_conn_client_new: {}", ngtcp2_strerror(rv))};
}
session.m_Quic->quicConnection.reset(tempConn);
ngtcp2_conn_set_tls_native_handle(session.m_Quic->quicConnection.get(),
session.m_Quic->session.get());
}
void ClientRead(CNetClientSession::Quic* c) {
std::array<std::uint8_t, MAX_UDP_PAYLOAD_SIZE> buf;
struct sockaddr_storage addr;
iovec iov{
.iov_base = buf.data(),
.iov_len = buf.size(),
};
msghdr msg{};
msg.msg_name = &addr;
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
ngtcp2_pkt_info pi{};
while (true)
{
msg.msg_namelen = sizeof(addr);
const ssize_t nread{recvmsg(c->fd, &msg, MSG_DONTWAIT)};
if (nread == -1)
{
if (errno != EAGAIN && errno != EWOULDBLOCK)
LOGERROR("recvmsg: %s", strerror(errno));
break;
}
ngtcp2_path path{
.local{
.addr{&c->localAddress.address.sa},
.addrlen{c->localAddress.length}
},
.remote{
.addr{static_cast<ngtcp2_sockaddr*>(msg.msg_name)},
.addrlen{msg.msg_namelen}
}
};
const int rv{ngtcp2_conn_read_pkt(c->quicConnection.get(), &path, &pi, buf.data(),
static_cast<std::size_t>(nread), timestamp())};
if (rv != 0)
throw std::runtime_error{fmt::format("ngtcp2_conn_read_pkt: {}", ngtcp2_strerror(rv))};
}
}
void ClientSendDatagram(CNetClientSession::Quic* c, const std::span<const std::uint8_t> data)
{
iovec iov{
.iov_base = const_cast<std::uint8_t*>(data.data()),
.iov_len = data.size(),
};
msghdr msg{};
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
ssize_t nwrite;
do
{
nwrite = sendmsg(c->fd, &msg, 0);
} while (nwrite == -1 && errno == EINTR);
if (nwrite == -1)
throw std::runtime_error{fmt::format("sendmsg: {}", strerror(errno))};
}
void ClientWriteStreams(CNetClientSession::Quic* c)
{
const ngtcp2_tstamp ts{timestamp()};
ngtcp2_pkt_info pi;
std::array<uint8_t, MAX_UDP_PAYLOAD_SIZE> buffer;
ngtcp2_path_storage ps;
ngtcp2_path_storage_zero(&ps);
std::uint32_t flags{NGTCP2_WRITE_STREAM_FLAG_MORE};
while (true)
{
ngtcp2_vec datav;
std::int64_t streamId;
const auto bytesToSend = c->streams.has_value() ? c->streams.value().PeekData() : std::nullopt;
if (c->streams.has_value() && bytesToSend.has_value())
{
datav.base = const_cast<uint8_t*>(bytesToSend->data());
datav.len = bytesToSend->size();
streamId = c->streams.value().m_Id;
}
else
{
datav.base = nullptr;
datav.len = 0;
streamId = -1;
if (c->streams.has_value())
flags &= ~NGTCP2_WRITE_STREAM_FLAG_MORE;
}
ngtcp2_ssize wdatalen;
const ngtcp2_ssize nwrite = ngtcp2_conn_writev_stream(c->quicConnection.get(), &ps.path, &pi,
buffer.data(), buffer.size(), &wdatalen, flags, streamId, &datav, 1, ts);
if (nwrite < 0)
{
if (nwrite == NGTCP2_ERR_STREAM_DATA_BLOCKED)
{
LOGWARNING("blocked");
break;
}
if (nwrite != NGTCP2_ERR_WRITE_MORE)
{
throw std::runtime_error{fmt::format("ngtcp2_conn_writev_stream: {}",
ngtcp2_strerror(static_cast<int>(nwrite)))};
}
if (c->streams.has_value() && wdatalen > 0)
c->streams.value().MarkSent(wdatalen);
continue;
}
if (nwrite == 0)
break;
if (c->streams.has_value() && wdatalen > 0)
c->streams.value().MarkSent(wdatalen);
ClientSendDatagram(c, {buffer.data(), static_cast<std::size_t>(nwrite)});
}
ngtcp2_conn_update_pkt_tx_time(c->quicConnection.get(), timestamp());
}
void ClientHandleExpiry(CNetClientSession::Quic* c)
{
if (const int rv = ngtcp2_conn_handle_expiry(c->quicConnection.get(), timestamp()))
throw std::runtime_error{fmt::format("ngtcp2_conn_handle_expiry: {}", ngtcp2_strerror(rv))};
}
ngtcp2_conn *GetConnection(ngtcp2_crypto_conn_ref* conn_ref)
{
CNetClientSession::Quic* c{static_cast<CNetClientSession::Quic*>(conn_ref->user_data)};
return c->quicConnection.get();
}
void ClientInit(CNetClientSession& session, const char* host, const std::uint16_t port)
{
*session.m_Quic = CNetClientSession::Quic{};
const auto [descriptor, remoteAddress] = CreateSocket(host, port);
session.m_Quic->fd = descriptor;
session.m_Quic->localAddress = ConnectSocket(session.m_Quic->fd, remoteAddress);
ClientGnutlsInit(session.m_Quic.get());
ClientQuicInit(session, remoteAddress, session.m_Quic->localAddress);
session.m_Quic->connectionReference.get_conn = &GetConnection;
session.m_Quic->connectionReference.user_data = session.m_Quic.get();
}
} // anonymous namespace
CNetClientSession::CNetClientSession(CNetClient& client) :
m_Client(client), m_FileTransferer(*this)
m_Client(client), m_FileTransferer(*this),
m_Quic(std::make_unique<Quic>())
{
}
CNetClientSession::~CNetClientSession()
{
ENSURE(!m_LoopRunning);
constexpr ngtcp2_ccerr reason{
.type{NGTCP2_CCERR_TYPE_TRANSPORT},
.error_code{0}
};
std::array<std::uint8_t, MAX_UDP_PAYLOAD_SIZE> buffer;
const ngtcp2_ssize amount{ngtcp2_conn_write_connection_close(m_Quic->quicConnection.get(), nullptr, nullptr, buffer.data(),
buffer.size(), &reason, timestamp())};
if (amount <= 0)
{
LOGERROR("closing connection %s", ngtcp2_strerror(amount));
}
ClientSendDatagram(m_Quic.get(), {buffer.data(), static_cast<std::size_t>(amount)});
}
bool CNetClientSession::Connect(const CStr& server, const u16 port, ENetHost* enetClient)
bool CNetClientSession::Connect(const CStr& server, const u16 port)
{
ENSURE(!m_LoopRunning);
ENSURE(!m_Host);
ENSURE(!m_Server);
// Create ENet host if necessary.
m_Host.reset(enetClient != nullptr ? enetClient : PS::Enet::CreateHost(nullptr, 1, CHANNEL_COUNT));
ClientInit(*this, server.c_str(), port);
ClientWriteStreams(m_Quic.get());
if (!m_Host)
return false;
// Bind to specified host
ENetAddress addr;
addr.port = port;
if (enet_address_set_host(&addr, server.c_str()) < 0)
return false;
// Initiate connection to server
m_Server.reset(enet_host_connect(m_Host.get(), &addr, CHANNEL_COUNT, 0));
if (!m_Server)
return false;
m_Stats = std::make_unique<CNetStatsTable>(*m_Server);
m_Stats = std::make_unique<CNetStatsTable>(m_Quic->quicConnection.get());
if (CProfileViewer::IsInitialised())
g_ProfileViewer.AddRootTable(m_Stats.get());
@ -84,14 +446,24 @@ void CNetClientSession::RunNetLoop(CNetClientSession* session)
while (!session->m_ShouldShutdown)
{
ENSURE(session->m_Host && session->m_Server);
// ENSURE(session->m_Host && session->m_Server);
session->m_FileTransferer.Poll();
session->Poll();
try {
session->Poll();
}
catch (std::runtime_error&)
{
// Report immediately.
LOGMESSAGE("Net client: Disconnected");
session->m_Connected = false;
session->m_IncomingMessages.push(Disconnect{});
return;
}
session->Flush();
session->m_LastReceivedTime = enet_time_get() - session->m_Server->lastReceiveTime;
session->m_MeanRTT = session->m_Server->roundTripTime;
// session->m_LastReceivedTime = timestamp() - session->m_Server->lastReceiveTime;
// session->m_MeanRTT = session->m_Server->roundTripTime;
}
session->m_LoopRunning = false;
@ -107,96 +479,77 @@ void CNetClientSession::Shutdown()
void CNetClientSession::Poll()
{
ENetEvent event;
pollfd pfd{
.fd{m_Quic->fd},
.events{POLLIN}
};
// Use the timeout to make the thread wait and save CPU time.
if (enet_host_service(m_Host.get(), &event, NETCLIENT_POLL_TIMEOUT) <= 0)
const int ret{poll(&pfd, 1, NETCLIENT_POLL_TIMEOUT)};
if (ret < 0)
{
LOGERROR("Error while waiting for poll: %s", std::strerror(errno));
return;
if (event.type == ENET_EVENT_TYPE_CONNECT)
{
ENSURE(event.peer == m_Server.get());
// Report the server address immediately.
char hostname[256] = "(error)";
enet_address_get_host_ip(&event.peer->address, hostname, ARRAY_SIZE(hostname));
LOGMESSAGE("Net client: Connected to %s:%u", hostname, (unsigned int)event.peer->address.port);
m_Connected = true;
m_WasConnected = true;
m_IncomingMessages.push(event);
}
else if (event.type == ENET_EVENT_TYPE_DISCONNECT)
if (ret == 0)
{
ENSURE(event.peer == m_Server.get());
// Report immediately.
LOGMESSAGE("Net client: Disconnected");
m_Connected = false;
m_IncomingMessages.push(event);
ClientHandleExpiry(m_Quic.get());
ClientWriteStreams(m_Quic.get());
return;
}
else if (event.type == ENET_EVENT_TYPE_RECEIVE)
m_IncomingMessages.push(event);
ClientRead(m_Quic.get());
ClientWriteStreams(m_Quic.get());
}
void CNetClientSession::Flush()
{
ENetPacket* packet;
while (m_OutgoingMessages.pop(packet))
if (enet_peer_send(m_Server.get(), CNetHost::DEFAULT_CHANNEL, packet) < 0)
{
// Report the error, but do so silently if we know we are disconnected.
if (m_Connected)
LOGERROR("NetClient: Failed to send packet to server");
else
LOGMESSAGE("NetClient: Failed to send packet to server");
}
enet_host_flush(m_Host.get());
std::vector<std::uint8_t>* message;
while (m_OutgoingMessages.pop(message))
{
std::unique_ptr<std::vector<std::uint8_t>> data{message};
if (m_Quic->streams.has_value())
m_Quic->streams.value().PushData(std::move(*data));
else
LOGERROR("no stream to send message");
}
}
void CNetClientSession::ProcessPolledMessages()
{
ENetEvent event;
while(m_IncomingMessages.pop(event))
IncommingMessage query{};
while(m_IncomingMessages.pop(query))
{
if (event.type == ENET_EVENT_TYPE_CONNECT)
m_Client.HandleConnect();
else if (event.type == ENET_EVENT_TYPE_DISCONNECT)
std::visit([&]<typename Message>(Message message)
{
// This deletes the session, so we must break;
if (event.data == 0 && !m_WasConnected)
m_Client.HandleDisconnect(NDR_CONNECTION_REQUEST_TIMED_OUT);
else
m_Client.HandleDisconnect(event.data);
break;
}
else if (event.type == ENET_EVENT_TYPE_RECEIVE)
{
CNetMessage* msg = CNetMessageFactory::CreateMessage(event.packet->data, event.packet->dataLength, m_Client.GetScriptInterface());
if (msg)
if constexpr (std::same_as<Message, ConnectionEstablished>)
{
LOGMESSAGE("Net client: Received message %s of size %lu from server", msg->ToString().c_str(), (unsigned long)msg->GetSerializedLength());
m_Client.HandleMessage(msg);
m_Client.HandleConnect();
}
// Thread-safe
enet_packet_destroy(event.packet);
}
else if constexpr (std::same_as<Message, Disconnect>)
{
m_Client.HandleDisconnect(NDR_UNKNOWN);
}
else
{
static_assert(std::same_as<Message, std::vector<std::uint8_t>*>);
std::unique_ptr<std::vector<std::uint8_t>> data{message};
CNetMessage* msg = CNetMessageFactory::CreateMessage(*data, m_Client.GetScriptInterface());
if (msg)
{
LOGMESSAGE("Net client: Received message %s of size %lu from server", msg->ToString().c_str(), (unsigned long)msg->GetSerializedLength());
m_Client.HandleMessage(msg);
}
}
}, query);
}
}
bool CNetClientSession::SendMessage(const CNetMessage* message)
{
ENSURE(m_Host && m_Server);
// ENSURE(m_Host && m_Server);
// Thread-safe.
ENetPacket* packet = CNetHost::CreatePacket(message);
if (!packet)
return false;
if (!m_OutgoingMessages.push(packet))
if (!m_OutgoingMessages.push(new std::vector{CNetHost::CreatePacket(message)}))
{
LOGERROR("NetClient: Failed to push message on the outgoing queue.");
return false;
@ -207,17 +560,11 @@ bool CNetClientSession::SendMessage(const CNetMessage* message)
u32 CNetClientSession::GetLastReceivedTime() const
{
if (!m_Server)
return 0;
return m_LastReceivedTime;
}
u32 CNetClientSession::GetMeanRTT() const
{
if (!m_Server)
return 0;
return m_MeanRTT;
}

View file

@ -19,7 +19,6 @@
#define NETSESSION_H
#include "lib/code_annotation.h"
#include "lib/external_libraries/enet.h"
#include "lib/types.h"
#include "network/NetFileTransfer.h"
#include "network/NetHost.h"
@ -32,8 +31,6 @@ class CNetMessage;
class CNetStatsTable;
class CStr;
typedef struct _ENetHost ENetHost;
/**
* @file
* Network client/server sessions.
@ -53,10 +50,11 @@ class CNetClientSession
NONCOPYABLE(CNetClientSession);
public:
struct Quic;
CNetClientSession(CNetClient& client);
~CNetClientSession();
bool Connect(const CStr& server, const u16 port, ENetHost* enetClient);
bool Connect(const CStr& server, const u16 port);
/**
* The client NetSession is threaded to avoid getting timeouts if the main thread hangs.
@ -104,11 +102,14 @@ private:
CNetClient& m_Client;
CNetFileTransferer m_FileTransferer;
public:
// Net messages received and waiting for fetching.
boost::lockfree::queue<ENetEvent> m_IncomingMessages{16};
struct ConnectionEstablished{};
struct Disconnect{};
using IncommingMessage = std::variant<ConnectionEstablished, std::vector<std::uint8_t>*, Disconnect>;
boost::lockfree::queue<IncommingMessage> m_IncomingMessages{16};
// Net messages to send on the next flush() call.
boost::lockfree::queue<ENetPacket*> m_OutgoingMessages{16};
boost::lockfree::queue<std::vector<std::uint8_t>*> m_OutgoingMessages{16};
// Last known state. If false, flushing errors are silenced.
bool m_Connected{false};
@ -116,7 +117,7 @@ private:
// Whether this session was ever connected to the server.
bool m_WasConnected{false};
// Wrapper around enet stats - those are atomic as the code is lock-free.
// Wrapper around stats - those are atomic as the code is lock-free.
std::atomic<u32> m_LastReceivedTime{0};
std::atomic<u32> m_MeanRTT{0};
@ -124,9 +125,9 @@ private:
std::atomic<bool> m_LoopRunning{false};
std::atomic<bool> m_ShouldShutdown{false};
std::unique_ptr<ENetHost, DestroyHost> m_Host;
std::unique_ptr<ENetPeer, DestroyPeer> m_Server;
std::unique_ptr<CNetStatsTable> m_Stats;
const std::unique_ptr<Quic> m_Quic;
};
#endif // NETSESSION_H

View file

@ -1,4 +1,4 @@
/* Copyright (C) 2025 Wildfire Games.
/* Copyright (C) 2026 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@ -25,32 +25,10 @@
#include "ps/CLogger.h"
#include <cstddef>
#include <numeric>
#include <vector>
bool CNetHost::SendMessage(const CNetMessage* message, ENetPeer* peer, const char* peerName)
{
ENetPacket* packet = CreatePacket(message);
if (!packet)
return false;
LOGMESSAGE("Net: Sending message %s of size %lu to %s", message->ToString().c_str(), (unsigned long)packet->dataLength, peerName);
// Let ENet send the message to peer
if (enet_peer_send(peer, DEFAULT_CHANNEL, packet) < 0)
{
LOGERROR("Net: Failed to send packet to peer");
return false;
}
// Don't call enet_host_flush now - let it queue up all the packets
// and send them during the next frame
//
// TODO: we should flush explicitly at some appropriate point before the next frame
return true;
}
ENetPacket* CNetHost::CreatePacket(const CNetMessage* message)
std::vector<std::uint8_t> CNetHost::CreatePacket(const CNetMessage* message)
{
size_t size = message->GetSerializedLength();
@ -63,12 +41,7 @@ ENetPacket* CNetHost::CreatePacket(const CNetMessage* message)
// Save message to internal buffer
message->Serialize(&buffer[0]);
// Create a reliable packet
ENetPacket* packet = enet_packet_create(&buffer[0], size, ENET_PACKET_FLAG_RELIABLE);
if (!packet)
LOGERROR("Net: Failed to construct packet");
return packet;
return buffer;
}
void CNetHost::Initialize()
@ -81,3 +54,81 @@ void CNetHost::Deinitialize()
{
enet_deinitialize();
}
Stream::Stream(const std::int64_t streamId):
m_Id{streamId}
{}
void Stream::PushData(std::vector<std::uint8_t> data)
{
m_SendBuffer.push_back(std::move(data));
}
void Stream::PushMessage(const CNetMessage* message)
{
m_SendBuffer.push_back(CNetHost::CreatePacket(message));
}
std::optional<std::span<const std::uint8_t>> Stream::PeekData()
{
const std::size_t startOffset{m_SentOffset - m_AckedOffset};
std::size_t offset{0};
for (std::vector<std::uint8_t>& bytes : m_SendBuffer)
{
if (startOffset - offset < bytes.size())
{
const std::size_t temp{startOffset - offset};
return std::span{bytes.data() + temp, bytes.size() - temp};
}
offset += bytes.size();
}
return std::nullopt;
}
void Stream::MarkSent(const std::size_t offset)
{
m_SentOffset += offset;
}
void Stream::MarkAcknowledged(const std::size_t offset)
{
while (!m_SendBuffer.empty())
{
std::vector<uint8_t>& head{m_SendBuffer.front()};
if (m_AckedOffset + head.size() > offset)
break;
m_AckedOffset += head.size();
m_SendBuffer.pop_front();
}
}
std::optional<std::vector<std::uint8_t>> Stream::Receive(const std::span<const std::uint8_t> data)
{
m_ReceiveBuffer.emplace_back(data.begin(), data.end());
const std::size_t bufferSize{std::transform_reduce(m_ReceiveBuffer.begin(), m_ReceiveBuffer.end(),
static_cast<std::size_t>(0), std::plus<>{}, std::mem_fn(&std::vector<std::uint8_t>::size))};
if (bufferSize < 3)
return std::nullopt;
const auto& message = m_ReceiveBuffer.front();
auto bufferIter = message.begin();
std::size_t messageSize;
Deserialize_int_1(bufferIter, std::ignore);
Deserialize_int_2(bufferIter, messageSize);
if (messageSize > bufferSize)
return std::nullopt;
std::vector<std::uint8_t> messageCopy;
while (messageCopy.size() < messageSize)
{
messageCopy.insert(messageCopy.end(), m_ReceiveBuffer.front().begin(),
m_ReceiveBuffer.front().end());
m_ReceiveBuffer.pop_front();
}
return messageCopy;
}

View file

@ -27,7 +27,14 @@
#include "lib/types.h"
#include "ps/CStr.h"
#include <deque>
#include <map>
#include <ngtcp2/ngtcp2.h>
#include <optional>
#include <span>
#include <vector>
#include <gnutls/gnutls.h>
class CNetMessage;
@ -35,6 +42,38 @@ typedef struct _ENetPeer ENetPeer;
typedef struct _ENetPacket ENetPacket;
typedef struct _ENetHost ENetHost;
constexpr std::size_t MAX_UDP_PAYLOAD_SIZE{64 * KiB};
struct CredentialsDeleter
{
void operator()(const gnutls_certificate_credentials_t cred) const
{
gnutls_certificate_free_credentials(cred);
}
};
struct SessionDeleter
{
void operator()(const gnutls_session_t p) const
{
gnutls_deinit(p);
}
};
struct ConnectionDeleter
{
void operator()(ngtcp2_conn* p) const
{
ngtcp2_conn_del(p);
}
};
struct AddressStorage
{
ngtcp2_sockaddr_union address;
ngtcp2_socklen length{sizeof(address)};
};
struct PlayerAssignment
{
/**
@ -106,20 +145,11 @@ class CNetHost
public:
static const int DEFAULT_CHANNEL = 0;
/**
* Transmit a message to the given peer.
* @param message message to send
* @param peer peer to send to
* @param peerName name of peer for debug logs
* @return true on success, false on failure
*/
static bool SendMessage(const CNetMessage* message, ENetPeer* peer, const char* peerName);
/**
* Construct an ENet packet by serialising the given message.
* @return NULL on failure
*/
static ENetPacket* CreatePacket(const CNetMessage* message);
static std::vector<std::uint8_t> CreatePacket(const CNetMessage* message);
/**
* Initialize ENet.
@ -133,4 +163,25 @@ public:
static void Deinitialize();
};
class Stream
{
public:
Stream(const std::int64_t streamId);
void PushData(std::vector<std::uint8_t> data);
void PushMessage(const CNetMessage* message);
std::optional<std::span<const std::uint8_t>> PeekData();
void MarkSent(const std::size_t offset);
void MarkAcknowledged(const std::size_t offset);
std::optional<std::vector<std::uint8_t>> Receive(const std::span<const std::uint8_t> data);
std::int64_t m_Id;
private:
std::deque<std::vector<std::uint8_t>> m_SendBuffer;
std::deque<std::vector<std::uint8_t>> m_ReceiveBuffer;
/* invariant: m_SentOffset >= m_AckedOffset */
std::size_t m_SentOffset{0};
std::size_t m_AckedOffset{0};
};
#endif // NETHOST_H

View file

@ -53,10 +53,7 @@ u8* CNetMessage::Serialize(u8* pBuffer) const
const u8* CNetMessage::Deserialize(const u8* pStart, const u8* pEnd)
{
if (pStart + 3 > pEnd)
{
LOGERROR("CNetMessage: Corrupt packet (smaller than header)");
return NULL;
}
throw std::invalid_argument{"CNetMessage: Corrupt packet (smaller than header)"};
const u8* pBuffer = pStart;
@ -67,10 +64,7 @@ const u8* CNetMessage::Deserialize(const u8* pStart, const u8* pEnd)
m_Type = (NetMessageType)type;
if (pStart + size != pEnd)
{
LOGERROR("CNetMessage: Corrupt packet (incorrect size)");
return NULL;
}
throw std::invalid_argument{fmt::format("CNetMessage: Corrupt packet (incorrect size) %i %i", size, pEnd - pStart)};
return pBuffer;
}
@ -91,15 +85,14 @@ CStr CNetMessage::ToString() const
return fmt::format("Unknown Message {}", static_cast<int>(GetType()));
}
CNetMessage* CNetMessageFactory::CreateMessage(const void* pData,
size_t dataSize,
const Script::Interface& scriptInterface)
CNetMessage* CNetMessageFactory::CreateMessage(const std::span<const std::uint8_t> data,
const Script::Interface& scriptInterface)
{
CNetMessage* pNewMessage = NULL;
CNetMessage header;
// Figure out message type
header.Deserialize((const u8*)pData, (const u8*)pData + dataSize);
header.Deserialize(std::to_address(data.begin()), std::to_address(data.end()));
switch (header.GetType())
{
@ -230,7 +223,7 @@ CNetMessage* CNetMessageFactory::CreateMessage(const void* pData,
}
if (pNewMessage)
pNewMessage->Deserialize((const u8*)pData, (const u8*)pData + dataSize);
pNewMessage->Deserialize(std::to_address(data.begin()), std::to_address(data.end()));
return pNewMessage;
}

View file

@ -27,6 +27,7 @@
#include <js/RootingAPI.h>
#include <js/TypeDecls.h>
#include <js/Value.h>
#include <span>
namespace Script { class Interface; }
@ -108,12 +109,12 @@ public:
/**
* Factory method which creates a message object based on the given data
*
* @param pData Data buffer
* @param dataSize Size of data buffer
* @param data Data buffer
* @param scriptInterface Script instance to use when constructing scripted messages
* @return The new message created
*/
static CNetMessage* CreateMessage(const void* pData, size_t dataSize, const Script::Interface& scriptInterface);
static CNetMessage* CreateMessage(const std::span<const std::uint8_t> data,
const Script::Interface& scriptInterface);
};
/**

View file

@ -35,6 +35,7 @@
#include <js/Value.h>
#include <sstream>
#include <string>
#include "ps/CLogger.h"
namespace Script { class Interface; }

View file

@ -1,4 +1,4 @@
/* Copyright (C) 2025 Wildfire Games.
/* Copyright (C) 2026 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@ -19,10 +19,14 @@
#include "NetProtocol.h"
#include "ps/CLogger.h"
#include "ps/CStr.h"
#include <algorithm>
#include <chrono>
#include <cstdint>
#include <ngtcp2/ngtcp2.h>
#include <gnutls/crypto.h>
namespace
{
@ -59,3 +63,36 @@ std::optional<HandshakeError> CheckHandshake(const CSrvHandshakeMessage& serverM
return {};
}
uint64_t timestamp()
{
return std::chrono::duration_cast<std::chrono::nanoseconds>(
std::chrono::steady_clock::now().time_since_epoch()).count();
}
void increaseWindow(ngtcp2_conn* conn, const std::uint64_t streamId, const std::uint64_t size)
{
ngtcp2_conn_extend_max_offset(conn, size);
ngtcp2_conn_extend_max_stream_offset(conn, streamId, size);
}
void OnRandomRequest(std::uint8_t* destination, const std::size_t destinationLength, const ngtcp2_rand_ctx*)
{
const int ret{gnutls_rnd(GNUTLS_RND_RANDOM, destination, destinationLength)};
if (ret < 0)
LOGERROR("gnutls_rnd: %s", gnutls_strerror(ret));
}
int OnNewConnectionIdRequest(ngtcp2_conn*, ngtcp2_cid* cid, std::uint8_t* token, const std::size_t cidlen,
void* /*userData*/)
{
if (gnutls_rnd(GNUTLS_RND_RANDOM, cid->data, cidlen))
return NGTCP2_ERR_CALLBACK_FAILURE;
cid->datalen = cidlen;
if (gnutls_rnd (GNUTLS_RND_RANDOM, token, NGTCP2_STATELESS_RESET_TOKENLEN))
return NGTCP2_ERR_CALLBACK_FAILURE;
return 0;
}

View file

@ -27,11 +27,17 @@
#include <type_traits>
#include <vector>
#include <gnutls/gnutls.h>
/**
* Report the peer if we didn't receive a packet after this time (milliseconds).
*/
inline constexpr u32 NETWORK_WARNING_TIMEOUT{2000};
inline constexpr const char* TLS_PRIORITY{
"PERFORMANCE:-VERS-DTLS-ALL:-VERS-TLS1.0:-VERS-TLS1.1:-VERS-TLS1.2:-SHA1:-AES-128-CBC:-AES-256-CBC:"
"-SIGN-RSA-SHA1:-SIGN-ECDSA-SHA1:%DISABLE_TLS13_COMPAT_MODE"};
struct HandshakeError
{
std::string componentType;
@ -67,4 +73,17 @@ Message CreateHandshake() {
std::optional<HandshakeError> CheckHandshake(const CSrvHandshakeMessage& serverMessage, const CCliHandshakeMessage& clientMessage);
uint64_t timestamp();
struct ngtcp2_conn;
void increaseWindow(ngtcp2_conn* conn, const std::uint64_t streamId, const std::uint64_t size);
struct ngtcp2_rand_ctx;
void OnRandomRequest(std::uint8_t* destination, const std::size_t destinationLength,
const ngtcp2_rand_ctx*);
struct ngtcp2_cid;
int OnNewConnectionIdRequest(ngtcp2_conn*, ngtcp2_cid* cid, std::uint8_t *token, const std::size_t cidlen,
void* /*userData*/);
#endif

View file

@ -22,6 +22,7 @@
#include "lib/code_generation.h"
#include "lib/debug.h"
#include "lib/external_libraries/enet.h"
#include "lib/hash.h"
#include "lib/secure_crt.h"
#include "lib/status.h"
#include "lib/types.h"
@ -39,6 +40,8 @@
#include "ps/CLogger.h"
#include "ps/ConfigDB.h"
#include "ps/GUID.h"
#include "ps/GameSetup/CmdLineArgs.h"
#include "ps/GameSetup/Paths.h"
#include "ps/Hashing.h"
#include "ps/ProfileViewer.h"
#include "ps/Profiler2.h"
@ -56,8 +59,19 @@
#include <cstring>
#include <fmt/format.h>
#include <functional>
#include <gnutls/crypto.h>
#include <gnutls/gnutls.h>
#include <gnutls/x509.h>
#include <iterator>
#include <memory>
#include <netdb.h>
#include <ngtcp2/ngtcp2.h>
#include <ngtcp2/ngtcp2_crypto.h>
#include <ngtcp2/ngtcp2_crypto_gnutls.h>
#include <numeric>
#include <poll.h>
#include <random>
#include <ranges>
#include <set>
#include <sstream>
#include <string>
@ -103,15 +117,366 @@ constexpr u32 NETWORK_BAD_PING = DEFAULT_TURN_LENGTH * COMMAND_DELAY_MP / 2;
CNetServer* g_NetServer = NULL;
static CStr DebugName(CNetServerSession* session)
namespace
{
if (session == NULL)
return "[unknown host]";
if (session->GetGUID().empty())
return "[unauthed host]";
return "[" + session->GetGUID().substr(0, 8) + "...]";
// static CStr DebugName(CNetServerSession* session)
// {
// if (session == NULL)
// return "[unknown host]";
// if (session->GetGUID().empty())
// return "[unauthed host]";
// return "[" + session->GetGUID().substr(0, 8) + "...]";
// }
struct CidHash
{
std::size_t operator()(const ngtcp2_cid& cid) const
{
return std::accumulate(cid.data, cid.data + cid.datalen, static_cast<std::size_t>(0),
[](std::size_t carry, const std::uint8_t byte)
{
hash_combine(carry, byte);
return carry;
});
}
};
struct CidEqual
{
bool operator()(const ngtcp2_cid& a, const ngtcp2_cid& b) const
{
return ngtcp2_cid_eq(&a, &b);
}
};
std::size_t ReceivePackage(const int fd, std::span<std::uint8_t> data, AddressStorage& remoteAddress)
{
iovec iov{
.iov_base{data.data()},
.iov_len{data.size()}
};
msghdr msg{};
msg.msg_name = &remoteAddress.address.sa;
msg.msg_namelen = remoteAddress.length;
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
ssize_t ret;
do
ret = recvmsg(fd, &msg, MSG_DONTWAIT);
while (ret < 0 && errno == EINTR);
if (ret < 0)
throw std::system_error{errno, std::generic_category(), "recvmsg"};
remoteAddress.length = msg.msg_namelen;
return ret;
}
void GetRandomCid(ngtcp2_cid* cid)
{
std::array<std::uint8_t, NGTCP2_MAX_CIDLEN> buf;
const int ret{gnutls_rnd(GNUTLS_RND_RANDOM, buf.data(), buf.size())};
if (ret < 0)
throw std::runtime_error{fmt::format("gnutls_rnd: {}", gnutls_strerror(ret))};
ngtcp2_cid_init(cid, buf.data(), buf.size());
}
int ResolveAndBind(const char *port, AddressStorage& localAddress)
{
addrinfo hints{};
// IPv4 clients can connect to a IPv6 socket.
hints.ai_family = AF_INET6;
hints.ai_socktype = SOCK_DGRAM;
hints.ai_flags = AI_PASSIVE;
addrinfo* res;
if (getaddrinfo(nullptr, port, &hints, &res))
return -1;
std::unique_ptr<addrinfo, decltype(&freeaddrinfo)> infoList{res, &freeaddrinfo};
int fd;
addrinfo* rp;
for (rp = res; rp; rp = rp->ai_next)
{
fd = socket(AF_INET6, rp->ai_socktype, rp->ai_protocol);
if (fd == -1)
continue;
// Allow IPv4 using the same socket.
const int onlyIPv6{0};
if (setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, &onlyIPv6, sizeof(onlyIPv6)))
continue;
if (bind(fd, rp->ai_addr, rp->ai_addrlen))
{
close(fd);
continue;
}
break;
}
if (!rp)
return -1;
localAddress.address.in6 = reinterpret_cast<ngtcp2_sockaddr_in6&>(*rp->ai_addr);
localAddress.length = rp->ai_addrlen;
return fd;
}
void GenerateKeyfiles(const std::filesystem::path& privatekey, const std::filesystem::path& certificate)
{
constexpr auto binaryOverrideMode = std::ios::binary | std::ios::trunc;
gnutls_x509_privkey_t newKey;
if (const int ret{gnutls_x509_privkey_init(&newKey)})
{
throw std::runtime_error{fmt::format("Error initializing privatkey: {}",
gnutls_strerror(ret))};
}
std::unique_ptr<gnutls_x509_privkey_int, decltype(&gnutls_x509_privkey_deinit)> keyDeleter(
newKey, gnutls_x509_privkey_deinit);
if (const int ret{gnutls_x509_privkey_generate(newKey, GNUTLS_PK_EDDSA_ED25519, 256, 0)})
{
throw std::runtime_error{fmt::format("Error generating privatkey: {}",
gnutls_strerror(ret))};
}
gnutls_datum_t exportedKey;
if (const int ret{gnutls_x509_privkey_export2_pkcs8(newKey, GNUTLS_X509_FMT_DER, nullptr, 0,
&exportedKey)})
{
throw std::runtime_error{fmt::format("Error exporting privatkey: {}",
gnutls_strerror(ret))};
}
const std::unique_ptr<unsigned char, decltype(gnutls_free)> exportedKeyDeleter{exportedKey.data,
gnutls_free};
std::ofstream{privatekey, binaryOverrideMode}.write(
reinterpret_cast<const char*>(exportedKey.data), exportedKey.size);
gnutls_x509_crt_t newCertificate;
if (const int ret{gnutls_x509_crt_init(&newCertificate)})
{
throw std::runtime_error{fmt::format("Error initializing certificate: {}",
gnutls_strerror(ret))};
}
std::unique_ptr<gnutls_x509_crt_int, decltype(&gnutls_x509_crt_deinit)> certificateDeleter(
newCertificate, gnutls_x509_crt_deinit);
std::array<unsigned char, 20> serialNumber;
auto generator = [&, randomDevice = std::random_device{},
distribution = std::uniform_int_distribution<unsigned char>{}]() mutable
{
return distribution(randomDevice);
};
std::ranges::generate(serialNumber, std::ref(generator));
std::get<0>(serialNumber) &= ~(1 << 8);
if (const int ret{gnutls_x509_crt_set_serial(newCertificate, serialNumber.data(),
serialNumber.size())})
{
throw std::runtime_error{fmt::format("Error setting serial number: {}",
gnutls_strerror(ret))};
}
if (const int ret{gnutls_x509_crt_set_activation_time(newCertificate, 0)})
{
throw std::runtime_error{fmt::format("Error setting activation time: {}",
gnutls_strerror(ret))};
}
if (const int ret{gnutls_x509_crt_set_expiration_time(newCertificate, -1)})
{
throw std::runtime_error{fmt::format("Error setting expiration time: {}",
gnutls_strerror(ret))};
}
if (const int ret{gnutls_x509_crt_set_key(newCertificate, newKey)})
{
throw std::runtime_error{fmt::format("Error setting certificate key: {}",
gnutls_strerror(ret))};
}
if (const int ret{gnutls_x509_crt_sign(newCertificate, newCertificate, newKey)})
{
throw std::runtime_error{fmt::format("Error signing certificate: {}",
gnutls_strerror(ret))};
}
gnutls_datum_t exportedCertificate;
if (const int ret{gnutls_x509_crt_export2(newCertificate, GNUTLS_X509_FMT_DER,
&exportedCertificate)})
{
throw std::runtime_error{fmt::format("Error exporting certificate: {}",
gnutls_strerror(ret))};
}
const std::unique_ptr<unsigned char, decltype(gnutls_free)> exportedCertificateDeleter{
exportedCertificate.data, gnutls_free};
std::ofstream{certificate, binaryOverrideMode}.write(
reinterpret_cast<const char*>(exportedCertificate.data), exportedCertificate.size);
}
std::unique_ptr<gnutls_certificate_credentials_st, CredentialsDeleter> CreateTlsServerCredentials()
{
gnutls_certificate_credentials_t temp;
if (const int ret{gnutls_certificate_allocate_credentials(&temp)})
{
throw std::runtime_error{fmt::format("gnutls_certificate_allocate_credentials: {}",
gnutls_strerror(ret))};
}
std::unique_ptr<gnutls_certificate_credentials_st, CredentialsDeleter> cred{temp};
const auto folder = (Paths(g_CmdLineArgs).UserData() / "encryption").fileSystemPath();
std::filesystem::create_directory(folder);
const auto privatekeyPath{folder / "privatekey.der"};
const auto certificatePath{folder / "certificate.der"};
if (!std::filesystem::is_regular_file(privatekeyPath) ||
!std::filesystem::is_regular_file(certificatePath))
{
GenerateKeyfiles(privatekeyPath, certificatePath);
}
if (const int ret{gnutls_certificate_set_x509_key_file(cred.get(), certificatePath.c_str(),
privatekeyPath.c_str(), GNUTLS_X509_FMT_DER)})
{
throw std::runtime_error{fmt::format("gnutls_certificate_set_x509_key_file: {}",
gnutls_strerror(ret))};
}
return cred;
}
ngtcp2_settings InitSettings()
{
ngtcp2_settings settings;
ngtcp2_settings_default(&settings);
settings.initial_ts = timestamp();
return settings;
}
} // anonymous namespace
class CNetServerWorker::Quic
{
public:
explicit Quic(const std::uint16_t port):
m_SocketFd{ResolveAndBind(std::to_string(port).c_str(), m_LocalAddress)}
{}
~Quic()
{
if (m_SocketFd >= 0)
close(m_SocketFd);
}
AddressStorage m_LocalAddress;
int m_SocketFd;
std::unique_ptr<gnutls_certificate_credentials_st, CredentialsDeleter> m_Credentials{
CreateTlsServerCredentials()};
ngtcp2_settings m_Settings{InitSettings()};
void HandleIncoming(CNetServerWorker& server)
{
std::array<std::uint8_t, MAX_UDP_PAYLOAD_SIZE> buf;
while (true)
{
AddressStorage remoteAddress;
std::size_t n_read;
try
{
n_read = ReceivePackage(m_SocketFd, buf, remoteAddress);
}
catch (std::system_error& e)
{
if (e.code().value() == EAGAIN || e.code().value() == EWOULDBLOCK)
return;
throw;
}
ngtcp2_version_cid version;
if (const int ret{ngtcp2_pkt_decode_version_cid(&version, buf.data(), n_read,
NGTCP2_MAX_CIDLEN)})
{
throw std::runtime_error{fmt::format("ngtcp2_pkt_decode_version_cid: {}",
ngtcp2_strerror(ret))};
}
const ngtcp2_addr remote{
.addr{&remoteAddress.address.sa},
.addrlen{remoteAddress.length}
};
/* Find any existing connection by DCID */
ngtcp2_cid tempCid;
ngtcp2_cid_init(&tempCid, version.dcid, version.dcidlen);
const auto connectionIter = std::ranges::find_if(server.m_Sessions,
[&tempCid](const std::vector<ngtcp2_cid> association)
{
return std::ranges::any_of(association, [&tempCid](const ngtcp2_cid& elem)
{
return ngtcp2_cid_eq(&elem, &tempCid);
});
},
[](auto& connection)
{
const std::size_t amount{ngtcp2_conn_get_scid(
connection->m_Connection.m_QuicConnection.get(), nullptr)};
std::vector<ngtcp2_cid> cids(amount);
ngtcp2_conn_get_scid(connection->m_Connection.m_QuicConnection.get(),
cids.data());
return cids;
});
const bool existing{connectionIter != server.m_Sessions.end()};
if (!existing)
{
ngtcp2_pkt_hd header;
if (ngtcp2_accept(&header, buf.data(), n_read))
throw std::invalid_argument{"Failed parsing package header"};
ngtcp2_cid newScid;
GetRandomCid(&newScid);
const ngtcp2_path path{
.local{
.addr{&m_LocalAddress.address.sa},
.addrlen{m_LocalAddress.length}
},
.remote{remote}
};
server.m_Sessions.push_back(std::make_unique<CNetServerSession>(server, m_SocketFd,
m_Settings, m_Credentials.get(), header, newScid, path));
server.SetupSession(server.m_Sessions.back().get());
}
auto& session{existing ? *connectionIter : server.m_Sessions.back()};
try
{
session->m_Connection.Read(remote, {buf.data(), n_read});
}
catch (const std::system_error&)
{
throw;
}
catch (const std::runtime_error&)
{
const auto session = existing ? std::move(*connectionIter) :
std::move(server.m_Sessions.back());
if (existing)
server.m_Sessions.erase(connectionIter);
else
server.m_Sessions.pop_back();
session->Update(static_cast<uint>(NMT_CONNECTION_LOST), nullptr);
}
}
}
};
/*
* XXX: We use some non-threadsafe functions from the worker thread.
* See https://gitea.wildfiregames.com/0ad/0ad/issues/654
@ -126,17 +491,17 @@ CNetServerWorker::CNetServerWorker(const bool continueSavedGame, std::uint16_t p
m_Password{std::move(password)}
{
// Bind to default host
ENetAddress addr;
addr.host = ENET_HOST_ANY;
addr.port = port;
// ENetAddress addr;
// addr.host = ENET_HOST_ANY;
// addr.port = port;
// Create ENet server
m_Host.reset(PS::Enet::CreateHost(&addr, MAX_CLIENTS, CHANNEL_COUNT));
if (!m_Host)
{
LOGERROR("Net server: enet_host_create failed");
throw std::runtime_error{"Failed to start server"};
}
// m_Host.reset(PS::Enet::CreateHost(&addr, MAX_CLIENTS, CHANNEL_COUNT));
// if (!m_Host)
// {
// LOGERROR("Net server: enet_host_create failed");
// throw std::runtime_error{"Failed to start server"};
// }
m_Stats = std::make_unique<CNetStatsTable>();
if (CProfileViewer::IsInitialised())
@ -146,7 +511,7 @@ CNetServerWorker::CNetServerWorker(const bool continueSavedGame, std::uint16_t p
// Launch the worker thread
m_WorkerThread = std::thread(Threading::HandleExceptions<RunThread>::Wrapper, this,
std::move(initAttributes));
std::move(initAttributes), port);
#if CONFIG2_MINIUPNPC
// Launch the UPnP thread
@ -169,10 +534,6 @@ CNetServerWorker::~CNetServerWorker()
if (m_UPnPThread.joinable())
m_UPnPThread.detach();
#endif
// Clean up resources
for (const auto& session : m_Sessions)
session->DisconnectNow(NDR_SERVER_SHUTDOWN);
}
@ -328,21 +689,17 @@ void CNetServerWorker::SetupUPnP(const u16 port)
}
#endif // CONFIG2_MINIUPNPC
bool CNetServerWorker::SendMessage(ENetPeer* peer, const CNetMessage* message)
bool CNetServerWorker::SendMessage(const CNetMessage* message)
{
ENSURE(m_Host);
m_Sessions.front()->SendMessage(message);
CNetServerSession* session = static_cast<CNetServerSession*>(peer->data);
return CNetHost::SendMessage(message, peer, DebugName(session).c_str());
return true;
}
bool CNetServerWorker::Multicast(const CNetMessage* message,
const std::vector<NetServerSessionState>& targetStates,
const std::optional<std::vector<std::string>>& receivers /* = std::nullopt */)
{
ENSURE(m_Host);
const auto isReceiver = [&](const CNetServerSession& session)
{
if (!PS::contains(targetStates,
@ -367,14 +724,14 @@ bool CNetServerWorker::Multicast(const CNetMessage* message,
return ok;
}
void CNetServerWorker::RunThread(CNetServerWorker* data, const std::string& initAttributes)
void CNetServerWorker::RunThread(CNetServerWorker* data, const std::string& initAttributes, u16 port)
{
debug_SetThreadName("NetServer");
data->Run(initAttributes);
data->Run(initAttributes, port);
}
void CNetServerWorker::Run(const std::string& initAttributes)
void CNetServerWorker::Run(const std::string& initAttributes, u16 port)
{
// The script context uses the profiler and therefore the thread must be registered before the context is created
g_Profiler2.RegisterCurrentThread("Net server");
@ -392,22 +749,27 @@ void CNetServerWorker::Run(const std::string& initAttributes)
m_InitAttributes = gameAttributesVal;
}
Quic quic{port};
while (true)
{
if (!RunStep())
if (!RunStep(quic))
break;
// Update profiler stats
m_Stats->LatchHostState(*m_Host);
m_Stats->LatchHostState(m_Sessions);
}
// Clear roots before deleting their context
m_SavedCommands.clear();
SAFE_DELETE(m_ScriptInterface);
for (const auto& session : m_Sessions)
session->Disconnect(NDR_SERVER_SHUTDOWN);
}
bool CNetServerWorker::RunStep()
bool CNetServerWorker::RunStep(Quic& quic)
{
// Check for messages from the game thread.
// (Do as little work as possible while the mutex is held open,
@ -448,103 +810,40 @@ bool CNetServerWorker::RunStep()
CheckClientConnections();
// Process network events:
pollfd pollFd{
.fd{quic.m_SocketFd},
.events{POLLIN | POLLOUT}
};
const int ready{poll(&pollFd, 1, 25)};
ENetEvent event;
int status = enet_host_service(m_Host.get(), &event, HOST_SERVICE_TIMEOUT);
if (status < 0)
if (ready < 0)
throw std::runtime_error{fmt::format("poll: {}", std::strerror(errno))};
if (ready == 0)
{
LOGERROR("CNetServerWorker: enet_host_service failed (%d)", status);
// TODO: notify game that the server has shut down
return false;
}
if (status == 0)
{
// Reached timeout with no events - try again
return true;
}
// Process the event:
switch (event.type)
{
case ENET_EVENT_TYPE_CONNECT:
{
// Report the client address
char hostname[256] = "(error)";
enet_address_get_host_ip(&event.peer->address, hostname, ARRAY_SIZE(hostname));
LOGMESSAGE("Net server: Received connection from %s:%u", hostname, (unsigned int)event.peer->address.port);
// Set up a session object for this peer
const std::unique_ptr<CNetServerSession>& session{m_Sessions.emplace_back(
std::make_unique<CNetServerSession>(*this, event.peer))};
SetupSession(session.get());
ENSURE(event.peer->data == NULL);
event.peer->data = session.get();
HandleConnect(session.get());
break;
}
case ENET_EVENT_TYPE_DISCONNECT:
{
// If there is an active session with this peer, then reset and delete it
CNetServerSession* session = static_cast<CNetServerSession*>(event.peer->data);
if (session)
for (auto& session : m_Sessions)
{
LOGMESSAGE("Net server: Disconnected %s", DebugName(session).c_str());
// Remove the session first, so we won't send player-update messages to it
// when updating the FSM
const auto iter = std::ranges::find(m_Sessions, session,
&std::unique_ptr<CNetServerSession>::get);
const std::unique_ptr<CNetServerSession> _ = std::move(*iter);
m_Sessions.erase(iter);
session->Update((uint)NMT_CONNECTION_LOST, NULL);
event.peer->data = NULL;
}
if (m_State == SERVER_STATE_LOADING)
CheckGameLoadStatus(NULL);
break;
}
case ENET_EVENT_TYPE_RECEIVE:
{
// If there is an active session with this peer, then process the message
CNetServerSession* session = static_cast<CNetServerSession*>(event.peer->data);
if (session)
{
// Create message from raw data
CNetMessage* msg = CNetMessageFactory::CreateMessage(event.packet->data, event.packet->dataLength, GetScriptInterface());
if (msg)
ngtcp2_conn *conn = session->m_Connection.m_QuicConnection.get();
const int ret{ngtcp2_conn_handle_expiry(conn, timestamp())};
if (ret < 0)
{
LOGMESSAGE("Net server: Received message %s of size %lu from %s", msg->ToString().c_str(), (unsigned long)msg->GetSerializedLength(), DebugName(session).c_str());
HandleMessageReceive(msg, session);
delete msg;
LOGERROR("ngtcp2_conn_handle_expiry: %s", ngtcp2_strerror(ret));
continue;
}
session->m_Connection.Write(quic.m_SocketFd);
}
// Done using the packet
enet_packet_destroy(event.packet);
break;
}
else
{
if (pollFd.revents & POLLIN)
quic.HandleIncoming(*this);
case ENET_EVENT_TYPE_NONE:
break;
if (pollFd.revents & POLLOUT)
{
for (auto& session : m_Sessions)
session->m_Connection.Write(quic.m_SocketFd);
}
}
return true;
@ -1650,10 +1949,8 @@ CStrW CNetServerWorker::DeduplicatePlayerName(const CStrW& original)
}
}
void CNetServerWorker::SendHolePunchingMessage(const CStr& ipStr, u16 port)
void CNetServerWorker::SendHolePunchingMessage(const CStr& /*ipStr*/, u16 /*port*/)
{
if (m_Host)
StunClient::SendHolePunchingMessages(*m_Host, ipStr, port);
}
@ -1671,9 +1968,9 @@ CNetServer::CNetServer(const bool continueSavedGame, std::uint16_t port, const b
// In lobby, we send our public ip and port on request to the players who want to connect.
// Thus we need to know our public IP and use STUN to get it.
std::lock_guard<std::mutex> lock(m_Worker.m_WorkerMutex);
if (!m_Worker.m_Host || !StunClient::FindPublicIP(*m_Worker.m_Host, m_PublicIp, m_PublicPort))
throw std::runtime_error{"Failed to resolve public IP-address."};
// std::lock_guard<std::mutex> lock(m_Worker.m_WorkerMutex);
// if (!m_Worker.m_Host || !StunClient::FindPublicIP(*m_Worker.m_Host, m_PublicIp, m_PublicPort))
// throw std::runtime_error{"Failed to resolve public IP-address."};
}
bool CNetServer::UseLobbyAuth() const
@ -1694,9 +1991,7 @@ u16 CNetServer::GetPublicPort() const
u16 CNetServer::GetLocalPort() const
{
std::lock_guard<std::mutex> lock(m_Worker.m_WorkerMutex);
if (!m_Worker.m_Host)
return 0;
return m_Worker.m_Host->address.port;
return 0; // m_Worker.m_Host->address.port;
}
bool CNetServer::CheckPasswordAndIncrement(const std::string& username, const std::string& password, const std::string& salt)
@ -1743,3 +2038,4 @@ void CNetServer::SendHolePunchingMessage(const CStr& ip, u16 port)
{
m_Worker.SendHolePunchingMessage(ip, port);
}

View file

@ -119,7 +119,7 @@ public:
/**
* Send a message to the given network peer.
*/
bool SendMessage(ENetPeer* peer, const CNetMessage* message);
bool SendMessage(const CNetMessage* message);
/**
* Disconnects a player from gamesetup or session.
@ -173,10 +173,12 @@ private:
*/
CStrW DeduplicatePlayerName(const CStrW& original);
public:
/**
* Get the script context used for init attributes.
*/
const Script::Interface& GetScriptInterface();
private:
/**
* Set the turn length to a fixed value.
@ -227,7 +229,9 @@ private:
void ConstructPlayerAssignmentMessage(CPlayerAssignmentMessage& message);
public:
void HandleMessageReceive(CNetMessage* message, CNetServerSession* session);
public:
/**
* Send a network warning if the connection to a client is being lost or has bad latency.
@ -263,7 +267,8 @@ private:
*/
const bool m_LobbyAuth;
std::unique_ptr<ENetHost, DestroyHost> m_Host;
class Quic;
std::vector<std::unique_ptr<CNetServerSession>> m_Sessions;
std::unique_ptr<CNetStatsTable> m_Stats;
@ -330,9 +335,9 @@ private:
std::thread m_UPnPThread;
#endif
static void RunThread(CNetServerWorker* data, const std::string& initAttributes);
void Run(const std::string& initAttributes);
bool RunStep();
static void RunThread(CNetServerWorker* data, const std::string& initAttributes, u16 port);
void Run(const std::string& initAttributes, u16 port);
bool RunStep(Quic& quic);
std::thread m_WorkerThread;
mutable std::mutex m_WorkerMutex;

View file

@ -25,8 +25,312 @@
#include "network/NetServer.h"
#include "ps/CLogger.h"
CNetServerSession::CNetServerSession(CNetServerWorker& server, ENetPeer* peer) :
m_Server(server), m_FileTransferer(*this), m_Peer(peer)
#include <gnutls/crypto.h>
#include <gnutls/gnutls.h>
namespace
{
void SendPacket(const int socketFd, const std::span<const std::uint8_t> data, const ngtcp2_addr remote)
{
iovec iov{
.iov_base{const_cast<std::uint8_t*>(data.data())},
.iov_len{data.size()}
};
msghdr msg{};
msg.msg_name = remote.addr;
msg.msg_namelen = remote.addrlen;
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
ssize_t ret;
do
ret = sendmsg(socketFd, &msg, MSG_DONTWAIT);
while (ret < 0 && errno == EINTR);
if (ret < 0)
throw std::system_error{errno, std::generic_category(), "Error sending message"};
}
ngtcp2_conn* GetConnection(ngtcp2_crypto_conn_ref* connRef)
{
return reinterpret_cast<std::unique_ptr<ngtcp2_conn, ConnectionDeleter>*>(connRef->user_data)->get();
}
int OnReceiveStreamData(ngtcp2_conn* conn, const std::uint32_t /*flags*/, const std::int64_t streamId,
const std::uint64_t /*offset*/, const std::uint8_t* data, const std::size_t datalen, void* userData,
void* /*streamUserData*/)
{
CNetServerSession& session{*static_cast<CNetServerSession*>(userData)};
const auto messageData = session.m_Connection.m_Stream.value().Receive({data, datalen});
if (messageData.has_value())
{
std::unique_ptr<CNetMessage> message{CNetMessageFactory::CreateMessage(messageData.value(),
session.GetServer().GetScriptInterface())};
session.GetServer().HandleMessageReceive(message.get(), &session);
}
increaseWindow(conn, streamId, datalen);
return 0;
}
int OnAcknowledgedStreamData(ngtcp2_conn*, const std::int64_t, const std::uint64_t offset,
const std::uint64_t dataLength, void* userData, void*)
{
Connection& connection{static_cast<CNetServerSession*>(userData)->m_Connection};
Stream& stream{connection.GetStream()};
stream.MarkAcknowledged(offset + dataLength);
return 0;
}
int OnConnect(ngtcp2_conn*, const ngtcp2_encryption_level level, void* userData)
{
if (level != NGTCP2_ENCRYPTION_LEVEL_1RTT)
return 0;
Connection& connection{static_cast<CNetServerSession*>(userData)->m_Connection};
try
{
connection.OpenStream();
}
catch (const std::runtime_error&)
{
return NGTCP2_ERR_CALLBACK_FAILURE;
}
const CSrvHandshakeMessage handshake(CreateHandshake<CSrvHandshakeMessage>());
connection.GetStream().PushMessage(&handshake);
return 0;
}
constexpr ngtcp2_callbacks callbacks{
.recv_client_initial{&ngtcp2_crypto_recv_client_initial_cb},
.recv_crypto_data{&ngtcp2_crypto_recv_crypto_data_cb},
.encrypt{&ngtcp2_crypto_encrypt_cb},
.decrypt{&ngtcp2_crypto_decrypt_cb},
.hp_mask{&ngtcp2_crypto_hp_mask_cb},
.recv_stream_data{&OnReceiveStreamData},
.acked_stream_data_offset{&OnAcknowledgedStreamData},
.recv_retry{&ngtcp2_crypto_recv_retry_cb},
.rand{&OnRandomRequest},
.get_new_connection_id{&OnNewConnectionIdRequest},
.update_key{&ngtcp2_crypto_update_key_cb},
.delete_crypto_aead_ctx{&ngtcp2_crypto_delete_crypto_aead_ctx_cb},
.delete_crypto_cipher_ctx{&ngtcp2_crypto_delete_crypto_cipher_ctx_cb},
.get_path_challenge_data{&ngtcp2_crypto_get_path_challenge_data_cb},
.recv_tx_key{&OnConnect}
};
void WriteToStream(const int socketFd, ngtcp2_conn* conn, Stream* stream, const ngtcp2_addr remote)
{
std::array<std::uint8_t, MAX_UDP_PAYLOAD_SIZE> buf;
ngtcp2_path_storage ps;
ngtcp2_path_storage_zero(&ps);
ngtcp2_pkt_info pi;
const std::uint64_t ts{timestamp()};
std::uint32_t flags{NGTCP2_WRITE_STREAM_FLAG_MORE};
while (true)
{
ngtcp2_vec datav;
std::int64_t stream_id;
if (stream)
{
auto bytesToSend = stream->PeekData();
if (bytesToSend.has_value())
{
datav.base = const_cast<uint8_t*>(bytesToSend->data());
datav.len = bytesToSend->size();
stream_id = stream->m_Id;
}
else
{
/* No stream data to be sent */
datav.base = nullptr;
datav.len = 0;
stream_id = -1;
flags &= ~NGTCP2_WRITE_STREAM_FLAG_MORE;
}
}
else
{
datav.base = NULL;
datav.len = 0;
stream_id = -1;
}
ngtcp2_ssize n_read;
const ngtcp2_ssize n_written{ngtcp2_conn_writev_stream(conn, &ps.path, &pi, buf.data(),
buf.size(), &n_read, flags, stream_id, &datav, 1, ts)};
if (n_written < 0)
{
if (n_written != NGTCP2_ERR_WRITE_MORE)
{
throw std::runtime_error{fmt::format("ngtcp2_conn_writev_stream: {}",
ngtcp2_strerror(static_cast<int>(n_written)))};
}
if (stream && n_read > 0)
stream->MarkSent(n_read);
continue;
}
if (n_written == 0)
break;
if (stream && n_read > 0)
stream->MarkSent(n_read);
try
{
SendPacket(socketFd, {buf.data(), static_cast<std::size_t>(n_written)}, remote);
}
catch (std::system_error& e)
{
if (e.code().value() == EAGAIN || e.code().value() == EWOULDBLOCK)
break;
throw;
}
/* No stream data to be sent */
if (stream && datav.len == 0)
break;
}
ngtcp2_conn_update_pkt_tx_time(conn, timestamp());
}
std::unique_ptr<gnutls_session_int, SessionDeleter> createTlsSession(
const gnutls_certificate_credentials_t cred)
{
gnutls_session_t tempSession;
if (const int ret{gnutls_init(&tempSession, GNUTLS_SERVER | GNUTLS_ENABLE_EARLY_DATA |
GNUTLS_NO_END_OF_EARLY_DATA)})
{
throw std::runtime_error{fmt::format("gnutls_init: {}", gnutls_strerror(ret))};
}
std::unique_ptr<gnutls_session_int, SessionDeleter> session{tempSession};
if (const int ret{gnutls_priority_set_direct(session.get(), TLS_PRIORITY, NULL)})
throw std::runtime_error{fmt::format("gnutls_priority_set_direct: {}", gnutls_strerror(ret))};
if (const int ret{gnutls_credentials_set(session.get(), GNUTLS_CRD_CERTIFICATE, cred)})
throw std::runtime_error{fmt::format("gnutls_credentials_set: {}", gnutls_strerror(ret))};
return session;
}
}
Connection::Connection(CNetServerSession& session, const ngtcp2_settings& settings,
gnutls_certificate_credentials_t credentials, const ngtcp2_pkt_hd& header,
const ngtcp2_cid& newScid, const ngtcp2_path& path):
m_TlsSession{createTlsSession(credentials)},
m_ConnRef{
.get_conn{&GetConnection},
.user_data{&m_QuicConnection}
}
{
ngtcp2_transport_params params;
ngtcp2_transport_params_default(&params);
params.initial_max_stream_data_bidi_local = 128 * KiB;
params.initial_max_data = 1 * MiB;
params.max_udp_payload_size = MAX_UDP_PAYLOAD_SIZE;
ngtcp2_cid_init(&params.original_dcid, header.dcid.data, header.dcid.datalen);
params.original_dcid_present = true;
params.grease_quic_bit = 1;
ngtcp2_conn* tempConn;
if (const int ret = ngtcp2_conn_server_new(&tempConn, &header.scid, &newScid, &path, header.version,
&callbacks, &settings, &params, nullptr, &session))
{
throw std::runtime_error{fmt::format("ngtcp2_conn_server_new: {}",
ngtcp2_strerror (ret))};
}
m_QuicConnection.reset(tempConn);
memcpy(&m_LocalAddress, path.local.addr, path.local.addrlen);
m_LocalAddressLength = path.local.addrlen;
memcpy(&m_RemoteAddress, path.remote.addr, path.remote.addrlen);
m_RemoteAddressLength = path.remote.addrlen;
ngtcp2_crypto_gnutls_configure_server_session(m_TlsSession.get());
ngtcp2_conn_set_tls_native_handle(m_QuicConnection.get(), m_TlsSession.get());
gnutls_session_set_ptr(m_TlsSession.get(), &m_ConnRef);
m_TimerFd = timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK);
if (m_TimerFd < 0)
throw std::system_error(errno, std::generic_category(), "timerfd_create");
}
void Connection::OpenStream()
{
std::int64_t streamId;
if (ngtcp2_conn_open_bidi_stream(m_QuicConnection.get(), &streamId, nullptr))
throw std::runtime_error{""};
m_Stream.emplace(streamId);
}
Stream& Connection::GetStream()
{
return m_Stream.value();
}
void Connection::Read(const ngtcp2_addr remote, const std::span<std::uint8_t> data)
{
const ngtcp2_path path{
.local{ngtcp2_conn_get_path(m_QuicConnection.get())->local},
.remote{remote}
};
ngtcp2_pkt_info pi{};
if (const int ret{ngtcp2_conn_read_pkt(m_QuicConnection.get(), &path, &pi, data.data(), data.size(),
timestamp())})
{
throw std::runtime_error{"Destroy connection"};
}
}
void Connection::Write(const int socketFd)
{
WriteToStream(socketFd, m_QuicConnection.get(), nullptr,
{&m_RemoteAddress.sa, m_RemoteAddressLength});
if (m_Stream.has_value())
{
WriteToStream(socketFd, m_QuicConnection.get(), &m_Stream.value(),
{&m_RemoteAddress.sa, m_RemoteAddressLength});
}
const ngtcp2_tstamp expiry{ngtcp2_conn_get_expiry(m_QuicConnection.get())};
const ngtcp2_tstamp now{timestamp()};
itimerspec it{};
if (const int ret{timerfd_settime(m_TimerFd, 0, &it, nullptr)})
throw std::system_error{errno, std::generic_category(), "timerfd_settime"};
if (expiry < now)
{
it.it_value.tv_sec = 0;
it.it_value.tv_nsec = 1;
}
else
{
it.it_value.tv_sec = (expiry - now) / NGTCP2_SECONDS;
it.it_value.tv_nsec = ((expiry - now) % NGTCP2_SECONDS) / NGTCP2_NANOSECONDS;
}
if (const int ret{timerfd_settime(m_TimerFd, 0, &it, nullptr)})
throw std::system_error{errno, std::generic_category(), "timerfd_settime"};
return;
}
CNetServerSession::CNetServerSession(CNetServerWorker& server, const int socketFd,
const ngtcp2_settings& settings, gnutls_certificate_credentials_t credentials,
const ngtcp2_pkt_hd& header, const ngtcp2_cid& newScid, const ngtcp2_path& path) :
m_Server(server),
m_Connection{*this, settings, credentials, header, newScid, path},
m_FileTransferer(*this),
m_SocketFd{socketFd}
{
}
@ -58,18 +362,30 @@ void CNetServerSession::Disconnect(NetDisconnectReason reason)
Update((uint)NMT_CONNECTION_LOST, NULL);
enet_peer_disconnect(m_Peer, static_cast<enet_uint32>(reason));
}
const ngtcp2_ccerr quicReason{
.type{NGTCP2_CCERR_TYPE_APPLICATION},
.error_code{reason}
};
void CNetServerSession::DisconnectNow(NetDisconnectReason reason)
{
if (reason == NDR_UNKNOWN)
LOGWARNING("Disconnecting client without communicating the disconnect reason!");
ngtcp2_sockaddr_union local;
ngtcp2_sockaddr_union remote;
ngtcp2_path path{
.local{.addr{&local.sa}},
.remote{.addr{&remote.sa}}
};
std::array<std::uint8_t, MAX_UDP_PAYLOAD_SIZE> buffer;
const ngtcp2_ssize amount{ngtcp2_conn_write_connection_close(m_Connection.m_QuicConnection.get(), &path, nullptr, buffer.data(),
buffer.size(), &quicReason, timestamp())};
if (amount <= 0)
LOGERROR("closing connection %s", ngtcp2_strerror(static_cast<int>(amount)));
enet_peer_disconnect_now(m_Peer, static_cast<enet_uint32>(reason));
SendPacket(m_SocketFd, {buffer.data(), static_cast<std::size_t>(amount)}, path.remote);
}
bool CNetServerSession::SendMessage(const CNetMessage* message)
{
return m_Server.SendMessage(m_Peer, message);
m_Connection.m_Stream.value().PushMessage(message);
return true;
}

View file

@ -23,10 +23,51 @@
#include "network/NetHost.h"
#include "ps/CStr.h"
#include <deque>
#include <ngtcp2/ngtcp2.h>
#include <ngtcp2/ngtcp2_crypto.h>
#include <ngtcp2/ngtcp2_crypto_gnutls.h>
#include <optional>
#include <span>
#include <sys/timerfd.h>
#include <sys/poll.h>
#include <sys/socket.h>
#include <sys/timerfd.h>
#include "network/NetProtocol.h"
#include "ps/CLogger.h"
class CNetServerWorker;
typedef struct _ENetPeer ENetPeer;
class CNetServerSession;
class Connection
{
public:
Connection(CNetServerSession& session, const ngtcp2_settings& settings,
gnutls_certificate_credentials_t credentials, const ngtcp2_pkt_hd& header,
const ngtcp2_cid& scid, const ngtcp2_path& path);
void OpenStream();
Stream& GetStream();
void Read(const ngtcp2_addr remote, const std::span<std::uint8_t> data);
void Write(const int socketFd);
std::unique_ptr<gnutls_session_int, SessionDeleter> m_TlsSession;
std::unique_ptr<ngtcp2_conn, ConnectionDeleter> m_QuicConnection;
int m_TimerFd{-1};
public:
ngtcp2_sockaddr_union m_LocalAddress;
ngtcp2_socklen m_LocalAddressLength;
ngtcp2_sockaddr_union m_RemoteAddress;
ngtcp2_socklen m_RemoteAddressLength;
std::optional<Stream> m_Stream;
ngtcp2_crypto_conn_ref m_ConnRef;
};
/**
* The server's end of a network session.
* Represents an abstraction of the state of the client, storing all the per-client data
@ -40,7 +81,9 @@ class CNetServerSession : public CFsm<CNetServerSession, CNetMessage*>
NONCOPYABLE(CNetServerSession);
public:
CNetServerSession(CNetServerWorker& server, ENetPeer* peer);
CNetServerSession(CNetServerWorker& server, const int socketFd, const ngtcp2_settings& settings,
gnutls_certificate_credentials_t credentials, const ngtcp2_pkt_hd& header,
const ngtcp2_cid& newScid, const ngtcp2_path& path);
CNetServerWorker& GetServer() { return m_Server; }
@ -73,13 +116,6 @@ public:
*/
void Disconnect(NetDisconnectReason reason);
/**
* Sends an unreliable disconnection notification to the client.
* The server will not receive any disconnection notification.
* The server will not receive any further messages sent via this session.
*/
void DisconnectNow(NetDisconnectReason reason);
/**
* Send a message to the client.
*/
@ -89,7 +125,10 @@ public:
private:
CNetServerWorker& m_Server;
public:
Connection m_Connection;
private:
CNetFileTransferer m_FileTransferer;
ENetPeer* m_Peer;
@ -98,6 +137,7 @@ private:
CStrW m_UserName;
u32 m_HostID{0};
CStr m_Password;
int m_SocketFd;
};
#endif // NET_SERVER_SESSION_H

View file

@ -19,7 +19,10 @@
#include "NetStats.h"
#include "network/NetServerSession.h"
#include <fmt/format.h>
#include <ngtcp2/ngtcp2.h>
#include <string>
enum
@ -38,13 +41,8 @@ enum
NumberRows
};
CNetStatsTable::CNetStatsTable(const ENetPeer& peer)
: m_Peer(&peer)
{
}
CNetStatsTable::CNetStatsTable()
: m_Peer(NULL)
CNetStatsTable::CNetStatsTable(ngtcp2_conn* conn):
m_Connection{conn}
{
}
@ -55,7 +53,7 @@ CStr CNetStatsTable::GetName()
CStr CNetStatsTable::GetTitle()
{
if (m_Peer)
if (m_Connection)
return "Network client statistics";
else
return "Network host statistics";
@ -71,7 +69,7 @@ const std::vector<ProfileColumn>& CNetStatsTable::GetColumns()
m_ColumnDescriptions.clear();
m_ColumnDescriptions.push_back(ProfileColumn("Name", 200));
if (m_Peer)
if (m_Connection)
m_ColumnDescriptions.push_back(ProfileColumn("Value", 80));
else
{
@ -96,22 +94,26 @@ CStr CNetStatsTable::GetCellText(size_t row, size_t col)
#define ROW(id, title, member) \
case id: \
if (col == 0) return title; \
if (m_Peer) return std::to_string(m_Peer->member); \
if (m_Connection) return member; \
return "???"
ngtcp2_conn_info info;
if (col != 0)
ngtcp2_conn_get_conn_info(m_Connection, &info);
switch(row)
{
ROW(Row_InData, "incoming bytes", incomingDataTotal);
ROW(Row_OutData, "outgoing bytes", outgoingDataTotal);
ROW(Row_LastSendTime, "last send time", lastSendTime);
ROW(Row_LastRecvTime, "last receive time", lastReceiveTime);
ROW(Row_NextTimeout, "next timeout", nextTimeout);
ROW(Row_PacketsSent, "packets sent", packetsSent);
ROW(Row_PacketsLost, "packets lost", packetsLost);
ROW(Row_LastRTT, "last RTT", lastRoundTripTime);
ROW(Row_RTT, "mean RTT", roundTripTime);
ROW(Row_MTU, "MTU", mtu);
ROW(Row_ReliableInTransit, "reliable data in transit", reliableDataInTransit);
ROW(Row_InData, "incoming bytes", {});
ROW(Row_OutData, "outgoing bytes", {});
ROW(Row_LastSendTime, "last send time", {});
ROW(Row_LastRecvTime, "last receive time", {});
ROW(Row_NextTimeout, "next timeout", std::to_string(ngtcp2_conn_get_expiry(m_Connection)));
ROW(Row_PacketsSent, "packets sent", {});
ROW(Row_PacketsLost, "packets lost", {});
ROW(Row_LastRTT, "last RTT", std::to_string(info.latest_rtt));
ROW(Row_RTT, "mean RTT", std::to_string(info.smoothed_rtt));
ROW(Row_MTU, "MTU", std::to_string(ngtcp2_conn_get_path_max_tx_udp_payload_size(m_Connection)));
ROW(Row_ReliableInTransit, "reliable data in transit", std::to_string(info.bytes_in_flight));
default:
return "???";
@ -125,29 +127,39 @@ AbstractProfileTable* CNetStatsTable::GetChild(size_t /*row*/)
return 0;
}
void CNetStatsTable::LatchHostState(const ENetHost& host)
void CNetStatsTable::LatchHostState(const std::span<std::unique_ptr<CNetServerSession>> sessions)
{
std::lock_guard<std::mutex> lock(m_Mutex);
#define ROW(id, title, member) \
m_LatchedData[i].push_back(std::to_string(host.peers[i].member));
m_LatchedData[i].push_back(std::to_string(info.member));
m_LatchedData.clear();
m_LatchedData.resize(host.peerCount);
m_LatchedData.resize(sessions.size());
for (size_t i = 0; i < host.peerCount; ++i)
for (size_t i = 0; i < sessions.size(); ++i)
{
ROW(Row_InData, "incoming bytes", incomingDataTotal);
ROW(Row_OutData, "outgoing bytes", outgoingDataTotal);
ROW(Row_LastSendTime, "last send time", lastSendTime);
ROW(Row_LastRecvTime, "last receive time", lastReceiveTime);
ROW(Row_NextTimeout, "next timeout", nextTimeout);
ROW(Row_PacketsSent, "packets sent", packetsSent);
ROW(Row_PacketsLost, "packets lost", packetsLost);
ROW(Row_LastRTT, "last RTT", lastRoundTripTime);
ROW(Row_RTT, "mean RTT", roundTripTime);
ROW(Row_MTU, "MTU", mtu);
ROW(Row_ReliableInTransit, "reliable data in transit", reliableDataInTransit);
ngtcp2_conn_info info;
ngtcp2_conn_get_conn_info(sessions[i]->m_Connection.m_QuicConnection.get(), &info);
// ROW(Row_InData, "incoming bytes", bytes_recv);
m_LatchedData[i].push_back({});
// ROW(Row_OutData, "outgoing bytes", bytes_sent);
m_LatchedData[i].push_back({});
// ROW(Row_LastSendTime, "last send time", lastSendTime);
m_LatchedData[i].push_back({});
// ROW(Row_LastRecvTime, "last receive time", lastReceiveTime);
m_LatchedData[i].push_back({});
m_LatchedData[i].push_back(std::to_string(ngtcp2_conn_get_expiry(
sessions[i]->m_Connection.m_QuicConnection.get())));
// ROW(Row_PacketsSent, "packets sent", pkt_sent);
m_LatchedData[i].push_back({});
// ROW(Row_PacketsLost, "packets lost", pkt_lost);
m_LatchedData[i].push_back({});
ROW(Row_LastRTT, "last RTT", latest_rtt);
ROW(Row_RTT, "mean RTT", smoothed_rtt);
m_LatchedData[i].push_back(std::to_string(ngtcp2_conn_get_path_max_tx_udp_payload_size(
sessions[i]->m_Connection.m_QuicConnection.get())));
ROW(Row_ReliableInTransit, "reliable data in transit", bytes_in_flight);
}
#undef ROW
}

View file

@ -25,10 +25,13 @@
#include <cstddef>
#include <mutex>
#include <span>
#include <vector>
typedef struct _ENetPeer ENetPeer;
typedef struct _ENetHost ENetHost;
class CNetServerSession;
struct ngtcp2_conn;
/**
* ENet connection statistics profiler table.
@ -43,8 +46,8 @@ class CNetStatsTable : public AbstractProfileTable
{
NONCOPYABLE(CNetStatsTable);
public:
CNetStatsTable();
CNetStatsTable(const ENetPeer& peer);
CNetStatsTable() = default;
CNetStatsTable(ngtcp2_conn* conn);
CStr GetName() override;
CStr GetTitle() override;
@ -53,10 +56,10 @@ public:
CStr GetCellText(size_t row, size_t col) override;
AbstractProfileTable* GetChild(size_t row) override;
void LatchHostState(const ENetHost& host);
void LatchHostState(const std::span<std::unique_ptr<CNetServerSession>> sessions);
private:
const ENetPeer* m_Peer;
ngtcp2_conn* m_Connection{nullptr};
std::vector<ProfileColumn> m_ColumnDescriptions;
std::mutex m_Mutex;

View file

@ -111,7 +111,7 @@ void StartNetworkHost(const CStrW& playerName, const u16 serverPort, const CStr&
// Generate a secret to identify the host client.
g_Game = new CGame(storeReplay);
g_NetClient = new CNetClient(g_Game, "127.0.0.1", serverPort, playerName, hostJID, hashedPassword,
g_NetClient = new CNetClient(g_Game, "::1", serverPort, playerName, hostJID, hashedPassword,
secret);
}

View file

@ -48,7 +48,7 @@ public:
TS_ASSERT_EQUALS(msg.Serialize(buf) - (buf+len), 0);
TS_ASSERT_EQUALS(buf[len], '!');
CNetMessage* msg2 = CNetMessageFactory::CreateMessage(buf, len, script);
CNetMessage* msg2 = CNetMessageFactory::CreateMessage({buf, len}, script);
TS_ASSERT_STR_EQUALS(((CSimulationMessage*)msg2)->ToString(), "CSimulationMessage { m_Client: 1, m_Player: 2, m_Turn: 3, m_Data: [4] }");
delete msg2;