Repository navigation
Gateway
How the gateway works as built, and why: the three client ports, what each connection does with the client, the gate every message passes, and the limits at the door. The facts to look up are on Gateway connections, Gateway sign-on, Gateway gate, Gateway limits and the generated Gateway opcode table. What's still to build is on Gateway design, and the conversation the client holds is on Client protocol.
flowchart TD
C["Client"] -->|"STS, 6600"| STS["Sign-on connection"]
C -->|"login, 23115"| LC["Login connection"]
C -->|"world, 24000"| WC["World connection"]
STS -->|"issue a game token"| AO["Account owner"]
LC -->|"redeem the game token"| AO
WC -->|"redeem the session key"| AO
WC -->|"character select"| RO["Realm owner"]
Three ports, one gateway. Sign-on (STS), login and the world each listen on a port of their own, through a Kestrel connection handler that takes a slot from the per-address connection limit and hands the connection to its class.
The three connections pass the client along. Sign-on proves the password and ends in a game token from the account owner. The login connection redeems that token and hands the client the world's address and a session key. The world connection redeems the session key, then carries character select to the realm owner. Gateway sign-on and Gateway connections have each step.
The gateway runs the login exchange itself. It's the one part of the server that reads the database on a client's behalf, for the account's salt and verifier from the account directory (Persistence), and it can because it owns no game state. The account owner only decides: who may play, and the game tokens and session keys it issues and redeems.
Every game connection has one shape: a read loop that frames each message, answers the transport ping, checks the message and hands it on, and a single writer per session that drains its outbound queue in order under the session's current key. Keys switch exactly where the client switches them, so a message that arrives in the same read as the one that changed the key is decoded under the new one.
Nothing reaches an owner unchecked. Every client message passes the gate: a verified declaration, a row in the opcode table, and that row's rate limit. The row, not the message, names the owner it goes to.
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.
The ports face whoever can reach them, so the limits belong at the door. Every port caps the frame size and the connections per address, and times out a connection that hasn't logged in (Gateway limits). The frame is the one allocation a client controls: the codec's reader bounds every count and string by the bits left in the body, so capping the frame bounds them all. An unknown account's sign-on runs exactly like a real one and fails at the proof, so nothing tells an attacker which accounts exist, and no secret is ever logged.
| Setting | Default | Sets |
|---|---|---|
Gateway:Sts, Gateway:Login, Gateway:World
|
127.0.0.1 on 6600, 23115 and 24000 |
Each listener's address and port; the ports have to differ |
Gateway:WorldPublicAddress |
127.0.0.1 |
The world address handed to the client, IPv4 |
Gateway:UnknownAccountSecret |
none, required | At least 32 bytes in base64, the same across restarts |
Gateway:MaxClientFrameBytes |
65,536 | The largest frame a client may send, 64 to 1,048,576 |
Gateway:ConnectionsPerAddress |
8 | Connections per address on each listener |
Gateway:PreAuthTimeout |
30 s | Time to complete a login |
Gateway:OutboundQueueCapacity |
4,096 | A session's outbound queue |
Gateway:DisconnectGrace |
5 s | Time a disconnecting session gets, 0 to 1 minute |
Gateway:LoginAttemptsPerMinute |
10 | Login attempts per address |
Gateway:DisabledOpcodes |
none | Client opcodes to drop, the kill switch |
Sign-on's header and body caps and the writer's batch of 64 are fixed in code.
| Term | Means here |
|---|---|
| Frame | One message on the wire: length, opcode, body |
| Envelope | The encrypted wrapper a message travels in after the first greeting |
| Game token | What sign-on issues and the login connection redeems |
| Session key | The key the login connection hands the client for the world connection, redeemed there once |
| Gate | The checks every client message passes before it reaches a handler |
| Opcode table | The list of client messages the gateway admits, each with its owner and rate |
- Sign-on answers every refusal with one error, until the client's other codes are verified.
- The opcode table holds pregame rows only; in-world routing comes with step 3's second stage.
- A logged-in connection has no idle limit yet, and there's no overall message rate per session.
- Switching an opcode off takes a restart.
- Sending and handling still need
Fields; the per-member rules (Message audit design) come with step 3's second stage.
| Piece | Path |
|---|---|
| Frames, envelopes and the packet cipher | Source/NexusTomorrow.Transport/ |
| The catalog, the opcode table, the decoder and the rate gate | Source/NexusTomorrow.Gateway/Catalog/ |
| Sign-on | Source/NexusTomorrow.Gateway/Sts/ |
| The connection base and the login and world connections | Source/NexusTomorrow.Gateway/Connections/ |
| Limits and the Kestrel handlers |
Source/NexusTomorrow.Gateway/Limits/, Source/NexusTomorrow.Gateway/Kestrel/
|
| Settings | Source/NexusTomorrow.Gateway/GatewayOptions.cs |
| SRP6 and RC4 | Source/NexusTomorrow.Cryptography/ |
| Tests |
Tests/NexusTomorrow.Transport.Tests/, Tests/NexusTomorrow.Gateway.Tests/
|
Start here
How the server works
- Server core design
- Host
- Gateway
- Owners
- Persistence
- Command layer
- Harness
- Map scheduler design
- Timer wheel design
The protocol
Working on it