This commit is contained in:
2025-06-16 15:14:23 +02:00
committed by devbeni
parent 60fe4620ff
commit 4ff561284f
3174 changed files with 428263 additions and 0 deletions

View File

@ -0,0 +1,75 @@
using System;
using System.Net;
using System.Net.Sockets;
using System.Security.Cryptography;
namespace kcp2k
{
public static class Common
{
// helper function to resolve host to IPAddress
public static bool ResolveHostname(string hostname, out IPAddress[] addresses)
{
try
{
// NOTE: dns lookup is blocking. this can take a second.
addresses = Dns.GetHostAddresses(hostname);
return addresses.Length >= 1;
}
catch (SocketException exception)
{
Log.Info($"[KCP] Failed to resolve host: {hostname} reason: {exception}");
addresses = null;
return false;
}
}
// if connections drop under heavy load, increase to OS limit.
// if still not enough, increase the OS limit.
public static void ConfigureSocketBuffers(Socket socket, int recvBufferSize, int sendBufferSize)
{
// log initial size for comparison.
// remember initial size for log comparison
int initialReceive = socket.ReceiveBufferSize;
int initialSend = socket.SendBufferSize;
// set to configured size
try
{
socket.ReceiveBufferSize = recvBufferSize;
socket.SendBufferSize = sendBufferSize;
}
catch (SocketException)
{
Log.Warning($"[KCP] failed to set Socket RecvBufSize = {recvBufferSize} SendBufSize = {sendBufferSize}");
}
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

@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: 9ced451c2954435f88cf718bcba020cb
timeCreated: 1669135138
AssetOrigin:
serializedVersion: 1
productId: 129321
packageName: Mirror
packageVersion: 96.0.1
assetPath: Assets/Mirror/Transports/KCP/kcp2k/highlevel/Common.cs
uploadId: 736421

View File

@ -0,0 +1,15 @@
// kcp specific error codes to allow for error switching, localization,
// translation to Mirror errors, etc.
namespace kcp2k
{
public enum ErrorCode : byte
{
DnsResolve, // failed to resolve a host name
Timeout, // ping timeout or dead link
Congestion, // more messages than transport / network can process
InvalidReceive, // recv invalid packet (possibly intentional attack)
InvalidSend, // user tried to send invalid data
ConnectionClosed, // connection closed voluntarily or lost involuntarily
Unexpected // unexpected error / exception, requires fix.
}
}

View File

@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: 3abbeffc1d794f11a45b7fcf110353f5
timeCreated: 1652320712
AssetOrigin:
serializedVersion: 1
productId: 129321
packageName: Mirror
packageVersion: 96.0.1
assetPath: Assets/Mirror/Transports/KCP/kcp2k/highlevel/ErrorCode.cs
uploadId: 736421

View File

@ -0,0 +1,166 @@
using System;
using System.Net;
using System.Net.Sockets;
namespace kcp2k
{
public static class Extensions
{
// ArraySegment as HexString for convenience
public static string ToHexString(this ArraySegment<byte> segment) =>
BitConverter.ToString(segment.Array, segment.Offset, segment.Count);
// non-blocking UDP send.
// allows for reuse when overwriting KcpServer/Client (i.e. for relays).
// => wrapped with Poll to avoid WouldBlock allocating new SocketException.
// => wrapped with try-catch to ignore WouldBlock exception.
// make sure to set socket.Blocking = false before using this!
public static bool SendToNonBlocking(this Socket socket, ArraySegment<byte> data, EndPoint remoteEP)
{
try
{
// when using non-blocking sockets, SendTo may return WouldBlock.
// in C#, WouldBlock throws a SocketException, which is expected.
// unfortunately, creating the SocketException allocates in C#.
// let's poll first to avoid the WouldBlock allocation.
// note that this entirely to avoid allocations.
// non-blocking UDP doesn't need Poll in other languages.
// and the code still works without the Poll call.
if (!socket.Poll(0, SelectMode.SelectWrite)) return false;
// send to the the endpoint.
// do not send to 'newClientEP', as that's always reused.
// fixes https://github.com/MirrorNetworking/Mirror/issues/3296
socket.SendTo(data.Array, data.Offset, data.Count, SocketFlags.None, remoteEP);
return true;
}
catch (SocketException e)
{
// for non-blocking sockets, SendTo may throw WouldBlock.
// in that case, simply drop the message. it's UDP, it's fine.
if (e.SocketErrorCode == SocketError.WouldBlock) return false;
// otherwise it's a real socket error. throw it.
throw;
}
}
// non-blocking UDP send.
// allows for reuse when overwriting KcpServer/Client (i.e. for relays).
// => wrapped with Poll to avoid WouldBlock allocating new SocketException.
// => wrapped with try-catch to ignore WouldBlock exception.
// make sure to set socket.Blocking = false before using this!
public static bool SendNonBlocking(this Socket socket, ArraySegment<byte> data)
{
try
{
// when using non-blocking sockets, SendTo may return WouldBlock.
// in C#, WouldBlock throws a SocketException, which is expected.
// unfortunately, creating the SocketException allocates in C#.
// let's poll first to avoid the WouldBlock allocation.
// note that this entirely to avoid allocations.
// non-blocking UDP doesn't need Poll in other languages.
// and the code still works without the Poll call.
if (!socket.Poll(0, SelectMode.SelectWrite)) return false;
// SendTo allocates. we used bound Send.
socket.Send(data.Array, data.Offset, data.Count, SocketFlags.None);
return true;
}
catch (SocketException e)
{
// for non-blocking sockets, SendTo may throw WouldBlock.
// in that case, simply drop the message. it's UDP, it's fine.
if (e.SocketErrorCode == SocketError.WouldBlock) return false;
// otherwise it's a real socket error. throw it.
throw;
}
}
// non-blocking UDP receive.
// allows for reuse when overwriting KcpServer/Client (i.e. for relays).
// => wrapped with Poll to avoid WouldBlock allocating new SocketException.
// => wrapped with try-catch to ignore WouldBlock exception.
// make sure to set socket.Blocking = false before using this!
public static bool ReceiveFromNonBlocking(this Socket socket, byte[] recvBuffer, out ArraySegment<byte> data, ref EndPoint remoteEP)
{
data = default;
try
{
// when using non-blocking sockets, ReceiveFrom may return WouldBlock.
// in C#, WouldBlock throws a SocketException, which is expected.
// unfortunately, creating the SocketException allocates in C#.
// let's poll first to avoid the WouldBlock allocation.
// note that this entirely to avoid allocations.
// non-blocking UDP doesn't need Poll in other languages.
// and the code still works without the Poll call.
if (!socket.Poll(0, SelectMode.SelectRead)) return false;
// NOTE: ReceiveFrom allocates.
// we pass our IPEndPoint to ReceiveFrom.
// receive from calls newClientEP.Create(socketAddr).
// IPEndPoint.Create always returns a new IPEndPoint.
// https://github.com/mono/mono/blob/f74eed4b09790a0929889ad7fc2cf96c9b6e3757/mcs/class/System/System.Net.Sockets/Socket.cs#L1761
//
// throws SocketException if datagram was larger than buffer.
// https://learn.microsoft.com/en-us/dotnet/api/system.net.sockets.socket.receive?view=net-6.0
int size = socket.ReceiveFrom(recvBuffer, 0, recvBuffer.Length, SocketFlags.None, ref remoteEP);
data = new ArraySegment<byte>(recvBuffer, 0, size);
return true;
}
catch (SocketException e)
{
// for non-blocking sockets, Receive throws WouldBlock if there is
// no message to read. that's okay. only log for other errors.
if (e.SocketErrorCode == SocketError.WouldBlock) return false;
// otherwise it's a real socket error. throw it.
throw;
}
}
// non-blocking UDP receive.
// allows for reuse when overwriting KcpServer/Client (i.e. for relays).
// => wrapped with Poll to avoid WouldBlock allocating new SocketException.
// => wrapped with try-catch to ignore WouldBlock exception.
// make sure to set socket.Blocking = false before using this!
public static bool ReceiveNonBlocking(this Socket socket, byte[] recvBuffer, out ArraySegment<byte> data)
{
data = default;
try
{
// when using non-blocking sockets, ReceiveFrom may return WouldBlock.
// in C#, WouldBlock throws a SocketException, which is expected.
// unfortunately, creating the SocketException allocates in C#.
// let's poll first to avoid the WouldBlock allocation.
// note that this entirely to avoid allocations.
// non-blocking UDP doesn't need Poll in other languages.
// and the code still works without the Poll call.
if (!socket.Poll(0, SelectMode.SelectRead)) return false;
// ReceiveFrom allocates. we used bound Receive.
// returns amount of bytes written into buffer.
// throws SocketException if datagram was larger than buffer.
// https://learn.microsoft.com/en-us/dotnet/api/system.net.sockets.socket.receive?view=net-6.0
//
// throws SocketException if datagram was larger than buffer.
// https://learn.microsoft.com/en-us/dotnet/api/system.net.sockets.socket.receive?view=net-6.0
int size = socket.Receive(recvBuffer, 0, recvBuffer.Length, SocketFlags.None);
data = new ArraySegment<byte>(recvBuffer, 0, size);
return true;
}
catch (SocketException e)
{
// for non-blocking sockets, Receive throws WouldBlock if there is
// no message to read. that's okay. only log for other errors.
if (e.SocketErrorCode == SocketError.WouldBlock) return false;
// otherwise it's a real socket error. throw it.
throw;
}
}
}
}

View File

@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: c0649195e5ba4fcf8e0e1231fee7d5f6
timeCreated: 1641701011
AssetOrigin:
serializedVersion: 1
productId: 129321
packageName: Mirror
packageVersion: 96.0.1
assetPath: Assets/Mirror/Transports/KCP/kcp2k/highlevel/Extensions.cs
uploadId: 736421

View File

@ -0,0 +1,10 @@
namespace kcp2k
{
// channel type and header for raw messages
public enum KcpChannel : byte
{
// don't react on 0x00. might help to filter out random noise.
Reliable = 1,
Unreliable = 2
}
}

View File

@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: 9e852b2532fb248d19715cfebe371db3
timeCreated: 1610081248
AssetOrigin:
serializedVersion: 1
productId: 129321
packageName: Mirror
packageVersion: 96.0.1
assetPath: Assets/Mirror/Transports/KCP/kcp2k/highlevel/KcpChannel.cs
uploadId: 736421

View File

@ -0,0 +1,292 @@
// kcp client logic abstracted into a class.
// for use in Mirror, DOTSNET, testing, etc.
using System;
using System.Net;
using System.Net.Sockets;
namespace kcp2k
{
public class KcpClient : KcpPeer
{
// IO
protected Socket socket;
public EndPoint remoteEndPoint;
// expose local endpoint for users / relays / nat traversal etc.
public EndPoint LocalEndPoint => socket?.LocalEndPoint;
// config
protected readonly KcpConfig config;
// raw receive buffer always needs to be of 'MTU' size, even if
// MaxMessageSize is larger. kcp always sends in MTU segments and having
// a buffer smaller than MTU would silently drop excess data.
// => we need the MTU to fit channel + message!
// => protected because someone may overwrite RawReceive but still wants
// to reuse the buffer.
protected readonly byte[] rawReceiveBuffer;
// callbacks
// even for errors, to allow liraries to show popups etc.
// instead of logging directly.
// (string instead of Exception for ease of use and to avoid user panic)
//
// events are readonly, set in constructor.
// this ensures they are always initialized when used.
// fixes https://github.com/MirrorNetworking/Mirror/issues/3337 and more
protected readonly Action OnConnectedCallback;
protected readonly Action<ArraySegment<byte>, KcpChannel> OnDataCallback;
protected readonly Action OnDisconnectedCallback;
protected readonly Action<ErrorCode, string> OnErrorCallback;
// state
bool active = false; // active between when connect() and disconnect() are called
public bool connected;
public KcpClient(Action OnConnected,
Action<ArraySegment<byte>, KcpChannel> OnData,
Action OnDisconnected,
Action<ErrorCode, string> OnError,
KcpConfig config)
: base(config, 0) // client has no cookie yet
{
// initialize callbacks first to ensure they can be used safely.
OnConnectedCallback = OnConnected;
OnDataCallback = OnData;
OnDisconnectedCallback = OnDisconnected;
OnErrorCallback = OnError;
this.config = config;
// create mtu sized receive buffer
rawReceiveBuffer = new byte[config.Mtu];
}
// callbacks ///////////////////////////////////////////////////////////
// some callbacks need to wrapped with some extra logic
protected override void OnAuthenticated()
{
Log.Info($"[KCP] Client: OnConnected");
connected = true;
OnConnectedCallback();
}
protected override void OnData(ArraySegment<byte> message, KcpChannel channel) =>
OnDataCallback(message, channel);
protected override void OnError(ErrorCode error, string message) =>
OnErrorCallback(error, message);
protected override void OnDisconnected()
{
Log.Info($"[KCP] Client: OnDisconnected");
connected = false;
socket?.Close();
socket = null;
remoteEndPoint = null;
OnDisconnectedCallback();
active = false;
}
////////////////////////////////////////////////////////////////////////
public void Connect(string address, ushort port)
{
if (connected)
{
Log.Warning("[KCP] Client: already connected!");
return;
}
// resolve host name before creating peer.
// fixes: https://github.com/MirrorNetworking/Mirror/issues/3361
if (!Common.ResolveHostname(address, out IPAddress[] addresses))
{
// pass error to user callback. no need to log it manually.
OnError(ErrorCode.DnsResolve, $"Failed to resolve host: {address}");
OnDisconnectedCallback();
return;
}
// create fresh peer for each new session
// client doesn't need secure cookie.
Reset(config);
Log.Info($"[KCP] Client: connect to {address}:{port}");
// create socket
remoteEndPoint = new IPEndPoint(addresses[0], port);
socket = new Socket(remoteEndPoint.AddressFamily, SocketType.Dgram, ProtocolType.Udp);
active = true;
// recv & send are called from main thread.
// need to ensure this never blocks.
// even a 1ms block per connection would stop us from scaling.
socket.Blocking = false;
// configure buffer sizes
Common.ConfigureSocketBuffers(socket, config.RecvBufferSize, config.SendBufferSize);
// bind to endpoint so we can use send/recv instead of sendto/recvfrom.
socket.Connect(remoteEndPoint);
// immediately send a hello message to the server.
// server will call OnMessage and add the new connection.
// note that this still has cookie=0 until we receive the server's hello.
SendHello();
}
// io - input.
// virtual so it may be modified for relays, etc.
// call this while it returns true, to process all messages this tick.
// returned ArraySegment is valid until next call to RawReceive.
protected virtual bool RawReceive(out ArraySegment<byte> segment)
{
segment = default;
if (socket == null) return false;
try
{
return socket.ReceiveNonBlocking(rawReceiveBuffer, out segment);
}
// for non-blocking sockets, Receive throws WouldBlock if there is
// no message to read. that's okay. only log for other errors.
catch (SocketException e)
{
// the other end closing the connection is not an 'error'.
// but connections should never just end silently.
// at least log a message for easier debugging.
// for example, his can happen when connecting without a server.
// see test: ConnectWithoutServer().
Log.Info($"[KCP] Client.RawReceive: looks like the other end has closed the connection. This is fine: {e}");
base.Disconnect();
return false;
}
}
// io - output.
// virtual so it may be modified for relays, etc.
protected override void RawSend(ArraySegment<byte> data)
{
// only if socket was connected / created yet.
// users may call send functions without having connected, causing NRE.
if (socket == null) return;
try
{
socket.SendNonBlocking(data);
}
catch (SocketException e)
{
// SendDisconnect() sometimes gets a SocketException with
// 'Connection Refused' if the other end already closed.
// this is not an 'error', it's expected to happen.
// but connections should never just end silently.
// at least log a message for easier debugging.
Log.Info($"[KCP] Client.RawSend: looks like the other end has closed the connection. This is fine: {e}");
// base.Disconnect(); <- don't call this, would deadlock if SendDisconnect() already throws
}
}
public void Send(ArraySegment<byte> segment, KcpChannel channel)
{
if (!connected)
{
Log.Warning("[KCP] Client: can't send because not connected!");
return;
}
SendData(segment, channel);
}
// insert raw IO. usually from socket.Receive.
// offset is useful for relays, where we may parse a header and then
// feed the rest to kcp.
public void RawInput(ArraySegment<byte> segment)
{
// ensure valid size: at least 1 byte for channel + 4 bytes for cookie
if (segment.Count <= 5) return;
// parse channel
// byte channel = segment[0]; ArraySegment[i] isn't supported in some older Unity Mono versions
byte channel = segment.Array[segment.Offset + 0];
// server messages always contain the security cookie.
// parse it, assign if not assigned, warn if suddenly different.
Utils.Decode32U(segment.Array, segment.Offset + 1, out uint messageCookie);
if (messageCookie == 0)
{
Log.Error($"[KCP] Client: received message with cookie=0, this should never happen. Server should always include the security cookie.");
}
if (cookie == 0)
{
cookie = messageCookie;
Log.Info($"[KCP] Client: received initial cookie: {cookie}");
}
else if (cookie != messageCookie)
{
Log.Warning($"[KCP] Client: dropping message with mismatching cookie: {messageCookie} expected: {cookie}.");
return;
}
// parse message
ArraySegment<byte> message = new ArraySegment<byte>(segment.Array, segment.Offset + 1+4, segment.Count - 1-4);
switch (channel)
{
case (byte)KcpChannel.Reliable:
{
OnRawInputReliable(message);
break;
}
case (byte)KcpChannel.Unreliable:
{
OnRawInputUnreliable(message);
break;
}
default:
{
// invalid channel indicates random internet noise.
// servers may receive random UDP data.
// just ignore it, but log for easier debugging.
Log.Warning($"[KCP] Client: invalid channel header: {channel}, likely internet noise");
break;
}
}
}
// process incoming messages. should be called before updating the world.
// virtual because relay may need to inject their own ping or similar.
public override void TickIncoming()
{
// recv on socket first, then process incoming
// (even if we didn't receive anything. need to tick ping etc.)
// (connection is null if not active)
if (active)
{
while (RawReceive(out ArraySegment<byte> segment))
RawInput(segment);
}
// RawReceive may have disconnected peer. active check again.
if (active) base.TickIncoming();
}
// process outgoing messages. should be called after updating the world.
// virtual because relay may need to inject their own ping or similar.
public override void TickOutgoing()
{
// process outgoing while active
if (active) base.TickOutgoing();
}
// process incoming and outgoing for convenience
// => ideally call ProcessIncoming() before updating the world and
// ProcessOutgoing() after updating the world for minimum latency
public virtual void Tick()
{
TickIncoming();
TickOutgoing();
}
}
}

View File

@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: 6aa069a28ed24fedb533c102d9742b36
timeCreated: 1603786960
AssetOrigin:
serializedVersion: 1
productId: 129321
packageName: Mirror
packageVersion: 96.0.1
assetPath: Assets/Mirror/Transports/KCP/kcp2k/highlevel/KcpClient.cs
uploadId: 736421

View File

@ -0,0 +1,97 @@
// common config struct, instead of passing 10 parameters manually every time.
using System;
namespace kcp2k
{
// [Serializable] to show it in Unity inspector.
// 'class' so we can set defaults easily.
[Serializable]
public class KcpConfig
{
// socket configuration ////////////////////////////////////////////////
// DualMode uses both IPv6 and IPv4. not all platforms support it.
// (Nintendo Switch, etc.)
public bool DualMode;
// UDP servers use only one socket.
// maximize buffer to handle as many connections as possible.
//
// M1 mac pro:
// recv buffer default: 786896 (771 KB)
// send buffer default: 9216 (9 KB)
// max configurable: ~7 MB
public int RecvBufferSize;
public int SendBufferSize;
// kcp configuration ///////////////////////////////////////////////////
// configurable MTU in case kcp sits on top of other abstractions like
// encrypted transports, relays, etc.
public int Mtu;
// NoDelay is recommended to reduce latency. This also scales better
// without buffers getting full.
public bool NoDelay;
// KCP internal update interval. 100ms is KCP default, but a lower
// interval is recommended to minimize latency and to scale to more
// networked entities.
public uint Interval;
// KCP fastresend parameter. Faster resend for the cost of higher
// bandwidth.
public int FastResend;
// KCP congestion window heavily limits messages flushed per update.
// congestion window may actually be broken in kcp:
// - sending max sized message @ M1 mac flushes 2-3 messages per update
// - even with super large send/recv window, it requires thousands of
// update calls
// best to leave this disabled, as it may significantly increase latency.
public bool CongestionWindow;
// KCP window size can be modified to support higher loads.
// for example, Mirror Benchmark requires:
// 128, 128 for 4k monsters
// 512, 512 for 10k monsters
// 8192, 8192 for 20k monsters
public uint SendWindowSize;
public uint ReceiveWindowSize;
// timeout in milliseconds
public int Timeout;
// maximum retransmission attempts until dead_link
public uint MaxRetransmits;
// constructor /////////////////////////////////////////////////////////
// constructor with defaults for convenience.
// makes it easy to define "new KcpConfig(DualMode=false)" etc.
public KcpConfig(
bool DualMode = true,
int RecvBufferSize = 1024 * 1024 * 7,
int SendBufferSize = 1024 * 1024 * 7,
int Mtu = Kcp.MTU_DEF,
bool NoDelay = true,
uint Interval = 10,
int FastResend = 0,
bool CongestionWindow = false,
uint SendWindowSize = Kcp.WND_SND,
uint ReceiveWindowSize = Kcp.WND_RCV,
int Timeout = KcpPeer.DEFAULT_TIMEOUT,
uint MaxRetransmits = Kcp.DEADLINK)
{
this.DualMode = DualMode;
this.RecvBufferSize = RecvBufferSize;
this.SendBufferSize = SendBufferSize;
this.Mtu = Mtu;
this.NoDelay = NoDelay;
this.Interval = Interval;
this.FastResend = FastResend;
this.CongestionWindow = CongestionWindow;
this.SendWindowSize = SendWindowSize;
this.ReceiveWindowSize = ReceiveWindowSize;
this.Timeout = Timeout;
this.MaxRetransmits = MaxRetransmits;
}
}
}

View File

@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: 99692f99c45c4b47b0500e7abbfd12da
timeCreated: 1670946969
AssetOrigin:
serializedVersion: 1
productId: 129321
packageName: Mirror
packageVersion: 96.0.1
assetPath: Assets/Mirror/Transports/KCP/kcp2k/highlevel/KcpConfig.cs
uploadId: 736421

View File

@ -0,0 +1,57 @@
using System;
namespace kcp2k
{
// header for messages processed by kcp.
// this is NOT for the raw receive messages(!) because handshake/disconnect
// need to be sent reliably. it's not enough to have those in rawreceive
// because those messages might get lost without being resent!
public enum KcpHeaderReliable : byte
{
// don't react on 0x00. might help to filter out random noise.
Hello = 1,
// ping goes over reliable & KcpHeader for now. could go over unreliable
// too. there is no real difference except that this is easier because
// we already have a KcpHeader for reliable messages.
// ping is only used to keep it alive, so latency doesn't matter.
Ping = 2,
Data = 3,
}
public enum KcpHeaderUnreliable : byte
{
// users may send unreliable messages
Data = 4,
// disconnect always goes through rapid fire unreliable (glenn fielder)
Disconnect = 5,
}
// save convert the enums from/to byte.
// attackers may attempt to send invalid values, so '255' may not convert.
public static class KcpHeader
{
public static bool ParseReliable(byte value, out KcpHeaderReliable header)
{
if (Enum.IsDefined(typeof(KcpHeaderReliable), value))
{
header = (KcpHeaderReliable)value;
return true;
}
header = KcpHeaderReliable.Ping; // any default
return false;
}
public static bool ParseUnreliable(byte value, out KcpHeaderUnreliable header)
{
if (Enum.IsDefined(typeof(KcpHeaderUnreliable), value))
{
header = (KcpHeaderUnreliable)value;
return true;
}
header = KcpHeaderUnreliable.Disconnect; // any default
return false;
}
}
}

View File

@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: 91b5edac31224a49bd76f960ae018942
timeCreated: 1610081248
AssetOrigin:
serializedVersion: 1
productId: 129321
packageName: Mirror
packageVersion: 96.0.1
assetPath: Assets/Mirror/Transports/KCP/kcp2k/highlevel/KcpHeader.cs
uploadId: 736421

View File

@ -0,0 +1,791 @@
// Kcp Peer, similar to UDP Peer but wrapped with reliability, channels,
// timeouts, authentication, state, etc.
//
// still IO agnostic to work with udp, nonalloc, relays, native, etc.
using System;
using System.Diagnostics;
using System.Net.Sockets;
namespace kcp2k
{
public abstract 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
internal uint cookie;
// state: connected as soon as we create the peer.
// leftover from KcpConnection. remove it after refactoring later.
protected KcpState state = KcpState.Connected;
// If we don't receive anything these many milliseconds
// then consider us disconnected
public const int DEFAULT_TIMEOUT = 10000;
public int timeout;
uint lastReceiveTime;
// internal time.
// StopWatch offers ElapsedMilliSeconds and should be more precise than
// Unity's time.deltaTime over long periods.
readonly Stopwatch watch = new Stopwatch();
// buffer to receive kcp's processed messages (avoids allocations).
// IMPORTANT: this is for KCP messages. so it needs to be of size:
// 1 byte header + MaxMessageSize content
readonly byte[] kcpMessageBuffer;// = new byte[1 + ReliableMaxMessageSize];
// send buffer for handing user messages to kcp for processing.
// (avoids allocations).
// IMPORTANT: needs to be of size:
// 1 byte header + MaxMessageSize content
readonly byte[] kcpSendBuffer;// = new byte[1 + ReliableMaxMessageSize];
// raw send buffer is exactly MTU.
readonly byte[] rawSendBuffer;
// send a ping occasionally so we don't time out on the other end.
// for example, creating a character in an MMO could easily take a
// minute of no data being sent. which doesn't mean we want to time out.
// same goes for slow paced card games etc.
public const int PING_INTERVAL = 1000;
uint lastPingTime;
// if we send more than kcp can handle, we will get ever growing
// send/recv buffers and queues and minutes of latency.
// => if a connection can't keep up, it should be disconnected instead
// to protect the server under heavy load, and because there is no
// point in growing to gigabytes of memory or minutes of latency!
// => 2k isn't enough. we reach 2k when spawning 4k monsters at once
// easily, but it does recover over time.
// => 10k seems safe.
//
// note: we have a ChokeConnectionAutoDisconnects test for this too!
internal const int QueueDisconnectThreshold = 10000;
// getters for queue and buffer counts, used for debug info
public int SendQueueCount => kcp.snd_queue.Count;
public int ReceiveQueueCount => kcp.rcv_queue.Count;
public int SendBufferCount => kcp.snd_buf.Count;
public int ReceiveBufferCount => kcp.rcv_buf.Count;
// 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.
public const int CHANNEL_HEADER_SIZE = 1;
public const int COOKIE_HEADER_SIZE = 4;
public 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
// all, so let's provide the helper here.
//
// kcp does fragmentation, so max message is way larger than MTU.
//
// -> runtime MTU changes are disabled: mss is always MTU_DEF-OVERHEAD
// -> Send() checks if fragment count < rcv_wnd, so we use rcv_wnd - 1.
// NOTE that original kcp has a bug where WND_RCV default is used
// instead of configured rcv_wnd, limiting max message size to 144 KB
// https://github.com/skywind3000/kcp/pull/291
// we fixed this in kcp2k.
// -> we add 1 byte KcpHeader enum to each message, so -1
//
// IMPORTANT: max message is MTU * rcv_wnd, in other words it completely
// fills the receive window! due to head of line blocking,
// all other messages have to wait while a maxed size message
// is being delivered.
// => in other words, DO NOT use max size all the time like
// for batching.
// => 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 - METADATA_SIZE) * ((int)rcv_wnd - 1) - 1;
// kcp encodes 'frg' as 1 byte.
// max message size can only ever allow up to 255 fragments.
// WND_RCV gives 127 fragments.
// WND_RCV * 2 gives 255 fragments.
// so we can limit max message size by limiting rcv_wnd parameter.
public static int ReliableMaxMessageSize(int mtu, uint rcv_wnd) =>
ReliableMaxMessageSize_Unconstrained(mtu, Math.Min(rcv_wnd, Kcp.FRG_MAX));
// unreliable max message size is simply MTU - channel header - kcp header
public static int UnreliableMaxMessageSize(int mtu) =>
mtu - METADATA_SIZE - 1;
// maximum send rate per second can be calculated from kcp parameters
// source: https://translate.google.com/translate?sl=auto&tl=en&u=https://wetest.qq.com/lab/view/391.html
//
// KCP can send/receive a maximum of WND*MTU per interval.
// multiple by 1000ms / interval to get the per-second rate.
//
// example:
// WND(32) * MTU(1400) = 43.75KB
// => 43.75KB * 1000 / INTERVAL(10) = 4375KB/s
//
// returns bytes/second!
public uint MaxSendRate => kcp.snd_wnd * kcp.mtu * 1000 / kcp.interval;
public uint MaxReceiveRate => kcp.rcv_wnd * kcp.mtu * 1000 / kcp.interval;
// calculate max message sizes based on mtu and wnd only once
public readonly int unreliableMax;
public readonly int reliableMax;
// SetupKcp creates and configures a new KCP instance.
// => useful to start from a fresh state every time the client connects
// => NoDelay, interval, wnd size are the most important configurations.
// let's force require the parameters so we don't forget it anywhere.
protected KcpPeer(KcpConfig config, uint cookie)
{
// initialize variable state in extra function so we can reuse it
// when reconnecting to reset state
Reset(config);
// set the cookie after resetting state so it's not overwritten again.
// with log message for debugging in case of cookie issues.
this.cookie = cookie;
Log.Info($"[KCP] {GetType()}: created with cookie={cookie}");
// create mtu sized send buffer
rawSendBuffer = new byte[config.Mtu];
// calculate max message sizes once
unreliableMax = UnreliableMaxMessageSize(config.Mtu);
reliableMax = ReliableMaxMessageSize(config.Mtu, config.ReceiveWindowSize);
// create message buffers AFTER window size is set
// see comments on buffer definition for the "+1" part
kcpMessageBuffer = new byte[1 + reliableMax];
kcpSendBuffer = new byte[1 + reliableMax];
}
// Reset all state once.
// useful for KcpClient to reconned with a fresh kcp state.
protected void Reset(KcpConfig config)
{
// reset state
cookie = 0;
state = KcpState.Connected;
lastReceiveTime = 0;
lastPingTime = 0;
watch.Restart(); // start at 0 each time
// set up kcp over reliable channel (that's what kcp is for)
kcp = new Kcp(0, RawSendReliable);
// 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);
kcp.SetWindowSize(config.SendWindowSize, config.ReceiveWindowSize);
// IMPORTANT: high level needs to add 1 channel byte to each raw
// 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 - METADATA_SIZE);
// set maximum retransmits (aka dead_link)
kcp.dead_link = config.MaxRetransmits;
timeout = config.Timeout;
}
// callbacks ///////////////////////////////////////////////////////////
// events are abstract, guaranteed to be implemented.
// this ensures they are always initialized when used.
// fixes https://github.com/MirrorNetworking/Mirror/issues/3337 and more
protected abstract void OnAuthenticated();
protected abstract void OnData(ArraySegment<byte> message, KcpChannel channel);
protected abstract void OnDisconnected();
// error callback instead of logging.
// allows libraries to show popups etc.
// (string instead of Exception for ease of use and to avoid user panic)
protected abstract void OnError(ErrorCode error, string message);
protected abstract void RawSend(ArraySegment<byte> data);
////////////////////////////////////////////////////////////////////////
void HandleTimeout(uint time)
{
// note: we are also sending a ping regularly, so timeout should
// only ever happen if the connection is truly gone.
if (time >= lastReceiveTime + timeout)
{
// pass error to user callback. no need to log it manually.
// GetType() shows Server/ClientConn instead of just Connection.
OnError(ErrorCode.Timeout, $"{GetType()}: Connection timed out after not receiving any message for {timeout}ms. Disconnecting.");
Disconnect();
}
}
void HandleDeadLink()
{
// kcp has 'dead_link' detection. might as well use it.
if (kcp.state == -1)
{
// pass error to user callback. no need to log it manually.
// GetType() shows Server/ClientConn instead of just Connection.
OnError(ErrorCode.Timeout, $"{GetType()}: dead_link detected: a message was retransmitted {kcp.dead_link} times without ack. Disconnecting.");
Disconnect();
}
}
// send a ping occasionally in order to not time out on the other end.
void HandlePing(uint time)
{
// enough time elapsed since last ping?
if (time >= lastPingTime + PING_INTERVAL)
{
// ping again and reset time
//Log.Debug("[KCP] sending ping...");
SendPing();
lastPingTime = time;
}
}
void HandleChoked()
{
// disconnect connections that can't process the load.
// see QueueSizeDisconnect comments.
// => include all of kcp's buffers and the unreliable queue!
int total = kcp.rcv_queue.Count + kcp.snd_queue.Count +
kcp.rcv_buf.Count + kcp.snd_buf.Count;
if (total >= QueueDisconnectThreshold)
{
// pass error to user callback. no need to log it manually.
// GetType() shows Server/ClientConn instead of just Connection.
OnError(ErrorCode.Congestion,
$"{GetType()}: disconnecting connection because it can't process data fast enough.\n" +
$"Queue total {total}>{QueueDisconnectThreshold}. rcv_queue={kcp.rcv_queue.Count} snd_queue={kcp.snd_queue.Count} rcv_buf={kcp.rcv_buf.Count} snd_buf={kcp.snd_buf.Count}\n" +
$"* Try to Enable NoDelay, decrease INTERVAL, disable Congestion Window (= enable NOCWND!), increase SEND/RECV WINDOW or compress data.\n" +
$"* Or perhaps the network is simply too slow on our end, or on the other end.");
// let's clear all pending sends before disconnting with 'Bye'.
// otherwise a single Flush in Disconnect() won't be enough to
// flush thousands of messages to finally deliver 'Bye'.
// this is just faster and more robust.
kcp.snd_queue.Clear();
Disconnect();
}
}
// reads the next reliable message type & content from kcp.
// -> to avoid buffering, unreliable messages call OnData directly.
bool ReceiveNextReliable(out KcpHeaderReliable header, out ArraySegment<byte> message)
{
message = default;
header = KcpHeaderReliable.Ping;
int msgSize = kcp.PeekSize();
if (msgSize <= 0) return false;
// only allow receiving up to buffer sized messages.
// otherwise we would get BlockCopy ArgumentException anyway.
if (msgSize > kcpMessageBuffer.Length)
{
// we don't allow sending messages > Max, so this must be an
// attacker. let's disconnect to avoid allocation attacks etc.
// pass error to user callback. no need to log it manually.
OnError(ErrorCode.InvalidReceive, $"{GetType()}: possible allocation attack for msgSize {msgSize} > buffer {kcpMessageBuffer.Length}. Disconnecting the connection.");
Disconnect();
return false;
}
// receive from kcp
int received = kcp.Receive(kcpMessageBuffer, msgSize);
if (received < 0)
{
// if receive failed, close everything
// pass error to user callback. no need to log it manually.
// GetType() shows Server/ClientConn instead of just Connection.
OnError(ErrorCode.InvalidReceive, $"{GetType()}: Receive failed with error={received}. closing connection.");
Disconnect();
return false;
}
// safely extract header. attackers may send values out of enum range.
byte headerByte = kcpMessageBuffer[0];
if (!KcpHeader.ParseReliable(headerByte, out header))
{
OnError(ErrorCode.InvalidReceive, $"{GetType()}: Receive failed to parse header: {headerByte} is not defined in {typeof(KcpHeaderReliable)}.");
Disconnect();
return false;
}
// extract content without header
message = new ArraySegment<byte>(kcpMessageBuffer, 1, msgSize - 1);
lastReceiveTime = (uint)watch.ElapsedMilliseconds;
return true;
}
void TickIncoming_Connected(uint time)
{
// detect common events & ping
HandleTimeout(time);
HandleDeadLink();
HandlePing(time);
HandleChoked();
// any reliable kcp message received?
if (ReceiveNextReliable(out KcpHeaderReliable header, out ArraySegment<byte> message))
{
// message type FSM. no default so we never miss a case.
switch (header)
{
case KcpHeaderReliable.Hello:
{
// we were waiting for a Hello message.
// it proves that the other end speaks our protocol.
// log with previously parsed cookie
Log.Info($"[KCP] {GetType()}: received hello with cookie={cookie}");
state = KcpState.Authenticated;
OnAuthenticated();
break;
}
case KcpHeaderReliable.Ping:
{
// ping keeps kcp from timing out. do nothing.
break;
}
case KcpHeaderReliable.Data:
{
// everything else is not allowed during handshake!
// pass error to user callback. no need to log it manually.
// GetType() shows Server/ClientConn instead of just Connection.
OnError(ErrorCode.InvalidReceive, $"[KCP] {GetType()}: received invalid header {header} while Connected. Disconnecting the connection.");
Disconnect();
break;
}
}
}
}
void TickIncoming_Authenticated(uint time)
{
// detect common events & ping
HandleTimeout(time);
HandleDeadLink();
HandlePing(time);
HandleChoked();
// process all received messages
while (ReceiveNextReliable(out KcpHeaderReliable header, out ArraySegment<byte> message))
{
// message type FSM. no default so we never miss a case.
switch (header)
{
case KcpHeaderReliable.Hello:
{
// should never receive another hello after auth
// GetType() shows Server/ClientConn instead of just Connection.
Log.Warning($"{GetType()}: received invalid header {header} while Authenticated. Disconnecting the connection.");
Disconnect();
break;
}
case KcpHeaderReliable.Data:
{
// call OnData IF the message contained actual data
if (message.Count > 0)
{
//Log.Warning($"Kcp recv msg: {BitConverter.ToString(message.Array, message.Offset, message.Count)}");
OnData(message, KcpChannel.Reliable);
}
// empty data = attacker, or something went wrong
else
{
// pass error to user callback. no need to log it manually.
// GetType() shows Server/ClientConn instead of just Connection.
OnError(ErrorCode.InvalidReceive, $"{GetType()}: received empty Data message while Authenticated. Disconnecting the connection.");
Disconnect();
}
break;
}
case KcpHeaderReliable.Ping:
{
// ping keeps kcp from timing out. do nothing.
break;
}
}
}
}
public virtual void TickIncoming()
{
uint time = (uint)watch.ElapsedMilliseconds;
try
{
switch (state)
{
case KcpState.Connected:
{
TickIncoming_Connected(time);
break;
}
case KcpState.Authenticated:
{
TickIncoming_Authenticated(time);
break;
}
case KcpState.Disconnected:
{
// do nothing while disconnected
break;
}
}
}
// TODO KcpConnection is IO agnostic. move this to outside later.
catch (SocketException exception)
{
// this is ok, the connection was closed
// pass error to user callback. no need to log it manually.
// GetType() shows Server/ClientConn instead of just Connection.
OnError(ErrorCode.ConnectionClosed, $"{GetType()}: Disconnecting because {exception}. This is fine.");
Disconnect();
}
catch (ObjectDisposedException exception)
{
// fine, socket was closed
// pass error to user callback. no need to log it manually.
// GetType() shows Server/ClientConn instead of just Connection.
OnError(ErrorCode.ConnectionClosed, $"{GetType()}: Disconnecting because {exception}. This is fine.");
Disconnect();
}
catch (Exception exception)
{
// unexpected
// pass error to user callback. no need to log it manually.
// GetType() shows Server/ClientConn instead of just Connection.
OnError(ErrorCode.Unexpected, $"{GetType()}: unexpected Exception: {exception}");
Disconnect();
}
}
public virtual void TickOutgoing()
{
uint time = (uint)watch.ElapsedMilliseconds;
try
{
switch (state)
{
case KcpState.Connected:
case KcpState.Authenticated:
{
// update flushes out messages
kcp.Update(time);
break;
}
case KcpState.Disconnected:
{
// do nothing while disconnected
break;
}
}
}
// TODO KcpConnection is IO agnostic. move this to outside later.
catch (SocketException exception)
{
// this is ok, the connection was closed
// pass error to user callback. no need to log it manually.
// GetType() shows Server/ClientConn instead of just Connection.
OnError(ErrorCode.ConnectionClosed, $"{GetType()}: Disconnecting because {exception}. This is fine.");
Disconnect();
}
catch (ObjectDisposedException exception)
{
// fine, socket was closed
// pass error to user callback. no need to log it manually.
// GetType() shows Server/ClientConn instead of just Connection.
OnError(ErrorCode.ConnectionClosed, $"{GetType()}: Disconnecting because {exception}. This is fine.");
Disconnect();
}
catch (Exception exception)
{
// unexpected
// pass error to user callback. no need to log it manually.
// GetType() shows Server/ClientConn instead of just Connection.
OnError(ErrorCode.Unexpected, $"{GetType()}: unexpected exception: {exception}");
Disconnect();
}
}
protected void OnRawInputReliable(ArraySegment<byte> message)
{
// input into kcp, but skip channel byte
int input = kcp.Input(message.Array, message.Offset, message.Count);
if (input != 0)
{
// GetType() shows Server/ClientConn instead of just Connection.
Log.Warning($"[KCP] {GetType()}: Input failed with error={input} for buffer with length={message.Count - 1}");
}
}
protected void OnRawInputUnreliable(ArraySegment<byte> message)
{
// need at least one byte for the KcpHeader enum
if (message.Count < 1) return;
// safely extract header. attackers may send values out of enum range.
byte headerByte = message.Array[message.Offset + 0];
if (!KcpHeader.ParseUnreliable(headerByte, out KcpHeaderUnreliable header))
{
OnError(ErrorCode.InvalidReceive, $"{GetType()}: Receive failed to parse header: {headerByte} is not defined in {typeof(KcpHeaderUnreliable)}.");
Disconnect();
return;
}
// subtract header from message content
// (above we already ensure it's at least 1 byte long)
message = new ArraySegment<byte>(message.Array, message.Offset + 1, message.Count - 1);
switch (header)
{
case KcpHeaderUnreliable.Data:
{
// ideally we would queue all unreliable messages and
// then process them in ReceiveNext() together with the
// reliable messages, but:
// -> queues/allocations/pools are slow and complex.
// -> DOTSNET 10k is actually slower if we use pooled
// unreliable messages for transform messages.
//
// DOTSNET 10k benchmark:
// reliable-only: 170 FPS
// unreliable queued: 130-150 FPS
// unreliable direct: 183 FPS(!)
//
// DOTSNET 50k benchmark:
// reliable-only: FAILS (queues keep growing)
// unreliable direct: 18-22 FPS(!)
//
// -> all unreliable messages are DATA messages anyway.
// -> let's skip the magic and call OnData directly if
// the current state allows it.
if (state == KcpState.Authenticated)
{
OnData(message, KcpChannel.Unreliable);
// set last receive time to avoid timeout.
// -> we do this in ANY case even if not enabled.
// a message is a message.
// -> we set last receive time for both reliable and
// unreliable messages. both count.
// otherwise a connection might time out even
// though unreliable were received, but no
// reliable was received.
lastReceiveTime = (uint)watch.ElapsedMilliseconds;
}
else
{
// it's common to receive unreliable messages before being
// authenticated, for example:
// - random internet noise
// - game server may send an unreliable message after authenticating,
// and the unreliable message arrives on the client before the
// 'auth_ok' message. this can be avoided by sending a final
// 'ready' message after being authenticated, but this would
// add another 'round trip time' of latency to the handshake.
//
// it's best to simply ignore invalid unreliable messages here.
// Log.Info($"{GetType()}: received unreliable message while not authenticated.");
}
break;
}
case KcpHeaderUnreliable.Disconnect:
{
// GetType() shows Server/ClientConn instead of just Connection.
Log.Info($"[KCP] {GetType()}: received disconnect message");
Disconnect();
break;
}
}
}
// raw send called by kcp
void RawSendReliable(byte[] data, int length)
{
// write channel header
// from 0, with 1 byte
rawSendBuffer[0] = (byte)KcpChannel.Reliable;
// write handshake cookie to protect against UDP spoofing.
// from 1, with 4 bytes
Utils.Encode32U(rawSendBuffer, 1, cookie); // allocation free
// 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+4);
RawSend(segment);
}
void SendReliable(KcpHeaderReliable header, ArraySegment<byte> content)
{
// 1 byte header + content needs to fit into send buffer
if (1 + content.Count > kcpSendBuffer.Length) // TODO
{
// otherwise content is larger than MaxMessageSize. let user know!
// GetType() shows Server/ClientConn instead of just Connection.
OnError(ErrorCode.InvalidSend, $"{GetType()}: Failed to send reliable message of size {content.Count} because it's larger than ReliableMaxMessageSize={reliableMax}");
return;
}
// 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);
// send to kcp for processing
int sent = kcp.Send(kcpSendBuffer, 0, 1 + content.Count);
if (sent < 0)
{
// GetType() shows Server/ClientConn instead of just Connection.
OnError(ErrorCode.InvalidSend, $"{GetType()}: Send failed with error={sent} for content with length={content.Count}");
}
}
void SendUnreliable(KcpHeaderUnreliable header, ArraySegment<byte> content)
{
// message size needs to be <= unreliable max size
if (content.Count > unreliableMax)
{
// otherwise content is larger than MaxMessageSize. let user know!
// GetType() shows Server/ClientConn instead of just Connection.
Log.Error($"[KCP] {GetType()}: Failed to send unreliable message of size {content.Count} because it's larger than UnreliableMaxMessageSize={unreliableMax}");
return;
}
// write channel header
// from 0, with 1 byte
rawSendBuffer[0] = (byte)KcpChannel.Unreliable;
// write handshake cookie to protect against UDP spoofing.
// from 1, with 4 bytes
Utils.Encode32U(rawSendBuffer, 1, cookie); // allocation free
// write kcp header
rawSendBuffer[5] = (byte)header;
// write data (if any)
// from 6, with N bytes
if (content.Count > 0)
Buffer.BlockCopy(content.Array, content.Offset, rawSendBuffer, 1 + 4 + 1, content.Count);
// IO send
ArraySegment<byte> segment = new ArraySegment<byte>(rawSendBuffer, 0, content.Count + 1 + 4 + 1);
RawSend(segment);
}
// server & client need to send handshake at different times, so we need
// to expose the function.
// * client should send it immediately.
// * server should send it as reply to client's handshake, not before
// (server should not reply to random internet messages with handshake)
// => handshake info needs to be delivered, so it goes over reliable.
public void SendHello()
{
// send an empty message with 'Hello' header.
// cookie is automatically included in all messages.
// GetType() shows Server/ClientConn instead of just Connection.
Log.Info($"[KCP] {GetType()}: sending handshake to other end with cookie={cookie}");
SendReliable(KcpHeaderReliable.Hello, default);
}
public void SendData(ArraySegment<byte> data, KcpChannel channel)
{
// sending empty segments is not allowed.
// nobody should ever try to send empty data.
// it means that something went wrong, e.g. in Mirror/DOTSNET.
// let's make it obvious so it's easy to debug.
if (data.Count == 0)
{
// pass error to user callback. no need to log it manually.
// GetType() shows Server/ClientConn instead of just Connection.
OnError(ErrorCode.InvalidSend, $"{GetType()}: tried sending empty message. This should never happen. Disconnecting.");
Disconnect();
return;
}
switch (channel)
{
case KcpChannel.Reliable:
SendReliable(KcpHeaderReliable.Data, data);
break;
case KcpChannel.Unreliable:
SendUnreliable(KcpHeaderUnreliable.Data, data);
break;
}
}
// ping goes through kcp to keep it from timing out, so it goes over the
// reliable channel.
void SendPing() => SendReliable(KcpHeaderReliable.Ping, default);
// send disconnect message
void SendDisconnect()
{
// sending over reliable to ensure delivery seems like a good idea:
// but if we close the connection immediately, it often doesn't get
// fully delivered: https://github.com/MirrorNetworking/Mirror/issues/3591
// SendReliable(KcpHeader.Disconnect, default);
//
// instead, rapid fire a few unreliable messages.
// they are sent immediately even if we close the connection after.
// this way we don't need to keep the connection alive for a while.
// (glenn fiedler method)
for (int i = 0; i < 5; ++i)
SendUnreliable(KcpHeaderUnreliable.Disconnect, default);
}
// disconnect this connection
public virtual void Disconnect()
{
// only if not disconnected yet
if (state == KcpState.Disconnected)
return;
// send a disconnect message
try
{
SendDisconnect();
}
// TODO KcpConnection is IO agnostic. move this to outside later.
catch (SocketException)
{
// this is ok, the connection was already closed
}
catch (ObjectDisposedException)
{
// this is normal when we stop the server
// the socket is stopped so we can't send anything anymore
// to the clients
// the clients will eventually timeout and realize they
// were disconnected
}
// set as Disconnected, call event
// GetType() shows Server/ClientConn instead of just Connection.
Log.Info($"[KCP] {GetType()}: Disconnected.");
state = KcpState.Disconnected;
OnDisconnected();
}
}
}

View File

@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: 3915c7c62b72d4dc2a9e4e76c94fc484
timeCreated: 1602600432
AssetOrigin:
serializedVersion: 1
productId: 129321
packageName: Mirror
packageVersion: 96.0.1
assetPath: Assets/Mirror/Transports/KCP/kcp2k/highlevel/KcpPeer.cs
uploadId: 736421

View File

@ -0,0 +1,412 @@
// kcp server logic abstracted into a class.
// for use in Mirror, DOTSNET, testing, etc.
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Runtime.InteropServices;
namespace kcp2k
{
public class KcpServer
{
// callbacks
// even for errors, to allow liraries to show popups etc.
// instead of logging directly.
// (string instead of Exception for ease of use and to avoid user panic)
//
// events are readonly, set in constructor.
// this ensures they are always initialized when used.
// fixes https://github.com/MirrorNetworking/Mirror/issues/3337 and more
protected readonly Action<int, IPEndPoint> OnConnected; // connectionId, address
protected readonly Action<int, ArraySegment<byte>, KcpChannel> OnData;
protected readonly Action<int> OnDisconnected;
protected readonly Action<int, ErrorCode, string> OnError;
// configuration
protected readonly KcpConfig config;
// state
protected Socket socket;
EndPoint newClientEP;
// expose local endpoint for users / relays / nat traversal etc.
public EndPoint LocalEndPoint => socket?.LocalEndPoint;
// raw receive buffer always needs to be of 'MTU' size, even if
// MaxMessageSize is larger. kcp always sends in MTU segments and having
// a buffer smaller than MTU would silently drop excess data.
// => we need the mtu to fit channel + message!
protected readonly byte[] rawReceiveBuffer;
// connections <connectionId, connection> where connectionId is EndPoint.GetHashCode
public Dictionary<int, KcpServerConnection> connections =
new Dictionary<int, KcpServerConnection>();
public KcpServer(Action<int, IPEndPoint> OnConnected,
Action<int, ArraySegment<byte>, KcpChannel> OnData,
Action<int> OnDisconnected,
Action<int, ErrorCode, string> OnError,
KcpConfig config)
{
// initialize callbacks first to ensure they can be used safely.
this.OnConnected = OnConnected;
this.OnData = OnData;
this.OnDisconnected = OnDisconnected;
this.OnError = OnError;
this.config = config;
// create mtu sized receive buffer
rawReceiveBuffer = new byte[config.Mtu];
// create newClientEP either IPv4 or IPv6
newClientEP = config.DualMode
? new IPEndPoint(IPAddress.IPv6Any, 0)
: new IPEndPoint(IPAddress.Any, 0);
}
public virtual bool IsActive() => socket != null;
static Socket CreateServerSocket(bool DualMode, ushort port)
{
if (DualMode)
{
// IPv6 socket with DualMode @ "::" : port
Socket socket = new Socket(AddressFamily.InterNetworkV6, SocketType.Dgram, ProtocolType.Udp);
// enabling DualMode may throw:
// https://learn.microsoft.com/en-us/dotnet/api/System.Net.Sockets.Socket.DualMode?view=net-7.0
// attempt it, otherwise log but continue
// fixes: https://github.com/MirrorNetworking/Mirror/issues/3358
try
{
socket.DualMode = true;
}
catch (NotSupportedException e)
{
Log.Warning($"[KCP] Failed to set Dual Mode, continuing with IPv6 without Dual Mode. Error: {e}");
}
// for windows sockets, there's a rare issue where when using
// a server socket with multiple clients, if one of the clients
// is closed, the single server socket throws exceptions when
// sending/receiving. even if the socket is made for N clients.
//
// this actually happened to one of our users:
// https://github.com/MirrorNetworking/Mirror/issues/3611
//
// here's the in-depth explanation & solution:
//
// "As you may be aware, if a host receives a packet for a UDP
// port that is not currently bound, it may send back an ICMP
// "Port Unreachable" message. Whether or not it does this is
// dependent on the firewall, private/public settings, etc.
// On localhost, however, it will pretty much always send this
// packet back.
//
// Now, on Windows (and only on Windows), by default, a received
// ICMP Port Unreachable message will close the UDP socket that
// sent it; hence, the next time you try to receive on the
// socket, it will throw an exception because the socket has
// been closed by the OS.
//
// Obviously, this causes a headache in the multi-client,
// single-server socket set-up you have here, but luckily there
// is a fix:
//
// You need to utilise the not-often-required SIO_UDP_CONNRESET
// Winsock control code, which turns off this built-in behaviour
// of automatically closing the socket.
//
// Note that this ioctl code is only supported on Windows
// (XP and later), not on Linux, since it is provided by the
// Winsock extensions. Of course, since the described behavior
// is only the default behavior on Windows, this omission is not
// a major loss. If you are attempting to create a
// cross-platform library, you should cordon this off as
// Windows-specific code."
// https://stackoverflow.com/questions/74327225/why-does-sending-via-a-udpclient-cause-subsequent-receiving-to-fail
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
const uint IOC_IN = 0x80000000U;
const uint IOC_VENDOR = 0x18000000U;
const int SIO_UDP_CONNRESET = unchecked((int)(IOC_IN | IOC_VENDOR | 12));
socket.IOControl(SIO_UDP_CONNRESET, new byte[] { 0x00 }, null);
}
socket.Bind(new IPEndPoint(IPAddress.IPv6Any, port));
return socket;
}
else
{
// IPv4 socket @ "0.0.0.0" : port
Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
socket.Bind(new IPEndPoint(IPAddress.Any, port));
return socket;
}
}
public virtual void Start(ushort port)
{
// only start once
if (socket != null)
{
Log.Warning("[KCP] Server: already started!");
return;
}
// listen
socket = CreateServerSocket(config.DualMode, port);
// recv & send are called from main thread.
// need to ensure this never blocks.
// even a 1ms block per connection would stop us from scaling.
socket.Blocking = false;
// configure buffer sizes
Common.ConfigureSocketBuffers(socket, config.RecvBufferSize, config.SendBufferSize);
}
public void Send(int connectionId, ArraySegment<byte> segment, KcpChannel channel)
{
if (connections.TryGetValue(connectionId, out KcpServerConnection connection))
{
connection.SendData(segment, channel);
}
}
public void Disconnect(int connectionId)
{
if (connections.TryGetValue(connectionId, out KcpServerConnection connection))
{
connection.Disconnect();
}
}
// expose the whole IPEndPoint, not just the IP address. some need it.
public IPEndPoint GetClientEndPoint(int connectionId)
{
if (connections.TryGetValue(connectionId, out KcpServerConnection connection))
{
return connection.remoteEndPoint as IPEndPoint;
}
return null;
}
// io - input.
// virtual so it may be modified for relays, nonalloc workaround, etc.
// https://github.com/vis2k/where-allocation
// bool return because not all receives may be valid.
// for example, relay may expect a certain header.
protected virtual bool RawReceiveFrom(out ArraySegment<byte> segment, out int connectionId)
{
segment = default;
connectionId = 0;
if (socket == null) return false;
try
{
if (socket.ReceiveFromNonBlocking(rawReceiveBuffer, out segment, ref newClientEP))
{
// set connectionId to hash from endpoint
connectionId = Common.ConnectionHash(newClientEP);
return true;
}
}
catch (SocketException e)
{
// NOTE: SocketException is not a subclass of IOException.
// the other end closing the connection is not an 'error'.
// but connections should never just end silently.
// at least log a message for easier debugging.
Log.Info($"[KCP] Server: ReceiveFrom failed: {e}");
}
return false;
}
// io - out.
// virtual so it may be modified for relays, nonalloc workaround, etc.
// relays may need to prefix connId (and remoteEndPoint would be same for all)
protected virtual void RawSend(int connectionId, ArraySegment<byte> data)
{
// get the connection's endpoint
if (!connections.TryGetValue(connectionId, out KcpServerConnection connection))
{
Log.Warning($"[KCP] Server: RawSend invalid connectionId={connectionId}");
return;
}
try
{
socket.SendToNonBlocking(data, connection.remoteEndPoint);
}
catch (SocketException e)
{
Log.Error($"[KCP] Server: SendTo failed: {e}");
}
}
protected virtual KcpServerConnection CreateConnection(int connectionId)
{
// 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();
// create empty connection without peer first.
// we need it to set up peer callbacks.
// afterwards we assign the peer.
// events need to be wrapped with connectionIds
KcpServerConnection connection = new KcpServerConnection(
OnConnectedCallback,
(message, channel) => OnData(connectionId, message, channel),
OnDisconnectedCallback,
(error, reason) => OnError(connectionId, error, reason),
(data) => RawSend(connectionId, data),
config,
cookie,
newClientEP);
return connection;
// setup authenticated event that also adds to connections
void OnConnectedCallback(KcpServerConnection conn)
{
// add to connections dict after being authenticated.
connections.Add(connectionId, conn);
Log.Info($"[KCP] Server: added connection({connectionId})");
// setup Data + Disconnected events only AFTER the
// handshake. we don't want to fire OnServerDisconnected
// every time we receive invalid random data from the
// internet.
// setup data event
// finally, call mirror OnConnected event
Log.Info($"[KCP] Server: OnConnected({connectionId})");
IPEndPoint endPoint = conn.remoteEndPoint as IPEndPoint;
OnConnected(connectionId, endPoint);
}
void OnDisconnectedCallback()
{
// flag for removal
// (can't remove directly because connection is updated
// and event is called while iterating all connections)
connectionsToRemove.Add(connectionId);
// call mirror event
Log.Info($"[KCP] Server: OnDisconnected({connectionId})");
OnDisconnected(connectionId);
}
}
// receive + add + process once.
// best to call this as long as there is more data to receive.
void ProcessMessage(ArraySegment<byte> segment, int connectionId)
{
//Log.Info($"[KCP] server raw recv {msgLength} bytes = {BitConverter.ToString(buffer, 0, msgLength)}");
// is this a new connection?
if (!connections.TryGetValue(connectionId, out KcpServerConnection connection))
{
// create a new KcpConnection based on last received
// EndPoint. can be overwritten for where-allocation.
connection = CreateConnection(connectionId);
// DO NOT add to connections yet. only if the first message
// is actually the kcp handshake. otherwise it's either:
// * random data from the internet
// * or from a client connection that we just disconnected
// but that hasn't realized it yet, still sending data
// from last session that we should absolutely ignore.
//
//
// TODO this allocates a new KcpConnection for each new
// internet connection. not ideal, but C# UDP Receive
// already allocated anyway.
//
// expecting a MAGIC byte[] would work, but sending the raw
// UDP message without kcp's reliability will have low
// probability of being received.
//
// for now, this is fine.
// now input the message & process received ones
// connected event was set up.
// tick will process the first message and adds the
// connection if it was the handshake.
connection.RawInput(segment);
connection.TickIncoming();
// again, do not add to connections.
// if the first message wasn't the kcp handshake then
// connection will simply be garbage collected.
}
// existing connection: simply input the message into kcp
else
{
connection.RawInput(segment);
}
}
// process incoming messages. should be called before updating the world.
// virtual because relay may need to inject their own ping or similar.
readonly HashSet<int> connectionsToRemove = new HashSet<int>();
public virtual void TickIncoming()
{
// input all received messages into kcp
while (RawReceiveFrom(out ArraySegment<byte> segment, out int connectionId))
{
ProcessMessage(segment, connectionId);
}
// process inputs for all server connections
// (even if we didn't receive anything. need to tick ping etc.)
foreach (KcpServerConnection connection in connections.Values)
{
connection.TickIncoming();
}
// remove disconnected connections
// (can't do it in connection.OnDisconnected because Tick is called
// while iterating connections)
foreach (int connectionId in connectionsToRemove)
{
connections.Remove(connectionId);
}
connectionsToRemove.Clear();
}
// process outgoing messages. should be called after updating the world.
// virtual because relay may need to inject their own ping or similar.
public virtual void TickOutgoing()
{
// flush all server connections
foreach (KcpServerConnection connection in connections.Values)
{
connection.TickOutgoing();
}
}
// process incoming and outgoing for convenience.
// => ideally call ProcessIncoming() before updating the world and
// ProcessOutgoing() after updating the world for minimum latency
public virtual void Tick()
{
TickIncoming();
TickOutgoing();
}
public virtual void Stop()
{
// need to clear connections, otherwise they are in next session.
// fixes https://github.com/vis2k/kcp2k/pull/47
connections.Clear();
socket?.Close();
socket = null;
}
}
}

View File

@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: 9759159c6589494a9037f5e130a867ed
timeCreated: 1603787747
AssetOrigin:
serializedVersion: 1
productId: 129321
packageName: Mirror
packageVersion: 96.0.1
assetPath: Assets/Mirror/Transports/KCP/kcp2k/highlevel/KcpServer.cs
uploadId: 736421

View File

@ -0,0 +1,126 @@
// server needs to store a separate KcpPeer for each connection.
// as well as remoteEndPoint so we know where to send data to.
using System;
using System.Net;
namespace kcp2k
{
public class KcpServerConnection : KcpPeer
{
public readonly EndPoint remoteEndPoint;
// callbacks
// even for errors, to allow liraries to show popups etc.
// instead of logging directly.
// (string instead of Exception for ease of use and to avoid user panic)
//
// events are readonly, set in constructor.
// this ensures they are always initialized when used.
// fixes https://github.com/MirrorNetworking/Mirror/issues/3337 and more
protected readonly Action<KcpServerConnection> OnConnectedCallback;
protected readonly Action<ArraySegment<byte>, KcpChannel> OnDataCallback;
protected readonly Action OnDisconnectedCallback;
protected readonly Action<ErrorCode, string> OnErrorCallback;
protected readonly Action<ArraySegment<byte>> RawSendCallback;
public KcpServerConnection(
Action<KcpServerConnection> OnConnected,
Action<ArraySegment<byte>, KcpChannel> OnData,
Action OnDisconnected,
Action<ErrorCode, string> OnError,
Action<ArraySegment<byte>> OnRawSend,
KcpConfig config,
uint cookie,
EndPoint remoteEndPoint)
: base(config, cookie)
{
OnConnectedCallback = OnConnected;
OnDataCallback = OnData;
OnDisconnectedCallback = OnDisconnected;
OnErrorCallback = OnError;
RawSendCallback = OnRawSend;
this.remoteEndPoint = remoteEndPoint;
}
// callbacks ///////////////////////////////////////////////////////////
protected override void OnAuthenticated()
{
// once we receive the first client hello,
// immediately reply with hello so the client knows the security cookie.
SendHello();
OnConnectedCallback(this);
}
protected override void OnData(ArraySegment<byte> message, KcpChannel channel) =>
OnDataCallback(message, channel);
protected override void OnDisconnected() =>
OnDisconnectedCallback();
protected override void OnError(ErrorCode error, string message) =>
OnErrorCallback(error, message);
protected override void RawSend(ArraySegment<byte> data) =>
RawSendCallback(data);
////////////////////////////////////////////////////////////////////////
// insert raw IO. usually from socket.Receive.
// offset is useful for relays, where we may parse a header and then
// feed the rest to kcp.
public void RawInput(ArraySegment<byte> segment)
{
// ensure valid size: at least 1 byte for channel + 4 bytes for cookie
if (segment.Count <= 5) return;
// parse channel
// byte channel = segment[0]; ArraySegment[i] isn't supported in some older Unity Mono versions
byte channel = segment.Array[segment.Offset + 0];
// all server->client messages include the server's security cookie.
// all client->server messages except for the initial 'hello' include it too.
// parse the cookie and make sure it matches (except for initial hello).
Utils.Decode32U(segment.Array, segment.Offset + 1, out uint messageCookie);
// security: messages after authentication are expected to contain the cookie.
// this protects against UDP spoofing.
// simply drop the message if the cookie doesn't match.
if (state == KcpState.Authenticated)
{
if (messageCookie != cookie)
{
// Info is enough, don't scare users.
// => this can happen for malicious messages
// => it can also happen if client's Hello message was retransmitted multiple times, which is totally normal.
Log.Info($"[KCP] ServerConnection: dropped message with invalid cookie: {messageCookie} from {remoteEndPoint} expected: {cookie} state: {state}. This can happen if the client's Hello message was transmitted multiple times, or if an attacker attempted UDP spoofing.");
return;
}
}
// parse message
ArraySegment<byte> message = new ArraySegment<byte>(segment.Array, segment.Offset + 1+4, segment.Count - 1-4);
switch (channel)
{
case (byte)KcpChannel.Reliable:
{
OnRawInputReliable(message);
break;
}
case (byte)KcpChannel.Unreliable:
{
OnRawInputUnreliable(message);
break;
}
default:
{
// invalid channel indicates random internet noise.
// servers may receive random UDP data.
// just ignore it, but log for easier debugging.
Log.Warning($"[KCP] ServerConnection: invalid channel header: {channel} from {remoteEndPoint}, likely internet noise");
break;
}
}
}
}
}

View File

@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: 80a9b1ce9a6f14abeb32bfa9921d097b
timeCreated: 1602601483
AssetOrigin:
serializedVersion: 1
productId: 129321
packageName: Mirror
packageVersion: 96.0.1
assetPath: Assets/Mirror/Transports/KCP/kcp2k/highlevel/KcpServerConnection.cs
uploadId: 736421

View File

@ -0,0 +1,4 @@
namespace kcp2k
{
public enum KcpState { Connected, Authenticated, Disconnected }
}

View File

@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: 81a02c141a88d45d4a2f5ef68c6da75f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 129321
packageName: Mirror
packageVersion: 96.0.1
assetPath: Assets/Mirror/Transports/KCP/kcp2k/highlevel/KcpState.cs
uploadId: 736421

View File

@ -0,0 +1,14 @@
// A simple logger class that uses Console.WriteLine by default.
// Can also do Logger.LogMethod = Debug.Log for Unity etc.
// (this way we don't have to depend on UnityEngine)
using System;
namespace kcp2k
{
public static class Log
{
public static Action<string> Info = Console.WriteLine;
public static Action<string> Warning = Console.WriteLine;
public static Action<string> Error = Console.Error.WriteLine;
}
}

View File

@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: 7b5e1de98d6d84c3793a61cf7d8da9a4
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 129321
packageName: Mirror
packageVersion: 96.0.1
assetPath: Assets/Mirror/Transports/KCP/kcp2k/highlevel/Log.cs
uploadId: 736421