Re-order WIFI Preferred Networks...

adroitboy
New Contributor III

Hello All.

I've been installing a configuration profile that sets up our wifi SSID and associates it with a machine certificate via the AD Certificate section. It associates the cert with the wifi config (so the user doesn’t get prompted) and overall has been working fine for quite some time.

I have one issue though. New networks place the network at the end of the preferred network list. Simple fix: should be to re-install right? Nope. The configuration profile places the network at the end of the Wifi Preferred Networks. I want it to be first.

There doesn’t seem to be a way to re-order it unless I remove the wifi network, then re-add it. That's fairly trivial to script, but that breaks the association with the computer’s certificate and the user now suffers.

Does anyone know how to programmatically re-order SSID preference without breaking the cert association OR how to add wifi and associate it with an existing machine cert so that the user isn't prompted?

Thanks!
Aaron

45 REPLIES 45

FritzsCorner
Contributor III

I opened a case with Apple on this a couple months back. Unfortunately we ran into the same issue as what you are seeing. Below is a suggestion they came back with that we have not tried or tested as of yet.

You’re right that the -addpreferredwirelessnetworkatindex flag only works when adding new SSIDs, not rearranging existing ones. As for suggestions, I have a few. The SSID name format you’re seeing in the PreferredOrder array is actually the name of the Wi-Fi network converted from ASCII to hexadecimal. There’s two quick ways to turn that into something human readable. First way, find the “KnowNetworks” key, which contains a dictionary of the hex SSIDs and their individual settings. Within the individual settings, there’s a key called ‘SSIDString', which contains the plaintext version of the name. Example: <key>wifi.ssid.<464f4f></key> <dict> <key>AutoLogin</key> <false/> <key>Captive</key> <false/> <key>ChannelHistory</key> <array/> <key>Closed</key> <false/> <key>CollocatedGroup</key> <array/> <key>Disabled</key> <false/> <key>Passpoint</key> <false/> <key>PossiblyHiddenNetwork</key> <false/> <key>RoamingProfileType</key> <string>None</string> <key>SPRoaming</key> <false/> <key>SSID</key> <data> Rk9P </data> <key>SSIDString</key> <string>FOO</string> <key>SecurityType</key> <string>WPA Personal</string> <key>SystemMode</key> <false/> <key>TemporarilyDisabled</key> <false/> </dict> As you’ll see, this is for my example FOO network from earlier. Everywhere I see wifi.ssid.<464f4f>, it’s referring to this network. The other and in my opinion much easier way to do this is to find a nice online hex to ascii converter, and paste in the hex version of the name. Here’s one such site: http://www.unit-conversion.info/texttools/hexadecimal/. You can also work it in the other direction, entering the ASCII name that you know and converting it to the hex name you should be hunting for. In theory, you could also do the conversion with the command line tool ‘iconv’, but it requires creating input files containing the data to be converted and was a bit convoluted for my taste. ‘man iconv’ if you want to try it out. Now, after you know the hex name of the network, you should be able to modify the .plist by hand. Just make sure your network is the first item listed in the PreferredOrder array, and that it only appears once in the list. In my testing, I found that this method worked to reorder the preferred network list, and that my change survived reboots, toggling of the Wi-Fi interface, and many other attempts to test if the system would revert to the original order. The official answer is still “make the change in the GUI”, so there’s no guarantee this hand editing method will continue to work with later OS releases. As such, if you plan on using this workaround for now, I highly suggest testing that it continues to work when new beta builds of the OS are made available.

adroitboy
New Contributor III

OK. I found the plist and the array PreferredOrder where i see the hex form of my network name. I'll be using plistbuddy to see if I can re-order the items in PreferredOrder by deleting the existing item I want to move, then inserting it at location 0. I just need to verify that it works.

When modifying plists with plistbuddy, is there a preferred method for making sure changes are properly applied and stick? Any cfprefsd killing, or other things to do before, during, after? Target OS's are 10.9 and 10.10.

Thanks!
Aaron

jjones
Contributor II

This is a script I made a few weeks ago, It does remove and re-add the SSID from the preferred network list but I've noticed as long as they are currently connected to that SSID they were never kicked off. However we use Configurations to push our networks out. Is it possible for you to add that certificate to be system-based rather than SSID specific?

If so then this script I made could be used:

https://jamfnation.jamfsoftware.com/discussion.html?id=17858

jjones
Contributor II

Heh..delete this one. Double posted....

Anthonyr
New Contributor

I've been using a python script made by pudquick on GitHub, linked here

I run this to change the order of preferred networks:

#!/usr/bin/python

# As written, this requires the following:
# - OS X 10.6+ (may not work in 10.10, haven't tested)
# - python 2.6 or 2.7 (for collections.namedtuple usage, should be fine as default python in 10.6 is 2.6)
# - pyObjC (as such, recommended to be used with native OS X python install)

# Only tested and confirmed to work against 10.9.5
# Run with root

import objc, ctypes.util, os.path, collections
from Foundation import NSOrderedSet

preferred_SSID    = 'yourSSIDhere'
next_to_last_SSID = 'yourSSIDhere'
last_SSID         = 'yourSSIDhere'

def load_objc_framework(framework_name):
    # Utility function that loads a Framework bundle and creates a namedtuple where the attributes are the loaded classes from the Framework bundle
    loaded_classes = dict()
    framework_bundle = objc.loadBundle(framework_name, bundle_path=os.path.dirname(ctypes.util.find_library(framework_name)), module_globals=loaded_classes)
    return collections.namedtuple('AttributedFramework', loaded_classes.keys())(**loaded_classes)

# Load the CoreWLAN.framework (10.6+)
CoreWLAN = load_objc_framework('CoreWLAN')

# Load all available wifi interfaces
interfaces = dict()
for i in CoreWLAN.CWInterface.interfaceNames():
    interfaces[i] = CoreWLAN.CWInterface.interfaceWithName_(i)

# Repeat the configuration with every wifi interface
for i in interfaces.keys():
    # Grab a mutable copy of this interface's configuration
    configuration_copy = CoreWLAN.CWMutableConfiguration.alloc().initWithConfiguration_(interfaces[i].configuration())
    # Find all the preferred/remembered network profiles
    profiles = list(configuration_copy.networkProfiles())
    # Grab all the SSIDs, in order
    SSIDs = [x.ssid() for x in profiles]
    # Check to see if our preferred SSID is in the list
    if (preferred_SSID in SSIDs):
        # Apparently it is, so let's adjust the order
        # Profiles with matching SSIDs will move to the front, the rest will remain at the end
        # Order is preserved, example where 'ssid3' is preferred:
        #    Original: [ssid1, ssid2, ssid3, ssid4]
        #   New order: [ssid3, ssid1, ssid2, ssid4]
        profiles.sort(key=lambda x: x.ssid() == preferred_SSID, reverse=True)
        # Now we move next_to_last_SSID to the end
        profiles.sort(key=lambda x: x.ssid() == next_to_last_SSID, reverse=False)
        # Now we move last_SSID to the end (bumping next_to_last_SSID)
        profiles.sort(key=lambda x: x.ssid() == last_SSID, reverse=False)
        # Now we have to update the mutable configuration
        # First convert it back to a NSOrderedSet
        profile_set = NSOrderedSet.orderedSetWithArray_(profiles)
        # Then set/overwrite the configuration copy's networkProfiles
        configuration_copy.setNetworkProfiles_(profile_set)
        # Then update the network interface configuration
        result = interfaces[i].commitConfiguration_authorization_error_(configuration_copy, None, None)

The problem I was having was with our users computers connecting to our guest wifi over our employee wifi, so I also run a script to disable/re-enable wifi so they actually connect to the preferred network right away:

networksetup -setairportpower en0 off
networksetup -setairportpower en0 on

I just add both of these scripts to a single policy and place it in Self Service.

Edit: Added link to source for pudquick's GitHub

MikeT
New Contributor III

Has anyone tried porting this python script to v3 yet with python being removed in macOS 12.3? I'm not a Python programmer but I tried my basic knowledge at porting the script from python2 to python 3 and manually installing python3 on macOS 12.3 beta. It appears python3 did not have objc so I installed objc with pip3. It appears "maybe?" the Python3 objc does not contain what is needed to access natively like the macOS provided python v2.x did as it appears broken. I also run the script through the python v2 to v3 converter 2to3 beforehand. Just curious if there is a new method scripted that someone has developed, swift/bash/other that is seamless. I noticed the dockutil dev rewrote dockutil in swift in the past few weeks and it's in beta. (We swapped to it and the beta seems to be fine on macOS 12.3) Several methods I have found could "disconnect" WiFi while changing the preferred order.

I just started looking into this today.  I also use this python script to set wireless network order.  If anyone finds a different solution, please post.

sdagley
Honored Contributor III

Please refer to post https://community.jamf.com/t5/jamf-pro/re-order-wifi-preferred-networks/m-p/262367/highlight/true#M2... in this thread for a Python 3 compatible script to re-order SSIDs while preserving 802.1x configuration, and the link to the necessary Python 3 install.

Thank you for sharing this.  One other silly question.... I have downloaded the python3 files from https://github.com/macadmins/python.

Now how do I "install" it?  It's just the source files.

FritzsCorner
Contributor III

@Anthonyr

That script worked perfectly on my 10.10 system. Thanks for sharing!

Edit For those interested I found the link to the script on pudquick's github.

https://gist.github.com/pudquick/fcbdd3924ee230592ab4

Aaron

Anthonyr
New Contributor

@FritzsCorner No problem! It completely solved our issue as well.
Let Forgor(/pudquick) know you owe him a beer in the MacAdmins Slack channel if you're a member.

jimbul
New Contributor

This is a lifesaver!!!

Thank you - been looking for a way to do this for some time. I added a sleep 5 to the wireless enable/disable as was not always seeing the power get switched back on to the wireless card after it was switched off. Now it works on all our hardware. Many thanks.

WUSLS
New Contributor

THANK YOU!!!!!!!!!!!!!!!!! I WILL BE IMPLEMENTING TOMORROW!!! I have users that have been driving me mad with OS X's Wifi lists. I can't wait to run it.

kirk_magill
New Contributor

12/22/15 at 12:54 PM by Anthonyr
Your solution also worked for me!
Thanks!

Michael_Scarbor
New Contributor

This thing worked like a champ on 10.12. Thank you so much!!

jonathan_rudge
New Contributor III

Legend! Script works perfectly! Thank you!

Bhughes
Contributor

This is great! Exactly what I needed! Thanks!

sdagley
Honored Contributor III

I tweaked pudquick's script so that you just have to edit the array of preferred SSIDs rather than modify the code if you have more than 3 in your environment:

#!/usr/bin/python

# As written, this requires the following:
# - OS X 10.6+ (has been reported working through 10.12.3)
# - python 2.6 or 2.7 (for collections.namedtuple usage, should be fine as default python in 10.6 is 2.6)
# - pyObjC (as such, recommended to be used with native OS X python install)

# Run with root

import objc, ctypes.util, os.path, collections
from Foundation import NSOrderedSet

# List of preferred SSIDs in priority order - edit/add/delete as needed
PreferredSSIDs = ["SSID_1", "SSID_2", "SSID_3"]

def load_objc_framework(framework_name):
    # Utility function that loads a Framework bundle and creates a namedtuple where the attributes are the loaded classes from the Framework bundle
    loaded_classes = dict()
    framework_bundle = objc.loadBundle(framework_name, bundle_path=os.path.dirname(ctypes.util.find_library(framework_name)), module_globals=loaded_classes)
    return collections.namedtuple('AttributedFramework', loaded_classes.keys())(**loaded_classes)

# Load the CoreWLAN.framework (10.6+)
CoreWLAN = load_objc_framework('CoreWLAN')

# Load all available wifi interfaces
interfaces = dict()
for i in CoreWLAN.CWInterface.interfaceNames():
    interfaces[i] = CoreWLAN.CWInterface.interfaceWithName_(i)

# Repeat the configuration with every wifi interface
for i in interfaces.keys():
    # Grab a mutable copy of this interface's configuration
    configuration_copy = CoreWLAN.CWMutableConfiguration.alloc().initWithConfiguration_(interfaces[i].configuration())
    # Find all the preferred/remembered network profiles
    profiles = list(configuration_copy.networkProfiles())
    # Grab all the SSIDs, in order
    SSIDs = [x.ssid() for x in profiles]
    # Loop through PreferredSSIDs list in reverse order sorting each entry to the front of profiles array so it
    # ends up sorted with PreferredSSIDs as the first items.
    # Order is preserved for other SSIDs, example where PreferredSSIDs is [ssid3, ssid4]:
    #    Original: [ssid1, ssid2, ssid3, ssid4]
    #   New order: [ssid3, ssid4, ssid1, ssid2]
    for aSSID in reversed(PreferredSSIDs):
        profiles.sort(key=lambda x: x.ssid() == aSSID, reverse=True)
    # Now we have to update the mutable configuration
    # First convert it back to a NSOrderedSet
    profile_set = NSOrderedSet.orderedSetWithArray_(profiles)
    # Then set/overwrite the configuration copy's networkProfiles
    configuration_copy.setNetworkProfiles_(profile_set)
    # Then update the network interface configuration
    result = interfaces[i].commitConfiguration_authorization_error_(configuration_copy, None, None)

MatG
Contributor III

This script it great :)

I'd like to be able to set the SSID in JSS. I know that this can be done by using $4, $5, $6 etc but what else is required in the script to get this to work?

# List of preferred SSIDs in priority order - edit/add/delete as needed
PreferredSSIDs = ["$4", "$5", "$6"]

sdagley
Honored Contributor III

@MatG Sorry, didn't notice your question earlier. The $4, $5, and $6 parameters are directly accessible as variables in a shell bash script, but in Python you access them by using sys.argv[parameter#]. See this post for more info: Adding a Python Script to JSS

apizz
Valued Contributor

This is great! Just tested this and confirmed on a 10.12.5 machine that our school's Wi-Fi network successfully is moved to the top while the rest of the Wi-Fi network order remains the same.

rqomsiya
Contributor III

Hi all,

I'm getting this error when its ran on some machines, but not all:

[STEP 1 of 4]
Executing Policy Fix Wi-Fi network order
[STEP 2 of 4]
Running script Set_Wifi_Order.sh...
Script exit code: 1
Script result: Traceback (most recent call last):
File "/Library/Application Support/JAMF/tmp/Set_Wifi_Order.sh", line 35, in 
profiles = list(configuration_copy.networkProfiles())
TypeError: 'NoneType' object is not iterable
Error running script: return code was 1.
[STEP 3 of 4]
[STEP 4 of 4]

Any ideas? I only have one SSID listed:

#!/usr/bin/python

# As written, this requires the following:
# - OS X 10.6+ (has been reported working through 10.12.3)
# - python 2.6 or 2.7 (for collections.namedtuple usage, should be fine as default python in 10.6 is 2.6)
# - pyObjC (as such, recommended to be used with native OS X python install)

# Run with root

import objc, ctypes.util, os.path, collections
from Foundation import NSOrderedSet

# List of preferred SSIDs in priority order - edit/add/delete as needed
PreferredSSIDs = ["ILAnetwork"]

def load_objc_framework(framework_name):
    # Utility function that loads a Framework bundle and creates a namedtuple where the attributes are the loaded classes from the Framework bundle
    loaded_classes = dict()
    framework_bundle = objc.loadBundle(framework_name, bundle_path=os.path.dirname(ctypes.util.find_library(framework_name)), module_globals=loaded_classes)
    return collections.namedtuple('AttributedFramework', loaded_classes.keys())(**loaded_classes)

# Load the CoreWLAN.framework (10.6+)
CoreWLAN = load_objc_framework('CoreWLAN')

# Load all available wifi interfaces
interfaces = dict()
for i in CoreWLAN.CWInterface.interfaceNames():
    interfaces[i] = CoreWLAN.CWInterface.interfaceWithName_(i)

# Repeat the configuration with every wifi interface
for i in interfaces.keys():
    # Grab a mutable copy of this interface's configuration
    configuration_copy = CoreWLAN.CWMutableConfiguration.alloc().initWithConfiguration_(interfaces[i].configuration())
    # Find all the preferred/remembered network profiles
    profiles = list(configuration_copy.networkProfiles())
    # Grab all the SSIDs, in order
    SSIDs = [x.ssid() for x in profiles]
    # Loop through PreferredSSIDs list in reverse order sorting each entry to the front of profiles array so it
    # ends up sorted with PreferredSSIDs as the first items.
    # Order is preserved for other SSIDs, example where PreferredSSIDs is [ssid3, ssid4]:
    #    Original: [ssid1, ssid2, ssid3, ssid4]
    #   New order: [ssid3, ssid4, ssid1, ssid2]
    for aSSID in reversed(PreferredSSIDs):
        profiles.sort(key=lambda x: x.ssid() == aSSID, reverse=True)
    # Now we have to update the mutable configuration
    # First convert it back to a NSOrderedSet
    profile_set = NSOrderedSet.orderedSetWithArray_(profiles)
    # Then set/overwrite the configuration copy's networkProfiles
    configuration_copy.setNetworkProfiles_(profile_set)
    # Then update the network interface configuration
    result = interfaces[i].commitConfiguration_authorization_error_(configuration_copy, None, None)

bearzooka
Contributor

@rqomsiya Sounds to me like those computers either don't have the SSID (profile) configured or don't have a WIFI interface.

shcsdrce
New Contributor

sdagley's version of the script works perfectly on OSX 11.x

dstranathan
Valued Contributor II

FYI: In Big Sur, Apple moved the known networks from /Library/Preferences/SystemConfiguration/com.apple.airport.preferences.plist to /Library/Preferences/com.apple.wifi.known-networks.plist file.

GabeShack
Valued Contributor III

Anyone still using a script to manage order of preferred wireless networks in MacOS 11 or above?  If so is it working?

Gabe Shackney
Princeton Public Schools

MatG
Contributor III
#!/bin/bash
##############################################################
#  Script     : Orders Mac Wi-Fi
##############################################################

# To order we remove and replace. Replace is always top of the list
SSID=$4
Security_Type=$5

networksetup -removepreferredwirelessnetwork en0 "$SSID"
networksetup -addpreferredwirelessnetworkatindex en0 "$SSID" 0 $Security_Type

We use this script and set the S4 & S5 values in the Script Parameters in the Script Payload of the policy.
The script removes and adds and when adding the network goes to the top of the list, even if you are connected to the Wi-Fi network being removed and added we see no loss of connection.
Screenshot 2022-01-21 at 14.12.45.png

sdagley
Honored Contributor III

@MatG That will break any 802.1x configuration that was in effect for the SSID.

MatG
Contributor III

@sdagley We have it in place on our Monterey devices and its not causing any issues and was thoroughly tested with numerous devices. We are not getting any reports of loss of connection.

 

sdagley
Honored Contributor III

@MatG And you're using 802.1x auth? That's good to hear as it definitely didn't work through macOS Sierra, and that's what created the need for the Python scripts posted below. 

dstranathan
Valued Contributor II

My policy/script works on Monterey, Big Sur and Catalina laptops.

Our policy runs once a day as needed (at check-in) and is scoped to only Macs laptops that do not already have our WLAN SSID in the top "favorites" location (index 0 of the array). This verification is performed via an EA.

The policy payload is a Z Shell script. It runs well on both Intel and Apple ARM CPUs and all our supported OSs.

We leverage 802.1x (EAP-TLS) and we use our JSS as a SCEP proxy server for machine certs issued by our interal ADCS.

Our RADIUS WALN security is a Cisco ISE appliance running 3.0.0.458

@dstranathan have you already shared the script somewhere, this sounds good, with python going away in 12.3 we're looking at alternatives

dstranathan
Valued Contributor II

I have been working on updating mine. Here is the latest Python3 version I am testing. This version requires MacAdmins Managed Python (it includes PyObjC).

#!/Library/ManagedFrameworks/Python/Python3.framework/Versions/Current/bin/python3
import objc, ctypes.util, os.path, collections
from Foundation import NSOrderedSet
preferred_SSID = 'MY_CORP_WLAN' ## Put your SSID here
def load_objc_framework(framework_name):
loaded_classes = dict()
framework_bundle = objc.loadBundle(framework_name, bundle_path=os.path.dirname(ctypes.util.find_library(framework_name)), module_globals=loaded_classes)
loaded_classes = dict(x for x in loaded_classes.items() if (not x[0].startswith('_')))
return collections.namedtuple('AttributedFramework', loaded_classes.keys())(**loaded_classes)
CoreWLAN = load_objc_framework('CoreWLAN')
interfaces = dict()
for i in CoreWLAN.CWInterface.interfaceNames():
interfaces[i] = CoreWLAN.CWInterface.interfaceWithName_(i)
for i in interfaces.keys():
configuration_copy = CoreWLAN.CWMutableConfiguration.alloc().initWithConfiguration_(interfaces[i].configuration())
profiles = list(configuration_copy.networkProfiles().array())
SSIDs = [x.ssid() for x in profiles]
if (preferred_SSID in SSIDs):
profiles.sort(key=lambda x: x.ssid() == preferred_SSID, reverse=True)
profile_set = NSOrderedSet.orderedSetWithArray_(profiles)
configuration_copy.setNetworkProfiles_(profile_set)
result = interfaces[i].commitConfiguration_authorization_error_(configuration_copy, None, None)
else:
print("\nThe SSID '{}' was not located in the macOS Preferred Networks list.".format(preferred_SSID))

MikeT
New Contributor III

Thanks for the info and work on this, have a great weekend!

Would you mind sharing your EA as well?

Rokas
New Contributor III

Hi @dstranathan 

I've installed python from https://github.com/macadmins/python, but then trying to run script still getting errors:

/wifi_prefered.sh: line 2: import: command not found
from: can't read /var/mail/Foundation
/wifi_prefered.sh: line 4: preferred_SSID: command not found
/wifi_prefered.sh: line 5: syntax error near unexpected token `('
/wifi_prefered.sh: line 5: `def load_objc_framework(framework_name):'

 

dstranathan
Valued Contributor II

Ill have to dig into this. I just tested it on Catalina, Big Sur and Monterey and it is working here. Im not a 'Python guy' at all (trying to get approval for more formal classes in 2022 actually!).

Some of this script is based on Michael Lynn's efforts: https://gist.github.com/pudquick/fcbdd3924ee230592ab4

tkimpton
Valued Contributor II

@dstranathan im getting the following error

 

File "/private/tmp/test4.sh", line 6

    loaded_classes = dict()

    ^

IndentationError: expected an indented block

dstranathan
Valued Contributor II

Here is my EA. It is intended to target laptops only (we dont support Wi-fi on dekstops here - Ethernet only), so you can remove or retool the script to better fir your org's needs.

 

#!/bin/zsh
#verify_ssid-EA.sh
RESULT="Unknown"
LAPTOP=$( system_profiler SPHardwareDataType | grep "Model Identifier" | grep "Book" )
if [[ -z "${LAPTOP}" ]]; then
RESULT="N/A"
else
SSID_INDEX=$( networksetup -listpreferredwirelessnetworks en0 | awk '/PUT_YOUR_SSID_HERE/{print NR-2}' )
if [[ -z "$SSID_INDEX" ]]; then
RESULT="Missing"
else
RESULT=${SSID_INDEX}
fi
fi
echo "<result>${RESULT}</result>"
exit 0