Network: Change protocol for game/rcon/settings password auth
Instead of sending a hash, do a DH/X25519 key exchange using the password. This also allows authenticating the associated rcon payload and response.
This commit is contained in:
@@ -1412,6 +1412,23 @@ void NetworkRandomBytesWithFallback(void *buf, size_t bytes)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void NetworkGameKeys::Initialise()
|
||||||
|
{
|
||||||
|
assert(!this->inited);
|
||||||
|
|
||||||
|
this->inited = true;
|
||||||
|
|
||||||
|
static_assert(sizeof(this->x25519_priv_key) == 32);
|
||||||
|
NetworkRandomBytesWithFallback(this->x25519_priv_key, sizeof(this->x25519_priv_key));
|
||||||
|
crypto_x25519_public_key(this->x25519_pub_key, this->x25519_priv_key);
|
||||||
|
}
|
||||||
|
|
||||||
|
NetworkSharedSecrets::~NetworkSharedSecrets()
|
||||||
|
{
|
||||||
|
static_assert(sizeof(*this) == 64);
|
||||||
|
crypto_wipe(this, sizeof(*this));
|
||||||
|
}
|
||||||
|
|
||||||
#ifdef __EMSCRIPTEN__
|
#ifdef __EMSCRIPTEN__
|
||||||
extern "C" {
|
extern "C" {
|
||||||
|
|
||||||
|
@@ -33,6 +33,7 @@
|
|||||||
#include "../core/checksum_func.hpp"
|
#include "../core/checksum_func.hpp"
|
||||||
#include "../fileio_func.h"
|
#include "../fileio_func.h"
|
||||||
#include "../debug_settings.h"
|
#include "../debug_settings.h"
|
||||||
|
#include "../3rdparty/monocypher/monocypher.h"
|
||||||
|
|
||||||
#include "table/strings.h"
|
#include "table/strings.h"
|
||||||
|
|
||||||
@@ -40,6 +41,8 @@
|
|||||||
|
|
||||||
/* This file handles all the client-commands */
|
/* This file handles all the client-commands */
|
||||||
|
|
||||||
|
static void ResetClientConnectionKeyStates();
|
||||||
|
|
||||||
/** Read some packets, and when do use that data as initial load filter. */
|
/** Read some packets, and when do use that data as initial load filter. */
|
||||||
struct PacketReader : LoadFilter {
|
struct PacketReader : LoadFilter {
|
||||||
static const size_t CHUNK = 32 * 1024; ///< 32 KiB chunks of memory.
|
static const size_t CHUNK = 32 * 1024; ///< 32 KiB chunks of memory.
|
||||||
@@ -174,6 +177,8 @@ ClientNetworkGameSocketHandler::~ClientNetworkGameSocketHandler()
|
|||||||
FioFCloseFile(this->desync_log_file);
|
FioFCloseFile(this->desync_log_file);
|
||||||
this->desync_log_file = nullptr;
|
this->desync_log_file = nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ResetClientConnectionKeyStates();
|
||||||
}
|
}
|
||||||
|
|
||||||
NetworkRecvStatus ClientNetworkGameSocketHandler::CloseConnection(NetworkRecvStatus status)
|
NetworkRecvStatus ClientNetworkGameSocketHandler::CloseConnection(NetworkRecvStatus status)
|
||||||
@@ -380,12 +385,10 @@ static uint32 last_ack_frame;
|
|||||||
|
|
||||||
/** One bit of 'entropy' used to generate a salt for the company passwords. */
|
/** One bit of 'entropy' used to generate a salt for the company passwords. */
|
||||||
static uint32 _company_password_game_seed;
|
static uint32 _company_password_game_seed;
|
||||||
/** One bit of 'entropy' used to generate a salt for the server passwords. */
|
/** Network server's x25519 public key, used for key derivation */
|
||||||
static uint64 _server_password_game_seed;
|
static byte _server_x25519_pub_key[32];
|
||||||
/** One bit of 'entropy' used to generate a salt for the rcon passwords. */
|
/** Key message ID counter */
|
||||||
static uint64 _rcon_password_game_seed;
|
static uint64 _next_key_message_id;
|
||||||
/** One bit of 'entropy' used to generate a salt for the settings passwords. */
|
|
||||||
static uint64 _settings_password_game_seed;
|
|
||||||
/** The other bit of 'entropy' used to generate a salt for the server, rcon, and settings passwords. */
|
/** The other bit of 'entropy' used to generate a salt for the server, rcon, and settings passwords. */
|
||||||
static std::string _password_server_id;
|
static std::string _password_server_id;
|
||||||
/** The other bit of 'entropy' used to generate a salt for the company passwords. */
|
/** The other bit of 'entropy' used to generate a salt for the company passwords. */
|
||||||
@@ -402,6 +405,60 @@ NetworkJoinInfo _network_join;
|
|||||||
/** Make sure the server ID length is the same as a md5 hash. */
|
/** Make sure the server ID length is the same as a md5 hash. */
|
||||||
static_assert(NETWORK_SERVER_ID_LENGTH == 16 * 2 + 1);
|
static_assert(NETWORK_SERVER_ID_LENGTH == 16 * 2 + 1);
|
||||||
|
|
||||||
|
NetworkRecvStatus ClientNetworkGameSocketHandler::SendKeyPasswordPacket(PacketType packet_type, NetworkSharedSecrets &ss, const std::string &password, const std::string *payload)
|
||||||
|
{
|
||||||
|
const NetworkGameKeys &keys = this->GetKeys();
|
||||||
|
|
||||||
|
byte shared_secret[32]; // Shared secret
|
||||||
|
crypto_x25519(shared_secret, keys.x25519_priv_key, _server_x25519_pub_key);
|
||||||
|
if (std::count(shared_secret, shared_secret + 32, 0) == 32) {
|
||||||
|
/* Secret is all 0 because public key is all 0, just give up at this point */
|
||||||
|
return NETWORK_RECV_STATUS_MALFORMED_PACKET;
|
||||||
|
}
|
||||||
|
|
||||||
|
crypto_blake2b_ctx ctx;
|
||||||
|
crypto_blake2b_init (&ctx, 64);
|
||||||
|
crypto_blake2b_update(&ctx, shared_secret, 32); // Shared secret
|
||||||
|
crypto_blake2b_update(&ctx, keys.x25519_pub_key, 32); // Client pub key
|
||||||
|
crypto_blake2b_update(&ctx, _server_x25519_pub_key, 32); // Server pub key
|
||||||
|
crypto_blake2b_update(&ctx, (const byte *)password.data(), password.size()); // Password
|
||||||
|
crypto_blake2b_final (&ctx, ss.shared_data);
|
||||||
|
|
||||||
|
/* NetworkSharedSecrets::shared_data now contains 2 keys worth of hash, first key is used for up direction, second key for down direction (if any) */
|
||||||
|
|
||||||
|
crypto_wipe(shared_secret, 32);
|
||||||
|
|
||||||
|
std::vector<byte> message;
|
||||||
|
BufferSerialiser buffer(message);
|
||||||
|
|
||||||
|
/* Put monotonically increasing counter in message */
|
||||||
|
buffer.Send_uint64(_next_key_message_id);
|
||||||
|
|
||||||
|
/* Put actual payload in message, if there is one */
|
||||||
|
if (payload != nullptr) buffer.Send_string(*payload);
|
||||||
|
|
||||||
|
/* Message authentication code */
|
||||||
|
uint8 mac[16];
|
||||||
|
|
||||||
|
/* Use only once per key: random */
|
||||||
|
uint8 nonce[24];
|
||||||
|
NetworkRandomBytesWithFallback(nonce, 24);
|
||||||
|
|
||||||
|
/* Encrypt in place, use first half of hash as key */
|
||||||
|
crypto_aead_lock(message.data(), mac, ss.shared_data, nonce, keys.x25519_pub_key, 32, message.data(), message.size());
|
||||||
|
|
||||||
|
Packet *p = new Packet(packet_type, SHRT_MAX);
|
||||||
|
p->Send_binary(keys.x25519_pub_key, 32);
|
||||||
|
p->Send_binary(nonce, 24);
|
||||||
|
p->Send_binary(mac, 16);
|
||||||
|
p->Send_binary(message.data(), message.size());
|
||||||
|
|
||||||
|
_next_key_message_id++;
|
||||||
|
|
||||||
|
my_client->SendPacket(p);
|
||||||
|
return NETWORK_RECV_STATUS_OKAY;
|
||||||
|
}
|
||||||
|
|
||||||
/***********
|
/***********
|
||||||
* Sending functions
|
* Sending functions
|
||||||
* DEF_CLIENT_SEND_COMMAND has no parameters
|
* DEF_CLIENT_SEND_COMMAND has no parameters
|
||||||
@@ -438,10 +495,8 @@ NetworkRecvStatus ClientNetworkGameSocketHandler::SendNewGRFsOk()
|
|||||||
*/
|
*/
|
||||||
NetworkRecvStatus ClientNetworkGameSocketHandler::SendGamePassword(const std::string &password)
|
NetworkRecvStatus ClientNetworkGameSocketHandler::SendGamePassword(const std::string &password)
|
||||||
{
|
{
|
||||||
Packet *p = new Packet(PACKET_CLIENT_GAME_PASSWORD, SHRT_MAX);
|
NetworkSharedSecrets ss;
|
||||||
p->Send_buffer(GenerateGeneralPasswordHash(password, _password_server_id, _server_password_game_seed));
|
return my_client->SendKeyPasswordPacket(PACKET_CLIENT_GAME_PASSWORD, ss, password, nullptr);
|
||||||
my_client->SendPacket(p);
|
|
||||||
return NETWORK_RECV_STATUS_OKAY;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -462,14 +517,14 @@ NetworkRecvStatus ClientNetworkGameSocketHandler::SendCompanyPassword(const std:
|
|||||||
*/
|
*/
|
||||||
NetworkRecvStatus ClientNetworkGameSocketHandler::SendSettingsPassword(const std::string &password)
|
NetworkRecvStatus ClientNetworkGameSocketHandler::SendSettingsPassword(const std::string &password)
|
||||||
{
|
{
|
||||||
Packet *p = new Packet(PACKET_CLIENT_SETTINGS_PASSWORD, SHRT_MAX);
|
|
||||||
if (password.empty()) {
|
if (password.empty()) {
|
||||||
p->Send_buffer(nullptr, 0);
|
Packet *p = new Packet(PACKET_CLIENT_SETTINGS_PASSWORD, SHRT_MAX);
|
||||||
|
my_client->SendPacket(p);
|
||||||
|
return NETWORK_RECV_STATUS_OKAY;
|
||||||
} else {
|
} else {
|
||||||
p->Send_buffer(GenerateGeneralPasswordHash(password, _password_server_id, _settings_password_game_seed));
|
NetworkSharedSecrets ss;
|
||||||
|
return my_client->SendKeyPasswordPacket(PACKET_CLIENT_SETTINGS_PASSWORD, ss, password, nullptr);
|
||||||
}
|
}
|
||||||
my_client->SendPacket(p);
|
|
||||||
return NETWORK_RECV_STATUS_OKAY;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Request the map from the server. */
|
/** Request the map from the server. */
|
||||||
@@ -637,11 +692,7 @@ NetworkRecvStatus ClientNetworkGameSocketHandler::SendQuit()
|
|||||||
*/
|
*/
|
||||||
NetworkRecvStatus ClientNetworkGameSocketHandler::SendRCon(const std::string &pass, const std::string &command)
|
NetworkRecvStatus ClientNetworkGameSocketHandler::SendRCon(const std::string &pass, const std::string &command)
|
||||||
{
|
{
|
||||||
Packet *p = new Packet(PACKET_CLIENT_RCON, SHRT_MAX);
|
return my_client->SendKeyPasswordPacket(PACKET_CLIENT_RCON, my_client->last_rcon_shared_secrets, pass, &command);
|
||||||
p->Send_buffer(GenerateGeneralPasswordHash(pass, _password_server_id, _rcon_password_game_seed));
|
|
||||||
p->Send_string(command);
|
|
||||||
my_client->SendPacket(p);
|
|
||||||
return NETWORK_RECV_STATUS_OKAY;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -836,7 +887,8 @@ NetworkRecvStatus ClientNetworkGameSocketHandler::Receive_SERVER_NEED_GAME_PASSW
|
|||||||
if (this->status < STATUS_JOIN || this->status >= STATUS_AUTH_GAME) return NETWORK_RECV_STATUS_MALFORMED_PACKET;
|
if (this->status < STATUS_JOIN || this->status >= STATUS_AUTH_GAME) return NETWORK_RECV_STATUS_MALFORMED_PACKET;
|
||||||
this->status = STATUS_AUTH_GAME;
|
this->status = STATUS_AUTH_GAME;
|
||||||
|
|
||||||
_server_password_game_seed = p->Recv_uint64();
|
static_assert(sizeof(_server_x25519_pub_key) == 32);
|
||||||
|
p->Recv_binary(_server_x25519_pub_key, sizeof(_server_x25519_pub_key));
|
||||||
_password_server_id = p->Recv_string(NETWORK_SERVER_ID_LENGTH);
|
_password_server_id = p->Recv_string(NETWORK_SERVER_ID_LENGTH);
|
||||||
if (this->HasClientQuit()) return NETWORK_RECV_STATUS_MALFORMED_PACKET;
|
if (this->HasClientQuit()) return NETWORK_RECV_STATUS_MALFORMED_PACKET;
|
||||||
|
|
||||||
@@ -876,9 +928,8 @@ NetworkRecvStatus ClientNetworkGameSocketHandler::Receive_SERVER_WELCOME(Packet
|
|||||||
|
|
||||||
/* Initialize the password hash salting variables, even if they were previously. */
|
/* Initialize the password hash salting variables, even if they were previously. */
|
||||||
_company_password_game_seed = p->Recv_uint32();
|
_company_password_game_seed = p->Recv_uint32();
|
||||||
_server_password_game_seed = p->Recv_uint64();
|
static_assert(sizeof(_server_x25519_pub_key) == 32);
|
||||||
_rcon_password_game_seed = p->Recv_uint64();
|
p->Recv_binary(_server_x25519_pub_key, sizeof(_server_x25519_pub_key));
|
||||||
_settings_password_game_seed = p->Recv_uint64();
|
|
||||||
_password_server_id = p->Recv_string(NETWORK_SERVER_ID_LENGTH);
|
_password_server_id = p->Recv_string(NETWORK_SERVER_ID_LENGTH);
|
||||||
_company_password_server_id = p->Recv_string(NETWORK_SERVER_ID_LENGTH);
|
_company_password_server_id = p->Recv_string(NETWORK_SERVER_ID_LENGTH);
|
||||||
|
|
||||||
@@ -1250,12 +1301,26 @@ NetworkRecvStatus ClientNetworkGameSocketHandler::Receive_SERVER_RCON(Packet *p)
|
|||||||
{
|
{
|
||||||
if (this->status < STATUS_AUTHORIZED) return NETWORK_RECV_STATUS_MALFORMED_PACKET;
|
if (this->status < STATUS_AUTHORIZED) return NETWORK_RECV_STATUS_MALFORMED_PACKET;
|
||||||
|
|
||||||
TextColour colour_code = (TextColour)p->Recv_uint16();
|
if (!p->CanReadFromPacket(1)) {
|
||||||
if (!IsValidConsoleColour(colour_code)) return NETWORK_RECV_STATUS_MALFORMED_PACKET;
|
IConsolePrint(CC_ERROR, "Access Denied");
|
||||||
|
return NETWORK_RECV_STATUS_OKAY;
|
||||||
|
}
|
||||||
|
|
||||||
std::string rcon_out = p->Recv_string(NETWORK_RCONCOMMAND_LENGTH);
|
byte nonce[24];
|
||||||
|
byte mac[16];
|
||||||
|
p->Recv_binary(nonce, 24);
|
||||||
|
p->Recv_binary(mac, 16);
|
||||||
|
|
||||||
IConsolePrint(colour_code, rcon_out.c_str());
|
std::vector<byte> message = p->Recv_binary(p->RemainingBytesToTransfer());
|
||||||
|
|
||||||
|
if (crypto_aead_unlock(message.data(), mac, this->last_rcon_shared_secrets.shared_data + 32, nonce, nullptr, 0, message.data(), message.size()) == 0) {
|
||||||
|
SubPacketDeserialiser spd(p, message);
|
||||||
|
TextColour colour_code = (TextColour)spd.Recv_uint16();
|
||||||
|
if (!IsValidConsoleColour(colour_code)) return NETWORK_RECV_STATUS_MALFORMED_PACKET;
|
||||||
|
|
||||||
|
std::string rcon_out = spd.Recv_string(NETWORK_RCONCOMMAND_LENGTH);
|
||||||
|
IConsolePrint(colour_code, rcon_out.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
return NETWORK_RECV_STATUS_OKAY;
|
return NETWORK_RECV_STATUS_OKAY;
|
||||||
}
|
}
|
||||||
@@ -1373,6 +1438,12 @@ std::string ClientNetworkGameSocketHandler::GetDebugInfo() const
|
|||||||
return stdstr_fmt("status: %d (%s)", this->status, GetServerStatusName(this->status));
|
return stdstr_fmt("status: %d (%s)", this->status, GetServerStatusName(this->status));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void ResetClientConnectionKeyStates()
|
||||||
|
{
|
||||||
|
_next_key_message_id = 0;
|
||||||
|
crypto_wipe(_server_x25519_pub_key, sizeof(_server_x25519_pub_key));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/** Is called after a client is connected to the server */
|
/** Is called after a client is connected to the server */
|
||||||
void NetworkClient_Connected()
|
void NetworkClient_Connected()
|
||||||
@@ -1381,6 +1452,7 @@ void NetworkClient_Connected()
|
|||||||
_frame_counter = 0;
|
_frame_counter = 0;
|
||||||
_frame_counter_server = 0;
|
_frame_counter_server = 0;
|
||||||
last_ack_frame = 0;
|
last_ack_frame = 0;
|
||||||
|
ResetClientConnectionKeyStates();
|
||||||
/* Request the game-info */
|
/* Request the game-info */
|
||||||
MyClient::SendJoin();
|
MyClient::SendJoin();
|
||||||
}
|
}
|
||||||
|
@@ -18,6 +18,7 @@ private:
|
|||||||
std::string connection_string; ///< Address we are connected to.
|
std::string connection_string; ///< Address we are connected to.
|
||||||
struct PacketReader *savegame; ///< Packet reader for reading the savegame.
|
struct PacketReader *savegame; ///< Packet reader for reading the savegame.
|
||||||
byte token; ///< The token we need to send back to the server to prove we're the right client.
|
byte token; ///< The token we need to send back to the server to prove we're the right client.
|
||||||
|
NetworkSharedSecrets last_rcon_shared_secrets; ///< Keys for last rcon (and incoming replies)
|
||||||
|
|
||||||
/** Status of the connection with the server. */
|
/** Status of the connection with the server. */
|
||||||
enum ServerStatus {
|
enum ServerStatus {
|
||||||
@@ -40,6 +41,8 @@ private:
|
|||||||
std::string server_desync_log;
|
std::string server_desync_log;
|
||||||
bool emergency_save_done = false;
|
bool emergency_save_done = false;
|
||||||
|
|
||||||
|
NetworkGameKeys intl_keys;
|
||||||
|
|
||||||
static const char *GetServerStatusName(ServerStatus status);
|
static const char *GetServerStatusName(ServerStatus status);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
@@ -81,6 +84,9 @@ protected:
|
|||||||
static NetworkRecvStatus SendGetMap();
|
static NetworkRecvStatus SendGetMap();
|
||||||
static NetworkRecvStatus SendMapOk();
|
static NetworkRecvStatus SendMapOk();
|
||||||
void CheckConnection();
|
void CheckConnection();
|
||||||
|
|
||||||
|
NetworkRecvStatus SendKeyPasswordPacket(PacketType packet_type, NetworkSharedSecrets &ss, const std::string &password, const std::string *payload);
|
||||||
|
|
||||||
public:
|
public:
|
||||||
ClientNetworkGameSocketHandler(SOCKET s, std::string connection_string);
|
ClientNetworkGameSocketHandler(SOCKET s, std::string connection_string);
|
||||||
~ClientNetworkGameSocketHandler();
|
~ClientNetworkGameSocketHandler();
|
||||||
@@ -90,6 +96,12 @@ public:
|
|||||||
|
|
||||||
std::string GetDebugInfo() const override;
|
std::string GetDebugInfo() const override;
|
||||||
|
|
||||||
|
const NetworkGameKeys &GetKeys()
|
||||||
|
{
|
||||||
|
if (!this->intl_keys.inited) this->intl_keys.Initialise();
|
||||||
|
return this->intl_keys;
|
||||||
|
}
|
||||||
|
|
||||||
static NetworkRecvStatus SendJoin();
|
static NetworkRecvStatus SendJoin();
|
||||||
static NetworkRecvStatus SendCommand(const CommandPacket *cp);
|
static NetworkRecvStatus SendCommand(const CommandPacket *cp);
|
||||||
static NetworkRecvStatus SendError(NetworkErrorCode errorno, NetworkRecvStatus recvstatus = NETWORK_RECV_STATUS_OKAY);
|
static NetworkRecvStatus SendError(NetworkErrorCode errorno, NetworkRecvStatus recvstatus = NETWORK_RECV_STATUS_OKAY);
|
||||||
|
@@ -84,6 +84,7 @@ bool NetworkServerChangeClientName(ClientID client_id, const std::string &new_na
|
|||||||
|
|
||||||
void NetworkServerDoMove(ClientID client_id, CompanyID company_id);
|
void NetworkServerDoMove(ClientID client_id, CompanyID company_id);
|
||||||
void NetworkServerSendRcon(ClientID client_id, TextColour colour_code, const std::string &string);
|
void NetworkServerSendRcon(ClientID client_id, TextColour colour_code, const std::string &string);
|
||||||
|
void NetworkServerSendRconDenied(ClientID client_id);
|
||||||
void NetworkServerSendChat(NetworkAction action, DestType type, int dest, const std::string &msg, ClientID from_id, NetworkTextMessageData data = NetworkTextMessageData(), bool from_admin = false);
|
void NetworkServerSendChat(NetworkAction action, DestType type, int dest, const std::string &msg, ClientID from_id, NetworkTextMessageData data = NetworkTextMessageData(), bool from_admin = false);
|
||||||
void NetworkServerSendExternalChat(const std::string &source, TextColour colour, const std::string &user, const std::string &msg);
|
void NetworkServerSendExternalChat(const std::string &source, TextColour colour, const std::string &user, const std::string &msg);
|
||||||
|
|
||||||
|
@@ -122,6 +122,20 @@ struct NetworkGameList *NetworkAddServer(const std::string &connection_string, b
|
|||||||
void NetworkRebuildHostList();
|
void NetworkRebuildHostList();
|
||||||
void UpdateNetworkGameWindow();
|
void UpdateNetworkGameWindow();
|
||||||
|
|
||||||
|
struct NetworkGameKeys {
|
||||||
|
byte x25519_priv_key[32]; ///< x25519 key: private part
|
||||||
|
byte x25519_pub_key[32]; ///< x25519 key: public part
|
||||||
|
bool inited = false;
|
||||||
|
|
||||||
|
void Initialise();
|
||||||
|
};
|
||||||
|
|
||||||
|
struct NetworkSharedSecrets {
|
||||||
|
byte shared_data[64];
|
||||||
|
|
||||||
|
~NetworkSharedSecrets();
|
||||||
|
};
|
||||||
|
|
||||||
/* From network_command.cpp */
|
/* From network_command.cpp */
|
||||||
/**
|
/**
|
||||||
* Everything we need to know about a command to be able to execute it.
|
* Everything we need to know about a command to be able to execute it.
|
||||||
@@ -148,6 +162,7 @@ StringID GetNetworkErrorMsg(NetworkErrorCode err);
|
|||||||
bool NetworkMakeClientNameUnique(std::string &new_name);
|
bool NetworkMakeClientNameUnique(std::string &new_name);
|
||||||
std::string GenerateCompanyPasswordHash(const std::string &password, const std::string &password_server_id, uint32 password_game_seed);
|
std::string GenerateCompanyPasswordHash(const std::string &password, const std::string &password_server_id, uint32 password_game_seed);
|
||||||
std::vector<uint8> GenerateGeneralPasswordHash(const std::string &password, const std::string &password_server_id, uint64 password_game_seed);
|
std::vector<uint8> GenerateGeneralPasswordHash(const std::string &password, const std::string &password_server_id, uint64 password_game_seed);
|
||||||
|
std::string BytesToHexString(const byte *data, uint length);
|
||||||
std::string NetworkGenerateRandomKeyString(uint bytes);
|
std::string NetworkGenerateRandomKeyString(uint bytes);
|
||||||
|
|
||||||
std::string_view ParseCompanyFromConnectionString(const std::string &connection_string, CompanyID *company_id);
|
std::string_view ParseCompanyFromConnectionString(const std::string &connection_string, CompanyID *company_id);
|
||||||
|
@@ -31,6 +31,7 @@
|
|||||||
#include "../rev.h"
|
#include "../rev.h"
|
||||||
#include "../crashlog.h"
|
#include "../crashlog.h"
|
||||||
#include "../3rdparty/randombytes/randombytes.h"
|
#include "../3rdparty/randombytes/randombytes.h"
|
||||||
|
#include "../3rdparty/monocypher/monocypher.h"
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
#include <condition_variable>
|
#include <condition_variable>
|
||||||
#if defined(__MINGW32__)
|
#if defined(__MINGW32__)
|
||||||
@@ -193,16 +194,6 @@ struct PacketWriter : SaveFilter {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const std::vector<byte> &ServerNetworkGameSocketHandler::CachedPassword::GetHash(const std::string &password, uint64 password_game_seed)
|
|
||||||
{
|
|
||||||
if (password != this->source) {
|
|
||||||
this->source = password;
|
|
||||||
this->cached_hash = GenerateGeneralPasswordHash(password, _settings_client.network.network_id, password_game_seed);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this->cached_hash;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new socket for the server side of the game connection.
|
* Create a new socket for the server side of the game connection.
|
||||||
* @param s The socket to connect with.
|
* @param s The socket to connect with.
|
||||||
@@ -213,14 +204,6 @@ ServerNetworkGameSocketHandler::ServerNetworkGameSocketHandler(SOCKET s) : Netwo
|
|||||||
this->client_id = _network_client_id++;
|
this->client_id = _network_client_id++;
|
||||||
this->receive_limit = _settings_client.network.bytes_per_frame_burst;
|
this->receive_limit = _settings_client.network.bytes_per_frame_burst;
|
||||||
|
|
||||||
uint64 seeds[3];
|
|
||||||
static_assert(sizeof(seeds) == 24);
|
|
||||||
NetworkRandomBytesWithFallback(seeds, sizeof(seeds));
|
|
||||||
|
|
||||||
this->server_hash_bits = seeds[0];
|
|
||||||
this->rcon_hash_bits = seeds[1];
|
|
||||||
this->settings_hash_bits = seeds[2];
|
|
||||||
|
|
||||||
/* The Socket and Info pools need to be the same in size. After all,
|
/* The Socket and Info pools need to be the same in size. After all,
|
||||||
* each Socket will be associated with at most one Info object. As
|
* each Socket will be associated with at most one Info object. As
|
||||||
* such if the Socket was allocated the Info object can as well. */
|
* such if the Socket was allocated the Info object can as well. */
|
||||||
@@ -244,6 +227,61 @@ ServerNetworkGameSocketHandler::~ServerNetworkGameSocketHandler()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool ServerNetworkGameSocketHandler::ParseKeyPasswordPacket(Packet *p, NetworkSharedSecrets &ss, const std::string &password, std::string *payload, size_t length)
|
||||||
|
{
|
||||||
|
byte client_pub_key[32];
|
||||||
|
byte nonce[24];
|
||||||
|
byte mac[16];
|
||||||
|
p->Recv_binary(client_pub_key, 32);
|
||||||
|
p->Recv_binary(nonce, 24);
|
||||||
|
p->Recv_binary(mac, 16);
|
||||||
|
|
||||||
|
const NetworkGameKeys &keys = this->GetKeys();
|
||||||
|
|
||||||
|
byte shared_secret[32]; // Shared secret
|
||||||
|
crypto_x25519(shared_secret, keys.x25519_priv_key, client_pub_key);
|
||||||
|
if (std::count(shared_secret, shared_secret + 32, 0) == 32) {
|
||||||
|
/* Secret is all 0 because public key is all 0, just reject it */
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
crypto_blake2b_ctx ctx;
|
||||||
|
crypto_blake2b_init (&ctx, 64);
|
||||||
|
crypto_blake2b_update(&ctx, shared_secret, 32); // Shared secret
|
||||||
|
crypto_blake2b_update(&ctx, client_pub_key, 32); // Client pub key
|
||||||
|
crypto_blake2b_update(&ctx, keys.x25519_pub_key, 32); // Server pub key
|
||||||
|
crypto_blake2b_update(&ctx, (const byte *)password.data(), password.size()); // Password
|
||||||
|
crypto_blake2b_final (&ctx, ss.shared_data);
|
||||||
|
|
||||||
|
/* NetworkSharedSecrets::shared_data now contains 2 keys worth of hash, first key is used for up direction, second key for down direction (if any) */
|
||||||
|
|
||||||
|
crypto_wipe(shared_secret, 32);
|
||||||
|
|
||||||
|
std::vector<byte> message = p->Recv_binary(p->RemainingBytesToTransfer());
|
||||||
|
if (message.size() < 8) return false;
|
||||||
|
if ((message.size() == 8) != (payload == nullptr)) {
|
||||||
|
/* Payload expected but not present, or vice versa, just give up */
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Decrypt in place, use first half of hash as key */
|
||||||
|
if (crypto_aead_unlock(message.data(), mac, ss.shared_data, nonce, client_pub_key, 32, message.data(), message.size()) == 0) {
|
||||||
|
SubPacketDeserialiser spd(p, message);
|
||||||
|
uint64 message_id = spd.Recv_uint64();
|
||||||
|
if (message_id < this->min_key_message_id) {
|
||||||
|
/* ID has not increased monotonically, reject the message */
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
this->min_key_message_id = message_id + 1;
|
||||||
|
if (payload != nullptr) {
|
||||||
|
*payload = spd.Recv_string(length);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
std::unique_ptr<Packet> ServerNetworkGameSocketHandler::ReceivePacket()
|
std::unique_ptr<Packet> ServerNetworkGameSocketHandler::ReceivePacket()
|
||||||
{
|
{
|
||||||
/* Only allow receiving when we have some buffer free; this value
|
/* Only allow receiving when we have some buffer free; this value
|
||||||
@@ -487,8 +525,11 @@ NetworkRecvStatus ServerNetworkGameSocketHandler::SendNeedGamePassword()
|
|||||||
/* Reset 'lag' counters */
|
/* Reset 'lag' counters */
|
||||||
this->last_frame = this->last_frame_server = _frame_counter;
|
this->last_frame = this->last_frame_server = _frame_counter;
|
||||||
|
|
||||||
|
const NetworkGameKeys &keys = this->GetKeys();
|
||||||
|
|
||||||
Packet *p = new Packet(PACKET_SERVER_NEED_GAME_PASSWORD, SHRT_MAX);
|
Packet *p = new Packet(PACKET_SERVER_NEED_GAME_PASSWORD, SHRT_MAX);
|
||||||
p->Send_uint64(this->server_hash_bits);
|
static_assert(sizeof(keys.x25519_pub_key) == 32);
|
||||||
|
p->Send_binary(keys.x25519_pub_key, sizeof(keys.x25519_pub_key));
|
||||||
p->Send_string(_settings_client.network.network_id);
|
p->Send_string(_settings_client.network.network_id);
|
||||||
this->SendPacket(p);
|
this->SendPacket(p);
|
||||||
return NETWORK_RECV_STATUS_OKAY;
|
return NETWORK_RECV_STATUS_OKAY;
|
||||||
@@ -525,12 +566,13 @@ NetworkRecvStatus ServerNetworkGameSocketHandler::SendWelcome()
|
|||||||
|
|
||||||
_network_game_info.clients_on++;
|
_network_game_info.clients_on++;
|
||||||
|
|
||||||
|
const NetworkGameKeys &keys = this->GetKeys();
|
||||||
|
|
||||||
p = new Packet(PACKET_SERVER_WELCOME, SHRT_MAX);
|
p = new Packet(PACKET_SERVER_WELCOME, SHRT_MAX);
|
||||||
p->Send_uint32(this->client_id);
|
p->Send_uint32(this->client_id);
|
||||||
p->Send_uint32(_settings_game.game_creation.generation_seed);
|
p->Send_uint32(_settings_game.game_creation.generation_seed);
|
||||||
p->Send_uint64(this->server_hash_bits);
|
static_assert(sizeof(keys.x25519_pub_key) == 32);
|
||||||
p->Send_uint64(this->rcon_hash_bits);
|
p->Send_binary(keys.x25519_pub_key, sizeof(keys.x25519_pub_key));
|
||||||
p->Send_uint64(this->settings_hash_bits);
|
|
||||||
p->Send_string(_settings_client.network.network_id);
|
p->Send_string(_settings_client.network.network_id);
|
||||||
p->Send_string(_network_company_server_id);
|
p->Send_string(_network_company_server_id);
|
||||||
this->SendPacket(p);
|
this->SendPacket(p);
|
||||||
@@ -804,10 +846,38 @@ NetworkRecvStatus ServerNetworkGameSocketHandler::SendNewGame()
|
|||||||
*/
|
*/
|
||||||
NetworkRecvStatus ServerNetworkGameSocketHandler::SendRConResult(uint16 colour, const std::string &command)
|
NetworkRecvStatus ServerNetworkGameSocketHandler::SendRConResult(uint16 colour, const std::string &command)
|
||||||
{
|
{
|
||||||
Packet *p = new Packet(PACKET_SERVER_RCON, SHRT_MAX);
|
assert(this->rcon_reply_key != nullptr);
|
||||||
|
|
||||||
p->Send_uint16(colour);
|
std::vector<byte> message;
|
||||||
p->Send_string(command);
|
BufferSerialiser buffer(message);
|
||||||
|
buffer.Send_uint16(colour);
|
||||||
|
buffer.Send_string(command);
|
||||||
|
|
||||||
|
/* Message authentication code */
|
||||||
|
uint8 mac[16];
|
||||||
|
|
||||||
|
/* Use only once per key: random */
|
||||||
|
uint8 nonce[24];
|
||||||
|
NetworkRandomBytesWithFallback(nonce, 24);
|
||||||
|
|
||||||
|
/* Encrypt in place, use first half of hash as key */
|
||||||
|
crypto_aead_lock(message.data(), mac, this->rcon_reply_key, nonce, nullptr, 0, message.data(), message.size());
|
||||||
|
|
||||||
|
Packet *p = new Packet(PACKET_SERVER_RCON, SHRT_MAX);
|
||||||
|
p->Send_binary(nonce, 24);
|
||||||
|
p->Send_binary(mac, 16);
|
||||||
|
p->Send_binary(message.data(), message.size());
|
||||||
|
|
||||||
|
this->SendPacket(p);
|
||||||
|
return NETWORK_RECV_STATUS_OKAY;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a deny result of a console action.
|
||||||
|
*/
|
||||||
|
NetworkRecvStatus ServerNetworkGameSocketHandler::SendRConDenied()
|
||||||
|
{
|
||||||
|
Packet *p = new Packet(PACKET_SERVER_RCON, SHRT_MAX);
|
||||||
this->SendPacket(p);
|
this->SendPacket(p);
|
||||||
return NETWORK_RECV_STATUS_OKAY;
|
return NETWORK_RECV_STATUS_OKAY;
|
||||||
}
|
}
|
||||||
@@ -979,13 +1049,13 @@ NetworkRecvStatus ServerNetworkGameSocketHandler::Receive_CLIENT_GAME_PASSWORD(P
|
|||||||
return this->SendError(NETWORK_ERROR_NOT_EXPECTED);
|
return this->SendError(NETWORK_ERROR_NOT_EXPECTED);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<byte> password = p->Recv_buffer();
|
|
||||||
|
|
||||||
/* Check game password. Allow joining if we cleared the password meanwhile */
|
/* Check game password. Allow joining if we cleared the password meanwhile */
|
||||||
if (!_settings_client.network.server_password.empty() &&
|
if (!_settings_client.network.server_password.empty()) {
|
||||||
password != this->game_password_hash_cache.GetHash(_settings_client.network.server_password, this->server_hash_bits)) {
|
NetworkSharedSecrets ss;
|
||||||
/* Password is invalid */
|
if (!this->ParseKeyPasswordPacket(p, ss, _settings_client.network.server_password, nullptr, 0)) {
|
||||||
return this->SendError(NETWORK_ERROR_WRONG_PASSWORD);
|
/* Password is invalid */
|
||||||
|
return this->SendError(NETWORK_ERROR_WRONG_PASSWORD);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const NetworkClientInfo *ci = this->GetInfo();
|
const NetworkClientInfo *ci = this->GetInfo();
|
||||||
@@ -1025,16 +1095,16 @@ NetworkRecvStatus ServerNetworkGameSocketHandler::Receive_CLIENT_SETTINGS_PASSWO
|
|||||||
return this->SendError(NETWORK_ERROR_NOT_EXPECTED);
|
return this->SendError(NETWORK_ERROR_NOT_EXPECTED);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<byte> password = p->Recv_buffer();
|
NetworkSharedSecrets ss;
|
||||||
|
|
||||||
/* Check settings password. Deny if no password is set */
|
/* Check settings password. Deny if no password is set */
|
||||||
if (password.empty()) {
|
if (!p->CanReadFromPacket(1)) {
|
||||||
if (this->settings_authed) DEBUG(net, 0, "[settings-ctrl] client-id %d deauthed", this->client_id);
|
if (this->settings_authed) DEBUG(net, 0, "[settings-ctrl] client-id %d deauthed", this->client_id);
|
||||||
this->settings_authed = false;
|
this->settings_authed = false;
|
||||||
} else if (_settings_client.network.settings_password.empty() ||
|
} else if (_settings_client.network.settings_password.empty() ||
|
||||||
password != this->settings_password_hash_cache.GetHash(_settings_client.network.settings_password, this->settings_hash_bits)) {
|
!this->ParseKeyPasswordPacket(p, ss, _settings_client.network.settings_password, nullptr, 0)) {
|
||||||
DEBUG(net, 0, "[settings-ctrl] wrong password from client-id %d", this->client_id);
|
DEBUG(net, 0, "[settings-ctrl] wrong password from client-id %d", this->client_id);
|
||||||
NetworkServerSendRcon(this->client_id, CC_ERROR, "Access Denied");
|
NetworkServerSendRconDenied(this->client_id);
|
||||||
this->settings_authed = false;
|
this->settings_authed = false;
|
||||||
NetworkRecvStatus status = this->HandleAuthFailure(this->settings_auth_failures);
|
NetworkRecvStatus status = this->HandleAuthFailure(this->settings_auth_failures);
|
||||||
if (status != NETWORK_RECV_STATUS_OKAY) return status;
|
if (status != NETWORK_RECV_STATUS_OKAY) return status;
|
||||||
@@ -1591,25 +1661,26 @@ NetworkRecvStatus ServerNetworkGameSocketHandler::Receive_CLIENT_RCON(Packet *p)
|
|||||||
if (this->status != STATUS_ACTIVE) return this->SendError(NETWORK_ERROR_NOT_EXPECTED);
|
if (this->status != STATUS_ACTIVE) return this->SendError(NETWORK_ERROR_NOT_EXPECTED);
|
||||||
|
|
||||||
if (_settings_client.network.rcon_password.empty()) {
|
if (_settings_client.network.rcon_password.empty()) {
|
||||||
NetworkServerSendRcon(this->client_id, CC_ERROR, "Access Denied");
|
NetworkServerSendRconDenied(this->client_id);
|
||||||
return this->HandleAuthFailure(this->rcon_auth_failures);
|
return this->HandleAuthFailure(this->rcon_auth_failures);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<byte> password = p->Recv_buffer();
|
std::string command;
|
||||||
std::string command = p->Recv_string(NETWORK_RCONCOMMAND_LENGTH);
|
NetworkSharedSecrets ss;
|
||||||
|
if (!this->ParseKeyPasswordPacket(p, ss, _settings_client.network.rcon_password, &command, NETWORK_RCONCOMMAND_LENGTH)) {
|
||||||
if (password != this->rcon_password_hash_cache.GetHash(_settings_client.network.rcon_password, this->rcon_hash_bits)) {
|
|
||||||
DEBUG(net, 0, "[rcon] wrong password from client-id %d", this->client_id);
|
DEBUG(net, 0, "[rcon] wrong password from client-id %d", this->client_id);
|
||||||
NetworkServerSendRcon(this->client_id, CC_ERROR, "Access Denied");
|
NetworkServerSendRconDenied(this->client_id);
|
||||||
return this->HandleAuthFailure(this->rcon_auth_failures);
|
return this->HandleAuthFailure(this->rcon_auth_failures);
|
||||||
}
|
}
|
||||||
|
|
||||||
DEBUG(net, 3, "[rcon] Client-id %d executed: %s", this->client_id, command.c_str());
|
DEBUG(net, 3, "[rcon] Client-id %d executed: %s", this->client_id, command.c_str());
|
||||||
|
|
||||||
_redirect_console_to_client = this->client_id;
|
_redirect_console_to_client = this->client_id;
|
||||||
|
this->rcon_reply_key = ss.shared_data + 32; /* second key */
|
||||||
IConsoleCmdExec(command.c_str());
|
IConsoleCmdExec(command.c_str());
|
||||||
_redirect_console_to_client = INVALID_CLIENT_ID;
|
_redirect_console_to_client = INVALID_CLIENT_ID;
|
||||||
this->rcon_auth_failures = 0;
|
this->rcon_auth_failures = 0;
|
||||||
|
this->rcon_reply_key = nullptr;
|
||||||
return NETWORK_RECV_STATUS_OKAY;
|
return NETWORK_RECV_STATUS_OKAY;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2204,6 +2275,17 @@ void NetworkServerSendRcon(ClientID client_id, TextColour colour_code, const std
|
|||||||
NetworkClientSocket::GetByClientID(client_id)->SendRConResult(colour_code, string);
|
NetworkClientSocket::GetByClientID(client_id)->SendRConResult(colour_code, string);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send an rcon reply to the client.
|
||||||
|
* @param client_id The identifier of the client.
|
||||||
|
* @param colour_code The colour of the text.
|
||||||
|
* @param string The actual reply.
|
||||||
|
*/
|
||||||
|
void NetworkServerSendRconDenied(ClientID client_id)
|
||||||
|
{
|
||||||
|
NetworkClientSocket::GetByClientID(client_id)->SendRConDenied();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Kick a single client.
|
* Kick a single client.
|
||||||
* @param client_id The client to kick.
|
* @param client_id The client to kick.
|
||||||
|
@@ -22,15 +22,9 @@ extern NetworkClientSocketPool _networkclientsocket_pool;
|
|||||||
|
|
||||||
/** Class for handling the server side of the game connection. */
|
/** Class for handling the server side of the game connection. */
|
||||||
class ServerNetworkGameSocketHandler : public NetworkClientSocketPool::PoolItem<&_networkclientsocket_pool>, public NetworkGameSocketHandler, public TCPListenHandler<ServerNetworkGameSocketHandler, PACKET_SERVER_FULL, PACKET_SERVER_BANNED> {
|
class ServerNetworkGameSocketHandler : public NetworkClientSocketPool::PoolItem<&_networkclientsocket_pool>, public NetworkGameSocketHandler, public TCPListenHandler<ServerNetworkGameSocketHandler, PACKET_SERVER_FULL, PACKET_SERVER_BANNED> {
|
||||||
struct CachedPassword {
|
NetworkGameKeys intl_keys;
|
||||||
std::string source;
|
uint64 min_key_message_id = 0;
|
||||||
std::vector<byte> cached_hash;
|
byte *rcon_reply_key = nullptr;
|
||||||
|
|
||||||
const std::vector<byte> &GetHash(const std::string &password, uint64 password_game_seed);
|
|
||||||
};
|
|
||||||
CachedPassword game_password_hash_cache;
|
|
||||||
CachedPassword rcon_password_hash_cache;
|
|
||||||
CachedPassword settings_password_hash_cache;
|
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
NetworkRecvStatus Receive_CLIENT_JOIN(Packet *p) override;
|
NetworkRecvStatus Receive_CLIENT_JOIN(Packet *p) override;
|
||||||
@@ -61,6 +55,8 @@ protected:
|
|||||||
NetworkRecvStatus SendNeedGamePassword();
|
NetworkRecvStatus SendNeedGamePassword();
|
||||||
NetworkRecvStatus SendNeedCompanyPassword();
|
NetworkRecvStatus SendNeedCompanyPassword();
|
||||||
|
|
||||||
|
bool ParseKeyPasswordPacket(Packet *p, NetworkSharedSecrets &ss, const std::string &password, std::string *payload, size_t length);
|
||||||
|
|
||||||
public:
|
public:
|
||||||
/** Status of a client */
|
/** Status of a client */
|
||||||
enum ClientStatus {
|
enum ClientStatus {
|
||||||
@@ -86,9 +82,6 @@ public:
|
|||||||
ClientStatus status; ///< Status of this client
|
ClientStatus status; ///< Status of this client
|
||||||
CommandQueue outgoing_queue; ///< The command-queue awaiting delivery
|
CommandQueue outgoing_queue; ///< The command-queue awaiting delivery
|
||||||
size_t receive_limit; ///< Amount of bytes that we can receive at this moment
|
size_t receive_limit; ///< Amount of bytes that we can receive at this moment
|
||||||
uint64 server_hash_bits; ///< Server password hash entropy bits
|
|
||||||
uint64 rcon_hash_bits; ///< Rcon password hash entropy bits
|
|
||||||
uint64 settings_hash_bits; ///< Settings password hash entropy bits
|
|
||||||
bool settings_authed = false;///< Authorised to control all game settings
|
bool settings_authed = false;///< Authorised to control all game settings
|
||||||
bool supports_zstd = false; ///< Client supports zstd compression
|
bool supports_zstd = false; ///< Client supports zstd compression
|
||||||
|
|
||||||
@@ -119,6 +112,7 @@ public:
|
|||||||
NetworkRecvStatus SendShutdown();
|
NetworkRecvStatus SendShutdown();
|
||||||
NetworkRecvStatus SendNewGame();
|
NetworkRecvStatus SendNewGame();
|
||||||
NetworkRecvStatus SendRConResult(uint16 colour, const std::string &command);
|
NetworkRecvStatus SendRConResult(uint16 colour, const std::string &command);
|
||||||
|
NetworkRecvStatus SendRConDenied();
|
||||||
NetworkRecvStatus SendMove(ClientID client_id, CompanyID company_id);
|
NetworkRecvStatus SendMove(ClientID client_id, CompanyID company_id);
|
||||||
|
|
||||||
NetworkRecvStatus SendClientInfo(NetworkClientInfo *ci);
|
NetworkRecvStatus SendClientInfo(NetworkClientInfo *ci);
|
||||||
@@ -138,6 +132,12 @@ public:
|
|||||||
|
|
||||||
std::string GetDebugInfo() const override;
|
std::string GetDebugInfo() const override;
|
||||||
|
|
||||||
|
const NetworkGameKeys &GetKeys()
|
||||||
|
{
|
||||||
|
if (!this->intl_keys.inited) this->intl_keys.Initialise();
|
||||||
|
return this->intl_keys;
|
||||||
|
}
|
||||||
|
|
||||||
static void Send();
|
static void Send();
|
||||||
static void AcceptConnection(SOCKET s, const NetworkAddress &address);
|
static void AcceptConnection(SOCKET s, const NetworkAddress &address);
|
||||||
static bool AllowConnection();
|
static bool AllowConnection();
|
||||||
|
Reference in New Issue
Block a user