Merge branch 'TestNT2-WebGL-NT-U' of https://github.com/MirrorNetworking/Mirror into TestNT2-WebGL-NT-U

This commit is contained in:
ninjakickja 2023-04-10 07:38:01 +08:00
commit 8ad45ad071
21 changed files with 245 additions and 64 deletions

View File

@ -13,8 +13,8 @@ jobs:
unityVersion:
- 2019.4.40f1
- 2020.3.46f1
- 2021.3.21f1
- 2022.2.12f1
- 2021.3.22f1
- 2022.2.13f1
steps:
- name: Checkout repository

View File

@ -23,7 +23,8 @@ public static void AddDefineSymbols()
"MIRROR_2022_10_OR_NEWER",
"MIRROR_70_0_OR_NEWER",
"MIRROR_71_0_OR_NEWER",
"MIRROR_73_OR_NEWER"
"MIRROR_73_OR_NEWER",
"MIRROR_78_OR_NEWER"
// Remove oldest when adding next month's symbol.
// Keep a rolling 12 months of symbols.
};

View File

@ -36,9 +36,6 @@ public class NetworkTransform : NetworkTransformBase
protected bool hasSentUnchangedPosition;
#endif
double lastClientSendTime;
double lastServerSendTime;
// update //////////////////////////////////////////////////////////////
// Update applies interpolation
void Update()

View File

@ -317,7 +317,8 @@ protected void SendCommandInternal(string functionFullName, int functionHashCode
return;
}
// local players can always send commands, regardless of authority, other objects must have authority.
// local players can always send commands, regardless of authority,
// other objects must have authority.
if (!(!requiresAuthority || isLocalPlayer || isOwned))
{
Debug.LogWarning($"Command Function {functionFullName} called on {name} without authority.", gameObject);
@ -648,7 +649,9 @@ protected void SetSyncVarGameObject(GameObject newGameObject, ref GameObject gam
protected GameObject GetSyncVarGameObject(uint netId, ref GameObject gameObjectField)
{
// server always uses the field
if (isServer)
// if neither, fallback to original field
// fixes: https://github.com/MirrorNetworking/Mirror/issues/3447
if (isServer || !isClient)
{
return gameObjectField;
}
@ -956,7 +959,9 @@ protected void SetSyncVarNetworkIdentity(NetworkIdentity newIdentity, ref Networ
protected NetworkIdentity GetSyncVarNetworkIdentity(uint netId, ref NetworkIdentity identityField)
{
// server always uses the field
if (isServer)
// if neither, fallback to original field
// fixes: https://github.com/MirrorNetworking/Mirror/issues/3447
if (isServer || !isClient)
{
return identityField;
}
@ -1019,7 +1024,9 @@ protected void SetSyncVarNetworkBehaviour<T>(T newBehaviour, ref T behaviourFiel
protected T GetSyncVarNetworkBehaviour<T>(NetworkBehaviourSyncVar syncNetBehaviour, ref T behaviourField) where T : NetworkBehaviour
{
// server always uses the field
if (isServer)
// if neither, fallback to original field
// fixes: https://github.com/MirrorNetworking/Mirror/issues/3447
if (isServer || !isClient)
{
return behaviourField;
}

View File

@ -0,0 +1,13 @@
// convenience interface for transports which use a port.
// useful for cases where someone wants to 'just set the port' independent of
// which transport it is.
//
// note that not all transports have ports, but most do.
namespace Mirror
{
public interface PortTransport
{
ushort Port { get; set; }
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f7c7c2820d7974cb28c7bfe9aae890a0
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -10,10 +10,11 @@ public static class CommandProcessor
// generates code like:
public void CmdThrust(float thrusting, int spin)
{
NetworkWriter networkWriter = new NetworkWriter();
networkWriter.Write(thrusting);
networkWriter.WritePackedUInt32((uint)spin);
base.SendCommandInternal(cmdName, networkWriter, channel);
NetworkWriterPooled writer = NetworkWriterPool.Get();
writer.Write(thrusting);
writer.WritePackedUInt32((uint)spin);
base.SendCommandInternal(cmdName, cmdHash, writer, channel);
NetworkWriterPool.Return(writer);
}
public void CallCmdThrust(float thrusting, int spin)

View File

@ -17,14 +17,6 @@ public override void Awake()
singleton = this;
}
#if UNITY_SERVER
public override void Start()
{
((SimpleWeb.SimpleWebTransport)Transport.active).port = 27778;
base.Start();
}
#endif
/// <summary>
/// Called on the server when a client adds a new player with NetworkClient.AddPlayer.
/// <para>The default implementation for this function creates a new player object from the playerPrefab.</para>

View File

@ -1,6 +1,3 @@
using System.Collections.Generic;
using UnityEngine;
namespace Mirror.Examples.Chat
{
public class Player : NetworkBehaviour

View File

@ -597,15 +597,13 @@ public void GetSyncVarGameObjectOnClient()
NetworkServer.Listen(1);
ConnectClientBlockingAuthenticatedAndReady(out _);
CreateNetworked(out GameObject _, out NetworkIdentity identity);
// create a networked object with test component
CreateNetworked(out GameObject _, out NetworkIdentity identity, out NetworkBehaviourGetSyncVarGameObjectComponent comp);
// are we on client and not on server?
identity.isClient = true;
Assert.That(identity.isServer, Is.False);
// create a networked object with test component
CreateNetworked(out GameObject _, out NetworkIdentity _, out NetworkBehaviourGetSyncVarGameObjectComponent comp);
// create a spawned, syncable GameObject
// (client tries to look up via netid, so needs to be spawned)
CreateNetworkedAndSpawn(

View File

@ -392,6 +392,7 @@ public void TestSyncingAbstractNetworkBehaviour()
{
// set up a "server" object
CreateNetworked(out _, out NetworkIdentity serverIdentity, out SyncVarAbstractNetworkBehaviour serverBehaviour);
serverIdentity.isServer = true;
// spawn syncvar targets
CreateNetworked(out _, out NetworkIdentity wolfIdentity, out SyncVarAbstractNetworkBehaviour.MockWolf wolf);
@ -400,6 +401,10 @@ public void TestSyncingAbstractNetworkBehaviour()
wolfIdentity.netId = 135;
zombieIdentity.netId = 246;
// add to spawned as if they were spawned on clients
NetworkClient.spawned.Add(wolfIdentity.netId, wolfIdentity);
NetworkClient.spawned.Add(zombieIdentity.netId, zombieIdentity);
serverBehaviour.monster1 = wolf;
serverBehaviour.monster2 = zombie;
@ -411,14 +416,62 @@ public void TestSyncingAbstractNetworkBehaviour()
// set up a "client" object
CreateNetworked(out _, out NetworkIdentity clientIdentity, out SyncVarAbstractNetworkBehaviour clientBehaviour);
clientIdentity.isClient = true;
// apply all the data from the server object
NetworkReader reader = new NetworkReader(ownerWriter.ToArray());
clientIdentity.DeserializeClient(reader, true);
// check that the syncvars got updated
Debug.Log($"{clientBehaviour.monster1} and {serverBehaviour.monster1}");
Assert.That(clientBehaviour.monster1, Is.EqualTo(serverBehaviour.monster1), "Data should be synchronized");
Assert.That(clientBehaviour.monster2, Is.EqualTo(serverBehaviour.monster2), "Data should be synchronized");
// remove spawned objects
NetworkClient.spawned.Remove(wolfIdentity.netId);
NetworkClient.spawned.Remove(zombieIdentity.netId);
}
// Tests if getter for GameObject SyncVar field returns proper value on server before the containing object is spawned.
[Test]
public void SyncVarGameObjectGetterOnServerBeforeSpawn()
{
// The test should only need server objects, but at the same time this belongs in SyncVar tests,
// and objects in the tests defined here need client objects to spawn.
CreateNetworkedAndSpawn(
out GameObject serverGO, out NetworkIdentity serverIdentity, out SyncVarNetworkBehaviour serverNB,
out _, out _, out _);
CreateNetworked(out _, out _, out SyncVarGameObject serverComponent);
serverComponent.value = serverGO;
Assert.That(serverComponent.value, Is.EqualTo(serverGO), "getter should return original field value on server");
}
[Test]
public void SyncVarNetworkIdentityGetterOnServerBeforeSpawn()
{
CreateNetworkedAndSpawn(
out GameObject serverGO, out NetworkIdentity serverIdentity, out SyncVarNetworkBehaviour serverNB,
out _, out _, out _);
CreateNetworked(out _, out _, out SyncVarNetworkIdentity serverComponent);
serverComponent.value = serverIdentity;
Assert.That(serverComponent.value, Is.EqualTo(serverIdentity), "getter should return original field value on server");
}
[Test]
public void SyncVarNetworkBehaviourGetterOnServerBeforeSpawn()
{
CreateNetworkedAndSpawn(
out GameObject serverGO, out NetworkIdentity serverIdentity, out SyncVarNetworkBehaviour serverNB,
out _, out _, out _);
CreateNetworked(out _, out _, out SyncVarNetworkBehaviour serverComponent);
serverComponent.value = serverNB;
Assert.That(serverComponent.value, Is.EqualTo(serverNB), "getter should return original field value on server");
}
}
}

View File

@ -11,14 +11,16 @@ namespace kcp2k
{
[HelpURL("https://mirror-networking.gitbook.io/docs/transports/kcp-transport")]
[DisallowMultipleComponent]
public class KcpTransport : Transport
public class KcpTransport : Transport, PortTransport
{
// scheme used by this transport
public const string Scheme = "kcp";
// common
[Header("Transport Configuration")]
public ushort Port = 7777;
[FormerlySerializedAs("Port")]
public ushort port = 7777;
public ushort Port { get => port; set => port=value; }
[Tooltip("DualMode listens to IPv6 and IPv4 simultaneously. Disable if the platform only supports IPv4.")]
public bool DualMode = true;
[Tooltip("NoDelay is recommended to reduce latency. This also scales better without buffers getting full.")]

View File

@ -1,3 +1,11 @@
V1.35 [2023-04-05]
- fix: KcpClients now need to validate with a secure cookie in order to protect against
UDP spoofing. fixes:
https://github.com/MirrorNetworking/Mirror/issues/3286
[disclosed by IncludeSec]
- KcpClient/Server: change callbacks to protected so inheriting classes can use them too
- KcpClient/Server: change config visibility to protected
V1.34 [2023-03-15]
- Send/SendTo/Receive/ReceiveFrom NonBlocking extensions.
to encapsulate WouldBlock allocations, exceptions, etc.

View File

@ -1,5 +1,7 @@
using System;
using System.Net;
using System.Net.Sockets;
using System.Security.Cryptography;
namespace kcp2k
{
@ -45,5 +47,29 @@ public static void ConfigureSocketBuffers(Socket socket, int recvBufferSize, int
Log.Info($"Kcp: RecvBuf = {initialReceive}=>{socket.ReceiveBufferSize} ({socket.ReceiveBufferSize/initialReceive}x) SendBuf = {initialSend}=>{socket.SendBufferSize} ({socket.SendBufferSize/initialSend}x)");
}
// generate a connection hash from IP+Port.
//
// NOTE: IPEndPoint.GetHashCode() allocates.
// it calls m_Address.GetHashCode().
// m_Address is an IPAddress.
// GetHashCode() allocates for IPv6:
// https://github.com/mono/mono/blob/bdd772531d379b4e78593587d15113c37edd4a64/mcs/class/referencesource/System/net/System/Net/IPAddress.cs#L699
//
// => using only newClientEP.Port wouldn't work, because
// different connections can have the same port.
public static int ConnectionHash(EndPoint endPoint) =>
endPoint.GetHashCode();
// cookies need to be generated with a secure random generator.
// we don't want them to be deterministic / predictable.
// RNG is cached to avoid runtime allocations.
static readonly RNGCryptoServiceProvider cryptoRandom = new RNGCryptoServiceProvider();
static readonly byte[] cryptoRandomBuffer = new byte[4];
public static uint GenerateCookie()
{
cryptoRandom.GetBytes(cryptoRandomBuffer);
return BitConverter.ToUInt32(cryptoRandomBuffer, 0);
}
}
}

View File

@ -79,7 +79,8 @@ public void Connect(string address, ushort port)
}
// create fresh peer for each new session
peer = new KcpPeer(RawSend, OnAuthenticatedWrap, OnData, OnDisconnectedWrap, OnError, config);
// client doesn't need secure cookie.
peer = new KcpPeer(RawSend, OnAuthenticatedWrap, OnData, OnDisconnectedWrap, OnError, config, 0);
// some callbacks need to wrapped with some extra logic
void OnAuthenticatedWrap()

View File

@ -15,6 +15,21 @@ public class KcpPeer
// kcp reliability algorithm
internal Kcp kcp;
// security cookie to prevent UDP spoofing.
// credits to IncludeSec for disclosing the issue.
//
// server passes the expected cookie to the client's KcpPeer.
// KcpPeer sends cookie to the connected client.
// KcpPeer only accepts packets which contain the cookie.
// => cookie can be a random number, but it needs to be cryptographically
// secure random that can't be easily predicted.
// => cookie can be hash(ip, port) BUT only if salted to be not predictable
readonly uint cookie;
// this is the cookie that the other end received during handshake.
// store byte[] representation to avoid runtime int->byte[] conversions.
internal readonly byte[] receivedCookie = new byte[4];
// IO agnostic
readonly Action<ArraySegment<byte>> RawSend;
@ -44,10 +59,12 @@ public class KcpPeer
// Unity's time.deltaTime over long periods.
readonly Stopwatch watch = new Stopwatch();
// we need to subtract the channel byte from every MaxMessageSize
// calculation.
// we need to subtract the channel and cookie bytes from every
// MaxMessageSize calculation.
// we also need to tell kcp to use MTU-1 to leave space for the byte.
const int CHANNEL_HEADER_SIZE = 1;
const int COOKIE_HEADER_SIZE = 4;
const int METADATA_SIZE = CHANNEL_HEADER_SIZE + COOKIE_HEADER_SIZE;
// reliable channel (= kcp) MaxMessageSize so the outside knows largest
// allowed message to send. the calculation in Send() is not obvious at
@ -72,7 +89,7 @@ public class KcpPeer
// => sending UNRELIABLE max message size most of the time is
// best for performance (use that one for batching!)
static int ReliableMaxMessageSize_Unconstrained(int mtu, uint rcv_wnd) =>
(mtu - Kcp.OVERHEAD - CHANNEL_HEADER_SIZE) * ((int)rcv_wnd - 1) - 1;
(mtu - Kcp.OVERHEAD - METADATA_SIZE) * ((int)rcv_wnd - 1) - 1;
// kcp encodes 'frg' as 1 byte.
// max message size can only ever allow up to 255 fragments.
@ -84,7 +101,7 @@ public static int ReliableMaxMessageSize(int mtu, uint rcv_wnd) =>
// unreliable max message size is simply MTU - channel header size
public static int UnreliableMaxMessageSize(int mtu) =>
mtu - CHANNEL_HEADER_SIZE;
mtu - METADATA_SIZE;
// buffer to receive kcp's processed messages (avoids allocations).
// IMPORTANT: this is for KCP messages. so it needs to be of size:
@ -153,7 +170,8 @@ public KcpPeer(
Action<ArraySegment<byte>, KcpChannel> OnData,
Action OnDisconnected,
Action<ErrorCode, string> OnError,
KcpConfig config)
KcpConfig config,
uint cookie)
{
// initialize callbacks first to ensure they can be used safely.
this.OnAuthenticated = OnAuthenticated;
@ -165,6 +183,9 @@ public KcpPeer(
// set up kcp over reliable channel (that's what kcp is for)
kcp = new Kcp(0, RawSendReliable);
// security cookie
this.cookie = cookie;
// set nodelay.
// note that kcp uses 'nocwnd' internally so we negate the parameter
kcp.SetNoDelay(config.NoDelay ? 1u : 0u, config.Interval, config.FastResend, !config.CongestionWindow);
@ -174,7 +195,7 @@ public KcpPeer(
// message. so while Kcp.MTU_DEF is perfect, we actually need to
// tell kcp to use MTU-1 so we can still put the header into the
// message afterwards.
kcp.SetMtu((uint)config.Mtu - CHANNEL_HEADER_SIZE);
kcp.SetMtu((uint)config.Mtu - METADATA_SIZE);
// create mtu sized send buffer
rawSendBuffer = new byte[config.Mtu];
@ -320,8 +341,22 @@ void TickIncoming_Connected(uint time)
{
// we were waiting for a handshake.
// it proves that the other end speaks our protocol.
// GetType() shows Server/ClientConn instead of just Connection.
Log.Info($"KcpPeer: received handshake");
// parse the cookie
if (message.Count != 4)
{
// pass error to user callback. no need to log it manually.
OnError(ErrorCode.InvalidReceive, $"KcpPeer: received invalid handshake message with size {message.Count} != 4. Disconnecting the connection.");
Disconnect();
return;
}
// store the cookie bytes to avoid int->byte[] conversions when sending.
// still convert to uint once, just for prettier logging.
Buffer.BlockCopy(message.Array, message.Offset, receivedCookie, 0, 4);
uint prettyCookie = BitConverter.ToUInt32(message.Array, message.Offset);
Log.Info($"KcpPeer: received handshake with cookie={prettyCookie}");
state = KcpState.Authenticated;
OnAuthenticated?.Invoke();
break;
@ -570,8 +605,21 @@ public void RawInput(ArraySegment<byte> segment)
// byte channel = segment[0]; ArraySegment[i] isn't supported in some older Unity Mono versions
byte channel = segment.Array[segment.Offset + 0];
// parse cookie
uint messageCookie = BitConverter.ToUInt32(segment.Array, segment.Offset + 1);
// compare cookie to protect against UDP spoofing.
// messages won't have a cookie until after handshake.
// so only compare if we are authenticated.
// simply drop the message if the cookie doesn't match.
if (state == KcpState.Authenticated && messageCookie != cookie)
{
Log.Warning($"KcpPeer: dropped message with invalid cookie: {messageCookie} expected: {cookie}.");
return;
}
// parse message
ArraySegment<byte> message = new ArraySegment<byte>(segment.Array, segment.Offset + 1, segment.Count - 1);
ArraySegment<byte> message = new ArraySegment<byte>(segment.Array, segment.Offset + 1 + 4, segment.Count - 1 - 4);
switch (channel)
{
@ -599,12 +647,20 @@ public void RawInput(ArraySegment<byte> segment)
// raw send called by kcp
void RawSendReliable(byte[] data, int length)
{
// copy channel header, data into raw send buffer, then send
// write channel header
// from 0, with 1 byte
rawSendBuffer[0] = (byte)KcpChannel.Reliable;
Buffer.BlockCopy(data, 0, rawSendBuffer, 1, length);
// write handshake cookie to protect against UDP spoofing.
// from 1, with 4 bytes
Buffer.BlockCopy(receivedCookie, 0, rawSendBuffer, 1, 4);
// write data
// from 5, with N bytes
Buffer.BlockCopy(data, 0, rawSendBuffer, 1+4, length);
// IO send
ArraySegment<byte> segment = new ArraySegment<byte>(rawSendBuffer, 0, length + 1);
ArraySegment<byte> segment = new ArraySegment<byte>(rawSendBuffer, 0, length + 1 + 4);
RawSend(segment);
}
@ -619,8 +675,10 @@ void SendReliable(KcpHeader header, ArraySegment<byte> content)
return;
}
// copy header, content (if any) into send buffer
// write channel header
kcpSendBuffer[0] = (byte)header;
// write data (if any)
if (content.Count > 0)
Buffer.BlockCopy(content.Array, content.Offset, kcpSendBuffer, 1, content.Count);
@ -644,12 +702,20 @@ void SendUnreliable(ArraySegment<byte> message)
return;
}
// copy channel header, data into raw send buffer, then send
// write channel header
// from 0, with 1 byte
rawSendBuffer[0] = (byte)KcpChannel.Unreliable;
Buffer.BlockCopy(message.Array, message.Offset, rawSendBuffer, 1, message.Count);
// write handshake cookie to protect against UDP spoofing.
// from 1, with 4 bytes
Buffer.BlockCopy(receivedCookie, 0, rawSendBuffer, 1, 4);
// write data
// from 5, with N bytes
Buffer.BlockCopy(message.Array, message.Offset, rawSendBuffer, 1 + 4, message.Count);
// IO send
ArraySegment<byte> segment = new ArraySegment<byte>(rawSendBuffer, 0, message.Count + 1);
ArraySegment<byte> segment = new ArraySegment<byte>(rawSendBuffer, 0, message.Count + 1 + 4);
RawSend(segment);
}
@ -661,9 +727,18 @@ void SendUnreliable(ArraySegment<byte> message)
// => handshake info needs to be delivered, so it goes over reliable.
public void SendHandshake()
{
// server includes a random cookie in handshake.
// client is expected to include in every message.
// this avoid UDP spoofing.
// KcpPeer simply always sends a cookie.
// in case client -> server cookies are ever implemented, etc.
// TODO nonalloc
byte[] cookieBytes = BitConverter.GetBytes(cookie);
// GetType() shows Server/ClientConn instead of just Connection.
Log.Info($"KcpPeer: sending Handshake to other end!");
SendReliable(KcpHeader.Handshake, default);
Log.Info($"KcpPeer: sending Handshake to other end with cookie={cookie}!");
SendReliable(KcpHeader.Handshake, new ArraySegment<byte>(cookieBytes));
}
public void SendData(ArraySegment<byte> data, KcpChannel channel)

View File

@ -157,15 +157,7 @@ protected virtual bool RawReceiveFrom(out ArraySegment<byte> segment, out int co
if (socket.ReceiveFromNonBlocking(rawReceiveBuffer, out segment, ref newClientEP))
{
// set connectionId to hash from endpoint
// NOTE: IPEndPoint.GetHashCode() allocates.
// it calls m_Address.GetHashCode().
// m_Address is an IPAddress.
// GetHashCode() allocates for IPv6:
// https://github.com/mono/mono/blob/bdd772531d379b4e78593587d15113c37edd4a64/mcs/class/referencesource/System/net/System/Net/IPAddress.cs#L699
//
// => using only newClientEP.Port wouldn't work, because
// different connections can have the same port.
connectionId = newClientEP.GetHashCode();
connectionId = Common.ConnectionHash(newClientEP);
return true;
}
}
@ -214,8 +206,12 @@ protected virtual KcpServerConnection CreateConnection(int connectionId)
// afterwards we assign the peer.
KcpServerConnection connection = new KcpServerConnection(newClientEP);
// generate a random cookie for this connection to avoid UDP spoofing.
// needs to be random, but without allocations to avoid GC.
uint cookie = Common.GenerateCookie();
// set up peer with callbacks
KcpPeer peer = new KcpPeer(RawSendWrap, OnAuthenticatedWrap, OnDataWrap, OnDisconnectedWrap, OnErrorWrap, config);
KcpPeer peer = new KcpPeer(RawSendWrap, OnAuthenticatedWrap, OnDataWrap, OnDisconnectedWrap, OnErrorWrap, config, cookie);
// assign peer to connection
connection.peer = peer;

View File

@ -7,13 +7,14 @@
namespace Mirror.SimpleWeb
{
[DisallowMultipleComponent]
public class SimpleWebTransport : Transport
public class SimpleWebTransport : Transport, PortTransport
{
public const string NormalScheme = "ws";
public const string SecureScheme = "wss";
[Tooltip("Port to use for server and client")]
public ushort port = 7778;
public ushort Port { get => port; set => port=value; }
[Tooltip("Protect against allocation attacks by keeping the max message size small. Otherwise an attacker might send multiple fake packets with 2GB headers, causing the server to run out of memory after allocating multiple large packets.")]
public int maxMessageSize = 16 * 1024;

View File

@ -9,13 +9,14 @@ namespace Mirror
{
[HelpURL("https://github.com/vis2k/Telepathy/blob/master/README.md")]
[DisallowMultipleComponent]
public class TelepathyTransport : Transport
public class TelepathyTransport : Transport, PortTransport
{
// scheme used by this transport
// "tcp4" means tcp with 4 bytes header, network byte order
public const string Scheme = "tcp4";
public ushort port = 7777;
public ushort Port { get => port; set => port=value; }
[Header("Common")]
[Tooltip("Nagle Algorithm can be disabled by enabling NoDelay")]

View File

@ -580,7 +580,7 @@ PlayerSettings:
webGLPowerPreference: 2
scriptingDefineSymbols:
Server: MIRROR;MIRROR_57_0_OR_NEWER;MIRROR_58_0_OR_NEWER;MIRROR_65_0_OR_NEWER;MIRROR_66_0_OR_NEWER;MIRROR_2022_9_OR_NEWER;MIRROR_2022_10_OR_NEWER;MIRROR_70_0_OR_NEWER;MIRROR_71_0_OR_NEWER;MIRROR_73_OR_NEWER
Standalone: MIRROR;MIRROR_57_0_OR_NEWER;MIRROR_58_0_OR_NEWER;MIRROR_65_0_OR_NEWER;MIRROR_66_0_OR_NEWER;MIRROR_2022_9_OR_NEWER;MIRROR_2022_10_OR_NEWER;MIRROR_70_0_OR_NEWER;MIRROR_71_0_OR_NEWER;MIRROR_73_OR_NEWER
Standalone: MIRROR;MIRROR_57_0_OR_NEWER;MIRROR_58_0_OR_NEWER;MIRROR_65_0_OR_NEWER;MIRROR_66_0_OR_NEWER;MIRROR_2022_9_OR_NEWER;MIRROR_2022_10_OR_NEWER;MIRROR_70_0_OR_NEWER;MIRROR_71_0_OR_NEWER;MIRROR_73_OR_NEWER;MIRROR_78_OR_NEWER
WebGL: MIRROR;MIRROR_57_0_OR_NEWER;MIRROR_58_0_OR_NEWER;MIRROR_65_0_OR_NEWER;MIRROR_66_0_OR_NEWER;MIRROR_2022_9_OR_NEWER;MIRROR_2022_10_OR_NEWER;MIRROR_70_0_OR_NEWER;MIRROR_71_0_OR_NEWER;MIRROR_73_OR_NEWER
additionalCompilerArguments: {}
platformArchitecture: {}

View File

@ -243,6 +243,7 @@ A lot of projects use Mirror in production. If you found a critical bug / exploi
**Credits / past findings / fixes:**
* 2020, fholm: fuzzing ConnectMessage to stop further connects [[#2397](https://github.com/vis2k/Mirror/pull/2397)]
* 2023-04-05: IncludeSec: [kcp2k UDP spoofing](http://blog.includesecurity.com/?p=1407) [[#3286](https://github.com/vis2k/Mirror/pull/2397)]
---
# Credits & Thanks 🙏