GameCult.Networking: RUDP socket transports drop and count strays
A malformed frame, a foreign connection id or a packet the session refuses threw out of TryReceiveOnce. They are now counted in Stats.PacketsDropped and the read reports progress. The client-side transport checks the connection id before it claims the peer endpoint; the server listener drops refused packets instead of throwing. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
dbe4f8a850
commit
b8dbcfe40c
2 changed files with 128 additions and 8 deletions
|
|
@ -33,6 +33,12 @@ namespace GameCult.Networking
|
|||
/// Gets the number of bytes received, including transport framing.
|
||||
/// </summary>
|
||||
public long BytesReceived { get; internal set; }
|
||||
/// <summary>
|
||||
/// Gets the number of datagrams read and discarded because they belong to no session on
|
||||
/// this transport: malformed frames, another session's connection id, a sender that is not
|
||||
/// the peer, or a packet the session refuses.
|
||||
/// </summary>
|
||||
public long PacketsDropped { get; internal set; }
|
||||
|
||||
internal CultNetTransportStats Snapshot()
|
||||
{
|
||||
|
|
@ -41,7 +47,8 @@ namespace GameCult.Networking
|
|||
FramesSent = FramesSent,
|
||||
FramesReceived = FramesReceived,
|
||||
BytesSent = BytesSent,
|
||||
BytesReceived = BytesReceived
|
||||
BytesReceived = BytesReceived,
|
||||
PacketsDropped = PacketsDropped
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -2081,23 +2088,55 @@ namespace GameCult.Networking
|
|||
}
|
||||
|
||||
_stats.BytesReceived += received;
|
||||
|
||||
// What a datagram carries is the sender's business, not a fault of this process: a
|
||||
// malformed frame, another session's connection id, a sender that is not the peer and
|
||||
// a packet the session refuses are dropped and counted. Only the socket ends a loop.
|
||||
// The id is checked before the peer endpoint is claimed, so a stray cannot become it.
|
||||
var wire = new byte[received];
|
||||
Array.Copy(buffer, wire, received);
|
||||
CultNetRudpPacket packet;
|
||||
try
|
||||
{
|
||||
packet = CultNetRudpPacketCodec.Decode(wire);
|
||||
}
|
||||
catch (Exception error) when (error is InvalidOperationException || error is ArgumentException)
|
||||
{
|
||||
_stats.PacketsDropped++;
|
||||
return true;
|
||||
}
|
||||
if (packet.ConnectionId != _session.ConnectionId)
|
||||
{
|
||||
_stats.PacketsDropped++;
|
||||
return true;
|
||||
}
|
||||
if (_remoteEndPoint == null)
|
||||
{
|
||||
_remoteEndPoint = remote;
|
||||
}
|
||||
else if (!_remoteEndPoint.Equals(remote))
|
||||
{
|
||||
_stats.PacketsDropped++;
|
||||
return true;
|
||||
}
|
||||
|
||||
var wire = new byte[received];
|
||||
Array.Copy(buffer, wire, received);
|
||||
var packet = CultNetRudpPacketCodec.Decode(wire);
|
||||
TracePacket("rx", packet, received, remote);
|
||||
if (_mode == CultNetRudpSocketMode.Server && packet.PacketType == CultNetRudpPacketType.Connect)
|
||||
{
|
||||
lock (_sessionGate)
|
||||
SendPacket(_session.AcceptConnect(packet, NowMs()));
|
||||
{
|
||||
CultNetRudpPacket accept;
|
||||
try
|
||||
{
|
||||
accept = _session.AcceptConnect(packet, NowMs());
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
_stats.PacketsDropped++;
|
||||
return true;
|
||||
}
|
||||
SendPacket(accept);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -2105,7 +2144,15 @@ namespace GameCult.Networking
|
|||
CultNetRudpPacket? acknowledgement = null;
|
||||
lock (_sessionGate)
|
||||
{
|
||||
result = _session.Receive(packet, NowMs());
|
||||
try
|
||||
{
|
||||
result = _session.Receive(packet, NowMs());
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
_stats.PacketsDropped++;
|
||||
return true;
|
||||
}
|
||||
if (result.Reply != null)
|
||||
SendPacket(result.Reply);
|
||||
SendPackets(result.ReadyToSend);
|
||||
|
|
@ -2478,13 +2525,15 @@ namespace GameCult.Networking
|
|||
{
|
||||
packet = CultNetRudpPacketCodec.Decode(wire);
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
catch (Exception error) when (error is InvalidOperationException || error is ArgumentException)
|
||||
{
|
||||
_stats.PacketsDropped++;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (packet.ConnectionId != _connectionId)
|
||||
{
|
||||
_stats.PacketsDropped++;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -2508,6 +2557,7 @@ namespace GameCult.Networking
|
|||
|
||||
if (!_peers.TryGetValue(peerKey, out var existingPeer))
|
||||
{
|
||||
_stats.PacketsDropped++;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -2515,7 +2565,15 @@ namespace GameCult.Networking
|
|||
CultNetRudpPacket? acknowledgement = null;
|
||||
lock (existingPeer.SessionGate)
|
||||
{
|
||||
result = existingPeer.Session.Receive(packet, NowMs());
|
||||
try
|
||||
{
|
||||
result = existingPeer.Session.Receive(packet, NowMs());
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
_stats.PacketsDropped++;
|
||||
return true;
|
||||
}
|
||||
if (result.Reply != null)
|
||||
SendPacket(existingPeer.RemoteEndPoint, result.Reply);
|
||||
SendPackets(existingPeer, result.ReadyToSend);
|
||||
|
|
|
|||
|
|
@ -1193,6 +1193,68 @@ namespace GameCult.Networking.Tests
|
|||
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void RudpSocketTransport_DropsStraysAndKeepsServingItsPeer()
|
||||
{
|
||||
using var serverSocket = BindUdpSocket();
|
||||
using var clientSocket = BindUdpSocket();
|
||||
using var strayHost = BindUdpSocket();
|
||||
var serverEndPoint = serverSocket.LocalEndPoint!;
|
||||
var clientEndPoint = clientSocket.LocalEndPoint!;
|
||||
const uint connectionId = 0x10203050;
|
||||
using var server = new CultNetRudpSocketTransportConnection(new CultNetRudpSocketTransportOptions
|
||||
{
|
||||
RuntimeId = "csharp-rudp-server",
|
||||
Socket = serverSocket,
|
||||
Mode = CultNetRudpSocketMode.Server,
|
||||
ConnectionId = connectionId
|
||||
});
|
||||
using var client = new CultNetRudpSocketTransportConnection(new CultNetRudpSocketTransportOptions
|
||||
{
|
||||
RuntimeId = "csharp-rudp-client",
|
||||
Socket = clientSocket,
|
||||
Mode = CultNetRudpSocketMode.Client,
|
||||
RemoteEndPoint = serverEndPoint,
|
||||
ConnectionId = connectionId
|
||||
});
|
||||
var foreign = new CultNetRudpSession(new CultNetRudpSessionOptions { ConnectionId = 0x0BADF10E });
|
||||
var strays = new[]
|
||||
{
|
||||
Encoding.UTF8.GetBytes("not a rudp packet"),
|
||||
new byte[] { 1 },
|
||||
CultNetRudpPacketCodec.Encode(foreign.CreateConnect(0, Encoding.UTF8.GetBytes("foreign")))
|
||||
};
|
||||
|
||||
// A stray that arrives before the peer must not become the peer.
|
||||
foreach (var stray in strays)
|
||||
strayHost.SendTo(stray, serverEndPoint);
|
||||
for (var index = 0; index < strays.Length; index++)
|
||||
Assert.That(server.ReceiveOnce(), Is.Null);
|
||||
Assert.That(server.Stats.PacketsDropped, Is.EqualTo(strays.Length));
|
||||
|
||||
client.Connect(Encoding.UTF8.GetBytes("join"));
|
||||
PumpRudpHandshake(client, server);
|
||||
|
||||
// A moved flow answers from the peer's own address with someone else's session.
|
||||
foreach (var stray in strays)
|
||||
{
|
||||
serverSocket.SendTo(stray, clientEndPoint);
|
||||
clientSocket.SendTo(stray, serverEndPoint);
|
||||
}
|
||||
for (var index = 0; index < strays.Length; index++)
|
||||
{
|
||||
Assert.That(client.ReceiveOnce(), Is.Null);
|
||||
Assert.That(server.ReceiveOnce(), Is.Null);
|
||||
}
|
||||
Assert.That(client.Stats.PacketsDropped, Is.EqualTo(strays.Length));
|
||||
Assert.That(server.Stats.PacketsDropped, Is.EqualTo(2 * strays.Length));
|
||||
|
||||
client.Send("schema", Encoding.UTF8.GetBytes("still here"));
|
||||
Assert.That(Encoding.UTF8.GetString(ReceiveRudpFrame(server).Payload), Is.EqualTo("still here"));
|
||||
server.Send("schema", Encoding.UTF8.GetBytes("server still here"));
|
||||
Assert.That(Encoding.UTF8.GetString(ReceiveRudpFrame(client).Payload), Is.EqualTo("server still here"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void RudpSocketTransport_CarriesCultNetSchemaMessages()
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in a new issue