aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/common/zstd_compression.cpp20
-rw-r--r--src/common/zstd_compression.h13
-rw-r--r--src/core/hle/service/ldn/lan_discovery.cpp85
-rw-r--r--src/core/hle/service/ldn/lan_discovery.h8
-rw-r--r--src/core/hle/service/nifm/nifm.cpp68
-rw-r--r--src/core/hle/service/sockets/bsd.cpp390
-rw-r--r--src/core/hle/service/sockets/bsd.h18
-rw-r--r--src/core/internal_network/network.cpp15
-rw-r--r--src/core/internal_network/network.h4
-rw-r--r--src/core/internal_network/socket_proxy.cpp157
-rw-r--r--src/core/internal_network/socket_proxy.h13
-rw-r--r--src/core/internal_network/sockets.h6
-rw-r--r--src/tests/CMakeLists.txt3
-rw-r--r--src/tests/common/zstd_compression.cpp59
-rw-r--r--src/tests/core/internal_network/socket_proxy.cpp261
-rw-r--r--src/tests/core/ldn.cpp202
-rw-r--r--src/yuzu_cmd/yuzu.cpp11
17 files changed, 1047 insertions, 286 deletions
diff --git a/src/common/zstd_compression.cpp b/src/common/zstd_compression.cpp
index 7665482ee6..9bb8d7b919 100644
--- a/src/common/zstd_compression.cpp
+++ b/src/common/zstd_compression.cpp
@@ -35,16 +35,26 @@ std::vector<u8> CompressDataZSTDDefault(const u8* source, std::size_t source_siz
}
std::vector<u8> DecompressDataZSTD(std::span<const u8> compressed) {
- const std::size_t decompressed_size =
+ return TryDecompressDataZSTD(compressed, std::vector<u8>{}.max_size())
+ .value_or(std::vector<u8>{});
+}
+
+std::optional<std::vector<u8>> TryDecompressDataZSTD(std::span<const u8> compressed,
+ std::size_t max_size) {
+ // The size comes from the frame header, so check it before allocating
+ const unsigned long long decompressed_size =
ZSTD_getFrameContentSize(compressed.data(), compressed.size());
- std::vector<u8> decompressed(decompressed_size);
+ if (decompressed_size == ZSTD_CONTENTSIZE_ERROR ||
+ decompressed_size == ZSTD_CONTENTSIZE_UNKNOWN || decompressed_size > max_size) {
+ return std::nullopt;
+ }
+ std::vector<u8> decompressed(static_cast<std::size_t>(decompressed_size));
const std::size_t uncompressed_result_size = ZSTD_decompress(
decompressed.data(), decompressed.size(), compressed.data(), compressed.size());
- if (decompressed_size != uncompressed_result_size || ZSTD_isError(uncompressed_result_size)) {
- // Decompression failed
- return {};
+ if (ZSTD_isError(uncompressed_result_size) || uncompressed_result_size != decompressed_size) {
+ return std::nullopt;
}
return decompressed;
}
diff --git a/src/common/zstd_compression.h b/src/common/zstd_compression.h
index 819d7d201a..c20d96adb0 100644
--- a/src/common/zstd_compression.h
+++ b/src/common/zstd_compression.h
@@ -6,6 +6,7 @@
#pragma once
+#include <optional>
#include <span>
#include <vector>
@@ -46,6 +47,18 @@ namespace Common::Compression {
[[nodiscard]] std::vector<u8> DecompressDataZSTD(std::span<const u8> compressed);
/**
+ * Decompresses a single Zstandard frame that declares its content size.
+ *
+ * @param compressed the compressed source memory region.
+ * @param max_size the largest content size to accept.
+ *
+ * @return the decompressed data, or nullopt if the frame is invalid, does not declare its size,
+ * or declares more than max_size bytes.
+ */
+[[nodiscard]] std::optional<std::vector<u8>> TryDecompressDataZSTD(std::span<const u8> compressed,
+ std::size_t max_size);
+
+/**
* Decompresses a single Zstandard frame into a caller-provided buffer of known size.
*
* @param compressed the compressed source memory region.
diff --git a/src/core/hle/service/ldn/lan_discovery.cpp b/src/core/hle/service/ldn/lan_discovery.cpp
index 90adcdcee4..d7debdd67b 100644
--- a/src/core/hle/service/ldn/lan_discovery.cpp
+++ b/src/core/hle/service/ldn/lan_discovery.cpp
@@ -5,6 +5,7 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include "core/hle/service/ldn/lan_discovery.h"
+#include "common/random.h"
#include "core/internal_network/network.h"
#include "core/internal_network/network_interface.h"
@@ -70,6 +71,7 @@ void LANDiscovery::InitNodeStateChange() {
}
State LANDiscovery::GetState() const {
+ std::scoped_lock lock{packet_mutex};
return state;
}
@@ -78,6 +80,7 @@ void LANDiscovery::SetState(State new_state) {
}
Result LANDiscovery::GetNetworkInfo(NetworkInfo& out_network) const {
+ std::scoped_lock lock{packet_mutex};
if (state == State::AccessPointCreated || state == State::StationConnected) {
std::memcpy(&out_network, &network_info, sizeof(network_info));
return ResultSuccess;
@@ -88,6 +91,7 @@ Result LANDiscovery::GetNetworkInfo(NetworkInfo& out_network) const {
Result LANDiscovery::GetNetworkInfo(NetworkInfo& out_network,
std::span<NodeLatestUpdate> out_updates) {
+ std::scoped_lock lock{packet_mutex};
if (out_updates.size() > NodeCountMax) {
return ResultInvalidBufferCount;
}
@@ -105,6 +109,7 @@ Result LANDiscovery::GetNetworkInfo(NetworkInfo& out_network,
}
DisconnectReason LANDiscovery::GetDisconnectReason() const {
+ std::scoped_lock lock{packet_mutex};
return disconnect_reason;
}
@@ -194,7 +199,7 @@ Result LANDiscovery::CloseAccessPoint() {
}
if (state == State::AccessPointCreated) {
- DestroyNetwork();
+ DestroyNetworkImpl();
}
ResetStations();
@@ -222,8 +227,8 @@ Result LANDiscovery::CloseStation() {
return ResultBadState;
}
- if (state == State::StationConnected) {
- Disconnect();
+ if (state == State::StationConnected || host_ip) {
+ DisconnectImpl();
}
ResetStations();
@@ -251,7 +256,8 @@ Result LANDiscovery::CreateNetwork(const SecurityConfig& security_config,
network_info.common.channel = network_config.channel;
}
- std::independent_bits_engine<std::mt19937, 64, u64> bits_engine;
+ std::independent_bits_engine<std::mt19937, 64, u64> bits_engine{
+ Common::Random::GetMT19937()};
network_info.network_id.session_id.high = bits_engine();
network_info.network_id.session_id.low = bits_engine();
network_info.network_id.intent_id = network_config.intent_id;
@@ -272,6 +278,11 @@ Result LANDiscovery::CreateNetwork(const SecurityConfig& security_config,
}
Result LANDiscovery::DestroyNetwork() {
+ std::scoped_lock lock{packet_mutex};
+ return DestroyNetworkImpl();
+}
+
+Result LANDiscovery::DestroyNetworkImpl() {
for (auto local_ip : connected_clients) {
SendPacket(Network::LDNPacketType::DestroyNetwork, local_ip);
}
@@ -286,7 +297,10 @@ Result LANDiscovery::DestroyNetwork() {
Result LANDiscovery::Connect(const NetworkInfo& network_info_, const UserConfig& user_config,
u16 local_communication_version) {
- std::scoped_lock lock{packet_mutex};
+ std::unique_lock lock{packet_mutex};
+ if (state != State::StationOpened || host_ip) {
+ return ResultBadState;
+ }
if (network_info_.ldn.node_count == 0) {
return ResultInvalidNodeCount;
}
@@ -299,21 +313,39 @@ Result LANDiscovery::Connect(const NetworkInfo& network_info_, const UserConfig&
Ipv4Address node_host = network_info_.ldn.nodes[0].ipv4_address;
std::reverse(std::begin(node_host), std::end(node_host)); // htonl
host_ip = node_host;
- SendPacket(Network::LDNPacketType::Connect, node_info, *host_ip);
-
+ network_info = network_info_;
InitNodeStateChange();
+ SendPacket(Network::LDNPacketType::Connect, node_info, *host_ip);
- std::this_thread::sleep_for(std::chrono::seconds(1));
+ // The room callback needs packet_mutex to install the host's connection reply.
+ const bool replied = connection_event.wait_for(lock, connection_timeout, [this] {
+ return state != State::StationOpened || !host_ip;
+ });
+ if (!replied) {
+ SendPacket(Network::LDNPacketType::Disconnect, node_info, *host_ip);
+ host_ip.reset();
+ return ResultAuthenticationTimeout;
+ }
+ if (state != State::StationConnected) {
+ return ResultConnectionFailed;
+ }
return ResultSuccess;
}
Result LANDiscovery::Disconnect() {
+ std::scoped_lock lock{packet_mutex};
+ return DisconnectImpl();
+}
+
+Result LANDiscovery::DisconnectImpl() {
if (host_ip) {
SendPacket(Network::LDNPacketType::Disconnect, node_info, *host_ip);
}
+ host_ip.reset();
SetState(State::StationOpened);
+ connection_event.notify_all();
lan_event();
return ResultSuccess;
@@ -346,10 +378,10 @@ Result LANDiscovery::Finalize() {
if (inited) {
if (state == State::AccessPointCreated) {
- DestroyNetwork();
+ DestroyNetworkImpl();
}
- if (state == State::StationConnected) {
- Disconnect();
+ if (state == State::StationConnected || host_ip) {
+ DisconnectImpl();
}
ResetStations();
@@ -392,6 +424,7 @@ void LANDiscovery::OnSyncNetwork(const NetworkInfo& info) {
SetState(State::StationConnected);
}
OnNetworkInfoChanged();
+ connection_event.notify_all();
}
void LANDiscovery::OnDisconnectFromHost() {
@@ -401,6 +434,7 @@ void LANDiscovery::OnDisconnectFromHost() {
SetState(State::StationOpened);
lan_event();
}
+ connection_event.notify_all();
}
void LANDiscovery::OnNetworkInfoChanged() {
@@ -489,6 +523,9 @@ void LANDiscovery::ReceivePacket(const Network::LDNPacket& packet) {
break;
}
case Network::LDNPacketType::ScanResp: {
+ if (packet.data.size() != sizeof(NetworkInfo)) {
+ break;
+ }
LOG_INFO(Frontend, "ScanResp packet received!");
NetworkInfo info{};
@@ -498,6 +535,17 @@ void LANDiscovery::ReceivePacket(const Network::LDNPacket& packet) {
break;
}
case Network::LDNPacketType::Connect: {
+ if (state != State::AccessPointCreated || packet.data.size() != sizeof(NodeInfo)) {
+ break;
+ }
+ if (std::find(connected_clients.begin(), connected_clients.end(), packet.local_ip) !=
+ connected_clients.end()) {
+ SendPacket(Network::LDNPacketType::SyncNetwork, network_info, packet.local_ip);
+ break;
+ }
+ if (network_info.ldn.node_count >= network_info.ldn.node_count_max) {
+ break;
+ }
LOG_INFO(Frontend, "Connect packet received!");
NodeInfo info{};
@@ -518,6 +566,9 @@ void LANDiscovery::ReceivePacket(const Network::LDNPacket& packet) {
break;
}
case Network::LDNPacketType::Disconnect: {
+ if (state != State::AccessPointCreated || packet.data.size() != sizeof(NodeInfo)) {
+ break;
+ }
LOG_INFO(Frontend, "Disconnect packet received!");
connected_clients.erase(
@@ -538,17 +589,23 @@ void LANDiscovery::ReceivePacket(const Network::LDNPacket& packet) {
break;
}
case Network::LDNPacketType::DestroyNetwork: {
+ if (!host_ip || packet.local_ip != *host_ip) {
+ break;
+ }
ResetStations();
OnDisconnectFromHost();
break;
}
case Network::LDNPacketType::SyncNetwork: {
- if (state == State::StationOpened || state == State::StationConnected) {
+ if (host_ip && packet.local_ip == *host_ip &&
+ packet.data.size() == sizeof(NetworkInfo) &&
+ (state == State::StationOpened || state == State::StationConnected)) {
LOG_INFO(Frontend, "SyncNetwork packet received!");
NetworkInfo info{};
std::memcpy(&info, packet.data.data(), sizeof(NetworkInfo));
-
- OnSyncNetwork(info);
+ if (info.network_id.session_id == network_info.network_id.session_id) {
+ OnSyncNetwork(info);
+ }
} else {
LOG_INFO(Frontend, "SyncNetwork packet received but in wrong State!");
}
diff --git a/src/core/hle/service/ldn/lan_discovery.h b/src/core/hle/service/ldn/lan_discovery.h
index f1f3c4bece..e641b11409 100644
--- a/src/core/hle/service/ldn/lan_discovery.h
+++ b/src/core/hle/service/ldn/lan_discovery.h
@@ -10,6 +10,8 @@
#pragma once
#include <array>
+#include <chrono>
+#include <condition_variable>
#include <cstring>
#include <functional>
#include <memory>
@@ -92,6 +94,8 @@ protected:
void InitNodeStateChange();
void ResetStations();
+ Result DestroyNetworkImpl();
+ Result DisconnectImpl();
void UpdateNodes();
void OnSyncNetwork(const NetworkInfo& info);
@@ -118,7 +122,9 @@ protected:
static constexpr Ssid fake_ssid{"YuzuFakeSsidForLdn"};
bool inited{};
- std::mutex packet_mutex;
+ mutable std::mutex packet_mutex;
+ std::condition_variable connection_event;
+ std::chrono::nanoseconds connection_timeout{std::chrono::seconds(5)};
std::array<LanStation, StationCountMax> stations;
std::array<NodeLatestUpdate, NodeCountMax> node_changes{};
std::array<u8, NodeCountMax> node_last_states{};
diff --git a/src/core/hle/service/nifm/nifm.cpp b/src/core/hle/service/nifm/nifm.cpp
index efada9b807..ea00e9f35c 100644
--- a/src/core/hle/service/nifm/nifm.cpp
+++ b/src/core/hle/service/nifm/nifm.cpp
@@ -70,6 +70,15 @@ static u128 MakeUuidFromName(std::string_view name) {
return {h1, h2};
}
+// A joined room is the emulated network, even in airplane mode. Its sockets only reach room
+// members, and LAN games (e.g. Mario Kart World LAN Play) need their connection request accepted.
+static bool IsNetworkAvailable() {
+ if (Network::GetRoomIPv4Address()) {
+ return true;
+ }
+ return Network::GetHostIPv4Address().has_value() && !Settings::values.airplane_mode.GetValue();
+}
+
// This is nn::nifm::RequestState
enum class RequestState : u32 {
NotSubmitted = 1,
@@ -110,6 +119,17 @@ struct IpAddressSetting {
};
static_assert(sizeof(IpAddressSetting) == 0xD, "IpAddressSetting has incorrect size.");
+// Room fake IPs are spread over 192.168.0.0/16, and some room servers give members different /24s.
+// A host /24 mask would then put room members on different subnets, so LAN games ignore each other.
+constexpr Network::IPv4Address RoomSubnetMask{255, 255, 0, 0};
+
+static void ApplyRoomAddress(IpAddressSetting& setting) {
+ if (const auto room_ip = Network::GetRoomIPv4Address()) {
+ setting.ip_address = *room_ip;
+ setting.subnet_mask = RoomSubnetMask;
+ }
+}
+
// This is nn::nifm::DnsSetting
struct DnsSetting {
bool is_automatic{};
@@ -430,8 +450,7 @@ private:
LOG_DEBUG(Service_NIFM, "(STUBBED) called");
const auto result = [this] {
- const auto has_connection = Network::GetHostIPv4Address().has_value() &&
- !Settings::values.airplane_mode.GetValue();
+ const auto has_connection = IsNetworkAvailable();
switch (state) {
case RequestState::NotSubmitted:
return has_connection ? ResultSuccess : ResultNetworkCommunicationDisabled;
@@ -572,10 +591,7 @@ void IGeneralService::GetCurrentNetworkProfile(HLERequestContext& ctx) {
std::strncpy(profile.network_name.data(), st.ssid, sizeof(profile.network_name) - 1);
if (auto room_member = Network::GetRoomMember().lock()) {
- if (room_member->IsConnected()) {
- profile.ip_setting_data.ip_address_setting.ip_address =
- room_member->GetFakeIpAddress();
- }
+ ApplyRoomAddress(profile.ip_setting_data.ip_address_setting);
profile.ip_setting_data.dns_setting.secondary_dns = {8, 8, 8, 8};
}
@@ -584,6 +600,19 @@ void IGeneralService::GetCurrentNetworkProfile(HLERequestContext& ctx) {
std::memcpy(profile.wireless_setting_data.ssid.data(), st.ssid,
profile.wireless_setting_data.ssid_length);
}
+ } else if (Network::GetRoomIPv4Address()) {
+ // The selected adapter is missing, but the room still works and IsNetworkAvailable
+ // reports it, so describe the room as a wired network
+ profile.ip_setting_data.ip_address_setting.is_automatic = true;
+ ApplyRoomAddress(profile.ip_setting_data.ip_address_setting);
+
+ profile.ip_setting_data.dns_setting.is_automatic = true;
+ profile.ip_setting_data.dns_setting.primary_dns = {1, 1, 1, 1};
+ profile.ip_setting_data.dns_setting.secondary_dns = {8, 8, 8, 8};
+
+ profile.uuid = MakeUuidFromName(st.ssid);
+ profile.profile_type = static_cast<u8>(NetworkProfileType::User);
+ profile.interface_type = static_cast<u8>(NetworkInterfaceType::Ethernet);
}
ctx.WriteBuffer(profile);
@@ -597,6 +626,8 @@ void IGeneralService::EnumerateNetworkInterfaces(HLERequestContext& ctx) {
using Network::HostAdapterKind;
const auto adapters = Network::GetAvailableNetworkInterfaces();
+ const auto selected = Network::GetSelectedNetworkInterface();
+ const auto room_ip = Network::GetRoomIPv4Address();
constexpr size_t kEntry = 0x3F0;
std::vector<u8> blob(adapters.size() * kEntry, 0);
@@ -612,6 +643,13 @@ void IGeneralService::EnumerateNetworkInterfaces(HLERequestContext& ctx) {
*reinterpret_cast<in_addr*>(base + 0x1C) = host.subnet_mask;
*reinterpret_cast<in_addr*>(base + 0x20) = host.gateway;
+ // The room traffic goes through the selected adapter, so report it like the other calls.
+ // Without a selected adapter, the first one stands for the room.
+ if (room_ip && (selected ? host.name == selected->name : i == 0)) {
+ std::memcpy(base + 0x18, room_ip->data(), room_ip->size());
+ std::memcpy(base + 0x1C, RoomSubnetMask.data(), RoomSubnetMask.size());
+ }
+
std::string name_utf8 = host.name;
name_utf8.resize(0x110, '\0');
std::memcpy(base + 0x2E0, name_utf8.data(), 0x110);
@@ -711,11 +749,7 @@ void IGeneralService::GetNetworkProfile(HLERequestContext& ctx) {
.wireless_setting_data{wifi}};
}();
- if (auto room_member = Network::GetRoomMember().lock()) {
- if (room_member->IsConnected()) {
- profile.ip_setting_data.ip_address_setting.ip_address = room_member->GetFakeIpAddress();
- }
- }
+ ApplyRoomAddress(profile.ip_setting_data.ip_address_setting);
ctx.WriteBuffer(profile);
@@ -852,10 +886,8 @@ void IGeneralService::GetCurrentIpAddress(HLERequestContext& ctx) {
ipv4.emplace(Network::IPv4Address{0, 0, 0, 0});
}
- if (auto room_member = Network::GetRoomMember().lock()) {
- if (room_member->IsConnected()) {
- ipv4 = room_member->GetFakeIpAddress();
- }
+ if (const auto room_ip = Network::GetRoomIPv4Address()) {
+ ipv4 = room_ip;
}
IPC::ResponseBuilder rb{ctx, 3};
@@ -902,9 +934,7 @@ void IGeneralService::GetCurrentIpConfigInfo(HLERequestContext& ctx) {
}
if (auto room_member = Network::GetRoomMember().lock()) {
- if (room_member->IsConnected()) {
- info.ip.ip_address = room_member->GetFakeIpAddress();
- }
+ ApplyRoomAddress(info.ip);
info.dns.is_automatic = true;
info.dns.primary_dns = {1, 1, 1, 1};
info.dns.secondary_dns = {8, 8, 8, 8};
@@ -984,7 +1014,7 @@ void IGeneralService::IsAnyInternetRequestAccepted(HLERequestContext& ctx) {
IPC::ResponseBuilder rb{ctx, 3};
rb.Push(ResultSuccess);
- if (Network::GetHostIPv4Address().has_value() && !Settings::values.airplane_mode.GetValue()) {
+ if (IsNetworkAvailable()) {
rb.Push<u8>(1);
} else {
rb.Push<u8>(0);
diff --git a/src/core/hle/service/sockets/bsd.cpp b/src/core/hle/service/sockets/bsd.cpp
index 02c6bb7762..730a7edcc1 100644
--- a/src/core/hle/service/sockets/bsd.cpp
+++ b/src/core/hle/service/sockets/bsd.cpp
@@ -4,6 +4,7 @@
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
+#include <algorithm>
#include <array>
#include <memory>
#include <utility>
@@ -13,6 +14,7 @@
#include "common/logging.h"
#include "common/socket_types.h"
+#include "common/zstd_compression.h"
#include "core/core.h"
#include "core/hle/kernel/k_thread.h"
#include "core/hle/service/ipc_helpers.h"
@@ -535,33 +537,36 @@ std::pair<s32, Errno> BSD_USA::SocketImpl(Domain domain, Type type, Protocol pro
UNIMPLEMENTED_IF_MSG(unk_flag, "Unknown flag in type");
type = static_cast<Type>(static_cast<u32>(type) & ~0x20000000);
- const s32 fd = FindFreeFileDescriptorHandle();
- if (fd < 0) {
- LOG_ERROR(Service, "No more file descriptors available");
- return {-1, Errno::MFILE};
+ // Checked before taking an fd. A joined room does not lift it: room TCP is only stubbed.
+ if (Settings::values.airplane_mode.GetValue() && IsConnectionBased(type)) {
+ LOG_ERROR(Service, "Airplane mode is enabled, cannot create socket");
+ return {-1, Errno::NOTCONN};
}
- file_descriptors[fd] = FileDescriptor{};
- FileDescriptor& descriptor = *file_descriptors[fd];
- // ENONMEM might be thrown here
-
- LOG_INFO(Service, "New socket fd={}", fd);
-
+ std::shared_ptr<Network::SocketBase> socket;
auto room_member = Network::GetRoomMember().lock();
if (room_member && room_member->IsConnected()) {
- descriptor.socket = std::make_shared<Network::ProxySocket>();
+ socket = std::make_shared<Network::ProxySocket>();
} else {
- descriptor.socket = std::make_shared<Network::Socket>();
+ socket = std::make_shared<Network::Socket>();
}
+ socket->Initialize(Translate(domain), Translate(type), Translate(protocol));
- descriptor.socket->Initialize(Translate(domain), Translate(type), Translate(protocol));
- descriptor.is_connection_based = IsConnectionBased(type);
-
- if (Settings::values.airplane_mode.GetValue() && descriptor.is_connection_based) {
- LOG_ERROR(Service, "Airplane mode is enabled, cannot create socket");
- return {-1, Errno::NOTCONN};
+ // Publish the socket only once it is set up, since the room thread delivers packets to it
+ std::scoped_lock lock{file_descriptors_mutex};
+ const s32 fd = FindFreeFileDescriptorHandle();
+ if (fd < 0) {
+ LOG_ERROR(Service, "No more file descriptors available");
+ return {-1, Errno::MFILE};
}
+ file_descriptors[fd] = FileDescriptor{
+ .socket = std::move(socket),
+ .is_connection_based = IsConnectionBased(type),
+ };
+
+ LOG_INFO(Service, "New socket fd={}", fd);
+
return {fd, Errno::SUCCESS};
}
@@ -595,31 +600,39 @@ std::pair<s32, Errno> BSD_USA::PollImpl(std::vector<u8>& write_buffer, std::span
return {-1, Errno::INVAL};
}
- for (PollFD& pollfd : fds) {
- ASSERT(False(pollfd.revents));
-
- if (pollfd.fd > static_cast<s32>(MAX_FD) || pollfd.fd < 0) {
- LOG_ERROR(Service, "File descriptor handle={} is invalid", pollfd.fd);
- pollfd.revents = PollEvents{};
- return {0, Errno::SUCCESS};
- }
-
- const std::optional<FileDescriptor>& descriptor = file_descriptors[pollfd.fd];
- if (!descriptor) {
- LOG_TRACE(Service, "File descriptor handle={} is not allocated", pollfd.fd);
- pollfd.revents = PollEvents::Nval;
- return {0, Errno::SUCCESS};
+ // The copies keep the sockets alive while Network::Poll waits, even if another bsd thread
+ // closes one of them meanwhile
+ std::vector<std::shared_ptr<Network::SocketBase>> sockets(fds.size());
+ {
+ std::scoped_lock lock{file_descriptors_mutex};
+ for (size_t i = 0; i < fds.size(); ++i) {
+ PollFD& pollfd = fds[i];
+ ASSERT(False(pollfd.revents));
+
+ if (pollfd.fd >= static_cast<s32>(MAX_FD) || pollfd.fd < 0) {
+ LOG_ERROR(Service, "File descriptor handle={} is invalid", pollfd.fd);
+ pollfd.revents = PollEvents{};
+ return {0, Errno::SUCCESS};
+ }
+
+ const std::optional<FileDescriptor>& descriptor = file_descriptors[pollfd.fd];
+ if (!descriptor) {
+ LOG_TRACE(Service, "File descriptor handle={} is not allocated", pollfd.fd);
+ pollfd.revents = PollEvents::Nval;
+ return {0, Errno::SUCCESS};
+ }
+ sockets[i] = descriptor->socket;
}
}
std::vector<Network::PollFD> host_pollfds(fds.size());
- std::transform(fds.begin(), fds.end(), host_pollfds.begin(), [](PollFD pollfd) {
- Network::PollFD result;
- result.socket = file_descriptors[pollfd.fd]->socket.get();
- result.events = Translate(pollfd.events);
- result.revents = Network::PollEvents{};
- return result;
- });
+ for (size_t i = 0; i < fds.size(); ++i) {
+ host_pollfds[i] = Network::PollFD{
+ .socket = sockets[i].get(),
+ .events = Translate(fds[i].events),
+ .revents = Network::PollEvents{},
+ };
+ }
const auto result = Network::Poll(host_pollfds, timeout);
@@ -633,26 +646,30 @@ std::pair<s32, Errno> BSD_USA::PollImpl(std::vector<u8>& write_buffer, std::span
}
std::pair<s32, Errno> BSD_USA::AcceptImpl(s32 fd, std::vector<u8>& write_buffer) {
- if (!IsFileDescriptorValid(fd)) {
+ const auto descriptor = CopyDescriptor(fd);
+ if (!descriptor) {
return {-1, Errno::BADF};
}
- const s32 new_fd = FindFreeFileDescriptorHandle();
- if (new_fd < 0) {
- LOG_ERROR(Service, "No more file descriptors available");
- return {-1, Errno::MFILE};
- }
-
- FileDescriptor& descriptor = *file_descriptors[fd];
- auto [result, bsd_errno] = descriptor.socket->Accept();
+ auto [result, bsd_errno] = descriptor->socket->Accept();
if (bsd_errno != Network::Errno::SUCCESS) {
return {-1, Translate(bsd_errno)};
}
- file_descriptors[new_fd] = FileDescriptor{};
- FileDescriptor& new_descriptor = *file_descriptors[new_fd];
- new_descriptor.socket = std::move(result.socket);
- new_descriptor.is_connection_based = descriptor.is_connection_based;
+ // Accept may block, so the fd is picked afterwards, in the same critical section as the write
+ s32 new_fd;
+ {
+ std::scoped_lock lock{file_descriptors_mutex};
+ new_fd = FindFreeFileDescriptorHandle();
+ if (new_fd < 0) {
+ LOG_ERROR(Service, "No more file descriptors available");
+ return {-1, Errno::MFILE};
+ }
+ file_descriptors[new_fd] = FileDescriptor{
+ .socket = std::move(result.socket),
+ .is_connection_based = descriptor->is_connection_based,
+ };
+ }
const SockAddrIn guest_addr_in = Translate(result.sockaddr_in);
PutValue(write_buffer, guest_addr_in);
@@ -661,34 +678,28 @@ std::pair<s32, Errno> BSD_USA::AcceptImpl(s32 fd, std::vector<u8>& write_buffer)
}
Errno BSD_USA::BindImpl(s32 fd, std::span<const u8> addr) {
- if (!IsFileDescriptorValid(fd)) {
+ const auto descriptor = CopyDescriptor(fd);
+ if (!descriptor) {
return Errno::BADF;
}
ASSERT(addr.size() >= 16);
- if (!file_descriptors[fd]->socket) {
- LOG_WARNING(Service, "Uninitialized socket");
- return Errno::BADF;
- }
auto addr_in = GetValue<SockAddrIn>(addr);
- return Translate(file_descriptors[fd]->socket->Bind(Translate(addr_in)));
+ return Translate(descriptor->socket->Bind(Translate(addr_in)));
}
Errno BSD_USA::ConnectImpl(s32 fd, std::span<const u8> addr) {
- if (!IsFileDescriptorValid(fd)) {
+ const auto descriptor = CopyDescriptor(fd);
+ if (!descriptor) {
return Errno::BADF;
}
ASSERT(addr.size() >= 16);
- if (!file_descriptors[fd]->socket) {
- LOG_WARNING(Service, "Uninitialized socket");
- return Errno::BADF;
- }
auto addr_in = GetValue<SockAddrIn>(addr);
- const Errno result = Translate(file_descriptors[fd]->socket->Connect(Translate(addr_in)));
+ const Errno result = Translate(descriptor->socket->Connect(Translate(addr_in)));
if (result == Errno::ISCONN) {
LOG_DEBUG(Service, "returned ISCONN - socket already connected");
@@ -699,16 +710,12 @@ Errno BSD_USA::ConnectImpl(s32 fd, std::span<const u8> addr) {
}
Errno BSD_USA::GetPeerNameImpl(s32 fd, std::vector<u8>& write_buffer) {
- if (!IsFileDescriptorValid(fd)) {
- return Errno::BADF;
- }
-
- if (!file_descriptors[fd]->socket) {
- LOG_WARNING(Service, "Uninitialized socket");
+ const auto descriptor = CopyDescriptor(fd);
+ if (!descriptor) {
return Errno::BADF;
}
- const auto [addr_in, bsd_errno] = file_descriptors[fd]->socket->GetPeerName();
+ const auto [addr_in, bsd_errno] = descriptor->socket->GetPeerName();
if (bsd_errno != Network::Errno::SUCCESS) {
return Translate(bsd_errno);
}
@@ -721,16 +728,12 @@ Errno BSD_USA::GetPeerNameImpl(s32 fd, std::vector<u8>& write_buffer) {
}
Errno BSD_USA::GetSockNameImpl(s32 fd, std::vector<u8>& write_buffer) {
- if (!IsFileDescriptorValid(fd)) {
+ const auto descriptor = CopyDescriptor(fd);
+ if (!descriptor) {
return Errno::BADF;
}
- if (!file_descriptors[fd]->socket) {
- LOG_WARNING(Service, "Uninitialized socket");
- return Errno::BADF;
- }
-
- const auto [addr_in, bsd_errno] = file_descriptors[fd]->socket->GetSockName();
+ const auto [addr_in, bsd_errno] = descriptor->socket->GetSockName();
if (bsd_errno != Network::Errno::SUCCESS) {
return Translate(bsd_errno);
}
@@ -743,38 +746,34 @@ Errno BSD_USA::GetSockNameImpl(s32 fd, std::vector<u8>& write_buffer) {
}
Errno BSD_USA::ListenImpl(s32 fd, s32 backlog) {
- if (!IsFileDescriptorValid(fd)) {
- return Errno::BADF;
- }
- if (!file_descriptors[fd]->socket) {
- LOG_WARNING(Service, "Uninitialized socket");
+ const auto descriptor = CopyDescriptor(fd);
+ if (!descriptor) {
return Errno::BADF;
}
- return Translate(file_descriptors[fd]->socket->Listen(backlog));
+ return Translate(descriptor->socket->Listen(backlog));
}
std::pair<s32, Errno> BSD_USA::FcntlImpl(s32 fd, FcntlCmd cmd, s32 arg) {
- if (!IsFileDescriptorValid(fd)) {
- return {-1, Errno::BADF};
- }
- if (!file_descriptors[fd]->socket) {
- LOG_WARNING(Service, "Uninitialized socket");
+ const auto descriptor = CopyDescriptor(fd);
+ if (!descriptor) {
return {-1, Errno::BADF};
}
- FileDescriptor& descriptor = *file_descriptors[fd];
-
switch (cmd) {
case FcntlCmd::GETFL:
ASSERT(arg == 0);
- return {descriptor.flags, Errno::SUCCESS};
+ return {descriptor->flags, Errno::SUCCESS};
case FcntlCmd::SETFL: {
const bool enable = (arg & Network::FLAG_O_NONBLOCK) != 0;
- const Errno bsd_errno = Translate(descriptor.socket->SetNonBlock(enable));
+ // Set the mode and the flags together, so SetDontWait never sees one without the other
+ std::scoped_lock lock{file_descriptors_mutex};
+ const Errno bsd_errno = Translate(descriptor->socket->SetNonBlock(enable));
if (bsd_errno != Errno::SUCCESS) {
return {-1, bsd_errno};
}
- descriptor.flags = arg;
+ if (auto& entry = file_descriptors[fd]; entry && entry->socket == descriptor->socket) {
+ entry->flags = arg;
+ }
return {0, Errno::SUCCESS};
}
default:
@@ -784,11 +783,8 @@ std::pair<s32, Errno> BSD_USA::FcntlImpl(s32 fd, FcntlCmd cmd, s32 arg) {
}
Errno BSD_USA::GetSockOptImpl(s32 fd, u32 level, OptName optname, std::vector<u8>& optval) {
- if (!IsFileDescriptorValid(fd)) {
- return Errno::BADF;
- }
- if (!file_descriptors[fd]->socket) {
- LOG_WARNING(Service, "Uninitialized socket");
+ const auto descriptor = CopyDescriptor(fd);
+ if (!descriptor) {
return Errno::BADF;
}
@@ -797,7 +793,7 @@ Errno BSD_USA::GetSockOptImpl(s32 fd, u32 level, OptName optname, std::vector<u8
return Errno::SUCCESS;
}
- Network::SocketBase* const socket = file_descriptors[fd]->socket.get();
+ Network::SocketBase* const socket = descriptor->socket.get();
switch (optname) {
case OptName::ERROR_: {
@@ -819,11 +815,8 @@ Errno BSD_USA::GetSockOptImpl(s32 fd, u32 level, OptName optname, std::vector<u8
}
Errno BSD_USA::SetSockOptImpl(s32 fd, u32 level, OptName optname, std::span<const u8> optval) {
- if (!IsFileDescriptorValid(fd)) {
- return Errno::BADF;
- }
- if (!file_descriptors[fd]->socket) {
- LOG_WARNING(Service, "Uninitialized socket");
+ const auto descriptor = CopyDescriptor(fd);
+ if (!descriptor) {
return Errno::BADF;
}
@@ -832,7 +825,7 @@ Errno BSD_USA::SetSockOptImpl(s32 fd, u32 level, OptName optname, std::span<cons
return Errno::SUCCESS;
}
- Network::SocketBase* const socket = file_descriptors[fd]->socket.get();
+ Network::SocketBase* const socket = descriptor->socket.get();
if (optname == OptName::LINGER) {
ASSERT(optval.size() == sizeof(Linger));
@@ -873,39 +866,30 @@ Errno BSD_USA::SetSockOptImpl(s32 fd, u32 level, OptName optname, std::span<cons
}
Errno BSD_USA::ShutdownImpl(s32 fd, s32 how) {
- if (!IsFileDescriptorValid(fd)) {
- return Errno::BADF;
- }
- if (!file_descriptors[fd]->socket) {
- LOG_WARNING(Service, "Uninitialized socket");
+ const auto descriptor = CopyDescriptor(fd);
+ if (!descriptor) {
return Errno::BADF;
}
const Network::ShutdownHow host_how = Translate(static_cast<ShutdownHow>(how));
- return Translate(file_descriptors[fd]->socket->Shutdown(host_how));
+ return Translate(descriptor->socket->Shutdown(host_how));
}
std::pair<s32, Errno> BSD_USA::RecvImpl(s32 fd, u32 flags, std::vector<u8>& message) {
- if (!IsFileDescriptorValid(fd)) {
+ const auto descriptor = CopyDescriptor(fd);
+ if (!descriptor) {
return {-1, Errno::BADF};
}
- FileDescriptor& descriptor = *file_descriptors[fd];
-
// Apply flags
using Network::FLAG_MSG_DONTWAIT;
- using Network::FLAG_O_NONBLOCK;
- if ((flags & FLAG_MSG_DONTWAIT) != 0) {
- flags &= ~FLAG_MSG_DONTWAIT;
- if ((descriptor.flags & FLAG_O_NONBLOCK) == 0) {
- descriptor.socket->SetNonBlock(true);
- }
- }
+ const bool dont_wait = (flags & FLAG_MSG_DONTWAIT) != 0 && SetDontWait(fd, *descriptor, true);
+ flags &= ~FLAG_MSG_DONTWAIT;
- const auto [ret, bsd_errno] = Translate(descriptor.socket->Recv(flags, message));
+ const auto [ret, bsd_errno] = Translate(descriptor->socket->Recv(flags, message));
// Restore original state
- if ((descriptor.flags & FLAG_O_NONBLOCK) == 0) {
- descriptor.socket->SetNonBlock(false);
+ if (dont_wait) {
+ SetDontWait(fd, *descriptor, false);
}
return {ret, bsd_errno};
@@ -913,15 +897,14 @@ std::pair<s32, Errno> BSD_USA::RecvImpl(s32 fd, u32 flags, std::vector<u8>& mess
std::pair<s32, Errno> BSD_USA::RecvFromImpl(s32 fd, u32 flags, std::vector<u8>& message,
std::vector<u8>& addr) {
- if (!IsFileDescriptorValid(fd)) {
+ const auto descriptor = CopyDescriptor(fd);
+ if (!descriptor) {
return {-1, Errno::BADF};
}
- FileDescriptor& descriptor = *file_descriptors[fd];
-
Network::SockAddrIn addr_in{};
Network::SockAddrIn* p_addr_in = nullptr;
- if (descriptor.is_connection_based) {
+ if (descriptor->is_connection_based) {
// Connection based file descriptors (e.g. TCP) zero addr
addr.clear();
} else {
@@ -930,19 +913,15 @@ std::pair<s32, Errno> BSD_USA::RecvFromImpl(s32 fd, u32 flags, std::vector<u8>&
// Apply flags
using Network::FLAG_MSG_DONTWAIT;
- using Network::FLAG_O_NONBLOCK;
- if ((flags & FLAG_MSG_DONTWAIT) != 0) {
- flags &= ~FLAG_MSG_DONTWAIT;
- if ((descriptor.flags & FLAG_O_NONBLOCK) == 0) {
- descriptor.socket->SetNonBlock(true);
- }
- }
+ const bool dont_wait = (flags & FLAG_MSG_DONTWAIT) != 0 && SetDontWait(fd, *descriptor, true);
+ flags &= ~FLAG_MSG_DONTWAIT;
- const auto [ret, bsd_errno] = Translate(descriptor.socket->RecvFrom(flags, message, p_addr_in));
+ const auto [ret, bsd_errno] =
+ Translate(descriptor->socket->RecvFrom(flags, message, p_addr_in));
// Restore original state
- if ((descriptor.flags & FLAG_O_NONBLOCK) == 0) {
- descriptor.socket->SetNonBlock(false);
+ if (dont_wait) {
+ SetDontWait(fd, *descriptor, false);
}
if (p_addr_in) {
@@ -959,23 +938,17 @@ std::pair<s32, Errno> BSD_USA::RecvFromImpl(s32 fd, u32 flags, std::vector<u8>&
}
std::pair<s32, Errno> BSD_USA::SendImpl(s32 fd, u32 flags, std::span<const u8> message) {
- if (!IsFileDescriptorValid(fd)) {
+ const auto descriptor = CopyDescriptor(fd);
+ if (!descriptor) {
return {-1, Errno::BADF};
}
- if (!file_descriptors[fd]->socket) {
- LOG_WARNING(Service, "Uninitialized socket");
- return {-1, Errno::BADF};
- }
- return Translate(file_descriptors[fd]->socket->Send(message, flags));
+ return Translate(descriptor->socket->Send(message, flags));
}
std::pair<s32, Errno> BSD_USA::SendToImpl(s32 fd, u32 flags, std::span<const u8> message,
std::span<const u8> addr) {
- if (!IsFileDescriptorValid(fd)) {
- return {-1, Errno::BADF};
- }
- if (!file_descriptors[fd]->socket) {
- LOG_WARNING(Service, "Uninitialized socket");
+ const auto descriptor = CopyDescriptor(fd);
+ if (!descriptor) {
return {-1, Errno::BADF};
}
@@ -988,31 +961,33 @@ std::pair<s32, Errno> BSD_USA::SendToImpl(s32 fd, u32 flags, std::span<const u8>
p_addr_in = &addr_in;
}
- return Translate(file_descriptors[fd]->socket->SendTo(flags, message, p_addr_in));
+ return Translate(descriptor->socket->SendTo(flags, message, p_addr_in));
}
Errno BSD_USA::CloseImpl(s32 fd) {
- if (!IsFileDescriptorValid(fd)) {
- return Errno::BADF;
- }
- if (!file_descriptors[fd]->socket) {
- LOG_WARNING(Service, "Uninitialized socket");
- return Errno::BADF;
- }
-
- const Errno bsd_errno = Translate(file_descriptors[fd]->socket->Close());
- if (bsd_errno != Errno::SUCCESS) {
- return bsd_errno;
+ std::shared_ptr<Network::SocketBase> socket;
+ {
+ // Free the slot first, so of two threads closing the same fd only one closes the socket.
+ // Like POSIX close(), the fd is released even if closing the socket fails.
+ std::scoped_lock lock{file_descriptors_mutex};
+ if (!IsFileDescriptorValid(fd)) {
+ return Errno::BADF;
+ }
+ if (!file_descriptors[fd]->socket) {
+ LOG_WARNING(Service, "Uninitialized socket");
+ return Errno::BADF;
+ }
+ socket = std::move(file_descriptors[fd]->socket);
+ file_descriptors[fd].reset();
}
LOG_INFO(Service, "Close socket fd={}", fd);
-
- file_descriptors[fd].reset();
- return bsd_errno;
+ return Translate(socket->Close());
}
std::variant<s32, Errno> BSD_USA::DuplicateSocketImpl(s32 fd) {
- if (!IsFileDescriptorValid(fd)) {
+ std::scoped_lock lock{file_descriptors_mutex};
+ if (!IsFileDescriptorValid(fd) || !file_descriptors[fd]->socket) {
return Errno::BADF;
}
@@ -1031,6 +1006,16 @@ std::variant<s32, Errno> BSD_USA::DuplicateSocketImpl(s32 fd) {
}
std::optional<std::shared_ptr<Network::SocketBase>> BSD_USA::GetSocket(s32 fd) {
+ if (auto descriptor = CopyDescriptor(fd)) {
+ return std::move(descriptor->socket);
+ }
+ return std::nullopt;
+}
+
+std::optional<BSD_USA::FileDescriptor> BSD_USA::CopyDescriptor(s32 fd) {
+ // The copy holds a reference to the socket, so a close on another bsd thread cannot destroy
+ // it while the caller uses it
+ std::scoped_lock lock{file_descriptors_mutex};
if (!IsFileDescriptorValid(fd)) {
return std::nullopt;
}
@@ -1038,7 +1023,7 @@ std::optional<std::shared_ptr<Network::SocketBase>> BSD_USA::GetSocket(s32 fd) {
LOG_WARNING(Service, "Uninitialized socket");
return std::nullopt;
}
- return file_descriptors[fd]->socket;
+ return file_descriptors[fd];
}
s32 BSD_USA::FindFreeFileDescriptorHandle() noexcept {
@@ -1050,8 +1035,21 @@ s32 BSD_USA::FindFreeFileDescriptorHandle() noexcept {
return -1;
}
+bool BSD_USA::SetDontWait(s32 fd, const FileDescriptor& descriptor, bool enable) {
+ // Read the current flags, not the copy: fcntl may have changed them during the receive
+ std::scoped_lock lock{file_descriptors_mutex};
+ const auto& entry = file_descriptors[fd];
+ const s32 current_flags =
+ entry && entry->socket == descriptor.socket ? entry->flags : descriptor.flags;
+ if ((current_flags & Network::FLAG_O_NONBLOCK) != 0) {
+ return false;
+ }
+ descriptor.socket->SetNonBlock(enable);
+ return true;
+}
+
bool BSD_USA::IsFileDescriptorValid(s32 fd) const noexcept {
- if (fd > static_cast<s32>(MAX_FD) || fd < 0) {
+ if (fd >= static_cast<s32>(MAX_FD) || fd < 0) {
LOG_ERROR(Service, "Invalid file descriptor handle={}", fd);
return false;
}
@@ -1071,12 +1069,45 @@ void BSD_USA::BuildErrnoResponse(HLERequestContext& ctx, Errno bsd_errno) const
}
void BSD_USA::OnProxyPacketReceived(const Network::ProxyPacket& packet) {
- for (auto& optional_descriptor : file_descriptors) {
- if (!optional_descriptor.has_value()) {
- continue;
+ // Runs on the room thread. The copies keep each socket alive if a bsd thread closes it, so
+ // the bsd threads do not wait for the decompression below. Only sockets that accept the
+ // packet are collected, so an unwanted packet is never decompressed.
+ std::array<std::shared_ptr<Network::SocketBase>, MAX_FD> sockets{};
+ size_t num_sockets = 0;
+ {
+ std::scoped_lock lock{file_descriptors_mutex};
+ for (const auto& descriptor : file_descriptors) {
+ if (!descriptor.has_value() || !descriptor->socket) {
+ continue;
+ }
+ // A duplicated fd shares its socket, which must still get each packet only once
+ const auto sockets_end = sockets.begin() + num_sockets;
+ if (std::find(sockets.begin(), sockets_end, descriptor->socket) == sockets_end &&
+ descriptor->socket->AcceptsProxyPacket(packet)) {
+ sockets[num_sockets++] = descriptor->socket;
+ }
}
- FileDescriptor& descriptor = *optional_descriptor;
- descriptor.socket.get()->HandleProxyPacket(packet);
+ }
+ if (num_sockets == 0) {
+ return;
+ }
+
+ // Decompress once for all sockets that share the port. The room does not check the frame, so
+ // drop a malformed or oversized one instead of throwing on the room thread.
+ auto data = Common::Compression::TryDecompressDataZSTD(packet.data, MAX_ROOM_PACKET_SIZE);
+ if (!data) {
+ LOG_WARNING(Service, "Dropped a room packet with an invalid or oversized payload");
+ return;
+ }
+ const Network::ProxyPacket decompressed{
+ .local_endpoint = packet.local_endpoint,
+ .remote_endpoint = packet.remote_endpoint,
+ .protocol = packet.protocol,
+ .broadcast = packet.broadcast,
+ .data = std::move(*data),
+ };
+ for (size_t i = 0; i < num_sockets; ++i) {
+ sockets[i]->HandleProxyPacket(decompressed);
}
}
@@ -1140,18 +1171,27 @@ BSD_USA::BSD_USA(Core::System& system_, const char* name, bool is_user_)
RegisterHandlers(functions);
+ std::scoped_lock lock{proxy_binding_mutex};
+ if (proxy_binding_count++ != 0) {
+ return;
+ }
if (auto room_member = Network::GetRoomMember().lock()) {
- proxy_packet_received = room_member->BindOnProxyPacketReceived(
- [this](const Network::ProxyPacket& packet) { OnProxyPacketReceived(packet); });
+ proxy_packet_received =
+ room_member->BindOnProxyPacketReceived(&BSD_USA::OnProxyPacketReceived);
} else {
LOG_ERROR(Service, "Network isn't initialized");
}
}
BSD_USA::~BSD_USA() {
+ std::scoped_lock lock{proxy_binding_mutex};
+ if (--proxy_binding_count != 0) {
+ return;
+ }
if (auto room_member = Network::GetRoomMember().lock()) {
room_member->Unbind(proxy_packet_received);
}
+ proxy_packet_received.reset();
}
std::unique_lock<std::mutex> BSD_USA::LockService() noexcept {
diff --git a/src/core/hle/service/sockets/bsd.h b/src/core/hle/service/sockets/bsd.h
index 5cc3eb5b81..bff80d7c00 100644
--- a/src/core/hle/service/sockets/bsd.h
+++ b/src/core/hle/service/sockets/bsd.h
@@ -7,6 +7,7 @@
#pragma once
#include <memory>
+#include <mutex>
#include <span>
#include <vector>
#include <variant>
@@ -43,6 +44,8 @@ public:
private:
/// Maximum number of file descriptors
static constexpr size_t MAX_FD = 128;
+ /// Largest decompressed room packet. A UDP datagram cannot be larger.
+ static constexpr size_t MAX_ROOM_PACKET_SIZE = 0x10000;
struct FileDescriptor {
std::shared_ptr<Network::SocketBase> socket;
@@ -177,16 +180,25 @@ private:
s32 FindFreeFileDescriptorHandle() noexcept;
bool IsFileDescriptorValid(s32 fd) const noexcept;
+ /// Copies a descriptor under file_descriptors_mutex; nullopt if fd has no socket
+ std::optional<FileDescriptor> CopyDescriptor(s32 fd);
+ /// Switches a blocking socket for one MSG_DONTWAIT call; false if fd is already nonblocking
+ bool SetDontWait(s32 fd, const FileDescriptor& descriptor, bool enable);
void BuildErrnoResponse(HLERequestContext& ctx, Errno bsd_errno) const noexcept;
static inline std::array<std::optional<FileDescriptor>, MAX_FD> file_descriptors{};
+ /// Guards file_descriptors. Calls use CopyDescriptor, so sockets outlive a concurrent close.
+ static inline std::mutex file_descriptors_mutex;
/// Callback to parse and handle a received wifi packet.
- void OnProxyPacketReceived(const Network::ProxyPacket& packet);
+ static void OnProxyPacketReceived(const Network::ProxyPacket& packet);
- // Callback identifier for the OnProxyPacketReceived event.
- Network::RoomMember::CallbackHandle<Network::ProxyPacket> proxy_packet_received;
+ // Callback identifier for the OnProxyPacketReceived event. bsd:s, bsd:u and bsd:a share
+ // file_descriptors, so they bind once; otherwise each packet is queued once per service.
+ static inline Network::RoomMember::CallbackHandle<Network::ProxyPacket> proxy_packet_received;
+ static inline std::mutex proxy_binding_mutex;
+ static inline size_t proxy_binding_count = 0;
protected:
std::unique_lock<std::mutex> LockService() noexcept override;
diff --git a/src/core/internal_network/network.cpp b/src/core/internal_network/network.cpp
index 01d37fb35b..806562076d 100644
--- a/src/core/internal_network/network.cpp
+++ b/src/core/internal_network/network.cpp
@@ -722,6 +722,17 @@ std::optional<IPv4Address> GetHostIPv4Address() {
return TranslateIPv4(network_interface->ip_address);
}
+std::optional<IPv4Address> GetRoomIPv4Address() {
+ if (const auto room_member = GetRoomMember().lock()) {
+ // The room host and moderators are in the Moderator state instead of Joined
+ const auto state = room_member->GetState();
+ if (state == RoomMember::State::Joined || state == RoomMember::State::Moderator) {
+ return room_member->GetFakeIpAddress();
+ }
+ }
+ return std::nullopt;
+}
+
std::string IPv4AddressToString(IPv4Address ip_addr) {
std::array<char, INET_ADDRSTRLEN> buf = {};
ASSERT(inet_ntop(AF_INET, &ip_addr, buf.data(), sizeof(buf)) == buf.data());
@@ -1084,6 +1095,10 @@ bool Socket::IsOpened() const {
return fd != INVALID_SOCKET;
}
+bool Socket::AcceptsProxyPacket(const ProxyPacket& packet) {
+ return false;
+}
+
void Socket::HandleProxyPacket(const ProxyPacket& packet) {
LOG_WARNING(Network, "ProxyPacket received, but not in Proxy mode!");
}
diff --git a/src/core/internal_network/network.h b/src/core/internal_network/network.h
index 2c9ae523c5..a598f75348 100644
--- a/src/core/internal_network/network.h
+++ b/src/core/internal_network/network.h
@@ -121,6 +121,10 @@ constexpr IPv4Address TranslateIPv4(in_addr addr) {
/// @return human ordered IPv4 address (e.g. 192.168.0.1) as an array
std::optional<IPv4Address> GetHostIPv4Address();
+/// @brief Returns the fake IPv4 address of the joined room, the emulated network while in a room
+/// @return nullopt outside a room and while still joining it
+std::optional<IPv4Address> GetRoomIPv4Address();
+
std::string IPv4AddressToString(IPv4Address ip_addr);
u32 IPv4AddressToInteger(IPv4Address ip_addr);
diff --git a/src/core/internal_network/socket_proxy.cpp b/src/core/internal_network/socket_proxy.cpp
index 744765bfcc..f12891e8f5 100644
--- a/src/core/internal_network/socket_proxy.cpp
+++ b/src/core/internal_network/socket_proxy.cpp
@@ -3,8 +3,9 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
+#include <algorithm>
#include <chrono>
-#include <thread>
+#include <cstring>
#include "common/assert.h"
#include "common/logging.h"
@@ -29,22 +30,35 @@ ProxySocket::~ProxySocket() {
fd = INVALID_SOCKET;
}
-void ProxySocket::HandleProxyPacket(const ProxyPacket& packet) {
+bool ProxySocket::MatchesPacket(const ProxyPacket& packet) const {
if (protocol != packet.protocol || local_endpoint.portno != packet.remote_endpoint.portno ||
closed) {
- return;
+ return false;
}
-
if (!broadcast && packet.broadcast) {
LOG_INFO(Network, "Received broadcast packet, but not configured for broadcast mode");
- return;
+ return false;
}
+ return true;
+}
- auto decompressed = packet;
- decompressed.data = Common::Compression::DecompressDataZSTD(packet.data);
-
+bool ProxySocket::AcceptsProxyPacket(const ProxyPacket& packet) {
std::lock_guard guard(packets_mutex);
- received_packets.push(decompressed);
+ return MatchesPacket(packet);
+}
+
+void ProxySocket::HandleProxyPacket(const ProxyPacket& packet) {
+ {
+ // Runs on the room thread, while the game thread may bind, close or reconfigure the socket.
+ // Check again: the socket may have changed since AcceptsProxyPacket.
+ std::lock_guard guard(packets_mutex);
+ if (!MatchesPacket(packet)) {
+ return;
+ }
+ received_packets.push(packet);
+ }
+ // A MSG_PEEK reader leaves the packet queued, so every waiter must see it
+ packets_cv.notify_all();
}
template <typename T>
@@ -54,7 +68,11 @@ Errno ProxySocket::SetSockOpt(SOCKET fd_, int option, T value) {
}
Errno ProxySocket::Initialize(Domain domain, Type type, Protocol socket_protocol) {
- protocol = socket_protocol;
+ {
+ std::lock_guard guard(packets_mutex);
+ socket_type = type;
+ protocol = socket_protocol;
+ }
SetSockOpt(fd, SO_TYPE, type);
return Errno::SUCCESS;
@@ -81,6 +99,7 @@ std::pair<SockAddrIn, Errno> ProxySocket::GetSockName() {
}
Errno ProxySocket::Bind(SockAddrIn addr) {
+ std::lock_guard guard(packets_mutex);
if (is_bound) {
LOG_WARNING(Network, "Rebinding Socket is unimplemented!");
return Errno::SUCCESS;
@@ -102,82 +121,73 @@ Errno ProxySocket::Shutdown(ShutdownHow how) {
}
std::pair<s32, Errno> ProxySocket::Recv(int flags, std::span<u8> message) {
- LOG_WARNING(Network, "(STUBBED) called");
- ASSERT(flags == 0);
- ASSERT(message.size() < static_cast<size_t>((std::numeric_limits<int>::max)()));
-
- return {static_cast<s32>(0), Errno::SUCCESS};
+ if (socket_type == Type::STREAM) {
+ // Room TCP sockets have no peer (Connect and Send are stubs). Report an empty queue as the
+ // end of the stream, as before, instead of blocking the bsd service until the timeout.
+ std::lock_guard guard(packets_mutex);
+ if (received_packets.empty()) {
+ return {0, Errno::SUCCESS};
+ }
+ }
+ return RecvFrom(flags, message, nullptr);
}
std::pair<s32, Errno> ProxySocket::RecvFrom(int flags, std::span<u8> message, SockAddrIn* addr) {
- ASSERT(flags == 0);
+ ASSERT((flags & ~FLAG_MSG_PEEK) == 0);
ASSERT(message.size() < static_cast<size_t>((std::numeric_limits<int>::max)()));
- // TODO (flTobi): Verify the timeout behavior and break when connection is lost
- const auto timestamp = std::chrono::steady_clock::now();
- // When receive_timeout is set to zero, the socket is supposed to wait indefinitely until a
- // packet arrives. In order to prevent lost packets from hanging the emulation thread, we set
- // the timeout to 5s instead
- const auto timeout = receive_timeout == 0 ? 5000 : receive_timeout;
- while (true) {
- {
- std::lock_guard guard(packets_mutex);
- if (received_packets.size() > 0) {
- return ReceivePacket(flags, message, addr, message.size());
- }
- }
-
+ std::unique_lock lock(packets_mutex);
+ if (closed) {
+ return {-1, Errno::BADF};
+ }
+ if (received_packets.empty()) {
if (!blocking) {
return {-1, Errno::AGAIN};
}
-
- std::this_thread::yield();
-
- const auto time_diff = std::chrono::steady_clock::now() - timestamp;
- const auto time_diff_ms =
- std::chrono::duration_cast<std::chrono::milliseconds>(time_diff).count();
-
- if (time_diff_ms > timeout) {
- return {-1, Errno::TIMEDOUT};
+ // TODO (flTobi): Verify the timeout behavior and break when connection is lost
+ // When receive_timeout is set to zero, the socket is supposed to wait indefinitely until a
+ // packet arrives. In order to prevent lost packets from hanging the emulation thread, we set
+ // the timeout to 5s instead. An expired wait is reported like an SO_RCVTIMEO timeout.
+ const u32 timeout = receive_timeout == 0 ? 5000 : receive_timeout;
+ if (!packets_cv.wait_for(lock, std::chrono::milliseconds{timeout},
+ [this] { return closed || !received_packets.empty(); })) {
+ return {-1, Errno::AGAIN};
+ }
+ if (closed) {
+ return {-1, Errno::BADF};
}
}
+
+ return ReceivePacket(flags, message, addr, message.size());
}
std::pair<s32, Errno> ProxySocket::ReceivePacket(int flags, std::span<u8> message, SockAddrIn* addr,
std::size_t max_length) {
- ProxyPacket& packet = received_packets.front();
+ const ProxyPacket& packet = received_packets.front();
if (addr) {
addr->family = Domain::INET;
addr->ip = packet.local_endpoint.ip; // The senders ip address
addr->portno = packet.local_endpoint.portno; // The senders port number
}
- bool peek = (flags & FLAG_MSG_PEEK) != 0;
- std::size_t read_bytes;
- if (packet.data.size() > max_length) {
- read_bytes = max_length;
- memcpy(message.data(), packet.data.data(), max_length);
-
- if (protocol == Protocol::UDP) {
- if (!peek) {
- received_packets.pop();
- }
- return {-1, Errno::MSGSIZE};
- } else if (protocol == Protocol::TCP) {
- std::vector<u8> numArray(packet.data.size() - max_length);
- std::copy(packet.data.begin() + max_length, packet.data.end(),
- std::back_inserter(numArray));
- packet.data = numArray;
- }
+ const std::span<const u8> unread = std::span{packet.data}.subspan(front_read_offset);
+ const std::size_t read_bytes = (std::min)(unread.size(), max_length);
+ std::memcpy(message.data(), unread.data(), read_bytes);
+
+ if ((flags & FLAG_MSG_PEEK) != 0) {
+ return {static_cast<s32>(read_bytes), Errno::SUCCESS};
+ }
+
+ if (socket_type == Type::STREAM && read_bytes < unread.size()) {
+ // Keep the unread part of the stream for the next call
+ front_read_offset += read_bytes;
} else {
- read_bytes = packet.data.size();
- memcpy(message.data(), packet.data.data(), read_bytes);
- if (!peek) {
- received_packets.pop();
- }
+ // Like BSD recv(), the rest of a datagram that does not fit is discarded
+ received_packets.pop();
+ front_read_offset = 0;
}
- return {static_cast<u32>(read_bytes), Errno::SUCCESS};
+ return {static_cast<s32>(read_bytes), Errno::SUCCESS};
}
std::pair<s32, Errno> ProxySocket::Send(std::span<const u8> message, int flags) {
@@ -238,8 +248,14 @@ std::pair<s32, Errno> ProxySocket::SendTo(u32 flags, std::span<const u8> message
}
Errno ProxySocket::Close() {
- fd = INVALID_SOCKET;
- closed = true;
+ {
+ std::lock_guard guard(packets_mutex);
+ fd = INVALID_SOCKET;
+ closed = true;
+ received_packets = {};
+ front_read_offset = 0;
+ }
+ packets_cv.notify_all();
return Errno::SUCCESS;
}
@@ -260,7 +276,10 @@ Errno ProxySocket::SetReuseAddr(bool enable) {
}
Errno ProxySocket::SetBroadcast(bool enable) {
- broadcast = enable;
+ {
+ std::lock_guard guard(packets_mutex);
+ broadcast = enable;
+ }
return SetSockOpt<u32>(fd, SO_BROADCAST, enable ? 1 : 0);
}
@@ -282,11 +301,15 @@ Errno ProxySocket::SetSndTimeo(u32 value) {
}
Errno ProxySocket::SetRcvTimeo(u32 value) {
- receive_timeout = value;
+ {
+ std::lock_guard guard(packets_mutex);
+ receive_timeout = value;
+ }
return SetSockOpt(fd, SO_RCVTIMEO, static_cast<int>(value));
}
Errno ProxySocket::SetNonBlock(bool enable) {
+ std::lock_guard guard(packets_mutex);
blocking = !enable;
return Errno::SUCCESS;
}
diff --git a/src/core/internal_network/socket_proxy.h b/src/core/internal_network/socket_proxy.h
index caf5d1ee23..078baa7806 100644
--- a/src/core/internal_network/socket_proxy.h
+++ b/src/core/internal_network/socket_proxy.h
@@ -1,10 +1,11 @@
-// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
+#include <condition_variable>
#include <mutex>
#include <span>
#include <vector>
@@ -21,6 +22,8 @@ public:
explicit ProxySocket() noexcept;
~ProxySocket() override;
+ bool AcceptsProxyPacket(const ProxyPacket& packet) override;
+
void HandleProxyPacket(const ProxyPacket& packet) override;
Errno Initialize(Domain domain, Type type, Protocol socket_protocol) override;
@@ -81,6 +84,9 @@ public:
bool IsOpened() const override;
private:
+ /// Expects packets_mutex to be held
+ bool MatchesPacket(const ProxyPacket& packet) const;
+
bool broadcast = false;
bool closed = false;
u32 send_timeout = 0;
@@ -89,9 +95,12 @@ private:
SockAddrIn local_endpoint{};
bool blocking = true;
std::queue<ProxyPacket> received_packets;
- Protocol protocol;
+ std::size_t front_read_offset = 0; ///< Bytes of the front TCP packet that were already read
+ Protocol protocol{};
+ Type socket_type{}; ///< Stream checks use the type, since protocol 0 means the default protocol
std::mutex packets_mutex;
+ std::condition_variable packets_cv;
};
} // namespace Network
diff --git a/src/core/internal_network/sockets.h b/src/core/internal_network/sockets.h
index 0b751b6622..29b5b0f18a 100644
--- a/src/core/internal_network/sockets.h
+++ b/src/core/internal_network/sockets.h
@@ -89,6 +89,10 @@ public:
virtual bool IsOpened() const = 0;
+ /// Whether HandleProxyPacket would queue the packet; checked before decompressing it
+ virtual bool AcceptsProxyPacket(const ProxyPacket& packet) = 0;
+
+ /// Queues a room packet whose data is already decompressed
virtual void HandleProxyPacket(const ProxyPacket& packet) = 0;
[[nodiscard]] SOCKET GetFD() const {
@@ -163,6 +167,8 @@ public:
bool IsOpened() const override;
+ bool AcceptsProxyPacket(const ProxyPacket& packet) override;
+
void HandleProxyPacket(const ProxyPacket& packet) override;
private:
diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt
index 1eb1040e34..6bf14f4cbd 100644
--- a/src/tests/CMakeLists.txt
+++ b/src/tests/CMakeLists.txt
@@ -18,7 +18,10 @@ add_executable(tests
common/slot_vector.cpp
common/undefined_fix.cpp
common/unique_function.cpp
+ common/zstd_compression.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/common/zstd_compression.cpp b/src/tests/common/zstd_compression.cpp
new file mode 100644
index 0000000000..6996f2e1ac
--- /dev/null
+++ b/src/tests/common/zstd_compression.cpp
@@ -0,0 +1,59 @@
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include <array>
+#include <vector>
+
+#include <catch2/catch_test_macros.hpp>
+
+#include "common/common_types.h"
+#include "common/zstd_compression.h"
+
+namespace {
+
+std::vector<u8> Compress(const std::vector<u8>& data) {
+ return Common::Compression::CompressDataZSTDDefault(data.data(), data.size());
+}
+
+} // namespace
+
+TEST_CASE("ZSTD frames within the size limit decompress", "[common][zstd]") {
+ const std::vector<u8> data(100, 0x5a);
+ const auto compressed = Compress(data);
+
+ const auto result = Common::Compression::TryDecompressDataZSTD(compressed, data.size());
+ REQUIRE(result.has_value());
+ REQUIRE(*result == data);
+ REQUIRE(Common::Compression::DecompressDataZSTD(compressed) == data);
+
+ const auto empty = Common::Compression::TryDecompressDataZSTD(Compress({}), 0);
+ REQUIRE(empty.has_value());
+ REQUIRE(empty->empty());
+}
+
+TEST_CASE("ZSTD frames over the size limit are rejected", "[common][zstd]") {
+ const std::vector<u8> data(100, 0x5a);
+ const auto compressed = Compress(data);
+ REQUIRE_FALSE(Common::Compression::TryDecompressDataZSTD(compressed, data.size() - 1));
+}
+
+TEST_CASE("Invalid ZSTD frames are rejected without throwing", "[common][zstd]") {
+ const std::array<u8, 1> one_byte{0x01};
+ const std::array<u8, 5> bad_magic{0x01, 0x02, 0x03, 0x04, 0x05};
+ REQUIRE_FALSE(Common::Compression::TryDecompressDataZSTD(one_byte, 0x10000));
+ REQUIRE_FALSE(Common::Compression::TryDecompressDataZSTD(bad_magic, 0x10000));
+ REQUIRE(Common::Compression::DecompressDataZSTD(one_byte).empty());
+
+ // The header declares the size, but the data after it is cut off
+ std::vector<u8> truncated = Compress(std::vector<u8>(100, 0x5a));
+ truncated.pop_back();
+ REQUIRE_FALSE(Common::Compression::TryDecompressDataZSTD(truncated, 0x10000));
+}
+
+TEST_CASE("ZSTD frames without a declared size are rejected", "[common][zstd]") {
+ // Magic number, a frame header without the content size, then one raw 3-byte last block
+ const std::array<u8, 12> frame{0x28, 0xb5, 0x2f, 0xfd, 0x00, 0x00,
+ 0x19, 0x00, 0x00, 'a', 'b', 'c'};
+ REQUIRE_FALSE(Common::Compression::TryDecompressDataZSTD(frame, 0x10000));
+ REQUIRE(Common::Compression::DecompressDataZSTD(frame).empty());
+}
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());
+}
diff --git a/src/yuzu_cmd/yuzu.cpp b/src/yuzu_cmd/yuzu.cpp
index bc4002d8af..6342c5d353 100644
--- a/src/yuzu_cmd/yuzu.cpp
+++ b/src/yuzu_cmd/yuzu.cpp
@@ -249,6 +249,7 @@ struct SdlState {
// Must outlive emu_window, which keeps a pointer to it.
InputCommon::InputSubsystem input_subsystem{};
std::unique_ptr<EmuWindow_SDL3> emu_window;
+ bool network_initialized{};
};
static std::atomic_bool pack_nup_cancel{false};
@@ -353,6 +354,12 @@ extern "C" SDL_AppResult SDL_AppInit(void **appstate, int argc, char **argv) {
return SDL_APP_FAILURE;
}
+ state->network_initialized = Network::Init();
+ if (!state->network_initialized) {
+ LOG_CRITICAL(Network, "Failed to initialize room networking");
+ return SDL_APP_FAILURE;
+ }
+
state->system.Initialize();
@@ -555,7 +562,11 @@ extern "C" void SDL_AppQuit(void *appstate, SDL_AppResult result) {
void(state->system.Pause());
state->system.ShutdownMainProcess();
}
+ const bool network_initialized = state->network_initialized;
delete state;
+ if (network_initialized) {
+ Network::Shutdown();
+ }
}
#define VMA_IMPLEMENTATION