Created
September 30, 2026 09:22
-
-
Save pelwell/7712f2f734748e1aa36eb4b1919f72b5 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/bin/bash | |
| # wifi-sec-info - summarise Wi-Fi security capabilities and the current connection | |
| # | |
| # Pulls together iw, wpa_cli, nmcli and iwlist output to show: | |
| # - the authentication mechanisms/features each local Wi-Fi interface supports | |
| # - the AP each interface is connected to, and what that AP advertises | |
| # - the authentication mechanism actually used for the current connection | |
| # | |
| # Usage: wifi-sec-info [interface...] (default: all wireless interfaces) | |
| # | |
| # Root is not required if the user is in the netdev group (for wpa_cli). | |
| PATH=$PATH:/usr/sbin:/sbin | |
| have() { command -v "$1" >/dev/null 2>&1; } | |
| hdr() { printf '\n=== %s ===\n' "$*"; } | |
| sub() { printf '\n -- %s --\n' "$*"; } | |
| kv() { [ -n "$2" ] && printf ' %-24s %s\n' "$1:" "$2"; } | |
| yn() { if eval "$2"; then kv "$1" yes; else kv "$1" no; fi; } | |
| has() { grep -qE -- "$1" <<<"$2"; } | |
| # Join words onto one line (xargs would choke on quotes, e.g. EAP method AKA') | |
| words() { tr -s ' \t\n' ' ' | sed 's/^ //; s/ $//'; } | |
| T=$'\t' | |
| if ! have iw; then | |
| echo "iw not found - install the 'iw' package" >&2 | |
| exit 1 | |
| fi | |
| if [ $# -gt 0 ]; then | |
| IFACES="$*" | |
| else | |
| IFACES=$(for d in /sys/class/net/*; do | |
| [ -e "$d/phy80211" ] && basename "$d" | |
| done) | |
| fi | |
| if [ -z "$IFACES" ]; then | |
| echo "No wireless interfaces found" >&2 | |
| exit 1 | |
| fi | |
| # Print the value of field $2 from IE section $1 (RSN or WPA) of an iw BSS dump | |
| ie_field() { | |
| awk -v s="$1" -v f="$2" ' | |
| $0 ~ "^\t" s ":" { p = 1 } | |
| p && /^\t[^\t]/ && $0 !~ "^\t" s ":" { p = 0 } | |
| p { | |
| l = $0 | |
| sub(/^[ \t]*([A-Z]+:)?[ \t]*\* /, "", l) | |
| if (index(l, f ": ") == 1) { print substr(l, length(f) + 3); exit } | |
| }' | |
| } | |
| # Print the items of an iw phy list block whose header matches $1 | |
| phy_list() { | |
| awk -v h="$1" ' | |
| $0 ~ "^\t" h { p = 1; next } | |
| p && /^\t[^\t]/ { exit } | |
| p && /\* / { sub(/^[ \t]*\* */, ""); print }' | |
| } | |
| # Translate an RSN AKM suite name to a friendly description | |
| akm_desc() { | |
| case "$1" in | |
| PSK) echo "WPA2-Personal (PSK)" ;; | |
| PSK/SHA-256) echo "WPA2-Personal (PSK-SHA256)" ;; | |
| FT/PSK) echo "WPA2-Personal with Fast Transition" ;; | |
| SAE) echo "WPA3-Personal (SAE)" ;; | |
| FT/SAE) echo "WPA3-Personal with Fast Transition" ;; | |
| SAE-EXT-KEY|00-0f-ac:24) echo "WPA3-Personal (SAE-EXT-KEY)" ;; | |
| FT/SAE-EXT-KEY|00-0f-ac:25) echo "WPA3-Personal (FT SAE-EXT-KEY)" ;; | |
| "IEEE 802.1X") echo "WPA2-Enterprise (802.1X)" ;; | |
| "IEEE 802.1X/SHA-256") echo "WPA2/3-Enterprise (802.1X-SHA256)" ;; | |
| "FT/IEEE 802.1X") echo "Enterprise with Fast Transition" ;; | |
| "IEEE 802.1X/SUITE-B") echo "WPA3-Enterprise Suite-B" ;; | |
| "IEEE 802.1X/SUITE-B-192") echo "WPA3-Enterprise 192-bit" ;; | |
| "FT/IEEE 802.1X/SHA-384") echo "Enterprise 192-bit with Fast Transition" ;; | |
| OWE) echo "Enhanced Open (OWE)" ;; | |
| FILS*) echo "FILS ($1)" ;; | |
| *) echo "$1" ;; | |
| esac | |
| } | |
| # Translate a wpa_supplicant key_mgmt status value to a friendly description | |
| keymgmt_desc() { | |
| case "$1" in | |
| WPA-PSK) echo "WPA1-Personal (PSK) - legacy" ;; | |
| WPA2-PSK) echo "WPA2-Personal (PSK)" ;; | |
| WPA2-PSK-SHA256) echo "WPA2-Personal (PSK-SHA256)" ;; | |
| FT-PSK) echo "WPA2-Personal with Fast Transition" ;; | |
| SAE) echo "WPA3-Personal (SAE)" ;; | |
| FT-SAE) echo "WPA3-Personal with Fast Transition" ;; | |
| SAE-EXT-KEY) echo "WPA3-Personal (SAE-EXT-KEY)" ;; | |
| *SUITE-B-192*) echo "WPA3-Enterprise 192-bit" ;; | |
| *SUITE-B*) echo "WPA3-Enterprise Suite-B" ;; | |
| WPA/IEEE\ 802.1X/EAP) echo "WPA1-Enterprise (802.1X) - legacy" ;; | |
| *802.1X*|*EAP*) echo "WPA2/3-Enterprise (802.1X)" ;; | |
| OWE) echo "Enhanced Open (OWE)" ;; | |
| NONE) echo "Open / WEP (no key management)" ;; | |
| *) echo "$1" ;; | |
| esac | |
| } | |
| sae_group_desc() { | |
| case "$1" in | |
| 19) echo "19 (NIST P-256)" ;; | |
| 20) echo "20 (NIST P-384)" ;; | |
| 21) echo "21 (NIST P-521)" ;; | |
| *) echo "$1" ;; | |
| esac | |
| } | |
| # Decode the first octet of an RSNX element (hex string) | |
| rsnx_desc() { | |
| local v=$((16#$1)) out="" | |
| ((v & 0x10)) && out+="Protected-TWT " | |
| ((v & 0x20)) && out+="SAE-H2E " | |
| ((v & 0x40)) && out+="SAE-PK " | |
| ((v & 0x80)) && out+="Protected-WUR " | |
| echo "${out% }" | sed "s/^$/none/" | |
| } | |
| declare -A PHY_DONE | |
| for IF in $IFACES; do | |
| if [ ! -e "/sys/class/net/$IF/phy80211" ]; then | |
| echo "$IF: not a wireless interface" >&2 | |
| continue | |
| fi | |
| PHY=$(basename "$(readlink -f "/sys/class/net/$IF/phy80211")") | |
| DRV=$(basename "$(readlink -f "/sys/class/net/$IF/device/driver")" 2>/dev/null) | |
| PHYINFO=$(iw phy "$PHY" info 2>/dev/null) | |
| IFINFO=$(iw dev "$IF" info 2>/dev/null) | |
| hdr "Interface $IF ($PHY, driver ${DRV:-unknown})" | |
| kv "MAC address" "$(cat "/sys/class/net/$IF/address")" | |
| kv "Mode" "$(awk '$1 == "type" { $1 = ""; print substr($0, 2) }' <<<"$IFINFO")" | |
| # ---- Local capabilities (driver/hardware and supplicant) ---- | |
| CIPHERS=$(phy_list "Supported Ciphers" <<<"$PHYINFO" | awk '{ print $1 }' | words) | |
| EXTFEAT=$(phy_list "Supported extended features" <<<"$PHYINFO" | | |
| sed -n 's/^\[ \(.*\) \].*/\1/p' | words) | |
| AUTHFEAT=$(tr ' ' '\n' <<<"$EXTFEAT" | | |
| grep -E 'SAE|4WAY|BEACON_PROT|OWE|FILS|MFP|CONTROL_PORT|AKM|SECURE|PROT' | words) | |
| DRV_SAE= | |
| if grep -q "Device supports SAE with AUTHENTICATE" <<<"$PHYINFO"; then | |
| DRV_SAE="host SAE (AUTHENTICATE cmd)" | |
| fi | |
| has 'SAE_OFFLOAD( |$)' "$EXTFEAT" && DRV_SAE="${DRV_SAE:+$DRV_SAE, }SAE offload" | |
| if [ -z "${PHY_DONE[$PHY]}" ]; then | |
| PHY_DONE[$PHY]=1 | |
| sub "Local hardware/driver capabilities ($PHY)" | |
| kv "Ciphers" "$CIPHERS" | |
| kv "SAE support" "${DRV_SAE:-none advertised}" | |
| kv "Auth-related features" "$AUTHFEAT" | |
| AKMS=$(phy_list "Supported AKM suites" <<<"$PHYINFO" | words) | |
| kv "Driver AKM suites" "$AKMS" | |
| kv "Interface modes" "$(phy_list "Supported interface modes" <<<"$PHYINFO" | words)" | |
| awk ' | |
| function flush() { | |
| if (band == "") return | |
| s = "802.11" (bn == "2.4 GHz" ? "b/g" : "a") | |
| if (ht) s = s "/n" | |
| if (vht) s = s "/ac" | |
| if (he) s = s "/ax" | |
| if (eht) s = s "/be" | |
| gen = eht ? "Wi-Fi 7" : he ? (bn == "6 GHz" ? "Wi-Fi 6E" : "Wi-Fi 6") : \ | |
| vht ? "Wi-Fi 5" : ht ? "Wi-Fi 4" : "legacy" | |
| printf " %-24s %s (%s)\n", "Band " bn ":", s, gen | |
| } | |
| /^\tBand [0-9]+:/ { flush(); band = $2; bn = ""; ht = vht = he = eht = 0; next } | |
| /^\t[^\t]/ { flush(); band = "" } | |
| band == "" { next } | |
| /^\t\tCapabilities: 0x/ { ht = 1 } | |
| /VHT Capabilities/ { vht = 1 } | |
| /HE Iftypes/ { he = 1 } | |
| /EHT Iftypes/ { eht = 1 } | |
| bn == "" && /\* [0-9.]+ MHz/ { | |
| f = $2 + 0 | |
| bn = f < 3000 ? "2.4 GHz" : f < 5925 ? "5 GHz" : f < 7200 ? "6 GHz" : "60 GHz" | |
| } | |
| END { flush() }' <<<"$PHYINFO" | |
| fi | |
| WPA_OK= | |
| if have wpa_cli && wpa_cli -i "$IF" ping 2>/dev/null | grep -q PONG; then | |
| WPA_OK=1 | |
| wcap() { wpa_cli -i "$IF" get_capability "$1" 2>/dev/null | words; } | |
| KM_CAP=$(wcap key_mgmt) | |
| # Some drivers (e.g. brcmfmac) only advertise SAE via the nl80211 | |
| # feature flag, which wpa_supplicant doesn't reflect here | |
| if [ -n "$DRV_SAE" ] && ! has '(^| )SAE( |$)' "$KM_CAP"; then | |
| KM_CAP="$KM_CAP (+SAE via driver)" | |
| fi | |
| sub "Supplicant capabilities ($(wpa_supplicant -v 2>/dev/null | awk 'NR == 1 { print $2 }'))" | |
| kv "Key management" "$KM_CAP" | |
| kv "Protocols" "$(wcap proto)" | |
| kv "Pairwise ciphers" "$(wcap pairwise)" | |
| kv "Group ciphers" "$(wcap group)" | |
| kv "Mgmt group ciphers" "$(wcap group_mgmt)" | |
| kv "Auth algorithms" "$(wcap auth_alg)" | |
| kv "EAP methods" "$(wcap eap)" | |
| fi | |
| if have iwlist; then | |
| WEXT=$(iwlist "$IF" auth 2>/dev/null | awk 'NR > 1 && NF { print $1 }' | words) | |
| [ -n "$WEXT" ] && { sub "Legacy WEXT view (iwlist auth)"; kv "Auth capabilities" "$WEXT"; } | |
| fi | |
| sub "Security modes this device can use" | |
| yn "WPA2-Personal" 'has "CCMP" "$CIPHERS"' | |
| yn "WPA3-Personal (SAE)" '[ -n "$DRV_SAE" ] || has "(^| )SAE( |$)" "$KM_CAP"' | |
| if [ -n "$WPA_OK" ]; then | |
| yn "WPA2/3-Enterprise" 'has "WPA-EAP" "$KM_CAP"' | |
| yn "WPA3-Ent 192-bit" 'has "GCMP-256" "$CIPHERS" && has "SUITE-B-192" "$KM_CAP"' | |
| yn "Enhanced Open (OWE)" 'has "OWE" "$KM_CAP" && has "CMAC|GMAC" "$CIPHERS"' | |
| fi | |
| yn "PMF / 802.11w" 'has "CMAC|GMAC" "$CIPHERS"' | |
| yn "Beacon protection" 'has "BEACON_PROTECTION" "$EXTFEAT"' | |
| yn "Legacy TKIP/WEP" 'has "TKIP|WEP" "$CIPHERS"' | |
| # ---- Associated AP ---- | |
| LINK=$(iw dev "$IF" link 2>/dev/null) | |
| BSSID=$(awk '/^Connected to/ { print $3 }' <<<"$LINK") | |
| if [ -z "$BSSID" ]; then | |
| sub "Not connected" | |
| continue | |
| fi | |
| sub "Connected AP" | |
| kv "SSID" "$(sed -n 's/^\tSSID: //p' <<<"$LINK")" | |
| kv "BSSID" "$BSSID" | |
| kv "Channel" "$(sed -n 's/^\tchannel \(.*\)/\1/p' <<<"$IFINFO")" | |
| kv "Signal" "$(sed -n 's/^\tsignal: //p' <<<"$LINK")" | |
| kv "RX bitrate" "$(sed -n 's/^\trx bitrate: //p' <<<"$LINK")" | |
| kv "TX bitrate" "$(sed -n 's/^\ttx bitrate: //p' <<<"$LINK")" | |
| BSS=$(iw dev "$IF" scan dump -u 2>/dev/null | | |
| awk -v b="$BSSID" 'tolower($0) ~ "^bss " b { p = 1; next } /^BSS / { p = 0 } p') | |
| if [ -z "$BSS" ] && [ "$(id -u)" = 0 ]; then | |
| # Scan results may have expired - try a fresh scan | |
| BSS=$(timeout 20 iw dev "$IF" scan -u 2>/dev/null | | |
| awk -v b="$BSSID" 'tolower($0) ~ "^bss " b { p = 1; next } /^BSS / { p = 0 } p') | |
| fi | |
| if [ -z "$BSS" ]; then | |
| kv "Advertised security" "unknown - no scan result for this BSS (re-run as root to rescan)" | |
| else | |
| sub "AP advertised security and features" | |
| RSN_AKM=$(ie_field RSN "Authentication suites" <<<"$BSS") | |
| RSN_CAP=$(ie_field RSN "Capabilities" <<<"$BSS") | |
| WPA_AKM=$(ie_field WPA "Authentication suites" <<<"$BSS") | |
| if [ -n "$RSN_AKM" ]; then | |
| kv "RSN (WPA2/3) AKMs" "$RSN_AKM" | |
| for a in $(sed 's/IEEE 802.1X/IEEE_802.1X/g' <<<"$RSN_AKM"); do | |
| kv " offers" "$(akm_desc "${a//_/ }")" | |
| done | |
| kv "RSN pairwise ciphers" "$(ie_field RSN "Pairwise ciphers" <<<"$BSS")" | |
| kv "RSN group cipher" "$(ie_field RSN "Group cipher" <<<"$BSS")" | |
| kv "RSN mgmt group cipher" "$(ie_field RSN "Group mgmt cipher suite" <<<"$BSS")" | |
| if has "MFP-required" "$RSN_CAP"; then | |
| kv "PMF (802.11w)" "required" | |
| elif has "MFP-capable" "$RSN_CAP"; then | |
| kv "PMF (802.11w)" "optional" | |
| else | |
| kv "PMF (802.11w)" "not supported" | |
| fi | |
| fi | |
| if [ -n "$WPA_AKM" ]; then | |
| kv "WPA1 AKMs (legacy)" "$WPA_AKM" | |
| kv "WPA1 ciphers" "$(ie_field WPA "Pairwise ciphers" <<<"$BSS")" | |
| fi | |
| if [ -z "$RSN_AKM" ] && [ -z "$WPA_AKM" ]; then | |
| if grep -q "^${T}capability:.*Privacy" <<<"$BSS"; then | |
| kv "Security" "WEP (legacy)" | |
| else | |
| kv "Security" "Open (no encryption)" | |
| fi | |
| fi | |
| RSNX=$(sed -n 's/^\tUnknown IE (244): \([0-9a-f]\{2\}\).*/\1/p' <<<"$BSS") | |
| [ -n "$RSNX" ] && kv "RSNX (WPA3 extras)" "$(rsnx_desc "$RSNX")" | |
| grep -qE "^${T}(Supported|Extended supported) rates:.* 61\.5\*" <<<"$BSS" && | |
| kv "SAE H2E only" "yes (BSS membership selector 123)" | |
| grep -q "^${T}WPS:" <<<"$BSS" && kv "WPS" "advertised" | |
| STD="" | |
| grep -q "^${T}HT capabilities:" <<<"$BSS" && STD+="n " | |
| grep -q "^${T}VHT capabilities:" <<<"$BSS" && STD+="ac " | |
| grep -qE "^${T}HE capabilities:|Extension ID \(35\)" <<<"$BSS" && STD+="ax " | |
| grep -qE "^${T}EHT capabilities:|Extension ID \(108\)" <<<"$BSS" && STD+="be " | |
| kv "802.11 standards" "${STD:-legacy a/b/g only}" | |
| EXT="" | |
| grep -q "BSS Transition" <<<"$BSS" && EXT+="802.11v(BSS-transition) " | |
| grep -q "RM enabled capabilities" <<<"$BSS" && EXT+="802.11k(radio-measurement) " | |
| has "FT/" "$RSN_AKM" && EXT+="802.11r(fast-transition) " | |
| grep -q "TWT Responder" <<<"$BSS" && EXT+="TWT " | |
| kv "Roaming/other" "$EXT" | |
| fi | |
| # ---- What was actually negotiated ---- | |
| sub "Current connection - negotiated security" | |
| if [ -n "$WPA_OK" ]; then | |
| ST=$(wpa_cli -i "$IF" status 2>/dev/null) | |
| get() { sed -n "s/^$1=//p" <<<"$ST"; } | |
| KM=$(get key_mgmt) | |
| kv "Authentication used" "$(keymgmt_desc "$KM")" | |
| kv "key_mgmt (supplicant)" "$KM" | |
| kv "Pairwise cipher" "$(get pairwise_cipher)" | |
| kv "Group cipher" "$(get group_cipher)" | |
| MGMT=$(get mgmt_group_cipher) | |
| if [ -n "$MGMT" ]; then | |
| kv "PMF (802.11w)" "in use ($MGMT)" | |
| else | |
| kv "PMF (802.11w)" "not in use" | |
| fi | |
| SG=$(get sae_group) | |
| if [ -n "$SG" ]; then | |
| kv "SAE group" "$(sae_group_desc "$SG")" | |
| [ "$(get sae_h2e)" = 1 ] && H2E="hash-to-element (H2E)" || H2E="hunting-and-pecking" | |
| kv "SAE PWE derivation" "$H2E" | |
| [ "$(get sae_pk)" = 1 ] && kv "SAE-PK" "in use" | |
| fi | |
| kv "EAP method" "$(get selectedMethod)" | |
| kv "EAP phase 2" "$(sed -n 's/^.*Phase2 method=//p' <<<"$ST")" | |
| kv "EAP TLS version" "$(get eap_tls_version)" | |
| kv "EAP state" "$(get 'EAP state')" | |
| kv "State" "$(get wpa_state)" | |
| fi | |
| if have nmcli; then | |
| CON=$(nmcli -g GENERAL.CONNECTION dev show "$IF" 2>/dev/null) | |
| if [ -n "$CON" ]; then | |
| kv "NM connection" "$CON" | |
| kv "NM configured key-mgmt" \ | |
| "$(nmcli -g 802-11-wireless-security.key-mgmt con show "$CON" 2>/dev/null)" | |
| case "$(nmcli -g 802-11-wireless-security.pmf con show "$CON" 2>/dev/null)" in | |
| 0) PMF="default (global setting)" ;; | |
| 1) PMF="disabled" ;; | |
| 2) PMF="optional" ;; | |
| 3) PMF="required" ;; | |
| *) PMF="" ;; | |
| esac | |
| kv "NM configured PMF" "$PMF" | |
| fi | |
| fi | |
| [ -z "$WPA_OK" ] && ! have nmcli && | |
| kv "Note" "wpa_supplicant control interface not reachable (try sudo)" | |
| done | |
| echo |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment