feat(backend): add OPNsense backend (#743)
Docker / Build and Push (push) Canceled after 0s
github-pages / deploy (push) Canceled after 0s
Test / make test (push) Canceled after 0s
Docker / release (push) Canceled after 0s

Manages interfaces and peers on OPNsense through the WireGuard API in
OPNsense core, so a stock appliance needs nothing installed. OPNsense
calls a tunnel a "server" and a peer on it a "client"; those map to
PhysicalInterface and PhysicalPeer.

Reads go through searchXxx because getXxx returns select fields as
{value, selected} maps that cannot be posted back to a write. Validation
failures arrive as HTTP 200 with a "result": "failed" body, so the body
is checked and not the status code.

Firewall rules are not managed, matching the pfSense backend: a new
tunnel handshakes but carries no traffic until a pass rule exists.

Alpha, and documented as such.

Signed-off-by: clark-ja <37738506+clark-ja@users.noreply.github.com>
This commit is contained in:
Jacopo Clark
2026-09-10 22:27:17 +02:00
committed by GitHub
parent 32ef6048fb
commit 7f5786f40f
14 changed files with 2855 additions and 14 deletions
+23 -1
View File
@@ -283,7 +283,8 @@ func (p *PhysicalInterface) SetExtras(extras any) {
switch extras.(type) {
case MikrotikInterfaceExtras: // OK
case PfsenseInterfaceExtras: // OK
default: // we only support MikrotikInterfaceExtras and PfsenseInterfaceExtras for now
case OpnsenseInterfaceExtras: // OK
default: // we only support Mikrotik, Pfsense and Opnsense interface extras for now
panic(fmt.Sprintf("unsupported interface backend extras type %T", extras))
}
@@ -354,6 +355,14 @@ func ConvertPhysicalInterface(pi *PhysicalInterface) *Interface {
} else {
iface.Disabled = nil
}
case ControllerTypeOpnsense:
extras := pi.GetExtras().(OpnsenseInterfaceExtras)
iface.DisplayName = extras.Comment
if extras.Disabled {
iface.Disabled = &now
} else {
iface.Disabled = nil
}
}
return iface
@@ -382,6 +391,19 @@ func MergeToPhysicalInterface(pi *PhysicalInterface, i *Interface) {
Disabled: i.IsDisabled(),
}
pi.SetExtras(extras)
case ControllerTypeOpnsense:
// Uuid and Instance are OPNsense's identity for this tunnel, not
// user-editable data, so carry them across the merge rather than
// letting the caller re-inject them afterwards.
extras := OpnsenseInterfaceExtras{
Comment: i.DisplayName,
Disabled: i.IsDisabled(),
}
if existing, ok := pi.GetExtras().(OpnsenseInterfaceExtras); ok {
extras.Uuid = existing.Uuid
extras.Instance = existing.Instance
}
pi.SetExtras(extras)
}
}