Files
coa-server-manager/docs/RELAY_PROTOCOL.md
T

6.9 KiB
Raw Blame History

Game Relay Protocol (Phase 13)

The Game Relay (services/coa-relay) provides TCP connectivity for WoW 3.3.5 clients when the realm's Host is behind NAT or CGNAT without port forwarding.

The Relay is logically and operationally separate from the Registry and Coordinator:

  • Registry: Public directory of realms (HTTPS).
  • Coordinator: Control plane signaling and routing for Noise channels (WSS).
  • Game Relay: Multiplexed byte tunnel for WoW client game traffic (authserver on TCP 3724 and worldserver on TCP 8085).

1. Security Architecture and Invariants

WoW Client (3.3.5)
   │
   ├── TCP :40000 (Auth)  ──► [coa-relay] ◄── Outbound WSS ── [Host Manager] ──► 127.0.0.1:3724 (local authserver)
   │                           (:8082 internal)
   └── TCP :40001 (World) ──► [coa-relay] ◄────────────────── [Host Manager] ──► 127.0.0.1:8085 (local worldserver)
  1. Host Outbound Only: The Host initiates an outbound WebSocket connection to wss://coa-manager.duckdns.org/relay/v1/host. The Host requires zero inbound firewall openings or port forwarding.
  2. Strict Host Local Targets: The Host's RelayLink worker strictly permits only two local targets:
    • RelayTarget::Auth -> 127.0.0.1:3724
    • RelayTarget::World -> 127.0.0.1:8085 Any attempt to request another destination (e.g., MySQL 3306, Remote Access console, or arbitrary intranet hosts) is rejected by the Host.
  3. No General Bearer Tokens: Relay allocations are requested by the Host on behalf of a specific authenticated PlayerIdentity/session via the control plane. Allocations are bound to RealmId, PlayerId, have short lifetimes (default 120s before connection), and cannot be used for administrative or control plane operations.
  4. Dumb Byte Tunnel: The Relay never terminates WoW encryption or parses character data. The only packet inspection performed is rewriting the address field in the plaintext CMD_REALM_LIST response (opcode 0x10) so the client seamlessly connects to the allocated WORLD port without patching AzerothCore or mutating MySQL realmlist.
  5. Zero Plaintext Logging: Relay logs contain only connection identifiers, timestamps, stream IDs, and byte counts. Zero credentials, account names, session keys, or packet contents are logged.
  6. Capacity Model & Resource Limits (Phase 13.1 Hardened):
    • Port Pool Range: 40000–43999/tcp (4,000 dynamic TCP ports).
    • Allocation Complexity: O(1) acquisition via randomized Fisher-Yates swap/pop and O(1) release with index tracking.
    • Capacity Model: 4,000 available ports support up to 2,000 concurrent player sessions (2 ports per allocation: Auth + World).
    • Source IP Binding: Each allocation binds expected_client_ip. Unsolicited connections from unexpected IPs are immediately dropped and ports released.
    • Strict Allocation Lifecycle:
      • Auth port is released immediately as soon as authentication finishes or fails.
      • World port is released immediately upon client disconnect or session termination.
      • All ports belonging to a Host are reclaimed immediately upon Host tunnel drop.
      • An active 5s background reaper reclaims expired unconsumed allocations (120s timeout).
    • Database Access Role: Relay connects to PostgreSQL using the dedicated least-privilege role coa_relay (USAGE on public schema, SELECT restricted strictly to (realm_id, public_key, published, advert_version) on realms).
    • Framing Limits: Standard control messages capped at 4 KiB; remote character transfer offers and chunks permitted up to 4 MiB.
    • Timeouts & Keepalive: 10s challenge timeout, 60s idle timeout, 30s ping/pong keepalive.
    • Bounded Streaming Chunks: 16 KiB chunks with backpressure.

2. Host Tunnel Handshake

When a Host Manager publishes a realm, it establishes an outbound connection to /relay/v1/host.

  1. Relay Challenge: The Relay sends a challenge frame containing a random nonce:
    { "nonce": "<base64url_32_bytes>" }
    
  2. Host Hello: The Host signs the challenge with the realm's Ed25519 private key:
    message = "coa-relay-host-v1\n" || protocol_version || "\n" || realm_id || "\n" || nonce
    
    The Host replies:
    { "realm_id": "<RealmId>", "signature": "<base64_signature>" }
    
  3. Relay Verification: The Relay verifies the signature against the realm's registered public key in the database (coa_registry.realms).
  4. Relay Welcome: On success:
    { "ok": true, "message": "welcome" }
    

3. Tunnel Messaging (TunnelMsg)

Multiplexed JSON messages over the WebSocket tunnel:

Message Fields Purpose
ping — Keepalive probe
pong — Keepalive response
allocate request_id, player_id Host requests game ports for a player
allocate_ok request_id, token, relay_host, auth_port, world_port, expires_at Relay provides allocated ports
allocate_err request_id, error Allocation failed (e.g. pool exhausted)
connect stream_id, token, target (auth | world) Client connected to relay port; requests Host to connect local target
connect_ok stream_id Host connected local target successfully
connect_err stream_id, error Host failed to connect local target
data stream_id, chunk (base64) Bidirectional raw stream bytes
close stream_id Half-close / end of stream
reset stream_id Immediate abnormal stream termination

4. Client Flow & REALM_LIST Address Rewriting

1. Player clicks "Join Realm" in Manager
2. Player Manager requests AllocateRelay over Noise control channel
3. Host Manager allocates (auth_port, world_port) via Relay tunnel
4. Host returns RelayAllocated to Player Manager
5. Player Manager writes realmlist.wtf: "set realmlist relay_host:auth_port" and starts WoW client
6. WoW client connects to relay_host:auth_port (authenticates with SRP6 against local authserver)
7. Authserver sends CMD_REALM_LIST (0x10) with local address (e.g. "127.0.0.1:8085")
8. Relay rewrites address string to "relay_host:world_port" and updates packet body length
9. WoW client receives rewritten realmlist and connects directly to relay_host:world_port
10. World session begins; bytes tunneled end-to-end through Host tunnel

Packet Layout (0x10 CMD_REALM_LIST)

  • [0]: Opcode (0x10)
  • [1..3]: Packet Body Length (uint16_le)
  • [3..7]: Unused (uint32)
  • [7..9]: Realm Count (uint16_le)
  • For each realm:
    • [0]: Type (uint8)
    • [1]: Lock (uint8)
    • [2]: Flags (uint8)
    • [...]: Name (null-terminated string)
    • [...]: Address (null-terminated string) -> Rewritten to <relay_host>:<world_port>\0
    • [...]: Population (float32)
    • [...]: Characters (uint8)
    • [...]: Timezone (uint8)
    • [...]: Realm ID (uint8)
  • Footer: 0x10 0x00