Add runtime observation and systemd restart adapter
This commit is contained in:
+168
@@ -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.
|
||||
Reference in New Issue
Block a user