From 9b00e437e85d140345b8fa58b6eb0c0dd4abd715 Mon Sep 17 00:00:00 2001 From: Aaron Kimbrell Date: Tue, 29 Sep 2026 19:07:27 -0500 Subject: [PATCH] fix(dashboard): Network connections are one entry per player or per connection, not per address A player on the servers' machine (or behind the same address as other players) was merged with the servers' own links and everyone else there. Co-Authored-By: Claude Opus 5.5 --- dDashboardServer/routes/NetworkView.cpp | 10 +++-- tests/dWebTests/NetworkViewTests.cpp | 55 ++++++++++++++++++++----- 2 files changed, 51 insertions(+), 14 deletions(-) diff --git a/dDashboardServer/routes/NetworkView.cpp b/dDashboardServer/routes/NetworkView.cpp index e93610706..4434c8e26 100644 --- a/dDashboardServer/routes/NetworkView.cpp +++ b/dDashboardServer/routes/NetworkView.cpp @@ -110,10 +110,14 @@ namespace NetworkView { const auto name = label ? label(server) : key; const auto seconds = std::max(server.connectionsSeconds, 1); for (const auto& c : server.connections) { - auto& group = groups[c.address]; - group.address = c.address; - group.seconds = std::max(group.seconds, seconds); const std::string kind = c.peer != TrafficStats::Peer::CLIENTS ? "server" : c.http ? "web" : "game"; + // One entry per player (their account), else per connection: an address alone is not enough, since the + // servers' own links and players on the same machine or network share it + const std::string groupKey = kind == "game" && c.accountId ? "account:" + std::to_string(c.accountId) + : kind + "|" + c.address + (c.http ? std::string() : ":" + std::to_string(c.port)); + auto& group = groups[groupKey]; + if (group.address.empty()) group.address = c.address; + group.seconds = std::max(group.seconds, seconds); if (rank(kind) > rank(group.kind)) group.kind = kind; if (c.accountId && !group.accountId) { group.accountId = c.accountId; diff --git a/tests/dWebTests/NetworkViewTests.cpp b/tests/dWebTests/NetworkViewTests.cpp index 0af46fa20..ce9ebb238 100644 --- a/tests/dWebTests/NetworkViewTests.cpp +++ b/tests/dWebTests/NetworkViewTests.cpp @@ -72,7 +72,7 @@ TEST(NetworkViewTest, HttpSplitOfTheUgcServer) { EXPECT_DOUBLE_EQ(ugc["split"]["http_from_servers"].get(), 1.0); } -TEST(NetworkViewTest, ConnectionsGroupByAddressAndHideIt) { +TEST(NetworkViewTest, ConnectionsGroupByPlayerOrConnectionAndHideIt) { TrafficHistory history; auto world = WorldReport(true); Connection player{ .address = "203.0.113.5", .port = 50000, .peer = Peer::CLIENTS, .packetsIn = 50, .packetsOut = 100, .bytesIn = 5000, .bytesOut = 10000, @@ -92,15 +92,22 @@ TEST(NetworkViewTest, ConnectionsGroupByAddressAndHideIt) { const auto shown = NetworkView::Connections(history, NOW, 20, true, 1234, Label); EXPECT_TRUE(shown["addresses_shown"].get()); - ASSERT_EQ(shown["peers"].size(), 2u); - const auto& first = shown["peers"][0]; // the busiest: the player on the world and the UGC server - EXPECT_EQ(first["address"], "203.0.113.5"); - EXPECT_EQ(first["kind"], "game"); - EXPECT_EQ(first["account"], "alice"); - EXPECT_EQ(first["character"], "Alice"); - EXPECT_EQ(first["character_id"], "1152921504606846976"); - ASSERT_EQ(first["servers"].size(), 2u); - EXPECT_EQ(shown["peers"][1]["kind"], "server"); + // The player's game connection, the same address fetching from the UGC server, and the master link are three + // entries: an address shared by a player's browser, other players or the servers' own links is not one peer + ASSERT_EQ(shown["peers"].size(), 3u); + const auto find = [&](const std::string& kind) -> const nlohmann::json& { + for (const auto& p : shown["peers"]) if (p["kind"] == kind) return p; + return shown["peers"][0]; + }; + const auto& gamePeer = find("game"); + EXPECT_EQ(gamePeer["kind"], "game"); + EXPECT_EQ(gamePeer["address"], "203.0.113.5"); + EXPECT_EQ(gamePeer["account"], "alice"); + EXPECT_EQ(gamePeer["character"], "Alice"); + EXPECT_EQ(gamePeer["character_id"], "1152921504606846976"); + ASSERT_EQ(gamePeer["servers"].size(), 1u); + EXPECT_EQ(find("web")["kind"], "web"); + EXPECT_EQ(find("server")["kind"], "server"); ASSERT_EQ(shown["others"].size(), 1u); EXPECT_EQ(shown["others"][0]["count"], 4); @@ -109,7 +116,7 @@ TEST(NetworkViewTest, ConnectionsGroupByAddressAndHideIt) { const auto dump = hidden.dump(); EXPECT_EQ(dump.find("203.0.113.5"), std::string::npos); EXPECT_EQ(dump.find("127.0.0.1"), std::string::npos); - EXPECT_EQ(hidden["peers"][0]["address"], NetworkView::MaskAddress("203.0.113.5", 1234)); + for (const auto& p : hidden["peers"]) if (p["kind"] == "game") EXPECT_EQ(p["address"], NetworkView::MaskAddress("203.0.113.5", 1234)); for (const auto& entry : hidden["peers"][0]["servers"]) EXPECT_TRUE(!entry.contains("port") || entry["port"].is_null()); EXPECT_NE(NetworkView::MaskAddress("203.0.113.5", 1234), NetworkView::MaskAddress("203.0.113.5", 99)); EXPECT_NE(NetworkView::MaskAddress("203.0.113.5", 1234), NetworkView::MaskAddress("203.0.113.6", 1234)); @@ -117,3 +124,29 @@ TEST(NetworkViewTest, ConnectionsGroupByAddressAndHideIt) { // The summary never carries addresses EXPECT_EQ(NetworkView::Summary(history, NOW, 20, Label).dump().find("203.0.113.5"), std::string::npos); } + +TEST(NetworkViewTest, ServerLinksOnThePlayersAddressStayApart) { + // Everything on one machine: a player and the chat server's links from the worlds all come from 127.0.0.1 + TrafficHistory history; + auto world = WorldReport(true); + world.connections = { Connection{ .address = "127.0.0.1", .port = 50001, .peer = Peer::CLIENTS, .bytesIn = 900, .bytesOut = 900, .accountId = 7, .account = "alice" } }; + world.hasConnections = true; + history.Ingest(static_cast(ServiceType::WORLD), 1200, 3, world, NOW); + auto chat = WorldReport(true); + chat.connections = { Connection{ .address = "127.0.0.1", .port = 50100, .peer = Peer::SERVERS, .bytesIn = 50 }, + Connection{ .address = "127.0.0.1", .port = 50101, .peer = Peer::SERVERS, .bytesIn = 50 } }; + chat.hasConnections = true; + history.Ingest(static_cast(ServiceType::CHAT), 0, 0, chat, NOW); + + const auto json = NetworkView::Connections(history, NOW, 20, true, 1, Label); + ASSERT_EQ(json["peers"].size(), 3u); + for (const auto& p : json["peers"]) { + if (p["kind"] == "game") { + EXPECT_EQ(p["account"], "alice"); + ASSERT_EQ(p["servers"].size(), 1u); // only the world, not the chat server's links + } else { + EXPECT_EQ(p["kind"], "server"); + EXPECT_FALSE(p.contains("account")); + } + } +}