feat(opnsense): implement send_wake_on_lan() via the os-wol plugin API
CI / test (3.10) (push) Failing after 7s
CI / test (3.11) (push) Failing after 8s
CI / test (3.12) (push) Failing after 7s
CI / test (3.9) (push) Failing after 7s

Calls POST /api/wol/wol/set with no uuid in the payload, which makes
the os-wol plugin's WolController::setAction validate and wake
immediately without persisting a host to config.xml. Requires the
os-wol plugin installed and the target interface to have a static
IPv4 (OPNsense derives the broadcast address from the interface's own
IP/subnet). Endpoint/payload verified against the plugin's source
(opnsense/plugins net/wol), not guessed.
This commit is contained in:
Christian Manivong
2026-07-12 11:17:48 +02:00
parent b8a68fc3a8
commit 62424cfd71
2 changed files with 91 additions and 0 deletions
+43
View File
@@ -1697,6 +1697,49 @@ class OPNsenseDriver(FirewallDriver):
community = general.get("community", "public") or "public"
return SNMPConfigDict(running=True, community=community, port=161, version="2c")
# ── Wake-on-LAN ──────────────────────────────────────────────────────────────
def send_wake_on_lan(self, mac_address: str, interface: str = "") -> dict[str, Any]:
"""Send a Wake-on-LAN magic packet via OPNsense's os-wol plugin.
Calls ``POST /api/wol/wol/set`` with an ephemeral (non-persisted) entry —
omitting ``uuid`` from the payload makes OPNsense's ``WolController::setAction``
validate and wake immediately without saving a host to config.xml. Requires
the os-wol plugin to be installed and the target interface to have a static
IPv4 address configured (OPNsense computes the broadcast address from the
interface's own IP/subnet; DHCP-assigned interfaces are rejected).
:param interface: OPNsense's *assigned* interface identifier (e.g. "lan",
"opt1") — NOT the physical device name returned by get_interfaces()/
get_networks() (e.g. "em0"). Required by this driver.
:raises ValueError: if interface is not provided.
"""
if not interface:
raise ValueError("OPNsense requires an interface for Wake-on-LAN")
try:
result = self._post(
"/api/wol/wol/set",
{"wake": {"interface": interface, "mac": mac_address.strip()}},
)
except Exception as exc:
logger.warning("Wake-on-LAN send failed for %s via %s: %s", mac_address, interface, exc)
return {"success": False, "output": str(exc)}
status = result.get("status")
if status == "error":
return {
"success": False,
"output": result.get("error_msg", "OPNsense rejected the Wake-on-LAN request"),
}
if not result:
# Model validation (malformed MAC/interface) fails silently with an
# empty {} response at HTTP 200 — no error_msg to surface.
return {
"success": False,
"output": "OPNsense rejected the request (check interface identifier and MAC format)",
}
return {"success": True, "output": f"Magic packet sent to {mac_address} via {interface}"}
def run_device_action(self, action: str) -> dict[str, Any]:
"""Execute a named action on the firewall."""
if action == "fix_snmp":