Skip to content

Commit

Permalink
fix: #54 mismatching cookie spam. server now always includes the cook…
Browse files Browse the repository at this point in the history
…ie, client now always checks the cookie, client now always includes the cookie
  • Loading branch information
mischa committed Oct 28, 2023
1 parent 64ac63b commit 480a5f8
Show file tree
Hide file tree
Showing 6 changed files with 36 additions and 47 deletions.
4 changes: 2 additions & 2 deletions kcp2k/kcp2k.Tests/ClientServerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -522,7 +522,7 @@ public void ClientToServerReliableInvalidCookie()
ConnectClientBlocking();

// change client to a wrong cookie
client.receivedCookie[0] += 1;
client.cookie += 1;

// try to send a message with wrong cookie
client.Send(new ArraySegment<byte>(new byte[]{0x01, 0x02}), KcpChannel.Reliable);
Expand All @@ -542,7 +542,7 @@ public void ClientToServerUnreliableInvalidCookie()
ConnectClientBlocking();

// change client to a wrong cookie
client.receivedCookie[0] += 1;
client.cookie += 1;

// try to send a message with wrong cookie
client.Send(new ArraySegment<byte>(new byte[]{0x01, 0x02}), KcpChannel.Unreliable);
Expand Down
3 changes: 3 additions & 0 deletions kcp2k/kcp2k.Tests/KcpPeerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ public void MaxReceiveRate()
Assert.That(peer.MaxReceiveRate, Is.EqualTo(15_296_000));
}

// TODO enable again with KcpClient and KcpServerConnection
/*
// test to prevent https://github.com/vis2k/kcp2k/issues/49
[Test]
public void InputTooSmall()
Expand All @@ -63,5 +65,6 @@ public void InputTooSmall()
peer.RawInput(new byte[]{1, 2, 3, 4});
peer.RawInput(new byte[]{1, 3, 3, 4, 5});
}
*/
}
}
6 changes: 4 additions & 2 deletions kcp2k/kcp2k/highlevel/KcpClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -128,8 +128,10 @@ public void Connect(string address, ushort port)
// bind to endpoint so we can use send/recv instead of sendto/recvfrom.
socket.Connect(remoteEndPoint);

// client should send handshake to server as very first message
SendHandshake();
// 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.
Expand Down
2 changes: 1 addition & 1 deletion kcp2k/kcp2k/highlevel/KcpHeader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ namespace kcp2k
public enum KcpHeader : byte
{
// don't react on 0x00. might help to filter out random noise.
Handshake = 1,
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.
Expand Down
59 changes: 23 additions & 36 deletions kcp2k/kcp2k/highlevel/KcpPeer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,10 @@ public abstract class KcpPeer
// => 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
protected uint cookie;
internal 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];
// TODO cookie conversion byte[] to avoid runtime allocations when converting
// internal readonly byte[] cookieBytes = new byte[4];

// state: connected as soon as we create the peer.
// leftover from KcpConnection. remove it after refactoring later.
Expand Down Expand Up @@ -133,7 +132,8 @@ protected KcpPeer(KcpConfig config, uint cookie)
protected void Reset(KcpConfig config)
{
// reset state
Array.Clear(receivedCookie);
// Array.Clear(cookieBytes);
cookie = 0;
state = KcpState.Connected;
lastReceiveTime = 0;
lastPingTime = 0;
Expand Down Expand Up @@ -295,26 +295,13 @@ void TickIncoming_Connected(uint time)
// message type FSM. no default so we never miss a case.
switch (header)
{
case KcpHeader.Handshake:
case KcpHeader.Hello:
{
// we were waiting for a handshake.
// we were waiting for a Hello message.
// it proves that the other end speaks our protocol.

// parse the cookie
if (message.Count != 4)
{
// pass error to user callback. no need to log it manually.
OnError(ErrorCode.InvalidReceive, $"{GetType()}: 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($"{GetType()}: received handshake with cookie={prettyCookie}");
// log with previously parsed cookie
Log.Info($"{GetType()}: received hello with cookie={cookie}");
state = KcpState.Authenticated;
OnAuthenticated();
break;
Expand Down Expand Up @@ -352,9 +339,9 @@ void TickIncoming_Authenticated(uint time)
// message type FSM. no default so we never miss a case.
switch (header)
{
case KcpHeader.Handshake:
case KcpHeader.Hello:
{
// should never receive another handshake after auth
// 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();
Expand Down Expand Up @@ -565,9 +552,12 @@ void RawSendReliable(byte[] data, int length)
// from 0, with 1 byte
rawSendBuffer[0] = (byte)KcpChannel.Reliable;

// convert cookie to bytes
byte[] cookieBytes = BitConverter.GetBytes(cookie); // TODO nonalloc

// write handshake cookie to protect against UDP spoofing.
// from 1, with 4 bytes
Buffer.BlockCopy(receivedCookie, 0, rawSendBuffer, 1, 4);
Buffer.BlockCopy(cookieBytes, 0, rawSendBuffer, 1, 4);

// write data
// from 5, with N bytes
Expand Down Expand Up @@ -620,9 +610,12 @@ void SendUnreliable(ArraySegment<byte> message)
// from 0, with 1 byte
rawSendBuffer[0] = (byte)KcpChannel.Unreliable;

// convert cookie to bytes
byte[] cookieBytes = BitConverter.GetBytes(cookie); // TODO nonalloc

// write handshake cookie to protect against UDP spoofing.
// from 1, with 4 bytes
Buffer.BlockCopy(receivedCookie, 0, rawSendBuffer, 1, 4);
Buffer.BlockCopy(cookieBytes, 0, rawSendBuffer, 1, 4);

// write data
// from 5, with N bytes
Expand All @@ -639,20 +632,14 @@ void SendUnreliable(ArraySegment<byte> message)
// * 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 SendHandshake()
public void SendHello()
{
// 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);
// send an empty message with 'Hello' header.
// cookie is automatically included in all messages.

// GetType() shows Server/ClientConn instead of just Connection.
Log.Info($"{GetType()}: sending handshake to other end with cookie={cookie}");
SendReliable(KcpHeader.Handshake, new ArraySegment<byte>(cookieBytes));
SendReliable(KcpHeader.Hello, ArraySegment<byte>.Empty);
}

public void SendData(ArraySegment<byte> data, KcpChannel channel)
Expand Down
9 changes: 3 additions & 6 deletions kcp2k/kcp2k/highlevel/KcpServerConnection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,9 @@ public KcpServerConnection(
// callbacks ///////////////////////////////////////////////////////////
protected override void OnAuthenticated()
{
// only send handshake to client AFTER we received his
// handshake in OnAuthenticated.
// we don't want to reply to random internet messages
// with handshakes each time.
SendHandshake();

// once we receive the first client hello,
// immediately reply with hello so the client knows the security cookie.
SendHello();
OnConnectedCallback(this);
}

Expand Down

0 comments on commit 480a5f8

Please sign in to comment.