get_vlans() relied on `bridge vlan show` for port membership. On devices
whose BusyBox ships without the bridge/ip-full packages the command does
not exist, _send_command returns the shell error, and the parser silently
finds nothing in it. All that survived was the sub-interface fallback,
which restates the bridge topology (br-ap tagged, br-ap.10 untagged) and
never names the uplink port — the only port whose tagging matters.
Measured on two Sophos AP100 (BusyBox 1.37.0): eth0 was absent from the
output entirely, while UCI held ports='eth0:u*' for the management VLAN
and 'eth0:t' for the rest.
UCI bridge-vlan sections are now parsed as a second source. They are
readable without the bridge binary and describe the configured state.
Section collection goes through the type declaration, so named sections
(network.apbr_vlan10) are recognised alongside anonymous ones — the old
regex matched only @bridge-vlan[N], so a hand-built bridge was invisible
even for name lookup. Runtime data keeps precedence where it exists,
since that is what the kernel actually enforces.
Option values are kept raw through collection; stripping quotes there
would collapse ports='lan1:t' 'lan2:t' into a single mangled item.
Also repairs TestGetVlans, red on master since get_vlans() moved to
separate tagged/untagged lists while the tests still asserted the old
'interfaces' key, and TestGetFacts, which never learned about the
number_of_interfaces key get_facts() sets deliberately. Both had left
the interesting behaviour uncovered.
get_device_warnings() now returns only {code, meta} — severity, title,
message, and action are resolved centrally by netork's
WARNING_CATALOG (netork/core/device_warnings.py), not by the driver.
Keeps this driver independent of netork and avoids per-vendor drift in
how the same warning code is presented.
OpenWrt's wifi-scripts validator rejects any macfilter value other than
"allow"/"deny" outright — confirmed on real hardware, setting
macfilter='disable' puts netifd in a permanent restart crash loop with the
radio stuck down. The only way to disable filtering is to delete the option
entirely. Also guards against pushing an empty whitelist (macfilter='allow'
with zero MACs blocks every client outright) by treating it as equivalent
to "off".
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.
NetworkDriver (parent of AccessPointDriver) raises NotImplementedError for all
standard NAPALM methods. With AccessPointDriver listed first, get_interfaces()
and get_vlans() from the mixins were shadowed and always raised NotImplementedError
(empty message) — causing all AP polls to report 0 interfaces and 0 VLANs.
Fix: reorder inheritance so mixins come before AccessPointDriver. The standard
NAPALM method stubs in NetworkDriver are now shadowed by the mixin implementations.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
'wifi reload' only reapplies the running config without physically
changing the channel on many ath9k/ath10k/mt76 hardware+driver
combinations. A full 'wifi' (down + up) cycle is required for channel
changes to take effect.
Also updated the test assertion accordingly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
board_name format is "vendor,model" (e.g. "sophos,ap100"). The prefix
before the comma is title-cased to give the manufacturer name (e.g.
"Sophos"). Falls back to "OpenWrt" when board_name is unavailable.
The model is already read from /tmp/sysinfo/model which gives the full
human-readable name (e.g. "Sophos AP100") — no change needed there.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Split from monolithic openwrt.py into separate mixin files for
interfaces, VLANs, packages, routing, wireless, LLDP, and config.
Already in use by the main driver; just missing from git tracking.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The previous attempt added a rule without src= which lands in the global
nftables input chain. Traffic from the management interface (br-ap.10)
jumps immediately to input_mgmt, so the global rule was never reached.
Now detects the management zone name by finding the zone whose allow-SSH
rule already exists (the named rule pattern allow_ssh_from_<zone>).
Removes any mis-scoped previous SNMP rule, then adds a named UCI rule
allow_snmp_from_<zone> with src=<zone> so fw4 places it in the correct
chain (input_mgmt). Persists across reboots via uci commit + fw4 reload.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The method was called but missing — always raised AttributeError, so
Fix SNMP did nothing on OpenWRT devices.
OpenWRT's default firewall (fw4) policy-drops everything except the
ports explicitly listed in the management zone (SSH/HTTP/HTTPS/ICMP).
SNMP (UDP/161) is not in that list, which is why snmpd runs but is
unreachable from outside the device.
Fix adds a persistent UCI firewall rule for UDP/161, reloads fw4
immediately, verifies snmpd is running, and probes locally if SNMP
client tools are available. Returns success so callers can clear the
snmp_no_data warning.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two bugs prevented the firewall step from working:
1. `netstat` was used to detect the SSH peer IP — not installed on
OpenWrt by default, so raw_conn was empty and the entire firewall
step was silently skipped.
2. Even if detection had worked, `src='*'` is wrong when zones have
`input='REJECT'`. The rule only takes effect before the zone policy
if `src` is the exact zone name.
Fix: switch to `ss` (always present), strip any IPv6-mapped prefix,
then walk `uci show firewall` to find the zone whose network interface
shares the same /24 as the peer IP. Use that zone name as `src`.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Added _lldpd_fix_interface() helper that detects the management interface
via the default route and strips any .VID suffix (e.g. br-ap.10 → br-ap).
LLDP is L2 and must run on the bridge itself — sending on a VLAN subinterface
produces tagged frames the switch won't recognize as LLDP.
The helper runs every poll so existing wrong configs (e.g. eth0 from the
original install action) are corrected automatically on the next poll.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
opkg list-upgradable and apk version output contains version strings and
comparison operators; meta.packages now stores only the bare package name
so the schedule-updates API validation passes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Install snmpd-nossl + luci-app-snmpd via opkg, configure via UCI with correct
field names (group/viewname/context='none'), bare port 161, stop+pkill before
start to break crash loops. get_snmp_config() reads current UCI snmpd state.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>