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

dstranathan
Valued Contributor II

I also have a zsh version that can be used instead of Python. I havent used it in a while but just tested it on a Monterey 12.3 MacBook with my target SID at the bottom of the array list and the script moved it back to the top as expected.

This version is failry verbose with several optional QA functions that can report on the status of the target Mac's such as model (laptop or desktop), if Wi-Fi is enabled at exectution time, What SSIDs are in the Preferred Network array, etc. You can remove thse QA functions if needed. I like them for troubleshooting logs etc. I found the info handy.

Like my EA (above), this script is intended for only laptops at my org, and will exit 1 on if it runs on a desktop (which never happens because the script's parent policy is scoped to a Smart Group that contains only laptops,  making it nearly impossible to ever encounter a desktop if scoped correctly - but its a nice 'saftey net'), so you may need to remove this logic if its not suitable for your org.

 

#!/bin/zsh

################################################################################
#  Global Variables
################################################################################

SCRIPT_NAME="configure_preferred_networks_ssid.sh"
COMPUTER_NAME=$( scutil --get LocalHostName )
SSID_NAME=YOUR_ORGS_SSID_HERE
WIFI_INTERFACE=$( networksetup -listallhardwareports | awk '/Wi-Fi/{getline; print $NF}' ) # Verify the target Mac's Wi-Fi interface.
SSID_LIST=$( networksetup -listpreferredwirelessnetworks ${WIFI_INTERFACE} | sed 's/^[    ]*//g;1d' ) # Verify the entire list of SSIDs in Preferred Networks for Wi-Fi interface.

################################################################################
#  Functions
################################################################################

function QA_VERIFY_HARDWARE_TYPE() {

    HARDWARE_TYPE=$( system_profiler SPHardwareDataType | grep "Model Identifier" | grep "Book" )
    
    if [ "$HARDWARE_TYPE" != "" ]; then
        echo "QA: This computer is a laptop."
    else
        echo "QA: This computer is a DESKTOP. This script is intended for laptops only. No action is required."
        # exit 1 here because the parent policy should already be targeting laptops only (no desktops in the scope). So if this script runs on a desktop...something is wrong.
        exit 1
    fi

}


function QA_VERIFY_WIFI_INTERFACE() {
    
    echo "QA: This computer's Wi-Fi is at interface '${WIFI_INTERFACE}'."

}


function QA_VERIFY_WIFI_STATE() {

     VERIFY_WIFI_STATE=$( networksetup -getairportpower ${WIFI_INTERFACE} | grep On )

    if [[ $VERIFY_WIFI_STATE ]]; then
        echo "QA: This computer's Wi-fi radio is currently ON."
    else
        echo "QA: This computer's Wi-fi radio is currently OFF."
    fi
}


function QA_PREFFERED_NETWORKS_TOTAL() {

    # Strip out (1) header string (Preferred networks on en0:) to get actual line count of SSIDs
    
    PREFERRED_NETWORKS_TOTAL=$( networksetup -listpreferredwirelessnetworks ${WIFI_INTERFACE} | sed '1d' | wc -l | awk '{$1=$1;print}' )
    PREFERRED_NETWORKS_TOTAL=$((PREFERRED_NETWORKS_TOTAL-1))
    echo "QA: This computer currently has a total of (${PREFERRED_NETWORKS_TOTAL}) SSIDs in the Preferred Networks array."
       
}


function QA_VERIFY_CURRENT_SSID() {

    # If not connected to any SSID, macOS will report 'You are not associated with an AirPort network (The string 'available' will be in the 4th column).
    
    CURRENT_CONNECTED_SSID=$( networksetup -getairportnetwork ${WIFI_INTERFACE} | cut -d " " -f 4 )
    
    if [[ $CURRENT_CONNECTED_SSID == *"associated"* ]]; then
        echo "QA: This computer is not currently connected to a Wi-fi SSID."
    else
        echo "QA: This commputer is currently connected to SSID '${CURRENT_CONNECTED_SSID}'."
    fi

}


function VERIFY_SSID() {

    if [[ "$SSID_LIST" =~ "$SSID_NAME" ]]; then
         
        echo
        echo "The SSID '${SSID_NAME}' is already in the Preferred Networks list..."
        VERIFY_SSID_INDEX
    else
        echo
        echo "The SSID '${SSID_NAME}' is NOT currently in the Preferred Networks list. It will be added (at index 0) next..."
        ADD_SSID
    fi
    
 }


function VERIFY_SSID_INDEX() {

    SSID_INDEX_NUMBER=$( networksetup -listpreferredwirelessnetworks ${WIFI_INTERFACE} | awk '/YOUR_ORGS_SSID_HERE/{print NR-2}' )
    
    if [[ "$SSID_INDEX_NUMBER" -eq 0 ]]; then
        echo
        echo "The SSID '${SSID_NAME}' is already located at index number ${SSID_INDEX_NUMBER}. No action is required."
        exit 0
    else
        echo
        echo "The SSID '${SSID_NAME}' is located at index number ${SSID_INDEX_NUMBER}. It will need to be removed and re-added to index 0..."
        REMOVE_SSID
    fi

}


function REMOVE_SSID() {
    
    echo
    echo "Removing the SSID '${SSID_NAME}' from the Preferred Networks list..."
    networksetup -removepreferredwirelessnetwork "${WIFI_INTERFACE}" "${SSID_NAME}" &>/dev/null
    ADD_SSID
        
}


function ADD_SSID() {

    SSID_INDEX=0
    SSID_SECURITY_MODE=WPA2E
    
    echo
    echo "Adding the SSID '${SSID_NAME}' to the Preferred Networks list (at index 0)..."
    networksetup -addpreferredwirelessnetworkatindex "${WIFI_INTERFACE}" "${SSID_NAME}" "${SSID_INDEX}" "${SSID_SECURITY_MODE}" &>/dev/null
    if [[ "$?" == 0 ]]; then
        echo
        echo "The SSID '${SSID_NAME}' was successfully added to the Preferred Networks list (at index 0)."
    exit 0
    else
        echo
        echo "ERROR: Unable to add the SSID '${SSID_NAME}' to the Preferred Networks list."
        exit 1
    fi
    
 }
 
function MAIN() {

    QA_VERIFY_HARDWARE_TYPE

    QA_VERIFY_WIFI_INTERFACE

    QA_VERIFY_WIFI_STATE

    QA_VERIFY_CURRENT_SSID

    QA_PREFFERED_NETWORKS_TOTAL

    VERIFY_SSID

}
 

################################################################################
#  Main
################################################################################
MAIN

 





elliots
New Contributor II

Hi @dstranathan,

I'm just wondering if you could share some specifics regarding how your 802.1x clients are configured to connect to your network (PEAP/TLS etc) as i'm having a challenge with my setup.

We use 802.1x EAP-TLS (computer) certificate authentication on our devices and after running this script, it did raise the wifi SSID to the top of the preferred list, but after a reboot of the macbook (or toggling the WiFi off and on), my test device prompted the user to select the certificate to use to connect to the wifi, which probably is not ideal.

elliots_0-1648548169730.png

whats weird is if you click cancel on the x3 prompts and then manually connect again by select the network from the drop down, it connects no questions asked. 

I'm hoping someone else has an ideas on what do do here.

Thanks,

Elliot

dstranathan
Valued Contributor II

Elliot

Im using 802.1x (our JSS is also a SCEP proxy for our ADCS environment) with EAP-TLS and Cisco ISE (WPA2 Enterprise).

The process of reordering the SSID position in the Preferred Networks arry doesnt technically move it, it deletes it and re-adds it. Maybe macOS security is smart enough to detect this change, and the SSID loses it pairing to the machine cert for 802.1x and thus prompts for a user to "rebind" it as a safety measure?

Ill do more testing soon on a couple laptops on my WLAN and see if I can reproduce this with any version (zsh/python3) of my scripts.



sdagley
Honored Contributor III

Just to clarify the behavior of the scripts in this discussion...

The python2 script that pudquick originally wrote, and I later modified, does NOT delete and re-add SSIDs to sort the preferred SSID array order, and does not remove the 802.1x security configuration associated with the SSID.

@dstranathan 's zsh script DOES remove remove and re-add the SSIDs, so it should NOT be used if you're dealing with SSIDs that use 802.1x auth. You should instead look at his version of the python script which has been updated for python3 (https://community.jamf.com/t5/jamf-pro/re-order-wifi-preferred-networks/m-p/259857/highlight/true#M2...)

dstranathan
Valued Contributor II

Thank you for clarification @sdagley 

sdagley
Honored Contributor III

Ok, here's my variant of the original pudquick python script that handles a variable number of SSIDs to move to the top of the preferred SSIDs list. Thanks to @dstranathan for posting the Python 3'ified version of pudquick's script as an example of the needed changes.

I do not have ready access to a SSID using 802.1x auth to test, but I have verified the 802.1x configuration associated with a SSID is preserved after running this script. Apologies for the line wrapping, but have I mentioned how much I dislike the code formatting options with the new forum software?

 

#!/Library/ManagedFrameworks/Python/Python3.framework/Versions/Current/bin/python3

# As written, this requires the following:
# - OS X 10.6+ (has been reported working through macOS 12.3)
# - The "Recommended" Python 3 framework from https://github.com/macadmins/python
#		installed in the path /Library/ManagedFrameworks/Python/

# Run as root - running at user level will not save the sorted SSID list

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)
    # Exclude class entries that start with an underscore
    loaded_classes = dict(x for x in loaded_classes.items() if (not x[0].startswith('_')))
    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().array())
    # 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)