Repository navigation
Gateway design
The design for the gateway: the part of the server that holds every client connection, from sign-on to the world, turns what arrives into checked messages for the owners, and writes each session's replies. Step 3's first stage builds it for the pregame connections, and in-world routing follows with the map side; Roadmap says how far that has got. The messages themselves are on Client protocol and in the message reference.
The gateway owns the three client ports and every connection on them. It frames, decrypts, unwraps and decodes each message, admits it only when its declaration is verified to its fields and the opcode table has a row for it, and posts it to the owner the row names. It runs the login exchange itself, never touches game state, and treats everything a client sends as hostile.
The client holds one socket per session and tags most of its messages with where their first check runs, so one place has to hold the sockets and decide where every message goes. Keeping that place free of game state lets it do the slow part of a login, reading the account's credentials, without holding up an owner. And since the ports face whoever can reach them, the limits belong at the door.
flowchart TD
C["Client"] -->|"STS, 6600"| STS["Sign-on connection"]
C -->|"login, 23115"| LC["Login connection"]
C -->|"world, 24000"| WC["World connection"]
STS -->|"read salt and verifier"| AD["Account directory"]
STS -->|"issue a game token"| AO["Account owner"]
LC -->|"redeem the game token"| AO
WC -->|"redeem the session key"| AO
WC -->|"list, create, delete, rename, select"| RO["Realm owner"]
The owners are on Owners design, and the account directory, the one read the gateway makes itself, is on Persistence design.
Three ports, one gateway. Sign-on (STS) listens on 6600, login on 23115 and the world on 24000, each through a Kestrel connection handler.
Every connection has one shape: a read loop that frames each message, answers the transport ping, decodes the message and hands it on, and a single writer per session that drains its outbound queue in order, wraps each message in an envelope under the session's current key, and flushes once per drain.
Transport is Kestrel's socket handling, with NoDelay on, send backpressure, the frame length written at the header's true width, and the client's transport ping answered. In the world, each session flushes once per tick.
A message gets in only through the verification gate. The gateway handles a client message only when its declaration is verified to Fields or beyond and the opcode table has a row for it. One below Fields is dropped and counted, never read, and the session carries on; sending one is an error in our own code. Message audit design has the rungs.
The gateway routes by an opcode table, one row per client opcode: its owner, its message priority, its rate limit per session (a token bucket, with a rate and a burst), the action past that limit (drop, log, kick or ban) and a kill switch, flipped through the runtime settings so an exploited system can be turned off without a restart. For a message that travels tagged, the client supplies the routing tag, so the gateway checks it against the row and disconnects on a mismatch; an untagged one is routed by its row alone. CI checks the table against the client facts: every opcode the client sends has a row, and every row's tag is the one the client uses. Client telemetry, bug reports, tickets and surveys go to an operations sink and never reach a map.
The gateway runs the login exchange itself: the STS exchange and its SRP6 proof, reading the salt and verifier with an async read, which it can do because it owns no game state. The account owner only decides: banned or suspended, already online, issuing and checking the game token and the realm session key, and loading entitlements.
-
An unknown account gets a fake but stable salt and verifier, derived per email from a server secret (
Gateway:UnknownAccountSecret), so the exchange looks the same either way, fails at the proof like a wrong password, and never reveals which accounts exist. - A second login replaces the first. The old session is dropped, and its character is handed back and saved before the new session can select it.
- Tokens and session keys are single use and expire within about a minute.
Sign-on follows its states. Each request is accepted only in the state that expects it, and anything else ends the connection. The exchange proves the password with SRP6 over SHA-256, turns RC4 on in both directions after the proof, and ends with a game token for the login server. Its reader takes a request split across reads, or two in one read, with explicit caps on the header and body sizes, and a body's length counts its UTF-8 bytes.
Keys switch at the byte the client switches them. The client's first message under a world connection's new key can arrive in the same read as WorldHello, so the gateway switches its receive key before it decodes the next frame. Sign-on does the same when RC4 turns on part way through a read.
The client ports take hostile input. The inbound frame size is capped: the frame length is 32 bits wide, and the codec's reader already bounds every count and string by the bits left in the body, so the frame is the one unbounded allocation. Every client port limits connections per IP and times out a connection that hasn't logged in, login attempts are rate-limited on STS and Auth, and a session that sends past its overall message rate limit is dropped.
No secret is ever logged: not a password, salt, verifier, game token, session key or RC4 key.
| Knob | Default | Sets |
|---|---|---|
| Client listener addresses | loopback | Where the STS, login and world ports listen |
| The client ports |
6600, 23115, 24000
|
The STS, login and world ports |
ConnectionsPerAddress |
not set yet | How many connections one address may hold on each listener |
Gateway:UnknownAccountSecret |
none, required | The secret an unknown account's salt and verifier are derived from |
Gateway:WorldPublicAddress |
none, required | The world server's address, handed to the client at login |
- Tagged routing arrives with the map side: a
TaggedMessage(0x038C) carries a message under its opcode's tag (3, 11 or 19), checked against the opcode table's owner.
| Idea | What killed it |
|---|---|
| Routing on the client's tag alone | The client supplies the tag. The opcode table decides, and a tag that disagrees with it disconnects |
| Claim | Basis |
|---|---|
| Frames, envelopes, keys and the transport ping | Established from the client; the conversation is on Client protocol |
| The re-key timing | Established from the client |
| The sign-on exchange | Carried over from the old code, which a real client signs on against. A real login against our own server is the check, owed once the first stage lands |
- What the
LoginDeniedandKickedvalues mean beyond the general ones; until they're read, the gateway sends the general one.
Guides
Explanation
Reference
Design
- Server core design
- Roadmap
- Codec design
- Message audit design
- Host design
- Gateway design
- Owners design
- Persistence design
- Command layer design
- Harness design
About this wiki