Files
wireguard-admin/docs/DELIVERY.md
T

12 KiB

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.