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); } }