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 60 additions and 91 deletions
+7 -3
View File
@@ -1,3 +1,7 @@
GamePath.props
obj
bin
bin/
obj/
/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>
<TargetFramework>net6.0</TargetFramework>
<AssemblyName>RecNetPlugin</AssemblyName>
<Product>RecNetPlugin</Product>
<AssemblyName>CannedNet.Client</AssemblyName>
<Product>My first plugin</Product>
<Version>1.0.0</Version>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<LangVersion>latest</LangVersion>
@@ -12,7 +12,7 @@
https://nuget.bepinex.dev/v3/index.json;
https://nuget.samboy.dev/v3/index.json
</RestoreAdditionalProjectSources>
<RootNamespace>RecNetPlugin</RootNamespace>
<RootNamespace>CannedNet.Client</RootNamespace>
</PropertyGroup>
<!-- GamePath = root of a Rec Room install whose BepInEx/interop/ has been populated.
@@ -3,7 +3,7 @@ using RecRoom.AntiCheat;
using System.Text;
using Il2CppSystem;
namespace RecNetPlugin.Patches;
namespace CannedNet.Client.Patches;
[HarmonyPatch]
public static class EACPatches
@@ -23,7 +23,7 @@ public static class EACPatches
if (!string.IsNullOrEmpty(PGCINMIEBJP))
__result = Convert.ToBase64String(Encoding.UTF8.GetBytes(PGCINMIEBJP));
else
__result = Convert.ToBase64String(Encoding.UTF8.GetBytes("nothing"));
__result = Convert.ToBase64String(Encoding.UTF8.GetBytes("i hate this"));
return false;
}
}
@@ -1,13 +1,9 @@
using HarmonyLib;
using Org.BouncyCastle.Crypto.Tls;
namespace RecNetPlugin.Patches;
namespace CannedNet.Client.Patches;
/**
Disables TLS certificate pinning. Even though we connect over SSL it seems some certificates
might be pinned.
*/
public class DisableTLSPinning
public class FuckOffTLS
{
[HarmonyPatch(typeof(LegacyTlsAuthentication), "NotifyServerCertificate")]
public class TlsPatch
@@ -17,4 +13,4 @@ public class DisableTLSPinning
return false;
}
}
}
}
@@ -1,18 +1,19 @@
using System;
using System.Reflection;
using ExitGames.Client.Photon;
using HarmonyLib;
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")]
public class PhotonPatches
{
[HarmonyPostfix]
private static void Postfix(ref AppSettings __result)
{
Plugin.Log.LogInfo("okay im patching now");
if (__result != null)
{
__result.AppIdRealtime = Plugin.AppIdRT.Value;
@@ -1,11 +1,5 @@
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")]
public static class PromisePatch
{
@@ -3,7 +3,7 @@ using BestHTTP;
using HarmonyLib;
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.
@@ -20,7 +20,6 @@ public class SendRequestPatch
"/data/heartbeat",
"/identify",
"/httpapi",
"/data/event",
};
private static bool IsIgnoredForLogging(string url)
@@ -31,16 +30,6 @@ public class SendRequestPatch
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)])]
public class ConnectToRecNetPatch
{
@@ -55,10 +44,10 @@ public class SendRequestPatch
if (entityBody == null)
body = "<none>";
else if (IsBinaryContentType(request.GetFirstHeaderValue("content-type")) || LooksBinary(entityBody))
body = BinaryPreview(entityBody);
body = "<binary>";
else
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;
@@ -103,7 +92,7 @@ public class SendRequestPatch
text = resp.DataAsText;
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)
Plugin.Log.LogInfo(msg);
else
@@ -139,31 +128,6 @@ public class SendRequestPatch
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
// 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
+8 -3
View File
@@ -1,15 +1,20 @@
using System;
using System.Collections;
using System.Text.Json;
using BepInEx;
using BepInEx.Configuration;
using BepInEx.Logging;
using BepInEx.Unity.IL2CPP;
using BepInEx.Unity.IL2CPP.Utils.Collections;
using HarmonyLib;
using UnityEngine;
using UnityEngine.Events;
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
{
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.");
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)");
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!)");
Harmony.CreateAndPatchAll(typeof(Plugin).Assembly);
+1 -1
View File
@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2026 djdevin
Copyright (c) 2026 Lapis
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
+10 -22
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.
## 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
> ⚠️ **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.
## What it does
@@ -31,7 +23,7 @@ See https://github.com/djdevin/recnet-plugin#from-source
- **.NET 6 SDK** to build the plugin.
- 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
@@ -74,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.
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/`.
@@ -82,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.
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]**
- `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]**
- `App Id Realtime` — Photon Realtime App ID.
@@ -104,8 +96,8 @@ Inside `config`, edit the `net.rec.plugin.cfg` file and update as needed:
| Path | Purpose |
| --- | --- |
| `Plugin.cs` | Plugin entry point, config bindings, Harmony bootstrap |
| `Patches/` | Harmony patches (HTTP, EAC, TLS, Photon) |
| `RecNetPlugin.csproj` | Build config + interop references (driven by `GamePath`), and the `DeployPlugin` post-build copy |
| `Patches/` | Harmony patches (networking, EAC, TLS, Photon) |
| `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` |
## FAQ
@@ -114,10 +106,6 @@ Inside `config`, edit the `net.rec.plugin.cfg` file and update as needed:
Yes. That's the point.
## Credits
Based on https://github.com/CannedNet/CannedNet.Client
## License
[MIT](LICENSE)