diff options
| author | Stefan Boberg <[email protected]> | 2026-03-12 15:03:03 +0100 |
|---|---|---|
| committer | GitHub Enterprise <[email protected]> | 2026-03-12 15:03:03 +0100 |
| commit | 81bc43aa96f0059cecb28d1bd88338b7d84667f9 (patch) | |
| tree | a3428cb7fddceae0b284d33562af5bf3e64a367e /src/zenserver/proxy/httptrafficrecorder.cpp | |
| parent | update fmt 12.0.0 -> 12.1.0 (#828) (diff) | |
| download | zen-81bc43aa96f0059cecb28d1bd88338b7d84667f9.tar.xz zen-81bc43aa96f0059cecb28d1bd88338b7d84667f9.zip | |
Transparent proxy mode (#823)
Adds a **transparent TCP proxy mode** to zenserver (activated via `zenserver proxy`), allowing it to sit between clients and upstream Zen servers to inspect and monitor HTTP/1.x traffic in real time. Primarily useful during development, to be able to observe multi-server/client interactions in one place.
- **Dedicated proxy port** -- Proxy mode defaults to port 8118 with its own data directory to avoid collisions with a normal zenserver instance.
- **TCP proxy core** (`src/zenserver/proxy/`) -- A new transparent TCP proxy that forwards connections to upstream targets, with support for both TCP/IP and Unix socket listeners. Multi-threaded I/O for connection handling. Supports Unix domain sockets for both upstream/downstream.
- **HTTP traffic inspection** -- Parses HTTP/1.x request/response streams inline to extract method, path, status, content length, and WebSocket upgrades without breaking the proxied data.
- **Proxy dashboard** -- A web UI showing live connection stats, per-target request counts, active connections, bytes transferred, and client IP/session ID rollups.
- **Server mode display** -- Dashboard banner now shows the running server mode (Zen Proxy, Zen Compute, etc.).
Supporting changes included in this branch:
- **Wildcard log level matching** -- Log levels can now be set per-category using wildcard patterns (e.g. `proxy.*=debug`).
- **`zen down --all`** -- New flag to shut down all running zenserver instances; also used by the new `xmake kill` task.
- Minor test stability fixes (flaky hash collisions, per-thread RNG seeds).
- Support ZEN_MALLOC environment variable for default allocator selection and switch default to rpmalloc
- Fixed sentry-native build to allow LTO on Windows
Diffstat (limited to 'src/zenserver/proxy/httptrafficrecorder.cpp')
| -rw-r--r-- | src/zenserver/proxy/httptrafficrecorder.cpp | 191 |
1 files changed, 191 insertions, 0 deletions
diff --git a/src/zenserver/proxy/httptrafficrecorder.cpp b/src/zenserver/proxy/httptrafficrecorder.cpp new file mode 100644 index 000000000..0279555a0 --- /dev/null +++ b/src/zenserver/proxy/httptrafficrecorder.cpp @@ -0,0 +1,191 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "httptrafficrecorder.h" + +#include <zencore/compactbinarybuilder.h> +#include <zencore/logging.h> + +#include <chrono> +#include <filesystem> + +namespace zen { + +HttpTrafficRecorder::HttpTrafficRecorder(const std::filesystem::path& OutputDir, std::string_view ClientLabel, std::string_view TargetLabel) +: m_Log(logging::Get("proxy.record")) +, m_Dir(OutputDir) +, m_ClientLabel(ClientLabel) +, m_TargetLabel(TargetLabel) +{ + auto Now = std::chrono::system_clock::now(); + m_StartTimeMs = uint64_t(std::chrono::duration_cast<std::chrono::milliseconds>(Now.time_since_epoch()).count()); + + std::error_code Ec; + std::filesystem::create_directories(m_Dir, Ec); + if (Ec) + { + ZEN_WARN("failed to create recording directory {} - {}", m_Dir.string(), Ec.message()); + return; + } + + std::error_code ReqEc; + m_RequestFile.Open(m_Dir / "request.bin", BasicFile::Mode::kTruncate, ReqEc); + if (ReqEc) + { + ZEN_WARN("failed to open request.bin in {} - {}", m_Dir.string(), ReqEc.message()); + return; + } + + std::error_code RespEc; + m_ResponseFile.Open(m_Dir / "response.bin", BasicFile::Mode::kTruncate, RespEc); + if (RespEc) + { + ZEN_WARN("failed to open response.bin in {} - {}", m_Dir.string(), RespEc.message()); + m_RequestFile.Close(); + return; + } + + m_Valid = true; + ZEN_DEBUG("recording started in {}", m_Dir.string()); +} + +HttpTrafficRecorder::~HttpTrafficRecorder() +{ + if (m_Valid && !m_Finalized) + { + Finalize(false, Oid::Zero); + } +} + +void +HttpTrafficRecorder::WriteRequest(const char* Data, size_t Length) +{ + if (!m_Valid) + { + return; + } + m_RequestFile.Write(Data, Length, m_RequestOffset); + m_RequestOffset += Length; +} + +void +HttpTrafficRecorder::WriteResponse(const char* Data, size_t Length) +{ + if (!m_Valid) + { + return; + } + m_ResponseFile.Write(Data, Length, m_ResponseOffset); + m_ResponseOffset += Length; +} + +void +HttpTrafficRecorder::OnMessageComplete(HttpTrafficInspector::Direction Dir, + std::string_view Method, + std::string_view Url, + uint16_t StatusCode, + int64_t /*ContentLength*/) +{ + if (!m_Valid) + { + return; + } + + if (Dir == HttpTrafficInspector::Direction::Request) + { + // Record the request boundary. The request spans from m_CurrentRequestStart to m_RequestOffset. + m_PendingReqOffset = m_CurrentRequestStart; + m_PendingReqSize = m_RequestOffset - m_CurrentRequestStart; + m_PendingMethod = Method; + m_PendingUrl = Url; + m_HasPendingRequest = true; + + // Advance start to current offset for the next request. + m_CurrentRequestStart = m_RequestOffset; + } + else + { + // Response complete -- pair with pending request. + RecordedEntry Entry; + if (m_HasPendingRequest) + { + Entry.ReqOffset = m_PendingReqOffset; + Entry.ReqSize = m_PendingReqSize; + Entry.Method = std::move(m_PendingMethod); + Entry.Url = std::move(m_PendingUrl); + m_HasPendingRequest = false; + } + + Entry.RespOffset = m_CurrentResponseStart; + Entry.RespSize = m_ResponseOffset - m_CurrentResponseStart; + Entry.Status = StatusCode; + + m_Entries.push_back(std::move(Entry)); + + // Advance start to current offset for the next response. + m_CurrentResponseStart = m_ResponseOffset; + } +} + +void +HttpTrafficRecorder::Finalize(bool WebSocket, const Oid& SessionId) +{ + if (!m_Valid || m_Finalized) + { + return; + } + m_Finalized = true; + + m_RequestFile.Close(); + m_ResponseFile.Close(); + + // Write index.ucb as a CbObject. + CbObjectWriter Cbo; + + Cbo << "client" << std::string_view(m_ClientLabel); + Cbo << "target" << std::string_view(m_TargetLabel); + Cbo << "startTime" << m_StartTimeMs; + Cbo << "websocket" << WebSocket; + if (SessionId != Oid::Zero) + { + std::string SessionIdStr = SessionId.ToString(); + Cbo << "sessionId" << std::string_view(SessionIdStr); + } + + Cbo.BeginArray("entries"); + for (const RecordedEntry& Entry : m_Entries) + { + Cbo.BeginObject(); + Cbo << "reqOffset" << Entry.ReqOffset; + Cbo << "reqSize" << Entry.ReqSize; + Cbo << "respOffset" << Entry.RespOffset; + Cbo << "respSize" << Entry.RespSize; + Cbo << "method" << std::string_view(Entry.Method); + Cbo << "url" << std::string_view(Entry.Url); + Cbo << "status" << Entry.Status; + Cbo.EndObject(); + } + Cbo.EndArray(); + + CbObject IndexObj = Cbo.Save(); + + MemoryView View = IndexObj.GetView(); + std::error_code Ec; + BasicFile IndexFile(m_Dir / "index.ucb", BasicFile::Mode::kTruncate, Ec); + if (!Ec) + { + IndexFile.Write(View, 0, Ec); + if (Ec) + { + ZEN_WARN("failed to write index.ucb in {} - {}", m_Dir.string(), Ec.message()); + } + IndexFile.Close(); + } + else + { + ZEN_WARN("failed to create index.ucb in {} - {}", m_Dir.string(), Ec.message()); + } + + ZEN_DEBUG("recording finalized in {} ({} entries, websocket: {})", m_Dir.string(), m_Entries.size(), WebSocket); +} + +} // namespace zen |