diff --git a/dNet/CommonPackets.cpp b/dNet/CommonPackets.cpp index 10c3939f5..060e93413 100644 --- a/dNet/CommonPackets.cpp +++ b/dNet/CommonPackets.cpp @@ -52,15 +52,32 @@ namespace CommonPackets { bitStream.Write(netVersion); bitStream.Write(unknown); bitStream.Write(serviceType); - bitStream.Write(trailing); + bitStream.Write(versionMajor); + bitStream.Write(versionMinor); + bitStream.Write(versionPatch); + bitStream.Write(buildFlags); + for (int shift = 24; shift >= 0; shift -= 8) bitStream.Write(commitPrefix >> shift); + BitStreamUtils::WriteLengthPrefixed(bitStream, buildString); } bool ServerVersionConfirm::Deserialize(RakNet::BitStream& bitStream) { VALIDATE_READ(bitStream.Read(netVersion)); VALIDATE_READ(bitStream.Read(unknown)); VALIDATE_READ(bitStream.Read(serviceType)); - VALIDATE_READ(bitStream.Read(trailing)); - return true; + VALIDATE_READ(bitStream.Read(versionMajor)); + VALIDATE_READ(bitStream.Read(versionMinor)); + VALIDATE_READ(bitStream.Read(versionPatch)); + VALIDATE_READ(bitStream.Read(buildFlags)); + commitPrefix = 0; + for (int i = 0; i < 4; i++) { + uint8_t byte{}; + VALIDATE_READ(bitStream.Read(byte)); + commitPrefix = (commitPrefix << 8) | byte; + } + // Older DLU servers stop after the 12 fixed bytes. + buildString.clear(); + if (bitStream.GetNumberOfUnreadBits() == 0) return true; + return BitStreamUtils::ReadLengthPrefixed(bitStream, buildString, MAX_BUILD_STRING_LENGTH); } void DisconnectNotify::Serialize(RakNet::BitStream& bitStream) const { diff --git a/dNet/CommonPackets.h b/dNet/CommonPackets.h index b843faa93..9e056a67c 100644 --- a/dNet/CommonPackets.h +++ b/dNet/CommonPackets.h @@ -2,10 +2,12 @@ #define COMMONPACKETS_H #include "BitStreamUtils.h" +#include "BuildInfo.h" #include "MessageType/Server.h" #include "RakNetTypes.h" #include +#include enum class eServerDisconnectIdentifiers : uint32_t; @@ -32,17 +34,27 @@ namespace CommonPackets { }; // Server -> client. The answer to ClientVersionConfirm. - // The client (ServerHandShakePacket, 1.10.64) only reads netVersion, unknown and serviceType; the trailing - // 8 bytes are what DLU has always sent after them. + // The client (1.10.64, PacketHandler_MSG_SERVER_VERSION_CONFIRM) reads only netVersion and serviceType; it never + // reads unknown and ignores anything after serviceType (no length check). DLU uses those bytes to say which build + // the server is: + // u32 netVersion | u32 unknown ("DLU3") | u32 serviceType + // | u8 major | u8 minor | u8 patch | u8 flags (bits 0-1 BuildInfo::eBuildKind, bit 2 dirty) + // | 4 bytes: first 32 bits of the commit hash, in hash order (a hex dump shows the hash) + // | u16 length + build string, e.g. "3.0.0-experimental+g1a2b3c4d-dirty" (optional on read) struct ServerVersionConfirm : public LUBitStream { static constexpr uint32_t DEFAULT_NET_VERSION = 171022; - static constexpr uint32_t UNKNOWN_VALUE = 861228100; - static constexpr uint64_t TRAILING_VALUE = 219818307120; + static constexpr uint32_t UNKNOWN_VALUE = 861228100; // "DLU3" + static constexpr uint32_t MAX_BUILD_STRING_LENGTH = 1024; uint32_t netVersion = DEFAULT_NET_VERSION; uint32_t unknown = UNKNOWN_VALUE; uint32_t serviceType{}; // The server's ServiceType, written as 4 bytes - uint64_t trailing = TRAILING_VALUE; + uint8_t versionMajor = BuildInfo::versionMajor; + uint8_t versionMinor = BuildInfo::versionMinor; + uint8_t versionPatch = BuildInfo::versionPatch; + uint8_t buildFlags = BuildInfo::Flags(); + uint32_t commitPrefix = BuildInfo::CommitPrefix(); + std::string buildString{ BuildInfo::buildString }; ServerVersionConfirm() : LUBitStream(ServiceType::COMMON, MessageType::Server::VERSION_CONFIRM) {} void Serialize(RakNet::BitStream& bitStream) const override; diff --git a/tests/dGameTests/dNetTests/CommonAuthPacketsTests.cpp b/tests/dGameTests/dNetTests/CommonAuthPacketsTests.cpp index 3f34fe279..e52d9528b 100644 --- a/tests/dGameTests/dNetTests/CommonAuthPacketsTests.cpp +++ b/tests/dGameTests/dNetTests/CommonAuthPacketsTests.cpp @@ -1,4 +1,5 @@ #include "AuthPackets.h" +#include "BuildInfo.h" #include "ClientPackets.h" #include "CommonPackets.h" #include "GameDependencies.h" @@ -10,6 +11,7 @@ #include "eServerDisconnectIdentifiers.h" #include "magic_enum.hpp" +#include #include #include #include @@ -71,6 +73,32 @@ namespace { FAIL() << "the clock kept changing seconds while comparing"; } + // VERSION_CONFIRM replies: the header, netVersion, unknown and serviceType (the 20 bytes the client reads up to) + // must match the legacy reply; the build identifier after them replaced the legacy's 8 fixed bytes. + constexpr size_t VERSION_CONFIRM_LEGACY_PREFIX = 20; + + void ExpectSameVersionConfirmPrefix(const std::function& legacy, const std::function& converted) { + const auto expected = Capture(legacy); + const auto actual = Capture(converted); + ASSERT_FALSE(expected.empty()); + ASSERT_EQ(expected.size(), actual.size()); + RakNet::BitStream thisBuild; + CommonPackets::ServerVersionConfirm{}.WritePacket(thisBuild); + const auto build = FromBitStream(thisBuild); + const std::vector expectedTail(build.bytes.begin() + VERSION_CONFIRM_LEGACY_PREFIX, build.bytes.end()); + for (size_t i = 0; i < expected.size(); i++) { + ASSERT_GE(expected[i].bytes.size(), VERSION_CONFIRM_LEGACY_PREFIX); + ASSERT_GE(actual[i].bytes.size(), VERSION_CONFIRM_LEGACY_PREFIX); + const std::vector expectedPrefix(expected[i].bytes.begin(), expected[i].bytes.begin() + VERSION_CONFIRM_LEGACY_PREFIX); + const std::vector actualPrefix(actual[i].bytes.begin(), actual[i].bytes.begin() + VERSION_CONFIRM_LEGACY_PREFIX); + EXPECT_EQ(expectedPrefix, actualPrefix); + const std::vector actualTail(actual[i].bytes.begin() + VERSION_CONFIRM_LEGACY_PREFIX, actual[i].bytes.end()); + EXPECT_EQ(expectedTail, actualTail); + EXPECT_EQ(expected[i].sysAddr, actual[i].sysAddr); + EXPECT_EQ(expected[i].broadcast, actual[i].broadcast); + } + } + template T RoundTrip(const T& packet) { RakNet::BitStream first; @@ -122,7 +150,7 @@ TEST_F(CommonAuthPacketsTests, VersionConfirmHandshakeMatchesLegacy) { request.WritePacket(bytes); const auto sysAddr = TestAddress(); - ExpectSameOutput( + ExpectSameVersionConfirmPrefix( [&] { auto packet = MakePacket(bytes, sysAddr); LegacyAuthPackets::HandleHandshake(Game::server, &packet); }, [&] { Dispatch(bytes, sysAddr, CommonPackets::Handle); }); @@ -136,10 +164,10 @@ TEST_F(CommonAuthPacketsTests, VersionConfirmHandshakeMatchesLegacy) { } } -TEST_F(CommonAuthPacketsTests, ServerVersionConfirmMatchesLegacy) { +TEST_F(CommonAuthPacketsTests, ServerVersionConfirmKeepsLegacyPrefix) { for (const auto serviceType : { ServiceType::AUTH, ServiceType::WORLD, ServiceType::CHAT }) { const auto sysAddr = TestAddress(); - ExpectSameOutput( + ExpectSameVersionConfirmPrefix( [&] { LegacyAuthPackets::SendHandshake(Game::server, sysAddr, "ignored", 1, serviceType); }, [&] { CommonPackets::ServerVersionConfirm response; @@ -152,11 +180,21 @@ TEST_F(CommonAuthPacketsTests, ServerVersionConfirmMatchesLegacy) { TEST_F(CommonAuthPacketsTests, VersionConfirmGoldenBytes) { CommonPackets::ServerVersionConfirm response; response.serviceType = static_cast(ServiceType::WORLD); + response.versionMajor = 3; + response.versionMinor = 0; + response.versionPatch = 1; + response.buildFlags = static_cast(BuildInfo::eBuildKind::CI) | BuildInfo::DIRTY_FLAG; + response.commitPrefix = 0x1a2b3c4d; + response.buildString = "3.0.1-main+g1a2b3c4d"; RakNet::BitStream bytes; response.WritePacket(bytes); - // 0x53 | COMMON u16 | VERSION_CONFIRM u32 | pad | 171022 | 861228100 | WORLD u32 | 219818307120 u64 - EXPECT_PACKET_EQ(FromHex("53 00 00 00 00 00 00 00 0e 9c 02 00 44 4c 55 33 04 00 00 00 30 2e 31 2e 33 00 00 00"), FromBitStream(bytes)); - RoundTrip(response); + // 0x53 | COMMON u16 | VERSION_CONFIRM u32 | pad | 171022 | 861228100 ("DLU3") | WORLD u32 + // | 3 0 1 | flags (CI | dirty) | commit 1a2b3c4d | u16 20 | "3.0.1-main+g1a2b3c4d" + EXPECT_PACKET_EQ(FromHex("53 00 00 00 00 00 00 00 0e 9c 02 00 44 4c 55 33 04 00 00 00 03 00 01 06 1a 2b 3c 4d 14 00" + " 33 2e 30 2e 31 2d 6d 61 69 6e 2b 67 31 61 32 62 33 63 34 64"), FromBitStream(bytes)); + const auto copy = RoundTrip(response); + EXPECT_EQ(copy.commitPrefix, 0x1a2b3c4du); + EXPECT_EQ(copy.buildString, "3.0.1-main+g1a2b3c4d"); CommonPackets::ClientVersionConfirm request; request.netVersion = 171022; @@ -172,6 +210,86 @@ TEST_F(CommonAuthPacketsTests, VersionConfirmGoldenBytes) { ExpectTruncatedFails(request); } +TEST_F(CommonAuthPacketsTests, ServerVersionConfirmRoundTrip) { + CommonPackets::ServerVersionConfirm response; + response.netVersion = 0xFFFFFFFF; + response.serviceType = static_cast(ServiceType::CHAT); + response.versionMajor = 0xFF; + response.versionMinor = 0x7F; + response.versionPatch = 0; + response.buildFlags = static_cast(BuildInfo::eBuildKind::RELEASE); + response.commitPrefix = 0xDEADBEEF; + for (const auto& buildString : g_Strings) { + response.buildString = buildString; + const auto copy = RoundTrip(response); + EXPECT_EQ(copy.netVersion, response.netVersion); + EXPECT_EQ(copy.unknown, CommonPackets::ServerVersionConfirm::UNKNOWN_VALUE); + EXPECT_EQ(copy.serviceType, response.serviceType); + EXPECT_EQ(copy.versionMajor, response.versionMajor); + EXPECT_EQ(copy.versionMinor, response.versionMinor); + EXPECT_EQ(copy.versionPatch, response.versionPatch); + EXPECT_EQ(copy.buildFlags, response.buildFlags); + EXPECT_EQ(copy.commitPrefix, response.commitPrefix); + EXPECT_EQ(copy.buildString, buildString); + } + + // The defaults describe this build. + const CommonPackets::ServerVersionConfirm current; + EXPECT_EQ(current.unknown, 861228100u); + EXPECT_EQ(current.versionMajor, BuildInfo::versionMajor); + EXPECT_EQ(current.versionMinor, BuildInfo::versionMinor); + EXPECT_EQ(current.versionPatch, BuildInfo::versionPatch); + EXPECT_EQ(current.buildFlags, BuildInfo::Flags()); + EXPECT_EQ(current.commitPrefix, BuildInfo::CommitPrefix()); + EXPECT_EQ(current.buildString, BuildInfo::buildString); + EXPECT_EQ(RoundTrip(current).buildString, BuildInfo::buildString); +} + +TEST_F(CommonAuthPacketsTests, ServerVersionConfirmReadsWithoutBuildString) { + // A reply that stops after the commit bytes still reads; the build string is optional on read. + const auto fixed = FromHex("53 00 00 00 00 00 00 00 0e 9c 02 00 44 4c 55 33 04 00 00 00 03 00 01 06 1a 2b 3c 4d"); + RakNet::BitStream bytes(const_cast(fixed.bytes.data()), fixed.bytes.size(), true); + CommonPackets::ServerVersionConfirm copy; + copy.buildString = "stale"; + ASSERT_TRUE(copy.ReadHeader(bytes)); + ASSERT_TRUE(copy.Deserialize(bytes)); + EXPECT_EQ(copy.netVersion, 171022u); + EXPECT_EQ(copy.serviceType, static_cast(ServiceType::WORLD)); + EXPECT_EQ(copy.versionMajor, 3); + EXPECT_EQ(copy.versionPatch, 1); + EXPECT_EQ(copy.buildFlags, 6); + EXPECT_EQ(copy.commitPrefix, 0x1a2b3c4du); + EXPECT_TRUE(copy.buildString.empty()); + + // Cut inside the fixed bytes or inside the string: fails. + CommonPackets::ServerVersionConfirm full; + full.buildString = "3.0.0"; + RakNet::BitStream fullBytes; + full.WritePacket(fullBytes); + for (const uint32_t cut : { 8u, 19u, 27u, 29u, 32u }) { + RakNet::BitStream truncated(fullBytes.GetData(), cut, true); + CommonPackets::ServerVersionConfirm partial; + ASSERT_TRUE(partial.ReadHeader(truncated)); + EXPECT_FALSE(partial.Deserialize(truncated)) << "cut at " << cut; + } +} + +TEST(BuildInfoTests, FlagsAndCommitPrefix) { + EXPECT_EQ(BuildInfo::Flags() & BuildInfo::BUILD_KIND_MASK, static_cast(BuildInfo::buildKind)); + EXPECT_EQ((BuildInfo::Flags() & BuildInfo::DIRTY_FLAG) != 0, BuildInfo::dirty); + EXPECT_EQ(BuildInfo::Flags() & ~(BuildInfo::BUILD_KIND_MASK | BuildInfo::DIRTY_FLAG), 0); + if (BuildInfo::commit.size() >= 8) { + char hex[9]; + std::snprintf(hex, sizeof(hex), "%08x", BuildInfo::CommitPrefix()); + EXPECT_EQ(std::string(hex), std::string(BuildInfo::commit.substr(0, 8))); + EXPECT_NE(BuildInfo::buildString.find("+g" + std::string(hex)), std::string_view::npos); + } else { + EXPECT_EQ(BuildInfo::CommitPrefix(), 0u); + } + const auto version = std::to_string(BuildInfo::versionMajor) + "." + std::to_string(BuildInfo::versionMinor) + "." + std::to_string(BuildInfo::versionPatch); + EXPECT_TRUE(BuildInfo::buildString.starts_with(version)); +} + TEST_F(CommonAuthPacketsTests, DisconnectNotifyMatchesLegacy) { for (const auto id : magic_enum::enum_values()) { RakNet::BitStream expected;