using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; 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+03aabb958fe128c55a02aa6089b1ef028d6a578f")] [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.3.0")] public class Plugin : BaseUnityPlugin { 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 discordChannelLink { get; set; } = ""; 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 admins { get; set; } = new List(); } private class AllowedModsDoc { [YamlMember(Alias = "required_mods", ApplyNamingConventions = false)] public List required_mods { get; set; } = new List(); [YamlMember(Alias = "allowed_mods", ApplyNamingConventions = false)] public List allowed_mods { get; set; } = new List(); [YamlMember(Alias = "banned_mods", ApplyNamingConventions = false)] public List banned_mods { get; set; } = new List(); } 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 top_detected_mods { get; set; } = new Dictionary(StringComparer.OrdinalIgnoreCase); public DateTime last_updated { get; set; } = DateTime.UtcNow; } private class RegistrationsDoc { public Dictionary> registrations { get; set; } = new Dictionary>(StringComparer.OrdinalIgnoreCase); } private class ViolationsDoc { public Dictionary> violations { get; set; } = new Dictionary>(StringComparer.OrdinalIgnoreCase); } [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); string peerPlayerName = GetPeerPlayerName(peer); LogS.LogInfo((object)("[ServerGuard] Incoming connection: " + peerPlayerName + " (" + peerPlatformId + ")")); if (Instance.IsAdmin(peerPlatformId)) { LogS.LogInfo((object)("[ServerGuard] " + peerPlatformId + " is admin - skipping attestation.")); if (Instance._settings.EnableMetrics) { Instance._metrics.admin_bypasses++; Instance.SaveMetrics(); } return; } if (Instance._settings.EnableMetrics) { Instance._metrics.total_players_checked++; Instance.SaveMetrics(); } peer.m_rpc.Register("ServerGuard_Manifest", (Action)delegate(ZRpc rpc, string json) { Instance.OnManifestReceived(peer, json); }); 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), "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(); 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 {peerPlatformId} -> '{charName}'"); return; } Instance.AddViolation(peerPlatformId, "CharacterNameLimitExceeded"); if (Instance._settings.Enforce) { 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}", peerPlatformId, num, charName, string.Join(", ", value))); } } catch (Exception arg) { LogS.LogError((object)$"[ServerGuard] RPC_PeerInfo error: {arg}"); } } } private struct PolicyVerdict { public bool Allowed; public string Rule; public string Reason; } 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 _buffer = new Queue(); 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 list = null; lock (_buffer) { if (_buffer.Count == 0) { return; } list = new List(_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 d__80 : IEnumerator, IDisposable, IEnumerator { private int <>1__state; private object <>2__current; public Plugin <>4__this; public ZNetPeer peer; public string steamId; private int 5__2; object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public d__80(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; 5__2 = Mathf.Max(1, plugin._settings.CompanionTimeoutSeconds); <>2__current = (object)new WaitForSeconds((float)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); } LogS.LogWarning((object)$"[ServerGuard] {steamId} did not deliver a manifest within {5__2}s. Treating as no-companion."); plugin.SendDiscordNow($":hourglass: No manifest from {steamId} in {5__2}s. Companion plugin missing or unreachable."); if (plugin._settings.RequireCompanion) { plugin.AddViolation(steamId, "CompanionMissing"); if (plugin._settings.Enforce) { 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(); } } 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 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 _admins = new HashSet(StringComparer.OrdinalIgnoreCase); private DetectionMetrics _metrics; private List _requiredMods = new List(); private List _allowedMods = new List(); private List _bannedMods = new List(); private Dictionary _pending = new Dictionary(); private readonly object _pendingLock = new object(); private Dictionary> _registrations = new Dictionary>(StringComparer.OrdinalIgnoreCase); private Dictionary> _violations = new Dictionary>(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_CHAR_NAME_LIMIT = "CharacterNameLimitExceeded"; private FileSystemWatcher _watchSettings; private FileSystemWatcher _watchAdmins; private FileSystemWatcher _watchAllowed; private readonly Dictionary _lastSeenWrite = new Dictionary(); 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)new DeserializerBuilder()).WithNamingConvention(CamelCaseNamingConvention.Instance).IgnoreUnmatchedProperties().Build(); _yamlOut = ((BuilderSkeleton)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.3.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)); } if (!string.IsNullOrWhiteSpace(_settings.discordWebhookUrl)) { try { ManualLogSource logS = LogS; string text = ((logS != null) ? logS.SourceName : null) ?? "Valheim ServerGuard"; Logger.Listeners.Add((ILogListener)(object)(_discordListener = new DiscordLogListener(_settings.discordWebhookUrl, "[ServerGuard]", text))); LogS.LogInfo((object)("[ServerGuard] Discord logging enabled for source '" + text + "'.")); } catch (Exception ex) { LogS.LogWarning((object)("[ServerGuard] Failed to enable Discord logging: " + ex.Message)); } } } 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 SendDiscordNow(string text) { try { string text2 = _settings?.discordWebhookUrl; if (string.IsNullOrWhiteSpace(text2)) { return; } HttpClient http = new HttpClient(); try { string text3 = JsonConvert.SerializeObject((object)new { content = text }); StringContent req = new StringContent(text3, Encoding.UTF8, "application/json"); try { await http.PostAsync(text2, (HttpContent)(object)req); } finally { ((IDisposable)req)?.Dispose(); } } finally { ((IDisposable)http)?.Dispose(); } } catch (Exception ex) { LogS.LogWarning((object)("[ServerGuard] SendDiscordNow failed: " + ex.Message)); } } 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.3.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("# Discord:"); stringBuilder.AppendLine("# discordWebhookUrl - full Discord Webhook URL for live event forwarding."); stringBuilder.AppendLine("#"); stringBuilder.AppendLine(_yamlOut.Serialize((object)settings)); File.WriteAllText(SettingsYaml, stringBuilder.ToString()); } if (!File.Exists(AdminsYaml)) { AdminsDoc adminsDoc = new AdminsDoc { admins = new List() }; 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 `|` 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("# To harvest GUIDs from a real client connection, set logPeerManifest: true in settings.yaml."); stringBuilder3.AppendLine("# The names below were bootstrapped from your modpack's BepInEx LogOutput.log; replace them"); stringBuilder3.AppendLine("# with GUIDs over time for stricter matching."); stringBuilder3.AppendLine(); stringBuilder3.AppendLine("required_mods:"); stringBuilder3.AppendLine(" - com.taeguk.valheim.serverguard.client # the ServerGuard companion plugin"); stringBuilder3.AppendLine(); stringBuilder3.AppendLine("allowed_mods:"); string[] array = new string[29] { "Armoire", "AzuAntiCheat", "FastLink", "Recycle_N_Reclaim", "BalrondShipyard", "ComfyQuickSlots", "Trader Overhaul", "Haldor Bounties", "Jotunn", "Offline Companions", "Newtonsoft.Json Detector", "YamlDotNet Detector", "Wandering Companions", "Better Networking", "SimpleMarket", "Quick Stack - Store - Sort - Trash - Restock", "PlanBuild", "ImpactfulSkills", "SlayerSkills", "DiscordConnectorClient", "Creature Level & Loot Control", "Groups", "Player Activity", "Protective Wards", "ValkyrieDeathMessages", "WackysDatabase", "More_World_Locations_AIO", "Zen.ModLib", "ZenBossStone" }; foreach (string text in array) { string text2 = ((text.IndexOfAny(new char[9] { ':', '|', '#', '&', '*', '!', '%', '@', '`' }) >= 0) ? ("\"" + text.Replace("\"", "\\\"") + "\"") : text); stringBuilder3.AppendLine(" - " + text2); } 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(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"); } catch (Exception ex2) { LogS.LogError((object)("[ServerGuard] Failed to load settings.yaml: " + ex2.Message)); _settings = new Settings(); } } private void LoadAdmins() { try { string text = File.ReadAllText(AdminsYaml); AdminsDoc adminsDoc = _yamlIn.Deserialize(text) ?? new AdminsDoc(); _admins = new HashSet(from s in adminsDoc.admins ?? new List() select s.Trim() into s where !string.IsNullOrWhiteSpace(s) select s, StringComparer.OrdinalIgnoreCase); LogS.LogInfo((object)$"[ServerGuard] admins.yaml loaded ({_admins.Count} admins)"); } catch (Exception ex) { LogS.LogError((object)("[ServerGuard] Failed to load admins.yaml: " + ex.Message)); _admins = new HashSet(StringComparer.OrdinalIgnoreCase); } } private void LoadAllowedMods() { try { string text = File.ReadAllText(AllowedModsYaml); AllowedModsDoc allowedModsDoc = _yamlIn.Deserialize(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})"); } catch (Exception ex) { LogS.LogError((object)("[ServerGuard] Failed to load allowed_mods.yaml: " + ex.Message)); _requiredMods = new List(); _allowedMods = new List(); _bannedMods = new List(); } } private static List ParseAllowedList(List raw) { List list = new List(); 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(text); if (registrationsDoc?.registrations != null && registrationsDoc.registrations.Count > 0) { _registrations = registrationsDoc.registrations; } else { Dictionary> dictionary = _yamlIn.Deserialize>>(text); if (dictionary != null && dictionary.TryGetValue("registrations", out var value) && value != null) { Dictionary> dictionary2 = new Dictionary>(StringComparer.OrdinalIgnoreCase); foreach (KeyValuePair item in value) { if (!string.IsNullOrWhiteSpace(item.Key) && !string.IsNullOrWhiteSpace(item.Value)) { dictionary2[item.Key] = new List { item.Value.Trim() }; } } _registrations = dictionary2; SaveRegistrations(); } else { _registrations = new Dictionary>(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>(StringComparer.OrdinalIgnoreCase); } } private void LoadViolations() { try { string text = File.ReadAllText(ViolationsYaml); ViolationsDoc violationsDoc = _yamlIn.Deserialize(text) ?? new ViolationsDoc(); _violations = violationsDoc.violations ?? new Dictionary>(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>(StringComparer.OrdinalIgnoreCase); } } private void LoadMetrics() { try { string text = File.ReadAllText(MetricsYaml); _metrics = _yamlIn.Deserialize(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 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 void AddViolation(string platformId, string rule) { if (!_violations.TryGetValue(platformId, out var value)) { value = new Dictionary(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] {platformId} violated {rule}. Count={value[rule]}/{_settings.ViolationThreshold}"); SendDiscordNow($":warning: Violation by {platformId} — **{rule}** ({value[rule]}/{_settings.ViolationThreshold})"); if (_settings.Enforce && value[rule] >= _settings.ViolationThreshold) { TryBan(platformId, _settings.BanReason); if (_settings.EnableMetrics) { _metrics.players_banned++; SaveMetrics(); } SendDiscordNow(":no_entry: Auto-banned " + platformId + ". Reason: " + _settings.BanReason); } } 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); try { ZRpc rpc = val.m_rpc; if (rpc != null) { rpc.Invoke("Error", new object[1] { 3 }); } } catch { } try { ZNet.instance.Disconnect(val); LogS.LogWarning((object)("[ServerGuard] Disconnected " + peerPlatformId + ". Reason: " + reason)); SendDiscordNow(":boot: Disconnected " + peerPlatformId + ". Reason: " + reason); return; } catch (Exception ex) { LogS.LogWarning((object)("[ServerGuard] ZNet.Disconnect threw (" + ex.Message + "); falling back to reflection.")); } object obj2 = typeof(ZNet).GetProperty("instance", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(null); if (obj2 == null) { return; } MethodInfo method = obj2.GetType().GetMethod("Disconnect", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, new Type[1] { typeof(ZNetPeer) }, null); if (method != null) { method.Invoke(obj2, new object[1] { val }); LogS.LogWarning((object)("[ServerGuard] Disconnected " + peerPlatformId + " (reflection). Reason: " + reason)); SendDiscordNow(":boot: Disconnected " + peerPlatformId + ". Reason: " + reason); return; } MethodInfo method2 = obj2.GetType().GetMethod("InternalKick", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, new Type[1] { typeof(ZNetPeer) }, null); if (method2 != null) { method2.Invoke(obj2, new object[1] { val }); LogS.LogWarning((object)("[ServerGuard] InternalKick'd " + peerPlatformId + ". Reason: " + reason)); SendDiscordNow(":boot: Kicked " + peerPlatformId + ". 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 }); LogS.LogWarning((object)("[ServerGuard] Auto-banned " + platformId + ". Reason: " + reason)); SendDiscordNow(":no_entry: Auto-banned " + platformId + ". 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() { byte[] array = new byte[32]; using (RandomNumberGenerator randomNumberGenerator = RandomNumberGenerator.Create()) { randomNumberGenerator.GetBytes(array); } return Convert.ToBase64String(array); } private static void PersistSharedSecret(string value) { List list = (File.Exists(SettingsYaml) ? File.ReadAllLines(SettingsYaml).ToList() : new List()); Regex regex = new Regex("^\\s*sharedSecret\\s*:.*$", RegexOptions.IgnoreCase); bool flag = false; for (int i = 0; i < list.Count; i++) { if (regex.IsMatch(list[i])) { list[i] = "sharedSecret: '" + value + "'"; flag = true; break; } } if (!flag) { list.Add("sharedSecret: '" + value + "'"); } File.WriteAllLines(SettingsYaml, list); } private void RegisterPending(ZNetPeer peer, string steamId, string challenge) { lock (_pendingLock) { _pending[peer.m_uid] = new PendingAttestation { Challenge = challenge, SentAt = DateTime.UtcNow, SteamId = steamId, Peer = peer }; } } [IteratorStateMachine(typeof(d__80))] public IEnumerator AttestationTimeoutCoroutine(ZNetPeer peer, string steamId) { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new d__80(0) { <>4__this = this, peer = peer, steamId = steamId }; } public void OnManifestReceived(ZNetPeer peer, string json) { string text = "UNKNOWN"; try { text = GetPeerPlatformId(peer); PendingAttestation value; lock (_pendingLock) { if (!_pending.TryGetValue(peer.m_uid, out value) || value == null) { LogS.LogWarning((object)("[ServerGuard] Manifest from " + text + " arrived with no pending challenge (timed out or duplicate). Ignoring.")); return; } _pending.Remove(peer.m_uid); } ModManifest modManifest; try { modManifest = JsonConvert.DeserializeObject(json); } catch (Exception ex) { LogS.LogWarning((object)("[ServerGuard] Failed to parse manifest from " + text + ": " + ex.Message)); AddViolation(text, "HmacInvalid"); if (_settings.Enforce) { TryKick(peer, _settings.KickMessage + " (Malformed manifest)"); } return; } if (modManifest == null) { LogS.LogWarning((object)("[ServerGuard] Empty manifest from " + text + ".")); AddViolation(text, "HmacInvalid"); if (_settings.Enforce) { TryKick(peer, _settings.KickMessage + " (Empty manifest)"); } return; } if (!ModManifest.ConstantTimeEquals(modManifest.Challenge ?? "", value.Challenge ?? "")) { LogS.LogWarning((object)("[ServerGuard] Challenge mismatch from " + text + ".")); AddViolation(text, "ChallengeMismatch"); if (_settings.Enforce) { TryKick(peer, _settings.KickMessage + " (Challenge mismatch)"); } return; } long num = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); if (Math.Abs(num - modManifest.TimestampUtc) > Math.Max(10, _settings.MaxClockSkewSeconds)) { LogS.LogWarning((object)$"[ServerGuard] Timestamp out of window for {text} (client={modManifest.TimestampUtc} server={num})."); AddViolation(text, "HmacInvalid"); if (_settings.Enforce) { TryKick(peer, _settings.KickMessage + " (Clock skew exceeds policy)"); } return; } if (_settings.RequireHmac) { if (string.IsNullOrEmpty(_settings.SharedSecret)) { LogS.LogError((object)("[ServerGuard] Cannot validate manifest from " + text + ": requireHmac=true but sharedSecret is empty on server.")); if (_settings.Enforce) { TryKick(peer, _settings.KickMessage + " (Server misconfiguration: missing sharedSecret)"); } return; } if (!ModManifest.ConstantTimeEquals(ModManifest.ComputeHmac(modManifest.CanonicalForHmac(), _settings.SharedSecret), modManifest.Hmac ?? "")) { LogS.LogWarning((object)("[ServerGuard] HMAC mismatch for " + text + ". Either bad sharedSecret on client, or tampered manifest.")); AddViolation(text, "HmacInvalid"); if (_settings.Enforce) { TryKick(peer, _settings.KickMessage + " (Invalid signature)"); } return; } } if (_settings.LogPeerManifest) { IEnumerable values = (modManifest.Mods ?? new List()).Select((ModManifestEntry m) => " - " + m.Guid + "|" + m.Name + "|" + m.Version + "|" + m.Sha256); LogS.LogInfo((object)($"[ServerGuard] Manifest from {text} ({modManifest.Mods?.Count ?? 0} mods):\n" + string.Join("\n", values))); } PolicyVerdict policyVerdict = ValidateAgainstPolicy(modManifest); if (!policyVerdict.Allowed) { LogS.LogWarning((object)("[ServerGuard] " + text + " REJECTED: " + policyVerdict.Rule + " - " + policyVerdict.Reason)); SendDiscordNow(":no_entry_sign: Rejected " + text + " - " + policyVerdict.Rule + ": " + policyVerdict.Reason); AddViolation(text, policyVerdict.Rule); if (_settings.Enforce) { TryKick(peer, _settings.KickMessage + " (" + policyVerdict.Reason + ")"); } } else { LogS.LogInfo((object)$"[ServerGuard] {text} attested OK ({modManifest.Mods?.Count ?? 0} mods)."); } } catch (Exception arg) { LogS.LogError((object)$"[ServerGuard] OnManifestReceived error for {text}: {arg}"); } } private PolicyVerdict ValidateAgainstPolicy(ModManifest manifest) { List list = manifest.Mods ?? new List(); Dictionary dictionary = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (ModManifestEntry item in list) { if (!string.IsNullOrEmpty(item?.Guid)) { dictionary[item.Guid.ToLowerInvariant()] = item; } if (!string.IsNullOrEmpty(item?.Name)) { dictionary[item.Name.ToLowerInvariant()] = item; } } foreach (AllowedModEntry bannedMod in _bannedMods) { if (dictionary.TryGetValue(bannedMod.Key, out var value)) { PolicyVerdict result = default(PolicyVerdict); result.Allowed = false; result.Rule = "BannedMod"; result.Reason = "Disallowed mod present: " + (value.Name ?? value.Guid); return result; } } PolicyVerdict result2; foreach (AllowedModEntry requiredMod in _requiredMods) { if (!dictionary.TryGetValue(requiredMod.Key, out var value2)) { result2 = default(PolicyVerdict); result2.Allowed = false; result2.Rule = "RequiredModMissing"; result2.Reason = "Required mod missing: " + requiredMod.Key; return result2; } if (!string.IsNullOrEmpty(requiredMod.Sha256) && !string.Equals(requiredMod.Sha256, value2.Sha256 ?? "", StringComparison.OrdinalIgnoreCase)) { result2 = default(PolicyVerdict); result2.Allowed = false; result2.Rule = "DisallowedMod"; result2.Reason = "Required mod hash mismatch: " + requiredMod.Key; return result2; } } if (!_settings.AllowUnlisted) { Dictionary dictionary2 = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (AllowedModEntry requiredMod2 in _requiredMods) { dictionary2[requiredMod2.Key] = requiredMod2; } foreach (AllowedModEntry allowedMod in _allowedMods) { dictionary2[allowedMod.Key] = allowedMod; } foreach (ModManifestEntry item2 in list) { AllowedModEntry allowedModEntry = null; AllowedModEntry value4; if (!string.IsNullOrEmpty(item2.Guid) && dictionary2.TryGetValue(item2.Guid.ToLowerInvariant(), out var value3)) { allowedModEntry = value3; } else if (!string.IsNullOrEmpty(item2.Name) && dictionary2.TryGetValue(item2.Name.ToLowerInvariant(), out value4)) { allowedModEntry = value4; } if (allowedModEntry == null) { string text = ((!string.IsNullOrEmpty(item2.Guid)) ? item2.Guid : item2.Name); result2 = default(PolicyVerdict); result2.Allowed = false; result2.Rule = "DisallowedMod"; result2.Reason = "Unapproved mod: " + text; return result2; } if (!string.IsNullOrEmpty(allowedModEntry.Sha256) && !string.Equals(allowedModEntry.Sha256, item2.Sha256 ?? "", StringComparison.OrdinalIgnoreCase)) { result2 = default(PolicyVerdict); result2.Allowed = false; result2.Rule = "DisallowedMod"; result2.Reason = "Hash pin mismatch: " + (item2.Name ?? item2.Guid); return result2; } } } result2 = default(PolicyVerdict); result2.Allowed = true; return result2; } private void StartWatchers() { _watchSettings = MakeWatcher(SettingsYaml, delegate { LoadSettings(); }); _watchAdmins = MakeWatcher(AdminsYaml, delegate { LoadAdmins(); }); _watchAllowed = MakeWatcher(AllowedModsYaml, delegate { LoadAllowedMods(); }); } private void StopWatchers() { try { _watchSettings?.Dispose(); } catch { } try { _watchAdmins?.Dispose(); } catch { } try { _watchAllowed?.Dispose(); } catch { } } private FileSystemWatcher MakeWatcher(string filePath, Action reloadAction) { FileSystemWatcher fileSystemWatcher = new FileSystemWatcher(Path.GetDirectoryName(filePath), Path.GetFileName(filePath)); fileSystemWatcher.NotifyFilter = NotifyFilters.FileName | NotifyFilters.Size | NotifyFilters.LastWrite; fileSystemWatcher.Changed += delegate(object s, FileSystemEventArgs e) { DebouncedReload(e.FullPath, reloadAction); }; fileSystemWatcher.Created += delegate(object s, FileSystemEventArgs e) { DebouncedReload(e.FullPath, reloadAction); }; fileSystemWatcher.Renamed += delegate(object s, RenamedEventArgs e) { DebouncedReload(e.FullPath, reloadAction); }; fileSystemWatcher.EnableRaisingEvents = true; return fileSystemWatcher; } private void DebouncedReload(string path, Action reloadAction, int debounceMs = 200) { DateTime utcNow = DateTime.UtcNow; if (_lastSeenWrite.TryGetValue(path, out var value) && (utcNow - value).TotalMilliseconds < (double)debounceMs) { return; } _lastSeenWrite[path] = utcNow; Timer t = new Timer(debounceMs); t.AutoReset = false; t.Elapsed += delegate { try { reloadAction(); LogS.LogInfo((object)("[ServerGuard] Reloaded: " + Path.GetFileName(path))); } catch (Exception ex) { LogS.LogError((object)("[ServerGuard] Reload failed for " + Path.GetFileName(path) + ": " + ex.Message)); } finally { t.Dispose(); } }; t.Start(); } } namespace ValheimServerGuard.Shared { [Serializable] public class ModManifestEntry { public string Guid; public string Name; public string Version; public string Sha256; } [Serializable] public class ModManifest { public string SchemaVersion = "1"; public string Challenge; public long TimestampUtc; public List Mods = new List(); public string Hmac; public string CanonicalForHmac() { StringBuilder stringBuilder = new StringBuilder(); stringBuilder.Append(SchemaVersion ?? "").Append('|'); stringBuilder.Append(Challenge ?? "").Append('|'); stringBuilder.Append(TimestampUtc).Append('|'); List list = new List(Mods ?? new List()); list.Sort(delegate(ModManifestEntry a, ModManifestEntry b) { string strA = ((!string.IsNullOrEmpty(a?.Guid)) ? a.Guid : (a?.Name ?? "")); string strB = ((!string.IsNullOrEmpty(b?.Guid)) ? b.Guid : (b?.Name ?? "")); return string.CompareOrdinal(strA, strB); }); foreach (ModManifestEntry item in list) { stringBuilder.Append(item?.Guid ?? "").Append(':'); stringBuilder.Append(item?.Name ?? "").Append(':'); stringBuilder.Append(item?.Version ?? "").Append(':'); stringBuilder.Append(item?.Sha256 ?? "").Append(';'); } return stringBuilder.ToString(); } public static string ComputeHmac(string canonical, string secret) { if (string.IsNullOrEmpty(secret)) { return ""; } using HMACSHA256 hMACSHA = new HMACSHA256(Encoding.UTF8.GetBytes(secret)); return Convert.ToBase64String(hMACSHA.ComputeHash(Encoding.UTF8.GetBytes(canonical ?? ""))); } public static bool ConstantTimeEquals(string a, string b) { if (a == null || b == null) { return false; } if (a.Length != b.Length) { return false; } int num = 0; for (int i = 0; i < a.Length; i++) { num |= a[i] ^ b[i]; } return num == 0; } } }