mirror of
https://github.com/djdevin/recnet-plugin.git
synced 2026-09-08 22:51:34 -07:00
attempting to patch some more noise calls
This commit is contained in:
+171
-7
@@ -1,7 +1,11 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using BestHTTP;
|
||||
using HarmonyLib;
|
||||
using Il2CppInterop.Runtime.InteropTypes.Arrays;
|
||||
|
||||
namespace RecNetPlugin.Patches;
|
||||
|
||||
@@ -9,8 +13,9 @@ namespace RecNetPlugin.Patches;
|
||||
// for (and which leaks play data off-box). Prefix every event-logging entrypoint and swallow the call
|
||||
// so nothing is ever queued, batched or sent.
|
||||
//
|
||||
// One knob, see [Analytics] in the .cfg:
|
||||
// Disable Amplitude Analytics -> default true; skip every AmplitudeAnalyticsClient.Log* method.
|
||||
// One knob for all telemetry, `[Analytics] Disable Telemetry`, default true — it gates this file plus
|
||||
// BacktracePatch and UnityTelemetryPatch. Deliberately not split per vendor: nobody wants Amplitude
|
||||
// gone but the collector alive, and four switches for one intention is four ways to be half-configured.
|
||||
//
|
||||
// Target resolution: AmplitudeAnalytics.AmplitudeAnalyticsClient in RecRoom.Analytics.Runtime.dll,
|
||||
// concrete (it derives from SingletonMonoBehaviour<T>, so there is no abstract-interface dispatch
|
||||
@@ -24,6 +29,15 @@ namespace RecNetPlugin.Patches;
|
||||
// never even called. Those events are the pre-serialized batch the client parks in the
|
||||
// `pending_room_stats` PlayerPref (visible in the DUID-PROBE log), which points at
|
||||
// LogSerializedEventAsync rather than LogEventAsync.
|
||||
//
|
||||
// Why the Log* prefixes alone STILL are not enough — and why `BlockAnalyticsUploadPatch` below is the
|
||||
// part that actually stops the traffic: uploads to api2.amplitude.com kept showing up in mitmproxy
|
||||
// with LogEventAsync/LogIdentifyAsync visibly blocked in LogOutput.log. The Log* methods only *queue*;
|
||||
// the queue is persisted (`pending_room_stats`) and drained later by the client's own flush coroutines
|
||||
// (Flush / AMEAMPDLJPN / PPOCFIHNKPP), which reach the network through the transport interface
|
||||
// `FOMPBHDLPDO` — concrete impl `MHBPNDOGOLG` in RecNet.Runtime.dll, i.e. BestHTTP. So a batch queued
|
||||
// in an earlier session ships on the next launch no matter what we do to the Log* doors. Blocking at
|
||||
// the BestHTTP layer catches every path, present and future, and costs no obfuscated names.
|
||||
[HarmonyPatch]
|
||||
public static class AmplitudePatch
|
||||
{
|
||||
@@ -42,24 +56,24 @@ public static class AmplitudePatch
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(AmplitudeAnalytics.AmplitudeAnalyticsClient), "LogEventAsync")]
|
||||
private static bool LogEventAsyncPrefix() =>
|
||||
Plugin.DisableAmplitudeAnalytics.Value && Block("LogEventAsync");
|
||||
Plugin.DisableTelemetry.Value && Block("LogEventAsync");
|
||||
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(AmplitudeAnalytics.AmplitudeAnalyticsClient), "LogPrevSessionEventAsync")]
|
||||
private static bool LogPrevSessionEventAsyncPrefix() =>
|
||||
Plugin.DisableAmplitudeAnalytics.Value && Block("LogPrevSessionEventAsync");
|
||||
Plugin.DisableTelemetry.Value && Block("LogPrevSessionEventAsync");
|
||||
|
||||
// The likely culprit for the room_stats/perf_stats batch — takes the already-serialized
|
||||
// Dictionary<string, object> that gets parked in `pending_room_stats`.
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(AmplitudeAnalytics.AmplitudeAnalyticsClient), "LogSerializedEventAsync")]
|
||||
private static bool LogSerializedEventAsyncPrefix() =>
|
||||
Plugin.DisableAmplitudeAnalytics.Value && Block("LogSerializedEventAsync");
|
||||
Plugin.DisableTelemetry.Value && Block("LogSerializedEventAsync");
|
||||
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(AmplitudeAnalytics.AmplitudeAnalyticsClient), "LogIdentifyAsync")]
|
||||
private static bool LogIdentifyAsyncPrefix() =>
|
||||
Plugin.DisableAmplitudeAnalytics.Value && Block("LogIdentifyAsync");
|
||||
Plugin.DisableTelemetry.Value && Block("LogIdentifyAsync");
|
||||
|
||||
// The odd one out: static, and it returns a promise instead of void. Skipping it with a null
|
||||
// __result would hand the caller something it will chain .Then() on, so we substitute an
|
||||
@@ -69,7 +83,7 @@ public static class AmplitudePatch
|
||||
[HarmonyPatch(typeof(AmplitudeAnalytics.AmplitudeAnalyticsClient), "LogOutOfSessionEvent")]
|
||||
private static bool LogOutOfSessionEventPrefix(ref LAHBDKNMNHN __result)
|
||||
{
|
||||
if (!Plugin.DisableAmplitudeAnalytics.Value)
|
||||
if (!Plugin.DisableTelemetry.Value)
|
||||
return true;
|
||||
|
||||
var resolved = ResolvedPromise();
|
||||
@@ -116,4 +130,154 @@ public static class AmplitudePatch
|
||||
|
||||
return _resolvedPromiseGetter.Invoke(null, null) as LAHBDKNMNHN;
|
||||
}
|
||||
|
||||
// Analytics hosts we refuse to talk to, matched two different ways because they identify
|
||||
// themselves two different ways:
|
||||
//
|
||||
// AmplitudeDomains — a registrable domain plus everything under it, so api2.amplitude.com
|
||||
// and api.eu.amplitude.com are both covered.
|
||||
// CollectorLabelPrefixes — the *first* hostname label, for the telemetry collector that lives
|
||||
// on whatever domain the deployment uses (datacollection.recflare.net,
|
||||
// datacollection.rec.net, datacollection-eu.…). The domain varies, the
|
||||
// label doesn't, so match on the label and stay deployment-agnostic.
|
||||
//
|
||||
// Split into separate lists because they're *matched* differently, not configured differently —
|
||||
// one knob covers the lot.
|
||||
private static readonly string[] AmplitudeDomains = { "amplitude.com" };
|
||||
private static readonly string[] CollectorLabelPrefixes = { "datacollection" };
|
||||
|
||||
// Backtrace and Unity's perf-events don't normally reach BestHTTP at all — Backtrace goes through
|
||||
// UnityWebRequest (see BacktracePatch) and Unity's is native (see UnityTelemetryPatch). They're
|
||||
// listed here anyway because it costs a string comparison to be right if that ever changes, and
|
||||
// because "the host block covers every host we don't want talked to" is easier to reason about
|
||||
// than a list with holes in it. Only perf-events is named, not all of cloud.unity3d.com — the
|
||||
// client uses other Unity services.
|
||||
private static readonly string[] BacktraceDomains = { "backtrace.io" };
|
||||
private static readonly string[] UnityTelemetryHosts = { "perf-events.cloud.unity3d.com" };
|
||||
|
||||
private static bool IsUnderAnyDomain(string host, string[] domains) =>
|
||||
domains.Any(d => host.Equals(d, StringComparison.OrdinalIgnoreCase)
|
||||
|| host.EndsWith("." + d, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
private static bool IsAmplitudeHost(string host) => IsUnderAnyDomain(host, AmplitudeDomains);
|
||||
|
||||
private static bool IsCollectorHost(string host)
|
||||
{
|
||||
var dot = host.IndexOf('.');
|
||||
var firstLabel = dot < 0 ? host : host.Substring(0, dot);
|
||||
|
||||
return CollectorLabelPrefixes.Any(p => firstLabel.StartsWith(p, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
// One knob for the lot. The vendors are split into separate lists above because they're matched
|
||||
// differently, not because they're configured differently.
|
||||
private static bool IsBlockedHost(string host)
|
||||
{
|
||||
if (string.IsNullOrEmpty(host) || !Plugin.DisableTelemetry.Value)
|
||||
return false;
|
||||
|
||||
return IsAmplitudeHost(host)
|
||||
|| IsCollectorHost(host)
|
||||
|| IsUnderAnyDomain(host, BacktraceDomains)
|
||||
|| UnityTelemetryHosts.Any(h => host.Equals(h, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
// The part that actually stops the traffic — see the note at the top of the file. Prefixes the
|
||||
// same BestHTTP entrypoint SendRequestPatch hooks (HTTPManager is BestHTTP's own type, so no
|
||||
// obfuscated names are involved and this survives game upgrades) and, for analytics hosts, hands
|
||||
// the caller a synthetic 200 instead of sending anything.
|
||||
//
|
||||
// Faking success rather than failure is deliberate: the transport resolves its promise, the flush
|
||||
// coroutine considers the batch delivered, and the client clears `pending_room_stats` — so nothing
|
||||
// accumulates and nothing retries. Failing the request instead would leave the batch queued and
|
||||
// re-attempted every session.
|
||||
[HarmonyPatch(typeof(HTTPManager), "SendRequest", [typeof(HTTPRequest)])]
|
||||
public static class BlockAnalyticsUploadPatch
|
||||
{
|
||||
private static bool Prefix(HTTPRequest request, ref HTTPRequest __result)
|
||||
{
|
||||
// SendRequest returns the request it was handed; callers chain off it, so hand it back
|
||||
// even though we never send it.
|
||||
__result = request;
|
||||
|
||||
return !Drop(request);
|
||||
}
|
||||
|
||||
// Second net, one layer down. Every SendRequest overload funnels into SendRequestImpl, and
|
||||
// IL2CPP is free to inline the tiny SendRequest(HTTPRequest) body into its callers — a hook on
|
||||
// it then never fires for those call sites (gotcha: a Harmony patch that loads clean can still
|
||||
// never run). SendRequestImpl is the last managed-visible chokepoint before the connection, so
|
||||
// anything that slipped past the hook above is caught here.
|
||||
[HarmonyPatch(typeof(HTTPManager), "SendRequestImpl", [typeof(HTTPRequest)])]
|
||||
public static class ImplPatch
|
||||
{
|
||||
private static bool Prefix(HTTPRequest request) => !Drop(request);
|
||||
}
|
||||
|
||||
// True when the request was blocked (caller should skip the original).
|
||||
private static bool Drop(HTTPRequest request)
|
||||
{
|
||||
if (!IsBlockedHost(request.Uri.Host))
|
||||
return false;
|
||||
|
||||
// Once per host normally; every request under [Advanced] Debug, since "did this specific
|
||||
// upload get dropped or did it slip past?" is exactly the question a mitmproxy trace
|
||||
// raises, and a deduped line can't answer it.
|
||||
if (Plugin.Debug.Value)
|
||||
Plugin.Log.LogInfo($"[ANALYTICS] dropped {request.MethodType} {request.Uri.AbsoluteUri}");
|
||||
else if (_loggedBlocked.Add("upload:" + request.Uri.Host))
|
||||
Plugin.Log.LogInfo($"[ANALYTICS] dropping uploads to {request.Uri.Host}");
|
||||
|
||||
try
|
||||
{
|
||||
CompleteWithFakeSuccess(request);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
// Couldn't synthesize the response — still don't send. The request's callback never
|
||||
// fires, so whatever promise the transport made stays pending; that's a stalled flush
|
||||
// coroutine at worst, versus telemetry leaving the box.
|
||||
Plugin.Log.LogWarning($"[ANALYTICS] blocked {request.Uri.Host} but could not fake a response: {e.Message}");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void CompleteWithFakeSuccess(HTTPRequest request)
|
||||
{
|
||||
var body = FakeBodyFor(request);
|
||||
|
||||
var response = new HTTPResponse(request, new Il2CppSystem.IO.MemoryStream(), false, false)
|
||||
{
|
||||
StatusCode = 200,
|
||||
Message = "OK",
|
||||
Data = new Il2CppStructArray<byte>(Encoding.UTF8.GetBytes(body)),
|
||||
};
|
||||
|
||||
request.Response = response;
|
||||
request.State = HTTPRequestStates.Finished;
|
||||
|
||||
// BestHTTP would normally fire this from HTTPManager's update loop a frame or more later.
|
||||
// Firing it inline is safe here because the callback is assigned before SendRequest is
|
||||
// called, and the promise it resolves already exists by then.
|
||||
request.Callback?.Invoke(request, response);
|
||||
}
|
||||
|
||||
// Whatever the endpoint would have said on a good day. Amplitude's real shapes are known:
|
||||
// /identify answers with the literal "success", the v2 batch endpoint with a small JSON
|
||||
// envelope. The collector's shape isn't known, so we fall back to `{"success":true}` — the
|
||||
// envelope every first-party RecNet endpoint uses (it's what the real deviceId endpoint
|
||||
// returns, see the DUID case study in CLAUDE.md) and a far better guess than an empty body,
|
||||
// which the RecNet HTTP wrapper rejects outright with "Response was empty".
|
||||
private static string FakeBodyFor(HTTPRequest request)
|
||||
{
|
||||
if (!IsAmplitudeHost(request.Uri.Host))
|
||||
return "{\"success\":true}";
|
||||
|
||||
return request.Uri.AbsoluteUri.Contains("/identify", StringComparison.OrdinalIgnoreCase)
|
||||
? "success"
|
||||
: "{\"code\":200,\"events_ingested\":0,\"payload_size_bytes\":0,\"server_upload_time\":"
|
||||
+ DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() + "}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user