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}