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
-
Loading vector.
RecRoom.exe/UnityPlayer.dllimportVERSION.dllby name, and the loader searches the game folder beforeSystem32. We ship our ownversion.dllthere; each of its 17 exports is a thin wrapper that lazily loads the real systemversion.dll(by full path, so no recursion) and calls through, so the game keeps working. ItsDllMainstarts the hook thread. Loads very early, beforeUnityPlayer.dll. Self-contained — nothing else to ship.RecRoom.exespawnsUnityCrashHandler64.exefrom the same folder, so our DLL loads there too. The hook thread checks the host executable and exits immediately in anything butRecRoom.exe— otherwise every launch opened a second debug console and left a stray process waiting on Unity. -
DNS host rewrite (
src/hooks/dns_hook.c). Detoursws2_32!getaddrinfo. A lookup for an exactfromhost inredirector.json(e.g.ns.rec.net) is resolved as itstohost (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 theHost:header sayingns.rec.net. Kept as a safety net under (3). -
HTTP host rewrite (
src/unity/http_rewrite.c) — the patch that actually moves traffic. Hooks the concrete staticBestHTTP.HTTPManager.SendRequest(HTTPRequest), readsreq.Uri.AbsoluteUri, swaps the host through the sameredirector.jsonpairs, and assigns a freshnew Uri(...)back before letting the realSendRequestrun. The new host therefore carries end-to-end — URL, SNI andHost:— so the target can serve it as its own vhost with its own cert. Native equivalent of the managed build'sSendRequestPatch. This is the one call-through hook, so it depends on the relocating trampoline insrc/memory/detour.c. -
TLS pinning bypass (
src/unity/ssl_patch.c). Redirecting HTTPS means the handshake presents a cert the client would reject. Resolves the concreteOrg.BouncyCastle.Crypto.Tls.LegacyTlsAuthentication.NotifyServerCertificateand detours its compiled body to a no-op that accepts unconditionally — the native equivalent of the managed build'sDisableTLSPinningHarmony patch. -
Memory-integrity scan neutralizer (
src/unity/memcheck_patch.c). The client runs a background scan that hashesGameAssembly.dllcode 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 inAssembly-CSharpthat holds both aThreadand aCancellationTokenSourcefield. 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 staticResolvedgetter), 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. -
EAC neutralizer (
src/unity/eac_patch.c). Two replace-only hooks onRecRoom.AntiCheat.EACManager: the readiness check (the sole static 0-paramboolnon-property-getter method — again resolved by signature, since the name rotates) is forced totrue, because the real check needs EasyAntiCheat services that no longer exist; andGenerateChallengeResponse(string)(unobfuscated) returnsbase64(challenge), withbase64("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)
- Copy
build\version.dllinto the Rec Room install root (next toRecRoom.exe). If BepInEx'sversion.dllis there, replace it (this build does not use BepInEx). - Copy
redirector.json.exampletoredirector.jsonthere and set therewritepairs ({ "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. - Launch. A console window opens; logs also go to
redirector_<pid>.logbesideRecRoom.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
WARNINGor a "not identified" line rather than a silent misfire. Watch for[MEMCHECK] scanner candidatelines: more than one means the field-signature match is no longer unique. - The detour engine's length decoder is minimal. It relocates rip-relative
disp32andrel32branches into a trampoline allocated within ±2 GB, but bails on two-byte (0F) opcodes,rel8branches, and anything it doesn't model — andInstallDetourthen refuses the hook rather than corrupt code. This only constrains call-through hooks (currently justSendRequest); 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.