diff --git a/dCommon/Permissions.cpp b/dCommon/Permissions.cpp index 657ca2652..d98f7baf9 100644 --- a/dCommon/Permissions.cpp +++ b/dCommon/Permissions.cpp @@ -89,6 +89,7 @@ namespace { { "logs_audit", "Logs", "Audit log", "What staff did on the dashboard", 8 }, { "logs_system", "Logs", "Server logs", "Server log files, log search and crash dumps", 8 }, { "health_view", "Logs", "Server health", "Player counts, running worlds, uptime and memory over time", 8 }, + { "network_ips", "Logs", "Network addresses", "See the IP addresses of game and web clients on the Network page (without it they are shown as tokens)", 9 }, { "metrics_view", "Logs", "Prometheus metrics", "Read /metrics (and /api/metrics) with an API token, when metrics_enabled is on", 8 }, { "reports_view", "Economy and map", "Economy reports", "Coins, U-score, items, trades and mail, item traces, duplicate scans, flags and the world map", 3 }, diff --git a/dDashboardServer/routes/CMakeLists.txt b/dDashboardServer/routes/CMakeLists.txt index 72fbaa0b8..1b6a64bd0 100644 --- a/dDashboardServer/routes/CMakeLists.txt +++ b/dDashboardServer/routes/CMakeLists.txt @@ -34,6 +34,7 @@ set(DASHBOARDROUTES_SOURCES "LogBundle.cpp" "Traffic.cpp" "TrafficHistory.cpp" + "NetworkView.cpp" "LeaderboardRoutes.cpp" "VanityRoutes.cpp" "ChatRoutes.cpp" diff --git a/dDashboardServer/routes/NetworkView.cpp b/dDashboardServer/routes/NetworkView.cpp new file mode 100644 index 000000000..e93610706 --- /dev/null +++ b/dDashboardServer/routes/NetworkView.cpp @@ -0,0 +1,168 @@ +#include "NetworkView.h" + +#include +#include +#include +#include + +#include "ServiceType.h" +#include "magic_enum.hpp" + +namespace { + double Rate(uint64_t value, int64_t seconds) { + return std::round(static_cast(value) / static_cast(std::max(seconds, 1)) * 100.0) / 100.0; + } + + nlohmann::json Rates(uint64_t packetsIn, uint64_t packetsOut, uint64_t bytesIn, uint64_t bytesOut, int64_t seconds) { + return { {"packets_in", Rate(packetsIn, seconds)}, {"packets_out", Rate(packetsOut, seconds)}, + {"bytes_in", Rate(bytesIn, seconds)}, {"bytes_out", Rate(bytesOut, seconds)} }; + } + + nlohmann::json Rates(const TrafficStats::PeerCounts& c, int64_t seconds) { + return Rates(c.packetsIn, c.packetsOut, c.bytesIn, c.bytesOut, seconds); + } + + nlohmann::json Rates(const TrafficStats::Connection& c, int64_t seconds) { + return Rates(c.packetsIn, c.packetsOut, c.bytesIn, c.bytesOut, seconds); + } +} + +namespace NetworkView { + const char* PeerName(TrafficStats::Peer peer) { + switch (peer) { + case TrafficStats::Peer::MASTER: return "master"; + case TrafficStats::Peer::SERVERS: return "servers"; + default: return "clients"; + } + } + + std::string MaskAddress(const std::string& address, uint64_t salt) { + // FNV-1a over the salt's bytes, then the address + uint64_t hash = 14695981039346656037ull; + const auto mix = [&hash](uint8_t byte) { hash = (hash ^ byte) * 1099511628211ull; }; + for (int i = 0; i < 8; i++) mix(static_cast(salt >> (8 * i))); + for (const char c : address) mix(static_cast(c)); + char out[16]{}; + std::snprintf(out, sizeof(out), "peer-%08x", static_cast(hash ^ (hash >> 32))); + return out; + } + + nlohmann::json Summary(const TrafficHistory& history, int64_t now, int64_t onlineSeconds, const Labeler& label) { + nlohmann::json servers = nlohmann::json::object(); + for (const auto& [key, server] : history.Servers()) { + if (now - server.lastSeen > onlineSeconds || server.seconds.empty()) continue; + const auto last = server.seconds.back().time; + const auto first = last - (WINDOW - 1); + TrafficHistory::Point p; + for (auto it = server.seconds.rbegin(); it != server.seconds.rend() && it->time >= first; ++it) p.Add(*it); + + nlohmann::json entry = Rates(p.packetsIn, p.packetsOut, p.bytesIn, p.bytesOut, WINDOW); + entry["key"] = key; + entry["label"] = label ? label(server) : key; + entry["type"] = std::string(magic_enum::enum_name(static_cast(server.type))); + entry["zone"] = server.zoneId; + entry["instance"] = server.instanceId; + entry["http"] = Rate(p.httpRequests, WINDOW); + entry["http_bytes_out"] = Rate(p.httpBytesOut, WINDOW); + entry["link"] = { {"connections", server.link.connections}, {"ping_ms", server.link.averagePingMs}, {"resends", server.link.resends}, + {"resend_queue", server.link.resendQueue} }; + nlohmann::json gauges = nlohmann::json::object(); + for (const auto& [name, value] : server.gauges) gauges[name] = value; + entry["gauges"] = gauges; + + TrafficHistory::Split split; + if (server.peerSplit && history.SplitOf(key, first, last + 1, split)) { + entry["split"] = { + {"clients", Rates(split.peers[static_cast(TrafficStats::Peer::CLIENTS)], WINDOW)}, + {"master", Rates(split.peers[static_cast(TrafficStats::Peer::MASTER)], WINDOW)}, + {"servers", Rates(split.peers[static_cast(TrafficStats::Peer::SERVERS)], WINDOW)}, + {"http_from_servers", Rate(split.httpFromServers, WINDOW)}, + {"http_from_servers_bytes_out", Rate(split.httpFromServersBytesOut, WINDOW)}, + {"http_out", Rate(split.httpOutRequests, WINDOW)}, + {"http_out_bytes_in", Rate(split.httpOutBytesIn, WINDOW)} }; + } else { + entry["split"] = nullptr; + } + servers[key] = std::move(entry); + } + return { {"time", now}, {"window", WINDOW}, {"servers", servers} }; + } + + nlohmann::json Connections(const TrafficHistory& history, int64_t now, int64_t onlineSeconds, bool showAddresses, uint64_t salt, const Labeler& label) { + struct Group { + std::string address; + std::string kind = "server"; + std::string account, character; + uint32_t accountId{}; + uint64_t characterId{}; + uint64_t bytes{}; + nlohmann::json servers = nlohmann::json::array(); + TrafficStats::Connection total; + int64_t seconds = 1; + }; + const auto rank = [](const std::string& kind) { return kind == "game" ? 2 : kind == "web" ? 1 : 0; }; + std::map groups; + nlohmann::json others = nlohmann::json::array(); + bool anyReported = false; + for (const auto& [key, server] : history.Servers()) { + if (now - server.lastSeen > onlineSeconds || !server.hasConnections) continue; + anyReported = true; + 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"; + if (rank(kind) > rank(group.kind)) group.kind = kind; + if (c.accountId && !group.accountId) { + group.accountId = c.accountId; + group.account = c.account; + group.characterId = c.characterId; + group.character = c.character; + } + group.bytes += c.Bytes(); + group.total.Merge(c); + auto entry = Rates(c, seconds); + entry["server"] = key; + entry["label"] = name; + entry["peer"] = PeerName(c.peer); + entry["http"] = c.http; + if (!c.http) { + entry["port"] = showAddresses ? nlohmann::json(c.port) : nlohmann::json(nullptr); + entry["ping_ms"] = c.pingMs; + entry["resends"] = c.resends; + } + group.servers.push_back(std::move(entry)); + } + if (server.otherConnectionCount) { + auto entry = Rates(server.otherConnections, seconds); + entry["server"] = key; + entry["label"] = name; + entry["count"] = server.otherConnectionCount; + others.push_back(std::move(entry)); + } + } + + std::vector sorted; + for (auto& [_, group] : groups) sorted.push_back(&group); + std::sort(sorted.begin(), sorted.end(), [](const Group* a, const Group* b) { return a->bytes != b->bytes ? a->bytes > b->bytes : a->address < b->address; }); + nlohmann::json list = nlohmann::json::array(); + for (const auto* group : sorted) { + nlohmann::json entry = Rates(group->total, group->seconds); + entry["address"] = showAddresses ? group->address : MaskAddress(group->address, salt); + entry["kind"] = group->kind; + entry["servers"] = group->servers; + if (group->accountId) { + entry["account_id"] = group->accountId; + entry["account"] = group->account; + if (group->characterId) { + entry["character_id"] = std::to_string(group->characterId); // 64-bit: a string for JavaScript + entry["character"] = group->character; + } + } + list.push_back(std::move(entry)); + } + return { {"time", now}, {"addresses_shown", showAddresses}, {"reported", anyReported}, {"peers", list}, {"others", others} }; + } +} diff --git a/dDashboardServer/routes/NetworkView.h b/dDashboardServer/routes/NetworkView.h new file mode 100644 index 000000000..dddb291da --- /dev/null +++ b/dDashboardServer/routes/NetworkView.h @@ -0,0 +1,40 @@ +#pragma once + +#include +#include +#include + +#include "json.hpp" +#include "TrafficHistory.h" + +/** + * What the Network page (and the `traffic` WebSocket topic) is sent, built from TrafficHistory. Pure (no database, + * network or clock), so it is unit tested; Traffic.cpp serves it. + */ +namespace NetworkView { + constexpr int64_t WINDOW = 5; // seconds the live rates are over (one report) + + // A server's name for people ("World 1200 Nimbus Station #3") + using Labeler = std::function; + + /** + * Every server that reported in the last `onlineSeconds`, by key: its packets and bytes per second in and out over + * its last WINDOW reported seconds, HTTP requests, RakNet link statistics, gauges, and the split of the packets by + * peer (clients, master, servers) with its HTTP requests from and to other servers, or split: null when its reports + * don't have one (an older server). Never holds addresses. + */ + nlohmann::json Summary(const TrafficHistory& history, int64_t now, int64_t onlineSeconds, const Labeler& label); + + /** + * The remote ends of every server (from each one's last report) grouped by address: game clients by their RakNet + * connections, HTTP clients of the dashboard and the UGC server, and the servers' own links. Rates per second. + * Without `showAddresses` each address is replaced by a token that stays the same for the same address and `salt`. + */ + nlohmann::json Connections(const TrafficHistory& history, int64_t now, int64_t onlineSeconds, bool showAddresses, uint64_t salt, const Labeler& label); + + // "peer-" and 8 hex digits: the same for the same address and salt, and says nothing about the address + std::string MaskAddress(const std::string& address, uint64_t salt); + + // "clients", "master" or "servers" + const char* PeerName(TrafficStats::Peer peer); +} diff --git a/dDashboardServer/routes/Traffic.cpp b/dDashboardServer/routes/Traffic.cpp index f3e12fb57..069e86e23 100644 --- a/dDashboardServer/routes/Traffic.cpp +++ b/dDashboardServer/routes/Traffic.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include "Background.h" #include "Database.h" @@ -12,6 +13,7 @@ #include "Logger.h" #include "MessageIdentifiers.h" #include "MetricsFormat.h" +#include "NetworkView.h" #include "Permissions.h" #include "RouteUtils.h" #include "ServerState.h" @@ -37,6 +39,7 @@ using namespace RouteUtils; namespace { constexpr const char* PERMISSION = "health_view"; constexpr const char* TOPIC = "traffic"; + constexpr const char* IPS_PERMISSION = "network_ips"; constexpr auto PUSH_INTERVAL = std::chrono::seconds(2); constexpr auto WRITE_INTERVAL = std::chrono::seconds(15); constexpr int64_t ONLINE_SECONDS = 20; // a server that reported this recently is shown as up @@ -275,18 +278,46 @@ namespace { {"messages_since", since}, {"routes", routes} }; } - // What open pages get every few seconds: each server's rates over its last five reported seconds + // What open pages get every few seconds (the Network page draws it; Diagnostics reloads on it) nlohmann::json Summary(int64_t now) { - nlohmann::json servers = nlohmann::json::object(); - for (const auto& [key, server] : g_History.Servers()) { - if (now - server.lastSeen > ONLINE_SECONDS || server.seconds.empty()) continue; - TrafficHistory::Point p; - const auto first = server.seconds.back().time - 4; - for (auto it = server.seconds.rbegin(); it != server.seconds.rend() && it->time >= first; ++it) p.Add(*it); - servers[key] = { {"packets_in", Rate(p.packetsIn, 5)}, {"packets_out", Rate(p.packetsOut, 5)}, {"bytes_in", Rate(p.bytesIn, 5)}, - {"bytes_out", Rate(p.bytesOut, 5)}, {"http", Rate(p.httpRequests, 5)} }; + return NetworkView::Summary(g_History, now, ONLINE_SECONDS, LabelOf); + } + + uint64_t AddressSalt() { + static const uint64_t salt = [] { + std::random_device device; + return (static_cast(device()) << 32) ^ device(); + }(); + return salt; + } + + // One server for the Network page's detail panel: what ServerInfo says, its busiest message types each way over the + // last 5 minutes, and its packets and bytes per second over the last 10 minutes (5 second steps) + nlohmann::json NetworkServer(const std::string& key, int64_t now) { + auto info = ServerInfo(key, now); + nlohmann::json messages = nlohmann::json::array(); + for (const auto& count : g_History.TopMessages(key, now - 300, 8)) { + const auto names = NamesOf(count.key); + messages.push_back({ {"direction", count.key.outbound ? "out" : "in"}, {"service", names.service}, {"packet", names.packet}, + {"game_message", names.gameMessage}, {"count", count.count}, {"bytes", count.bytes} }); } - return { {"time", now}, {"servers", servers} }; + info["messages"] = messages; + constexpr int64_t STEP = 5, SPAN = 600; + const int64_t to = now - (now % STEP), from = to - SPAN; + nlohmann::json times = nlohmann::json::array(), packetsIn = nlohmann::json::array(), packetsOut = nlohmann::json::array(), + bytesIn = nlohmann::json::array(), bytesOut = nlohmann::json::array(); + const auto buckets = g_History.Buckets(from, to, STEP); + const auto it = buckets.find(key); + for (int64_t i = 0; i < SPAN / STEP; i++) { + times.push_back(from + i * STEP); + const auto* p = it != buckets.end() ? &it->second[static_cast(i)] : nullptr; + packetsIn.push_back(p ? Rate(p->packetsIn, STEP) : 0.0); + packetsOut.push_back(p ? Rate(p->packetsOut, STEP) : 0.0); + bytesIn.push_back(p ? Rate(p->bytesIn, STEP) : 0.0); + bytesOut.push_back(p ? Rate(p->bytesOut, STEP) : 0.0); + } + info["series"] = { {"step", STEP}, {"times", times}, {"packets_in", packetsIn}, {"packets_out", packetsOut}, {"bytes_in", bytesIn}, {"bytes_out", bytesOut} }; + return info; } void Write() { @@ -401,6 +432,26 @@ namespace Traffic { TrafficStats::Local().SetGauge("workers_queued", [] { return static_cast(Workers::Pool().Queued()); }); TrafficStats::Local().SetGauge("workers_threads", [] { return static_cast(Workers::Pool().Threads()); }); + Route(eHTTPMethod::GET, "/api/diagnostics/network", Perm(PERMISSION), + "The Network page's live view: every reporting server's packets and bytes per second over its last report, its split by peer (clients, master, other servers; null for servers too old to report one), HTTP requests, RakNet link statistics and gauges", + [](HTTPReply& reply, const HTTPContext&) { + JsonSuccess(reply, Summary(TrafficStats::Now())); + }); + + Route(eHTTPMethod::GET, "/api/diagnostics/network/server", Perm(PERMISSION), + "One server for the Network page. Query: ?key=world:1200:3 (or master, auth, chat, dashboard, ugc). Its link statistics and gauges, busiest message types over 5 minutes, packets and bytes per second over 10 minutes", + [](HTTPReply& reply, const HTTPContext& context) { + const auto key = QueryValue(context.queryString, "key"); + if (key.empty() || key.size() > 64) return JsonError(reply, eHTTPStatusCode::BAD_REQUEST, "key is required"); + JsonSuccess(reply, NetworkServer(key, TrafficStats::Now())); + }); + + Route(eHTTPMethod::GET, "/api/diagnostics/network/connections", Perm(PERMISSION), + "Every server's remote ends from its last report, grouped by address (game clients' RakNet connections, HTTP clients, server links), with rates, ping and the logged-in player. Addresses only with network_ips; otherwise a token per address", + [](HTTPReply& reply, const HTTPContext& context) { + JsonSuccess(reply, NetworkView::Connections(g_History, TrafficStats::Now(), ONLINE_SECONDS, Can(context, IPS_PERMISSION), AddressSalt(), LabelOf)); + }); + Route(eHTTPMethod::GET, "/api/diagnostics/traffic", Perm(PERMISSION), "Packets, bytes and HTTP requests per second of every server. Query: ?range=5m|1h|24h|7d. Series are per-second rates of each step, latency in ms", [](HTTPReply& reply, const HTTPContext& context) { diff --git a/dDashboardServer/routes/TrafficHistory.cpp b/dDashboardServer/routes/TrafficHistory.cpp index 5b79b9f43..4aa773e71 100644 --- a/dDashboardServer/routes/TrafficHistory.cpp +++ b/dDashboardServer/routes/TrafficHistory.cpp @@ -90,6 +90,38 @@ void TrafficHistory::Point::Add(const Point& other) { latencySum += other.latencySum; } +void TrafficHistory::Split::Add(const TrafficStats::Second& second) { + for (size_t i = 0; i < peers.size(); i++) peers[i].Merge(second.peers[i]); + httpRequests += second.httpRequests; + httpBytesOut += second.httpBytesOut; + httpFromServers += second.httpFromServers; + httpFromServersBytesOut += second.httpFromServersBytesOut; + httpOutRequests += second.httpOutRequests; + httpOutBytesIn += second.httpOutBytesIn; +} + +void TrafficHistory::Split::Add(const Split& other) { + for (size_t i = 0; i < peers.size(); i++) peers[i].Merge(other.peers[i]); + httpRequests += other.httpRequests; + httpBytesOut += other.httpBytesOut; + httpFromServers += other.httpFromServers; + httpFromServersBytesOut += other.httpFromServersBytesOut; + httpOutRequests += other.httpOutRequests; + httpOutBytesIn += other.httpOutBytesIn; +} + +bool TrafficHistory::SplitOf(const std::string& serverKey, int64_t from, int64_t to, Split& out) const { + const auto it = m_Servers.find(serverKey); + if (it == m_Servers.end()) return false; + bool any = false; + for (const auto& split : it->second.splits) { + if (split.time < from || split.time >= to) continue; + out.Add(split); + any = true; + } + return any; +} + TrafficStats::Histogram TrafficHistory::Point::Latency() const { return TrafficStats::Histogram::FromSparse(latency, latencySum); } @@ -122,6 +154,12 @@ void TrafficHistory::Ingest(uint16_t serviceType, uint32_t zoneId, uint32_t inst server.lastSeen = now; server.link = report.link; server.gauges = report.gauges; + server.peerSplit = report.peerSplit; + server.hasConnections = report.hasConnections; + server.connections = report.connections; + server.otherConnections = report.otherConnections; + server.otherConnectionCount = report.otherConnectionCount; + server.connectionsSeconds = std::max(1, static_cast(report.seconds.size())); auto& totals = server.totals; int64_t lastSecond = now; @@ -134,6 +172,13 @@ void TrafficHistory::Ingest(uint16_t serviceType, uint32_t zoneId, uint32_t inst totals.bytesIn += second.bytesIn; totals.bytesOut += second.bytesOut; + if (report.peerSplit) { + auto& splits = server.splits; + auto at = std::lower_bound(splits.begin(), splits.end(), second.time, [](const Split& x, int64_t t) { return x.time < t; }); + if (at == splits.end() || at->time != second.time) at = splits.insert(at, Split{ .time = second.time }); + at->Add(second); + } + if (server.seconds.empty() || server.seconds.back().time < second.time) { server.seconds.push_back(Point{ .time = second.time }); server.seconds.back().Add(second); @@ -154,6 +199,7 @@ void TrafficHistory::Ingest(uint16_t serviceType, uint32_t zoneId, uint32_t inst } } while (!server.seconds.empty() && server.seconds.front().time < now - SECONDS_KEPT) server.seconds.pop_front(); + while (!server.splits.empty() && server.splits.front().time < now - SPLIT_SECONDS) server.splits.pop_front(); const auto& link = report.link; totals.datagramsSent += link.datagramsSent; @@ -198,6 +244,10 @@ void TrafficHistory::Forget(int64_t now) { for (auto it = m_Servers.begin(); it != m_Servers.end();) { auto& seconds = it->second.seconds; while (!seconds.empty() && seconds.front().time < now - SECONDS_KEPT) seconds.pop_front(); + auto& splits = it->second.splits; + while (!splits.empty() && splits.front().time < now - SPLIT_SECONDS) splits.pop_front(); + // Addresses are only kept while the server reports + if (it->second.lastSeen < now - 60) it->second.connections.clear(); if (it->second.lastSeen < now - FORGET_AFTER && it->second.minutes.empty()) it = m_Servers.erase(it); else ++it; } diff --git a/dDashboardServer/routes/TrafficHistory.h b/dDashboardServer/routes/TrafficHistory.h index 979baa274..50b66438b 100644 --- a/dDashboardServer/routes/TrafficHistory.h +++ b/dDashboardServer/routes/TrafficHistory.h @@ -26,6 +26,7 @@ public: static constexpr size_t MAX_TOTAL_MESSAGES = 256; // message types a server's totals keep static constexpr size_t MAX_TOTAL_ROUTES = 128; // routes a server's totals keep static constexpr size_t ROUTE_MINUTES = 60; // per-minute route stats kept + static constexpr int64_t SPLIT_SECONDS = 300; // per-second splits kept (memory only) // One second (or a bucket of them) of one server struct Point { @@ -47,6 +48,18 @@ public: using MessageCounts = std::map; // by MessageKey::Packed + // One second's packets split by peer, and its HTTP requests from and to other servers (the network page's links) + struct Split { + int64_t time{}; + std::array peers{}; + uint64_t httpRequests{}, httpBytesOut{}; // all HTTP requests answered + uint64_t httpFromServers{}, httpFromServersBytesOut{}; // of those, from other servers + uint64_t httpOutRequests{}, httpOutBytesIn{}; // made to other servers + + void Add(const TrafficStats::Second& second); + void Add(const Split& other); + }; + struct Totals { uint64_t packetsIn{}, packetsOut{}, bytesIn{}, bytesOut{}; uint64_t datagramsSent{}, datagramsReceived{}, linkBytesSent{}, linkBytesReceived{}, resends{}; @@ -70,6 +83,14 @@ public: std::deque> messageMinutes; std::deque> messageHours; std::deque>> routeMinutes; + bool peerSplit{}; // whether the last report had the split (older servers' don't) + std::deque splits; // oldest first, up to SPLIT_SECONDS + // The last report's remote ends (memory only, never written: they hold addresses) + bool hasConnections{}; + std::vector connections; + TrafficStats::Connection otherConnections; + uint32_t otherConnectionCount{}; + int64_t connectionsSeconds{}; // the seconds the last report covered, to make rates of them }; // "master", "auth", "chat", "dashboard", "ugc", "world::" @@ -94,6 +115,9 @@ public: // The busiest message types since `since` (minute counts for the last hour, hour counts before), one server or all ("") std::vector TopMessages(const std::string& server, int64_t since, size_t limit) const; + // A server's seconds from `from` (inclusive) to `to` (exclusive) summed; whether any split was reported in them + bool SplitOf(const std::string& server, int64_t from, int64_t to, Split& out) const; + // The HTTP routes since `since` (the last hour at most) of every server with a web server, as (server, route); busiest first std::vector> Routes(int64_t since) const; diff --git a/tests/dWebTests/CMakeLists.txt b/tests/dWebTests/CMakeLists.txt index 2a65763b3..5ab1c6eae 100644 --- a/tests/dWebTests/CMakeLists.txt +++ b/tests/dWebTests/CMakeLists.txt @@ -53,6 +53,8 @@ set(DWEBTESTS_SOURCES "MetricsFormatTests.cpp" "TrafficHistoryTests.cpp" "${PROJECT_SOURCE_DIR}/dDashboardServer/routes/TrafficHistory.cpp" + "NetworkViewTests.cpp" + "${PROJECT_SOURCE_DIR}/dDashboardServer/routes/NetworkView.cpp" "${PROJECT_SOURCE_DIR}/dDashboardServer/routes/SettingsCatalog.cpp" "AlertsAndTwoFactorTests.cpp" "${PROJECT_SOURCE_DIR}/dDashboardServer/auth/Totp.cpp" diff --git a/tests/dWebTests/NetworkViewTests.cpp b/tests/dWebTests/NetworkViewTests.cpp new file mode 100644 index 000000000..0af46fa20 --- /dev/null +++ b/tests/dWebTests/NetworkViewTests.cpp @@ -0,0 +1,119 @@ +#include + +#include "NetworkView.h" +#include "ServiceType.h" + +using namespace TrafficStats; + +namespace { + constexpr int64_t NOW = 1700000100; + + // Five seconds of a world: 10 packets in and 20 out a second, split between its players and master + Report WorldReport(bool split) { + Report report; + for (int64_t t = NOW - 5; t < NOW; t++) { + Second s{ .time = t, .packetsIn = 10, .packetsOut = 20, .bytesIn = 1000, .bytesOut = 4000 }; + s.peers[static_cast(Peer::CLIENTS)] = { 8, 18, 800, 3800 }; + s.peers[static_cast(Peer::MASTER)] = { 1, 1, 100, 100 }; + s.peers[static_cast(Peer::SERVERS)] = { 1, 1, 100, 100 }; + report.seconds.push_back(s); + } + report.peerSplit = split; + report.link = { .connections = 3, .resends = 2, .averagePingMs = 40 }; + return report; + } + + std::string Label(const TrafficHistory::Server& server) { return "label of " + server.key; } +} + +TEST(NetworkViewTest, SummaryHasRatesAndTheSplit) { + TrafficHistory history; + history.Ingest(static_cast(ServiceType::WORLD), 1200, 3, WorldReport(true), NOW); + const auto json = NetworkView::Summary(history, NOW, 20, Label); + ASSERT_TRUE(json["servers"].contains("world:1200:3")); + const auto& world = json["servers"]["world:1200:3"]; + EXPECT_EQ(world["label"], "label of world:1200:3"); + EXPECT_EQ(world["type"], "WORLD"); + EXPECT_EQ(world["zone"], 1200); + EXPECT_DOUBLE_EQ(world["packets_in"].get(), 10.0); + EXPECT_DOUBLE_EQ(world["bytes_out"].get(), 4000.0); + EXPECT_EQ(world["link"]["connections"], 3); + EXPECT_EQ(world["link"]["ping_ms"], 40); + ASSERT_TRUE(world["split"].is_object()); + EXPECT_DOUBLE_EQ(world["split"]["clients"]["packets_out"].get(), 18.0); + EXPECT_DOUBLE_EQ(world["split"]["master"]["bytes_in"].get(), 100.0); + EXPECT_DOUBLE_EQ(world["split"]["servers"]["packets_in"].get(), 1.0); + EXPECT_EQ(json["window"], NetworkView::WINDOW); +} + +TEST(NetworkViewTest, OlderServersHaveNoSplit) { + TrafficHistory history; + history.Ingest(static_cast(ServiceType::AUTH), 0, 0, WorldReport(false), NOW); + const auto json = NetworkView::Summary(history, NOW, 20, Label); + ASSERT_TRUE(json["servers"].contains("auth")); + EXPECT_TRUE(json["servers"]["auth"]["split"].is_null()); + EXPECT_DOUBLE_EQ(json["servers"]["auth"]["packets_in"].get(), 10.0); + + // Silent servers are left out + EXPECT_TRUE(NetworkView::Summary(history, NOW + 60, 20, Label)["servers"].empty()); +} + +TEST(NetworkViewTest, HttpSplitOfTheUgcServer) { + // The UGC server: all its requests came from the dashboard (the wire format is tested in ServerTrafficTests) + auto report = WorldReport(true); + report.seconds[0].httpRequests = 5; + report.seconds[0].httpFromServers = 5; + TrafficHistory history; + history.Ingest(static_cast(ServiceType::UGC), 0, 0, report, NOW); + const auto json = NetworkView::Summary(history, NOW, 20, nullptr); + const auto& ugc = json["servers"]["ugc"]; + EXPECT_EQ(ugc["label"], "ugc"); + EXPECT_DOUBLE_EQ(ugc["http"].get(), 1.0); + EXPECT_DOUBLE_EQ(ugc["split"]["http_from_servers"].get(), 1.0); +} + +TEST(NetworkViewTest, ConnectionsGroupByAddressAndHideIt) { + 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, + .resends = 1, .pingMs = 45, .accountId = 7, .characterId = 1152921504606846976ull, .account = "alice", .character = "Alice" }; + Connection master{ .address = "127.0.0.1", .port = 2000, .peer = Peer::MASTER, .bytesIn = 10, .bytesOut = 10 }; + world.connections = { player, master }; + world.otherConnections = Connection{ .bytesIn = 500 }; + world.otherConnectionCount = 4; + world.hasConnections = true; + history.Ingest(static_cast(ServiceType::WORLD), 1200, 3, world, NOW); + + Report ugc; + ugc.seconds = { Second{ .time = NOW - 1 } }; + ugc.connections = { Connection{ .address = "203.0.113.5", .peer = Peer::CLIENTS, .http = true, .packetsIn = 2, .packetsOut = 2, .bytesIn = 300, .bytesOut = 90000 } }; + ugc.hasConnections = true; + history.Ingest(static_cast(ServiceType::UGC), 0, 0, ugc, NOW); + + 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"); + ASSERT_EQ(shown["others"].size(), 1u); + EXPECT_EQ(shown["others"][0]["count"], 4); + + // Without network_ips: a token instead of the address, the same for the same address, no ports + const auto hidden = NetworkView::Connections(history, NOW, 20, false, 1234, Label); + 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& 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)); + + // The summary never carries addresses + EXPECT_EQ(NetworkView::Summary(history, NOW, 20, Label).dump().find("203.0.113.5"), std::string::npos); +}