Rust Wiki

Revision Difference

RustRelay#567974

# RustRelay Recipient Server⤶ # RustRelay⤶ RustRelay is built into Rust. When enabled, the Rust server forwards its network packets to an HTTP endpoint you run. This is the contract that endpoint has to implement.⤶ ⤶ ## Configuring RustRelay on the Rust server⤶ endpoint you run, called the recipient server. This page covers both halves: configuring the relay⤶ on the Rust server, then implementing a recipient server to receive it.⤶ ⤶ ---⤶ ⤶ # Rust Server Configuration⤶ Set these server convars, then run `relay.restart` to apply. Config is saved to `cfg/relay_cfg.json`. Changing the URL, token, encryption, or fake-player convar stops the relay, so follow it with `relay.restart`. | Convar | Default | Meaning |⤶ | Convar | Default | Description|⤶ |--------|---------|---------| | `relay.cfg_server_url` | `""` | Base URL of your recipient server, e.g. `https://my-relay.example.com`. | | `relay.cfg_server_auth_token` | — | Bearer token sent with every request. | | `relay.cfg_encryptpackets` | `false` | Encrypt every packet, not just console packets (see Encryption). | | `relay.cfg_fakeplayer` | `false` | Relay only packets destined for the fake spectator connection. | | `relay.cfg_filtermode` | `0` | RPC filter: `0` ignore, `1` relay all, `2` whitelist only. | | `relay.cfg_sendvoicedata` | `false` | Relay voice packets (off by default for legal reasons). | | `relay.cfg_sendconsoledata` | `true` | Relay console/chat packets (always encrypted, needs key exchange). | Commands: | Convar | Action | |--------|--------| | `relay.restart` | Start, or restart to apply config changes. | | `relay.shutdown` | Stop the relay. | | `relay.status` | Print a status report. | | `relay.clear_queue` | Clear the send queue. | | `relay.cfg_reload` | Reload `relay_cfg.json` from disk (stops the relay). | | `relay.cfg_save` | Save the current config to disk. | | `relay.rpc_whitelist_add <names…>` | Add RPC names to the whitelist. | | `relay.rpc_whitelist_remove <names…>` | Remove RPC names from the whitelist. | | `relay.rpc_whitelist_rebuild` | Rebuild the whitelist id lookup. | ## FakePlayer (recommended) A Rust server sends its packets per connection: every online player gets their own copy of each entity update and position. With `relay.cfg_fakeplayer` off, the relay forwards all of that, so you receive one duplicate stream per connected player and your volume scales with the player count. With `relay.cfg_fakeplayer` on, Rust registers a single fake spectator connection and the relay forwards only the packets destined for it. You get one stream instead of N copies, which is much less data to handle. It defaults to disabled, so you have to turn it on deliberately. Do that unless you have a specific reason to capture the raw per-connection traffic. ⤶ ---⤶ ⤶ # Recipient Server Outline⤶ ⤶ This is the contract your HTTP endpoint has to implement. All integers are little-endian.⤶ ## Authentication Rust sends `AuthToken` as a bearer token. Validate it however you like; the protocol just carries the string. - HTTP: `Authorization: Bearer <token>` - `/ws/ingest` handshake: also as the `access_token` query parameter. Reject anything without a valid token. ## Context on every request - **Wipe id**: `X-Wipe-Id` header or `wipeId` query param. Identifies one session; keep wipes separate. - **Server time**: `X-Server-Time` header, in 100-nanosecond ticks since the Unix epoch. Divide by 10,000 for milliseconds. ## Endpoints | Method | Path | Body | Purpose | |--------|------|------|---------| | WS | `/ws/ingest?wipeId=<id>` | binary, one packet per message | live packet stream | | POST | `/api/Packet` | `octet-stream`, length-prefixed packets | packet batch | | POST | `/api/Snapshot` | binary | base world snapshot | | POST | `/api/MapSnapshot` | multipart (`map` required, `dat` optional) | map files | | POST | `/api/Manifest` | JSON | prefab metadata | | POST | `/api/StringPool` | JSON `{ "<hash>": "<string>" }` | pooled-string lookup | | POST | `/api/Auth/exchangeKey` | JSON (see Encryption) | key exchange | Return `200 OK` on success. All require a valid token. ## Packet ingest ⤶ Two transports, same packets:⤶ - `/ws/ingest`: each binary WebSocket message is one whole packet (reassemble frames until `EndOfMessage`). - `POST /api/Packet`: body is packets back to back, each prefixed with an `int32` length. Accept⤶ bodies up to 200 MB.⤶ **Session marker.** Before real packets, Rust sends a 12-byte marker: ``` [ int32 magic = 0x53545252 ][ int64 serverTime ] ``` `0x53545252` is `RRTS`. A packet is a marker only if it's exactly 12 bytes and starts with that magic. `0x53545252` is `RRTS (RustRelayTimeStamp)`. A packet is a marker only if it's exactly 12 bytes and starts with that magic number. A marker isn't sent once and forgotten, nor is it sent on every frame. Rust sends one whenever the server time changes from the last marker, immediately before that frame's packets. In practice that means a fresh marker precedes each new frame that actually has packets to relay (frames with nothing to send produce no marker). Treat each marker as the timestamp for the packets that follow it, and drop ordinary packets until you've seen the first one. ## Packet format Each packet starts with a type byte: - `<= 140` → RakNet, ignore. - `<= 140` → RakNet internals, ignore (these aren't sent anyway). - otherwise subtract 140 to get the message type. | Value | Name | Body (after type byte) | |-------|------|------------------------| | 5 | Entities | `[uint32 reserved][bytes: protobuf Entity]` | | 6 | EntityDestroy | `[uint64 id][byte mode]` | | 9 | RPCMessage | `[uint64 id][uint32 rpcId]` | | 10 | EntityPosition | `[uint64 id][float x,y,z][float rotX,rotY,rotZ][float time][uint64 parent?]` | | 11 | ConsoleMessage | `[uint32 len][utf8]` (encrypted) | | 12 | ConsoleCommand | `[uint32 len][utf8]` (encrypted) | | 13 | Effect | `[bytes: protobuf EffectData]` | Other values (1–4, 7–8, 14–28) are valid but carry nothing you need to handle. `parent` on EntityPosition is present only if bytes remain. Drop any packet too short for its layout. The `Entities` and `Effect` payloads are Rust protobufs. The `Rust.Polyfills` NuGet package provides the classes (`ProtoBuf.Entity`, `EffectData`), so `ProtoBuf.Entity.Deserialize(bytes)` gives a typed object. It does not provide the message-type enum above; declare that yourself. ## Encryption `ConsoleMessage` and `ConsoleCommand` are always encrypted (skip the key exchange and just forward them undecoded if you don't need them). With `relay.cfg_encryptpackets` on, every packet is encrypted the same way. Key exchange is X25519 ECDH at `POST /api/Auth/exchangeKey`: ```js { "WipeId": "...", "ClientPublicKey": "rk1..." } // -> { "ServerPublicKey": "rk1..." } ``` Keys are Bech32-encoded with the `rk` prefix. Derive the AES-256 key as `SHA256(sharedSecret)`. Encrypted packet layout (AES-256-GCM), type byte in the clear: ``` [ byte type ][ 12-byte nonce ][ ciphertext ][ 16-byte tag ] ``` ## Deserializing packets (.NET) Add the package: ⤶ ```xml⤶ ⤶ ```html⤶ <PackageReference Include="Rust.Polyfills" Version="0.0.2" /> ``` Strip the type byte, then either deserialize the protobuf payload or read the fixed fields: ```csharp using System.Buffers.Binary; using ProtoBuf; // Rust.Polyfills: Entity, EffectData, ... void Handle(ReadOnlySpan<byte> packet) { if (packet.Length == 0) return; byte raw = packet[0]; if (raw <= 140) return; // RakNet, ignore var type = raw - 140; var body = packet[1..]; switch (type) { case 5: // Entities — skip the leading uint32, rest is a protobuf Entity Entity entity = Entity.Deserialize(body[sizeof(uint)..].ToArray()); ulong id = entity.baseNetworkable.uid.Value; break; case 13: // Effect EffectData effect = EffectData.Deserialize(body.ToArray()); break; case 10: // EntityPosition — fixed layout, read by hand ulong uid = BinaryPrimitives.ReadUInt64LittleEndian(body); var f = body[sizeof(ulong)..]; float x = BinaryPrimitives.ReadSingleLittleEndian(f); float y = BinaryPrimitives.ReadSingleLittleEndian(f[4..]); float z = BinaryPrimitives.ReadSingleLittleEndian(f[8..]); // rotX/Y/Z at 12/16/20, time at 24, optional parent uint64 at 28 break; case 6: // EntityDestroy ulong destroyed = BinaryPrimitives.ReadUInt64LittleEndian(body); break; } } ``` The `Entity` object exposes the networked sub-objects: `basePlayer`, `buildingBlock`, `baseCombat`, metabolism, flags, and the rest.