master ohmu/fastfetch-openbsd / src / detection / localip / localip_apple.c
ohmu  ·  2026-10-15
  1#include "localip.h"
  2#include "common/apple/cf_helpers.h"
  3#include "common/debug.h"
  4
  5#include <IOKit/IOBSD.h>
  6#include <IOKit/IOKitLib.h>
  7#include <IOKit/network/IONetworkController.h>
  8#include <ifaddrs.h>
  9#include <net/if.h>
 10
 11// macOS 27 hides the link-layer address from every process that is not an Apple platform
 12// binary, i.e. anything whose code signature has no `Platform identifier` field -- which
 13// includes ad-hoc and linker-signed builds. Both getifaddrs() and sysctl(NET_RT_IFLIST)
 14// then report the placeholder 02:00:00:00:00:00 for all interfaces. Re-signing the very
 15// same /sbin/ifconfig ad-hoc flips its `ether` line from the real address to the
 16// placeholder while every other field stays byte-identical, so the placeholder itself is
 17// the tell.
 18//
 19// The IORegistry is not affected. The node matching the BSD name never carries the
 20// property itself and the depth at which it appears depends on the driver -- en0 has it on
 21// the parent (IO80211Controller), en4 on the parent (AppleUSBDeviceNCM11Data), ap1 on the
 22// node itself -- so walk up until it shows up.
 23//
 24// This reports the *hardware* address. That differs from the address a platform binary
 25// sees whenever Private Wi-Fi Address is enabled (for en0 the randomized address sits one
 26// level further up, on IOUserNetworkWLAN), so it is only used to replace the placeholder
 27// and never preferred over a value read from the interface.
 28//
 29// Measured with `fastfetch -s localip --stat` on this machine: replacing the placeholders
 30// costs ~0.2ms for a single interface and ~1.3ms for the 21 interfaces getifaddrs()
 31// reports. The SystemConfiguration alternative needed a single SCNetworkInterfaceCopyAll()
 32// at 2.6-5.2ms (an XPC round trip to configd) and does not even list anpi0-2 or ap1, so the
 33// IORegistry covers strictly more interfaces for less.
 34#define FF_LOCALIP_REDACTED_MAC "02:00:00:00:00:00"
 35
 36// The classes that expose a `BSD Name` in the IORegistry. anpi*/en* are IONetworkInterface
 37// subclasses, but the dext-based Wi-Fi interfaces (ap1) are IOUserNetworkWLAN and are not,
 38// so they need a pass of their own.
 39static const char* const iokitInterfaceClasses[] = {
 40    "IONetworkInterface",
 41    "IOUserNetworkWLAN",
 42};
 43
 44// Looks for the first IOMACAddress on `entry` or one of its ancestors. Does not consume it.
 45static bool getMacFromRegistry(io_object_t entry, FFstrbuf* result) {
 46    if (!entry) {
 47        return false;
 48    }
 49
 50    FF_IOOBJECT_AUTO_RELEASE io_object_t current = entry;
 51    IOObjectRetain(current);
 52
 53    for (uint32_t depth = 0; depth < 4; ++depth) {
 54        FF_CFTYPE_AUTO_RELEASE CFDataRef data = (CFDataRef) IORegistryEntryCreateCFProperty(current, CFSTR(kIOMACAddress), kCFAllocatorDefault, kNilOptions);
 55        if (data) {
 56            if (CFGetTypeID(data) != CFDataGetTypeID() || CFDataGetLength(data) < 6) {
 57                return false;
 58            }
 59            const uint8_t* ptr = CFDataGetBytePtr(data);
 60            ffStrbufSetF(result, "%02x:%02x:%02x:%02x:%02x:%02x", ptr[0], ptr[1], ptr[2], ptr[3], ptr[4], ptr[5]);
 61            return true;
 62        }
 63
 64        io_object_t parent = IO_OBJECT_NULL;
 65        if (IORegistryEntryGetParentEntry(current, kIOServicePlane, &parent) != KERN_SUCCESS) {
 66            return false;
 67        }
 68        IOObjectRelease(current);
 69        current = parent;
 70    }
 71
 72    return false;
 73}
 74
 75static FFLocalIpResult* findRedacted(FFlist* results, const char* ifName) {
 76    FF_LIST_FOR_EACH (FFLocalIpResult, item, *results) {
 77        if (ffStrbufEqualS(&item->mac, FF_LOCALIP_REDACTED_MAC) && ffStrbufEqualS(&item->name, ifName)) {
 78            return item;
 79        }
 80    }
 81    return nullptr;
 82}
 83
 84// Enumerating the interface classes once and matching on `BSD Name` is measurably cheaper
 85// than looking every interface up by name -- 1.5ms versus 3.0ms for the 21 interfaces
 86// getifaddrs() reports here -- which matters as soon as defaultRouteOnly is disabled and a
 87// dozen interfaces are listed at once. Interfaces the enumeration does not reach are looked
 88// up by name afterwards, so this can only ever be faster than, never worse than, the direct
 89// lookup.
 90void ffLocalIpFixRedactedMacs(FFlist* results) {
 91    bool hasRedacted = false;
 92    FF_LIST_FOR_EACH (FFLocalIpResult, item, *results) {
 93        if (ffStrbufEqualS(&item->mac, FF_LOCALIP_REDACTED_MAC)) {
 94            hasRedacted = true;
 95            break;
 96        }
 97    }
 98    if (!hasRedacted) {
 99        return;
100    }
101
102    for (uint32_t c = 0; c < ARRAY_SIZE(iokitInterfaceClasses); ++c) {
103        FF_IOOBJECT_AUTO_RELEASE io_iterator_t iterator = IO_OBJECT_NULL;
104        if (IOServiceGetMatchingServices(MACH_PORT_NULL, IOServiceMatching(iokitInterfaceClasses[c]), &iterator) != KERN_SUCCESS) {
105            continue;
106        }
107
108        io_object_t entry;
109        while ((entry = IOIteratorNext(iterator)) != IO_OBJECT_NULL) {
110            FF_IOOBJECT_AUTO_RELEASE io_object_t entryRef = entry;
111
112            FF_CFTYPE_AUTO_RELEASE CFStringRef bsdName = (CFStringRef) IORegistryEntryCreateCFProperty(entryRef, CFSTR(kIOBSDNameKey), kCFAllocatorDefault, kNilOptions);
113            if (!bsdName) {
114                continue;
115            }
116
117            char ifName[IFNAMSIZ];
118            if (CFGetTypeID(bsdName) != CFStringGetTypeID() || !CFStringGetCString(bsdName, ifName, sizeof(ifName), kCFStringEncodingUTF8)) {
119                continue;
120            }
121
122            FFLocalIpResult* item = findRedacted(results, ifName);
123            if (item && getMacFromRegistry(entryRef, &item->mac)) {
124                FF_DEBUG("Replaced the redacted MAC address for interface %s", ifName);
125            }
126        }
127    }
128
129    FF_LIST_FOR_EACH (FFLocalIpResult, item, *results) {
130        if (!ffStrbufEqualS(&item->mac, FF_LOCALIP_REDACTED_MAC)) {
131            continue;
132        }
133
134        FF_IOOBJECT_AUTO_RELEASE io_object_t service = IOServiceGetMatchingService(MACH_PORT_NULL, IOBSDNameMatching(MACH_PORT_NULL, 0, item->name.chars));
135        if (service && getMacFromRegistry(service, &item->mac)) {
136            FF_DEBUG("Replaced the redacted MAC address for interface %s", item->name.chars);
137        } else {
138            FF_DEBUG("No IOMACAddress in the IORegistry for interface %s", item->name.chars);
139        }
140    }
141}