Skip to content

Archive the traffic of observed accounts - #917

Merged
sven-n merged 2 commits into
masterfrom
claude/network-observation-archive
Sep 4, 2026
Merged

Archive the traffic of observed accounts#917
sven-n merged 2 commits into
masterfrom
claude/network-observation-archive

Conversation

@sven-n

@sven-n sven-n commented Aug 29, 2026

Copy link
Copy Markdown
Member

Phase 5 of #895 — the observation flag, the session archive and its settings. It doesn't touch anything of phase 4 (#916), so the two are independent.

An account can be marked with the new Account.IsNetworkObservationActive. While it's set, the game server archives the traffic of every session of that account on the file system. The flag renders in the account editor through AutoForm, so there is no new page for it.

Where it runs

The archive belongs to the game server, not to the admin panel (decision 9 of the issue): it's registered in the all-in-one host and in Dapr/GameServer.Host, so an observed account is archived even when no admin panel is running anywhere. GameServer takes an optional IPacketArchive; without one, nothing changes at all.

What a session looks like

captures/
  SuspiciousAccount/
    2026-08-29_21-12-37_1/
      session.json
      part-000.mucap
      part-001.mucap

A session is a directory rather than a single file — the plan sketched <account>/<date>_<server>.mucap plus a sidecar, but with rotation a session has more than one file, and a directory keeps "open it", "copy it", "delete it" and "measure it" simple. Each part is a .mucap in the format of the WinForms tool, complete with its own start-timestamp header, so a part can be opened there directly.

session.json carries what the capture format has no place for: account, character names (added when the player enters the world), server type/id/description, remote endpoint, client version, start/end time, packet count, dropped packet count and the list of parts.

Each packet line gets the sequence number as a fifth field. The existing loader reads fields 0..3 and ignores the rest, so the files stay backwards compatible. It's what makes a gap visible: when the file system can't keep up, the writer drops packets instead of slowing the player's connection down (the count ends up in the metadata).

Writing

The packets go through a bounded channel into an own task, which appends lines and flushes as soon as the queue ran empty. So the network thread never waits for the file system, a session is readable while it runs, and a crashed process leaves everything up to the last flush. Reader and writer share the files (FileShare.ReadWrite | FileShare.Delete), and a half-written last line is skipped rather than throwing.

The archive starts at login, not at connect: the version check and the login request are not part of it. Capturing them would mean capturing every connection unconditionally — exactly what this feature avoids (also stated in the issue).

Player got a PlayerLoggedIn event, raised by a new SetAccountAsync. The state machine reaches Authenticated before LoginAction assigns player.Account, so the state change is not the right moment to decide whether a session has to be archived — the account isn't there yet.

Housekeeping

Rotation (per file), quota (whole archive, oldest first) and retention (age) keep it bounded. It runs when a session starts and when one ends; a session which is currently being written is never removed.

Settings

SystemConfiguration gained five properties, editable in the admin panel like the existing ones:

Property Default
NetworkAnalyzerLiveBufferSize 5000
NetworkObservationArchivePath captures
NetworkObservationMaxSessionSizeMb 50
NetworkObservationMaxTotalSizeMb 1000
NetworkObservationRetentionDays 30

The live buffer size is now used by the capture service of phase 2 instead of its hard-coded default.

One migration covers the five properties and the account flag. It was generated with dotnet ef migrations add; the only hand edit is the defaultValue of each new column, so that an existing database gets the documented defaults instead of 0 (which would mean "no quota, no retention"). A value of 0 is still read as "unlimited"/"default" at runtime, so a database which was updated by other means behaves sensibly too.

Deviations worth knowing

  • No client version change points in the metadata. The plan wanted them because a capture may start before the version is known — but this archive starts at login, where the version is already final, so there is exactly one.
  • The observation toggle in the analyzer page and the archive browser are phase 6, as the issue splits them.

Data protection

Starting a session, finishing it and deleting one are logged at information level, so observing a player leaves a trace. The default archive path (captures, next to the binaries) is outside anything served statically, and captures/ was added to .gitignore.

Tests

  • PacketArchiveTest (14): round trip of the captured packets and their direction, reading a session while it's being written, rotation into several files, the newest-packets cap, filtering by account, delete, "a running session is not deleted", an id which points outside the archive, retention, quota, a sanitized account name, an incomplete last line, and the metadata being readable as json.
  • NetworkObservationTests (5): the traffic of an observed account is archived and the archive is a sink of the connection; an unobserved account is not touched; the session is finished when the player disconnects; and the mapping of the system configuration to the options, including the fallbacks. The disconnect test found a real bug on the way: the sink was read back from RemotePlayer.Connection, which is already gone during the teardown, so the session kept its sink. The connection is part of the session state now.
  • dotnet build src/MUnique.OpenMU.sln -p:ci=true → 0 errors, no warning from a file this PR touches.
  • Complete test suite green: MUnique.OpenMU.Tests 807, Network.Tests 73 (4 skipped), Network.Packets.Tests 588, Web.Tests 63, ChatServer.Tests 26, PlugIns.Tests 41, Persistence.Initialization.Tests 12 (2 skipped), AttributeSystem.Tests 44, Pathfinding.Tests 9.

Not verified against a real PostgreSQL database or a running server — that's yours, as agreed.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Pb82LmoaUVdZtBtQs7xrtA


Generated by Claude Code

An account can be marked with the new flag IsNetworkObservationActive.
While it's set, the game server archives the traffic of each session of
that account on the file system, so that it can be analyzed later - for
example when a player is suspected of cheating.

The archive is a feature of the game server, not of the admin panel: it's
registered in the all-in-one host and in the Dapr game server host, so an
observed account is archived even when no admin panel is running.

* A session is a directory below the one of its account, which holds the
  packets in one or more files of the analyzer tool format plus the
  metadata as json. That way a session can be opened, copied or deleted as
  a whole - and each of its files can be loaded by the WinForms tool.
* The packets are written by an own task and flushed as soon as its queue
  ran empty, so that the network thread never waits for the file system, a
  running session can be read, and a crash doesn't lose everything.
* Each line carries a sequence number as a fifth field, which the loader
  of the analyzer tool ignores. It makes a gap visible - the writer drops
  packets rather than slowing the connection of the player down when the
  file system can't keep up.
* Rotation, quota and retention keep the archive bounded. The housekeeping
  runs when a session starts and when one ends; a running session is never
  removed.
* The archive starts when the player is logged in, so the version check and
  the login request are not part of it. Capturing them would mean to
  capture every connection unconditionally, which is exactly what this
  feature avoids.

The archive path, the rotation size, the quota, the retention and the live
buffer size of the analyzer page are new properties of the system
configuration, so they can be edited in the admin panel.

Player got a PlayerLoggedIn event, raised by the new SetAccountAsync: at
the moment the state machine reaches "Authenticated", the account of the
player isn't assigned yet, so the state change is not the right moment to
decide whether a session has to be archived.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pb82LmoaUVdZtBtQs7xrtA
@sven-n
sven-n force-pushed the claude/network-observation-archive branch from 8ca7c49 to 67bb727 Compare September 4, 2026 19:16
@sven-n
sven-n merged commit 3c8c85c into master Sep 4, 2026
2 of 3 checks passed
sven-n pushed a commit that referenced this pull request Sep 4, 2026
The only conflict was in NetworkObservationHandler, which arrived on master
with the merge of #917: the observation toggle of this branch added
ApplyObservationAsync and split StopSessionAsync out of the disconnect
handler, so both additions are kept on top of the version of master.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pb82LmoaUVdZtBtQs7xrtA
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants