mirror of
https://github.com/djdevin/recnet-plugin.git
synced 2026-09-08 14:41:30 -07:00
143 lines
8.8 KiB
Markdown
143 lines
8.8 KiB
Markdown
# 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:
|
|
|
|
```powershell
|
|
& "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):
|
|
|
|
```powershell
|
|
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.
|