Implement backups, commit and restoration

This commit is contained in:
2026-10-20 00:13:39 +03:00
parent c879a8101b
commit 5418841b92
17 changed files with 5049 additions and 38 deletions
+116
View File
@@ -0,0 +1,116 @@
# Backup archive format
Backups are uncompressed tar archives with a versioned TOML manifest. Version 1
uses only fixed logical member names and is parsed without extracting files.
The archive is an opaque saved-state copy; its tar checksum detects accidental
header damage but does not authenticate contents or make the archive tamper-proof.
## Members and limits
The first member is always `manifest.toml`. The only permitted payload names are
`settings.toml`, `server.conf`, `internal-state.toml`, and `vault.toml`, in that
order when present. Settings and server configuration are required. Internal
state and vault are optional components: a missing component is recorded as
`present = false` and `length = none`, which means restore removes that owned
component. Absence is not represented by an empty placeholder file.
| Member | Maximum bytes | Presence |
| --- | ---: | --- |
| `manifest.toml` | 64 KiB | Required, first |
| `settings.toml` | 1 MiB | Required |
| `server.conf` | 4 MiB | Required |
| `internal-state.toml` | 2 MiB | Optional |
| `vault.toml` | 16 MiB | Optional |
The archive is limited to 26 MiB and five entries. Only checksummed UStar regular
files are accepted. Absolute names, path separators, traversal, links, device or
special entries, sparse entries, GNU/PAX extension headers, nonzero path prefixes,
unexpected metadata fields, duplicate/missing/unexpected members, size mismatches,
truncation, nonzero padding, and nonzero trailing data are rejected. The reader
checks declared sizes before allocating payload buffers and never follows a
tar-supplied path. It does not restore files or validate their application
semantics; restoration must run settings, configuration, state, and vault
validators before mutation.
## Manifest v1
The manifest is UTF-8 TOML with unknown fields denied:
```toml
version = 1
created_at_utc = "2026-10-19T12:30:00Z"
reason = "pre-commit"
[historical_apply]
status = "unknown"
[[members]]
name = "settings.toml"
present = true
length = 420
[[members]]
name = "server.conf"
present = true
length = 640
[[members]]
name = "internal-state.toml"
present = false
[[members]]
name = "vault.toml"
present = true
length = 8192
```
`reason` is one of `manual`, `pre-commit`, `pre-restore`, or `pre-restart`.
Timestamps are canonical RFC 3339 UTC values with whole-second precision. The
historical apply record is either `status = "unknown"` or
`status = "verified"` with a `verified_at_utc` timestamp. It is historical
metadata only: a pre-operation reason or a verified marker does not establish
that these bytes match the currently running interface. Application-created
backups derive this record from the timestamp stored in the internal-state bytes
captured with the saved set. A successful Commit or restore clears that timestamp
for its new saved generation; a failed operation leaves it intact. Reopening may
show the saved generation's historical verification time, but runtime observation
always begins as unknown.
The inventory always has exactly four records in the specified order. Required
members have `present = true` and an exact byte `length`; optional absence has
`present = false` and omits `length` (TOML has no null value). Present optional members follow the required
members in fixed order. The manifest itself is excluded from its payload
inventory. Readers reject unsupported versions and any discrepancy between
inventory and tar entries.
## Publication and trust boundary
Creation receives exact saved bytes from a trusted caller. It does not discover
files, inspect the current working directory, recurse into the archive directory,
or read client source/output directories. The caller must supply installed bytes,
never a proposed draft or decrypted vault records. New archives are fully
serialized and synced in a private temporary file, then published without
replacement under a timestamped collision-safe name; the archive directory is
synced after publication. The archive file is mode `0600` from temporary-file
creation. A write or file-sync failure removes the unpublished temporary file;
a publication or directory-sync failure returns an error and blocks the protected
operation. A complete archive may already be visible if failure occurs after its
atomic publication, and a failed temporary cleanup may leave a hidden `0600`
temporary file. Neither condition is reported as backup success. Readers should
expose the list and preview without passwords, but must keep payload bytes out of
Debug output and logs.
The application restore preview validates both the archive structure and its
settings, server document, optional internal state, and opaque vault before it
returns an opaque confirmation value. That value owns the validated payload
bytes, so confirmation applies the exact content shown during preview even if
the selected archive path changes afterward. Confirmation first completes a
`pre-restore` archive and then uses the saved-file transaction journal for the
full replacement. Dropping the preview is cancellation and does not discard a
draft. Historical verification metadata is informational; opening or restoring
an archive always leaves current runtime state unknown.
This module only creates, lists, and validates archives. It does not write saved
state, apply WireGuard changes, or restore content. A backup is not encrypted:
it can contain the server's private configuration and the encrypted vault, so
filesystem access must remain restricted to root.
+105
View File
@@ -0,0 +1,105 @@
# 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.
+3 -4
View File
@@ -7,12 +7,11 @@ unknown fields, malformed labels, invalid Base64, and oversized bundles.
## Version 1 structure
For M4, the current encrypted bundle is intended to live at
The current encrypted bundle lives at
`ResolvedSettings::data_directory.join("vault.toml")`. This stable filename is
relative to the resolved data directory, whose default is `Backup/data` beside
the executable. M3 only prepares and serializes vault bytes in memory; it does
not create this file or install application state. M4 persistence will own that
write.
the executable. The application installs the opaque serialized bytes through
the saved-file transaction after completing its mandatory pre-Commit backup.
The top level contains: