Add runtime observation and systemd restart adapter

This commit is contained in:
2026-10-20 23:01:43 +03:00
parent 5418841b92
commit 6ec593c783
18 changed files with 6001 additions and 66 deletions
+168
View File
@@ -0,0 +1,168 @@
# 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](https://docs.kernel.org/netlink/specs/wireguard.html)
and the [Linux Netlink message specification](https://docs.kernel.org/6.1/userspace-api/netlink/intro.html).
## 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](https://www.freedesktop.org/software/systemd/man/latest/org.freedesktop.systemd1.html)
and [zbus 5.19.0 API](https://docs.rs/zbus/5.19.0/zbus/).
## 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. The Linux transport has not been validated
against a real interface; that check belongs to the isolated Alpine suite at
M8. The real zbus/systemd path and end-to-end restart acceptance require the
disposable systemd VM at M9. The TUI and management startup wiring are not
implemented.
## 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. Real-interface behavior remains
subject to the isolated Alpine integration milestone.