diff --git a/docs/admin_network.md b/docs/admin_network.md index 4703df322a..7dae8a0de8 100644 --- a/docs/admin_network.md +++ b/docs/admin_network.md @@ -1,6 +1,6 @@ # OpenTTD's admin network -Last updated: 2011-01-20 +Last updated: 2024-03-26 ## Table of contents @@ -49,10 +49,29 @@ Last updated: 2011-01-20 Create a TCP connection to the server on port 3977. The application is expected to authenticate within 10 seconds. - To authenticate send a `ADMIN_PACKET_ADMIN_JOIN` packet. + To authenticate send either an `ADMIN_PACKET_ADMIN_JOIN` or an + `ADMIN_PACKET_ADMIN_JOIN_SECURE` packet. - The server will reply with `ADMIN_PACKET_SERVER_PROTOCOL` followed directly by - `ADMIN_PACKET_SERVER_WELCOME`. + The `ADMIN_PACKET_ADMIN_JOIN` packet sends the password without any + encryption or safeguards over the connection, and as such has been disabled + by default. + + The `ADMIN_PACKET_ADMIN_JOIN_SECURE` packet initiates a key exchange + authentication schema which tells te server which methods the client + supports and the server makes a choice. The server will then send an + `ADMIN_PACKET_SERVER_AUTH_REQUEST` packet to which the client has to respond + with an `ADMIN_PACKET_ADMIN_AUTH_RESPONSE` packet. + + The current choices for secure authentication are authorized keys, where + the client has a private key and the server a list of authorized public + keys, and a so-called password-authenticated key exchange which allows to + authenticate using a password without actually sending the password. + The server falls back to password authentication when the client's key is + not in the list of authorized keys. + + When authentication has succeeded for either of the `JOIN` schemas, the + server will reply with `ADMIN_PACKET_SERVER_PROTOCOL` followed directly + by `ADMIN_PACKET_SERVER_WELCOME`. `ADMIN_PACKET_SERVER_PROTOCOL` contains details about the protocol version. It is the job of your application to check this number and decide whether diff --git a/src/console_cmds.cpp b/src/console_cmds.cpp index 62a8e18c6d..a4c321ee92 100644 --- a/src/console_cmds.cpp +++ b/src/console_cmds.cpp @@ -1964,6 +1964,7 @@ DEF_CONSOLE_CMD(ConCompanyPassword) /** All the known authorized keys with their name. */ static std::vector> _console_cmd_authorized_keys{ + { "admin", &_settings_client.network.admin_authorized_keys }, { "rcon", &_settings_client.network.rcon_authorized_keys }, { "server", &_settings_client.network.server_authorized_keys }, }; diff --git a/src/lang/english.txt b/src/lang/english.txt index 1530dd1e20..103ddef3de 100644 --- a/src/lang/english.txt +++ b/src/lang/english.txt @@ -2590,7 +2590,7 @@ STR_NETWORK_ERROR_INVALID_CLIENT_NAME :{WHITE}Your pla STR_NETWORK_ERROR_CLIENT_GUI_LOST_CONNECTION_CAPTION :{WHITE}Possible connection loss STR_NETWORK_ERROR_CLIENT_GUI_LOST_CONNECTION :{WHITE}The last {NUM} second{P "" s} no data has arrived from the server -###length 22 +###length 23 STR_NETWORK_ERROR_CLIENT_GENERAL :general error STR_NETWORK_ERROR_CLIENT_DESYNC :desync error STR_NETWORK_ERROR_CLIENT_SAVEGAME :could not load map @@ -2603,6 +2603,7 @@ STR_NETWORK_ERROR_CLIENT_WRONG_REVISION :wrong revision STR_NETWORK_ERROR_CLIENT_NAME_IN_USE :name already in use STR_NETWORK_ERROR_CLIENT_WRONG_PASSWORD :wrong password STR_NETWORK_ERROR_CLIENT_NOT_ON_ALLOW_LIST :not on allow list +STR_NETWORK_ERROR_CLIENT_NO_AUTHENTICATION_METHOD_AVAILABLE :none of the requested authentication methods is available STR_NETWORK_ERROR_CLIENT_COMPANY_MISMATCH :wrong company in DoCommand STR_NETWORK_ERROR_CLIENT_KICKED :kicked by server STR_NETWORK_ERROR_CLIENT_CHEATER :was trying to use a cheat diff --git a/src/network/core/tcp_admin.cpp b/src/network/core/tcp_admin.cpp index d29e08ba63..044950940c 100644 --- a/src/network/core/tcp_admin.cpp +++ b/src/network/core/tcp_admin.cpp @@ -63,6 +63,8 @@ NetworkRecvStatus NetworkAdminSocketHandler::HandlePacket(Packet &p) case ADMIN_PACKET_ADMIN_RCON: return this->Receive_ADMIN_RCON(p); case ADMIN_PACKET_ADMIN_GAMESCRIPT: return this->Receive_ADMIN_GAMESCRIPT(p); case ADMIN_PACKET_ADMIN_PING: return this->Receive_ADMIN_PING(p); + case ADMIN_PACKET_ADMIN_JOIN_SECURE: return this->Receive_ADMIN_JOIN_SECURE(p); + case ADMIN_PACKET_ADMIN_AUTH_RESPONSE: return this->Receive_ADMIN_AUTH_RESPONSE(p); case ADMIN_PACKET_SERVER_FULL: return this->Receive_SERVER_FULL(p); case ADMIN_PACKET_SERVER_BANNED: return this->Receive_SERVER_BANNED(p); @@ -91,6 +93,8 @@ NetworkRecvStatus NetworkAdminSocketHandler::HandlePacket(Packet &p) case ADMIN_PACKET_SERVER_CMD_LOGGING: return this->Receive_SERVER_CMD_LOGGING(p); case ADMIN_PACKET_SERVER_RCON_END: return this->Receive_SERVER_RCON_END(p); case ADMIN_PACKET_SERVER_PONG: return this->Receive_SERVER_PONG(p); + case ADMIN_PACKET_SERVER_AUTH_REQUEST: return this->Receive_SERVER_AUTH_REQUEST(p); + case ADMIN_PACKET_SERVER_ENABLE_ENCRYPTION: return this->Receive_SERVER_ENABLE_ENCRYPTION(p); default: Debug(net, 0, "[tcp/admin] Received invalid packet type {} from '{}' ({})", type, this->admin_name, this->admin_version); @@ -137,6 +141,8 @@ NetworkRecvStatus NetworkAdminSocketHandler::Receive_ADMIN_EXTERNAL_CHAT(Packet NetworkRecvStatus NetworkAdminSocketHandler::Receive_ADMIN_RCON(Packet &) { return this->ReceiveInvalidPacket(ADMIN_PACKET_ADMIN_RCON); } NetworkRecvStatus NetworkAdminSocketHandler::Receive_ADMIN_GAMESCRIPT(Packet &) { return this->ReceiveInvalidPacket(ADMIN_PACKET_ADMIN_GAMESCRIPT); } NetworkRecvStatus NetworkAdminSocketHandler::Receive_ADMIN_PING(Packet &) { return this->ReceiveInvalidPacket(ADMIN_PACKET_ADMIN_PING); } +NetworkRecvStatus NetworkAdminSocketHandler::Receive_ADMIN_JOIN_SECURE(Packet &) { return this->ReceiveInvalidPacket(ADMIN_PACKET_ADMIN_JOIN_SECURE); } +NetworkRecvStatus NetworkAdminSocketHandler::Receive_ADMIN_AUTH_RESPONSE(Packet &) { return this->ReceiveInvalidPacket(ADMIN_PACKET_ADMIN_AUTH_RESPONSE); } NetworkRecvStatus NetworkAdminSocketHandler::Receive_SERVER_FULL(Packet &) { return this->ReceiveInvalidPacket(ADMIN_PACKET_SERVER_FULL); } NetworkRecvStatus NetworkAdminSocketHandler::Receive_SERVER_BANNED(Packet &) { return this->ReceiveInvalidPacket(ADMIN_PACKET_SERVER_BANNED); } @@ -165,3 +171,5 @@ NetworkRecvStatus NetworkAdminSocketHandler::Receive_SERVER_CMD_NAMES(Packet &) NetworkRecvStatus NetworkAdminSocketHandler::Receive_SERVER_CMD_LOGGING(Packet &) { return this->ReceiveInvalidPacket(ADMIN_PACKET_SERVER_CMD_LOGGING); } NetworkRecvStatus NetworkAdminSocketHandler::Receive_SERVER_RCON_END(Packet &) { return this->ReceiveInvalidPacket(ADMIN_PACKET_SERVER_RCON_END); } NetworkRecvStatus NetworkAdminSocketHandler::Receive_SERVER_PONG(Packet &) { return this->ReceiveInvalidPacket(ADMIN_PACKET_SERVER_PONG); } +NetworkRecvStatus NetworkAdminSocketHandler::Receive_SERVER_AUTH_REQUEST(Packet &) { return this->ReceiveInvalidPacket(ADMIN_PACKET_SERVER_AUTH_REQUEST); } +NetworkRecvStatus NetworkAdminSocketHandler::Receive_SERVER_ENABLE_ENCRYPTION(Packet &) { return this->ReceiveInvalidPacket(ADMIN_PACKET_SERVER_ENABLE_ENCRYPTION); } diff --git a/src/network/core/tcp_admin.h b/src/network/core/tcp_admin.h index 4320a06f66..e3433effbc 100644 --- a/src/network/core/tcp_admin.h +++ b/src/network/core/tcp_admin.h @@ -22,7 +22,7 @@ * This protocol may only be extended to ensure stability. */ enum PacketAdminType : uint8_t { - ADMIN_PACKET_ADMIN_JOIN, ///< The admin announces and authenticates itself to the server. + ADMIN_PACKET_ADMIN_JOIN, ///< The admin announces and authenticates itself to the server using an unsecured passwords. ADMIN_PACKET_ADMIN_QUIT, ///< The admin tells the server that it is quitting. ADMIN_PACKET_ADMIN_UPDATE_FREQUENCY, ///< The admin tells the server the update frequency of a particular piece of information. ADMIN_PACKET_ADMIN_POLL, ///< The admin explicitly polls for a piece of information. @@ -31,6 +31,8 @@ enum PacketAdminType : uint8_t { ADMIN_PACKET_ADMIN_GAMESCRIPT, ///< The admin sends a JSON string for the GameScript. ADMIN_PACKET_ADMIN_PING, ///< The admin sends a ping to the server, expecting a ping-reply (PONG) packet. ADMIN_PACKET_ADMIN_EXTERNAL_CHAT, ///< The admin sends a chat message from external source. + ADMIN_PACKET_ADMIN_JOIN_SECURE, ///< The admin announces and starts a secure authentication handshake. + ADMIN_PACKET_ADMIN_AUTH_RESPONSE, ///< The admin responds to the authentication request. ADMIN_PACKET_SERVER_FULL = 100, ///< The server tells the admin it cannot accept the admin. ADMIN_PACKET_SERVER_BANNED, ///< The server tells the admin it is banned. @@ -61,6 +63,8 @@ enum PacketAdminType : uint8_t { ADMIN_PACKET_SERVER_RCON_END, ///< The server indicates that the remote console command has completed. ADMIN_PACKET_SERVER_PONG, ///< The server replies to a ping request from the admin. ADMIN_PACKET_SERVER_CMD_LOGGING, ///< The server gives the admin copies of incoming command packets. + ADMIN_PACKET_SERVER_AUTH_REQUEST, ///< The server gives the admin the used authentication method and required parameters. + ADMIN_PACKET_SERVER_ENABLE_ENCRYPTION, ///< The server tells that authentication has completed and requests to enable encryption with the keys of the last \c ADMIN_PACKET_ADMIN_AUTH_RESPONSE. INVALID_ADMIN_PACKET = 0xFF, ///< An invalid marker for admin packets. }; @@ -68,6 +72,7 @@ enum PacketAdminType : uint8_t { /** Status of an admin. */ enum AdminStatus { ADMIN_STATUS_INACTIVE, ///< The admin is not connected nor active. + ADMIN_STATUS_AUTHENTICATE, ///< The admin is connected and working on authentication. ADMIN_STATUS_ACTIVE, ///< The admin is active. ADMIN_STATUS_END, ///< Must ALWAYS be on the end of this list!! (period) }; @@ -118,8 +123,8 @@ protected: NetworkRecvStatus ReceiveInvalidPacket(PacketAdminType type); /** - * Join the admin network: - * string Password the server is expecting for this network. + * Join the admin network using an unsecured password exchange: + * string Unsecured password the server is expecting for this network. * string Name of the application being used to connect. * string Version string of the application being used to connect. * @param p The packet that was just received. @@ -200,6 +205,32 @@ protected: */ virtual NetworkRecvStatus Receive_ADMIN_PING(Packet &p); + /** + * Join the admin network using a secure authentication method: + * string Name of the application being used to connect. + * string Version string of the application being used to connect. + * uint16_t Bitmask of supported authentication methods. See \c NetworkAuthenticationMethod for the supported methods. + * + * The server will determine which of the authentication methods supplied by the client will be used. + * When there is no supported authentication method, an \c ADMIN_PACKET_SERVER_ERROR packet will be + * sent with \c NETWORK_ERROR_NO_AUTHENTICATION_METHOD_AVAILABLE as error. + * @param p The packet that was just received. + * @return The state the network should have. + */ + virtual NetworkRecvStatus Receive_ADMIN_JOIN_SECURE(Packet &p); + + /** + * Admin responds to \c ADMIN_PACKET_SERVER_AUTH_REQUEST with the appropriate + * data given the agreed upon \c NetworkAuthenticationMethod. + * With \c NETWORK_AUTH_METHOD_X25519_PAKE and \c NETWORK_AUTH_METHOD_X25519_AUTHORIZED_KEY: + * 32 * uint8_t Public key of the client. + * 16 * uint8_t Message authentication code (mac). + * 8 * uint8_t Encrypted message of the authentication (just random bytes). + * @param p The packet that was just received. + * @return The state the network should have. + */ + virtual NetworkRecvStatus Receive_ADMIN_AUTH_RESPONSE(Packet &p); + /** * The server is full (connection gets closed). * @param p The packet that was just received. @@ -472,6 +503,25 @@ protected: */ virtual NetworkRecvStatus Receive_SERVER_CMD_LOGGING(Packet &p); + /** + * Server requests authentication challenge from the admin. + * uint8_t The chosen authentication method from \c NetworkAuthenticationMethod. + * With \c NETWORK_AUTH_METHOD_X25519_PAKE and \c NETWORK_AUTH_METHOD_X25519_AUTHORIZED_KEY: + * 32 * uint8_t Public key of the server. + * 24 * uint8_t Nonce to use for the encryption. + * @param p The packet that was just received. + * @return The state the network should have. + */ + virtual NetworkRecvStatus Receive_SERVER_AUTH_REQUEST(Packet &p); + + /** + * Indication to the client that authentication is complete and encryption has to be used from here on forward. + * The encryption uses the shared keys generated by the last AUTH_REQUEST key exchange. + * 24 * uint8_t Nonce for encrypted connection. + * @param p The packet that was just received. + */ + virtual NetworkRecvStatus Receive_SERVER_ENABLE_ENCRYPTION(Packet &p); + /** * Send a ping-reply (pong) to the admin that sent us the ping packet. * uint32_t Integer identifier - should be the same as read from the admins ping packet. diff --git a/src/network/network.cpp b/src/network/network.cpp index 5eb64da827..0c4503f768 100644 --- a/src/network/network.cpp +++ b/src/network/network.cpp @@ -379,6 +379,7 @@ StringID GetNetworkErrorMsg(NetworkErrorCode err) STR_NETWORK_ERROR_CLIENT_TIMEOUT_JOIN, STR_NETWORK_ERROR_CLIENT_INVALID_CLIENT_NAME, STR_NETWORK_ERROR_CLIENT_NOT_ON_ALLOW_LIST, + STR_NETWORK_ERROR_CLIENT_NO_AUTHENTICATION_METHOD_AVAILABLE, }; static_assert(lengthof(network_error_strings) == NETWORK_ERROR_END); @@ -948,8 +949,8 @@ bool NetworkServerStart() Debug(net, 5, "Starting listeners for clients"); if (!ServerNetworkGameSocketHandler::Listen(_settings_client.network.server_port)) return false; - /* Only listen for admins when the password isn't empty. */ - if (!_settings_client.network.admin_password.empty()) { + /* Only listen for admins when the authentication is configured. */ + if (_settings_client.network.AdminAuthenticationConfigured()) { Debug(net, 5, "Starting listeners for admins"); if (!ServerNetworkAdminSocketHandler::Listen(_settings_client.network.server_admin_port)) return false; } diff --git a/src/network/network_admin.cpp b/src/network/network_admin.cpp index 82cebe186e..b635bca1a5 100644 --- a/src/network/network_admin.cpp +++ b/src/network/network_admin.cpp @@ -38,6 +38,9 @@ uint8_t _network_admins_connected = 0; NetworkAdminSocketPool _networkadminsocket_pool("NetworkAdminSocket"); INSTANTIATE_POOL_METHODS(NetworkAdminSocket) +static NetworkAuthenticationDefaultPasswordProvider _admin_password_provider(_settings_client.network.admin_password); ///< Provides the password validation for the game's password. +static NetworkAuthenticationDefaultAuthorizedKeyHandler _admin_authorized_key_handler(_settings_client.network.admin_authorized_keys); ///< Provides the authorized key handling for the game authentication. + /** The timeout for authorisation of the client. */ static const std::chrono::seconds ADMIN_AUTHORISATION_TIMEOUT(10); @@ -90,7 +93,7 @@ ServerNetworkAdminSocketHandler::~ServerNetworkAdminSocketHandler() */ /* static */ bool ServerNetworkAdminSocketHandler::AllowConnection() { - bool accept = !_settings_client.network.admin_password.empty() && _network_admins_connected < MAX_ADMINS; + bool accept = _settings_client.network.AdminAuthenticationConfigured() && _network_admins_connected < MAX_ADMINS; /* We can't go over the MAX_ADMINS limit here. However, if we accept * the connection, there has to be space in the pool. */ static_assert(NetworkAdminSocketPool::MAX_SIZE == MAX_ADMINS); @@ -102,7 +105,7 @@ ServerNetworkAdminSocketHandler::~ServerNetworkAdminSocketHandler() /* static */ void ServerNetworkAdminSocketHandler::Send() { for (ServerNetworkAdminSocketHandler *as : ServerNetworkAdminSocketHandler::Iterate()) { - if (as->status == ADMIN_STATUS_INACTIVE && std::chrono::steady_clock::now() > as->connect_time + ADMIN_AUTHORISATION_TIMEOUT) { + if (as->status <= ADMIN_STATUS_AUTHENTICATE && std::chrono::steady_clock::now() > as->connect_time + ADMIN_AUTHORISATION_TIMEOUT) { Debug(net, 2, "[admin] Admin did not send its authorisation within {} seconds", std::chrono::duration_cast(ADMIN_AUTHORISATION_TIMEOUT).count()); as->CloseConnection(true); continue; @@ -134,6 +137,9 @@ ServerNetworkAdminSocketHandler::~ServerNetworkAdminSocketHandler() */ NetworkRecvStatus ServerNetworkAdminSocketHandler::SendError(NetworkErrorCode error) { + /* Whatever the error might be, authentication (keys) must be released as soon as possible. */ + this->authentication_handler = nullptr; + auto p = std::make_unique(this, ADMIN_PACKET_SERVER_ERROR); p->Send_uint8(error); @@ -149,6 +155,8 @@ NetworkRecvStatus ServerNetworkAdminSocketHandler::SendError(NetworkErrorCode er /** Send the protocol version to the admin. */ NetworkRecvStatus ServerNetworkAdminSocketHandler::SendProtocol() { + this->status = ADMIN_STATUS_ACTIVE; + auto p = std::make_unique(this, ADMIN_PACKET_SERVER_PROTOCOL); /* announce the protocol version */ @@ -491,7 +499,7 @@ NetworkRecvStatus ServerNetworkAdminSocketHandler::SendRcon(uint16_t colour, con NetworkRecvStatus ServerNetworkAdminSocketHandler::Receive_ADMIN_RCON(Packet &p) { - if (this->status == ADMIN_STATUS_INACTIVE) return this->SendError(NETWORK_ERROR_NOT_EXPECTED); + if (this->status <= ADMIN_STATUS_AUTHENTICATE) return this->SendError(NETWORK_ERROR_NOT_EXPECTED); std::string command = p.Recv_string(NETWORK_RCONCOMMAND_LENGTH); @@ -505,7 +513,7 @@ NetworkRecvStatus ServerNetworkAdminSocketHandler::Receive_ADMIN_RCON(Packet &p) NetworkRecvStatus ServerNetworkAdminSocketHandler::Receive_ADMIN_GAMESCRIPT(Packet &p) { - if (this->status == ADMIN_STATUS_INACTIVE) return this->SendError(NETWORK_ERROR_NOT_EXPECTED); + if (this->status <= ADMIN_STATUS_AUTHENTICATE) return this->SendError(NETWORK_ERROR_NOT_EXPECTED); std::string json = p.Recv_string(NETWORK_GAMESCRIPT_JSON_LENGTH); @@ -517,7 +525,7 @@ NetworkRecvStatus ServerNetworkAdminSocketHandler::Receive_ADMIN_GAMESCRIPT(Pack NetworkRecvStatus ServerNetworkAdminSocketHandler::Receive_ADMIN_PING(Packet &p) { - if (this->status == ADMIN_STATUS_INACTIVE) return this->SendError(NETWORK_ERROR_NOT_EXPECTED); + if (this->status <= ADMIN_STATUS_AUTHENTICATE) return this->SendError(NETWORK_ERROR_NOT_EXPECTED); uint32_t d1 = p.Recv_uint32(); @@ -631,6 +639,11 @@ NetworkRecvStatus ServerNetworkAdminSocketHandler::Receive_ADMIN_JOIN(Packet &p) { if (this->status != ADMIN_STATUS_INACTIVE) return this->SendError(NETWORK_ERROR_NOT_EXPECTED); + if (!_settings_client.network.allow_insecure_admin_login) { + /* You're not authorized to login using this method. */ + return this->SendError(NETWORK_ERROR_NOT_AUTHORIZED); + } + std::string password = p.Recv_string(NETWORK_PASSWORD_LENGTH); if (_settings_client.network.admin_password.empty() || @@ -647,8 +660,6 @@ NetworkRecvStatus ServerNetworkAdminSocketHandler::Receive_ADMIN_JOIN(Packet &p) return this->SendError(NETWORK_ERROR_ILLEGAL_PACKET); } - this->status = ADMIN_STATUS_ACTIVE; - Debug(net, 3, "[admin] '{}' ({}) has connected", this->admin_name, this->admin_version); return this->SendProtocol(); @@ -662,7 +673,7 @@ NetworkRecvStatus ServerNetworkAdminSocketHandler::Receive_ADMIN_QUIT(Packet &) NetworkRecvStatus ServerNetworkAdminSocketHandler::Receive_ADMIN_UPDATE_FREQUENCY(Packet &p) { - if (this->status == ADMIN_STATUS_INACTIVE) return this->SendError(NETWORK_ERROR_NOT_EXPECTED); + if (this->status <= ADMIN_STATUS_AUTHENTICATE) return this->SendError(NETWORK_ERROR_NOT_EXPECTED); AdminUpdateType type = (AdminUpdateType)p.Recv_uint16(); AdminUpdateFrequency freq = (AdminUpdateFrequency)p.Recv_uint16(); @@ -682,7 +693,7 @@ NetworkRecvStatus ServerNetworkAdminSocketHandler::Receive_ADMIN_UPDATE_FREQUENC NetworkRecvStatus ServerNetworkAdminSocketHandler::Receive_ADMIN_POLL(Packet &p) { - if (this->status == ADMIN_STATUS_INACTIVE) return this->SendError(NETWORK_ERROR_NOT_EXPECTED); + if (this->status <= ADMIN_STATUS_AUTHENTICATE) return this->SendError(NETWORK_ERROR_NOT_EXPECTED); AdminUpdateType type = (AdminUpdateType)p.Recv_uint8(); uint32_t d1 = p.Recv_uint32(); @@ -748,7 +759,7 @@ NetworkRecvStatus ServerNetworkAdminSocketHandler::Receive_ADMIN_POLL(Packet &p) NetworkRecvStatus ServerNetworkAdminSocketHandler::Receive_ADMIN_CHAT(Packet &p) { - if (this->status == ADMIN_STATUS_INACTIVE) return this->SendError(NETWORK_ERROR_NOT_EXPECTED); + if (this->status <= ADMIN_STATUS_AUTHENTICATE) return this->SendError(NETWORK_ERROR_NOT_EXPECTED); NetworkAction action = (NetworkAction)p.Recv_uint8(); DestType desttype = (DestType)p.Recv_uint8(); @@ -774,7 +785,7 @@ NetworkRecvStatus ServerNetworkAdminSocketHandler::Receive_ADMIN_CHAT(Packet &p) NetworkRecvStatus ServerNetworkAdminSocketHandler::Receive_ADMIN_EXTERNAL_CHAT(Packet &p) { - if (this->status == ADMIN_STATUS_INACTIVE) return this->SendError(NETWORK_ERROR_NOT_EXPECTED); + if (this->status <= ADMIN_STATUS_AUTHENTICATE) return this->SendError(NETWORK_ERROR_NOT_EXPECTED); std::string source = p.Recv_string(NETWORK_CHAT_LENGTH); TextColour colour = (TextColour)p.Recv_uint16(); @@ -791,6 +802,86 @@ NetworkRecvStatus ServerNetworkAdminSocketHandler::Receive_ADMIN_EXTERNAL_CHAT(P return NETWORK_RECV_STATUS_OKAY; } +/* + * Secure authentication send and receive methods. + */ + +NetworkRecvStatus ServerNetworkAdminSocketHandler::Receive_ADMIN_JOIN_SECURE(Packet &p) +{ + if (this->status != ADMIN_STATUS_INACTIVE) return this->SendError(NETWORK_ERROR_NOT_EXPECTED); + + this->admin_name = p.Recv_string(NETWORK_CLIENT_NAME_LENGTH); + this->admin_version = p.Recv_string(NETWORK_REVISION_LENGTH); + NetworkAuthenticationMethodMask method_mask = p.Recv_uint16(); + + /* Always exclude key exchange only, as that provides no credential checking. */ + ClrBit(method_mask, NETWORK_AUTH_METHOD_X25519_KEY_EXCHANGE_ONLY); + + if (this->admin_name.empty() || this->admin_version.empty()) { + /* No name or version supplied. */ + return this->SendError(NETWORK_ERROR_ILLEGAL_PACKET); + } + + auto handler = NetworkAuthenticationServerHandler::Create(&_admin_password_provider, &_admin_authorized_key_handler, method_mask); + if (!handler->CanBeUsed()) return this->SendError(NETWORK_ERROR_NO_AUTHENTICATION_METHOD_AVAILABLE); + + this->authentication_handler = std::move(handler); + Debug(net, 3, "[admin] '{}' ({}) has connected", this->admin_name, this->admin_version); + + return this->SendAuthRequest(); +} + +NetworkRecvStatus ServerNetworkAdminSocketHandler::SendAuthRequest() +{ + this->status = ADMIN_STATUS_AUTHENTICATE; + + Debug(net, 6, "[admin] '{}' ({}) authenticating using {}", this->admin_name, this->admin_version, this->authentication_handler->GetName()); + + auto p = std::make_unique(this, ADMIN_PACKET_SERVER_AUTH_REQUEST); + this->authentication_handler->SendRequest(*p); + + this->SendPacket(std::move(p)); + + return NETWORK_RECV_STATUS_OKAY; +} + +NetworkRecvStatus ServerNetworkAdminSocketHandler::SendEnableEncryption() +{ + if (this->status != ADMIN_STATUS_AUTHENTICATE) return this->SendError(NETWORK_ERROR_NOT_EXPECTED); + + auto p = std::make_unique(this, ADMIN_PACKET_SERVER_ENABLE_ENCRYPTION); + this->authentication_handler->SendEnableEncryption(*p); + this->SendPacket(std::move(p)); + + return NETWORK_RECV_STATUS_OKAY; +} + +NetworkRecvStatus ServerNetworkAdminSocketHandler::Receive_ADMIN_AUTH_RESPONSE(Packet &p) +{ + if (this->status != ADMIN_STATUS_AUTHENTICATE) return this->SendError(NETWORK_ERROR_NOT_EXPECTED); + + switch (this->authentication_handler->ReceiveResponse(p)) { + case NetworkAuthenticationServerHandler::AUTHENTICATED: + Debug(net, 3, "[admin] '{}' ({}) authenticated", this->admin_name, this->admin_version); + + this->SendEnableEncryption(); + + this->receive_encryption_handler = this->authentication_handler->CreateClientToServerEncryptionHandler(); + this->send_encryption_handler = this->authentication_handler->CreateServerToClientEncryptionHandler(); + this->authentication_handler = nullptr; + return this->SendProtocol(); + + case NetworkAuthenticationServerHandler::RETRY_NEXT_METHOD: + Debug(net, 6, "[admin] '{}' ({}) authentication failed, trying next method", this->admin_name, this->admin_version); + return this->SendAuthRequest(); + + case NetworkAuthenticationServerHandler::NOT_AUTHENTICATED: + default: + Debug(net, 3, "[admin] '{}' ({}) authentication failed", this->admin_name, this->admin_version); + return this->SendError(NETWORK_ERROR_WRONG_PASSWORD); + } +} + /* * Useful wrapper functions */ diff --git a/src/network/network_admin.h b/src/network/network_admin.h index 766bee1d7e..4984ea6847 100644 --- a/src/network/network_admin.h +++ b/src/network/network_admin.h @@ -23,6 +23,8 @@ extern NetworkAdminSocketPool _networkadminsocket_pool; /** Class for handling the server side of the game connection. */ class ServerNetworkAdminSocketHandler : public NetworkAdminSocketPool::PoolItem<&_networkadminsocket_pool>, public NetworkAdminSocketHandler, public TCPListenHandler { +private: + std::unique_ptr authentication_handler; ///< The handler for the authentication. protected: NetworkRecvStatus Receive_ADMIN_JOIN(Packet &p) override; NetworkRecvStatus Receive_ADMIN_QUIT(Packet &p) override; @@ -33,9 +35,13 @@ protected: NetworkRecvStatus Receive_ADMIN_RCON(Packet &p) override; NetworkRecvStatus Receive_ADMIN_GAMESCRIPT(Packet &p) override; NetworkRecvStatus Receive_ADMIN_PING(Packet &p) override; + NetworkRecvStatus Receive_ADMIN_JOIN_SECURE(Packet &p) override; + NetworkRecvStatus Receive_ADMIN_AUTH_RESPONSE(Packet &p) override; NetworkRecvStatus SendProtocol(); NetworkRecvStatus SendPong(uint32_t d1); + NetworkRecvStatus SendAuthRequest(); + NetworkRecvStatus SendEnableEncryption(); public: AdminUpdateFrequency update_frequency[ADMIN_UPDATE_END]; ///< Admin requested update intervals. std::chrono::steady_clock::time_point connect_time; ///< Time of connection. diff --git a/src/network/network_client.cpp b/src/network/network_client.cpp index f6302a40e3..47bc5b0748 100644 --- a/src/network/network_client.cpp +++ b/src/network/network_client.cpp @@ -682,6 +682,7 @@ NetworkRecvStatus ClientNetworkGameSocketHandler::Receive_SERVER_ERROR(Packet &p STR_NETWORK_ERROR_TIMEOUT_JOIN, // NETWORK_ERROR_TIMEOUT_JOIN STR_NETWORK_ERROR_INVALID_CLIENT_NAME, // NETWORK_ERROR_INVALID_CLIENT_NAME STR_NETWORK_ERROR_NOT_ON_ALLOW_LIST, // NETWORK_ERROR_NOT_ON_ALLOW_LIST + STR_NETWORK_ERROR_SERVER_ERROR, // NETWORK_ERROR_NO_AUTHENTICATION_METHOD_AVAILABLE }; static_assert(lengthof(network_error_strings) == NETWORK_ERROR_END); diff --git a/src/network/network_crypto.h b/src/network/network_crypto.h index b63c90d8a0..d692acc923 100644 --- a/src/network/network_crypto.h +++ b/src/network/network_crypto.h @@ -175,7 +175,7 @@ public: /** The authentication method that can be used. */ enum NetworkAuthenticationMethod : uint8_t { - NETWORK_AUTH_METHOD_X25519_KEY_EXCHANGE_ONLY, ///< No actual authentication is taking place, just perform a x25519 key exchange. + NETWORK_AUTH_METHOD_X25519_KEY_EXCHANGE_ONLY, ///< No actual authentication is taking place, just perform a x25519 key exchange. This method is not supported for the admin connection. NETWORK_AUTH_METHOD_X25519_PAKE, ///< Authentication using x25519 password-authenticated key agreement. NETWORK_AUTH_METHOD_X25519_AUTHORIZED_KEY, ///< Authentication using x22519 key exchange and authorized keys. NETWORK_AUTH_METHOD_END, ///< Must ALWAYS be on the end of this list!! (period) diff --git a/src/network/network_type.h b/src/network/network_type.h index 4f8617ff37..805a2a651d 100644 --- a/src/network/network_type.h +++ b/src/network/network_type.h @@ -145,6 +145,7 @@ enum NetworkErrorCode { NETWORK_ERROR_TIMEOUT_JOIN, NETWORK_ERROR_INVALID_CLIENT_NAME, NETWORK_ERROR_NOT_ON_ALLOW_LIST, + NETWORK_ERROR_NO_AUTHENTICATION_METHOD_AVAILABLE, NETWORK_ERROR_END, }; diff --git a/src/settings.cpp b/src/settings.cpp index ed2f239d8f..bbae3265fa 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -140,6 +140,7 @@ private: "server_bind_addresses", "server_authorized_keys", "rcon_authorized_keys", + "admin_authorized_keys" }; public: @@ -1266,6 +1267,7 @@ static void HandleSettingDescs(IniFile &generic_ini, IniFile &private_ini, IniFi proc_list(private_ini, "bans", _network_ban_list); proc_list(private_ini, "server_authorized_keys", _settings_client.network.server_authorized_keys); proc_list(private_ini, "rcon_authorized_keys", _settings_client.network.rcon_authorized_keys); + proc_list(private_ini, "admin_authorized_keys", _settings_client.network.admin_authorized_keys); } } diff --git a/src/settings_type.h b/src/settings_type.h index 017a74664e..2790909efa 100644 --- a/src/settings_type.h +++ b/src/settings_type.h @@ -316,7 +316,9 @@ struct NetworkSettings { NetworkAuthorizedKeys server_authorized_keys; ///< Public keys of clients that are authorized to connect to the game. std::string rcon_password; ///< password for rconsole (server side) NetworkAuthorizedKeys rcon_authorized_keys; ///< Public keys of clients that are authorized to use the rconsole (server side). + bool allow_insecure_admin_login; ///< Whether to allow logging in as admin using the insecure old JOIN packet. std::string admin_password; ///< password for the admin network + NetworkAuthorizedKeys admin_authorized_keys; ///< Public keys of clients that are authorized to use the admin network. std::string client_name; ///< name of the player (as client) std::string client_secret_key; ///< The secret key of the client for authorized key logins. std::string client_public_key; ///< The public key of the client for authorized key logins. @@ -336,6 +338,8 @@ struct NetworkSettings { std::string last_joined; ///< Last joined server UseRelayService use_relay_service; ///< Use relay service? ParticipateSurvey participate_survey; ///< Participate in the automated survey + + bool AdminAuthenticationConfigured() const { return !this->admin_password.empty() || !this->admin_authorized_keys.empty(); } }; /** Settings related to the creation of games. */ diff --git a/src/table/settings/network_settings.ini b/src/table/settings/network_settings.ini index a96c2c9a5c..9cfceef8b8 100644 --- a/src/table/settings/network_settings.ini +++ b/src/table/settings/network_settings.ini @@ -176,6 +176,12 @@ flags = SF_NOT_IN_SAVE | SF_NO_NETWORK_SYNC | SF_NETWORK_ONLY def = true cat = SC_EXPERT +[SDTC_BOOL] +var = network.allow_insecure_admin_login +flags = SF_NOT_IN_SAVE | SF_NO_NETWORK_SYNC | SF_NETWORK_ONLY +def = false +cat = SC_EXPERT + [SDTC_OMANY] var = network.server_game_type type = SLE_UINT8