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.
This commit is contained in:
Frederico Luz 2026-05-08 16:32:40 +01:00
parent 4e1cedcb0f
commit 97adb673e8
10 changed files with 227 additions and 203 deletions

View File

@ -27,7 +27,6 @@
<Link>
<AdditionalDependencies>
ws2_32.lib;
Iphlpapi.lib;
Bcrypt.lib;
avcodec.lib;
avformat.lib;

View File

@ -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

View File

@ -12,12 +12,6 @@
#include "Emu/NP/np_handler.h"
#include "Emu/NP/np_helpers.h"
#ifdef _WIN32
#include <winsock2.h>
#else
#include <unistd.h>
#endif
LOG_CHANNEL(cellNetCtl);
template <>
@ -254,39 +248,7 @@ error_code cellNetCtlGetInfo(s32 code, vm::ptr<CellNetCtlInfo> 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<u8>(*p);
info->ether_addr.data[0] = 0x02; // Locally administered, unicast
info->ether_addr.data[1] = static_cast<u8>((hash >> 0) & 0xff);
info->ether_addr.data[2] = static_cast<u8>((hash >> 8) & 0xff);
info->ether_addr.data[3] = static_cast<u8>((hash >> 16) & 0xff);
info->ether_addr.data[4] = static_cast<u8>((hash >> 24) & 0xff);
info->ether_addr.data[5] = static_cast<u8>((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;
}

View File

@ -20,16 +20,13 @@
#ifdef _WIN32
#include <winsock2.h>
#include <WS2tcpip.h>
#include <iphlpapi.h>
#else
#ifdef __clang__
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wold-style-cast"
#endif
#include <sys/socket.h>
#include <sys/ioctl.h>
#include <netinet/in.h>
#include <net/if.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <unistd.h>
@ -38,11 +35,6 @@
#endif
#endif
#if defined(__FreeBSD__) || defined(__APPLE__)
#include <ifaddrs.h>
#include <net/if_dl.h>
#endif
#include "util/yaml.hpp"
#include <span>
@ -446,9 +438,9 @@ namespace np
g_fxo->need<named_thread<signaling_handler>>();
// 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<u8, 6> 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<sockaddr_dl*>(p->ifa_addr);
// Validate hardware address length
if (sdp->sdl_alen < 6)
continue;
const u8* mac = reinterpret_cast<const u8*>(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<u8> adapter_infos(sizeof(IP_ADAPTER_INFO));
ULONG size_infos = sizeof(IP_ADAPTER_INFO);
if (GetAdaptersInfo(reinterpret_cast<PIP_ADAPTER_INFO>(adapter_infos.data()), &size_infos) == ERROR_BUFFER_OVERFLOW)
adapter_infos.resize(size_infos);
if (GetAdaptersInfo(reinterpret_cast<PIP_ADAPTER_INFO>(adapter_infos.data()), &size_infos) == NO_ERROR && size_infos)
{
PIP_ADAPTER_INFO adapter = reinterpret_cast<PIP_ADAPTER_INFO>(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<const u8*>(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<u8>(*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<u8>((hash >> 0) & 0xff);
ether_address[2] = static_cast<u8>((hash >> 8) & 0xff);
ether_address[3] = static_cast<u8>((hash >> 16) & 0xff);
ether_address[4] = static_cast<u8>((hash >> 24) & 0xff);
ether_address[5] = static_cast<u8>((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<u8, 6>& np_handler::get_ether_addr() const

View File

@ -296,7 +296,8 @@ namespace np
private:
// Various generic helpers
bool discover_ip_address();
bool discover_ether_address();
std::array<u8, 6> resolve_ether_address() const;
void reset_ether_address();
bool error_and_disconnect(const std::string& error_msg);
// Notification handlers

View File

@ -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 <WS2tcpip.h>
@ -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<u8, 6>& 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<u8> hex_pair_to_byte(char high, char low)
{
const auto hex_to_nibble = [](char ch) -> std::optional<u8>
{
if (ch >= '0' && ch <= '9')
{
return static_cast<u8>(ch - '0');
}
if (ch >= 'A' && ch <= 'F')
{
return static_cast<u8>(ch - 'A' + 10);
}
if (ch >= 'a' && ch <= 'f')
{
return static_cast<u8>(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<u8>((*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<u8>(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<u8>(ch);
hash *= fnv_prime;
}
return hash;
}
}
std::optional<std::array<u8, 6>> string_to_ether_addr(std::string_view str)
{
if (str.size() != 17)
{
return std::nullopt;
}
std::array<u8, 6> 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<u8, 6> 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<u64>(console_psid));
hash = fnv1a64_append(hash, static_cast<u64>(console_psid >> 64));
hash = fnv1a64_append(hash, user_id);
std::array<u8, 6> ether{};
for (usz i = 0; i < ether.size(); i++)
{
ether[i] = static_cast<u8>(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;

View File

@ -2,12 +2,20 @@
#include "util/types.hpp"
#include "Emu/Cell/Modules/sceNp.h"
#include <array>
#include <optional>
#include <string>
#include <string_view>
#include "rpcn_client.h"
namespace np
{
std::string ip_to_string(u32 addr);
std::string ether_to_string(const std::array<u8, 6>& ether);
bool is_valid_ether_addr(const std::array<u8, 6>& ether);
std::optional<std::array<u8, 6>> string_to_ether_addr(std::string_view str);
std::array<u8, 6> 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<SceNpCommunicationId> string_to_communication_id(std::string_view str);

View File

@ -320,6 +320,7 @@ struct cfg_root : cfg::node
cfg::_enum<np_internet_status> 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};

View File

@ -101,6 +101,7 @@
<ClCompile Include="test_simple_array.cpp" />
<ClCompile Include="test_address_range.cpp" />
<ClCompile Include="test_sys_fs.cpp" />
<ClCompile Include="test_np_helpers.cpp" />
<ClCompile Include="test_tuple.cpp" />
<ClCompile Include="test_pair.cpp" />
</ItemGroup>
@ -115,4 +116,4 @@
</PropertyGroup>
<Warning Condition="!Exists('$(GTestPath)')" Text="$([System.String]::Format('$(ErrorText)', '$(GTestPath)'))" />
</Target>
</Project>
</Project>

View File

@ -0,0 +1,51 @@
#include <gtest/gtest.h>
#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<u8, 6>{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));
}
}