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/.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/CLAUDE.md b/CLAUDE.md index 2e6939bf8..5b5884320 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -67,19 +67,20 @@ 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 | 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 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 @@ -91,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/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/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/Source/src/peer.cpp b/Source/src/peer.cpp index 47e49a2be..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; } @@ -6555,7 +6558,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 +6596,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 +6744,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 +6851,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 +6866,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..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); } @@ -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 ) @@ -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 000000000..b7bf60b30 Binary files /dev/null and b/Tests/Fuzz/corpus/peer_datagram/regress-debug-assert-1 differ 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 000000000..5dab64f93 Binary files /dev/null and b/Tests/Fuzz/corpus/peer_datagram/regress-debug-assert-2 differ 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 000000000..2d97cab30 Binary files /dev/null and b/Tests/Fuzz/corpus/peer_datagram/regress-debug-assert-3 differ 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 000000000..12cc46fea Binary files /dev/null and b/Tests/Fuzz/corpus/peer_datagram/regress-internal-id-from-remote differ 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/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/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/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;iSendLoopback(str, (int) strlen(str)+1); client->SendLoopback(str, (int) strlen(str)+1); client->SendLoopback(str, (int) strlen(str)+1); diff --git a/Tests/Integration/MTUNegotiationTests.cpp b/Tests/Integration/MTUNegotiationTests.cpp deleted file mode 100644 index 7b22900d0..000000000 --- a/Tests/Integration/MTUNegotiationTests.cpp +++ /dev/null @@ -1,467 +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 "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/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/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 d7a0868ce..639501dec 100644 --- a/Tests/Support/SimulatedNetwork.h +++ b/Tests/Support/SimulatedNetwork.h @@ -88,14 +88,17 @@ 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. 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/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..962127f02 --- /dev/null +++ b/Tests/Unit/DisconnectReasonTests.cpp @@ -0,0 +1,145 @@ +/* + * 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..95bdd0561 --- /dev/null +++ b/Tests/Unit/DroppedConnectionConvertTests.cpp @@ -0,0 +1,176 @@ +/* + * 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..6767169d3 --- /dev/null +++ b/Tests/Unit/EightPeerTests.cpp @@ -0,0 +1,149 @@ +/* + * 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..3d8ee76f0 --- /dev/null +++ b/Tests/Unit/MaximumConnectTests.cpp @@ -0,0 +1,102 @@ +/* + * 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..9c17674b7 --- /dev/null +++ b/Tests/Unit/PeerConnectDisconnectTests.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 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..4feaa2cad --- /dev/null +++ b/Tests/Unit/PeerConnectDisconnectWithCancelPendingTests.cpp @@ -0,0 +1,157 @@ +/* + * 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..51c563c84 --- /dev/null +++ b/Tests/Unit/PingTestsTests.cpp @@ -0,0 +1,192 @@ +/* + * 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); + + // 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)); + 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/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/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/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))); +} 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 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 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``