Improvements on shadnet reachabilty (#4691)

* disconnect on srv offline and protocol change

* fixes for macOS
This commit is contained in:
georgemoralis
2026-07-10 16:54:26 +03:00
committed by GitHub
parent bffa4d1b63
commit cf3bb346bf
8 changed files with 324 additions and 3 deletions
+2
View File
@@ -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)
+21 -1
View File
@@ -3,6 +3,7 @@
#pragma once
#include <atomic>
#include <filesystem>
#include <functional>
#include <memory>
@@ -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<bool> m_shadnet_session_disabled{false};
bool m_loaded{false};
static std::shared_ptr<EmulatorSettingsImpl> 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)
+77
View File
@@ -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<int>(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<int>(auth_state));
handle_protocol_mismatch(auth_state);
client->Stop();
return false;
}
@@ -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();
+4 -1
View File
@@ -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;
}
}
+4
View File
@@ -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<u32> m_addr_local{0};
std::atomic<u32> m_addr_server{0};
std::atomic<u32> m_server_protocol_version{0};
std::atomic<bool> m_matching2_enabled{false};
std::atomic<bool> m_server_features_received{false};
+185
View File
@@ -0,0 +1,185 @@
// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <string>
#include <vector>
#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<u32>(p[0]) | (static_cast<u32>(p[1]) << 8) | (static_cast<u32>(p[2]) << 16) |
(static_cast<u32>(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<char*>(buf) + got, static_cast<int>(n - got), 0);
if (r <= 0)
return false;
got += static_cast<u32>(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<int>(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<decltype(tv.tv_sec)>(timeout_ms / 1000);
tv.tv_usec = static_cast<decltype(tv.tv_usec)>((timeout_ms % 1000) * 1000);
if (::select(static_cast<int>(sock) + 1, nullptr, &wfds, nullptr, &tv) > 0) {
int err = 0;
socklen_t len = sizeof(err);
::getsockopt(sock, SOL_SOCKET, SO_ERROR, reinterpret_cast<char*>(&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<DWORD>(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<decltype(so_rcv.tv_sec)>(timeout_ms / 1000);
so_rcv.tv_usec = static_cast<decltype(so_rcv.tv_usec)>((timeout_ms % 1000) * 1000);
#endif
::setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, reinterpret_cast<const char*>(&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<PacketType>(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<u8> 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
+30
View File
@@ -0,0 +1,30 @@
// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <string>
#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