Fetching Node Status from AirPort APs

Seven years ago, I hacked together some code to update my Ecobee WiFi thermostat temperature depending on whether I was home. While my newer Ecobee thermostat has room occupancy sensors that make this tracking automatic, back then I had to poll my WiFi access point through SNMP to look for my phone's MAC address in its table of associated clients.

Recently I needed to do something similar to pass to my Z-Wave controller but it seems that Apple has removed SNMP support from its Airport Extreme firmware some time ago.

Since the macOS Airport Utility is able to show the list of connected clients on each AP, I figured there must be a way to easily get that same information through something other than SNMP.

AirPyrt Tools

Some searching landed me at AirPyrt-Tools, which is a tool that can communicate with AirPorts over their older ACP protocol. While I didn't find a way to get the client node status directly through ACP, I did discover that it's possible to enable SSH with the tool:

$ python -m acp -t 192.168.1.15 -p (password) --setprop dbug 0x3000
INFO:connecting to host 192.168.1.15:5009
[...]
$ python -m acp -t 192.168.1.15 -p (password) --reboot         

Enabling SSH reveals an OpenSSH daemon on port 22 which can be logged into as root with the AP's admin password. To my delight, I learned that the Airport Extreme runs NetBSD (full dmesg):

Copyright (c) 1996, 1997, 1998, 1999, 2000, 2001, 2002, 2003, 2004, 2005,
    2006, 2007, 2008, 2009, 2010, 2011, 2012
    The NetBSD Foundation, Inc.  All rights reserved.
Copyright (c) 1982, 1986, 1989, 1991, 1993
    The Regents of the University of California.  All rights reserved.

NetBSD 6.0 (build.kernel-target.conf) #0: Thu Nov  2 10:49:34 PDT 2017
    root@xapp29.apple.com:/BuildRoot/Library/Caches/com.apple.xbs/Sources/J28E/AirPortFW-77900.2/Embedded/Firmware/NetBSD/Targets/J28E/release/obj/build.kernel-target.conf
total memory = 256 MB
avail memory = 229 MB
timecounter: Timecounters tick every 10.000 msec
mainbus0 (root)
cpu0 at mainbus0 core 0: 1 GHz Cortex-A9 r4p0 (Cortex core)

Accessing the list of associated clients can be done with ifconfig wlanN list sta on each of the 4 wlan interfaces.

# ifconfig wlan0 list sta
ADDR               AID CHAN TXRATE RXRATE  RSSI IDLE  TXSEQ  RXSEQ CAPS FLAG
08:66:98:xx:xx:xx    7  149   526M    24M -66.0    9      0      0 EP   AQEP RSN HTCAP WME (rssi -73:-66:-74 nf -92:-92:-92)
dc:a4:ca:xx:xx:xx    6  149   877M   877M -55.0    4      0      0 EP   AQEH RSN HTCAP WME (rssi -58:-55:-56 nf -92:-92:-92)

To gather a single list of all associated client MAC addresses on a group of AirPort APs, a Ruby script with net/ssh can make it easy:

#!/usr/bin/env ruby
require "net/ssh"

APS = [ "192.168.1.15", "192.168.1.17", "192.168.1.19" ]
ROOT_PW = ".."

puts APS.map{|ap|
  Net::SSH.start(ap, "root", :password => ROOT_PW, :port => 22) do |ssh|
    ssh.exec!(3.times.map{|z| "/sbin/ifconfig wlan#{z} list sta" }.join(";"))
      .split("\n")
      .reject{|l| l.match(/^ADDR/) }
      .map{|l| l.gsub(/ .*/, "") }
  end
}.flatten
.reject{|a| a == "" }
.join("\n")
Questions or comments?
Please feel free to contact me.