OpenWrt
The OpenWrt adapter speaks ubus/UCI to OpenWrt routers and access points. It is a Preview / Foundation tier adapter - the client code exists and the UCI/ubus communication layer is plausibly complete, but the adapter has not been through the gold-standard contract pass. Use it for development and evaluation; do not deploy it against production OpenWrt fleets.
| Fact | Detail |
|---|---|
| Maturity | Preview / Foundation |
| Gold-standard contract | Unverified |
| Frontend UI | None |
| Backend routes | Three routers active - see below |
What the client can do
Section titled “What the client can do”The adapter client speaks ubus (the OpenWrt IPC bus) and UCI (Unified Configuration Interface):
- ubus call dispatch - invoke any ubus namespace/method
- UCI config read / write / commit / revert
- Basic system info (
system.board,system.info) - Interface list (
network.interface.dump) These are the building blocks. What higher-level operations are reliably safe has not been empirically determined by a structured review.
Backend HTTP routes
Section titled “Backend HTTP routes”OpenWrt routers are imported and mounted in app/api/v1/__init__.py:
| Router | Prefix | Endpoints |
|---|---|---|
adapter_openwrt | /api/v1/gateway-openwrt/{controller_id}/ | 8 read-only: device-info, interfaces, firewall-rules, port-forwards, dhcp-leases, dhcp-static-mappings, arp-table, summary |
adapter_openwrt_firewall | /api/v1/gateway-openwrt-firewall/{controller_id}/ | POST /changes/{feature} (stage), GET /changes (list pending) - openwrt.firewall.* features only |
adapter_openwrt_dhcp | /api/v1/gateway-openwrt-dhcp/{controller_id}/ | POST /changes/{feature} (stage), GET /changes (list pending) - openwrt.dhcp.* / openwrt.dns.* features only |
The staged-apply dispatcher (/gateway-vpn/changes/{id}/apply) handles the actual push to the device; the write endpoints above only enqueue changes.
What does not work
Section titled “What does not work”- OpenWrt is not listed in
adapter_factory.get_available_adapter_types()(the static allowlist used by the setup wizard), so it cannot be added through the standard controller-creation UI flow. It IS registered by the adapter registry at startup (registry.pydiscover_adapters()) and CAN be reached through the/api/v1/gateway-openwrt/*routes if you create the controller record directly via the API. - No frontend pages.
- The following gold-standard contract items are not verified: dual-gate writes, circuit breaker, secret redaction, SSRF guard, and role gates.
- No test coverage comparable to the Production adapters.
rpcd / ubus ACL requirement
Section titled “rpcd / ubus ACL requirement”OpenWrt’s rpcd daemon enforces an ACL for every ubus call. The default luci-base ACL does not grant the permissions that FreeSDN needs for write operations. Without the correct ACL, writes appear to succeed but the changes are silently dropped and never persisted.
At minimum, the FreeSDN service account on the OpenWrt device needs:
| Permission | Why |
|---|---|
uci.commit | Persist UCI writes to disk |
rc.exec | Restart services after config changes |
To add these, create or edit /usr/share/rpcd/acl.d/freesdn.json on the OpenWrt device:
{ "freesdn": { "description": "FreeSDN adapter access", "read": { "ubus": { "uci": ["get", "configs", "changes"], "network.interface": ["dump", "status"], "system": ["board", "info"] } }, "write": { "ubus": { "uci": ["set", "delete", "rename", "commit", "revert"], "rc": ["exec"] } } }}After adding the file, restart rpcd:
service rpcd restartThen create a dedicated user in /etc/config/rpcd with the freesdn ACL group and a strong password.
Testing the adapter (developer use only)
Section titled “Testing the adapter (developer use only)”Because the adapter is not registered in the factory, you must instantiate it directly in Python:
from app.adapters.openwrt.adapter import OpenWRTAdapter
async with OpenWRTAdapter( host="192.168.1.1", # bare IP or hostname - no scheme username="freesdn", password="<password>", port=80, # OpenWrt default HTTP; omit for HTTPS on 443 verify_ssl=False,) as adapter: result = await adapter.get_system_info() if result.success: print(result.data)Do not use this pattern in production code paths. It bypasses tenant scoping, circuit breaker, rate limiting, and all other contract requirements.
See also
Section titled “See also”- Adapter Overview - maturity tiers, the full vendor matrix, and what the gold-standard contract requires
- MikroTik RouterOS - production-grade alternative for RouterOS-based deployments