#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, const Endpoints& endpoints, uint64_t salt) { 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; const auto endpoint = endpoints.find(key); const bool known = endpoint != endpoints.end(); entry["port"] = known && endpoint->second.port ? nlohmann::json(endpoint->second.port) : nlohmann::json(nullptr); entry["host"] = known && !endpoint->second.host.empty() ? nlohmann::json(MaskAddress(endpoint->second.host, salt)) : nlohmann::json(nullptr); 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, const Endpoints& endpoints) { 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) { 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. Web clients: one per address // and signed-in dashboard account, so people behind one address are listed apart 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)) + (c.http && c.accountId ? "|" + std::to_string(c.accountId) : std::string()); 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; 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 && group->kind == "web") { // The dashboard account the requests were signed in as (a session or an API key) entry["account_id"] = group->accountId; entry["user"] = group->account; } else 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)); } // The machines the Summary names by token nlohmann::json hosts = nlohmann::json::object(); if (showAddresses) { for (const auto& [_, endpoint] : endpoints) { if (!endpoint.host.empty()) hosts[MaskAddress(endpoint.host, salt)] = endpoint.host; } } return { {"time", now}, {"addresses_shown", showAddresses}, {"reported", anyReported}, {"peers", list}, {"others", others}, {"hosts", hosts} }; } }