27 Commits

Author SHA1 Message Date
Devin Zuczek 7c78a0187a Update license link 2026-06-30 11:01:31 -04:00
Devin Zuczek ff9749483a updating readme 2026-06-30 10:49:09 -04:00
Devin Zuczek 4b413728a7 add readme 2026-06-30 10:16:16 -04:00
Devin Zuczek c28ec48bbb skip logging some collection endpoints 2026-06-30 10:10:53 -04:00
Lapis e36f1e447e Merge pull request #2 from djdevin/2023-improve-logging 2026-06-29 16:25:26 -04:00
Lapis 733a13682d Merge pull request #3 from djdevin/2023-project-path-variables 2026-06-29 16:22:02 -04:00
Devin Zuczek 65a3560fc7 improve logging 2026-06-29 16:12:54 -04:00
Devin Zuczek 9148e7b6dd configurable rec room location 2026-06-29 16:05:59 -04:00
Lapis ede406e25b Merge pull request #1 from djdevin/2023-eac-account-creation 2026-06-29 15:33:05 -04:00
Devin Zuczek 35d0a4281a disable cheatmanager to preserve DUID function 2026-06-29 15:21:43 -04:00
Lapis 9bafbcd188 fix account creation... ooops... 2026-06-18 11:20:38 -04:00
Lapis f56ed0d8de fix room loading + metadata changes allowed 2026-06-18 09:22:48 -04:00
Lapis dcb7faec5d fix photon patches 2026-06-14 06:04:49 -04:00
Lapis 837543f5e2 add advanced settings 2026-06-04 18:58:56 -04:00
Lapis 6855abe9f6 kill cheatmanager on any scene 2026-06-04 18:49:44 -04:00
Lapis 9c6448a3f4 2023 eac patch 2026-06-03 18:07:50 -04:00
Lapis d0299ae50e fix: scene name 2026-06-03 15:05:15 -04:00
Lapis 095b199568 oops 2026-06-03 09:34:54 -04:00
Lapis 0e53f35fb6 fix cheatmanager destroy 2026-06-02 18:41:58 -04:00
Lapis e1459b2de2 fix: 2023 references 2026-06-02 18:13:57 -04:00
Lapis 2448d8daae chore: clean up project file 2026-05-21 15:50:50 -04:00
Lapis b4e83bddbf fix for VR 2026-04-05 14:49:32 -04:00
Lapis 5a8b4b2d52 kill cheatmanager, add configs 2026-04-03 13:39:51 -04:00
Lapis 78a9a2e824 stupid dumb photon patch 2026-04-02 14:10:24 -04:00
Lapis f70ed0c548 Merge branch 'main' of https://github.com/LapisGit/CannedNet.Client 2026-04-01 09:19:22 -04:00
Lapis a442466f98 h 2026-04-01 09:19:01 -04:00
Lapis 7c103ac7e1 Initial commit 2026-04-01 09:17:03 -04:00
13 changed files with 61 additions and 94 deletions
+7 -3
View File
@@ -1,3 +1,7 @@
GamePath.props bin/
obj obj/
bin /packages/
riderModule.iml
/_ReSharper.Caches/
/.idea
CannedNet.Client.sln.DotSettings.user
+16
View File
@@ -0,0 +1,16 @@
Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CannedNet.Client", "CannedNet.Client\CannedNet.Client.csproj", "{B6704474-3934-43AE-9E89-E5851F6A266B}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{B6704474-3934-43AE-9E89-E5851F6A266B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B6704474-3934-43AE-9E89-E5851F6A266B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B6704474-3934-43AE-9E89-E5851F6A266B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B6704474-3934-43AE-9E89-E5851F6A266B}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal
+1
View File
@@ -0,0 +1 @@
GamePath.props
@@ -2,8 +2,8 @@
<PropertyGroup> <PropertyGroup>
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net6.0</TargetFramework>
<AssemblyName>RecNetPlugin</AssemblyName> <AssemblyName>CannedNet.Client</AssemblyName>
<Product>RecNetPlugin</Product> <Product>My first plugin</Product>
<Version>1.0.0</Version> <Version>1.0.0</Version>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<LangVersion>latest</LangVersion> <LangVersion>latest</LangVersion>
@@ -12,7 +12,7 @@
https://nuget.bepinex.dev/v3/index.json; https://nuget.bepinex.dev/v3/index.json;
https://nuget.samboy.dev/v3/index.json https://nuget.samboy.dev/v3/index.json
</RestoreAdditionalProjectSources> </RestoreAdditionalProjectSources>
<RootNamespace>RecNetPlugin</RootNamespace> <RootNamespace>CannedNet.Client</RootNamespace>
</PropertyGroup> </PropertyGroup>
<!-- GamePath = root of a Rec Room install whose BepInEx/interop/ has been populated. <!-- GamePath = root of a Rec Room install whose BepInEx/interop/ has been populated.
@@ -3,7 +3,7 @@ using RecRoom.AntiCheat;
using System.Text; using System.Text;
using Il2CppSystem; using Il2CppSystem;
namespace RecNetPlugin.Patches; namespace CannedNet.Client.Patches;
[HarmonyPatch] [HarmonyPatch]
public static class EACPatches public static class EACPatches
@@ -23,7 +23,7 @@ public static class EACPatches
if (!string.IsNullOrEmpty(PGCINMIEBJP)) if (!string.IsNullOrEmpty(PGCINMIEBJP))
__result = Convert.ToBase64String(Encoding.UTF8.GetBytes(PGCINMIEBJP)); __result = Convert.ToBase64String(Encoding.UTF8.GetBytes(PGCINMIEBJP));
else else
__result = Convert.ToBase64String(Encoding.UTF8.GetBytes("nothing")); __result = Convert.ToBase64String(Encoding.UTF8.GetBytes("i hate this"));
return false; return false;
} }
} }
@@ -1,13 +1,9 @@
using HarmonyLib; using HarmonyLib;
using Org.BouncyCastle.Crypto.Tls; using Org.BouncyCastle.Crypto.Tls;
namespace RecNetPlugin.Patches; namespace CannedNet.Client.Patches;
/** public class FuckOffTLS
Disables TLS certificate pinning. Even though we connect over SSL it seems some certificates
might be pinned.
*/
public class DisableTLSPinning
{ {
[HarmonyPatch(typeof(LegacyTlsAuthentication), "NotifyServerCertificate")] [HarmonyPatch(typeof(LegacyTlsAuthentication), "NotifyServerCertificate")]
public class TlsPatch public class TlsPatch
@@ -17,4 +13,4 @@ public class DisableTLSPinning
return false; return false;
} }
} }
} }
@@ -1,18 +1,19 @@
using System;
using System.Reflection;
using ExitGames.Client.Photon; using ExitGames.Client.Photon;
using HarmonyLib; using HarmonyLib;
using Photon.Realtime; using Photon.Realtime;
namespace RecNetPlugin.Patches; namespace CannedNet.Client.Patches;
/**
Patches Photon to use the App IDs and server hostname/port specified in the plugin config.
*/
[HarmonyPatch(typeof(GPFPFDBGCEK), "AMOHMPKKGHL")] [HarmonyPatch(typeof(GPFPFDBGCEK), "AMOHMPKKGHL")]
public class PhotonPatches public class PhotonPatches
{ {
[HarmonyPostfix] [HarmonyPostfix]
private static void Postfix(ref AppSettings __result) private static void Postfix(ref AppSettings __result)
{ {
Plugin.Log.LogInfo("okay im patching now");
if (__result != null) if (__result != null)
{ {
__result.AppIdRealtime = Plugin.AppIdRT.Value; __result.AppIdRealtime = Plugin.AppIdRT.Value;
@@ -1,11 +1,5 @@
using HarmonyLib; using HarmonyLib;
namespace RecNetPlugin.Patches;
/**
* This allows the global-metadata.dat to be different on the client
* patched to allow a different modulus so we can sign images.
*/
[HarmonyPatch(typeof(JAPJPGNBMNM), "JOKECJKBJGD")] [HarmonyPatch(typeof(JAPJPGNBMNM), "JOKECJKBJGD")]
public static class PromisePatch public static class PromisePatch
{ {
@@ -3,7 +3,7 @@ using BestHTTP;
using HarmonyLib; using HarmonyLib;
using Il2CppInterop.Runtime; using Il2CppInterop.Runtime;
namespace RecNetPlugin.Patches; namespace CannedNet.Client.Patches;
/** /**
Intercept a variety of HTTP requests and rewrite them to point to our own custom server. Intercept a variety of HTTP requests and rewrite them to point to our own custom server.
@@ -20,7 +20,6 @@ public class SendRequestPatch
"/data/heartbeat", "/data/heartbeat",
"/identify", "/identify",
"/httpapi", "/httpapi",
"/data/event",
}; };
private static bool IsIgnoredForLogging(string url) private static bool IsIgnoredForLogging(string url)
@@ -31,16 +30,6 @@ public class SendRequestPatch
return false; return false;
} }
// Cap logged bodies so a large response/request doesn't flood the log.
private const int MaxLoggedBodyLength = 1000;
private static string Truncate(string s)
{
if (string.IsNullOrEmpty(s) || s.Length <= MaxLoggedBodyLength)
return s;
return s.Substring(0, MaxLoggedBodyLength) + $"... <truncated {s.Length - MaxLoggedBodyLength} chars>";
}
[HarmonyPatch(typeof(HTTPManager), "SendRequest", [typeof(HTTPRequest)])] [HarmonyPatch(typeof(HTTPManager), "SendRequest", [typeof(HTTPRequest)])]
public class ConnectToRecNetPatch public class ConnectToRecNetPatch
{ {
@@ -55,10 +44,10 @@ public class SendRequestPatch
if (entityBody == null) if (entityBody == null)
body = "<none>"; body = "<none>";
else if (IsBinaryContentType(request.GetFirstHeaderValue("content-type")) || LooksBinary(entityBody)) else if (IsBinaryContentType(request.GetFirstHeaderValue("content-type")) || LooksBinary(entityBody))
body = BinaryPreview(entityBody); body = "<binary>";
else else
body = System.Text.Encoding.UTF8.GetString(entityBody); body = System.Text.Encoding.UTF8.GetString(entityBody);
Plugin.Log.LogInfo($"[HTTP] {request.MethodType} {request.Uri.AbsoluteUri} body={Truncate(body)}"); Plugin.Log.LogInfo($"[HTTP] {request.MethodType} {request.Uri.AbsoluteUri} body={body}");
} }
var host = request.Uri.Host; var host = request.Uri.Host;
@@ -103,7 +92,7 @@ public class SendRequestPatch
text = resp.DataAsText; text = resp.DataAsText;
if (string.IsNullOrEmpty(text)) text = "<empty>"; if (string.IsNullOrEmpty(text)) text = "<empty>";
} }
var msg = $"[HTTP] <- {resp.StatusCode} {url} body={Truncate(text)}"; var msg = $"[HTTP] <- {resp.StatusCode} {url} body={text}";
if (resp.StatusCode is >= 200 and < 300) if (resp.StatusCode is >= 200 and < 300)
Plugin.Log.LogInfo(msg); Plugin.Log.LogInfo(msg);
else else
@@ -139,31 +128,6 @@ public class SendRequestPatch
return true; return true;
} }
// Render the leading bytes of a binary body as text so structured framing (e.g. multipart form
// boundaries and part headers) stays readable, while raw bytes are shown as \xNN escapes. Capped
// at MaxLoggedBodyLength since the interesting framing is at the front.
private static string BinaryPreview(byte[] data)
{
if (data.Length == 0) return "<binary empty>";
var sb = new System.Text.StringBuilder(MaxLoggedBodyLength + 32);
sb.Append("<binary ").Append(data.Length).Append(" bytes> ");
var i = 0;
// Cap on rendered length, not byte count: escapes expand a byte to 4 chars, so this keeps the
// preview near MaxLoggedBodyLength and avoids a second pass by Truncate at the log site.
for (; i < data.Length && sb.Length < MaxLoggedBodyLength; i++)
{
var b = data[i];
if (b == 0x09 || b == 0x0A || b == 0x0D || (b >= 0x20 && b < 0x7F))
sb.Append((char)b);
else
sb.Append("\\x").Append(b.ToString("x2"));
}
if (i < data.Length)
sb.Append($"... <truncated {data.Length - i} bytes>");
return sb.ToString();
}
// Content sniff for raw request bytes — the Content-Type header isn't reliably set at // Content sniff for raw request bytes — the Content-Type header isn't reliably set at
// SendRequest time (e.g. multipart form bodies set it lazily, and the body still embeds the // SendRequest time (e.g. multipart form bodies set it lazily, and the body still embeds the
// raw image), so look at the bytes: a NUL byte, or a high ratio of non-text control bytes in // raw image), so look at the bytes: a NUL byte, or a high ratio of non-text control bytes in
+8 -3
View File
@@ -1,15 +1,20 @@
using System; using System;
using System.Collections;
using System.Text.Json;
using BepInEx; using BepInEx;
using BepInEx.Configuration; using BepInEx.Configuration;
using BepInEx.Logging; using BepInEx.Logging;
using BepInEx.Unity.IL2CPP; using BepInEx.Unity.IL2CPP;
using BepInEx.Unity.IL2CPP.Utils.Collections;
using HarmonyLib; using HarmonyLib;
using UnityEngine; using UnityEngine;
using UnityEngine.Events;
using UnityEngine.SceneManagement; using UnityEngine.SceneManagement;
using UnityEngine.Networking;
namespace RecNetPlugin; namespace CannedNet.Client;
[BepInPlugin("net.rec.plugin", "RecNet Plugin", "1.0.0")] [BepInPlugin("lapis.cannednet.client", "CannedNet Client", "1.0.0")]
public class Plugin : BasePlugin public class Plugin : BasePlugin
{ {
internal static new ManualLogSource Log; internal static new ManualLogSource Log;
@@ -33,7 +38,7 @@ public class Plugin : BasePlugin
EnableAdvancedSettings = Config.Bind("Advanced", "Enabled Advanced Settings", false, "Allows other fields below in the advanced section to be modified."); EnableAdvancedSettings = Config.Bind("Advanced", "Enabled Advanced Settings", false, "Allows other fields below in the advanced section to be modified.");
PhotonHostname = Config.Bind("Advanced", "Photon NameServer", "", "Custom Photon NameServer"); PhotonHostname = Config.Bind("Advanced", "Photon NameServer", "", "Custom Photon NameServer");
PhotonPort = Config.Bind("Advanced", "Photon NameServer Port", 0, "Custom Photon NameServer Port (if 0, it will be default)"); PhotonPort = Config.Bind("Advanced", "Photon NameServer Port", 0, "Custom Photon NameServer Port (if 0, it will be default)");
ServerHostname = Config.Bind("Server", "RecNet NameServer Host", "https://ns.rec.net", "Host for the RecNet NameServer."); ServerHostname = Config.Bind("Server", "RecNet NameServer Host", "https://ns.lapis.codes", "Host for the RecNet NameServer.");
Debug = Config.Bind("Advanced", "Debug", false, "Show debug logs (HTTP tracing, etc. WARNING: will include sensitive information such as passwords and auth tokens in the logs, be careful when sharing them!)"); Debug = Config.Bind("Advanced", "Debug", false, "Show debug logs (HTTP tracing, etc. WARNING: will include sensitive information such as passwords and auth tokens in the logs, be careful when sharing them!)");
Harmony.CreateAndPatchAll(typeof(Plugin).Assembly); Harmony.CreateAndPatchAll(typeof(Plugin).Assembly);
+1 -1
View File
@@ -1,6 +1,6 @@
MIT License MIT License
Copyright (c) 2026 djdevin Copyright (c) 2026 Lapis
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
+11 -25
View File
@@ -1,18 +1,10 @@
# RecNet Plugin # CannedNet Client
A [BepInEx 6](https://github.com/BepInEx/BepInEx) (IL2CPP) plugin that points the Rec Room client at a self-hosted / private server. A [BepInEx 6](https://github.com/BepInEx/BepInEx) (IL2CPP) plugin that points the **Rec Room** client at a self-hosted / private "CannedNet" server instead of the official Rec Room backend.
It does this entirely client-side with [Harmony](https://harmony.pardeike.net/) patches — no game files are modified on disk (except global-metadata.dat - needed for image signatures). The plugin rewrites the RecNet name-server lookups, swaps in your own Photon credentials, and disables the client-side guards (EasyAntiCheat, TLS certificate pinning) that would otherwise reject a non-official server. It does this entirely client-side with [Harmony](https://harmony.pardeike.net/) patches — no game files are modified on disk. The plugin rewrites the RecNet name-server lookups, swaps in your own Photon credentials, and disables the client-side guards (EasyAntiCheat, TLS certificate pinning) that would otherwise reject a non-official server.
> ⚠️ This disables anti-cheat and certificate validation on the client. Use at your own risk. > ⚠️ **For private/experimental servers only.** This redirects traffic away from official Rec Room infrastructure and disables anti-cheat and certificate validation on the client. Do not use it against `rec.net` or any service you don't control. Use at your own risk.
## Safety
Using BepInEx plugins may cause anti-virus scanners or Windows Defender to pick it up as a threat.
If you don't trust the complied .DLL, you can build it yourself.
See https://github.com/djdevin/recnet-plugin#from-source
## What it does ## What it does
@@ -31,16 +23,14 @@ See https://github.com/djdevin/recnet-plugin#from-source
- **.NET 6 SDK** to build the plugin. - **.NET 6 SDK** to build the plugin.
- Your own server endpoints: a RecNet name server, and [Photon](https://www.photonengine.com) app keys. - Your own server endpoints: a RecNet name server, and [Photon](https://www.photonengine.com) app keys.
_Looking for a custom RecNet server?_ Try https://github.com/djdevin/recflare _Looking for a custom RecNet server?_ Try https://github.com/CannedNet/CannedNet.Client
## Installing ## Installing
1. Download the game using https://github.com/SteamRE/DepotDownloader. The manifest ID is `7859140924515540835`. 1. Download the game using https://github.com/SteamRE/DepotDownloader. The manifest ID is `7859140924515540835`.
Example: `depotdownloader -app 471710 -depot 471711 -manifest 7859140924515540835` Example: `depotdownloader -app 471710 -depot 471711 -manifest 7859140924515540835`
**You must use this specific version.** **You must use this specific version.**
3. Install BepInEx to the game. See https://docs.bepinex.dev/articles/user_guide/installation/index.html. **Note that you must use version 6!** 2. Install BepInEx to the game. See https://docs.bepinex.dev/articles/user_guide/installation/index.html. **Note that you must use version 6!**
Alternatively, use the [RecFlare client](https://github.com/djdevin/recflare-client)
### From release ### From release
@@ -76,7 +66,7 @@ dotnet build
The build validates that `GamePath` is set and that `$(GamePath)\BepInEx\interop` exists, and fails with a clear message otherwise. The build validates that `GamePath` is set and that `$(GamePath)\BepInEx\interop` exists, and fails with a clear message otherwise.
A post-build step (the `DeployPlugin` target in the `.csproj`) automatically copies the built `RecNetPatcher.dll` into your Rec Room install's `BepInEx/plugins/` folder after every build. Since `GamePath` already points at your install, you don't need to copy anything by hand — just `dotnet build` and launch the game. A post-build step (the `DeployPlugin` target in the `.csproj`) automatically copies the built `CannedNet.Client.dll` into your Rec Room install's `BepInEx/plugins/` folder after every build. Since `GamePath` already points at your install, you don't need to copy anything by hand — just `dotnet build` and launch the game.
If you need the DLL elsewhere, it's also left in `bin/Debug/net6.0/`. If you need the DLL elsewhere, it's also left in `bin/Debug/net6.0/`.
@@ -84,10 +74,10 @@ If you need the DLL elsewhere, it's also left in `bin/Debug/net6.0/`.
Start the game for the first time. In `BepInEx` you should now see a `config` folder. If not, verify BepInEx installation and version. Start the game for the first time. In `BepInEx` you should now see a `config` folder. If not, verify BepInEx installation and version.
Inside `config`, edit the `net.rec.plugin.cfg` file and update as needed: Inside `config`, edit the `lapis.cannednet.client.cfg` file and update as needed:
**[Server]** **[Server]**
- `RecNet NameServer Host` — base URL of your RecNet name server (like `https://ns.rec.net`). - `RecNet NameServer Host` — base URL of your RecNet name server (default `https://ns.lapis.codes`).
**[Photon]** **[Photon]**
- `App Id Realtime` — Photon Realtime App ID. - `App Id Realtime` — Photon Realtime App ID.
@@ -106,8 +96,8 @@ Inside `config`, edit the `net.rec.plugin.cfg` file and update as needed:
| Path | Purpose | | Path | Purpose |
| --- | --- | | --- | --- |
| `Plugin.cs` | Plugin entry point, config bindings, Harmony bootstrap | | `Plugin.cs` | Plugin entry point, config bindings, Harmony bootstrap |
| `Patches/` | Harmony patches (HTTP, EAC, TLS, Photon) | | `Patches/` | Harmony patches (networking, EAC, TLS, Photon) |
| `RecNetPlugin.csproj` | Build config + interop references (driven by `GamePath`), and the `DeployPlugin` post-build copy | | `CannedNet.Client.csproj` | Build config + interop references (driven by `GamePath`), and the `DeployPlugin` post-build copy |
| `GamePath.props.example` | Template for your local `GamePath.props` | | `GamePath.props.example` | Template for your local `GamePath.props` |
## FAQ ## FAQ
@@ -116,10 +106,6 @@ Inside `config`, edit the `net.rec.plugin.cfg` file and update as needed:
Yes. That's the point. Yes. That's the point.
## Credits
Based on https://github.com/CannedNet/CannedNet.Client
## License ## License
[MIT](LICENSE) [MIT](LICENSE)