Skip to content

Manage, not view: persistence (Save), rule edit and move - #16

Merged
edimarlnx merged 2 commits into
mainfrom
manage-persistence
Sep 1, 2026
Merged

edimarlnx merged 2 commits into
mainfrom
manage-persistence

Conversation

@edimarlnx

Copy link
Copy Markdown
Contributor

The nftables backend built rules, NAT and aliases but everything was runtime-only and immutable: a reboot reverted the ruleset to whatever the router profile rendered, and the only way to change a rule was delete-and-re-add. This PR closes the three gaps.

Save (W) — persistence

  • Captures the tool's own table with nft list table inet tui and installs it with install -m 644 /dev/stdin <path>. The listing rides on stdin, never on a command line, and nft's own text is a valid nft script, so nft -f loads the file straight back on boot.
  • Path: /etc/omarchy/router/tui-firewall.nft on an Omarchy router (the profile includes it on boot — the profile hook lands separately), /etc/nftables.d/tui-firewall.nft on any other host, TUI_FIREWALL_SAVE_PATH override for tests and labs.
  • The confirm dialog shows the install command plus a unified diff against the file as it stands (new UnifiedDiff helper); a file that already matches is reported without opening a dialog.
  • install(1) gets a lazily-built runner of its own inside internal/nftables, dispatched per command, so the exec boundary is unchanged (check-exec.sh clean).

Edit in place (E)

  • The add-rule form opens pre-filled from the rule's modelled Match (new SpecForRule) and submits as nft replace rule … handle H, reusing the log toggle's rebuild machinery — same handle, same position.
  • Same refusals as BuildToggleLog, made shared in checkRebuildable: a rule with an unmodeled match, a NAT rule, a non-accept/drop/reject verdict, plus edit-specific refusals for source-port matches, log levels / nflog groups, empty-verdict rules and reject-with answers the form cannot round-trip. Refused before the form opens, with the reason.

Move (K/J)

  • nftables has no move, so a move is a copy at the new position plus a delete of the old handle — and the two always apply as one nft -f transaction: staged into the atomic batch when staging is on, wrapped by the new staging.AtomicCommand when it is off. The confirm shows the whole transaction script.
  • First/last positions are refused; the rebuilt copy keeps the rule's own log statement, prefix included.

Deliberately left out

Per-rule enable/disable needs a persisted-spec format (v2); DESIGN.md records the approach instead of shipping a fake disable that drops rules.

Evidence

  • go build, go vet, gofmt, make check-exec: clean.
  • go test ./...: all green. New coverage: exact-argv builder tests, injection guards on every edited operand, refusal paths (unmodeled/NAT/sport/log-args/first/last/position-on-edit), save path guards and env override, diff rendering, demo parity (fake applies edit, atomic move and pretends the install; --demo never touches disk), and UI tests for the W/E/K/J flows including staged-move and non-nftables refusals.
  • README updated in the same PR (keys table, a "Manage, not view" section, can/cannot lists).

🤖 Generated with Claude Code

https://claude.ai/code/session_01MCypLQF5AD8JD831Ea9S9q

edimarlnx and others added 2 commits September 1, 2026 13:36
The nftables backend could create rules, NAT and aliases but everything it
did was runtime-only and immutable: a reboot reverted the ruleset, and the
only way to change a rule was delete-and-re-add. This closes those gaps.

Save (W): the tool's own table is captured with `nft list table inet tui`
and installed with `install -m 644 /dev/stdin <path>` — the listing rides
on stdin, never on a command line. The path is
/etc/omarchy/router/tui-firewall.nft on an Omarchy router (the profile
includes it on boot), /etc/nftables.d/tui-firewall.nft elsewhere, and
TUI_FIREWALL_SAVE_PATH overrides it for tests. The confirm dialog shows a
unified diff against the file as it stands. install(1) gets a runner of
its own inside the backend, so the exec boundary stays where it was.

Edit (E): the add-rule form opens pre-filled from the rule's modelled
match and submits as `nft replace rule … handle H`, the same
replace-by-handle rebuild the log toggle uses, with the same refusals — a
rule shown only as text, a NAT rule, a source-port match or a log level
the form cannot round-trip is refused before the form opens.

Move (K/J): a copy written at the new position plus a delete of the old
handle, always one `nft -f` transaction — staged into the atomic batch
when staging is on, wrapped by the new staging.AtomicCommand when it is
off — so the rule is never duplicated or missing in between.

Per-rule enable/disable is deliberately left out; DESIGN.md records the
persisted-spec format it waits for. The demo applies every new command
in memory, so --demo behaves exactly like the real backend, and the new
builders carry injection-guard and refusal tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MCypLQF5AD8JD831Ea9S9q
@edimarlnx
edimarlnx merged commit 1afcf1e into main Sep 1, 2026
8 checks passed
@edimarlnx
edimarlnx deleted the manage-persistence branch September 1, 2026 20:28
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.

1 participant