From 3065b892f2e28657fa4adf00593305ed17f9a75d Mon Sep 17 00:00:00 2001 From: Rick <99503318+0xRickx@users.noreply.github.com> Date: Tue, 13 Oct 2026 12:00:35 +0200 Subject: [PATCH] fix(libSceNetCtl): answer sceNetCtlGetInfo from the host IPv4 interface sceNetCtlGetState reports IPOBTAINED from the host's addresses, but sceNetCtlGetInfo returned NOT_CONNECTED for every code. It now answers from the host interface that holds an IPv4 address, preferring the one with the default route: its address, netmask, gateway, MTU and DNS servers. The link is presented as wired, connected, configured by DHCP and without a proxy, with the Ethernet address sceNetGetMacAddress reports. A null info returns INVALID_ADDR and a code outside 1..21 INVALID_CODE; the Wi-Fi, DHCP host name, PPPoE and proxy server codes throw. Co-Authored-By: Claude Opus 5.5 (1M context) --- core/libs/prx/libSceNetCtl/CMakeLists.txt | 14 ++ core/libs/prx/libSceNetCtl/Export.cpp | 203 +++++++++++++++++++++- core/libs/tests/GuestNetCtl.cpp | 136 +++++++++++++++ docs/dev/debt/libSceNetCtl.md | 5 + 4 files changed, 353 insertions(+), 5 deletions(-) create mode 100644 core/libs/tests/GuestNetCtl.cpp create mode 100644 docs/dev/debt/libSceNetCtl.md diff --git a/core/libs/prx/libSceNetCtl/CMakeLists.txt b/core/libs/prx/libSceNetCtl/CMakeLists.txt index f221f21932..0083a712af 100644 --- a/core/libs/prx/libSceNetCtl/CMakeLists.txt +++ b/core/libs/prx/libSceNetCtl/CMakeLists.txt @@ -1,3 +1,17 @@ add_library(libSceNetCtl SHARED EXCLUDE_FROM_ALL Export.cpp) target_include_directories(libSceNetCtl PRIVATE ${LIBS_INCLUDE_DIR}) configure_windows_unwind(libSceNetCtl) + +if(BUILD_TESTING) + add_test_executable(guest_net_ctl_tests ${LIBS_INCLUDE_DIR}/tests/GuestNetCtl.cpp) + target_include_directories(guest_net_ctl_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_net_ctl_tests PRIVATE libSceNetCtl libc) + configure_windows_unwind(guest_net_ctl_tests) + add_test(NAME guest_net_ctl COMMAND guest_net_ctl_tests) + set_tests_properties(guest_net_ctl PROPERTIES TIMEOUT 20 SKIP_RETURN_CODE 77) + if(WIN32) + target_link_libraries(guest_net_ctl_tests PRIVATE ws2_32) + set_tests_properties(guest_net_ctl PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceNetCtl/Export.cpp b/core/libs/prx/libSceNetCtl/Export.cpp index 5f0ab11e26..332ee323f7 100644 --- a/core/libs/prx/libSceNetCtl/Export.cpp +++ b/core/libs/prx/libSceNetCtl/Export.cpp @@ -4,32 +4,58 @@ #include #include #else +#include #include #include +#include #include +#include +#include +#include #ifdef __linux__ #include #endif #endif #include #include +#include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" #include +#include #include +#include #include #include -#ifdef _WIN32 #include -#endif // Network-control state reflects host interface addressing, not PSN sign-in status. static constexpr int SCE_NET_CTL_ERROR_CALLBACK_MAX = static_cast(0x80412103); static constexpr int SCE_NET_CTL_ERROR_INVALID_ID = static_cast(0x80412105); +static constexpr int SCE_NET_CTL_ERROR_INVALID_CODE = static_cast(0x80412106); static constexpr int SCE_NET_CTL_ERROR_INVALID_ADDR = static_cast(0x80412107); static constexpr int SCE_NET_CTL_ERROR_NOT_CONNECTED = static_cast(0x80412108); static constexpr int NET_CTL_STATE_DISCONNECTED = 0; static constexpr int NET_CTL_STATE_IPOBTAINED = 3; +static constexpr int NET_CTL_INFO_DEVICE = 1; +static constexpr int NET_CTL_INFO_ETHER_ADDR = 2; +static constexpr int NET_CTL_INFO_MTU = 3; +static constexpr int NET_CTL_INFO_LINK = 4; +static constexpr int NET_CTL_INFO_IP_CONFIG = 11; +static constexpr int NET_CTL_INFO_IP_ADDRESS = 14; +static constexpr int NET_CTL_INFO_NETMASK = 15; +static constexpr int NET_CTL_INFO_DEFAULT_ROUTE = 16; +static constexpr int NET_CTL_INFO_PRIMARY_DNS = 17; +static constexpr int NET_CTL_INFO_SECONDARY_DNS = 18; +static constexpr int NET_CTL_INFO_HTTP_PROXY_CONFIG = 19; +static constexpr int NET_CTL_INFO_HTTP_PROXY_PORT = 21; +static constexpr std::uint32_t NET_CTL_DEVICE_WIRED = 0; +static constexpr std::uint32_t NET_CTL_LINK_CONNECTED = 1; +static constexpr std::uint32_t NET_CTL_IP_DHCP = 0; +static constexpr std::uint32_t NET_CTL_HTTP_PROXY_OFF = 0; +static constexpr std::uint8_t NET_CTL_ETHER_ADDR[6] = {0x02, 0x50, 0x53, 0x35, 0x00, 0x01}; +static constexpr std::size_t NET_CTL_IPV4_ADDR_STR_LEN = 16; static constexpr int MAX_CALLBACKS = 8; static std::mutex g_callbackLock; @@ -84,6 +110,157 @@ static bool HostHasAddress(int family) { #endif } +struct HostIpv4Interface { + in_addr address{}; + in_addr netmask{}; + in_addr gateway{}; + std::uint32_t mtu = 0; + std::vector dns; +}; + +static bool UsableIpv4(in_addr address) { + const auto host = ntohl(address.s_addr); + return host != 0 && (host >> 24) != 127; +} + +#ifdef _WIN32 +static const sockaddr_in* Ipv4Of(const SOCKET_ADDRESS& address) { + if (!address.lpSockaddr || address.lpSockaddr->sa_family != AF_INET) return nullptr; + return reinterpret_cast(address.lpSockaddr); +} + +static std::uint32_t PrefixMask(unsigned length) { + if (length == 0) return 0; + return htonl(length >= 32 ? 0xFFFFFFFFu : 0xFFFFFFFFu << (32 - length)); +} +#else +struct HostDefaultRoute { + std::string name; + in_addr gateway{}; + unsigned long metric = 0; +}; + +static std::vector HostDefaultRoutes() { + std::ifstream table("/proc/net/route"); + if (!table) throw std::runtime_error("sceNetCtlGetInfo: the host routing table is unavailable"); + std::vector routes; + std::string line; + std::getline(table, line); + while (std::getline(table, line)) { + std::istringstream fields(line); + HostDefaultRoute route; + unsigned long destination = 0; + unsigned long gateway = 0; + unsigned long flags = 0; + unsigned long references = 0; + unsigned long uses = 0; + unsigned long mask = 0; + if (!(fields >> route.name >> std::hex >> destination >> gateway >> flags >> std::dec >> references >> uses >> route.metric >> std::hex >> mask)) continue; + if (destination != 0 || mask != 0 || !(flags & RTF_UP) || (flags & RTF_REJECT)) continue; + route.gateway.s_addr = static_cast(gateway); + routes.push_back(route); + } + return routes; +} + +static std::uint32_t HostInterfaceMtu(const char* name) { + const int descriptor = socket(AF_INET, SOCK_DGRAM, 0); + if (descriptor < 0) throw std::runtime_error("sceNetCtlGetInfo: no socket to read the host interface MTU"); + ifreq request{}; + std::strncpy(request.ifr_name, name, IFNAMSIZ - 1); + const int result = ioctl(descriptor, SIOCGIFMTU, &request); + close(descriptor); + if (result != 0) throw std::runtime_error(std::string("sceNetCtlGetInfo: no MTU for host interface ") + name); + return static_cast(request.ifr_mtu); +} + +static std::vector HostDnsServers() { + std::ifstream config("/etc/resolv.conf"); + std::vector servers; + std::string line; + while (std::getline(config, line)) { + std::istringstream fields(line); + std::string keyword; + std::string value; + in_addr server{}; + if (fields >> keyword >> value && keyword == "nameserver" && inet_pton(AF_INET, value.c_str(), &server) == 1) servers.push_back(server); + } + return servers; +} +#endif + +static bool FindHostIpv4Interface(HostIpv4Interface& found) { +#ifdef _WIN32 + ULONG size = 0; + const ULONG flags = GAA_FLAG_SKIP_ANYCAST | GAA_FLAG_SKIP_MULTICAST | GAA_FLAG_INCLUDE_GATEWAYS; + if (GetAdaptersAddresses(AF_INET, flags, nullptr, nullptr, &size) != ERROR_BUFFER_OVERFLOW) return false; + std::vector storage(size); + auto* adapters = reinterpret_cast(storage.data()); + if (GetAdaptersAddresses(AF_INET, flags, nullptr, adapters, &size) != NO_ERROR) return false; + bool chosen = false; + bool chosenHasGateway = false; + ULONG chosenMetric = 0; + for (auto* adapter = adapters; adapter; adapter = adapter->Next) { + if (adapter->OperStatus != IfOperStatusUp || adapter->IfType == IF_TYPE_SOFTWARE_LOOPBACK) continue; + auto* unicast = adapter->FirstUnicastAddress; + while (unicast && !(Ipv4Of(unicast->Address) && UsableIpv4(Ipv4Of(unicast->Address)->sin_addr))) unicast = unicast->Next; + if (!unicast) continue; + auto* gateway = adapter->FirstGatewayAddress; + while (gateway && !Ipv4Of(gateway->Address)) gateway = gateway->Next; + if (chosen && !(gateway && (!chosenHasGateway || adapter->Ipv4Metric < chosenMetric))) continue; + HostIpv4Interface candidate; + candidate.address = Ipv4Of(unicast->Address)->sin_addr; + candidate.netmask.s_addr = PrefixMask(unicast->OnLinkPrefixLength); + if (gateway) candidate.gateway = Ipv4Of(gateway->Address)->sin_addr; + candidate.mtu = adapter->Mtu; + for (auto* server = adapter->FirstDnsServerAddress; server; server = server->Next) { + if (Ipv4Of(server->Address)) candidate.dns.push_back(Ipv4Of(server->Address)->sin_addr); + } + found = std::move(candidate); + chosen = true; + chosenHasGateway = gateway != nullptr; + chosenMetric = adapter->Ipv4Metric; + } + return chosen; +#else + ifaddrs* list = nullptr; + if (getifaddrs(&list) != 0) return false; + const std::unique_ptr interfaces(list, freeifaddrs); + const auto routes = HostDefaultRoutes(); + const ifaddrs* chosen = nullptr; + const HostDefaultRoute* chosenRoute = nullptr; + for (auto* item = list; item; item = item->ifa_next) { + if (!item->ifa_addr || item->ifa_addr->sa_family != AF_INET || !(item->ifa_flags & IFF_UP) || (item->ifa_flags & IFF_LOOPBACK)) continue; + if (!UsableIpv4(reinterpret_cast(item->ifa_addr)->sin_addr)) continue; + const HostDefaultRoute* route = nullptr; + for (const auto& entry : routes) { + if (entry.name == item->ifa_name && (!route || entry.metric < route->metric)) route = &entry; + } + if (chosen && !(route && (!chosenRoute || route->metric < chosenRoute->metric))) continue; + chosen = item; + chosenRoute = route; + } + if (!chosen) return false; + found.address = reinterpret_cast(chosen->ifa_addr)->sin_addr; + if (chosen->ifa_netmask) found.netmask = reinterpret_cast(chosen->ifa_netmask)->sin_addr; + if (chosenRoute) found.gateway = chosenRoute->gateway; + found.mtu = HostInterfaceMtu(chosen->ifa_name); + found.dns = HostDnsServers(); + return true; +#endif +} + +static void WriteInfoWord(NetCtlInfo* info, std::uint32_t value) { + std::memcpy(info->opaque, &value, sizeof(value)); +} + +static void WriteInfoAddress(NetCtlInfo* info, in_addr address) { + const auto* bytes = reinterpret_cast(&address.s_addr); + char text[NET_CTL_IPV4_ADDR_STR_LEN] = {}; + std::snprintf(text, sizeof(text), "%u.%u.%u.%u", static_cast(bytes[0]), static_cast(bytes[1]), static_cast(bytes[2]), static_cast(bytes[3])); + std::memcpy(info->opaque, text, sizeof(text)); +} + extern "C" std::uint64_t NetCtlReadByteCounter_nid_no_patch(const char* path) { std::ifstream file(path); std::uint64_t value = 0; @@ -135,9 +312,25 @@ int APS5_VABI sceNetCtlGetIfStat(NetCtlIfStat* stat) { } int APS5_VABI sceNetCtlGetInfo(int code, NetCtlInfo* info) { - (void)code; - (void)info; - return SCE_NET_CTL_ERROR_NOT_CONNECTED; + if (!info) return SCE_NET_CTL_ERROR_INVALID_ADDR; + if (code < NET_CTL_INFO_DEVICE || code > NET_CTL_INFO_HTTP_PROXY_PORT) return SCE_NET_CTL_ERROR_INVALID_CODE; + HostIpv4Interface host; + if (!FindHostIpv4Interface(host)) return SCE_NET_CTL_ERROR_NOT_CONNECTED; + switch (code) { + case NET_CTL_INFO_DEVICE: WriteInfoWord(info, NET_CTL_DEVICE_WIRED); break; + case NET_CTL_INFO_ETHER_ADDR: std::memcpy(info->opaque, NET_CTL_ETHER_ADDR, sizeof(NET_CTL_ETHER_ADDR)); break; + case NET_CTL_INFO_MTU: WriteInfoWord(info, host.mtu); break; + case NET_CTL_INFO_LINK: WriteInfoWord(info, NET_CTL_LINK_CONNECTED); break; + case NET_CTL_INFO_IP_CONFIG: WriteInfoWord(info, NET_CTL_IP_DHCP); break; + case NET_CTL_INFO_IP_ADDRESS: WriteInfoAddress(info, host.address); break; + case NET_CTL_INFO_NETMASK: WriteInfoAddress(info, host.netmask); break; + case NET_CTL_INFO_DEFAULT_ROUTE: WriteInfoAddress(info, host.gateway); break; + case NET_CTL_INFO_PRIMARY_DNS: WriteInfoAddress(info, host.dns.size() > 0 ? host.dns[0] : in_addr{}); break; + case NET_CTL_INFO_SECONDARY_DNS: WriteInfoAddress(info, host.dns.size() > 1 ? host.dns[1] : in_addr{}); break; + case NET_CTL_INFO_HTTP_PROXY_CONFIG: WriteInfoWord(info, NET_CTL_HTTP_PROXY_OFF); break; + default: throw std::runtime_error("sceNetCtlGetInfo: info code " + std::to_string(code) + " is not implemented"); + } + return 0; } int APS5_VABI sceNetCtlGetNatInfo(NetCtlNatInfo* nat_info) { diff --git a/core/libs/tests/GuestNetCtl.cpp b/core/libs/tests/GuestNetCtl.cpp new file mode 100644 index 0000000000..f0196de5a7 --- /dev/null +++ b/core/libs/tests/GuestNetCtl.cpp @@ -0,0 +1,136 @@ +#ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#include +#include +#else +#include +#include +#include +#include +#endif +#include "prx/libc/include/general/VabiMacros.hpp" +#include "SceTypes.hpp" +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceNetCtlGetInfo(int, NetCtlInfo*); +int APS5_VABI sceNetCtlGetState(int*); +} + +namespace { + +void Require(bool value) { if (!value) std::abort(); } + +constexpr int kErrInvalidCode = static_cast(0x80412106); +constexpr int kErrInvalidAddr = static_cast(0x80412107); +constexpr int kErrNotConnected = static_cast(0x80412108); +constexpr int kStateIpObtained = 3; +constexpr std::uint8_t kFill = 0xa5; +constexpr std::size_t kAddressLength = 16; + +NetCtlInfo Query(int code) { + NetCtlInfo info; + std::memset(&info, kFill, sizeof(info)); + Require(sceNetCtlGetInfo(code, &info) == 0); + return info; +} + +void RequireUntouchedFrom(const NetCtlInfo& info, std::size_t offset) { + for (std::size_t i = offset; i < sizeof(info.opaque); ++i) Require(info.opaque[i] == kFill); +} + +std::uint32_t QueryWord(int code) { + const auto info = Query(code); + std::uint32_t value = 0; + std::memcpy(&value, info.opaque, sizeof(value)); + RequireUntouchedFrom(info, sizeof(value)); + return value; +} + +in_addr QueryAddress(int code) { + const auto info = Query(code); + char text[kAddressLength]; + std::memcpy(text, info.opaque, sizeof(text)); + Require(std::memchr(text, '\0', sizeof(text)) != nullptr); + RequireUntouchedFrom(info, kAddressLength); + in_addr address{}; + Require(inet_pton(AF_INET, text, &address) == 1); + return address; +} + +bool Binds(in_addr address) { + sockaddr_in local{}; + local.sin_family = AF_INET; + local.sin_addr = address; +#ifdef _WIN32 + const SOCKET descriptor = socket(AF_INET, SOCK_DGRAM, 0); + Require(descriptor != INVALID_SOCKET); + const bool bound = bind(descriptor, reinterpret_cast(&local), sizeof(local)) == 0; + closesocket(descriptor); +#else + const int descriptor = socket(AF_INET, SOCK_DGRAM, 0); + Require(descriptor >= 0); + const bool bound = bind(descriptor, reinterpret_cast(&local), sizeof(local)) == 0; + close(descriptor); +#endif + return bound; +} + +} + +int main() { +#ifdef _WIN32 + WSADATA data{}; + Require(WSAStartup(MAKEWORD(2, 2), &data) == 0); +#endif + NetCtlInfo info{}; + Require(sceNetCtlGetInfo(0, &info) == kErrInvalidCode); + Require(sceNetCtlGetInfo(22, &info) == kErrInvalidCode); + Require(sceNetCtlGetInfo(14, nullptr) == kErrInvalidAddr); + + const int result = sceNetCtlGetInfo(14, &info); + if (result == kErrNotConnected) { + std::puts("skipped, the host has no IPv4 address"); + return 77; + } + Require(result == 0); + int state = -1; + Require(sceNetCtlGetState(&state) == 0 && state == kStateIpObtained); + + Require(QueryWord(1) == 0); + const auto ether = Query(2); + const std::uint8_t mac[6] = {0x02, 0x50, 0x53, 0x35, 0x00, 0x01}; + Require(std::memcmp(ether.opaque, mac, sizeof(mac)) == 0); + RequireUntouchedFrom(ether, sizeof(mac)); + const auto mtu = QueryWord(3); + Require(mtu >= 68 && mtu <= 65535); + Require(QueryWord(4) == 1); + Require(QueryWord(11) == 0); + Require(QueryWord(19) == 0); + + const auto address = QueryAddress(14); + const auto host = ntohl(address.s_addr); + Require(host != 0 && (host >> 24) != 127); + Require(Binds(address)); + const auto mask = ntohl(QueryAddress(15).s_addr); + Require(mask != 0 && (~mask & (~mask + 1)) == 0); + QueryAddress(16); + QueryAddress(17); + QueryAddress(18); + + for (const int code : {5, 6, 7, 8, 9, 10, 12, 13, 20, 21}) { + bool threw = false; + try { + sceNetCtlGetInfo(code, &info); + } catch (const std::runtime_error&) { + threw = true; + } + Require(threw); + } +} diff --git a/docs/dev/debt/libSceNetCtl.md b/docs/dev/debt/libSceNetCtl.md new file mode 100644 index 0000000000..4da9e097aa --- /dev/null +++ b/docs/dev/debt/libSceNetCtl.md @@ -0,0 +1,5 @@ +# libSceNetCtl technical debt + +### Unknown function info + +- [sceNetCtlGetInfo](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - answers from the host interface that holds an IPv4 address, preferring the one that carries the default route: its address, netmask, gateway, MTU and first two IPv4 DNS servers. The device is reported as wired, the link as connected, the IP configuration as DHCP and the HTTP proxy as off, whatever the host uses, and the Ethernet address is the one `sceNetGetMacAddress` reports. A gateway or DNS server the interface does not have is reported as `0.0.0.0`; what a console reports then is unknown. The Wi-Fi codes (5-10), the DHCP host name (12), the PPPoE user name (13) and the HTTP proxy server and port (20, 21) throw