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));
+ }
+}