1 Commits

Author SHA1 Message Date
Lapis b29da5b5f0 Initial RecNetPlugin 2026-07-08 16:50:43 -04:00
13 changed files with 91 additions and 60 deletions
+3 -7
View File
@@ -1,7 +1,3 @@
bin/
obj/
/packages/
riderModule.iml
/_ReSharper.Caches/
/.idea
CannedNet.Client.sln.DotSettings.user
GamePath.props
obj
bin
-16
View File
@@ -1,16 +0,0 @@
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
@@ -1 +0,0 @@
GamePath.props
+1 -1
View File
@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2026 Lapis
Copyright (c) 2026 djdevin
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
@@ -1,9 +1,13 @@
using HarmonyLib;
using Org.BouncyCastle.Crypto.Tls;
namespace CannedNet.Client.Patches;
namespace RecNetPlugin.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")]
public class TlsPatch
@@ -3,7 +3,7 @@ using RecRoom.AntiCheat;
using System.Text;
using Il2CppSystem;
namespace CannedNet.Client.Patches;
namespace RecNetPlugin.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("i hate this"));
__result = Convert.ToBase64String(Encoding.UTF8.GetBytes("nothing"));
return false;
}
}
@@ -1,19 +1,18 @@
using System;
using System.Reflection;
using ExitGames.Client.Photon;
using HarmonyLib;
using Photon.Realtime;
namespace CannedNet.Client.Patches;
namespace RecNetPlugin.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,5 +1,11 @@
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 CannedNet.Client.Patches;
namespace RecNetPlugin.Patches;
/**
Intercept a variety of HTTP requests and rewrite them to point to our own custom server.
@@ -20,6 +20,7 @@ public class SendRequestPatch
"/data/heartbeat",
"/identify",
"/httpapi",
"/data/event",
};
private static bool IsIgnoredForLogging(string url)
@@ -30,6 +31,16 @@ 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
{
@@ -44,10 +55,10 @@ public class SendRequestPatch
if (entityBody == null)
body = "<none>";
else if (IsBinaryContentType(request.GetFirstHeaderValue("content-type")) || LooksBinary(entityBody))
body = "<binary>";
body = BinaryPreview(entityBody);
else
body = System.Text.Encoding.UTF8.GetString(entityBody);
Plugin.Log.LogInfo($"[HTTP] {request.MethodType} {request.Uri.AbsoluteUri} body={body}");
Plugin.Log.LogInfo($"[HTTP] {request.MethodType} {request.Uri.AbsoluteUri} body={Truncate(body)}");
}
var host = request.Uri.Host;
@@ -92,7 +103,7 @@ public class SendRequestPatch
text = resp.DataAsText;
if (string.IsNullOrEmpty(text)) text = "<empty>";
}
var msg = $"[HTTP] <- {resp.StatusCode} {url} body={text}";
var msg = $"[HTTP] <- {resp.StatusCode} {url} body={Truncate(text)}";
if (resp.StatusCode is >= 200 and < 300)
Plugin.Log.LogInfo(msg);
else
@@ -128,6 +139,31 @@ 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
+3 -8
View File
@@ -1,20 +1,15 @@
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 CannedNet.Client;
namespace RecNetPlugin;
[BepInPlugin("lapis.cannednet.client", "CannedNet Client", "1.0.0")]
[BepInPlugin("net.rec.plugin", "RecNet Plugin", "1.0.0")]
public class Plugin : BasePlugin
{
internal static new ManualLogSource Log;
@@ -38,7 +33,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.lapis.codes", "Host for the RecNet NameServer.");
ServerHostname = Config.Bind("Server", "RecNet NameServer Host", "https://ns.rec.net", "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);
+22 -10
View File
@@ -1,10 +1,18 @@
# CannedNet Client
# RecNet Plugin
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.
A [BepInEx 6](https://github.com/BepInEx/BepInEx) (IL2CPP) plugin that points the Rec Room client at a self-hosted / private 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.
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.
> ⚠️ **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.
> ⚠️ 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
## What it does
@@ -23,7 +31,7 @@ It does this entirely client-side with [Harmony](https://harmony.pardeike.net/)
- **.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/CannedNet/CannedNet.Client
_Looking for a custom RecNet server?_ Try https://github.com/djdevin/recflare
## Installing
@@ -66,7 +74,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 `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.
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.
If you need the DLL elsewhere, it's also left in `bin/Debug/net6.0/`.
@@ -74,10 +82,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 `lapis.cannednet.client.cfg` file and update as needed:
Inside `config`, edit the `net.rec.plugin.cfg` file and update as needed:
**[Server]**
- `RecNet NameServer Host` — base URL of your RecNet name server (default `https://ns.lapis.codes`).
- `RecNet NameServer Host` — base URL of your RecNet name server (like `https://ns.rec.net`).
**[Photon]**
- `App Id Realtime` — Photon Realtime App ID.
@@ -96,8 +104,8 @@ Inside `config`, edit the `lapis.cannednet.client.cfg` file and update as needed
| Path | Purpose |
| --- | --- |
| `Plugin.cs` | Plugin entry point, config bindings, Harmony bootstrap |
| `Patches/` | Harmony patches (networking, EAC, TLS, Photon) |
| `CannedNet.Client.csproj` | Build config + interop references (driven by `GamePath`), and the `DeployPlugin` post-build copy |
| `Patches/` | Harmony patches (HTTP, EAC, TLS, Photon) |
| `RecNetPlugin.csproj` | Build config + interop references (driven by `GamePath`), and the `DeployPlugin` post-build copy |
| `GamePath.props.example` | Template for your local `GamePath.props` |
## FAQ
@@ -106,6 +114,10 @@ Inside `config`, edit the `lapis.cannednet.client.cfg` file and update as needed
Yes. That's the point.
## Credits
Based on https://github.com/CannedNet/CannedNet.Client
## License
[MIT](LICENSE)
@@ -2,8 +2,8 @@
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<AssemblyName>CannedNet.Client</AssemblyName>
<Product>My first plugin</Product>
<AssemblyName>RecNetPlugin</AssemblyName>
<Product>RecNetPlugin</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>CannedNet.Client</RootNamespace>
<RootNamespace>RecNetPlugin</RootNamespace>
</PropertyGroup>
<!-- GamePath = root of a Rec Room install whose BepInEx/interop/ has been populated.