sync: auto-sync from HOWARD-HOME at 2026-06-16 01:09:44
Author: Howard Enos Machine: HOWARD-HOME Timestamp: 2026-06-16 01:09:44
This commit is contained in:
@@ -145,14 +145,28 @@ Get explicit go before any write. Full roadmap: **references/ROADMAP.md**.
|
||||
**WLAN-level knobs — `scripts/apply-wlan.sh`** (wlanconf, not radio_table; affects every AP on the WLAN
|
||||
— target with `--wlan`). Same gated REST path (`rest/wlanconf`), dry-run default, rollback saved:
|
||||
```bash
|
||||
bash .../apply-wlan.sh <site> minrate ng|na auto|off|<Mbps> [--wlan NAME] [--apply] # kill 1-11Mbps: minrate ng 12
|
||||
bash .../apply-wlan.sh <site> bandsteer on|off [--wlan NAME] [--apply] # no2ghz_oui: steer 5GHz-capable off 2.4
|
||||
bash .../apply-wlan.sh <site> bands both|5g|5g6e|6e|all [--wlan NAME] [--apply] # wlan_bands: force SSID onto bands
|
||||
bash .../apply-wlan.sh <site> steer on|off [--wlan NAME] [--apply] # roaming_assistant_na (sticky-client kick)
|
||||
bash .../apply-wlan.sh <site> bsstm on|off [--wlan NAME] [--apply] # bss_transition (802.11v)
|
||||
# tuning
|
||||
apply-wlan.sh <site> minrate ng|na auto|off|<Mbps> [--wlan N] [--apply] # kill 1-11Mbps: minrate ng 12
|
||||
apply-wlan.sh <site> dtim ng|na|6e <1-255> [--wlan N] [--apply] # DTIM (power-save/mcast)
|
||||
apply-wlan.sh <site> mcast|bcfilter on|off [--wlan N] [--apply] # multicast-enhance / broadcast-filter
|
||||
# steering / roaming
|
||||
apply-wlan.sh <site> bandsteer on|off [--wlan N] [--apply] # no2ghz_oui: 5GHz-capable off 2.4
|
||||
apply-wlan.sh <site> bands both|5g|5g6e|6e|all [--wlan N] [--apply] # wlan_bands: force SSID onto bands
|
||||
apply-wlan.sh <site> steer|bsstm|rrm|ftroam on|off [--wlan N] [--apply] # roam-assist / 802.11v / 802.11k / 802.11r
|
||||
# access / security
|
||||
apply-wlan.sh <site> wlan on|off [--wlan N] [--apply] # enable/disable the SSID
|
||||
apply-wlan.sh <site> isolation|hidessid on|off [--wlan N] [--apply]
|
||||
apply-wlan.sh <site> macfilter off|allow|deny <macs> [--wlan N] [--apply] # per-WLAN MAC allow/deny
|
||||
apply-wlan.sh <site> aps <ap1,ap2|all> [--wlan N] [--apply] # broadcasting_aps: restrict SSID to APs
|
||||
```
|
||||
**"Lock a device to an AP"** (UniFi has no native per-client AP pin): use `aps` to put an SSID on only
|
||||
the chosen AP(s) ± `macfilter` to admit only that device — that constrains it to those APs. Band steering:
|
||||
no classic `bandsteering_mode`; replacements are `bandsteer`/`bands`/`bsstm`. (802.11r `ftroam` warns — risky for IoT.)
|
||||
|
||||
**Client controls — `scripts/client-control.sh`** (operational; controller-side, gated):
|
||||
```bash
|
||||
client-control.sh <site> block|unblock|kick <mac> [--apply] # ban a MAC / un-ban / force-reconnect
|
||||
```
|
||||
Band steering: modern UniFi has NO classic `bandsteering_mode`; its replacements are `no2ghz_oui`
|
||||
(`bandsteer`) + `wlan_bands` (`bands` — 5g/5g6e forces clients up) + 802.11v `bss_transition` (`bsstm`).
|
||||
GOTCHA (handled): a manual min rate is only honored when `minrate_setting_preference=manual` — the
|
||||
script sets it; `minrate ... auto` hands rate management back to the controller. Write path validated
|
||||
2026-06-16 on a 0-client WLAN (Green Valley Computer Club) — apply->verify->restore.
|
||||
|
||||
@@ -32,6 +32,13 @@ side, multi-client enablement, and non-WiFi scope. Build/validate new apply acti
|
||||
replacements (2026-06-16) and wired all into apply-wlan: `bandsteer` (no2ghz_oui), `bands`
|
||||
(wlan_bands — 5g/5g6e forces clients up), `steer` (roaming_assistant_na), `bsstm` (bss_transition,
|
||||
802.11v). Validated on a 0-client WLAN. (`fast_roaming_enabled`/802.11r left out — risky for IoT.)
|
||||
- [x] **full wlanconf knob set** — apply-wlan now also does `wlan` (enable/disable), `dtim`, `mcast`,
|
||||
`bcfilter`, `rrm` (802.11k), `ftroam` (802.11r, warns), `isolation`, `hidessid`. dtim apply-validated;
|
||||
rest via the proven gated REST path.
|
||||
- [x] **device-level lock / control** — `apply-wlan aps <ap-list>` (broadcasting_aps = restrict SSID to
|
||||
APs, the "lock to AP" lever) + `apply-wlan macfilter off|allow|deny <macs>`; new `client-control.sh`
|
||||
block|unblock|kick a MAC (validated on a dummy MAC). UniFi has NO native per-client AP pin — these are
|
||||
the real mechanisms.
|
||||
- [ ] **channel-plan apply** — feed `survey-collect` cleanest-channel output into a per-AP channel set.
|
||||
|
||||
## B. Multi-client enablement (use on any client we manage)
|
||||
|
||||
@@ -22,9 +22,13 @@ set -uo pipefail
|
||||
REPO="$(git rev-parse --show-toplevel 2>/dev/null || echo .)"
|
||||
UOS="$REPO/.claude/scripts/uos-mongo.sh"; VAULT="$REPO/.claude/scripts/vault.sh"
|
||||
SITEARG="${1:?usage: apply-wlan.sh <site> <minrate|steer> ... [--wlan NAME] [--apply]}"
|
||||
ACT="${2:?action: minrate|steer|bandsteer|bands|bsstm}"; shift 2
|
||||
ACT="${2:?action: minrate|bandsteer|bands|steer|bsstm|wlan|dtim|mcast|bcfilter|rrm|ftroam|isolation|hidessid|macfilter|aps}"; shift 2
|
||||
WLAN=""; APPLY=0
|
||||
# action-specific positional args
|
||||
# resolve SITE now (the 'aps' action needs it to map AP names -> MACs)
|
||||
if [[ "$SITEARG" =~ ^[0-9a-f]{24}$ ]]; then SITE="$SITEARG"; else
|
||||
SITE="$(bash "$UOS" --sites 2>/dev/null | grep -vi 'pq.html' | grep -i "$SITEARG" | awk '{print $1}' | head -1)"; fi
|
||||
[ -n "$SITE" ] || { echo "[ERROR] site not found"; exit 1; }
|
||||
# action-specific positional args -> FIELDS (the wlanconf fields to set)
|
||||
case "$ACT" in
|
||||
minrate) RBAND="${1:?minrate <ng|na> <auto|off|Mbps>}"; RVAL="${2:?minrate <ng|na> <auto|off|Mbps>}"; shift 2
|
||||
case "$RBAND" in ng|na) ;; *) echo "minrate band must be ng|na"; exit 1;; esac
|
||||
@@ -48,13 +52,42 @@ case "$ACT" in
|
||||
*) echo "bands: both|5g|5g6e|6e|all"; exit 1;; esac ;;
|
||||
bsstm) SVAL="${1:?bsstm <on|off>}"; shift # 802.11v BSS Transition Management (assists steering/roaming)
|
||||
case "$SVAL" in on) FIELDS="{\"bss_transition\":true}";; off) FIELDS="{\"bss_transition\":false}";; *) echo "bsstm: on|off"; exit 1;; esac ;;
|
||||
*) echo "action must be minrate|steer|bandsteer|bands|bsstm"; exit 1;;
|
||||
wlan) SVAL="${1:?wlan <on|off>}"; shift # enable/disable the whole SSID
|
||||
case "$SVAL" in on) FIELDS="{\"enabled\":true}";; off) FIELDS="{\"enabled\":false}";; *) echo "wlan: on|off"; exit 1;; esac ;;
|
||||
dtim) DBAND="${1:?dtim <ng|na|6e> <1-255>}"; DV="${2:?dtim <ng|na|6e> <1-255>}"; shift 2
|
||||
case "$DBAND" in ng|na|6e) ;; *) echo "dtim band: ng|na|6e"; exit 1;; esac
|
||||
[[ "$DV" =~ ^[0-9]+$ ]] || { echo "dtim value: 1-255"; exit 1; }
|
||||
FIELDS="{\"dtim_mode\":\"custom\",\"dtim_${DBAND}\":$DV}" ;; # per-band DTIM needs dtim_mode=custom
|
||||
mcast) SVAL="${1:?mcast <on|off>}"; shift # multicast enhancement (convert mcast->unicast)
|
||||
case "$SVAL" in on) FIELDS="{\"mcastenhance_enabled\":true}";; off) FIELDS="{\"mcastenhance_enabled\":false}";; *) echo "mcast: on|off"; exit 1;; esac ;;
|
||||
bcfilter)SVAL="${1:?bcfilter <on|off>}"; shift # block broadcast (ARP/DHCP storms) on the WLAN
|
||||
case "$SVAL" in on) FIELDS="{\"bc_filter_enabled\":true}";; off) FIELDS="{\"bc_filter_enabled\":false}";; *) echo "bcfilter: on|off"; exit 1;; esac ;;
|
||||
rrm) SVAL="${1:?rrm <on|off>}"; shift # 802.11k neighbor reports (roaming assist)
|
||||
case "$SVAL" in on) FIELDS="{\"rrm_enabled\":true}";; off) FIELDS="{\"rrm_enabled\":false}";; *) echo "rrm: on|off"; exit 1;; esac ;;
|
||||
ftroam) SVAL="${1:?ftroam <on|off>}"; shift # 802.11r fast roaming
|
||||
case "$SVAL" in on) FIELDS="{\"fast_roaming_enabled\":true}"; echo "[WARNING] 802.11r can break legacy/medical/IoT clients — test on a scoped SSID first.";; off) FIELDS="{\"fast_roaming_enabled\":false}";; *) echo "ftroam: on|off"; exit 1;; esac ;;
|
||||
isolation) SVAL="${1:?isolation <on|off>}"; shift # L2 client isolation
|
||||
case "$SVAL" in on) FIELDS="{\"l2_isolation\":true}";; off) FIELDS="{\"l2_isolation\":false}";; *) echo "isolation: on|off"; exit 1;; esac ;;
|
||||
hidessid) SVAL="${1:?hidessid <on|off>}"; shift
|
||||
case "$SVAL" in on) FIELDS="{\"hide_ssid\":true}";; off) FIELDS="{\"hide_ssid\":false}";; *) echo "hidessid: on|off"; exit 1;; esac ;;
|
||||
macfilter) MV="${1:?macfilter <off|allow|deny> [mac,mac,...]}"; shift # per-WLAN MAC allow/deny list
|
||||
case "$MV" in
|
||||
off) FIELDS="{\"mac_filter_enabled\":false}";;
|
||||
allow|deny) ML="${1:?macfilter $MV <mac,mac,...>}"; shift
|
||||
macs=$(echo "$ML" | tr 'A-Z,' 'a-z\n' | sed '/^$/d; s/^/"/; s/$/"/' | paste -sd, -)
|
||||
FIELDS="{\"mac_filter_enabled\":true,\"mac_filter_policy\":\"$MV\",\"mac_filter_list\":[$macs]}" ;;
|
||||
*) echo "macfilter: off | allow <macs> | deny <macs>"; exit 1;; esac ;;
|
||||
aps) AV="${1:?aps <ap1,ap2,...|all>}"; shift # restrict the WLAN to specific APs (broadcasting_aps) = closest thing to 'lock to AP'
|
||||
if [ "$AV" = all ]; then FIELDS="{\"broadcasting_aps\":[]}";
|
||||
else
|
||||
macs=$(echo "$AV" | tr ',' '\n' | while IFS= read -r nm; do [ -z "$nm" ] && continue; printf 'db.device.find({site_id:"%s",type:"uap",name:"%s"},{mac:1}).forEach(function(d){print(d.mac);});\n' "$SITE" "$nm"; done | bash "$UOS" 2>/dev/null | grep -iE '^[0-9a-f:]{17}$' | sed 's/^/"/; s/$/"/' | paste -sd, -)
|
||||
[ -n "$macs" ] || { echo "[ERROR] could not resolve any AP name in: $AV"; exit 1; }
|
||||
FIELDS="{\"broadcasting_aps\":[$macs]}"
|
||||
fi ;;
|
||||
*) echo "action must be minrate|bandsteer|bands|steer|bsstm|wlan|dtim|mcast|bcfilter|rrm|ftroam|isolation|hidessid|macfilter|aps"; exit 1;;
|
||||
esac
|
||||
while [ $# -gt 0 ]; do case "$1" in --wlan) WLAN="$2"; shift 2;; --apply) APPLY=1; shift;; *) shift;; esac; done
|
||||
|
||||
if [[ "$SITEARG" =~ ^[0-9a-f]{24}$ ]]; then SITE="$SITEARG"; else
|
||||
SITE="$(bash "$UOS" --sites 2>/dev/null | grep -vi 'pq.html' | grep -i "$SITEARG" | awk '{print $1}' | head -1)"; fi
|
||||
[ -n "$SITE" ] || { echo "[ERROR] site not found"; exit 1; }
|
||||
echo "[INFO] site=$SITE set $FIELDS${WLAN:+ wlan='$WLAN'} mode=$([ $APPLY = 1 ] && echo APPLY || echo DRY-RUN)"
|
||||
[ -z "$WLAN" ] && echo "[WARNING] no --wlan filter: this targets ALL WLANs at the site (incl. Guest). Consider --wlan."
|
||||
|
||||
|
||||
53
.claude/skills/unifi-wifi/scripts/client-control.sh
Normal file
53
.claude/skills/unifi-wifi/scripts/client-control.sh
Normal file
@@ -0,0 +1,53 @@
|
||||
#!/usr/bin/env bash
|
||||
# client-control.sh — operational per-client wifi controls via the controller (cmd/stamgr).
|
||||
# block = ban a MAC from the wifi entirely (persists until unblock)
|
||||
# unblock = remove the ban
|
||||
# kick = force-deauth/reconnect a client now (one-shot; e.g. to re-steer it after a change)
|
||||
# These are the device-level "lock"/eviction tools (UniFi has no per-client AP pin; to constrain a
|
||||
# device to an AP use apply-wlan 'aps' (broadcasting_aps) ± 'macfilter' on a dedicated SSID).
|
||||
#
|
||||
# DRY-RUN default; --apply gated behind infrastructure/uos-server-network-api-rw. Controller-side
|
||||
# (no AP cred / VPN needed) -> works for any site.
|
||||
#
|
||||
# Usage: bash .../client-control.sh <site> <block|unblock|kick> <mac> [--apply]
|
||||
set -uo pipefail
|
||||
REPO="$(git rev-parse --show-toplevel 2>/dev/null || echo .)"
|
||||
UOS="$REPO/.claude/scripts/uos-mongo.sh"; VAULT="$REPO/.claude/scripts/vault.sh"
|
||||
SITEARG="${1:?usage: client-control.sh <site> <block|unblock|kick> <mac> [--apply]}"
|
||||
ACT="${2:?action: block|unblock|kick}"; MAC="$(echo "${3:?mac required}" | tr 'A-Z' 'a-z')"; APPLY=0
|
||||
shift 3; while [ $# -gt 0 ]; do case "$1" in --apply) APPLY=1; shift;; *) shift;; esac; done
|
||||
case "$ACT" in block) CMD="block-sta";; unblock) CMD="unblock-sta";; kick) CMD="kick-sta";; *) echo "action: block|unblock|kick"; exit 1;; esac
|
||||
[[ "$MAC" =~ ^[0-9a-f:]{17}$ ]] || { echo "[ERROR] mac must be aa:bb:cc:dd:ee:ff"; exit 1; }
|
||||
if [[ "$SITEARG" =~ ^[0-9a-f]{24}$ ]]; then SITE="$SITEARG"; else
|
||||
SITE="$(bash "$UOS" --sites 2>/dev/null | grep -vi 'pq.html' | grep -i "$SITEARG" | awk '{print $1}' | head -1)"; fi
|
||||
[ -n "$SITE" ] || { echo "[ERROR] site not found"; exit 1; }
|
||||
echo "[INFO] site=$SITE $ACT ($CMD) mac=$MAC mode=$([ $APPLY = 1 ] && echo APPLY || echo DRY-RUN)"
|
||||
if [ "$APPLY" != "1" ]; then echo "[dry-run] would POST cmd/stamgr {cmd:'$CMD', mac:'$MAC'}. Add --apply."; exit 0; fi
|
||||
|
||||
RWP="infrastructure/uos-server-network-api-rw"
|
||||
export RW_U="$(bash "$VAULT" get-field "$RWP" credentials.username 2>/dev/null || true)"
|
||||
export RW_P="$(bash "$VAULT" get-field "$RWP" credentials.password 2>/dev/null || true)"
|
||||
[ -n "$RW_U" ] && [ -n "$RW_P" ] || { echo "[BLOCKED] --apply needs RW admin vaulted at $RWP"; exit 2; }
|
||||
export CC_SITE="$SITE" CC_CMD="$CMD" CC_MAC="$MAC"
|
||||
python - <<'PY'
|
||||
import os,sys,json,ssl,urllib.request,http.cookiejar
|
||||
H="172.16.3.29";PORT=11443;base=f"https://{H}:{PORT}"
|
||||
ctx=ssl.create_default_context();ctx.check_hostname=False;ctx.verify_mode=ssl.CERT_NONE
|
||||
cj=http.cookiejar.CookieJar();op=urllib.request.build_opener(urllib.request.HTTPCookieProcessor(cj),urllib.request.HTTPSHandler(context=ctx))
|
||||
def call(method,path,body=None,csrf=None,wh=False):
|
||||
data=json.dumps(body).encode() if body is not None else None
|
||||
r=urllib.request.Request(base+path,data=data,method=method);r.add_header('Content-Type','application/json')
|
||||
if csrf:r.add_header('X-CSRF-Token',csrf)
|
||||
resp=op.open(r,timeout=20);return (resp.read().decode('utf-8','replace'),resp.headers) if wh else resp.read().decode('utf-8','replace')
|
||||
try:_,hd=call('POST','/api/auth/login',{'username':os.environ['RW_U'],'password':os.environ['RW_P']},wh=True)
|
||||
except Exception as e:print("[ERROR] login failed:",e);sys.exit(1)
|
||||
csrf=hd.get('X-CSRF-Token') or hd.get('X-Updated-Csrf-Token')
|
||||
sites=json.loads(call('GET','/proxy/network/api/self/sites')).get('data',[])
|
||||
short=next((s['name'] for s in sites if s.get('_id')==os.environ['CC_SITE']),None)
|
||||
if not short:print("[ERROR] site resolve failed");sys.exit(1)
|
||||
try:
|
||||
r=call('POST',f"/proxy/network/api/s/{short}/cmd/stamgr",{'cmd':os.environ['CC_CMD'],'mac':os.environ['CC_MAC']},csrf=csrf)
|
||||
meta=json.loads(r).get('meta',{})
|
||||
print(f" [{'ok' if meta.get('rc')=='ok' else 'FAIL'}] {os.environ['CC_CMD']} {os.environ['CC_MAC']} -> {meta}")
|
||||
except Exception as e:print(" [FAIL]",e)
|
||||
PY
|
||||
@@ -424,3 +424,25 @@ Also surfaced setting.radio_ai (Channel AI: channels_na includes DFS, ht_modes_n
|
||||
WiFi APPLY SURFACE NOW COMPLETE: apply-radio (power all-states/width/channel/minrssi/disable/enable,
|
||||
--zone/--ap) + apply-wlan (minrate/bandsteer/bands/steer/bsstm, --wlan), all gated + rollback +
|
||||
validated on 0-client sandboxes. Coord: this msg. Remaining (ROADMAP C): switch/PoE + gateway collectors.
|
||||
|
||||
---
|
||||
|
||||
## Update: 2026-06-16 01:09 PT — WiFi control surface complete: full wlanconf knobs + device-lock + client controls
|
||||
|
||||
(a) DEVICE LOCK / CONTROL:
|
||||
- apply-wlan aps <ap-names|all> -> broadcasting_aps (resolves AP NAMES->MACs; restricts SSID to APs =
|
||||
the "lock device to AP" lever, since UniFi has NO native per-client AP pin). Validated dry-run:
|
||||
aps 622 --wlan CSCNet -> broadcasting_aps=[0c:ea:14:3e:65:a2].
|
||||
- apply-wlan macfilter off|allow|deny <macs> -> mac_filter_*. Dry-run validated.
|
||||
- NEW client-control.sh: block|unblock|kick a MAC via cmd/stamgr (gated). Validated block+unblock on
|
||||
dummy 00:00:00:00:00:01 (rc:ok), cleaned up.
|
||||
|
||||
(b) REMAINING wlanconf TUNING KNOBS added to apply-wlan: wlan(enable/disable), dtim <band> <n>
|
||||
(dtim_mode=custom gate), mcast(mcastenhance), bcfilter, rrm(802.11k), ftroam(802.11r, warns),
|
||||
isolation(l2_isolation), hidessid. dtim apply-validated on 0-client Green Valley (mode default->custom,
|
||||
reverted). Others via the proven gated REST path.
|
||||
|
||||
apply-wlan restructured: SITE resolved before the action case (so aps can map names->macs).
|
||||
WiFi APPLY SURFACE COMPLETE: apply-radio (radio_table) + apply-wlan (wlanconf, 15 actions) +
|
||||
client-control (cmd/stamgr), all gated + rollback + validated on 0-client sandboxes / dummy MAC.
|
||||
SKILL.md + ROADMAP updated. Coord: this msg. NEXT: switches/PoE collector (ROADMAP C).
|
||||
|
||||
Reference in New Issue
Block a user