Access to MAC addresses of local network interfaces in macOS 27

Hi all,

we are building a custom controller for ATDECC, which is a layer 2 protocol standardized by IEEE in 1722.1. Our controller can work on multiple network interfaces at the same time . It uses the interface's MAC address to identify, on which interface a certain AVB / ATDECC device was discovered. It then sends replies for this device only to this interface.

This controller worked fine up to and including macOS 26, but when running the same code on macOS 27, we cannot get the MAC addresses for the local interfaces anymore, but we receive 02:00:00:00:00:00 for each of them. This seems to indicate that the MAC address was redacted (looks like the same MAC address, that is being returned since iOS 11 due to privacy reason).

Is this a bug or is macOS going to redact the MAC addresses also in the final release? If MAC addresses are being redacted, would it help to request access to the new entitlement called com.apple.developer.networking.topology-observation?

I attached a little code snippet, that returns actual MAC addresses on macOS 26, but redacted ones on macOS 27.

Build with clang++ -std=c++23 -o ifprobe ifprobe.cpp and then run it with ./ifprobe.

Answered by DTS Engineer in 901589022
Looks like the file content doesn't display correctly.

Yeah, sorry about that. The forums platform has a number of attachment issues (hey hey :-). I’m hoping that we can improve this sooner rather than later, but I don’t have any specific timeline to share.

Fortunately you found a reasonable workaround.

Is this a bug … ?

No. It’s a deliberate privacy hardening.

would it help to request access to the new entitlement called com.apple.developer.networking.topology-observation?

That’s the right entitlement, but you don’t need to “request access”. It’s available to all Apple developers. In Xcode 27 beta, you can simply enable the Network Topology Observation capability.

This is a restricted entitlement, which means it must be authorised by a provisioning profile (see TN3125 for more about that). If this code ships within a bundled program, like an app or an app extension, then you just embed the profile in the bundle and you’re golden (or let Xcode do it for you).

If this program ships outside of a bundle — for example, as a command-line tool — then things get trickier. The standard workaround is to embed the program in an app-like wrapper. See Signing a daemon with a restricted entitlement.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Looks like the file content doesn't display correctly. Here it is again.

// clang++ -std=c++23 -o ifprobe ifprobe.cpp && ./ifprobe
#include <ifaddrs.h>
#include <net/bpf.h>
#include <net/if.h>
#include <net/if_dl.h>
#include <net/route.h>
#include <sys/ioctl.h>
#include <sys/sockio.h>
#include <sys/sysctl.h>
#include <fcntl.h>
#include <unistd.h>
#include <cerrno>
#include <cstdio>
#include <cstring>
#include <string>
#include <vector>

static int open_bpf()
{
    for ( int i = 0; i < 256; ++i )
    {
        std::string p = "/dev/bpf" + std::to_string( i );
        int fd = open( p.c_str(), O_RDWR );
        if ( fd != -1 ) return fd;
        if ( errno != EBUSY ) break;
    }
    return -1;
}

static const char* functional_type_name( unsigned t )
{
    switch ( t )
    {
        case IFRTYPE_FUNCTIONAL_LOOPBACK:   return "LOOPBACK";
        case IFRTYPE_FUNCTIONAL_WIRED:      return "WIRED";
        case IFRTYPE_FUNCTIONAL_WIFI_INFRA: return "WIFI_INFRA";
        case IFRTYPE_FUNCTIONAL_WIFI_AWDL:  return "WIFI_AWDL";
        case IFRTYPE_FUNCTIONAL_CELLULAR:   return "CELLULAR";
        default:                            return "UNKNOWN";
    }
}

static size_t sockaddr_aligned_size(const sockaddr* address)
{
    if (address->sa_len == 0)
        return sizeof(long);

    return (address->sa_len + sizeof(long) - 1) &
           ~(sizeof(long) - 1);
}

static void print_sysctl_mac(const char* name)
{
    const unsigned int index = if_nametoindex(name);
    if (index == 0)
    {
        printf(" sysctl_mac=<unknown interface>");
        return;
    }

    int mib[] = {
        CTL_NET, PF_ROUTE, 0, 0, NET_RT_IFLIST,
        static_cast<int>(index)
    };

    size_t length = 0;
    if (sysctl(mib, 6, nullptr, &length, nullptr, 0) != 0)
    {
        printf(" sysctl_mac=<size query failed errno=%d>", errno);
        return;
    }

    std::vector<unsigned char> buffer(length);
    if (sysctl(mib, 6, buffer.data(), &length, nullptr, 0) != 0)
    {
        printf(" sysctl_mac=<read failed errno=%d>", errno);
        return;
    }

    auto* message = reinterpret_cast<const if_msghdr*>(buffer.data());
    const auto* address = reinterpret_cast<const sockaddr*>(
        message + 1);

    for (int address_index = 0;
         address_index < RTAX_MAX;
         ++address_index)
    {
        if ((message->ifm_addrs & (1 << address_index)) == 0)
            continue;

        if (address_index == RTAX_IFP &&
            address->sa_family == AF_LINK)
        {
            auto* link_address =
                reinterpret_cast<const sockaddr_dl*>(address);

            if (link_address->sdl_alen == 6)
            {
                const auto* mac =
                    reinterpret_cast<const unsigned char*>(
                        LLADDR(link_address));

                printf(" sysctl_mac=%02x:%02x:%02x:%02x:%02x:%02x",
                       mac[0], mac[1], mac[2],
                       mac[3], mac[4], mac[5]);
                return;
            }
        }

        address = reinterpret_cast<const sockaddr*>(
            reinterpret_cast<const unsigned char*>(address) +
            sockaddr_aligned_size(address));
    }

    printf(" sysctl_mac=<unavailable>");
}

int main()
{
    ifaddrs* ifap = nullptr;
    if ( getifaddrs( &ifap ) != 0 ) return 1;

    int probe = socket( AF_INET, SOCK_DGRAM, 0 );

    for ( ifaddrs* p = ifap; p; p = p->ifa_next )
    {
        if ( !p->ifa_addr || p->ifa_addr->sa_family != AF_LINK ) continue;

        auto* dl = reinterpret_cast< sockaddr_dl* >( p->ifa_addr );

        unsigned ftype = 0;
        ifreq fr{};
        strncpy( fr.ifr_name, p->ifa_name, IFNAMSIZ - 1 );
        if ( ioctl( probe, SIOCGIFFUNCTIONALTYPE, &fr ) == 0 )
            ftype = fr.ifr_ifru.ifru_functional_type;

        // BIOCGDLT is the decisive check: DLT_EN10MB means our 14-byte Ethernet header is correct framing.
        int dlt = -1;
        int fd = open_bpf();
        if ( fd >= 0 )
        {
            ifreq br{};
            strncpy( br.ifr_name, p->ifa_name, IFNAMSIZ - 1 );
            if ( ioctl( fd, BIOCSETIF, &br ) == 0 ) ioctl( fd, BIOCGDLT, &dlt );
            close( fd );
        }

        printf( "%-10s maclen=%-2u ftype=%-11s dlt=%-3d up=%d running=%d ",
                p->ifa_name, dl->sdl_alen, functional_type_name( ftype ), dlt,
                ( p->ifa_flags & IFF_UP ) != 0, ( p->ifa_flags & IFF_RUNNING ) != 0 );
        print_sysctl_mac(p->ifa_name);
        printf("\n");
    }

    close( probe );
    freeifaddrs( ifap );
}
Accepted Answer
Looks like the file content doesn't display correctly.

Yeah, sorry about that. The forums platform has a number of attachment issues (hey hey :-). I’m hoping that we can improve this sooner rather than later, but I don’t have any specific timeline to share.

Fortunately you found a reasonable workaround.

Is this a bug … ?

No. It’s a deliberate privacy hardening.

would it help to request access to the new entitlement called com.apple.developer.networking.topology-observation?

That’s the right entitlement, but you don’t need to “request access”. It’s available to all Apple developers. In Xcode 27 beta, you can simply enable the Network Topology Observation capability.

This is a restricted entitlement, which means it must be authorised by a provisioning profile (see TN3125 for more about that). If this code ships within a bundled program, like an app or an app extension, then you just embed the profile in the bundle and you’re golden (or let Xcode do it for you).

If this program ships outside of a bundle — for example, as a command-line tool — then things get trickier. The standard workaround is to embed the program in an app-like wrapper. See Signing a daemon with a restricted entitlement.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Hi Quinn,

thanks for the quick reply. We will look into using this entitlement, then.

While we were investigating this, we stumbeeld upon NWEthernetChannel. It seems like it might be a good idea to migrate to using NWEthernetChannel in the future onmacOS. If so, what about the entitlement com.apple.developer.networking.custom-protocol. Is this available to all Apple developers, too?

And further: if we moved to using NWEthernetChannel, would we still have to keep using access_bpf group and LaunchDaemon to grant access to the bpf interfaces? Or would this be covered by the entitlement?

Thanks Arno

I’m not familiar with ATDECC and my, admittedly perfunctory, web searches didn’t turn up useful details [1]. So I’m assuming that this protocol requires you send and receive raw Ethernet frames. If I got that wrong, lemme know.

Is this available to all Apple developers, too?

Yes.

The main caveat here with NWEthernetChannel is that it only works for custom EtherTypes, that is, not IPv4 and not IPv6.

would we still have to keep [BPF]?

No. Well, assuming that all you need to do is send and receive raw Ethernet frames for a custom EtherType.

NWEthernetChannel had a few teething problems but I just tried it again on my main work Mac (macOS 26.6.1) and things worked as expected. I’ve added some snippets below.

Probably the trickiest problem is dealing with the interface. See this thread.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

[1] I found AVDECC, but it’s not clear how ATDECC relates to that.


// Set up the channel:

let interface: NWInterface = …
let etherType: UInt16 = …
let channel = NWEthernetChannel(on: interface, etherType: etherType)
channel.stateUpdateHandler = { newState in
    … handle the state change …
}
channel.receiveHandler = { content, tag, local, remote in
    … handle the received frame …
}
channel.start(queue: .main)

// Send a frame:

let content: Data = …
let remoteAddress: NWEthernetChannel.EthernetAddress = …
channel.send(content: content, to: remoteAddress, vlanTag: 0) { error in
    … handle the error …
}

AVDECC has been renamed to ATDECC a while ago, since AVB (Audio Video Bridging) was renamed to TSN (Time Sensitive Networking). ATDECC is basically an evolution of AVDECC. So your search was spot on. Sometimes both therms are used as synonyms.

AVDECC / ATDECC is using the AVTP Ethertype 0x22f0, so I would assume that this should work for us.

I will give it a try and see how far I can get with it.

Thanks again!

Thanks for all that extra context.

I will give it a try and see how far I can get with it.

Cool. Let me know how you get along.

If you search the forums for NWEthernetChannel you’ll find a number of threads where folks have bumped into issues with it. AFAIK most of those bugs are resolved in macOS 26. So I recommend that you start with that release, and then work backwards to your minimum deployment target. It’s possible that you’ll hit that without seeing any showstopping problems. But if not, you can then make a call as to whether it’s worth trying to aim for a workaround, or raising your minimum deployment target, or just sticking with BPF for older releases.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Access to MAC addresses of local network interfaces in macOS 27
 
 
Q