Revision Difference
RustRelay#567973
# RustRelay Recipient Server⤶
⤶
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⤶
⤶
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 |⤶
|--------|---------|---------|⤶
| `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.⤶
⤶
## 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.⤶
⤶
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.⤶
- 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⤶
<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.
Garry's Mod
Rust
Steamworks
Wiki Help