Have you ever just wanted to see all the wireless networks around you? Apple
provides quite a few tools to help do this out of the box including a really
nice command line tool called airport
. But what if there was more…
Airport
A common utility known by many mac users is airport
. If you have never heard
of airport
you can find the binary at:
/System/Library/PrivateFrameworks/Apple80211.framework/Versions/A/Resources/airport
and if you check the help page you can see:
Supported arguments:
-c[<arg>] --channel=[<arg>] Set arbitrary channel on the card
-z --disassociate Disassociate from any network
-I --getinfo Print current wireless status, e.g. signal info, BSSID, port type etc.
-s[<arg>] --scan=[<arg>] Perform a wireless broadcast scan.
Will perform a directed scan if the optional <arg> is provided
-x --xml Print info as XML
-P --psk Create PSK from specified pass phrase and SSID.
The following additional arguments must be specified with this command:
--password=<arg> Specify a WPA password
--ssid=<arg> Specify SSID when creating a PSK
-h --help Show this help
One of the useful flags is --scan
to look for current wireless networks
around you. In addition you can add the --xml
flag to output machine
parsable xml data.
/System/Library/PrivateFrameworks/Apple80211.framework/Versions/A/Resources/airport -s --xml
This will output ~120 lines of data per wireless network in the local vicinity. In most cases, you will not care about all of the data returned but it is nice to know you can access it programmatically if the need came up.
Airport Bug
Recently, I ran into a nice bug with airport
on macOS 10.13 (this might
affect older OS’s I did not test).
When you run airport
with the --xml
flag the command would fail to output
properly formatted xml data. I did not spend a bunch of time trying to find
the root issue since I wanted a working solution now and knew any potential
bug fixes would take 3-18 months. One idea is that HP printers are
broadcasting a SSID with unsafe characters, or maybe the airport
command
chokes when scanning in a highly saturated area.
You can see the result of a failed run below, without proper ending tags:
<dict>
<!-- removed for brevity-->
<key>SSID_STR</key>
<string>DIRECT-54-HP ENVY 4520 series</string>
<key>WPS_PROB_RESP_IE</key>
<dict>
<key>IE_KEY_WPS_AP_SETUP_LOCKED</key>
<true/>
<key>IE_KEY_WPS_CFG_METHODS</key>
<integer>0</integer>
<key>IE_KEY_WPS_DEV_NAME</key>
<string>DIRECT-54-HP ENVY 4520 series</string>
<key>IE_KEY_WPS_DEV_NAME_DATA</key>
<data>
RElSRUNULTU0LUhQIEVOVlkgNDUyMCBzZXJpZXM=
</data>
<key>IE_KEY_WPS_MANUFACTURER</key>
<string>HP</string>
<key>IE_KEY_WPS_MODEL_NAME</key>
<string>ENVY 4520 series
CoreWLAN
Since airport
was no longer doing what I needed I started to wonder how
else I could get this data. macOS apps like WiFi Explorer and NetSpot are
able to obtain this wireless data so I was hopeful Apple exposed this via a
framework. Luckily they do and it is call CoreWLAN. A topic of the
CoreWLAN framework is CWInterface which included a scan method that was just
want I needed.
Looking up the declaration for the scan function you will see:
- (NSSet<CWNetwork *> *)scanForNetworksWithName:(NSString *)networkName
includeHidden:(BOOL)includeHidden
error:(out NSError * _Nullable *)error;
So now I just needed to convert that to python for my use case.
import objc
bundle_path = '/System/Library/Frameworks/CoreWLAN.framework'
objc.loadBundle('CoreWLAN',
bundle_path=bundle_path,
module_globals=globals())
iface = CWInterface.interface()
iface.scanForNetworksWithName_includeHidden_error_(None, True, None)
and a sample output from the above code:
({(
<CWNetwork: 0x7f927850e8e0> [ssid=ATT555g6d6, bssid=XX:XX:XX:XX:XX:XX, security=WPA2 Personal, rssi=-87, channel=<CWChannel: 0x7f927b45d100> [channelNumber=1(2GHz), channelWidth={20MHz}], ibss=0],
<CWNetwork: 0x7f927850ed00> [ssid=Homer-Guest, bssid=XX:XX:XX:XX:XX:XX, security=Open, rssi=-50, channel=<CWChannel: 0x7f927b473790> [channelNumber=6(2GHz), channelWidth={20MHz}], ibss=0],
<CWNetwork: 0x7f927b440730> [ssid=MW-HM1, bssid=XX:XX:XX:XX:XX:XX, security=WPA2 Personal, rssi=-85, channel=<CWChannel: 0x7f927b442660> [channelNumber=11(2GHz), channelWidth={20MHz}], ibss=0],
<!-- removed for brevity-->
)}, None)
So maybe not quite as pretty as the xml output from airport
but it gives us
many of the important values we would normally want: SSID, BSSID, Security,
and RSSI. Not as obvious but that CWChannel also contains a bunch of
potentially useful data. An added benefit it does not fail when trying
to output the data.
Converting to PyOjbC
In the above section I skipped over converting the Objective-C code to Python. However in the past multiple community members have helped me with this. I figured here I would break down how I made the conversion, knowing that most people reading this are Mac Admins and some might not have experience with Objective-C and the PyObjC bridge.
At this point, I recommend launching an interactive /usr/bin/python
session
and pasting these commands as we go. Each section from here on out is additive
and requires the code above it to run. This way you can see the output of each
step as we progress. No seriously this section is designed for user interaction
open up your shell!
Info
First tip: Always make sure you are viewing Apple documentation under the ‘objective-c’ language if you are planing on using PyObjC.
Import
Lets start with the base import code of the CoreWLAN framework. Honestly, I learned this from years of looking at sample code from Frogor. If you wanted to replicate this yourself you would need to review the PyObjC documentation.
import objc
bundle_path = '/System/Library/Frameworks/CoreWLAN.framework'
objc.loadBundle('CoreWLAN',
bundle_path=bundle_path,
module_globals=globals())
The objc
module ships with PyObjC so as long as you use the stock system
python located at /usr/bin/python
you can use this code on any mac right
out of the box.
At this point, we have direct access to all of the classes in the CoreWLAN Framework.
Help
Go ahead and view the help:
help(CWInterface)
Go ahead and scroll through the help menu using j
& k
. Here you can see all
the methods that might be available. Once you are done scanning type gg
<enter>
to go to the top of the help menu followed by /disassociate
<enter>
to search the document for the string “disassociate”. Lastly type, q
to
exit the help menu.
Instantiation, disassociate, & power cycling
Now lets try calling the disassociate method:
CWInterface.disassociate()
Info
Note: All we did was add a period and the parentheses to call the method.
Hopefully you got back the following error:
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: Missing argument: self
Strange, right? The option was in our help list. So why didn’t it work? Lets go back to the CWInterface documentation. You see that important warning message up top? Here I’ll paste it below:
Warning
Important
Do not instantiate interface objects directly. Instead, use interface objects vended by a CWWiFiClient instance via the interface method or one of its relatives. This enables your app to adopt App Sandbox even when it uses CoreWLAN without the need for any special exceptions. Directly instantiating interface objects causes low level access to system sockets, which by default is not allowed in a sandboxed environment.
So the positive is we don’t care about Sandboxing since we are in python land. This also means we can be lazy and instantiate the deprecated interface. IE:
iface = CWInterface.interface()
Now lets try to disassociate:
iface.disassociate()
If you check your airport status in the menu bar you should no longer be connected to a wireless network. Now lets power cycle to rejoin.
iface.setPower_error_(False, None)
iface.setPower_error_(True, None)
Isn’t python fun? Yes this had nothing to do with the section but we are learning here.
Scanning
Lets get onto the scanning so lets revisit the objective-c code:
- (NSSet<CWNetwork *> *)scanForNetworksWithName:(NSString *)networkName
includeHidden:(BOOL)includeHidden
error:(out NSError * _Nullable *)error;
Info
The documentation on CWInterface has multiple types of scans but we will be working with the “WithName” + “includeHidden” option. For bonus when we are complete try using one of the other scan options.
Now on to the scan. I would recommend revisiting the help(CWInterface)
help menu and searching for “scan”. Since we have an instantiation object
help(iface)
will also work.
In the help output you should see our option:
| scanForNetworksWithChannels_ssid_bssid_restTime_dwellTime_ssidList_error_
|
| scanForNetworksWithName_error_
|
| scanForNetworksWithName_includeHidden_error_
|
| scanForNetworksWithParameters_error_
|
| scanForNetworksWithSSID_error_
|
| scanForNetworksWithSSID_includeHidden_error_
Go ahead and compare the output from the above help menu to the objective-c
code. To do the conversion manually the general process is: :
to _
and
combine all the options into one method name.
Now lets run the scan code:
iface.scanForNetworksWithName_includeHidden_error_()
While the above code is malformed it gives a very helpful error message of:
>>> iface.scanForNetworksWithName_includeHidden_error_()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: Need 3 arguments, got 0
You now know definitively that this method needs three arguments. They can be found from the Apple declaration documentation:
(NSString *)networkName
(BOOL)includeHidden
(out NSError * _Nullable *)error
Try substituting the name of your SSID into the first field:
result, error = iface.scanForNetworksWithName_includeHidden_error_("YOUR_SSID", True, None)
print(result)
print(error)
Pulling out the data
This is great but I really need to be able to pull our the ssid, rssi, and
bssid fields. Looking at the documentation we can see we are getting back an
object that looks like a CWNetwork
? Maybe? But it is surrounded in those
{(
squiggly brackets things and parentheses.
Let us find out for sure:
type(result)
which outputs:
<objective-c class __NSSetI at 0x7fff84009be0>
Not sure what that is? Well lets cheat and go back to the Apple declaration documentation.
- (NSSet<CWNetwork *> *)
NSSet and CWNetwork. Still lost? That is okay. Most NS
objects come from
PyObjC and due to the bridge python can natively interactive with them. So
a NSSet most closely links to a python set IE:
from sets import Set
>>> engineers = Set(['John', 'Jane', 'Jack', 'Janice'])
That means we should be able to loop over it, so lets try:
for i in result:
print(type(i))
help(i)
break
Looking through the help menu you will see this i
object is a CWNetwork
.
This can be validated when you exit the help menu and check the type output.
To quickly finish the code lets pull out the data keys I wanted:
for i in results:
if i.ssid() is None:
continue
print({'RSSI': i.rssiValue(), 'BSSID': i.bssid(), 'SSID_STR': i.ssid()})
Wrap up
To be completely honest the above CoreWLAN sample is a very straight forward example. Some other PyObjC code can become very complex very quickly. If this was your first time interacting with PyOjbC hopefully the above walk through helped guide you through the weeds. The above process was almost exactly how I stepped through the code to figure out how things worked…while maybe a bit exaggerated in this post.