explicit message types

This commit is contained in:
mischa 2024-09-12 14:03:49 +02:00
parent 72132b89a8
commit d8e33f933f
3 changed files with 186 additions and 99 deletions

View File

@ -103,8 +103,25 @@ public struct EntityStateMessage : NetworkMessage
public ArraySegment<byte> payload; public ArraySegment<byte> payload;
} }
// state update for unreliable sync.
// baseline is always sent over Reliable channel.
public struct EntityStateMessageUnreliableBaseline : NetworkMessage
{
// baseline messages send their tick number as byte.
// delta messages are checked against that tick to avoid applying a
// delta on top of the wrong baseline.
// (byte is enough, we just need something small to compare against)
public byte baselineTick;
public uint netId;
// the serialized component data
// -> ArraySegment to avoid unnecessary allocations
public ArraySegment<byte> payload;
}
// state update for unreliable sync // state update for unreliable sync
public struct EntityStateMessageUnreliable : NetworkMessage // delta is always sent over Unreliable channel.
public struct EntityStateMessageUnreliableDelta : NetworkMessage
{ {
// baseline messages send their tick number as byte. // baseline messages send their tick number as byte.
// delta messages are checked against that tick to avoid applying a // delta messages are checked against that tick to avoid applying a

View File

@ -516,7 +516,8 @@ internal static void RegisterMessageHandlers(bool hostMode)
RegisterHandler<ObjectSpawnFinishedMessage>(_ => { }); RegisterHandler<ObjectSpawnFinishedMessage>(_ => { });
// host mode doesn't need state updates // host mode doesn't need state updates
RegisterHandler<EntityStateMessage>(_ => { }); RegisterHandler<EntityStateMessage>(_ => { });
RegisterHandler<EntityStateMessageUnreliable>(_ => { }); RegisterHandler<EntityStateMessageUnreliableBaseline>(_ => { });
RegisterHandler<EntityStateMessageUnreliableDelta>(_ => { });
} }
else else
{ {
@ -528,7 +529,8 @@ internal static void RegisterMessageHandlers(bool hostMode)
RegisterHandler<ObjectSpawnStartedMessage>(OnObjectSpawnStarted); RegisterHandler<ObjectSpawnStartedMessage>(OnObjectSpawnStarted);
RegisterHandler<ObjectSpawnFinishedMessage>(OnObjectSpawnFinished); RegisterHandler<ObjectSpawnFinishedMessage>(OnObjectSpawnFinished);
RegisterHandler<EntityStateMessage>(OnEntityStateMessage); RegisterHandler<EntityStateMessage>(OnEntityStateMessage);
RegisterHandler<EntityStateMessageUnreliable>(OnEntityStateMessageUnreliable); RegisterHandler<EntityStateMessageUnreliableBaseline>(OnEntityStateMessageUnreliableBaseline);
RegisterHandler<EntityStateMessageUnreliableDelta>(OnEntityStateMessageUnreliableDelta);
} }
// These handlers are the same for host and remote clients // These handlers are the same for host and remote clients
@ -1447,63 +1449,84 @@ static void OnEntityStateMessage(EntityStateMessage message)
else Debug.LogWarning($"Did not find target for sync message for {message.netId}. Were all prefabs added to the NetworkManager's spawnable list?\nNote: this can be completely normal because UDP messages may arrive out of order, so this message might have arrived after a Destroy message."); else Debug.LogWarning($"Did not find target for sync message for {message.netId}. Were all prefabs added to the NetworkManager's spawnable list?\nNote: this can be completely normal because UDP messages may arrive out of order, so this message might have arrived after a Destroy message.");
} }
static void OnEntityStateMessageUnreliable(EntityStateMessageUnreliable message, int channelId) static void OnEntityStateMessageUnreliableBaseline(EntityStateMessageUnreliableBaseline message, int channelId)
{ {
// safety check: baseline should always arrive over Reliable channel.
if (channelId != Channels.Reliable)
{
Debug.LogError($"Client OnEntityStateMessageUnreliableBaseline arrived on channel {channelId} instead of Reliable. This should never happen!");
return;
}
// Debug.Log($"NetworkClient.OnUpdateVarsMessage {msg.netId}"); // Debug.Log($"NetworkClient.OnUpdateVarsMessage {msg.netId}");
if (spawned.TryGetValue(message.netId, out NetworkIdentity identity) && identity != null) if (spawned.TryGetValue(message.netId, out NetworkIdentity identity) && identity != null)
{ {
// unreliable delta? // set the last received reliable baseline tick number.
if (channelId == Channels.Unreliable) identity.lastUnreliableBaselineReceived = message.baselineTick;
{
// unreliable state sync messages may arrive out of order.
// only ever apply state that's newer than the last received state.
// note that we send one EntityStateMessage per Entity,
// so there will be multiple with the same == timestamp.
//
// note that a reliable baseline may arrive before/after a delta.
// that is fine.
if (connection.remoteTimeStamp < identity.lastUnreliableStateTime)
{
// debug log to show that it's working.
// can be tested via LatencySimulation scramble easily.
Debug.Log($"Client caught out of order Unreliable state message for {identity.name}. This is fine.\nIdentity timestamp={identity.lastUnreliableStateTime:F3} batch remoteTimestamp={connection.remoteTimeStamp:F3}");
return;
}
// UDP messages may accidentally arrive twice.
// or even intentionally, if unreliableRedundancy is turned on.
else if (connection.remoteTimeStamp == identity.lastUnreliableStateTime)
{
// only log this if unreliableRedundancy is disabled.
// otherwise it's expected and will happen a lot.
if (!unreliableRedundancy) Debug.Log($"Client caught duplicate Unreliable state message for {identity.name}. This is fine.\nIdentity timestamp={identity.lastUnreliableStateTime:F3} batch remoteTimestamp={connection.remoteTimeStamp:F3}");
return;
}
// make sure this delta is for the correct baseline.
// we don't want to apply an old delta on top of a new baseline.
if (message.baselineTick != identity.lastUnreliableBaselineReceived)
{
Debug.Log($"Client caught Unreliable state message for old baseline for {identity} with baselineTick={identity.lastUnreliableBaselineReceived} messageBaseline={message.baselineTick}. This is fine.");
return;
}
// set the new last received time for unreliable
identity.lastUnreliableStateTime = connection.remoteTimeStamp;
}
// reliable baseline?
else if (channelId == Channels.Reliable)
{
// set the last received reliable baseline tick number.
identity.lastUnreliableBaselineReceived = message.baselineTick;
}
// iniital is always 'true' because unreliable state sync alwasy serializes full // iniital is always 'true' because unreliable state sync alwasy serializes full
using (NetworkReaderPooled reader = NetworkReaderPool.Get(message.payload)) using (NetworkReaderPooled reader = NetworkReaderPool.Get(message.payload))
{ {
// full state updates (initial=true) arrive over reliable. // full state updates (initial=true) arrive over reliable.
identity.DeserializeClient(reader, true);
}
}
// no warning. unreliable messages often arrive before/after the reliable spawn/despawn messages.
// else Debug.LogWarning($"Did not find target for sync message for {message.netId}. Were all prefabs added to the NetworkManager's spawnable list?\nNote: this can be completely normal because UDP messages may arrive out of order, so this message might have arrived after a Destroy message.");
}
static void OnEntityStateMessageUnreliableDelta(EntityStateMessageUnreliableDelta message, int channelId)
{
// safety check: baseline should always arrive over Reliable channel.
if (channelId != Channels.Unreliable)
{
Debug.LogError($"Client OnEntityStateMessageUnreliableDelta arrived on channel {channelId} instead of Unreliable. This should never happen!");
return;
}
// Debug.Log($"NetworkClient.OnUpdateVarsMessage {msg.netId}");
if (spawned.TryGetValue(message.netId, out NetworkIdentity identity) && identity != null)
{
// unreliable state sync messages may arrive out of order.
// only ever apply state that's newer than the last received state.
// note that we send one EntityStateMessage per Entity,
// so there will be multiple with the same == timestamp.
//
// note that a reliable baseline may arrive before/after a delta.
// that is fine.
if (connection.remoteTimeStamp < identity.lastUnreliableStateTime)
{
// debug log to show that it's working.
// can be tested via LatencySimulation scramble easily.
Debug.Log($"Client caught out of order Unreliable state message for {identity.name}. This is fine.\nIdentity timestamp={identity.lastUnreliableStateTime:F3} batch remoteTimestamp={connection.remoteTimeStamp:F3}");
return;
}
// UDP messages may accidentally arrive twice.
// or even intentionally, if unreliableRedundancy is turned on.
else if (connection.remoteTimeStamp == identity.lastUnreliableStateTime)
{
// only log this if unreliableRedundancy is disabled.
// otherwise it's expected and will happen a lot.
if (!unreliableRedundancy) Debug.Log($"Client caught duplicate Unreliable state message for {identity.name}. This is fine.\nIdentity timestamp={identity.lastUnreliableStateTime:F3} batch remoteTimestamp={connection.remoteTimeStamp:F3}");
return;
}
// make sure this delta is for the correct baseline.
// we don't want to apply an old delta on top of a new baseline.
if (message.baselineTick != identity.lastUnreliableBaselineReceived)
{
Debug.Log($"Client caught Unreliable state message for old baseline for {identity} with baselineTick={identity.lastUnreliableBaselineReceived} messageBaseline={message.baselineTick}. This is fine.");
return;
}
// set the new last received time for unreliable
identity.lastUnreliableStateTime = connection.remoteTimeStamp;
// iniital is always 'true' because unreliable state sync alwasy serializes full
using (NetworkReaderPooled reader = NetworkReaderPool.Get(message.payload))
{
// delta state updates (initial=false) arrive over unreliable. // delta state updates (initial=false) arrive over unreliable.
bool initialState = channelId == Channels.Reliable; identity.DeserializeClient(reader, false);
identity.DeserializeClient(reader, initialState);
} }
} }
// no warning. unreliable messages often arrive before/after the reliable spawn/despawn messages. // no warning. unreliable messages often arrive before/after the reliable spawn/despawn messages.
@ -1755,7 +1778,7 @@ static void BroadcastToServer(bool unreliableBaselineElapsed)
// (do this before baseline, since baseline clears dirty bits) // (do this before baseline, since baseline clears dirty bits)
if (writer.Position > 0) if (writer.Position > 0)
{ {
EntityStateMessageUnreliable message = new EntityStateMessageUnreliable EntityStateMessageUnreliableDelta message = new EntityStateMessageUnreliableDelta
{ {
baselineTick = identity.lastUnreliableBaselineSent, baselineTick = identity.lastUnreliableBaselineSent,
netId = identity.netId, netId = identity.netId,
@ -1789,7 +1812,7 @@ static void BroadcastToServer(bool unreliableBaselineElapsed)
identity.lastUnreliableBaselineSent = (byte)Time.frameCount; identity.lastUnreliableBaselineSent = (byte)Time.frameCount;
// send state update message // send state update message
EntityStateMessageUnreliable message = new EntityStateMessageUnreliable EntityStateMessageUnreliableBaseline message = new EntityStateMessageUnreliableBaseline
{ {
baselineTick = identity.lastUnreliableBaselineSent, baselineTick = identity.lastUnreliableBaselineSent,
netId = identity.netId, netId = identity.netId,

View File

@ -323,7 +323,8 @@ internal static void RegisterMessageHandlers()
RegisterHandler<NetworkPingMessage>(NetworkTime.OnServerPing, false); RegisterHandler<NetworkPingMessage>(NetworkTime.OnServerPing, false);
RegisterHandler<NetworkPongMessage>(NetworkTime.OnServerPong, false); RegisterHandler<NetworkPongMessage>(NetworkTime.OnServerPong, false);
RegisterHandler<EntityStateMessage>(OnEntityStateMessage, true); RegisterHandler<EntityStateMessage>(OnEntityStateMessage, true);
RegisterHandler<EntityStateMessageUnreliable>(OnEntityStateMessageUnreliable, true); RegisterHandler<EntityStateMessageUnreliableBaseline>(OnEntityStateMessageUnreliableBaseline, true);
RegisterHandler<EntityStateMessageUnreliableDelta>(OnEntityStateMessageUnreliableDelta, true);
RegisterHandler<TimeSnapshotMessage>(OnTimeSnapshotMessage, false); // unreliable may arrive before reliable authority went through RegisterHandler<TimeSnapshotMessage>(OnTimeSnapshotMessage, false); // unreliable may arrive before reliable authority went through
} }
@ -439,8 +440,15 @@ static void OnEntityStateMessage(NetworkConnectionToClient connection, EntitySta
} }
// for client's owned ClientToServer components. // for client's owned ClientToServer components.
static void OnEntityStateMessageUnreliable(NetworkConnectionToClient connection, EntityStateMessageUnreliable message, int channelId) static void OnEntityStateMessageUnreliableBaseline(NetworkConnectionToClient connection, EntityStateMessageUnreliableBaseline message, int channelId)
{ {
// safety check: baseline should always arrive over Reliable channel.
if (channelId != Channels.Reliable)
{
Debug.LogError($"Server OnEntityStateMessageUnreliableBaseline arrived on channel {channelId} instead of Reliable. This should never happen!");
return;
}
// need to validate permissions carefully. // need to validate permissions carefully.
// an attacker may attempt to modify a not-owned or not-ClientToServer component. // an attacker may attempt to modify a not-owned or not-ClientToServer component.
@ -450,47 +458,8 @@ static void OnEntityStateMessageUnreliable(NetworkConnectionToClient connection,
// owned by the connection? // owned by the connection?
if (identity.connectionToClient == connection) if (identity.connectionToClient == connection)
{ {
// unreliable delta? // set the last received reliable baseline tick number.
if (channelId == Channels.Unreliable) identity.lastUnreliableBaselineReceived = message.baselineTick;
{
// unreliable state sync messages may arrive out of order.
// only ever apply state that's newer than the last received state.
// note that we send one EntityStateMessage per Entity,
// so there will be multiple with the same == timestamp.
if (connection.remoteTimeStamp < identity.lastUnreliableStateTime)
{
// debug log to show that it's working.
// can be tested via LatencySimulation scramble easily.
Debug.Log($"Server caught out of order Unreliable state message for {identity.name}. This is fine.\nIdentity timestamp={identity.lastUnreliableStateTime:F3} batch remoteTimestamp={connection.remoteTimeStamp:F3}");
return;
}
// UDP messages may accidentally arrive twice.
// or even intentionally, if unreliableRedundancy is turned on.
else if (connection.remoteTimeStamp == identity.lastUnreliableStateTime)
{
// only log this if unreliableRedundancy is disabled.
// otherwise it's expected and will happen a lot.
if (!unreliableRedundancy) Debug.Log($"Server caught duplicate Unreliable state message for {identity.name}. This is fine.\nIdentity timestamp={identity.lastUnreliableStateTime:F3} batch remoteTimestamp={connection.remoteTimeStamp:F3}");
return;
}
// make sure this delta is for the correct baseline.
// we don't want to apply an old delta on top of a new baseline.
if (message.baselineTick != identity.lastUnreliableBaselineReceived)
{
Debug.Log($"Server caught Unreliable state message for old baseline for {identity} with baselineTick={identity.lastUnreliableBaselineReceived} messageBaseline={message.baselineTick}. This is fine.");
return;
}
// set the new last received time for unreliable
identity.lastUnreliableStateTime = connection.remoteTimeStamp;
}
// reliable baseline?
else if (channelId == Channels.Reliable)
{
// set the last received reliable baseline tick number.
identity.lastUnreliableBaselineReceived = message.baselineTick;
}
using (NetworkReaderPooled reader = NetworkReaderPool.Get(message.payload)) using (NetworkReaderPooled reader = NetworkReaderPool.Get(message.payload))
{ {
@ -498,9 +467,87 @@ static void OnEntityStateMessageUnreliable(NetworkConnectionToClient connection,
// failure to deserialize disconnects to prevent exploits. // failure to deserialize disconnects to prevent exploits.
// //
// full state updates (initial=true) arrive over reliable. // full state updates (initial=true) arrive over reliable.
if (!identity.DeserializeServer(reader, true))
{
if (exceptionsDisconnect)
{
Debug.LogError($"Server failed to deserialize client unreliable state for {identity.name} with netId={identity.netId}, Disconnecting.");
connection.Disconnect();
}
else
Debug.LogWarning($"Server failed to deserialize client unreliable state for {identity.name} with netId={identity.netId}.");
}
}
}
// An attacker may attempt to modify another connection's entity
// This could also be a race condition of message in flight when
// RemoveClientAuthority is called, so not malicious.
// Don't disconnect, just log the warning.
else
Debug.LogWarning($"EntityStateMessage from {connection} for {identity.name} without authority.");
}
// no warning. unreliable messages often arrive before/after the reliable spawn/despawn messages.
// else Debug.LogWarning($"Did not find target for sync message for {message.netId} . Note: this can be completely normal because UDP messages may arrive out of order, so this message might have arrived after a Destroy message.");
}
// for client's owned ClientToServer components.
static void OnEntityStateMessageUnreliableDelta(NetworkConnectionToClient connection, EntityStateMessageUnreliableDelta message, int channelId)
{
// safety check: baseline should always arrive over Reliable channel.
if (channelId != Channels.Unreliable)
{
Debug.LogError($"Server OnEntityStateMessageUnreliableDelta arrived on channel {channelId} instead of Unreliable. This should never happen!");
return;
}
// need to validate permissions carefully.
// an attacker may attempt to modify a not-owned or not-ClientToServer component.
// valid netId?
if (spawned.TryGetValue(message.netId, out NetworkIdentity identity) && identity != null)
{
// owned by the connection?
if (identity.connectionToClient == connection)
{
// unreliable state sync messages may arrive out of order.
// only ever apply state that's newer than the last received state.
// note that we send one EntityStateMessage per Entity,
// so there will be multiple with the same == timestamp.
if (connection.remoteTimeStamp < identity.lastUnreliableStateTime)
{
// debug log to show that it's working.
// can be tested via LatencySimulation scramble easily.
Debug.Log($"Server caught out of order Unreliable state message for {identity.name}. This is fine.\nIdentity timestamp={identity.lastUnreliableStateTime:F3} batch remoteTimestamp={connection.remoteTimeStamp:F3}");
return;
}
// UDP messages may accidentally arrive twice.
// or even intentionally, if unreliableRedundancy is turned on.
else if (connection.remoteTimeStamp == identity.lastUnreliableStateTime)
{
// only log this if unreliableRedundancy is disabled.
// otherwise it's expected and will happen a lot.
if (!unreliableRedundancy) Debug.Log($"Server caught duplicate Unreliable state message for {identity.name}. This is fine.\nIdentity timestamp={identity.lastUnreliableStateTime:F3} batch remoteTimestamp={connection.remoteTimeStamp:F3}");
return;
}
// make sure this delta is for the correct baseline.
// we don't want to apply an old delta on top of a new baseline.
if (message.baselineTick != identity.lastUnreliableBaselineReceived)
{
Debug.Log($"Server caught Unreliable state message for old baseline for {identity} with baselineTick={identity.lastUnreliableBaselineReceived} messageBaseline={message.baselineTick}. This is fine.");
return;
}
// set the new last received time for unreliable
identity.lastUnreliableStateTime = connection.remoteTimeStamp;
using (NetworkReaderPooled reader = NetworkReaderPool.Get(message.payload))
{
// DeserializeServer checks permissions internally.
// failure to deserialize disconnects to prevent exploits.
//
// delta state updates (initial=false) arrive over unreliable. // delta state updates (initial=false) arrive over unreliable.
bool initialState = channelId == Channels.Reliable; if (!identity.DeserializeServer(reader, false))
if (!identity.DeserializeServer(reader, initialState))
{ {
if (exceptionsDisconnect) if (exceptionsDisconnect)
{ {
@ -2099,7 +2146,7 @@ static void BroadcastToConnection(NetworkConnectionToClient connection, bool unr
// reliable baseline also clears dirty bits, so unreliable must be sent first. // reliable baseline also clears dirty bits, so unreliable must be sent first.
if (deltaSerialization != null) if (deltaSerialization != null)
{ {
EntityStateMessageUnreliable message = new EntityStateMessageUnreliable EntityStateMessageUnreliableDelta message = new EntityStateMessageUnreliableDelta
{ {
baselineTick = identity.lastUnreliableBaselineSent, baselineTick = identity.lastUnreliableBaselineSent,
netId = identity.netId, netId = identity.netId,
@ -2124,7 +2171,7 @@ static void BroadcastToConnection(NetworkConnectionToClient connection, bool unr
// just something small to compare against. // just something small to compare against.
identity.lastUnreliableBaselineSent = (byte)Time.frameCount; identity.lastUnreliableBaselineSent = (byte)Time.frameCount;
EntityStateMessageUnreliable message = new EntityStateMessageUnreliable EntityStateMessageUnreliableBaseline message = new EntityStateMessageUnreliableBaseline
{ {
baselineTick = identity.lastUnreliableBaselineSent, baselineTick = identity.lastUnreliableBaselineSent,
netId = identity.netId, netId = identity.netId,