opnsense-set-interface-v6: script the LAN IPv6 the REST API cannot set (D-113 amendment)
...
Operator ruling 2026-07-14: take the PHP-model-over-SSH path, not a GUI click and
not the unproven VIP path. Rationale: the DC0 and DC1 edges get the same treatment
at Stage 3, so this must be a script, not a manual step paid three times.
WHY THIS EXISTS: measured on the live Office1 edge (OPNsense 26.1) -- there is NO
REST endpoint for a base interface's ipaddrv6. Interfaces > [LAN] is served by the
LEGACY page /interfaces.php?if=lan; only the Devices and Neighbors sub-trees were
ever migrated to MVC. D-113(a2)'s claim that "interfaces" are API-covered was
inferred from DHCP/firewall being API-native, never measured, and is false.
WHY IT IS NOT THE FORBIDDEN config.xml PUSH -- the distinction is the whole point:
it never AUTHORS a config. It ships opnsense-set-iface-v6.php, which loads the
edge's LIVE config through OPNsense's own Config singleton, mutates exactly two
leaf fields (ipaddrv6, subnetv6), and lets the vendor's own code serialize it --
byte-for-byte the code path interfaces.php runs on Save. Nothing is replaced
wholesale, so the API-managed Kea DHCP and the 8 firewall rules cannot be
clobbered. Same principle as opnsense-bootstrap-apikey.sh, which mints a key by
calling the vendor's own model rather than re-implementing its $6$ crypt format:
call the interface, do not re-implement the internals.
Safety properties, all pinned by the harness (40/40):
- DRY BY DEFAULT in both halves; --commit required to write.
- The interface COUNT is asserted unchanged before saving AND re-asserted on a
fresh forceReload() read-back. A silent structural drop on a live router fails
loud rather than being found later by a ping that stopped.
- Only ipaddrv6/subnetv6 may be assigned -- the harness counts the assignments.
- Values validated locally BEFORE they ever reach the router.
- Remote commands go through `sh -s`: root's shell on the edge is TCSH, where a
$(...) in a quoted remote command dies QUIETLY.
- Ground truth is the KERNEL (ifconfig), never the config file or a self-report.
NOT YET RUN against the edge -- that is the next, individually-gated step.
repo-lint 0 fail; gauntlet ALL GREEN (57 harnesses, +1).
Revert: git revert HEAD (removes both scripts + the harness; the edge is untouched)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013ZBxzAbmqwLW7jEmMG23Xw