201 lines
12 KiB
Markdown
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.
|