11 KiB
Runtime observation and verification boundary
runtime defines public, read-only observation values, a bounded Linux
WireGuard Generic Netlink reader, and a pure comparison between an observed
interface and the authoritative saved server document. The reader accesses
only the configured interface and never changes the running interface.
Observation values
An observation records an optional UTC sample time and distinguishes an unknown
interface, an unavailable read, a missing interface, and a present interface.
A present interface carries its observed interface name and peers keyed by
PublicKey; duplicate keys are retained as an explicit ambiguity signal. The
comparison requires the currently configured interface name and rejects a
different observed interface, even when its peer set happens to match. The peer
set states whether it is unknown, unavailable, partial, or complete. Only a
complete set can prove that a saved peer is absent or that an extra runtime peer
exists.
Each peer has AllowedIPs and statistics with their own availability. A valid
zero byte counter is Available(0). A peer with no recorded handshake is
Available(None). Unavailable statistics do not become zero or a disconnected
state. Counters are current values from the server interface's perspective and
may reset after restart; they are not history and do not establish online or
offline status.
Saved technical projection
SavedPeerProjection::from_document includes every [Peer] section from the
byte-preserving ConfigDocument, including valid routes unsupported by the
narrow domain::Client address model and legacy IPv4-only peers. Routes are
normalized and compared as complete sets, independent of ordering and duplicate
entries. Aliases and managed creation dates are not technical runtime fields;
metadata anomalies alone do not prevent comparison. Missing or ambiguous keys,
malformed routes, structural document issues, and duplicate identities remain
unverifiable and cannot be omitted to obtain a matching reduced set.
compare_peers(saved, expected_interface, runtime) checks interface presence
and identity, the complete public-key set, and each peer's complete AllowedIPs
set. It reports a wrong interface, missing, extra, route-mismatched, duplicate,
partial, and unverifiable cases. An empty saved peer set can match only a
present expected interface with an explicitly complete empty runtime peer set.
Handshake values, counters (including resets), endpoint changes, ordering, and
sample time do not affect technical equivalence. The comparison returns only a
technical result; it does not create online status, applied state, or history.
This result is separate from M4's domain::Snapshot and applied marker. A
successful technical comparison is necessary evidence for applying state, but
it is not itself a durable M4 transition. Application restart orchestration
joins saved aliases and representable addresses by public key only after full
technical equivalence succeeds. If any authoritative peer cannot be safely
represented for that narrower durable view, verification remains unverifiable
and reservations are retained instead of supplying a fabricated subset.
WireGuard Generic Netlink decoder
runtime::netlink::GetDeviceDecoder is a pure decoder for one WireGuard
GET_DEVICE dump. It consumes caller supplied datagrams and expected request
sequence, local netlink port ID, family ID, and interface name. It has no socket
or host access. The transport rejects truncated datagrams, enforces its
monotonic receive deadline, and wipes its receive buffer when its lifetime ends.
The decoder checks every netlink and attribute length/alignment, message type,
sequence and port ID, Generic Netlink command/version, configured interface,
nested peer and AllowedIPs records, duplicate fields and aggregate limits. It
returns data only after an explicit NLMSG_DONE with an embedded zero error
code. It rejects NLMSG_ERROR, interrupted/overrun dumps, malformed or
incomplete data, and limits exceeded. Peer fragments are coalesced only when a
repeated public key is adjacent across multipart messages and the fragment
contains only that key and AllowedIPs; duplicate routes remain invalid. A
complete empty peer list is valid.
Server private keys and peer preshared keys are length-checked through borrowed input slices and are neither copied into decoder-owned structures nor returned. The result contains only public keys, AllowedIPs, last-handshake timestamps and current received/transmitted counters. Errors carry stable categories without raw message data. The decoder uses configurable aggregate limits with bounded defaults; future transport code must set request-specific limits explicitly.
Protocol details follow the WireGuard Generic Netlink specification and the Linux Netlink message specification.
Systemd restart job tracking boundary
service::RestartJobTracker is the pure event state machine for one resolved
primary systemd unit ID. The bounded zbus 5.19.0 adapter is internal and
crate-private; public runtime mutation is exposed only through
Application::restart, after an explicit restart choice. The adapter installs
JobRemoved and systemd name-owner observation, calls Manager Subscribe, and
resolves the configured target with read-only Manager GetUnit followed by the
unit's Id property. It then submits RestartUnit using the original
configured target and tracks the returned job object path together with the
resolved primary ID, which is the unit name reported by JobRemoved. If
GetUnit cannot resolve a loaded unit, or reading its ID fails, the operation
stops with a redacted resolution failure; it does not load a unit or guess its
canonical name. The signal stream is installed before resolution and restart,
so a fast completion queued before the method reply remains available once the
reply supplies the path. The pure tracker also supports a fixed bound of early
events for transports that deliver events before the method reply; overflow
fails closed.
Only systemd's done job result is successful. Failed, canceled, timed-out,
skipped, dependency, or unknown job results remain failures. Bus disconnection,
manager owner change, subscription failure, and permission/request failures are
separate redacted outcomes. RestartDeadline carries the operation's monotonic
start and timeout so connection, match registration, Subscribe, unit
resolution, RestartUnit, job wait, and later runtime verification can share one
budget. The adapter drives zbus async futures with a standard-library thread
waker and parks only for the remaining budget. zbus's async-io backend
continues socket I/O on its own executor; its connection is short lived and is
dropped after the adapter returns. Deadline expiry stops the application's
wait; a submitted job may still complete. The adapter never cancels or retries
the job, checks unit active state, or verifies the WireGuard interface.
Ordinary adapter tests inject a fake event transport and never connect to the
system bus. Real D-Bus behavior remains for the disposable systemd VM
acceptance at M9. Protocol
references: systemd Manager D-Bus API
and zbus 5.19.0 API.
Application restart orchestration
Application::preview_restart binds a read-only preview to the current saved
generation and resolves the configured interface, systemd unit, and timeout
from that generation. Application::restart accepts an explicit choice. Cancel
returns before backup, service, runtime, or persistence adapters run. A
confirmed operation completes a pre-restart backup, submits a systemd restart,
waits for a successful terminal job, reads and compares the complete runtime
peer set within the shared monotonic deadline, then writes verification state
through the saved-file journal. Only after that write succeeds does in-memory
applied state advance and deleted-client reservations release.
Failure reports identify the stage, whether a restart was submitted, whether it may still complete, the backup path when available, and whether manual recovery is required. A timeout does not cancel a submitted systemd job. A runtime mismatch or verification-state write failure does not roll back the VPN or release reservations. Commit and restore remain saved-file operations and never invoke the runtime or service adapters.
Application unit tests use private fake adapters and an injected private monotonic clock. Public integration tests exercise cancellation, preview, restore, and saved-setting selection without confirming a host restart. These are synthetic fixture results. M7 implemented the TUI and management startup wiring, with synthetic workflow and PTY coverage. M8 also validated the Linux Rustix transport against a real WireGuard interface in the isolated Alpine fixture, including IPv4 peer, route, handshake, and counter observations (see M8 evidence). That fixture does not exercise the real zbus/systemd path: systemd/D-Bus behavior and end-to-end restart acceptance still require the disposable VM at M9. Real IPv6 capability and traffic remain unverified.
Bounded Linux transport
runtime::transport::observe_interface resolves the WireGuard Generic Netlink
family through nlctrl, then sends only WG_CMD_GET_DEVICE for the supplied
interface. It uses rustix 1.1.5's safe nonblocking socket, poll, and datagram
APIs; no SET_DEVICE, subprocess, unsafe block, or host socket is used by the
default tests. Family lookup and the complete multipart dump share one
monotonic deadline. The datagram size is capped, MSG_TRUNC is detected using
the returned full datagram length, the sender socket address must be kernel
port 0, and decoder aggregate limits apply across the dump.
Linux's Generic Netlink reply header nlmsg_pid carries the requester port ID
(the destination); the actual kernel sender port ID is 0 in the datagram
address. The transport validates both identities. A complete read is returned
only after the decoder consumes successful NLMSG_DONE; kernel errno values
map to distinct no-device and permission outcomes. Malformed data, transport
errors, timeouts, and limit excess remain separate, redacted categories.
Receive buffers are zeroized on every return path because device replies may
include private and preshared key attributes, although the decoder borrows and
does not retain those attributes. Runtime observation does not itself perform
saved-peer comparison or mark state applied. M8 exercised this Rustix reader
against a real Alpine WireGuard interface for IPv4 observations; real IPv6
capability and traffic remain unverified. See M8 evidence
for the accepted scope.