Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
189 changes: 118 additions & 71 deletions F1Server.Core/Data/ReceivedPacketData.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

Expand Down Expand Up @@ -65,6 +66,21 @@ public ReceivedPacketData()
/// </summary>
public PacketHeader? PacketHeader { get; private set; }

/// <summary>
/// Bounded classification of why the packet header was rejected, safe to use as a metric dimension
/// </summary>
public HeaderRejectionCode HeaderRejectionCode { get; private set; }

/// <summary>
/// Exception caught while parsing the packet header, set only when <see cref="HeaderRejectionCode"/> is <see cref="Enumerations.HeaderRejectionCode.ParseException"/>
/// </summary>
public Exception? HeaderParseException { get; private set; }

/// <summary>
/// Game version reported by the packet, set only when <see cref="HeaderRejectionCode"/> is <see cref="Enumerations.HeaderRejectionCode.Undersized2023Header"/>
/// </summary>
public ushort ReportedGameVersion { get; private set; }

#endregion // Properties

#region Methods
Expand All @@ -76,6 +92,9 @@ public ReceivedPacketData()
public void SetRawData(byte[] rawData)
{
PacketHeader = null;
HeaderRejectionCode = HeaderRejectionCode.None;
HeaderParseException = null;
ReportedGameVersion = 0;

_rawData = new byte[rawData.Length];

Expand All @@ -92,105 +111,133 @@ public void SetRawData(byte[] rawData)
/// <param name="dataPacket">Complete received packet content</param>
private void AnalyzePacketHeader(ReadOnlySpan<byte> dataPacket)
{
ref var memRef = ref MemoryMarshal.GetReference(dataPacket);

if (dataPacket.Length >= ConstData.F12019HeaderSize)
{
try
{
var contentOffset = 0;
ParseHeader(dataPacket);
}
else
{
HeaderRejectionCode = HeaderRejectionCode.PacketTooShort;
}
}

/// <summary>
/// Parses the packet header fields, containing any parsing exception so a malformed packet cannot crash the receiver
/// </summary>
/// <param name="dataPacket">Complete received packet content, at least <see cref="ConstData.F12019HeaderSize"/> bytes long</param>
[ExcludeFromCodeCoverage(Justification = "The try/catch wrapper itself cannot be exercised: every offset ParseHeaderFields reads is validated against the packet length by the guards in AnalyzePacketHeader before this method is called, and neither Enum.ToObject nor the object initializer can throw, so no packet observed in practice reaches this catch. Unsafe.ReadUnaligned itself performs no bounds checking; safety here comes entirely from those length guards, not from the read call")]
private void ParseHeader(ReadOnlySpan<byte> dataPacket)
{
try
{
ParseHeaderFields(dataPacket);
}
catch (Exception ex)
{
PacketHeader = null;
HeaderRejectionCode = HeaderRejectionCode.ParseException;
HeaderParseException = ex;
}
}

/// <summary>
/// Reads the packet header fields from the raw packet bytes
/// </summary>
/// <param name="dataPacket">Complete received packet content, at least <see cref="ConstData.F12019HeaderSize"/> bytes long</param>
private void ParseHeaderFields(ReadOnlySpan<byte> dataPacket)
{
ref var memRef = ref MemoryMarshal.GetReference(dataPacket);

var contentOffset = 0;

var gameVersion = Unsafe.ReadUnaligned<ushort>(ref memRef);
var gameVersion = Unsafe.ReadUnaligned<ushort>(ref memRef);

contentOffset += ConstData.TypeUInt16;
contentOffset += ConstData.TypeUInt16;

// From 2023 the header carries additional fields (GameYear, OverallFrameIdentifier)
// that are read further below without their own bounds check; reject undersized
// packets here so those reads cannot go past the end of the array.
if (gameVersion >= 2023 && dataPacket.Length < ConstData.F12023HeaderSize)
{
return;
}
// From 2023 the header carries additional fields (GameYear, OverallFrameIdentifier)
// that are read further below without their own bounds check; reject undersized
// packets here so those reads cannot go past the end of the array.
if (gameVersion >= 2023 && dataPacket.Length < ConstData.F12023HeaderSize)
{
HeaderRejectionCode = HeaderRejectionCode.Undersized2023Header;
ReportedGameVersion = gameVersion;

PacketHeader = new()
{
// Format - uint16
GameVersion = gameVersion
};
return;
}

// Game year (since 2023) - uint8
if (PacketHeader.GameVersion >= 2023)
{
PacketHeader.GameYear = Unsafe.ReadUnaligned<byte>(ref Unsafe.Add(ref memRef, contentOffset));
PacketHeader = new()
{
// Format - uint16
GameVersion = gameVersion
};

contentOffset += ConstData.TypeUInt8;
}
// Game year (since 2023) - uint8
if (PacketHeader.GameVersion >= 2023)
{
PacketHeader.GameYear = Unsafe.ReadUnaligned<byte>(ref Unsafe.Add(ref memRef, contentOffset));

contentOffset += ConstData.TypeUInt8;
}

// Major version - uint8
PacketHeader.MajorGameVersion = Unsafe.ReadUnaligned<byte>(ref Unsafe.Add(ref memRef, contentOffset));
// Major version - uint8
PacketHeader.MajorGameVersion = Unsafe.ReadUnaligned<byte>(ref Unsafe.Add(ref memRef, contentOffset));

contentOffset += ConstData.TypeUInt8;
contentOffset += ConstData.TypeUInt8;

// Minor version - uint8
PacketHeader.MinorGameVersion = Unsafe.ReadUnaligned<byte>(ref Unsafe.Add(ref memRef, contentOffset));
// Minor version - uint8
PacketHeader.MinorGameVersion = Unsafe.ReadUnaligned<byte>(ref Unsafe.Add(ref memRef, contentOffset));

contentOffset += ConstData.TypeUInt8;
contentOffset += ConstData.TypeUInt8;

// Packet version - uint8
PacketHeader.PacketVersion = Unsafe.ReadUnaligned<byte>(ref Unsafe.Add(ref memRef, contentOffset));
// Packet version - uint8
PacketHeader.PacketVersion = Unsafe.ReadUnaligned<byte>(ref Unsafe.Add(ref memRef, contentOffset));

contentOffset += ConstData.TypeUInt8;
contentOffset += ConstData.TypeUInt8;

// Packet type (id) - uint8
var packetType = Unsafe.ReadUnaligned<byte>(ref Unsafe.Add(ref memRef, contentOffset));
// Packet type (id) - uint8
var packetType = Unsafe.ReadUnaligned<byte>(ref Unsafe.Add(ref memRef, contentOffset));

PacketHeader.PacketType = (PacketTypes)Enum.ToObject(typeof(PacketTypes), packetType + 1);
PacketHeader.PacketType = (PacketTypes)Enum.ToObject(typeof(PacketTypes), packetType + 1);

contentOffset += ConstData.TypeUInt8;
contentOffset += ConstData.TypeUInt8;

// Session id - uint64
PacketHeader.UniqueSessionId = Unsafe.ReadUnaligned<ulong>(ref Unsafe.Add(ref memRef, contentOffset));
// Session id - uint64
PacketHeader.UniqueSessionId = Unsafe.ReadUnaligned<ulong>(ref Unsafe.Add(ref memRef, contentOffset));

contentOffset += ConstData.TypeUInt64;
contentOffset += ConstData.TypeUInt64;

// Session time - float
PacketHeader.SessionTime = Unsafe.ReadUnaligned<float>(ref Unsafe.Add(ref memRef, contentOffset));
PacketHeader.SessionTimeNum = Unsafe.ReadUnaligned<uint>(ref Unsafe.Add(ref memRef, contentOffset));
// Session time - float
PacketHeader.SessionTime = Unsafe.ReadUnaligned<float>(ref Unsafe.Add(ref memRef, contentOffset));
PacketHeader.SessionTimeNum = Unsafe.ReadUnaligned<uint>(ref Unsafe.Add(ref memRef, contentOffset));

contentOffset += ConstData.TypeFloat;
contentOffset += ConstData.TypeFloat;

// Frame identifier - uint32
PacketHeader.FrameIdentifier = Unsafe.ReadUnaligned<uint>(ref Unsafe.Add(ref memRef, contentOffset));
// Frame identifier - uint32
PacketHeader.FrameIdentifier = Unsafe.ReadUnaligned<uint>(ref Unsafe.Add(ref memRef, contentOffset));

contentOffset += ConstData.TypeUInt32;
contentOffset += ConstData.TypeUInt32;

// Overall frame identifier (doesn't go back after flashbacks)
if (PacketHeader.GameVersion >= 2023)
{
PacketHeader.OverallFrameIdentifier = Unsafe.ReadUnaligned<uint>(ref Unsafe.Add(ref memRef, contentOffset));
// Overall frame identifier (doesn't go back after flashbacks)
if (PacketHeader.GameVersion >= 2023)
{
PacketHeader.OverallFrameIdentifier = Unsafe.ReadUnaligned<uint>(ref Unsafe.Add(ref memRef, contentOffset));

contentOffset += ConstData.TypeUInt32;
}
contentOffset += ConstData.TypeUInt32;
}

// Car index - uint8
PacketHeader.PlayerCarIndex = Unsafe.ReadUnaligned<byte>(ref Unsafe.Add(ref memRef, contentOffset));
// Car index - uint8
PacketHeader.PlayerCarIndex = Unsafe.ReadUnaligned<byte>(ref Unsafe.Add(ref memRef, contentOffset));

if (PacketHeader.GameVersion >= 2020 && dataPacket.Length >= ConstData.F12020HeaderSize)
{
contentOffset += ConstData.TypeUInt8;
if (PacketHeader.GameVersion >= 2020 && dataPacket.Length >= ConstData.F12020HeaderSize)
{
contentOffset += ConstData.TypeUInt8;

// Secondary car index - uint8
PacketHeader.PlayerCarIndexSecondary = Unsafe.ReadUnaligned<byte>(ref Unsafe.Add(ref memRef, contentOffset));
}
else
{
PacketHeader.PlayerCarIndexSecondary = 255;
}
}
catch
{
PacketHeader = null;
}
// Secondary car index - uint8
PacketHeader.PlayerCarIndexSecondary = Unsafe.ReadUnaligned<byte>(ref Unsafe.Add(ref memRef, contentOffset));
}
else
{
PacketHeader.PlayerCarIndexSecondary = 255;
}
}

Expand Down
27 changes: 27 additions & 0 deletions F1Server.Core/Enumerations/HeaderRejectionCode.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
namespace F1Server.Core.Enumerations;

/// <summary>
/// Bounded classification of why a packet header was rejected, safe to use as a metric dimension
/// </summary>
public enum HeaderRejectionCode
{
/// <summary>
/// The header was not rejected
/// </summary>
None = 0,

/// <summary>
/// The packet was shorter than the minimum header size
/// </summary>
PacketTooShort,

/// <summary>
/// The packet reported game version 2023 or later but was shorter than the 2023+ header size
/// </summary>
Undersized2023Header,

/// <summary>
/// An exception was thrown while parsing the packet header
/// </summary>
ParseException
}
36 changes: 36 additions & 0 deletions F1Server.Service/Runtime/PacketProcessor.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;

using F1Server.Core;
Expand Down Expand Up @@ -199,6 +200,10 @@ public bool ProcessPacket(ReceivedPacketData receivedPacketData)

isProcessed = InternalProcessPackets(receivedPacketData);
}
else
{
RecordRejectedHeader(receivedPacketData);
}
}
catch (Exception ex)
{
Expand Down Expand Up @@ -241,6 +246,37 @@ private void RaisePacketReceived(PacketHeader packetHeader)
}
}

/// <summary>
/// Logs and counts a rejected packet header using its bounded <see cref="HeaderRejectionCode"/> as the metric dimension,
/// keeping the untrusted packet details (length, reported game version, exception text) in the log message only
/// </summary>
/// <param name="receivedPacketData">Received packet whose header was rejected</param>
private void RecordRejectedHeader(ReceivedPacketData receivedPacketData)
{
LastError = receivedPacketData.HeaderRejectionCode.ToString();

_appData?.AppMetrics?.ProcessingErrors.Add(1, new KeyValuePair<string, object?>("LastError", receivedPacketData.HeaderRejectionCode.ToString()));

RecordHeaderParseExceptionIfPresent(receivedPacketData.HeaderParseException);

Logger?.LogWarning("Rejected packet header: {RejectionCode}, packet length {PacketLength} bytes, reported game version {GameVersion}", receivedPacketData.HeaderRejectionCode, receivedPacketData.PacketLength, receivedPacketData.ReportedGameVersion);
}

/// <summary>
/// Logs the exception caught while parsing a packet header, if one was recorded
/// </summary>
/// <param name="exception">Exception caught while parsing the packet header, or <see langword="null"/> if the header was rejected without an exception</param>
[ExcludeFromCodeCoverage(Justification = "Defensive fallback: ReceivedPacketData's header parsing is bounds-checked before this exception can occur, so no packet observed in practice reaches this path")]
private void RecordHeaderParseExceptionIfPresent(Exception? exception)
{
if (exception is null)
{
return;
}

Logger?.LogError(exception, "Error parsing packet header!");
}

/// <summary>
/// Internal method for processing received packets
/// </summary>
Expand Down
Loading
Loading