opnsense-go
A Go client library for the OPNsense API.
Installation
go get github.com/biptec/opnsense-go
Requires Go 1.23 or later.
Usage
Creating a client
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/biptec/opnsense-go/pkg/api"
"github.com/biptec/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 |
CRUD resources
Most OPNsense resources (host overrides, firewall aliases, IPsec connections,
etc.) follow the same Add/Get/Update/Delete pattern.
import (
"context"
"github.com/biptec/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.
RPC / settings calls
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)
SelectedMap and SelectedMapList
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 plain string-backed type.
api.SelectedMapList -- a multi-selection field; unmarshals to a
[]string-backed type. Marshals back as a comma-separated string.
api.SelectedMapListNL -- same as SelectedMapList but marshals with
newline separators instead of commas.
When constructing a resource to send to the API, assign the key directly:
import "github.com/biptec/opnsense-go/pkg/api"
alias := &firewall.Alias{
Type: api.SelectedMap("host"),
}
override := &unbound.HostOverride{
Type: api.SelectedMap("A"),
}
Error handling
When a GET request targets a resource that does not exist, the library returns
an *errs.NotFoundError:
import "github.com/biptec/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
}
Development
Code generation
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 all
There are two categories of generated output:
- Controllers (
pkg/<service>/controller.go, pkg/<service>/*.go) --
one per service, generated from schema/<service>.yml.
- Opnsense client (
pkg/opnsense/client.go) -- aggregates all controllers
into a single Client interface.
Adding a new service
- Create
schema/<service>.yml describing the endpoints and data types.
Use an existing schema file as a reference.
- Create
pkg/<service>/generate.go with the //go:generate directive.
Copy the file from any existing service package.
- Run
make all to generate the controller and update the opnsense client.
Running tests
Ordinary unit and compile checks run without a firewall:
make check
Live integration tests require an OPNsense instance. Set the following environment variables before running make testacc:
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 certificate
Run tests for a specific package:
go test -v ./pkg/<service>/...
License
MIT