Add try/confirm safe-apply with out-of-process revert #17

Merged
benvin merged 4 commits from benvin/safe-apply into main 2026-10-03 21:13:57 +10:00
Member

A firewall change that cuts off SSH leaves the host unreachable until someone reaches the console.

  • add tomswall try: snapshot to /var/lib/tomswall, apply, revert unless confirmed in time
  • arm a transient systemd timer (revert --id <try>) so the revert runs even if try is killed; a stale timer cannot revert a newer try
  • add confirm (fails if already reverted) and revert; a failed revert keeps the snapshot for retry
  • restore rule order and chain policies; an absent table is removed
  • apply, flush, purge, a second try and agent applies refuse while a try is pending
  • readCurrentState now errors instead of returning empty state

Refs #11

A firewall change that cuts off SSH leaves the host unreachable until someone reaches the console. - add `tomswall try`: snapshot to `/var/lib/tomswall`, apply, revert unless confirmed in time - arm a transient systemd timer (`revert --id <try>`) so the revert runs even if `try` is killed; a stale timer cannot revert a newer try - add `confirm` (fails if already reverted) and `revert`; a failed revert keeps the snapshot for retry - restore rule order and chain policies; an absent table is removed - `apply`, `flush`, `purge`, a second `try` and agent applies refuse while a `try` is pending - `readCurrentState` now errors instead of returning empty state Refs #11
unkin-agent added 1 commit 2026-10-03 20:46:06 +10:00
Add try/confirm safe-apply and agent auto-revert
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
cc12c4a43a
Author
Member
  • cmd/tomswall/try.go:50 — kill -9/OOM/crash of try mid-wait leaves the bad ruleset applied with nothing left to revert it; operator stays locked out (issue acceptance fails) → arm an out-of-process fallback before applying (e.g. systemd-run --on-active=<timeout> tomswall revert-pending using a snapshot persisted under /run/tomswall, cancelled on confirm).
  • cmd/tomswall/try.go:99 — confirm prints "Confirmed." as soon as SIGUSR1 is sent; if the timeout/abort already fired the signal is swallowed, revert runs, and the operator is told it was confirmed → make try ack (or confirm wait for try's exit and report its result); do not report success from a fire-and-forget signal.
  • cmd/tomswall/try.go:42 — no lock on the pidfile: a second try overwrites it, snapshots the first try's unconfirmed ruleset (so its revert restores the bad state), and the first's defer os.Remove deletes the second's pidfile → flock the pidfile, refuse if held.
  • internal/agent/agent.go:107 — errors.As(err, *url.Error) matches ctx cancellation (agent shutdown), TLS/cert errors and single transient failures; a good config is reverted and its generation is skipped → check ctx.Err(), treat only network/dial/timeout errors as unreachable, retry a few times before reverting.
  • internal/agent/agent.go:107 — issue requires "auto-revert + report status: reverted"; nothing is reported after the revert → report reverted status (generation) once the API is reachable again.
  • internal/agent/agent.go:31 — reverted is in-memory only; agent restart re-applies the bad generation → persist it (next to the cache).
  • internal/agent/agent.go:95 — agent apply and a pending try both mutate the table with no coordination; an agent cycle during the try window is clobbered by the try revert (or vice versa) → agent skips apply while the try lock is held.
  • internal/nftables/engine.go:207 — Restore re-runs ensureChains, which resets chain policies to the hard-coded drop/accept; policies are not part of the snapshot → capture and restore chain policies.
  • internal/nftables/engine.go:149 — readCurrentState now returns errors instead of an empty state; this changes plan/apply/status behaviour outside this PR's why → keep, but call out in the body, or split into its own PR.
  • nit: cmd/tomswall/try_test.go — only the select helper is tested; no test for confirm (stale pidfile, race with timeout), signal wiring, or the table-absent Snapshot/Restore path (engine.go:206) → add tests for confirm and for the nil-snapshot restore.
  • nit: cmd/tomswall/try.go:67 — if confirm and abort are both ready, select picks randomly → prefer abort/confirm deterministically.
- cmd/tomswall/try.go:50 — `kill -9`/OOM/crash of `try` mid-wait leaves the bad ruleset applied with nothing left to revert it; operator stays locked out (issue acceptance fails) → arm an out-of-process fallback before applying (e.g. `systemd-run --on-active=<timeout> tomswall revert-pending` using a snapshot persisted under /run/tomswall, cancelled on confirm). - cmd/tomswall/try.go:99 — `confirm` prints "Confirmed." as soon as SIGUSR1 is sent; if the timeout/abort already fired the signal is swallowed, revert runs, and the operator is told it was confirmed → make `try` ack (or confirm wait for try's exit and report its result); do not report success from a fire-and-forget signal. - cmd/tomswall/try.go:42 — no lock on the pidfile: a second `try` overwrites it, snapshots the first try's unconfirmed ruleset (so its revert restores the bad state), and the first's `defer os.Remove` deletes the second's pidfile → flock the pidfile, refuse if held. - internal/agent/agent.go:107 — `errors.As(err, *url.Error)` matches ctx cancellation (agent shutdown), TLS/cert errors and single transient failures; a good config is reverted and its generation is skipped → check `ctx.Err()`, treat only network/dial/timeout errors as unreachable, retry a few times before reverting. - internal/agent/agent.go:107 — issue requires "auto-revert + report `status: reverted`"; nothing is reported after the revert → report reverted status (generation) once the API is reachable again. - internal/agent/agent.go:31 — `reverted` is in-memory only; agent restart re-applies the bad generation → persist it (next to the cache). - internal/agent/agent.go:95 — agent apply and a pending `try` both mutate the table with no coordination; an agent cycle during the try window is clobbered by the try revert (or vice versa) → agent skips apply while the try lock is held. - internal/nftables/engine.go:207 — Restore re-runs `ensureChains`, which resets chain policies to the hard-coded drop/accept; policies are not part of the snapshot → capture and restore chain policies. - internal/nftables/engine.go:149 — `readCurrentState` now returns errors instead of an empty state; this changes `plan`/`apply`/`status` behaviour outside this PR's why → keep, but call out in the body, or split into its own PR. - nit: cmd/tomswall/try_test.go — only the select helper is tested; no test for `confirm` (stale pidfile, race with timeout), signal wiring, or the table-absent Snapshot/Restore path (engine.go:206) → add tests for confirm and for the nil-snapshot restore. - nit: cmd/tomswall/try.go:67 — if confirm and abort are both ready, `select` picks randomly → prefer abort/confirm deterministically.
unkin-agent added 2 commits 2026-10-03 20:52:44 +10:00
Persist try snapshot and arm a systemd revert timer
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
6ac03e1012
unkin-agent changed title from Add safe-apply: try/confirm and agent auto-revert to Add try/confirm safe-apply with out-of-process revert 2026-10-03 20:52:51 +10:00
Author
Member
  • cmd/tomswall/main.go:79 — tomswall apply never calls tryapply.Acquire, so it runs during a pending try and the later confirm/timeout revert silently overwrites it with the stale snapshot → take the lock and return ErrPending there as the agent does.
  • internal/tryapply/tryapply_test.go — Revert() with a pending snapshot is never exercised (only the nothing-pending path); restore ordering, Discard after restore, and revert-failure-keeps-snapshot are untested → add a test that injects an engine/restore seam and asserts snapshot removal and timer stop on success, retention on error.
  • internal/nftables/snapshot_test.go:73 — only the absent-table Restore is tested; the present-table path (decodeState + apply with policies) has no engine-level test → add one asserting the sent del/add rule messages and chain policies.
  • nit: internal/tryapply/tryapply.go:Revert — a revert that fails (e.g. decode error) leaves the snapshot but the one-shot timer is spent, so nothing retries and Acquire returns ErrPending forever → document tomswall revert as the recovery in the error text.
- cmd/tomswall/main.go:79 — `tomswall apply` never calls `tryapply.Acquire`, so it runs during a pending try and the later confirm/timeout revert silently overwrites it with the stale snapshot → take the lock and return `ErrPending` there as the agent does. - internal/tryapply/tryapply_test.go — `Revert()` with a pending snapshot is never exercised (only the nothing-pending path); restore ordering, `Discard` after restore, and revert-failure-keeps-snapshot are untested → add a test that injects an engine/restore seam and asserts snapshot removal and timer stop on success, retention on error. - internal/nftables/snapshot_test.go:73 — only the absent-table `Restore` is tested; the present-table path (`decodeState` + `apply` with policies) has no engine-level test → add one asserting the sent del/add rule messages and chain policies. - nit: internal/tryapply/tryapply.go:Revert — a revert that fails (e.g. decode error) leaves the snapshot but the one-shot timer is spent, so nothing retries and `Acquire` returns `ErrPending` forever → document `tomswall revert` as the recovery in the error text.
unkin-agent added 1 commit 2026-10-03 20:56:53 +10:00
Refuse mutating commands during a try and scope reverts to the try ID
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
0b92f1c2f3
apply, flush and purge take the try lock and fail with ErrPending, which now
names 'tomswall revert' as the recovery after a failed automatic revert. Each
try gets an ID passed to the timer's 'revert --id', so a stale timer cannot
revert a newer try. Adds tests for Revert success/failure/stale ID and for
restoring a present table at the engine level.
Author
Member

No findings.

No findings.
benvin merged commit 937556abeb into main 2026-10-03 21:13:57 +10:00
benvin deleted branch benvin/safe-apply 2026-10-03 21:13:57 +10:00
Sign in to join this conversation.