A Go client library for the OPNsense API.
go get github.com/browningluke/opnsense-goRequires Go 1.23 or later.
The library has two layers: a low-level api.Client that handles HTTP and
authentication, and a higher-level opnsense.Client that exposes typed
controllers for each OPNsense service.
import (
"github.com/browningluke/opnsense-go/pkg/api"
"github.com/browningluke/opnsense-go/pkg/opnsense"
)
apiClient := api.NewClient(api.Options{
Uri: "https://<opnsense-host>",
APIKey: "<api-key>",
APISecret: "<api-secret>",
AllowInsecure: true, // set to true when the firewall uses a self-signed certificate
})
client := opnsense.NewClient(apiClient)api.Options also accepts retry tuning fields:
| Field | Default | Description |
|---|---|---|
MaxRetries |
4 | Number of times to retry a failed request |
MinBackoff |
1s | Minimum wait between retries |
MaxBackoff |
30s | Maximum wait between retries |
Logger |
stdlib default | Custom *log.Logger for request/response logging |
Most OPNsense resources (host overrides, firewall aliases, IPsec connections, etc.) follow the same Add/Get/Update/Delete pattern.
import (
"context"
"github.com/browningluke/opnsense-go/pkg/unbound"
)
ctx := context.Background()
// Add
id, err := client.Unbound().AddDomainOverride(ctx, &unbound.DomainOverride{
Enabled: "1",
Domain: "example.internal",
Server: "192.168.1.1",
Description: "internal zone",
})
// Get
override, err := client.Unbound().GetDomainOverride(ctx, id)
// Update
override.Server = "192.168.1.2"
err = client.Unbound().UpdateDomainOverride(ctx, id, override)
// Delete
err = client.Unbound().DeleteDomainOverride(ctx, id)Each mutating call (Add, Update, Delete) automatically triggers the relevant OPNsense service reconfigure so changes take effect immediately.
Some controllers expose RPC-style calls for reading or writing global settings rather than individual records.
// Read the current Unbound settings
settings, err := client.Unbound().SettingsGet(ctx)
if err != nil {
return err
}
// Modify a field and write back
settings.Unbound.Advanced.HideIdentity = "1"
result, err := client.Unbound().SettingsUpdate(ctx, &settings.Unbound)OPNsense represents enumerated fields and multi-select fields as maps with a
selected key rather than plain strings. This library provides two types that
unmarshal those responses transparently:
api.SelectedMap-- a single-selection field; unmarshals to the selected key as a plainstring-backed type.api.SelectedMapList-- a multi-selection field; unmarshals to a[]string-backed type. Marshals back as a comma-separated string.api.SelectedMapListNL-- same asSelectedMapListbut marshals with newline separators instead of commas.
When constructing a resource to send to the API, assign the key directly:
import "github.com/browningluke/opnsense-go/pkg/api"
alias := &firewall.Alias{
Type: api.SelectedMap("host"),
}
override := &unbound.HostOverride{
Type: api.SelectedMap("A"),
}When a GET request targets a resource that does not exist, the library returns
an *errs.NotFoundError:
import "github.com/browningluke/opnsense-go/pkg/errs"
override, err := client.Unbound().GetDomainOverride(ctx, id)
if err != nil {
var notFound *errs.NotFoundError
if errors.As(err, ¬Found) {
// resource was deleted upstream
}
return err
}The typed controllers and data structs are generated from YAML schema files
under the schema/ directory. The generator lives in internal/generate/.
Regenerate everything:
make allThere are two categories of generated output:
- Controllers (
pkg/<service>/controller.go,pkg/<service>/*.go) -- one per service, generated fromschema/<service>.yml. - Opnsense client (
pkg/opnsense/client.go) -- aggregates all controllers into a singleClientinterface.
- Create
schema/<service>.ymldescribing the endpoints and data types. Use an existing schema file as a reference. - Create
pkg/<service>/generate.gowith the//go:generatedirective. Copy the file from any existing service package. - Run
make allto generate the controller and update the opnsense client.
Tests require a live OPNsense instance. Set the following environment variables before running:
export OPNSENSE_URI="https://<opnsense-host>"
export OPNSENSE_API_KEY="<key>"
export OPNSENSE_API_SECRET="<secret>"
export OPNSENSE_ALLOW_INSECURE="true" # if using a self-signed certificateRun tests for a specific package:
go test -v ./pkg/<service>/...