diff options
| -rw-r--r-- | src/tests/CMakeLists.txt | 2 | ||||
| -rw-r--r-- | src/tests/core/internal_network/socket_proxy.cpp | 231 | ||||
| -rw-r--r-- | src/tests/core/ldn.cpp | 202 |
3 files changed, 435 insertions, 0 deletions
diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt index 1eb1040e34..a31c073765 100644 --- a/src/tests/CMakeLists.txt +++ b/src/tests/CMakeLists.txt @@ -19,6 +19,8 @@ add_executable(tests common/undefined_fix.cpp common/unique_function.cpp core/core_timing.cpp + core/ldn.cpp + core/internal_network/socket_proxy.cpp core/nup_writer.cpp core/priority_queue.cpp core/internal_network/network.cpp diff --git a/src/tests/core/internal_network/socket_proxy.cpp b/src/tests/core/internal_network/socket_proxy.cpp new file mode 100644 index 0000000000..d97d7d5102 --- /dev/null +++ b/src/tests/core/internal_network/socket_proxy.cpp @@ -0,0 +1,231 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#include <array> +#include <chrono> +#include <thread> + +#include <catch2/catch_test_macros.hpp> + +#include "common/socket_types.h" +#include "common/zstd_compression.h" +#include "core/internal_network/socket_proxy.h" + +namespace { + +void BindSocket(Network::ProxySocket& socket, Network::Protocol protocol) { + const auto type = + protocol == Network::Protocol::TCP ? Network::Type::STREAM : Network::Type::DGRAM; + REQUIRE(socket.Initialize(Network::Domain::INET, type, protocol) == Network::Errno::SUCCESS); + REQUIRE(socket.Bind({.family = Network::Domain::INET, + .ip = {192, 168, 1, 1}, + .portno = 35000}) == Network::Errno::SUCCESS); +} + +template <std::size_t N> +void QueuePacket(Network::ProxySocket& socket, Network::Protocol protocol, + const std::array<u8, N>& payload) { + Network::ProxyPacket packet{}; + packet.local_endpoint = {.family = Network::Domain::INET, + .ip = {192, 168, 1, 2}, + .portno = 35000}; + packet.remote_endpoint = {.family = Network::Domain::INET, + .ip = {192, 168, 1, 1}, + .portno = 35000}; + packet.protocol = protocol; + packet.data = Common::Compression::CompressDataZSTDDefault(payload.data(), payload.size()); + socket.HandleProxyPacket(packet); +} + +} // namespace + +TEST_CASE("Room UDP sockets receive queued datagrams with recv", "[core][sockets]") { + Network::ProxySocket socket; + BindSocket(socket, Network::Protocol::UDP); + REQUIRE(socket.SetNonBlock(true) == Network::Errno::SUCCESS); + + const std::array<u8, 4> payload{1, 2, 3, 4}; + QueuePacket(socket, Network::Protocol::UDP, payload); + + std::array<u8, 4> received{}; + const auto [count, error] = socket.Recv(0, received); + REQUIRE(error == Network::Errno::SUCCESS); + REQUIRE(count == payload.size()); + REQUIRE(received == payload); + + const auto [empty_count, empty_error] = socket.Recv(0, received); + REQUIRE(empty_count == -1); + REQUIRE(empty_error == Network::Errno::AGAIN); +} + +TEST_CASE("Room sockets peek without consuming", "[core][sockets]") { + Network::ProxySocket socket; + BindSocket(socket, Network::Protocol::UDP); + REQUIRE(socket.SetNonBlock(true) == Network::Errno::SUCCESS); + + const std::array<u8, 4> payload{1, 2, 3, 4}; + QueuePacket(socket, Network::Protocol::UDP, payload); + + std::array<u8, 4> received{}; + REQUIRE(socket.Recv(Network::FLAG_MSG_PEEK, received) == + std::pair<s32, Network::Errno>{4, Network::Errno::SUCCESS}); + REQUIRE(socket.Recv(0, received) == std::pair<s32, Network::Errno>{4, Network::Errno::SUCCESS}); + REQUIRE(received == payload); + REQUIRE(socket.Recv(0, received).second == Network::Errno::AGAIN); +} + +TEST_CASE("Room UDP sockets truncate oversized datagrams", "[core][sockets]") { + Network::ProxySocket socket; + BindSocket(socket, Network::Protocol::UDP); + REQUIRE(socket.SetNonBlock(true) == Network::Errno::SUCCESS); + + QueuePacket(socket, Network::Protocol::UDP, std::array<u8, 4>{1, 2, 3, 4}); + + std::array<u8, 2> received{}; + REQUIRE(socket.Recv(0, received) == std::pair<s32, Network::Errno>{2, Network::Errno::SUCCESS}); + REQUIRE(received == std::array<u8, 2>{1, 2}); + REQUIRE(socket.Recv(0, received).second == Network::Errno::AGAIN); +} + +TEST_CASE("Room TCP sockets keep the unread part of a packet", "[core][sockets]") { + Network::ProxySocket socket; + BindSocket(socket, Network::Protocol::TCP); + REQUIRE(socket.SetNonBlock(true) == Network::Errno::SUCCESS); + + QueuePacket(socket, Network::Protocol::TCP, std::array<u8, 6>{1, 2, 3, 4, 5, 6}); + + std::array<u8, 4> received{}; + REQUIRE(socket.Recv(0, received) == std::pair<s32, Network::Errno>{4, Network::Errno::SUCCESS}); + REQUIRE(received == std::array<u8, 4>{1, 2, 3, 4}); + REQUIRE(socket.Recv(0, received) == std::pair<s32, Network::Errno>{2, Network::Errno::SUCCESS}); + REQUIRE(received[0] == 5); + REQUIRE(received[1] == 6); + REQUIRE(socket.Recv(0, received) == std::pair<s32, Network::Errno>{0, Network::Errno::SUCCESS}); +} + +TEST_CASE("Room TCP sockets read a packet in small parts and peek at the unread part", + "[core][sockets]") { + Network::ProxySocket socket; + BindSocket(socket, Network::Protocol::TCP); + REQUIRE(socket.SetNonBlock(true) == Network::Errno::SUCCESS); + + QueuePacket(socket, Network::Protocol::TCP, std::array<u8, 5>{1, 2, 3, 4, 5}); + QueuePacket(socket, Network::Protocol::TCP, std::array<u8, 2>{6, 7}); + + std::array<u8, 2> received{}; + REQUIRE(socket.Recv(0, received) == std::pair<s32, Network::Errno>{2, Network::Errno::SUCCESS}); + REQUIRE(received == std::array<u8, 2>{1, 2}); + REQUIRE(socket.Recv(Network::FLAG_MSG_PEEK, received) == + std::pair<s32, Network::Errno>{2, Network::Errno::SUCCESS}); + REQUIRE(received == std::array<u8, 2>{3, 4}); + REQUIRE(socket.Recv(0, received) == std::pair<s32, Network::Errno>{2, Network::Errno::SUCCESS}); + REQUIRE(received == std::array<u8, 2>{3, 4}); + REQUIRE(socket.Recv(0, received) == std::pair<s32, Network::Errno>{1, Network::Errno::SUCCESS}); + REQUIRE(received[0] == 5); + REQUIRE(socket.Recv(0, received) == std::pair<s32, Network::Errno>{2, Network::Errno::SUCCESS}); + REQUIRE(received == std::array<u8, 2>{6, 7}); +} + +TEST_CASE("Room TCP sockets report an empty queue as end of stream", "[core][sockets]") { + Network::ProxySocket socket; + BindSocket(socket, Network::Protocol::TCP); + + std::array<u8, 4> received{}; + const auto start = std::chrono::steady_clock::now(); + REQUIRE(socket.Recv(0, received) == std::pair<s32, Network::Errno>{0, Network::Errno::SUCCESS}); + REQUIRE(std::chrono::steady_clock::now() - start < std::chrono::seconds{1}); +} + +TEST_CASE("Blocking room sockets wait for packets and time out with EAGAIN", "[core][sockets]") { + Network::ProxySocket socket; + BindSocket(socket, Network::Protocol::UDP); + REQUIRE(socket.SetRcvTimeo(50) == Network::Errno::SUCCESS); + + std::array<u8, 4> received{}; + const auto start = std::chrono::steady_clock::now(); + REQUIRE(socket.Recv(0, received) == std::pair<s32, Network::Errno>{-1, Network::Errno::AGAIN}); + REQUIRE(std::chrono::steady_clock::now() - start >= std::chrono::milliseconds{50}); + + REQUIRE(socket.SetRcvTimeo(5000) == Network::Errno::SUCCESS); + const std::array<u8, 4> payload{1, 2, 3, 4}; + std::jthread sender([&socket, &payload] { + std::this_thread::sleep_for(std::chrono::milliseconds{20}); + QueuePacket(socket, Network::Protocol::UDP, payload); + }); + REQUIRE(socket.Recv(0, received) == std::pair<s32, Network::Errno>{4, Network::Errno::SUCCESS}); + REQUIRE(received == payload); +} + +TEST_CASE("Closing a room socket wakes a blocked recv and drops queued packets", "[core][sockets]") { + Network::ProxySocket socket; + BindSocket(socket, Network::Protocol::UDP); + REQUIRE(socket.SetRcvTimeo(5000) == Network::Errno::SUCCESS); + + std::array<u8, 4> received{}; + const auto start = std::chrono::steady_clock::now(); + std::jthread closer([&socket] { + std::this_thread::sleep_for(std::chrono::milliseconds{20}); + socket.Close(); + }); + REQUIRE(socket.Recv(0, received) == std::pair<s32, Network::Errno>{-1, Network::Errno::BADF}); + REQUIRE(std::chrono::steady_clock::now() - start < std::chrono::seconds{1}); + closer.join(); + + Network::ProxySocket queued; + BindSocket(queued, Network::Protocol::UDP); + QueuePacket(queued, Network::Protocol::UDP, std::array<u8, 4>{1, 2, 3, 4}); + REQUIRE(queued.Close() == Network::Errno::SUCCESS); + QueuePacket(queued, Network::Protocol::UDP, std::array<u8, 4>{5, 6, 7, 8}); + REQUIRE(queued.Recv(0, received) == std::pair<s32, Network::Errno>{-1, Network::Errno::BADF}); +} + +TEST_CASE("Room stream sockets with the default protocol keep stream behavior", "[core][sockets]") { + // socket(AF_INET, SOCK_STREAM, 0) leaves the protocol unspecified + Network::ProxySocket socket; + REQUIRE(socket.Initialize(Network::Domain::INET, Network::Type::STREAM, + Network::Protocol::Unspecified) == Network::Errno::SUCCESS); + REQUIRE(socket.Bind({.family = Network::Domain::INET, + .ip = {192, 168, 1, 1}, + .portno = 35000}) == Network::Errno::SUCCESS); + REQUIRE(socket.SetRcvTimeo(1000) == Network::Errno::SUCCESS); + + std::array<u8, 2> received{}; + const auto start = std::chrono::steady_clock::now(); + REQUIRE(socket.Recv(0, received) == std::pair<s32, Network::Errno>{0, Network::Errno::SUCCESS}); + REQUIRE(std::chrono::steady_clock::now() - start < std::chrono::milliseconds{500}); + + QueuePacket(socket, Network::Protocol::Unspecified, std::array<u8, 4>{1, 2, 3, 4}); + REQUIRE(socket.Recv(0, received) == std::pair<s32, Network::Errno>{2, Network::Errno::SUCCESS}); + REQUIRE(received == std::array<u8, 2>{1, 2}); + REQUIRE(socket.Recv(0, received) == std::pair<s32, Network::Errno>{2, Network::Errno::SUCCESS}); + REQUIRE(received == std::array<u8, 2>{3, 4}); +} + +TEST_CASE("One room packet wakes every blocked peek reader", "[core][sockets]") { + Network::ProxySocket socket; + BindSocket(socket, Network::Protocol::UDP); + REQUIRE(socket.SetRcvTimeo(2000) == Network::Errno::SUCCESS); + + // Catch2 assertions are not thread-safe here, so the readers only record their results + std::array<std::pair<s32, Network::Errno>, 2> results{}; + std::array<std::chrono::steady_clock::time_point, 2> finished{}; + std::chrono::steady_clock::time_point queued{}; + { + std::array<std::jthread, 2> readers; + for (std::size_t i = 0; i < readers.size(); ++i) { + readers[i] = std::jthread([&socket, &results, &finished, i] { + std::array<u8, 4> peeked{}; + results[i] = socket.Recv(Network::FLAG_MSG_PEEK, peeked); + finished[i] = std::chrono::steady_clock::now(); + }); + } + std::this_thread::sleep_for(std::chrono::milliseconds{50}); + queued = std::chrono::steady_clock::now(); + QueuePacket(socket, Network::Protocol::UDP, std::array<u8, 4>{1, 2, 3, 4}); + } + + for (std::size_t i = 0; i < results.size(); ++i) { + REQUIRE(results[i] == std::pair<s32, Network::Errno>{4, Network::Errno::SUCCESS}); + REQUIRE(finished[i] - queued < std::chrono::milliseconds{1000}); + } +} diff --git a/src/tests/core/ldn.cpp b/src/tests/core/ldn.cpp new file mode 100644 index 0000000000..4e6da2d52e --- /dev/null +++ b/src/tests/core/ldn.cpp @@ -0,0 +1,202 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#include <algorithm> +#include <chrono> +#include <cstring> +#include <future> +#include <mutex> +#include <thread> +#include <catch2/catch_test_macros.hpp> + +#include "core/hle/service/ldn/lan_discovery.h" +#include "core/internal_network/network_interface.h" + +using namespace std::chrono_literals; + +namespace { + +class TestLANDiscovery : public Service::LDN::LANDiscovery { +public: + void SetConnectionTimeout(std::chrono::nanoseconds timeout) { + connection_timeout = timeout; + } + + std::chrono::nanoseconds GetConnectionTimeout() const { + return connection_timeout; + } + + bool WaitUntilConnecting() { + const auto deadline = std::chrono::steady_clock::now() + 5s; + do { + { + std::scoped_lock lock{packet_mutex}; + if (host_ip) { + // Connect has published its target and released this lock to wait for a reply. + return true; + } + } + std::this_thread::sleep_for(1ms); + } while (std::chrono::steady_clock::now() < deadline); + return false; + } +}; + +} // namespace + +TEST_CASE("LDN waits for the selected host's connection reply", "[core][ldn]") { + using namespace Service::LDN; + if (!Network::GetSelectedNetworkInterface()) { + SKIP("LDN requires a host IPv4 interface"); + } + + TestLANDiscovery station; + REQUIRE(station.Initialize([] {}).IsSuccess()); + REQUIRE(station.OpenStation().IsSuccess()); + + NetworkInfo info{}; + info.network_id.session_id = {1, 2}; + info.ldn.node_count = 1; + info.ldn.node_count_max = 8; + info.ldn.nodes[0].ipv4_address = {1, 0, 168, 192}; + + Network::LDNPacket reply{}; + reply.type = Network::LDNPacketType::SyncNetwork; + reply.local_ip = {192, 168, 0, 1}; + reply.data.resize(sizeof(info)); + std::memcpy(reply.data.data(), &info, sizeof(info)); + + // A scan result or unsolicited sync must not connect an idle station. + station.ReceivePacket(reply); + REQUIRE(station.GetState() == State::StationOpened); + + auto connecting = std::async(std::launch::async, [&] { return station.Connect(info, {}, 1); }); + REQUIRE(station.WaitUntilConnecting()); + + auto unrelated = reply; + unrelated.local_ip[3] = 2; + station.ReceivePacket(unrelated); + REQUIRE(connecting.wait_for(0ms) == std::future_status::timeout); + + auto stale = info; + stale.network_id.session_id.low++; + std::memcpy(unrelated.data.data(), &stale, sizeof(stale)); + unrelated.local_ip = reply.local_ip; + station.ReceivePacket(unrelated); + REQUIRE(connecting.wait_for(0ms) == std::future_status::timeout); + + unrelated.data.resize(1); + station.ReceivePacket(unrelated); + REQUIRE(connecting.wait_for(0ms) == std::future_status::timeout); + + station.ReceivePacket(reply); + REQUIRE(connecting.wait_for(5s) == std::future_status::ready); + REQUIRE(connecting.get().IsSuccess()); + REQUIRE(station.GetState() == State::StationConnected); + REQUIRE(station.Finalize().IsSuccess()); +} + +TEST_CASE("LDN host validates join packets and does not allocate duplicate stations", + "[core][ldn]") { + using namespace Service::LDN; + if (!Network::GetSelectedNetworkInterface()) { + SKIP("LDN requires a host IPv4 interface"); + } + + LANDiscovery host; + REQUIRE(host.Initialize([] {}).IsSuccess()); + REQUIRE(host.OpenAccessPoint().IsSuccess()); + NetworkConfig config{}; + config.node_count_max = 2; + REQUIRE(host.CreateNetwork({}, {}, config).IsSuccess()); + + NodeInfo node{}; + node.is_connected = 1; + node.mac_address.raw[5] = 1; + Network::LDNPacket request{}; + request.type = Network::LDNPacketType::Connect; + request.local_ip = {192, 168, 0, 2}; + request.data.resize(sizeof(node)); + std::memcpy(request.data.data(), &node, sizeof(node)); + host.ReceivePacket(request); + NetworkInfo info{}; + REQUIRE(host.GetNetworkInfo(info).IsSuccess()); + REQUIRE(info.ldn.node_count == 2); + + host.ReceivePacket(request); + REQUIRE(host.GetNetworkInfo(info).IsSuccess()); + REQUIRE(info.ldn.node_count == 2); + + // A third console cannot enter a two-console network. + request.local_ip[3] = 3; + host.ReceivePacket(request); + REQUIRE(host.GetNetworkInfo(info).IsSuccess()); + REQUIRE(info.ldn.node_count == 2); + + request.type = Network::LDNPacketType::Disconnect; + request.data.resize(1); + host.ReceivePacket(request); + REQUIRE(host.GetNetworkInfo(info).IsSuccess()); + REQUIRE(info.ldn.node_count == 2); + REQUIRE(host.Finalize().IsSuccess()); +} + +TEST_CASE("LDN reports a missing connection reply and ignores late replies", "[core][ldn]") { + using namespace Service::LDN; + if (!Network::GetSelectedNetworkInterface()) { + SKIP("LDN requires a host IPv4 interface"); + } + + TestLANDiscovery station; + REQUIRE(station.GetConnectionTimeout() == 5s); + station.SetConnectionTimeout(25ms); + REQUIRE(station.Initialize([] {}).IsSuccess()); + REQUIRE(station.OpenStation().IsSuccess()); + NetworkInfo info{}; + info.ldn.node_count = 1; + info.ldn.nodes[0].ipv4_address = {1, 0, 168, 192}; + REQUIRE(station.Connect(info, {}, 1) == ResultAuthenticationTimeout); + REQUIRE(station.GetState() == State::StationOpened); + + Network::LDNPacket reply{}; + reply.type = Network::LDNPacketType::SyncNetwork; + reply.local_ip = {192, 168, 0, 1}; + reply.data.resize(sizeof(info)); + std::memcpy(reply.data.data(), &info, sizeof(info)); + station.ReceivePacket(reply); + REQUIRE(station.GetState() == State::StationOpened); + REQUIRE(station.Finalize().IsSuccess()); +} + +TEST_CASE("LDN creates fresh identities for recreated networks and separate hosts", "[core][ldn]") { + using namespace Service::LDN; + if (!Network::GetSelectedNetworkInterface()) { + SKIP("LDN requires a host IPv4 interface"); + } + + LANDiscovery host; + LANDiscovery other_host; + REQUIRE(host.Initialize([] {}).IsSuccess()); + REQUIRE(other_host.Initialize([] {}).IsSuccess()); + REQUIRE(host.OpenAccessPoint().IsSuccess()); + REQUIRE(other_host.OpenAccessPoint().IsSuccess()); + NetworkConfig config{}; + config.node_count_max = 2; + REQUIRE(host.CreateNetwork({}, {}, config).IsSuccess()); + NetworkInfo first{}; + REQUIRE(host.GetNetworkInfo(first).IsSuccess()); + + REQUIRE(host.DestroyNetwork().IsSuccess()); + REQUIRE(host.CreateNetwork({}, {}, config).IsSuccess()); + NetworkInfo recreated{}; + REQUIRE(host.GetNetworkInfo(recreated).IsSuccess()); + REQUIRE(recreated.network_id.session_id != first.network_id.session_id); + + REQUIRE(other_host.CreateNetwork({}, {}, config).IsSuccess()); + NetworkInfo other{}; + REQUIRE(other_host.GetNetworkInfo(other).IsSuccess()); + REQUIRE(other.network_id.session_id != first.network_id.session_id); + REQUIRE(other.network_id.session_id != recreated.network_id.session_id); + REQUIRE(host.Finalize().IsSuccess()); + REQUIRE(other_host.Finalize().IsSuccess()); +} |
