Files
2026-10-14 06:59:51 +02:00

3.9 KiB

Follow-up request: the RecRoomClientEncryptLayer 9-byte format

Your Tachyon findings resolved the handshake failure. Reconciled against the server side:

  • The client's inbound layer strips 9 bytes off every datagram before LiteNetLib parses it.
  • Our stock server sent a 15-byte ConnectAccept; after the client strips 9 it is 6 bytes, so NetConnectAcceptPacket.FromData fails Size != 15 and returns null — the exact silent ignore-and-retry-to-timeout we observed.
  • Confirmed on the server: a stock server absorbs the client's over-long ConnectRequest (the 9 extra bytes fold into variable-length connect data), which is why the server logged "peer connected" while the client never did.

So the server needs the matching layer. We have built it as a configurable PacketLayerBase with ExtraPacketSizeForLayer = 9, and proved the plumbing: with the same 9-byte framing on both ends the handshake completes; with it on the server only, a stock client fails identically to the game today.

The one thing blocking a live test is the exact 9-byte format and the null-key behaviour. This is §3 / step 1 of your own task doc, and it is now the whole critical path.

Q1 — RecRoomClientEncryptLayer.ProcessOutBoundPacket (RVA 0x735D760)

Full disassembly / decompile. Specifically:

  • Are the 9 bytes prepended (written at the front, payload shifted) or appended (written after the existing payload)? The offset math in the method answers this.
  • What are the 9 bytes when the session key field (DAKIIEIEKNI, NetPacket+0x18) is null — the state during the whole LiteNetLib + NGO handshake, before RecRoom_SetEncryptionInfo runs? Zero fill? A counter? A magic constant? Copied from somewhere in the packet?
  • When the key is non-null, what transforms the payload — AES over the body, and the 9 bytes carry a nonce/tag? We need the outbound path, but the null-key branch is what unblocks the handshake.
  • Does it branch on packet property (e.g. treat ConnectRequest/Accept differently from Channeled data), or is it uniform for every datagram?

Q2 — RecRoomClientEncryptLayer.ProcessInboundPacket (RVA 0x735DF90)

Full disassembly / decompile. Specifically:

  • Does it validate the 9 bytes (compare a MAC/magic/counter and drop on mismatch), or just strip them? This is the single most important question: if it only strips when the key is null, a zero-fill server reply connects immediately; if it validates, we must reproduce the exact bytes.
  • Same prepend-vs-append question from the read side (which 9 bytes are removed, front or back).
  • The null-key branch specifically — what happens to a datagram that arrives before a key is set (which is every handshake packet). Does it early-out, strip-only, or reject?
  • Any length guard (e.g. drop if size < 9).

Q3 — the two unidentified layer methods

0x735DC20 and 0x735E140 are listed as unidentified members of the layer. One-line summary of each — likely a key setter (the sink for RecRoom_SetEncryptionInfo) and/or a nonce/counter helper. If one installs the key into DAKIIEIEKNI, note its signature.

Q4 — the .cctor at 0x735E220

If the layer has static state (a fixed magic, a base nonce, an RNG seed), it is initialised here. Dump any constants it writes.

Why this precise scoping

We are one bounded read from a live handshake. If Q2's null-key path is strip-only, the layer we have built connects the real client as-is and we move straight to the NGO ConnectionRequest (where your Tachyon JSON / "Carrot" findings take over). If it validates, we need the byte recipe before any wire test is meaningful — a guess here is the "plausible-but-wrong costs more than not-found" trap you flagged.

Report bytes and offsets, not prose. Where the null-key path is genuinely absent (i.e. the layer assumes a key is always set), say so — that itself would be a finding, implying the key is established out-of-band before the first datagram.