Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of Valheim ServerGuard v1.4.0
Valheim-ServerGuard.dll
Decompiled a week ago
The result has been truncated due to the large size, download it to view full contents!
using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; using System.Net.Http; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Security.Cryptography; using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; using System.Timers; using BepInEx; using BepInEx.Logging; using HarmonyLib; using Microsoft.CodeAnalysis; using Newtonsoft.Json; using UnityEngine; using ValheimServerGuard.Shared; using YamlDotNet.Serialization; using YamlDotNet.Serialization.NamingConventions; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETFramework,Version=v4.6.2", FrameworkDisplayName = ".NET Framework 4.6.2")] [assembly: AssemblyCompany("yesu0725")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyDescription("Valheim Server Guard - Anti-cheat and security mod for Valheim servers")] [assembly: AssemblyFileVersion("1.3.0.0")] [assembly: AssemblyInformationalVersion("1.3.0+aec5ff4e1c5c57d0d11bef9eaeebb37e9dabfb4d")] [assembly: AssemblyProduct("Valheim-ServerGuard")] [assembly: AssemblyTitle("Valheim-ServerGuard")] [assembly: AssemblyVersion("1.3.0.0")] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Microsoft.CodeAnalysis.Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Microsoft.CodeAnalysis.Embedded] [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } [BepInPlugin("com.taeguk.valheim.serverguard", "Valheim ServerGuard", "1.4.0")] public class Plugin : BaseUnityPlugin { private class PingState { public bool FirstPosted; public List<float> Samples = new List<float>(); } private class SpeedState { public Vector3 LastPos; public bool HasLastPos; public float LastSampleTime; public int OverThresholdCount; } private class Settings { public int ViolationThreshold { get; set; } = 3; public bool Enforce { get; set; } = true; public string KickMessage { get; set; } = "You cannot join: server security policy violation. Contact an administrator."; public string BanReason { get; set; } = "Auto-banned due to repeated security violations."; public int CharacterLimit { get; set; } = 1; public bool RequireCompanion { get; set; } = true; public int CompanionTimeoutSeconds { get; set; } = 10; public bool RequireHmac { get; set; } = true; public string SharedSecret { get; set; } = ""; public bool AllowUnlisted { get; set; } public int MaxClockSkewSeconds { get; set; } = 120; public bool LogPeerManifest { get; set; } public bool EnableMetrics { get; set; } = true; public string discordWebhookUrl { get; set; } = ""; public string discordWebhookUrlAdmin { get; set; } = ""; public bool DiscordVerboseMirror { get; set; } public string discordChannelLink { get; set; } = ""; public bool DailySummaryEnabled { get; set; } = true; public int DailySummaryHourUtc { get; set; } public string DailySummaryChannel { get; set; } = "admin"; [YamlMember(Alias = "countAsViolation", ApplyNamingConventions = false)] public Dictionary<string, bool> CountAsViolation { get; set; } = new Dictionary<string, bool>(StringComparer.OrdinalIgnoreCase) { ["CompanionMissing"] = false, ["HmacInvalid"] = false, ["ChallengeMismatch"] = false, ["RequiredModMissing"] = false, ["DisallowedMod"] = false, ["BannedMod"] = false, ["HashMismatch"] = false, ["CharacterNameLimitExceeded"] = true, ["DevcommandAttempt"] = true, ["SpeedHack"] = true, ["IllegalItem"] = false, ["StackOverflow"] = false, ["AnimationCancel"] = false, ["SkillOverflow"] = false }; public bool EnableDevcommandGate { get; set; } = true; public bool EnableSpeedCheck { get; set; } = true; public double SpeedCheckMaxMetersPerSecond { get; set; } = 15.0; public double SpeedCheckSampleSeconds { get; set; } = 1.0; public int SpeedCheckConsecutiveStrikes { get; set; } = 3; public double SpeedCheckTeleportToleranceMeters { get; set; } = 60.0; public bool EnableInventoryCheck { get; set; } = true; public bool InventoryCheckLogOnly { get; set; } = true; public double InventoryCheckStackTolerance { get; set; } = 1.0; public bool EnableAnimationCancelGate { get; set; } = true; public bool EnableSkillCap { get; set; } = true; public double SkillCapMaxLevel { get; set; } = 100.0; public double SkillCapTolerance { get; set; } = 5.0; public bool EnableDeathLog { get; set; } = true; public bool EnableBuildLog { get; set; } = true; public int BuildLogRetentionDays { get; set; } = 30; public bool EnableSelfTest { get; set; } = true; public bool SelfTestPostOnPass { get; set; } public bool EnablePingLog { get; set; } public int PingLogSampleSeconds { get; set; } = 5; public bool DiscordPublicMode { get; set; } = true; public bool AggressiveNoModCheck { get; set; } public bool EnableAssemblyScanning { get; set; } public bool UseWhitelistMode { get; set; } public bool RequireAttestation { get; set; } } private class AdminsDoc { public List<string> admins { get; set; } = new List<string>(); } private class AllowedModsDoc { [YamlMember(Alias = "required_mods", ApplyNamingConventions = false)] public List<string> required_mods { get; set; } = new List<string>(); [YamlMember(Alias = "allowed_mods", ApplyNamingConventions = false)] public List<string> allowed_mods { get; set; } = new List<string>(); [YamlMember(Alias = "banned_mods", ApplyNamingConventions = false)] public List<string> banned_mods { get; set; } = new List<string>(); } private class AllowedModEntry { public string Key; public string Sha256; } private class PendingAttestation { public string Challenge; public DateTime SentAt; public string SteamId; public ZNetPeer Peer; } private class DetectionMetrics { public long total_players_checked { get; set; } public long total_mods_detected { get; set; } public long phase1_rpc_detections { get; set; } public long phase2_assembly_detections { get; set; } public long version_keyword_detections { get; set; } public long allowlist_bypasses { get; set; } public long admin_bypasses { get; set; } public long violations_issued { get; set; } public long players_banned { get; set; } public Dictionary<string, long> top_detected_mods { get; set; } = new Dictionary<string, long>(StringComparer.OrdinalIgnoreCase); public DateTime last_updated { get; set; } = DateTime.UtcNow; } private class RegistrationsDoc { public Dictionary<string, List<string>> registrations { get; set; } = new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase); } private class ViolationsDoc { public Dictionary<string, Dictionary<string, int>> violations { get; set; } = new Dictionary<string, Dictionary<string, int>>(StringComparer.OrdinalIgnoreCase); } private enum DiscordChannel { Public, Admin, Both } private sealed class SelfTestResult { public string Name; public bool Pass; public string Detail; } [HarmonyPatch(typeof(Inventory), "AddItem", new Type[] { typeof(ItemData) })] public static class Patch_Inventory_AddItem { public static bool Prefix(Inventory __instance, ItemData item) { try { if ((Object)(object)Instance == (Object)null) { return true; } Settings settings = Instance._settings; if (settings == null || !settings.EnableInventoryCheck) { return true; } if ((Object)(object)ZNet.instance == (Object)null || !ZNet.instance.IsServer()) { return true; } if (item == null || item.m_shared == null) { return true; } List<string> list = Instance.ValidateInventoryItem(item); if (list == null || list.Count == 0) { return true; } foreach (string item2 in list) { LogS.LogWarning((object)$"[ServerGuard] Inventory check: {item2} (logOnly={settings.InventoryCheckLogOnly})"); } string text = (list[0].StartsWith("unknown item") ? "IllegalItem" : "StackOverflow"); string text2 = list[0]; LogS.LogWarning((object)("[ServerGuard] " + text + " - " + text2)); if (!settings.InventoryCheckLogOnly) { return false; } } catch (Exception ex) { LogS.LogWarning((object)("[ServerGuard] Inventory check error: " + ex.Message)); } return true; } } [HarmonyPatch(typeof(ZNet), "OnNewConnection")] public static class Patch_OnNewConnection { public static void Postfix(ZNetPeer peer) { try { if (peer == null || peer.m_rpc == null || !Object.op_Implicit((Object)(object)ZNet.instance) || !ZNet.instance.IsServer()) { return; } string peerPlatformId = GetPeerPlatformId(peer); LogS.LogInfo((object)("[ServerGuard] Incoming connection: " + Instance.FormatPlayer(peerPlatformId))); peer.m_rpc.Register<string>("ServerGuard_Manifest", (Action<ZRpc, string>)delegate(ZRpc rpc, string json) { Instance.OnManifestReceived(peer, json); }); peer.m_rpc.Register<string>("ServerGuard_DevcommandAttempt", (Action<ZRpc, string>)delegate(ZRpc rpc, string command) { Instance.OnDevcommandAttemptReceived(peer, command); }); peer.m_rpc.Register<string>("ServerGuard_AnimationCancelAttempt", (Action<ZRpc, string>)delegate(ZRpc rpc, string source) { Instance.OnAnimationCancelReceived(peer, source); }); peer.m_rpc.Register<string>("ServerGuard_SkillReport", (Action<ZRpc, string>)delegate(ZRpc rpc, string payload) { Instance.OnSkillReportReceived(peer, payload); }); peer.m_rpc.Register<string>("ServerGuard_PlayerDeath", (Action<ZRpc, string>)delegate(ZRpc rpc, string payload) { Instance.OnPlayerDeathReceived(peer, payload); }); peer.m_rpc.Register<string>("ServerGuard_BuildPlace", (Action<ZRpc, string>)delegate(ZRpc rpc, string payload) { Instance.OnBuildPlaceReceived(peer, payload); }); peer.m_rpc.Register<string>("ServerGuard_BuildDestroy", (Action<ZRpc, string>)delegate(ZRpc rpc, string payload) { Instance.OnBuildDestroyReceived(peer, payload); }); peer.m_rpc.Register<string>("ServerGuard_AdminCommand", (Action<ZRpc, string>)delegate(ZRpc rpc, string command) { Instance.OnAdminCommandReceived(peer, command); }); if (Instance.IsAdmin(peerPlatformId)) { LogS.LogInfo((object)("[ServerGuard] " + Instance.FormatPlayer(peerPlatformId) + " is admin - skipping attestation challenge.")); if (Instance._settings.EnableMetrics) { Instance._metrics.admin_bypasses++; Instance.SaveMetrics(); } Instance.PostPlayerEvent(":shield:", peerPlatformId, "joined as admin"); return; } if (Instance._settings.EnableMetrics) { Instance._metrics.total_players_checked++; Instance.SaveMetrics(); } string text = Instance.GenerateChallenge(); Instance.RegisterPending(peer, peerPlatformId, text); peer.m_rpc.Invoke("ServerGuard_RequestManifest", new object[1] { text }); ((MonoBehaviour)Instance).StartCoroutine(Instance.AttestationTimeoutCoroutine(peer, peerPlatformId)); } catch (Exception arg) { LogS.LogError((object)$"[ServerGuard] OnNewConnection error: {arg}"); } } } [HarmonyPatch(typeof(ZNet), "Disconnect")] public static class Patch_Disconnect { public static void Prefix(ZNetPeer peer) { try { if (peer == null || !Object.op_Implicit((Object)(object)ZNet.instance) || !ZNet.instance.IsServer() || (Object)(object)Instance == (Object)null) { return; } bool flag; lock (Instance._suppressLogoutFor) { flag = Instance._suppressLogoutFor.Remove(peer.m_uid); } if (!flag) { lock (Instance._pendingLock) { Instance._pending.Remove(peer.m_uid); } Instance._speedState.Remove(peer.m_uid); Instance._skillOverflowState.Remove(peer.m_uid); string peerPlatformId = GetPeerPlatformId(peer); if (!string.IsNullOrWhiteSpace(peerPlatformId)) { string text = Instance.FormatPlayer(peerPlatformId); LogS.LogInfo((object)("[ServerGuard] " + text + " left the server.")); Instance.PostPlayerEvent(":wave:", peerPlatformId, "left"); Instance.FlushPingOnDisconnect(peer.m_uid, peerPlatformId); } } } catch (Exception ex) { LogS.LogWarning((object)("[ServerGuard] Disconnect hook error: " + ex.Message)); } } } [HarmonyPatch(typeof(ZNet), "RPC_PeerInfo")] public static class Patch_RPC_PeerInfo { public static void Postfix(ZNet __instance, ZRpc rpc) { try { if (!Object.op_Implicit((Object)(object)ZNet.instance) || !ZNet.instance.IsServer()) { return; } ZNetPeer val = ResolvePeerFromRpc(__instance, rpc); if (val == null) { return; } string peerPlatformId = GetPeerPlatformId(val); string charName = GetPeerPlayerName(val)?.Trim(); if (!IsValidSteamId(peerPlatformId)) { LogS.LogWarning((object)"[ServerGuard] PeerInfo without valid SteamID; deferring."); } else { if (string.IsNullOrWhiteSpace(charName) || string.Equals(charName, "Unknown", StringComparison.OrdinalIgnoreCase) || Instance.IsAdmin(peerPlatformId)) { return; } if (!Instance._registrations.TryGetValue(peerPlatformId, out var value) || value == null) { value = new List<string>(); Instance._registrations[peerPlatformId] = value; } if (value.Any((string n) => string.Equals(n, charName, StringComparison.Ordinal))) { return; } int num = Math.Max(1, Instance._settings.CharacterLimit); if (value.Count < num) { value.Add(charName); Instance.SaveRegistrations(); LogS.LogInfo((object)$"[ServerGuard] Registered character #{value.Count}/{num} for {Instance.FormatPlayer(peerPlatformId)} -> '{charName}'"); return; } Instance.AddViolation(peerPlatformId, "CharacterNameLimitExceeded", charName); if (Instance._settings.Enforce) { Instance.PostPlayerEvent(":door:", peerPlatformId, "was kicked", FriendlyReason("CharacterNameLimitExceeded")); Instance.TryKick(val, string.Format("{0} (Character limit {1} reached: {2})", Instance._settings.KickMessage, num, string.Join(", ", value))); return; } LogS.LogWarning((object)string.Format("[ServerGuard] {0} exceeded character limit ({1}). Tried '{2}'. Allowed: {3}", Instance.FormatPlayer(peerPlatformId), num, charName, string.Join(", ", value))); } } catch (Exception arg) { LogS.LogError((object)$"[ServerGuard] RPC_PeerInfo error: {arg}"); } } } private class SkillOverflowState { public Dictionary<string, double> LastReportedLevel = new Dictionary<string, double>(StringComparer.OrdinalIgnoreCase); } private sealed class LastHitBox { public ZDOID Attacker; public string AttackerKind; public string AttackerName; public DateTime At; } [HarmonyPatch(typeof(WearNTear), "Damage")] public static class Patch_WearNTear_Damage_Track { public static void Prefix(WearNTear __instance, HitData hit) { //IL_00d3: Unknown result type (might be due to invalid IL or missing references) //IL_00d4: Unknown result type (might be due to invalid IL or missing references) //IL_007d: Unknown result type (might be due to invalid IL or missing references) //IL_0074: Unknown result type (might be due to invalid IL or missing references) //IL_0082: Unknown result type (might be due to invalid IL or missing references) try { if ((Object)(object)Instance == (Object)null || Instance._settings == null || !Instance._settings.EnableBuildLog || (Object)(object)ZNet.instance == (Object)null || !ZNet.instance.IsServer() || (Object)(object)__instance == (Object)null || hit == null) { return; } ZDOID attacker = (ZDOID)(((??)GetHitAttacker(hit)) ?? ZDOID.None); string attackerKind = ""; string attackerName = ""; try { Character attacker2 = hit.GetAttacker(); if ((Object)(object)attacker2 != (Object)null) { if (attacker2 is Player) { attackerKind = "player"; attackerName = GetCharacterDisplayName(attacker2); } else { attackerKind = "creature"; attackerName = GetCharacterDisplayName(attacker2); } } } catch { } LastHitBox value = new LastHitBox { Attacker = attacker, AttackerKind = attackerKind, AttackerName = attackerName, At = DateTime.UtcNow }; Instance._lastHitOnPiece.Remove(__instance); Instance._lastHitOnPiece.Add(__instance, value); } catch { } } } [HarmonyPatch(typeof(WearNTear), "Destroy")] public static class Patch_WearNTear_Destroy_Log { public static void Prefix(WearNTear __instance) { //IL_00ab: Unknown result type (might be due to invalid IL or missing references) //IL_00b0: Unknown result type (might be due to invalid IL or missing references) //IL_01d6: Unknown result type (might be due to invalid IL or missing references) //IL_00f1: Unknown result type (might be due to invalid IL or missing references) //IL_00f6: Unknown result type (might be due to invalid IL or missing references) //IL_0132: Unknown result type (might be due to invalid IL or missing references) //IL_0139: Unknown result type (might be due to invalid IL or missing references) //IL_00a2: Unknown result type (might be due to invalid IL or missing references) //IL_00a7: Unknown result type (might be due to invalid IL or missing references) try { if ((Object)(object)Instance == (Object)null || Instance._settings == null || !Instance._settings.EnableBuildLog || (Object)(object)ZNet.instance == (Object)null || !ZNet.instance.IsServer() || (Object)(object)__instance == (Object)null) { return; } GameObject gameObject = ((Component)__instance).gameObject; string text = ((gameObject != null) ? ((Object)gameObject).name : null) ?? "unknown"; int num = text.IndexOf("(Clone)", StringComparison.Ordinal); if (num > 0) { text = text.Substring(0, num).Trim(); } Vector3 pos; try { pos = ((Component)__instance).transform.position; } catch { pos = Vector3.zero; } string text2 = ""; string text3 = ""; if (Instance._lastHitOnPiece.TryGetValue(__instance, out var value) && value != null) { Instance._lastHitOnPiece.Remove(__instance); if (value.Attacker != ZDOID.None) { List<ZNetPeer> peers = ZNet.instance.GetPeers(); if (peers != null) { foreach (ZNetPeer item in peers) { if (item != null && item.m_characterID == value.Attacker) { text2 = GetPeerPlatformId(item); if (Instance._registrations != null && Instance._registrations.TryGetValue(text2, out var value2) && value2 != null && value2.Count > 0) { text3 = value2[0]; } break; } } } } if (string.IsNullOrEmpty(text2) && string.IsNullOrEmpty(text3) && !string.IsNullOrEmpty(value.AttackerName)) { text3 = value.AttackerName; } } Instance.LogBuildEvent("destroy", text2, text3, text, pos); } catch (Exception ex) { ManualLogSource logS = LogS; if (logS != null) { logS.LogWarning((object)("[ServerGuard] WearNTear.Destroy hook error: " + ex.Message)); } } } } private struct PolicyVerdict { public bool Allowed; public string Rule; public string Reason; public string Detail; } private sealed class DiscordLogListener : ILogListener, IDisposable { private readonly string _webhook; private readonly string _prefix; private readonly string _allowedSourceName; private readonly Timer _flushTimer; private readonly Queue<string> _buffer = new Queue<string>(); private static readonly HttpClient _http = new HttpClient(); private bool _isFlushing; private const int MaxDiscordLength = 2000; private const int MaxPostLength = 1800; public DiscordLogListener(string webhook, string prefixTag, string allowedSourceName) { _webhook = webhook?.Trim(); _prefix = (string.IsNullOrWhiteSpace(prefixTag) ? "[ServerGuard]" : prefixTag.Trim()); _allowedSourceName = allowedSourceName ?? string.Empty; _flushTimer = new Timer(2000.0); _flushTimer.AutoReset = true; _flushTimer.Elapsed += delegate { FlushIfNeeded(); }; _flushTimer.Start(); } public void LogEvent(object sender, LogEventArgs eventArgs) { //IL_0041: Unknown result type (might be due to invalid IL or missing references) //IL_0046: Unknown result type (might be due to invalid IL or missing references) try { if (string.IsNullOrWhiteSpace(_webhook)) { return; } ILogSource source = eventArgs.Source; if (!string.Equals(((source != null) ? source.SourceName : null) ?? string.Empty, _allowedSourceName, StringComparison.Ordinal)) { return; } LogLevel level = eventArgs.Level; string text = ((object)(LogLevel)(ref level)).ToString().ToUpperInvariant(); string text2 = eventArgs.Data?.ToString() ?? ""; string item = (_prefix + " [" + text + "] " + text2).Trim(); lock (_buffer) { _buffer.Enqueue(item); if (_buffer.Count > 1000) { _buffer.Dequeue(); } } } catch { } } private async void FlushIfNeeded() { if (string.IsNullOrWhiteSpace(_webhook) || _isFlushing) { return; } List<string> list = null; lock (_buffer) { if (_buffer.Count == 0) { return; } list = new List<string>(_buffer); _buffer.Clear(); } _isFlushing = true; try { StringBuilder chunk = new StringBuilder(); foreach (string line in list) { int num = line.Length + 1; if (chunk.Length + num > 1800) { await PostAsync(chunk.ToString()); chunk.Clear(); } chunk.AppendLine((line.Length > 2000) ? line.Substring(0, 2000) : line); } if (chunk.Length > 0) { await PostAsync(chunk.ToString()); } } catch { } finally { _isFlushing = false; } } private async Task PostAsync(string content) { if (!string.IsNullOrWhiteSpace(content)) { string text = JsonConvert.SerializeObject((object)new { content }); StringContent req = new StringContent(text, Encoding.UTF8, "application/json"); try { await _http.PostAsync(_webhook, (HttpContent)(object)req); } finally { ((IDisposable)req)?.Dispose(); } } } public void Dispose() { try { _flushTimer?.Stop(); _flushTimer?.Dispose(); } catch { } } } [CompilerGenerated] private sealed class <AttestationTimeoutCoroutine>d__134 : IEnumerator<object>, IDisposable, IEnumerator { private int <>1__state; private object <>2__current; public Plugin <>4__this; public ZNetPeer peer; public string steamId; private int <seconds>5__2; object IEnumerator<object>.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public <AttestationTimeoutCoroutine>d__134(int <>1__state) { this.<>1__state = <>1__state; } [DebuggerHidden] void IDisposable.Dispose() { <>1__state = -2; } private bool MoveNext() { //IL_003d: Unknown result type (might be due to invalid IL or missing references) //IL_0047: Expected O, but got Unknown int num = <>1__state; Plugin plugin = <>4__this; switch (num) { default: return false; case 0: <>1__state = -1; <seconds>5__2 = Mathf.Max(1, plugin._settings.CompanionTimeoutSeconds); <>2__current = (object)new WaitForSeconds((float)<seconds>5__2); <>1__state = 1; return true; case 1: { <>1__state = -1; lock (plugin._pendingLock) { if (!plugin._pending.TryGetValue(peer.m_uid, out var value) || value == null) { return false; } plugin._pending.Remove(peer.m_uid); } string arg = plugin.FormatPlayer(steamId); LogS.LogWarning((object)$"[ServerGuard] {arg} did not deliver a manifest within {<seconds>5__2}s. Treating as no-companion."); if (plugin._settings.RequireCompanion) { plugin.AddViolation(steamId, "CompanionMissing"); if (plugin._settings.Enforce) { plugin.PostPlayerEvent(":door:", steamId, "was kicked", FriendlyReason("CompanionMissing")); plugin.TryKick(peer, plugin._settings.KickMessage + " (Missing required companion plugin: ServerGuard.Client)"); } } return false; } } } bool IEnumerator.MoveNext() { //ILSpy generated this explicit interface implementation from .override directive in MoveNext return this.MoveNext(); } [DebuggerHidden] void IEnumerator.Reset() { throw new NotSupportedException(); } } [CompilerGenerated] private sealed class <BuildLogCleanupLoop>d__146 : IEnumerator<object>, IDisposable, IEnumerator { private int <>1__state; private object <>2__current; public Plugin <>4__this; object IEnumerator<object>.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public <BuildLogCleanupLoop>d__146(int <>1__state) { this.<>1__state = <>1__state; } [DebuggerHidden] void IDisposable.Dispose() { <>1__state = -2; } private bool MoveNext() { //IL_002f: Unknown result type (might be due to invalid IL or missing references) //IL_0039: Expected O, but got Unknown //IL_0075: Unknown result type (might be due to invalid IL or missing references) //IL_007f: Expected O, but got Unknown int num = <>1__state; Plugin plugin = <>4__this; switch (num) { default: return false; case 0: <>1__state = -1; <>2__current = (object)new WaitForSeconds(300f); <>1__state = 1; return true; case 1: <>1__state = -1; break; case 2: <>1__state = -1; break; } try { plugin.PruneOldBuildLogs(); } catch (Exception ex) { LogS.LogWarning((object)("[ServerGuard] PruneOldBuildLogs failed: " + ex.Message)); } <>2__current = (object)new WaitForSeconds(3600f); <>1__state = 2; return true; } bool IEnumerator.MoveNext() { //ILSpy generated this explicit interface implementation from .override directive in MoveNext return this.MoveNext(); } [DebuggerHidden] void IEnumerator.Reset() { throw new NotSupportedException(); } } [CompilerGenerated] private sealed class <DailySummaryLoop>d__120 : IEnumerator<object>, IDisposable, IEnumerator { private int <>1__state; private object <>2__current; public Plugin <>4__this; object IEnumerator<object>.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public <DailySummaryLoop>d__120(int <>1__state) { this.<>1__state = <>1__state; } [DebuggerHidden] void IDisposable.Dispose() { <>1__state = -2; } private bool MoveNext() { //IL_0033: Unknown result type (might be due to invalid IL or missing references) //IL_003d: Expected O, but got Unknown //IL_008f: Unknown result type (might be due to invalid IL or missing references) //IL_0099: Expected O, but got Unknown int num = <>1__state; Plugin plugin = <>4__this; switch (num) { default: return false; case 0: { <>1__state = -1; float num2 = (float)plugin.SecondsUntilNextFire(); <>2__current = (object)new WaitForSeconds(num2); <>1__state = 1; return true; } case 1: <>1__state = -1; break; case 2: <>1__state = -1; break; } try { plugin.PostDailySummary(scheduled: true); } catch (Exception ex) { LogS.LogWarning((object)("[ServerGuard] Daily summary post failed: " + ex.Message)); } float num3 = (float)plugin.SecondsUntilNextFire(); if (num3 <= 60f) { num3 = 86400f; } <>2__current = (object)new WaitForSeconds(num3); <>1__state = 2; return true; } bool IEnumerator.MoveNext() { //ILSpy generated this explicit interface implementation from .override directive in MoveNext return this.MoveNext(); } [DebuggerHidden] void IEnumerator.Reset() { throw new NotSupportedException(); } } [CompilerGenerated] private sealed class <PingLogLoop>d__112 : IEnumerator<object>, IDisposable, IEnumerator { private int <>1__state; private object <>2__current; public Plugin <>4__this; object IEnumerator<object>.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public <PingLogLoop>d__112(int <>1__state) { this.<>1__state = <>1__state; } [DebuggerHidden] void IDisposable.Dispose() { <>1__state = -2; } private bool MoveNext() { //IL_002f: Unknown result type (might be due to invalid IL or missing references) //IL_0039: Expected O, but got Unknown //IL_0065: Unknown result type (might be due to invalid IL or missing references) //IL_006f: Expected O, but got Unknown int num = <>1__state; Plugin plugin = <>4__this; switch (num) { default: return false; case 0: <>1__state = -1; <>2__current = (object)new WaitForSeconds(15f); <>1__state = 1; return true; case 1: <>1__state = -1; break; case 2: <>1__state = -1; try { if (plugin._settings != null && plugin._settings.EnablePingLog && !((Object)(object)ZNet.instance == (Object)null) && ZNet.instance.IsServer()) { plugin.TickPingLog(); } } catch (Exception ex) { LogS.LogWarning((object)("[ServerGuard] Ping log tick error: " + ex.Message)); } break; } int num2 = Math.Max(2, plugin._settings?.PingLogSampleSeconds ?? 5); <>2__current = (object)new WaitForSeconds((float)num2); <>1__state = 2; return true; } bool IEnumerator.MoveNext() { //ILSpy generated this explicit interface implementation from .override directive in MoveNext return this.MoveNext(); } [DebuggerHidden] void IEnumerator.Reset() { throw new NotSupportedException(); } } [CompilerGenerated] private sealed class <SpeedCheckLoop>d__116 : IEnumerator<object>, IDisposable, IEnumerator { private int <>1__state; private object <>2__current; public Plugin <>4__this; object IEnumerator<object>.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public <SpeedCheckLoop>d__116(int <>1__state) { this.<>1__state = <>1__state; } [DebuggerHidden] void IDisposable.Dispose() { <>1__state = -2; } private bool MoveNext() { //IL_002f: Unknown result type (might be due to invalid IL or missing references) //IL_0039: Expected O, but got Unknown //IL_0062: Unknown result type (might be due to invalid IL or missing references) //IL_006c: Expected O, but got Unknown int num = <>1__state; Plugin plugin = <>4__this; switch (num) { default: return false; case 0: <>1__state = -1; <>2__current = (object)new WaitForSeconds(5f); <>1__state = 1; return true; case 1: <>1__state = -1; break; case 2: <>1__state = -1; try { if (plugin._settings.EnableSpeedCheck && !((Object)(object)ZNet.instance == (Object)null) && ZNet.instance.IsServer()) { plugin.TickSpeedCheck(); } } catch (Exception ex) { LogS.LogWarning((object)("[ServerGuard] Speed check tick error: " + ex.Message)); } break; } float num2 = Mathf.Max(0.25f, (float)plugin._settings.SpeedCheckSampleSeconds); <>2__current = (object)new WaitForSeconds(num2); <>1__state = 2; return true; } bool IEnumerator.MoveNext() { //ILSpy generated this explicit interface implementation from .override directive in MoveNext return this.MoveNext(); } [DebuggerHidden] void IEnumerator.Reset() { throw new NotSupportedException(); } } internal static Plugin Instance; internal static ManualLogSource LogS; private Harmony _harmony; private DiscordLogListener _discordListener; private static readonly string RootDir = Path.Combine(Paths.ConfigPath, "ServerGuard"); private static readonly string ConfDir = Path.Combine(RootDir, "conf"); private static readonly string BuildLogDir = Path.Combine(RootDir, "build_log"); private static readonly string ReadmeMD = Path.Combine(RootDir, "README.md"); private static readonly string SettingsYaml = Path.Combine(ConfDir, "settings.yaml"); private static readonly string AdminsYaml = Path.Combine(ConfDir, "admins.yaml"); private static readonly string AllowedModsYaml = Path.Combine(ConfDir, "allowed_mods.yaml"); private static readonly string RegistrationsYaml = Path.Combine(ConfDir, "registrations.yaml"); private static readonly string ViolationsYaml = Path.Combine(ConfDir, "violations.yaml"); private static readonly string MetricsYaml = Path.Combine(ConfDir, "metrics.yaml"); private static readonly string LegacyIgnoreModsYaml = Path.Combine(ConfDir, "ignore_mods.yaml"); private static readonly string LegacyModPatternsYaml = Path.Combine(ConfDir, "mod_patterns.yaml"); private static IDeserializer _yamlIn; private static ISerializer _yamlOut; private Settings _settings; private HashSet<string> _admins = new HashSet<string>(StringComparer.OrdinalIgnoreCase); private DetectionMetrics _metrics; private List<AllowedModEntry> _requiredMods = new List<AllowedModEntry>(); private List<AllowedModEntry> _allowedMods = new List<AllowedModEntry>(); private List<AllowedModEntry> _bannedMods = new List<AllowedModEntry>(); private string _modsetFingerprintStrict = ""; private string _modsetFingerprintLoose = ""; private Dictionary<long, PendingAttestation> _pending = new Dictionary<long, PendingAttestation>(); private readonly object _pendingLock = new object(); private readonly HashSet<long> _suppressLogoutFor = new HashSet<long>(); private readonly Dictionary<long, PingState> _pingState = new Dictionary<long, PingState>(); private static FieldInfo _rpcPingField; private readonly Dictionary<long, SpeedState> _speedState = new Dictionary<long, SpeedState>(); private Dictionary<string, List<string>> _registrations = new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase); private Dictionary<string, Dictionary<string, int>> _violations = new Dictionary<string, Dictionary<string, int>>(StringComparer.OrdinalIgnoreCase); private const string RULE_COMPANION_MISSING = "CompanionMissing"; private const string RULE_HMAC_INVALID = "HmacInvalid"; private const string RULE_CHALLENGE_MISMATCH = "ChallengeMismatch"; private const string RULE_REQUIRED_MOD_MISSING = "RequiredModMissing"; private const string RULE_DISALLOWED_MOD = "DisallowedMod"; private const string RULE_BANNED_MOD = "BannedMod"; private const string RULE_HASH_MISMATCH = "HashMismatch"; private const string RULE_CHAR_NAME_LIMIT = "CharacterNameLimitExceeded"; private const string RULE_DEVCOMMAND_ATTEMPT = "DevcommandAttempt"; private const string RULE_SPEED_HACK = "SpeedHack"; private const string RULE_ILLEGAL_ITEM = "IllegalItem"; private const string RULE_STACK_OVERFLOW = "StackOverflow"; private const string RULE_ANIMATION_CANCEL = "AnimationCancel"; private const string RULE_SKILL_OVERFLOW = "SkillOverflow"; private static readonly string[] ALL_RULES = new string[14] { "CompanionMissing", "HmacInvalid", "ChallengeMismatch", "RequiredModMissing", "DisallowedMod", "BannedMod", "HashMismatch", "CharacterNameLimitExceeded", "DevcommandAttempt", "SpeedHack", "IllegalItem", "StackOverflow", "AnimationCancel", "SkillOverflow" }; private FileSystemWatcher _watchSettings; private FileSystemWatcher _watchAdmins; private FileSystemWatcher _watchAllowed; private readonly Dictionary<string, DateTime> _lastSeenWrite = new Dictionary<string, DateTime>(); private string _attachedAdminWebhookUrl = ""; private bool _bootCompleted; private bool _dailySummaryStarted; private readonly object _summaryLock = new object(); private DateTime _summarySince = DateTime.UtcNow; private int _summaryJoins; private int _summaryKicks; private int _summaryBans; private readonly Dictionary<string, int> _summaryKickReasons = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase); private int _summaryLeaves; private readonly Dictionary<long, SkillOverflowState> _skillOverflowState = new Dictionary<long, SkillOverflowState>(); private readonly ConditionalWeakTable<WearNTear, LastHitBox> _lastHitOnPiece = new ConditionalWeakTable<WearNTear, LastHitBox>(); private static FieldInfo _hitAttackerField; private const int AdminReplyMaxLines = 25; private const int BuildQueryDefaultDays = 7; private const int BuildQueryMaxResults = 20; private void Awake() { //IL_0011: Unknown result type (might be due to invalid IL or missing references) //IL_0020: Expected O, but got Unknown //IL_002f: Unknown result type (might be due to invalid IL or missing references) //IL_003e: Expected O, but got Unknown //IL_0084: Unknown result type (might be due to invalid IL or missing references) //IL_008e: Expected O, but got Unknown Instance = this; LogS = ((BaseUnityPlugin)this).Logger; _yamlIn = ((BuilderSkeleton<DeserializerBuilder>)new DeserializerBuilder()).WithNamingConvention(CamelCaseNamingConvention.Instance).IgnoreUnmatchedProperties().Build(); _yamlOut = ((BuilderSkeleton<SerializerBuilder>)new SerializerBuilder()).WithNamingConvention(CamelCaseNamingConvention.Instance).ConfigureDefaultValuesHandling((DefaultValuesHandling)2).Build(); EnsureFoldersAndFiles(); LoadSettings(); LoadAdmins(); LoadAllowedMods(); LoadRegistrations(); LoadViolations(); LoadMetrics(); StartWatchers(); _harmony = new Harmony("com.taeguk.valheim.serverguard"); _harmony.PatchAll(); LogS.LogInfo((object)("[ServerGuard] Loaded (v1.4.0). Enforcement: " + (_settings.Enforce ? "ON" : "LOG-ONLY") + ". RequireCompanion: " + (_settings.RequireCompanion ? "ON" : "OFF") + ". RequireHmac: " + (_settings.RequireHmac ? "ON" : "OFF") + ". AllowUnlisted: " + (_settings.AllowUnlisted ? "ON" : "OFF") + ". " + $"Required: {_requiredMods.Count}, Allowed: {_allowedMods.Count}, Banned: {_bannedMods.Count}. " + "Metrics: " + (_settings.EnableMetrics ? "ON" : "OFF"))); if (_settings.RequireHmac && !string.IsNullOrEmpty(_settings.SharedSecret)) { LogS.LogInfo((object)("[ServerGuard] sharedSecret in use (copy to every client.yaml): " + _settings.SharedSecret)); } ReconfigureDiscordAndSummary(); ((MonoBehaviour)this).StartCoroutine(SpeedCheckLoop()); ((MonoBehaviour)this).StartCoroutine(BuildLogCleanupLoop()); ((MonoBehaviour)this).StartCoroutine(PingLogLoop()); if (_settings.EnableSelfTest) { try { List<SelfTestResult> list = RunSelfTest(); LogS.LogInfo((object)FormatSelfTestReport(list)); if (list.Any((SelfTestResult r) => !r.Pass) || _settings.SelfTestPostOnPass) { PostAdminEvent(FormatSelfTestForDiscord(list)); } } catch (Exception arg) { LogS.LogError((object)$"[ServerGuard] Self-test failed to run: {arg}"); } } PostAdminEvent(":rocket: **ServerGuard online** v1.4.0 enforce=" + (_settings.Enforce ? "ON" : "off") + " requireHmac=" + (_settings.RequireHmac ? "ON" : "off") + " " + $"req/allow/ban={_requiredMods.Count}/{_allowedMods.Count}/{_bannedMods.Count} " + "modset=`" + ModsetFingerprint.Short(_modsetFingerprintLoose) + "`"); _bootCompleted = true; if (_settings.EnableSpeedCheck) { LogS.LogInfo((object)($"[ServerGuard] Speed check enabled threshold={_settings.SpeedCheckMaxMetersPerSecond:F1}m/s " + $"sample={_settings.SpeedCheckSampleSeconds:F1}s " + $"strikes={_settings.SpeedCheckConsecutiveStrikes} " + $"teleport-tol={_settings.SpeedCheckTeleportToleranceMeters:F1}m")); } } private void OnDestroy() { try { Harmony harmony = _harmony; if (harmony != null) { harmony.UnpatchSelf(); } } catch (Exception ex) { ManualLogSource logS = LogS; if (logS != null) { logS.LogWarning((object)("[ServerGuard] UnpatchSelf failed: " + ex.Message)); } } try { if (_discordListener != null) { try { Logger.Listeners.Remove((ILogListener)(object)_discordListener); } catch (Exception ex2) { ManualLogSource logS2 = LogS; if (logS2 != null) { logS2.LogWarning((object)("[ServerGuard] Removing Discord listener failed: " + ex2.Message)); } } try { _discordListener.Dispose(); } catch (Exception ex3) { ManualLogSource logS3 = LogS; if (logS3 != null) { logS3.LogWarning((object)("[ServerGuard] Disposing Discord listener failed: " + ex3.Message)); } } _discordListener = null; } } catch (Exception ex4) { ManualLogSource logS4 = LogS; if (logS4 != null) { logS4.LogWarning((object)("[ServerGuard] Discord listener cleanup failed: " + ex4.Message)); } } try { StopWatchers(); } catch (Exception ex5) { ManualLogSource logS5 = LogS; if (logS5 != null) { logS5.LogWarning((object)("[ServerGuard] StopWatchers failed: " + ex5.Message)); } } try { SaveAll(); } catch (Exception ex6) { ManualLogSource logS6 = LogS; if (logS6 != null) { logS6.LogWarning((object)("[ServerGuard] SaveAll failed: " + ex6.Message)); } } } private async Task PostToWebhook(string url, string text) { if (string.IsNullOrWhiteSpace(url) || string.IsNullOrWhiteSpace(text)) { return; } try { HttpClient http = new HttpClient(); try { string text2 = JsonConvert.SerializeObject((object)new { content = text }); StringContent req = new StringContent(text2, Encoding.UTF8, "application/json"); try { await http.PostAsync(url, (HttpContent)(object)req); } finally { ((IDisposable)req)?.Dispose(); } } finally { ((IDisposable)http)?.Dispose(); } } catch (Exception ex) { LogS.LogWarning((object)("[ServerGuard] PostToWebhook failed: " + ex.Message)); } } private async Task SendDiscordNow(string text, DiscordChannel target = DiscordChannel.Public) { if (_settings == null) { return; } string pub = _settings.discordWebhookUrl; string adm = _settings.discordWebhookUrlAdmin; switch (target) { case DiscordChannel.Public: await PostToWebhook(pub, text); break; case DiscordChannel.Admin: await PostToWebhook(string.IsNullOrWhiteSpace(adm) ? pub : adm, text); break; case DiscordChannel.Both: await PostToWebhook(pub, text); if (!string.IsNullOrWhiteSpace(adm) && !string.Equals(adm, pub, StringComparison.Ordinal)) { await PostToWebhook(adm, text); } break; } } private void EnsureFoldersAndFiles() { Directory.CreateDirectory(RootDir); Directory.CreateDirectory(ConfDir); if (!File.Exists(SettingsYaml)) { Settings settings = new Settings { SharedSecret = GenerateSharedSecret() }; StringBuilder stringBuilder = new StringBuilder(); stringBuilder.AppendLine("# ServerGuard settings (v1.4.0)"); stringBuilder.AppendLine("#"); stringBuilder.AppendLine("# Client-attestation handshake:"); stringBuilder.AppendLine("# requireCompanion - if true, peers without the ServerGuard.Client plugin are kicked."); stringBuilder.AppendLine("# companionTimeoutSeconds - how long to wait for the manifest before declaring 'no companion'."); stringBuilder.AppendLine("# requireHmac - if true, manifests must carry a valid HMAC signature."); stringBuilder.AppendLine("# sharedSecret - secret string. Must match every client's client.yaml `sharedSecret`."); stringBuilder.AppendLine("# Generate something long and random (e.g. `openssl rand -hex 32`)."); stringBuilder.AppendLine("# allowUnlisted - if true, mods absent from allowed_mods.yaml are tolerated."); stringBuilder.AppendLine("# Default false = strict allowlist."); stringBuilder.AppendLine("# maxClockSkewSeconds - reject manifests whose timestamp is more than this off from server time."); stringBuilder.AppendLine("# logPeerManifest - if true, log every connecting peer's full manifest (verbose)."); stringBuilder.AppendLine("# Useful for harvesting plugin GUIDs to populate allowed_mods.yaml."); stringBuilder.AppendLine("#"); stringBuilder.AppendLine("# Identity / character limits:"); stringBuilder.AppendLine("# characterLimit - max distinct character names a SteamID may use on this server."); stringBuilder.AppendLine("#"); stringBuilder.AppendLine("# Devcommands gate (anti-cheat):"); stringBuilder.AppendLine("# enableDevcommandGate - if true, devcommand attempts reported by the companion"); stringBuilder.AppendLine("# plugin are logged + posted + counted. The companion"); stringBuilder.AppendLine("# ALWAYS blocks `devcommands` and forces"); stringBuilder.AppendLine("# Console.IsCheatsEnabled=false on multiplayer clients;"); stringBuilder.AppendLine("# this toggle only affects server-side accounting."); stringBuilder.AppendLine("#"); stringBuilder.AppendLine("# Movement-speed sanity check (anti-cheat):"); stringBuilder.AppendLine("# enableSpeedCheck - master toggle."); stringBuilder.AppendLine("# speedCheckMaxMetersPerSecond - horizontal speed cap. Vanilla sprint ~5 m/s,"); stringBuilder.AppendLine("# longship sail ~9-10 m/s. 15 m/s is a generous"); stringBuilder.AppendLine("# default; raise for modded mounts/skills."); stringBuilder.AppendLine("# speedCheckSampleSeconds - poll interval. Lower = faster detection,"); stringBuilder.AppendLine("# more sensitive to lag spikes. 1.0 is balanced."); stringBuilder.AppendLine("# speedCheckConsecutiveStrikes - over-threshold samples needed to fire SpeedHack."); stringBuilder.AppendLine("# speedCheckTeleportToleranceMeters - single-sample displacements larger than this"); stringBuilder.AppendLine("# are treated as legitimate teleports (portals,"); stringBuilder.AppendLine("# stones) and reset the strike counter rather"); stringBuilder.AppendLine("# than incrementing it."); stringBuilder.AppendLine("#"); stringBuilder.AppendLine("# Inventory item validation (anti-cheat):"); stringBuilder.AppendLine("# enableInventoryCheck - master toggle for Inventory.AddItem validation."); stringBuilder.AppendLine("# inventoryCheckLogOnly - if true (default), invalid items are logged but"); stringBuilder.AppendLine("# still added. Flip to false to actively block them."); stringBuilder.AppendLine("# Start in log-only mode to audit false positives,"); stringBuilder.AppendLine("# then tighten."); stringBuilder.AppendLine("# inventoryCheckStackTolerance - multiplier on each item's m_maxStackSize. 1.0 is"); stringBuilder.AppendLine("# strict; 2.0 allows up to 2x the vanilla cap for"); stringBuilder.AppendLine("# modpacks that legitimately raise limits."); stringBuilder.AppendLine("#"); stringBuilder.AppendLine("# Animation-cancel gate (anti-cheat):"); stringBuilder.AppendLine("# enableAnimationCancelGate - if true, attempts to cancel attack-recovery"); stringBuilder.AppendLine("# animations (emote, sheathe) reported by the"); stringBuilder.AppendLine("# companion are logged + posted + counted."); stringBuilder.AppendLine("# The companion ALWAYS blocks the cancel client-side;"); stringBuilder.AppendLine("# this toggle only controls server-side accounting."); stringBuilder.AppendLine("#"); stringBuilder.AppendLine("# Skill-level cap (anti-cheat):"); stringBuilder.AppendLine("# enableSkillCap - master toggle. Companion plugin sends a snapshot of"); stringBuilder.AppendLine("# each player's m_skills every ~60s; server flags any"); stringBuilder.AppendLine("# skill above the cap."); stringBuilder.AppendLine("# skillCapMaxLevel - max allowed level. Vanilla is 100."); stringBuilder.AppendLine("# skillCapTolerance - added to skillCapMaxLevel to form the actual flag"); stringBuilder.AppendLine("# threshold. Use to absorb float rounding / minor over-shoot."); stringBuilder.AppendLine("# Raise both for modpacks that legitimately allow higher."); stringBuilder.AppendLine("#"); stringBuilder.AppendLine("# Death log (public Discord):"); stringBuilder.AppendLine("# enableDeathLog - if true, posts a public-channel message every time a"); stringBuilder.AppendLine("# player dies. Includes position and killer (player name"); stringBuilder.AppendLine("# + SteamID for PvP, creature name for mobs, cause for"); stringBuilder.AppendLine("# environmental). Pure forensic log - no violation rule."); stringBuilder.AppendLine("#"); stringBuilder.AppendLine("# Build / destroy heatmap:"); stringBuilder.AppendLine("# enableBuildLog - if true, every piece placement and destruction is"); stringBuilder.AppendLine("# appended to a daily CSV file at"); stringBuilder.AppendLine("# BepInEx/config/ServerGuard/build_log/YYYY-MM-DD.csv."); stringBuilder.AppendLine("# Useful for investigating grief reports. No Discord"); stringBuilder.AppendLine("# output, no violation rule - pure forensic log."); stringBuilder.AppendLine("# buildLogRetentionDays - delete CSV files older than this. Default 30."); stringBuilder.AppendLine("#"); stringBuilder.AppendLine("# Self-test (boot-time smoke checks):"); stringBuilder.AppendLine("# enableSelfTest - run a suite of smoke tests (HMAC, fingerprint,"); stringBuilder.AppendLine("# build-log dir, webhook syntax, ...) at startup."); stringBuilder.AppendLine("# Result is logged and posted to admin Discord on FAIL."); stringBuilder.AppendLine("# Re-run on demand via the `sg selftest` console cmd."); stringBuilder.AppendLine("# selfTestPostOnPass - if true, also post a green-checkmark line to admin"); stringBuilder.AppendLine("# even when all tests pass. Default false (only FAILs)."); stringBuilder.AppendLine("#"); stringBuilder.AppendLine("# Ping / latency log:"); stringBuilder.AppendLine("# enablePingLog - if true, sample each peer's RTT and post the first"); stringBuilder.AppendLine("# measurement after join + session avg on disconnect"); stringBuilder.AppendLine("# to the admin channel. Useful for proxy / VPN spotting."); stringBuilder.AppendLine("# Default false."); stringBuilder.AppendLine("# pingLogSampleSeconds - sampling interval. Default 5."); stringBuilder.AppendLine("#"); stringBuilder.AppendLine("# Discord (two independent channels - either, both, or neither):"); stringBuilder.AppendLine("# discordWebhookUrl - PUBLIC channel. Receives only player-facing"); stringBuilder.AppendLine("# events (joined / kicked / banned / died) in plain"); stringBuilder.AppendLine("# language. Safe for community-visible channels."); stringBuilder.AppendLine("# discordWebhookUrlAdmin - ADMIN channel. Receives CURATED admin-relevant"); stringBuilder.AppendLine("# events: violation strikes, config reloads, admin"); stringBuilder.AppendLine("# command audit, kicks/bans, daily summary. Clean"); stringBuilder.AppendLine("# enough to scan; use a moderator-only channel."); stringBuilder.AppendLine("# discordVerboseMirror - if true, ALSO mirror every ServerGuard log line"); stringBuilder.AppendLine("# to the admin channel (noisy). Default false."); stringBuilder.AppendLine("#"); stringBuilder.AppendLine("# Daily summary:"); stringBuilder.AppendLine("# dailySummaryEnabled - if true, post a one-paragraph digest each day."); stringBuilder.AppendLine("# dailySummaryHourUtc - 0..23, UTC hour at which the post fires."); stringBuilder.AppendLine("# dailySummaryChannel - 'public' | 'admin' | 'both'."); stringBuilder.AppendLine("#"); stringBuilder.AppendLine("# Per-rule violation accounting (countAsViolation):"); stringBuilder.AppendLine("# Each rule can independently decide whether a failure increments the"); stringBuilder.AppendLine("# player's violation count toward auto-ban. A 'false' rule still kicks the"); stringBuilder.AppendLine("# player (when enforce: true) but doesn't add a strike. Tune to match how"); stringBuilder.AppendLine("# strict you want your server to be. Defaults shown below."); stringBuilder.AppendLine("#"); stringBuilder.AppendLine(_yamlOut.Serialize((object)settings)); File.WriteAllText(SettingsYaml, stringBuilder.ToString()); } if (!File.Exists(AdminsYaml)) { AdminsDoc adminsDoc = new AdminsDoc { admins = new List<string>() }; StringBuilder stringBuilder2 = new StringBuilder(); stringBuilder2.AppendLine("# Admin whitelist: one SteamID per entry"); stringBuilder2.AppendLine(_yamlOut.Serialize((object)adminsDoc)); File.WriteAllText(AdminsYaml, stringBuilder2.ToString()); } TryRenameLegacy(LegacyIgnoreModsYaml, LegacyIgnoreModsYaml + ".legacy"); TryRenameLegacy(LegacyModPatternsYaml, LegacyModPatternsYaml + ".legacy"); if (!File.Exists(AllowedModsYaml)) { StringBuilder stringBuilder3 = new StringBuilder(); stringBuilder3.AppendLine("# ServerGuard allowed_mods.yaml (v1.3+)"); stringBuilder3.AppendLine("#"); stringBuilder3.AppendLine("# Each entry references a mod by its BepInEx plugin GUID (preferred) or display Name."); stringBuilder3.AppendLine("# Optional `|<sha256_hex>` suffix pins the DLL hash; mismatch -> kick."); stringBuilder3.AppendLine("#"); stringBuilder3.AppendLine("# required_mods: every connecting client MUST report all of these in its manifest."); stringBuilder3.AppendLine("# allowed_mods : extra mods the client may run beyond the required set."); stringBuilder3.AppendLine("# banned_mods : if any of these appear in the client manifest, the client is kicked."); stringBuilder3.AppendLine("#"); stringBuilder3.AppendLine("# Recommended workflow:"); stringBuilder3.AppendLine("# 1. Install the ServerGuard companion plugin on a client that has your full modpack."); stringBuilder3.AppendLine("# 2. Launch Valheim once. The client writes a snippet to:"); stringBuilder3.AppendLine("# <profile>/BepInEx/config/ServerGuard/mods_for_allowed_mods.yaml"); stringBuilder3.AppendLine("# 3. Paste that snippet's `allowed_mods:` block into this file."); stringBuilder3.AppendLine("#"); stringBuilder3.AppendLine("# Or for ad-hoc harvesting: set logPeerManifest: true in settings.yaml and connect a"); stringBuilder3.AppendLine("# real client - every GUID will appear in BepInEx/LogOutput.log."); stringBuilder3.AppendLine(); stringBuilder3.AppendLine("required_mods:"); stringBuilder3.AppendLine(" - com.taeguk.valheim.serverguard.client # the ServerGuard companion plugin"); stringBuilder3.AppendLine(); stringBuilder3.AppendLine("allowed_mods: []"); stringBuilder3.AppendLine(); stringBuilder3.AppendLine("banned_mods: []"); stringBuilder3.AppendLine(); File.WriteAllText(AllowedModsYaml, stringBuilder3.ToString()); } if (!File.Exists(RegistrationsYaml)) { RegistrationsDoc registrationsDoc = new RegistrationsDoc(); File.WriteAllText(RegistrationsYaml, _yamlOut.Serialize((object)registrationsDoc)); } if (!File.Exists(ViolationsYaml)) { ViolationsDoc violationsDoc = new ViolationsDoc(); File.WriteAllText(ViolationsYaml, _yamlOut.Serialize((object)violationsDoc)); } if (!File.Exists(MetricsYaml)) { DetectionMetrics detectionMetrics = new DetectionMetrics(); StringBuilder stringBuilder4 = new StringBuilder(); stringBuilder4.AppendLine("# ServerGuard Detection Metrics (auto-updated)"); stringBuilder4.AppendLine(_yamlOut.Serialize((object)detectionMetrics)); File.WriteAllText(MetricsYaml, stringBuilder4.ToString()); } } private static void TryRenameLegacy(string from, string to) { try { if (File.Exists(from)) { if (File.Exists(to)) { File.Delete(to); } File.Move(from, to); ManualLogSource logS = LogS; if (logS != null) { logS.LogWarning((object)("[ServerGuard] Renamed legacy config '" + Path.GetFileName(from) + "' -> '" + Path.GetFileName(to) + "'. The new client-attestation flow uses allowed_mods.yaml.")); } } } catch (Exception ex) { ManualLogSource logS2 = LogS; if (logS2 != null) { logS2.LogWarning((object)("[ServerGuard] Could not rename legacy file '" + from + "': " + ex.Message)); } } } private void LoadSettings() { try { _settings = _yamlIn.Deserialize<Settings>(File.ReadAllText(SettingsYaml)) ?? new Settings(); if (_settings.RequireHmac && string.IsNullOrWhiteSpace(_settings.SharedSecret)) { _settings.SharedSecret = GenerateSharedSecret(); try { PersistSharedSecret(_settings.SharedSecret); LogS.LogWarning((object)"[ServerGuard] sharedSecret was empty - generated a new one and wrote it back to settings.yaml. Copy this value into every client's client.yaml:"); LogS.LogWarning((object)("[ServerGuard] sharedSecret: " + _settings.SharedSecret)); } catch (Exception ex) { LogS.LogError((object)("[ServerGuard] Failed to persist generated sharedSecret: " + ex.Message + ". Generated value (use this in client.yaml): " + _settings.SharedSecret)); } } LogS.LogInfo((object)"[ServerGuard] settings.yaml loaded"); if (_bootCompleted) { PostAdminEvent(":arrows_counterclockwise: settings.yaml reloaded"); } } catch (Exception ex2) { LogS.LogError((object)("[ServerGuard] Failed to load settings.yaml: " + ex2.Message)); _settings = new Settings(); } try { ReconfigureDiscordAndSummary(); } catch (Exception ex3) { ManualLogSource logS = LogS; if (logS != null) { logS.LogWarning((object)("[ServerGuard] Discord/summary reconfigure failed: " + ex3.Message)); } } } private void ReconfigureDiscordAndSummary() { if (_settings == null) { return; } string value = _settings.discordWebhookUrl ?? ""; string text = _settings.discordWebhookUrlAdmin ?? ""; string.IsNullOrWhiteSpace(value); bool flag = !string.IsNullOrWhiteSpace(text) && _settings.DiscordVerboseMirror; bool flag2 = _discordListener != null; bool flag3 = !string.Equals(text, _attachedAdminWebhookUrl, StringComparison.Ordinal); if (flag2 && (!flag || flag3)) { try { Logger.Listeners.Remove((ILogListener)(object)_discordListener); } catch { } try { _discordListener.Dispose(); } catch { } _discordListener = null; flag2 = false; } if (flag && !flag2) { try { ManualLogSource logS = LogS; string text2 = ((logS != null) ? logS.SourceName : null) ?? "Valheim ServerGuard"; _discordListener = new DiscordLogListener(text, "[ServerGuard]", text2); Logger.Listeners.Add((ILogListener)(object)_discordListener); LogS.LogInfo((object)("[ServerGuard] Admin Discord verbose mirror enabled for source '" + text2 + "'.")); } catch (Exception ex) { LogS.LogWarning((object)("[ServerGuard] Failed to enable admin Discord verbose mirror: " + ex.Message)); } } if (flag3) { if (!string.IsNullOrWhiteSpace(text)) { LogS.LogInfo((object)("[ServerGuard] Admin Discord channel armed (curated events; verbose mirror: " + (_settings.DiscordVerboseMirror ? "ON" : "OFF") + ").")); } else { LogS.LogInfo((object)"[ServerGuard] Admin Discord channel disabled (URL not set)."); } _attachedAdminWebhookUrl = text; } if (!_dailySummaryStarted && _settings.DailySummaryEnabled && (!string.IsNullOrWhiteSpace(value) || !string.IsNullOrWhiteSpace(text))) { ((MonoBehaviour)this).StartCoroutine(DailySummaryLoop()); _dailySummaryStarted = true; LogS.LogInfo((object)$"[ServerGuard] Daily summary enabled (fires at {_settings.DailySummaryHourUtc:D2}:00 UTC, channel: {_settings.DailySummaryChannel})."); } } private void LoadAdmins() { try { string text = File.ReadAllText(AdminsYaml); AdminsDoc adminsDoc = _yamlIn.Deserialize<AdminsDoc>(text) ?? new AdminsDoc(); _admins = new HashSet<string>(from s in adminsDoc.admins ?? new List<string>() select s.Trim() into s where !string.IsNullOrWhiteSpace(s) select s, StringComparer.OrdinalIgnoreCase); LogS.LogInfo((object)$"[ServerGuard] admins.yaml loaded ({_admins.Count} admins)"); if (_bootCompleted) { PostAdminEvent($":arrows_counterclockwise: admins.yaml reloaded ({_admins.Count} admins)"); } } catch (Exception ex) { LogS.LogError((object)("[ServerGuard] Failed to load admins.yaml: " + ex.Message)); _admins = new HashSet<string>(StringComparer.OrdinalIgnoreCase); } } private void LoadAllowedMods() { try { string text = File.ReadAllText(AllowedModsYaml); AllowedModsDoc allowedModsDoc = _yamlIn.Deserialize<AllowedModsDoc>(text) ?? new AllowedModsDoc(); _requiredMods = ParseAllowedList(allowedModsDoc.required_mods); _allowedMods = ParseAllowedList(allowedModsDoc.allowed_mods); _bannedMods = ParseAllowedList(allowedModsDoc.banned_mods); LogS.LogInfo((object)$"[ServerGuard] allowed_mods.yaml loaded (required={_requiredMods.Count}, allowed={_allowedMods.Count}, banned={_bannedMods.Count})"); if (_bootCompleted) { PostAdminEvent($":arrows_counterclockwise: allowed_mods.yaml reloaded — req={_requiredMods.Count} allow={_allowedMods.Count} ban={_bannedMods.Count}"); } } catch (Exception ex) { LogS.LogError((object)("[ServerGuard] Failed to load allowed_mods.yaml: " + ex.Message)); _requiredMods = new List<AllowedModEntry>(); _allowedMods = new List<AllowedModEntry>(); _bannedMods = new List<AllowedModEntry>(); } RecomputeModsetFingerprint(); } private void RecomputeModsetFingerprint() { try { List<KeyValuePair<string, string>> entries = (from e in _requiredMods.Concat(_allowedMods) select new KeyValuePair<string, string>(e?.Key ?? "", e?.Sha256 ?? "")).ToList(); _modsetFingerprintStrict = ModsetFingerprint.ComputeStrict(entries); _modsetFingerprintLoose = ModsetFingerprint.ComputeLoose(entries); string path = Path.Combine(ConfDir, "modset_fingerprint.txt"); StringBuilder stringBuilder = new StringBuilder(); stringBuilder.AppendLine("# Modset fingerprint for this server."); stringBuilder.AppendLine("# Re-generated on every hot reload of allowed_mods.yaml."); stringBuilder.AppendLine("#"); stringBuilder.AppendLine("# LOOSE - matches across version bumps. Useful for 'are we on the same modpack?'"); stringBuilder.AppendLine("# STRICT - also pins each mod's DLL hash. Matches only on identical binaries."); stringBuilder.AppendLine("#"); stringBuilder.AppendLine("# Players can compare these against their client startup log line:"); stringBuilder.AppendLine("# [ServerGuard.Client] Modset fingerprint loose=XXXXXXXX strict=YYYYYYYY"); stringBuilder.AppendLine(); stringBuilder.AppendLine("loose: " + _modsetFingerprintLoose); stringBuilder.AppendLine("strict: " + _modsetFingerprintStrict); stringBuilder.AppendLine(); stringBuilder.AppendLine("short_loose: " + ModsetFingerprint.Short(_modsetFingerprintLoose)); stringBuilder.AppendLine("short_strict: " + ModsetFingerprint.Short(_modsetFingerprintStrict)); File.WriteAllText(path, stringBuilder.ToString()); LogS.LogInfo((object)("[ServerGuard] Modset fingerprint loose=" + ModsetFingerprint.Short(_modsetFingerprintLoose) + " strict=" + ModsetFingerprint.Short(_modsetFingerprintStrict) + " (full values in " + Path.GetFileName(path) + ")")); } catch (Exception ex) { LogS.LogWarning((object)("[ServerGuard] Failed to compute modset fingerprint: " + ex.Message)); } } private static List<AllowedModEntry> ParseAllowedList(List<string> raw) { List<AllowedModEntry> list = new List<AllowedModEntry>(); if (raw == null) { return list; } foreach (string item in raw) { if (!string.IsNullOrWhiteSpace(item)) { string[] array = item.Split(new char[1] { '|' }); string text = array[0].Trim(); string sha = ((array.Length > 1) ? array[1].Trim().ToLowerInvariant() : null); if (!string.IsNullOrEmpty(text)) { list.Add(new AllowedModEntry { Key = text.ToLowerInvariant(), Sha256 = sha }); } } } return list; } private void LoadRegistrations() { try { string text = File.ReadAllText(RegistrationsYaml); RegistrationsDoc registrationsDoc = _yamlIn.Deserialize<RegistrationsDoc>(text); if (registrationsDoc?.registrations != null && registrationsDoc.registrations.Count > 0) { _registrations = registrationsDoc.registrations; } else { Dictionary<string, Dictionary<string, string>> dictionary = _yamlIn.Deserialize<Dictionary<string, Dictionary<string, string>>>(text); if (dictionary != null && dictionary.TryGetValue("registrations", out var value) && value != null) { Dictionary<string, List<string>> dictionary2 = new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase); foreach (KeyValuePair<string, string> item in value) { if (!string.IsNullOrWhiteSpace(item.Key) && !string.IsNullOrWhiteSpace(item.Value)) { dictionary2[item.Key] = new List<string> { item.Value.Trim() }; } } _registrations = dictionary2; SaveRegistrations(); } else { _registrations = new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase); } } LogS.LogInfo((object)$"[ServerGuard] registrations.yaml loaded ({_registrations.Count} SteamIDs)"); } catch (Exception ex) { LogS.LogError((object)("[ServerGuard] Failed to load registrations.yaml: " + ex.Message)); _registrations = new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase); } } private void LoadViolations() { try { string text = File.ReadAllText(ViolationsYaml); ViolationsDoc violationsDoc = _yamlIn.Deserialize<ViolationsDoc>(text) ?? new ViolationsDoc(); _violations = violationsDoc.violations ?? new Dictionary<string, Dictionary<string, int>>(StringComparer.OrdinalIgnoreCase); LogS.LogInfo((object)$"[ServerGuard] violations.yaml loaded ({_violations.Count} players)"); } catch (Exception ex) { LogS.LogError((object)("[ServerGuard] Failed to load violations.yaml: " + ex.Message)); _violations = new Dictionary<string, Dictionary<string, int>>(StringComparer.OrdinalIgnoreCase); } } private void LoadMetrics() { try { string text = File.ReadAllText(MetricsYaml); _metrics = _yamlIn.Deserialize<DetectionMetrics>(text) ?? new DetectionMetrics(); _metrics.last_updated = DateTime.UtcNow; LogS.LogInfo((object)$"[ServerGuard] metrics.yaml loaded (Checked: {_metrics.total_players_checked}, Detected: {_metrics.total_mods_detected})"); } catch (Exception ex) { LogS.LogWarning((object)("[ServerGuard] Failed to load metrics.yaml: " + ex.Message)); _metrics = new DetectionMetrics(); } } private void SaveRegistrations() { RegistrationsDoc registrationsDoc = new RegistrationsDoc { registrations = _registrations }; File.WriteAllText(RegistrationsYaml, _yamlOut.Serialize((object)registrationsDoc)); } private void SaveViolations() { ViolationsDoc violationsDoc = new ViolationsDoc { violations = _violations }; File.WriteAllText(ViolationsYaml, _yamlOut.Serialize((object)violationsDoc)); } private void SaveMetrics() { try { if (_settings.EnableMetrics) { _metrics.last_updated = DateTime.UtcNow; DetectionMetrics metrics = _metrics; File.WriteAllText(MetricsYaml, _yamlOut.Serialize((object)metrics)); } } catch (Exception ex) { LogS.LogWarning((object)("[ServerGuard] Failed to save metrics.yaml: " + ex.Message)); } } private void SaveAll() { SaveRegistrations(); SaveViolations(); SaveMetrics(); } private static string GetPeerPlatformId(object znetPeer) { try { FieldInfo field = znetPeer.GetType().GetField("m_platformUserID", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (field != null && TryNormalizeSteamId(field.GetValue(znetPeer), out var normalized) && IsValidSteamId(normalized)) { return normalized; } MethodInfo method = znetPeer.GetType().GetMethod("GetPlatformUserID", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (method != null && TryNormalizeSteamId(method.Invoke(znetPeer, null), out var normalized2) && IsValidSteamId(normalized2)) { return normalized2; } object obj = znetPeer.GetType().GetField("m_socket", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(znetPeer); if (obj != null) { FieldInfo field2 = obj.GetType().GetField("m_peerID", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (field2 != null && TryNormalizeSteamId(field2.GetValue(obj), out var normalized3) && IsValidSteamId(normalized3)) { return normalized3; } MethodInfo method2 = obj.GetType().GetMethod("GetPeerID", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (method2 != null && TryNormalizeSteamId(method2.Invoke(obj, null), out var normalized4) && IsValidSteamId(normalized4)) { return normalized4; } MethodInfo method3 = obj.GetType().GetMethod("GetSteamID", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (method3 != null && TryNormalizeSteamId(method3.Invoke(obj, null), out var normalized5) && IsValidSteamId(normalized5)) { return normalized5; } MethodInfo method4 = obj.GetType().GetMethod("GetSteamID64", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (method4 != null && TryNormalizeSteamId(method4.Invoke(obj, null), out var normalized6) && IsValidSteamId(normalized6)) { return normalized6; } PropertyInfo property = obj.GetType().GetProperty("SteamID", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (property != null && TryNormalizeSteamId(property.GetValue(obj, null), out var normalized7) && IsValidSteamId(normalized7)) { return normalized7; } FieldInfo field3 = obj.GetType().GetField("m_SteamID", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (field3 != null && TryNormalizeSteamId(field3.GetValue(obj), out var normalized8) && IsValidSteamId(normalized8)) { return normalized8; } FieldInfo field4 = obj.GetType().GetField("m_steamID", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (field4 != null && TryNormalizeSteamId(field4.GetValue(obj), out var normalized9) && IsValidSteamId(normalized9)) { return normalized9; } MethodInfo method5 = obj.GetType().GetMethod("GetHostName", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (method5 != null) { string text = ExtractSteamIdFromString(Convert.ToString(method5.Invoke(obj, null))); if (IsValidSteamId(text)) { return text; } } string text2 = ExtractSteamIdFromString(obj.ToString()); if (IsValidSteamId(text2)) { return text2; } } string text3 = ExtractSteamIdFromString(znetPeer.ToString()); if (IsValidSteamId(text3)) { return text3; } } catch { } return "UNKNOWN"; } private static bool TryNormalizeSteamId(object raw, out string normalized) { normalized = null; if (raw == null) { return false; } if (!(raw is ulong num)) { if (!(raw is long num2)) { if (raw is string text && IsValidSteamId(text)) { normalized = text; return true; } } else if (num2 > 0) { normalized = num2.ToString(); return true; } } else if (num != 0L) { normalized = num.ToString(); return true; } Type type = raw.GetType(); FieldInfo field = type.GetField("m_SteamID", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (field != null) { object value = field.GetValue(raw); if (value != null && ulong.TryParse(value.ToString(), out var result) && result != 0L) { normalized = result.ToString(); return true; } } PropertyInfo property = type.GetProperty("Value", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (property != null) { object value2 = property.GetValue(raw, null); if (value2 != null && ulong.TryParse(value2.ToString(), out var result2) && result2 != 0L) { normalized = result2.ToString(); return true; } } string text2 = ExtractSteamIdFromString(raw.ToString()); if (IsValidSteamId(text2)) { normalized = text2; return true; } return false; } private static string ExtractSteamIdFromString(string s) { if (string.IsNullOrEmpty(s)) { return null; } int num = 0; int startIndex = -1; for (int i = 0; i < s.Length; i++) { if (char.IsDigit(s[i])) { if (num == 0) { startIndex = i; } num++; if (num == 17) { return s.Substring(startIndex, 17); } } else { num = 0; startIndex = -1; } } return null; } private static bool IsValidSteamId(string candidate) { if (string.IsNullOrWhiteSpace(candidate)) { return false; } if (candidate.Length != 17) { return false; } for (int i = 0; i < candidate.Length; i++) { if (candidate[i] < '0' || candidate[i] > '9') { return false; } } return candidate != "00000000000000000"; } private static string GetPeerPlayerName(object znetPeer) { return znetPeer.GetType().GetField("m_playerName", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(znetPeer)?.ToString() ?? "Unknown"; } private static string GetPeerCharacterId(object znetPeer) { return (znetPeer.GetType().GetField("m_characterID", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(znetPeer))?.ToString() ?? "CHAR_UNKNOWN"; } private bool IsAdmin(string platformId) { return _admins.Contains(platformId); } private string FormatPlayer(string steamId) { if (string.IsNullOrWhiteSpace(steamId)) { return "NewPlayer (UNKNOWN)"; } if (_registrations != null && _registrations.TryGetValue(steamId, out var value) && value != null && value.Count > 0) { return string.Join(", ", value) + " (" + steamId + ")"; } return "NewPlayer (" + steamId + ")"; } private void RecordMetricDetection(string modToken, string detectionMethod) { if (_settings.EnableMetrics && _metrics != null) { _metrics.total_mods_detected++; switch (detectionMethod) { case "RPC": _metrics.phase1_rpc_detections++; break; case "Assembly": _metrics.phase2_assembly_detections++; break; case "Version": _metrics.version_keyword_detections++; break; } if (!_metrics.top_detected_mods.ContainsKey(modToken)) { _metrics.top_detected_mods[modToken] = 0L; } _metrics.top_detected_mods[modToken]++; SaveMetrics(); } } private static string FriendlyReason(string rule, string detail = null) { switch (rule) { case "CompanionMissing": return "missing the required companion mod"; case "HmacInvalid": return "wrong password"; case "ChallengeMismatch": return "wrong password"; case "RequiredModMissing": if (!string.IsNullOrEmpty(detail)) { return "missing a required mod (" + detail + ")"; } return "missing a required mod"; case "DisallowedMod": if (!string.IsNullOrEmpty(detail)) { return "had a mod that isn't allowed (" + detail + ")"; } return "had a mod that isn't allowed"; case "BannedMod": if (!string.IsNullOrEmpty(detail)) { return "had a banned mod (" + detail + ")"; } return "had a banned mod"; case "HashMismatch": if (!string.IsNullOrEmpty(detail)) { return "mod file doesn't match the server's copy (" + detail + ")"; } return "mod file doesn't match the server's copy"; case "CharacterNameLimitExceeded": return "tried to use too many characters"; case "DevcommandAttempt": if (!string.IsNullOrEmpty(detail)) { return "tried to use cheats (`" + detail + "`)"; } return "tried to use cheats"; case "SpeedHack": if (!string.IsNullOrEmpty(detail)) { return "moved suspiciously fast (~" + detail + ")"; } return "moved suspiciously fast"; case "IllegalItem": if (!string.IsNullOrEmpty(detail)) { return "had an unknown item (" + detail + ")"; } return "had an unknown item"; case "StackOverflow": if (!string.IsNullOrEmpty(detail)) { return "had an over-sized item stack (" + detail + ")"; } return "had an over-sized item stack"; case "AnimationCancel": if (!string.IsNullOrEmpty(detail)) { return "tried to cancel attack with " + detail; } return "tried to cancel attack animation"; case "SkillOverflow": if (!string.IsNullOrEmpty(detail)) { return "skill level above cap (" + detail + ")"; } return "skill level above cap"; default: return "policy violation"; } } private bool RuleCountsAsViolation(string rule) { if (_settings?.CountAsViolation == null) { return false; } bool value; return _settings.CountAsViolation.TryGetValue(rule, out value) && value; } private void PostAdminEvent(string text) { if (_settings != null && !string.IsNullOrWhiteSpace(_settings.discordWebhookUrlAdmin) && !string.IsNullOrWhiteSpace(text)) { SendDiscordNow(text, DiscordChannel.Admin); } } private void PostPlayerEvent(string emoji, string platformId, string action, string reason = null) { TrackEventForDailySummary(action, reason); string text = FormatPlayer(platformId); string text2 = (string.IsNullOrEmpty(reason) ? (emoji + " " + text + " " + action) : (emoji + " " + text + " " + action + " — " + reason)); if (!string.IsNullOrWhiteSpace(platformId) && IsAdmin(platformId)) { if (!string.IsNullOrWhiteSpace(_settings?.discordWebhookUrlAdmin)) { SendDiscordNow(text2, DiscordChannel.Admin); } } else if (!string.IsNullOrWhiteSpace(_settings?.discordWebhookUrl)) { SendDiscordNow(text2); } } private void TrackEventForDailySummary(string action, string reason) { if (string.IsNullOrEmpty(action)) { return; } lock (_summaryLock) { if (action.IndexOf("joined", StringComparison.OrdinalIgnoreCase) >= 0) { _summaryJoins++; } else if (action.IndexOf("auto-banned", StringComparison.OrdinalIgnoreCase) >= 0) { _summaryBans++; } else if (action.IndexOf("kicked", StringComparison.OrdinalIgnoreCase) >= 0) { _summaryKicks++; string key = (string.IsNullOrWhiteSpace(reason) ? "other" : reason.Trim()); _summaryKickReasons.TryGetValue(key, out var value); _summaryKickReasons[key] = value + 1; } else if (action.IndexOf("left", StringComparison.OrdinalIgnoreCase) >= 0) { _summaryLeaves++; } } } private List<SelfTestResult> RunSelfTest() { List<SelfTestResult> results = new List<SelfTestResult>(); try { if (_settings.RequireHmac && string.IsNullOrEmpty(_settings.SharedSecret)) { Add("HMAC sharedSecret", pass: false, "requireHmac=true but sharedSecret is empty"); } else if (_settings.RequireHmac) { Add("HMAC sharedSecret", pass: true, $"len={_settings.SharedSecret.Length}"); } else { Add("HMAC sharedSecret", pass: true, "requireHmac disabled - skipped"); } } catch (Exception ex) { Add("HMAC sharedSecret", pass: false, ex.Message); } try { string canonical = new ModManifest { SchemaVersion = "1", Challenge = "selftest-challenge", TimestampUtc = DateTimeOffset.UtcNow.ToUnixTimeSeconds(), Mods = new List<ModManifestEntry>() }.CanonicalForHmac(); bool flag = ModManifest.ConstantTimeEquals(ModManifest.ComputeHmac(canonical, _settings.SharedSecret ?? ""), ModManifest.ComputeHmac(canonical, _settings.SharedSecret ?? "")); Add("HMAC roundtrip", flag, flag ? "sign/verify match" : "sign/verify MISMATCH"); } catch (Exception ex2) { Add("HMAC roundtrip", pass: false, ex2.Message); } try { ModManifest manifest = new ModManifest { Mods = new List<ModManifestEntry>() }; PolicyVerdict policyVerdict = ValidateAgainstPolicy(manifest); bool num = _requiredMods.Count > 0; bool pass2 = (num ? (!policyVerdict.Allowed) : policyVerdict.Allowed); string detail2 = ((!num) ? (policyVerdict.Allowed ? "empty manifest allowed (no required mods)" : "empty manifest unexpectedly rejected") : (policyVerdict.Allowed ? "empty manifest passed but required mods exist!" : ("empty manifest rejected as expected (" + policyVerdict.Rule + ")"))); Add("Policy validator", pass2, detail2); } catch (Exception ex3) { Add("Policy validator", pass: false, ex3.Message); } try { Directory.CreateDirectory(BuildLogDir); string path = Path.Combine(BuildLogDir, ".selftest_probe"); File.WriteAllText(path, "ok"); File.Delete(path); Add("Build-log dir writable", pass: true, BuildLogDir); } catch (Exception ex4) { Add("Build-log dir writable", pass: false, ex4.Message); } try { bool flag2 = !string.IsNullOrEmpty(_modsetFingerprintLoose) && !string.IsNullOrEmpty(_modsetFingerprintStrict); Add("Modset fingerprint", flag2, flag2 ? ("loose=" + ModsetFingerprint.Short(_modsetFingerprintLoose) + " strict=" + ModsetFingerprint.Short(_modsetFingerprintStrict)) : "empty"); } catch (Exception ex5) { Add("Modset fingerprint", pass: false, ex5.Message); } Add("Public webhook URL", string.IsNullOrEmpty(_settings.discordWebhookUrl) || IsWebhookUrlSane(_settings.discordWebhookUrl), string.IsNullOrEmpty(_settings.discordWebhookUrl) ? "not configured (ok)" : "looks valid"); Add("Admin webhook URL", string.IsNullOrEmpty(_settings.discordWebhookUrlAdmin) || IsWebhookUrlSane(_settings.discordWebhookUrlAdmin), string.IsNullOrEmpty(_settings.discordWebhookUrlAdmin) ? "not configured (ok)" : "looks valid"); Add("Admins configured", _admins.Count > 0, (_admins.Count > 0) ? $"{_admins.Count} admin SteamID(s) in admins.yaml" : "admins.yaml is empty - sg commands will be unusable"); return results; SelfTestResult Add(string name, bool pass, string detail) { SelfTestResult selfTestResult = new SelfTestResult { Name = name, Pass = pass, Detail = (detail ?? "") }; results.Add(selfTestResult); return selfTestResult; } } private static bool IsWebhookUrlSane(string url) { if (string.IsNullOrWhiteSpace(url)) { return false; } if (!url.StartsWith("https://", StringComparison.OrdinalIgnoreCase)) { return false; } if (url.IndexOf("/webhooks/", StringComparison.OrdinalIgnoreCase) < 0) { return false; } if (Uri.TryCreate(url, UriKind.Absolute, out Uri result)) { return result.Host.IndexOf('.') >= 0; } return false; } private string FormatSelfTestReport(List<SelfTestResult> results) { int num = results.Count((SelfTestResult r) => r.Pass); int num2 = results.Count - num; StringBuilder stringBuilder = new StringBuilder(); stringBuilder.AppendLine($"[ServerGuard] Self-test pass={num} fail={num2}"); foreach (SelfTestResult result in results) { string arg = (result.Pass ? "PASS" : "FAIL"); stringBuilder.AppendLine($" [{arg}] {result.Name,-26} {result.Detail}"); } return stringBuilder.ToString().TrimEnd(Array.Empty<char>()); } private string FormatSelfTestForDiscord(List<SelfTestResult> results) { int num = results.Count((SelfTestResult r) => r.Pass); int num2 = results.Count - num; StringBuilder stringBuilder = new StringBuilder(); string arg = ((num2 == 0) ? ":white_check_mark:" : ":rotating_light:"); stringBuilder.AppendLine($"{arg} **Self-test** — {num} pass / {num2} fail"); if (num2 > 0) { foreach (SelfTestResult item in results.Where((SelfTestResult r) => !r.Pass)) { stringBuilder.AppendLine(" • **" + item.Name + "**: " + item.Detail); } } return stringBuilder.ToString().TrimEnd(Array.Empty<char>()); } [IteratorStateMachine(typeof(<PingLogLoop>d__112))] public IEnumerator PingLogLoop() { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new <PingLogLoop>d__112(0) { <>4__this = this }; } private void TickPingLog() { List<ZNetPeer> peers = ZNet.instance.GetPeers(); if (peers == null) { return; } foreach (ZNetPeer item in peers) { if (item == null || item.m_rpc == null) { continue; } string peerPlatformId = GetPeerPlatformId(item); if (string.IsNullOrWhiteSpace(peerPlatformId)) { continue; } float num = ReadPingMs(item.m_rpc); if (!(num <= 0f) && !(num > 10000f)) { if (!_pingState.TryGetValue(item.m_uid, out var value) || value == null) { value = new PingState(); _pingState[item.m_uid] = value; } value.Samples.Add(num); if (value.Samples.Count > 200) { value.Samples.RemoveAt(0); } if (!value.FirstPosted && value.Samples.Count >= 2) { value.FirstPosted = true; PostAdminEvent($":satellite: **{FormatPlayer(peerPlatformId)}** first ping: **{num:F0} ms**"); } } } } private static float ReadPingMs(object rpc) { if (rpc == null) { return 0f; } try { if (_rpcPingField == null) { _rpcPingField = rpc.GetType().GetField("m_ping", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); } if (_rpcPingField == null) { return 0f; } object value = _rpcPingField.GetValue(rpc); if (value is float num) { return num * 1000f; } if (value is double) { double num2 = (double)value; return (float)(num2 * 1000.0); } } catch { } return 0f; } internal void FlushPingOnDisconnect(long peerUid, string steamId) { if (_settings != null && _settings.EnablePingLog && _pingState.TryGetValue(peerUid, out var value) && value != null) { _pingState.Remove(peerUid); if (value.Samples.Count != 0) { float num = value.Samples.Sum() / (float)value.Samples.Count; PostAdminEvent($":satellite: **{FormatPlayer(steamId)}** session ping avg: **{num:F0} ms** ({value.Samples.Count} samples)"); } } } [IteratorStateMachine(typeof(<SpeedCheckLoop>d__116))] public IEnumerator SpeedCheckLoop() { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new <SpeedCheckLoop>d__116(0) { <>4__this = this }; } private void TickSpeedCheck() { //IL_0084: Unknown result type (might be due to invalid IL or missing references) //IL_0089: Unknown result type (might be due to invalid IL or missing references) //IL_0093: Unknown result type (might be due to invalid IL or missing references) //IL_0095: Unknown result type (might be due to invalid IL or missing references) //IL_00b0: Unknown result type (might be due to invalid IL or missing references) //IL_00c3: Unknown result type (might be due to invalid IL or missing references) //IL_00c8: Unknown result type (might be due to invalid IL or missing references) //IL_024d: Unknown result type (might be due to invalid IL or missing references) //IL_024f: Unknown result type (might be due to invalid IL or missing references) //IL_012a: Unknown result type (might be due to invalid IL or missing references) //IL_013e: Unknown result type (might be due to invalid IL or missing references) List<ZNetPeer> peers = ZNet.instance.GetPeers(); if (peers == null) { return; } float realtimeSinceStartup = Time.realtimeSinceStartup; float num = (float)_settings.SpeedCheckMaxMetersPerSecond; float num2 = (float)_settings.SpeedCheckTeleportToleranceMeters; int num3 = Math.Max(1, _settings.SpeedCheckConsecutiveStrikes); foreach (ZNetPeer item in peers) { if (item == null) { continue; } string peerPlatformId = GetPeerPlatformId(item); if (string.IsNullOrWhiteSpace(peerPlatformId) || IsAdmin(peerPlatformId)) { continue; } ZDOID characterID; try { characterID = item.m_characterID; } catch { continue; } if (characterID == ZDOID.None) { continue; } ZDOMan instance = ZDOMan.instance; ZDO val = ((instance != null) ? instance.GetZDO(characterID) : null); if (val == null) { continue; } Vector3 position; try { position = val.GetPosition(); } catch { continue; } if (!_speedState.TryGetValue(item.m_uid, out var value) || value == null) { value = new SpeedState(); _speedState[item.m_uid] = value; } if (value.HasLastPos) { float num4 = realtimeSinceStartup - value.LastSampleTime; if (num4 > 0.01f) { float num5 = position.x - value.LastPos.x; float num6 = position.z - value.LastPos.z; float num7 = (float)Math.Sqrt(num5 * num5 + num6 * num6); float num8 = num7 / num4; if (num7 > num2) { value.OverThresholdCount = 0; } else if (num8 > num) { value.OverThresholdCount++; LogS.LogInfo((object)("[ServerGuard] Speed alert " + FormatPlayer(peerPlatformId) + ": " + $"{num8:F1} m/s over {num4:F2}s ({value.OverThresholdCount}/{num3})")); if (value.OverThresholdCount >= num3) { string text = $"{num8:F1} m/s"; PostPlayerEvent(":runner:", peerPlatformId, "flagged for speed", "~" + text); AddViolation(peerPlatformId, "SpeedHack", text); value.OverThresholdCount = 0; } } else { value.OverThresholdCount = 0; } } } value.LastPos = position; value.LastSampleTime = realtimeSinceStartup; value.HasLastPos = true; } } internal List<string> ValidateInventoryItem(ItemData item) { List<string> list = new List<string>(); if (item == null || item.m_shared == null) { return list; } string text = item.m_shared.m_name ?? ""; string text2 = text.TrimStart(new char[1] { '$' }); try { ObjectDB instance = ObjectDB.instance; if ((Object)(object)instance != (Object)null) { bool flag = false; if (instance.m_items != null) { foreach (GameObject item2 in instance.m_items) { if (!((Object)(object)item2 == (Object)null)) { ItemDrop component = item2.GetComponent<ItemDrop>(); if (!((Object)(object)component == (Object)null) && component.m_itemData?.m_shared != null && string.Equals(component.m_itemData.m_shared.m_name, text, StringComparison.Ordinal)) { flag = true; break; } } } } if (!flag) { list.Add("unknown item '" + text2 + "'"); } } } catch { } try { int num = Math.Max(1, item.m_shared.m_maxStackSize); double num2 = Math.Max(1.0, _settings.InventoryCheckStackTolerance); int num3 = (int)Math.Ceiling((double)num * num2); if (item.m_stack > num3) { list.Add($"stack {item.m_stack} > max {num} for '{text2}'"); } } catch { } return list; } [IteratorStateMachine(typeof(<DailySummaryLoop>d__120))] private IEnumerator DailySummaryLoop() { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new <DailySummaryLoop>d__120(0) { <>4__this = this }; } private double SecondsUntilNextFire() { int hour = Math.Max(0, Math.Min(23, _settings.DailySummaryHourUtc)); DateTime utcNow = DateTime.UtcNow; DateTime dateTime = new DateTime(utcNow.Year, utcNow.Month, utcNow.Day, hour, 0, 0, DateTimeKind.Utc); DateTime dateTime2 = ((utcNow < dateTime) ? dateTime : dateTime.AddDays(1.0)); return Math.Max(60.0, (dateTime2 - utcNow).TotalSeconds); } private async Task PostDailySummary(bool scheduled) { int summaryJoins; int summaryLeaves; int summaryKicks; int summaryBans; DateTime summarySince; List<KeyValuePair<string, int>> list; lock (_summaryLock) { summaryJoins = _summaryJoins; summaryLeaves = _summaryLeaves; summaryKicks = _summaryKicks; summaryBans = _summaryBans; summarySince = _summarySince; list = _summaryKickReasons.OrderByDescending((KeyValuePair<string, int> kv) => kv.Value).Take(5).ToList(); _summaryJoins = 0; _summaryLeaves = 0; _summaryKicks = 0; _summaryBans = 0; _summaryKickReasons.Clear(); _summarySince = DateTime.UtcNow; } if (summaryJoins == 0 && summaryLeaves == 0 && summaryKicks == 0 && summaryBans == 0 && scheduled) { return; } string value = $"{summarySince:yyyy-MM-dd HH:mm} UTC → {DateTime.UtcNow:yyyy-MM-dd HH:mm} UTC"; StringBuilder stringBuilder = new StringBuilder(); stringBuilder.AppendLine(":bar_chart: **Daily summary**"); stringBuilder.AppendLine(value); stringBuilder.AppendLine($"• Joins: **{summaryJoins}**"); stringBuilder.AppendLine($"• Leaves: **{summaryLeaves}**"); stringBuilder.AppendLine($"• Kicks: **{summaryKicks}**"); stringBuilder.AppendLine($"• Auto-bans: **{summaryBans}**"); if (list.Count > 0) { stringBuilder.AppendLine("Top kick reasons:"); foreach (KeyValuePair<string, int> item in list) { stringBuilder.AppendLine($" – {item.Key} ({item.Value})"); } } string text = (_settings.DailySummaryChannel ?? "admin").ToLowerInvariant(); DiscordChannel discordChannel = ((!(text == "public")) ? ((!(text == "both")) ? DiscordChannel.Admin : DiscordChannel.Both) : DiscordChannel.Public); DiscordChannel target = discordChannel; await SendDiscordNow(stringBuilder.ToString(), target); } private void AddViolation(string platformId, string rule, string detail = null) { bool num = RuleCountsAsViolation(rule); string text = FormatPlayer(platformId); if (num) { if (!_violations.TryGetValue(platformId, out var value)) { value = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase); _violations[platformId] = value; } value.TryGetValue(rule, out var value2); value[rule] = value2 + 1; SaveViolations(); if (_settings.EnableMetrics) { _metrics.violations_issued++; SaveMetrics(); } LogS.LogWarning((object)$"[ServerGuard] {text} violated {rule}. Count={value[rule]}/{_settings.ViolationThreshold}"); PostAdminEvent($":warning: **{text}** violated **{rule}** ({value[rule]}/{_settings.ViolationThreshold})" + (string.IsNullOrEmpty(detail) ? "" : (" — " + detail))); if (_settings.Enforce && value[rule] >= _settings.ViolationThreshold) { TryBan(platformId, _settings.BanReason); if (_settings.EnableMetrics) { _metrics.players_banned++; SaveMetrics(); } PostPlayerEvent(":no_entry:", platformId, "was auto-banned", "too many strikes"); PostAdminEvent(":no_entry: Auto-banned **" + text + "** (threshold reached)"); } } else { LogS.LogWarning((object)("[ServerGuard] " + text + " hit rule '" + rule + "' (countAsViolation: false - no strike recorded).")); PostAdminEvent(":eye: **" + text + "** triggered **" + rule + "** (informational — not counted)" + (string.IsNullOrEmpty(detail) ? "" : (" — " + detail))); } } private void TryKick(object znetPeer, string reason) { try { ZNetPeer val = (ZNetPeer)((znetPeer is ZNetPeer) ? znetPeer : null); if (val == null || val == null || (Object)(object)ZNet.instance == (Object)null) { return; } string peerPlatformId = GetPeerPlatformId(val); string text = FormatPlayer(peerPlatformId); try { ZRpc rpc = val.m_rpc; if (rpc != null) { rpc.Invoke("Error", new object[1] { 3 }); } } catch { } try { lock (_suppressLogoutFor) { _suppressLogoutFor.Add(val.m_uid); } } catch { } try { ZNet.instance.Disconnect(val); LogS.LogWarning((object)("[ServerGuard] Disconnected " + text + ". Reason: " + reason)); PostAdminEvent(":door: Disconnected **" + text + "** — " + reason); return; } catch (Exception ex) { LogS.LogWarning((object)("[ServerGuard] ZNet.Disconnect threw (" + ex.Message + "); falling back to reflection.")); } object obj3 = typeof(ZNet).GetProperty("instance", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(null); if (obj3 == null) { return; } MethodInfo method = obj3.GetType().GetMethod("Disconnect", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, new Type[1] { typeof(ZNetPeer) }, null); if (method != null) { method.Invoke(obj3, new object[1] { val }); LogS.LogWarning((object)("[ServerGuard] Disconnected " + text + " (reflection). Reason: " + reason)); return; } MethodInfo method2 = obj3.GetType().GetMethod("InternalKick", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, new Type[1] { typeof(ZNetPeer) }, null); if (method2 != null) { method2.Invoke(obj3, new object[1] { val }); LogS.LogWarning((object)("[ServerGuard] InternalKick'd " + text + ". Reason: " + reason)); } } catch (Exception arg) { LogS.LogError((object)$"[ServerGuard] Kick failed: {arg}"); } } private void TryBan(string platformId, string reason) { try { object obj = typeof(ZNet).GetProperty("instance", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(null); if (obj != null) { MethodInfo method = obj.GetType().GetMethod("Ban", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, new Type[1] { typeof(string) }, null); if (method != null) { method.Invoke(obj, new object[1] { platformId }); string text = FormatPlayer(platformId); LogS.LogWarning((object)("[ServerGuard] Auto-banned " + text + ". Reason: " + reason)); } } } catch (Exception arg) { LogS.LogError((object)$"[ServerGuard] Ban failed: {arg}"); } } private static ZNetPeer ResolvePeerFromRpc(ZNet znet, ZRpc rpc) { //IL_0054: Unknown result type (might be due to invalid IL or missing references) //IL_005a: Expected O, but got Unknown //IL_00dc: Unknown result type (might be due to invalid IL or missing references) //IL_00e2: Expected O, but got Unknown if ((Object)(object)znet == (Object)null || rpc == null) { return null; } MethodInfo method = typeof(ZNet).GetMethod("GetPeer", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, new Type[1] { typeof(ZRpc) }, null); if (method != null) { return (ZNetPeer)method.Invoke(znet, new object[1] { rpc }); } MethodInfo method2 = ((object)rpc).GetType().GetMethod("GetUID", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (method2 != null && method2.Invoke(rpc, null) is long num) { MethodInfo method3 = typeof(ZNet).GetMethod("GetPeer", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, new Type[1] { typeof(long) }, null); if (method3 != null) { return (ZNetPeer)method3.Invoke(znet, new object[1] { num }); } } LogS.LogWarning((object)"[ServerGuard] ResolvePeerFromRpc: unable to resolve peer from ZRpc."); return null; } private string GenerateChallenge() { byte[] array = new byte[24]; using (RandomNumberGenerator randomNumberGenerator = RandomNumberGenerator.Create()) { randomNumberGenerator.GetBytes(array); } return Convert.ToBase64String(array); } private static string GenerateSharedSecret()