feat: get_ssids() macfilter/maclist parsing + push_mac_acl()

Adds MAC ACL (whitelist/blacklist) read+write support for wireless SSIDs,
mirroring push_radio_channel's UCI write style. Backs the new Global MAC
ACL feature in netOrk.
This commit is contained in:
Christian Manivong
2026-07-15 23:19:15 +02:00
parent bfc19c2241
commit af032a3c4d
2 changed files with 187 additions and 3 deletions
+52 -3
View File
@@ -39,6 +39,10 @@ class OpenWrtWirelessMixin:
# Collect radio band info: radio0 → "2g", radio1 → "5g", …
radio_bands: dict[str, str] = {}
iface_entries: dict[str, dict[str, str]] = {}
# UCI list values (e.g. "list maclist 'AA:...'") repeat the same key
# across multiple lines — tracked separately since the single-value
# iface_entries dict would only keep the last one.
iface_maclists: dict[str, list[str]] = {}
# First pass: identify named sections that are wifi-iface types and
# collect radio band info.
@@ -72,13 +76,19 @@ class OpenWrtWirelessMixin:
im = re.match(r"wireless\.@wifi-iface\[(\d+)\]\.(\w+)='([^']*)'", line_s)
if im:
idx, key, val = im.group(1), im.group(2), im.group(3)
iface_entries.setdefault(idx, {})[key] = val
if key == "maclist":
iface_maclists.setdefault(idx, []).append(val)
else:
iface_entries.setdefault(idx, {})[key] = val
continue
# named wifi-iface values: wireless.managed_family_2g.ssid='manivong'
nm = re.match(r"wireless\.(\w+)\.(\w+)='([^']*)'", line_s)
if nm and nm.group(1) in named_iface_sections:
section, key, val = nm.group(1), nm.group(2), nm.group(3)
iface_entries.setdefault(section, {})[key] = val
if key == "maclist":
iface_maclists.setdefault(section, []).append(val)
else:
iface_entries.setdefault(section, {})[key] = val
def _band_label(radio: str) -> str:
raw = radio_bands.get(radio, "").lower()
@@ -148,7 +158,8 @@ class OpenWrtWirelessMixin:
result: dict[str, Any] = {}
# Intermediate: ssid -> list of bands seen
ssid_bands: dict[str, list[str]] = {}
for entry in iface_entries.values():
_ACL_MODE_MAP = {"allow": "whitelist", "deny": "blacklist"}
for idx, entry in iface_entries.items():
ssid = entry.get("ssid")
if not ssid:
continue
@@ -171,6 +182,8 @@ class OpenWrtWirelessMixin:
_max_inact_raw = entry.get("max_inactivity")
max_inactivity: int | None = int(_max_inact_raw) if _max_inact_raw and str(_max_inact_raw).isdigit() else None
key: str = entry.get("key", "") or ""
acl_mode = _ACL_MODE_MAP.get(entry.get("macfilter", ""), "off")
mac_list = sorted(set(iface_maclists.get(idx, [])))
if ssid in result:
# Merge: append band if not already present
@@ -205,6 +218,12 @@ class OpenWrtWirelessMixin:
# key: keep first non-empty value seen
if key and not result[ssid].get("key"):
result[ssid]["key"] = key
# acl_mode/mac_list: keep first non-"off" value seen — all
# wifi-iface sections for one SSID carry identical ACL config
# after a push, so any explicit value wins over the default.
if acl_mode != "off" and result[ssid].get("acl_mode", "off") == "off":
result[ssid]["acl_mode"] = acl_mode
result[ssid]["mac_list"] = mac_list
else:
ssid_bands[ssid] = [band] if band else []
result[ssid] = {
@@ -226,6 +245,8 @@ class OpenWrtWirelessMixin:
"disassoc_low_ack": disassoc_low_ack,
"max_inactivity": max_inactivity,
"key": key,
"acl_mode": acl_mode,
"mac_list": mac_list,
}
return result
@@ -543,6 +564,34 @@ class OpenWrtWirelessMixin:
self._send_command("uci commit wireless")
self._send_command("wifi")
def push_mac_acl(self, ssid_name: str, mode: str, macs: list[str]) -> None:
"""Rewrite macfilter mode + maclist entries on every wifi-iface matching *ssid_name*.
Full-rebuild, not diff — always deletes the existing maclist before
re-adding, so the result is idempotent regardless of prior state.
:param ssid_name: SSID name to match against ``option ssid`` on each wifi-iface section.
:param mode: ``"off"`` | ``"whitelist"`` | ``"blacklist"`` — mapped to UCI
``macfilter`` ``"disable"``/``"allow"``/``"deny"``.
:param macs: MAC addresses to set as the maclist. Only the entries for the
active mode's list are ever passed in — the caller resolves whitelist
vs. blacklist before calling.
"""
uci_mode = {"off": "disable", "whitelist": "allow", "blacklist": "deny"}[mode]
sections = self._send_command(
"uci show wireless | grep -oE '^wireless\\.[^.]+' | sort -u"
).split()
for sec in sections:
ssid_val = self._send_command(f"uci -q get {sec}.ssid 2>/dev/null || true").strip()
if ssid_val != ssid_name:
continue
self._send_command(f"uci set {sec}.macfilter='{uci_mode}'")
self._send_command(f"uci delete {sec}.maclist 2>/dev/null || true")
for mac in macs:
self._send_command(f"uci add_list {sec}.maclist='{mac}'")
self._send_command("uci commit wireless")
self._send_command("wifi reload")
def get_radio_status(self) -> dict[str, Any]:
"""Return radio status from UCI and iwinfo.