174 lines
11 KiB
Markdown
174 lines
11 KiB
Markdown
# 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. 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](evidence/milestone-8.md)). 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](evidence/milestone-8.md)
|
|
for the accepted scope.
|