119 lines
7.1 KiB
Markdown
119 lines
7.1 KiB
Markdown
# Saved state persistence
|
|
|
|
The persistence adapter reads and installs the application's saved files at the
|
|
paths in `config::ResolvedSettings`. Settings remain beside the executable,
|
|
the server configuration stays at its configured invariant path, and internal
|
|
state and the opaque encrypted vault live in `data_directory`. No path is
|
|
resolved from the process working directory. Archive member names never become
|
|
filesystem paths.
|
|
|
|
## Internal state v1
|
|
|
|
`internal-state.toml` is optional. Its absence is valid only during initial
|
|
setup; once an installed state file exists, malformed or unsupported data is an
|
|
error and cannot be treated as an empty reservation set. Version 1 contains no
|
|
alias registry and does not duplicate server client metadata:
|
|
|
|
```toml
|
|
version = 1
|
|
reserved_addresses = ["10.0.0.8", "fd00::8"]
|
|
last_verified_at_utc = "2026-10-19T12:30:00Z"
|
|
```
|
|
|
|
`reserved_addresses` records addresses retained after saved deletion until a
|
|
later explicit runtime verification. It is canonically sorted and duplicate
|
|
free. `last_verified_at_utc` records when this saved generation was last verified
|
|
against the runtime. It is historical bookkeeping only: loading it always
|
|
produces `RuntimeObservation::Unknown` and never establishes that the current
|
|
interface matches saved files. The timestamp is stored in the same journaled
|
|
internal-state component as the saved set it describes. Successful Commit and
|
|
restore write a new generation with this timestamp cleared; failed operations do
|
|
not advance or clear it. M5 records verification only after a completed
|
|
pre-restart backup, successful runtime verification, and successful journaled
|
|
state write. If that write fails, the in-memory applied state remains unchanged.
|
|
This binding follows the application's single-administrator save boundary; it is
|
|
not a digest that detects manual edits made outside the application.
|
|
Unknown fields, duplicate addresses, invalid addresses/timestamps, and
|
|
unsupported versions are rejected.
|
|
|
|
Settings bytes are parsed and semantically validated, server bytes are parsed
|
|
with the byte-preserving `ConfigDocument`, and a present vault is parsed by
|
|
`VaultBundle::from_toml` without unlocking it. Saved reads are bounded and
|
|
return exact bytes; their `Debug` representations redact content. The server
|
|
parser reports its existing anomalies without rewriting or normalizing bytes.
|
|
The server configuration and a present vault must already have mode `0600` or
|
|
stricter when read. A broader mode fails closed with an actionable permission
|
|
error; the application does not silently chmod an existing secret-bearing file.
|
|
The operator must restrict its mode and retry. Newly installed replacements
|
|
are created as `0600`.
|
|
During restore, operational settings such as endpoint, DNS, systemd unit, and
|
|
timeout may come from the archive. The configured interface name remains fixed
|
|
for this installation, along with the resolved server configuration, client
|
|
source, and data-root paths (including their derived data/archive directories).
|
|
This preserves the single-interface mapping while allowing an archive to restore
|
|
other settings without redirecting writes.
|
|
|
|
## Destination and path safety
|
|
|
|
The adapter accepts only the resolved settings path, configured server path,
|
|
`data_directory/internal-state.toml`, and `data_directory/vault.toml`. These
|
|
paths are checked for overlap/aliasing before a write. Every existing path
|
|
component is checked without following symlinks; saved files must be regular
|
|
files. Destination directories are private (`0700`), and replacement and
|
|
journal files are created `0600` from their creation. Symlinks, non-regular
|
|
files, unsafe directory components, and path aliasing fail closed. No archive
|
|
metadata controls destination names, permissions, or ownership.
|
|
|
|
## Multi-file installation and recovery
|
|
|
|
Installation is not a cross-directory atomic operation. The caller must first
|
|
complete the mandatory operation backup and supply the adapter's
|
|
`BackupComplete` capability. The adapter then validates the complete proposed
|
|
saved set and checks that no earlier transaction needs recovery. It creates a
|
|
fixed-name private transaction directory under the trusted data directory and
|
|
writes the prior bytes (including explicit absence) and proposed bytes under
|
|
fixed component names. Each payload and the transaction directory are synced,
|
|
then the journal metadata and a `prepared` marker are atomically published and
|
|
the journal and parent data directory are synced. Only then may destination
|
|
files be changed. An interrupted journal without a valid prepared marker is
|
|
safe to clean because the adapter cannot have started replacements yet.
|
|
|
|
Replacement order is settings, server configuration, internal state, then
|
|
vault. A deletion of an optional state or vault is part of that order. Each
|
|
new file is written completely to a same-directory `0600` temporary file,
|
|
flushed and synced, atomically renamed, and followed by a parent-directory
|
|
sync. After all replacements, the adapter syncs all affected parents, records
|
|
the transaction as committed, syncs that marker and its parent, then removes
|
|
the transaction directory and syncs the data directory. Only a fully completed
|
|
sequence returns success.
|
|
|
|
On reopen, an uncommitted journal means rollback: prior bytes are reinstalled
|
|
in reverse replacement order, previously absent optional files are removed,
|
|
and all affected parent directories are synced. If rollback or cleanup fails,
|
|
the adapter returns `RecoveryRequired` and rejects every new mutation. Recovery
|
|
can be retried after correcting the filesystem problem. A committed journal
|
|
means all replacements finished; reopening performs cleanup only and never
|
|
rolls back the completed set. If writing/syncing the committed marker or
|
|
cleaning the journal fails during installation, the result is explicitly
|
|
`RecoveryRequired`, not success. The next open determines rollback versus
|
|
cleanup from the durable marker.
|
|
|
|
This journal repairs interrupted saved-file writes only. It never invokes a
|
|
WireGuard or systemd operation, does not provide concurrent-editor locking,
|
|
and is not automatic rollback after a failed runtime restart. The separate
|
|
pre-operation archive remains the recovery source if local journal data is
|
|
unusable; the adapter does not silently discard an invalid journal.
|
|
|
|
For an explicit restart, the application creates the pre-restart archive before
|
|
calling the service adapter. After a successful terminal service job, it checks
|
|
the complete observed peer set against the saved server document. A match is
|
|
then recorded by installing the updated `internal-state.toml` through this same
|
|
journal, with reservations removed only from the proposed state as appropriate.
|
|
The in-memory saved/applied state advances only after installation succeeds. If
|
|
that write or journal recovery fails after the VPN may have changed, the
|
|
application reports the persistence stage, retains in-memory reservations, and
|
|
requires manual recovery; it does not roll back or restart again. Default tests
|
|
exercise this orchestration with private fake adapters and synthetic files.
|
|
Actual interface and systemd behavior remain deferred to the isolated Alpine
|
|
and disposable VM acceptance environments described in `RUNTIME.md`.
|