mirror of
https://github.com/DorkSquadRR/DorkNet.git
synced 2026-10-25 23:18:44 -07:00
a7879d7ede
A Quest page on the public site (/quest) installs a DorkNet .xapk from Chrome or Edge over WebUSB, with no adb, SideQuest or scripts: pick the .xapk, connect the headset, install. It installs the APK, copies the OBB to its install path (skipped when an identical one is already there) and launches the app, with progress bars and plain-language errors for an unsupported browser, a headset held by adb/SideQuest, a disconnect and a copy signed with a different key. The .xapk is read from the player's own disk: the APK and OBB are stored uncompressed, so they stream straight out of the file without loading 1.4 GB into memory, and the server never hosts game files. Uses Tango (@yume-chan/adb 2.6.4 and friends, MIT), pinned and bundled; the page is lazy-loaded so the rest of the site doesn't grow. Credited in NOTICE; README, DorkNet.Quest/README.md, docs/quest.md and the packaged README.txt point players at the page.
227 lines
12 KiB
Markdown
227 lines
12 KiB
Markdown
# 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 the browser (easiest).** Open the **Quest** page of your DorkNet site
|
|
(`https://<apex>/quest`) in Chrome or Edge on a computer, plug the headset in
|
|
over USB, pick the `.xapk`, click **Connect**, accept "Allow USB debugging" in
|
|
the headset, then **Install**. The page talks to the headset over WebUSB
|
|
(Tango, `@yume-chan/adb`): it installs the APK, copies the OBB to its install
|
|
path (skipped if an identical one is already there) and launches the app. The
|
|
`.xapk` never leaves the player's computer; the server hosts no game files.
|
|
Close SideQuest / Meta Quest Developer Hub, or run `adb kill-server`, first:
|
|
only one program can hold the headset's USB connection. Firefox, Safari and
|
|
phones don't support WebUSB.
|
|
|
|
**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).
|