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")