From e699a35f20bcf790366e73ce6573d04a3e0a789f Mon Sep 17 00:00:00 2001 From: nmzik Date: Mon, 12 Oct 2026 01:19:07 +0200 Subject: [PATCH] netctl: add opt-in host IPv4 connectivity and launcher toggle --- src/common/emulatorConfig.cpp | 4 + src/common/emulatorConfig.h | 2 + .../forms/configuration_edit_dialog.ui | 17 +++ src/launcher/include/configuration.h | 2 + src/launcher/src/configuration.cpp | 2 + src/launcher/src/configurationEditDialog.cpp | 2 + src/launcher/src/mainDialog.cpp | 1 + src/libs/network.cpp | 143 +++++++++++------- src/main.cpp | 6 + tests/KernelFileSystemTests.cpp | 84 ++++++++++ 10 files changed, 206 insertions(+), 57 deletions(-) diff --git a/src/common/emulatorConfig.cpp b/src/common/emulatorConfig.cpp index 9dc6aefce..5470bce62 100644 --- a/src/common/emulatorConfig.cpp +++ b/src/common/emulatorConfig.cpp @@ -100,6 +100,10 @@ bool VulkanValidationEnabled() { return g_config->vulkan_validation_enabled; } +bool IsNetworkConnected() { + return g_config->network_connected; +} + bool ShaderValidationEnabled() { return g_config->shader_validation_enabled; } diff --git a/src/common/emulatorConfig.h b/src/common/emulatorConfig.h index 6cd047fc3..c7d641878 100644 --- a/src/common/emulatorConfig.h +++ b/src/common/emulatorConfig.h @@ -58,6 +58,7 @@ struct ConfigOptions { bool splash_enabled = true; bool vr_enabled = false; bool amd_cpu_enabled = false; + bool network_connected = false; uint32_t vblank_frequency = 60; uint32_t console_language = DEFAULT_CONSOLE_LANGUAGE; bool vulkan_validation_enabled = false; @@ -105,6 +106,7 @@ bool HdrEnabled(); bool HideCursorEnabled(); bool VrEnabled(); bool AmdCpuEnabled(); +bool IsNetworkConnected(); uint32_t GetVblankFrequency(); uint32_t GetConsoleLanguage(); bool VulkanValidationEnabled(); diff --git a/src/launcher/forms/configuration_edit_dialog.ui b/src/launcher/forms/configuration_edit_dialog.ui index 5c0baf00d..b79c83f0e 100644 --- a/src/launcher/forms/configuration_edit_dialog.ui +++ b/src/launcher/forms/configuration_edit_dialog.ui @@ -329,6 +329,23 @@ + + Network + + + + Let games use your computer's local network connection. + Enable network connection + + + + + Qt::Vertical + 2040 + + + + Debug diff --git a/src/launcher/include/configuration.h b/src/launcher/include/configuration.h index 649c7cee3..e4bf3c8bf 100644 --- a/src/launcher/include/configuration.h +++ b/src/launcher/include/configuration.h @@ -124,6 +124,7 @@ public: bool tessellation_enabled = false; bool trophy_enabled = true; bool skip_notice_screen = false; + bool network_connected = false; int vblank_frequency = 60; int console_language = DEFAULT_CONSOLE_LANGUAGE; bool vulkan_validation_enabled = false; @@ -160,6 +161,7 @@ public: tessellation_enabled = other.tessellation_enabled; trophy_enabled = other.trophy_enabled; skip_notice_screen = other.skip_notice_screen; + network_connected = other.network_connected; vblank_frequency = other.vblank_frequency; console_language = other.console_language; vulkan_validation_enabled = other.vulkan_validation_enabled; diff --git a/src/launcher/src/configuration.cpp b/src/launcher/src/configuration.cpp index 76bc81d7b..b2527f39e 100644 --- a/src/launcher/src/configuration.cpp +++ b/src/launcher/src/configuration.cpp @@ -17,6 +17,7 @@ QVariantMap Configuration::GameSettings() const { KYTY_CFG_SET(tessellation_enabled); KYTY_CFG_SET(trophy_enabled); KYTY_CFG_SET(skip_notice_screen); + KYTY_CFG_SET(network_connected); KYTY_CFG_SET(vblank_frequency); KYTY_CFG_SET(console_language); KYTY_CFG_SET(vulkan_validation_enabled); @@ -63,6 +64,7 @@ void Configuration::ReadGameSettingsValues(const Settings& s) { KYTY_CFG_GET(tessellation_enabled); trophy_enabled = s.value("trophy_enabled", trophy_enabled).toBool(); KYTY_CFG_GET(skip_notice_screen); + KYTY_CFG_GET(network_connected); vblank_frequency = s.value("vblank_frequency", vblank_frequency).toInt(); console_language = s.value("console_language", console_language).toInt(); if (console_language < 0 || console_language > MAX_CONSOLE_LANGUAGE) { diff --git a/src/launcher/src/configurationEditDialog.cpp b/src/launcher/src/configurationEditDialog.cpp index 3493b00e2..4c95ef6a7 100644 --- a/src/launcher/src/configurationEditDialog.cpp +++ b/src/launcher/src/configurationEditDialog.cpp @@ -279,6 +279,7 @@ void ConfigurationEditDialog::Init(const Configuration& info) { m_ui->checkBox_sync_raw_image_buffers->setChecked(info.sync_raw_image_buffers); m_ui->checkBox_tessellation->setChecked(info.tessellation_enabled); m_ui->checkBox_skip_notice_screen->setChecked(info.skip_notice_screen); + m_ui->checkBox_network_connected->setChecked(info.network_connected); m_ui->checkBox_trophy_notifications->setChecked(info.trophy_enabled); m_ui->spinBox_vblank_frequency->setValue(info.vblank_frequency); m_ui->comboBox_console_language->clear(); @@ -441,6 +442,7 @@ static void UpdateInfo(Configuration& info, Ui::ConfigurationEditDialog& ui, boo info.sync_raw_image_buffers = ui.checkBox_sync_raw_image_buffers->isChecked(); info.tessellation_enabled = ui.checkBox_tessellation->isChecked(); info.skip_notice_screen = ui.checkBox_skip_notice_screen->isChecked(); + info.network_connected = ui.checkBox_network_connected->isChecked(); info.trophy_enabled = ui.checkBox_trophy_notifications->isChecked(); info.vblank_frequency = ui.spinBox_vblank_frequency->value(); info.console_language = ui.comboBox_console_language->currentIndex(); diff --git a/src/launcher/src/mainDialog.cpp b/src/launcher/src/mainDialog.cpp index 8d3ba3da0..260999331 100644 --- a/src/launcher/src/mainDialog.cpp +++ b/src/launcher/src/mainDialog.cpp @@ -255,6 +255,7 @@ static QStringList CreateEmulatorArgs(const Configuration& info) { args << "--sync-raw-image-buffers" << BoolArg(info.sync_raw_image_buffers); args << "--trophy-notifications" << BoolArg(info.trophy_enabled); args << "--skip-notice-screen" << BoolArg(info.skip_notice_screen); + args << "--network-connected" << BoolArg(info.network_connected); if (info.tessellation_enabled) { args << "--tessellation"; } diff --git a/src/libs/network.cpp b/src/libs/network.cpp index 847f53f2d..fc9688544 100644 --- a/src/libs/network.cpp +++ b/src/libs/network.cpp @@ -15,6 +15,9 @@ #else #include #include +#include +#include +#include #include #include #include @@ -41,10 +44,12 @@ #include #include #include +#include #include #include #include #include +#include #include #include #include @@ -3386,7 +3391,6 @@ constexpr int NET_CTL_STATE_DISCONNECTED = 0; constexpr int NET_CTL_STATE_IPOBTAINED = 3; constexpr int NET_CTL_DEVICE_WIRED = 0; constexpr int NET_CTL_DEVICE_WIRELESS = 1; -constexpr int NET_CTL_LINK_DISCONNECTED = 0; constexpr int NET_CTL_LINK_CONNECTED = 1; constexpr int NET_CTL_EVENT_DISCONNECTED = 1; constexpr int NET_CTL_EVENT_IPOBTAINED = 3; @@ -3448,8 +3452,6 @@ struct NetCtlCallbackSlot { static Common::Mutex g_net_ctl_callbacks_mutex; static std::array g_net_ctl_callbacks; -static std::atomic_bool g_net_ctl_connected = false; -static std::atomic_bool g_net_ctl_status_initialized = false; struct HostNetworkInfo { bool connected = false; @@ -3463,22 +3465,16 @@ struct HostNetworkInfo { }; static void CopyIpv4String(char* dst, size_t dst_size, const sockaddr* addr) { -#if defined(_WIN32) if (dst == nullptr || dst_size == 0 || addr == nullptr || addr->sa_family != AF_INET) { return; } const auto* in = reinterpret_cast(addr); inet_ntop(AF_INET, &in->sin_addr, dst, static_cast(dst_size)); -#else - (void)dst; - (void)dst_size; - (void)addr; -#endif } -static void CopyIpv4Netmask(char* dst, size_t dst_size, uint8_t prefix_len) { #if defined(_WIN32) +static void CopyIpv4Netmask(char* dst, size_t dst_size, uint8_t prefix_len) { if (dst == nullptr || dst_size == 0 || prefix_len > 32) { return; } @@ -3487,26 +3483,22 @@ static void CopyIpv4Netmask(char* dst, size_t dst_size, uint8_t prefix_len) { in_addr in {}; in.S_un.S_addr = htonl(mask); inet_ntop(AF_INET, &in, dst, static_cast(dst_size)); -#else - (void)dst; - (void)dst_size; - (void)prefix_len; -#endif } +#endif static HostNetworkInfo QueryHostNetworkInfo() { HostNetworkInfo info; #if defined(_WIN32) - ULONG flags = GAA_FLAG_SKIP_ANYCAST | GAA_FLAG_SKIP_MULTICAST; + ULONG flags = GAA_FLAG_SKIP_ANYCAST | GAA_FLAG_SKIP_MULTICAST | GAA_FLAG_INCLUDE_GATEWAYS; ULONG size = 0; - auto ret = GetAdaptersAddresses(AF_UNSPEC, flags, nullptr, nullptr, &size); + auto ret = GetAdaptersAddresses(AF_INET, flags, nullptr, nullptr, &size); if (ret != ERROR_BUFFER_OVERFLOW || size == 0) { return info; } std::vector buffer(size); auto* adapters = reinterpret_cast(buffer.data()); - ret = GetAdaptersAddresses(AF_UNSPEC, flags, nullptr, adapters, &size); + ret = GetAdaptersAddresses(AF_INET, flags, nullptr, adapters, &size); if (ret != NO_ERROR) { return info; } @@ -3557,40 +3549,76 @@ static HostNetworkInfo QueryHostNetworkInfo() { } return info; } - } else if (addr->sa_family == AF_INET6) { - const auto* in6 = reinterpret_cast(addr); - if (!IN6_IS_ADDR_LOOPBACK(&in6->sin6_addr) && - !IN6_IS_ADDR_UNSPECIFIED(&in6->sin6_addr)) { - info.connected = true; - } } } } return info; #else - info.connected = true; + ifaddrs* addresses = nullptr; + if (getifaddrs(&addresses) != 0) { + return info; + } + const auto usable = [](const ifaddrs* address) { + return address->ifa_addr != nullptr && address->ifa_addr->sa_family == AF_INET && + (address->ifa_flags & (IFF_UP | IFF_RUNNING | IFF_LOOPBACK)) == + (IFF_UP | IFF_RUNNING) && + reinterpret_cast(address->ifa_addr)->sin_addr.s_addr != 0; + }; + const ifaddrs* selected = nullptr; + for (auto* address = addresses; address != nullptr; address = address->ifa_next) { + if (usable(address)) { + selected = address; + break; + } + } + std::ifstream routes("/proc/net/route"); + std::string line; + uint32_t best_metric = std::numeric_limits::max(); + uint32_t gateway = 0; + bool has_default_route = false; + while (std::getline(routes, line)) { + std::istringstream fields(line); + std::string name; + uint32_t destination = 0, candidate_gateway = 0, flags = 0, refs = 0, uses = 0; + uint32_t metric = 0, mask = 0; + if (!(fields >> name >> std::hex >> destination >> candidate_gateway >> flags >> + std::dec >> refs >> uses >> metric >> std::hex >> mask) || + destination != 0 || mask != 0 || (flags & (RTF_UP | RTF_REJECT)) != RTF_UP || + (has_default_route && metric >= best_metric)) { + continue; + } + for (auto* address = addresses; address != nullptr; address = address->ifa_next) { + if (usable(address) && name == address->ifa_name) { + selected = address; + gateway = candidate_gateway; + best_metric = metric; + has_default_route = true; + break; + } + } + } + if (selected != nullptr) { + CopyIpv4String(info.ip_address, sizeof(info.ip_address), selected->ifa_addr); + CopyIpv4String(info.netmask, sizeof(info.netmask), selected->ifa_netmask); + info.connected = info.ip_address[0] != '\0'; + if (has_default_route) { + in_addr address {gateway}; + inet_ntop(AF_INET, &address, info.default_route, sizeof(info.default_route)); + } + } + freeifaddrs(addresses); return info; #endif } static bool NetCtlConnected() { - if (!g_net_ctl_status_initialized.load()) { - g_net_ctl_connected = false; - g_net_ctl_status_initialized = true; - LOGF("\t host network connected = false (forced offline)\n"); - } - - return g_net_ctl_connected.load(); + return Config::IsNetworkConnected() && QueryHostNetworkInfo().connected; } int KYTY_SYSV_ABI NetCtlInit() { PRINT_NAME(); - g_net_ctl_connected = false; - g_net_ctl_status_initialized = true; - LOGF("\t host network connected = false (forced offline)\n"); - return OK; } @@ -3651,7 +3679,7 @@ int KYTY_SYSV_ABI NetCtlGetStateV6(int* state) { return NET_CTL_ERROR_INVALID_ADDR; } - *state = (NetCtlConnected() ? NET_CTL_STATE_IPOBTAINED : NET_CTL_STATE_DISCONNECTED); + *state = NET_CTL_STATE_DISCONNECTED; return OK; } @@ -3720,24 +3748,28 @@ int KYTY_SYSV_ABI NetCtlGetResult(int event_type, int* error_code) { int KYTY_SYSV_ABI NetCtlGetInfo(int code, NetCtlInfo* info) { PRINT_NAME(); - EXIT_NOT_IMPLEMENTED(info == nullptr); - + if (info == nullptr) { + return NET_CTL_ERROR_INVALID_ADDR; + } + if (code < 1 || code > 21) { + return NET_CTL_ERROR_INVALID_CODE; + } + if (!Config::IsNetworkConnected()) { + return NET_CTL_ERROR_NOT_CONNECTED; + } + const auto host = QueryHostNetworkInfo(); + if (!host.connected || (code == 16 && host.default_route[0] == '\0')) { + return NET_CTL_ERROR_NOT_CONNECTED; + } memset(info, 0, sizeof(NetCtlInfo)); - // Online/host-backed info responses are left below for testing, but disabled - // to preserve the disconnected-console behavior. - return NET_CTL_ERROR_NOT_CONNECTED; - switch (code) { case 1: - info->device = - (QueryHostNetworkInfo().wireless ? NET_CTL_DEVICE_WIRELESS : NET_CTL_DEVICE_WIRED); + info->device = (host.wireless ? NET_CTL_DEVICE_WIRELESS : NET_CTL_DEVICE_WIRED); break; - case 2: info->ether_addr = QueryHostNetworkInfo().ether_addr; break; + case 2: info->ether_addr = host.ether_addr; break; case 3: info->mtu = 1500; break; - case 4: - info->link = (NetCtlConnected() ? NET_CTL_LINK_CONNECTED : NET_CTL_LINK_DISCONNECTED); - break; + case 4: info->link = NET_CTL_LINK_CONNECTED; break; case 5: break; case 6: break; case 7: info->wifi_security = 0; break; @@ -3748,28 +3780,25 @@ int KYTY_SYSV_ABI NetCtlGetInfo(int code, NetCtlInfo* info) { case 12: break; case 13: break; case 14: - std::strncpy(info->ip_address, QueryHostNetworkInfo().ip_address, + std::strncpy(info->ip_address, host.ip_address, sizeof(info->ip_address) - 1); break; case 15: - std::strncpy(info->netmask, QueryHostNetworkInfo().netmask, sizeof(info->netmask) - 1); + std::strncpy(info->netmask, host.netmask, sizeof(info->netmask) - 1); break; case 16: - std::strncpy(info->default_route, QueryHostNetworkInfo().default_route, + std::strncpy(info->default_route, host.default_route, sizeof(info->default_route) - 1); break; case 17: - std::strncpy(info->primary_dns, QueryHostNetworkInfo().primary_dns, - sizeof(info->primary_dns) - 1); + std::strncpy(info->primary_dns, host.primary_dns, sizeof(info->primary_dns) - 1); break; case 18: - std::strncpy(info->secondary_dns, QueryHostNetworkInfo().secondary_dns, - sizeof(info->secondary_dns) - 1); + std::strncpy(info->secondary_dns, host.secondary_dns, sizeof(info->secondary_dns) - 1); break; case 19: info->http_proxy_config = NET_CTL_HTTP_PROXY_OFF; break; case 20: break; case 21: info->http_proxy_port = 0; break; - default: LOGF("\t unknown NetCtl info code: %d\n", code); return NET_CTL_ERROR_INVALID_CODE; } return OK; diff --git a/src/main.cpp b/src/main.cpp index 8057e56f1..a96101022 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -70,6 +70,7 @@ static void PrintUsage() { ::printf(" --vblank-frequency Virtual vblank frequency. Default: 60.\n"); ::printf(" --console-language <0-29> Console language. Default: 1 (English US).\n"); ::printf(" --vulkan-validation Enable Vulkan validation.\n"); + ::printf(" --network-connected Use the host local network (default: false).\n"); ::printf(" --gpu-assisted-validation Bounds-check shader accesses on the GPU.\n" " Implies --vulkan-validation; very slow.\n"); ::printf(" --shader-validation Enable shader validation.\n"); @@ -388,6 +389,11 @@ static bool ParseArgs(int argc, char* argv[], RunOptions& options, bool& show_he ::printf("invalid console language: %s\n", value.c_str()); return false; } + } else if (arg == "--network-connected") { + if (!ParseBool(value, options.config.network_connected)) { + ::printf("invalid boolean for %s: %s\n", arg.c_str(), value.c_str()); + return false; + } } else if (arg == "--vulkan-validation") { if (!ParseBool(value, options.config.vulkan_validation_enabled)) { ::printf("invalid boolean for %s: %s\n", arg.c_str(), value.c_str()); diff --git a/tests/KernelFileSystemTests.cpp b/tests/KernelFileSystemTests.cpp index a631dfeb8..1da819da6 100644 --- a/tests/KernelFileSystemTests.cpp +++ b/tests/KernelFileSystemTests.cpp @@ -35,6 +35,9 @@ #include #if defined(__linux__) +#include +#include +#include #include #include #endif @@ -1459,6 +1462,84 @@ void CheckSocketReceiveBuffer(int reader, int writer) { } #endif +#if defined(__linux__) +void KYTY_SYSV_ABI RecordNetCtlEvent(int event, void* arg) { + auto& events = *static_cast*>(arg); + ++events[0]; + events[1] = event; +} + +void TestNetCtlHostIpv4() { + using namespace Libs::Network::NetCtl; + ifaddrs* interfaces = nullptr; + Check(getifaddrs(&interfaces) == 0, "enumerate host IPv4 addresses"); + std::vector host_addresses; + for (auto* interface = interfaces; interface != nullptr; interface = interface->ifa_next) { + if (interface->ifa_addr == nullptr || interface->ifa_addr->sa_family != AF_INET || + (interface->ifa_flags & IFF_UP) == 0 || (interface->ifa_flags & IFF_RUNNING) == 0 || + (interface->ifa_flags & IFF_LOOPBACK) != 0) { + continue; + } + const auto address = reinterpret_cast(interface->ifa_addr)->sin_addr; + if (address.s_addr != 0 && (ntohl(address.s_addr) >> 24) != 127) { + host_addresses.push_back(address.s_addr); + } + } + freeifaddrs(interfaces); + + Config::ConfigOptions options; + options.printf_direction = Config::LogDirection::Silent; + Check(!options.network_connected, "host networking requires explicit opt-in"); + alignas(uint32_t) std::array storage {}; + auto* info = reinterpret_cast(storage.data()); + constexpr int NotConnected = static_cast(0x80412108u); + Check(NetCtlGetInfo(14, nullptr) == static_cast(0x80412107u) && + NetCtlGetInfo(0, info) == static_cast(0x80412106u) && + NetCtlGetInfo(22, info) == static_cast(0x80412106u), + "NetCtl reports invalid output addresses and information codes"); + for (bool enabled : {false, true}) { + options.network_connected = enabled; + Config::Load(options); + Check(NetCtlInit() == OK, "initialize NetCtl for selected network mode"); + const bool connected = enabled && !host_addresses.empty(); + int state = -1; + Check(NetCtlGetState(&state) == OK && state == (connected ? 3 : 0), + "NetCtl IPv4 state matches opt-in and host connectivity"); + Check(NetCtlGetStateV6(&state) == OK && state == 0, + "IPv4 connectivity does not advertise IPv6 support"); + + Check(NetCtlGetInfo(14, info) == (connected ? OK : NotConnected), + "NetCtl IPv4 query respects network mode and host state"); + in_addr address {}; + if (connected) { + Check(inet_pton(AF_INET, storage.data(), &address) == 1 && + std::find(host_addresses.begin(), host_addresses.end(), address.s_addr) != + host_addresses.end(), + "NetCtl reports an IPv4 address assigned to an active host interface"); + } + Check(NetCtlGetInfo(15, info) == (connected ? OK : NotConnected) && + (!connected || inet_pton(AF_INET, storage.data(), &address) == 1), + "NetCtl returns the active IPv4 netmask only in connected mode"); + const int gateway_result = NetCtlGetInfo(16, info); + Check(gateway_result == NotConnected || + (connected && gateway_result == OK && + inet_pton(AF_INET, storage.data(), &address) == 1), + "NetCtl reports a valid IPv4 gateway or its absence"); + + std::array events {}; + int callback_id = -1; + Check(NetCtlRegisterCallback(RecordNetCtlEvent, &events, &callback_id) == OK && + NetCtlCheckCallback() == OK && events[0] == 1 && + events[1] == (connected ? 3 : 1) && NetCtlCheckCallback() == OK && events[0] == 1, + "NetCtl delivers the matching connection event once while state is unchanged"); + Check(NetCtlUnregisterCallback(callback_id) == OK, "release NetCtl callback"); + NetCtlTerm(); + } + options.network_connected = false; + Config::Load(options); +} +#endif + void CheckEtherAddressFormatting() { Loader::SymbolDatabase symbols; Libs::LibNet::InitNet_1_Net(&symbols); @@ -1790,6 +1871,9 @@ int main(int, char**) { FileSystem::Shutdown(); CheckSocketWakeup(); CheckEtherAddressFormatting(); +#if defined(__linux__) + TestNetCtlHostIpv4(); +#endif TestNpWebApi2Memory(); TestNpCommerceDialog(); graphics.reset();