RustRelay
RustRelay
RustRelay is built into Rust. When enabled, the Rust server forwards its network packets to an HTTP 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 | Description |
|---|---|---|
relay.cfg_server_url |
"" |
Base URL of your recipient server, e.g. https://my-relay.example.com. |
relay.cfg_server_auth_token |
null |
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/ingesthandshake: also as theaccess_tokenquery parameter.
Reject anything without a valid token.
Context on every request
- Wipe id:
X-Wipe-Idheader orwipeIdquery param. Identifies one session; keep wipes separate. - Server time:
X-Server-Timeheader, 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
/ws/ingest: each binary WebSocket message is one whole packet (reassemble frames untilEndOfMessage).
Session marker. Before real packets, Rust sends a 12-byte marker:
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 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:
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:
Deserializing packets (.NET)
Add the package:
Strip the type byte, then either deserialize the protobuf payload or read the fixed fields:
The Entity object exposes the networked sub-objects: basePlayer, buildingBlock, baseCombat,
metabolism, flags, and the rest.
Garry's Mod
Rust
Steamworks
Wiki Help