aboutsummaryrefslogtreecommitdiff
path: root/src/tests/core
diff options
context:
space:
mode:
authorMako <137927095+Takoroka@users.noreply.github.com>2026-11-02 19:48:22 -0500
committerGitHub <noreply@github.com>2026-11-02 19:48:22 -0500
commit28051fc764f6d2a6f9c64f8166c02dd2d13d77e7 (patch)
tree3aa8c43b64f68d23d3c24b00e0e1498bc4b4c4a1 /src/tests/core
parentbd12e7c4ef7ddfcc56d88ee71612a671f582e47b (diff)
parentd8d16713b891245fd786b27d375f68d695c76d0e (diff)
Merge pull request #313 from cira0100/ldn/room-handshake
Diffstat (limited to 'src/tests/core')
-rw-r--r--src/tests/core/internal_network/socket_proxy.cpp261
-rw-r--r--src/tests/core/ldn.cpp202
2 files changed, 463 insertions, 0 deletions
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..137de5b421
--- /dev/null
+++ b/src/tests/core/internal_network/socket_proxy.cpp
@@ -0,0 +1,261 @@
+// 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 "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);
+}
+
+Network::ProxyPacket MakePacket(Network::Protocol protocol) {
+ 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;
+ return packet;
+}
+
+template <std::size_t N>
+void QueuePacket(Network::ProxySocket& socket, Network::Protocol protocol,
+ const std::array<u8, N>& payload) {
+ Network::ProxyPacket packet = MakePacket(protocol);
+ packet.data.assign(payload.begin(), payload.end());
+ 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});
+ }
+}
+
+TEST_CASE("Room sockets accept only packets for their port and protocol", "[core][sockets]") {
+ Network::ProxySocket socket;
+ BindSocket(socket, Network::Protocol::UDP);
+
+ REQUIRE(socket.AcceptsProxyPacket(MakePacket(Network::Protocol::UDP)));
+ REQUIRE_FALSE(socket.AcceptsProxyPacket(MakePacket(Network::Protocol::TCP)));
+
+ Network::ProxyPacket other_port = MakePacket(Network::Protocol::UDP);
+ other_port.remote_endpoint.portno = 35001;
+ REQUIRE_FALSE(socket.AcceptsProxyPacket(other_port));
+
+ Network::ProxyPacket broadcast = MakePacket(Network::Protocol::UDP);
+ broadcast.broadcast = true;
+ REQUIRE_FALSE(socket.AcceptsProxyPacket(broadcast));
+ REQUIRE(socket.SetBroadcast(true) == Network::Errno::SUCCESS);
+ REQUIRE(socket.AcceptsProxyPacket(broadcast));
+
+ REQUIRE(socket.Close() == Network::Errno::SUCCESS);
+ REQUIRE_FALSE(socket.AcceptsProxyPacket(MakePacket(Network::Protocol::UDP)));
+}
+
+TEST_CASE("Native sockets accept no room packets", "[core][sockets]") {
+ Network::Socket socket;
+ REQUIRE_FALSE(socket.AcceptsProxyPacket(MakePacket(Network::Protocol::UDP)));
+}
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());
+}