8000 feat: add gateway and routing apis to the library by dquixote-jr · Pull Request #8 · commure/opnsense-go · GitHub
[go: up one dir, main page]

Skip to content

feat: add gateway and routing apis to the library - #8

Open
dquixote-jr wants to merge 32 commits into
dqjr/frrfrom
dqjr/route
Open

dquixote-jr wants to merge 32 commits into
dqjr/frrfrom
dqjr/route

Conversation

@dquixote-jr
Copy link
Copy Markdown
Member

No description provided.

browningluke and others added 30 commits May 24, 2026 02:22
* Support multi-interface NAT port forwards

* Use selected map list for port forward interfaces
* Update lagg options

LaggOptions and LaggStastics are missing and should have their values

* ran make all
…ield (browningluke#62)

Adds PathExpression field to the Alias struct to support the urljson
alias type, which allows downloading a JSON file and running a jq
expression to extract IP addresses.
…sponse (browningluke#63)

ReconfigureService panicked with errors.New when the response payload
contained neither a 'status' nor a 'result' field. As a library a panic
crashes the calling process (e.g. terraform apply); every other failure
in this function returns an error, so unify the behaviour.
The 'i := 0'/'i++' counter in GetAll was never read. While here,
tighten the loop to use 'for key := range ...' instead of
'for key, _ := range ...' to satisfy gosimple S1005.
…ne (browningluke#68)

The resource template falls back to `{{ .Controller.Name }}ReconfigureEndpoint`
when a resource has neither `reconfigure: "null"`, `readOnly: true`, nor
its own `endpoints.reconfigure`. controller.tmpl only declares that const
when the controller's top-level `reconfigureEndpoint` is non-empty, so a
schema that combines empty controller-wide reconfigure with a resource
that omits its own reconfigure would generate code referencing an
undeclared constant.

Add an explicit fallback to "" when both the resource-level and
controller-level endpoints are empty. Regenerating all current schemas
yields no diff (every existing resource hits one of the prior branches),
so this is purely defensive — but it removes a foot-gun the next time
someone adds a service whose reconfigure semantics live entirely on
per-resource endpoints with no controller-wide fallback.
…luke#65)

Get-style endpoints occasionally return 200 with a body that does not
include the configured monad key (for example {} when an ID has been
deleted out from under the caller). resourceUnwrap previously read the
nil RawMessage and forwarded the json.Unmarshal error 'unexpected end
of JSON input', which leaks an implementation detail and prevents
errors.As(err, &errs.NotFoundError) from matching.

Detect the missing/empty wrapped value up front and return
errs.NewNotFoundError() so callers' existing not-found branches fire.
…rowningluke#66)

Both SelectedMapList.MarshalJSON and SelectedMapListNL.MarshalJSON
called sort.Strings(*s) on the receiver. json.Marshal is conventionally
a read-only operation; mutating the underlying slice meant any other
goroutine iterating over the same SelectedMapList raced with the sort,
and the in-memory order observed by other code changed after a marshal.

Sort a defensive copy and Join from that instead.
…e#67)

GetControllerNames iterates every file returned by os.ReadDir and feeds
it to newController, which calls yaml.Unmarshal and panics on any error.
A single stray file in schema/ — .DS_Store on macOS, a README.md, an
editor swap file — broke 'go generate' with an opaque YAML error.

Skip directories and dir entries whose name does not end in .yml/.yaml
before handing them to newController.
…ningluke#69)

Every test under pkg/auth, pkg/dnsmasq, pkg/firewall, pkg/interfaces,
pkg/ipsec, pkg/kea and pkg/unbound is an integration test that requires
a live OPNsense. Without the env vars set, running 'go test ./...' on
a fresh checkout fails with cryptic network errors as the client dials
an empty URI.

Add a TestMain in each integration-only package that exits cleanly
(after printing a notice to stderr) when any of OPNSENSE_URI,
OPNSENSE_API_KEY or OPNSENSE_API_SECRET is unset. Other packages
(pkg/api, which holds pure unit tests) are unaffected.
mutexKV.Lock accepted a context.Context but used a plain sync.Mutex.Lock
under the hood, so a caller whose context was cancelled while another
goroutine held the global lock through a 30-second reconfigure could
not abort. Convert Lock to return an error: spawn a goroutine that
acquires the inner mutex, race it against ctx.Done(), and release the
inner lock immediately if the outer select returned via ctx.Err().

Callers (set and Delete) now early-return when Lock fails — they
already accept context cancellation everywhere else in the request
path, and never holding the lock means the existing 'defer Unlock' is
naturally a no-op on the error path (it would panic if invoked, but
the early return prevents that).
…e#71)

The CRUD helpers (set, Delete) lock GlobalMutexKV around the write +
reconfigure pair so concurrent goroutines can't trip OPNsense's
config.xml apply mid-write. The RPC path never took the same lock, so
a Terraform apply that touches both a CRUD resource (e.g.
opnsense_firewall_filter) and a singleton settings resource (e.g.
opnsense_unbound_settings) in parallel could race the mutating RPCs
(SettingsUpdate, SettingsReconfigure, service start/stop/restart, etc.)
against an in-flight CRUD reconfigure — the data-loss case AGENTS.md
documents the global mutex to prevent.

Lock unconditionally in api.Call. Search-style POST endpoints are
serialised too; correctness is the priority here, and read throughput
is not a meaningful axis for a Terraform-provider dependency where each
resource issues a small bounded number of calls per apply.
…wningluke#74)

The goroutine acquiring the inner mutex used a `default:` branch on the
send to `acquired`. If it reached the select before the outer goroutine
parked on `<-acquired`, the unbuffered send found no receiver, `default`
fired, the mutex was immediately unlocked, and the outer then waited
forever on a channel no one would send to.

Replace the `default:` with an `abandoned` channel the outer closes on
ctx cancellation. The inner select now has no non-blocking escape: it
either hands the lock off via `acquired` or waits for the explicit
abandon signal.
Adds a generic `queryParameter: true` flag on RPC `params` entries so the
generator can emit `?name=value` query strings, the way OPNsense routes
that take URL-encoded or hyphenated argument values require (e.g.
`/openvpn/instances/gen_key?type=tls-auth` — the path-parameter form
the existing generator emits only works for the default route value).

RPCOpts gains a `QueryParameters` map; `EndpointURL()` appends it with
deterministic key ordering and proper URL escaping. Existing schemas
(path-only and body-only) regenerate to declare an empty `queryParams`
map and pass it through — wire-compatible, no behaviour change.
…writes (browningluke#72)

* feat(generate): support `queryParameter` on RPC params

Adds a generic `queryParameter: true` flag on RPC `params` entries so the
generator can emit `?name=value` query strings, the way OPNsense routes
that take URL-encoded or hyphenated argument values require (e.g.
`/openvpn/instances/gen_key?type=tls-auth` — the path-parameter form
the existing generator emits only works for the default route value).

RPCOpts gains a `QueryParameters` map; `EndpointURL()` appends it with
deterministic key ordering and proper URL escaping. Existing schemas
(path-only and body-only) regenerate to declare an empty `queryParams`
map and pass it through — wire-compatible, no behaviour change.

* feat(openvpn): add controller for instances, static keys, client overwrites and service RPCs
Adds CRUD support for Unbound ACL entries via the
/unbound/settings/{add,get,set,del}_acl endpoints.

Co-authored-by: browningluke <browningluke@users.noreply.github.com>
…luke#77)

Adds generated controller and structs for:
- Trust/Ca CRUD (no post-mutation reconfigure required)
- Trust/Cert CRUD (no post-mutation reconfigure required)
- Trust/Settings get/set/reconfigure RPC (singleton)

Co-authored-by: browningluke <browningluke@users.noreply.github.com>
…ers (browningluke#75)

- Extend schema/wireguard.yml with an rpc section for:
  - General (get/set the singleton wireguard enabled flag)
  - Service (GenKeyPair via /server/keyPair, GenPsk via /client/psk)
- Run make all to regenerate pkg/wireguard/general.go and pkg/wireguard/gen.go
- Add integration tests covering GeneralGet/Set, ServiceGenKeyPair, ServiceGenPsk
Co-authored-by: Vijayan Sampath <vijayanms@gmail.com>
….10+ (browningluke#83)

* fix(routes): rename route disabled field to enabled for OPNsense 26.1.10+

OPNsense 26.1.10 (core commit 3acfb5f2, PR #10027) replaced the route
model's 'disabled' field (default 0) with 'enabled' (default 1) and
migrated existing configs. Since then the API silently drops the
'disabled' key on add/set — routes are always created enabled — and
omits it from getroute responses, so reads decode the zero value.
Rename the schema attribute so the field round-trips again.

Requires OPNsense >= 26.1.10 for the routes controller.

* fix(routes): keep disabled field alongside enabled for pre-26.1.10 compatibility

Expose both keys on the Route struct. OPNsense ignores unknown model
fields, so writes carrying both values work on either side of the
26.1.10 rename, and readers can fall back to whichever key the server
populated.

* docs: document dual-key compatibility pattern for renamed API fields

* docs(routes): mark disabled as deprecated compat field in schema

Enabled is the canonical field since OPNsense 26.1.10; Disabled exists
only for pre-26.1.10 compatibility.
…luke#84)

The dnsmasq integration suite now takes ~9 minutes, right at go test's
default 10m per-binary timeout. Recent runner-image updates have slowed
individual tests by ~5-15%, making it plausible that any long-running
suite tips over the limit and panics with 'test timed out after 10m0s'
— misread as a deadlock. Set -timeout 120m in CI and make testacc so
package runtime is bounded by the suite, not the Go default.
Run the integration suite as a matrix of shards, each on its own runner
with its own OPNsense VM: dedicated shards for the long-running dnsmasq
(~9 min), ipsec (~3.7 min) and firewall (~2.7 min) suites, and a shared
bucket for everything else. Wall-clock drops from the full-suite serial
~15 min to the slowest shard (~9 min including VM boot).

Timing evidence from run 27878904613 (feat/monit-syslog PR, 2026-06-20):
  dnsmasq 539s, ipsec 226s, firewall 166s, openvpn 74s, trust 41s,
  unbound 39s, kea 32s, cron 21s, auth 14s, interfaces 12s, wireguard 3s.

A plan job maps the requested packages onto shards, so Tier 1's
changed-package detection still narrows what runs. It fails the build
if a pkg/<service> directory is not assigned to a shard, and AGENTS.md
now documents that new service packages must be added to SHARDS.
Adds three Monit resources (Service, Test, Alert) and one Syslog
resource (Destination) with full CRUD support and acceptance tests.

Co-authored-by: Vijayan Sampath <vijayanms@gmail.com>
)

PR browningluke#81 added pkg/monit and pkg/syslog but didn't register them in the
shard map, failing the plan job's must-be-assigned check.
…ngluke#89)

* fix(ci): don't skip Tier 2 tests when only pipeline workflows change

go-test.yml ignored all of .github/**, so a push to main that only
edits go-test.yml or go-test-reusable.yml never ran the full suite —
confirmed this happened for browningluke#88, which fixed the shard map but was
never actually exercised by CI after merging.

* fix(ci): run full suite in Tier 1 when the test pipeline itself changes

detect-changes only looked at .go/go.mod/go.sum diffs, so a PR editing
go-test-pr.yml or go-test-reusable.yml alone (e.g. browningluke#88) triggered the
workflow but immediately skipped the test job as "no Go files changed" —
the pipeline change went completely unexercised before merge.
…rowningluke#90)

OPNsense's addDot/setDot endpoints always store type=dot and
addForward/setForward always store type=forward, regardless of the
type value in the request body - the endpoint alone determines the
stored type server-side. The Forward resource previously only ever
called the Dot-suffixed endpoints, so every entry was created as
type=dot no matter what was sent.

Repoint Forward at addForward/getForward/setForward/delForward and add
a Dot resource for addDot/getDot/setDot/delDot, so callers can create
either type by choosing the matching resource.
* feat(monit): add monit service, alert, and settings controllers

Adds pkg/monit with API bindings for OPNsense Monit services,
alerts, and general settings, wired into the opnsense client.
CI test sharding updated to cover the new package.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(monit): unwrap general nesting in settings API payload

/api/monit/settings/get returns {"monit":{"general":{...}}} but the
structs expected the settings directly under "monit", so every field
decoded to its zero value. Add SettingsGeneral wrapper for get and set,
and map mailserver/httpdAllow as SelectedMapList since the API returns
them as selected-option maps.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(monit): satisfy required fields in Alert/Service integration tests

TestAlert omitted the required noton field and sent an invalid events
option ("Action"), and TestService omitted the required starttimeout
field, causing the OPNsense API to reject the add calls.

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* feat(interfaces): add bridge controller

Adds generated bridge_settings API bindings (add/get/update/delete)
covering members, STP, and advanced bridge options. Integration test
is gated behind OPNSENSE_TEST_BRIDGE_MEMBER since bridge changes apply
immediately; CI sets it to lan on the throwaway VM.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(ci): don't force bridge test onto a nonexistent lan interface

The CI VM is single-NIC (wan-only); "lan" is never an assigned interface
there, so TestBridge failed with "Option [lan] not in list." on every
run. The test already skips gracefully when the member interface isn't
set — let it do that here rather than forcing a bridge onto wan, which
is the VM's only live network path.

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants

0