From 0faf7580c7f538ca72a4002d387cbfe9bc7b2507 Mon Sep 17 00:00:00 2001 From: Segfault <5221072+Segfaultd@users.noreply.github.com> Date: Sat, 17 Oct 2026 17:33:08 +0200 Subject: [PATCH 1/6] fix: zero MSVC /W4 warnings; build Windows CI with /WX The Windows job already compiled MafiaNet's targets with /W4 but non-fatal. The 25 warnings in our own code are fixed: - five 64-bit Time -> 32-bit TimeMS assignments in peer.cpp made explicit (the value is compared with GetTimeMS(), so 32-bit is intended); - a local in ReliabilityLayer shadowing the splitPacketId member, renamed; - test-side message-id-to-char truncations made explicit, and two shadowed names renamed. C4251 (std::atomic members of exported classes have no DLL interface) is disabled for our targets: expected for a C++ DLL used with the same toolset. Warnings from fetched dependencies and samples are not ours and stay at their own levels. --- .github/workflows/build.yml | 2 +- CLAUDE.md | 5 +++-- Source/src/peer.cpp | 10 +++++----- Source/src/reliability_layer.cpp | 8 ++++---- Tests/Integration/ComprehensiveConvertTests.cpp | 2 +- .../DirectoryDeltaTransferStreamingLiveTests.cpp | 4 ++-- Tests/Integration/LocalIsConnectedTests.cpp | 2 +- Tests/Integration/PacketAndLowLevelTests.cpp | 2 +- Tests/Support/SimulatedNetwork.h | 4 ++-- Tests/Support/TestHelpers.cpp | 6 +++--- Tests/Unit/RakVoiceRelayTests.cpp | 2 +- cmake/MafiaNetHelpers.cmake | 4 +++- 12 files changed, 27 insertions(+), 24 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6334b04de..fed4c2f18 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -444,7 +444,7 @@ jobs: persist-credentials: false - name: Configure CMake - run: cmake -B build -DMAFIANET_BUILD_SAMPLES=ON -DMAFIANET_BUILD_TESTS=ON + run: cmake -B build -DMAFIANET_BUILD_SAMPLES=ON -DMAFIANET_BUILD_TESTS=ON -DMAFIANET_WARNINGS_AS_ERRORS=ON - name: Build run: cmake --build build --config Release --parallel 4 diff --git a/CLAUDE.md b/CLAUDE.md index 2e6939bf8..63651c890 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -67,7 +67,7 @@ Available generators (run `cmake --help` for full list): | `MAFIANET_MINIMAL` | OFF | Compile out every optional plugin (core transport only); built by the `linux-minimal` CI job | | `MAFIANET_SANITIZER` | (empty) | `address+undefined` or `thread`; applied to every target. Prefer the `asan` / `tsan` presets | | `MAFIANET_USE_COMPILER_CACHE` | ON | Use sccache/ccache as compiler launcher when found on `PATH` | -| `MAFIANET_WARNINGS_AS_ERRORS` | OFF | `-Werror` / `/WX` on MafiaNet's own targets (on in the Linux/macOS CI jobs) | +| `MAFIANET_WARNINGS_AS_ERRORS` | OFF | `-Werror` / `/WX` on MafiaNet's own targets (on in every CI job except the oldest-toolchain Docker one) | | `MAFIANET_BUILD_FUZZERS` | OFF | libFuzzer harnesses in `Tests/Fuzz` (Clang); use the `fuzz` preset | | `MAFIANET_BUILD_BENCHMARKS` | OFF | google/benchmark microbenchmarks in `Benchmarks/`; use the `bench` preset | | `MAFIANET_COVERAGE` | OFF | `--coverage` instrumentation; use the `coverage` preset, report with gcovr | @@ -79,7 +79,8 @@ sanitizer tests through `ctest --preset asan|tsan` so the sanitizer options and `ci/tsan.supp` are applied. Known by-design TSan races live in `ci/tsan.supp`; add an entry only for a race that is part of RakNet's lock-free getter model, never to silence a new bug. -**Warnings:** MafiaNet's own code builds with zero `-Wall -Wextra` warnings on Clang and GCC, and +**Warnings:** MafiaNet's own code builds with zero `-Wall -Wextra` warnings on Clang and GCC and zero `/W4` +warnings on MSVC (C4251 is disabled), and CI enforces it (`MAFIANET_WARNINGS_AS_ERRORS`). Fix a new warning properly; suppress only with a narrow, commented `[[maybe_unused]]` or pragma, never by loosening the flags. Warnings are applied per target by `mafianet_enable_warnings()` (`cmake/MafiaNetHelpers.cmake`), never globally, so diff --git a/Source/src/peer.cpp b/Source/src/peer.cpp index 47e49a2be..81371bcf5 100644 --- a/Source/src/peer.cpp +++ b/Source/src/peer.cpp @@ -6555,7 +6555,7 @@ bool RakPeer::RunUpdateCycle(BitStream &updateBitStream ) // arrives in ID_SESSION_CONFIG_REQUEST and we answer it. remoteSystem->connectMode=RemoteSystemStruct::EXCHANGING_SESSION_DATA; remoteSystem->sessionConfigIsConnectingSide=false; - remoteSystem->sessionConfigStartTime=timeMS!=0 ? timeMS : MafiaNet::GetTimeMS(); + remoteSystem->sessionConfigStartTime=timeMS!=0 ? (MafiaNet::TimeMS) timeMS : MafiaNet::GetTimeMS(); // compared with GetTimeMS(), 32-bit PingInternal( systemAddress, true, MafiaNet::Reliability::Unreliable ); // Update again immediately after this tick so the ping goes out right away @@ -6593,7 +6593,7 @@ bool RakPeer::RunUpdateCycle(BitStream &updateBitStream ) // Hold this until the session handshake completes; the bytes are copied because `data` // is freed as soon as this branch returns. ClearSessionConfig(remoteSystem); - remoteSystem->sessionConfigStartTime=timeMS!=0 ? timeMS : MafiaNet::GetTimeMS(); + remoteSystem->sessionConfigStartTime=timeMS!=0 ? (MafiaNet::TimeMS) timeMS : MafiaNet::GetTimeMS(); // compared with GetTimeMS(), 32-bit remoteSystem->withheldConnectionPacketData = (unsigned char*) rakMalloc_Ex(byteSize, _FILE_AND_LINE_); if (remoteSystem->withheldConnectionPacketData!=0) { @@ -6741,7 +6741,7 @@ bool RakPeer::RunUpdateCycle(BitStream &updateBitStream ) // connecting side like the other replies, so a client cannot push one at a server. if (remoteSystem->connectMode==RemoteSystemStruct::EXCHANGING_SESSION_DATA && remoteSystem->sessionConfigIsConnectingSide) { - remoteSystem->sessionConfigStartTime=timeMS!=0 ? timeMS : MafiaNet::GetTimeMS(); + remoteSystem->sessionConfigStartTime=timeMS!=0 ? (MafiaNet::TimeMS) timeMS : MafiaNet::GetTimeMS(); // compared with GetTimeMS(), 32-bit packet=AllocPacket(byteSize, data, _FILE_AND_LINE_); packet->bitSize = bitSize; packet->systemAddress = systemAddress; @@ -6848,7 +6848,7 @@ bool RakPeer::RunUpdateCycle(BitStream &updateBitStream ) // Transport is up, but the application is not told so until the session payloads have been // exchanged. ID_CONNECTION_REQUEST_ACCEPTED is stashed and replayed from ID_SESSION_CONFIG. remoteSystem->connectMode=RemoteSystemStruct::EXCHANGING_SESSION_DATA; - remoteSystem->sessionConfigStartTime=timeMS!=0 ? timeMS : MafiaNet::GetTimeMS(); + remoteSystem->sessionConfigStartTime=timeMS!=0 ? (MafiaNet::TimeMS) timeMS : MafiaNet::GetTimeMS(); // compared with GetTimeMS(), 32-bit // Bug: If A connects to B through R, A's firstExternalID is set to R. If A tries to send to R, sends to loopback because R==firstExternalID // Correct fix is to specify in Connect() if target is through a proxy. @@ -6863,7 +6863,7 @@ bool RakPeer::RunUpdateCycle(BitStream &updateBitStream ) // copied because `data` is freed as soon as this branch returns. ClearSessionConfig(remoteSystem); remoteSystem->sessionConfigIsConnectingSide=true; - remoteSystem->sessionConfigStartTime=timeMS!=0 ? timeMS : MafiaNet::GetTimeMS(); + remoteSystem->sessionConfigStartTime=timeMS!=0 ? (MafiaNet::TimeMS) timeMS : MafiaNet::GetTimeMS(); // compared with GetTimeMS(), 32-bit remoteSystem->withheldConnectionPacketData = (unsigned char*) rakMalloc_Ex(byteSize, _FILE_AND_LINE_); if (remoteSystem->withheldConnectionPacketData!=0) { diff --git a/Source/src/reliability_layer.cpp b/Source/src/reliability_layer.cpp index cfe895cd7..6f05513ef 100644 --- a/Source/src/reliability_layer.cpp +++ b/Source/src/reliability_layer.cpp @@ -1172,10 +1172,10 @@ bool ReliabilityLayer::HandleSocketReceiveFromConnectedPlayer( internalPacket->orderingChannel = 255; // Use 255 to designate not sequenced and not ordered // Insert may free the fragment (duplicate or malformed): read its id first. - const SplitPacketIdType splitPacketId = internalPacket->splitPacketId; + const SplitPacketIdType fragmentMessageId = internalPacket->splitPacketId; InsertIntoSplitPacketList( internalPacket, timeRead ); - internalPacket = BuildPacketFromSplitPacketList( splitPacketId, timeRead, + internalPacket = BuildPacketFromSplitPacketList( fragmentMessageId, timeRead, s, systemAddress, rnr, extraSocketOptions); if ( internalPacket == 0 ) @@ -1269,10 +1269,10 @@ bool ReliabilityLayer::HandleSocketReceiveFromConnectedPlayer( internalPacket->orderingChannel = 255; // Use 255 to designate not sequenced and not ordered // Insert may free the fragment (duplicate or malformed): read its id first. - const SplitPacketIdType splitPacketId = internalPacket->splitPacketId; + const SplitPacketIdType fragmentMessageId = internalPacket->splitPacketId; InsertIntoSplitPacketList( internalPacket, timeRead ); - internalPacket = BuildPacketFromSplitPacketList( splitPacketId, timeRead, + internalPacket = BuildPacketFromSplitPacketList( fragmentMessageId, timeRead, s, systemAddress, rnr, updateBitStream); if ( internalPacket == 0 ) diff --git a/Tests/Integration/ComprehensiveConvertTests.cpp b/Tests/Integration/ComprehensiveConvertTests.cpp index 0b45bbfad..445cefc6d 100644 --- a/Tests/Integration/ComprehensiveConvertTests.cpp +++ b/Tests/Integration/ComprehensiveConvertTests.cpp @@ -191,7 +191,7 @@ TEST_F(ComprehensiveConvert, DoesALittleBitOfEverything) bool broadcast; // data[0]=ID_RESERVED1+(randomMT()%10); - data[0]=ID_USER_PACKET_ENUM; + data[0]=(char)(unsigned char)ID_USER_PACKET_ENUM; dataLength=3+(randomMT()%8000); // dataLength=600+(randomMT()%7000); priority=(MafiaNet::Priority)(randomMT()%(int)MafiaNet::NUMBER_OF_PRIORITIES); diff --git a/Tests/Integration/DirectoryDeltaTransferStreamingLiveTests.cpp b/Tests/Integration/DirectoryDeltaTransferStreamingLiveTests.cpp index adb854e3c..17db631e7 100644 --- a/Tests/Integration/DirectoryDeltaTransferStreamingLiveTests.cpp +++ b/Tests/Integration/DirectoryDeltaTransferStreamingLiveTests.cpp @@ -203,8 +203,8 @@ TEST(DirectoryDeltaTransferStreaming, LargeFilesAreWrittenChunkByChunk) out.write(changedBytes.data(), (std::streamsize)changedBytes.size()); } serverDelta.ClearUploads(); - for (const auto &entry : expected) - serverDelta.AddFile((source / entry.first).string().c_str(), entry.first.c_str()); + for (const auto &file : expected) + serverDelta.AddFile((source / file.first).string().c_str(), file.first.c_str()); Observer changed; download(changed); EXPECT_TRUE(changed.complete); diff --git a/Tests/Integration/LocalIsConnectedTests.cpp b/Tests/Integration/LocalIsConnectedTests.cpp index 8af7224c6..fb64f3cf9 100644 --- a/Tests/Integration/LocalIsConnectedTests.cpp +++ b/Tests/Integration/LocalIsConnectedTests.cpp @@ -141,7 +141,7 @@ TEST_F(LocalIsConnected, ConnectionStateAndLocalAddressQueries) // Testing SendLoopback char str[]="AAAAAAAAAA"; - str[0]=(char)(ID_USER_PACKET_ENUM+1); + str[0]=(char)(unsigned char)(ID_USER_PACKET_ENUM+1); client->SendLoopback(str, (int) strlen(str)+1); client->SendLoopback(str, (int) strlen(str)+1); client->SendLoopback(str, (int) strlen(str)+1); diff --git a/Tests/Integration/PacketAndLowLevelTests.cpp b/Tests/Integration/PacketAndLowLevelTests.cpp index 20c1f5607..28730f3e9 100644 --- a/Tests/Integration/PacketAndLowLevelTests.cpp +++ b/Tests/Integration/PacketAndLowLevelTests.cpp @@ -104,7 +104,7 @@ TEST_F(PacketAndLowLevelTests, LowLevelPacketFunctions) for (int i = 0; i < 5; i++) { - dataList[i][0] = ID_USER_PACKET_ENUM + 1 + i; + dataList[i][0] = (char)(unsigned char)(ID_USER_PACKET_ENUM + 1 + i); lengths[i] = 5; } diff --git a/Tests/Support/SimulatedNetwork.h b/Tests/Support/SimulatedNetwork.h index d7a0868ce..39d756362 100644 --- a/Tests/Support/SimulatedNetwork.h +++ b/Tests/Support/SimulatedNetwork.h @@ -93,9 +93,9 @@ namespace MafiaNet // rules: for fuzzing what an unconnected remote can send. void InjectDatagram(const SystemAddress &from, const RakPeer *to, const unsigned char *data, int length); // Called with every datagram any peer sends (before link rules). - void SetTap(std::function tap) + void SetTap(std::function callback) { - this->tap = std::move(tap); + tap = std::move(callback); } TimeUS Now() const; diff --git a/Tests/Support/TestHelpers.cpp b/Tests/Support/TestHelpers.cpp index 9914e46a6..8401abc79 100644 --- a/Tests/Support/TestHelpers.cpp +++ b/Tests/Support/TestHelpers.cpp @@ -84,7 +84,7 @@ bool TestHelpers::BroadCastTestPacket(RakPeerInterface *sender,MafiaNet::Reliabi { char str2[]="AAAAAAAAAA"; - str2[0]=typeNum; + str2[0]=(char)(unsigned char)typeNum; return sender->Send(str2,(int) strlen(str2)+1, pr, rel ,0, UNASSIGNED_SYSTEM_ADDRESS, true)>0; } @@ -94,10 +94,10 @@ bool TestHelpers::SendTestPacketDirected(RakPeerInterface *sender,char * ip,int SystemAddress recAddress; recAddress.SetBinaryAddress(ip); - recAddress.SetPortHostOrder(port); + recAddress.SetPortHostOrder((unsigned short)port); char str2[]="AAAAAAAAAA"; - str2[0]=typeNum; + str2[0]=(char)(unsigned char)typeNum; return sender->Send(str2,(int) strlen(str2)+1, pr, rel ,0, recAddress, false)>0; } diff --git a/Tests/Unit/RakVoiceRelayTests.cpp b/Tests/Unit/RakVoiceRelayTests.cpp index 59e11dcd2..789730bfe 100644 --- a/Tests/Unit/RakVoiceRelayTests.cpp +++ b/Tests/Unit/RakVoiceRelayTests.cpp @@ -129,7 +129,7 @@ TEST(RakVoiceRelayOrigin, RejectsEmptyPacket) { // The version byte is the forward-compatibility escape hatch: a future format // must be rejected by today's build rather than misparsed as version 1. TEST(RakVoiceRelayOrigin, RejectsUnknownFormatVersion) { - for (unsigned char v : {0, 2, 7, 255}) { + for (unsigned char v : {(unsigned char)0, (unsigned char)2, (unsigned char)7, (unsigned char)255}) { auto bytes = MakeRelayFrame(0xAABBCCDDEEFF0011ull, 8, v); Packet p = MakePacket(bytes); EXPECT_EQ(RakVoice::ReadRelayOrigin(&p), UNASSIGNED_RAKNET_GUID) << "version=" << int(v); diff --git a/cmake/MafiaNetHelpers.cmake b/cmake/MafiaNetHelpers.cmake index 13b4dd3df..8d630c81f 100644 --- a/cmake/MafiaNetHelpers.cmake +++ b/cmake/MafiaNetHelpers.cmake @@ -112,7 +112,9 @@ endfunction() function(mafianet_enable_warnings) foreach(target IN LISTS ARGN) if(MSVC) - target_compile_options(${target} PRIVATE /W4 + # C4251: an exported class has std:: members (std::atomic) without a + # DLL interface. Expected for a C++ DLL used from the same toolset. + target_compile_options(${target} PRIVATE /W4 /wd4251 $<$:/WX>) else() target_compile_options(${target} PRIVATE -Wall -Wextra From ba74b7f664bde27b892632b4fc41b80e9e78c5fb Mon Sep 17 00:00:00 2001 From: Segfault <5221072+Segfaultd@users.noreply.github.com> Date: Sat, 17 Oct 2026 17:43:52 +0200 Subject: [PATCH 2/6] test: move 12 loopback scenarios onto the simulated network Connection lifecycle, reliability, MTU negotiation, ping, offline messages, disconnect reasons and the session-config suite tested protocol behaviour only, so they now run as deterministic unit tests on SimulatedNetwork instead of real UDP with wall-clock waits. Every assertion of the old integration files is covered, and several are stronger: - ping statistics are exact (fixed link delay) instead of a latency threshold that flaked on loaded machines; - session-config gating is checked after every 1 ms update instead of sampled racily from another thread; - negative checks ("never connected", "never delivered") cover the whole scenario, not a window after a sleep; - MTU negotiation is also tested under a path ceiling, ReliableOrdered under loss and jitter, and connection limits on every tick. The 12 integration files are deleted; 34 integration tests remain for what needs real sockets or threads (socket layer and batching, binding, local addresses, threaded dispatch, plugins over loopback, stress). The unit suite (322 tests) runs in under a second. SimulatedNetwork gains StartConnect (start an attempt without stepping). The manual-update Shutdown flush sends nothing when the clock has not moved since the last update; documented on SetManualUpdate (test mode only). The TSan suppression for the deleted loopback SlotInspector is gone. --- Source/include/mafianet/peer.h | 5 +- .../CrossConnectionConvertTests.cpp | 199 --- Tests/Integration/DisconnectReasonTests.cpp | 311 ---- .../DroppedConnectionConvertTests.cpp | 298 ---- Tests/Integration/EightPeerTests.cpp | 263 --- Tests/Integration/MTUNegotiationTests.cpp | 467 ------ Tests/Integration/MaximumConnectTests.cpp | 138 -- .../OfflineMessagesConvertTests.cpp | 139 -- .../PeerConnectDisconnectTests.cpp | 260 --- ...onnectDisconnectWithCancelPendingTests.cpp | 279 ---- Tests/Integration/PingTestsTests.cpp | 325 ---- .../ReliableOrderedConvertedTests.cpp | 206 --- Tests/Integration/SessionConfigLiveTests.cpp | 1420 ----------------- Tests/Support/SimulatedNetwork.cpp | 11 +- Tests/Support/SimulatedNetwork.h | 3 + Tests/Unit/CrossConnectionConvertTests.cpp | 106 ++ Tests/Unit/DisconnectReasonTests.cpp | 143 ++ Tests/Unit/DroppedConnectionConvertTests.cpp | 170 ++ Tests/Unit/EightPeerTests.cpp | 147 ++ Tests/Unit/MTUNegotiationTests.cpp | 220 +++ Tests/Unit/MaximumConnectTests.cpp | 100 ++ Tests/Unit/OfflineMessagesConvertTests.cpp | 131 ++ Tests/Unit/PeerConnectDisconnectTests.cpp | 153 ++ ...onnectDisconnectWithCancelPendingTests.cpp | 155 ++ Tests/Unit/PingTestsTests.cpp | 187 +++ Tests/Unit/ReliableOrderedConvertedTests.cpp | 138 ++ Tests/Unit/SessionConfigSimulatedTests.cpp | 1035 ++++++++++++ ci/tsan.supp | 5 - 28 files changed, 2700 insertions(+), 4314 deletions(-) delete mode 100644 Tests/Integration/CrossConnectionConvertTests.cpp delete mode 100644 Tests/Integration/DisconnectReasonTests.cpp delete mode 100644 Tests/Integration/DroppedConnectionConvertTests.cpp delete mode 100644 Tests/Integration/EightPeerTests.cpp delete mode 100644 Tests/Integration/MTUNegotiationTests.cpp delete mode 100644 Tests/Integration/MaximumConnectTests.cpp delete mode 100644 Tests/Integration/OfflineMessagesConvertTests.cpp delete mode 100644 Tests/Integration/PeerConnectDisconnectTests.cpp delete mode 100644 Tests/Integration/PeerConnectDisconnectWithCancelPendingTests.cpp delete mode 100644 Tests/Integration/PingTestsTests.cpp delete mode 100644 Tests/Integration/ReliableOrderedConvertedTests.cpp delete mode 100644 Tests/Integration/SessionConfigLiveTests.cpp create mode 100644 Tests/Unit/CrossConnectionConvertTests.cpp create mode 100644 Tests/Unit/DisconnectReasonTests.cpp create mode 100644 Tests/Unit/DroppedConnectionConvertTests.cpp create mode 100644 Tests/Unit/EightPeerTests.cpp create mode 100644 Tests/Unit/MTUNegotiationTests.cpp create mode 100644 Tests/Unit/MaximumConnectTests.cpp create mode 100644 Tests/Unit/OfflineMessagesConvertTests.cpp create mode 100644 Tests/Unit/PeerConnectDisconnectTests.cpp create mode 100644 Tests/Unit/PeerConnectDisconnectWithCancelPendingTests.cpp create mode 100644 Tests/Unit/PingTestsTests.cpp create mode 100644 Tests/Unit/ReliableOrderedConvertedTests.cpp create mode 100644 Tests/Unit/SessionConfigSimulatedTests.cpp diff --git a/Source/include/mafianet/peer.h b/Source/include/mafianet/peer.h index 16408dd58..8f8032c9d 100644 --- a/Source/include/mafianet/peer.h +++ b/Source/include/mafianet/peer.h @@ -639,7 +639,10 @@ class RAK_DLL_EXPORT RakPeer : public RakPeerInterface, public RNS2EventHandler void SetSocketFactory(RakNetSocket2Factory *factory); /// No internal update thread: the caller drives the peer with /// RunManualUpdate(). Shutdown(blockDuration) then flushes once instead of - /// waiting, since nothing else would advance the peer meanwhile. + /// waiting, since nothing else would advance the peer meanwhile. That flush + /// sends nothing if the clock has not moved since the last update (the + /// reliability layer skips a zero-length tick), so advance a test clock + /// before shutting down if the disconnection notification must go out. void SetManualUpdate(bool manual); /// One update-thread iteration: the user update callback, then the update /// cycle. Only valid with SetManualUpdate(true). diff --git a/Tests/Integration/CrossConnectionConvertTests.cpp b/Tests/Integration/CrossConnectionConvertTests.cpp deleted file mode 100644 index 1efc69b55..000000000 --- a/Tests/Integration/CrossConnectionConvertTests.cpp +++ /dev/null @@ -1,199 +0,0 @@ -/* - * Copyright (c) 2026, MafiaHub - * - * This source code is licensed under the MIT-style license found in the - * license.txt file in the root directory of this source tree. - */ - -#include - -#include "mafianet/peer_interface.h" -#include "mafianet/message_identifiers.h" -#include "mafianet/bit_stream.h" -#include "mafianet/sleep.h" -#include "mafianet/get_time.h" - -#include // For getenv -#include -#include - -using namespace MafiaNet; - -// Ported from Samples/Tests/CrossConnectionConvertTest.cpp - -class CrossConnectionConvert : public ::testing::Test -{ -protected: - void TearDown() override - { - // Shutdown all peers before destroying to let threads clean up - for (RakPeerInterface *peer : destroyList) - peer->Shutdown(100); - - for (RakPeerInterface *peer : destroyList) - RakPeerInterface::DestroyInstance(peer); - } - - RakPeerInterface *RegisterPeer(RakPeerInterface *peer) - { - destroyList.push_back(peer); - return peer; - } - - std::vector destroyList; -}; - -/* -Description: Tests what happens if two instances of RakNet connect to each other at the same time. This has caused handshaking problems in the past. - -Success conditions: -Everything connects and sends normally. - -Failure conditions: -Expected values from ping/pong do not occur within expected time. -*/ -TEST_F(CrossConnectionConvert, SimultaneousConnectHandshakes) -{ - // Reduce test duration in CI environments - const bool isCI = (getenv("CI") != nullptr); - const TimeMS testDurationMs = isCI ? 2000 : 10000; // 2 seconds in CI - - char serverIP[64]; - - strcpy(serverIP,"127.0.0.1"); - - char clientIP[64]; - RakPeerInterface *server,*client; - unsigned short clientPort=0; - bool gotNotification=false; // Legacy left this uninitialized - server = RegisterPeer(RakPeerInterface::GetInstance()); - client = RegisterPeer(RakPeerInterface::GetInstance()); - - SocketDescriptor serverSd(0, "127.0.0.1"); - ASSERT_EQ(server->Startup(1, &serverSd, 1), RAKNET_STARTED) << "Server failed to start"; - server->SetMaximumIncomingConnections(1); - const unsigned short serverPort=server->GetInternalID().GetPort(); - - SocketDescriptor clientSd(0, 0); - ASSERT_EQ(client->Startup(1, &clientSd, 1), RAKNET_STARTED) << "Client failed to start"; - - client->Ping(serverIP,serverPort,false); - - // PacketLogger pl; - // pl.LogHeader(); - // rakPeer->AttachPlugin(&pl); - - TimeMS connectionAttemptTime=0,connectionResultDeterminationTime=0,nextTestStartTime=0; - - TimeMS entryTime=GetTimeMS();//Loop entry time - - while(GetTimeMS()-entryTimeReceive(); p; server->DeallocatePacket(p), p=server->Receive()) - { - - if (p->data[0]==ID_NEW_INCOMING_CONNECTION) - { - gotNotification=true; - } - else if (p->data[0]==ID_CONNECTION_REQUEST_ACCEPTED) - { - gotNotification=true; - } - else if (p->data[0]==ID_UNCONNECTED_PING) - { - connectionAttemptTime=GetTimeMS()+1000; - p->systemAddress.ToString(false,clientIP); - clientPort=p->systemAddress.GetPort(); - gotNotification=false; - } - else if (p->data[0]==ID_UNCONNECTED_PONG) - { - TimeMS sendPingTime; - BitStream bs(p->data,p->length,false); - bs.IgnoreBytes(1); - bs.Read(sendPingTime); - TimeMS rtt = GetTimeMS() - sendPingTime; - if (rtt/2<=500) - connectionAttemptTime=GetTimeMS()+1000-rtt/2; - else - connectionAttemptTime=GetTimeMS(); - gotNotification=false; - } - } - - for (p=client->Receive(); p; client->DeallocatePacket(p), p=client->Receive()) - { - - if (p->data[0]==ID_NEW_INCOMING_CONNECTION) - { - gotNotification=true; - } - else if (p->data[0]==ID_CONNECTION_REQUEST_ACCEPTED) - { - gotNotification=true; - } - else if (p->data[0]==ID_UNCONNECTED_PING) - { - connectionAttemptTime=GetTimeMS()+1000; - p->systemAddress.ToString(false,clientIP); - clientPort=p->systemAddress.GetPort(); - gotNotification=false; - } - else if (p->data[0]==ID_UNCONNECTED_PONG) - { - TimeMS sendPingTime; - BitStream bs(p->data,p->length,false); - bs.IgnoreBytes(1); - bs.Read(sendPingTime); - TimeMS rtt = GetTimeMS() - sendPingTime; - if (rtt/2<=500) - connectionAttemptTime=GetTimeMS()+1000-rtt/2; - else - connectionAttemptTime=GetTimeMS(); - gotNotification=false; - } - } - - if (connectionAttemptTime!=0 && GetTimeMS()>=connectionAttemptTime) - { - connectionAttemptTime=0; - - server->Connect(clientIP,clientPort,0,0); - client->Connect(serverIP,serverPort,0,0); - - connectionResultDeterminationTime=GetTimeMS()+2000; - } - if (connectionResultDeterminationTime!=0 && GetTimeMS()>=connectionResultDeterminationTime) - { - connectionResultDeterminationTime=0; - ASSERT_TRUE(gotNotification) << "Did not recieve expected response."; - - SystemAddress sa; - sa.SetBinaryAddress(serverIP); - sa.SetPortHostOrder(serverPort); - client->CancelConnectionAttempt(sa); - - sa.SetBinaryAddress(clientIP); - sa.SetPortHostOrder(clientPort); - server->CancelConnectionAttempt(sa); - - server->CloseConnection(server->GetSystemAddressFromIndex(0),true,0); - client->CloseConnection(client->GetSystemAddressFromIndex(0),true,0); - - //if (isServer==false) - nextTestStartTime=GetTimeMS()+1000; - - } - if (nextTestStartTime!=0 && GetTimeMS()>=nextTestStartTime) - { - client->Ping(serverIP,serverPort,false); - nextTestStartTime=0; - } - RakSleep(0); - - } -} diff --git a/Tests/Integration/DisconnectReasonTests.cpp b/Tests/Integration/DisconnectReasonTests.cpp deleted file mode 100644 index 934ee9cd4..000000000 --- a/Tests/Integration/DisconnectReasonTests.cpp +++ /dev/null @@ -1,311 +0,0 @@ -/* - * Copyright (c) 2026, MafiaHub - * - * This source code is licensed under the MIT-style license found in the - * license.txt file in the root directory of this source tree. - */ - -#include - -#include "mafianet/peer_interface.h" -#include "mafianet/message_identifiers.h" -#include "mafianet/bit_stream.h" -#include "mafianet/sleep.h" -#include "CommonFunctions.h" - -using namespace MafiaNet; - -namespace -{ - // How many times to retry a full connect -> close -> receive cycle before - // declaring failure. Delivering ID_DISCONNECTION_NOTIFICATION to Receive() - // depends on a reliable ACK round-trip completing, which in turn needs both - // peers' internal network threads to be scheduled. On a CPU-starved CI runner - // running the whole suite in one process, a single cycle can intermittently - // miss its window even with a generous timeout. Re-driving a fresh exchange - // recovers from that transient starvation. Wrong-payload failures are NOT - // retried (see below), so this only absorbs transient misses, never real bugs. - const int kMaxAttempts = 3; - // Per-attempt wait for the notification. Comfortably above a healthy loopback - // round-trip (milliseconds) even under load, and bounded so kMaxAttempts - // attempts can't stall CI for long on a genuine failure. - const int kReceiveTimeoutMs = 12000; - - // TryDisconnectCase outcomes. Non-negative values are concrete error codes - // (translated to messages by DisconnectErrorToString below); negative values - // are transient signals the caller may retry. - const int kTransientNeverReceived = -1; // connected, but notification never surfaced - const int kTransientConnectFailed = -2; // never established the connection - - SystemAddress LoopbackServerAddress(unsigned short serverPort) - { - SystemAddress serverAddr; - serverAddr.SetBinaryAddress("127.0.0.1"); - serverAddr.SetPortHostOrder(serverPort); - return serverAddr; - } - - // Connect the client to the server over loopback and return the client's - // guid as observed by the server (via ID_NEW_INCOMING_CONNECTION). Returns - // UNASSIGNED_RAKNET_GUID on failure. - RakNetGUID ConnectAndGetClientGuid(RakPeerInterface *server, RakPeerInterface *client, unsigned short serverPort) - { - if (!CommonFunctions::WaitAndConnect(client, (char *)"127.0.0.1", serverPort, 10000)) - return UNASSIGNED_RAKNET_GUID; - - Packet *inc = CommonFunctions::WaitAndReturnMessageWithID(server, ID_NEW_INCOMING_CONNECTION, 10000); - if (inc == 0) - return UNASSIGNED_RAKNET_GUID; - - RakNetGUID clientGuid = inc->guid; - server->DeallocatePacket(inc); - return clientGuid; - } - - // Force both peers to drop any connection between them and drain their queues, - // so a fresh connect can succeed. Used between cases and between retry attempts. - // Closes silently (no notification) on both sides: after a transient miss the - // server may still hold the connection (waiting for an ACK that never came), - // which would make the client's reconnect race against an "already connected" - // rejection. Explicitly tearing down both sides removes that race. - void ResetConnection(RakPeerInterface *server, RakPeerInterface *client, const RakNetGUID &clientGuid, unsigned short serverPort) - { - SystemAddress serverAddr = LoopbackServerAddress(serverPort); - client->CloseConnection(serverAddr, false); - if (clientGuid != UNASSIGNED_RAKNET_GUID) - server->CloseConnection(clientGuid, false); - - TimeMS entry = GetTimeMS(); - while (GetTimeMS() - entry < 10000) - { - Packet *p; - for (p = client->Receive(); p; client->DeallocatePacket(p), p = client->Receive()) - ; - for (p = server->Receive(); p; server->DeallocatePacket(p), p = server->Receive()) - ; - if (!CommonFunctions::ConnectionStateMatchesOptions(client, serverAddr, true, true, true, true)) - break; - RakSleep(30); - } - } - - // Run one connect -> CloseConnection(reason) -> receive-notification cycle and - // verify the delivered notification. - // - // Returns: - // 0 success (notification delivered and verified) - // kTransientConnectFailed never connected — caller may retry - // kTransientNeverReceived connected, no notification — caller may retry - // > 0 hard verification failure (wrong payload) — do NOT retry - // - // On every non-success path the connection is reset, leaving a clean slate for - // a retry or the next case. - int TryDisconnectCase(RakPeerInterface *server, RakPeerInterface *client, unsigned short serverPort, - const MafiaNet::BitStream *reasonData, bool expectPayload, - unsigned char expectCode, const RakString &expectText, - int noPayloadErr, int deserialErr, int codeErr, int textErr, - bool isVerbose, const char *okMsg) - { - RakNetGUID clientGuid = ConnectAndGetClientGuid(server, client, serverPort); - if (clientGuid == UNASSIGNED_RAKNET_GUID) - { - ResetConnection(server, client, clientGuid, serverPort); - return kTransientConnectFailed; - } - - server->CloseConnection(clientGuid, true, 0, MafiaNet::Priority::Low, reasonData); - - Packet *note = CommonFunctions::WaitAndReturnMessageWithID(client, ID_DISCONNECTION_NOTIFICATION, kReceiveTimeoutMs); - if (note == 0) - { - // Transient: the reliable round-trip didn't surface the notification in - // time. A fresh exchange (retry) typically recovers under load. - ResetConnection(server, client, clientGuid, serverPort); - return kTransientNeverReceived; - } - - if (!expectPayload) - { - // Default / empty reason: the notification must be a bare 1-byte message. - unsigned int len = note->length; - client->DeallocatePacket(note); - ResetConnection(server, client, clientGuid, serverPort); - if (len != 1) - return noPayloadErr; // hard failure: payload present when none was sent - if (isVerbose) - printf("%s\n", okMsg); - return 0; - } - - // Reason supplied: the notification must carry more than the bare 1-byte ID. - if (note->length <= 1) - { - client->DeallocatePacket(note); - ResetConnection(server, client, clientGuid, serverPort); - return noPayloadErr; - } - - // The body after the ID must round-trip the exact reason we sent. - BitStream readBack(note->data + 1, note->length - 1, false); - unsigned char gotCode = 0; - RakString gotText; - readBack.Read(gotCode); - bool textOk = gotText.Deserialize(&readBack); - client->DeallocatePacket(note); - ResetConnection(server, client, clientGuid, serverPort); - - // Content mismatches are hard failures: a real regression in the reason - // payload would fail these identically on every attempt, so they are never - // retried away. - if (!textOk) - return deserialErr; - if (gotCode != expectCode) - return codeErr; - if (gotText != expectText) - return textErr; - - if (isVerbose) - printf("%s code=%u text=\"%s\"\n", okMsg, (unsigned)gotCode, gotText.C_String()); - return 0; - } - - // Run a case with bounded retries. Transient outcomes are retried up to - // kMaxAttempts; a hard verification failure returns immediately. If every - // attempt is transient, the dominant CI symptom (never received) is reported, - // or the connect failure if we never managed to connect on the last attempt. - int RunDisconnectCaseWithRetries(RakPeerInterface *server, RakPeerInterface *client, unsigned short serverPort, - const MafiaNet::BitStream *reasonData, bool expectPayload, - unsigned char expectCode, const RakString &expectText, - int connectErr, int neverReceivedErr, - int noPayloadErr, int deserialErr, int codeErr, int textErr, - bool isVerbose, const char *okMsg) - { - int lastTransient = kTransientNeverReceived; - for (int attempt = 0; attempt < kMaxAttempts; ++attempt) - { - int result = TryDisconnectCase(server, client, serverPort, reasonData, expectPayload, - expectCode, expectText, noPayloadErr, deserialErr, codeErr, textErr, isVerbose, okMsg); - if (result >= 0) - return result; // success (0) or hard failure (> 0) - lastTransient = result; - if (isVerbose && attempt + 1 < kMaxAttempts) - printf("(transient miss, retrying %d/%d)\n", attempt + 2, kMaxAttempts); - } - return (lastTransient == kTransientConnectFailed) ? connectErr : neverReceivedErr; - } - - // The legacy harness's ErrorCodeToString table, folded into assertion messages. - const char *DisconnectErrorToString(int errorCode) - { - switch (errorCode) - { - case 0: return "No error"; - case 3: return "Case 1: client failed to connect / server saw no incoming connection"; - case 4: return "Case 1: client never received ID_DISCONNECTION_NOTIFICATION"; - case 5: return "Case 1: notification carried no reason payload (length <= 1)"; - case 6: return "Case 1: reason string failed to deserialize"; - case 7: return "Case 1: reason enum code did not round-trip"; - case 8: return "Case 1: reason string did not round-trip"; - case 9: return "Case 2: client failed to reconnect / server saw no incoming connection"; - case 10: return "Case 2: client never received ID_DISCONNECTION_NOTIFICATION"; - case 11: return "Case 2: reasonless notification was not a bare 1-byte message"; - case 12: return "Case 3: client failed to reconnect / server saw no incoming connection"; - case 13: return "Case 3: client never received ID_DISCONNECTION_NOTIFICATION"; - case 14: return "Case 3: empty-reason notification was not a bare 1-byte message"; - default: return "Undefined Error"; - } - } -} - -// Mirrors the legacy DestroyPeers(): both peers are shut down and destroyed in -// TearDown() so cleanup survives a mid-test ASSERT failure. -class DisconnectReason : public ::testing::Test -{ -protected: - void SetUp() override - { - server = RakPeerInterface::GetInstance(); - client = RakPeerInterface::GetInstance(); - - // Bind the server to an OS-assigned ephemeral port. - SocketDescriptor sdServer(0, "127.0.0.1"); - ASSERT_EQ(server->Startup(8, &sdServer, 1), RAKNET_STARTED) << "Server failed to start"; - server->SetMaximumIncomingConnections(8); - - serverPort = server->GetInternalID().GetPort(); - - SocketDescriptor sdClient(0, "127.0.0.1"); - ASSERT_EQ(client->Startup(1, &sdClient, 1), RAKNET_STARTED) << "Client failed to start"; - - // Keep connections alive well past the default 10s timeout. The whole suite - // runs in one process and CI runners are CPU-starved; if a peer's internal - // thread is starved the connection can time out at ~10s and the reliable - // disconnect notification (still being retransmitted from the resend buffer) - // is dropped when the connection dies. A generous timeout lets the - // notification get through under load; it costs nothing on a healthy run, - // where it arrives in milliseconds. - server->SetTimeoutTime(30000, UNASSIGNED_SYSTEM_ADDRESS); - client->SetTimeoutTime(30000, UNASSIGNED_SYSTEM_ADDRESS); - } - - void TearDown() override - { - server->Shutdown(100); - client->Shutdown(100); - RakPeerInterface::DestroyInstance(server); - RakPeerInterface::DestroyInstance(client); - } - - RakPeerInterface *server = nullptr; - RakPeerInterface *client = nullptr; - unsigned short serverPort = 0; -}; - -// The three cases share the server/client peers and run sequentially, exactly -// like the legacy test. -TEST_F(DisconnectReason, NotificationCarriesOptionalReasonPayload) -{ - // --- Case 1: graceful disconnect WITH a reason payload --- - { - // A representative reason: an enum code plus an admin-typed custom string, - // exactly the "Kicked: " use case the feature targets. - const unsigned char kReasonCode = 7; - RakString kReasonText("Cheating detected in match #4821"); - - BitStream reason; - reason.Write(kReasonCode); - kReasonText.Serialize(&reason); - - int err = RunDisconnectCaseWithRetries(server, client, serverPort, &reason, true, - kReasonCode, kReasonText, - /*connectErr*/ 3, /*neverReceivedErr*/ 4, /*noPayloadErr*/ 5, - /*deserialErr*/ 6, /*codeErr*/ 7, /*textErr*/ 8, - false, "Case 1 OK: reason delivered."); - ASSERT_EQ(err, 0) << DisconnectErrorToString(err); - } - - // --- Case 2: graceful disconnect WITHOUT a reason (nullptr default) --- - { - // Default reasonData == nullptr: notification must stay payload-less. - int err = RunDisconnectCaseWithRetries(server, client, serverPort, nullptr, false, - 0, RakString(), - /*connectErr*/ 9, /*neverReceivedErr*/ 10, /*noPayloadErr*/ 11, - /*deserialErr*/ 11, /*codeErr*/ 11, /*textErr*/ 11, - false, "Case 2 OK: reasonless notification is a bare 1-byte message."); - ASSERT_EQ(err, 0) << DisconnectErrorToString(err); - } - - // --- Case 3: graceful disconnect with an EMPTY (non-null) reason BitStream --- - { - // A non-null but empty BitStream exercises the GetNumberOfBytesUsed() > 0 - // guard (distinct from the nullptr check): no bytes were written, so the - // notification must stay payload-less just like the nullptr case. - BitStream emptyReason; - int err = RunDisconnectCaseWithRetries(server, client, serverPort, &emptyReason, false, - 0, RakString(), - /*connectErr*/ 12, /*neverReceivedErr*/ 13, /*noPayloadErr*/ 14, - /*deserialErr*/ 14, /*codeErr*/ 14, /*textErr*/ 14, - false, "Case 3 OK: empty reason BitStream yields a bare 1-byte message."); - ASSERT_EQ(err, 0) << DisconnectErrorToString(err); - } -} diff --git a/Tests/Integration/DroppedConnectionConvertTests.cpp b/Tests/Integration/DroppedConnectionConvertTests.cpp deleted file mode 100644 index c6285ef21..000000000 --- a/Tests/Integration/DroppedConnectionConvertTests.cpp +++ /dev/null @@ -1,298 +0,0 @@ -/* - * Copyright (c) 2026, MafiaHub - * - * This source code is licensed under the MIT-style license found in the - * license.txt file in the root directory of this source tree. - */ - -#include - -#include "mafianet/peer_interface.h" -#include "mafianet/message_identifiers.h" -#include "mafianet/sleep.h" -#include "mafianet/get_time.h" -#include "mafianet/rand.h" // randomMT - -#include "CommonFunctions.h" -#include "RakTimer.h" - -#include - -using namespace MafiaNet; - -// Ported from Samples/Tests/DroppedConnectionConvertTest.cpp - -static const int NUMBER_OF_CLIENTS=9; - -class DroppedConnectionConvert : public ::testing::Test -{ -protected: - void TearDown() override - { - // Shutdown all peers before destroying to let threads clean up - for (RakPeerInterface *peer : destroyList) - peer->Shutdown(100); - - for (RakPeerInterface *peer : destroyList) - RakPeerInterface::DestroyInstance(peer); - } - - RakPeerInterface *RegisterPeer(RakPeerInterface *peer) - { - destroyList.push_back(peer); - return peer; - } - - std::vector destroyList; -}; - -/* -Description: - -Tests silently dropping multiple instances of RakNet. This is used to test that lost connections are detected properly. - -Randomly tests the timout detections to see if the connections are dropped. - -Success conditions: -Clients connect and reconnect normally and do not have an extra connection. -Random timout detection passes. - -Failure conditions: -Client has more than one connection. -Client unable to reconnect. -Connect function fails and there is no pending operation and there is no current connection with server. -Random timout detection fails. - -*/ -TEST_F(DroppedConnectionConvert, LostConnectionsDetected) -{ - const int testDurationMs = 30000; - - RakPeerInterface *server; - RakPeerInterface *clients[NUMBER_OF_CLIENTS]; - unsigned index, connectionCount; - SystemAddress serverID; - Packet *p; - unsigned short numberOfSystems; - unsigned short numberOfSystems2; - int sender; - - server = RegisterPeer(RakPeerInterface::GetInstance()); - // server->InitializeSecurity(0,0,0,0); - SocketDescriptor socketDescriptor(0,"127.0.0.1"); - StartupResult serverResult = server->Startup(NUMBER_OF_CLIENTS, &socketDescriptor, 1); - ASSERT_EQ(serverResult, RAKNET_STARTED) << "Connect failed: Server failed to start (error " << serverResult << ")"; - - // Used to refer to systems. We already know the IP - const unsigned short serverPort = server->GetInternalID().GetPort(); - serverID.FromStringExplicitPort("127.0.0.1", serverPort); - server->SetMaximumIncomingConnections(NUMBER_OF_CLIENTS); - server->SetTimeoutTime(2000,UNASSIGNED_SYSTEM_ADDRESS); - - for (index=0; index < NUMBER_OF_CLIENTS; index++) - { - clients[index] = RegisterPeer(RakPeerInterface::GetInstance()); - SocketDescriptor socketDescriptor2(0,0); - StartupResult clientResult = clients[index]->Startup(1, &socketDescriptor2, 1); - ASSERT_EQ(clientResult, RAKNET_STARTED) << "Connect failed: Client " << index << " failed to start (error " << clientResult << ")"; - ASSERT_EQ(clients[index]->Connect("127.0.0.1", serverPort, 0, 0), CONNECTION_ATTEMPT_STARTED) << "Connect function failed."; - clients[index]->SetTimeoutTime(5000,UNASSIGNED_SYSTEM_ADDRESS); - - RakSleep(1000); - } - - TimeMS entryTime=GetTimeMS();//Loop entry time - - int seed = 12345; - seedMT(seed);//specify seed to keep execution path the same. - - int randomTest; - - bool dropTest=false; - RakTimer timeoutWaitTimer(1000); - - while (GetTimeMS()-entryTimeGetConnectionList(0, &numberOfSystems); - numberOfSystems2=numberOfSystems; - - connectionCount=0; - for (index=0; index < NUMBER_OF_CLIENTS; index++) - { - clients[index]->GetConnectionList(0, &numberOfSystems); - ASSERT_LE(numberOfSystems, 1) << "Client has more than one connection: client " << index << " has " << numberOfSystems << " connections"; - if (numberOfSystems==1) - { - connectionCount++; - } - } - - ASSERT_EQ(connectionCount, numberOfSystems2) << "Timeout not detected: Timeout on dropped clients not detected"; - - } - dropTest=false; - - switch(randomTest) - { - - case 0: - { - index = randomMT() % NUMBER_OF_CLIENTS; - - clients[index]->GetConnectionList(0, &numberOfSystems); - clients[index]->CloseConnection(serverID, false,0); - } - - break; - case 1: - { - index = randomMT() % NUMBER_OF_CLIENTS; - - clients[index]->GetConnectionList(0, &numberOfSystems); - - if(!CommonFunctions::ConnectionStateMatchesOptions (clients[index],serverID,true,true,true,true) )//Are we connected or is there a pending operation ? - { - ASSERT_EQ(clients[index]->Connect("127.0.0.1", serverPort, 0, 0), CONNECTION_ATTEMPT_STARTED) << "Connect function failed."; - } - } - - break; - case 2: - { - - // Randomly connecting and disconnecting each client - for (index=0; index < NUMBER_OF_CLIENTS; index++) - { - if (NUMBER_OF_CLIENTS==1 || (randomMT()%2)==0) - { - if (clients[index]->IsActive()) - { - - int randomTest2=randomMT() %2; - if (randomTest2) - clients[index]->CloseConnection(serverID, false, 0); - else - clients[index]->CloseConnection(serverID, true, 0); - } - } - else - { - if(!CommonFunctions::ConnectionStateMatchesOptions (clients[index],serverID,true,true,true,true) )//Are we connected or is there a pending operation ? - { - ASSERT_EQ(clients[index]->Connect("127.0.0.1", serverPort, 0, 0), CONNECTION_ATTEMPT_STARTED) << "Connect function failed."; - } - } - } - } - break; - - case 3: - { - - // Testing if clients dropped after timeout. - timeoutWaitTimer.Start(); - //Wait half the timeout time, the other half after receive so we don't drop all connections only missing ones, Active ait so the threads run on linux - while (!timeoutWaitTimer.IsExpired()) - { - RakSleep(50); - } - dropTest=true; - - } - break; - default: - // Ignore anything else - break; - } - - server->GetConnectionList(0, &numberOfSystems); - numberOfSystems2=numberOfSystems; - connectionCount=0; - for (index=0; index < NUMBER_OF_CLIENTS; index++) - { - clients[index]->GetConnectionList(0, &numberOfSystems); - ASSERT_LE(numberOfSystems, 1) << "Client has more than one connection: client " << index << " has " << numberOfSystems << " connections"; - if (numberOfSystems==1) - { - connectionCount++; - } - } - - // Parse messages - - while (1) - { - p = server->Receive(); - sender=NUMBER_OF_CLIENTS; - if (p==0) - { - for (index=0; index < NUMBER_OF_CLIENTS; index++) - { - p = clients[index]->Receive(); - if (p!=0) - { - sender=index; - break; - } - } - } - - if (p) - { - switch (p->data[0]) - { - case ID_CONNECTION_REQUEST_ACCEPTED: - break; - case ID_DISCONNECTION_NOTIFICATION: - // Connection lost normally - break; - - case ID_NEW_INCOMING_CONNECTION: - // Somebody connected. We have their IP now - break; - - - case ID_CONNECTION_LOST: - // Couldn't deliver a reliable packet - i.e. the other system was abnormally - // terminated - break; - - case ID_NO_FREE_INCOMING_CONNECTIONS: - break; - - default: - // Ignore anything else - break; - } - } - else - break; - - if (sender==NUMBER_OF_CLIENTS) - server->DeallocatePacket(p); - else - clients[sender]->DeallocatePacket(p); - } - if (dropTest) - { - //Trigger the timeout if no recieve - timeoutWaitTimer.Start(); - while (!timeoutWaitTimer.IsExpired()) - { - RakSleep(50); - } - } - - // Sleep so this loop doesn't take up all the CPU time - - RakSleep(10); - - } -} diff --git a/Tests/Integration/EightPeerTests.cpp b/Tests/Integration/EightPeerTests.cpp deleted file mode 100644 index 10c7f08a0..000000000 --- a/Tests/Integration/EightPeerTests.cpp +++ /dev/null @@ -1,263 +0,0 @@ -/* - * Copyright (c) 2026, MafiaHub - * - * This source code is licensed under the MIT-style license found in the - * license.txt file in the root directory of this source tree. - */ - -#include - -#include "mafianet/peer_interface.h" -#include "mafianet/message_identifiers.h" -#include "mafianet/bit_stream.h" -#include "mafianet/peer.h" -#include "mafianet/sleep.h" -#include "mafianet/time.h" -#include "mafianet/get_time.h" -#include "TestHelpers.h" - -#include - -using namespace MafiaNet; - -class EightPeer : public ::testing::Test -{ -protected: - void TearDown() override - { - // Shutdown all peers before destroying to let threads clean up - for (RakPeerInterface *peer : destroyList) - peer->Shutdown(100); - - for (RakPeerInterface *peer : destroyList) - RakPeerInterface::DestroyInstance(peer); - } - - std::vector destroyList; -}; - -/* -What is being done here is having 8 peers all connect to eachother and be -connected. Then it check if they all connect. If so send data in ordered reliable mode for 100 -loops. - -Possible ideas for changes: -Possibly use rakpeerinterfaces GetSystemList() for number of -connected peers instead of manually tracking. Would be slower though, -shouldn't be significant at this number but the recieve speed it part of the test. - -Success conditions: -Peers connect and receive all packets in order. -No disconnections allowed in this version of the test. - -Failure conditions: - -If cannot connect to all peers for 20 seconds. -All packets are not recieved. -All packets are not in order. -Disconnection. -*/ -TEST_F(EightPeer, FullMeshReliableOrderedBroadcast) -{ - const int peerNum= 8; - RakPeerInterface *peerList[peerNum];//A list of 8 peers - unsigned short ports[peerNum];//OS-assigned port of each peer - int connectionAmount[peerNum];//Counter for me to keep track of connection requests and accepts - int recievedFromList[peerNum][peerNum];//Counter for me to keep track of packets received - int lastNumberReceivedFromList[peerNum][peerNum];//Counter for me to keep track of last recieved sequence number - const int numPackets=100; - Packet *packet; - BitStream bitStream; - - //Initializations of the arrays - for (int i=0;iStartup(peerNum*2, &sd, 1); - ASSERT_EQ(result, RAKNET_STARTED) << "Peer " << i << " failed to start (error " << result << ")"; - ports[i] = TestHelpers::BoundPort(peerList[i]); - peerList[i]->SetMaximumIncomingConnections(peerNum); - - } - - // Give peers time to fully initialize sockets before connecting - RakSleep(100); - - //Connect all the peers together - for (int i=0;iConnect("127.0.0.1", ports[j], 0,0), CONNECTION_ATTEMPT_STARTED) << "Connect function returned failure. Problem while calling connect."; - } - - } - - TimeMS entryTime=GetTimeMS();//Loop entry time - TimeMS finishTimer=GetTimeMS(); - bool initialConnectOver=false;//Our initial connect all has been done. - - for (int k=0;k20000 &&!initialConnectOver &&!allConnected)//failed for 20 seconds - { - FAIL() << "Peers failed to connect. Failed to connect to all peers after 20 seconds"; - } - - if (allConnected) - { - if(!initialConnectOver) - initialConnectOver=true; - if (kSend(&bitStream, MafiaNet::Priority::High, MafiaNet::Reliability::ReliableOrdered ,0, UNASSIGNED_SYSTEM_ADDRESS, true); - - } - } - k++; - } - - if (k>=numPackets-3)//This is our last 3 packets, give it time to send packet and arrive on interface, 2 seconds is more than enough - { - RakSleep(300); - if (k==numPackets) - { - finishTimer=GetTimeMS(); - } - } - - for (int i=0;iReceive(); - waittime++; - - if (!packet) - { - RakSleep(1); - - } - - if (waittime>1000)//Check for packet every millisec and if one second has passed move on, don't block execution - { - break; - } - } - while(!packet);//For testing purposes wait for packet a little while, go if not recieved - } - else//Otherwise just keep recieving quickly until connected - { - packet=peerList[i]->Receive(); - } - while(packet) - { - switch (packet->data[0]) - { - case ID_REMOTE_DISCONNECTION_NOTIFICATION: - FAIL() << "There was a disconnection. Another client has disconnected."; - break; - case ID_REMOTE_CONNECTION_LOST: - FAIL() << "There was a disconnection. Another client has lost the connection."; - break; - case ID_REMOTE_NEW_INCOMING_CONNECTION: - break; - case ID_CONNECTION_REQUEST_ACCEPTED: - connectionAmount[i]++; - - break; - case ID_CONNECTION_ATTEMPT_FAILED: - FAIL() << "Peers failed to connect. A connection has failed."; - break; - - case ID_NEW_INCOMING_CONNECTION: - connectionAmount[i]++;//For this test assume connection. Test will fail if connection fails. - break; - case ID_NO_FREE_INCOMING_CONNECTIONS://Should not happend - FAIL() << "Peers failed to connect. The server is full. This shouldn't happen in this test ever."; - break; - - case ID_ALREADY_CONNECTED: - //Shouldn't happen - break; - - case ID_DISCONNECTION_NOTIFICATION: - FAIL() << "There was a disconnection. We have been disconnected."; - break; - case ID_CONNECTION_LOST: - allConnected=false; - connectionAmount[i]--; - FAIL() << "There was a disconnection. Connection lost."; - break; - default: - - if (packet->data[0]==ID_USER_PACKET_ENUM+1) - { - int thePeerNum; - int sequenceNum; - bitStream.Reset(); - bitStream.Write((char*)packet->data, packet->length); - bitStream.IgnoreBits(8); - bitStream.Read(sequenceNum); - bitStream.Read(thePeerNum); - - if (thePeerNum>=0&&thePeerNumDeallocatePacket(packet); - // Stay in the loop as long as there are more packets. - packet = peerList[i]->Receive(); - } - } - RakSleep(0);//If needed for testing - } - - for (int i=0;i - -#include -#include - -#include "mafianet/peer.h" -#include "mafianet/peer_interface.h" -#include "mafianet/message_identifiers.h" -#include "mafianet/mtu_size.h" -#include "mafianet/bit_stream.h" -#include "mafianet/sleep.h" -#include "mafianet/get_time.h" - -#ifdef _WIN32 -#include // inet_pton, htons (winsock2 pulled in via peer.h) -typedef SOCKET RawSocket; -typedef int RawSockLen; -#define CLOSE_RAW_SOCKET closesocket -#define INVALID_RAW_SOCKET INVALID_SOCKET -#else -#include -#include -#include -#include -typedef int RawSocket; -typedef socklen_t RawSockLen; -#define CLOSE_RAW_SOCKET close -#define INVALID_RAW_SOCKET (-1) -#endif - -using namespace MafiaNet; - -/* -Description: -Covers the MTU the connection handshake settles on, and the connect loop that probes for it. - -The handshake pads ID_OPEN_CONNECTION_REQUEST_1 down a ladder of sizes and keeps the largest one -the path passed. That value is then frozen for the life of the connection and used to size every -datagram in BOTH directions -- nothing re-probes, and nothing detects a path-MTU black hole -afterwards -- so two properties matter, and neither is visible without a real connection: - -- Both peers must end up on the same size, and that size must be inside [MINIMUM, MAXIMUM]. A - value above MAXIMUM_MTU_SIZE would have the reliability layer build datagrams larger than the - buffers it builds them into. -- Splitting and reassembly must still work at whatever that size is. Changing MAXIMUM_MTU_SIZE - changes the fragment size of every message bigger than one datagram. - -Success conditions: -- A loopback connection negotiates exactly MAXIMUM_MTU_SIZE (loopback passes the top rung), and - both sides report it. -- A message many times the MTU arrives byte-for-byte intact. -- Connect() works when asked for fewer attempts than there are rungs in the ladder. - -Failure conditions: any of the above does not hold. -*/ - -namespace -{ - const int kConnectTimeoutMs = 15000; - const int kTransferTimeoutMs = 20000; - - // Drain a peer, returning the first packet with the given id, or 0 if the deadline passes. - // Packets that are not the wanted id are discarded; both peers are pumped so a handshake that - // needs traffic from either side can make progress. - Packet *PumpUntil(RakPeerInterface *wanted, int wantedId, RakPeerInterface *alsoPump, int timeoutMs) - { - TimeMS entry = GetTimeMS(); - while (GetTimeMS() - entry < (TimeMS)timeoutMs) - { - Packet *p; - for (p = wanted->Receive(); p; wanted->DeallocatePacket(p), p = wanted->Receive()) - { - if (p->data[0] == (unsigned char)wantedId) - return p; // caller deallocates - } - if (alsoPump) - { - for (p = alsoPump->Receive(); p; alsoPump->DeallocatePacket(p), p = alsoPump->Receive()) - ; - } - RakSleep(15); - } - return 0; - } - - // Wait until each peer has reported its own connection packet, capturing the address each saw. - // - // This has to be a single loop over both peers rather than two PumpUntil calls. PumpUntil - // discards every packet that is not the one it is waiting for, so waiting on the client first - // throws away the server's ID_NEW_INCOMING_CONNECTION when it happens to arrive during that - // wait, and the second call then blocks until its deadline. That is a real flake, not a - // theoretical one -- it took 15 repeats to surface. - bool PumpUntilBothConnected(RakPeerInterface *client, RakPeerInterface *server, SystemAddress *serverAddressOut, SystemAddress *clientAddressOut, int timeoutMs) - { - bool clientSaw = false; - bool serverSaw = false; - TimeMS entry = GetTimeMS(); - while (GetTimeMS() - entry < (TimeMS)timeoutMs) - { - Packet *p; - for (p = client->Receive(); p; client->DeallocatePacket(p), p = client->Receive()) - { - if (p->data[0] == ID_CONNECTION_REQUEST_ACCEPTED) - { - if (serverAddressOut) - *serverAddressOut = p->systemAddress; - clientSaw = true; - } - } - for (p = server->Receive(); p; server->DeallocatePacket(p), p = server->Receive()) - { - if (p->data[0] == ID_NEW_INCOMING_CONNECTION) - { - if (clientAddressOut) - *clientAddressOut = p->systemAddress; - serverSaw = true; - } - } - if (clientSaw && serverSaw) - return true; - RakSleep(15); - } - return false; - } - - // Poll the closing side's own view of a connection until its slot is gone. - // - // CloseConnection is asynchronous, and the server's ID_DISCONNECTION_NOTIFICATION says nothing - // about when the closing peer finishes its own teardown. Connect() refuses an address that still - // holds an active slot with ALREADY_CONNECTED_TO_ENDPOINT, so a test that closes and reconnects - // has to wait for this and not for the notification. IS_DISCONNECTED means the slot is no longer - // active and IS_NOT_CONNECTED means it is gone entirely; both clear that gate. - bool PumpUntilDisconnected(RakPeerInterface *peer, const SystemAddress &address, RakPeerInterface *alsoPump, int timeoutMs) - { - TimeMS entry = GetTimeMS(); - while (GetTimeMS() - entry < (TimeMS)timeoutMs) - { - Packet *p; - for (p = peer->Receive(); p; peer->DeallocatePacket(p), p = peer->Receive()) - ; - if (alsoPump) - { - for (p = alsoPump->Receive(); p; alsoPump->DeallocatePacket(p), p = alsoPump->Receive()) - ; - } - const ConnectionState state = peer->GetConnectionState(address); - if (state == IS_NOT_CONNECTED || state == IS_DISCONNECTED) - return true; - RakSleep(15); - } - return false; - } - - // Mirrors the constant of the same name in peer.cpp, which is file-static and so cannot be - // included. It is the marker that makes a receiving peer treat a datagram as an offline - // (pre-connection) message rather than reliability-layer traffic. Duplicated deliberately: a - // test for what an unconforming peer can put on the wire should pin the wire format itself. - const unsigned char kOfflineMessageDataId[16] = - {0x00,0xFF,0xFF,0x00,0xFE,0xFE,0xFE,0xFE,0xFD,0xFD,0xFD,0xFD,0x12,0x34,0x56,0x78}; - - // Returned by PumpUntilMTUAssigned when no slot ever appeared. - const int kNoMTUAssigned = -1; - - // A plain UDP socket, standing in for a peer that does not run this library. The MTU in the - // handshake is just a number on the wire, so sending a value no conforming peer would send is - // the only way to reach the receiving side's clamp. - // - // Winsock needs no setup here: RakPeer's constructor holds a WSAStartup reference for as long - // as a peer exists, and the fixture creates both peers in SetUp. - class RawUdpSocket - { - public: - RawUdpSocket() : fd(INVALID_RAW_SOCKET), port(0) {} - ~RawUdpSocket() { Close(); } - - bool Open() - { - fd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); - if (fd == INVALID_RAW_SOCKET) - return false; - - sockaddr_in addr; - memset(&addr, 0, sizeof(addr)); - addr.sin_family = AF_INET; - addr.sin_port = 0; // OS-assigned, like every other port in this suite - if (inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr) != 1) - return false; - if (bind(fd, (const sockaddr *)&addr, sizeof(addr)) != 0) - return false; - - sockaddr_in bound; - memset(&bound, 0, sizeof(bound)); - RawSockLen boundLen = (RawSockLen)sizeof(bound); - if (getsockname(fd, (sockaddr *)&bound, &boundLen) != 0) - return false; - port = ntohs(bound.sin_port); - return true; - } - - bool SendTo(BitStream &bs, unsigned short destPort) - { - sockaddr_in dest; - memset(&dest, 0, sizeof(dest)); - dest.sin_family = AF_INET; - dest.sin_port = htons(destPort); - if (inet_pton(AF_INET, "127.0.0.1", &dest.sin_addr) != 1) - return false; - - const int length = (int)bs.GetNumberOfBytesUsed(); - return sendto(fd, (const char *)bs.GetData(), length, 0, (const sockaddr *)&dest, sizeof(dest)) == length; - } - - void Close() - { - if (fd != INVALID_RAW_SOCKET) - { - CLOSE_RAW_SOCKET(fd); - fd = INVALID_RAW_SOCKET; - } - } - - unsigned short Port() const { return port; } - - private: - RawSocket fd; - unsigned short port; - }; - - // Build the ID_OPEN_CONNECTION_REQUEST_2 an accepting peer parses, with a caller-chosen MTU. - // Written with the same BitStream calls the real handshake uses, so the encoding of the address - // and GUID matches whatever those types currently serialise to. - void WriteOpenConnectionRequest2(BitStream &out, const SystemAddress &bindingAddress, uint16_t mtu, const RakNetGUID &guid) - { - out.Write((MessageID)ID_OPEN_CONNECTION_REQUEST_2); - out.WriteAlignedBytes(kOfflineMessageDataId, sizeof(kOfflineMessageDataId)); - out.Write(bindingAddress); - out.Write(mtu); - out.Write(guid); - } - - // Poll until the peer has given this address a slot, and return that slot's MTU. - // - // An address with no slot reports defaultMTUSize, which is exactly what GetMTUSize() returns for - // the sentinel, so that value doubles as "not yet". Callers must therefore not forge an MTU - // equal to defaultMTUSize -- there would be no way to tell the two apart. - int PumpUntilMTUAssigned(RakPeerInterface *peer, const SystemAddress &address, int timeoutMs) - { - const int unassigned = peer->GetMTUSize(UNASSIGNED_SYSTEM_ADDRESS); - TimeMS entry = GetTimeMS(); - while (GetTimeMS() - entry < (TimeMS)timeoutMs) - { - Packet *p; - for (p = peer->Receive(); p; peer->DeallocatePacket(p), p = peer->Receive()) - ; - const int mtu = peer->GetMTUSize(address); - if (mtu != unassigned) - return mtu; - RakSleep(15); - } - return kNoMTUAssigned; - } - - class MTUNegotiation : public ::testing::Test - { - public: - void SetUp() override - { - server = RakPeerInterface::GetInstance(); - client = RakPeerInterface::GetInstance(); - ASSERT_NE(server, nullptr); - ASSERT_NE(client, nullptr); - } - - void TearDown() override - { - // In TearDown rather than after the assertions, so a failed ASSERT_ still destroys the - // peers: a leaked peer keeps a socket bound and a network thread alive for the rest of - // the process, which in a serial suite surfaces as an unrelated later test failing. - if (client) - { - client->Shutdown(100); - RakPeerInterface::DestroyInstance(client); - client = 0; - } - if (server) - { - server->Shutdown(100); - RakPeerInterface::DestroyInstance(server); - server = 0; - } - } - - // Start both peers on OS-assigned ports and return the server's port. - unsigned short StartPeers() - { - SocketDescriptor serverSd(0, "127.0.0.1"); - EXPECT_EQ(server->Startup(8, &serverSd, 1), RAKNET_STARTED); - server->SetMaximumIncomingConnections(8); - - SocketDescriptor clientSd(0, "127.0.0.1"); - EXPECT_EQ(client->Startup(1, &clientSd, 1), RAKNET_STARTED); - - return server->GetInternalID(UNASSIGNED_SYSTEM_ADDRESS).GetPort(); - } - - RakPeerInterface *server = 0; - RakPeerInterface *client = 0; - }; -} // namespace - -// The negotiated MTU is applied to both directions, so both peers must hold the same number and it -// must be one the datagram buffers can hold. Loopback passes the top rung, so the expected value is -// exact -- which also proves the ladder is not silently starting below MAXIMUM_MTU_SIZE. -// -// This cannot prove the clamp on a remote peer's reported MTU: both peers here are built against -// the same MAXIMUM_MTU_SIZE, so neither can name a larger one through the public API. What it does -// catch is the negotiated value drifting out of range for any other reason. -TEST_F(MTUNegotiation, LoopbackNegotiatesTheMaximumOnBothSides) -{ - const unsigned short port = StartPeers(); - ASSERT_EQ(client->Connect("127.0.0.1", port, 0, 0), CONNECTION_ATTEMPT_STARTED); - - SystemAddress serverAddress; - SystemAddress clientAddress; - ASSERT_TRUE(PumpUntilBothConnected(client, server, &serverAddress, &clientAddress, kConnectTimeoutMs)) - << "both peers did not report the connection"; - - const int clientSideMTU = client->GetMTUSize(serverAddress); - const int serverSideMTU = server->GetMTUSize(clientAddress); - - EXPECT_EQ(clientSideMTU, MAXIMUM_MTU_SIZE); - EXPECT_EQ(serverSideMTU, MAXIMUM_MTU_SIZE); - EXPECT_EQ(clientSideMTU, serverSideMTU) << "the two peers disagree about the datagram size"; - EXPECT_GE(clientSideMTU, MINIMUM_MTU_SIZE); -} - -// Splitting and reassembly happen at the negotiated MTU, so changing that number changes the -// fragment size of every message larger than one datagram. Send one many times the MTU and check it -// back byte for byte. -TEST_F(MTUNegotiation, MessageManyTimesTheMTUArrivesIntact) -{ - const unsigned short port = StartPeers(); - ASSERT_EQ(client->Connect("127.0.0.1", port, 0, 0), CONNECTION_ATTEMPT_STARTED); - - SystemAddress serverAddress; - ASSERT_TRUE(PumpUntilBothConnected(client, server, &serverAddress, 0, kConnectTimeoutMs)) - << "both peers did not report the connection"; - - // ~30 fragments at the current MTU. Deterministic contents, so a reassembly that reorders or - // drops a fragment fails on the comparison rather than only on the length. - const size_t payloadSize = (size_t)MAXIMUM_MTU_SIZE * 30; - std::vector payload(payloadSize); - for (size_t i = 0; i < payloadSize; ++i) - payload[i] = (char)((i * 31 + (i >> 8)) & 0xFF); - - BitStream bs; - bs.Write((MessageID)(ID_USER_PACKET_ENUM + 1)); - bs.Write(payload.data(), (unsigned int)payloadSize); - ASSERT_NE(client->Send(&bs, Priority::High, Reliability::ReliableOrdered, 0, serverAddress, false), 0u); - - Packet *received = PumpUntil(server, ID_USER_PACKET_ENUM + 1, client, kTransferTimeoutMs); - ASSERT_NE(received, nullptr) << "the split message never reassembled"; - ASSERT_EQ((size_t)received->length, payloadSize + sizeof(MessageID)); - EXPECT_EQ(memcmp(received->data + sizeof(MessageID), payload.data(), payloadSize), 0); - server->DeallocatePacket(received); -} - -// Regression: the connect loop divided sendConnectionAttemptCount by the number of rungs in the MTU -// ladder to decide how many attempts to spend on each. Connect() takes that count from the caller -// and validates nothing, so any value below the rung count divided by zero on the first tick of the -// network thread. It was unreachable only because the default (12) happened to exceed the ladder -- -// and adding a rung is exactly the kind of change that makes a latent crash like this reachable. -TEST_F(MTUNegotiation, ConnectWorksWithFewerAttemptsThanMTURungs) -{ - const unsigned short port = StartPeers(); - - // One, two and three are all below the rung count. The first attempt uses the top rung, which - // loopback passes, so the connection completes however small the budget is. - for (unsigned attempts = 1; attempts <= 3; ++attempts) - { - SCOPED_TRACE(testing::Message() << "sendConnectionAttemptCount=" << attempts); - - ASSERT_EQ(client->Connect("127.0.0.1", port, 0, 0, 0, 0, attempts, 500, 0), CONNECTION_ATTEMPT_STARTED); - - SystemAddress serverAddress; - ASSERT_TRUE(PumpUntilBothConnected(client, server, &serverAddress, 0, kConnectTimeoutMs)) - << "both peers did not report the connection"; - - EXPECT_EQ(client->GetMTUSize(serverAddress), MAXIMUM_MTU_SIZE); - - client->CloseConnection(serverAddress, true); - Packet *closed = PumpUntil(server, ID_DISCONNECTION_NOTIFICATION, client, kConnectTimeoutMs); - ASSERT_NE(closed, nullptr) << "the server never saw the connection close"; - server->DeallocatePacket(closed); - - // The server's notification does not mean the client has finished tearing its own slot down, - // and the next iteration's Connect() would be refused while it has not. - ASSERT_TRUE(PumpUntilDisconnected(client, serverAddress, server, kConnectTimeoutMs)) - << "the client never released its connection slot"; - } -} - -// The MTU an accepting peer adopts arrives in ID_OPEN_CONNECTION_REQUEST_2, which is unauthenticated -// wire data: nothing but the clamp stops a peer naming any uint16 it likes. That matters because the -// value sizes every datagram the reliability layer builds into buffers declared MAXIMUM_MTU_SIZE -// bytes long, so an unclamped one writes past the end of them. -// -// Two conforming peers cannot produce this -- both are built against the same MAXIMUM_MTU_SIZE and -// the connecting side never pads above it -- so the packet has to come from a socket that is not -// running this library. -// -// The reliability layer cannot end up disagreeing with the value asserted here: -// AssignSystemAddressToRemoteSystemList hands MTUSize to reliabilityLayer.Reset() on the next line, -// with no second path that could set one without the other. -TEST_F(MTUNegotiation, ForgedOversizedMTUFromTheWireIsClamped) -{ - const unsigned short port = StartPeers(); - - RawUdpSocket forger; - ASSERT_TRUE(forger.Open()); - - SystemAddress serverAddress; - ASSERT_TRUE(serverAddress.FromStringExplicitPort("127.0.0.1", port)); - - BitStream forged; - WriteOpenConnectionRequest2(forged, serverAddress, 0xFFFF, RakNetGUID(0x00C0FFEE00C0FFEEull)); - ASSERT_TRUE(forger.SendTo(forged, port)); - - SystemAddress forgedAddress; - ASSERT_TRUE(forgedAddress.FromStringExplicitPort("127.0.0.1", forger.Port())); - - const int mtu = PumpUntilMTUAssigned(server, forgedAddress, kConnectTimeoutMs); - ASSERT_NE(mtu, kNoMTUAssigned) << "the server never accepted the forged handshake packet"; - EXPECT_EQ(mtu, MAXIMUM_MTU_SIZE) << "a peer-supplied MTU of 0xFFFF was not clamped"; -} - -// The other half of the clamp: a size the buffers can hold must survive untouched, or the fix would -// be silently capping every connection at the wrong number. 1024 is a real rung of the ladder, and -// is deliberately not defaultMTUSize -- see PumpUntilMTUAssigned. -TEST_F(MTUNegotiation, ForgedInRangeMTUIsKept) -{ - const unsigned short port = StartPeers(); - - RawUdpSocket forger; - ASSERT_TRUE(forger.Open()); - - SystemAddress serverAddress; - ASSERT_TRUE(serverAddress.FromStringExplicitPort("127.0.0.1", port)); - - BitStream forged; - WriteOpenConnectionRequest2(forged, serverAddress, 1024, RakNetGUID(0x00DEFACED00DEFACull)); - ASSERT_TRUE(forger.SendTo(forged, port)); - - SystemAddress forgedAddress; - ASSERT_TRUE(forgedAddress.FromStringExplicitPort("127.0.0.1", forger.Port())); - - const int mtu = PumpUntilMTUAssigned(server, forgedAddress, kConnectTimeoutMs); - ASSERT_NE(mtu, kNoMTUAssigned) << "the server never accepted the forged handshake packet"; - EXPECT_EQ(mtu, 1024) << "an MTU within the limit was altered"; -} diff --git a/Tests/Integration/MaximumConnectTests.cpp b/Tests/Integration/MaximumConnectTests.cpp deleted file mode 100644 index b6a7e1d7f..000000000 --- a/Tests/Integration/MaximumConnectTests.cpp +++ /dev/null @@ -1,138 +0,0 @@ -/* - * Copyright (c) 2026, MafiaHub - * - * This source code is licensed under the MIT-style license found in the - * license.txt file in the root directory of this source tree. - */ - -#include - -#include "mafianet/peer_interface.h" -#include "mafianet/message_identifiers.h" -#include "mafianet/bit_stream.h" -#include "mafianet/peer.h" -#include "mafianet/sleep.h" -#include "mafianet/time.h" -#include "mafianet/get_time.h" -#include "TestHelpers.h" - -#include - -using namespace MafiaNet; - -class MaximumConnect : public ::testing::Test -{ -protected: - void TearDown() override - { - // Shutdown all peers before destroying to let threads clean up - for (RakPeerInterface *peer : destroyList) - peer->Shutdown(100); - - for (RakPeerInterface *peer : destroyList) - RakPeerInterface::DestroyInstance(peer); - } - - std::vector destroyList; -}; - -/* -What is being done here is having 8 peers all connect to eachother over the max defined connection. - -It runs the connect, wait 20 seconds then see the current connections. - -Success conditions: -All extra connections Refused. - -Failure conditions: -There are more connected than allowed. -The connect function fails, the test is not even done. -GetMaximumIncomingConnections returns wrong value. - -RakPeerInterface Functions used, tested indirectly by its use: -Startup -Connect -SetMaximumIncomingConnections -Receive -DeallocatePacket -GetSystemList - -RakPeerInterface Functions Explicitly Tested: -SetMaximumIncomingConnections -GetMaximumIncomingConnections - -*/ -TEST_F(MaximumConnect, RefusesConnectionsBeyondMaximumIncoming) -{ - const int peerNum= 8; - const int maxConnections=4;//Max allowed connections for test - RakPeerInterface *peerList[peerNum];//A list of 8 peers - unsigned short ports[peerNum];//OS-assigned port of each peer - - Packet *packet; - - int connReturn; - //Initializations of the arrays - for (int i=0;iStartup(maxConnections, &sd, 1); - ASSERT_EQ(result, RAKNET_STARTED) << "Peer " << i << " failed to start (error " << result << ")"; - ports[i] = TestHelpers::BoundPort(peerList[i]); - peerList[i]->SetMaximumIncomingConnections(maxConnections); - - connReturn=peerList[i]->GetMaximumIncomingConnections(); - EXPECT_EQ(connReturn, maxConnections) << "GetMaximumIncomingConnections returned wrong value for peer " << i; - - } - - //Connect all the peers together - - for (int i=0;iConnect("127.0.0.1", ports[j], 0,0), CONNECTION_ATTEMPT_STARTED) << "The connect function failed. Problem while calling connect."; - - } - - } - - TimeMS entryTime=GetTimeMS();//Loop entry time - - while(GetTimeMS()-entryTime<20000)//Run for 20 Secoonds - { - - for (int i=0;iReceive(); - - while(packet) - { - peerList[i]->DeallocatePacket(packet); - - // Stay in the loop as long as there are more packets. - packet = peerList[i]->Receive(); - } - } - RakSleep(0);//If needed for testing - } - - DataStructures::List< SystemAddress > systemList; - DataStructures::List< RakNetGUID > guidList; - - for (int i=0;iGetSystemList(systemList,guidList); - int connNum=guidList.Size();//Get the number of connections for the current peer - ASSERT_LE(connNum, maxConnections) << "An extra connection was allowed. More connections were allowed to peer " << i << ", " << connNum << " total."; - - } -} diff --git a/Tests/Integration/OfflineMessagesConvertTests.cpp b/Tests/Integration/OfflineMessagesConvertTests.cpp deleted file mode 100644 index a44ee8382..000000000 --- a/Tests/Integration/OfflineMessagesConvertTests.cpp +++ /dev/null @@ -1,139 +0,0 @@ -/* - * Copyright (c) 2026, MafiaHub - * - * This source code is licensed under the MIT-style license found in the - * license.txt file in the root directory of this source tree. - */ - -#include - -#include "TestHelpers.h" -#include "CommonFunctions.h" - -#include -#include - -using namespace MafiaNet; - -/* -Description: -Description: Tests / Demonstrates sending messages to systems you are not connected to. - -Success conditions: -Proper offline response. -Proper offline ping response. - -Failure conditions: -Any success conditions failed - -RakPeerInterface Functions used, tested indirectly by its use: -GetGuidFromSystemAddress -Startup -SetMaximumIncomingConnections -Receive -DeallocatePacket - -RakPeerInterface Functions Explicitly Tested: -SetOfflinePingResponse -GetOfflinePingResponse -AdvertiseSystem -Ping -*/ - -class OfflineMessagesConvert : public ::testing::Test -{ -protected: - void TearDown() override - { - int theSize = destroyList.Size(); - - // Shutdown all peers before destroying to let threads clean up - for (int i = 0; i < theSize; i++) - destroyList[i]->Shutdown(100); - - for (int i = 0; i < theSize; i++) - RakPeerInterface::DestroyInstance(destroyList[i]); - } - - DataStructures::List destroyList; -}; - -TEST_F(OfflineMessagesConvert, AdvertiseSystemAndOfflinePing) -{ - // Reduce test duration in CI environments - const bool isCI = (getenv("CI") != nullptr); - const int testDurationMs = isCI ? 2000 : 10000; // 2 seconds in CI - - bool recievedProperOfflineData = false; - bool recievedProperPingData = false; - - int nextTest; - - RakPeerInterface *peer1 = RakPeerInterface::GetInstance(); - destroyList.Push(peer1, _FILE_AND_LINE_); - RakPeerInterface *peer2 = RakPeerInterface::GetInstance(); - destroyList.Push(peer2, _FILE_AND_LINE_); - - nextTest = 0; - - const unsigned short peer1Port = TestHelpers::StartOnLoopback(peer1, 1, 1); - ASSERT_NE(peer1Port, 0) << "peer1 failed to start"; - const unsigned short peer2Port = TestHelpers::StartOnLoopback(peer2, 1); - ASSERT_NE(peer2Port, 0) << "peer2 failed to start"; - char *pingResponseData = 0; - unsigned int responseLen = 0; - peer1->SetOfflinePingResponse("Offline Ping Data", (int) strlen("Offline Ping Data") + 1); - peer1->GetOfflinePingResponse(&pingResponseData, &responseLen); - - ASSERT_STREQ(pingResponseData, "Offline Ping Data") << "GetOfflinePingResponse failed."; - - // Wait for connection to complete - RakSleep(300); - - peer1->AdvertiseSystem("127.0.0.1", peer2Port, "hello world", (int) strlen("hello world") + 1); - - TimeMS entryTime = GetTimeMS();//Loop entry time - - while (nextTest != 2 && GetTimeMS() - entryTime < (TimeMS) testDurationMs)// run for testDurationMs - { - peer1->DeallocatePacket(peer1->Receive()); - Packet *packet = peer2->Receive(); - if (packet) - { - if (packet->data[0] == ID_ADVERTISE_SYSTEM) - { - ASSERT_GT(packet->length, 1u) << "Got Advertise system with unexpected data"; - ASSERT_STREQ((const char *) (packet->data + 1), "hello world") << "Got Advertise system with unexpected data"; - recievedProperOfflineData = true; - - peer2->Ping("127.0.0.1", peer1Port, false); - nextTest++; - } - else if (packet->data[0] == ID_UNCONNECTED_PONG) - { - // Peer or client. Response from a ping for an unconnected system. - TimeMS packetTime, dataLength; - memcpy((char *) &packetTime, packet->data + sizeof(unsigned char), sizeof(TimeMS)); - dataLength = packet->length - sizeof(unsigned char) - sizeof(TimeMS); - - const char *recString = (const char *) (packet->data + sizeof(unsigned char) + sizeof(TimeMS)); - if (dataLength > 0) - { - printf("Data is %s\n", recString); - - ASSERT_STREQ(recString, "Offline Ping Data") << "Received wrong offline ping response"; - - recievedProperPingData = true; - } - nextTest++; - } - peer2->DeallocatePacket(packet); - } - - RakSleep(30); - } - - ASSERT_TRUE(recievedProperOfflineData) << "Never got proper offline data"; - - ASSERT_TRUE(recievedProperPingData) << "Never got proper ping data"; -} diff --git a/Tests/Integration/PeerConnectDisconnectTests.cpp b/Tests/Integration/PeerConnectDisconnectTests.cpp deleted file mode 100644 index 8b3e52339..000000000 --- a/Tests/Integration/PeerConnectDisconnectTests.cpp +++ /dev/null @@ -1,260 +0,0 @@ -/* - * Copyright (c) 2026, MafiaHub - * - * This source code is licensed under the MIT-style license found in the - * license.txt file in the root directory of this source tree. - */ - -#include - -#include "mafianet/peer_interface.h" -#include "mafianet/message_identifiers.h" -#include "mafianet/bit_stream.h" -#include "mafianet/peer.h" -#include "mafianet/sleep.h" -#include "mafianet/time.h" -#include "mafianet/get_time.h" -#include "CommonFunctions.h" -#include "TestHelpers.h" - -#include - -using namespace MafiaNet; - -class PeerConnectDisconnect : public ::testing::Test -{ -protected: - void TearDown() override - { - // Shutdown all peers before destroying to let threads clean up - for (RakPeerInterface *peer : destroyList) - peer->Shutdown(100); - - for (RakPeerInterface *peer : destroyList) - RakPeerInterface::DestroyInstance(peer); - } - - static void WaitForConnectionRequestsToComplete(RakPeerInterface **peerList, const unsigned short *ports, int peerNum) - { - SystemAddress currentSystem; - bool msgWasPrinted=false; - - for (int i=0;iReceive(); packet; peerList[i]->DeallocatePacket(packet), packet=peerList[i]->Receive()) - { - } - } - } - - std::vector destroyList; -}; - -/* -What is being done here is having 8 peers all connect to eachother, disconnect, connect again. - -Do this for about 10 seconds. Then allow them all to connect for one last time. - -Good ideas for changes: -After the last check run a eightpeers like test an add the conditions -of that test as well. - -Make sure that if we initiate the connection we get a proper message -and if not we get a proper message. Add proper conditions. - -Randomize sending the disconnect notes - -Success conditions: -All connected normally. - -Failure conditions: -Doesn't reconnect normally. - -During the very first connect loop any connect returns false. - -Connect function returns false and peer is not connected to anything. - -*/ -TEST_F(PeerConnectDisconnect, MeshReconnectsAfterRepeatedDisconnects) -{ - const int testDurationMs = 10000; - - const int peerNum= 8; - const int maxConnections=peerNum*3;//Max allowed connections for test set to times 3 to eliminate problem variables - RakPeerInterface *peerList[peerNum];//A list of 8 peers - unsigned short ports[peerNum];//OS-assigned port of each peer - - SystemAddress currentSystem; - - //Initializations of the arrays - for (int i=0;iStartup(maxConnections, &sd, 1); - ASSERT_EQ(result, RAKNET_STARTED) << "Peer " << i << " failed to start (error " << result << ")"; - ports[i] = TestHelpers::BoundPort(peerList[i]); - peerList[i]->SetMaximumIncomingConnections(maxConnections); - - } - - //Connect all the peers together - - for (int i=0;iConnect("127.0.0.1", ports[j], 0,0), CONNECTION_ATTEMPT_STARTED) << "The connect function failed. Problem while calling connect."; - - } - - } - - TimeMS entryTime=GetTimeMS();//Loop entry time - - DataStructures::List< SystemAddress > systemList; - DataStructures::List< RakNetGUID > guidList; - - printf("Entering disconnect loop \n"); - - while(GetTimeMS()-entryTimeGetSystemList(systemList,guidList);//Get connectionlist - int len=systemList.Size(); - - for (int j=0;jCloseConnection (systemList[j],true,0,MafiaNet::Priority::Low); - } - - } - - RakSleep(100); - - //Connect - - for (int i=0;iConnect("127.0.0.1", ports[j], 0,0), CONNECTION_ATTEMPT_STARTED) << "The connect function failed. Problem while calling connect."; - } - - } - - } - - WaitAndPrintResults(peerList,ports,peerNum); - - } - - WaitAndPrintResults(peerList,ports,peerNum); - - printf("Connecting peers\n"); - - // Bring the full mesh back up and wait for it to converge before verifying. - // Establishing peerNum*(peerNum-1) connections can take longer than a single pass, - // especially under load or in containerized/emulated environments, so poll until - // every peer has peerNum-1 connections (re-initiating any missing links) rather - // than checking exactly once. Only fail if convergence does not happen in time. - const int convergeTimeoutMs = 30000; - TimeMS convergeStart = GetTimeMS(); - bool allConnected = false; - int failedPeer = -1; - int failedPeerConns = 0; - - while (GetTimeMS()-convergeStart < (TimeMS)convergeTimeoutMs) - { - // (Re)initiate any link that is not already connected/connecting/pending. - for (int i=0;iConnect("127.0.0.1", ports[j], 0,0); - // STARTED is success. ALREADY_CONNECTED_TO_ENDPOINT and - // CONNECTION_ATTEMPT_ALREADY_IN_PROGRESS are benign races (the link came up - // between the state check above and this call) and are expected while the - // mesh re-converges. Anything else (INVALID_PARAMETER, CANNOT_RESOLVE_*, - // SECURITY_INITIALIZATION_FAILED) is a real failure: report it with context - // and fail fast instead of spinning until the convergence timeout. - ASSERT_TRUE(car==CONNECTION_ATTEMPT_STARTED || - car==ALREADY_CONNECTED_TO_ENDPOINT || - car==CONNECTION_ATTEMPT_ALREADY_IN_PROGRESS) - << "The connect function failed. Connect() from peer " << i << " to 127.0.0.1:" << ports[j] - << " (" << currentSystem.ToString() << ") failed with result " << (int)car << "."; - } - } - } - - WaitAndPrintResults(peerList,ports,peerNum); - - allConnected = true; - for (int i=0;iGetSystemList(systemList,guidList); - if ((int)guidList.Size()!=peerNum-1)//Did we connect to all? - { - allConnected = false; - failedPeer = i; - failedPeerConns = (int)guidList.Size(); - break; - } - } - - if (allConnected) - break; - - RakSleep(100); - } - - ASSERT_TRUE(allConnected) << "Peers did not connect normally. Not all peers reconnected normally. Failed on peer number " << failedPeer << " with " << failedPeerConns << " peers"; -} diff --git a/Tests/Integration/PeerConnectDisconnectWithCancelPendingTests.cpp b/Tests/Integration/PeerConnectDisconnectWithCancelPendingTests.cpp deleted file mode 100644 index 7c5264fae..000000000 --- a/Tests/Integration/PeerConnectDisconnectWithCancelPendingTests.cpp +++ /dev/null @@ -1,279 +0,0 @@ -/* - * Copyright (c) 2026, MafiaHub - * - * This source code is licensed under the MIT-style license found in the - * license.txt file in the root directory of this source tree. - */ - -#include - -#include "mafianet/peer_interface.h" -#include "mafianet/message_identifiers.h" -#include "mafianet/bit_stream.h" -#include "mafianet/peer.h" -#include "mafianet/sleep.h" -#include "mafianet/time.h" -#include "mafianet/get_time.h" -#include "CommonFunctions.h" -#include "TestHelpers.h" - -#include - -using namespace MafiaNet; - -class PeerConnectDisconnectWithCancelPending : public ::testing::Test -{ -protected: - void TearDown() override - { - // Shutdown all peers before destroying to let threads clean up - for (RakPeerInterface *peer : destroyList) - peer->Shutdown(100); - - for (RakPeerInterface *peer : destroyList) - RakPeerInterface::DestroyInstance(peer); - } - - std::vector destroyList; -}; - -/* -What is being done here is having 8 peers all connect to eachother, disconnect, connect again. - -Do this for about 10 seconds. Then allow them all to connect for one last time. - -This test also tests the cancelpendingconnections. - -Also tests nonblocking connects, the simpler one PeerConnectDisconnect tests without it - -Good ideas for changes: -After the last check run a eightpeers like test an add the conditions -of that test as well. - -Make sure that if we initiate the connection we get a proper message -and if not we get a proper message. Add proper conditions. - -Randomize sending the disconnect notes - -Success conditions: -All connected normally and pending requests get canceled normally. - -Failure conditions: -Doesn't reconnect normally. - -During the very first connect loop any connect returns false. - -Connect function returns false and peer is not connected to anything. - -Pending request is not canceled. - -*/ -TEST_F(PeerConnectDisconnectWithCancelPending, MeshReconnectsWithCancelledPendingAttempts) -{ - const int testDurationMs = 10000; - const int postLoopWaitMs = 2000; - const int finalWaitMs = 5000; - - const int peerNum= 8; - const int maxConnections=peerNum*3;//Max allowed connections for test set to times 3 to eliminate problem variables - RakPeerInterface *peerList[peerNum];//A list of 8 peers - unsigned short ports[peerNum];//OS-assigned port of each peer - - SystemAddress currentSystem; - - Packet *packet; - - //Initializations of the arrays - for (int i=0;iStartup(maxConnections, &sd, 1); - ASSERT_EQ(result, RAKNET_STARTED) << "Peer " << i << " failed to start (error " << result << ")"; - ports[i] = TestHelpers::BoundPort(peerList[i]); - peerList[i]->SetMaximumIncomingConnections(maxConnections); - - } - - //Connect all the peers together - - for (int i=0;iConnect("127.0.0.1", ports[j], 0,0), CONNECTION_ATTEMPT_STARTED) << "The connect function failed. Problem while calling connect."; - - } - - } - - TimeMS entryTime=GetTimeMS();//Loop entry time - - DataStructures::List< SystemAddress > systemList; - DataStructures::List< RakNetGUID > guidList; - - printf("Entering disconnect loop \n"); - - while(GetTimeMS()-entryTimeGetSystemList(systemList,guidList);//Get connectionlist - int len=systemList.Size(); - - for (int j=0;jCloseConnection (systemList[j],true,0,MafiaNet::Priority::Low); - } - - } - - RakSleep(100); - //Clear pending if not finished - - for (int i=0;iCancelConnectionAttempt(currentSystem); //Make sure a connection is not pending before trying to connect. - - } - - } - - RakSleep(100); - //Connect - - for (int i=0;iConnect("127.0.0.1", ports[j], 0,0)!=CONNECTION_ATTEMPT_STARTED) - { - - currentSystem.SetBinaryAddress("127.0.0.1"); - currentSystem.SetPortHostOrder(ports[j]); - - peerList[i]->GetSystemList(systemList,guidList);//Get connectionlist - - ASSERT_FALSE(CommonFunctions::ConnectionStateMatchesOptions (peerList[i],currentSystem,false,true,true))//Did we drop the pending connection? - << "Pending connection was not canceled. Did not cancel the pending request"; - - ASSERT_TRUE(CommonFunctions::ConnectionStateMatchesOptions (peerList[i],currentSystem,true,true,true,true)) - << "The connect function failed. Problem while calling connect."; - - } - - } - - } - - for (int i=0;iReceive(); - - while(packet) - { - peerList[i]->DeallocatePacket(packet); - - // Stay in the loop as long as there are more packets. - packet = peerList[i]->Receive(); - } - } - RakSleep(0);//If needed for testing - } - - while(GetTimeMS()-entryTimeReceive(); - - while(packet) - { - peerList[i]->DeallocatePacket(packet); - - // Stay in the loop as long as there are more packets. - packet = peerList[i]->Receive(); - } - } - RakSleep(0);//If needed for testing - } - - //Connect - - for (int i=0;iCancelConnectionAttempt(currentSystem); //Make sure a connection is not pending before trying to connect. - - if (peerList[i]->Connect("127.0.0.1", ports[j], 0,0)!=CONNECTION_ATTEMPT_STARTED) - { - - peerList[i]->GetSystemList(systemList,guidList);//Get connectionlist - int len=systemList.Size(); - - ASSERT_NE(len, 0)//No connections, should not fail. - << "The connect function failed. Problem while calling connect"; - - } - - } - - } - - entryTime=GetTimeMS(); - - while(GetTimeMS()-entryTimeReceive(); - - while(packet) - { - peerList[i]->DeallocatePacket(packet); - - // Stay in the loop as long as there are more packets. - packet = peerList[i]->Receive(); - } - } - RakSleep(0);//If needed for testing - } - - for (int i=0;iGetSystemList(systemList,guidList); - int connNum=guidList.Size();//Get the number of connections for the current peer - ASSERT_EQ(connNum, peerNum-1)//Did we connect to all? - << "Peers did not connect normally. Not all peers reconnected normally."; - - } -} diff --git a/Tests/Integration/PingTestsTests.cpp b/Tests/Integration/PingTestsTests.cpp deleted file mode 100644 index 61980f4a0..000000000 --- a/Tests/Integration/PingTestsTests.cpp +++ /dev/null @@ -1,325 +0,0 @@ -/* - * Copyright (c) 2026, MafiaHub - * - * This source code is licensed under the MIT-style license found in the - * license.txt file in the root directory of this source tree. - */ - -#include - -#include "TestHelpers.h" -#include "CommonFunctions.h" -#include "RakTimer.h" - -#include -#include - -#include "mafianet/internal_packet.h" -#include "mafianet/message_identifiers.h" -#include "mafianet/plugin_interface2.h" - -using namespace MafiaNet; - -/* -Description: -Tests out: -virtual int GetAveragePing (const SystemAddress systemAddress)=0 -virtual int GetLastPing (const SystemAddress systemAddress) const =0 -virtual int GetLowestPing (const SystemAddress systemAddress) const =0 -virtual void SetOccasionalPing (bool doPing)=0 - -Ping is tested in CrossConnectionConvertTest,SetOfflinePingResponse and GetOfflinePingResponse tested in OfflineMessagesConvertTest - -Success conditions: -Currently is that GetAveragePing and SetOccasionalPing works - -Failure conditions: - -RakPeerInterface Functions used, tested indirectly by its use, not all encompassing list: -Startup -SetMaximumIncomingConnections -Receive -DeallocatePacket - -RakPeerInterface Functions Explicitly Tested: -GetAveragePing -GetLastPing -GetLowestPing -SetOccasionalPing -*/ - -namespace -{ - // Counts ID_CONNECTED_PING arriving from each of two senders. - // - // SetOccasionalPing has no public observable: the pings it triggers are consumed inside the - // library and never surface as packets. The old test inferred it from the average ping dropping - // below a 10 ms threshold -- one extra sample moving an average across a hard bound, which is both - // indirect and the reason the test tripped on ordinary machine load. OnInternalPacket sees the - // pings themselves, on the receiving side, and counting them is a direct assertion. - // - // UsesReliabilityLayer() must return true for OnInternalPacket to be wired up, which also means - // this has to be attached before Startup(). - class ConnectedPingCounter : public PluginInterface2 - { - public: - std::atomic portA; - std::atomic portB; - std::atomic countA; - std::atomic countB; - - ConnectedPingCounter() : portA(0), portB(0), countA(0), countB(0) {} - - virtual bool UsesReliabilityLayer(void) const { return true; } - - // Runs on the network thread; the counters are atomic because the test reads them from its own. - virtual void OnInternalPacket(InternalPacket *internalPacket, unsigned frameNumber, - SystemAddress remoteSystemAddress, MafiaNet::TimeMS time, int isSend) - { - (void) frameNumber; - (void) time; - if (isSend) - return; - if (internalPacket == 0 || internalPacket->data == 0 || internalPacket->dataBitLength < 8) - return; - if (internalPacket->data[0] != (unsigned char)ID_CONNECTED_PING) - return; - - const unsigned short port = remoteSystemAddress.GetPort(); - if (port != 0 && port == portA.load()) - ++countA; - else if (port != 0 && port == portB.load()) - ++countB; - } - }; - - ::testing::AssertionResult AverageValueOk(int averagePing, int maxAveragePingMs) - { - if (averagePing < 0) - return ::testing::AssertionFailure() << "Problem with the average ping time, should never be less than zero in this test (average ping " << averagePing << ")"; - - if (averagePing > maxAveragePingMs) - return ::testing::AssertionFailure() << "Average Ping exceeded expected threshold for localhost (average ping " << averagePing << " exceeded threshold " << maxAveragePingMs << ")"; - - return ::testing::AssertionSuccess(); - } -} - -class PingTests : public ::testing::Test -{ -protected: - void TearDown() override - { - int theSize = destroyList.Size(); - - // Shutdown all peers before destroying to let threads clean up - for (int i = 0; i < theSize; i++) - destroyList[i]->Shutdown(100); - - for (int i = 0; i < theSize; i++) - RakPeerInterface::DestroyInstance(destroyList[i]); - } - - DataStructures::List destroyList; - - // Fixture-owned, not a local in the test body. A failed ASSERT_ returns before any cleanup the - // body would have done, and the peers are destroyed later in TearDown() -- Shutdown() makes a - // virtual call into every attached plugin (peer.cpp, OnRakPeerShutdown), and the network thread - // can still deliver OnInternalPacket during the shutdown window. A stack-local plugin is already - // destroyed by then, which ASAN reports as a BUS on a corrupted vtable pointer: one failed - // assertion becomes a crash that hides the original failure. - // - // A fixture member is destroyed after TearDown() returns, so no explicit detach is needed: the - // plugin outlives every peer that could call into it. - ConnectedPingCounter counter; -}; - -TEST_F(PingTests, PingStatisticsAndOccasionalPing) -{ - // Absolute ceilings are a sanity bound against a gross regression -- a stall, a milliseconds/ - // microseconds mix-up -- not a measurement of the host. The old local values (10 ms average and - // lowest) were tight enough that ordinary machine load tripped them: a loopback average of 12 ms on - // a busy machine is the library behaving correctly and the assertion measuring the CPU scheduler. - // - // They were also carrying the only check that SetOccasionalPing did anything, by way of one extra - // ping sample pulling the average under the bound. That is now asserted directly in - // OccasionalPingIsObservableAsPingTraffic, so these can be loosened without losing coverage. - // - // One set of numbers rather than a CI/non-CI split: a test that holds to different standards - // depending on the environment cannot be reproduced locally when it fails in CI. - const int maxLastPingMs = 1000; - const int maxLowestPingMs = 500; - const int maxAveragePingMs = 500; - - RakPeerInterface *sender, *sender2, *receiver; - - TestHelpers::StandardClientPrep(sender, destroyList); - - TestHelpers::StandardClientPrep(sender2, destroyList); - - receiver = RakPeerInterface::GetInstance(); - destroyList.Push(receiver, _FILE_AND_LINE_); - SocketDescriptor receiverSd(0, "127.0.0.1"); - ASSERT_EQ(receiver->Startup(2, &receiverSd, 1), RAKNET_STARTED) << "Receiver failed to start"; - receiver->SetMaximumIncomingConnections(2); - const unsigned short receiverPort = receiver->GetInternalID(UNASSIGNED_SYSTEM_ADDRESS).GetPort(); - Packet *packet; - - SystemAddress currentSystem; - - currentSystem.SetBinaryAddress("127.0.0.1"); - currentSystem.SetPortHostOrder(receiverPort); - - printf("Connecting sender2\n"); - ASSERT_TRUE(TestHelpers::WaitAndConnectTwoPeersLocally(sender2, receiver, 5000)) << "Could not connect after 5 seconds"; - - printf("Getting ping data for lastping and lowestping\n"); - sender2->SetOccasionalPing(false);//Test the lowest ping and such without occassionalping,occasional ping comes later - RakTimer timer(1500); - - int lastPing = 0; - int lowestPing = 0; - TimeMS nextPing = 0; - - while (!timer.IsExpired()) - { - for (packet = receiver->Receive(); packet; receiver->DeallocatePacket(packet), packet = receiver->Receive()) - { - } - - for (packet = sender2->Receive(); packet; sender2->DeallocatePacket(packet), packet = sender2->Receive()) - { - } - - if (GetTimeMS() > nextPing) - { - sender2->Ping(currentSystem); - nextPing = GetTimeMS() + 30; - } - - RakSleep(3); - } - - int averagePing = sender2->GetAveragePing(currentSystem); - - lastPing = sender2->GetLastPing(currentSystem); - lowestPing = sender2->GetLowestPing(currentSystem); - - // The relational properties are the real correctness content: they hold at any latency, so they - // fail only on a genuine bug rather than on a loaded machine. - ASSERT_GE(lastPing, 0) << "GetLastPing reported no data after an explicitly pinged connection"; - ASSERT_GE(lowestPing, 0) << "GetLowestPing reported no data after an explicitly pinged connection"; - ASSERT_GE(averagePing, 0) << "GetAveragePing reported no data after an explicitly pinged connection"; - - ASSERT_GE(lastPing, lowestPing) << "There is a problem if the lastping is lower than the lowestping stat"; - EXPECT_GE(averagePing, lowestPing) << "the average ping cannot be below the lowest sample"; - - // Absolute bounds last, so a load-induced trip is reported after the real invariants have been - // checked rather than hiding them. - ASSERT_TRUE(AverageValueOk(averagePing, maxAveragePingMs)); - - ASSERT_LE(lastPing, maxLastPingMs) << "Problem with the last ping time, greater than expected for localhost"; - - ASSERT_LE(lowestPing, maxLowestPingMs) << "The lowest ping for localhost should drop below expected threshold at least once"; - - CommonFunctions::DisconnectAndWait(sender2, (char *) "127.0.0.1", receiverPort);//Eliminate variables. - - printf("Connecting sender\n"); - ASSERT_TRUE(TestHelpers::WaitAndConnectTwoPeersLocally(sender, receiver, 5000)) << "Could not connect after 5 seconds"; - - sender->SetOccasionalPing(true); - - printf("Testing SetOccasionalPing\n"); - - timer.Start(); - while (!timer.IsExpired()) - { - for (packet = receiver->Receive(); packet; receiver->DeallocatePacket(packet), packet = receiver->Receive()) - { - } - - for (packet = sender->Receive(); packet; sender->DeallocatePacket(packet), packet = sender->Receive()) - { - } - - RakSleep(3); - } - - averagePing = sender->GetAveragePing(currentSystem); - - ASSERT_TRUE(AverageValueOk(averagePing, maxAveragePingMs)); -} - -// Direct coverage for SetOccasionalPing, which the statistics test only ever inferred from an -// average-ping threshold. -// -// Both senders run in the SAME observation window against the same receiver, one with occasional -// ping on and one off. Sequential phases would compare two different stretches of machine time; side -// by side, the only difference between the two counts is the flag. -// -// The window has to outrun the 5 s occasional-ping interval (peer.cpp: nextPingTime = timeMS + 5000), -// so the enabled sender is expected to produce the immediate ping plus at least one interval ping. -// The assertion that carries the test is the relative one: pings keep coming when it is on and stop -// when it is off. -TEST_F(PingTests, OccasionalPingIsObservableAsPingTraffic) -{ - RakPeerInterface *receiver = RakPeerInterface::GetInstance(); - destroyList.Push(receiver, _FILE_AND_LINE_); - receiver->AttachPlugin(&counter); // before Startup: UsesReliabilityLayer() is true - SocketDescriptor receiverSd(0, "127.0.0.1"); - ASSERT_EQ(receiver->Startup(2, &receiverSd, 1), RAKNET_STARTED); - receiver->SetMaximumIncomingConnections(2); - - RakPeerInterface *pinging, *quiet; - TestHelpers::StandardClientPrep(pinging, destroyList); - TestHelpers::StandardClientPrep(quiet, destroyList); - - counter.portA.store(pinging->GetInternalID(UNASSIGNED_SYSTEM_ADDRESS).GetPort()); - counter.portB.store(quiet->GetInternalID(UNASSIGNED_SYSTEM_ADDRESS).GetPort()); - ASSERT_NE(counter.portA.load(), 0); - ASSERT_NE(counter.portB.load(), 0); - ASSERT_NE(counter.portA.load(), counter.portB.load()); - - pinging->SetOccasionalPing(true); - quiet->SetOccasionalPing(false); - - ASSERT_TRUE(TestHelpers::WaitAndConnectTwoPeersLocally(pinging, receiver, 5000)) << "Could not connect after 5 seconds"; - ASSERT_TRUE(TestHelpers::WaitAndConnectTwoPeersLocally(quiet, receiver, 5000)) << "Could not connect after 5 seconds"; - - // Counts from the handshake are not what is under test; only what happens from here on is. - counter.countA.store(0); - counter.countB.store(0); - - // Longer than the 5 s interval so an enabled sender must ping more than once. - RakTimer timer(11000); - timer.Start(); - Packet *packet; - while (!timer.IsExpired()) - { - for (packet = receiver->Receive(); packet; receiver->DeallocatePacket(packet), packet = receiver->Receive()) - { - } - for (packet = pinging->Receive(); packet; pinging->DeallocatePacket(packet), packet = pinging->Receive()) - { - } - for (packet = quiet->Receive(); packet; quiet->DeallocatePacket(packet), packet = quiet->Receive()) - { - } - RakSleep(10); - } - - const int withOccasionalPing = counter.countA.load(); - const int withoutOccasionalPing = counter.countB.load(); - - // Printed on success too: the margin between these two is what makes the test meaningful, so a - // future reader can see it narrowing before it starts failing. - printf("ID_CONNECTED_PING received over 11 s: occasional ping on = %d, off = %d\n", - withOccasionalPing, withoutOccasionalPing); - - EXPECT_GE(withOccasionalPing, 2) - << "SetOccasionalPing(true) produced " << withOccasionalPing - << " pings over 11 s; the interval is 5 s, so at least two were due"; - EXPECT_GT(withOccasionalPing, withoutOccasionalPing) - << "occasional ping made no difference: " << withOccasionalPing << " pings with it enabled vs " - << withoutOccasionalPing << " with it disabled"; -} diff --git a/Tests/Integration/ReliableOrderedConvertedTests.cpp b/Tests/Integration/ReliableOrderedConvertedTests.cpp deleted file mode 100644 index 4a52f1559..000000000 --- a/Tests/Integration/ReliableOrderedConvertedTests.cpp +++ /dev/null @@ -1,206 +0,0 @@ -/* - * Copyright (c) 2026, MafiaHub - * - * This source code is licensed under the MIT-style license found in the - * license.txt file in the root directory of this source tree. - */ - -#include - -#include "mafianet/peer_interface.h" -#include "mafianet/get_time.h" -#include "mafianet/message_identifiers.h" -#include "mafianet/bit_stream.h" -#include "mafianet/rand.h" -#include "mafianet/statistics.h" -#include "mafianet/sleep.h" - -#include // For atoi -#include -#include - -using namespace MafiaNet; - -// Ported from Samples/Tests/ReliableOrderedConvertedTest.cpp - -class ReliableOrderedConverted : public ::testing::Test -{ -protected: - void TearDown() override - { - // Shutdown all peers before destroying to let threads clean up - for (RakPeerInterface *peer : destroyList) - peer->Shutdown(100); - - for (RakPeerInterface *peer : destroyList) - RakPeerInterface::DestroyInstance(peer); - } - - RakPeerInterface *RegisterPeer(RakPeerInterface *peer) - { - destroyList.push_back(peer); - return peer; - } - - std::vector destroyList; -}; - -/* -What is being done here is having a server connect to a client. - -Packets are sent at 30 millisecond intervals for 12 seconds. - -Length and sequence are checked for each packet. - -Success conditions: -All packets are correctly recieved in order. - -Failure conditions: - -All packets are not correctly recieved. -All packets are not in order. - -*/ -TEST_F(ReliableOrderedConverted, AllPacketsReceivedInOrder) -{ - RakPeerInterface *sender, *receiver; - unsigned int packetNumberSender[32],packetNumberReceiver[32], receivedPacketNumberReceiver, receivedTimeReceiver; - char str[256]; - char ip[32]; - TimeMS sendInterval, nextSend, currentTime, quitTime; - unsigned short remotePort; - unsigned char streamNumberSender,streamNumberReceiver; - BitStream bitStream; - Packet *packet; - bool doSend=false; - - for (int i=0; i < 32; i++) - { - packetNumberSender[i]=0; - packetNumberReceiver[i]=0; - - } - - // The receiver is started first so the sender can connect to its OS-assigned port. - receiver = RegisterPeer(RakPeerInterface::GetInstance()); - - SocketDescriptor receiverSd(0, "127.0.0.1"); - StartupResult receiverResult = receiver->Startup(32, &receiverSd, 1); - ASSERT_EQ(receiverResult, RAKNET_STARTED) << "Receiver failed to start (error " << receiverResult << ")"; - remotePort = receiver->GetInternalID().GetPort(); - receiver->SetMaximumIncomingConnections(32); - - sender = RegisterPeer(RakPeerInterface::GetInstance()); - //sender->ApplyNetworkSimulator(.02, 100, 50); - - sendInterval=30; - - strcpy(ip, "127.0.0.1"); - - SocketDescriptor senderSd(0, 0); - StartupResult senderResult = sender->Startup(1, &senderSd, 1); - ASSERT_EQ(senderResult, RAKNET_STARTED) << "Sender failed to start (error " << senderResult << ")"; - sender->Connect(ip, remotePort, 0, 0); - - // if (sender) - // sender->ApplyNetworkSimulator(128000, 50, 100); - // if (receiver) - // receiver->ApplyNetworkSimulator(128000, 50, 100); - - strcpy(str, "12"); - - currentTime = GetTimeMS(); - quitTime = atoi(str) * 1000 + currentTime; - - nextSend=currentTime; - - while (currentTime < quitTime) - { - - packet = sender->Receive(); - while (packet) - { - // PARSE TYPES - switch(packet->data[0]) - { - case ID_CONNECTION_REQUEST_ACCEPTED: - doSend=true; - nextSend=currentTime; - break; - case ID_NO_FREE_INCOMING_CONNECTIONS: - break; - case ID_DISCONNECTION_NOTIFICATION: - break; - case ID_CONNECTION_LOST: - break; - case ID_CONNECTION_ATTEMPT_FAILED: - break; - } - - sender->DeallocatePacket(packet); - packet = sender->Receive(); - } - - while (doSend && currentTime > nextSend) - { - streamNumberSender=0; - // streamNumber = randomMT() % 32; - // Do the send - bitStream.Reset(); - bitStream.Write((unsigned char) (ID_USER_PACKET_ENUM+1)); - bitStream.Write(packetNumberSender[streamNumberSender]++); - bitStream.Write(streamNumberSender); - bitStream.Write(currentTime); - char *pad; - int padLength = (randomMT() % 5000) + 1; - pad = new char [padLength]; - bitStream.Write(pad, padLength); - delete [] pad; - // Send on a random priority with a random stream - // if (sender->Send(&bitStream, MafiaNet::Priority::High, (MafiaNet::Reliability) (MafiaNet::Reliability::Reliable + (randomMT() %2)) ,streamNumber, UNASSIGNED_SYSTEM_ADDRESS, true)==false) - if (sender->Send(&bitStream, MafiaNet::Priority::High, MafiaNet::Reliability::ReliableOrdered ,streamNumberSender, UNASSIGNED_SYSTEM_ADDRESS, true)==false) - packetNumberSender[streamNumberSender]--; // Didn't finish connecting yet? - - nextSend+=sendInterval; - - // Test halting - // if (rand()%20==0) - // nextSend+=1000; - } - - packet = receiver->Receive(); - while (packet) - { - switch(packet->data[0]) - { - case ID_NEW_INCOMING_CONNECTION: - break; - case ID_DISCONNECTION_NOTIFICATION: - break; - case ID_CONNECTION_LOST: - break; - case ID_USER_PACKET_ENUM+1: - bitStream.Reset(); - bitStream.Write((char*)packet->data, packet->length); - bitStream.IgnoreBits(8); // Ignore ID_USER_PACKET_ENUM+1 - bitStream.Read(receivedPacketNumberReceiver); - bitStream.Read(streamNumberReceiver); - bitStream.Read(receivedTimeReceiver); - - ASSERT_EQ(receivedPacketNumberReceiver, packetNumberReceiver[streamNumberReceiver]) - << "The very last error for this object was Expecting " << packetNumberReceiver[streamNumberReceiver] - << " got " << receivedPacketNumberReceiver << " (channel " << (int) streamNumberReceiver << ")."; - - packetNumberReceiver[streamNumberReceiver]++; - break; - } - - receiver->DeallocatePacket(packet); - packet = receiver->Receive(); - } - - RakSleep(0); - - currentTime=GetTimeMS(); - } -} diff --git a/Tests/Integration/SessionConfigLiveTests.cpp b/Tests/Integration/SessionConfigLiveTests.cpp deleted file mode 100644 index 5c987359d..000000000 --- a/Tests/Integration/SessionConfigLiveTests.cpp +++ /dev/null @@ -1,1420 +0,0 @@ -/* - * Copyright (c) 2026, MafiaHub - * - * This source code is licensed under the MIT-style license found in the - * license.txt file in the root directory of this source tree. - */ - -#include - -#include -#include -#include - -#include "mafianet/peer.h" -#include "mafianet/peer_interface.h" -#include "mafianet/message_identifiers.h" -#include "mafianet/bit_stream.h" -#include "mafianet/sleep.h" -#include "mafianet/get_time.h" -#include "mafianet/native_feature_includes.h" -#include "mafianet/packet_logger.h" - -using namespace MafiaNet; - -/* -Description: -Tests the session handshake: an opaque payload exchanged in both directions after the transport -connection is up but BEFORE either side reports a connection. - -The load-bearing property is not that the payload arrives — it is that the connection packet is -withheld until it does. ID_CONNECTION_REQUEST_ACCEPTED means "the server's payload is in hand" and -ID_NEW_INCOMING_CONNECTION means "the client's payload is in hand", so an application cannot observe -a connection without its session data. - -Success conditions: -- Static mode: both peers read the other's payload the moment their connection packet surfaces. -- Interactive mode: the server sees ID_SESSION_CONFIG_REQUEST and NO connection is reported on - either side until it answers. -- AcceptSession completes the handshake and releases both connection packets. -- RejectSession produces ID_CONNECTION_ATTEMPT_FAILED on the client, carrying the reason, and no - connection is reported anywhere. -- An interactive server that never answers times the handshake out rather than hanging. - -Failure conditions: any of the above does not hold. -*/ - -namespace -{ - const int kConnectTimeoutMs = 15000; - // Shortened from the 10s default so the never-answered case does not dominate suite runtime. - const TimeMS kHandshakeTimeoutMs = 3000; - - // Message ids are printed by name so a timeout says which handshake step is missing rather than a - // bare number. BaseIDTOString() returns 0 for ids it does not know (user messages), so fall back. - std::string IdName(int id) - { -#if _RAKNET_SUPPORT_PacketLogger==1 - const char *name = PacketLogger::BaseIDTOString((unsigned char)id); -#else - const char *name = 0; // names come from PacketLogger, which this build compiled out -#endif - char buf[64]; - if (name) - snprintf(buf, sizeof(buf), "%s (%d)", name, id); - else - snprintf(buf, sizeof(buf), "id %d", id); - return std::string(buf); - } - - // Drain a peer, returning the first packet with the given id, or 0 if the deadline passes. - // Packets that are not the wanted id are discarded; both peers are pumped so a handshake that - // needs traffic from either side can make progress. - // - // The optional ledgers record the id of every packet this helper drains and throws away, from the - // waited-on peer and from alsoPump respectively. A negative assertion placed AFTER a wait cannot - // otherwise see those packets -- they were deallocated in here -- so a violation that surfaces - // early is swallowed by the very wait that precedes the assertion looking for it. Pass a ledger - // whenever a later assertion needs to know what already went by; see - // InteractiveAcceptGatesBothConnectionPackets. - Packet *PumpUntil(RakPeerInterface *wanted, int wantedId, RakPeerInterface *alsoPump, int timeoutMs, - std::vector *seenOnWanted = 0, std::vector *seenOnAlsoPump = 0) - { - TimeMS entry = GetTimeMS(); - while (GetTimeMS() - entry < (TimeMS)timeoutMs) - { - Packet *p; - for (p = wanted->Receive(); p; wanted->DeallocatePacket(p), p = wanted->Receive()) - { - if (p->data[0] == (unsigned char)wantedId) - return p; // caller deallocates - if (seenOnWanted) - seenOnWanted->push_back((int)p->data[0]); - } - if (alsoPump) - { - for (p = alsoPump->Receive(); p; alsoPump->DeallocatePacket(p), p = alsoPump->Receive()) - { - if (seenOnAlsoPump) - seenOnAlsoPump->push_back((int)p->data[0]); - } - } - RakSleep(15); - } - return 0; - } - - // Waits for one packet on each of two peers at once, for the stages where both sides are notified - // of the same event. Pumping them one at a time with PumpUntil() discards the other peer's packets - // while waiting on the first, so whichever notification lands first on the "wrong" peer is thrown - // away and the next wait times out. On success both packets are the caller's to deallocate. - // - // Returns an AssertionResult rather than a bool so a timeout reports WHICH side was still missing. - // Both out-params are nulled before returning, so the information is gone by the time the caller - // sees it; ASSERT_TRUE prints the message below ahead of the caller's own, and every call site - // already wraps this in ASSERT_TRUE, so nothing needed changing to gain it. - ::testing::AssertionResult PumpUntilBoth(RakPeerInterface *first, int firstId, Packet **firstOut, RakPeerInterface *second, int secondId, Packet **secondOut, int timeoutMs) - { - *firstOut = 0; - *secondOut = 0; - TimeMS entry = GetTimeMS(); - while (GetTimeMS() - entry < (TimeMS)timeoutMs) - { - Packet *p; - for (p = first->Receive(); p; p = first->Receive()) - { - if (*firstOut == 0 && p->data[0] == (unsigned char)firstId) - *firstOut = p; - else - first->DeallocatePacket(p); - } - for (p = second->Receive(); p; p = second->Receive()) - { - if (*secondOut == 0 && p->data[0] == (unsigned char)secondId) - *secondOut = p; - else - second->DeallocatePacket(p); - } - if (*firstOut && *secondOut) - return ::testing::AssertionSuccess(); - RakSleep(15); - } - const bool hadFirst = (*firstOut != 0); - const bool hadSecond = (*secondOut != 0); - if (*firstOut) - first->DeallocatePacket(*firstOut); - if (*secondOut) - second->DeallocatePacket(*secondOut); - *firstOut = 0; - *secondOut = 0; - - ::testing::AssertionResult failure = ::testing::AssertionFailure(); - failure << "timed out after " << timeoutMs << " ms waiting for " << IdName(firstId) - << " on the first peer and " << IdName(secondId) << " on the second: "; - if (!hadFirst && !hadSecond) - failure << "neither arrived"; - else if (!hadFirst) - failure << IdName(secondId) << " arrived, " << IdName(firstId) << " did not"; - else - failure << IdName(firstId) << " arrived, " << IdName(secondId) << " did not"; - return failure; - } - - // Drain everything a peer receives over a fixed window and keep the ids. - // - // Negative assertions must not be built out of successive PumpUntil() probes: that helper discards - // every packet that is not the one it is waiting for, so the first probe swallows the very packet a - // later probe looks for and the test then passes for the wrong reason. Collect once, assert after. - std::vector CollectIds(RakPeerInterface *wanted, RakPeerInterface *alsoPump, int windowMs) - { - std::vector ids; - TimeMS entry = GetTimeMS(); - while (GetTimeMS() - entry < (TimeMS)windowMs) - { - Packet *p; - for (p = wanted->Receive(); p; wanted->DeallocatePacket(p), p = wanted->Receive()) - ids.push_back((int)p->data[0]); - if (alsoPump) - { - for (p = alsoPump->Receive(); p; alsoPump->DeallocatePacket(p), p = alsoPump->Receive()) - ; - } - RakSleep(15); - } - return ids; - } - - // Drain BOTH peers over a window, appending every id each one yields to its own ledger. - // - // The single-peer CollectIds() above discards the other peer's packets, and two successive - // PumpUntil()/CollectIds() probes swallow each other's evidence for the same reason. A negative - // assertion about a PAIR of events -- neither peer may report a connection -- therefore has to - // drain both in one loop. Appends rather than assigns so a ledger already carrying what an earlier - // PumpUntil() drained keeps it. - void CollectIdsBoth(RakPeerInterface *a, std::vector *aIds, RakPeerInterface *b, std::vector *bIds, int windowMs) - { - TimeMS entry = GetTimeMS(); - while (GetTimeMS() - entry < (TimeMS)windowMs) - { - Packet *p; - for (p = a->Receive(); p; a->DeallocatePacket(p), p = a->Receive()) - aIds->push_back((int)p->data[0]); - for (p = b->Receive(); p; b->DeallocatePacket(p), p = b->Receive()) - bIds->push_back((int)p->data[0]); - RakSleep(15); - } - } - - bool Contains(const std::vector &ids, int id) - { - for (size_t i = 0; i < ids.size(); ++i) - { - if (ids[i] == id) - return true; - } - return false; - } - - // Injects application-layer bytes onto an established connection while bypassing the public Send() - // handshake gate, standing in for a peer that does not respect the protocol. - // - // Needed because the gates added for un-accepted peers mask each other between two conforming peers: - // the sender's broadcast filter and directed-send check mean nothing reaches the wire, so a negative - // assertion on the receiver holds whether or not the receiving-side check exists. Injecting below the - // gate is the only way to prove the receiving side actually rejects what it is sent. - // - // Never instantiated. RakPeer::SendBuffered is protected and a derived type is the only way to reach - // it, but the peer itself must still come from RakPeerInterface::GetInstance() so that it is - // allocated inside the library with the library's own layout. Constructing a RakPeer-derived object - // in this translation unit would size it with THIS unit's view of the class, and a mismatch there is - // silent memory corruption rather than a compile error. - // - // The derived type only names the member; the call goes through a pointer-to-member on the real - // RakPeer. Downcasting the peer to this type instead would be undefined behaviour (it is not one), - // and UBSan's vptr check aborts on it. - // - // SendBuffered rather than SendImmediate: it is the layer the public Send() drops into once its own - // checks pass, so injected traffic still crosses the network thread exactly like real traffic. - class SendGateBypass : public RakPeer - { - public: - static void Inject(RakPeerInterface *peer, const MafiaNet::BitStream &bs, const AddressOrGUID &target) - { - (static_cast(peer)->*&SendGateBypass::SendBuffered)( - (const char *)bs.GetData(), bs.GetNumberOfBitsUsed(), MafiaNet::Priority::Immediate, - MafiaNet::Reliability::ReliableOrdered, 0, target, false, - RemoteSystemStruct::NO_ACTION, 0); - } - }; - - // Reads the server's own view of a connection slot. - // - // Needed because static-mode gating has no observable signature from outside the library. The - // server releases its withheld ID_NEW_INCOMING_CONNECTION in the same step that it answers the - // client's request, so a peer that wrongly released it early still looks correct to a polling test: - // by the time the poll comes round, the payload it should have waited for has arrived anyway. The - // invariant only holds still on the slot itself. - // - // Never instantiated, and the peer must still come from RakPeerInterface::GetInstance() -- see - // SendGateBypass above for why, and for why members are read through pointers-to-member rather than - // by downcasting. - class SlotInspector : public RakPeer - { - public: - struct Snapshot - { - bool sawSlot; - bool reported; - unsigned int payloadLength; - }; - - // The tests using this have exactly one remote, so the first active slot is it. - static Snapshot FirstActiveSlot(RakPeerInterface *peer) - { - const RakPeer *rakPeer = static_cast(peer); - const RemoteSystemStruct *slots = rakPeer->*&SlotInspector::remoteSystemList; - const unsigned int slotCount = rakPeer->*&SlotInspector::maximumNumberOfPeers; - Snapshot out; - out.sawSlot = false; - out.reported = false; - out.payloadLength = 0; - if (slots == 0) - return out; - for (unsigned int i = 0; i < slotCount; ++i) - { - if (slots[i].isActive == false) - continue; - out.sawSlot = true; - out.reported = slots[i].connectionReportedToApplication; - out.payloadLength = slots[i].remoteSessionConfigLength; - return out; - } - return out; - } - }; - - class SessionConfigLive : public ::testing::Test - { - public: - void SetUp() override - { - server = RakPeerInterface::GetInstance(); - client = RakPeerInterface::GetInstance(); - ASSERT_NE(server, nullptr); - ASSERT_NE(client, nullptr); - } - - // A third peer for tests that need one. Fixture-owned rather than local, so a failed ASSERT_ - // still destroys it -- a leaked peer keeps a socket bound and a network thread alive for the rest - // of the process, which in a serial suite shows up as an unrelated later test failing to bind. - RakPeerInterface *MakeExtraPeer() - { - EXPECT_EQ(extra, nullptr) << "only one extra peer is supported"; - extra = RakPeerInterface::GetInstance(); - return extra; - } - - void TearDown() override - { - if (extra) - { - extra->Shutdown(100); - RakPeerInterface::DestroyInstance(extra); - extra = 0; - } - if (client) - { - client->Shutdown(100); - RakPeerInterface::DestroyInstance(client); - client = 0; - } - if (server) - { - server->Shutdown(100); - RakPeerInterface::DestroyInstance(server); - server = 0; - } - } - - // Start both peers on OS-assigned ports and return the server's port. - unsigned short StartPeers() - { - SocketDescriptor serverSd(0, "127.0.0.1"); - EXPECT_EQ(server->Startup(8, &serverSd, 1), RAKNET_STARTED); - server->SetMaximumIncomingConnections(8); - server->SetTimeoutTime(kHandshakeTimeoutMs, UNASSIGNED_SYSTEM_ADDRESS); - - SocketDescriptor clientSd(0, "127.0.0.1"); - EXPECT_EQ(client->Startup(1, &clientSd, 1), RAKNET_STARTED); - client->SetTimeoutTime(kHandshakeTimeoutMs, UNASSIGNED_SYSTEM_ADDRESS); - - return server->GetInternalID(UNASSIGNED_SYSTEM_ADDRESS).GetPort(); - } - - RakPeerInterface *server = 0; - RakPeerInterface *client = 0; - RakPeerInterface *extra = 0; - }; - - const char kServerPayload[] = "{\"season\":\"winter\",\"map_file\":\"downtown.m2map\"}"; - const char kClientPayload[] = "{\"build\":\"m2o|1.2.3\"}"; -} // namespace - -// Static mode: no application code beyond SetSessionConfig. Each peer must be able to read the -// other's payload at the instant its connection packet surfaces. -TEST_F(SessionConfigLive, StaticExchangeDeliversBothPayloads) -{ - server->SetSessionConfig(kServerPayload, (unsigned int)strlen(kServerPayload)); - client->SetSessionConfig(kClientPayload, (unsigned int)strlen(kClientPayload)); - - const unsigned short port = StartPeers(); - ASSERT_EQ(client->Connect("127.0.0.1", port, 0, 0), CONNECTION_ATTEMPT_STARTED); - - // Both waits have to run in the same loop. In static mode the server releases its withheld - // ID_NEW_INCOMING_CONNECTION in the same step that it sends ID_SESSION_CONFIG, so it is queued - // strictly BEFORE the client turns that message into ID_CONNECTION_REQUEST_ACCEPTED. Waiting on - // the client first with PumpUntil() discards whatever the server has queued meanwhile, so any - // scheduling gap between the two throws ID_NEW_INCOMING_CONNECTION away and the second wait then - // burns its full deadline. - Packet *accepted = 0; - Packet *incoming = 0; - ASSERT_TRUE(PumpUntilBoth(client, ID_CONNECTION_REQUEST_ACCEPTED, &accepted, server, ID_NEW_INCOMING_CONNECTION, &incoming, kConnectTimeoutMs)) - << "the connection was not reported on both sides"; - const RakNetGUID serverGuid = accepted->guid; - const RakNetGUID clientGuid = incoming->guid; - client->DeallocatePacket(accepted); - server->DeallocatePacket(incoming); - - // The payload must already be readable — that is the whole contract of the withheld packet. - unsigned int length = 0; - const char *fromServer = client->GetRemoteSessionConfig(serverGuid, &length); - ASSERT_NE(fromServer, nullptr); - ASSERT_EQ(length, (unsigned int)strlen(kServerPayload)); - EXPECT_EQ(memcmp(fromServer, kServerPayload, length), 0); - - length = 0; - const char *fromClient = server->GetRemoteSessionConfig(clientGuid, &length); - ASSERT_NE(fromClient, nullptr); - ASSERT_EQ(length, (unsigned int)strlen(kClientPayload)); - EXPECT_EQ(memcmp(fromClient, kClientPayload, length), 0); -} - -// Static-mode gating. The interactive tests prove the connection packet is withheld while the -// APPLICATION decides; this proves it is withheld while the library waits for the client's payload, -// which is a different trigger reaching the same release point. -// -// Why it is written against the slot rather than against received packets: in static mode the server -// answers the request the instant it arrives, so correct code and code that released the packet at -// transport-up are indistinguishable to a poll -- both have the payload in hand by the time the test -// looks. The assertion is therefore the slot invariant "reported implies payload stored", sampled -// continuously, and the client's payload is deliberately large enough to need many datagrams so the -// handshake spends a long, reliably observable time in the gated state. -// -// A mutant that releases the packet at transport-up in static mode only (leaving the interactive path -// gated) passes all 23 other tests in this suite and fails this one. -TEST_F(SessionConfigLive, StaticModeWithholdsTheConnectionUntilTheClientPayloadArrives) -{ - // Large enough to split across many datagrams, so the gated window is milliseconds rather than - // microseconds. Distinctive bytes so a truncated or mixed-up payload fails the length check below. - std::string bigPayload; - bigPayload.reserve(48000); - while (bigPayload.size() < 48000) - bigPayload.push_back((char)('a' + (int)(bigPayload.size() % 26))); - - server->SetSessionConfig(kServerPayload, (unsigned int)strlen(kServerPayload)); - client->SetSessionConfig(bigPayload.data(), (unsigned int)bigPayload.size()); - - const unsigned short port = StartPeers(); - ASSERT_EQ(client->Connect("127.0.0.1", port, 0, 0), CONNECTION_ATTEMPT_STARTED); - - // Sample the server's slot as tightly as possible for the whole handshake. The invariant must hold - // at every single sample: the connection is never reported to the application while the client's - // payload is still missing. - bool sawGatedState = false; - unsigned int violations = 0; - bool reportedWithPayload = false; - TimeMS entry = GetTimeMS(); - while (GetTimeMS() - entry < (TimeMS)kConnectTimeoutMs) - { - SlotInspector::Snapshot snap = SlotInspector::FirstActiveSlot(server); - if (snap.sawSlot) - { - if (snap.reported == false && snap.payloadLength == 0) - sawGatedState = true; // the window this test exists to observe - if (snap.reported && snap.payloadLength == 0) - ++violations; - if (snap.reported && snap.payloadLength == bigPayload.size()) - reportedWithPayload = true; - } - if (reportedWithPayload) - break; - // No sleep: the gated window is short and every sample counts. - } - - EXPECT_EQ(violations, 0u) - << "the server reported the connection to the application " << violations - << " times while the client's session payload was still missing"; - EXPECT_TRUE(sawGatedState) - << "never observed the handshake in its gated state, so this test proved nothing"; - ASSERT_TRUE(reportedWithPayload) - << "the server never reported the connection with the client's payload in hand"; - - // And the payload really did survive the split and reassembly. - Packet *incoming = PumpUntil(server, ID_NEW_INCOMING_CONNECTION, client, kConnectTimeoutMs); - ASSERT_NE(incoming, nullptr); - const RakNetGUID clientGuid = incoming->guid; - server->DeallocatePacket(incoming); - - unsigned int length = 0; - const char *fromClient = server->GetRemoteSessionConfig(clientGuid, &length); - ASSERT_NE(fromClient, nullptr); - ASSERT_EQ(length, (unsigned int)bigPayload.size()); - EXPECT_EQ(memcmp(fromClient, bigPayload.data(), length), 0); -} - -// A peer that configures no payload still completes the handshake; the remote simply reads none. -TEST_F(SessionConfigLive, EmptyPayloadsStillConnect) -{ - const unsigned short port = StartPeers(); - ASSERT_EQ(client->Connect("127.0.0.1", port, 0, 0), CONNECTION_ATTEMPT_STARTED); - - // Same ordering hazard as StaticExchangeDeliversBothPayloads: the handshake still runs with empty - // payloads, so the server's connection packet is queued before the client's and must not be - // discarded by a one-sided wait. - Packet *accepted = 0; - Packet *incoming = 0; - ASSERT_TRUE(PumpUntilBoth(client, ID_CONNECTION_REQUEST_ACCEPTED, &accepted, server, ID_NEW_INCOMING_CONNECTION, &incoming, kConnectTimeoutMs)) - << "the connection was not reported on both sides"; - const RakNetGUID serverGuid = accepted->guid; - client->DeallocatePacket(accepted); - server->DeallocatePacket(incoming); - - unsigned int length = 12345; - client->GetRemoteSessionConfig(serverGuid, &length); - EXPECT_EQ(length, 0u); -} - -// The load-bearing assertion. In interactive mode the server holds the decision, so neither peer may -// report a connection until it answers — proving the connection packets really are gated on the -// exchange and not merely racing it. -TEST_F(SessionConfigLive, InteractiveAcceptGatesBothConnectionPackets) -{ - server->SetSessionConfigInteractive(true); - client->SetSessionConfig(kClientPayload, (unsigned int)strlen(kClientPayload)); - - const unsigned short port = StartPeers(); - ASSERT_EQ(client->Connect("127.0.0.1", port, 0, 0), CONNECTION_ATTEMPT_STARTED); - - // Ledgers, because the wait below drains and frees everything that is not the session request -- - // including an early connection packet, which is precisely the violation this test exists to - // catch. Asserting only over a window that starts after the wait cannot see it. - std::vector serverSaw; - std::vector clientSaw; - Packet *request = PumpUntil(server, ID_SESSION_CONFIG_REQUEST, client, kConnectTimeoutMs, &serverSaw, &clientSaw); - ASSERT_NE(request, nullptr) << "server never saw the session request"; - const RakNetGUID clientGuid = request->guid; - - // The client's payload rides the request itself. - ASSERT_GT(request->length, (unsigned int)1); - EXPECT_EQ(memcmp(request->data + 1, kClientPayload, strlen(kClientPayload)), 0); - server->DeallocatePacket(request); - - // Nothing may be reported while the decision is outstanding. Both peers are drained in one loop - // and both ledgers carry forward what the wait above already consumed, so neither assertion can - // pass because the other probe -- or the wait -- swallowed its evidence. - CollectIdsBoth(server, &serverSaw, client, &clientSaw, 400); - EXPECT_FALSE(Contains(serverSaw, ID_NEW_INCOMING_CONNECTION)) - << "server reported a connection before answering the session request"; - EXPECT_FALSE(Contains(clientSaw, ID_CONNECTION_REQUEST_ACCEPTED)) - << "client reported a connection before the server answered"; - - server->AcceptSession(clientGuid, kServerPayload, (unsigned int)strlen(kServerPayload)); - - Packet *incoming = PumpUntil(server, ID_NEW_INCOMING_CONNECTION, client, kConnectTimeoutMs); - ASSERT_NE(incoming, nullptr) << "AcceptSession did not release the server's connection packet"; - server->DeallocatePacket(incoming); - - Packet *accepted = PumpUntil(client, ID_CONNECTION_REQUEST_ACCEPTED, server, kConnectTimeoutMs); - ASSERT_NE(accepted, nullptr) << "AcceptSession did not release the client's connection packet"; - const RakNetGUID serverGuid = accepted->guid; - client->DeallocatePacket(accepted); - - unsigned int length = 0; - const char *fromServer = client->GetRemoteSessionConfig(serverGuid, &length); - ASSERT_NE(fromServer, nullptr); - ASSERT_EQ(length, (unsigned int)strlen(kServerPayload)); - EXPECT_EQ(memcmp(fromServer, kServerPayload, length), 0); -} - -// A rejected peer must never see a connection, and must be told why. -TEST_F(SessionConfigLive, InteractiveRejectFailsTheConnectionAttempt) -{ - server->SetSessionConfigInteractive(true); - - const unsigned short port = StartPeers(); - ASSERT_EQ(client->Connect("127.0.0.1", port, 0, 0), CONNECTION_ATTEMPT_STARTED); - - Packet *request = PumpUntil(server, ID_SESSION_CONFIG_REQUEST, client, kConnectTimeoutMs); - ASSERT_NE(request, nullptr); - const RakNetGUID clientGuid = request->guid; - server->DeallocatePacket(request); - - const char reason[] = "build mismatch"; - server->RejectSession(clientGuid, reason); - - Packet *failed = PumpUntil(client, ID_CONNECTION_ATTEMPT_FAILED, server, kConnectTimeoutMs); - ASSERT_NE(failed, nullptr) << "rejected client was never told the attempt failed"; - // The reason rides after the id, matching the ID_DISCONNECTION_NOTIFICATION convention. - ASSERT_EQ(failed->length, (unsigned int)(1 + strlen(reason))); - EXPECT_EQ(memcmp(failed->data + 1, reason, strlen(reason)), 0); - client->DeallocatePacket(failed); - - // Collect everything the server sees for long enough that the DISCONNECT_ON_NO_ACK teardown has - // completed -- the refusal is sent reliably, so the slot closes once the client acks it. The server - // was never told this peer connected, so it must be told nothing at all about it going away: no - // ID_NEW_INCOMING_CONNECTION, and equally no close notification for a connection that never was. - const std::vector serverSaw = CollectIds(server, client, 3000); - - EXPECT_FALSE(Contains(serverSaw, ID_NEW_INCOMING_CONNECTION)) - << "server reported a connection it had rejected"; - EXPECT_FALSE(Contains(serverSaw, ID_DISCONNECTION_NOTIFICATION)) - << "server was notified of a disconnect for a connection it never reported"; - EXPECT_FALSE(Contains(serverSaw, ID_CONNECTION_LOST)) - << "server was notified of a lost connection it never reported"; - EXPECT_FALSE(Contains(serverSaw, ID_CONNECTION_ATTEMPT_FAILED)) - << "server was told an inbound connection attempt failed; that is a connecting-side message"; -} - -// An interactive server that never answers must not pin the slot or hang the client: acks keep -// flowing during the exchange, so the ordinary dead-connection detection would never fire. -TEST_F(SessionConfigLive, UnansweredHandshakeTimesOut) -{ - server->SetSessionConfigInteractive(true); - - const unsigned short port = StartPeers(); - ASSERT_EQ(client->Connect("127.0.0.1", port, 0, 0), CONNECTION_ATTEMPT_STARTED); - - Packet *request = PumpUntil(server, ID_SESSION_CONFIG_REQUEST, client, kConnectTimeoutMs); - ASSERT_NE(request, nullptr); - server->DeallocatePacket(request); - - // Deliberately never answer. - Packet *failed = PumpUntil(client, ID_CONNECTION_ATTEMPT_FAILED, server, (int)kHandshakeTimeoutMs * 4); - ASSERT_NE(failed, nullptr) << "an unanswered session handshake never timed out"; - client->DeallocatePacket(failed); - - EXPECT_NE(client->GetConnectionState(server->GetInternalID(UNASSIGNED_SYSTEM_ADDRESS)), IS_CONNECTED); -} - -// Role binding: the session-handshake replies are server->client messages, so a malicious client -// must not be able to push them at a listening server. ID_SESSION_CONFIG_REJECTED is the dangerous -// one -- unbound, it lets a client inject ID_CONNECTION_ATTEMPT_FAILED (a packet an application only -// ever expects for its OWN outbound connects) into the server's receive queue. -TEST_F(SessionConfigLive, ServerIgnoresSessionRepliesSentByAClient) -{ - // The forged replies go through SendGateBypass deliberately. A conforming client cannot send these - // at all -- its own broadcast filter and directed-send gate stop them before the wire -- so driving - // this through the public Send() would make every assertion below hold even with the server-side - // role checks deleted. What is under test is the SERVER's rejection, so the client has to transmit. - server->SetSessionConfigInteractive(true); - - const unsigned short port = StartPeers(); - ASSERT_EQ(client->Connect("127.0.0.1", port, 0, 0), CONNECTION_ATTEMPT_STARTED); - - Packet *request = PumpUntil(server, ID_SESSION_CONFIG_REQUEST, client, kConnectTimeoutMs); - ASSERT_NE(request, nullptr); - const RakNetGUID clientGuid = request->guid; - server->DeallocatePacket(request); - // The server is parked in EXCHANGING_SESSION_DATA awaiting a decision. Forge both server-to-client - // replies back at it, in the wrong direction. - - // The client holds no packet carrying the server's guid -- ID_CONNECTION_REQUEST_ACCEPTED is - // withheld precisely because the handshake has not finished -- so address the server by endpoint. - SystemAddress serverAddr; - serverAddr.SetBinaryAddress("127.0.0.1"); - serverAddr.SetPortHostOrder(port); - - const char reason[] = "spoofed"; - MafiaNet::BitStream rejectBs; - rejectBs.Write((MessageID)ID_SESSION_CONFIG_REJECTED); - rejectBs.Write(reason, (unsigned int)strlen(reason)); - SendGateBypass::Inject(client, rejectBs, serverAddr); - - MafiaNet::BitStream configBs; - configBs.Write((MessageID)ID_SESSION_CONFIG); - configBs.Write("spoofed-config", 14); - SendGateBypass::Inject(client, configBs, serverAddr); - - const std::vector serverSaw = CollectIds(server, client, 1500); - - // ID_SESSION_CONFIG_REJECTED is the dangerous one: unbound, it becomes ID_CONNECTION_ATTEMPT_FAILED - // on the receiver, a packet an application only ever expects for its OWN outbound connects. - EXPECT_FALSE(Contains(serverSaw, ID_CONNECTION_ATTEMPT_FAILED)) - << "a client forged ID_CONNECTION_ATTEMPT_FAILED into a listening server's queue"; - EXPECT_FALSE(Contains(serverSaw, ID_NEW_INCOMING_CONNECTION)) - << "a client completed the server's half of the handshake by replying to itself"; - EXPECT_FALSE(Contains(serverSaw, ID_DISCONNECTION_NOTIFICATION)) - << "the forged replies tore down a connection that was never reported"; - - // The legitimate path must still work afterwards. - server->AcceptSession(clientGuid, kServerPayload, (unsigned int)strlen(kServerPayload)); - - Packet *incoming = PumpUntil(server, ID_NEW_INCOMING_CONNECTION, client, kConnectTimeoutMs); - ASSERT_NE(incoming, nullptr) << "the spoof attempt broke the real handshake"; - server->DeallocatePacket(incoming); -} - -// The stored payload is arbitrary attacker-controlled bytes. It is kept NUL-terminated past the -// reported length so an application that reaches for a C-string API cannot run off the buffer. -TEST_F(SessionConfigLive, RemotePayloadIsNulTerminatedPastItsLength) -{ - const char payload[] = "no-trailing-nul"; - server->SetSessionConfig(payload, (unsigned int)strlen(payload)); - - const unsigned short port = StartPeers(); - ASSERT_EQ(client->Connect("127.0.0.1", port, 0, 0), CONNECTION_ATTEMPT_STARTED); - - Packet *accepted = PumpUntil(client, ID_CONNECTION_REQUEST_ACCEPTED, server, kConnectTimeoutMs); - ASSERT_NE(accepted, nullptr); - const RakNetGUID serverGuid = accepted->guid; - client->DeallocatePacket(accepted); - - unsigned int length = 0; - const char *config = client->GetRemoteSessionConfig(serverGuid, &length); - ASSERT_NE(config, nullptr); - ASSERT_EQ(length, (unsigned int)strlen(payload)); - // The terminator is past the reported length, so it never changes what the length means. - EXPECT_EQ(config[length], '\0'); - EXPECT_EQ(strlen(config), (size_t)length); -} - -// Admission control must see peers that are still running the session handshake. They already own a -// slot, so counting only CONNECTED peers would let clients that stall the handshake push the real -// total past SetMaximumIncomingConnections() -- and do it invisibly, since the application is never -// told those peers exist. -TEST_F(SessionConfigLive, StalledHandshakeStillConsumesAnIncomingSlot) -{ - server->SetSessionConfigInteractive(true); // never answered, so the first client parks mid-handshake - - SocketDescriptor serverSd(0, "127.0.0.1"); - ASSERT_EQ(server->Startup(8, &serverSd, 1), RAKNET_STARTED); - server->SetMaximumIncomingConnections(1); // exactly one incoming slot - server->SetTimeoutTime(60000, UNASSIGNED_SYSTEM_ADDRESS); // outlive the test, so the stall persists - - SocketDescriptor clientSd(0, "127.0.0.1"); - ASSERT_EQ(client->Startup(1, &clientSd, 1), RAKNET_STARTED); - - const unsigned short port = server->GetInternalID(UNASSIGNED_SYSTEM_ADDRESS).GetPort(); - ASSERT_EQ(client->Connect("127.0.0.1", port, 0, 0), CONNECTION_ATTEMPT_STARTED); - - Packet *request = PumpUntil(server, ID_SESSION_CONFIG_REQUEST, client, kConnectTimeoutMs); - ASSERT_NE(request, nullptr); - server->DeallocatePacket(request); - // The first client now occupies the only slot while parked in EXCHANGING_SESSION_DATA. - - RakPeerInterface *second = MakeExtraPeer(); - ASSERT_NE(second, nullptr); - SocketDescriptor secondSd(0, "127.0.0.1"); - ASSERT_EQ(second->Startup(1, &secondSd, 1), RAKNET_STARTED); - ASSERT_EQ(second->Connect("127.0.0.1", port, 0, 0), CONNECTION_ATTEMPT_STARTED); - - Packet *full = PumpUntil(second, ID_NO_FREE_INCOMING_CONNECTIONS, server, kConnectTimeoutMs); - ASSERT_NE(full, nullptr) << "a peer stalled in the session handshake did not count against the incoming limit"; - second->DeallocatePacket(full); -} - -namespace -{ - const int kUserMessageId = ID_USER_PACKET_ENUM + 1; - - // Drive a complete connection lifecycle and assert every stage behaves identically whether or not a - // session payload is configured. The handshake sits in front of everything else a peer does, so the - // question is not only "does the payload arrive" but "does normal traffic, teardown and reconnect - // still work with it in the path". - // - // The reconnect leg is the load-bearing one: it reuses the server's slot, so a payload that outlived - // its connection, or session state that was not cleared on teardown, shows up here and nowhere else. - // - // The parameter is a plain bool rather than a struct so ctest renders a readable test name; gtest - // appends the printed parameter to the discovered name, and a struct prints as "1-byte object <00>". -} // namespace - -class SessionConfigPipeline : public SessionConfigLive, public ::testing::WithParamInterface -{ -}; - -TEST_P(SessionConfigPipeline, FullConnectionLifecycleBehavesIdentically) -{ - const bool withConfig = GetParam(); - const unsigned int expectedServerLen = withConfig ? (unsigned int)strlen(kServerPayload) : 0u; - const unsigned int expectedClientLen = withConfig ? (unsigned int)strlen(kClientPayload) : 0u; - - if (withConfig) - { - server->SetSessionConfig(kServerPayload, (unsigned int)strlen(kServerPayload)); - client->SetSessionConfig(kClientPayload, (unsigned int)strlen(kClientPayload)); - } - - const unsigned short port = StartPeers(); - ASSERT_EQ(client->Connect("127.0.0.1", port, 0, 0), CONNECTION_ATTEMPT_STARTED); - - // ---- stage 1: both sides report the connection ------------------------------------------------- - Packet *accepted = 0; - Packet *incoming = 0; - ASSERT_TRUE(PumpUntilBoth(client, ID_CONNECTION_REQUEST_ACCEPTED, &accepted, server, ID_NEW_INCOMING_CONNECTION, &incoming, kConnectTimeoutMs)) << "the connection was not reported on both sides"; - const RakNetGUID serverGuid = accepted->guid; - client->DeallocatePacket(accepted); - - const RakNetGUID clientGuid = incoming->guid; - server->DeallocatePacket(incoming); - - unsigned int length = 12345; - client->GetRemoteSessionConfig(serverGuid, &length); - EXPECT_EQ(length, expectedServerLen); - length = 12345; - server->GetRemoteSessionConfig(clientGuid, &length); - EXPECT_EQ(length, expectedClientLen); - - EXPECT_EQ(client->GetConnectionState(serverGuid), IS_CONNECTED); - EXPECT_EQ(server->GetConnectionState(clientGuid), IS_CONNECTED); - - // ---- stage 2: ordinary traffic flows both ways ------------------------------------------------- - { - MafiaNet::BitStream up; - up.Write((MessageID)kUserMessageId); - up.Write("client-to-server", 16); - client->Send(&up, MafiaNet::Priority::High, MafiaNet::Reliability::ReliableOrdered, 0, UNASSIGNED_SYSTEM_ADDRESS, true); - - Packet *got = PumpUntil(server, kUserMessageId, client, kConnectTimeoutMs); - ASSERT_NE(got, nullptr) << "server never received client traffic"; - ASSERT_EQ(got->length, (unsigned int)(1 + 16)); - EXPECT_EQ(memcmp(got->data + 1, "client-to-server", 16), 0); - server->DeallocatePacket(got); - } - { - MafiaNet::BitStream down; - down.Write((MessageID)kUserMessageId); - down.Write("server-to-client", 16); - server->Send(&down, MafiaNet::Priority::High, MafiaNet::Reliability::ReliableOrdered, 0, UNASSIGNED_SYSTEM_ADDRESS, true); - - Packet *got = PumpUntil(client, kUserMessageId, server, kConnectTimeoutMs); - ASSERT_NE(got, nullptr) << "client never received server traffic"; - ASSERT_EQ(got->length, (unsigned int)(1 + 16)); - EXPECT_EQ(memcmp(got->data + 1, "server-to-client", 16), 0); - client->DeallocatePacket(got); - } - - // ---- stage 3: clean teardown still notifies ---------------------------------------------------- - // This connection WAS reported, so unlike a rejected peer it must produce a disconnect notification. - client->CloseConnection(serverGuid, true); - - Packet *bye = PumpUntil(server, ID_DISCONNECTION_NOTIFICATION, client, kConnectTimeoutMs); - ASSERT_NE(bye, nullptr) << "server was not notified of a clean disconnect"; - server->DeallocatePacket(bye); - - // ---- stage 4: reconnect over the reused slot --------------------------------------------------- - // CloseConnection is asynchronous on the closing side as well, so the local slot is still occupied - // for a moment after the notification reaches the peer. Reconnecting before it frees returns - // ALREADY_CONNECTED_TO_ENDPOINT; wait for the teardown rather than racing it. - { - TimeMS entry = GetTimeMS(); - while (GetTimeMS() - entry < (TimeMS)kConnectTimeoutMs) - { - const ConnectionState state = client->GetConnectionState(serverGuid); - if (state == IS_NOT_CONNECTED || state == IS_DISCONNECTED) - break; - Packet *drain; - for (drain = client->Receive(); drain; client->DeallocatePacket(drain), drain = client->Receive()) - ; - for (drain = server->Receive(); drain; server->DeallocatePacket(drain), drain = server->Receive()) - ; - RakSleep(15); - } - } - - ASSERT_EQ(client->Connect("127.0.0.1", port, 0, 0), CONNECTION_ATTEMPT_STARTED); - - Packet *accepted2 = 0; - Packet *incoming2 = 0; - ASSERT_TRUE(PumpUntilBoth(client, ID_CONNECTION_REQUEST_ACCEPTED, &accepted2, server, ID_NEW_INCOMING_CONNECTION, &incoming2, kConnectTimeoutMs)) << "the reconnection was not reported on both sides"; - const RakNetGUID serverGuid2 = accepted2->guid; - client->DeallocatePacket(accepted2); - - const RakNetGUID clientGuid2 = incoming2->guid; - server->DeallocatePacket(incoming2); - - // The payload must be freshly delivered for the new connection -- neither stale from the previous - // one nor lost because teardown cleared it and nothing repopulated it. - length = 12345; - const char *serverCfg = client->GetRemoteSessionConfig(serverGuid2, &length); - EXPECT_EQ(length, expectedServerLen) << "server payload wrong after reconnect"; - if (withConfig) - { - ASSERT_NE(serverCfg, nullptr); - EXPECT_EQ(memcmp(serverCfg, kServerPayload, expectedServerLen), 0); - } - - length = 12345; - const char *clientCfg = server->GetRemoteSessionConfig(clientGuid2, &length); - EXPECT_EQ(length, expectedClientLen) << "client payload wrong after reconnect"; - if (withConfig) - { - ASSERT_NE(clientCfg, nullptr); - EXPECT_EQ(memcmp(clientCfg, kClientPayload, expectedClientLen), 0); - } - - // ---- stage 5: traffic still flows on the reconnected session ----------------------------------- - MafiaNet::BitStream again; - again.Write((MessageID)kUserMessageId); - again.Write("after-reconnect!", 16); - client->Send(&again, MafiaNet::Priority::High, MafiaNet::Reliability::ReliableOrdered, 0, UNASSIGNED_SYSTEM_ADDRESS, true); - - Packet *got = PumpUntil(server, kUserMessageId, client, kConnectTimeoutMs); - ASSERT_NE(got, nullptr) << "traffic did not flow after reconnect"; - EXPECT_EQ(memcmp(got->data + 1, "after-reconnect!", 16), 0); - server->DeallocatePacket(got); -} - -INSTANTIATE_TEST_SUITE_P(WithAndWithoutSessionConfig, SessionConfigPipeline, - ::testing::Values(false, true), - [](const ::testing::TestParamInfo &info) { - return info.param ? "WithSessionConfig" : "WithoutSessionConfig"; - }); - -// Application traffic must not cross a connection the application has not been told about. -// -// Three defences, and they are NOT equally testable, so be precise about what this asserts: -// -// directed send - Send() to an un-accepted peer returns 0. Directly observable, asserted below. -// broadcast - the fan-out skips peers mid-handshake. -// inbound - application data from a peer mid-handshake is dropped instead of delivered. -// -// The last two cannot be exercised against each other by two conforming peers: the sender's -// broadcast filter means nothing reaches the wire, and even if it did the receiver's inbound drop -// would discard it. Each masks the other, so an assertion on either would pass vacuously whether or -// not the code is present -- which is worse than no assertion, because it reads like coverage. They -// are wire-level defences against a peer that does NOT respect the protocol (an old client, a custom -// implementation, an attacker), which this harness cannot synthesise. Asserted here instead: the -// state the connection is in, that no connection is reported, and that everything works after accept. -TEST_F(SessionConfigLive, ApplicationTrafficIsBlockedUntilTheHandshakeCompletes) -{ - server->SetSessionConfigInteractive(true); - - const unsigned short port = StartPeers(); - ASSERT_EQ(client->Connect("127.0.0.1", port, 0, 0), CONNECTION_ATTEMPT_STARTED); - - Packet *request = PumpUntil(server, ID_SESSION_CONFIG_REQUEST, client, kConnectTimeoutMs); - ASSERT_NE(request, nullptr); - const RakNetGUID clientGuid = request->guid; - server->DeallocatePacket(request); - // Both peers are now parked in EXCHANGING_SESSION_DATA awaiting the server's decision. - - EXPECT_EQ(server->GetConnectionState(clientGuid), IS_CONNECTING) - << "a peer mid-handshake must not read as connected"; - - // The application only holds this guid because interactive mode surfaced the request to it. Sending - // to it would hand data to a peer it has not accepted and may still refuse. - MafiaNet::BitStream world; - world.Write((MessageID)kUserMessageId); - world.Write("world-state-leak", 16); - EXPECT_EQ(server->Send(&world, MafiaNet::Priority::High, MafiaNet::Reliability::ReliableOrdered, 0, clientGuid, false), 0u) - << "a directed send reached a peer the application has not accepted"; - - const std::vector serverSaw = CollectIds(server, client, 700); - EXPECT_FALSE(Contains(serverSaw, ID_NEW_INCOMING_CONNECTION)) - << "server reported a connection before answering the session request"; - const std::vector clientSaw = CollectIds(client, server, 400); - EXPECT_FALSE(Contains(clientSaw, ID_CONNECTION_REQUEST_ACCEPTED)) - << "client reported a connection before the handshake completed"; - - // ---- once accepted, both directions work normally ---------------------------------------------- - server->AcceptSession(clientGuid, kServerPayload, (unsigned int)strlen(kServerPayload)); - - Packet *incoming = PumpUntil(server, ID_NEW_INCOMING_CONNECTION, client, kConnectTimeoutMs); - ASSERT_NE(incoming, nullptr); - server->DeallocatePacket(incoming); - Packet *accepted = PumpUntil(client, ID_CONNECTION_REQUEST_ACCEPTED, server, kConnectTimeoutMs); - ASSERT_NE(accepted, nullptr); - client->DeallocatePacket(accepted); - - // The same directed send that was refused a moment ago must now be accepted. - MafiaNet::BitStream now; - now.Write((MessageID)kUserMessageId); - now.Write("after-accept-ok!", 16); - EXPECT_NE(server->Send(&now, MafiaNet::Priority::High, MafiaNet::Reliability::ReliableOrdered, 0, clientGuid, false), 0u) - << "a directed send was still refused after the handshake completed"; - - Packet *got = PumpUntil(client, kUserMessageId, server, kConnectTimeoutMs); - ASSERT_NE(got, nullptr) << "traffic did not flow after the handshake completed"; - EXPECT_EQ(memcmp(got->data + 1, "after-accept-ok!", 16), 0); - client->DeallocatePacket(got); -} - -// The receiving half of the traffic gate, provable only with the non-conforming injector: a peer that -// ignores its own send gate and pushes application data during the handshake must not have it -// delivered, because the application has not been told that connection exists. -// -// The sending half -- the broadcast fan-out skipping such peers -- has no equivalent test. Proving it -// means observing bytes on the wire, and any receiver capable of observing them also runs the inbound -// drop asserted here. It stays as defence for the case that matters, which is bytes leaving the -// machine toward a peer the application never accepted and may still refuse. -TEST_F(SessionConfigLive, InboundApplicationDataDuringHandshakeIsNotDelivered) -{ - // The receiving half of the traffic gate, provable only by bypassing the sender's own gate: a peer - // that pushes application data during the handshake must not have it delivered, because the - // application has not been told that connection exists. - // - // The sending half -- the broadcast fan-out skipping such peers -- has no equivalent test. Proving - // it means observing bytes on the wire, and any receiver able to observe them also runs the inbound - // drop asserted here. It stays as defence for the case that matters: bytes leaving the machine - // toward a peer the application never accepted and may still refuse. - server->SetSessionConfigInteractive(true); - - const unsigned short port = StartPeers(); - ASSERT_EQ(client->Connect("127.0.0.1", port, 0, 0), CONNECTION_ATTEMPT_STARTED); - - Packet *request = PumpUntil(server, ID_SESSION_CONFIG_REQUEST, client, kConnectTimeoutMs); - ASSERT_NE(request, nullptr); - const RakNetGUID clientGuid = request->guid; - server->DeallocatePacket(request); - - SystemAddress serverAddr; - serverAddr.SetBinaryAddress("127.0.0.1"); - serverAddr.SetPortHostOrder(port); - - MafiaNet::BitStream early; - early.Write((MessageID)kUserMessageId); - early.Write("premature-inbound", 17); - SendGateBypass::Inject(client, early, serverAddr); - - const std::vector serverSaw = CollectIds(server, client, 1500); - EXPECT_FALSE(Contains(serverSaw, kUserMessageId)) - << "application data from a peer mid-handshake reached the application"; - EXPECT_FALSE(Contains(serverSaw, ID_NEW_INCOMING_CONNECTION)) - << "server reported a connection before answering the session request"; - - // Once accepted, the same peer's traffic is delivered normally -- the gate is about the connection - // state, not about this peer being permanently distrusted. - server->AcceptSession(clientGuid, kServerPayload, (unsigned int)strlen(kServerPayload)); - - Packet *incoming = PumpUntil(server, ID_NEW_INCOMING_CONNECTION, client, kConnectTimeoutMs); - ASSERT_NE(incoming, nullptr); - server->DeallocatePacket(incoming); - - MafiaNet::BitStream now; - now.Write((MessageID)kUserMessageId); - now.Write("after-accept-ok!", 16); - SendGateBypass::Inject(client, now, serverAddr); - - Packet *got = PumpUntil(server, kUserMessageId, client, kConnectTimeoutMs); - ASSERT_NE(got, nullptr) << "traffic was still dropped after the handshake completed"; - EXPECT_EQ(memcmp(got->data + 1, "after-accept-ok!", 16), 0); - server->DeallocatePacket(got); -} - -// ========================================================================================================= -// Pending-session pool, session timeout, status and abandonment (SetMaximumPendingSessions, -// SetSessionTimeout, SendSessionStatus, ID_SESSION_CONFIG_STATUS, ID_SESSION_CONFIG_ABANDONED). -// -// The pool is what lets an interactive server hold a connection while it decides without that connection -// taking a player's slot: a flood of stalled handshakes exhausts the pool, never the server. -// ========================================================================================================= - -namespace -{ - // Starts the server with an incoming limit and a pending pool, and the client plain. Returns the port. - unsigned short StartPooled(RakPeerInterface *server, RakPeerInterface *client, unsigned short maxIncoming, unsigned short pool, unsigned short perAddress, TimeMS connectionTimeoutMs = 60000) - { - server->SetSessionConfigInteractive(true); - server->SetMaximumPendingSessions(pool, perAddress); - - SocketDescriptor serverSd(0, "127.0.0.1"); - EXPECT_EQ(server->Startup((unsigned int)maxIncoming + pool, &serverSd, 1), RAKNET_STARTED); - server->SetMaximumIncomingConnections(maxIncoming); - server->SetTimeoutTime(connectionTimeoutMs, UNASSIGNED_SYSTEM_ADDRESS); - - SocketDescriptor clientSd(0, "127.0.0.1"); - EXPECT_EQ(client->Startup(1, &clientSd, 1), RAKNET_STARTED); - client->SetTimeoutTime(connectionTimeoutMs, UNASSIGNED_SYSTEM_ADDRESS); - - return server->GetInternalID(UNASSIGNED_SYSTEM_ADDRESS).GetPort(); - } - - // Starts a further client and has it connect. - void StartAndConnect(RakPeerInterface *peer, unsigned short port, TimeMS connectionTimeoutMs = 60000) - { - SocketDescriptor sd(0, "127.0.0.1"); - EXPECT_EQ(peer->Startup(1, &sd, 1), RAKNET_STARTED); - peer->SetTimeoutTime(connectionTimeoutMs, UNASSIGNED_SYSTEM_ADDRESS); - EXPECT_EQ(peer->Connect("127.0.0.1", port, 0, 0), CONNECTION_ATTEMPT_STARTED); - } - - // Waits for a session request on the server, pumping another peer meanwhile; hands back its guid. - bool WaitForRequest(RakPeerInterface *server, RakPeerInterface *alsoPump, RakNetGUID &guid) - { - Packet *request = PumpUntil(server, ID_SESSION_CONFIG_REQUEST, alsoPump, kConnectTimeoutMs); - if (!request) - return false; - guid = request->guid; - server->DeallocatePacket(request); - return true; - } -} // namespace - -// A handshake parked with the server undecided does not take the player slot: with the only slot's -// would-be owner stalled, another player still gets in. Without the pool the same setup refuses the -// second player (StalledHandshakeStillConsumesAnIncomingSlot). -TEST_F(SessionConfigLive, PendingPoolKeepsStalledHandshakesOutOfThePlayerSlots) -{ - const unsigned short port = StartPooled(server, client, 1, 4, 0); - ASSERT_EQ(client->Connect("127.0.0.1", port, 0, 0), CONNECTION_ATTEMPT_STARTED); - - RakNetGUID stalledGuid; - ASSERT_TRUE(WaitForRequest(server, client, stalledGuid)) << "first client never reached the handshake"; - // Deliberately never answered. - - RakPeerInterface *second = MakeExtraPeer(); - ASSERT_NE(second, nullptr); - StartAndConnect(second, port); - - RakNetGUID secondGuid; - ASSERT_TRUE(WaitForRequest(server, second, secondGuid)) << "the stalled handshake took the only player slot"; - EXPECT_NE(secondGuid, stalledGuid); - - server->AcceptSession(secondGuid, kServerPayload, (unsigned int)strlen(kServerPayload)); - - Packet *accepted = 0; - Packet *incoming = 0; - ASSERT_TRUE(PumpUntilBoth(second, ID_CONNECTION_REQUEST_ACCEPTED, &accepted, server, ID_NEW_INCOMING_CONNECTION, &incoming, kConnectTimeoutMs)) - << "the second player was not let in while the first sat in the handshake"; - EXPECT_EQ(incoming->guid, secondGuid); - second->DeallocatePacket(accepted); - server->DeallocatePacket(incoming); - - EXPECT_EQ(server->NumberOfConnections(), 1u) << "a peer still in the handshake must not read as connected"; - EXPECT_EQ(server->GetConnectionState(stalledGuid), IS_CONNECTING); -} - -// The pool is itself bounded: once it is full a newcomer is refused at the transport, the same way a -// full server always refused one. -TEST_F(SessionConfigLive, PendingPoolRefusesNewcomersOnceFull) -{ - const unsigned short port = StartPooled(server, client, 8, 1, 0); - ASSERT_EQ(client->Connect("127.0.0.1", port, 0, 0), CONNECTION_ATTEMPT_STARTED); - - RakNetGUID stalledGuid; - ASSERT_TRUE(WaitForRequest(server, client, stalledGuid)); - - RakPeerInterface *second = MakeExtraPeer(); - ASSERT_NE(second, nullptr); - StartAndConnect(second, port); - - Packet *full = PumpUntil(second, ID_NO_FREE_INCOMING_CONNECTIONS, server, kConnectTimeoutMs); - ASSERT_NE(full, nullptr) << "a full pending pool let another handshake start"; - second->DeallocatePacket(full); -} - -// One address cannot fill the pool on its own. Both clients are on 127.0.0.1, so the second is the same -// address as the first. -TEST_F(SessionConfigLive, PendingPoolBoundsWhatOneAddressMayHold) -{ - const unsigned short port = StartPooled(server, client, 8, 8, 1); - ASSERT_EQ(client->Connect("127.0.0.1", port, 0, 0), CONNECTION_ATTEMPT_STARTED); - - RakNetGUID stalledGuid; - ASSERT_TRUE(WaitForRequest(server, client, stalledGuid)); - - RakPeerInterface *second = MakeExtraPeer(); - ASSERT_NE(second, nullptr); - StartAndConnect(second, port); - - Packet *full = PumpUntil(second, ID_NO_FREE_INCOMING_CONNECTIONS, server, kConnectTimeoutMs); - ASSERT_NE(full, nullptr) << "one address held more pending handshakes than its share"; - second->DeallocatePacket(full); -} - -// A decision gives the pool slot back. A refused peer lingers only to deliver the refusal, and that must -// not keep the next newcomer out. -TEST_F(SessionConfigLive, PendingPoolSlotIsFreedByADecision) -{ - const unsigned short port = StartPooled(server, client, 8, 1, 0); - ASSERT_EQ(client->Connect("127.0.0.1", port, 0, 0), CONNECTION_ATTEMPT_STARTED); - - RakNetGUID firstGuid; - ASSERT_TRUE(WaitForRequest(server, client, firstGuid)); - server->RejectSession(firstGuid, "not on the whitelist"); - - Packet *failed = PumpUntil(client, ID_CONNECTION_ATTEMPT_FAILED, server, kConnectTimeoutMs); - ASSERT_NE(failed, nullptr); - client->DeallocatePacket(failed); - - RakPeerInterface *second = MakeExtraPeer(); - ASSERT_NE(second, nullptr); - StartAndConnect(second, port); - - // Collected rather than waited on with PumpUntil: the refused peer must not be reported as abandoned. - std::vector serverSaw; - RakNetGUID secondGuid; - bool gotRequest = false; - const TimeMS entry = GetTimeMS(); - while (GetTimeMS() - entry < (TimeMS)kConnectTimeoutMs && !gotRequest) - { - Packet *p; - for (p = server->Receive(); p; server->DeallocatePacket(p), p = server->Receive()) - { - serverSaw.push_back((int)p->data[0]); - if (p->data[0] == ID_SESSION_CONFIG_REQUEST) - { - gotRequest = true; - secondGuid = p->guid; - } - } - for (p = second->Receive(); p; second->DeallocatePacket(p), p = second->Receive()) - ; - for (p = client->Receive(); p; client->DeallocatePacket(p), p = client->Receive()) - ; - RakSleep(15); - } - ASSERT_TRUE(gotRequest) << "the refused peer still held the only pool slot"; - EXPECT_NE(secondGuid, firstGuid); - EXPECT_FALSE(Contains(serverSaw, ID_SESSION_CONFIG_ABANDONED)) << "a peer the application refused was reported as abandoned"; -} - -// With a pool the handshake may start on a full server -- that is what makes a queue possible -- but an -// AcceptSession() that would overfill it is refused rather than honoured. -TEST_F(SessionConfigLive, AcceptSessionCannotOverfillTheIncomingLimit) -{ - const unsigned short port = StartPooled(server, client, 1, 4, 0); - ASSERT_EQ(client->Connect("127.0.0.1", port, 0, 0), CONNECTION_ATTEMPT_STARTED); - - RakNetGUID firstGuid; - ASSERT_TRUE(WaitForRequest(server, client, firstGuid)); - server->AcceptSession(firstGuid, 0, 0); - - Packet *incoming = PumpUntil(server, ID_NEW_INCOMING_CONNECTION, client, kConnectTimeoutMs); - ASSERT_NE(incoming, nullptr); - server->DeallocatePacket(incoming); - // The only player slot is now taken. - - RakPeerInterface *second = MakeExtraPeer(); - ASSERT_NE(second, nullptr); - StartAndConnect(second, port); - - RakNetGUID secondGuid; - ASSERT_TRUE(WaitForRequest(server, second, secondGuid)) << "a full server with a pool refused to even start the handshake"; - - server->AcceptSession(secondGuid, 0, 0); - - Packet *failed = PumpUntil(second, ID_CONNECTION_ATTEMPT_FAILED, server, kConnectTimeoutMs); - ASSERT_NE(failed, nullptr) << "an accept past the incoming limit was honoured"; - second->DeallocatePacket(failed); - - const std::vector serverSaw = CollectIds(server, second, 1000); - EXPECT_FALSE(Contains(serverSaw, ID_NEW_INCOMING_CONNECTION)) << "the server reported a connection past its limit"; - EXPECT_EQ(server->NumberOfConnections(), 1u); -} - -// SetMaximumPendingSessions(0, ...) restores the historic accounting exactly. -TEST_F(SessionConfigLive, ClearingThePoolRestoresTheDefaultAccounting) -{ - server->SetMaximumPendingSessions(4, 0); - server->SetMaximumPendingSessions(0, 0); - server->SetSessionConfigInteractive(true); - - SocketDescriptor serverSd(0, "127.0.0.1"); - ASSERT_EQ(server->Startup(8, &serverSd, 1), RAKNET_STARTED); - server->SetMaximumIncomingConnections(1); - server->SetTimeoutTime(60000, UNASSIGNED_SYSTEM_ADDRESS); - - SocketDescriptor clientSd(0, "127.0.0.1"); - ASSERT_EQ(client->Startup(1, &clientSd, 1), RAKNET_STARTED); - - const unsigned short port = server->GetInternalID(UNASSIGNED_SYSTEM_ADDRESS).GetPort(); - ASSERT_EQ(client->Connect("127.0.0.1", port, 0, 0), CONNECTION_ATTEMPT_STARTED); - - RakNetGUID stalledGuid; - ASSERT_TRUE(WaitForRequest(server, client, stalledGuid)); - - RakPeerInterface *second = MakeExtraPeer(); - ASSERT_NE(second, nullptr); - StartAndConnect(second, port); - - Packet *full = PumpUntil(second, ID_NO_FREE_INCOMING_CONNECTIONS, server, kConnectTimeoutMs); - ASSERT_NE(full, nullptr) << "without a pool a stalled handshake must still hold its incoming slot"; - second->DeallocatePacket(full); -} - -// The session timeout replaces the connection timeout for the handshake, on both ends, and the server is -// told the request it was holding is gone. -TEST_F(SessionConfigLive, SessionTimeoutBoundsAnUnansweredHandshake) -{ - const TimeMS sessionTimeoutMs = 1000; - server->SetSessionTimeout(sessionTimeoutMs); - client->SetSessionTimeout(sessionTimeoutMs); - // A connection timeout far longer than the test, so only the session timeout can end the attempt. - const unsigned short port = StartPooled(server, client, 8, 4, 0, 60000); - - const TimeMS begin = GetTimeMS(); - ASSERT_EQ(client->Connect("127.0.0.1", port, 0, 0), CONNECTION_ATTEMPT_STARTED); - - RakNetGUID guid; - ASSERT_TRUE(WaitForRequest(server, client, guid)); - - Packet *failed = 0; - Packet *abandoned = 0; - ASSERT_TRUE(PumpUntilBoth(client, ID_CONNECTION_ATTEMPT_FAILED, &failed, server, ID_SESSION_CONFIG_ABANDONED, &abandoned, 15000)) - << "the session timeout did not end the handshake on both ends"; - const TimeMS elapsed = GetTimeMS() - begin; - EXPECT_EQ(abandoned->guid, guid); - client->DeallocatePacket(failed); - server->DeallocatePacket(abandoned); - - EXPECT_LT(elapsed, (TimeMS)15000) << "the handshake waited out something other than the session timeout"; - EXPECT_GE(elapsed, sessionTimeoutMs); -} - -// Status reaches the client before any connection is reported, carries its payload, and restarts the -// session timeout on both ends: a peer kept informed for three timeouts' worth is still let in. -TEST_F(SessionConfigLive, SessionStatusReachesTheClientAndKeepsTheHandshakeAlive) -{ - const TimeMS sessionTimeoutMs = 1000; - server->SetSessionTimeout(sessionTimeoutMs); - client->SetSessionTimeout(sessionTimeoutMs); - const unsigned short port = StartPooled(server, client, 8, 4, 0); - ASSERT_EQ(client->Connect("127.0.0.1", port, 0, 0), CONNECTION_ATTEMPT_STARTED); - - RakNetGUID guid; - ASSERT_TRUE(WaitForRequest(server, client, guid)); - - std::vector statuses; - bool reportedEarly = false; - bool failed = false; - const TimeMS holdFor = sessionTimeoutMs * 3; - const TimeMS begin = GetTimeMS(); - TimeMS lastStatus = 0; - int sent = 0; - while (GetTimeMS() - begin < holdFor) - { - if (sent == 0 || GetTimeMS() - lastStatus >= 400) - { - const std::string status = "queue:" + std::to_string(++sent); - server->SendSessionStatus(guid, status.data(), (unsigned int)status.size()); - lastStatus = GetTimeMS(); - } - Packet *p; - for (p = client->Receive(); p; client->DeallocatePacket(p), p = client->Receive()) - { - if (p->data[0] == ID_SESSION_CONFIG_STATUS) - statuses.push_back(std::string((const char *)p->data + 1, p->length - 1)); - else if (p->data[0] == ID_CONNECTION_REQUEST_ACCEPTED) - reportedEarly = true; - else if (p->data[0] == ID_CONNECTION_ATTEMPT_FAILED) - failed = true; - } - for (p = server->Receive(); p; server->DeallocatePacket(p), p = server->Receive()) - { - if (p->data[0] == ID_NEW_INCOMING_CONNECTION) - reportedEarly = true; - else if (p->data[0] == ID_SESSION_CONFIG_ABANDONED) - failed = true; - } - RakSleep(15); - } - - EXPECT_FALSE(failed) << "status did not keep the handshake alive past the session timeout"; - EXPECT_FALSE(reportedEarly) << "a connection was reported while the server was still deciding"; - ASSERT_GE(statuses.size(), 3u) << "status did not reach the client"; - EXPECT_EQ(statuses[0], "queue:1"); - EXPECT_EQ(statuses[1], "queue:2"); - EXPECT_EQ(server->GetConnectionState(guid), IS_CONNECTING); - - server->AcceptSession(guid, kServerPayload, (unsigned int)strlen(kServerPayload)); - - Packet *accepted = 0; - Packet *incoming = 0; - ASSERT_TRUE(PumpUntilBoth(client, ID_CONNECTION_REQUEST_ACCEPTED, &accepted, server, ID_NEW_INCOMING_CONNECTION, &incoming, kConnectTimeoutMs)) - << "a peer held with status was not let in when accepted"; - client->DeallocatePacket(accepted); - server->DeallocatePacket(incoming); -} - -// Status is for a peer awaiting a decision only; once accepted nothing more is sent. -TEST_F(SessionConfigLive, SessionStatusIsIgnoredOnceTheSessionIsDecided) -{ - const unsigned short port = StartPooled(server, client, 8, 4, 0); - ASSERT_EQ(client->Connect("127.0.0.1", port, 0, 0), CONNECTION_ATTEMPT_STARTED); - - RakNetGUID guid; - ASSERT_TRUE(WaitForRequest(server, client, guid)); - server->AcceptSession(guid, 0, 0); - - Packet *accepted = 0; - Packet *incoming = 0; - ASSERT_TRUE(PumpUntilBoth(client, ID_CONNECTION_REQUEST_ACCEPTED, &accepted, server, ID_NEW_INCOMING_CONNECTION, &incoming, kConnectTimeoutMs)); - const RakNetGUID serverGuid = accepted->guid; - client->DeallocatePacket(accepted); - server->DeallocatePacket(incoming); - - server->SendSessionStatus(guid, "late", 4); - const std::vector clientSaw = CollectIds(client, server, 800); - EXPECT_FALSE(Contains(clientSaw, ID_SESSION_CONFIG_STATUS)) << "status reached a peer that was already accepted"; - EXPECT_EQ(client->GetConnectionState(serverGuid), IS_CONNECTED); -} - -// Role binding: status is a server-to-client message. A client pushing one at a server must not have it -// surfaced there. -TEST_F(SessionConfigLive, ServerIgnoresSessionStatusSentByAClient) -{ - const unsigned short port = StartPooled(server, client, 8, 4, 0); - ASSERT_EQ(client->Connect("127.0.0.1", port, 0, 0), CONNECTION_ATTEMPT_STARTED); - - RakNetGUID guid; - ASSERT_TRUE(WaitForRequest(server, client, guid)); - - SystemAddress serverAddr; - serverAddr.SetBinaryAddress("127.0.0.1"); - serverAddr.SetPortHostOrder(port); - - MafiaNet::BitStream forged; - forged.Write((MessageID)ID_SESSION_CONFIG_STATUS); - forged.Write("spoofed", 7); - SendGateBypass::Inject(client, forged, serverAddr); - - const std::vector serverSaw = CollectIds(server, client, 1200); - EXPECT_FALSE(Contains(serverSaw, ID_SESSION_CONFIG_STATUS)) << "a client pushed a status into the server's queue"; - EXPECT_FALSE(Contains(serverSaw, ID_NEW_INCOMING_CONNECTION)); - - // The real handshake is unaffected. - server->AcceptSession(guid, 0, 0); - Packet *incoming = PumpUntil(server, ID_NEW_INCOMING_CONNECTION, client, kConnectTimeoutMs); - ASSERT_NE(incoming, nullptr); - server->DeallocatePacket(incoming); -} - -// A client that gives up while the server decides is reported to the server as abandoned, exactly once, -// and answering it afterwards is inert. -TEST_F(SessionConfigLive, ClientLeavingMidDecisionIsReportedAsAbandoned) -{ - const unsigned short port = StartPooled(server, client, 8, 4, 0); - ASSERT_EQ(client->Connect("127.0.0.1", port, 0, 0), CONNECTION_ATTEMPT_STARTED); - - RakNetGUID guid; - ASSERT_TRUE(WaitForRequest(server, client, guid)); - - // Shutdown with a block duration sends the disconnection notification, so the server learns now - // rather than after a timeout. - client->Shutdown(300); - - Packet *abandoned = PumpUntil(server, ID_SESSION_CONFIG_ABANDONED, 0, kConnectTimeoutMs); - ASSERT_NE(abandoned, nullptr) << "the server was never told the held request went away"; - EXPECT_EQ(abandoned->guid, guid); - server->DeallocatePacket(abandoned); - - server->AcceptSession(guid, 0, 0); - const std::vector serverSaw = CollectIds(server, 0, 1200); - EXPECT_FALSE(Contains(serverSaw, ID_SESSION_CONFIG_ABANDONED)) << "abandonment was reported twice"; - EXPECT_FALSE(Contains(serverSaw, ID_NEW_INCOMING_CONNECTION)) << "accepting an abandoned request reported a connection"; - EXPECT_FALSE(Contains(serverSaw, ID_CONNECTION_LOST)) << "a connection never reported was reported lost"; - EXPECT_FALSE(Contains(serverSaw, ID_DISCONNECTION_NOTIFICATION)) << "a connection never reported was reported closed"; - EXPECT_EQ(server->NumberOfConnections(), 0u); -} diff --git a/Tests/Support/SimulatedNetwork.cpp b/Tests/Support/SimulatedNetwork.cpp index 455716e93..b6fa4fbd6 100644 --- a/Tests/Support/SimulatedNetwork.cpp +++ b/Tests/Support/SimulatedNetwork.cpp @@ -278,12 +278,17 @@ namespace MafiaNet return RunUntil([&] { return Received(peer, id); }, maxSimMs); } - bool SimulatedNetwork::Connect(RakPeer *client, RakPeer *server, uint32_t maxSimMs) + ConnectionAttemptResult SimulatedNetwork::StartConnect(RakPeer *from, const RakPeer *to) { - const SystemAddress target = AddressOf(server); + const SystemAddress target = AddressOf(to); char host[64]; target.ToString(false, host, sizeof(host)); - if (client->Connect(host, target.GetPort(), nullptr, 0) != CONNECTION_ATTEMPT_STARTED) + return from->Connect(host, target.GetPort(), nullptr, 0); + } + + bool SimulatedNetwork::Connect(RakPeer *client, RakPeer *server, uint32_t maxSimMs) + { + if (StartConnect(client, server) != CONNECTION_ATTEMPT_STARTED) return false; return RunUntil([&] { return Received(client, ID_CONNECTION_REQUEST_ACCEPTED) && Received(server, ID_NEW_INCOMING_CONNECTION); diff --git a/Tests/Support/SimulatedNetwork.h b/Tests/Support/SimulatedNetwork.h index 39d756362..639501dec 100644 --- a/Tests/Support/SimulatedNetwork.h +++ b/Tests/Support/SimulatedNetwork.h @@ -88,6 +88,9 @@ namespace MafiaNet bool RunUntilReceived(const RakPeer *peer, MessageID id, uint32_t maxSimMs); // Client connects to server and both see the connection. bool Connect(RakPeer *client, RakPeer *server, uint32_t maxSimMs = 5000); + // Start a connection attempt from `from` to `to` without stepping: the + // raw RakPeer::Connect result, for scenarios that drive the handshake. + ConnectionAttemptResult StartConnect(RakPeer *from, const RakPeer *to); // Deliver raw bytes to a peer as if `from` had sent them, bypassing link // rules: for fuzzing what an unconnected remote can send. diff --git a/Tests/Unit/CrossConnectionConvertTests.cpp b/Tests/Unit/CrossConnectionConvertTests.cpp new file mode 100644 index 000000000..e9c7b8de6 --- /dev/null +++ b/Tests/Unit/CrossConnectionConvertTests.cpp @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2026, MafiaHub + * + * This source code is licensed under the MIT-style license found in the + * license.txt file in the root directory of this source tree. + * + * Two peers on the SimulatedNetwork connect to each other at the same instant + * (agreed through an unconnected ping/pong). Crossing handshakes have broken + * RakNet in the past; both sides must still end up with one working link. + */ + +#include + +#include "SimulatedNetwork.h" + +#include "mafianet/bit_stream.h" +#include "mafianet/message_identifiers.h" + +using namespace MafiaNet; + +namespace +{ + const int kRounds = 5; + + void PingServer(SimulatedNetwork &net, RakPeer *client, const RakPeer *server) + { + const SystemAddress target = net.AddressOf(server); + char host[64]; + target.ToString(false, host, sizeof(host)); + ASSERT_TRUE(client->Ping(host, target.GetPort(), false)); + } + + bool GotConnectionNotification(SimulatedNetwork &net, const RakPeer *peer) + { + return net.Received(peer, ID_NEW_INCOMING_CONNECTION) || net.Received(peer, ID_CONNECTION_REQUEST_ACCEPTED); + } + + void RunCrossConnectRounds(unsigned short clientMaxIncoming) + { + SimulatedNetwork net; + RakPeer *server = net.AddPeer(1, 1); + RakPeer *client = net.AddPeer(1, clientMaxIncoming); + const SystemAddress serverAddress = net.AddressOf(server); + const SystemAddress clientAddress = net.AddressOf(client); + + for (int round = 0; round < kRounds; ++round) + { + SCOPED_TRACE(testing::Message() << "round " << round); + net.Inbox(server).clear(); + net.Inbox(client).clear(); + + // Ping/pong: the server learns the client's address, the client + // gets its own send time echoed back. + const TimeMS pingSentAt = GetTimeMS(); + ASSERT_NO_FATAL_FAILURE(PingServer(net, client, server)); + ASSERT_TRUE(net.RunUntil([&] { + return net.Received(server, ID_UNCONNECTED_PING) && net.Received(client, ID_UNCONNECTED_PONG); + }, 1000)); + EXPECT_EQ(clientAddress, net.Find(server, ID_UNCONNECTED_PING)->systemAddress); + { + const SimulatedNetwork::Message *pong = net.Find(client, ID_UNCONNECTED_PONG); + BitStream bs(const_cast(pong->data.data()), (unsigned int) pong->data.size(), false); + bs.IgnoreBytes(1); + TimeMS echoed = 0; + ASSERT_TRUE(bs.Read(echoed)); + EXPECT_EQ(pingSentAt, echoed); + } + + // Both sides wait out the agreed second and connect in the same tick. + net.Step(1000); + EXPECT_EQ(CONNECTION_ATTEMPT_STARTED, net.StartConnect(server, client)); + EXPECT_EQ(CONNECTION_ATTEMPT_STARTED, net.StartConnect(client, server)); + + net.RunUntil([&] { + return client->GetConnectionState(serverAddress) == IS_CONNECTED && + server->GetConnectionState(clientAddress) == IS_CONNECTED; + }, 2000); + EXPECT_TRUE(GotConnectionNotification(net, server) || GotConnectionNotification(net, client)) + << "neither side was told about a connection"; + EXPECT_EQ(IS_CONNECTED, client->GetConnectionState(serverAddress)); + EXPECT_EQ(IS_CONNECTED, server->GetConnectionState(clientAddress)); + EXPECT_EQ(server->GetMyGUID(), client->GetGuidFromSystemAddress(serverAddress)); + EXPECT_EQ(client->GetMyGUID(), server->GetGuidFromSystemAddress(clientAddress)); + ASSERT_FALSE(testing::Test::HasFailure()); + + // Tear down for the next round. + client->CancelConnectionAttempt(serverAddress); + server->CancelConnectionAttempt(clientAddress); + server->CloseConnection(server->GetSystemAddressFromIndex(0), true, 0); + client->CloseConnection(client->GetSystemAddressFromIndex(0), true, 0); + net.Step(1000); + for (const ConnectionState state : {client->GetConnectionState(serverAddress), server->GetConnectionState(clientAddress)}) + ASSERT_TRUE(state == IS_DISCONNECTED || state == IS_NOT_CONNECTED) << "state " << state; + } + } +} + +TEST(CrossConnectionConvertSim, SimultaneousConnectWhenOnlyOneSideAcceptsIncoming) +{ + RunCrossConnectRounds(0); +} + +TEST(CrossConnectionConvertSim, SimultaneousConnectWhenBothSidesAcceptIncoming) +{ + RunCrossConnectRounds(1); +} diff --git a/Tests/Unit/DisconnectReasonTests.cpp b/Tests/Unit/DisconnectReasonTests.cpp new file mode 100644 index 000000000..d14f7451d --- /dev/null +++ b/Tests/Unit/DisconnectReasonTests.cpp @@ -0,0 +1,143 @@ +/* + * Copyright (c) 2026, MafiaHub + * + * This source code is licensed under the MIT-style license found in the + * license.txt file in the root directory of this source tree. + * + * CloseConnection(..., reasonData): the ID_DISCONNECTION_NOTIFICATION the + * remote side receives carries the reason bytes after the message ID, and is + * a bare 1-byte message when no reason (or an empty one) is given. + * + * Runs over the in-process SimulatedNetwork, so the reliable notification + * always arrives and there is nothing to retry: a missing notification here is + * a real failure. + */ + +#include + +#include "SimulatedNetwork.h" + +#include "mafianet/bit_stream.h" +#include "mafianet/message_identifiers.h" +#include "mafianet/string.h" + +using namespace MafiaNet; + +namespace +{ + const uint32_t kNotificationBudgetMs = 2000; + + // Connects a fresh client to the server, has the server close it with the + // given reason, and returns the notification the client received (or null). + const SimulatedNetwork::Message *KickWithReason(SimulatedNetwork &net, RakPeer *server, RakPeer *client, + const BitStream *reasonData) + { + if (!net.Connect(client, server)) + { + ADD_FAILURE() << "client never connected"; + return nullptr; + } + const RakNetGUID clientGuid = net.Find(server, ID_NEW_INCOMING_CONNECTION)->guid; + EXPECT_EQ(client->GetMyGUID(), clientGuid); + + server->CloseConnection(clientGuid, true, 0, Priority::Low, reasonData); + if (!net.RunUntilReceived(client, ID_DISCONNECTION_NOTIFICATION, kNotificationBudgetMs)) + { + ADD_FAILURE() << "client never received ID_DISCONNECTION_NOTIFICATION"; + return nullptr; + } + const SimulatedNetwork::Message *note = net.Find(client, ID_DISCONNECTION_NOTIFICATION); + EXPECT_EQ(server->GetMyGUID(), note->guid); + return note; + } +} + +TEST(DisconnectReason, ReasonPayloadRoundTripsInTheNotification) +{ + SimulatedNetwork net; + RakPeer *server = net.AddPeer(8, 8); + RakPeer *client = net.AddPeer(1); + + // The "Kicked: " use case: an enum code plus an admin-typed string. + const unsigned char kReasonCode = 7; + const RakString kReasonText("Cheating detected in match #4821"); + BitStream reason; + reason.Write(kReasonCode); + kReasonText.Serialize(&reason); + + const SimulatedNetwork::Message *note = KickWithReason(net, server, client, &reason); + ASSERT_NE(nullptr, note); + ASSERT_GT(note->data.size(), 1u) << "notification carried no reason payload"; + EXPECT_EQ(1u + reason.GetNumberOfBytesUsed(), note->data.size()); + + BitStream readBack(const_cast(note->data.data()) + 1, (unsigned int) note->data.size() - 1, false); + unsigned char gotCode = 0; + RakString gotText; + ASSERT_TRUE(readBack.Read(gotCode)); + ASSERT_TRUE(gotText.Deserialize(&readBack)) << "reason string failed to deserialize"; + EXPECT_EQ(kReasonCode, gotCode); + EXPECT_EQ(kReasonText, gotText); +} + +TEST(DisconnectReason, NullReasonGivesABareOneByteNotification) +{ + SimulatedNetwork net; + RakPeer *server = net.AddPeer(8, 8); + RakPeer *client = net.AddPeer(1); + + const SimulatedNetwork::Message *note = KickWithReason(net, server, client, nullptr); + ASSERT_NE(nullptr, note); + EXPECT_EQ(1u, note->data.size()); +} + +// A non-null but empty BitStream exercises the GetNumberOfBytesUsed() > 0 +// guard, distinct from the nullptr check. +TEST(DisconnectReason, EmptyReasonGivesABareOneByteNotification) +{ + SimulatedNetwork net; + RakPeer *server = net.AddPeer(8, 8); + RakPeer *client = net.AddPeer(1); + + BitStream emptyReason; + const SimulatedNetwork::Message *note = KickWithReason(net, server, client, &emptyReason); + ASSERT_NE(nullptr, note); + EXPECT_EQ(1u, note->data.size()); +} + +// The legacy test ran the three cases back to back on the same pair of peers: +// each kick must free the slot so the same client can reconnect and be kicked +// again, and each notification must carry only its own reason. +TEST(DisconnectReason, SamePeersReconnectAndCarryEachReasonInTurn) +{ + SimulatedNetwork net; + RakPeer *server = net.AddPeer(8, 8); + RakPeer *client = net.AddPeer(1); + const SystemAddress serverAddress = net.AddressOf(server); + const SystemAddress clientAddress = net.AddressOf(client); + + BitStream reason; + reason.Write((unsigned char) 42); + BitStream emptyReason; + const BitStream *reasons[] = {&reason, nullptr, &emptyReason}; + const size_t expectedLength[] = {2u, 1u, 1u}; + + for (int round = 0; round < 3; ++round) + { + SCOPED_TRACE(testing::Message() << "round " << round); + net.Inbox(client).clear(); + net.Inbox(server).clear(); + + const SimulatedNetwork::Message *note = KickWithReason(net, server, client, reasons[round]); + ASSERT_NE(nullptr, note); + ASSERT_EQ(expectedLength[round], note->data.size()); + if (round == 0) + EXPECT_EQ(42, note->data[1]); + + // Both sides must release the slot before the next round's Connect(). + ASSERT_TRUE(net.RunUntil([&] { + const ConnectionState c = client->GetConnectionState(serverAddress); + const ConnectionState s = server->GetConnectionState(clientAddress); + return (c == IS_NOT_CONNECTED || c == IS_DISCONNECTED) && (s == IS_NOT_CONNECTED || s == IS_DISCONNECTED); + }, 5000)); + } +} diff --git a/Tests/Unit/DroppedConnectionConvertTests.cpp b/Tests/Unit/DroppedConnectionConvertTests.cpp new file mode 100644 index 000000000..1f800802e --- /dev/null +++ b/Tests/Unit/DroppedConnectionConvertTests.cpp @@ -0,0 +1,170 @@ +/* + * Copyright (c) 2026, MafiaHub + * + * This source code is licensed under the MIT-style license found in the + * license.txt file in the root directory of this source tree. + * + * Nine clients on the SimulatedNetwork connect to a server, then silently + * drop, gracefully disconnect and reconnect at random. The server must detect + * every silent drop through its timeout, and no client may ever hold more + * than one connection. + */ + +#include + +#include + +#include "SimulatedNetwork.h" + +#include "mafianet/message_identifiers.h" + +using namespace MafiaNet; + +namespace +{ + const int kClients = 9; + const TimeMS kServerTimeoutMs = 2000; + const TimeMS kClientTimeoutMs = 5000; + // A silent drop is noticed once a reliable message (at worst the keepalive + // ping, sent after timeout/2 without reliable traffic) goes unacknowledged + // for the timeout. Leave one more timeout/2 of slack for the update cadence. + const uint32_t kDetectionBudgetMs = kServerTimeoutMs + kServerTimeoutMs / 2; + + class DroppedConnectionConvertSim : public ::testing::Test + { + protected: + void SetUp() override + { + server = net.AddPeer(kClients, kClients); + server->SetTimeoutTime(kServerTimeoutMs, UNASSIGNED_SYSTEM_ADDRESS); + serverAddress = net.AddressOf(server); + net.SetTap([this](const SystemAddress &from, const SystemAddress &to, const char *, int) { + if (to == serverAddress) + lastHeardFrom[from] = net.Now(); + }); + for (int i = 0; i < kClients; ++i) + { + clients[i] = net.AddPeer(1); + clients[i]->SetTimeoutTime(kClientTimeoutMs, UNASSIGNED_SYSTEM_ADDRESS); + ASSERT_EQ(CONNECTION_ATTEMPT_STARTED, net.StartConnect(clients[i], server)) << "client " << i; + net.Step(1000); + } + } + + static unsigned short ConnectionCount(RakPeer *peer) + { + unsigned short count = 0; + peer->GetConnectionList(nullptr, &count); + return count; + } + + // ASSERTs no client has more than one connection; returns how many have one. + int ConnectedClients() + { + int connected = 0; + for (int i = 0; i < kClients; ++i) + { + const unsigned short count = ConnectionCount(clients[i]); + EXPECT_LE(count, 1) << "client " << i << " has " << count << " connections"; + if (count == 1) + ++connected; + } + return connected; + } + + bool BusyOrConnected(int i) + { + const ConnectionState state = clients[i]->GetConnectionState(serverAddress); + return state == IS_CONNECTED || state == IS_CONNECTING || state == IS_PENDING || state == IS_DISCONNECTING; + } + + uint32_t NextRandom() + { + lcg = lcg * 1664525u + 1013904223u; + return lcg >> 8; + } + + SimulatedNetwork net; + RakPeer *server = nullptr; + RakPeer *clients[kClients] = {}; + SystemAddress serverAddress; + std::map lastHeardFrom; + uint32_t lcg = 12345; + }; +} + +TEST_F(DroppedConnectionConvertSim, AllClientsConnect) +{ + EXPECT_EQ(kClients, ConnectedClients()); + EXPECT_EQ(kClients, ConnectionCount(server)); +} + +TEST_F(DroppedConnectionConvertSim, SilentDropIsDetectedByTheServerTimeout) +{ + clients[0]->CloseConnection(serverAddress, false, 0); + ASSERT_TRUE(net.RunUntil([&] { return ConnectionCount(clients[0]) == 0; }, 100)); + EXPECT_EQ(kClients, ConnectionCount(server)) << "a silent close must not tell the server"; + + ASSERT_TRUE(net.RunUntilReceived(server, ID_CONNECTION_LOST, kDetectionBudgetMs)); + // The timeout runs from the last datagram the server got from the client. + const TimeUS silentForMs = (net.Now() - lastHeardFrom[net.AddressOf(clients[0])]) / 1000; + EXPECT_GE(silentForMs, kServerTimeoutMs) << "dropped before the timeout elapsed"; + EXPECT_LE(silentForMs, kDetectionBudgetMs); + EXPECT_EQ(clients[0]->GetMyGUID(), net.Find(server, ID_CONNECTION_LOST)->guid); + EXPECT_EQ(kClients - 1, ConnectionCount(server)); + EXPECT_FALSE(net.Received(server, ID_DISCONNECTION_NOTIFICATION)); +} + +TEST_F(DroppedConnectionConvertSim, RandomDropsAndReconnectsStayConsistent) +{ + const TimeUS start = net.Now(); + int timeoutChecks = 0; + while (net.Now() - start < (TimeUS) 30000 * 1000) + { + switch (NextRandom() % 4) + { + case 0: // silently drop one client + clients[NextRandom() % kClients]->CloseConnection(serverAddress, false, 0); + break; + case 1: // reconnect one client unless it is busy + { + const int index = (int) (NextRandom() % kClients); + if (!BusyOrConnected(index)) + ASSERT_EQ(CONNECTION_ATTEMPT_STARTED, net.StartConnect(clients[index], server)) << "client " << index; + break; + } + case 2: // every client: drop (silently or not) or reconnect + for (int index = 0; index < kClients; ++index) + { + if (NextRandom() % 2 == 0) + clients[index]->CloseConnection(serverAddress, NextRandom() % 2 == 0, 0); + else if (!BusyOrConnected(index)) + ASSERT_EQ(CONNECTION_ATTEMPT_STARTED, net.StartConnect(clients[index], server)) << "client " << index; + } + break; + case 3: // quiet period: the server must catch up with every drop + { + ++timeoutChecks; + const bool consistent = net.RunUntil([&] { return ConnectedClients() == ConnectionCount(server); }, kDetectionBudgetMs); + ASSERT_TRUE(consistent) << "timeout on dropped clients not detected: server has " << ConnectionCount(server) + << " connections, " << ConnectedClients() << " clients are connected"; + break; + } + } + ConnectedClients(); + ASSERT_FALSE(HasFailure()); + net.Step(10); + } + EXPECT_GT(timeoutChecks, 5); + + // Every client can get back in once the dust settles. A reconnect that + // races the server still holding the silently dropped connection is + // refused (ID_ALREADY_CONNECTED) until the timeout clears it, so retry. + const bool allBack = net.RunUntil([&] { + for (int index = 0; index < kClients; ++index) + if (!BusyOrConnected(index)) + EXPECT_EQ(CONNECTION_ATTEMPT_STARTED, net.StartConnect(clients[index], server)) << "client " << index; + return ConnectedClients() == kClients && ConnectionCount(server) == kClients; + }, 20000, 100); + EXPECT_TRUE(allBack) << ConnectedClients() << " clients and " << ConnectionCount(server) << " server connections"; +} diff --git a/Tests/Unit/EightPeerTests.cpp b/Tests/Unit/EightPeerTests.cpp new file mode 100644 index 000000000..df797d3c5 --- /dev/null +++ b/Tests/Unit/EightPeerTests.cpp @@ -0,0 +1,147 @@ +/* + * Copyright (c) 2026, MafiaHub + * + * This source code is licensed under the MIT-style license found in the + * license.txt file in the root directory of this source tree. + * + * Eight peers on the SimulatedNetwork form a full mesh, then every peer + * broadcasts numbered ReliableOrdered messages to all the others. + */ + +#include + +#include "SimulatedNetwork.h" + +#include "mafianet/bit_stream.h" +#include "mafianet/message_identifiers.h" + +using namespace MafiaNet; + +namespace +{ + const int kPeerNum = 8; + const int kNumPackets = 100; + const MessageID ID_MESH_BROADCAST = ID_USER_PACKET_ENUM + 1; + + int CountOf(SimulatedNetwork &net, const RakPeer *peer, MessageID id) + { + int count = 0; + for (const auto &message : net.Inbox(peer)) + if (message.id == id) + ++count; + return count; + } + + // Messages that must never show up in this scenario. + const MessageID kFailureIds[] = { + ID_REMOTE_DISCONNECTION_NOTIFICATION, + ID_REMOTE_CONNECTION_LOST, + ID_CONNECTION_ATTEMPT_FAILED, + ID_NO_FREE_INCOMING_CONNECTIONS, + ID_ALREADY_CONNECTED, + ID_DISCONNECTION_NOTIFICATION, + ID_CONNECTION_LOST, + }; + + class EightPeerSim : public ::testing::Test + { + protected: + void SetUp() override + { + for (int i = 0; i < kPeerNum; ++i) + peers[i] = net.AddPeer(kPeerNum * 2, kPeerNum); + } + + int ConnectionsSeen(int i) + { + return CountOf(net, peers[i], ID_CONNECTION_REQUEST_ACCEPTED) + + CountOf(net, peers[i], ID_NEW_INCOMING_CONNECTION); + } + + void ExpectNoFailures() + { + for (int i = 0; i < kPeerNum; ++i) + for (MessageID id : kFailureIds) + EXPECT_FALSE(net.Received(peers[i], id)) << "peer " << i << " received message id " << (int) id; + } + + void ConnectMesh() + { + for (int i = 0; i < kPeerNum; ++i) + for (int j = i + 1; j < kPeerNum; ++j) + ASSERT_EQ(CONNECTION_ATTEMPT_STARTED, net.StartConnect(peers[i], peers[j])) << i << " -> " << j; + + const bool allConnected = net.RunUntil([&] { + for (int i = 0; i < kPeerNum; ++i) + if (ConnectionsSeen(i) < kPeerNum - 1) + return false; + return true; + }, 20000); + ASSERT_TRUE(allConnected) << "peers failed to connect to all peers within 20s"; + } + + SimulatedNetwork net; + RakPeer *peers[kPeerNum] = {}; + }; +} + +TEST_F(EightPeerSim, FullMeshConnectsWithoutFailures) +{ + ASSERT_NO_FATAL_FAILURE(ConnectMesh()); + for (int i = 0; i < kPeerNum; ++i) + { + EXPECT_EQ(kPeerNum - 1, ConnectionsSeen(i)) << "peer " << i; + DataStructures::List systemList; + DataStructures::List guidList; + peers[i]->GetSystemList(systemList, guidList); + EXPECT_EQ((unsigned) (kPeerNum - 1), guidList.Size()) << "peer " << i; + } + ExpectNoFailures(); +} + +TEST_F(EightPeerSim, ReliableOrderedBroadcastArrivesCompleteAndInOrder) +{ + ASSERT_NO_FATAL_FAILURE(ConnectMesh()); + + for (int k = 0; k < kNumPackets; ++k) + { + for (int i = 0; i < kPeerNum; ++i) + { + BitStream bs; + bs.Write(ID_MESH_BROADCAST); + bs.Write(k); + bs.Write(i); + ASSERT_NE(0u, peers[i]->Send(&bs, Priority::High, Reliability::ReliableOrdered, 0, UNASSIGNED_SYSTEM_ADDRESS, true)); + } + net.Step(10); + } + + ASSERT_TRUE(net.RunUntil([&] { + for (int i = 0; i < kPeerNum; ++i) + if (CountOf(net, peers[i], ID_MESH_BROADCAST) < kNumPackets * (kPeerNum - 1)) + return false; + return true; + }, 5000)); + + for (int i = 0; i < kPeerNum; ++i) + { + int nextFrom[kPeerNum] = {}; + for (const auto &message : net.Inbox(peers[i])) + { + if (message.id != ID_MESH_BROADCAST) + continue; + BitStream bs(const_cast(message.data.data()), (unsigned int) message.data.size(), false); + bs.IgnoreBytes(1); + int sequence = -1, sender = -1; + ASSERT_TRUE(bs.Read(sequence)); + ASSERT_TRUE(bs.Read(sender)); + ASSERT_TRUE(sender >= 0 && sender < kPeerNum && sender != i) << "bad sender " << sender; + ASSERT_EQ(nextFrom[sender], sequence) << "peer " << i << ": out of order from " << sender; + ++nextFrom[sender]; + } + for (int j = 0; j < kPeerNum; ++j) + if (j != i) + EXPECT_EQ(kNumPackets, nextFrom[j]) << "peer " << i << " received " << nextFrom[j] << " from " << j; + } + ExpectNoFailures(); +} diff --git a/Tests/Unit/MTUNegotiationTests.cpp b/Tests/Unit/MTUNegotiationTests.cpp new file mode 100644 index 000000000..8a24d0b84 --- /dev/null +++ b/Tests/Unit/MTUNegotiationTests.cpp @@ -0,0 +1,220 @@ +/* + * Copyright (c) 2026, MafiaHub + * + * This source code is licensed under the MIT-style license found in the + * license.txt file in the root directory of this source tree. + * + * The MTU the connection handshake settles on, and the connect loop that + * probes for it, over the in-process SimulatedNetwork. + * + * The handshake pads ID_OPEN_CONNECTION_REQUEST_1 down a ladder of sizes and + * keeps the largest one the path passed. That value is frozen for the life of + * the connection and sizes every datagram in BOTH directions, so: + * - both peers must end up on the same size, inside [MINIMUM, MAXIMUM]; + * - splitting and reassembly must work at whatever that size is; + * - the accepting side must clamp an MTU a remote names on the wire, since + * ID_OPEN_CONNECTION_REQUEST_2 is unauthenticated. + * + * A link rule's maxDatagramBytes is a path ceiling: datagrams above it vanish, + * as they would behind a router with a smaller MTU. The datagram the ladder + * sends for a rung of size M is M - UDP_HEADER_SIZE bytes. + */ + +#include + +#include +#include + +#include "SimulatedNetwork.h" + +#include "mafianet/bit_stream.h" +#include "mafianet/message_identifiers.h" +#include "mafianet/mtu_size.h" + +using namespace MafiaNet; + +namespace +{ + const MessageID ID_BIG = ID_USER_PACKET_ENUM + 1; + + size_t CountOf(SimulatedNetwork &net, const RakPeer *peer, MessageID id) + { + size_t n = 0; + for (const auto &message : net.Inbox(peer)) + n += message.id == id; + return n; + } + + // Mirrors the file-static OFFLINE_MESSAGE_DATA_ID in peer.cpp. Duplicated + // deliberately: a test of what an unconforming peer can put on the wire + // should pin the wire format itself. + const unsigned char kOfflineMessageDataId[16] = + {0x00,0xFF,0xFF,0x00,0xFE,0xFE,0xFE,0xFE,0xFD,0xFD,0xFD,0xFD,0x12,0x34,0x56,0x78}; + + // The ID_OPEN_CONNECTION_REQUEST_2 an accepting peer parses, with a + // caller-chosen MTU, written with the calls the real handshake uses. + void WriteOpenConnectionRequest2(BitStream &out, const SystemAddress &bindingAddress, uint16_t mtu, const RakNetGUID &guid) + { + out.Write((MessageID) ID_OPEN_CONNECTION_REQUEST_2); + out.WriteAlignedBytes(kOfflineMessageDataId, sizeof(kOfflineMessageDataId)); + out.Write(bindingAddress); + out.Write(mtu); + out.Write(guid); + } + + // Injects a forged REQUEST_2 from an address no peer owns and returns the + // MTU the server assigned that address, or -1 if it never assigned a slot. + // + // An address with no slot reports defaultMTUSize, which doubles as "not + // yet", so callers must not forge an MTU equal to defaultMTUSize. + int ForgeRequest2(SimulatedNetwork &net, RakPeer *server, uint16_t mtu, uint64_t guid) + { + SystemAddress forger("10.0.0.200", 4321); + BitStream forged; + WriteOpenConnectionRequest2(forged, net.AddressOf(server), mtu, RakNetGUID(guid)); + net.InjectDatagram(forger, server, forged.GetData(), (int) forged.GetNumberOfBytesUsed()); + + const int unassigned = server->GetMTUSize(UNASSIGNED_SYSTEM_ADDRESS); + if (!net.RunUntil([&] { return server->GetMTUSize(forger) != unassigned; }, 2000)) + return -1; + return server->GetMTUSize(forger); + } +} + +// With no ceiling the top rung passes, so the value is exact -- which also +// proves the ladder does not silently start below MAXIMUM_MTU_SIZE. +TEST(MTUNegotiation, UnrestrictedPathNegotiatesTheMaximumOnBothSides) +{ + SimulatedNetwork net; + RakPeer *server = net.AddPeer(8, 8); + RakPeer *client = net.AddPeer(1); + ASSERT_TRUE(net.Connect(client, server)); + + const int clientSide = client->GetMTUSize(net.AddressOf(server)); + const int serverSide = server->GetMTUSize(net.AddressOf(client)); + EXPECT_EQ(MAXIMUM_MTU_SIZE, clientSide); + EXPECT_EQ(MAXIMUM_MTU_SIZE, serverSide); + EXPECT_GE(clientSide, MINIMUM_MTU_SIZE); +} + +// Behind a ceiling both peers must still agree, on a rung that fits it. +TEST(MTUNegotiation, PathCeilingGivesBothSidesTheSameRungBelowIt) +{ + SimulatedNetwork net; + RakPeer *server = net.AddPeer(8, 8); + RakPeer *client = net.AddPeer(1); + SimulatedNetwork::LinkRule narrow; + narrow.maxDatagramBytes = 1100; + net.SetLinkBothWays(client, server, narrow); + ASSERT_TRUE(net.Connect(client, server, 20000)); + + const int clientSide = client->GetMTUSize(net.AddressOf(server)); + const int serverSide = server->GetMTUSize(net.AddressOf(client)); + EXPECT_EQ(clientSide, serverSide) << "the two peers disagree about the datagram size"; + EXPECT_LE(clientSide - UDP_HEADER_SIZE, 1100); + EXPECT_GE(clientSide, MINIMUM_MTU_SIZE); + EXPECT_LT(clientSide, MAXIMUM_MTU_SIZE); +} + +// Splitting and reassembly happen at the negotiated MTU. Send a message many +// times that size and check it back byte for byte, at the maximum and behind a +// ceiling (where every fragment must also fit the path). +TEST(MTUNegotiation, MessageManyTimesTheMTUArrivesIntact) +{ + for (int ceiling : {0, 1100}) + { + SCOPED_TRACE(testing::Message() << "ceiling " << ceiling); + SimulatedNetwork net; + RakPeer *server = net.AddPeer(8, 8); + RakPeer *client = net.AddPeer(1); + if (ceiling) + { + SimulatedNetwork::LinkRule narrow; + narrow.maxDatagramBytes = ceiling; + net.SetLinkBothWays(client, server, narrow); + } + ASSERT_TRUE(net.Connect(client, server, 20000)); + const uint64_t droppedAfterConnect = net.DatagramsDropped(); + + const size_t payloadSize = (size_t) MAXIMUM_MTU_SIZE * 30; + std::vector payload(payloadSize); + for (size_t i = 0; i < payloadSize; ++i) + payload[i] = (char) ((i * 31 + (i >> 8)) & 0xFF); + + BitStream bs; + bs.Write(ID_BIG); + bs.Write(payload.data(), (unsigned int) payloadSize); + ASSERT_NE(0u, client->Send(&bs, Priority::High, Reliability::ReliableOrdered, 0, net.AddressOf(server), false)); + + ASSERT_TRUE(net.RunUntilReceived(server, ID_BIG, 10000)) << "the split message never reassembled"; + const SimulatedNetwork::Message *received = net.Find(server, ID_BIG); + ASSERT_EQ(payloadSize + 1, received->data.size()); + EXPECT_EQ(0, std::memcmp(received->data.data() + 1, payload.data(), payloadSize)); + EXPECT_EQ(droppedAfterConnect, net.DatagramsDropped()) << "a fragment was larger than the negotiated path"; + } +} + +// Regression: the connect loop divided sendConnectionAttemptCount by the number +// of rungs in the ladder, so any count below the rung count divided by zero. +// With no ceiling the first attempt uses the top rung and passes, so the +// connection completes however small the budget is. Closing and reconnecting on +// the same pair also covers slot release between attempts. +TEST(MTUNegotiation, ConnectWorksWithFewerAttemptsThanMTURungs) +{ + SimulatedNetwork net; + RakPeer *server = net.AddPeer(8, 8); + RakPeer *client = net.AddPeer(1); + const SystemAddress serverAddress = net.AddressOf(server); + char host[64]; + serverAddress.ToString(false, host, sizeof(host)); + + for (unsigned attempts = 1; attempts <= 3; ++attempts) + { + SCOPED_TRACE(testing::Message() << "sendConnectionAttemptCount=" << attempts); + const size_t accepted = CountOf(net, client, ID_CONNECTION_REQUEST_ACCEPTED); + const size_t incoming = CountOf(net, server, ID_NEW_INCOMING_CONNECTION); + const size_t closed = CountOf(net, server, ID_DISCONNECTION_NOTIFICATION); + + ASSERT_EQ(CONNECTION_ATTEMPT_STARTED, + client->Connect(host, serverAddress.GetPort(), nullptr, 0, nullptr, 0, attempts, 500, 0)); + ASSERT_TRUE(net.RunUntil([&] { + return CountOf(net, client, ID_CONNECTION_REQUEST_ACCEPTED) > accepted && + CountOf(net, server, ID_NEW_INCOMING_CONNECTION) > incoming; + }, 5000)) << "both peers did not report the connection"; + + EXPECT_EQ(MAXIMUM_MTU_SIZE, client->GetMTUSize(serverAddress)); + + client->CloseConnection(serverAddress, true); + ASSERT_TRUE(net.RunUntil([&] { return CountOf(net, server, ID_DISCONNECTION_NOTIFICATION) > closed; }, 5000)) + << "the server never saw the connection close"; + ASSERT_TRUE(net.RunUntil([&] { + const ConnectionState state = client->GetConnectionState(serverAddress); + return state == IS_NOT_CONNECTED || state == IS_DISCONNECTED; + }, 5000)) << "the client never released its connection slot"; + } +} + +// An unauthenticated REQUEST_2 can name any uint16. The value sizes datagrams +// built into MAXIMUM_MTU_SIZE buffers, so it must be clamped. +TEST(MTUNegotiation, ForgedOversizedMTUFromTheWireIsClamped) +{ + SimulatedNetwork net; + RakPeer *server = net.AddPeer(8, 8); + + const int mtu = ForgeRequest2(net, server, 0xFFFF, 0x00C0FFEE00C0FFEEull); + ASSERT_NE(-1, mtu) << "the server never accepted the forged handshake packet"; + EXPECT_EQ(MAXIMUM_MTU_SIZE, mtu) << "a peer-supplied MTU of 0xFFFF was not clamped"; +} + +// The other half of the clamp: an in-range size survives untouched. 1024 is a +// real rung and deliberately not defaultMTUSize (see ForgeRequest2). +TEST(MTUNegotiation, ForgedInRangeMTUIsKept) +{ + SimulatedNetwork net; + RakPeer *server = net.AddPeer(8, 8); + ASSERT_NE(1024, server->GetMTUSize(UNASSIGNED_SYSTEM_ADDRESS)); + + const int mtu = ForgeRequest2(net, server, 1024, 0x00DEFACED00DEFACull); + ASSERT_NE(-1, mtu) << "the server never accepted the forged handshake packet"; + EXPECT_EQ(1024, mtu) << "an MTU within the limit was altered"; +} diff --git a/Tests/Unit/MaximumConnectTests.cpp b/Tests/Unit/MaximumConnectTests.cpp new file mode 100644 index 000000000..6d12ba923 --- /dev/null +++ b/Tests/Unit/MaximumConnectTests.cpp @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2026, MafiaHub + * + * This source code is licensed under the MIT-style license found in the + * license.txt file in the root directory of this source tree. + * + * Eight peers on the SimulatedNetwork all try to connect to each other while + * each allows only four connections: every extra connection must be refused. + */ + +#include + +#include + +#include "SimulatedNetwork.h" + +#include "mafianet/message_identifiers.h" + +using namespace MafiaNet; + +namespace +{ + const int kPeerNum = 8; + const unsigned short kMaxConnections = 4; + + int ConnectionCount(RakPeer *peer) + { + DataStructures::List systemList; + DataStructures::List guidList; + peer->GetSystemList(systemList, guidList); + return (int) guidList.Size(); + } +} + +TEST(MaximumConnectSim, GetMaximumIncomingConnectionsReturnsTheSetValue) +{ + SimulatedNetwork net; + RakPeer *peer = net.AddPeer(kMaxConnections, kMaxConnections); + EXPECT_EQ(kMaxConnections, peer->GetMaximumIncomingConnections()); + + peer->SetMaximumIncomingConnections(2); + EXPECT_EQ(2u, peer->GetMaximumIncomingConnections()); +} + +TEST(MaximumConnectSim, MeshNeverExceedsTheConnectionLimit) +{ + SimulatedNetwork net; + RakPeer *peers[kPeerNum]; + for (int i = 0; i < kPeerNum; ++i) + peers[i] = net.AddPeer(kMaxConnections, kMaxConnections); + + for (int i = 0; i < kPeerNum; ++i) + for (int j = i + 1; j < kPeerNum; ++j) + ASSERT_EQ(CONNECTION_ATTEMPT_STARTED, net.StartConnect(peers[i], peers[j])) << i << " -> " << j; + + // The limit must hold at every instant, not only once things settle. + const auto allAttemptsResolved = [&] { + for (int i = 0; i < kPeerNum; ++i) + for (int j = 0; j < kPeerNum; ++j) + { + if (i == j) + continue; + const ConnectionState state = peers[i]->GetConnectionState(net.AddressOf(peers[j])); + if (state == IS_PENDING || state == IS_CONNECTING || state == IS_DISCONNECTING || + state == IS_SILENTLY_DISCONNECTING) + return false; + } + return true; + }; + int worst = 0; + const bool resolved = net.RunUntil([&] { + for (int i = 0; i < kPeerNum; ++i) + worst = std::max(worst, ConnectionCount(peers[i])); + return allAttemptsResolved(); + }, 20000); + EXPECT_LE(worst, (int) kMaxConnections) << "an extra connection was allowed at some point"; + ASSERT_TRUE(resolved) << "connection attempts still pending after 20s"; + + int total = 0; + for (int i = 0; i < kPeerNum; ++i) + { + const int connections = ConnectionCount(peers[i]); + EXPECT_LE(connections, (int) kMaxConnections) << "peer " << i << " has " << connections << " connections"; + total += connections; + + // Both ends agree on every link. + for (int j = 0; j < kPeerNum; ++j) + if (i != j) + EXPECT_EQ(peers[i]->GetConnectionState(net.AddressOf(peers[j])) == IS_CONNECTED, + peers[j]->GetConnectionState(net.AddressOf(peers[i])) == IS_CONNECTED) + << "asymmetric link " << i << " <-> " << j; + } + // A full mesh would need 7 per peer: some attempts were refused, but the + // peers did connect rather than refusing everything. (Not every peer + // reaches the limit: half-open incoming handshakes from peers that then + // ran out of slots hold a target's slots until they time out, and its own + // attempts fail meanwhile. Nobody retries in this scenario.) + EXPECT_LT(total, kPeerNum * (kPeerNum - 1)); + EXPECT_GT(total, 0); +} diff --git a/Tests/Unit/OfflineMessagesConvertTests.cpp b/Tests/Unit/OfflineMessagesConvertTests.cpp new file mode 100644 index 000000000..d11520fc9 --- /dev/null +++ b/Tests/Unit/OfflineMessagesConvertTests.cpp @@ -0,0 +1,131 @@ +/* + * Copyright (c) 2026, MafiaHub + * + * This source code is licensed under the MIT-style license found in the + * license.txt file in the root directory of this source tree. + * + * Messages to systems you are not connected to: SetOfflinePingResponse / + * GetOfflinePingResponse, AdvertiseSystem and the unconnected Ping, over the + * in-process SimulatedNetwork. + */ + +#include + +#include "SimulatedNetwork.h" + +#include "mafianet/bit_stream.h" +#include "mafianet/message_identifiers.h" + +using namespace MafiaNet; + +namespace +{ + const char kPingData[] = "Offline Ping Data"; + const char kAdvertiseData[] = "hello world"; + + void HostOf(SimulatedNetwork &net, const RakPeer *peer, char (&host)[64]) + { + net.AddressOf(peer).ToString(false, host, sizeof(host)); + } +} + +TEST(OfflineMessagesConvert, GetOfflinePingResponseReturnsWhatWasSet) +{ + SimulatedNetwork net; + RakPeer *peer = net.AddPeer(1, 1); + + peer->SetOfflinePingResponse(kPingData, (unsigned int) sizeof(kPingData)); + char *data = nullptr; + unsigned int length = 0; + peer->GetOfflinePingResponse(&data, &length); + ASSERT_NE(nullptr, data); + EXPECT_EQ(sizeof(kPingData), length); + EXPECT_STREQ(kPingData, data); +} + +TEST(OfflineMessagesConvert, AdvertiseSystemDeliversItsDataUnconnected) +{ + SimulatedNetwork net; + RakPeer *peer1 = net.AddPeer(1, 1); + RakPeer *peer2 = net.AddPeer(1); + + char host[64]; + HostOf(net, peer2, host); + ASSERT_TRUE(peer1->AdvertiseSystem(host, net.AddressOf(peer2).GetPort(), kAdvertiseData, (int) sizeof(kAdvertiseData))); + + ASSERT_TRUE(net.RunUntilReceived(peer2, ID_ADVERTISE_SYSTEM, 1000)); + const SimulatedNetwork::Message *advert = net.Find(peer2, ID_ADVERTISE_SYSTEM); + ASSERT_EQ(1u + sizeof(kAdvertiseData), advert->data.size()) << "Got Advertise system with unexpected data"; + EXPECT_STREQ(kAdvertiseData, (const char *) advert->data.data() + 1); + EXPECT_EQ(net.AddressOf(peer1), advert->systemAddress); + EXPECT_EQ(peer1->GetMyGUID(), advert->guid); + EXPECT_EQ(IS_NOT_CONNECTED, peer2->GetConnectionState(net.AddressOf(peer1))); +} + +// The pong carries the ping's own send time back, then the responder's offline +// ping response. With a virtual clock both are exact. +TEST(OfflineMessagesConvert, UnconnectedPingIsAnsweredWithTheOfflinePingResponse) +{ + SimulatedNetwork net; + RakPeer *peer1 = net.AddPeer(1, 1); + RakPeer *peer2 = net.AddPeer(1); + peer1->SetOfflinePingResponse(kPingData, (unsigned int) sizeof(kPingData)); + + char host[64]; + HostOf(net, peer1, host); + const TimeMS sentAt = (TimeMS) (net.Now() / 1000); + ASSERT_TRUE(peer2->Ping(host, net.AddressOf(peer1).GetPort(), false)); + + ASSERT_TRUE(net.RunUntilReceived(peer2, ID_UNCONNECTED_PONG, 1000)); + const SimulatedNetwork::Message *pong = net.Find(peer2, ID_UNCONNECTED_PONG); + ASSERT_EQ(1u + sizeof(TimeMS) + sizeof(kPingData), pong->data.size()); + + // Written with BitStream (network byte order), so read it the same way. + BitStream bs(const_cast(pong->data.data()), (unsigned int) pong->data.size(), false); + bs.IgnoreBytes(1); + TimeMS echoedTime = 0; + ASSERT_TRUE(bs.Read(echoedTime)); + EXPECT_EQ(sentAt, echoedTime); + EXPECT_STREQ(kPingData, (const char *) pong->data.data() + 1 + sizeof(TimeMS)) << "Received wrong offline ping response"; + EXPECT_EQ(net.AddressOf(peer1), pong->systemAddress); + EXPECT_EQ(peer1->GetMyGUID(), pong->guid); +} + +// A responder with no offline ping response still answers, with no data. +TEST(OfflineMessagesConvert, UnconnectedPingWithoutAResponseGetsAnEmptyPong) +{ + SimulatedNetwork net; + RakPeer *peer1 = net.AddPeer(1, 1); + RakPeer *peer2 = net.AddPeer(1); + + char host[64]; + HostOf(net, peer1, host); + ASSERT_TRUE(peer2->Ping(host, net.AddressOf(peer1).GetPort(), false)); + + ASSERT_TRUE(net.RunUntilReceived(peer2, ID_UNCONNECTED_PONG, 1000)); + EXPECT_EQ(1u + sizeof(TimeMS), net.Find(peer2, ID_UNCONNECTED_PONG)->data.size()); +} + +// The legacy sequence: advertise, and answer the advert with a ping. +TEST(OfflineMessagesConvert, AdvertiseSystemAndOfflinePing) +{ + SimulatedNetwork net; + RakPeer *peer1 = net.AddPeer(1, 1); + RakPeer *peer2 = net.AddPeer(1); + peer1->SetOfflinePingResponse(kPingData, (unsigned int) sizeof(kPingData)); + + char host[64]; + HostOf(net, peer2, host); + ASSERT_TRUE(peer1->AdvertiseSystem(host, net.AddressOf(peer2).GetPort(), kAdvertiseData, (int) sizeof(kAdvertiseData))); + ASSERT_TRUE(net.RunUntilReceived(peer2, ID_ADVERTISE_SYSTEM, 1000)) << "Never got proper offline data"; + EXPECT_STREQ(kAdvertiseData, (const char *) net.Find(peer2, ID_ADVERTISE_SYSTEM)->data.data() + 1); + + // Reply to whoever advertised, at the address the advert came from. + const SystemAddress advertiser = net.Find(peer2, ID_ADVERTISE_SYSTEM)->systemAddress; + char advertiserHost[64]; + advertiser.ToString(false, advertiserHost, sizeof(advertiserHost)); + ASSERT_TRUE(peer2->Ping(advertiserHost, advertiser.GetPort(), false)); + + ASSERT_TRUE(net.RunUntilReceived(peer2, ID_UNCONNECTED_PONG, 1000)) << "Never got proper ping data"; + EXPECT_STREQ(kPingData, (const char *) net.Find(peer2, ID_UNCONNECTED_PONG)->data.data() + 1 + sizeof(TimeMS)); +} diff --git a/Tests/Unit/PeerConnectDisconnectTests.cpp b/Tests/Unit/PeerConnectDisconnectTests.cpp new file mode 100644 index 000000000..f32cd16cc --- /dev/null +++ b/Tests/Unit/PeerConnectDisconnectTests.cpp @@ -0,0 +1,153 @@ +/* + * Copyright (c) 2026, MafiaHub + * + * This source code is licensed under the MIT-style license found in the + * license.txt file in the root directory of this source tree. + * + * Eight peers on the SimulatedNetwork connect to each other, all disconnect, + * connect again, over and over; at the end the full mesh must come back. + */ + +#include + +#include "SimulatedNetwork.h" + +#include "mafianet/message_identifiers.h" + +using namespace MafiaNet; + +namespace +{ + const int kPeerNum = 8; + const unsigned short kMaxConnections = kPeerNum * 3; // generous, to rule out the limit + const int kCycles = 25; + + class PeerConnectDisconnectSim : public ::testing::Test + { + protected: + void SetUp() override + { + for (int i = 0; i < kPeerNum; ++i) + peers[i] = net.AddPeer(kMaxConnections, kMaxConnections); + } + + ConnectionState StateOf(int i, int j) + { + return peers[i]->GetConnectionState(net.AddressOf(peers[j])); + } + + // Connected, or something is already under way for this link. + bool BusyOrConnected(int i, int j) + { + const ConnectionState state = StateOf(i, j); + return state == IS_CONNECTED || state == IS_CONNECTING || state == IS_PENDING || state == IS_DISCONNECTING; + } + + int ConnectionCount(int i) + { + DataStructures::List systemList; + DataStructures::List guidList; + peers[i]->GetSystemList(systemList, guidList); + return (int) guidList.Size(); + } + + void CloseAllConnections() + { + for (int i = 0; i < kPeerNum; ++i) + { + DataStructures::List systemList; + DataStructures::List guidList; + peers[i]->GetSystemList(systemList, guidList); + for (unsigned j = 0; j < systemList.Size(); ++j) + peers[i]->CloseConnection(systemList[j], true, 0, Priority::Low); + } + } + + // No outgoing attempt (i -> j, j > i) is still connecting or pending. + bool ConnectionRequestsComplete() + { + for (int i = 0; i < kPeerNum; ++i) + for (int j = i + 1; j < kPeerNum; ++j) + { + const ConnectionState state = StateOf(i, j); + if (state == IS_CONNECTING || state == IS_PENDING) + return false; + } + return true; + } + + SimulatedNetwork net; + RakPeer *peers[kPeerNum] = {}; + }; +} + +TEST_F(PeerConnectDisconnectSim, MeshReconnectsAfterRepeatedDisconnects) +{ + for (int i = 0; i < kPeerNum; ++i) + for (int j = i + 1; j < kPeerNum; ++j) + ASSERT_EQ(CONNECTION_ATTEMPT_STARTED, net.StartConnect(peers[i], peers[j])) << i << " -> " << j; + + for (int cycle = 0; cycle < kCycles; ++cycle) + { + CloseAllConnections(); + net.Step(100); + + for (int i = 0; i < kPeerNum; ++i) + for (int j = i + 1; j < kPeerNum; ++j) + if (!BusyOrConnected(i, j)) + ASSERT_EQ(CONNECTION_ATTEMPT_STARTED, net.StartConnect(peers[i], peers[j])) + << "cycle " << cycle << ": " << i << " -> " << j << " in state " << StateOf(i, j); + + ASSERT_TRUE(net.RunUntil([&] { return ConnectionRequestsComplete(); }, 10000)) + << "cycle " << cycle << ": connection requests never completed"; + } + + // Bring the full mesh back up, re-initiating any link that dropped out. + bool allConnected = false; + int failedPeer = -1; + int failedPeerConns = 0; + const TimeUS convergeStart = net.Now(); + while (net.Now() - convergeStart < (TimeUS) 30000 * 1000) + { + for (int i = 0; i < kPeerNum; ++i) + for (int j = i + 1; j < kPeerNum; ++j) + if (!BusyOrConnected(i, j)) + { + // STARTED is success; the other two are benign races with + // a link that came up in between. + const ConnectionAttemptResult result = net.StartConnect(peers[i], peers[j]); + ASSERT_TRUE(result == CONNECTION_ATTEMPT_STARTED || result == ALREADY_CONNECTED_TO_ENDPOINT || + result == CONNECTION_ATTEMPT_ALREADY_IN_PROGRESS) + << i << " -> " << j << " failed with result " << (int) result; + } + + ASSERT_TRUE(net.RunUntil([&] { return ConnectionRequestsComplete(); }, 10000)); + + allConnected = true; + for (int i = 0; i < kPeerNum && allConnected; ++i) + if (ConnectionCount(i) != kPeerNum - 1) + { + allConnected = false; + failedPeer = i; + failedPeerConns = ConnectionCount(i); + } + if (allConnected) + break; + net.Step(100); + } + + ASSERT_TRUE(allConnected) << "not all peers reconnected: peer " << failedPeer << " has " << failedPeerConns << " connections"; + + // The churn really happened (every cycle tore down and rebuilt every + // link), and both ends saw each handshake. + const int kPairs = kPeerNum * (kPeerNum - 1) / 2; + int incoming = 0, accepted = 0; + for (int i = 0; i < kPeerNum; ++i) + for (const auto &message : net.Inbox(peers[i])) + { + incoming += message.id == ID_NEW_INCOMING_CONNECTION; + accepted += message.id == ID_CONNECTION_REQUEST_ACCEPTED; + } + EXPECT_EQ(incoming, accepted); + EXPECT_GE(accepted, kPairs * kCycles); +} diff --git a/Tests/Unit/PeerConnectDisconnectWithCancelPendingTests.cpp b/Tests/Unit/PeerConnectDisconnectWithCancelPendingTests.cpp new file mode 100644 index 000000000..21b5ea480 --- /dev/null +++ b/Tests/Unit/PeerConnectDisconnectWithCancelPendingTests.cpp @@ -0,0 +1,155 @@ +/* + * Copyright (c) 2026, MafiaHub + * + * This source code is licensed under the MIT-style license found in the + * license.txt file in the root directory of this source tree. + * + * Eight peers on the SimulatedNetwork repeatedly disconnect, cancel any + * pending connection attempt and connect again; at the end the full mesh + * must come back. Also checks CancelConnectionAttempt on its own. + */ + +#include + +#include "SimulatedNetwork.h" + +#include "mafianet/message_identifiers.h" + +using namespace MafiaNet; + +namespace +{ + const int kPeerNum = 8; + const unsigned short kMaxConnections = kPeerNum * 3; // generous, to rule out the limit + const int kCycles = 25; + + class PeerConnectDisconnectWithCancelPendingSim : public ::testing::Test + { + protected: + void SetUp() override + { + for (int i = 0; i < kPeerNum; ++i) + peers[i] = net.AddPeer(kMaxConnections, kMaxConnections); + } + + ConnectionState StateOf(int i, int j) + { + return peers[i]->GetConnectionState(net.AddressOf(peers[j])); + } + + int ConnectionCount(int i) + { + DataStructures::List systemList; + DataStructures::List guidList; + peers[i]->GetSystemList(systemList, guidList); + return (int) guidList.Size(); + } + + void CloseAllConnections() + { + for (int i = 0; i < kPeerNum; ++i) + { + DataStructures::List systemList; + DataStructures::List guidList; + peers[i]->GetSystemList(systemList, guidList); + for (unsigned j = 0; j < systemList.Size(); ++j) + peers[i]->CloseConnection(systemList[j], true, 0, Priority::Low); + } + } + + void CancelAllAttempts() + { + for (int i = 0; i < kPeerNum; ++i) + for (int j = i + 1; j < kPeerNum; ++j) + peers[i]->CancelConnectionAttempt(net.AddressOf(peers[j])); + } + + SimulatedNetwork net; + RakPeer *peers[kPeerNum] = {}; + }; +} + +TEST_F(PeerConnectDisconnectWithCancelPendingSim, MeshReconnectsWithCancelledPendingAttempts) +{ + for (int i = 0; i < kPeerNum; ++i) + for (int j = i + 1; j < kPeerNum; ++j) + ASSERT_EQ(CONNECTION_ATTEMPT_STARTED, net.StartConnect(peers[i], peers[j])) << i << " -> " << j; + + for (int cycle = 0; cycle < kCycles; ++cycle) + { + CloseAllConnections(); + net.Step(100); + CancelAllAttempts(); + net.Step(100); + + for (int i = 0; i < kPeerNum; ++i) + for (int j = i + 1; j < kPeerNum; ++j) + { + if (net.StartConnect(peers[i], peers[j]) == CONNECTION_ATTEMPT_STARTED) + continue; + // Refused: the cancel must have dropped any pending attempt, so + // the only acceptable reason is an existing connection. + const ConnectionState state = StateOf(i, j); + ASSERT_FALSE(state == IS_CONNECTING || state == IS_PENDING) + << "cycle " << cycle << ": pending attempt " << i << " -> " << j << " was not cancelled"; + ASSERT_TRUE(state == IS_CONNECTED || state == IS_DISCONNECTING) + << "cycle " << cycle << ": Connect " << i << " -> " << j << " refused in state " << state; + } + } + + // Let the last disconnects and handshakes play out. + net.Step(2000); + + for (int i = 0; i < kPeerNum; ++i) + for (int j = i + 1; j < kPeerNum; ++j) + { + peers[i]->CancelConnectionAttempt(net.AddressOf(peers[j])); + if (net.StartConnect(peers[i], peers[j]) != CONNECTION_ATTEMPT_STARTED) + ASSERT_NE(0, ConnectionCount(i)) << "Connect " << i << " -> " << j << " refused with no connection at all"; + } + + net.RunUntil([&] { + for (int i = 0; i < kPeerNum; ++i) + if (ConnectionCount(i) != kPeerNum - 1) + return false; + return true; + }, 5000); + for (int i = 0; i < kPeerNum; ++i) + EXPECT_EQ(kPeerNum - 1, ConnectionCount(i)) << "peer " << i << " did not reconnect to all peers"; + + // The churn really happened, and both ends saw each handshake. A link + // still handshaking when a cycle closes connections survives it, so every + // other cycle rebuilds the mesh. + const int kPairs = kPeerNum * (kPeerNum - 1) / 2; + int incoming = 0, accepted = 0; + for (int i = 0; i < kPeerNum; ++i) + for (const auto &message : net.Inbox(peers[i])) + { + incoming += message.id == ID_NEW_INCOMING_CONNECTION; + accepted += message.id == ID_CONNECTION_REQUEST_ACCEPTED; + } + EXPECT_EQ(incoming, accepted); + EXPECT_GE(accepted, kPairs * kCycles / 2); +} + +TEST(CancelConnectionAttemptSim, CancelledAttemptNeverConnects) +{ + SimulatedNetwork net; + RakPeer *server = net.AddPeer(1, 1); + RakPeer *client = net.AddPeer(1); + const SystemAddress serverAddress = net.AddressOf(server); + + ASSERT_EQ(CONNECTION_ATTEMPT_STARTED, net.StartConnect(client, server)); + EXPECT_EQ(IS_PENDING, client->GetConnectionState(serverAddress)); + client->CancelConnectionAttempt(serverAddress); + EXPECT_EQ(IS_NOT_CONNECTED, client->GetConnectionState(serverAddress)); + + net.Step(2000); + EXPECT_FALSE(net.Received(client, ID_CONNECTION_REQUEST_ACCEPTED)); + EXPECT_FALSE(net.Received(client, ID_CONNECTION_ATTEMPT_FAILED)); + EXPECT_FALSE(net.Received(server, ID_NEW_INCOMING_CONNECTION)); + EXPECT_EQ(IS_NOT_CONNECTED, client->GetConnectionState(serverAddress)); + + // The slot is free again: a fresh attempt goes through. + ASSERT_TRUE(net.Connect(client, server)); +} diff --git a/Tests/Unit/PingTestsTests.cpp b/Tests/Unit/PingTestsTests.cpp new file mode 100644 index 000000000..e0f2dba92 --- /dev/null +++ b/Tests/Unit/PingTestsTests.cpp @@ -0,0 +1,187 @@ +/* + * Copyright (c) 2026, MafiaHub + * + * This source code is licensed under the MIT-style license found in the + * license.txt file in the root directory of this source tree. + * + * GetAveragePing / GetLastPing / GetLowestPing and SetOccasionalPing, over the + * in-process SimulatedNetwork. + * + * On loopback these could only be held to loose sanity bounds, because the + * measured RTT was the CPU scheduler's. Here the clock is virtual and the link + * delay fixed, so every ping sample is the same exact number and the + * statistics are asserted exactly. + */ + +#include + +#include "SimulatedNetwork.h" + +#include "mafianet/internal_packet.h" +#include "mafianet/message_identifiers.h" +#include "mafianet/plugin_interface2.h" + +using namespace MafiaNet; + +namespace +{ + const uint32_t kOneWayDelayMs = 20; + + // The round trip on the wire plus two 1 ms ticks of turnaround: the ping is + // stamped between steps and goes out on the next tick, and the receiver + // answers it on the tick after the one it arrived on. + const int kTurnaroundMs = 2; + const int kExpectedRttMs = kTurnaroundMs + 2 * (int) kOneWayDelayMs; + + // Counts ID_CONNECTED_PING arriving at the peer it is attached to, per + // sender. SetOccasionalPing has no public observable -- the pings it + // triggers are consumed inside the library -- but OnInternalPacket sees them. + class ConnectedPingCounter : public PluginInterface2 + { + public: + SystemAddress senderA; + SystemAddress senderB; + int countA = 0; + int countB = 0; + + bool UsesReliabilityLayer(void) const override { return true; } + + void OnInternalPacket(InternalPacket *internalPacket, unsigned frameNumber, + SystemAddress remoteSystemAddress, MafiaNet::TimeMS time, int isSend) override + { + (void) frameNumber; + (void) time; + if (isSend || internalPacket == nullptr || internalPacket->data == nullptr || internalPacket->dataBitLength < 8) + return; + if (internalPacket->data[0] != (unsigned char) ID_CONNECTED_PING) + return; + if (remoteSystemAddress == senderA) + ++countA; + else if (remoteSystemAddress == senderB) + ++countB; + } + }; + + SimulatedNetwork::LinkRule FixedDelay() + { + SimulatedNetwork::LinkRule rule; + rule.delayMs = kOneWayDelayMs; + return rule; + } + + // Calls Ping() every intervalMs for durationMs of simulated time. + void PingRepeatedly(SimulatedNetwork &net, RakPeer *from, const SystemAddress &to, uint32_t intervalMs, uint32_t durationMs) + { + for (uint32_t elapsed = 0; elapsed < durationMs; elapsed += intervalMs) + { + from->Ping(to); + net.Step(intervalMs); + } + } +} + +TEST(PingTests, NoStatisticsForAnUnknownSystem) +{ + SimulatedNetwork net; + RakPeer *peer = net.AddPeer(1); + const SystemAddress nobody("10.0.0.99", 60000); + EXPECT_EQ(-1, peer->GetAveragePing(nobody)); + EXPECT_EQ(-1, peer->GetLastPing(nobody)); + EXPECT_EQ(-1, peer->GetLowestPing(nobody)); +} + +// Explicit pings every 30 ms for 1.5 s with occasional ping off: every sample +// is the same RTT, so last == lowest == average == that RTT. +TEST(PingTests, ExplicitPingStatisticsMatchTheLinkDelayExactly) +{ + SimulatedNetwork net; + RakPeer *receiver = net.AddPeer(2, 2); + RakPeer *sender = net.AddPeer(1); + net.SetLinkBothWays(sender, receiver, FixedDelay()); + ASSERT_TRUE(net.Connect(sender, receiver)); + sender->SetOccasionalPing(false); + + const SystemAddress target = net.AddressOf(receiver); + PingRepeatedly(net, sender, target, 30, 1500); + net.Step(2 * kOneWayDelayMs + 10); // let the last pong land + + EXPECT_EQ(kExpectedRttMs, sender->GetLastPing(target)); + EXPECT_EQ(kExpectedRttMs, sender->GetLowestPing(target)); + EXPECT_EQ(kExpectedRttMs, sender->GetAveragePing(target)); +} + +// The statistics follow the link: the lowest ping keeps the best sample, the +// last ping tracks the newest, and the average sits in between. +TEST(PingTests, LowestLastAndAverageTrackALatencyChange) +{ + SimulatedNetwork net; + RakPeer *receiver = net.AddPeer(2, 2); + RakPeer *sender = net.AddPeer(1); + net.SetLinkBothWays(sender, receiver, FixedDelay()); + ASSERT_TRUE(net.Connect(sender, receiver)); + sender->SetOccasionalPing(false); + const SystemAddress target = net.AddressOf(receiver); + + PingRepeatedly(net, sender, target, 100, 1000); + net.Step(100); + + SimulatedNetwork::LinkRule slower; + slower.delayMs = 3 * kOneWayDelayMs; + net.SetLinkBothWays(sender, receiver, slower); + PingRepeatedly(net, sender, target, 200, 600); + net.Step(200); + + const int slowRtt = kTurnaroundMs + 2 * (int) slower.delayMs; + EXPECT_EQ(slowRtt, sender->GetLastPing(target)); + EXPECT_EQ(kExpectedRttMs, sender->GetLowestPing(target)); + const int average = sender->GetAveragePing(target); + EXPECT_GT(average, kExpectedRttMs); + EXPECT_LT(average, slowRtt); +} + +// The legacy second phase: a fresh connection with occasional ping on and no +// explicit pings still has exact statistics. +TEST(PingTests, OccasionalPingAloneProducesExactStatistics) +{ + SimulatedNetwork net; + RakPeer *receiver = net.AddPeer(2, 2); + RakPeer *sender = net.AddPeer(1); + net.SetLinkBothWays(sender, receiver, FixedDelay()); + sender->SetOccasionalPing(true); + ASSERT_TRUE(net.Connect(sender, receiver)); + net.Step(1500); + + const SystemAddress target = net.AddressOf(receiver); + EXPECT_EQ(kExpectedRttMs, sender->GetAveragePing(target)); + EXPECT_EQ(kExpectedRttMs, sender->GetLastPing(target)); + EXPECT_EQ(kExpectedRttMs, sender->GetLowestPing(target)); +} + +// SetOccasionalPing observed directly: two senders against one receiver in the +// same window, one with it on and one off. The interval is 5 s and the window +// 11 s, so exactly two pings are due from the enabled sender and none from the +// disabled one (it pings only until it has its first sample). +TEST(PingTests, OccasionalPingIsObservableAsPingTraffic) +{ + ConnectedPingCounter counter; // outlives the network, which shuts the peers down + SimulatedNetwork net; + RakPeer *receiver = net.AddPeer(2, 2); + RakPeer *pinging = net.AddPeer(1); + RakPeer *quiet = net.AddPeer(1); + receiver->AttachPlugin(&counter); + counter.senderA = net.AddressOf(pinging); + counter.senderB = net.AddressOf(quiet); + + pinging->SetOccasionalPing(true); + quiet->SetOccasionalPing(false); + ASSERT_TRUE(net.Connect(pinging, receiver)); + ASSERT_TRUE(net.Connect(quiet, receiver)); + net.Step(500); // let the post-connect pings settle + + counter.countA = 0; + counter.countB = 0; + net.Step(11000); + + EXPECT_EQ(2, counter.countA) << "SetOccasionalPing(true) should ping every 5 s"; + EXPECT_EQ(0, counter.countB) << "SetOccasionalPing(false) should stop after the first sample"; +} diff --git a/Tests/Unit/ReliableOrderedConvertedTests.cpp b/Tests/Unit/ReliableOrderedConvertedTests.cpp new file mode 100644 index 000000000..713dfd19a --- /dev/null +++ b/Tests/Unit/ReliableOrderedConvertedTests.cpp @@ -0,0 +1,138 @@ +/* + * Copyright (c) 2026, MafiaHub + * + * This source code is licensed under the MIT-style license found in the + * license.txt file in the root directory of this source tree. + * + * Ported from Samples/Tests/ReliableOrderedConvertedTest.cpp, by way of the + * loopback integration test of the same name, onto the in-process + * SimulatedNetwork (Tests/Support/SimulatedNetwork.h). + * + * A sender streams ID_USER_PACKET_ENUM+1 messages every 30 ms for 12 s of + * simulated time, each with a 1..5000 byte pad (so many of them are split), + * on Reliability::ReliableOrdered channel 0. The receiver checks the sequence + * number and the length of every message. + */ + +#include + +#include +#include + +#include "SimulatedNetwork.h" + +#include "mafianet/bit_stream.h" +#include "mafianet/message_identifiers.h" + +using namespace MafiaNet; + +namespace +{ + const MessageID kStreamId = ID_USER_PACKET_ENUM + 1; + const unsigned char kStreamNumber = 0; + const uint32_t kSendIntervalMs = 30; + const uint32_t kStreamDurationMs = 12000; + + struct Sent + { + uint32_t number; + uint32_t length; + }; + + // Deterministic stand-in for the original's randomMT() pad length. + uint32_t NextPadLength(uint32_t &state) + { + state = state * 1664525u + 1013904223u; + return ((state >> 8) % 5000u) + 1u; + } + + // Streams messages for kStreamDurationMs of simulated time, then waits for + // the tail to drain, and checks every received message against what was + // sent: same count, strictly increasing sequence from 0, same length. + void StreamAndVerify(SimulatedNetwork &net, RakPeer *sender, RakPeer *receiver, uint32_t drainBudgetMs) + { + std::vector sent; + uint32_t padState = 12345; + const SystemAddress to = net.AddressOf(receiver); + + for (uint32_t elapsed = 0; elapsed < kStreamDurationMs; elapsed += kSendIntervalMs) + { + const uint32_t number = (uint32_t) sent.size(); + const uint32_t padLength = NextPadLength(padState); + std::vector pad(padLength, (char) (number & 0xFF)); + + BitStream bs; + bs.Write(kStreamId); + bs.Write(number); + bs.Write(kStreamNumber); + bs.Write((uint32_t) (net.Now() / 1000)); + bs.Write(pad.data(), padLength); + ASSERT_NE(0u, sender->Send(&bs, Priority::High, Reliability::ReliableOrdered, kStreamNumber, to, false)); + sent.push_back(Sent{number, (uint32_t) bs.GetNumberOfBytesUsed()}); + + net.Step(kSendIntervalMs); + } + ASSERT_GT(sent.size(), 300u); + + auto countReceived = [&] { + size_t n = 0; + for (const auto &message : net.Inbox(receiver)) + n += message.id == kStreamId; + return n; + }; + ASSERT_TRUE(net.RunUntil([&] { return countReceived() >= sent.size(); }, drainBudgetMs)) + << "received " << countReceived() << " of " << sent.size(); + + size_t expected = 0; + for (const auto &message : net.Inbox(receiver)) + { + if (message.id != kStreamId) + continue; + ASSERT_LT(expected, sent.size()) << "more messages arrived than were sent"; + + BitStream bs(const_cast(message.data.data()), (unsigned int) message.data.size(), false); + bs.IgnoreBits(8); + uint32_t number = 0; + unsigned char stream = 0xFF; + ASSERT_TRUE(bs.Read(number)); + ASSERT_TRUE(bs.Read(stream)); + + ASSERT_EQ(sent[expected].number, number) + << "Expecting " << sent[expected].number << " got " << number << " (channel " << (int) stream << ")"; + EXPECT_EQ(kStreamNumber, stream); + ASSERT_EQ(sent[expected].length, message.data.size()) << "length mismatch on message " << number; + ++expected; + } + EXPECT_EQ(sent.size(), expected); + } +} + +// The original scenario: a clean link, every message in order and intact. +TEST(ReliableOrderedConverted, AllPacketsReceivedInOrder) +{ + SimulatedNetwork net; + RakPeer *receiver = net.AddPeer(32, 32); + RakPeer *sender = net.AddPeer(1); + ASSERT_TRUE(net.Connect(sender, receiver)); + + StreamAndVerify(net, sender, receiver, 10000); +} + +// The original had its network simulator calls commented out; the simulated +// network makes the lossy, reordering variant cheap and deterministic. +TEST(ReliableOrderedConverted, AllPacketsReceivedInOrderOverALossyReorderingLink) +{ + SimulatedNetwork net(99); + RakPeer *receiver = net.AddPeer(32, 32); + RakPeer *sender = net.AddPeer(1); + ASSERT_TRUE(net.Connect(sender, receiver)); + + SimulatedNetwork::LinkRule lossy; + lossy.lossPercent = 5; + lossy.delayMs = 25; + lossy.jitterMs = 30; + net.SetLinkBothWays(sender, receiver, lossy); + + StreamAndVerify(net, sender, receiver, 60000); + EXPECT_GT(net.DatagramsDropped(), 0u) << "the link rule never dropped anything; the variant tested nothing"; +} diff --git a/Tests/Unit/SessionConfigSimulatedTests.cpp b/Tests/Unit/SessionConfigSimulatedTests.cpp new file mode 100644 index 000000000..aad18cb9c --- /dev/null +++ b/Tests/Unit/SessionConfigSimulatedTests.cpp @@ -0,0 +1,1035 @@ +/* + * Copyright (c) 2026, MafiaHub + * + * This source code is licensed under the MIT-style license found in the + * license.txt file in the root directory of this source tree. + * + * The session handshake over the in-process SimulatedNetwork: whole RakPeer + * instances, no sockets, no threads, a virtual clock. See + * Tests/Support/SimulatedNetwork.h. + */ + +#include + +#include +#include +#include +#include +#include + +#include "SimulatedNetwork.h" + +#include "mafianet/bit_stream.h" +#include "mafianet/message_identifiers.h" +#include "mafianet/peer.h" +#include "mafianet/peer_interface.h" + +using namespace MafiaNet; + +/* +Tests the session handshake: an opaque payload exchanged in both directions after the transport +connection is up but BEFORE either side reports a connection. + +The load-bearing property is not that the payload arrives -- it is that the connection packet is +withheld until it does. ID_CONNECTION_REQUEST_ACCEPTED means "the server's payload is in hand" and +ID_NEW_INCOMING_CONNECTION means "the client's payload is in hand", so an application cannot observe +a connection without its session data. + +These scenarios used to run over loopback with real threads (Tests/Integration/SessionConfigLiveTests.cpp) +and had to sample state racily and drain packets through helpers that could swallow the evidence a later +negative assertion needed. Here every peer runs on the test thread, one millisecond of virtual time per +update cycle, and everything every peer's Receive() returned stays in its inbox for the whole test. A +negative assertion therefore covers the entire scenario, not a window after a wait, and slot state is +read between every pair of update cycles rather than whenever the poll happened to come round. +*/ + +namespace +{ + typedef SimulatedNetwork::Message Message; + + const uint32_t kConnectTimeoutMs = 15000; + // Shortened from the 10s default, as in the loopback version. + const TimeMS kHandshakeTimeoutMs = 3000; + const MessageID kUserMessageId = ID_USER_PACKET_ENUM + 1; + + const char kServerPayload[] = "{\"season\":\"winter\",\"map_file\":\"downtown.m2map\"}"; + const char kClientPayload[] = "{\"build\":\"m2o|1.2.3\"}"; + + // Injects application-layer bytes onto an established connection while bypassing the public Send() + // handshake gate, standing in for a peer that does not respect the protocol. + // + // Needed because the gates for un-accepted peers mask each other between two conforming peers: the + // sender's broadcast filter and directed-send check mean nothing reaches the wire, so a negative + // assertion on the receiver holds whether or not the receiving-side check exists. + // + // Never instantiated: the peer comes from RakPeerInterface::GetInstance() (SimulatedNetwork::AddPeer + // uses it too), so it has the library's own layout. The derived type only names the protected member; + // the call goes through a pointer-to-member on the real RakPeer, because downcasting to this type + // would be undefined behaviour (UBSan's vptr check aborts on it). + // + // SendBuffered is the layer the public Send() drops into once its own checks pass, so the injected + // bytes are framed and sent by the next update cycle exactly like real traffic. + class SendGateBypass : public RakPeer + { + public: + static void Inject(RakPeer *peer, const BitStream &bs, const AddressOrGUID &target) + { + (peer->*&SendGateBypass::SendBuffered)( + (const char *) bs.GetData(), bs.GetNumberOfBitsUsed(), Priority::Immediate, + Reliability::ReliableOrdered, 0, target, false, + RemoteSystemStruct::NO_ACTION, 0); + } + }; + + // Reads a peer's own view of a connection slot. Never instantiated; see SendGateBypass. + class SlotInspector : public RakPeer + { + public: + struct Snapshot + { + bool sawSlot = false; + bool reported = false; + unsigned int payloadLength = 0; + }; + + // The tests using this have exactly one remote, so the first active slot is it. + static Snapshot FirstActiveSlot(const RakPeer *peer) + { + const RemoteSystemStruct *slots = peer->*&SlotInspector::remoteSystemList; + const unsigned int slotCount = peer->*&SlotInspector::maximumNumberOfPeers; + Snapshot out; + if (slots == nullptr) + return out; + for (unsigned int i = 0; i < slotCount; ++i) + { + if (!slots[i].isActive) + continue; + out.sawSlot = true; + out.reported = slots[i].connectionReportedToApplication; + out.payloadLength = slots[i].remoteSessionConfigLength; + return out; + } + return out; + } + }; + + std::string IdList(const std::vector &inbox) + { + std::string out; + for (const Message &message : inbox) + { + if (!out.empty()) + out += ","; + out += std::to_string((int) message.id); + } + return "[" + out + "]"; + } + + class SessionConfigSim : public ::testing::Test + { + protected: + void TearDown() override + { + // Peers the fixture drives itself go before the network the fixture owns. + for (RakPeer *peer : manual) + { + if (peer->IsActive()) + peer->Shutdown(0); + RakPeerInterface::DestroyInstance(peer); + } + manual.clear(); + } + + // The server and first client of StartPeers() in the loopback version. + RakPeer *AddServer(unsigned int maxConnections, unsigned short maxIncoming, TimeMS timeoutMs = kHandshakeTimeoutMs) + { + RakPeer *peer = net.AddPeer(maxConnections, maxIncoming); + peer->SetTimeoutTime(timeoutMs, UNASSIGNED_SYSTEM_ADDRESS); + return peer; + } + + RakPeer *AddClient(TimeMS timeoutMs = kHandshakeTimeoutMs) + { + RakPeer *peer = net.AddPeer(1); + peer->SetTimeoutTime(timeoutMs, UNASSIGNED_SYSTEM_ADDRESS); + return peer; + } + + // A client the fixture drives itself rather than SimulatedNetwork, for two things the network's + // own peers cannot do: share a host with another peer (per-address limits), and Shutdown() in + // the middle of a test (the network keeps updating its peers until it is destroyed). + RakPeer *AddManualClient(const char *host, unsigned short port, TimeMS timeoutMs = kHandshakeTimeoutMs) + { + RakPeer *peer = static_cast(RakPeerInterface::GetInstance()); + manual.push_back(peer); + peer->SetSocketFactory(&net); + peer->SetManualUpdate(true); + peer->SetInternalID(SystemAddress(host, port), 0); + SocketDescriptor descriptor(port, host); + EXPECT_EQ(RAKNET_STARTED, peer->Startup(1, &descriptor, 1)); + peer->SetTimeoutTime(timeoutMs, UNASSIGNED_SYSTEM_ADDRESS); + manualInbox[peer]; + return peer; + } + + RakPeer *AddManualClientBeside(const RakPeer *neighbour, TimeMS timeoutMs = kHandshakeTimeoutMs) + { + const SystemAddress address = net.AddressOf(neighbour); + char host[64]; + address.ToString(false, host, sizeof(host)); + return AddManualClient(host, (unsigned short) (address.GetPort() + 1), timeoutMs); + } + + std::vector &Inbox(const RakPeer *peer) + { + auto it = manualInbox.find(peer); + return it != manualInbox.end() ? it->second : net.Inbox(peer); + } + + const Message *Find(const RakPeer *peer, MessageID id) + { + for (const Message &message : Inbox(peer)) + if (message.id == id) + return &message; + return nullptr; + } + + bool Has(const RakPeer *peer, MessageID id) { return Find(peer, id) != nullptr; } + + size_t Count(const RakPeer *peer, MessageID id) + { + size_t n = 0; + for (const Message &message : Inbox(peer)) + n += message.id == id ? 1 : 0; + return n; + } + + uint32_t NowMs() const { return (uint32_t) (net.Now() / 1000); } + + // One update cycle per millisecond for every peer. + void Pump(uint32_t ms) + { + for (uint32_t i = 0; i < ms; ++i) + { + net.Step(1); + for (RakPeer *peer : manual) + { + if (!peer->IsActive()) + continue; + peer->RunManualUpdate(); + for (Packet *packet = peer->Receive(); packet; packet = peer->Receive()) + { + Message message; + message.id = packet->length ? packet->data[0] : 0; + message.data.assign(packet->data, packet->data + packet->length); + message.systemAddress = packet->systemAddress; + message.guid = packet->guid; + manualInbox[peer].push_back(std::move(message)); + peer->DeallocatePacket(packet); + } + } + } + } + + // Pump until pred() holds, checking after every update cycle. + bool Until(const std::function &pred, uint32_t maxMs) + { + for (uint32_t elapsed = 0; elapsed < maxMs; ++elapsed) + { + if (pred()) + return true; + Pump(1); + } + return pred(); + } + + bool WaitFor(const RakPeer *peer, MessageID id, uint32_t maxMs = kConnectTimeoutMs) + { + return Until([&] { return Has(peer, id); }, maxMs); + } + + bool WaitForBoth(const RakPeer *a, MessageID aId, const RakPeer *b, MessageID bId, uint32_t maxMs = kConnectTimeoutMs) + { + return Until([&] { return Has(a, aId) && Has(b, bId); }, maxMs); + } + + // Waits for the session request on the server and hands back its guid. + bool WaitForRequest(const RakPeer *server, RakNetGUID &guid) + { + if (!WaitFor(server, ID_SESSION_CONFIG_REQUEST)) + return false; + guid = Find(server, ID_SESSION_CONFIG_REQUEST)->guid; + return true; + } + + ConnectionAttemptResult StartConnect(RakPeer *client, const RakPeer *server) + { + const SystemAddress target = net.AddressOf(server); + char host[64]; + target.ToString(false, host, sizeof(host)); + return client->Connect(host, target.GetPort(), nullptr, 0); + } + + // The pending-session pool setup: an interactive server with an incoming limit and a pool. + RakPeer *AddPooledServer(unsigned short maxIncoming, unsigned short pool, unsigned short perAddress, TimeMS timeoutMs = 60000) + { + RakPeer *peer = AddServer((unsigned int) maxIncoming + pool, maxIncoming, timeoutMs); + peer->SetSessionConfigInteractive(true); + peer->SetMaximumPendingSessions(pool, perAddress); + return peer; + } + + SimulatedNetwork net; + std::vector manual; + std::map > manualInbox; + }; + + void ExpectPayload(const char *got, unsigned int length, const char *expected, size_t expectedLength) + { + ASSERT_NE(got, nullptr); + ASSERT_EQ(length, (unsigned int) expectedLength); + EXPECT_EQ(0, std::memcmp(got, expected, length)); + } + + void WriteUserMessage(BitStream &bs, const char *text) + { + bs.Write(kUserMessageId); + bs.Write(text, (unsigned int) std::strlen(text)); + } + + bool CarriesText(const Message *message, const char *text) + { + const size_t n = std::strlen(text); + return message && message->data.size() == 1 + n && std::memcmp(message->data.data() + 1, text, n) == 0; + } +} // namespace + +// Static mode: no application code beyond SetSessionConfig. Each peer can read the other's payload at +// the instant its connection packet surfaces. +TEST_F(SessionConfigSim, StaticExchangeDeliversBothPayloads) +{ + RakPeer *server = AddServer(8, 8); + RakPeer *client = AddClient(); + server->SetSessionConfig(kServerPayload, (unsigned int) std::strlen(kServerPayload)); + client->SetSessionConfig(kClientPayload, (unsigned int) std::strlen(kClientPayload)); + ASSERT_EQ(CONNECTION_ATTEMPT_STARTED, StartConnect(client, server)); + + // Read the payload in the same cycle the connection packet surfaces, not some time after. + bool clientChecked = false; + bool serverChecked = false; + ASSERT_TRUE(Until([&] { + if (!clientChecked && Has(client, ID_CONNECTION_REQUEST_ACCEPTED)) + { + unsigned int length = 0; + const char *fromServer = client->GetRemoteSessionConfig(Find(client, ID_CONNECTION_REQUEST_ACCEPTED)->guid, &length); + ExpectPayload(fromServer, length, kServerPayload, std::strlen(kServerPayload)); + clientChecked = true; + } + if (!serverChecked && Has(server, ID_NEW_INCOMING_CONNECTION)) + { + unsigned int length = 0; + const char *fromClient = server->GetRemoteSessionConfig(Find(server, ID_NEW_INCOMING_CONNECTION)->guid, &length); + ExpectPayload(fromClient, length, kClientPayload, std::strlen(kClientPayload)); + serverChecked = true; + } + return clientChecked && serverChecked; + }, kConnectTimeoutMs)) << "client " << IdList(Inbox(client)) << " server " << IdList(Inbox(server)); +} + +// Static-mode gating: the server's connection packet is withheld while the library waits for the +// client's payload. In static mode correct code and code that released the packet at transport-up are +// indistinguishable to a poll, so this asserts the slot invariant "reported implies the whole payload is +// stored" between every pair of update cycles. The client payload is large enough to need many +// datagrams, so the handshake spends a long, observable time in the gated state. +TEST_F(SessionConfigSim, StaticModeWithholdsTheConnectionUntilTheClientPayloadArrives) +{ + std::string bigPayload; + bigPayload.reserve(48000); + while (bigPayload.size() < 48000) + bigPayload.push_back((char) ('a' + (int) (bigPayload.size() % 26))); + + RakPeer *server = AddServer(8, 8); + RakPeer *client = AddClient(); + server->SetSessionConfig(kServerPayload, (unsigned int) std::strlen(kServerPayload)); + client->SetSessionConfig(bigPayload.data(), (unsigned int) bigPayload.size()); + ASSERT_EQ(CONNECTION_ATTEMPT_STARTED, StartConnect(client, server)); + + unsigned int gatedCycles = 0; + unsigned int violations = 0; + bool reportedWithPayload = false; + for (uint32_t elapsed = 0; elapsed < kConnectTimeoutMs && !reportedWithPayload; ++elapsed) + { + Pump(1); + const SlotInspector::Snapshot snap = SlotInspector::FirstActiveSlot(server); + const bool packetSurfaced = Has(server, ID_NEW_INCOMING_CONNECTION); + if (snap.sawSlot && !snap.reported && snap.payloadLength == 0) + ++gatedCycles; + if ((snap.reported || packetSurfaced) && snap.payloadLength != bigPayload.size()) + ++violations; + if (snap.reported && packetSurfaced && snap.payloadLength == bigPayload.size()) + reportedWithPayload = true; + } + + EXPECT_EQ(0u, violations) + << "the server reported the connection while the client's session payload was still missing"; + EXPECT_GT(gatedCycles, 1u) << "never observed the handshake in its gated state, so this test proved nothing"; + ASSERT_TRUE(reportedWithPayload) << "the server never reported the connection with the client's payload in hand"; + + // And the payload really did survive the split and reassembly. + unsigned int length = 0; + const char *fromClient = server->GetRemoteSessionConfig(Find(server, ID_NEW_INCOMING_CONNECTION)->guid, &length); + ExpectPayload(fromClient, length, bigPayload.data(), bigPayload.size()); +} + +// A peer that configures no payload still completes the handshake; the remote simply reads none. +TEST_F(SessionConfigSim, EmptyPayloadsStillConnect) +{ + RakPeer *server = AddServer(8, 8); + RakPeer *client = AddClient(); + ASSERT_EQ(CONNECTION_ATTEMPT_STARTED, StartConnect(client, server)); + ASSERT_TRUE(WaitForBoth(client, ID_CONNECTION_REQUEST_ACCEPTED, server, ID_NEW_INCOMING_CONNECTION)); + + unsigned int length = 12345; + client->GetRemoteSessionConfig(Find(client, ID_CONNECTION_REQUEST_ACCEPTED)->guid, &length); + EXPECT_EQ(0u, length); + length = 12345; + server->GetRemoteSessionConfig(Find(server, ID_NEW_INCOMING_CONNECTION)->guid, &length); + EXPECT_EQ(0u, length); +} + +// The load-bearing assertion. In interactive mode the server holds the decision, so neither peer may +// report a connection until it answers. +TEST_F(SessionConfigSim, InteractiveAcceptGatesBothConnectionPackets) +{ + RakPeer *server = AddServer(8, 8); + RakPeer *client = AddClient(); + server->SetSessionConfigInteractive(true); + client->SetSessionConfig(kClientPayload, (unsigned int) std::strlen(kClientPayload)); + ASSERT_EQ(CONNECTION_ATTEMPT_STARTED, StartConnect(client, server)); + + RakNetGUID clientGuid; + ASSERT_TRUE(WaitForRequest(server, clientGuid)) << "server never saw the session request"; + + // The client's payload rides the request itself. + const Message *request = Find(server, ID_SESSION_CONFIG_REQUEST); + ASSERT_EQ(1 + std::strlen(kClientPayload), request->data.size()); + EXPECT_EQ(0, std::memcmp(request->data.data() + 1, kClientPayload, std::strlen(kClientPayload))); + + // Nothing may be reported while the decision is outstanding, checked after every cycle. The inboxes + // hold everything since the connect, so this covers the wait above too. + for (int i = 0; i < 400; ++i) + { + Pump(1); + ASSERT_FALSE(Has(server, ID_NEW_INCOMING_CONNECTION)) << "server reported a connection before answering the session request"; + ASSERT_FALSE(Has(client, ID_CONNECTION_REQUEST_ACCEPTED)) << "client reported a connection before the server answered"; + ASSERT_FALSE(SlotInspector::FirstActiveSlot(server).reported); + } + + server->AcceptSession(clientGuid, kServerPayload, (unsigned int) std::strlen(kServerPayload)); + + ASSERT_TRUE(WaitFor(server, ID_NEW_INCOMING_CONNECTION)) << "AcceptSession did not release the server's connection packet"; + ASSERT_TRUE(WaitFor(client, ID_CONNECTION_REQUEST_ACCEPTED)) << "AcceptSession did not release the client's connection packet"; + + unsigned int length = 0; + const char *fromServer = client->GetRemoteSessionConfig(Find(client, ID_CONNECTION_REQUEST_ACCEPTED)->guid, &length); + ExpectPayload(fromServer, length, kServerPayload, std::strlen(kServerPayload)); +} + +// A rejected peer must never see a connection, and must be told why. +TEST_F(SessionConfigSim, InteractiveRejectFailsTheConnectionAttempt) +{ + RakPeer *server = AddServer(8, 8); + RakPeer *client = AddClient(); + server->SetSessionConfigInteractive(true); + ASSERT_EQ(CONNECTION_ATTEMPT_STARTED, StartConnect(client, server)); + + RakNetGUID clientGuid; + ASSERT_TRUE(WaitForRequest(server, clientGuid)); + + const char reason[] = "build mismatch"; + server->RejectSession(clientGuid, reason); + + ASSERT_TRUE(WaitFor(client, ID_CONNECTION_ATTEMPT_FAILED)) << "rejected client was never told the attempt failed"; + // The reason rides after the id, matching the ID_DISCONNECTION_NOTIFICATION convention. + const Message *failed = Find(client, ID_CONNECTION_ATTEMPT_FAILED); + ASSERT_EQ(1 + std::strlen(reason), failed->data.size()); + EXPECT_EQ(0, std::memcmp(failed->data.data() + 1, reason, std::strlen(reason))); + EXPECT_FALSE(Has(client, ID_CONNECTION_REQUEST_ACCEPTED)); + + // Long enough for the DISCONNECT_ON_NO_ACK teardown to complete: the refusal is sent reliably, so the + // slot closes once the client acks it. The server was never told this peer connected, so it must be + // told nothing about it going away either. + Pump(3000); + EXPECT_FALSE(SlotInspector::FirstActiveSlot(server).sawSlot) << "the refused peer's slot was never released"; + EXPECT_FALSE(Has(server, ID_NEW_INCOMING_CONNECTION)) << "server reported a connection it had rejected"; + EXPECT_FALSE(Has(server, ID_DISCONNECTION_NOTIFICATION)) << "server was notified of a disconnect for a connection it never reported"; + EXPECT_FALSE(Has(server, ID_CONNECTION_LOST)) << "server was notified of a lost connection it never reported"; + EXPECT_FALSE(Has(server, ID_CONNECTION_ATTEMPT_FAILED)) << "server was told an inbound connection attempt failed; that is a connecting-side message"; +} + +// An interactive server that never answers must not pin the slot or hang the client: acks keep flowing +// during the exchange, so the ordinary dead-connection detection would never fire. +TEST_F(SessionConfigSim, UnansweredHandshakeTimesOut) +{ + RakPeer *server = AddServer(8, 8); + RakPeer *client = AddClient(); + server->SetSessionConfigInteractive(true); + const uint32_t begin = NowMs(); + ASSERT_EQ(CONNECTION_ATTEMPT_STARTED, StartConnect(client, server)); + + RakNetGUID clientGuid; + ASSERT_TRUE(WaitForRequest(server, clientGuid)); + + // Deliberately never answer. + ASSERT_TRUE(WaitFor(client, ID_CONNECTION_ATTEMPT_FAILED, kHandshakeTimeoutMs * 4)) << "an unanswered session handshake never timed out"; + EXPECT_GE(NowMs() - begin, (uint32_t) kHandshakeTimeoutMs) << "the attempt failed before the handshake timeout"; + EXPECT_LT(NowMs() - begin, (uint32_t) kHandshakeTimeoutMs * 2) << "the attempt outlived the handshake timeout"; + EXPECT_NE(IS_CONNECTED, client->GetConnectionState(net.AddressOf(server))); + EXPECT_FALSE(Has(client, ID_CONNECTION_REQUEST_ACCEPTED)); + EXPECT_FALSE(Has(server, ID_NEW_INCOMING_CONNECTION)); +} + +// Role binding: the session-handshake replies are server->client messages, so a client must not be +// able to push them at a listening server. ID_SESSION_CONFIG_REJECTED is the dangerous one -- unbound, +// it lets a client inject ID_CONNECTION_ATTEMPT_FAILED into the server's receive queue. +TEST_F(SessionConfigSim, ServerIgnoresSessionRepliesSentByAClient) +{ + RakPeer *server = AddServer(8, 8); + RakPeer *client = AddClient(); + server->SetSessionConfigInteractive(true); + ASSERT_EQ(CONNECTION_ATTEMPT_STARTED, StartConnect(client, server)); + + RakNetGUID clientGuid; + ASSERT_TRUE(WaitForRequest(server, clientGuid)); + // The server is parked in EXCHANGING_SESSION_DATA. Forge both server-to-client replies back at it. + // The client holds no packet carrying the server's guid, so address the server by endpoint. + const SystemAddress serverAddr = net.AddressOf(server); + + BitStream rejectBs; + rejectBs.Write((MessageID) ID_SESSION_CONFIG_REJECTED); + rejectBs.Write("spoofed", 7); + SendGateBypass::Inject(client, rejectBs, serverAddr); + + BitStream configBs; + configBs.Write((MessageID) ID_SESSION_CONFIG); + configBs.Write("spoofed-config", 14); + SendGateBypass::Inject(client, configBs, serverAddr); + + Pump(1500); + + EXPECT_FALSE(Has(server, ID_CONNECTION_ATTEMPT_FAILED)) << "a client forged ID_CONNECTION_ATTEMPT_FAILED into a listening server's queue"; + EXPECT_FALSE(Has(server, ID_NEW_INCOMING_CONNECTION)) << "a client completed the server's half of the handshake by replying to itself"; + EXPECT_FALSE(Has(server, ID_DISCONNECTION_NOTIFICATION)) << "the forged replies tore down a connection that was never reported"; + EXPECT_EQ(IS_CONNECTING, server->GetConnectionState(clientGuid)); + + // The legitimate path must still work afterwards. + server->AcceptSession(clientGuid, kServerPayload, (unsigned int) std::strlen(kServerPayload)); + ASSERT_TRUE(WaitFor(server, ID_NEW_INCOMING_CONNECTION)) << "the spoof attempt broke the real handshake"; + ASSERT_TRUE(WaitFor(client, ID_CONNECTION_REQUEST_ACCEPTED)); +} + +// The stored payload is arbitrary attacker-controlled bytes. It is kept NUL-terminated past the reported +// length so an application that reaches for a C-string API cannot run off the buffer. +TEST_F(SessionConfigSim, RemotePayloadIsNulTerminatedPastItsLength) +{ + RakPeer *server = AddServer(8, 8); + RakPeer *client = AddClient(); + const char payload[] = "no-trailing-nul"; + server->SetSessionConfig(payload, (unsigned int) std::strlen(payload)); + ASSERT_EQ(CONNECTION_ATTEMPT_STARTED, StartConnect(client, server)); + ASSERT_TRUE(WaitFor(client, ID_CONNECTION_REQUEST_ACCEPTED)); + + unsigned int length = 0; + const char *config = client->GetRemoteSessionConfig(Find(client, ID_CONNECTION_REQUEST_ACCEPTED)->guid, &length); + ASSERT_NE(config, nullptr); + ASSERT_EQ((unsigned int) std::strlen(payload), length); + EXPECT_EQ('\0', config[length]); + EXPECT_EQ((size_t) length, std::strlen(config)); +} + +// Admission control must see peers that are still running the session handshake: they already own a +// slot, so counting only CONNECTED peers would let stalled handshakes push past the incoming limit. +TEST_F(SessionConfigSim, StalledHandshakeStillConsumesAnIncomingSlot) +{ + RakPeer *server = AddServer(8, 1, 60000); + server->SetSessionConfigInteractive(true); // never answered, so the first client parks mid-handshake + RakPeer *client = AddClient(); + ASSERT_EQ(CONNECTION_ATTEMPT_STARTED, StartConnect(client, server)); + + RakNetGUID stalledGuid; + ASSERT_TRUE(WaitForRequest(server, stalledGuid)); + + RakPeer *second = AddClient(); + ASSERT_EQ(CONNECTION_ATTEMPT_STARTED, StartConnect(second, server)); + ASSERT_TRUE(WaitFor(second, ID_NO_FREE_INCOMING_CONNECTIONS)) + << "a peer stalled in the session handshake did not count against the incoming limit"; + EXPECT_EQ(1u, Count(server, ID_SESSION_CONFIG_REQUEST)); +} + +// A complete connection lifecycle behaves identically whether or not a session payload is configured. +// The reconnect leg is the load-bearing one: it reuses the server's slot, so a payload that outlived its +// connection, or session state not cleared on teardown, shows up here and nowhere else. +class SessionConfigSimPipeline : public SessionConfigSim, public ::testing::WithParamInterface +{ +}; + +TEST_P(SessionConfigSimPipeline, FullConnectionLifecycleBehavesIdentically) +{ + const bool withConfig = GetParam(); + const unsigned int expectedServerLen = withConfig ? (unsigned int) std::strlen(kServerPayload) : 0u; + const unsigned int expectedClientLen = withConfig ? (unsigned int) std::strlen(kClientPayload) : 0u; + + RakPeer *server = AddServer(8, 8); + RakPeer *client = AddClient(); + if (withConfig) + { + server->SetSessionConfig(kServerPayload, expectedServerLen); + client->SetSessionConfig(kClientPayload, expectedClientLen); + } + + // ---- stage 1: both sides report the connection -------------------------------------------------- + ASSERT_EQ(CONNECTION_ATTEMPT_STARTED, StartConnect(client, server)); + ASSERT_TRUE(WaitForBoth(client, ID_CONNECTION_REQUEST_ACCEPTED, server, ID_NEW_INCOMING_CONNECTION)) + << "the connection was not reported on both sides"; + const RakNetGUID serverGuid = Find(client, ID_CONNECTION_REQUEST_ACCEPTED)->guid; + const RakNetGUID clientGuid = Find(server, ID_NEW_INCOMING_CONNECTION)->guid; + + unsigned int length = 12345; + client->GetRemoteSessionConfig(serverGuid, &length); + EXPECT_EQ(expectedServerLen, length); + length = 12345; + server->GetRemoteSessionConfig(clientGuid, &length); + EXPECT_EQ(expectedClientLen, length); + EXPECT_EQ(IS_CONNECTED, client->GetConnectionState(serverGuid)); + EXPECT_EQ(IS_CONNECTED, server->GetConnectionState(clientGuid)); + + // ---- stage 2: ordinary traffic flows both ways -------------------------------------------------- + { + BitStream up; + WriteUserMessage(up, "client-to-server"); + client->Send(&up, Priority::High, Reliability::ReliableOrdered, 0, UNASSIGNED_SYSTEM_ADDRESS, true); + ASSERT_TRUE(WaitFor(server, kUserMessageId)) << "server never received client traffic"; + EXPECT_TRUE(CarriesText(Find(server, kUserMessageId), "client-to-server")); + } + { + BitStream down; + WriteUserMessage(down, "server-to-client"); + server->Send(&down, Priority::High, Reliability::ReliableOrdered, 0, UNASSIGNED_SYSTEM_ADDRESS, true); + ASSERT_TRUE(WaitFor(client, kUserMessageId)) << "client never received server traffic"; + EXPECT_TRUE(CarriesText(Find(client, kUserMessageId), "server-to-client")); + } + + // ---- stage 3: clean teardown still notifies ----------------------------------------------------- + // This connection WAS reported, so unlike a rejected peer it must produce a disconnect notification. + client->CloseConnection(serverGuid, true); + ASSERT_TRUE(WaitFor(server, ID_DISCONNECTION_NOTIFICATION)) << "server was not notified of a clean disconnect"; + + // ---- stage 4: reconnect over the reused slot ---------------------------------------------------- + // CloseConnection is asynchronous on the closing side too: reconnecting before the local slot frees + // returns ALREADY_CONNECTED_TO_ENDPOINT, so wait for the teardown. + ASSERT_TRUE(Until([&] { + const ConnectionState state = client->GetConnectionState(serverGuid); + return state == IS_NOT_CONNECTED || state == IS_DISCONNECTED; + }, kConnectTimeoutMs)); + + Inbox(client).clear(); + Inbox(server).clear(); + ASSERT_EQ(CONNECTION_ATTEMPT_STARTED, StartConnect(client, server)); + ASSERT_TRUE(WaitForBoth(client, ID_CONNECTION_REQUEST_ACCEPTED, server, ID_NEW_INCOMING_CONNECTION)) + << "the reconnection was not reported on both sides"; + const RakNetGUID serverGuid2 = Find(client, ID_CONNECTION_REQUEST_ACCEPTED)->guid; + const RakNetGUID clientGuid2 = Find(server, ID_NEW_INCOMING_CONNECTION)->guid; + + // The payload must be freshly delivered for the new connection -- neither stale from the previous one + // nor lost because teardown cleared it and nothing repopulated it. + length = 12345; + const char *serverCfg = client->GetRemoteSessionConfig(serverGuid2, &length); + EXPECT_EQ(expectedServerLen, length) << "server payload wrong after reconnect"; + if (withConfig) + ExpectPayload(serverCfg, length, kServerPayload, expectedServerLen); + + length = 12345; + const char *clientCfg = server->GetRemoteSessionConfig(clientGuid2, &length); + EXPECT_EQ(expectedClientLen, length) << "client payload wrong after reconnect"; + if (withConfig) + ExpectPayload(clientCfg, length, kClientPayload, expectedClientLen); + + // ---- stage 5: traffic still flows on the reconnected session ------------------------------------ + BitStream again; + WriteUserMessage(again, "after-reconnect!"); + client->Send(&again, Priority::High, Reliability::ReliableOrdered, 0, UNASSIGNED_SYSTEM_ADDRESS, true); + ASSERT_TRUE(WaitFor(server, kUserMessageId)) << "traffic did not flow after reconnect"; + EXPECT_TRUE(CarriesText(Find(server, kUserMessageId), "after-reconnect!")); +} + +INSTANTIATE_TEST_SUITE_P(WithAndWithoutSessionConfig, SessionConfigSimPipeline, + ::testing::Values(false, true), + [](const ::testing::TestParamInfo &info) { + return info.param ? "WithSessionConfig" : "WithoutSessionConfig"; + }); + +// Application traffic must not cross a connection the application has not been told about. This one +// covers the directed-send half and the connection state; the inbound half is the next test. +TEST_F(SessionConfigSim, ApplicationTrafficIsBlockedUntilTheHandshakeCompletes) +{ + RakPeer *server = AddServer(8, 8); + RakPeer *client = AddClient(); + server->SetSessionConfigInteractive(true); + ASSERT_EQ(CONNECTION_ATTEMPT_STARTED, StartConnect(client, server)); + + RakNetGUID clientGuid; + ASSERT_TRUE(WaitForRequest(server, clientGuid)); + EXPECT_EQ(IS_CONNECTING, server->GetConnectionState(clientGuid)) << "a peer mid-handshake must not read as connected"; + + // The application only holds this guid because interactive mode surfaced the request to it. + BitStream world; + WriteUserMessage(world, "world-state-leak"); + EXPECT_EQ(0u, server->Send(&world, Priority::High, Reliability::ReliableOrdered, 0, clientGuid, false)) + << "a directed send reached a peer the application has not accepted"; + + Pump(700); + EXPECT_FALSE(Has(server, ID_NEW_INCOMING_CONNECTION)) << "server reported a connection before answering the session request"; + EXPECT_FALSE(Has(client, ID_CONNECTION_REQUEST_ACCEPTED)) << "client reported a connection before the handshake completed"; + EXPECT_FALSE(Has(client, kUserMessageId)) << "the refused send reached the client anyway"; + + // ---- once accepted, both directions work normally ----------------------------------------------- + server->AcceptSession(clientGuid, kServerPayload, (unsigned int) std::strlen(kServerPayload)); + ASSERT_TRUE(WaitForBoth(server, ID_NEW_INCOMING_CONNECTION, client, ID_CONNECTION_REQUEST_ACCEPTED)); + + BitStream now; + WriteUserMessage(now, "after-accept-ok!"); + EXPECT_NE(0u, server->Send(&now, Priority::High, Reliability::ReliableOrdered, 0, clientGuid, false)) + << "a directed send was still refused after the handshake completed"; + ASSERT_TRUE(WaitFor(client, kUserMessageId)) << "traffic did not flow after the handshake completed"; + EXPECT_TRUE(CarriesText(Find(client, kUserMessageId), "after-accept-ok!")); + EXPECT_EQ(1u, Count(client, kUserMessageId)); +} + +// The receiving half of the traffic gate, provable only by bypassing the sender's own gate: a peer that +// pushes application data during the handshake must not have it delivered. +TEST_F(SessionConfigSim, InboundApplicationDataDuringHandshakeIsNotDelivered) +{ + RakPeer *server = AddServer(8, 8); + RakPeer *client = AddClient(); + server->SetSessionConfigInteractive(true); + ASSERT_EQ(CONNECTION_ATTEMPT_STARTED, StartConnect(client, server)); + + RakNetGUID clientGuid; + ASSERT_TRUE(WaitForRequest(server, clientGuid)); + const SystemAddress serverAddr = net.AddressOf(server); + + BitStream early; + WriteUserMessage(early, "premature-inbound"); + SendGateBypass::Inject(client, early, serverAddr); + + Pump(1500); + EXPECT_FALSE(Has(server, kUserMessageId)) << "application data from a peer mid-handshake reached the application"; + EXPECT_FALSE(Has(server, ID_NEW_INCOMING_CONNECTION)) << "server reported a connection before answering the session request"; + + // Once accepted, the same peer's traffic is delivered normally -- the gate is about the connection + // state, not about this peer being permanently distrusted. + server->AcceptSession(clientGuid, kServerPayload, (unsigned int) std::strlen(kServerPayload)); + ASSERT_TRUE(WaitFor(server, ID_NEW_INCOMING_CONNECTION)); + + BitStream now; + WriteUserMessage(now, "after-accept-ok!"); + SendGateBypass::Inject(client, now, serverAddr); + ASSERT_TRUE(WaitFor(server, kUserMessageId)) << "traffic was still dropped after the handshake completed"; + EXPECT_TRUE(CarriesText(Find(server, kUserMessageId), "after-accept-ok!")) + << "the message dropped during the handshake was delivered late instead"; + EXPECT_EQ(1u, Count(server, kUserMessageId)); +} + +// ========================================================================================================= +// Pending-session pool, session timeout, status and abandonment (SetMaximumPendingSessions, +// SetSessionTimeout, SendSessionStatus, ID_SESSION_CONFIG_STATUS, ID_SESSION_CONFIG_ABANDONED). +// ========================================================================================================= + +// A handshake parked with the server undecided does not take the player slot. +TEST_F(SessionConfigSim, PendingPoolKeepsStalledHandshakesOutOfThePlayerSlots) +{ + RakPeer *server = AddPooledServer(1, 4, 0); + RakPeer *client = AddClient(60000); + ASSERT_EQ(CONNECTION_ATTEMPT_STARTED, StartConnect(client, server)); + + RakNetGUID stalledGuid; + ASSERT_TRUE(WaitForRequest(server, stalledGuid)) << "first client never reached the handshake"; + // Deliberately never answered. + + RakPeer *second = AddClient(60000); + ASSERT_EQ(CONNECTION_ATTEMPT_STARTED, StartConnect(second, server)); + ASSERT_TRUE(Until([&] { return Count(server, ID_SESSION_CONFIG_REQUEST) == 2; }, kConnectTimeoutMs)) + << "the stalled handshake took the only player slot; second saw " << IdList(Inbox(second)); + const RakNetGUID secondGuid = Inbox(server).back().guid; + ASSERT_EQ(ID_SESSION_CONFIG_REQUEST, Inbox(server).back().id); + EXPECT_NE(stalledGuid, secondGuid); + EXPECT_EQ(second->GetMyGUID(), secondGuid); + + server->AcceptSession(secondGuid, kServerPayload, (unsigned int) std::strlen(kServerPayload)); + ASSERT_TRUE(WaitForBoth(second, ID_CONNECTION_REQUEST_ACCEPTED, server, ID_NEW_INCOMING_CONNECTION)) + << "the second player was not let in while the first sat in the handshake"; + EXPECT_EQ(secondGuid, Find(server, ID_NEW_INCOMING_CONNECTION)->guid); + EXPECT_EQ(1u, server->NumberOfConnections()) << "a peer still in the handshake must not read as connected"; + EXPECT_EQ(IS_CONNECTING, server->GetConnectionState(stalledGuid)); +} + +// The pool is itself bounded: once it is full a newcomer is refused at the transport. +TEST_F(SessionConfigSim, PendingPoolRefusesNewcomersOnceFull) +{ + RakPeer *server = AddPooledServer(8, 1, 0); + RakPeer *client = AddClient(60000); + ASSERT_EQ(CONNECTION_ATTEMPT_STARTED, StartConnect(client, server)); + + RakNetGUID stalledGuid; + ASSERT_TRUE(WaitForRequest(server, stalledGuid)); + + RakPeer *second = AddClient(60000); + ASSERT_EQ(CONNECTION_ATTEMPT_STARTED, StartConnect(second, server)); + ASSERT_TRUE(WaitFor(second, ID_NO_FREE_INCOMING_CONNECTIONS)) << "a full pending pool let another handshake start"; + EXPECT_EQ(1u, Count(server, ID_SESSION_CONFIG_REQUEST)); +} + +// One address cannot fill the pool on its own. The second client shares the first one's host. +TEST_F(SessionConfigSim, PendingPoolBoundsWhatOneAddressMayHold) +{ + RakPeer *server = AddPooledServer(8, 8, 1); + RakPeer *client = AddClient(60000); + ASSERT_EQ(CONNECTION_ATTEMPT_STARTED, StartConnect(client, server)); + + RakNetGUID stalledGuid; + ASSERT_TRUE(WaitForRequest(server, stalledGuid)); + + RakPeer *second = AddManualClientBeside(client, 60000); + ASSERT_TRUE(net.AddressOf(second).EqualsExcludingPort(net.AddressOf(client))); + ASSERT_EQ(CONNECTION_ATTEMPT_STARTED, StartConnect(second, server)); + ASSERT_TRUE(WaitFor(second, ID_NO_FREE_INCOMING_CONNECTIONS)) << "one address held more pending handshakes than its share"; + EXPECT_EQ(1u, Count(server, ID_SESSION_CONFIG_REQUEST)); + + // The limit is per address, not a smaller pool: a client on another host still gets in. + RakPeer *third = AddClient(60000); + ASSERT_EQ(CONNECTION_ATTEMPT_STARTED, StartConnect(third, server)); + ASSERT_TRUE(Until([&] { return Count(server, ID_SESSION_CONFIG_REQUEST) == 2; }, kConnectTimeoutMs)) + << "third saw " << IdList(Inbox(third)); +} + +// A decision gives the pool slot back. A refused peer lingers only to deliver the refusal, and that must +// not keep the next newcomer out -- nor be reported as abandoned. +TEST_F(SessionConfigSim, PendingPoolSlotIsFreedByADecision) +{ + RakPeer *server = AddPooledServer(8, 1, 0); + RakPeer *client = AddClient(60000); + ASSERT_EQ(CONNECTION_ATTEMPT_STARTED, StartConnect(client, server)); + + RakNetGUID firstGuid; + ASSERT_TRUE(WaitForRequest(server, firstGuid)); + server->RejectSession(firstGuid, "not on the whitelist"); + ASSERT_TRUE(WaitFor(client, ID_CONNECTION_ATTEMPT_FAILED)); + + RakPeer *second = AddClient(60000); + ASSERT_EQ(CONNECTION_ATTEMPT_STARTED, StartConnect(second, server)); + ASSERT_TRUE(Until([&] { return Count(server, ID_SESSION_CONFIG_REQUEST) == 2; }, kConnectTimeoutMs)) + << "the refused peer still held the only pool slot; second saw " << IdList(Inbox(second)); + EXPECT_EQ(second->GetMyGUID(), Inbox(server).back().guid); + EXPECT_NE(firstGuid, Inbox(server).back().guid); + EXPECT_FALSE(Has(server, ID_SESSION_CONFIG_ABANDONED)) << "a peer the application refused was reported as abandoned"; +} + +// With a pool the handshake may start on a full server, but an AcceptSession() that would overfill it is +// refused rather than honoured. +TEST_F(SessionConfigSim, AcceptSessionCannotOverfillTheIncomingLimit) +{ + RakPeer *server = AddPooledServer(1, 4, 0); + RakPeer *client = AddClient(60000); + ASSERT_EQ(CONNECTION_ATTEMPT_STARTED, StartConnect(client, server)); + + RakNetGUID firstGuid; + ASSERT_TRUE(WaitForRequest(server, firstGuid)); + server->AcceptSession(firstGuid, nullptr, 0); + ASSERT_TRUE(WaitFor(server, ID_NEW_INCOMING_CONNECTION)); + // The only player slot is now taken. + + RakPeer *second = AddClient(60000); + ASSERT_EQ(CONNECTION_ATTEMPT_STARTED, StartConnect(second, server)); + ASSERT_TRUE(Until([&] { return Count(server, ID_SESSION_CONFIG_REQUEST) == 2; }, kConnectTimeoutMs)) + << "a full server with a pool refused to even start the handshake; second saw " << IdList(Inbox(second)); + const RakNetGUID secondGuid = Inbox(server).back().guid; + + server->AcceptSession(secondGuid, nullptr, 0); + ASSERT_TRUE(WaitFor(second, ID_CONNECTION_ATTEMPT_FAILED)) << "an accept past the incoming limit was honoured"; + + Pump(1000); + EXPECT_EQ(1u, Count(server, ID_NEW_INCOMING_CONNECTION)) << "the server reported a connection past its limit"; + EXPECT_FALSE(Has(second, ID_CONNECTION_REQUEST_ACCEPTED)); + EXPECT_EQ(1u, server->NumberOfConnections()); +} + +// SetMaximumPendingSessions(0, ...) restores the historic accounting exactly. +TEST_F(SessionConfigSim, ClearingThePoolRestoresTheDefaultAccounting) +{ + RakPeer *server = AddServer(8, 1, 60000); + server->SetMaximumPendingSessions(4, 0); + server->SetMaximumPendingSessions(0, 0); + server->SetSessionConfigInteractive(true); + RakPeer *client = AddClient(); + ASSERT_EQ(CONNECTION_ATTEMPT_STARTED, StartConnect(client, server)); + + RakNetGUID stalledGuid; + ASSERT_TRUE(WaitForRequest(server, stalledGuid)); + + RakPeer *second = AddClient(60000); + ASSERT_EQ(CONNECTION_ATTEMPT_STARTED, StartConnect(second, server)); + ASSERT_TRUE(WaitFor(second, ID_NO_FREE_INCOMING_CONNECTIONS)) << "without a pool a stalled handshake must still hold its incoming slot"; +} + +// The session timeout replaces the connection timeout for the handshake, on both ends, and the server is +// told the request it was holding is gone. +TEST_F(SessionConfigSim, SessionTimeoutBoundsAnUnansweredHandshake) +{ + const TimeMS sessionTimeoutMs = 1000; + // A connection timeout far longer than the test, so only the session timeout can end the attempt. + RakPeer *server = AddPooledServer(8, 4, 0, 60000); + RakPeer *client = AddClient(60000); + server->SetSessionTimeout(sessionTimeoutMs); + client->SetSessionTimeout(sessionTimeoutMs); + + const uint32_t begin = NowMs(); + ASSERT_EQ(CONNECTION_ATTEMPT_STARTED, StartConnect(client, server)); + + RakNetGUID guid; + ASSERT_TRUE(WaitForRequest(server, guid)); + + ASSERT_TRUE(WaitForBoth(client, ID_CONNECTION_ATTEMPT_FAILED, server, ID_SESSION_CONFIG_ABANDONED)) + << "the session timeout did not end the handshake on both ends"; + const uint32_t elapsed = NowMs() - begin; + EXPECT_EQ(guid, Find(server, ID_SESSION_CONFIG_ABANDONED)->guid); + EXPECT_EQ(1u, Count(server, ID_SESSION_CONFIG_ABANDONED)); + EXPECT_GE(elapsed, (uint32_t) sessionTimeoutMs); + EXPECT_LT(elapsed, (uint32_t) sessionTimeoutMs * 2) << "the handshake waited out something other than the session timeout"; + EXPECT_FALSE(Has(client, ID_CONNECTION_REQUEST_ACCEPTED)); + EXPECT_FALSE(Has(server, ID_NEW_INCOMING_CONNECTION)); +} + +// Status reaches the client before any connection is reported, carries its payload, and restarts the +// session timeout on both ends: a peer kept informed for three timeouts' worth is still let in. +TEST_F(SessionConfigSim, SessionStatusReachesTheClientAndKeepsTheHandshakeAlive) +{ + const TimeMS sessionTimeoutMs = 1000; + RakPeer *server = AddPooledServer(8, 4, 0); + RakPeer *client = AddClient(60000); + server->SetSessionTimeout(sessionTimeoutMs); + client->SetSessionTimeout(sessionTimeoutMs); + ASSERT_EQ(CONNECTION_ATTEMPT_STARTED, StartConnect(client, server)); + + RakNetGUID guid; + ASSERT_TRUE(WaitForRequest(server, guid)); + + std::vector sentStatuses; + const uint32_t begin = NowMs(); + uint32_t lastStatus = 0; + while (NowMs() - begin < sessionTimeoutMs * 3) + { + if (sentStatuses.empty() || NowMs() - lastStatus >= 400) + { + sentStatuses.push_back("queue:" + std::to_string(sentStatuses.size() + 1)); + server->SendSessionStatus(guid, sentStatuses.back().data(), (unsigned int) sentStatuses.back().size()); + lastStatus = NowMs(); + } + Pump(1); + } + Pump(50); // let the last status land + + EXPECT_FALSE(Has(client, ID_CONNECTION_ATTEMPT_FAILED)) << "status did not keep the handshake alive past the session timeout"; + EXPECT_FALSE(Has(server, ID_SESSION_CONFIG_ABANDONED)) << "status did not keep the handshake alive past the session timeout"; + EXPECT_FALSE(Has(client, ID_CONNECTION_REQUEST_ACCEPTED)) << "a connection was reported while the server was still deciding"; + EXPECT_FALSE(Has(server, ID_NEW_INCOMING_CONNECTION)) << "a connection was reported while the server was still deciding"; + + std::vector statuses; + for (const Message &message : Inbox(client)) + if (message.id == ID_SESSION_CONFIG_STATUS) + statuses.emplace_back((const char *) message.data.data() + 1, message.data.size() - 1); + ASSERT_GE(sentStatuses.size(), 3u); + EXPECT_EQ(sentStatuses, statuses) << "every status, in order, with its payload"; + EXPECT_EQ(IS_CONNECTING, server->GetConnectionState(guid)); + + server->AcceptSession(guid, kServerPayload, (unsigned int) std::strlen(kServerPayload)); + ASSERT_TRUE(WaitForBoth(client, ID_CONNECTION_REQUEST_ACCEPTED, server, ID_NEW_INCOMING_CONNECTION)) + << "a peer held with status was not let in when accepted"; +} + +// Status is for a peer awaiting a decision only; once accepted nothing more is sent. +TEST_F(SessionConfigSim, SessionStatusIsIgnoredOnceTheSessionIsDecided) +{ + RakPeer *server = AddPooledServer(8, 4, 0); + RakPeer *client = AddClient(60000); + ASSERT_EQ(CONNECTION_ATTEMPT_STARTED, StartConnect(client, server)); + + RakNetGUID guid; + ASSERT_TRUE(WaitForRequest(server, guid)); + server->AcceptSession(guid, nullptr, 0); + ASSERT_TRUE(WaitForBoth(client, ID_CONNECTION_REQUEST_ACCEPTED, server, ID_NEW_INCOMING_CONNECTION)); + const RakNetGUID serverGuid = Find(client, ID_CONNECTION_REQUEST_ACCEPTED)->guid; + + server->SendSessionStatus(guid, "late", 4); + Pump(800); + EXPECT_FALSE(Has(client, ID_SESSION_CONFIG_STATUS)) << "status reached a peer that was already accepted"; + EXPECT_EQ(IS_CONNECTED, client->GetConnectionState(serverGuid)); +} + +// Role binding: status is a server-to-client message. A client pushing one at a server must not have it +// surfaced there. +TEST_F(SessionConfigSim, ServerIgnoresSessionStatusSentByAClient) +{ + RakPeer *server = AddPooledServer(8, 4, 0); + RakPeer *client = AddClient(60000); + ASSERT_EQ(CONNECTION_ATTEMPT_STARTED, StartConnect(client, server)); + + RakNetGUID guid; + ASSERT_TRUE(WaitForRequest(server, guid)); + + BitStream forged; + forged.Write((MessageID) ID_SESSION_CONFIG_STATUS); + forged.Write("spoofed", 7); + SendGateBypass::Inject(client, forged, net.AddressOf(server)); + + Pump(1200); + EXPECT_FALSE(Has(server, ID_SESSION_CONFIG_STATUS)) << "a client pushed a status into the server's queue"; + EXPECT_FALSE(Has(server, ID_NEW_INCOMING_CONNECTION)); + + // The real handshake is unaffected. + server->AcceptSession(guid, nullptr, 0); + ASSERT_TRUE(WaitFor(server, ID_NEW_INCOMING_CONNECTION)); +} + +// A client that gives up while the server decides is reported to the server as abandoned, exactly once, +// and answering it afterwards is inert. +TEST_F(SessionConfigSim, ClientLeavingMidDecisionIsReportedAsAbandoned) +{ + RakPeer *server = AddPooledServer(8, 4, 0); + RakPeer *client = AddManualClient("10.0.9.1", 60000, 60000); + ASSERT_EQ(CONNECTION_ATTEMPT_STARTED, StartConnect(client, server)); + + RakNetGUID guid; + ASSERT_TRUE(WaitForRequest(server, guid)); + + // Shutdown with a block duration sends the disconnection notification, so the server learns now + // rather than after the 60s connection timeout. + // + // Under manual update Shutdown() flushes with a single update cycle, and ReliabilityLayer::Update + // sends nothing at a timestamp it has already updated at (time <= lastUpdateTime). Let a millisecond + // pass first -- net.Step() advances the clock without updating this fixture-driven client -- as it + // always does on a real clock, or the flush is a no-op and the server learns only by timeout. + net.Step(1); + client->Shutdown(300); + + ASSERT_TRUE(WaitFor(server, ID_SESSION_CONFIG_ABANDONED, 1000)) << "the server was never told the held request went away"; + EXPECT_EQ(guid, Find(server, ID_SESSION_CONFIG_ABANDONED)->guid); + + server->AcceptSession(guid, nullptr, 0); + Pump(1200); + EXPECT_EQ(1u, Count(server, ID_SESSION_CONFIG_ABANDONED)) << "abandonment was reported twice"; + EXPECT_FALSE(Has(server, ID_NEW_INCOMING_CONNECTION)) << "accepting an abandoned request reported a connection"; + EXPECT_FALSE(Has(server, ID_CONNECTION_LOST)) << "a connection never reported was reported lost"; + EXPECT_FALSE(Has(server, ID_DISCONNECTION_NOTIFICATION)) << "a connection never reported was reported closed"; + EXPECT_EQ(0u, server->NumberOfConnections()); + EXPECT_FALSE(SlotInspector::FirstActiveSlot(server).sawSlot); +} diff --git a/ci/tsan.supp b/ci/tsan.supp index b5a1e9a37..48660695d 100644 --- a/ci/tsan.supp +++ b/ci/tsan.supp @@ -48,8 +48,3 @@ race:MafiaNet::RakPeer::SetOccasionalPing # pending a review of the intended locking discipline --- race:DataStructures::Queue::Push race:DataStructures::Queue::Push - -# --- Test-only: SessionConfigLive's SlotInspector samples the server's slot -# state from the test thread without a lock, deliberately, to observe the -# static-mode gating window (see the comment on it in the test) --- -race:SlotInspector::FirstActiveSlot From 5b0d5a426a4aba9a4949a28fc95ffadb9a8ab9be Mon Sep 17 00:00:00 2001 From: Segfault <5221072+Segfaultd@users.noreply.github.com> Date: Sat, 17 Oct 2026 17:45:20 +0200 Subject: [PATCH 3/6] fix: no RakAssert on malformed datagrams; fuzz with asserts enabled The new fuzz-debug preset builds the harnesses in Debug, so a RakAssert a remote can reach through crafted input aborts and is reported. Two did, in ReliabilityLayer, reachable by any connected peer against a Debug build: - "Couldn't read all the data": a packet claiming more data than its datagram carries; - "Hole count too high": a peer jumping its message numbering far ahead. Both inputs were already dropped in Release; now Debug drops them too. Three reproducers join the peer_datagram corpus. The libFuzzer targets now mirror the library's _DEBUG definition like the replay targets do; without it, a Debug harness stack-allocated a ReliabilityLayer with the wrong layout. The nightly fuzz workflow runs both presets. --- .github/workflows/fuzz.yml | 21 ++++++++++++------ CMakePresets.json | 10 +++++++++ Source/src/reliability_layer.cpp | 9 ++++---- Tests/Fuzz/CMakeLists.txt | 3 +++ .../peer_datagram/regress-debug-assert-1 | Bin 0 -> 649 bytes .../peer_datagram/regress-debug-assert-2 | Bin 0 -> 1744 bytes .../peer_datagram/regress-debug-assert-3 | Bin 0 -> 1754 bytes 7 files changed, 31 insertions(+), 12 deletions(-) create mode 100644 Tests/Fuzz/corpus/peer_datagram/regress-debug-assert-1 create mode 100644 Tests/Fuzz/corpus/peer_datagram/regress-debug-assert-2 create mode 100644 Tests/Fuzz/corpus/peer_datagram/regress-debug-assert-3 diff --git a/.github/workflows/fuzz.yml b/.github/workflows/fuzz.yml index 70ef52a6c..48f30097e 100644 --- a/.github/workflows/fuzz.yml +++ b/.github/workflows/fuzz.yml @@ -16,9 +16,16 @@ permissions: contents: read jobs: + # fuzz: optimized, NDEBUG, as shipped. fuzz-debug: RakAssert enabled, so any + # assert a remote can reach through crafted input is reported as a crash. fuzz: runs-on: ubuntu-latest timeout-minutes: 120 + strategy: + fail-fast: false + matrix: + preset: [fuzz, fuzz-debug] + name: fuzz (${{ matrix.preset }}) steps: - name: Checkout repository uses: actions/checkout@v4 @@ -30,15 +37,15 @@ jobs: - name: Build harnesses run: | - cmake --preset fuzz -G Ninja - cmake --build --preset fuzz + cmake --preset ${{ matrix.preset }} -G Ninja + cmake --build --preset ${{ matrix.preset }} - name: Restore corpus uses: actions/cache/restore@v4 with: path: fuzz-corpus - key: fuzz-corpus-${{ github.run_id }} - restore-keys: fuzz-corpus- + key: fuzz-corpus-${{ matrix.preset }}-${{ github.run_id }} + restore-keys: fuzz-corpus-${{ matrix.preset }}- - name: Fuzz env: @@ -48,7 +55,7 @@ jobs: run: | mkdir -p fuzz-artifacts status=0 - for harness in build/fuzz/Tests/Fuzz/*_fuzz; do + for harness in build/${{ matrix.preset }}/Tests/Fuzz/*_fuzz; do name=$(basename "$harness" _fuzz) mkdir -p fuzz-corpus/"$name" cp -n Tests/Fuzz/corpus/"$name"/* fuzz-corpus/"$name"/ || true @@ -64,11 +71,11 @@ jobs: uses: actions/cache/save@v4 with: path: fuzz-corpus - key: fuzz-corpus-${{ github.run_id }} + key: fuzz-corpus-${{ matrix.preset }}-${{ github.run_id }} - name: Upload crash reproducers if: failure() uses: actions/upload-artifact@v4 with: - name: fuzz-crashes + name: fuzz-crashes-${{ matrix.preset }} path: fuzz-artifacts/ diff --git a/CMakePresets.json b/CMakePresets.json index 530972b03..af586e258 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -124,6 +124,15 @@ "MAFIANET_BUILD_FUZZERS": "ON", "MAFIANET_SANITIZER": "address+undefined+fuzzer-no-link" } + }, + { + "name": "fuzz-debug", + "inherits": "fuzz", + "displayName": "libFuzzer harnesses with RakAssert enabled", + "description": "Debug defines _DEBUG, so any RakAssert a remote can reach through crafted input aborts and is reported as a crash. Network input must never trip an assert.", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug" + } } ], "buildPresets": [ @@ -134,6 +143,7 @@ { "name": "asan", "configurePreset": "asan" }, { "name": "tsan", "configurePreset": "tsan" }, { "name": "fuzz", "configurePreset": "fuzz" }, + { "name": "fuzz-debug", "configurePreset": "fuzz-debug" }, { "name": "coverage", "configurePreset": "coverage" }, { "name": "bench", "configurePreset": "bench", "targets": ["MafiaNetBenchmarks"] } ], diff --git a/Source/src/reliability_layer.cpp b/Source/src/reliability_layer.cpp index 6f05513ef..4b37369f4 100644 --- a/Source/src/reliability_layer.cpp +++ b/Source/src/reliability_layer.cpp @@ -1051,8 +1051,8 @@ bool ReliabilityLayer::HandleSocketReceiveFromConnectedPlayer( } } else { // holeCount>=receivedPackets.Size() if (holeCount > (DatagramSequenceNumberType)1000000) { - RakAssert("Hole count too high. See reliability_layer.h" && 0); - + // A peer jumped its message numbering far ahead: drop the + // packet. No RakAssert -- the remote chooses this value. for (unsigned int messageHandlerIndex = 0; messageHandlerIndex < messageHandlerList.Size(); messageHandlerIndex++) { messageHandlerList[messageHandlerIndex]->OnReliabilityLayerNotification("holeCount > 1000000", BYTES_TO_BITS(length), systemAddress, true); } @@ -2898,9 +2898,8 @@ InternalPacket* ReliabilityLayer::CreateInternalPacketFromBitStream(MafiaNet::Bi if ( bitStreamSucceeded == false ) { - // If this hits, most likely the variable buff is too small in RunUpdateCycle in peer.cpp - RakAssert("Couldn't read all the data" && 0); - + // The packet claims more data than the datagram carries: malformed or + // hostile input, so drop it rather than RakAssert. FreeInternalPacketData(internalPacket, _FILE_AND_LINE_ ); ReleaseToInternalPacketPool( internalPacket ); return 0; diff --git a/Tests/Fuzz/CMakeLists.txt b/Tests/Fuzz/CMakeLists.txt index 9a819f6cf..089c7cf1d 100644 --- a/Tests/Fuzz/CMakeLists.txt +++ b/Tests/Fuzz/CMakeLists.txt @@ -43,6 +43,9 @@ foreach(harness_source IN LISTS MAFIANET_FUZZ_HARNESSES) add_executable(${harness} "${harness_source}" ${MAFIANET_FUZZ_SUPPORT}) target_include_directories(${harness} PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/../Support") target_link_libraries(${harness} PRIVATE MafiaNet::MafiaNetStatic) + # Must match the library: several classes the harnesses put on the + # stack carry _DEBUG-only members (see Tests/CMakeLists.txt). + target_compile_definitions(${harness} PRIVATE $<$:_DEBUG>) target_compile_options(${harness} PRIVATE -fsanitize=fuzzer) target_link_options(${harness} PRIVATE -fsanitize=fuzzer) set_target_properties(${harness} PROPERTIES FOLDER "Tests/Fuzz") diff --git a/Tests/Fuzz/corpus/peer_datagram/regress-debug-assert-1 b/Tests/Fuzz/corpus/peer_datagram/regress-debug-assert-1 new file mode 100644 index 0000000000000000000000000000000000000000..b7bf60b3042f7e72c2283aedbdbcbbdec2729d65 GIT binary patch literal 649 zcmY#nU}T^XP-0+b`2U~b9}xTn0wI&I3YM?`|NncHz*=EtqTXqAfBHTdh8Cdh4h$23 zBqvM|;vVL=49yHJK-VQOeSo@A7^)vc{e6|d@*fBo7)YZ2f%G6WL9KcZqB$6tKPE76 zFfo9Gq=A9K10o9))?sJ?3NtWtFhhixKSSxSAld=Kp$=(}VT=lf2!;S6m{nl$$^7CK wa_FOL;XoH4f&W*?BCMiZv8pqFb`%r<0QdBH=>Px# literal 0 HcmV?d00001 diff --git a/Tests/Fuzz/corpus/peer_datagram/regress-debug-assert-2 b/Tests/Fuzz/corpus/peer_datagram/regress-debug-assert-2 new file mode 100644 index 0000000000000000000000000000000000000000..5dab64f9326a0ece533b97837136ac02a92816cf GIT binary patch literal 1744 zcma!HWo7vPpWz=6`~?CblduYQ2I>QS>Ki$zO#J;H7>I)rI1GTJI0nGZQ7cD7U^E11 z9|B4Y?CA9c%h&(^|Gi3Jt*|mt?=-qUeV+_N3(z4B3=@DPCrl9HH0HMq%?vG!3=9cO gAAlqX2t)OQsK2igSb!xduqcI5F@)+T6y*#I082ztg#Z8m literal 0 HcmV?d00001 diff --git a/Tests/Fuzz/corpus/peer_datagram/regress-debug-assert-3 b/Tests/Fuzz/corpus/peer_datagram/regress-debug-assert-3 new file mode 100644 index 0000000000000000000000000000000000000000..2d97cab304bad6f4a5b01ef880639a01f80b0e68 GIT binary patch literal 1754 zcma!HWo7vPpWz=6`~?CblduYQ2I>QS>Ki$zO#J;H7>I)rI1ENdjk;+x1V%$(NQQtC z13P+s!SeP0|9`I%SSzed)H{vtPv0lQ&;sV)JAnNa{1QuXP3M@*Y)R(W{zW@07>-V3(|Nb*DGBLBTvaxe;a&hzU^6?7@3JHsd OLJUD6#l#sHBp3iFhfDAP literal 0 HcmV?d00001 From 66ce6046a46545342abd1aca9358943c2b6d5b88 Mon Sep 17 00:00:00 2001 From: Segfault <5221072+Segfaultd@users.noreply.github.com> Date: Sat, 17 Oct 2026 18:01:50 +0200 Subject: [PATCH 4/6] fix: drop an internal-only message id sent by a remote instead of leaking it A connected peer sending ID_CONNECTION_ATTEMPT_FAILED (generated locally only) hit RakAssert(0) in Debug. In Release the receive loop zeroed its size and continued without freeing the message, leaking it and leaving the rest of that peer's queue for the next update. It is now freed and the loop moves on. Found by the peer_datagram fuzzer with asserts enabled; covered by a SimulatedPeer test and a corpus reproducer. --- Source/src/peer.cpp | 9 +++++--- .../regress-internal-id-from-remote | Bin 0 -> 3344 bytes Tests/Unit/SimulatedPeerTests.cpp | 20 ++++++++++++++++++ 3 files changed, 26 insertions(+), 3 deletions(-) create mode 100644 Tests/Fuzz/corpus/peer_datagram/regress-internal-id-from-remote diff --git a/Source/src/peer.cpp b/Source/src/peer.cpp index 81371bcf5..4d55856de 100644 --- a/Source/src/peer.cpp +++ b/Source/src/peer.cpp @@ -6485,11 +6485,14 @@ bool RakPeer::RunUpdateCycle(BitStream &updateBitStream ) while ( bitSize > 0 ) { - // These types are for internal use and should never arrive from a network packet + // These types are for internal use and should never arrive from a network packet. + // A remote that sends one anyway gets it dropped: freed, and the rest of the + // queue still processed (this used to leak it and stall the queue, and to + // RakAssert in Debug). if (data[0]==ID_CONNECTION_ATTEMPT_FAILED) { - RakAssert(0); - bitSize=0; + rakFree_Ex(data, _FILE_AND_LINE_ ); + bitSize = remoteSystem->reliabilityLayer.Receive( &data ); continue; } diff --git a/Tests/Fuzz/corpus/peer_datagram/regress-internal-id-from-remote b/Tests/Fuzz/corpus/peer_datagram/regress-internal-id-from-remote new file mode 100644 index 0000000000000000000000000000000000000000..12cc46feab0563959f2792f48af929cfd8a20be9 GIT binary patch literal 3344 zcma!HWo7vPpWz=6`~?CblduYQhEXsY0)s0AVn&nc;EE8^UGx9{e>T!}5!QiRdKrgR z5Y{lNVl)IsLtv1GfD!{cdV7TB>;M1%UL~+rSedAI8r`41Pllld=z0f+2|$t)CJ6Bl z^IL{yh89K!h6JV$KoSImq5478-&YANz;ft60|U7f#7yS*Q2Jv60|yfW1IVfd1_lp7 zAz=|wF>wh=DQOv5Ie7&|C1n*=HFXV5Eo~iLJ$(a1BV!X&D|0goOKTfjJ9`I5CubK| zH+K(DFK-`TKmUNhpx}_uu<(e;sOXs3xcG#`q~w&;wDgS3tn8fJy!?W~qT-U$vhs?` zs_L5By84F3rskH`w)T$BuI`@RzWxalCrzF*b=ve9GiS}7Gk4zn1q&A~Ub1xA@)avr ztzNTs-TDn1H$mJzz@+s6+durQALQBQ@4x>Hj7-cdtZeKYoLt;IynOruMCBDTa|=r= QD(4kw>@#ELnfDBH0Mq3#EdT%j literal 0 HcmV?d00001 diff --git a/Tests/Unit/SimulatedPeerTests.cpp b/Tests/Unit/SimulatedPeerTests.cpp index a451e1ed2..dad6615be 100644 --- a/Tests/Unit/SimulatedPeerTests.cpp +++ b/Tests/Unit/SimulatedPeerTests.cpp @@ -200,3 +200,23 @@ TEST(SimulatedPeer, SameSeedGivesTheSameTraffic) EXPECT_EQ(sent[0], sent[1]); EXPECT_EQ(dropped[0], dropped[1]); } + +TEST(SimulatedPeer, InternalOnlyMessageIdFromARemoteIsDroppedAndTheQueueContinues) +{ + // ID_CONNECTION_ATTEMPT_FAILED is generated locally only. A connected peer + // sending it used to trip a RakAssert (Debug), leak the message and stop + // the receive loop for that update (found by the peer_datagram fuzzer). + SimulatedNetwork net; + RakPeer *server = net.AddPeer(1, 1); + RakPeer *client = net.AddPeer(1); + ASSERT_TRUE(net.Connect(client, server)); + + BitStream forged; + forged.Write((MessageID) ID_CONNECTION_ATTEMPT_FAILED); + ASSERT_NE(0u, client->Send(&forged, Priority::High, Reliability::ReliableOrdered, 0, net.AddressOf(server), false)); + SendNumbered(client, net.AddressOf(server), 7, 4); + + ASSERT_TRUE(net.RunUntilReceived(server, ID_TEST_PAYLOAD, 2000)); + EXPECT_FALSE(net.Received(server, ID_CONNECTION_ATTEMPT_FAILED)); + EXPECT_EQ(IS_CONNECTED, server->GetConnectionState(net.AddressOf(client))); +} From bbfc093ddbc662390cc25f30b4df6b62f6cf8722 Mon Sep 17 00:00:00 2001 From: Segfault <5221072+Segfaultd@users.noreply.github.com> Date: Sat, 17 Oct 2026 18:02:39 +0200 Subject: [PATCH 5/6] docs: document the fuzz-debug preset --- CLAUDE.md | 3 ++- docs/getting-started/building.rst | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index 63651c890..5b5884320 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -73,7 +73,7 @@ Available generators (run `cmake --help` for full list): | `MAFIANET_COVERAGE` | OFF | `--coverage` instrumentation; use the `coverage` preset, report with gcovr | Presets (`CMakePresets.json`, CMake 3.25+): `debug`, `release`, `ci`, `minimal`, `asan`, `tsan`, -`fuzz`, `bench`, `coverage`. +`fuzz`, `fuzz-debug` (harnesses with RakAssert enabled), `bench`, `coverage`. `cmake --workflow --preset ` configures, builds and tests in one go; run sanitizer tests through `ctest --preset asan|tsan` so the sanitizer options and `ci/tsan.supp` are applied. Known by-design TSan races live in `ci/tsan.supp`; add an entry only @@ -92,6 +92,7 @@ Each is also built as `_replay`, which replays `Tests/Fuzz/corpus//` `FuzzCorpus.` on every compiler. When a fuzzer finds a crash, fix it and add the minimized reproducer to that corpus directory. Code that parses network input must not `RakAssert` on malformed data: a Debug build would then abort on whatever a peer sends — reject and drop instead. +The `fuzz-debug` preset (run nightly) builds the harnesses with asserts enabled to find such cases. When adding or removing a plugin guarded by a `_RAKNET_SUPPORT_` flag, update `MAFIANET_OPTIONAL_FEATURES` in `Source/CMakeLists.txt` as well, or the CMake diff --git a/docs/getting-started/building.rst b/docs/getting-started/building.rst index 460cf8d5a..3bf9b1a24 100644 --- a/docs/getting-started/building.rst +++ b/docs/getting-started/building.rst @@ -239,6 +239,8 @@ preset builds into ``build/``: - RelWithDebInfo and tests under AddressSanitizer + UBSan, or ThreadSanitizer. Not available on Windows. * - ``fuzz`` - Clang, RelWithDebInfo, the libFuzzer harnesses under ASan + UBSan. + * - ``fuzz-debug`` + - The same in Debug, so ``RakAssert`` is live: an assert reachable from network input is reported as a crash. * - ``bench`` - Release, the microbenchmarks. * - ``coverage`` From 1429ad9cdc1712a5c6313474c058c60e225175a6 Mon Sep 17 00:00:00 2001 From: Segfault <5221072+Segfaultd@users.noreply.github.com> Date: Sat, 17 Oct 2026 18:10:46 +0200 Subject: [PATCH 6/6] test: make the migrated tests build-type independent and GCC-clean - PingTests.OccasionalPingIsObservableAsPingTraffic counted keep-alive pings too in Release: the keep-alive fires every half timeout, and the default timeout is 30 s in Debug but 10 s in Release. Pin the timeout. - Brace single-assertion if bodies: gtest macros expand to if/else, and GCC's -Wdangling-else (fatal under -Werror) flags them. --- Tests/Unit/DisconnectReasonTests.cpp | 2 ++ Tests/Unit/DroppedConnectionConvertTests.cpp | 6 ++++++ Tests/Unit/EightPeerTests.cpp | 2 ++ Tests/Unit/MaximumConnectTests.cpp | 2 ++ Tests/Unit/PeerConnectDisconnectTests.cpp | 2 ++ Tests/Unit/PeerConnectDisconnectWithCancelPendingTests.cpp | 2 ++ Tests/Unit/PingTestsTests.cpp | 5 +++++ 7 files changed, 21 insertions(+) diff --git a/Tests/Unit/DisconnectReasonTests.cpp b/Tests/Unit/DisconnectReasonTests.cpp index d14f7451d..962127f02 100644 --- a/Tests/Unit/DisconnectReasonTests.cpp +++ b/Tests/Unit/DisconnectReasonTests.cpp @@ -131,7 +131,9 @@ TEST(DisconnectReason, SamePeersReconnectAndCarryEachReasonInTurn) ASSERT_NE(nullptr, note); ASSERT_EQ(expectedLength[round], note->data.size()); if (round == 0) + { EXPECT_EQ(42, note->data[1]); + } // Both sides must release the slot before the next round's Connect(). ASSERT_TRUE(net.RunUntil([&] { diff --git a/Tests/Unit/DroppedConnectionConvertTests.cpp b/Tests/Unit/DroppedConnectionConvertTests.cpp index 1f800802e..95bdd0561 100644 --- a/Tests/Unit/DroppedConnectionConvertTests.cpp +++ b/Tests/Unit/DroppedConnectionConvertTests.cpp @@ -130,7 +130,9 @@ TEST_F(DroppedConnectionConvertSim, RandomDropsAndReconnectsStayConsistent) { const int index = (int) (NextRandom() % kClients); if (!BusyOrConnected(index)) + { ASSERT_EQ(CONNECTION_ATTEMPT_STARTED, net.StartConnect(clients[index], server)) << "client " << index; + } break; } case 2: // every client: drop (silently or not) or reconnect @@ -139,7 +141,9 @@ TEST_F(DroppedConnectionConvertSim, RandomDropsAndReconnectsStayConsistent) if (NextRandom() % 2 == 0) clients[index]->CloseConnection(serverAddress, NextRandom() % 2 == 0, 0); else if (!BusyOrConnected(index)) + { ASSERT_EQ(CONNECTION_ATTEMPT_STARTED, net.StartConnect(clients[index], server)) << "client " << index; + } } break; case 3: // quiet period: the server must catch up with every drop @@ -163,7 +167,9 @@ TEST_F(DroppedConnectionConvertSim, RandomDropsAndReconnectsStayConsistent) const bool allBack = net.RunUntil([&] { for (int index = 0; index < kClients; ++index) if (!BusyOrConnected(index)) + { EXPECT_EQ(CONNECTION_ATTEMPT_STARTED, net.StartConnect(clients[index], server)) << "client " << index; + } return ConnectedClients() == kClients && ConnectionCount(server) == kClients; }, 20000, 100); EXPECT_TRUE(allBack) << ConnectedClients() << " clients and " << ConnectionCount(server) << " server connections"; diff --git a/Tests/Unit/EightPeerTests.cpp b/Tests/Unit/EightPeerTests.cpp index df797d3c5..6767169d3 100644 --- a/Tests/Unit/EightPeerTests.cpp +++ b/Tests/Unit/EightPeerTests.cpp @@ -141,7 +141,9 @@ TEST_F(EightPeerSim, ReliableOrderedBroadcastArrivesCompleteAndInOrder) } for (int j = 0; j < kPeerNum; ++j) if (j != i) + { EXPECT_EQ(kNumPackets, nextFrom[j]) << "peer " << i << " received " << nextFrom[j] << " from " << j; + } } ExpectNoFailures(); } diff --git a/Tests/Unit/MaximumConnectTests.cpp b/Tests/Unit/MaximumConnectTests.cpp index 6d12ba923..3d8ee76f0 100644 --- a/Tests/Unit/MaximumConnectTests.cpp +++ b/Tests/Unit/MaximumConnectTests.cpp @@ -86,9 +86,11 @@ TEST(MaximumConnectSim, MeshNeverExceedsTheConnectionLimit) // Both ends agree on every link. for (int j = 0; j < kPeerNum; ++j) if (i != j) + { EXPECT_EQ(peers[i]->GetConnectionState(net.AddressOf(peers[j])) == IS_CONNECTED, peers[j]->GetConnectionState(net.AddressOf(peers[i])) == IS_CONNECTED) << "asymmetric link " << i << " <-> " << j; + } } // A full mesh would need 7 per peer: some attempts were refused, but the // peers did connect rather than refusing everything. (Not every peer diff --git a/Tests/Unit/PeerConnectDisconnectTests.cpp b/Tests/Unit/PeerConnectDisconnectTests.cpp index f32cd16cc..9c17674b7 100644 --- a/Tests/Unit/PeerConnectDisconnectTests.cpp +++ b/Tests/Unit/PeerConnectDisconnectTests.cpp @@ -95,8 +95,10 @@ TEST_F(PeerConnectDisconnectSim, MeshReconnectsAfterRepeatedDisconnects) for (int i = 0; i < kPeerNum; ++i) for (int j = i + 1; j < kPeerNum; ++j) if (!BusyOrConnected(i, j)) + { ASSERT_EQ(CONNECTION_ATTEMPT_STARTED, net.StartConnect(peers[i], peers[j])) << "cycle " << cycle << ": " << i << " -> " << j << " in state " << StateOf(i, j); + } ASSERT_TRUE(net.RunUntil([&] { return ConnectionRequestsComplete(); }, 10000)) << "cycle " << cycle << ": connection requests never completed"; diff --git a/Tests/Unit/PeerConnectDisconnectWithCancelPendingTests.cpp b/Tests/Unit/PeerConnectDisconnectWithCancelPendingTests.cpp index 21b5ea480..4feaa2cad 100644 --- a/Tests/Unit/PeerConnectDisconnectWithCancelPendingTests.cpp +++ b/Tests/Unit/PeerConnectDisconnectWithCancelPendingTests.cpp @@ -105,7 +105,9 @@ TEST_F(PeerConnectDisconnectWithCancelPendingSim, MeshReconnectsWithCancelledPen { peers[i]->CancelConnectionAttempt(net.AddressOf(peers[j])); if (net.StartConnect(peers[i], peers[j]) != CONNECTION_ATTEMPT_STARTED) + { ASSERT_NE(0, ConnectionCount(i)) << "Connect " << i << " -> " << j << " refused with no connection at all"; + } } net.RunUntil([&] { diff --git a/Tests/Unit/PingTestsTests.cpp b/Tests/Unit/PingTestsTests.cpp index e0f2dba92..51c563c84 100644 --- a/Tests/Unit/PingTestsTests.cpp +++ b/Tests/Unit/PingTestsTests.cpp @@ -172,6 +172,11 @@ TEST(PingTests, OccasionalPingIsObservableAsPingTraffic) counter.senderA = net.AddressOf(pinging); counter.senderB = net.AddressOf(quiet); + // The keep-alive also pings, every half timeout without reliable traffic, + // and the default timeout is 30 s in Debug but 10 s in Release. Pin it so + // only occasional pings fall inside the measured window in every build. + for (RakPeer *peer : {receiver, pinging, quiet}) + peer->SetTimeoutTime(30000, UNASSIGNED_SYSTEM_ADDRESS); pinging->SetOccasionalPing(true); quiet->SetOccasionalPing(false); ASSERT_TRUE(net.Connect(pinging, receiver));