Files

201 lines
12 KiB
Markdown

# Client configuration rendering
`delivery` contains pure rendering functions. They accept the committed
byte-preserving `ConfigDocument`, validated saved settings, and either the new
client's private key or a freshly password-retrieved `RetrievedClientRecord`.
Each renderer derives the server public key inside its boundary from exactly one
valid `[Interface] PrivateKey` in that saved document. Missing or duplicate
interface sections, missing or duplicate private-key directives, malformed key
text, and structurally malformed private-key directives fail closed. Callers
cannot supply or retain an unrelated server identity. The derivation keeps the
source key borrowed and the parsed secret local to the call; neither is copied
into an error or retained object. The renderer does not read paths, inspect the
host, access the running interface, save a file, or authorize a QR request.
The renderer locates the client's peer by public key and requires one unique
peer with one valid public key field and supported authoritative address routes.
Addresses come from that saved peer's `AllowedIPs` host entries. Each assigned
address is rendered as a host prefix (`/32` for IPv4 and `/128` for IPv6); these
client `[Interface] Address` values remain distinct from server-side
`[Peer] AllowedIPs` routes. A new client requires one IPv4 and one IPv6 saved
address, uses `0.0.0.0/0, ::/0`, and gets endpoint and DNS from the saved
application settings. No MTU, keepalive, or preshared-key defaults are added.
For imported reissue, the record's client addresses and optional saved server
public key are checked against current authoritative saved values. A mismatch,
stale address, missing or ambiguous peer, unsupported peer shape, or malformed
retained field fails closed. The renderer never truncates routes. It preserves
validated retained DNS, MTU, endpoint, AllowedIPs, keepalive, and preshared key
when present; application DNS and endpoint settings supply values absent from
the imported record. Saved peer addresses and derived server identity always
remain authoritative. DNS and endpoint values are parsed and emitted as
canonical single-line text; unknown directives and hooks are never copied.
The precedence is therefore:
1. The saved interface private key determines the server public identity, and
saved server peer identity and host addresses are authoritative for both
rendering paths. Imported retained server identity, when present, must match
this derived identity.
2. For new clients, saved application endpoint and DNS settings fill the
template; full-tunnel routes are the fixed new-client template.
3. For imported clients, authenticated retained optional values take precedence
over current endpoint and DNS defaults when present; retained server identity
and addresses must still match saved state. Retained AllowedIPs are preserved
after validation.
`ClientConfigPayload` owns rendered plaintext in a zeroizing byte buffer. Its
`Debug` output reports only length and redaction; it has no generic serialization
or display implementation. `as_bytes()` is the explicit disclosure boundary
that later delivery code must keep operation-local. The payload is capped at
64 KiB. Temporary Base64 encodings and application-owned copies created after
`as_bytes()` cannot be erased by this owner; delivery code must minimize and
bound those copies. No payload, password, or key is returned in errors.
Fresh retrieval is required for imported records: `VaultBundle` checks the
authenticated reissue-blocked state before constructing a
`RetrievedClientRecord`. This renderer offers no password-free retrieval API
and does not create first-issue eligibility. Retained fields are readable
through reference accessors, while their authenticated values cannot be changed
by library callers after retrieval. The imported renderer requires imported
provenance and a present matching server public key; a key-only new-template
record has no such binding and cannot use the imported path. The application grants initial
output only after Commit in the same creation session and supplies the payload
only inside its file-publication closure.
Later reissue is exposed by `Application::reissue_client_file`. Every call
reads installed saved bytes, freshly decrypts the installed vault identity with
the supplied per-action password, checks the authenticated blocked status, and
renders against the saved server document and settings. Draft peers and
retained original client files are never consulted. The short-lived decrypted
record is dropped after the file and optional QR operation; no unlocked vault
or identity is cached in the application.
Authenticated key-only records created by this application are classified as
new-template records only when every retained connection field is absent or
empty. That validated provenance path renders the current saved peer addresses
with the saved endpoint/DNS template. An empty address list combined with any
retained field is treated as a corrupt partial record. Imported records must
carry valid addresses and a server identity; those values are checked against
the saved peer and derived server identity, and retained optional parameters
are validated before rendering. Missing or malformed required retained values
fail closed instead of falling through to the new-client template.
First-issue file and file-plus-QR success results, including definite
publication with uncertain post-publication bookkeeping, carry an activation
warning. The warning is suppressed only when the current application session
has verified the exact installed saved bytes and matching saved/applied peer
snapshot. Unknown runtime state and historical verification timestamps do not
suppress it.
Reissue failures expose a fixed stage (`SavedState`, `Authorization`,
`Rendering`, or `FilePublication`) and secret-free category. The mandatory file
is published before optional QR encoding; QR failure is returned separately
after file success. Reissue uses the same generation-bound activation warning.
Delivery performs no runtime read, service operation, restart, or applied-state
mutation.
## Plaintext file publication
Initial delivery asks the caller for an explicit destination path; aliases are
never used to construct a path. Destination preparation performs no writes.
It rejects non-absolute paths, parent traversal, symlinks in existing path
components, symlink or special-file targets, the settings/server/internal-state/
vault files, the data and archive directories, and every existing entry in the
configured original-client directory. An existing regular file can be selected,
but preparation records its filesystem identity and requires a separate
confirmation tied to that prepared destination. The configured source directory
is enumerated with the same entry bound used by import; enumeration failures or
an unsafe source directory stop output preparation.
Publication requires the chosen output parent to be owned by the effective
user or root. The output parent cannot be group- or world-writable; existing
ancestors cannot be group- or world-writable unless the sticky bit protects
entries (for example `/tmp`). These checks are repeated immediately before
opening the parent, and the opened directory is checked as well. This keeps
other users from substituting the temporary entry or destination inside the
output parent. A bounded payload is written to a same-directory temporary
file created with mode `0600` and a 128-bit operating-system-random name, then
all bytes are flushed and synced before publication. A new destination uses
no-replace publication, so a file created after preparation is not silently
overwritten. A confirmed replacement uses an atomic exchange: the displaced
entry is checked against the confirmed inode before it is removed. If a
different entry was substituted at publication, the adapter attempts a second
exchange and checks both resulting identities. It never unlinks by name during
rollback: a same-privilege concurrent substitution could otherwise cause an
unconfirmed entry to be destroyed. Even after an identity-verified rollback,
the staged configuration remains under its mode-`0600` temporary name because
unlink-by-name cannot be made conditional on inode identity. A completed
rollback is reported as `DestinationChanged` only after the parent directory
sync succeeds. If the second exchange, identity checks, or sync fail,
publication is reported as uncertain and all entries still reachable by their
names are retained.
Missing parent directories are created with mode `0700`; existing directories
are not chmodded. The destination parent is synced after publication. On a
pre-publication error, the temporary file is removed and first-issue eligibility
remains retryable. If removal of a pre-publication temporary file fails, the
operation reports that a private temporary configuration may remain and
requires manual cleanup. First-issue retry requires an explicit cleanup
acknowledgement; inspect the selected output parent for
`.wg-admin-client-*.tmp` files and remove the retained file after verifying its
identity. This result does not mean the selected destination was published.
If parent sync fails after publication, the output may
already exist: it is reported as uncertain, retained, and a retry requires a
fresh selection and explicit overwrite confirmation. The application never
removes a published file to pretend an uncertain save did not happen.
The Linux exchange operation is atomic for the two names but cannot condition
the exchange on previously observed inode identities. The parent-directory
ownership and permission checks plus the unpredictable temporary name prevent
other users from racing those names. A process with the same effective
privilege can still change names after identity checks; concurrent edits by the
administrator are outside this adapter's single-administrator model. Callers
must treat `PublicationUncertain` as requiring inspection before retrying. A
retained temporary file is private to the current user and may need explicit
cleanup after the output state is understood.
Successful file publication consumes the session's first-issue eligibility. If
the file is published but the subsequent saved-state read needed to record that
transition fails, the result explicitly reports the file as published with
bookkeeping uncertain and drops the cached first-issue key; a later issuance
requires password authorization.
The selected path and confirmation are operation-local and contain no
configuration bytes or key material. These checks are filesystem adapter tests
using synthetic payloads and temporary directories; they do not represent a
privileged production-path test.
The renderer is verified with synthetic keys and configurations, including
round-trip parsing through `vault::import::parse_client_config`. The file
adapter is verified with synthetic payloads and temporary paths. This is not
evidence of a phone import, live tunnel, production filesystem, or QR scan.
## Optional QR output
An authorized first-file operation may request a QR representation in the same
operation. The app first publishes the mandatory configuration file, then gives
the same borrowed `ClientConfigPayload` to the QR encoder. There is no QR-only
entry point and no reconstruction from cached key material. File publication
failure skips QR encoding and preserves the existing retry behavior. If QR
encoding fails after the file was published, the result reports the file as
delivered and the QR error separately; first-issue eligibility is consumed.
The QR module and encoder are crate-private. External callers can request QR
output only through the application file-and-QR operation; the returned matrix
and fixed-category error types remain public so callers can display the result.
The encoder uses `qrcode` 0.14.1 with default features disabled (no image, SVG,
or external utility). The application owns a row-major matrix of 0/1 modules with
a four-module quiet zone. Its side is at most 185 modules (version 40 plus the
quiet zone). Matrix access is explicit; its `Debug` output redacts module data,
and the owner zeroizes its buffer on drop. The qrcode encoder necessarily keeps
internal payload/codeword and matrix copies that this application cannot
zeroize; the app drops its local encoder result immediately after copying the
modules into the bounded owner. QR errors contain only a fixed category.
Tests establish that the encoder receives bytes equal to the just-published file,
that encoding succeeds for a synthetic payload, and that capacity failure does
not undo file delivery. They do not decode the matrix, establish phone scanning
compatibility, or test terminal rendering; those remain future UI/integration
checks.