This project enables play on existing legacy WoW emulation cores using the modern clients. It serves as a translation layer, converting all network traffic to the appropriate format each side can understand.
There are 4 major components to the application:
- The modern BNetServer to which the client initially logs into.
- The legacy AuthClient which will in turn login to the remote authentication server (realmd).
- The modern WorldServer to which the game client will connect once a realm has been selected.
- The legacy WorldClient which communicates with the remote world server (mangosd).
HermesProxy translates between modern WoW Classic clients and legacy private server emulators.
These are the Blizzard WoW Classic client versions you can use:
| Version | Expansion | Build Range | Notes |
|---|---|---|---|
| 1.14.0 | Classic Era | 39802 - 40618 | Season of Mastery |
| 1.14.1 | Classic Era | 40487 - 42032 | |
| 1.14.2 | Classic Era | 41858 - 42597 | |
| 2.5.2 | TBC Classic | 39570 - 41510 | |
| 2.5.3 | TBC Classic | 41402 - 42598 |
These are the private server versions HermesProxy can connect to:
| Version | Expansion | Build | Server Software |
|---|---|---|---|
| 1.12.1 | Vanilla | 5875 | CMaNGOS, VMaNGOS, etc. |
| 1.12.2 | Vanilla | 6005 | CMaNGOS, VMaNGOS, etc. |
| 1.12.3 | Vanilla | 6141 | CMaNGOS, VMaNGOS, etc. |
| 2.4.3 | TBC | 8606 | CMaNGOS, etc. |
The proxy automatically selects the best legacy version based on your client:
| Modern Client | Connects To |
|---|---|
| 1.14.x | 1.12.x (Vanilla) |
| 2.5.x | 2.4.3 (TBC) |
Stable Downloads: Releases
- Edit
appsettings.jsonnext to the proxy executable — setClientOptions:ClientBuildfor your game client, andLegacyServerOptions:Build/:Address/:Portfor the remote realmd. Anything in the file can also be overridden at launch via--Section:Key=Valueor the legacy--set Key=Value. - Go into your game folder, in the Classic or Classic Era subdirectory, and edit
WTF/Config.wtfto set the portal to127.0.0.1. - Download Arctium Launcher into the main game folder
- Vanilla
--staticseed --version=ClassicEra - TBC
--staticseed --version=Classic
- Vanilla
- Start the proxy app and login through the game with your usual credentials.
Note: Keep Optimize Network for Speed enabled (it's under System -> Network), otherwise you will get kicked every now and then.
See docs/known-issues.md for current client/server quirks and their workarounds.
HermesProxy provides some internal chat commands:
| Command | Description |
|---|---|
!qcomplete <questId> |
Manually marks a quest as already completed (useful for quest helper addons) |
!quncomplete <questId> |
Unmarks a quest as completed |
| Argument | Description |
|---|---|
--config <path> |
Use an alternate appsettings.json (default: appsettings.json in the working directory) |
--Section:Key=Value |
Native override of any appsettings key (repeatable). Section = ClientOptions, LegacyServerOptions, ProxyNetworkOptions, LoggingOptions, DiagnosticsOptions. |
--set <Key>=<Value> |
Legacy override syntax — flat keys translated to their section-qualified equivalents. |
--no-version-check |
Disable the check for newer versions on startup |
--metrics |
Enable per-opcode latency metrics collection. A top-20 summary is logged every 60 s (see below) |
Environment variables prefixed with HERMES_ are also picked up — e.g. HERMES_LegacyServerOptions__Address=logon.example.com.
Examples:
# Use a custom appsettings file
HermesProxy --config MyServer.json
# Override server address (native section:key syntax)
HermesProxy --LegacyServerOptions:Address=logon.example.com
# Override multiple values
HermesProxy --LegacyServerOptions:Address=logon.example.com --LegacyServerOptions:Port=3725
# Legacy --set form still works
HermesProxy --set ServerAddress=logon.example.com --set ServerPort=3725
# Run with latency metrics enabled (a summary is written to the log every 60 seconds)
HermesProxy --metrics
# Metrics + verbose packet capture, handy when profiling a suspicious opcode
HermesProxy --metrics --LoggingOptions:PacketLevel=DebugWhen enabled, HermesProxy tracks per-opcode round-trip latency for both directions (Client → Server and Server → Client) and emits a summary line every minute through the Server logging category. Nothing is printed when no traffic has been observed, so idle proxies stay quiet.
17:42:14 | I | S | Server | Latency Metrics: 842 C->S opcodes, 1203 S->C opcodes tracked
17:42:14 | I | S | Server | <top 20 opcodes with min / max / avg / p99 latency>
Useful for: identifying slow-dispatching opcodes, spotting regressions after a packet-handler change, and validating that hot-path migrations didn't shift the latency profile. Leave it off in normal play — it has a small per-packet timestamp overhead.
Primary config is appsettings.json (loaded from the working directory, required). Layered overrides applied in order:
appsettings.json— base.appsettings.{Environment}.json— environment-specific overlay (e.g.appsettings.Development.json), optional.HERMES_*environment variables —HERMES_Section__Key=Value(__doubles as the section separator).- CLI args —
--Section:Key=Valuenative or--set Key=Valuelegacy.
| Key | Default | Description |
|---|---|---|
ClientBuild |
V2_5_2_40892 |
ClientVersionBuild enum value: V1_14_0_40618, V1_14_1_41794, V1_14_2_42597, V2_5_2_40892, V2_5_3_42328. |
SeedHex |
179D3DC3235629D07113A9B3867F97A7 |
32-character hex string (16 bytes). |
ReportedOS |
OSX |
OS identifier sent to the legacy server (OSX, Win, etc.). |
ReportedPlatform |
x86 |
Platform identifier sent to legacy server (x86, x64). |
| Key | Default | Description |
|---|---|---|
Build |
auto |
Legacy server build to target. auto, V1_12_1_5875, V2_4_3_8606. |
Address |
127.0.0.1 |
Address of the legacy realmd (what you'd use in SET REALMLIST). |
Port |
3724 |
Port of the legacy authentication server. |
All ports must be in the range 1-65535.
| Key | Default | Description |
|---|---|---|
ExternalAddress |
127.0.0.1 |
Your IP address for others to connect (for hosting). |
BNetPort |
1119 |
BNet/Portal server (use this in your Config.wtf). |
RestPort |
8081 |
REST API server. |
RealmPort |
8084 |
Realm server. |
InstancePort |
8086 |
Instance server. |
CertificatePfxPath |
(none) | Optional path to a custom PKCS#12 (.pfx) certificate served on the BNet TLS endpoints (BNetPort/RestPort). When unset, the embedded TrinityCore-compatible certificate is used. |
CertificatePfxPassword |
(none) | Password for CertificatePfxPath; omit for a passwordless pfx. |
By default the proxy serves the well-known TrinityCore development certificate (CN=*.*, issued by the TrinityCore Battle.net Aurora CA). This is what virtually every patched modern client expects: Arctium-launched clients skip certificate validation entirely, and clients patched with a TrinityCore-style connection patcher are pinned to exactly this CA chain. Leave the default alone unless you know you need something else. The startup log prints the active certificate's subject, issuer and expiry.
CertificatePfxPath exists for setups that validate against the system trust store instead (e.g. wowpatch). Those need a certificate that chains to a trusted root CA plus a hostname (not an IP) in the portal CVar:
- Hosted proxy (recommended for server operators): run HermesProxy on a host with a real domain name and point
CertificatePfxPathat a publicly trusted certificate for that domain (e.g. Let's Encrypt). Players setportal "yourdomain.com"and need no client-side certificate setup at all. - Local proxy with a custom CA: generate your own CA and a
localhostleaf certificate, install the CA into the local trust store, and pointCertificatePfxPathat the leaf pfx. Never distribute a CA's private key — anyone holding it can forge trusted certificates for every user who installed that CA.
Serilog-backed pipe-delimited logger. Per-category levels (Server / Network / Storage / Packet), color-coded console + plain rolling file at Logs/hermes-YYYYMMDD.log. Override at runtime via --LoggingOptions:PacketLevel=Debug. Optional movement trace via HERMES_TRACE_MOVEMENT=1.
Full reference (line format, severity/category letters, settings table, CLI examples, movement trace): docs/logging.md.
| Key | Default | Description |
|---|---|---|
PacketsLog |
true |
Dump each session's raw packets to PacketsLog/*.pkt for replay/inspection. |
EnableMetrics |
false |
Same as --metrics flag; per-opcode latency summary every 60 s. |
EnableVersionCheck |
true |
Check for newer HermesProxy versions on startup. |
{
"ClientOptions": {
"ClientBuild": "V2_5_2_40892",
"SeedHex": "179D3DC3235629D07113A9B3867F97A7",
"ReportedOS": "OSX",
"ReportedPlatform": "x86"
},
"LegacyServerOptions": {
"Build": "auto",
"Address": "127.0.0.1",
"Port": 3724
},
"ProxyNetworkOptions": {
"ExternalAddress": "127.0.0.1",
"RestPort": 8081,
"BNetPort": 1119,
"RealmPort": 8084,
"InstancePort": 8086
},
"LoggingOptions": {
"MinimumLevel": "Information",
"ServerLevel": "Information",
"NetworkLevel": "Information",
"StorageLevel": "Information",
"PacketLevel": "Warning",
"ConsoleLevel": "Information",
"ToFile": true,
"Directory": "Logs"
},
"DiagnosticsOptions": {
"PacketsLog": true,
"EnableMetrics": false,
"EnableVersionCheck": true
}
}The proxy auto-enables the .NET createdump facility on startup so native crashes (AVs, FailFast, stack overflow, GC corruption) drop a heap mini-dump to bin/<Release|Debug>/Logs/crash-<pid>.dmp. Managed exceptions are routed through the existing log/flush handlers and don't need this. Open the dump in WinDbg / Visual Studio (Debug → Open → Dump) for the full managed stack + reachable object graph at the crash site.
To opt out — for example to defer to a different crash reporter — set DOTNET_DbgEnableMiniDump to any value before launch (the proxy honours a pre-existing value and skips its own configuration).
Related env vars set automatically (overridable):
| Variable | Default value | Purpose |
|---|---|---|
DOTNET_DbgEnableMiniDump |
1 |
Master switch for createdump on unrecoverable faults |
DOTNET_DbgMiniDumpType |
2 (Heap) |
Dump scope — 1=Mini, 2=Heap, 3=Triage, 4=Full |
DOTNET_DbgMiniDumpName |
Logs/crash-<pid>.dmp |
Output path |
DOTNET_CreateDumpDiagnostics |
1 |
Verbose stderr logging if dump generation fails |
Zero-allocation packet I/O via Span<T>/ref struct, ArrayPool-backed ByteBuffer, cached enum mappings, FrozenDictionary opcode lookups, value-type WowGuid. Benchmarks and full breakdown in docs/perf.md.
Current coverage: 84.7% (272 / 321 server packets converted). See docs/ispanwritable.md for the full converted/unconverted breakdown and MaxSize optimization table.
Parts of this project's code are based on CypherCore and BotFarm. I would like to extend my sincere thanks to these projects, as the creation of this app might have never happened without them. And I would also like to expressly thank Modox for all his work on reverse engineering the classic clients and all the help he has personally given me.