mirror of
https://github.com/DorkSquadRR/DorkNet.git
synced 2026-10-26 15:38:45 -07:00
feat: DorkNet.Quest, a sideload patcher for the 2023 Quest client
Patches the Quest build of Rec Room 2023.03.21 (20230317 / 2128210, the
same appVersion as the PC client) to connect to a DorkNet server, using
one native hook library loaded via DT_NEEDED on libmain.so.
- host rewrite onto <svc>.<ServerHost> or a {service}-dev.example.com template
- Photon app ids and region injection (PhotonRegion must match PC)
- TLS / pinning / image-signature bypass, integrity callbacks neutralised
- no-Meta-account build; the server treats platform id 0/1/blank as a
placeholder and keys those accounts by device id
- build.ps1 -Obb packages a DorkNet-<ver>.xapk with one-command
installers for Windows and macOS/Linux; app is named via AppName
- README, docs/quest.md and NOTICE updated; credits RecFlare and
And64InlineHook (MIT)
Tested on a Quest 2: all hooks arm, requests reach the dev server.
This commit is contained in:
@@ -0,0 +1,215 @@
|
||||
# DorkNet.Quest
|
||||
|
||||
Sideload-patches the **Meta Quest** build of Rec Room **2023.03.21**
|
||||
(versionName `20230317`, versionCode `2128210`) to connect to a DorkNet server.
|
||||
|
||||
It is the Quest counterpart of `DorkNet.ClientMod` (the PC MelonLoader mod) plus
|
||||
`tools/patch-client.ps1` (the PC Photon byte-patch). Android/IL2CPP has no
|
||||
MelonLoader, so everything is done in **one injected native library**
|
||||
(`libdorknet.so`): it is added as a `DT_NEEDED` entry on `libmain.so` so the
|
||||
loader maps it before Unity, then it installs arm64 inline hooks at addresses
|
||||
resolved from the build's own `dump.cs`.
|
||||
|
||||
This build reports appVersion **`20230317`** — the *same* version the PC client
|
||||
reports — so a Quest player patched with this tool shares rooms, presence and
|
||||
Photon shards with PC DorkNet players with no server-side version change.
|
||||
|
||||
> This ships **no** Rec Room code or assets. You supply your own extracted store
|
||||
> APK and OBB. It disables the client's certificate pinning and image-signature
|
||||
> checks so a self-hosted server works; do not redistribute game binaries.
|
||||
|
||||
## What it does
|
||||
|
||||
| Hook | Effect | PC equivalent |
|
||||
| --- | --- | --- |
|
||||
| `HTTPManager.SendRequest` | rewrite `<svc>.rec.net` → `<svc>.<ServerHost>`, or into a `{service}` template such as `{service}-dev.example.com` for hyphen-style hosts (the name server supplies every other URL) | `ClientMod` UriPatches |
|
||||
| Photon `AppSettings` factory | inject the operator's Realtime/Voice/Chat app ids and fixed region (`PhotonRegion`, must match the PC clients) | `tools/patch-client.ps1` |
|
||||
| `PlatformSettings.get_AppID` / `get_MobileAppID` | return the operator's Meta app id (only when set) | — |
|
||||
| `UnityTlsContext.VerifyCallback` + BouncyCastle `NotifyServerCertificate` | accept the self-hosted TLS chain | `ClientMod` TlsPatches |
|
||||
| three certificate-pin verifiers | accept the chain (pins never match a self-hosted host) | `ClientMod` DisableTLSPinning |
|
||||
| image RSA/SHA1 verify | accept Content-Signatures the server signs with its own key | PC ImageSigning |
|
||||
| hile-warning / unknown-DLL / file-hash / ToxMod callbacks | neutralised so a re-signed, hook-loaded client is not warned/quit | `ClientMod` AntiTamper/ToxMod/FileHash |
|
||||
| integrity report + quit funnels | **log only** — names any new rejection in logcat | — |
|
||||
| Oculus `ovr_*` platform calls | (no-Meta build only) steer the entitlement/login path to the client's own credential screen | — |
|
||||
|
||||
### Two builds, selected by `OculusAppId`
|
||||
|
||||
- **Set** — the *Meta build*: Meta's real entitlement + login run against the
|
||||
app you registered; the platform hooks stay disarmed. Players need a Meta
|
||||
account, and your package + signing cert must be registered with that app.
|
||||
- **Blank** — the *no-Meta-account build*: the platform path is steered past
|
||||
Meta's checks to Rec Room's own username/password screen. It authenticates
|
||||
nothing — every headset sends the placeholder platform user id `1`. DorkNet's
|
||||
server treats platform id `1` (and `0`/blank) as a non-identity and keys
|
||||
accounts by `deviceId` instead (`PlayerService.IsPlaceholderPlatformId`), so
|
||||
players are **not** merged into one account and are never offered each other's
|
||||
"remembered logins". **Never** add a server rule that accepts platform id `1`
|
||||
as an identity.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **Android NDK** r27c-class (`ANDROID_NDK`, or `ndk/<ver>` under
|
||||
`ANDROID_SDK_ROOT`) — compiles the hook library against bionic.
|
||||
- **Android SDK build-tools** (`zipalign`, `apksigner`) — `ANDROID_SDK_ROOT`.
|
||||
- **apktool** `apktool.jar` (`APKTOOL_JAR`, or next to `ANDROID_SDK_ROOT`).
|
||||
- **JDK** (`keytool` for the debug keystore; apksigner needs a JRE).
|
||||
- **Python 3** with **LIEF** (`pip install lief`) — the `DT_NEEDED` injection.
|
||||
- **[Il2CppDumper](https://github.com/Perfare/Il2CppDumper)** — to produce
|
||||
`dump.cs` for the target APK (see below).
|
||||
- Your own **store APK** (`com.AgainstGravity.RecRoom`, versionCode `2128210`)
|
||||
and its matching **OBB**.
|
||||
|
||||
> On Windows with Controlled Folder Access on, the NDK linker and apksigner
|
||||
> can't write under `Documents`. `build.ps1` builds in `%TEMP%` by default;
|
||||
> keep the work dir outside protected folders.
|
||||
|
||||
## Build
|
||||
|
||||
```powershell
|
||||
# 1. Dump the target build (Il2CppDumper), once, into a scratch dir:
|
||||
dotnet Il2CppDumper.dll `
|
||||
<extracted>\lib\arm64-v8a\libil2cpp.so `
|
||||
<extracted>\assets\bin\Data\Managed\Metadata\global-metadata.dat `
|
||||
<scratch>\dump
|
||||
|
||||
# 2. Configure:
|
||||
copy DorkNet.Quest\dorknet-quest.cfg.example DorkNet.Quest\dorknet-quest.cfg
|
||||
# edit ServerHost, Photon app ids, (optional) OculusAppId, PackageName
|
||||
|
||||
# 3. Build the signed APK:
|
||||
$env:ANDROID_SDK_ROOT = 'C:\path\to\android-sdk'
|
||||
$env:ANDROID_NDK = 'C:\path\to\android-sdk\ndk\27.2.12479018'
|
||||
$env:APKTOOL_JAR = 'C:\path\to\apktool.jar'
|
||||
DorkNet.Quest\scripts\build.ps1 `
|
||||
-Apk C:\path\to\RecRoom-store-2128210.apk `
|
||||
-DumpCs <scratch>\dump\dump.cs `
|
||||
-Obb C:\path o\main.2128210.com.AgainstGravity.RecRoom.obb
|
||||
# -> %TEMP%\dorknet-quest-build\dorknet-recroom-20230317.apk
|
||||
# -> %TEMP%\dorknet-quest-build\DorkNet-Quest-20230317\ (with -Obb)
|
||||
```
|
||||
|
||||
With `-Obb`, the build also runs `scripts/package.ps1`, which writes a release
|
||||
folder ready to hand to players:
|
||||
|
||||
| File | Purpose |
|
||||
| --- | --- |
|
||||
| `DorkNet-20230317.xapk` | standard XAPK: `manifest.json`, the APK, the OBB at its install path, `icon.png` |
|
||||
| `install.cmd` / `install.ps1` | one-command install on Windows (double-click `install.cmd`) |
|
||||
| `install.sh` | one-command install on macOS / Linux |
|
||||
| `README.txt` | player instructions |
|
||||
|
||||
To package an APK you already built, run `scripts/package.ps1 -Apk <apk> -Obb <obb>`.
|
||||
The package contains game binaries from your store copy, so only share it with
|
||||
people who own the game.
|
||||
|
||||
`build.ps1` chains: resolve targets → generate headers → compile
|
||||
`libdorknet.so` → apktool decode → package rename + LAUNCHER + manifest →
|
||||
app name (`AppName`) → drop the library in + `DT_NEEDED` it onto `libmain.so` → apktool build →
|
||||
zipalign → apksigner (v2+v3). It generates a fresh debug keystore in the work
|
||||
dir on first run. **Keep that keystore** — Meta ties tokens to app id + package
|
||||
+ signing cert, so re-signing with a different key breaks any Meta registration.
|
||||
It is never committed.
|
||||
|
||||
### Rebasing onto a different build
|
||||
|
||||
Obfuscated Assembly-CSharp names are re-randomised each release but shared with
|
||||
the PC build of the same release, so the PC decompile is the oracle. For a new
|
||||
build:
|
||||
|
||||
1. Dump it with Il2CppDumper.
|
||||
2. Re-run `scripts/resolve_targets.py --dump dump.cs --il2cpp libil2cpp.so
|
||||
--version-name <v> --version-code <c> --objdump <ndk>/llvm-objdump` and
|
||||
confirm every target resolves, is ≥ 20 bytes, and has a real frame-setup
|
||||
prologue. Adjust `targets/spec.json` signatures if a name/shape changed
|
||||
(cross-check against the PC decompile first).
|
||||
3. Commit the new `targets/<v>-<c>.json`.
|
||||
|
||||
The library refuses to arm any target whose 20-byte fingerprint does not match
|
||||
the running `libil2cpp.so`, so a table can never be applied to the wrong build.
|
||||
|
||||
## Install
|
||||
|
||||
From a release folder, one command does everything (finds adb and the headset,
|
||||
installs the APK, copies the OBB to the right path, skips the OBB if it is
|
||||
already there, and launches the app):
|
||||
|
||||
```powershell
|
||||
.\install.cmd # Windows (or double-click it)
|
||||
./install.sh # macOS / Linux
|
||||
```
|
||||
|
||||
Both take an optional package path, `-Serial`/`--serial` for a specific headset
|
||||
and `-NoLaunch`/`--no-launch`. SideQuest and other XAPK installers can open the
|
||||
`.xapk` directly. The app shows up as `AppName` (default **DorkNet**) under
|
||||
Library > Unknown Sources.
|
||||
|
||||
By hand:
|
||||
|
||||
```sh
|
||||
PKG=net.dorknet.RecRoom # your PackageName
|
||||
VC=2128210
|
||||
adb uninstall $PKG 2>/dev/null
|
||||
adb install -r dorknet-recroom-20230317.apk
|
||||
adb shell mkdir -p /sdcard/Android/obb/$PKG
|
||||
adb push main.2128210.com.AgainstGravity.RecRoom.obb \
|
||||
/sdcard/Android/obb/$PKG/main.$VC.$PKG.obb
|
||||
```
|
||||
|
||||
The on-device OBB filename **must** be `main.<versionCode>.<package>.obb` —
|
||||
Unity rebuilds that exact path at runtime, so the source file's own name is
|
||||
irrelevant, only the destination is.
|
||||
|
||||
## Check it on the headset
|
||||
|
||||
**Wear the headset** — off-head it sleeps within seconds and never finishes
|
||||
init, which looks like a crash.
|
||||
|
||||
```sh
|
||||
adb logcat -c
|
||||
adb shell am start -n net.dorknet.RecRoom/com.unity3d.player.UnityPlayerActivity
|
||||
adb logcat -d -s DORKNET
|
||||
```
|
||||
|
||||
A healthy boot logs: `DorkNet.Quest hooks loaded`, `init: build 20230317/...`,
|
||||
every `armed <target>`, then `intercepted https://ns.rec.net/ -> https://ns.<host>/`,
|
||||
`unitytls verify: forcing 0x8 -> SUCCESS`, `[http<-] 200 ...`, and
|
||||
`photon appsettings: RT=... Voice=... Chat=...`.
|
||||
|
||||
| Symptom | Likely cause |
|
||||
| --- | --- |
|
||||
| `REFUSING <target>: ... fingerprint mismatch` | wrong APK for this target table — rebuild from the matching store APK, or rebase |
|
||||
| install fails `-124` / `NO_CERTIFICATES` | signing/align step — rebuild with `build.ps1`, don't hand-zip |
|
||||
| boots to an empty world | OBB missing or named for the wrong versionCode |
|
||||
| `[integrity] report reason=... NetworkCertificateMismatch` then a quit | TLS bypass off, or a pin hook didn't arm |
|
||||
| `[http<-]` shows a Cloudflare challenge page | edge WAF blocked the request before your origin |
|
||||
| multiplayer never connects | Photon app ids not set in the cfg |
|
||||
| Quest and PC players never see each other's rooms | `PhotonRegion` differs from the PC ClientMod `PhotonCloudRegion` |
|
||||
|
||||
## Layout
|
||||
|
||||
| Path | Purpose |
|
||||
| --- | --- |
|
||||
| `src/dorknet_quest.cpp` | all hooks + init |
|
||||
| `src/config.h` / `config.cpp` | compile-time defaults + runtime cfg loader |
|
||||
| `src/third_party/And64InlineHook/` | vendored arm64 inline hooker (MIT, © Rprop) |
|
||||
| `targets/spec.json` | target signatures (class header + method), build-independent |
|
||||
| `targets/<ver>-<code>.json` | resolved RVAs + fingerprints for one build |
|
||||
| `scripts/resolve_targets.py` | dump.cs + libil2cpp.so → target table (+ prologue check) |
|
||||
| `scripts/gen_targets_header.py` | target table → `targets_generated.h` |
|
||||
| `scripts/gen_config_header.py` | `dorknet-quest.cfg` → `config_generated.h` |
|
||||
| `scripts/inject_needed.py` | LIEF `DT_NEEDED` injection |
|
||||
| `scripts/build.ps1` | one-shot: store APK → signed patched APK (+ release package with `-Obb`) |
|
||||
| `scripts/package.ps1` | patched APK + OBB → `.xapk` + installers |
|
||||
| `scripts/installer/` | `install.ps1`, `install.cmd`, `install.sh` copied into each package |
|
||||
| `dorknet-quest.cfg.example` | config template; copy to `dorknet-quest.cfg` (git-ignored) |
|
||||
|
||||
## Credits
|
||||
|
||||
- [RecFlare](https://github.com/recflare) by djdevin. Their Quest client
|
||||
showed the way this patcher is built: one native hook library loaded
|
||||
through a `DT_NEEDED` entry on `libmain.so`, and which client checks need
|
||||
hooking. They target a newer build (20230414), so every hook target here
|
||||
was re-derived from the 20230317 build; no RecFlare code is included.
|
||||
- [And64InlineHook](https://github.com/Rprop/And64InlineHook) © Rprop (MIT) —
|
||||
vendored under `src/third_party/`.
|
||||
- [Il2CppDumper](https://github.com/Perfare/Il2CppDumper), [LIEF](https://lief.re).
|
||||
Reference in New Issue
Block a user