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:
@@ -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.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user