From 97adb673e89657622ce6fa3289381e2755da3e3a Mon Sep 17 00:00:00 2001 From: Frederico Luz Date: Fri, 8 May 2026 16:32:40 +0100 Subject: [PATCH] Use emulated Ethernet identity for NP MAC Derive a stable locally administered Ethernet address from Console PSID and PS3 user id, with a hidden network config override. Expose that identity through cellNetCtl instead of host MAC discovery and add focused NP helper tests. --- buildfiles/msvc/rpcs3_default.props | 1 - rpcs3/CMakeLists.txt | 3 +- rpcs3/Emu/Cell/Modules/cellNetCtl.cpp | 40 +----- rpcs3/Emu/NP/np_handler.cpp | 189 +++++--------------------- rpcs3/Emu/NP/np_handler.h | 3 +- rpcs3/Emu/NP/np_helpers.cpp | 131 +++++++++++++++++- rpcs3/Emu/NP/np_helpers.h | 8 ++ rpcs3/Emu/system_config.h | 1 + rpcs3/tests/rpcs3_test.vcxproj | 3 +- rpcs3/tests/test_np_helpers.cpp | 51 +++++++ 10 files changed, 227 insertions(+), 203 deletions(-) create mode 100644 rpcs3/tests/test_np_helpers.cpp diff --git a/buildfiles/msvc/rpcs3_default.props b/buildfiles/msvc/rpcs3_default.props index b612560a8e..3809aca81e 100644 --- a/buildfiles/msvc/rpcs3_default.props +++ b/buildfiles/msvc/rpcs3_default.props @@ -27,7 +27,6 @@ ws2_32.lib; - Iphlpapi.lib; Bcrypt.lib; avcodec.lib; avformat.lib; diff --git a/rpcs3/CMakeLists.txt b/rpcs3/CMakeLists.txt index ba65a16eaf..9587db81c5 100644 --- a/rpcs3/CMakeLists.txt +++ b/rpcs3/CMakeLists.txt @@ -88,7 +88,7 @@ if (NOT ANDROID) endif() if(WIN32) - target_link_libraries(rpcs3_lib PRIVATE ws2_32 Iphlpapi Winmm Psapi gdi32 setupapi) + target_link_libraries(rpcs3_lib PRIVATE ws2_32 Winmm Psapi gdi32 setupapi) else() target_link_libraries(rpcs3_lib PRIVATE ${CMAKE_DL_LIBS}) endif() @@ -188,6 +188,7 @@ if(BUILD_RPCS3_TESTS) tests/test_simple_array.cpp tests/test_address_range.cpp tests/test_sys_fs.cpp + tests/test_np_helpers.cpp tests/test_rsx_cfg.cpp tests/test_rsx_fp_asm.cpp tests/test_dmux_pamf.cpp diff --git a/rpcs3/Emu/Cell/Modules/cellNetCtl.cpp b/rpcs3/Emu/Cell/Modules/cellNetCtl.cpp index 25b3746e87..8e4dd666c3 100644 --- a/rpcs3/Emu/Cell/Modules/cellNetCtl.cpp +++ b/rpcs3/Emu/Cell/Modules/cellNetCtl.cpp @@ -12,12 +12,6 @@ #include "Emu/NP/np_handler.h" #include "Emu/NP/np_helpers.h" -#ifdef _WIN32 -#include -#else -#include -#endif - LOG_CHANNEL(cellNetCtl); template <> @@ -254,39 +248,7 @@ error_code cellNetCtlGetInfo(s32 code, vm::ptr info) if (code == CELL_NET_CTL_INFO_ETHER_ADDR) { const auto& ether = nph.get_ether_addr(); - - // Check if MAC address is valid (non-zero) - if (ether[0] == 0 && ether[1] == 0 && ether[2] == 0 && - ether[3] == 0 && ether[4] == 0 && ether[5] == 0) - { - cellNetCtl.error("MAC address is all zeros - generating fallback"); - - // Generate a fallback locally-administered MAC based on a simple hash - char hostname[256] = {}; - if (gethostname(hostname, sizeof(hostname) - 1) != 0) - { - std::strcpy(hostname, "rpcs3"); - } - - u64 hash = 0; - for (const char* p = hostname; *p; ++p) - hash = hash * 31 + static_cast(*p); - - info->ether_addr.data[0] = 0x02; // Locally administered, unicast - info->ether_addr.data[1] = static_cast((hash >> 0) & 0xff); - info->ether_addr.data[2] = static_cast((hash >> 8) & 0xff); - info->ether_addr.data[3] = static_cast((hash >> 16) & 0xff); - info->ether_addr.data[4] = static_cast((hash >> 24) & 0xff); - info->ether_addr.data[5] = static_cast((hash >> 32) & 0xff); - - cellNetCtl.notice("Generated fallback MAC: %02x:%02x:%02x:%02x:%02x:%02x", - info->ether_addr.data[0], info->ether_addr.data[1], info->ether_addr.data[2], - info->ether_addr.data[3], info->ether_addr.data[4], info->ether_addr.data[5]); - } - else - { - memcpy(info->ether_addr.data, ether.data(), 6); - } + std::memcpy(info->ether_addr.data, ether.data(), 6); return CELL_OK; } diff --git a/rpcs3/Emu/NP/np_handler.cpp b/rpcs3/Emu/NP/np_handler.cpp index 5581f981b9..ed86fced33 100644 --- a/rpcs3/Emu/NP/np_handler.cpp +++ b/rpcs3/Emu/NP/np_handler.cpp @@ -20,16 +20,13 @@ #ifdef _WIN32 #include #include -#include #else #ifdef __clang__ #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wold-style-cast" #endif #include -#include #include -#include #include #include #include @@ -38,11 +35,6 @@ #endif #endif -#if defined(__FreeBSD__) || defined(__APPLE__) -#include -#include -#endif - #include "util/yaml.hpp" #include @@ -446,9 +438,9 @@ namespace np g_fxo->need>(); - // Always discover MAC address - games may query it regardless of network status - // A real PS3 always has a hardware MAC address - discover_ether_address(); + // Always initialize a stable emulated MAC address - games may query it regardless of network status. + // A real PS3 always has a hardware MAC address. + reset_ether_address(); is_connected = (g_cfg.net.net_active == np_internet_status::enabled); is_psn_active = (g_cfg.net.psn_status >= np_psn_status::psn_fake) && is_connected; @@ -501,6 +493,18 @@ namespace np ar(is_NP_Lookup_init, is_NP_Score_init, is_NP2_init, is_NP2_Match2_init, is_NP_Auth_init, manager_cb, manager_cb_arg, std::as_bytes(std::span(&basic_handler, 1)), is_connected, is_psn_active, hostname, ether_address, local_ip_addr, public_ip_addr, dns_ip); + const auto resolved_ether_address = resolve_ether_address(); + if (!is_valid_ether_addr(ether_address)) + { + nph_log.warning("Savestate contained an invalid Ethernet address %s; using %s instead.", ether_to_string(ether_address), ether_to_string(resolved_ether_address)); + ether_address = resolved_ether_address; + } + else if (ether_address != resolved_ether_address) + { + nph_log.notice("Savestate Ethernet address %s differs from the current emulated identity; using %s instead.", ether_to_string(ether_address), ether_to_string(resolved_ether_address)); + ether_address = resolved_ether_address; + } + // Call init func if needed (np_memory is unaffected when an empty pool is provided) init_NP(0, vm::null); @@ -613,158 +617,29 @@ namespace np return true; } - // Helper to check if MAC address is valid (non-zero, non-broadcast) - static bool is_valid_mac(const u8* mac) + std::array np_handler::resolve_ether_address() const { - // Check for all-zero MAC - if (mac[0] == 0 && mac[1] == 0 && mac[2] == 0 && mac[3] == 0 && mac[4] == 0 && mac[5] == 0) - return false; + const std::string override_value = g_cfg.net.ethernet_address.to_string(); + if (!override_value.empty()) + { + if (const auto parsed = string_to_ether_addr(override_value)) + { + nph_log.notice("Using configured emulated Ethernet address %s.", ether_to_string(*parsed)); + return *parsed; + } - // Check for broadcast MAC (ff:ff:ff:ff:ff:ff) - if (mac[0] == 0xff && mac[1] == 0xff && mac[2] == 0xff && mac[3] == 0xff && mac[4] == 0xff && mac[5] == 0xff) - return false; + nph_log.error("Configured Ethernet address '%s' is invalid; deriving an emulated address instead.", override_value); + } - return true; + auto resolved = generate_emulated_ether_addr(g_cfg.sys.console_psid.get(), Emu.GetUsrId()); + ensure(is_valid_ether_addr(resolved)); + nph_log.notice("Using derived emulated Ethernet address %s for user %08u.", ether_to_string(resolved), Emu.GetUsrId()); + return resolved; } - bool np_handler::discover_ether_address() + void np_handler::reset_ether_address() { - bool discovered = false; - -#if defined(__FreeBSD__) || defined(__APPLE__) - ifaddrs* ifap; - - if (getifaddrs(&ifap) == 0) - { - for (ifaddrs* p = ifap; p; p = p->ifa_next) - { - // Skip interfaces without addresses - if (!p->ifa_addr) - continue; - - // Skip loopback interfaces - if (p->ifa_flags & IFF_LOOPBACK) - continue; - - if (p->ifa_addr->sa_family == AF_LINK) - { - sockaddr_dl* sdp = reinterpret_cast(p->ifa_addr); - - // Validate hardware address length - if (sdp->sdl_alen < 6) - continue; - - const u8* mac = reinterpret_cast(sdp->sdl_data + sdp->sdl_nlen); - if (!is_valid_mac(mac)) - continue; - - memcpy(ether_address.data(), mac, 6); - nph_log.notice("Discovered Ethernet address %02x:%02x:%02x:%02x:%02x:%02x from interface %s", - ether_address[0], ether_address[1], ether_address[2], - ether_address[3], ether_address[4], ether_address[5], p->ifa_name); - discovered = true; - break; - } - } - freeifaddrs(ifap); - } -#elif defined(_WIN32) - std::vector adapter_infos(sizeof(IP_ADAPTER_INFO)); - ULONG size_infos = sizeof(IP_ADAPTER_INFO); - - if (GetAdaptersInfo(reinterpret_cast(adapter_infos.data()), &size_infos) == ERROR_BUFFER_OVERFLOW) - adapter_infos.resize(size_infos); - - if (GetAdaptersInfo(reinterpret_cast(adapter_infos.data()), &size_infos) == NO_ERROR && size_infos) - { - PIP_ADAPTER_INFO adapter = reinterpret_cast(adapter_infos.data()); - while (adapter) - { - if (adapter->AddressLength >= 6 && is_valid_mac(adapter->Address)) - { - memcpy(ether_address.data(), adapter->Address, 6); - nph_log.notice("Discovered Ethernet address %02x:%02x:%02x:%02x:%02x:%02x from adapter %s", - ether_address[0], ether_address[1], ether_address[2], - ether_address[3], ether_address[4], ether_address[5], adapter->AdapterName); - discovered = true; - break; - } - adapter = adapter->Next; - } - } -#else - ifreq ifr; - ifconf ifc; - char buf[1024]; - - int sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_IP); - if (sock != -1) - { - ifc.ifc_len = sizeof(buf); - ifc.ifc_buf = buf; - if (ioctl(sock, SIOCGIFCONF, &ifc) != -1) - { - ifreq* it = ifc.ifc_req; - const ifreq* const end = it + (ifc.ifc_len / sizeof(ifreq)); - - for (; it != end; ++it) - { - strcpy_trunc(ifr.ifr_name, it->ifr_name); - if (ioctl(sock, SIOCGIFFLAGS, &ifr) == 0) - { - if (!(ifr.ifr_flags & IFF_LOOPBACK)) - { - if (ioctl(sock, SIOCGIFHWADDR, &ifr) == 0) - { - const u8* mac = reinterpret_cast(ifr.ifr_hwaddr.sa_data); - if (is_valid_mac(mac)) - { - memcpy(ether_address.data(), mac, 6); - nph_log.notice("Discovered Ethernet address %02x:%02x:%02x:%02x:%02x:%02x from interface %s", - mac[0], mac[1], mac[2], mac[3], mac[4], mac[5], ifr.ifr_name); - discovered = true; - break; - } - } - } - } - } - } - close(sock); - } -#endif - - if (!discovered) - { - // Generate a fallback MAC address if discovery failed - // Use a locally administered, unicast MAC (02:xx:xx:xx:xx:xx) - // Based on a hash of the hostname to keep it consistent across runs - nph_log.warning("Failed to discover MAC address from network interfaces, generating fallback"); - - char host_buf[256] = {}; - if (gethostname(host_buf, sizeof(host_buf) - 1) != 0) - { - strcpy_trunc(host_buf, "rpcs3-fallback"); - } - - u64 hash = 0; - for (const char* p = host_buf; *p; ++p) - hash = hash * 31 + static_cast(*p); - - // Set locally administered bit (bit 1 of first octet) and clear multicast bit (bit 0) - ether_address[0] = 0x02; // Locally administered, unicast - ether_address[1] = static_cast((hash >> 0) & 0xff); - ether_address[2] = static_cast((hash >> 8) & 0xff); - ether_address[3] = static_cast((hash >> 16) & 0xff); - ether_address[4] = static_cast((hash >> 24) & 0xff); - ether_address[5] = static_cast((hash >> 32) & 0xff); - - nph_log.notice("Generated fallback Ethernet address %02x:%02x:%02x:%02x:%02x:%02x", - ether_address[0], ether_address[1], ether_address[2], - ether_address[3], ether_address[4], ether_address[5]); - } - - return true; // Always succeed - we have a fallback + ether_address = resolve_ether_address(); } const std::array& np_handler::get_ether_addr() const diff --git a/rpcs3/Emu/NP/np_handler.h b/rpcs3/Emu/NP/np_handler.h index 7e43bbf3cd..1d8924a6f5 100644 --- a/rpcs3/Emu/NP/np_handler.h +++ b/rpcs3/Emu/NP/np_handler.h @@ -296,7 +296,8 @@ namespace np private: // Various generic helpers bool discover_ip_address(); - bool discover_ether_address(); + std::array resolve_ether_address() const; + void reset_ether_address(); bool error_and_disconnect(const std::string& error_msg); // Notification handlers diff --git a/rpcs3/Emu/NP/np_helpers.cpp b/rpcs3/Emu/NP/np_helpers.cpp index 9d651693d9..09da74b631 100644 --- a/rpcs3/Emu/NP/np_helpers.cpp +++ b/rpcs3/Emu/NP/np_helpers.cpp @@ -1,8 +1,6 @@ -#include "Emu/Cell/Modules/sceNp.h" #include "stdafx.h" -#include "util/types.hpp" +#include "np_helpers.h" #include "Utilities/StrUtil.h" -#include "rpcn_client.h" #ifdef _WIN32 #include @@ -25,6 +23,133 @@ namespace np return fmt::format("%02X:%02X:%02X:%02X:%02X:%02X", ether[0], ether[1], ether[2], ether[3], ether[4], ether[5]); } + bool is_valid_ether_addr(const std::array& ether) + { + return std::any_of(ether.begin(), ether.end(), [](u8 value) { return value != 0; }) && + std::any_of(ether.begin(), ether.end(), [](u8 value) { return value != 0xff; }) && + (ether[0] & 0x01) == 0; + } + + namespace + { + std::optional hex_pair_to_byte(char high, char low) + { + const auto hex_to_nibble = [](char ch) -> std::optional + { + if (ch >= '0' && ch <= '9') + { + return static_cast(ch - '0'); + } + + if (ch >= 'A' && ch <= 'F') + { + return static_cast(ch - 'A' + 10); + } + + if (ch >= 'a' && ch <= 'f') + { + return static_cast(ch - 'a' + 10); + } + + return std::nullopt; + }; + + const auto high_nibble = hex_to_nibble(high); + const auto low_nibble = hex_to_nibble(low); + + if (!high_nibble || !low_nibble) + { + return std::nullopt; + } + + return static_cast((*high_nibble << 4) | *low_nibble); + } + + u64 fnv1a64_append(u64 hash, u64 value) + { + constexpr u64 fnv_prime = 1099511628211ull; + + for (usz i = 0; i < sizeof(value); i++) + { + hash ^= static_cast(value >> (i * 8)); + hash *= fnv_prime; + } + + return hash; + } + + u64 fnv1a64_append(u64 hash, std::string_view value) + { + constexpr u64 fnv_prime = 1099511628211ull; + + for (char ch : value) + { + hash ^= static_cast(ch); + hash *= fnv_prime; + } + + return hash; + } + } + + std::optional> string_to_ether_addr(std::string_view str) + { + if (str.size() != 17) + { + return std::nullopt; + } + + std::array ether{}; + + for (usz i = 0; i < ether.size(); i++) + { + const usz pos = i * 3; + + if (i != 5 && str[pos + 2] != ':') + { + return std::nullopt; + } + + const auto byte = hex_pair_to_byte(str[pos], str[pos + 1]); + + if (!byte) + { + return std::nullopt; + } + + ether[i] = *byte; + } + + if (!is_valid_ether_addr(ether)) + { + return std::nullopt; + } + + return ether; + } + + std::array generate_emulated_ether_addr(u128 console_psid, u32 user_id) + { + constexpr u64 fnv_offset_basis = 14695981039346656037ull; + + u64 hash = fnv_offset_basis; + hash = fnv1a64_append(hash, "rpcs3-emulated-ethernet-v1"); + hash = fnv1a64_append(hash, static_cast(console_psid)); + hash = fnv1a64_append(hash, static_cast(console_psid >> 64)); + hash = fnv1a64_append(hash, user_id); + + std::array ether{}; + for (usz i = 0; i < ether.size(); i++) + { + ether[i] = static_cast(hash >> (i * 8)); + } + + ether[0] &= 0xfe; // Unicast. + ether[0] |= 0x02; // Locally administered. + + return ether; + } + bool validate_communication_id(const SceNpCommunicationId& com_id) { return std::all_of(com_id.data, com_id.data + 9, [](char c) { return (c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z'); }) && com_id.num <= 99; diff --git a/rpcs3/Emu/NP/np_helpers.h b/rpcs3/Emu/NP/np_helpers.h index d9e2a9d076..64b38fdb64 100644 --- a/rpcs3/Emu/NP/np_helpers.h +++ b/rpcs3/Emu/NP/np_helpers.h @@ -2,12 +2,20 @@ #include "util/types.hpp" #include "Emu/Cell/Modules/sceNp.h" + +#include +#include +#include +#include #include "rpcn_client.h" namespace np { std::string ip_to_string(u32 addr); std::string ether_to_string(const std::array& ether); + bool is_valid_ether_addr(const std::array& ether); + std::optional> string_to_ether_addr(std::string_view str); + std::array generate_emulated_ether_addr(u128 console_psid, u32 user_id); bool validate_communication_id(const SceNpCommunicationId& com_id); std::string communication_id_to_string(const SceNpCommunicationId& communicationId); std::optional string_to_communication_id(std::string_view str); diff --git a/rpcs3/Emu/system_config.h b/rpcs3/Emu/system_config.h index 1272896f75..1396df9b00 100644 --- a/rpcs3/Emu/system_config.h +++ b/rpcs3/Emu/system_config.h @@ -320,6 +320,7 @@ struct cfg_root : cfg::node cfg::_enum net_active{this, "Internet enabled", np_internet_status::disabled}; cfg::string ip_address{this, "IP address", "0.0.0.0"}; cfg::string bind_address{this, "Bind address", "0.0.0.0"}; + cfg::string ethernet_address{this, "Ethernet address", "", false}; cfg::string dns{this, "DNS address", "8.8.8.8"}; cfg::string swap_list{this, "IP swap list", ""}; cfg::_bool upnp_enabled{this, "UPNP Enabled", false}; diff --git a/rpcs3/tests/rpcs3_test.vcxproj b/rpcs3/tests/rpcs3_test.vcxproj index a60b150469..a641fb366d 100644 --- a/rpcs3/tests/rpcs3_test.vcxproj +++ b/rpcs3/tests/rpcs3_test.vcxproj @@ -101,6 +101,7 @@ + @@ -115,4 +116,4 @@ - \ No newline at end of file + diff --git a/rpcs3/tests/test_np_helpers.cpp b/rpcs3/tests/test_np_helpers.cpp new file mode 100644 index 0000000000..2b030b3640 --- /dev/null +++ b/rpcs3/tests/test_np_helpers.cpp @@ -0,0 +1,51 @@ +#include +#include "Emu/NP/np_helpers.h" + +namespace np +{ + TEST(NpHelpers, ParsesCanonicalEtherAddress) + { + const auto ether = string_to_ether_addr("02:00:00:00:00:01"); + + ASSERT_TRUE(ether); + EXPECT_EQ((std::array{0x02, 0x00, 0x00, 0x00, 0x00, 0x01}), *ether); + EXPECT_TRUE(is_valid_ether_addr(*ether)); + EXPECT_EQ("02:00:00:00:00:01", ether_to_string(*ether)); + } + + TEST(NpHelpers, RejectsInvalidEtherAddresses) + { + EXPECT_FALSE(string_to_ether_addr("00:00:00:00:00:00")); + EXPECT_FALSE(string_to_ether_addr("ff:ff:ff:ff:ff:ff")); + EXPECT_FALSE(string_to_ether_addr("01:00:00:00:00:00")); + EXPECT_FALSE(string_to_ether_addr("03:00:00:00:00:00")); + EXPECT_FALSE(string_to_ether_addr("02:00:00:00:00")); + EXPECT_FALSE(string_to_ether_addr("02:00:00:00:00:001")); + EXPECT_FALSE(string_to_ether_addr("2:00:00:00:00:01")); + EXPECT_FALSE(string_to_ether_addr("02-00-00-00-00-01")); + EXPECT_FALSE(string_to_ether_addr("02:00:00:00:00:0g")); + EXPECT_FALSE(string_to_ether_addr(" 02:00:00:00:00:01")); + } + + TEST(NpHelpers, GeneratedEtherAddressIsValidDeterministicAndLocallyAdministered) + { + const u128 psid = (u128{0x0123456789abcdefull} << 64) | u128{0xfedcba9876543210ull}; + + const auto ether1 = generate_emulated_ether_addr(psid, 1); + const auto ether2 = generate_emulated_ether_addr(psid, 1); + + EXPECT_EQ(ether1, ether2); + EXPECT_TRUE(is_valid_ether_addr(ether1)); + EXPECT_EQ(0x00, ether1[0] & 0x01); // Unicast. + EXPECT_EQ(0x02, ether1[0] & 0x02); // Locally administered. + } + + TEST(NpHelpers, GeneratedEtherAddressChangesWithSeedInputs) + { + const u128 psid1 = (u128{0x0123456789abcdefull} << 64) | u128{0xfedcba9876543210ull}; + const u128 psid2 = (u128{0x1123456789abcdefull} << 64) | u128{0xfedcba9876543210ull}; + + EXPECT_NE(generate_emulated_ether_addr(psid1, 1), generate_emulated_ether_addr(psid1, 2)); + EXPECT_NE(generate_emulated_ether_addr(psid1, 1), generate_emulated_ether_addr(psid2, 1)); + } +}