A self-hosted replacement for Dungeon Siege's matchmaking service, which Microsoft shut down years ago. Runs on Linux, speaks the game's own protocol, and needs no modification to the game client.
Recon evidence lives in docs/findings.md. The content and
modding half of this project is a separate repo, resiege.
The matchmaking endpoint is not hardcoded. It is a plain-text INI setting:
[multiplayer]
gun_server = dungeonsiege.zone.com
gun_server_port = 2300
news_server = motd.dungeonsiege.zone.comSo pointing a client at your own server takes an INI edit — no binary patch, no DNS hijack, no hosts file. That removes the obstacle that usually kills these projects before they start, and it is why "let players pick a server" is a realistic feature rather than a fantasy.
The lobby works. Two clients find each other through it and play together (findings 23–26). The MOTD leg has worked since early on. What is left is robustness and NAT, not feasibility.
Three legs, wildly different costs:
| Leg | Transport | State |
|---|---|---|
| News/MOTD | plain HTTP | working (tools/motd_server.py) |
| AutoUpdate | HTTP | stubbed |
| GUN lobby | TCP 2300, custom binary + XML | working (tools/gun_recon.py) |
ZoneMatch was only ever discovery. The lobby hands out IpAddr/Ip2 and
then gameplay runs peer-to-peer over DirectPlay 8. A revived lobby gets you a
browsable game list; it does not make players reachable through NAT. Pair it
with an overlay network (Tailscale, ZeroTier) or you will have a beautiful
lobby full of games nobody can join.
These are decisions, not preferences, and they shape the architecture — so they are settled here rather than discovered later:
-
Never hand a player's address to another player. The original protocol does exactly that, in
IpAddr/Ip2. Any public deployment relays instead. This is the single biggest difference between "faithful to 2002" and "safe to run in 2026", and it has to be decided before the transport is finalised. -
Credentials are protected by default, not by remembering to.
ConInfocarriesCdKeyandLogincarries a password — see docs/gun-messages.txt — and this server holds the private half of the handshake, so it is the first place either has existed in plaintext since the player typed it.tools/confidential.pyis the one place that decides what happens to them:- On screen, secret-named fields render as a truncated SHA-256 handle
rather than being printed.
--show-secretsturns that off for a recon session. The default matters becauserun-servers.shrunsdp_server.pyas a lobby, and a lobby's stdout is a terminal someone screenshots or a journal someone greps. - On disk,
captures/is created0700and every capture written0600, explicitly rather than inheriting the operator's umask — a common022would otherwise leave CD keys world-readable. An existing directory is tightened too, because whoever needs this most has been running an older build.
captures/stays gitignored regardless. Committed test data goes intools/fixtures/and is checked for personal data first; seedp_dpl8cs.selftest()for the standard.The fingerprints are correlation handles for a human reading a terminal — the same password shows the same handle across two frames, which is what the recon work actually needs — not a defence against brute-forcing a short secret from its hash. A transcript full of them is low-sensitivity, not sanitised.
- On screen, secret-named fields render as a truncated SHA-256 handle
rather than being printed.
-
So is
DungeonSiege.ini, and that one gets pasted into bug reports. A signed-in client writes its ZoneMatch credential straight into the INI:zone_userpassword = <base64-ish blob>This matters more than the capture rule above, because pointing a client at a server is an INI edit — so the one file this project asks every user to open is also the one holding their password. Any instruction to edit or share it must say "strip
zone_userpasswordfirst", and that includes issue templates andpoint-at-server.sh's own output. -
Everything headless. Nothing in this repo launches the game, so the whole server can be tested without a Windows install or a Wine prefix.
tools/gun_client.pyis a headless client for exactly that.
export DS_GAME_DIR="/path/to/Dungeon Siege" # only the DLL/INI readers need this
uv sync # or: pip install cryptography
./tools/run-servers.shThere is deliberately no default install path: the GOG, Steam and disc installs
all land somewhere different. $DS_GAME_DIR is how gun_views.py finds the
game's own match.ini, which defines the lobby's table schema — without it the
Games tab has no columns and stays empty.
Requirements: Python 3.11+ and cryptography. That one dependency covers
the RSA keypair and PKCS#1 v1.5 wrapping in the ZoneMatch handshake. RC4, the
CryptoAPI blob formats and the GUN framing are implemented here against
published vectors, because knowing what the bytes are is the point. It is
imported lazily, so every decoder, fixture and selftest runs without it.
Development tools (ruff, pyright) come from uv sync, pinned by uv.lock:
uv run ruff check tools/
uv run pyrightBoth are clean and meant to stay that way. Type checking is standard across
tools/, with a per-file strict list in pyrightconfig.json — the same
ratchet the resiege repo uses. confidential.py is the first file on it, on
purpose: it decides whether a credential reaches a terminal, so a wrong type
there is a disclosure rather than a traceback.
To point a client at your server, use point-at-server.sh from the
resiege repo — client-side tools live client-side.
LICENSE MIT, plus what this does and does not include
pyproject.toml uv project + ruff config, and why each waiver exists
uv.lock pinned dev tools, so `uv sync` is reproducible
pyrightconfig.json type checking: standard everywhere, strict per file
docs/findings.md recon evidence — protocol, crypto, DirectPlay
docs/gun-messages.txt the GUN message/field catalogue
tools/gun_recon.py the lobby: TCP 2300, framing, hexdump + serving
tools/gun_lobby.py lobby state — rooms, game list, players
tools/gun_proto.py framing and message types
tools/gun_crypto.py the ZoneAuth handshake
tools/gun_views.py view/field decoding
tools/gun_res_fields.py resource field tables
tools/gun_client.py headless client, for testing without the game
tools/motd_server.py serves /news/news.txt (working)
tools/confidential.py what happens to a credential: masked on screen
unless --show-secrets, 0600 on disk
tools/dp_proto.py DirectPlay 8, reverse-engineered from captures
tools/dp_dpl8cs.py DirectPlay 8, the published [MC-DPL8CS] spec,
checked against real capture bytes
tools/dp_server.py DirectPlay host-side probing
tools/dp_learn.py captures ground truth from the game's own stack
tools/dp_probe.py DirectPlay enumeration probe
tools/dp_ports.sh port helpers
tools/gunproxy/ proxy DLL (Zig) that forwards to the real GunDll.dll
while logging every call
tools/fixtures/ committed protocol reference bytes, personal-data
checked (see dp_dpl8cs.selftest)
tools/run-servers.sh bring up MOTD + lobby together
tools/test-lobby.sh exercise the lobby end to end
captures/ raw dumps, written at runtime. Gitignored and NEVER
publishable: ConInfo carries CdKey.
- A: speak GUN on the wire — faithful, works with unmodified installs.
- B: replace
GunDll.dll— the gameLoadLibrarys it by name and it is a COM in-proc server with only the four standard exports, so a drop-in replacement needs no registration at all. Behind our own DLL we could speak JSON over WebSocket instead of 2002 binary.
tools/gunproxy/ forwards to the real DLL while logging every call, which
recovers the COM interface shape for B and gives A a plaintext view of the
protocol, from one piece of work. B is the better destination; A's artifacts
are the road there.
uv run python -c "import sys; sys.path.insert(0,'tools'); import dp_dpl8cs; dp_dpl8cs.selftest()"
./tools/test-lobby.sh # end to end: two headless clients host, a third browsestest-lobby.sh needs $DS_GAME_DIR set, for the same match.ini reason as
above — without it the header checks fail and the game list comes back empty.
A structure that only matches the documentation is a hypothesis; one that also matches a capture is a fact. The selftest checks both, and reports which fixtures were missing rather than quietly passing with half the checks gone.
MIT. Original work only: an independent implementation of protocols a retail Dungeon Siege client already speaks, so a client can be pointed at a server its owner runs. No code, assets or data from Dungeon Siege or from Microsoft's ZoneMatch service is included or redistributed — you need your own copy of the game.
The DirectPlay 8 session layer follows Microsoft's published [MC-DPL8CS]
specification; the GUN lobby protocol was reconstructed by observing a retail
client talk to software on the same machine, for the sole purpose of
interoperating with it. LICENSE has the full notice, including the part worth
reading twice before deploying anything: this software receives other people's
passwords and CD keys in plaintext, because the 2002 protocol sends them that
way.