Getting the Wi-Fi router BSSID from a Daemon.

Our macOS application (running as a LaunchDaemon) has been able to report the current Wi-Fi SSID and BSSID (if connected) using the airport command. Since airport has been removed from macOS, we have not been able to collect BSSID information.

First, I demonstrate that the BSSID exists: I can option-click the Wi-Fi status menu icon and see the following:

Wi-Fi
Interface Name: en0
Address: a8:8f:d9:52:10:7d
* * *
Enable Wi-Fi Logging
Create Diagnostics Report...
Open Wireless Diagnostics...
* * *
Known Network
    polymorphic
    IP Address: 192.168.86.50
    Router: 192.168.86.1
    Security: WPA2 Personal
    BSSID: 88:3d:24:ba:36:81
    Channel: 149 (5 GHz, 80 MHZ)
    Country Code: US
    RSSI: -60 dBm
    Noise: -89 dBm
    Tx Rate: 520 Mbps
    PHY Mode: 802.11ac
    MCS Index: 5
    NSS: 2
* * *
Other Networks
* * *
Wi-Fi Settings...

This says to me that:

  1. The WiFi router I am connected to has SSID = polymorphic.
  2. The WiFi router I am connected to has BSSID = 88:3d:24:ba:36:81.
  3. My computer's Wi-Fi hardware has MAC address = a8:8f:d9:52:10:7d.
  4. My computer's Wi-Fi interface name = en0.

To get this information now (from within an application), I have attempted to run:

/usr/sbin/networksetup -listallhardwareports

The output of that command includes the following

Hardware Port: Wi-Fi
Device: en0
Ethernet Address: a8:8f:d9:52:10:7d

To get the SSID, I can then execute:

$ /usr/sbin/networksetup -getairportnetwork en0
Current Wi-Fi Network: polymorphic

But I still can't get the router's BSSID.

So I try

$/usr/sbin/networksetup -getinfo 'Wi-Fi'
DHCP Configuration
IP address: 192.168.86.50
Subnet mask: 255.255.255.0
Router: 192.168.86.1
Client ID:
IPv6: Automatic
IPv6 IP address: none
IPv6 Router: none
Wi-Fi ID: a8:8f:d9:52:10:7d

Still no new information.

$ /usr/sbin/networksetup -getmacaddress en0
Ethernet Address: a8:8f:d9:52:10:7d (Device: en0)

This is not helpful either.


Let's try another approach:

$ /usr/sbin/netstat -nr -f inet | grep ^default
default    192.168.86.1   UGScg   en0

This tells me that my router's IP address is 192.168.86.1.

The arp tool should be able to translate

$ /usr/sbin/arp -a -n | grep "(192.168.86.1)"
? (192.168.86.1) at 88:3d:24:ba:36:7f on en0 ifscope [ethernet]

This tells me that the router's MAC address is "88:3d:24:ba:36:7f", but it is not the same value as the router's BSSID, which we know to be 88:3d:24:ba:36:81!


Another approach. I wrote the following Swift program:

import CoreWLAN
let c : CWWiFiClient = CWWiFiClient.shared()
if let ifs : [CWInterface] = c.interfaces() {
  for i in ifs {
    print(
      i.interfaceName ?? "<nil>",
      i.powerOn(),
      i.ssid() ?? "<nil>",
      i.bssid() ?? "<nil>")
  }
}

When executing it with swift, I got:

en0 true polymorphic <nil>

So for some reason, the CoreWLAN API is hiding the BSSID, but not the SSID.

When I use swiftc to compile before executing, I get:

en0 true <nil> <nil>

Why is the CoreWLAN API now hiding the SSID as well?

I even tried an Objective-C program:

// Link with:
//   -framework Foundation
//   -framework CoreWLAN
#include <stdio.h>
#include <CoreWLAN/CoreWLAN.h>
void printWifi() {
    NSArray<CWInterface*>* ifs = [[CWWiFiClient sharedWiFiClient] interfaces];
    for (CWInterface* i in ifs) {
        printf("%s %s %s %s\n",
             [i.interfaceName UTF8String],
             [i powerOn] ? "true" : "false",
             [[i ssid] UTF8String],
             [[i bssid] UTF8String]);
    }
}
int main() {
    printWifi();
    return 0;
}

It prints out:

en0 true (null) (null)

Based on <https://developer.apple.com/forums/thread/131636>, I tried

// Link with:
//   -framework Foundation
//   -framework CoreWLAN
//   -framework CoreLocation 
#include <stdio.h>
#include <CoreWLAN/CoreWLAN.h>
#include <CoreLocation/CoreLocation.h>
void printWifi() {
    NSArray<CWInterface*>* ifs = [[CWWiFiClient sharedWiFiClient] interfaces];
    for (CWInterface* i in ifs) {
        printf("%s %s %s %s\n",
             [i.interfaceName UTF8String],
             [i powerOn] ? "true" : "false",
             [[i ssid] UTF8String],
             [[i bssid] UTF8String]);
    }
}
CLLocationManager* startCoreLocation() {
    CLLocationManager* mgr = [[CLLocationManager alloc] init];
    [mgr requestAlwaysAuthorization];
    [mgr startUpdatingLocation];
    return mgr;
}
int main() {
    CLLocationManager* locMgr = startCoreLocation();
    printWifi();
    return 0;
}

That change did not seem to make a difference.

After more work, I found that I can not even figure out CLLocationManager authorization. So I attempted to create a minimal program that can get that: <https://github.com/HalCanary/location>.

I am not sure how to proceed here. What is wrong with my location code? Will our application need to get the com.apple.security.personal-information.location entitlement in order to get the BSSID?

Answered by DTS Engineer in 794539022

Our macOS … LaunchDaemon … has been able to report the current Wi-Fi SSID and BSSID

That’s not easy on recent versions of macOS because:

  • macOS now gates access to SSID and BSSID information on the System Settings > Privacy & Security > Location privilege.

  • It’s not possible for a daemon to gain the Location privilege )-:

This restricted rolled out a while back and in recently macOS 14.x software updates we’ve been closing various ways to bypass it (such as the airport tool).

Will our application need to get the com.apple.security.personal-information.location entitlement in order to get the BSSID?

That won’t help. It’s an App Sandbox entitlement, and so is only relevant if you’re in a sandbox. And that’s not the issue here, but rather that a daemon can’t get the Location privilege.

The only approach that will work is to split this functionality out of your launchd daemon into a launchd agent. An agent runs in a user context and can gain the Location privilege. And once it has that, it can use Core WLAN to get Wi-Fi information.

There are, however, significant drawbacks to this approach:

  • It’s substantially more complicated.

  • It only works if at least one user is logged in. If all users log out, there are no GUI login sessions and hence no launchd agents running.

Share and Enjoy

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

Our macOS … LaunchDaemon … has been able to report the current Wi-Fi SSID and BSSID

That’s not easy on recent versions of macOS because:

  • macOS now gates access to SSID and BSSID information on the System Settings > Privacy & Security > Location privilege.

  • It’s not possible for a daemon to gain the Location privilege )-:

This restricted rolled out a while back and in recently macOS 14.x software updates we’ve been closing various ways to bypass it (such as the airport tool).

Will our application need to get the com.apple.security.personal-information.location entitlement in order to get the BSSID?

That won’t help. It’s an App Sandbox entitlement, and so is only relevant if you’re in a sandbox. And that’s not the issue here, but rather that a daemon can’t get the Location privilege.

The only approach that will work is to split this functionality out of your launchd daemon into a launchd agent. An agent runs in a user context and can gain the Location privilege. And once it has that, it can use Core WLAN to get Wi-Fi information.

There are, however, significant drawbacks to this approach:

  • It’s substantially more complicated.

  • It only works if at least one user is logged in. If all users log out, there are no GUI login sessions and hence no launchd agents running.

Share and Enjoy

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

Thanks!

How do I give a launchd agent the entitlement?

How do I give a launchd agent the entitlement?

Again, entitlements aren’t the issue here, but privileges.

The best way to grant your agent that privilege is to embed the agent within a container app and have that install the agent with SMAppService. The container app then becomes the responsible code and, if the user grants it access, the agent can take advantage of it.

However, that probably won’t work in your situation because you want the agent to run in all GUI sessions. SMAppService doesn’t have a way to set that up (r. 92457638)-: However, this overall model should still work; you will have to set AssociatedBundleIdentifiers in your launchd property list so that the system can track responsibility from your agent to the user-visible app.

Keep in mind that the Location privilege is per user.

FWIW, I agree that this is all very suboptimal, and I encourage you to file an enhancement request for a better way to achieve your goal. Be careful to explain your requirements carefully, but the way things are currently set up isn’t accidentally, but fallout from a number of privacy-oriented design choices.

Specifically, if your app works exclusively in a managed environment, you should be sure to mention that.

Please post your bug number, just for the record.

Share and Enjoy

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

Getting the Wi-Fi router BSSID from a Daemon.
 
 
Q