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 LethalGuardPlus v1.0.2
LethalGuardPlus.dll
Decompiled a day agousing System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Text; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using Dissonance; using Dissonance.Audio.Codecs; using Dissonance.Integrations.Unity_NFGO; using Dissonance.Networking; using GameNetcodeStuff; using HarmonyLib; using Microsoft.CodeAnalysis; using Steamworks; using Steamworks.Data; using Unity.Netcode; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")] [assembly: AssemblyCompany("LethalGuardPlus")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("1.0.0.0")] [assembly: AssemblyInformationalVersion("1.0.0")] [assembly: AssemblyProduct("LethalGuardPlus")] [assembly: AssemblyTitle("LethalGuardPlus")] [assembly: AssemblyVersion("1.0.0.0")] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [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; } } } namespace LethalGuardPlus { internal static class ClientGuard { [HarmonyPatch] private static class PlayerJoinedPatch { private static MethodBase TargetMethod() { return AccessTools.Method("Dissonance.Networking.Client.EventQueue:EnqueuePlayerJoined", (Type[])null, (Type[])null); } [HarmonyPrefix] private static void Prefix(string playerName, ref CodecSettings codecSettings) { //IL_001d: Unknown result type (might be due to invalid IL or missing references) //IL_0033: Expected I4, but got Unknown //IL_0062: Unknown result type (might be due to invalid IL or missing references) //IL_0067: Unknown result type (might be due to invalid IL or missing references) string text = Validate.Name(playerName); if (text != null) { MarkSuspicious(playerName); Offenders.ReportVoicePlayer(playerName, "CLIENT-LONG-NAME", text); } string text2 = Validate.Codec((byte)(int)((CodecSettings)(ref codecSettings)).Codec, ((CodecSettings)(ref codecSettings)).FrameSize, ((CodecSettings)(ref codecSettings)).SampleRate); if (text2 != null) { MarkSuspicious(playerName); Offenders.ReportVoicePlayer(playerName, "CLIENT-CODEC", text2); if (Plugin.BlockPackets.Value) { codecSettings = new CodecSettings((Codec)1, 960u, 48000); } } } } [HarmonyPatch] private static class ReceiveFramePatch { private static MethodBase TargetMethod() { return AccessTools.Method("Dissonance.Audio.Playback.SpeechSessionStream:ReceiveFrame", (Type[])null, (Type[])null); } [HarmonyPrefix] private static bool Prefix(VoicePacket packet) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) string senderPlayerId = packet.SenderPlayerId; if (IsSuspicious(senderPlayerId)) { Offenders.ReportVoicePlayer(senderPlayerId, "CLIENT-SUSPICIOUS-VOICE", "voice frame from suspicious player"); return !Plugin.BlockPackets.Value; } int num = CountFrame(senderPlayerId ?? ""); if (num > Plugin.MaxVoiceFramesPerSecond.Value) { Offenders.ReportVoicePlayer(senderPlayerId, "CLIENT-VOICE-FLOOD", $"{num} frames per second (limit {Plugin.MaxVoiceFramesPerSecond.Value})"); return !Plugin.BlockPackets.Value; } return true; } } private static readonly object Lock = new object(); private static readonly HashSet<string> Suspicious = new HashSet<string>(); private static readonly Dictionary<string, (long start, int count)> FrameRates = new Dictionary<string, (long, int)>(); internal static bool IsSuspicious(string name) { lock (Lock) { return name != null && Suspicious.Contains(name); } } internal static void MarkSuspicious(string name) { lock (Lock) { Suspicious.Add(name ?? ""); } } internal static int CountFrame(string name) { long num = Environment.TickCount; lock (Lock) { FrameRates.TryGetValue(name, out (long, int) value); if (num - value.Item1 >= 1000) { value = (num, 0); } value.Item2++; FrameRates[name] = value; return value.Item2; } } } internal static class LobbyTag { internal static void Apply(Harmony harmony) { //IL_0044: Unknown result type (might be due to invalid IL or missing references) //IL_0051: Expected O, but got Unknown try { MethodInfo methodInfo = AccessTools.Method(typeof(GameNetworkManager), "SteamMatchmaking_OnLobbyCreated", (Type[])null, (Type[])null); if (methodInfo == null) { Plugin.Log.LogWarning((object)"HideModdedTag: lobby creation method not found, lobby stays in the modded list."); } else { harmony.Patch((MethodBase)methodInfo, (HarmonyMethod)null, new HarmonyMethod(typeof(LobbyTag), "Postfix", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); } } catch (Exception arg) { Plugin.Log.LogWarning((object)$"HideModdedTag: patch failed, lobby stays in the modded list. {arg}"); } } private static void Postfix(Result result, Lobby lobby) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0002: Invalid comparison between Unknown and I4 if ((int)result == 1 && Plugin.HideModdedTag.Value) { ((Lobby)(ref lobby)).SetData("dmods", "f"); Plugin.Log.LogInfo((object)"HideModdedTag: lobby listed as vanilla."); } } } internal static class Offenders { private class Counter { public int Pending; public long Total; public DateTime LastLog; } private const double SummaryIntervalSec = 5.0; private static readonly object Lock = new object(); private static readonly Dictionary<string, Counter> Counters = new Dictionary<string, Counter>(); private static readonly HashSet<ulong> Kicked = new HashSet<ulong>(); private static string _logFile; public static void Init(string bepinexRoot) { _logFile = Path.Combine(bepinexRoot, "LethalGuardPlus.log"); WriteFile($"===== Session started {DateTime.Now:yyyy-MM-dd HH:mm:ss} ====="); } public static void Report(ulong clientId, string rule, string detail) { string text = Describe(clientId); Emit($"{clientId}|{rule}", "[HOST] " + rule + " | " + text + " | " + detail); TryKick(clientId, rule); } public static void ReportVoicePlayer(string dissonanceName, string rule, string detail) { Emit("v:" + dissonanceName + "|" + rule, "[CLIENT] " + rule + " | dissonance=" + Validate.Short(dissonanceName) + " | " + detail); } private static void Emit(string key, string line) { int num = 0; bool flag; lock (Lock) { if (!Counters.TryGetValue(key, out var value)) { Dictionary<string, Counter> counters = Counters; Counter obj = new Counter { LastLog = DateTime.MinValue }; value = obj; counters[key] = obj; } value.Total++; value.Pending++; DateTime utcNow = DateTime.UtcNow; flag = (utcNow - value.LastLog).TotalSeconds >= 5.0; if (flag) { num = value.Pending - 1; value.Pending = 0; value.LastLog = utcNow; } } if (flag) { string text = ((num > 0) ? $"{line} (+{num} repeats in last {5.0:0}s)" : line); Plugin.Log.LogWarning((object)text); WriteFile($"[{DateTime.Now:HH:mm:ss.fff}] {text}"); } } public static string Describe(ulong clientId) { try { StartOfRound instance = StartOfRound.Instance; if ((Object)(object)instance != (Object)null && instance.allPlayerScripts != null) { PlayerControllerB[] allPlayerScripts = instance.allPlayerScripts; foreach (PlayerControllerB val in allPlayerScripts) { if ((Object)(object)val != (Object)null && val.actualClientId == clientId && (val.isPlayerControlled || val.isPlayerDead)) { return $"clientId={clientId} steamId={val.playerSteamId} name=\"{val.playerUsername}\""; } } } } catch { } return $"clientId={clientId} steamId=?"; } private static void TryKick(ulong clientId, string rule) { if (!Plugin.KickOffender.Value || rule == "RATE-LIMIT") { return; } NetworkManager singleton = NetworkManager.Singleton; if ((Object)(object)singleton == (Object)null || !singleton.IsServer || clientId == singleton.LocalClientId) { return; } lock (Lock) { if (!Kicked.Add(clientId)) { return; } } Plugin.Log.LogWarning((object)("[KICK] " + Describe(clientId) + " rule=" + rule)); WriteFile($"[{DateTime.Now:HH:mm:ss.fff}] [KICK] {Describe(clientId)} rule={rule}"); StartOfRound instance = StartOfRound.Instance; if ((Object)(object)instance != (Object)null) { for (int i = 0; i < instance.allPlayerScripts.Length; i++) { if (instance.allPlayerScripts[i].actualClientId == clientId) { instance.KickPlayer(i); return; } } } singleton.DisconnectClient(clientId); } private static void WriteFile(string line) { if (_logFile == null) { return; } try { lock (Lock) { File.AppendAllText(_logFile, line + Environment.NewLine); } } catch { } } } internal enum MsgType : byte { ClientState = 1, VoiceData, TextData, HandshakeRequest, HandshakeResponse, ErrorWrongSession, ServerRelayReliable, ServerRelayUnreliable, DeltaChannelState, RemoveClient, HandshakeP2P } internal class MalformedPacketException : Exception { public MalformedPacketException(string what) : base(what) { } } internal struct Packet { public const ushort Magic = 35783; private readonly byte[] _buf; private readonly int _end; private int _pos; public int Remaining => _end - _pos; public Packet(ArraySegment<byte> seg) { _buf = seg.Array; _pos = seg.Offset; _end = seg.Offset + seg.Count; } private void Need(int n, string what) { if (n < 0 || _end - _pos < n) { throw new MalformedPacketException(what); } } public byte U8() { Need(1, "byte"); return _buf[_pos++]; } public ushort U16() { Need(2, "ushort"); ushort result = (ushort)((_buf[_pos] << 8) | _buf[_pos + 1]); _pos += 2; return result; } public uint U32() { Need(4, "uint"); int result = (_buf[_pos] << 24) | (_buf[_pos + 1] << 16) | (_buf[_pos + 2] << 8) | _buf[_pos + 3]; _pos += 4; return (uint)result; } public string Str() { ushort num = U16(); if (num == 0) { return null; } int num2 = num - 1; Need(num2, "string"); string result = Encoding.UTF8.GetString(_buf, _pos, num2); _pos += num2; return result; } public ArraySegment<byte> Segment() { ushort num = U16(); Need(num, "byte[]"); ArraySegment<byte> result = new ArraySegment<byte>(_buf, _pos, num); _pos += num; return result; } public bool Header(out MsgType type) { type = (MsgType)0; if (Remaining < 3 || U16() != 35783) { return false; } type = (MsgType)U8(); return true; } public void Codec(out byte codec, out uint frameSize, out int sampleRate) { codec = U8(); frameSize = U32(); sampleRate = (int)U32(); } } internal static class Validate { private static readonly int[] SampleRates = new int[5] { 8000, 12000, 16000, 24000, 48000 }; public static string Codec(byte codec, uint frameSize, int sampleRate) { if (codec > 1) { return $"unknown codec={codec}"; } if (frameSize == 0 || frameSize > (uint)Plugin.MaxFrameSize.Value) { return $"FrameSize={frameSize} (limit {Plugin.MaxFrameSize.Value})"; } if (Array.IndexOf(SampleRates, sampleRate) < 0) { return $"SampleRate={sampleRate}"; } return null; } public static string Name(string name) { if (name == null) { return "name is null"; } if (name.Length > Plugin.MaxNameLength.Value) { return $"name is {name.Length} characters (limit {Plugin.MaxNameLength.Value}): {Short(name)}"; } return null; } public static string Short(string s) { if (s == null) { return "null"; } if (s.Length > 40) { return s.Substring(0, 40) + $"…(+{s.Length - 40})"; } return s; } } [BepInPlugin("lethalguardplus", "LethalGuard+", "1.0.2")] public class Plugin : BaseUnityPlugin { public const string Guid = "lethalguardplus"; public const string Name = "LethalGuard+"; public const string Version = "1.0.2"; internal static ManualLogSource Log; internal static ConfigEntry<bool> KickOffender; internal static ConfigEntry<bool> HideModdedTag; internal static ConfigEntry<bool> BlockPackets; internal static ConfigEntry<int> MaxNameLength; internal static ConfigEntry<int> MaxRoomNameLength; internal static ConfigEntry<int> MaxRooms; internal static ConfigEntry<int> MaxRelayDestinations; internal static ConfigEntry<int> MaxPacketsPerSecond; internal static ConfigEntry<int> MaxVoiceFramesPerSecond; internal static ConfigEntry<int> MaxFrameSize; private void Awake() { //IL_016d: Unknown result type (might be due to invalid IL or missing references) //IL_0172: Unknown result type (might be due to invalid IL or missing references) //IL_018c: Expected O, but got Unknown Log = ((BaseUnityPlugin)this).Logger; KickOffender = ((BaseUnityPlugin)this).Config.Bind<bool>("General", "KickOffender", false, "Host only. true: a player who sends an attack packet is kicked (rate limit violations only drop packets). false: the player stays, their packets are dropped."); HideModdedTag = ((BaseUnityPlugin)this).Config.Bind<bool>("General", "HideModdedTag", true, "Host only. true: your lobby is listed as a normal (vanilla) lobby, so players see it without ticking the modded filter. Set false if you also run mods that change gameplay and every player must have them."); BlockPackets = ((BaseUnityPlugin)this).Config.Bind<bool>("Voice", "BlockPackets", true, "true: Dissonance packets that break the rules are dropped and logged. false: ONLY logged, packets reach the game (the attack really works and the game can crash)."); MaxNameLength = ((BaseUnityPlugin)this).Config.Bind<int>("Voice", "MaxNameLength", 64, "Max characters for a Dissonance player name (a normal name is a 36 character GUID)."); MaxRoomNameLength = ((BaseUnityPlugin)this).Config.Bind<int>("Voice", "MaxRoomNameLength", 128, "Max characters for a room name."); MaxRooms = ((BaseUnityPlugin)this).Config.Bind<int>("Voice", "MaxRooms", 32, "Max rooms in one ClientState packet."); MaxRelayDestinations = ((BaseUnityPlugin)this).Config.Bind<int>("Voice", "MaxRelayDestinations", 64, "Max destinations in one relay packet."); MaxPacketsPerSecond = ((BaseUnityPlugin)this).Config.Bind<int>("Voice", "MaxPacketsPerSecond", 200, "Host: max Dissonance packets accepted per second from one connection (normal talking is ~50)."); MaxVoiceFramesPerSecond = ((BaseUnityPlugin)this).Config.Bind<int>("Voice", "MaxVoiceFramesPerSecond", 150, "Client: max voice frames played per second from one player."); MaxFrameSize = ((BaseUnityPlugin)this).Config.Bind<int>("Voice", "MaxFrameSize", 5760, "Codec FrameSize upper limit (Opus max is 120ms@48kHz = 5760)."); Offenders.Init(Paths.BepInExRootPath); Harmony val = new Harmony("lethalguardplus"); val.PatchAll(typeof(Plugin).Assembly); LobbyTag.Apply(val); Log.LogInfo((object)string.Format("{0} {1} loaded. BlockPackets={2} KickOffender={3}", "LethalGuard+", "1.0.2", BlockPackets.Value, KickOffender.Value)); } } [HarmonyPatch(typeof(NfgoServer), "NamedMessageHandler")] internal static class ServerGuard { private class Rate { public long WindowStart; public int Count; } private static readonly FieldRef<NfgoServer, byte[]> ReceiveBuffer = AccessTools.FieldRefAccess<NfgoServer, byte[]>("_receiveBuffer"); private static readonly FieldInfo ClientsField = AccessTools.Field(typeof(BaseServer<NfgoServer, NfgoClient, NfgoConn>), "_clients"); private static MethodInfo _findByConnection; private static readonly Dictionary<ulong, Rate> Rates = new Dictionary<ulong, Rate>(); [HarmonyPrefix] private static bool Prefix(NfgoServer __instance, ulong sender, FastBufferReader stream) { //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_000c: Unknown result type (might be due to invalid IL or missing references) //IL_00e8: Unknown result type (might be due to invalid IL or missing references) ArraySegment<byte> arraySegment; try { uint num = default(uint); ((FastBufferReader)(ref stream)).ReadValueSafe<uint>(ref num, default(ForPrimitives)); int num2 = ((FastBufferReader)(ref stream)).Length - ((FastBufferReader)(ref stream)).Position; if (num > (uint)num2) { Offenders.Report(sender, "FAKE-LENGTH", $"declared={num} bytes, actual={num2} bytes"); if (Plugin.BlockPackets.Value) { return false; } num = (uint)num2; } ref byte[] reference = ref ReceiveBuffer.Invoke(__instance); if (reference == null || reference.Length < num) { reference = new byte[Math.Max(1024u, num)]; } ((FastBufferReader)(ref stream)).ReadBytesSafe(ref reference, (int)num, 0); arraySegment = new ArraySegment<byte>(reference, 0, (int)num); } catch (Exception ex) { Offenders.Report(sender, "UNREADABLE-PACKET", ex.Message); return false; } string rule; string text = Check(__instance, sender, arraySegment, out rule); if (text != null) { Offenders.Report(sender, rule, text); if (Plugin.BlockPackets.Value) { return false; } } ((BaseServer<NfgoServer, NfgoClient, NfgoConn>)(object)__instance).NetworkReceivedPacket(new NfgoConn(sender), arraySegment); return false; } private static string Check(NfgoServer server, ulong sender, ArraySegment<byte> data, out string rule) { rule = "RATE-LIMIT"; int num = CountPacket(sender); if (num > Plugin.MaxPacketsPerSecond.Value) { return $"{num} packets per second (limit {Plugin.MaxPacketsPerSecond.Value})"; } Packet p = new Packet(data); try { if (!p.Header(out var type)) { return null; } switch (type) { case MsgType.HandshakeRequest: return CheckHandshake(ref p, out rule); case MsgType.ClientState: return CheckClientState(server, sender, ref p, out rule); case MsgType.DeltaChannelState: return CheckDelta(server, sender, ref p, out rule); case MsgType.ServerRelayReliable: case MsgType.ServerRelayUnreliable: return CheckRelay(server, sender, ref p, out rule); default: return null; } } catch (MalformedPacketException ex) { rule = "MALFORMED-PACKET"; return "unreadable: " + ex.Message; } } private static string CheckHandshake(ref Packet p, out string rule) { p.Codec(out var codec, out var frameSize, out var sampleRate); string text = p.Str(); rule = "HANDSHAKE-CODEC"; string text2 = Validate.Codec(codec, frameSize, sampleRate); if (text2 != null) { return text2 + " name=" + Validate.Short(text); } rule = "HANDSHAKE-NAME"; return Validate.Name(text); } private static string CheckClientState(NfgoServer server, ulong sender, ref Packet p, out string rule) { p.U32(); string text = p.Str(); ushort num = p.U16(); p.Codec(out var codec, out var frameSize, out var sampleRate); rule = "CLIENTSTATE-NAME"; string text2 = Validate.Name(text); if (text2 != null) { return text2; } rule = "CLIENTSTATE-CODEC"; text2 = Validate.Codec(codec, frameSize, sampleRate); if (text2 != null) { return text2 + " name=" + Validate.Short(text); } rule = "SPOOFED-PLAYER"; if (!TryGetOwner(server, sender, out var id, out var name)) { return $"sent ClientState before handshake (id={num} name={Validate.Short(text)})"; } if (num != id || text != name) { return $"claims another identity: id={num} name={Validate.Short(text)} (own: id={id} name={Validate.Short(name)})"; } ushort num2 = p.U16(); rule = "CLIENTSTATE-ROOM"; if (num2 > Plugin.MaxRooms.Value) { return $"{num2} rooms (limit {Plugin.MaxRooms.Value})"; } for (int i = 0; i < num2; i++) { string text3 = p.Str(); if (text3 != null && text3.Length > Plugin.MaxRoomNameLength.Value) { return $"room name is {text3.Length} characters: {Validate.Short(text3)}"; } } return null; } private static string CheckDelta(NfgoServer server, ulong sender, ref Packet p, out string rule) { p.U32(); p.U8(); ushort num = p.U16(); string text = p.Str(); rule = "DELTA-SPOOFED-PLAYER"; if (!TryGetOwner(server, sender, out var id, out var _)) { return $"sent DeltaChannelState before handshake (peer={num})"; } if (num != id) { return $"changing another player's rooms: peer={num} (own id={id})"; } rule = "DELTA-ROOM"; if (text != null && text.Length > Plugin.MaxRoomNameLength.Value) { return $"room name is {text.Length} characters: {Validate.Short(text)}"; } return null; } private static string CheckRelay(NfgoServer server, ulong sender, ref Packet p, out string rule) { p.U32(); int num = p.U8(); rule = "RELAY-TARGETS"; if (num > Plugin.MaxRelayDestinations.Value) { return $"{num} destinations (limit {Plugin.MaxRelayDestinations.Value})"; } for (int i = 0; i < num; i++) { p.U16(); } Packet packet = new Packet(p.Segment()); rule = "RELAY-CONTENT"; if (!packet.Header(out var type)) { return null; } switch (type) { case MsgType.HandshakeP2P: return null; default: return $"packet type not allowed inside relay: {type}"; case MsgType.VoiceData: case MsgType.TextData: { packet.U32(); if (type == MsgType.TextData) { packet.U8(); } ushort num2 = packet.U16(); rule = ((type == MsgType.VoiceData) ? "SPOOFED-VOICE" : "SPOOFED-TEXT"); if (!TryGetOwner(server, sender, out var id, out var _)) { return $"relaying {type} before handshake (sender id={num2})"; } if (num2 != id) { return $"{type} on behalf of another player: sender id={num2} (own id={id})"; } return null; } } } private static int CountPacket(ulong sender) { long num = Environment.TickCount; if (!Rates.TryGetValue(sender, out var value)) { Dictionary<ulong, Rate> rates = Rates; Rate obj = new Rate { WindowStart = num }; value = obj; rates[sender] = obj; } if (num - value.WindowStart >= 1000) { value.WindowStart = num; value.Count = 0; } return ++value.Count; } private static bool TryGetOwner(NfgoServer server, ulong sender, out ushort id, out string name) { //IL_005a: Unknown result type (might be due to invalid IL or missing references) id = 0; name = null; object obj = ClientsField?.GetValue(server); if (obj == null) { return false; } if (_findByConnection == null) { _findByConnection = AccessTools.Method(obj.GetType(), "TryFindClientByConnection", (Type[])null, (Type[])null); } if (_findByConnection == null) { return false; } object[] array = new object[2] { (object)new NfgoConn(sender), null }; if (!(bool)_findByConnection.Invoke(obj, array) || !(array[1] is ClientInfo<NfgoConn> val)) { return false; } id = val.PlayerId; name = val.PlayerName; return true; } } }