Last active
September 25, 2025 20:53
-
-
Save Fail-Safe/6f9bac241191da60529ea755b8a17242 to your computer and use it in GitHub Desktop.
AP PPSK (per-PSK) conversion script for OpenWrt
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/sh | |
| # Local AP PPSK (per-PSK) conversion script – run directly ON the WAP. | |
| # Converts existing multi-SSID setup to use central wpa_psk_file + dynamic VLANs. | |
| set -e | |
| BACKUP_DIR=${BACKUP_DIR:-/root/backups} | |
| PPSK_MAIN_SRC=${PPSK_MAIN_SRC:-./ppsk-vlans} | |
| PPSK_MAIN_DIR=/etc/hostapd | |
| PPSK_MAIN_DST=$PPSK_MAIN_DIR/ppsk-vlans | |
| WPAD_CAP_DST=/etc/capabilities/wpad.json | |
| echo "[1/7] Creating backup..." | |
| mkdir -p "$BACKUP_DIR" | |
| HOSTNAME=$(uci get system.@system[0].hostname 2>/dev/null || echo unknown) | |
| STAMP=$(date +%F-%H%M) | |
| BACKUP_TMP=/tmp/backup-${HOSTNAME}-${STAMP}.tar.gz | |
| sysupgrade -b "$BACKUP_TMP" | |
| mv "$BACKUP_TMP" "$BACKUP_DIR"/ | |
| echo " Backup stored at $BACKUP_DIR/$(basename "$BACKUP_TMP")" | |
| echo "[2/7] Create PPSK directory..." | |
| mkdir -p "$PPSK_MAIN_DIR" | |
| chown network:network "$PPSK_MAIN_DIR" 2>/dev/null || true | |
| chmod 640 "$PPSK_MAIN_DIR" 2>/dev/null || true | |
| echo "[3/7] Generating PPSK file from wireless config..." | |
| # Desired format lines: | |
| # vlanid=<VID> 00:00:00:00:00:00 <PASSWD> | |
| # If you later want MAC-specific PPSKs replace the all-zero MAC with the target. | |
| # Source can be overridden with WIRELESS_SRC (defaults /etc/config/wireless). | |
| WIRELESS_SRC=${WIRELESS_SRC:-/etc/config/wireless} | |
| TMP_PPSK=$(mktemp) | |
| if [ ! -f "$WIRELESS_SRC" ]; then | |
| echo " Wireless source $WIRELESS_SRC not found; skipping generation." >&2 | |
| else | |
| awk -v OUTFILE="$TMP_PPSK" ' | |
| function unq(s){ gsub(/^["\047]|["\047]$/,"",s); return s } | |
| function flush_station(){ | |
| if(in_station && st_key!="" && st_vid!="") { | |
| st_key=unq(st_key); st_vid=unq(st_vid); | |
| print "vlanid=" st_vid " 00:00:00:00:00:00 " st_key > OUTFILE; | |
| } | |
| in_station=0; st_key=""; st_vid=""; | |
| } | |
| function flush_iface(){ | |
| if(in_iface && if_key!="") { | |
| if_key=unq(if_key); | |
| print "vlanid=0 00:00:00:00:00:00 " if_key > OUTFILE; | |
| } | |
| in_iface=0; if_key=""; | |
| } | |
| BEGIN{in_station=0; in_iface=0; st_key=""; st_vid=""; if_key=""} | |
| /^config[[:space:]]+wifi-station/ { flush_station(); flush_iface(); in_station=1; next } | |
| /^config[[:space:]]+wifi-iface/ { flush_station(); flush_iface(); in_iface=1; next } | |
| /^config[[:space:]]+/ { flush_station(); flush_iface(); next } | |
| in_station && /option[[:space:]]+key[[:space:]]+/ { st_key=$3 } | |
| in_station && /option[[:space:]]+vid[[:space:]]+/ { st_vid=$3 } | |
| in_iface && /option[[:space:]]+key[[:space:]]+/ { if_key=$3 } | |
| END{ flush_station(); flush_iface() } | |
| ' "$WIRELESS_SRC" | |
| # Deduplicate in case same key appears multiple times | |
| sort -u "$TMP_PPSK" -o "$TMP_PPSK" | |
| # If we produced anything, install/merge. | |
| if [ -s "$TMP_PPSK" ]; then | |
| # Backup existing file if present. | |
| if [ -f "$PPSK_MAIN_DST" ]; then | |
| cp "$PPSK_MAIN_DST" "$PPSK_MAIN_DST.bak.$(date +%s)" | |
| fi | |
| mv "$TMP_PPSK" "$PPSK_MAIN_DST" | |
| echo " Generated $(wc -l < "$PPSK_MAIN_DST") PPSK entries into $PPSK_MAIN_DST" | |
| else | |
| echo " No wifi-station entries with vid+key found; leaving existing PPSK file intact." >&2 | |
| rm -f "$TMP_PPSK" | |
| fi | |
| fi | |
| [ -f "$PPSK_MAIN_DST" ] && chown network:network "$PPSK_MAIN_DST" && chmod 640 "$PPSK_MAIN_DST" | |
| echo "[4/7] Applying UCI wireless edits..." | |
| # Assumptions: iface[0] + iface[1] are main dual-band SSID. | |
| # Adjust indices via env if needed: IFACE_MAIN_24, IFACE_MAIN_5 | |
| IM24=${IFACE_MAIN_24:-0} | |
| IM5=${IFACE_MAIN_5:-1} | |
| # Set both main SSIDs to use the new PPSK file and dynamic VLANs. | |
| uci set wireless.@wifi-iface[$IM24].wpa_psk_file="$PPSK_MAIN_DST" | |
| uci set wireless.@wifi-iface[$IM5].wpa_psk_file="$PPSK_MAIN_DST" | |
| uci set wireless.@wifi-iface[$IM24].dynamic_vlan='2' | |
| uci set wireless.@wifi-iface[$IM5].dynamic_vlan='2' | |
| # Remove legacy single PSKs (ignore errors if already gone) | |
| uci delete wireless.@wifi-iface[$IM24].key 2>/dev/null || true | |
| uci delete wireless.@wifi-iface[$IM5].key 2>/dev/null || true | |
| # Loop over the PPSK file and remove the corresponding `wifi-station` sections | |
| # for each VLAN that we created in the PPSK file. | |
| # A `wifi-station` section will look like this: | |
| # config wifi-station | |
| # option iface 'default_radio0' | |
| # option vid '178' | |
| # option key 'redacted' | |
| while read -r line; do | |
| vlanid=$(echo "$line" | sed -n "s/^vlanid=\([0-9][0-9]*\).*/\1/p") | |
| echo " Processing VLAN $vlanid..." | |
| # We must loop over all the `wifi-station` sections in the wireless config | |
| # and remove the ones that match the VLAN ID. `option vid '<vlanid>'` | |
| uci_section_num=$(uci show wireless \ | |
| | grep -E "wireless\.@wifi-station\[[0-9]+\]\.vid='${vlanid}'" \ | |
| | sed -n "s/.*@wifi-station\[\([0-9][0-9]*\)\].*/\1/p") | |
| if [ -z "$uci_section_num" ]; then | |
| echo " No matching wifi-station section found for VLAN $vlanid; skipping." | |
| continue | |
| fi | |
| if [ -n "$uci_section_num" ] && [ "$uci_section_num" -ge 0 ]; then | |
| echo " Deleting wifi-station section number $uci_section_num for VLAN $vlanid..." | |
| uci delete wireless.@wifi-station["$uci_section_num"] 2>/dev/null || true | |
| fi | |
| done < "$PPSK_MAIN_DST" | |
| uci commit wireless | |
| echo " Wireless config updated." | |
| echo "[5/7] Ensuring /etc/hostapd filesystem entry exists in wpad.json..." | |
| # This entry is required for hostapd/wpad to access the | |
| # new PPSK file in the new /etc/hostapd path. | |
| # { | |
| # "path": "/etc/hostapd", | |
| # "rw": false | |
| # }, | |
| if [ -f "$WPAD_CAP_DST" ]; then | |
| if grep -q '"path" *: *"/etc/hostapd"' "$WPAD_CAP_DST"; then | |
| echo " Entry already present." | |
| else | |
| tmpf=$(mktemp) | |
| awk ' | |
| BEGIN{in_fs=0; inserted=0} | |
| /"filesystem"[[:space:]]*:[[:space:]]*\[/ {in_fs=1} | |
| in_fs && /{/ && inserted==0 { | |
| print " {"; | |
| print " \"path\": \"/etc/hostapd\","; | |
| print " \"rw\": false"; | |
| print " },"; | |
| inserted=1 | |
| } | |
| {print} | |
| in_fs && /]/ {in_fs=0} | |
| END{if(inserted==0) fprintf(stderr,"Note: could not insert /etc/hostapd entry (no filesystem array objects found)\n")} | |
| ' "$WPAD_CAP_DST" > "$tmpf" && mv "$tmpf" "$WPAD_CAP_DST" | |
| echo " Added /etc/hostapd entry." | |
| fi | |
| chmod 644 "$WPAD_CAP_DST" 2>/dev/null || true | |
| else | |
| echo " No existing $WPAD_CAP_DST found; skipping (create manually if needed)." | |
| fi | |
| echo "[6/7] Adding new files/dirs to sysupgrade configuration file..." | |
| echo "$$WPAD_CAP_DST" >> /etc/sysupgrade.conf | |
| echo "$$PPSK_MAIN_DIR" >> /etc/sysupgrade.conf | |
| echo "[7/7] Restarting wpad and checking logs..." | |
| /etc/init.d/wpad restart | |
| sleep 3 | |
| logread | grep -m1 "Assigned VLAN ID" || echo "No VLAN assignment observed yet (connect a PPSK client to trigger)." | |
| echo "Done." |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment