From a93f5f810bf2f6ad064acb0d1a72bd19cca82979 Mon Sep 17 00:00:00 2001 From: Aaron Kimbrell Date: Wed, 30 Sep 2026 02:13:41 -0500 Subject: [PATCH] feat(master): the server list names each server's port and machine Master keeps the address and port auth, chat, the dashboard and the UGC server report when they connect and sends them, with its own and every world's, after the list's world states. A server's machine is the address master sees its connection come from; loopback and master's external_ip are master's machine. Servers can report another port than their RakNet one; the UGC server reports its web port. Co-Authored-By: Claude Opus 5.5 --- dMasterServer/MasterServer.cpp | 38 +++++++++++++++++++ dNet/MasterPackets.cpp | 21 ++++++++++ dNet/MasterPackets.h | 15 ++++++++ dNet/dServer.cpp | 2 +- dNet/dServer.h | 5 +++ dUgcServer/UgcServer.cpp | 2 + .../dNetTests/MasterPacketsTests.cpp | 27 ++++++++++++- 7 files changed, 108 insertions(+), 2 deletions(-) diff --git a/dMasterServer/MasterServer.cpp b/dMasterServer/MasterServer.cpp index 92961396b..d926f16a1 100644 --- a/dMasterServer/MasterServer.cpp +++ b/dMasterServer/MasterServer.cpp @@ -107,6 +107,18 @@ namespace { std::optional g_ChatHandoff; // SIGUSR2 starts a live update (docs/LiveUpdate.md) volatile std::sig_atomic_t g_LiveUpdateSignal = 0; + // Where auth, chat, the dashboard and the UGC server said they listen, and their machine, from their ServerInfo + std::map g_Endpoints; + + // The machine a server runs on for the dashboard: the address its connection comes from, or master's + // external_ip for master's own machine (a loopback connection, or one from that address) + std::string HostOf(const SystemAddress& sysAddr) { + const auto self = Game::config->GetValue("external_ip", "localhost"); + if (sysAddr == UNASSIGNED_SYSTEM_ADDRESS) return self; // a world master launched that hasn't connected yet + const std::string address = sysAddr.ToString(false); + if (address.starts_with("127.") || address == self) return self; + return address; + } } namespace { @@ -120,6 +132,17 @@ namespace { response.ugcEnabled = Game::config->GetValue("enable_ugc_server") == "1" ? 1 : 0; response.ugcOnline = ugcServerMasterPeerSysAddr != UNASSIGNED_SYSTEM_ADDRESS ? 1 : 0; response.ugcPid = ugcServerPid; + // Master itself, then the other servers that are connected + auto& self = response.endpoints.emplace_back(); + self.type = ServiceType::MASTER; + self.ip = LUString(Game::config->GetValue("external_ip", "localhost")); + self.port = Game::config->GetValue("master_server_port", 2000); + self.host = self.ip; + for (const auto& [type, peer] : { std::pair{ ServiceType::AUTH, authServerMasterPeerSysAddr }, std::pair{ ServiceType::CHAT, chatServerMasterPeerSysAddr }, + std::pair{ ServiceType::DASHBOARD, dashboardServerMasterPeerSysAddr }, std::pair{ ServiceType::UGC, ugcServerMasterPeerSysAddr } }) { + const auto it = g_Endpoints.find(type); + if (peer != UNASSIGNED_SYSTEM_ADDRESS && it != g_Endpoints.end()) response.endpoints.push_back(it->second); + } if (!Game::im) return response; for (const auto& inst : Game::im->GetInstances()) { if (!inst || inst->GetShutdownComplete()) continue; @@ -133,6 +156,13 @@ namespace { entry.isPrivate = inst->GetIsPrivate() ? 1 : 0; entry.state = inst->GetIsShuttingDown() ? eState::STOPPING : !inst->GetIsReady() ? eState::STARTING : inst->GetIsDraining() ? eState::DRAINING : eState::READY; + auto& endpoint = response.endpoints.emplace_back(); + endpoint.type = ServiceType::WORLD; + endpoint.zoneID = entry.mapID; + endpoint.instanceID = entry.instanceID; + endpoint.ip = entry.ip; + endpoint.port = entry.port; + endpoint.host = LUString(HostOf(inst->GetSysAddr())); } return response; } @@ -775,6 +805,14 @@ namespace { break; } + if (theirServerType != ServiceType::WORLD) { + auto& endpoint = g_Endpoints[theirServerType]; + endpoint.type = theirServerType; + endpoint.ip = theirIP; + endpoint.port = theirPort; + endpoint.host = LUString(HostOf(sysAddr)); + } + if (theirServerType != ServiceType::DASHBOARD && dashboardServerMasterPeerSysAddr != UNASSIGNED_SYSTEM_ADDRESS) { MasterPackets::SendTo(dashboardServerMasterPeerSysAddr, info); } diff --git a/dNet/MasterPackets.cpp b/dNet/MasterPackets.cpp index a012671a1..82e450872 100644 --- a/dNet/MasterPackets.cpp +++ b/dNet/MasterPackets.cpp @@ -248,6 +248,15 @@ namespace MasterPackets { bitStream.Write(ugcOnline); bitStream.Write(ugcPid); for (const auto& instance : instances) bitStream.Write(static_cast(instance.state)); + bitStream.Write(endpoints.size()); + for (const auto& endpoint : endpoints) { + bitStream.Write(endpoint.type); + bitStream.Write(endpoint.zoneID); + bitStream.Write(endpoint.instanceID); + bitStream.Write(endpoint.ip); + bitStream.Write(endpoint.port); + bitStream.Write(endpoint.host); + } } bool ServerListResponse::Deserialize(RakNet::BitStream& bitStream) { @@ -275,6 +284,18 @@ namespace MasterPackets { if (state > static_cast(eState::DRAINING)) return false; instance.state = static_cast(state); } + uint32_t endpointCount{}; + VALIDATE_READ(bitStream.Read(endpointCount)); + if (endpointCount > MAX_INSTANCES + MAX_OTHER_ENDPOINTS) return false; + endpoints.resize(endpointCount); + for (auto& endpoint : endpoints) { + VALIDATE_READ(bitStream.Read(endpoint.type)); + VALIDATE_READ(bitStream.Read(endpoint.zoneID)); + VALIDATE_READ(bitStream.Read(endpoint.instanceID)); + VALIDATE_READ(bitStream.Read(endpoint.ip)); + VALIDATE_READ(bitStream.Read(endpoint.port)); + VALIDATE_READ(bitStream.Read(endpoint.host)); + } return true; } diff --git a/dNet/MasterPackets.h b/dNet/MasterPackets.h index cbeb4b7e6..8996a94ff 100644 --- a/dNet/MasterPackets.h +++ b/dNet/MasterPackets.h @@ -278,6 +278,21 @@ namespace MasterPackets { uint8_t ugcOnline{}; uint32_t ugcPid{}; + // Where each server listens and which machine it runs on, after the worlds' states: master, auth, chat, the + // dashboard, the UGC server and every world. `ip` and `port` are what the server said it listens on (the + // dashboard and the UGC server: their web port); `host` is its machine, the address master sees its + // connection come from (master's own machine, and servers connecting from it, are master's external_ip) + struct Endpoint { + ServiceType type{}; + uint32_t zoneID{}; + uint32_t instanceID{}; + LUString ip{}; + uint32_t port{}; + LUString host{}; + }; + std::vector endpoints; // u32 count, at most MAX_INSTANCES + MAX_OTHER_ENDPOINTS + static constexpr uint32_t MAX_OTHER_ENDPOINTS = 16; + ServerListResponse() : LUBitStream(ServiceType::MASTER, MessageType::Master::SERVER_LIST_RESPONSE) {} void Serialize(RakNet::BitStream& bitStream) const override; bool Deserialize(RakNet::BitStream& bitStream) override; diff --git a/dNet/dServer.cpp b/dNet/dServer.cpp index 3fc5f9515..0e7dce385 100644 --- a/dNet/dServer.cpp +++ b/dNet/dServer.cpp @@ -170,7 +170,7 @@ Packet* dServer::ReceiveFromMaster() { mMasterSystemAddress = packet->systemAddress; LOG("SendServerInfo called for server type %i", static_cast(GetServerType())); MasterPackets::ServerInfo info; - info.port = GetPort(); + info.port = mAdvertisedPort ? mAdvertisedPort : GetPort(); info.zoneID = GetZoneID(); info.instanceID = GetInstanceID(); info.serverType = GetServerType(); diff --git a/dNet/dServer.h b/dNet/dServer.h index 590a8185d..3682f8666 100644 --- a/dNet/dServer.h +++ b/dNet/dServer.h @@ -72,6 +72,10 @@ public: using ConnectionIdentity = std::function; void SetConnectionIdentity(ConnectionIdentity identity) { mConnectionIdentity = std::move(identity); } + // The port this server tells master it listens on (its ServerInfo, shown on the dashboard's Network page): its + // RakNet port unless set. The dashboard and the UGC server set their web port. + void SetAdvertisedPort(uint32_t port) { mAdvertisedPort = port; } + bool IsConnected(const SystemAddress& sysAddr); const std::string& GetIP() const { return mIP; } const int GetPort() const { return mPort; } @@ -124,6 +128,7 @@ protected: Logger* mLogger = nullptr; dConfig* mConfig = nullptr; RakPeerInterface* mPeer = nullptr; + uint32_t mAdvertisedPort = 0; ReplicaManager* mReplicaManager = nullptr; NetworkIDManager* mNetIDManager = nullptr; diff --git a/dUgcServer/UgcServer.cpp b/dUgcServer/UgcServer.cpp index 8eb89a6b0..51b213622 100644 --- a/dUgcServer/UgcServer.cpp +++ b/dUgcServer/UgcServer.cpp @@ -745,6 +745,8 @@ int main(int argc, char** argv) { // The master starts it again when this link drops g_Server = new dServer(masterIP, Setting("net_port", 2012), 0, 16, false, false, Game::logger, masterIP, masterPort, ServiceType::UGC, Game::config, &Game::lastSignal, masterPassword); + // Master (and the dashboard's Network page) show the port clients fetch files from + g_Server->SetAdvertisedPort(Setting("port", 2008)); Game::server = g_Server; UgcBricks::BrickLibrary library(res, 0, ClientReader()); diff --git a/tests/dGameTests/dNetTests/MasterPacketsTests.cpp b/tests/dGameTests/dNetTests/MasterPacketsTests.cpp index f5ff40482..a78e6cbe6 100644 --- a/tests/dGameTests/dNetTests/MasterPacketsTests.cpp +++ b/tests/dGameTests/dNetTests/MasterPacketsTests.cpp @@ -374,13 +374,31 @@ TEST(MasterPacketsTests, ServerListMatchesLegacy) { response.ugcEnabled = 1; response.ugcOnline = count % 2; response.ugcPid = 4242 + count; - // The legacy list, then the UGC server's state + for (size_t i = 0; i < count; i++) { + auto& endpoint = response.endpoints.emplace_back(); + endpoint.type = i == 0 ? ServiceType::AUTH : ServiceType::WORLD; + endpoint.zoneID = static_cast(1000 + i); + endpoint.instanceID = static_cast(i); + endpoint.ip = LUString("10.0.0." + std::to_string(i)); + endpoint.port = static_cast(3000 + i); + endpoint.host = LUString(i == 2 ? "192.168.1.20" : "localhost"); + } + // The legacy list, then the UGC server's state, each world's state and where every server listens EXPECT_PACKET_EQ(Written([&](RakNet::BitStream& b) { LegacyMaster::WriteServerList(b, true, count % 2, instances); b.Write(1); b.Write(count % 2); b.Write(4242 + count); for (size_t i = 0; i < count; i++) b.Write(static_cast(i % 3)); // each world's state + b.Write(count); + for (size_t i = 0; i < count; i++) { + b.Write(i == 0 ? ServiceType::AUTH : ServiceType::WORLD); + b.Write(1000 + i); + b.Write(i); + b.Write(LUString("10.0.0." + std::to_string(i))); + b.Write(3000 + i); + b.Write(LUString(i == 2 ? "192.168.1.20" : "localhost")); + } }), StructPacket(response)); RakNet::BitStream stream; LoadPayload(stream, response); @@ -397,6 +415,13 @@ TEST(MasterPacketsTests, ServerListMatchesLegacy) { EXPECT_EQ(copy.ugcEnabled, 1); EXPECT_EQ(copy.ugcOnline, count % 2); EXPECT_EQ(copy.ugcPid, 4242 + count); + ASSERT_EQ(copy.endpoints.size(), count); + for (size_t i = 0; i < count; i++) { + EXPECT_EQ(copy.endpoints[i].type, i == 0 ? ServiceType::AUTH : ServiceType::WORLD); + EXPECT_EQ(copy.endpoints[i].zoneID, 1000 + i); + EXPECT_EQ(copy.endpoints[i].port, 3000 + i); + EXPECT_EQ(copy.endpoints[i].host.string, i == 2 ? "192.168.1.20" : "localhost"); + } ExpectTruncatedFails(response); } }