the files

This commit is contained in:
niko
2026-10-14 06:59:51 +02:00
parent f6175893fc
commit 2040419525
1855 changed files with 6735668 additions and 0 deletions
@@ -0,0 +1,194 @@
# Tachyon voice server — findings from client static analysis
Source: static analysis of Rec Room build `20250718.01`. Every RVA and obfuscated name below
is valid only for that build and must be re-derived by shape for any other. Facts are tagged
**[PROVEN]** (read from the image) or **[UNVERIFIED]** (hypothesis — confirm before building on it).
This file is the reference; the wire mechanics we have implemented/verified live in
`ngo-litenetlib-wire.md`. Open reads are tracked in `client-agent-request-2.md`.
## Architecture
**[PROVEN]** Dedicated server, star topology, server relays audio. The client can only ever be
a client: `NetworkManager.StartHost`/`StartServer` have zero call sites;
`LiteNetLibTransport.StartServer` is dead code; `StartClient` binds an ephemeral port and only
connects outbound. Dissonance role selection always resolves to client.
## Protocol stack
```
UDP
└─ RecRoomClientEncryptLayer +9 bytes/datagram, always present [PROVEN]
└─ LiteNetLib 1.x ProtocolId/HeaderSize UNVERIFIED
└─ NGO 1.6+ (trimmed) 7 message types, no objects/vars/scenes [PROVEN]
└─ Tachyon handshake JSON in the NGO connection payload
└─ Dissonance 8.x over NGO named/unnamed messages
```
## LiteNetLib layer (task doc section 4)
**[PROVEN]** Bundled inside `Netcode.Transports.LiteNetLib.dll`, name-obfuscated except
`LiteNetLibTransport`. `NetConnectAcceptPacket` matches stock exactly: `Size == 15`,
connectionNumber at `[9]` (reject `>= 4`), isReused at `[10]` (reject `> 1`), peerId at
`[11..14]` (reject negative). `ProcessConnectAccept` matches stock: requires state `Outgoing`
and `ConnectionTime == peer.ConnectTime`.
**[UNVERIFIED]** `NetConstants.ProtocolId` and `NetConnectRequestPacket.HeaderSize` — live in
metadata `fieldDefaultValues`, not yet walked. Our capture pins ProtocolId to **13** from the
wire, but that is not yet confirmed from the image. The 3 connect-data bytes `01 00 11` were
not traced to their writer.
## Encrypt layer (task doc section 3) — SOLVED
`RecRoomClientEncryptLayer` (`MGHAOMEMAHF`), always installed. **[PROVEN, read from image]** It
is a variable-length **trailing-marker** scheme — `ExtraPacketSizeForLayer = 9` is the
worst-case reservation, not the bytes added. The marker is always the LAST byte.
Method map (the task doc had inbound/outbound swapped — corrected):
| RVA | method |
|---|---|
| `0x735DC20` | `ProcessOutBoundPacket` |
| `0x735D760` | `ProcessInboundPacket` |
| `0x735DF90` | `SetKey` (copies exactly 32 bytes, AES-256 sized) |
| `0x735E140` | `ClearKey` (nulls key on disconnect) |
Outbound: `key == null` → append one byte `0x11`, `length += 1`. `key != null` → append 8-byte
RNG nonce + marker `0x2A` (`length += 9`) and encrypt the payload. No branch on packet property.
Inbound: read last byte. `0x11` → strip 1 (no key needed). `0x2A` → decrypt, needs key,
requires `length > 9`, strip 9. Any other marker → **drop** (this is why our earlier
`--accept-extra 00` sweep silently failed — `0x00` is an unknown marker).
**The whole LiteNetLib + NGO handshake runs key == null, so plaintext `0x11` is all that is
needed to reach NGO.** Implemented and verified end-to-end in
`src/main.cpp` (marker lambdas + `patches/`) (`--encrypt-layer`): the ConnectAccept goes out as 16
bytes (15 + `0x11`), the client accepts, and marker-framed NGO data flows.
**[UNVERIFIED]** The encrypted-path cipher. The transform (`0x73653E0`) is Burst-compiled and
dispatches through function pointers; the 32-byte key does not by itself prove AES. Not needed
for the handshake. It only matters if the session actually sets a key — which the Tachyon
approval flow controls, so keeping the client key-null (plaintext `0x11` throughout) is the
path worth checking before touching the cipher.
**[PROVEN deduction]** The connect-data bytes we saw as `01 00 11` are really `01 00` (2 bytes)
plus the layer's trailing `0x11` marker.
## NGO layer (task doc section 5)
**[PROVEN]** Trimmed to 7 message types, no NetworkObject/Behaviour/Variable/spawn/scene. We do
not reimplement NGO — just handshake, time sync, and a named/unnamed relay. NGO type names are
not obfuscated. Rec Room forked `NetworkManager` with `RecRoom_ConnectionInfo` get/set.
**[UNVERIFIED]** Hypothesis: `RecRoom_ConnectionInfo` carries the approval JSON to the client,
since stock `ConnectionApprovedMessage` has no arbitrary payload. Accessors are shared stubs —
call-xref is unreliable here.
## Tachyon handshake (task doc section 6)
Session wrapper `BAOFAOBLAMJ` = `TachyonClient`, `RecRoom.Networking.PhotonImpl.Runtime`.
**[PROVEN] Client to server** (NGO connection-request payload), `HCNIOOINPCA` `0x82786B0`,
JSON type `PAFCNOLGBNM`, all keys `[JsonProperty]` `Required.Always`, `byte[]` as base64:
| key | member | type |
|---|---|---|
| `AI` | AccountId | string |
| `AT` | AccessToken | byte[] |
| `VB` | VerificationBlob | byte[] |
| `CKA` | ClientKeyA | byte[] |
| `CIA` | ClientIVA | byte[] |
| `CPK` | ClientPublicKey | byte[] (RSA CspBlob) |
**[PROVEN] Server to client** (approval payload), `CDJMICBEALM` =
`NetworkManager_OnClientConnectedCallback` `0x8277880`, JSON type `PMFAAFNFMAL`:
| key | member | type | note |
|---|---|---|---|
| `DR` | Reason | string | `Required.Always`; **must equal `"Carrot"`** |
| `KT` | SessionKeyB | byte[] | `Required.Default`; RSA-encrypted to the client's `CPK` |
Split-key: transport key = f(client `CKA`/`CIA`, server `SessionKeyB`). **[UNVERIFIED]** the
combination function — read `0x8277880`.
**[PROVEN] The RSA blocker.** `DMIBBCKIGCG` (`.cctor` `0x827A820`) holds a hardcoded 1024-bit
RSA **public** key; the client encrypts `AT`/`VB`/`CKA`/`CIA` to it. A working server needs the
matching **private** key, which is Rec Room's and not present in the client. Recommended route:
generate our own 1024-bit keypair and **patch the embedded modulus literal** in the client
(plain string in `.data`), coordinated with the client patcher so both ship the same keypair.
Key material and the RSA modulus are NOT committed to this repo — supplied at runtime.
## CPK is opaque — the RSA blocker still stands
The client-agent's "you can mint KT without Rec Room's private key" does **not** hold against
the capture. The `CPK` in the request JSON is 160 bytes of high-entropy data: no
`PUBLICKEYBLOB` header (`0x06 0x02`), no `RSA1` magic, and an even low byte — so it is not a
plaintext RSA modulus or CspBlob. **CPK is itself RSA-encrypted** (to Rec Room's key, like
AT/VB/CKA/CIA). A third-party server cannot read it, so it cannot encrypt a `KT` the client
will decrypt. The `KT`-minting path is therefore not viable without Rec Room's private key.
Consequence: to run voice, the client-patch route is the real option — neuter the connected
callback's encryption setup / the layer's `SetKey` so the transport key stays null and the
layer keeps emitting plaintext `0x11`. This is independent of reaching NGO "Connected", which
does not need a valid `KT` at all (ConnectionApproved.Deserialize does no validation).
## NGO handshake: implemented
The server now speaks the fork batch format (`--fork`) and sends a ConnectionApproved:
24-byte header (`BatchCount` at offset 16, hash `XXHash64` over `[24:]`), message type 0
(ConnectionApproved in the client's pre-approval local order), the client's 8-entry version
header echoed verbatim, `OwnerClientId`, `NetworkTick`, and a length-prefixed
`{"DR":"Carrot"}` payload. Verified offline against the real captured request: the fork parser
confirms the batch hash and our reply round-trips. `KT` is omitted (CPK opaque), so the
client's post-connect callback will throw on `RSA.Decrypt` *after* the Connected event — which
is the expected boundary of what a patch-free server can reach.
## Dissonance (task doc section 7)
**[PROVEN — live]** After NGO Connected, the real client sends a Dissonance handshake as an NGO
`NamedMessage` (our message index 3), plaintext (marker `0x11`), and **repeats it every ~4s
because the server does not answer**:
```
NamedMessage hash = 0xc78b0000000f0001
payload (13 bytes) = 04 01 00 00 03 c0 00 00 bb 80 00 02 32
```
`0x04` looks like a Dissonance message-type byte; `03c0` = 960 and `bb80` = 48000 decode as the
Opus frame size and sample rate, so this is the client advertising its audio config and waiting
for a server handshake reply. This is the next layer to implement — see
`docs/client-agent-request-4.md`. For a proximity relay the *voice* packets can still be
forwarded opaque; only the session handshake needs server logic.
**[PROVEN]** `NfgoCommsNetwork` present, role from NGO client flags; integration rewired onto
raw named/unnamed messages (no NetworkBehaviour). Derive the handshake reply from
`NfgoCommsNetwork` methods, `DissonanceNetworkManagerInjector` `0x826A370`, and `DissonanceVoip.dll`.
## MILESTONE (2026-08-21): full handshake works, plaintext, no client patch
The server (`reference`, `--encrypt-layer --fork`) completes the entire stack against the
real client: LiteNetLib + encrypt layer + forked NGO + Tachyon `"Carrot"` approval. The game
**connects, does not crash, and reports "connected to voice."** Traffic is plaintext `0x11` —
omitting `KT` left the client key-null (its post-connect RSA callback is caught, non-fatal), so
**no client patch is needed** for plaintext voice. What remains is the Dissonance session
protocol above, then multi-client relay.
## Matchmaking DTO (task doc section 8)
**[PROVEN]** `GET https://match.<host>/player/connection-info?roomInstanceId={long}` returns a
bare 9-key DTO including `VoiceConnectionInfo` (`"<ip>:<port>"`) and `VoiceServerId`. Client
advertises `VoiceServerVersion` in the `matchmake/v2/room` body; server returns the allocated
voice server here. Empty strings for both voice fields = "no voice", definitely accepted — so
voice can ship dark while the handshake is brought up.
## Order of work (from the task doc)
1. Read the encrypt-layer 9-byte format + null-key path (`client-agent-request-2.md`). **now.**
2. Pin ProtocolId / HeaderSize from `fieldDefaultValues`.
3. Confirm how the approval JSON reaches the client (`RecRoom_ConnectionInfo`?).
4. Read the key-combination function at `0x8277880`.
5. Client to `Connected`: LiteNetLib + NGO ConnectionRequest/Approved + `"Carrot"`.
6. Time sync, then the Dissonance relay.
Steps 1–4 are each a bounded read that silently invalidates the implementation if guessed —
land them as written findings before server code.