Files
2026-08-05 12:48:09 -04:00

8.8 KiB

RR Redirector (native)

A native (C/Win32) DLL that points the Rec Room client at a self-hosted server, without any managed mod loader. BepInEx 6 and MelonLoader both fail on current Rec Room builds (crash in il2cpp_init / loader trips the anti-cheat memory-integrity scan). This build sidesteps that: it is loaded as a version.dll proxy and applies its patches — Winsock DNS, the HTTP request URL, TLS pinning, the memory-integrity scan and EAC — directly in native code.

How it works

  1. Loading vector. RecRoom.exe/UnityPlayer.dll import VERSION.dll by name, and the loader searches the game folder before System32. We ship our own version.dll there; each of its 17 exports is a thin wrapper that lazily loads the real system version.dll (by full path, so no recursion) and calls through, so the game keeps working. Its DllMain starts the hook thread. Loads very early, before UnityPlayer.dll. Self-contained — nothing else to ship.

    RecRoom.exe spawns UnityCrashHandler64.exe from the same folder, so our DLL loads there too. The hook thread checks the host executable and exits immediately in anything but RecRoom.exe — otherwise every launch opened a second debug console and left a stray process waiting on Unity.

  2. DNS host rewrite (src/hooks/dns_hook.c). Detours ws2_32!getaddrinfo. A lookup for an exact from host in redirector.json (e.g. ns.rec.net) is resolved as its to host (ns.recflare.net) instead — we hand the rewritten name to real DNS, so the client reaches the target's current IP (survives dynamic IPs) rather than a pinned address. Surgical: only the configured hosts are affected. Necessary but not sufficient on its own — it changes only name resolution, leaving SNI and the Host: header saying ns.rec.net. Kept as a safety net under (3).

  3. HTTP host rewrite (src/unity/http_rewrite.c) — the patch that actually moves traffic. Hooks the concrete static BestHTTP.HTTPManager.SendRequest(HTTPRequest), reads req.Uri.AbsoluteUri, swaps the host through the same redirector.json pairs, and assigns a fresh new Uri(...) back before letting the real SendRequest run. The new host therefore carries end-to-end — URL, SNI and Host: — so the target can serve it as its own vhost with its own cert. Native equivalent of the managed build's SendRequestPatch. This is the one call-through hook, so it depends on the relocating trampoline in src/memory/detour.c.

  4. TLS pinning bypass (src/unity/ssl_patch.c). Redirecting HTTPS means the handshake presents a cert the client would reject. Resolves the concrete Org.BouncyCastle.Crypto.Tls.LegacyTlsAuthentication.NotifyServerCertificate and detours its compiled body to a no-op that accepts unconditionally — the native equivalent of the managed build's DisableTLSPinning Harmony patch.

  5. Memory-integrity scan neutralizer (src/unity/memcheck_patch.c). The client runs a background scan that hashes GameAssembly.dll code against baked-in hashes; the inline hooks above change that memory, so boot dies with "Launch validation failed." The scanner's name is obfuscated and rotates every build, so it is found by signature instead: the class in Assembly-CSharp that holds both a Thread and a CancellationTokenSource field. Its public instance 0-param non-void method is the scan entry point; we detour it to return an already-resolved promise (fetched from the promise type's static Resolved getter), so boot's await satisfies instantly. Started first among the il2cpp patches — the boot step that awaits the scan can fire early, and the reflection sweep needs a head start.

  6. EAC neutralizer (src/unity/eac_patch.c). Two replace-only hooks on RecRoom.AntiCheat.EACManager: the readiness check (the sole static 0-param bool non-property-getter method — again resolved by signature, since the name rotates) is forced to true, because the real check needs EasyAntiCheat services that no longer exist; and GenerateChallengeResponse(string) (unobfuscated) returns base64(challenge), with base64("nothing") for an empty/null challenge. Safe to patch only because (5) has already neutralized the hash check.

Everything from (2) on runs off one background thread spawned in DllMain; each il2cpp patch gets its own thread, since they must wait on the runtime independently. src/unity/module_watch.c just logs GameAssembly.dll / UnityPlayer.dll as they appear and then stops.

The connect and gethostbyname hooks are present but intentionally not installed: the connect hook redirects all :443 traffic (would break Photon/CDN/telemetry), and getaddrinfo already covers the il2cpp DNS path.

Resolving obfuscated targets

Rec Room obfuscates its own type/method names and they rotate every build, so nothing here hard-codes one. Framework names (SendRequest, get_Uri, NotifyServerCertificate, GenerateChallengeResponse, EACManager) are stable and resolved literally; the anti-cheat internals are resolved by shape — field types, method signature, return type — through the il2cpp reflection API at runtime. Every candidate is logged, and an ambiguous match logs a WARNING rather than silently guessing.

Build

Requires VS 2022 (C toolchain) + CMake + Ninja (both ship with VS). Must build x64 — a 32-bit DLL silently fails to load. Import the amd64 VC environment first:

& "C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Auxiliary\Build\vcvarsall.bat" amd64
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release
cmake --build build

Output: build\version.dll — a single self-contained proxy (it loads the real system version.dll at runtime, so there is nothing else to ship).

One-step deploy into the game folder (close Rec Room first — the DLL is locked while it runs):

cmake -S . -B build -G Ninja -DGAME_DIR="C:\Games\recflare-client-unstable"
cmake --build build

Install (manual)

  1. Copy build\version.dll into the Rec Room install root (next to RecRoom.exe). If BepInEx's version.dll is there, replace it (this build does not use BepInEx).
  2. Copy redirector.json.example to redirector.json there and set the rewrite pairs ({ "from": "ns.rec.net", "to": "ns.recflare.net" }). The same pairs drive both the DNS and the HTTP rewrite. Matching is exact — add one entry per host. Parsed by a flat key scan, not a real JSON parser, so keep it flat: one object per rewrite.
  3. Launch. A console window opens; logs also go to redirector_<pid>.log beside RecRoom.exe.

A healthy run logs all of these (each patch runs on its own thread, so they interleave; [MEMCHECK] lands last — its reflection sweep takes a moment):

[STATUS] DNS REDIRECT ACTIVE
[SSL]     TLS pinning bypassed (NotifyServerCertificate -> accept-all)
[EAC]     readiness check forced true
[EAC]     GenerateChallengeResponse -> base64(challenge)
[HTTP]    host rewrite installed on SendRequest
[MEMCHECK] native memory integrity scan skipped (scan-start -> resolved promise)
[HTTP]    https://ns.rec.net/ -> https://ns.recflare.net/          (one per request)

The per-request [HTTP] ... -> ... lines are the proof traffic is actually moving; everything above them only says the hooks installed. [DETOUR] ... refusing hook means the detour engine wouldn't touch that prologue (see below) and that patch is not active.

Known limitations / open items

  • Obfuscated targets are matched by shape, not name. A Rec Room build that changes the structure of the scanner class or the EAC readiness method — not just its name — will break that patch. The logs list every candidate considered, and warn when more than one matched, so a drift shows up as a WARNING or a "not identified" line rather than a silent misfire. Watch for [MEMCHECK] scanner candidate lines: more than one means the field-signature match is no longer unique.
  • The detour engine's length decoder is minimal. It relocates rip-relative disp32 and rel32 branches into a trampoline allocated within ±2 GB, but bails on two-byte (0F) opcodes, rel8 branches, and anything it doesn't model — and InstallDetour then refuses the hook rather than corrupt code. This only constrains call-through hooks (currently just SendRequest); replace-only hooks take a blind 14-byte overwrite, which is safe because they jump away and never execute the torn tail.
  • Nothing is undone on unload. The detours stay installed for the life of the process; the saved original bytes are kept but never restored.
  • The anti-cheat may catch up. The memory-integrity scan is neutralized at its managed entry point, not at the native scanner itself — a build that calls the scan from somewhere else, or adds a second check, would reject the client again.