diff options
| -rw-r--r-- | src/common/zstd_compression.cpp | 20 | ||||
| -rw-r--r-- | src/common/zstd_compression.h | 13 | ||||
| -rw-r--r-- | src/core/hle/service/ldn/lan_discovery.cpp | 85 | ||||
| -rw-r--r-- | src/core/hle/service/ldn/lan_discovery.h | 8 | ||||
| -rw-r--r-- | src/core/hle/service/nifm/nifm.cpp | 68 | ||||
| -rw-r--r-- | src/core/hle/service/sockets/bsd.cpp | 390 | ||||
| -rw-r--r-- | src/core/hle/service/sockets/bsd.h | 18 | ||||
| -rw-r--r-- | src/core/internal_network/network.cpp | 15 | ||||
| -rw-r--r-- | src/core/internal_network/network.h | 4 | ||||
| -rw-r--r-- | src/core/internal_network/socket_proxy.cpp | 157 | ||||
| -rw-r--r-- | src/core/internal_network/socket_proxy.h | 13 | ||||
| -rw-r--r-- | src/core/internal_network/sockets.h | 6 | ||||
| -rw-r--r-- | src/tests/CMakeLists.txt | 3 | ||||
| -rw-r--r-- | src/tests/common/zstd_compression.cpp | 59 | ||||
| -rw-r--r-- | src/tests/core/internal_network/socket_proxy.cpp | 261 | ||||
| -rw-r--r-- | src/tests/core/ldn.cpp | 202 | ||||
| -rw-r--r-- | src/yuzu_cmd/yuzu.cpp | 11 |
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 |
