From 31d44f85a7cee0e2e8bdade041a4a621aff23971 Mon Sep 17 00:00:00 2001 From: Alibek Omarov Date: Wed, 29 Jul 2026 03:08:23 +0500 Subject: [PATCH] engine: implement reading hardware serial ID on Mac, implement reading MAC addresses on *BSDs, refactoring --- engine/client/identification.c | 204 ++++++++++++++++++------------- engine/platform/apple/id_apple.c | 50 ++++++++ engine/platform/platform.h | 6 + engine/platform/posix/id_posix.c | 141 +++++++++++++++++++++ engine/wscript | 16 ++- 5 files changed, 324 insertions(+), 93 deletions(-) create mode 100644 engine/platform/apple/id_apple.c create mode 100644 engine/platform/posix/id_posix.c diff --git a/engine/client/identification.c b/engine/client/identification.c index b492f065..44a8dfed 100644 --- a/engine/client/identification.c +++ b/engine/client/identification.c @@ -15,14 +15,13 @@ GNU General Public License for more details. #include "build.h" #include +#if XASH_LINUX #include -#if !XASH_WIN32 #include -#else -#include #endif #include "common.h" #include "client.h" +#include "platform/platform.h" /* ========================================================== @@ -97,8 +96,6 @@ IDENTIFICATION #define MAXBITS_GEN 30 #define MAXBITS_CHECK MAXBITS_GEN + 6 -static qboolean ID_ProcessFile( bloomfilter_t *value, const char *path ); - static void ID_BloomFilter_f( void ) { bloomfilter_t value = 0; @@ -203,84 +200,6 @@ static qboolean ID_ProcessCPUInfo( bloomfilter_t *value ) return true; } -static qboolean ID_ValidateNetDevice( const char *dev ) -{ - const char *prefix = "/sys/class/net"; - - // These devices are fake, their mac address is generated each boot, while assign_type is 0 - if( !Q_strnicmp( dev, "ccmni", sizeof( "ccmni" ) ) || - !Q_strnicmp( dev, "ifb", sizeof( "ifb" ) ) ) - return false; - - byte *pfile = FS_LoadDirectFile( va( "%s/%s/addr_assign_type", prefix, dev ), NULL ); - - // if NULL, it may be old kernel - if( pfile ) - { - int assignType = Q_atoi( (char*)pfile ); - - Mem_Free( pfile ); - - // check is MAC address is constant - if( assignType != 0 ) - return false; - } - - return true; -} - -static int ID_ProcessNetDevices( bloomfilter_t *value ) -{ - const char *prefix = "/sys/class/net"; - DIR *dir; - struct dirent *entry; - int count = 0; - - if( !( dir = opendir( prefix ) ) ) - return 0; - - while( ( entry = readdir( dir ) ) && BloomFilter_Weight( *value ) < MAXBITS_GEN ) - { - if( !Q_strcmp( entry->d_name, "." ) || !Q_strcmp( entry->d_name, ".." ) ) - continue; - - if( !ID_ValidateNetDevice( entry->d_name ) ) - continue; - - count += ID_ProcessFile( value, va( "%s/%s/address", prefix, entry->d_name ) ); - } - closedir( dir ); - return count; -} - -static int ID_CheckNetDevices( bloomfilter_t value ) -{ - const char *prefix = "/sys/class/net"; - - DIR *dir; - struct dirent *entry; - int count = 0; - bloomfilter_t filter = 0; - - if( !( dir = opendir( prefix ) ) ) - return 0; - - while( ( entry = readdir( dir ) ) ) - { - if( !Q_strcmp( entry->d_name, "." ) || !Q_strcmp( entry->d_name, ".." ) ) - continue; - - if( !ID_ValidateNetDevice( entry->d_name ) ) - continue; - - if( ID_ProcessFile( &filter, va( "%s/%s/address", prefix, entry->d_name ) ) ) - count += ( value & filter ) == filter, filter = 0; - } - - closedir( dir ); - return count; -} - static void ID_TestCPUInfo_f( void ) { bloomfilter_t value = 0; @@ -291,8 +210,6 @@ static void ID_TestCPUInfo_f( void ) Msg( "Could not get serial\n" ); } -#endif - static qboolean ID_ProcessFile( bloomfilter_t *value, const char *path ) { int fd = open( path, O_RDONLY ); @@ -322,7 +239,6 @@ static qboolean ID_ProcessFile( bloomfilter_t *value, const char *path ) return true; } -#if !XASH_WIN32 static int ID_ProcessFiles( bloomfilter_t *value, const char *prefix, const char *postfix ) { DIR *dir; @@ -365,7 +281,94 @@ static int ID_CheckFiles( bloomfilter_t value, const char *prefix, const char *p closedir( dir ); return count; } -#else +#endif // XASH_LINUX + +#if XASH_POSIX +#define MAX_NETDEVICES 16 + +static qboolean ID_IsReservedMAC( uint64_t mac ) +{ + byte first = ( mac >> 40 ) & 0xff; + + // no address at all + if( mac == 0 ) + return true; + + // group and local bits of the first octet, see https://www.rfc-editor.org/rfc/rfc9542#section-2.1.1 + if( FBitSet( first, 0x01 ) || FBitSet( first, 0x02 )) + return true; + + // IANA OUI, see https://www.iana.org/assignments/ethernet-numbers/ethernet-numbers.xhtml and https://www.rfc-editor.org/rfc/rfc9568#section-7.3 + if(( mac >> 24 ) == 0x00005e ) + return true; + + return false; +} + +static void ID_FormatMAC( char *out, size_t size, uint64_t mac ) +{ + // format exactly like /sys/class/net/ */address contents, so ids generated by older versions stay valid + Q_snprintf( out, size, "%02x:%02x:%02x:%02x:%02x:%02x\n", + (uint)(( mac >> 40 ) & 0xffu ), + (uint)(( mac >> 32 ) & 0xffu ), + (uint)(( mac >> 24 ) & 0xffu ), + (uint)(( mac >> 16 ) & 0xffu ), + (uint)(( mac >> 8 ) & 0xffu ), + (uint)( mac & 0xffu )); +} + +static int ID_ProcessNetDevices( bloomfilter_t *value ) +{ + uint64_t macs[MAX_NETDEVICES]; + int total = Posix_GetNetDeviceAddresses( macs, MAX_NETDEVICES ); + int count = 0; + + for( int i = 0; i < total && BloomFilter_Weight( *value ) < MAXBITS_GEN; i++ ) + { + char buf[32]; + + if( ID_IsReservedMAC( macs[i] )) + continue; + + ID_FormatMAC( buf, sizeof( buf ), macs[i] ); + + if( !ID_VerifyHEX( buf )) + continue; + + *value |= BloomFilter_ProcessStr( buf ); + count++; + } + + return count; +} + +static int ID_CheckNetDevices( bloomfilter_t value ) +{ + uint64_t macs[MAX_NETDEVICES]; + int total = Posix_GetNetDeviceAddresses( macs, MAX_NETDEVICES ); + int count = 0; + + for( int i = 0; i < total; i++ ) + { + char buf[32]; + + if( ID_IsReservedMAC( macs[i] )) + continue; + + ID_FormatMAC( buf, sizeof( buf ), macs[i] ); + + if( !ID_VerifyHEX( buf )) + continue; + + bloomfilter_t filter = BloomFilter_ProcessStr( buf ); + count += ( value & filter ) == filter; + } + + return count; +} +#endif // XASH_POSIX + +#if XASH_WIN32 static int ID_GetKeyData( HKEY hRootKey, char *subKey, char *value, LPBYTE data, DWORD cbData ) { HKEY hKey; @@ -539,6 +542,17 @@ static bloomfilter_t ID_GenerateRawId( void ) count += ID_ProcessCPUInfo( &value ); count += ID_ProcessFiles( &value, "/sys/block", "device/cid" ); +#endif +#if XASH_APPLE && !XASH_IOS + char buf[64]; + + if( Apple_GetSerialNumber( buf, sizeof( buf ))) + { + value |= BloomFilter_ProcessStr( buf ); + count++; + } +#endif +#if XASH_POSIX count += ID_ProcessNetDevices( &value ); #endif #if XASH_WIN32 @@ -587,12 +601,26 @@ static uint ID_CheckRawId( bloomfilter_t filter ) } #endif // !XASH_ANDROID - count += ID_CheckNetDevices( filter ); count += ID_CheckFiles( filter, "/sys/block", "device/cid" ); if( ID_ProcessCPUInfo( &value ) ) count += (filter & value) == value; #endif +#if XASH_APPLE && !XASH_IOS + char buf[64]; + + if( Apple_GetSerialNumber( buf, sizeof( buf ))) + { + value = BloomFilter_ProcessStr( buf ); + count += (filter & value) == value; + value = 0; + } +#endif + +#if XASH_POSIX + count += ID_CheckNetDevices( filter ); +#endif + #if XASH_WIN32 count += ID_CheckWMIC( filter, L"wmic path win32_physicalmedia get SerialNumber" ); count += ID_CheckWMIC( filter, L"wmic bios get serialnumber" ); diff --git a/engine/platform/apple/id_apple.c b/engine/platform/apple/id_apple.c new file mode 100644 index 00000000..3c9ad060 --- /dev/null +++ b/engine/platform/apple/id_apple.c @@ -0,0 +1,50 @@ +/* +id_apple.c - macOS hardware serial id source +Copyright (C) 2026 Xash3D FWGS Contributors + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. +*/ + +#include "platform/platform.h" +#include +#include +#include + +// kIOMasterPortDefault was renamed to kIOMainPortDefault in macOS 12 SDK +#if MAC_OS_X_VERSION_MIN_REQUIRED < 120000 +#define kIOMainPortDefault kIOMasterPortDefault +#endif + +static qboolean Apple_GetPlatformExpertProperty( CFStringRef key, char *out, size_t size ) +{ + io_service_t service = IOServiceGetMatchingService( kIOMainPortDefault, IOServiceMatching( "IOPlatformExpertDevice" )); + + if( !service ) + return false; + + qboolean ret = false; + CFTypeRef prop = IORegistryEntryCreateCFProperty( service, key, kCFAllocatorDefault, 0 ); + + if( prop ) + { + if( CFGetTypeID( prop ) == CFStringGetTypeID() && CFStringGetCString( (CFStringRef)prop, out, size, kCFStringEncodingUTF8 )) + ret = true; + CFRelease( prop ); + } + + IOObjectRelease( service ); + return ret; +} + +qboolean Apple_GetSerialNumber( char *out, size_t size ) +{ + return Apple_GetPlatformExpertProperty( CFSTR( "IOPlatformSerialNumber" ), out, size ); +} diff --git a/engine/platform/platform.h b/engine/platform/platform.h index 19829f4c..369bc5e5 100644 --- a/engine/platform/platform.h +++ b/engine/platform/platform.h @@ -69,6 +69,12 @@ void IOS_LaunchDialog( void ); void Posix_Daemonize( void ); void Posix_SetupSigtermHandling( void ); char *Posix_Input( void ); +// returns the number of stable network device MAC addresses, each packed into low 48 bits +int Posix_GetNetDeviceAddresses( uint64_t *addresses, int max ); +#endif + +#if XASH_APPLE && !XASH_IOS +qboolean Apple_GetSerialNumber( char *out, size_t size ); #endif #if XASH_SDL diff --git a/engine/platform/posix/id_posix.c b/engine/platform/posix/id_posix.c new file mode 100644 index 00000000..b8cfdac0 --- /dev/null +++ b/engine/platform/posix/id_posix.c @@ -0,0 +1,141 @@ +/* +id_posix.c - network device enumeration for unique id generation +Copyright (C) 2026 Xash3D FWGS contributors + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. +*/ + +#include "platform/platform.h" + +#if XASH_LINUX +#include +#include +#include + +static qboolean Posix_ValidateNetDevice( const char *dev ) +{ + const char *prefix = "/sys/class/net"; + + // These devices are fake, their mac address is generated each boot, while assign_type is 0 + if( !Q_strnicmp( dev, "ccmni", sizeof( "ccmni" ) - 1 ) || !Q_strnicmp( dev, "ifb", sizeof( "ifb" ) - 1 )) + return false; + + byte *pfile = FS_LoadDirectFile( va( "%s/%s/addr_assign_type", prefix, dev ), NULL ); + + // if NULL, it may be old kernel + if( pfile ) + { + int assignType = Q_atoi( (char*)pfile ); + + Mem_Free( pfile ); + + // check is MAC address is constant + if( assignType != 0 ) + return false; + } + + return true; +} + +int Posix_GetNetDeviceAddresses( uint64_t *addresses, int max ) +{ + const char *prefix = "/sys/class/net"; + DIR *dir = opendir( prefix ); + struct dirent *entry; + int count = 0; + + if( !dir ) + return 0; + + while(( entry = readdir( dir )) && count < max ) + { + if( !Q_strcmp( entry->d_name, "." ) || !Q_strcmp( entry->d_name, ".." )) + continue; + + if( !Posix_ValidateNetDevice( entry->d_name )) + continue; + + int fd = open( va( "%s/%s/address", prefix, entry->d_name ), O_RDONLY ); + + if( fd < 0 ) + continue; + + char buffer[64]; + int ret = read( fd, buffer, sizeof( buffer ) - 1 ); + + close( fd ); + + if( ret <= 0 ) + continue; + + buffer[ret] = 0; + + uint mac[6]; + char term = 0; + + if( sscanf( buffer, "%02x:%02x:%02x:%02x:%02x:%02x%c", &mac[0], &mac[1], &mac[2], &mac[3], &mac[4], &mac[5], &term ) < 6 ) + continue; + + // skip malformed or long address devices, we only need MAC addresses and they're always 48-bit + if( term != '\n' && term != '\0' ) + continue; + + addresses[count] = ((uint64_t)mac[0] << 40) | ((uint64_t)mac[1] << 32) | ((uint64_t)mac[2] << 24) | ((uint64_t)mac[3] << 16) | ((uint64_t)mac[4] << 8) | (uint64_t)mac[5]; + count++; + } + + closedir( dir ); + return count; +} +#elif XASH_APPLE || XASH_FREEBSD || XASH_NETBSD || XASH_OPENBSD +#include +#include +#include +#include +#include + +int Posix_GetNetDeviceAddresses( uint64_t *addresses, int max ) +{ + struct ifaddrs *ifaddr; + int count = 0; + + if( getifaddrs( &ifaddr ) < 0 ) + return 0; + + for( struct ifaddrs *ifa = ifaddr; ifa != NULL && count < max; ifa = ifa->ifa_next ) + { + if( !ifa->ifa_addr || ifa->ifa_addr->sa_family != AF_LINK ) + continue; + + if( FBitSet( ifa->ifa_flags, IFF_LOOPBACK | IFF_POINTOPOINT )) + continue; + + const struct sockaddr_dl *sdl = (const struct sockaddr_dl *)ifa->ifa_addr; + + if( sdl->sdl_alen != 6 ) + continue; + + const byte *mac = (const byte *)LLADDR( sdl ); + + addresses[count] = ((uint64_t)mac[0] << 40) | ((uint64_t)mac[1] << 32) | ((uint64_t)mac[2] << 24) | ((uint64_t)mac[3] << 16) | ((uint64_t)mac[4] << 8) | (uint64_t)mac[5]; + count++; + } + + freeifaddrs( ifaddr ); + return count; +} +#else +int Posix_GetNetDeviceAddresses( uint64_t *addresses, int max ) +{ + Con_Reportf( S_WARN "%s: implement me!\n", __func__ ); + return 0; +} +#endif diff --git a/engine/wscript b/engine/wscript index feaff82e..a3ac9437 100644 --- a/engine/wscript +++ b/engine/wscript @@ -26,7 +26,12 @@ int main(int argc, char **argv) } ''' -frameworks = ['Foundation', 'UIKit', 'QuartzCore', 'GameController', 'SystemConfiguration', 'CFNetwork', 'AVFoundation', 'CoreGraphics'] +def get_frameworks(env): + if env.IOS: + return ['Foundation', 'UIKit', 'QuartzCore', 'GameController', 'SystemConfiguration', 'CFNetwork', 'AVFoundation', 'CoreGraphics'] + if env.DEST_OS == 'darwin': + return ['IOKit', 'CoreFoundation'] + return [] @TaskGen.extension('.m') def m_hook(self, node): @@ -124,9 +129,8 @@ def configure(conf): if not conf.env.DEST_OS in ['win32', 'android']: conf.check_pthreads(mode='c') - if conf.env.IOS: - for i in frameworks : - conf.check(features='c cprogram', framework=i, uselib_store=i, msg='Checking for %s framework' % i) + for i in get_frameworks(conf.env): + conf.check(features='c cprogram', framework=i, uselib_store=i, msg='Checking for %s framework' % i) conf.define('ENGINE_DLL', 1) @@ -179,6 +183,8 @@ def build(bld): source += bld.path.ant_glob('platform/ios/*.c') source += bld.path.ant_glob('platform/ios/*.m') includes += bld.path.ant_glob('platform/ios') + elif bld.env.DEST_OS == 'darwin': + source += bld.path.ant_glob('platform/apple/*.c') # include sources for optional features if bld.get_define('XASH_CUSTOM_SWAP'): @@ -206,6 +212,7 @@ def build(bld): ] else: # POSIX libs += ['M', 'RT', 'PTHREAD', 'ASOUND', 'HAIKU', 'MAGX', 'LOG', 'SOCKET'] + libs += get_frameworks(bld.env) if not bld.env.STATIC: libs += ['DL'] @@ -274,7 +281,6 @@ def build(bld): install_path = bld.env.LIBDIR if bld.env.IOS: - libs += frameworks defines += ['XASH_SDLMAIN=1'] if bld.env.DEST_OS in ['nswitch', 'psvita', 'emscripten']: