Decompiled source of LethalGuardPlus v1.0.2

LethalGuardPlus.dll

Decompiled a day ago
using 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;
		}
	}
}