From cf3bb346bf9d73471e7bb64a8bcafd52f36c3685 Mon Sep 17 00:00:00 2001 From: georgemoralis Date: Fri, 10 Jul 2026 16:54:26 +0300 Subject: [PATCH] Improvements on shadnet reachabilty (#4691) * disconnect on srv offline and protocol change * fixes for macOS --- CMakeLists.txt | 2 + src/core/emulator_settings.h | 22 ++- src/core/libraries/np/np_handler.cpp | 77 ++++++++ .../big_picture/settings_dialog_imgui.cpp | 2 +- src/shadnet/client.cpp | 5 +- src/shadnet/client.h | 4 + src/shadnet/server_probe.cpp | 185 ++++++++++++++++++ src/shadnet/server_probe.h | 30 +++ 8 files changed, 324 insertions(+), 3 deletions(-) create mode 100644 src/shadnet/server_probe.cpp create mode 100644 src/shadnet/server_probe.h diff --git a/CMakeLists.txt b/CMakeLists.txt index fc7ad361a..d0c0a90b0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1213,6 +1213,8 @@ add_custom_command( set(SHADNET src/shadnet/client.cpp src/shadnet/client.h + src/shadnet/server_probe.cpp + src/shadnet/server_probe.h "${SHADNET_PROTO_OUT}/shadnet.pb.cc" ) if (APPLE) diff --git a/src/core/emulator_settings.h b/src/core/emulator_settings.h index aff785773..3f0af8072 100644 --- a/src/core/emulator_settings.h +++ b/src/core/emulator_settings.h @@ -3,6 +3,7 @@ #pragma once +#include #include #include #include @@ -518,6 +519,10 @@ private: VulkanSettings m_vulkan{}; ConfigMode m_configMode{ConfigMode::Default}; + // Runtime-only override: when true, IsShadNetEnabled() reports false for the + // rest of this run regardless of the persisted setting + std::atomic m_shadnet_session_disabled{false}; + bool m_loaded{false}; static std::shared_ptr s_instance; @@ -596,7 +601,22 @@ public: SETTING_FORWARD_BOOL(m_general, Neo, neo_mode) SETTING_FORWARD_BOOL(m_general, DevKit, dev_kit_mode) SETTING_FORWARD(m_general, ExtraDmemInMBytes, extra_dmem_in_mbytes) - SETTING_FORWARD_BOOL(m_general, ShadNetEnabled, shad_net_enabled) + bool IsShadNetEnabled() const { + return m_general.shad_net_enabled.get(m_configMode) && + !m_shadnet_session_disabled.load(std::memory_order_relaxed); + } + void SetShadNetEnabled(bool v, bool specific = false) { + m_general.shad_net_enabled.set(v, specific); + } + bool IsShadNetEnabledSetting() const { + return m_general.shad_net_enabled.get(m_configMode); + } + void SetShadNetSessionDisabled(bool v) { + m_shadnet_session_disabled.store(v, std::memory_order_relaxed); + } + bool IsShadNetSessionDisabled() const { + return m_shadnet_session_disabled.load(std::memory_order_relaxed); + } SETTING_FORWARD_BOOL(m_general, TrophyPopupDisabled, trophy_popup_disabled) SETTING_FORWARD(m_general, TrophyNotificationDuration, trophy_notification_duration) SETTING_FORWARD(m_general, TrophyNotificationSide, trophy_notification_side) diff --git a/src/core/libraries/np/np_handler.cpp b/src/core/libraries/np/np_handler.cpp index ba218ae15..d9ce291fc 100644 --- a/src/core/libraries/np/np_handler.cpp +++ b/src/core/libraries/np/np_handler.cpp @@ -22,6 +22,7 @@ #include "imgui/shadnet_notifications_layer.h" #include "np_handler.h" #include "shadnet.pb.h" +#include "shadnet/server_probe.h" namespace Libraries::Np { @@ -102,6 +103,51 @@ void NpHandler::Initialize() { return; } + { + const auto [host, port] = ParseServerAddress(); + const ShadNet::ProbeInfo probe = ShadNet::ProbeServer(host, port); + if (probe.result != ShadNet::ProbeResult::Ok) { + EmulatorSettings.SetShadNetSessionDisabled(true); + switch (probe.result) { + case ShadNet::ProbeResult::VersionMismatch: + LOG_WARNING(NpHandler, + "shadNet server {}:{} protocol version mismatch (server v{}, emulator " + "v{}); disabling shadNet for this run (saved setting unchanged)", + host, port, probe.server_version, ShadNet::SHAD_PROTOCOL_VERSION); + ImGui::ShadNetNotify::Push( + ImGui::ShadNetNotify::Kind::Info, + fmt::format("shadNet protocol version mismatch (server v{}, emulator v{}). " + "Please update shadPS4. Online features are disabled for this " + "session.", + probe.server_version, ShadNet::SHAD_PROTOCOL_VERSION)); + break; + case ShadNet::ProbeResult::ProtocolError: + LOG_WARNING(NpHandler, + "shadNet server {}:{} sent an invalid ServerInfo handshake; disabling " + "shadNet for this run (saved setting unchanged)", + host, port); + ImGui::ShadNetNotify::Push( + ImGui::ShadNetNotify::Kind::Info, + fmt::format("shadNet server ({}:{}) uses an incompatible protocol. Online " + "features are disabled for this session.", + host, port)); + break; + default: // Unreachable + LOG_WARNING(NpHandler, + "shadNet server {}:{} is offline/unreachable; disabling shadNet for " + "this run (saved setting unchanged)", + host, port); + ImGui::ShadNetNotify::Push( + ImGui::ShadNetNotify::Kind::Info, + fmt::format("shadNet server ({}:{}) is offline. Online features are disabled " + "for this session.", + host, port)); + break; + } + return; + } + } + const auto logged_in = UserManagement.GetLoggedInUsers(); // get all login users int connected_count = 0; for (int i = 0; i < Libraries::UserService::ORBIS_USER_SERVICE_MAX_LOGIN_USERS; ++i) { @@ -184,10 +230,40 @@ bool NpHandler::ConnectUser(s32 user_id, const std::string& host, u16 port, cons client->SetAppearOffline(m_appear_offline.load()); client->Start(host, port, npid, password, token); + // Shared handling for an incompatible-protocol failure (version mismatch or + // corrupt/unparseable stream): tell the user and disable shadNet for this run, + // since reconnect attempts against an incompatible server are pointless. + const auto handle_protocol_mismatch = [&client](ShadNet::ShadNetState st) { + if (st != ShadNet::ShadNetState::FailureProtocol) + return; + const u32 server_ver = client->GetServerProtocolVersion(); + EmulatorSettings.SetShadNetSessionDisabled(true); + if (server_ver != 0) { + LOG_ERROR(NpHandler, + "shadNet protocol version mismatch (server v{}, emulator v{}); disabling " + "shadNet for this run (saved setting unchanged)", + server_ver, ShadNet::SHAD_PROTOCOL_VERSION); + ImGui::ShadNetNotify::Push( + ImGui::ShadNetNotify::Kind::Info, + fmt::format("shadNet protocol version mismatch (server v{}, emulator v{}). " + "Please update shadPS4. Online features are disabled for this " + "session.", + server_ver, ShadNet::SHAD_PROTOCOL_VERSION)); + } else { + LOG_ERROR(NpHandler, "shadNet protocol error during handshake; disabling shadNet for " + "this run (saved setting unchanged)"); + ImGui::ShadNetNotify::Push( + ImGui::ShadNetNotify::Kind::Info, + "shadNet server uses an incompatible protocol. Online features are disabled " + "for this session."); + } + }; + const ShadNet::ShadNetState conn_state = client->WaitForConnection(); if (conn_state != ShadNet::ShadNetState::Ok) { LOG_ERROR(NpHandler, "user_id={} connection failed (state={})", user_id, static_cast(conn_state)); + handle_protocol_mismatch(conn_state); client->Stop(); return false; } @@ -196,6 +272,7 @@ bool NpHandler::ConnectUser(s32 user_id, const std::string& host, u16 port, cons if (auth_state != ShadNet::ShadNetState::Ok) { LOG_ERROR(NpHandler, "user_id={} authentication failed (state={})", user_id, static_cast(auth_state)); + handle_protocol_mismatch(auth_state); client->Stop(); return false; } diff --git a/src/imgui/big_picture/settings_dialog_imgui.cpp b/src/imgui/big_picture/settings_dialog_imgui.cpp index 83289229b..d6d528f6e 100644 --- a/src/imgui/big_picture/settings_dialog_imgui.cpp +++ b/src/imgui/big_picture/settings_dialog_imgui.cpp @@ -83,7 +83,7 @@ void SettingsWindow::LoadSettings(std::string profile) { directMemoryAccessSetting = EmulatorSettings.IsDirectMemoryAccessEnabled(); devkitConsoleSetting = EmulatorSettings.IsDevKit(); neoModeSetting = EmulatorSettings.IsNeo(); - shadnetEnabledSetting = EmulatorSettings.IsShadNetEnabled(); + shadnetEnabledSetting = EmulatorSettings.IsShadNetEnabledSetting(); connectedNetworkSetting = EmulatorSettings.IsConnectedToNetwork(); pipelineCacheEnabledSetting = EmulatorSettings.IsPipelineCacheEnabled(); pipelineCacheArchiveSetting = EmulatorSettings.IsPipelineCacheArchived(); diff --git a/src/shadnet/client.cpp b/src/shadnet/client.cpp index 4075f9a30..7088df394 100644 --- a/src/shadnet/client.cpp +++ b/src/shadnet/client.cpp @@ -169,6 +169,8 @@ void ShadNetClient::ConnectThread() { connected = true; break; } + if (m_state == ShadNetState::FailureProtocol) + break; if (m_terminate || attempt == SHAD_CONNECT_MAX_ATTEMPTS) break; LOG_WARNING(ShadNet, "ShadNet: connect attempt {}/{} to {}:{} failed, retrying in {} ms", @@ -400,11 +402,12 @@ bool ShadNetClient::DoConnect() { } if (payload_sz >= 4) { const u32 server_ver = GetLE32(si_payload.data()); + m_server_protocol_version.store(server_ver); if (server_ver != SHAD_PROTOCOL_VERSION) { LOG_ERROR(ShadNet, "Protocol version mismatch server={} client={}", server_ver, SHAD_PROTOCOL_VERSION); DoDisconnect(); - m_state = ShadNetState::FailureServerInfo; + m_state = ShadNetState::FailureProtocol; return false; } } diff --git a/src/shadnet/client.h b/src/shadnet/client.h index fc438ce66..5c0d0414a 100644 --- a/src/shadnet/client.h +++ b/src/shadnet/client.h @@ -272,6 +272,9 @@ public: const std::string& GetAvatarUrl() const; u64 GetUserId() const; + u32 GetServerProtocolVersion() const { + return m_server_protocol_version.load(); + } u32 GetAddrLocal() const; u32 GetAddrServer() const; bool IsMatching2Enabled() const; @@ -369,6 +372,7 @@ private: std::string m_bearer_token; std::atomic m_addr_local{0}; std::atomic m_addr_server{0}; + std::atomic m_server_protocol_version{0}; std::atomic m_matching2_enabled{false}; std::atomic m_server_features_received{false}; diff --git a/src/shadnet/server_probe.cpp b/src/shadnet/server_probe.cpp new file mode 100644 index 000000000..f616b4792 --- /dev/null +++ b/src/shadnet/server_probe.cpp @@ -0,0 +1,185 @@ +// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include +#include "common/logging/log.h" +#include "shadnet/client.h" // socket platform typedefs + protocol constants +#include "shadnet/server_probe.h" + +#ifdef _WIN32 +#pragma comment(lib, "Ws2_32.lib") +static void ProbePlatformInit() { + static bool done = false; + if (!done) { + WSADATA wd; + WSAStartup(MAKEWORD(2, 2), &wd); + done = true; + } +} +#else +static void ProbePlatformInit() {} +#endif + +namespace ShadNet { + +static u32 ProbeGetLE32(const u8* p) { + return static_cast(p[0]) | (static_cast(p[1]) << 8) | (static_cast(p[2]) << 16) | + (static_cast(p[3]) << 24); +} + +// recv() exactly n bytes (blocking, bounded by the socket's SO_RCVTIMEO). +static bool ProbeRecvN(ShadSocketHandle sock, u8* buf, u32 n) { + u32 got = 0; + while (got < n) { + const int r = + ::recv(sock, reinterpret_cast(buf) + got, static_cast(n - got), 0); + if (r <= 0) + return false; + got += static_cast(r); + } + return true; +} + +ProbeInfo ProbeServer(const std::string& host, u16 port, u32 timeout_ms) { + ProbeInfo info{}; + + if (host.empty()) { + LOG_WARNING(ShadNet, "probe skipped: empty shadNet host"); + return info; // Unreachable + } + + ProbePlatformInit(); + + struct addrinfo hints{}, *res_list = nullptr; + hints.ai_family = AF_INET; + hints.ai_socktype = SOCK_STREAM; + + if (::getaddrinfo(host.c_str(), std::to_string(port).c_str(), &hints, &res_list) != 0 || + !res_list) { + LOG_WARNING(ShadNet, "probe: DNS resolution failed for '{}'", host); + return info; // Unreachable + } + + ShadSocketHandle sock = ::socket(res_list->ai_family, SOCK_STREAM, IPPROTO_TCP); + if (sock == SHAD_INVALID_SOCK) { + ::freeaddrinfo(res_list); + return info; // Unreachable + } + +#ifdef _WIN32 + { + u_long nb = 1; + ::ioctlsocket(sock, FIONBIO, &nb); + } +#else + { + int fl = ::fcntl(sock, F_GETFL, 0); + ::fcntl(sock, F_SETFL, fl | O_NONBLOCK); + } +#endif + + const int cr = ::connect(sock, res_list->ai_addr, static_cast(res_list->ai_addrlen)); + ::freeaddrinfo(res_list); + + bool connected = false; +#ifdef _WIN32 + const bool in_progress = (cr < 0 && WSAGetLastError() == WSAEWOULDBLOCK); +#else + const bool in_progress = (cr < 0 && errno == EINPROGRESS); +#endif + if (cr == 0 || in_progress) { + fd_set wfds; + FD_ZERO(&wfds); + FD_SET(sock, &wfds); + struct timeval tv{}; + tv.tv_sec = static_cast(timeout_ms / 1000); + tv.tv_usec = static_cast((timeout_ms % 1000) * 1000); + if (::select(static_cast(sock) + 1, nullptr, &wfds, nullptr, &tv) > 0) { + int err = 0; + socklen_t len = sizeof(err); + ::getsockopt(sock, SOL_SOCKET, SO_ERROR, reinterpret_cast(&err), &len); + connected = (err == 0); + } + } + + if (!connected) { + SHAD_CLOSE(sock); + LOG_WARNING(ShadNet, "probe: server {}:{} is unreachable (timeout {} ms)", host, port, + timeout_ms); + return info; // Unreachable + } + + // Restore blocking mode and apply a receive timeout for the ServerInfo read. +#ifdef _WIN32 + { + u_long nb = 0; + ::ioctlsocket(sock, FIONBIO, &nb); + } + DWORD so_rcv = static_cast(timeout_ms); +#else + { + int fl = ::fcntl(sock, F_GETFL, 0); + ::fcntl(sock, F_SETFL, fl & ~O_NONBLOCK); + } + struct timeval so_rcv{}; + so_rcv.tv_sec = static_cast(timeout_ms / 1000); + so_rcv.tv_usec = static_cast((timeout_ms % 1000) * 1000); +#endif + ::setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, reinterpret_cast(&so_rcv), + sizeof(so_rcv)); + + info.result = ProbeResult::ProtocolError; + + u8 hdr[SHAD_HEADER_SIZE]; + if (!ProbeRecvN(sock, hdr, SHAD_HEADER_SIZE)) { + SHAD_CLOSE(sock); + LOG_WARNING(ShadNet, "probe: {}:{} reachable but no ServerInfo header received", host, + port); + return info; + } + if (static_cast(hdr[0]) != PacketType::ServerInfo) { + SHAD_CLOSE(sock); + LOG_WARNING(ShadNet, "probe: {}:{} sent packet type {:02x} instead of ServerInfo", host, + port, hdr[0]); + return info; + } + + const u32 total_sz = ProbeGetLE32(hdr + 3); + if (total_sz < SHAD_HEADER_SIZE || total_sz > SHAD_MAX_PACKET_SIZE) { + SHAD_CLOSE(sock); + LOG_WARNING(ShadNet, "probe: {}:{} sent corrupt ServerInfo (total_sz={})", host, port, + total_sz); + return info; + } + const u32 payload_sz = total_sz - SHAD_HEADER_SIZE; + std::vector payload(payload_sz); + if (payload_sz > 0 && !ProbeRecvN(sock, payload.data(), payload_sz)) { + SHAD_CLOSE(sock); + LOG_WARNING(ShadNet, "probe: {}:{} ServerInfo payload read failed", host, port); + return info; + } + SHAD_CLOSE(sock); + + if (payload_sz < 4) { + info.result = ProbeResult::Ok; + LOG_INFO(ShadNet, "probe: server {}:{} is reachable (no version field in ServerInfo)", host, + port); + return info; + } + + info.server_version = ProbeGetLE32(payload.data()); + if (info.server_version != SHAD_PROTOCOL_VERSION) { + info.result = ProbeResult::VersionMismatch; + LOG_WARNING(ShadNet, "probe: server {}:{} protocol version mismatch (server v{}, ours v{})", + host, port, info.server_version, SHAD_PROTOCOL_VERSION); + return info; + } + + info.result = ProbeResult::Ok; + LOG_INFO(ShadNet, "probe: server {}:{} is reachable (protocol v{})", host, port, + info.server_version); + return info; +} + +} // namespace ShadNet diff --git a/src/shadnet/server_probe.h b/src/shadnet/server_probe.h new file mode 100644 index 000000000..203553544 --- /dev/null +++ b/src/shadnet/server_probe.h @@ -0,0 +1,30 @@ +// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include "common/types.h" + +namespace ShadNet { + +// Default timeout for the startup reachability probe. +static constexpr u32 SHAD_PROBE_TIMEOUT_MS = 3000; + +enum class ProbeResult { + Ok, // reachable and protocol version matches SHAD_PROTOCOL_VERSION + Unreachable, // DNS failure, connect failure, or timeout + VersionMismatch, // reachable, but ServerInfo protocol version differs from ours + ProtocolError, // reachable, but the ServerInfo handshake was malformed/absent +}; + +struct ProbeInfo { + ProbeResult result = ProbeResult::Unreachable; + // Protocol version reported by the server's ServerInfo packet. + // 0 when unknown (unreachable / malformed / server sent no version field). + u32 server_version = 0; +}; + +ProbeInfo ProbeServer(const std::string& host, u16 port, u32 timeout_ms = SHAD_PROBE_TIMEOUT_MS); + +} // namespace ShadNet