34 Commits
Author SHA1 Message Date
christianmanivong 995282c5be Merge pull request 'feat: port forwards, read from destination NAT on the WAN' (#4) from feature/port-forwards into master
CI / test (3.10) (push) Failing after 13s
CI / test (3.11) (push) Failing after 13s
CI / test (3.12) (push) Failing after 13s
CI / test (3.9) (push) Failing after 14s
2026-10-03 14:31:04 +00:00
Christian Manivong 3506f20606 feat: port forwards, read from destination NAT on the WAN
CI / test (3.10) (push) Failing after 1m39s
CI / test (3.11) (push) Failing after 25s
CI / test (3.12) (push) Failing after 12s
CI / test (3.9) (push) Failing after 31s
CI / test (3.10) (pull_request) Failing after 13s
CI / test (3.11) (pull_request) Failing after 12s
CI / test (3.12) (pull_request) Failing after 13s
CI / test (3.9) (pull_request) Failing after 12s
get_port_forwards reads /api/firewall/d_nat/search_rule and keeps only what
the contract asks for: rules on an interface with an upstream gateway (the
WAN, and a second uplink as well). Internal redirects, anti-lockout rules
(nordr) and rules the captive portal generates are left out -- on the first
real box (OPNsense 26.7) that was 20 of 22 rules, and each would have made an
internal host look reachable from the internet.

Targets resolve through host/network aliases, one entry per address; an
interface address or a DNS name gives no address and the rule is skipped
rather than put on a guessed host. Ports resolve as numbers, the start of a
range, port aliases or service names; no port is every port (0), and tcp/udp
is two entries. The filtering is pure, in port_forwards.py, and the driver
method does the three reads.

A box without the destination-NAT API raises instead of answering "nothing
forwarded", which nobody checked.
2026-10-03 16:30:38 +02:00
Christian Manivong 4dd0fc2aee fix: say what uninstall_package cannot reach, instead of posting anyway
CI / test (3.10) (push) Failing after 1m10s
CI / test (3.11) (push) Failing after 1m7s
CI / test (3.12) (push) Failing after 13s
CI / test (3.9) (push) Failing after 24s
`firmware/remove` acts on the OPNsense plugin set, and `get_packages`
reads the same list — so software installed as a plain FreeBSD package is
invisible to the one and unreachable by the other. The Wazuh agent is
exactly that, on a driver the agent plugin lists as supported.

Observed during a fleet-wide rollback on 2026-09-19: the `gw` device
could not be handled through netOrk at all, and the request posted for it
could never have succeeded.

A name that is not a plugin now raises NotImplementedError rather than
being POSTed. A request that cannot work reports failure for the wrong
reason and sends whoever reads it looking in the wrong place; netOrk
turns NotImplementedError into a 501, which is the accurate answer.

Reaching plain packages would need shell access, and the credentials
stored for these devices are frequently API-key only — that is a decision
of its own, not a detail of this one.

The injection guard still runs first: a malformed name is a ValueError
before anything asks whether it is a plugin.

netork#241
2026-09-20 22:44:04 +02:00
Christian Manivong 5d193ba7e8 fix(ping): post the settings where the API expects them, not one node above
CI / test (3.11) (push) Failing after 7s
CI / test (3.10) (push) Failing after 12s
CI / test (3.9) (push) Failing after 6s
CI / test (3.12) (push) Failing after 18s
Every ping against a live OPNsense failed:

    ping job creation failed for 10.30.0.1: {'result': 'failed',
    'validations': {'ping.settings.hostname': 'A value is required.'}}

_ping_model_node read GET /api/diagnostics/ping/get and took the single
dict-valued key as the node to post under. The real model nests two levels:

    {"ping": {"settings": {"hostname": "", "fam": {"ip": {...}, "ip6": {...}},
                           "source_address": "", "packetsize": "", ...}}}

so the helper answered "ping" and the job was created with the fields sitting
where the settings node belongs. The hostname never arrived, and the firewall
said so on every single call.

_ping_model_path walks the whole chain of single-dict wrappers and stops at the
first level holding more than one key — the field level, where fam is a dict
too and one more step would land inside a form field. _wrap_in_model nests the
settings accordingly, so a one-level model keeps working and the default, for
when /get cannot be read, is what current firmware ships.

The tests missed this because FakePingAPI answered /get with a one-level model
and read the posted payload back through the same assumption: the fake agreed
with the code about a shape neither of them shares with a device. It now speaks
what an OPNsense speaks, reads the payload through the model path, and a second
test keeps the one-level case covered.

Verified against a live firewall: the job is accepted ({"result": "ok"}) and
10.30.0.1 answers 3 of 3 at 0.116 ms.

Closes #3
2026-08-22 15:44:56 +07:00
Christian Manivong efba4334ff feat: declare USES_SSH = False
CI / test (3.10) (push) Failing after 6s
CI / test (3.11) (push) Failing after 7s
CI / test (3.9) (push) Failing after 7s
CI / test (3.12) (push) Failing after 17s
Everything this driver does runs over the OPNsense REST API; there is no SSH
session. netOrk kept that fact in two hardcoded driver-name sets on its side
(netork#113) and now reads it from the driver.
2026-08-21 13:07:14 +07:00
Christian Manivong d27c32096d fix(dns): report every host override so duplicates can be cleared
CI / test (3.10) (push) Failing after 21s
CI / test (3.11) (push) Failing after 21s
CI / test (3.12) (push) Failing after 26s
CI / test (3.9) (push) Failing after 14s
The inventory collapsed rows that shared a name, domain, address and type,
which was meant to hide the alias records searchhostoverride lists alongside
their parent. It hid genuine duplicates too — and since sync_dns_zone deletes
what the inventory tells it about, it could only ever remove one copy per name
before adding a fresh one. A pile of identical overrides could grow but never
shrink.

An office firewall reached fifteen identical A records for its own name, all
tagged [netork], while netOrk's database showed one.

OPNsense flags alias rows with isAlias, so filter on that and report every
remaining row under its own UUID. Releases that predate the flag give no way to
tell an alias from a copy, so the content collapse stays as a fallback there.
sync_dns_zone now also pushes one override per logical record, because two
netOrk rows for one host is a state its database can legitimately be in.

The auto-PTR flag is read from addptr, which is what OPNsense 26.1 returns;
ptrrecord was absent from every row, so the default made every A record claim
it managed a PTR — and netOrk derives reverse-zone entries from exactly that
flag. addptr now goes out on writes alongside the legacy name.

Refs christianmanivong/netork#103, christianmanivong/netork#107
2026-08-20 23:35:04 +07:00
Christian Manivong 960aaefa13 fix(dhcp): read the Kea subnet record from subnet4, not subnet
CI / test (3.10) (push) Failing after 8s
CI / test (3.11) (push) Failing after 8s
CI / test (3.12) (push) Failing after 13s
CI / test (3.9) (push) Failing after 8s
getSubnet wraps its record under `subnet4`. The driver read `subnet`, got
nothing, and carried on:

  - get_dhcp_subnets() reported every subnet with no pools, no options and no
    description. Only the CIDR survived, and only because it falls back to the
    searchSubnet row. Confirmed against a live OPNsense serving six subnets:
    all six came back with empty pools while the device had
    "10.10.0.100-10.10.0.250" and routers/DNS/NTP set on each.

  - apply_dhcp_subnet() read the same key to merge the options it was not
    asked to change. An empty record means nothing to preserve, so updating a
    subnet with only domain_search set would have written back only that one
    option and blanked the routers Kea autocollected — stranding every client
    on that VLAN without a gateway. That is precisely the failure the merge
    exists to prevent.

The unit fixtures encoded the wrong shape, which is why the safety test
test_unnamed_options_are_preserved_on_update passed while the real thing was
broken. They now carry the response captured from OPNsense 25.x, and correcting
them turns that test red against the old parse.

Both call sites go through _kea_subnet_record(), which prefers `subnet4` and
falls back to `subnet` for older builds.
2026-08-20 11:49:16 +07:00
Christian Manivong 0c5670981d feat(interfaces): report the assigned interface name alongside the physical one
CI / test (3.10) (push) Failing after 8s
CI / test (3.11) (push) Failing after 7s
CI / test (3.12) (push) Failing after 10s
CI / test (3.9) (push) Failing after 8s
get_interfaces() keys entries by the physical device ("em0"), which is what
every other call in this driver speaks. Wake-on-LAN is the exception: it needs
the name OPNsense assigned ("lan", "opt1") and silently rejects anything else
with an empty {} at HTTP 200.

The overview export already carries it, so pass it through as "identifier".
Empty for interfaces OPNsense has not assigned.
2026-08-20 11:10:25 +07:00
Christian Manivong 8ba95a0709 feat(dhcp): implement subnet get/apply/commit against Kea DHCPv4
CI / test (3.10) (push) Failing after 8s
CI / test (3.11) (push) Failing after 8s
CI / test (3.12) (push) Failing after 10s
CI / test (3.9) (push) Failing after 8s
Part of netork#85. Fills in the three device-specific methods the new
DhcpServerMixin subnet layer expects.

searchSubnet only carries uuid/subnet/description, so get_dhcp_subnets
follows each row with getSubnet for the option data. That is one request per
subnet; a firewall serves a handful, so the round trips cost less than the
reconfigure they help avoid. An option Kea does not carry is omitted rather
than reported as empty, because the generic diff reads an absent key as
"not managed" — reporting [] would make every unmanaged option look like a
pending change.

apply_dhcp_subnet honours the mixin's partial-update contract: on an update
it reads the subnet's current options first and replaces only the named
ones. Without that, managing domain_search alone would blank the routers Kea
autocollected and strand every client on that VLAN without a gateway.
Setting any option also forces option_data_autocollect off — left on, Kea
keeps re-filling routers/DNS/NTP and the next diff sees a change again,
which is a reconfigure loop rather than a converged state.

OPNsense renders repeatable option fields as comma-separated strings in some
versions and as a selection map in others, for the same logical field. Both
shapes are accepted rather than pinning the driver to one release. Pools are
a newline-separated text block.

A subnet whose detail fetch fails is skipped with a log line instead of
aborting, same rule as get_dhcp_reservations: one broken record must not
make the whole inventory unreadable.

16 new tests. Not yet verified against a live device — no reachable OPNsense
at the time of writing, same caveat the reservation support shipped with.
2026-08-20 07:21:55 +07:00
Christian Manivong 1eb378c04c feat(dhcp): implement DhcpServerMixin against Kea DHCPv4
CI / test (3.10) (push) Failing after 10s
CI / test (3.11) (push) Failing after 8s
CI / test (3.12) (push) Failing after 10s
CI / test (3.9) (push) Failing after 7s
Fills in the three device-specific methods so the generic diff/apply from
napalm-device-types works against OPNsense: searchReservation for the read,
addReservation/setReservation for the write, service/reconfigure for the
commit.

Until now the driver could only create and delete reservations as a
side-effect of VM provisioning, and never read them back — so there was no
way to see what a firewall already had.

Kea's `subnet` field on a reservation is a model relation that comes back as
the related subnet's CIDR in some versions and as its UUID in others. Both
are accepted and normalised to a CIDR; an unresolvable relation degrades to
an empty string rather than raising, so one orphaned entry cannot make the
whole inventory unreadable.

apply_dhcp_reservation deliberately does not reconfigure: that is the
commit's job, so a batch costs one daemon reload instead of one per entry.

Verified against mocked Kea responses only — no live OPNsense was available
at the time of writing. The CIDR-vs-UUID branch in particular is defensive
rather than empirically confirmed.
2026-08-19 07:24:48 +07:00
Christian Manivong f934cc0cfa feat(ping): implement ping and a batched ping_sweep over the diagnostics API
CI / test (3.10) (push) Failing after 19s
CI / test (3.11) (push) Failing after 8s
CI / test (3.12) (push) Failing after 11s
CI / test (3.9) (push) Failing after 8s
OPNsense has no synchronous ping endpoint. /api/diagnostics/ping is a job API —
create, start, read statistics, stop, remove — so a single ping costs five
requests and roughly a second of waiting, which makes the generic per-host
sweep from napalm-device-types unusable for a whole subnet.

The override exploits what the job API does offer instead: jobs are
independent and run on the firewall in parallel, and search_jobs reports all of
them in one response. A batch (32 by default) is created and started, waited
for once, harvested with a single request and then cleaned up, so waiting time
per batch is constant rather than linear in hosts. PING_SWEEP_MAX_TARGETS
bounds the sweep as a whole — this runs on production firewalls.

Two details the API forces: results are polled, because search_jobs signals the
running ping with SIGINFO and then parses whatever it has written so far, so
the first read of a healthy host can still show zero probes; and the model's
root node is read from /get rather than hardcoded, so a rename in a future
OPNsense release cannot silently break job creation.

Tested against mocked API responses only — no live device is reachable at the
moment, so the endpoint shapes come from the OPNsense sources (PingController,
scripts/interfaces/ping.py).
2026-08-13 16:51:05 +07:00
Christian Manivong 1f29d9d57d fix(tests): correct ADDRESSES_RESPONSE fixture shape in TestGetInterfacesIp
CI / test (3.10) (push) Failing after 19s
CI / test (3.11) (push) Failing after 7s
CI / test (3.12) (push) Failing after 7s
CI / test (3.9) (push) Failing after 7s
Verified against a live OPNsense 24.7 instance: GET
/api/interfaces/overview/export returns a bare list of interface dicts
keyed by "device" with CIDR "addr4"/"addr6" strings, matching what
get_interfaces_ip() already parses. The fixture's "items"/"interface"/
"address"/"prefix" shape never matched, so all four TestGetInterfacesIp
tests failed regardless of driver correctness.
2026-07-24 10:26:01 +02:00
Christian Manivong 8d3c443159 feat(firewall): implement apply_firewall_rule + commit_firewall_rules
CI / test (3.10) (push) Failing after 7s
CI / test (3.11) (push) Failing after 7s
CI / test (3.12) (push) Failing after 8s
CI / test (3.9) (push) Failing after 7s
OPNsense-specific half of the FirewallDriver diff/apply mechanism added
in napalm-device-types: translates the vendor-neutral rule dict into the
/api/firewall/filter/addRule or setRule/<uuid> payload (string "1"/"0"
booleans, empty interface = floating rule -- same shape as the existing
SNMP self-provisioning rule in _action_fix_snmp), and commit_firewall_rules
reloads the filter via /api/firewall/filter/apply. get_firewall_rules()
already returns compatible field names, no changes needed there.
2026-07-20 15:05:53 +02:00
Christian Manivong d6a0b21dc6 refactor(warnings): report raw signal only, no severity/presentation
CI / test (3.10) (push) Failing after 8s
CI / test (3.11) (push) Failing after 7s
CI / test (3.12) (push) Failing after 8s
CI / test (3.9) (push) Failing after 7s
get_device_warnings() now returns only {code, meta} — severity, title,
message, and action are resolved centrally by netork's
WARNING_CATALOG (netork/core/device_warnings.py), not by the driver.
Keeps this driver independent of netork and avoids per-vendor drift in
how the same warning code is presented.
2026-07-20 09:54:14 +02:00
Christian Manivong 20d9d9651a fix(freeradius): return the created entry's remote id from create_radius_*
CI / test (3.10) (push) Failing after 8s
CI / test (3.11) (push) Failing after 7s
CI / test (3.12) (push) Failing after 7s
CI / test (3.9) (push) Failing after 7s
add_client/add_user's response carries no id, so create_radius_client()
and create_radius_user() now look the new entry up via
get_radius_clients()/get_radius_users() (matched by name/username)
immediately after creation. Callers need this id to address the entry in
later set_*/del_* calls -- without it there was no way to store a
reference to what was just created.
2026-07-15 15:30:06 +02:00
Christian Manivong e51607a020 feat(freeradius): add NAS client and user CRUD driver methods
CI / test (3.10) (push) Failing after 7s
CI / test (3.11) (push) Failing after 7s
CI / test (3.12) (push) Failing after 8s
CI / test (3.9) (push) Failing after 7s
get/create/delete_radius_client and get/create/delete_radius_user, backed
by /api/freeradius/{client,user}/{search,add,del}_* and a reconfigure call
to apply changes. Endpoints and field names (client.ip, not ipaddr) verified
against a live OPNsense 24.7 instance via a real add -> search/get -> set ->
del round trip, cleaned up immediately after.
2026-07-15 15:26:05 +02:00
Christian Manivong c8caa14176 feat(dyndns): add get_ddns_status() for os-ddclient enabled/running state
CI / test (3.10) (push) Failing after 8s
CI / test (3.11) (push) Failing after 7s
CI / test (3.12) (push) Failing after 7s
CI / test (3.9) (push) Failing after 7s
Verified against a live OPNsense 24.7 instance: the service id is
"ddclient" but the REST module is "dyndns" (/api/ddclient/* all 404).
Scoped to enabled/running only -- no ddclient/dyndns account was
configured on the test device to verify a per-account "registered IP"
shape against, so that comparison is deliberately left out rather than
guessed.
2026-07-15 13:52:20 +02:00
Christian Manivong 26470676ce feat(trust): add get_certificates() for Trust store certificate inventory
CI / test (3.10) (push) Failing after 8s
CI / test (3.11) (push) Failing after 7s
CI / test (3.12) (push) Failing after 8s
CI / test (3.9) (push) Failing after 7s
Reads certificates via POST /api/trust/cert/search, normalising each row
to {name, issuer, valid_from, valid_to, in_use_by}. Field mapping (Unix
timestamps for validity, %caref for the resolved issuer label) verified
against a live OPNsense 24.7 instance. Never surfaces crt_payload/
prv_payload/csr_payload -- those carry private key material.
2026-07-15 12:26:36 +02:00
Christian Manivong 62424cfd71 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.
2026-07-12 11:17:48 +02:00
Christian ManivongandClaude Sonnet 5 b8a68fc3a8 fix(opnsense): correct Kea leases4 del_lease endpoint — path param, not body
CI / test (3.10) (push) Failing after 7s
CI / test (3.11) (push) Failing after 7s
CI / test (3.12) (push) Failing after 8s
CI / test (3.9) (push) Failing after 7s
The lease-delete call never actually worked: it posted {"ip-address": ip}
to /api/kea/leases4/delLease, both wrong. Verified live against a real
OPNsense instance while cleaning up stale leases left by failed NetOrk VM
provisioning attempts — every call returned {"status": "error", "message":
"Missing lease IP parameter"} despite three different body-parameter
guesses (ips as list, ips as string, ip singular). The official API docs
(docs.opnsense.org/development/api/core/kea.html) show LeasesController as
"Abstract [non-callable]" with a del_lease($ips=null) action; despite that
signature looking like a body field, the concrete leases4 route only
accepts the IP as a URL path segment: POST /api/kea/leases4/del_lease/{ip}
confirmed {"status": "ok"} and the lease actually gone from a follow-up
search.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-09 10:31:05 +02:00
Christian ManivongandClaude Sonnet 5 b8dac1db63 feat(opnsense): add delete_dhcp_reservation_and_lease() for Kea DHCPv4
CI / test (3.10) (push) Failing after 7s
CI / test (3.11) (push) Failing after 7s
CI / test (3.12) (push) Failing after 7s
CI / test (3.9) (push) Failing after 7s
Combined removal of a static reservation and its active lease, needed by
NetOrk's VM-deletion cleanup flow. Reservation deletion follows the same
search-then-del<X>/{uuid} + reconfigure pattern as create_dhcp_reservation
and raises on failure; lease deletion is best-effort/non-fatal since the
Kea lease-delete endpoint shape is unverified against a real box.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-08 19:24:04 +02:00
Christian Manivong c12065c114 Merge feature/dhcp-static-reservation: create_dhcp_reservation() for Kea DHCPv4
CI / test (3.10) (push) Failing after 8s
CI / test (3.11) (push) Failing after 7s
CI / test (3.12) (push) Failing after 7s
CI / test (3.9) (push) Failing after 7s
2026-07-08 09:09:45 +02:00
Christian Manivong 806a23018d feat(opnsense): add create_dhcp_reservation() for Kea DHCPv4 static mappings
Only Kea (os-kea plugin) is supported — no active OPNsense environment
with legacy ISC DHCP was available to verify a second code path against.
Payload/response shapes (searchSubnet, searchReservation, addReservation,
setReservation, delReservation, service/reconfigure) were confirmed
against a real OPNsense box via a live add + verify + delete cycle
before writing this method and its tests.
2026-07-08 09:09:41 +02:00
Christian ManivongandClaude Sonnet 4.6 e3c36a1d34 feat: Fingerprint-Attribute für Discovery-Scoring
CI / test (3.10) (push) Failing after 7s
CI / test (3.11) (push) Failing after 7s
CI / test (3.12) (push) Failing after 7s
CI / test (3.9) (push) Failing after 7s
Ergänzt DRIVER_NAME, HTTP_FINGERPRINT, SNMP_FINGERPRINT, SSH_FINGERPRINT,
PORT_SPECS und SNMP_OBJECT_ID_PREFIX gemäß docs/DISCOVERY_FINGERPRINTING.md.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-24 14:47:08 +02:00
Christian ManivongandClaude Sonnet 4.6 629822c055 fix: set listen IP in _action_fix_snmp so snmpd binds to management IF
Without an explicit listen address, os-net-snmp on OPNsense may not
respond on non-loopback interfaces. The fix sets
listen = {self.hostname: {"selected": 1}} which is always the
management IP used to reach this device in netOrk.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-20 03:46:48 +02:00
Christian ManivongandClaude Sonnet 4.6 9f9fafb6f5 fix: _action_fix_snmp skips reinstall if plugin already present
Calling firmware/install on an already-installed os-net-snmp plugin
triggers an async reinstall that overwrites the config with factory
defaults a few minutes later — causing SNMP to stop working again.
Now checks /api/netsnmp/general/get first and only installs if the
plugin is genuinely absent.

Also adds a lightweight UDP/161 probe to verify SNMP is actually
reachable after the fix (falls back to API config check if the
socket probe is unavailable).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-20 03:41:18 +02:00
Christian ManivongandClaude Sonnet 4.6 8e413e4c60 fix: _action_fix_snmp adds floating firewall rule for UDP/161
OPNsense default-drops traffic arriving on non-LAN interfaces (e.g.
WireGuard tunnels used as management networks). Even with os-net-snmp
running and configured, SNMP is unreachable from external management
hosts because no firewall rule allows it.

Now adds a floating pass rule for UDP/161 → (self) after configuring
the service, then applies the firewall. Skips the rule if one with
the same description already exists (idempotent).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-20 02:24:21 +02:00
Christian ManivongandClaude Sonnet 4.6 0928584b5d feat: DNS host overrides — ptrrecord support, dedup, arpa guard
- _get_unbound_host_overrides: deduplicate by (hostname, domain, ip, rr)
  to suppress alias rows that OPNsense returns alongside parent records
- _get_unbound_host_overrides: read ptrrecord field so callers know which
  A records have an auto-managed PTR in the reverse zone
- sync_dns_zone: set ptrrecord=1 when creating A/AAAA host overrides so
  OPNsense Unbound manages the PTR record internally
- sync_dns_zone: refuse arpa zone names with ValueError — PTR records
  must never be written back via the host override API

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 15:02:04 +02:00
Christian ManivongandClaude Sonnet 4.6 b93628ac57 feat: include VLAN tag in get_networks() output
Routed subnets on 802.1Q sub-interfaces now report their vlan_id so
callers can associate a subnet with the VLAN it belongs to. None for
untagged interfaces.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-12 17:17:15 +02:00
Christian ManivongandClaude Sonnet 4.6 5d5bd77b08 feat: get_route_to() — Protokoll aus BSD-Flags, family-Feld, OSPF-Enrichment
- Protokoll-Erkennung aus flags: S=static, kein Gateway=connected, sonst=kernel
- Optionale OSPF-Anreicherung via /api/quagga/ospf/routes (FRR)
- family-Feld (ipv4/ipv6) aus Netzadresse abgeleitet
- link#X und 0.0.0.0 als Next-Hop bereinigt
- API-Response kann Liste oder Dict sein (beide Formate unterstützt)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-06 15:46:55 +02:00
Christian ManivongandClaude Sonnet 4.6 908024345e fix: get_interfaces_ip() uses overview/export statt addresses/export
Das /api/interfaces/addresses/export Endpoint existiert nicht auf allen
OPNsense-Versionen. Stattdessen wird /api/interfaces/overview/export
genutzt (gleiche Quelle wie get_interfaces()), um addr4/addr6 zu parsen.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-06 15:00:38 +02:00
Christian ManivongandClaude Sonnet 4.6 4434d4195f fix: accept verify_ssl/ssl_verify optional args; add get_firewall_aliases/rules
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-04 10:07:45 +02:00
Christian ManivongandClaude Sonnet 4.6 d926218eff feat: SNMP support — get_snmp_config(), fix_snmp action
get_snmp_config() calls GET /api/netsnmp/general/get with 5s timeout (plugin
may not be installed). fix_snmp installs os-net-snmp package, configures via
POST /api/netsnmp/general/set with community 'public', restarts service.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 13:09:57 +02:00
Christian Manivong ae27cd5469 initial commit 2026-05-29 09:22:10 +02:00