Fix systemd restart support
This commit is contained in:
Generated
+1
@@ -3840,6 +3840,7 @@ version = "1.28.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7cc1186384beb7dd8eedea376413fd654937285ea6c9cfbb928dc3043ea4b606"
|
checksum = "7cc1186384beb7dd8eedea376413fd654937285ea6c9cfbb928dc3043ea4b606"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"getrandom 0.4.3",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"serde_core",
|
"serde_core",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
|
|||||||
+1
-1
@@ -30,7 +30,7 @@ time = { version = "=0.3.47", default-features = false, features = ["formatting"
|
|||||||
toml = "=1.1.8"
|
toml = "=1.1.8"
|
||||||
x25519-dalek = { version = "=2.0.1", default-features = false, features = ["static_secrets", "zeroize"] }
|
x25519-dalek = { version = "=2.0.1", default-features = false, features = ["static_secrets", "zeroize"] }
|
||||||
zeroize = "=1.9.0"
|
zeroize = "=1.9.0"
|
||||||
zbus = { version = "=5.19.0", default-features = false, features = ["async-io"] }
|
zbus = { version = "=5.19.0", default-features = false, features = ["async-io", "p2p"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
testcontainers = { version = "=0.28.0", default-features = false, features = ["blocking"] }
|
testcontainers = { version = "=0.28.0", default-features = false, features = ["blocking"] }
|
||||||
|
|||||||
@@ -125,6 +125,7 @@ Each row is a delivery milestone, not a completion claim. Expand its record befo
|
|||||||
| 13 | Compact terminal QR disclosure | 12 | accepted |
|
| 13 | Compact terminal QR disclosure | 12 | accepted |
|
||||||
| 14 | Restart verification for valid legacy peers | 13 | accepted |
|
| 14 | Restart verification for valid legacy peers | 13 | accepted |
|
||||||
| 15 | Diagnosable systemd restart and unloaded-unit resolution | 14 | accepted |
|
| 15 | Diagnosable systemd restart and unloaded-unit resolution | 14 | accepted |
|
||||||
|
| 16 | Root systemd private peer restart transport | 15 | blocked |
|
||||||
|
|
||||||
### Milestone 1 — Rust foundation, settings, and domain states
|
### Milestone 1 — Rust foundation, settings, and domain states
|
||||||
|
|
||||||
@@ -3673,8 +3674,37 @@ Checks: focused service/application/TUI tests; `cargo fmt --all -- --check`; `ca
|
|||||||
|
|
||||||
Accepted for the synthetic adapter and application scope after R1 (owner-change uncertainty) and R2 (unit-resolution test gap) were repaired and independently re-reviewed. Official systemd D-Bus documentation confirms `GetUnit` fails for an unloaded unit and `LoadUnit` loads it from disk if possible; the prior call was a confirmed API mismatch. The cause of the reported host failure and actual systemd acceptance remain unproven without a disposable-VM run. [M15 evidence](docs/evidence/milestone-15.md) records the reviewed source, tests and release artifact.
|
Accepted for the synthetic adapter and application scope after R1 (owner-change uncertainty) and R2 (unit-resolution test gap) were repaired and independently re-reviewed. Official systemd D-Bus documentation confirms `GetUnit` fails for an unloaded unit and `LoadUnit` loads it from disk if possible; the prior call was a confirmed API mismatch. The cause of the reported host failure and actual systemd acceptance remain unproven without a disposable-VM run. [M15 evidence](docs/evidence/milestone-15.md) records the reviewed source, tests and release artifact.
|
||||||
|
|
||||||
|
### Milestone 16 — Root systemd private peer restart transport
|
||||||
|
|
||||||
|
Status: blocked (2026-10-27T22:42:48+02:00) on end-to-end private-peer and disposable-VM systemd checks. M15 is accepted for synthetic behavior; M9 disposable-VM systemd acceptance remains retired and unaccepted.
|
||||||
|
|
||||||
|
#### Outcome and scope
|
||||||
|
|
||||||
|
Support explicit restart on a root-managed systemd host without a system bus by connecting directly to systemd's root-only private Unix socket using pinned zbus. Retain the system-bus path when available. Audit destination, signal match, owner monitoring and disconnect behavior separately for each transport; choose transport before `RestartUnit` and never retry after possible submission. Keep backup, confirmation, generation, full runtime verification, durable applied state and Commit/restore isolation unchanged. No production or host service access.
|
||||||
|
|
||||||
|
#### Tasks and acceptance
|
||||||
|
|
||||||
|
| Task | Owner | State | Evidence required |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| M16.1 | explorer | complete | Pinned zbus 5.19.0 source and official systemd private-socket protocol findings |
|
||||||
|
| M16.2 | implementer | complete for implementation scope | Transport implementation, zbus feature/docs and 16 focused synthetic tests |
|
||||||
|
| M16.3 | independent reviewer; repairer | blocked | R1 private permission category closed on re-review; R2 private-path behavioral evidence remains unavailable |
|
||||||
|
| M16.4 | coordinator | complete for available checks | Integrated source, pinned Rust gates and [M16 evidence](docs/evidence/milestone-16.md); VM unavailable |
|
||||||
|
|
||||||
|
- [ ] M16-A1: root-only private peer connection and system-bus path work against real systemd with correct method destination and job signal tracking. Source audit and synthetic selection checks pass; real transport checks remain unavailable.
|
||||||
|
- [x] M16-A2: source and synthetic checks preserve subscribe-before-request, canonical alias ID, bounded deadline, disconnect/uncertainty behavior, and no retry after possible submission.
|
||||||
|
- [ ] M16-A3: synthetic selection/failure and existing job-tracker tests pass, but the private `SystemdBus` branch was not exercised end to end because local peer credential access returned sandbox `EPERM`.
|
||||||
|
- [ ] M16-A4: R1 was closed, but R2 remains open; pinned Rust gates pass. Disposable-VM private and system-bus acceptance is unavailable.
|
||||||
|
|
||||||
|
Available checks: focused service/application tests, `cargo fmt --all -- --check`, `cargo clippy --locked --all-targets -- -D warnings`, `cargo test --locked`, `cargo build --locked --release`, `git diff --check` all passed on repaired inputs. Synthetic checks used no production socket or keys. [M16 evidence](docs/evidence/milestone-16.md) records exact results and identities. Disposable-VM checks were not run.
|
||||||
|
|
||||||
|
#### Outcome and handoff
|
||||||
|
|
||||||
|
Official systemd source confirms root `systemctl` connects to `/run/systemd/private` directly when PID 1 is systemd; official `systemd(1)` docs caution this socket is private to systemd. Pinned zbus 5.19.0 exposes peer mode behind its approved crate's `p2p` feature. The implementation uses destination-free peer calls and local signal matching without bus owner monitoring; the bus path remains available. M16-R1 (P2, private permission category) was repaired and independently closed. M16-R2 (P2, private-branch behavioral evidence) remains open: local zbus peer credential access returned sandbox `EPERM`, and the prior disposable Debian VM has no installed/verified guest. No production or host socket was touched. The next action is a disposable-VM run with synthetic configuration covering both transports; until then M16 cannot be accepted.
|
||||||
|
|
||||||
## Current progress and resume point
|
## Current progress and resume point
|
||||||
|
|
||||||
|
- Milestone 16 is blocked after implementation, independent review/repair and passing pinned Rust gates: synthetic tests cover selection/failures, but private-peer behavior and both real systemd transports lack disposable-VM evidence. [M16 evidence](docs/evidence/milestone-16.md) records the source and artifact. No production result is claimed.
|
||||||
- Milestone 15 is accepted (2026-10-27) for synthetic service-adapter and application behavior: typed failure/phase/unit reporting, documented `LoadUnit` resolution, conservative uncertainty, independent R1/R2 closure and pinned Rust gates. [M15 evidence](docs/evidence/milestone-15.md) records source/artifact identity. Disposable-VM systemd acceptance remains incomplete and unclaimed.
|
- Milestone 15 is accepted (2026-10-27) for synthetic service-adapter and application behavior: typed failure/phase/unit reporting, documented `LoadUnit` resolution, conservative uncertainty, independent R1/R2 closure and pinned Rust gates. [M15 evidence](docs/evidence/milestone-15.md) records source/artifact identity. Disposable-VM systemd acceptance remains incomplete and unclaimed.
|
||||||
- Milestone 14 is accepted (2026-10-27) for fixture restart orchestration: complete saved-peer projection, legacy/IPv4-only/broader-route eligibility, safe blockers and durable verified history passed independent review and pinned Rust gates. [M14 evidence](docs/evidence/milestone-14.md) records source/artifact identity. No disposable-VM systemd acceptance or production result is claimed.
|
- Milestone 14 is accepted (2026-10-27) for fixture restart orchestration: complete saved-peer projection, legacy/IPv4-only/broader-route eligibility, safe blockers and durable verified history passed independent review and pinned Rust gates. [M14 evidence](docs/evidence/milestone-14.md) records source/artifact identity. No disposable-VM systemd acceptance or production result is claimed.
|
||||||
- Milestone 13 is accepted (2026-10-27): compact, explicitly colored QR disclosure, complete quiet zone and dimensioned fallback passed synthetic rendering tests, independent R5 closure and pinned Rust gates. [M13 evidence](docs/evidence/milestone-13.md) records the reviewed source and release artifact. No manual scan, Alpine rerun, VM or production validation is claimed.
|
- Milestone 13 is accepted (2026-10-27): compact, explicitly colored QR disclosure, complete quiet zone and dimensioned fallback passed synthetic rendering tests, independent R5 closure and pinned Rust gates. [M13 evidence](docs/evidence/milestone-13.md) records the reviewed source and release artifact. No manual scan, Alpine rerun, VM or production validation is claimed.
|
||||||
@@ -3696,7 +3726,7 @@ Accepted for the synthetic adapter and application scope after R1 (owner-change
|
|||||||
|
|
||||||
- Milestone 8 delivered reviewed opt-in Alpine Testcontainers coverage for real IPv4 WireGuard peers, application lifecycle, saved/runtime separation, restoration, import/reissue, backup failures and the real Rustix reader. Host gates and three fresh real-container runs passed on one stable source fingerprint; M8-A1–A10 accepted. Real IPv6 behavior is unverified by user-authorized scope.
|
- Milestone 8 delivered reviewed opt-in Alpine Testcontainers coverage for real IPv4 WireGuard peers, application lifecycle, saved/runtime separation, restoration, import/reissue, backup failures and the real Rustix reader. Host gates and three fresh real-container runs passed on one stable source fingerprint; M8-A1–A10 accepted. Real IPv6 behavior is unverified by user-authorized scope.
|
||||||
- Milestone 9 is retired and unaccepted: the reviewed disposable VM preparation and guarded first start did not establish Debian/systemd acceptance. The exact VM was powered off after the user stopped work. M9.2–M9.3 were not started; M9-A1–A9 are unchecked.
|
- Milestone 9 is retired and unaccepted: the reviewed disposable VM preparation and guarded first start did not establish Debian/systemd acceptance. The exact VM was powered off after the user stopped work. M9.2–M9.3 were not started; M9-A1–A9 are unchecked.
|
||||||
- Next action: use a disposable systemd VM if actual D-Bus acceptance is requested; no VM result is claimed here. Do not access production, commit, or deploy.
|
- Next action: finish the selected disposable VM's guest installation/authorization, then run synthetic private-peer and system-bus restart acceptance before considering M16 accepted. Do not access production, commit, or deploy.
|
||||||
|
|
||||||
## Plan maintenance
|
## Plan maintenance
|
||||||
|
|
||||||
|
|||||||
@@ -29,14 +29,14 @@ The user has approved the following choices. They may be introduced when their i
|
|||||||
- Use a separate vault key pair from all WireGuard client/server key pairs. Encrypt each client secret to the vault public key; protect the vault secret key with password-based encryption using the established `age` format. Do not replace this with password-only encryption of every new client record, which would require a password on creation.
|
- Use a separate vault key pair from all WireGuard client/server key pairs. Encrypt each client secret to the vault public key; protect the vault secret key with password-based encryption using the established `age` format. Do not replace this with password-only encryption of every new client record, which would require a password on creation.
|
||||||
- `wireguard-uapi` remains an approved option but is not selected for the bounded runtime reader: its public read API does not expose a receive deadline or a complete-dump guarantee. A systemd D-Bus request being accepted is not proof of job completion or successful VPN application.
|
- `wireguard-uapi` remains an approved option but is not selected for the bounded runtime reader: its public read API does not expose a receive deadline or a complete-dump guarantee. A systemd D-Bus request being accepted is not proof of job completion or successful VPN application.
|
||||||
- Runtime observation uses the already approved `rustix` 1.1.5 safe `net` and `event` APIs for a nonblocking Linux Generic Netlink socket, polling and datagram I/O. This keeps one monotonic deadline and exact truncation/sender checks around the checked WireGuard decoder. It does not add a direct dependency or use a subprocess.
|
- Runtime observation uses the already approved `rustix` 1.1.5 safe `net` and `event` APIs for a nonblocking Linux Generic Netlink socket, polling and datagram I/O. This keeps one monotonic deadline and exact truncation/sender checks around the checked WireGuard decoder. It does not add a direct dependency or use a subprocess.
|
||||||
- The systemd adapter pins `zbus` 5.19.0 with default features disabled and only `async-io` enabled. It uses zbus's asynchronous signal and method APIs, driven by a small standard-library wake/park loop so every call shares the caller's monotonic deadline. zbus's async-io backend owns internal I/O executor thread(s); callers must allow the connection to be dropped after each operation. Signal polling uses zbus 5.19.0's doc-hidden `export::futures_core` re-export to avoid another direct dependency; keep zbus pinned until that implementation is deliberately migrated.
|
- The systemd adapter pins `zbus` 5.19.0 with default features disabled and `async-io` plus `p2p` enabled. It first uses the system bus, then selects `/run/systemd/private` peer mode only when the bus connection reports a missing/refused/disconnected endpoint, before submitting any restart request. Permission errors, authentication/protocol failures, and deadline expiry do not trigger fallback. Private mode uses EXTERNAL authentication and checks the peer UID is root. Bus calls use the systemd well-known destination; private calls omit the destination and avoid bus-daemon owner monitoring. zbus's local peer signal matching tracks `JobRemoved` after systemd `Subscribe`. Each call uses zbus's asynchronous APIs driven by a small standard-library wake/park loop so operations share the caller's monotonic deadline. zbus's async-io backend owns internal I/O executor thread(s); callers must allow the connection to be dropped after each operation. Signal polling uses zbus 5.19.0's doc-hidden `export::futures_core` re-export to avoid another direct dependency; keep zbus pinned until that implementation is deliberately migrated. The private systemd socket is an internal systemd interface and may change between releases.
|
||||||
- Avoid incompatible duplicate versions of secret-handling types where library APIs exchange them. Use supported re-exports where appropriate rather than adding redundant direct dependencies.
|
- Avoid incompatible duplicate versions of secret-handling types where library APIs exchange them. Use supported re-exports where appropriate rather than adding redundant direct dependencies.
|
||||||
- Additional direct dependencies still require the comparison and prior agreement required by [AGENTS.md](../AGENTS.md), including any test/build helpers. Transitive dependencies of approved crates are resolved through Cargo; review their compatibility and runtime requirements. Do not add a direct `tokio` dependency merely because an approved library uses an async runtime internally.
|
- Additional direct dependencies still require the comparison and prior agreement required by [AGENTS.md](../AGENTS.md), including any test/build helpers. Transitive dependencies of approved crates are resolved through Cargo; review their compatibility and runtime requirements. Do not add a direct `tokio` dependency merely because an approved library uses an async runtime internally.
|
||||||
- `clap`, `chrono`, SQLite, a generic INI writer, a mocking framework, and archive compression libraries are not part of the approved initial set. Use the standard library for the small startup argument surface and background threads/channels where sufficient.
|
- `clap`, `chrono`, SQLite, a generic INI writer, a mocking framework, and archive compression libraries are not part of the approved initial set. Use the standard library for the small startup argument surface and background threads/channels where sufficient.
|
||||||
|
|
||||||
### External runtime and build requirements
|
### External runtime and build requirements
|
||||||
|
|
||||||
- Production requires Linux with WireGuard support, the existing configured systemd service, and the system D-Bus needed by `zbus`. Existing `wg-quick` service dependencies remain necessary if that is how the service manages the interface.
|
- Production requires Linux with WireGuard support and the existing configured systemd service. Restart uses the system D-Bus when available and can use systemd's root-only `/run/systemd/private` peer socket when the bus endpoint is unavailable. Permission or authentication failures and timeouts stop the operation without trying the private socket. The private socket is not a stable public systemd API. Existing `wg-quick` service dependencies remain necessary if that is how the service manages the interface.
|
||||||
- The application uses library APIs rather than invoking `wg`, `systemctl`, `age`, `qrencode`, `tar`, or `openssl` for its own operations. Test fixture setup and the existing WireGuard service may use their required system tools; that does not create an application subprocess fallback.
|
- The application uses library APIs rather than invoking `wg`, `systemctl`, `age`, `qrencode`, `tar`, or `openssl` for its own operations. Test fixture setup and the existing WireGuard service may use their required system tools; that does not create an application subprocess fallback.
|
||||||
- Rust/Cargo and the target toolchain/linker are build requirements, not production application requirements. Verify the release binary's actual dynamic-library requirements rather than promising an automatically static executable.
|
- Rust/Cargo and the target toolchain/linker are build requirements, not production application requirements. Verify the release binary's actual dynamic-library requirements rather than promising an automatically static executable.
|
||||||
- A Docker-API-compatible runtime, test images, and container tools belong to the integration environment only. Production does not need Docker or Testcontainers.
|
- A Docker-API-compatible runtime, test images, and container tools belong to the integration environment only. Production does not need Docker or Testcontainers.
|
||||||
|
|||||||
+1
-1
@@ -235,7 +235,7 @@ the user-excluded IPv6 capability and traffic checks.
|
|||||||
|
|
||||||
### Systemd acceptance and test execution
|
### Systemd acceptance and test execution
|
||||||
|
|
||||||
- Use a disposable Linux VM with WireGuard, systemd, and a system D-Bus to verify the actual `zbus` service adapter, job completion/timeouts, restart verification, and recovery from a selected backup. Do not substitute a mounted host D-Bus or claim Alpine tests cover systemd.
|
- Use a disposable Linux VM with WireGuard and systemd to verify the actual `zbus` service adapter, job completion/timeouts, restart verification, and recovery from a selected backup. Exercise both the system-bus path and the root-only `/run/systemd/private` path with the system bus endpoint unavailable. Also verify that permission/authentication failures and connection deadlines do not trigger fallback. Current synthetic tests cover transport selection and failure classification; they do not emulate systemd or prove the D-Bus peer protocol. Do not substitute a mounted host D-Bus or claim Alpine tests cover systemd. The private socket is an internal systemd interface and may change between releases.
|
||||||
- Keep container and VM suites out of the default `cargo test` execution. Once implemented, document their exact opt-in commands and prerequisites in this document. Do not list a planned command as available before its test target exists.
|
- Keep container and VM suites out of the default `cargo test` execution. Once implemented, document their exact opt-in commands and prerequisites in this document. Do not list a planned command as available before its test target exists.
|
||||||
- When integration testing is explicitly requested, unavailable Docker, WireGuard support, IPv4 networking, or required capabilities means the suite is not executed or incomplete, not passed. Distinguish prerequisite failures from application failures in the report. IPv6 capability and traffic are intentionally not tested by M8.
|
- When integration testing is explicitly requested, unavailable Docker, WireGuard support, IPv4 networking, or required capabilities means the suite is not executed or incomplete, not passed. Distinguish prerequisite failures from application failures in the report. IPv6 capability and traffic are intentionally not tested by M8.
|
||||||
- Report unit/fixture results, Alpine real-WireGuard results, and VM real-systemd results separately. Full acceptance requires the relevant real-system checks, not just mocks or successful container startup.
|
- Report unit/fixture results, Alpine real-WireGuard results, and VM real-systemd results separately. Full acceptance requires the relevant real-system checks, not just mocks or successful container startup.
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
# Milestone 16 evidence: private systemd peer transport
|
||||||
|
|
||||||
|
Recorded 2026-10-27T22:42:48+02:00 on the native
|
||||||
|
`x86_64-unknown-linux-gnu` development host. This is **implementation and
|
||||||
|
synthetic evidence, not systemd acceptance**. The milestone remains blocked on
|
||||||
|
private-peer behavioral and disposable-VM checks.
|
||||||
|
|
||||||
|
## Source and protocol basis
|
||||||
|
|
||||||
|
The existing adapter connected only with `zbus::Connection::system()`. That
|
||||||
|
cannot reach systemd where no system D-Bus daemon exists. Official systemd
|
||||||
|
[source](https://github.com/systemd/systemd/blob/main/src/shared/bus-util.c)
|
||||||
|
shows root `systemctl` connecting to `unix:path=/run/systemd/private` and
|
||||||
|
checking peer credentials. Its [manual](https://www.freedesktop.org/software/systemd/man/latest/systemd.html)
|
||||||
|
calls this an internal interface; compatibility across systemd releases is
|
||||||
|
not guaranteed. Pinned [zbus 5.19.0](https://docs.rs/zbus/5.19.0/zbus/connection/struct.Builder.html)
|
||||||
|
provides `p2p` and EXTERNAL-authentication support.
|
||||||
|
|
||||||
|
The adapter now selects the system bus first and falls back to the private
|
||||||
|
socket only for a missing/refused/disconnected bus endpoint during connection
|
||||||
|
setup, before any restart request. Permission, handshake and timeout failures
|
||||||
|
do not trigger fallback. The private path checks peer UID 0; method calls omit
|
||||||
|
the bus destination; its `JobRemoved` stream uses a local peer match without
|
||||||
|
`org.freedesktop.DBus` owner monitoring. The system-bus path retains its
|
||||||
|
destination and owner-change watch. Both preserve `Subscribe` before
|
||||||
|
`RestartUnit`, canonical unit identity, the shared deadline, job matching and
|
||||||
|
conservative submission uncertainty. No retry follows a possibly submitted
|
||||||
|
restart. Application backup/confirmation/runtime/applied logic is unchanged.
|
||||||
|
|
||||||
|
## Checks, review, and gap
|
||||||
|
|
||||||
|
An independent GPT-6-Luna/high reviewer found M16-R1 (P2): private socket
|
||||||
|
permission errors were reduced to `RequestFailed`. A separate GPT-6-Luna/high
|
||||||
|
repairer changed the production builder error mapping to preserve
|
||||||
|
`PermissionDenied` and added a focused synthetic assertion. The reviewer
|
||||||
|
rechecked and closed R1. M16-R2 (P2) remains open: synthetic tests do not
|
||||||
|
execute the production private peer branch end to end. An attempted local zbus
|
||||||
|
UnixStream peer fixture failed at peer credential access with `EPERM` in the
|
||||||
|
sandbox; later fixture variants did not establish a successful protocol path.
|
||||||
|
Those experiments were removed from the test tree. They are not evidence that
|
||||||
|
real systemd rejects the implementation. No host private socket was accessed.
|
||||||
|
|
||||||
|
Pinned `rustc`/Cargo 1.97.1, rustfmt 1.9.0, and Clippy 0.1.97 were verified.
|
||||||
|
On the repaired inputs, from the repository root:
|
||||||
|
|
||||||
|
- `cargo test --locked --lib service::dbus::tests -- --test-threads=1`: 16 passed.
|
||||||
|
- `cargo fmt --all -- --check`: exit 0.
|
||||||
|
- `cargo clippy --locked --all-targets -- -D warnings`: exit 0.
|
||||||
|
- `cargo test --locked`: 367 passed, 3 explicitly ignored, 0 failed, including 2 documentation tests. Selected Docker/VM suites were not run.
|
||||||
|
- `cargo build --locked --release`: exit 0.
|
||||||
|
- `git diff --check`: exit 0.
|
||||||
|
|
||||||
|
Base commit: `fe7e3d61c22108acba5c4a86480254a12e089a53`. Dirty inputs and
|
||||||
|
SHA256 at validation:
|
||||||
|
|
||||||
|
- `Cargo.toml`: `807d08cc43b4c3f986134b7aa5b051fd2f0966fab95ac01d861c65f173c77caa`
|
||||||
|
- `Cargo.lock`: `6a929bb72d90da16b5ca7d209566e31a611d1ddcd5817ee778d7e9fe25b10c2e`
|
||||||
|
- `src/service/dbus.rs`: `d1ed34dd8a46d2495299c776176d7d5f6cfa9b6abb29ec84b6045e74bdffeb26`
|
||||||
|
- `docs/DEPENDENCIES.md`: `1db85ecd3f892e3f42f1220eb7627cfd423406e46e5024bdf6599a647f07be1f`
|
||||||
|
- `docs/TESTING.md`: `7ecab091999b50964b1641772a4cfc370639507b9c6621a058829a54b0f71325`
|
||||||
|
|
||||||
|
Native release artifact `target/release/wireguard-admin` SHA256:
|
||||||
|
`64c2851431ec406901e76dc866a05abd630a0e71adfd4efc16d3b44ea3c72b73`.
|
||||||
|
|
||||||
|
The prior disposable Debian VM was stopped before guest installation and has
|
||||||
|
no verified guest marker or running systemd acceptance environment, per
|
||||||
|
[`VM_ACCEPTANCE.md`](../VM_ACCEPTANCE.md). No private-socket or system-bus
|
||||||
|
restart was run in a disposable VM. Neither transport is accepted against real
|
||||||
|
systemd yet; production must not be used to fill this gap. The next check is
|
||||||
|
to finish and authorize a disposable VM with synthetic WireGuard configuration,
|
||||||
|
then run both transport paths and verify job completion plus full runtime
|
||||||
|
matching, with exact source/artifact identity.
|
||||||
+428
-107
@@ -12,6 +12,7 @@ use std::task::{Context, Poll, Wake, Waker};
|
|||||||
use std::thread;
|
use std::thread;
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
|
|
||||||
|
use zbus::connection::Builder;
|
||||||
use zbus::export::futures_core::Stream;
|
use zbus::export::futures_core::Stream;
|
||||||
use zbus::message::Type;
|
use zbus::message::Type;
|
||||||
use zbus::{Connection, MatchRule, MessageStream, Proxy};
|
use zbus::{Connection, MatchRule, MessageStream, Proxy};
|
||||||
@@ -26,6 +27,7 @@ const SYSTEMD_PATH: &str = "/org/freedesktop/systemd1";
|
|||||||
const SYSTEMD_MANAGER: &str = "org.freedesktop.systemd1.Manager";
|
const SYSTEMD_MANAGER: &str = "org.freedesktop.systemd1.Manager";
|
||||||
const DBUS_NAME: &str = "org.freedesktop.DBus";
|
const DBUS_NAME: &str = "org.freedesktop.DBus";
|
||||||
const DBUS_INTERFACE: &str = "org.freedesktop.DBus";
|
const DBUS_INTERFACE: &str = "org.freedesktop.DBus";
|
||||||
|
const SYSTEMD_PRIVATE_ADDRESS: &str = "unix:path=/run/systemd/private";
|
||||||
|
|
||||||
/// Resolve the configured unit's primary ID, request a restart of exactly
|
/// Resolve the configured unit's primary ID, request a restart of exactly
|
||||||
/// `configured_unit`, and wait for its matching systemd `JobRemoved` result.
|
/// `configured_unit`, and wait for its matching systemd `JobRemoved` result.
|
||||||
@@ -175,11 +177,17 @@ fn execution(outcome: RestartOutcome, submitted: bool, phase: RestartPhase) -> R
|
|||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
struct SystemdBus {
|
struct SystemdBus {
|
||||||
connection: Option<Connection>,
|
connection: Option<Connection>,
|
||||||
|
private_peer: bool,
|
||||||
manager: Option<Proxy<'static>>,
|
manager: Option<Proxy<'static>>,
|
||||||
job_events: Option<zbus::proxy::SignalStream<'static>>,
|
job_events: Option<JobSignalStream>,
|
||||||
owner_events: Option<zbus::MessageStream>,
|
owner_events: Option<zbus::MessageStream>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum JobSignalStream {
|
||||||
|
Bus(Box<zbus::proxy::SignalStream<'static>>),
|
||||||
|
Peer(Box<MessageStream>),
|
||||||
|
}
|
||||||
|
|
||||||
/// The small systemd unit API used by the shared resolver. Keeping method
|
/// The small systemd unit API used by the shared resolver. Keeping method
|
||||||
/// selection and canonical-ID lookup here lets synthetic tests exercise the
|
/// selection and canonical-ID lookup here lets synthetic tests exercise the
|
||||||
/// same resolution flow as the production adapter.
|
/// same resolution flow as the production adapter.
|
||||||
@@ -223,12 +231,11 @@ impl UnitLoadApi for SystemdBus {
|
|||||||
configured_unit: &str,
|
configured_unit: &str,
|
||||||
deadline: RestartDeadline,
|
deadline: RestartDeadline,
|
||||||
) -> Result<Self::UnitPath, RestartFailure> {
|
) -> Result<Self::UnitPath, RestartFailure> {
|
||||||
let manager = self
|
self.call_manager("LoadUnit", &(configured_unit,), deadline)
|
||||||
.manager
|
.map_err(map_unit_resolution_failure)?
|
||||||
.as_ref()
|
.body()
|
||||||
.ok_or(RestartFailure::UnitResolutionFailed)?;
|
.deserialize()
|
||||||
drive(manager.call("LoadUnit", &(configured_unit,)), deadline)?
|
.map_err(|_| RestartFailure::UnitResolutionFailed)
|
||||||
.map_err(map_unit_resolution_error)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_state(
|
fn load_state(
|
||||||
@@ -236,9 +243,7 @@ impl UnitLoadApi for SystemdBus {
|
|||||||
unit_path: &Self::UnitPath,
|
unit_path: &Self::UnitPath,
|
||||||
deadline: RestartDeadline,
|
deadline: RestartDeadline,
|
||||||
) -> Result<String, RestartFailure> {
|
) -> Result<String, RestartFailure> {
|
||||||
let unit = self.unit_proxy(unit_path, deadline)?;
|
self.unit_property(unit_path, "LoadState", deadline)
|
||||||
drive(unit.get_property::<String>("LoadState"), deadline)?
|
|
||||||
.map_err(map_unit_resolution_error)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn canonical_id(
|
fn canonical_id(
|
||||||
@@ -246,87 +251,184 @@ impl UnitLoadApi for SystemdBus {
|
|||||||
unit_path: &Self::UnitPath,
|
unit_path: &Self::UnitPath,
|
||||||
deadline: RestartDeadline,
|
deadline: RestartDeadline,
|
||||||
) -> Result<String, RestartFailure> {
|
) -> Result<String, RestartFailure> {
|
||||||
let unit = self.unit_proxy(unit_path, deadline)?;
|
self.unit_property(unit_path, "Id", deadline)
|
||||||
drive(unit.get_property::<String>("Id"), deadline)?.map_err(map_unit_resolution_error)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SystemdBus {
|
impl SystemdBus {
|
||||||
fn unit_proxy(
|
fn call_manager<B: serde::ser::Serialize + zbus::zvariant::DynamicType>(
|
||||||
&self,
|
&self,
|
||||||
unit_path: &zbus::zvariant::OwnedObjectPath,
|
method: &'static str,
|
||||||
|
body: &B,
|
||||||
deadline: RestartDeadline,
|
deadline: RestartDeadline,
|
||||||
) -> Result<Proxy<'static>, RestartFailure> {
|
) -> Result<zbus::message::Message, RestartFailure> {
|
||||||
let connection = self
|
let connection = self
|
||||||
.connection
|
.connection
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.ok_or(RestartFailure::BusDisconnected)?;
|
.ok_or(RestartFailure::BusDisconnected)?;
|
||||||
|
let destination = manager_destination(self.private_peer);
|
||||||
drive(
|
drive(
|
||||||
Proxy::new_owned(
|
connection.call_method(
|
||||||
connection.clone(),
|
destination,
|
||||||
SYSTEMD_NAME,
|
SYSTEMD_PATH,
|
||||||
unit_path.clone(),
|
Some(SYSTEMD_MANAGER),
|
||||||
"org.freedesktop.systemd1.Unit",
|
method,
|
||||||
|
body,
|
||||||
),
|
),
|
||||||
deadline,
|
deadline,
|
||||||
)?
|
)?
|
||||||
.map_err(map_unit_resolution_error)
|
.map_err(map_bus_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unit_property(
|
||||||
|
&self,
|
||||||
|
unit_path: &zbus::zvariant::OwnedObjectPath,
|
||||||
|
property: &'static str,
|
||||||
|
deadline: RestartDeadline,
|
||||||
|
) -> Result<String, RestartFailure> {
|
||||||
|
let connection = self
|
||||||
|
.connection
|
||||||
|
.as_ref()
|
||||||
|
.ok_or(RestartFailure::BusDisconnected)?;
|
||||||
|
if !self.private_peer {
|
||||||
|
let unit = drive(
|
||||||
|
Proxy::new_owned(
|
||||||
|
connection.clone(),
|
||||||
|
SYSTEMD_NAME,
|
||||||
|
unit_path.clone(),
|
||||||
|
"org.freedesktop.systemd1.Unit",
|
||||||
|
),
|
||||||
|
deadline,
|
||||||
|
)?
|
||||||
|
.map_err(map_unit_resolution_error)?;
|
||||||
|
return drive(unit.get_property::<String>(property), deadline)?
|
||||||
|
.map_err(map_unit_resolution_error);
|
||||||
|
}
|
||||||
|
let message = drive(
|
||||||
|
connection.call_method(
|
||||||
|
Option::<&str>::None,
|
||||||
|
unit_path.clone(),
|
||||||
|
Some("org.freedesktop.DBus.Properties"),
|
||||||
|
"Get",
|
||||||
|
&("org.freedesktop.systemd1.Unit", property),
|
||||||
|
),
|
||||||
|
deadline,
|
||||||
|
)?
|
||||||
|
.map_err(map_unit_resolution_error)?;
|
||||||
|
let value: zbus::zvariant::OwnedValue = message
|
||||||
|
.body()
|
||||||
|
.deserialize()
|
||||||
|
.map_err(|_| RestartFailure::UnitResolutionFailed)?;
|
||||||
|
String::try_from(value).map_err(|_| RestartFailure::UnitResolutionFailed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn manager_destination(private_peer: bool) -> Option<&'static str> {
|
||||||
|
(!private_peer).then_some(SYSTEMD_NAME)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn owner_monitoring_enabled(private_peer: bool) -> bool {
|
||||||
|
!private_peer
|
||||||
|
}
|
||||||
|
|
||||||
|
fn select_connection<T>(
|
||||||
|
system_bus: Result<T, (RestartFailure, bool)>,
|
||||||
|
private_peer: impl FnOnce() -> Result<T, RestartFailure>,
|
||||||
|
) -> Result<(T, bool), RestartFailure> {
|
||||||
|
match system_bus {
|
||||||
|
Ok(connection) => Ok((connection, false)),
|
||||||
|
// Once private fallback is attempted, keep its typed failure: it is
|
||||||
|
// the selected transport's actionable result.
|
||||||
|
Err((_failure, true)) => private_peer().map(|connection| (connection, true)),
|
||||||
|
Err((failure, false)) => Err(failure),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RestartTransport for SystemdBus {
|
impl RestartTransport for SystemdBus {
|
||||||
fn connect(&mut self, deadline: RestartDeadline) -> Result<(), RestartFailure> {
|
fn connect(&mut self, deadline: RestartDeadline) -> Result<(), RestartFailure> {
|
||||||
let connection = drive(Connection::system(), deadline)?.map_err(map_bus_error)?;
|
let bus_connection = connect_system_bus(deadline);
|
||||||
let manager = drive(
|
let ((connection, manager), private_peer) = select_connection(bus_connection, || {
|
||||||
Proxy::new_owned(
|
let connection = drive(
|
||||||
connection.clone(),
|
Builder::address(SYSTEMD_PRIVATE_ADDRESS)
|
||||||
SYSTEMD_NAME,
|
.map_err(map_bus_error)?
|
||||||
SYSTEMD_PATH,
|
.p2p()
|
||||||
SYSTEMD_MANAGER,
|
.auth_mechanism(zbus::connection::AuthMechanism::External)
|
||||||
),
|
.build(),
|
||||||
deadline,
|
deadline,
|
||||||
)?
|
)?
|
||||||
.map_err(map_bus_error)?;
|
.map_err(map_private_builder_error)?;
|
||||||
self.manager = Some(manager);
|
let uid = drive(connection.peer_creds(), deadline)?
|
||||||
|
.map_err(map_peer_creds_error)?
|
||||||
|
.unix_user_id();
|
||||||
|
if !is_root_peer_uid(uid) {
|
||||||
|
return Err(RestartFailure::PermissionDenied);
|
||||||
|
}
|
||||||
|
Ok((connection, None))
|
||||||
|
})?;
|
||||||
|
self.private_peer = private_peer;
|
||||||
|
self.manager = manager;
|
||||||
self.connection = Some(connection);
|
self.connection = Some(connection);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn subscribe(&mut self, deadline: RestartDeadline) -> Result<(), RestartFailure> {
|
fn subscribe(&mut self, deadline: RestartDeadline) -> Result<(), RestartFailure> {
|
||||||
let manager = self
|
let connection = self
|
||||||
.manager
|
.connection
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.ok_or(RestartFailure::SubscriptionFailed)?;
|
.ok_or(RestartFailure::SubscriptionFailed)?;
|
||||||
let job_events = drive(manager.receive_signal("JobRemoved"), deadline)?
|
let job_events = if self.private_peer {
|
||||||
.map_err(|_| RestartFailure::SubscriptionFailed)?;
|
let rule = MatchRule::builder()
|
||||||
|
.msg_type(Type::Signal)
|
||||||
|
.path(SYSTEMD_PATH)
|
||||||
|
.map_err(|_| RestartFailure::SubscriptionFailed)?
|
||||||
|
.interface(SYSTEMD_MANAGER)
|
||||||
|
.map_err(|_| RestartFailure::SubscriptionFailed)?
|
||||||
|
.member("JobRemoved")
|
||||||
|
.map_err(|_| RestartFailure::SubscriptionFailed)?
|
||||||
|
.build();
|
||||||
|
JobSignalStream::Peer(Box::new(
|
||||||
|
drive(
|
||||||
|
MessageStream::for_match_rule(rule, connection, Some(8)),
|
||||||
|
deadline,
|
||||||
|
)?
|
||||||
|
.map_err(|_| RestartFailure::SubscriptionFailed)?,
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
let manager = self
|
||||||
|
.manager
|
||||||
|
.as_ref()
|
||||||
|
.ok_or(RestartFailure::SubscriptionFailed)?;
|
||||||
|
JobSignalStream::Bus(Box::new(
|
||||||
|
drive(manager.receive_signal("JobRemoved"), deadline)?
|
||||||
|
.map_err(|_| RestartFailure::SubscriptionFailed)?,
|
||||||
|
))
|
||||||
|
};
|
||||||
|
|
||||||
let owner_rule = MatchRule::builder()
|
// Peer mode uses zbus's local p2p match and must not call the bus daemon.
|
||||||
.msg_type(Type::Signal)
|
// The bus mode additionally watches owner changes before Subscribe.
|
||||||
.sender(DBUS_NAME)
|
|
||||||
.map_err(|_| RestartFailure::SubscriptionFailed)?
|
|
||||||
.interface(DBUS_INTERFACE)
|
|
||||||
.map_err(|_| RestartFailure::SubscriptionFailed)?
|
|
||||||
.member("NameOwnerChanged")
|
|
||||||
.map_err(|_| RestartFailure::SubscriptionFailed)?
|
|
||||||
.add_arg(SYSTEMD_NAME)
|
|
||||||
.map_err(|_| RestartFailure::SubscriptionFailed)?
|
|
||||||
.build();
|
|
||||||
let owner_events = drive(
|
|
||||||
MessageStream::for_match_rule(
|
|
||||||
owner_rule,
|
|
||||||
self.connection
|
|
||||||
.as_ref()
|
|
||||||
.ok_or(RestartFailure::SubscriptionFailed)?,
|
|
||||||
Some(1),
|
|
||||||
),
|
|
||||||
deadline,
|
|
||||||
)?
|
|
||||||
.map_err(|_| RestartFailure::SubscriptionFailed)?;
|
|
||||||
|
|
||||||
// Both signal matches are installed before Subscribe and RestartUnit.
|
|
||||||
self.job_events = Some(job_events);
|
self.job_events = Some(job_events);
|
||||||
self.owner_events = Some(owner_events);
|
if owner_monitoring_enabled(self.private_peer) {
|
||||||
drive(manager.call::<_, _, ()>("Subscribe", &()), deadline)?.map_err(map_bus_error)
|
let owner_rule = MatchRule::builder()
|
||||||
|
.msg_type(Type::Signal)
|
||||||
|
.sender(DBUS_NAME)
|
||||||
|
.map_err(|_| RestartFailure::SubscriptionFailed)?
|
||||||
|
.interface(DBUS_INTERFACE)
|
||||||
|
.map_err(|_| RestartFailure::SubscriptionFailed)?
|
||||||
|
.member("NameOwnerChanged")
|
||||||
|
.map_err(|_| RestartFailure::SubscriptionFailed)?
|
||||||
|
.add_arg(SYSTEMD_NAME)
|
||||||
|
.map_err(|_| RestartFailure::SubscriptionFailed)?
|
||||||
|
.build();
|
||||||
|
self.owner_events = Some(
|
||||||
|
drive(
|
||||||
|
MessageStream::for_match_rule(owner_rule, connection, Some(1)),
|
||||||
|
deadline,
|
||||||
|
)?
|
||||||
|
.map_err(|_| RestartFailure::SubscriptionFailed)?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
self.call_manager("Subscribe", &(), deadline)?;
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn resolve_primary_unit(
|
fn resolve_primary_unit(
|
||||||
@@ -342,45 +444,54 @@ impl RestartTransport for SystemdBus {
|
|||||||
unit: &str,
|
unit: &str,
|
||||||
deadline: RestartDeadline,
|
deadline: RestartDeadline,
|
||||||
) -> Result<String, RestartFailure> {
|
) -> Result<String, RestartFailure> {
|
||||||
let manager = self.manager.as_ref().ok_or(RestartFailure::RequestFailed)?;
|
|
||||||
let connection = self
|
let connection = self
|
||||||
.connection
|
.connection
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.ok_or(RestartFailure::BusDisconnected)?;
|
.ok_or(RestartFailure::BusDisconnected)?;
|
||||||
let owner_events = self
|
|
||||||
.owner_events
|
|
||||||
.as_mut()
|
|
||||||
.ok_or(RestartFailure::BusDisconnected)?;
|
|
||||||
let request_body = (unit, "replace");
|
let request_body = (unit, "replace");
|
||||||
let request = manager.call("RestartUnit", &request_body);
|
let job_path: zbus::zvariant::OwnedObjectPath = if self.private_peer {
|
||||||
let job_path: zbus::zvariant::OwnedObjectPath = drive(
|
self.call_manager("RestartUnit", &request_body, deadline)?
|
||||||
async {
|
.body()
|
||||||
let mut request = pin!(request);
|
.deserialize()
|
||||||
let mut closed = pin!(connection.closed());
|
.map_err(|_| RestartFailure::RequestFailed)?
|
||||||
poll_fn(|cx| {
|
} else {
|
||||||
if connection.is_closed() {
|
let manager = self.manager.as_ref().ok_or(RestartFailure::RequestFailed)?;
|
||||||
return Poll::Ready(Err(RestartFailure::BusDisconnected));
|
let owner_events = self
|
||||||
}
|
.owner_events
|
||||||
if let Poll::Ready(event) = Stream::poll_next(Pin::new(&mut *owner_events), cx)
|
.as_mut()
|
||||||
{
|
.ok_or(RestartFailure::BusDisconnected)?;
|
||||||
return Poll::Ready(match event {
|
let request =
|
||||||
None => Err(RestartFailure::BusDisconnected),
|
manager.call::<_, _, zbus::zvariant::OwnedObjectPath>("RestartUnit", &request_body);
|
||||||
Some(Err(error)) => Err(map_bus_error(error)),
|
drive(
|
||||||
Some(Ok(_)) => Err(RestartFailure::ManagerOwnerChanged),
|
async {
|
||||||
});
|
let mut request = pin!(request);
|
||||||
}
|
let mut closed = pin!(connection.closed());
|
||||||
if let Poll::Ready(result) = Future::poll(request.as_mut(), cx) {
|
poll_fn(|cx| {
|
||||||
return Poll::Ready(result.map_err(map_bus_error));
|
if connection.is_closed() {
|
||||||
}
|
return Poll::Ready(Err(RestartFailure::BusDisconnected));
|
||||||
if Future::poll(closed.as_mut(), cx).is_ready() {
|
}
|
||||||
return Poll::Ready(Err(RestartFailure::BusDisconnected));
|
if let Poll::Ready(event) =
|
||||||
}
|
Stream::poll_next(Pin::new(&mut *owner_events), cx)
|
||||||
Poll::Pending
|
{
|
||||||
})
|
return Poll::Ready(match event {
|
||||||
.await
|
None => Err(RestartFailure::BusDisconnected),
|
||||||
},
|
Some(Err(error)) => Err(map_bus_error(error)),
|
||||||
deadline,
|
Some(Ok(_)) => Err(RestartFailure::ManagerOwnerChanged),
|
||||||
)??;
|
});
|
||||||
|
}
|
||||||
|
if let Poll::Ready(result) = Future::poll(request.as_mut(), cx) {
|
||||||
|
return Poll::Ready(result.map_err(map_bus_error));
|
||||||
|
}
|
||||||
|
if Future::poll(closed.as_mut(), cx).is_ready() {
|
||||||
|
return Poll::Ready(Err(RestartFailure::BusDisconnected));
|
||||||
|
}
|
||||||
|
Poll::Pending
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
},
|
||||||
|
deadline,
|
||||||
|
)??
|
||||||
|
};
|
||||||
Ok(job_path.to_string())
|
Ok(job_path.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -393,10 +504,7 @@ impl RestartTransport for SystemdBus {
|
|||||||
.job_events
|
.job_events
|
||||||
.as_mut()
|
.as_mut()
|
||||||
.ok_or(RestartFailure::BusDisconnected)?;
|
.ok_or(RestartFailure::BusDisconnected)?;
|
||||||
let owner_events = self
|
let mut owner_events = self.owner_events.as_mut();
|
||||||
.owner_events
|
|
||||||
.as_mut()
|
|
||||||
.ok_or(RestartFailure::BusDisconnected)?;
|
|
||||||
drive(
|
drive(
|
||||||
async {
|
async {
|
||||||
let mut closed = pin!(connection.closed());
|
let mut closed = pin!(connection.closed());
|
||||||
@@ -404,8 +512,8 @@ impl RestartTransport for SystemdBus {
|
|||||||
if connection.is_closed() {
|
if connection.is_closed() {
|
||||||
return Poll::Ready(Ok(ServiceEvent::Disconnected));
|
return Poll::Ready(Ok(ServiceEvent::Disconnected));
|
||||||
}
|
}
|
||||||
if let Poll::Ready(message) =
|
if let Some(owner_events) = owner_events.as_mut()
|
||||||
Stream::poll_next(Pin::new(&mut *owner_events), cx)
|
&& let Poll::Ready(message) = Stream::poll_next(Pin::new(owner_events), cx)
|
||||||
{
|
{
|
||||||
return Poll::Ready(match message {
|
return Poll::Ready(match message {
|
||||||
None => Ok(ServiceEvent::Disconnected),
|
None => Ok(ServiceEvent::Disconnected),
|
||||||
@@ -427,11 +535,17 @@ impl RestartTransport for SystemdBus {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if let Poll::Ready(message) = Stream::poll_next(Pin::new(&mut *job_events), cx)
|
let message = match job_events {
|
||||||
{
|
JobSignalStream::Bus(stream) => {
|
||||||
|
Stream::poll_next(Pin::new(stream), cx).map(|message| message.map(Ok))
|
||||||
|
}
|
||||||
|
JobSignalStream::Peer(stream) => Stream::poll_next(Pin::new(stream), cx),
|
||||||
|
};
|
||||||
|
if let Poll::Ready(message) = message {
|
||||||
return Poll::Ready(match message {
|
return Poll::Ready(match message {
|
||||||
None => Ok(ServiceEvent::Disconnected),
|
None => Ok(ServiceEvent::Disconnected),
|
||||||
Some(message) => {
|
Some(Err(error)) => Err(map_bus_error(error)),
|
||||||
|
Some(Ok(message)) => {
|
||||||
let decoded: Result<
|
let decoded: Result<
|
||||||
(u32, zbus::zvariant::OwnedObjectPath, String, String),
|
(u32, zbus::zvariant::OwnedObjectPath, String, String),
|
||||||
_,
|
_,
|
||||||
@@ -459,6 +573,34 @@ impl RestartTransport for SystemdBus {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn connect_system_bus(
|
||||||
|
deadline: RestartDeadline,
|
||||||
|
) -> Result<(Connection, Option<Proxy<'static>>), (RestartFailure, bool)> {
|
||||||
|
let connection = match drive(Connection::system(), deadline) {
|
||||||
|
Err(failure) => return Err((failure, false)),
|
||||||
|
Ok(Err(error)) => {
|
||||||
|
return Err((map_connect_error(&error), private_fallback_allowed(&error)));
|
||||||
|
}
|
||||||
|
Ok(Ok(connection)) => connection,
|
||||||
|
};
|
||||||
|
let manager = match drive(
|
||||||
|
Proxy::new_owned(
|
||||||
|
connection.clone(),
|
||||||
|
SYSTEMD_NAME,
|
||||||
|
SYSTEMD_PATH,
|
||||||
|
SYSTEMD_MANAGER,
|
||||||
|
),
|
||||||
|
deadline,
|
||||||
|
) {
|
||||||
|
Err(failure) => return Err((failure, false)),
|
||||||
|
Ok(Err(error)) => {
|
||||||
|
return Err((map_connect_error(&error), private_fallback_allowed(&error)));
|
||||||
|
}
|
||||||
|
Ok(Ok(manager)) => manager,
|
||||||
|
};
|
||||||
|
Ok((connection, Some(manager)))
|
||||||
|
}
|
||||||
|
|
||||||
fn unit_is_loaded(load_state: &str) -> bool {
|
fn unit_is_loaded(load_state: &str) -> bool {
|
||||||
load_state == "loaded"
|
load_state == "loaded"
|
||||||
}
|
}
|
||||||
@@ -483,11 +625,61 @@ fn map_bus_error(error: zbus::Error) -> RestartFailure {
|
|||||||
{
|
{
|
||||||
RestartFailure::PermissionDenied
|
RestartFailure::PermissionDenied
|
||||||
}
|
}
|
||||||
zbus::Error::InputOutput(_) | zbus::Error::Handshake(_) => RestartFailure::BusDisconnected,
|
zbus::Error::InputOutput(error) if error.kind() == std::io::ErrorKind::PermissionDenied => {
|
||||||
|
RestartFailure::PermissionDenied
|
||||||
|
}
|
||||||
|
zbus::Error::InputOutput(_) => RestartFailure::BusDisconnected,
|
||||||
_ => RestartFailure::RequestFailed,
|
_ => RestartFailure::RequestFailed,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn map_connect_error(error: &zbus::Error) -> RestartFailure {
|
||||||
|
match error {
|
||||||
|
zbus::Error::Connection(error, _) | zbus::Error::InputOutput(error)
|
||||||
|
if error.kind() == std::io::ErrorKind::PermissionDenied =>
|
||||||
|
{
|
||||||
|
RestartFailure::PermissionDenied
|
||||||
|
}
|
||||||
|
zbus::Error::Connection(_, _) | zbus::Error::InputOutput(_) => {
|
||||||
|
RestartFailure::BusDisconnected
|
||||||
|
}
|
||||||
|
// A handshake rejection is an authentication/protocol failure, not an
|
||||||
|
// unavailable transport. Never bypass it via the private socket.
|
||||||
|
_ => RestartFailure::RequestFailed,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn map_private_builder_error(error: zbus::Error) -> RestartFailure {
|
||||||
|
map_connect_error(&error)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn private_fallback_allowed(error: &zbus::Error) -> bool {
|
||||||
|
match error {
|
||||||
|
zbus::Error::Connection(io, _) | zbus::Error::InputOutput(io) => matches!(
|
||||||
|
io.kind(),
|
||||||
|
std::io::ErrorKind::NotFound
|
||||||
|
| std::io::ErrorKind::ConnectionRefused
|
||||||
|
| std::io::ErrorKind::ConnectionReset
|
||||||
|
| std::io::ErrorKind::ConnectionAborted
|
||||||
|
| std::io::ErrorKind::NotConnected
|
||||||
|
| std::io::ErrorKind::BrokenPipe
|
||||||
|
),
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_root_peer_uid(uid: Option<u32>) -> bool {
|
||||||
|
uid == Some(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn map_peer_creds_error(error: std::io::Error) -> RestartFailure {
|
||||||
|
if error.kind() == std::io::ErrorKind::PermissionDenied {
|
||||||
|
RestartFailure::PermissionDenied
|
||||||
|
} else {
|
||||||
|
RestartFailure::BusDisconnected
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn map_unit_resolution_error(error: zbus::Error) -> RestartFailure {
|
fn map_unit_resolution_error(error: zbus::Error) -> RestartFailure {
|
||||||
match map_bus_error(error) {
|
match map_bus_error(error) {
|
||||||
RestartFailure::PermissionDenied => RestartFailure::PermissionDenied,
|
RestartFailure::PermissionDenied => RestartFailure::PermissionDenied,
|
||||||
@@ -496,6 +688,13 @@ fn map_unit_resolution_error(error: zbus::Error) -> RestartFailure {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn map_unit_resolution_failure(failure: RestartFailure) -> RestartFailure {
|
||||||
|
match failure {
|
||||||
|
RestartFailure::RequestFailed => RestartFailure::UnitResolutionFailed,
|
||||||
|
other => other,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct ThreadWake(thread::Thread);
|
struct ThreadWake(thread::Thread);
|
||||||
|
|
||||||
impl Wake for ThreadWake {
|
impl Wake for ThreadWake {
|
||||||
@@ -621,6 +820,109 @@ mod tests {
|
|||||||
RestartDeadline::new(Instant::now(), Duration::from_secs(2))
|
RestartDeadline::new(Instant::now(), Duration::from_secs(2))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn transport_selection_falls_back_only_before_restart_setup() {
|
||||||
|
let (selected, private) =
|
||||||
|
select_connection(Ok("bus"), || Ok("private")).expect("available bus transport");
|
||||||
|
assert_eq!(selected, "bus");
|
||||||
|
assert!(!private);
|
||||||
|
|
||||||
|
let (selected, private) = select_connection(
|
||||||
|
Err::<&str, _>((RestartFailure::BusDisconnected, true)),
|
||||||
|
|| Ok("private"),
|
||||||
|
)
|
||||||
|
.expect("private peer fallback");
|
||||||
|
assert_eq!(selected, "private");
|
||||||
|
assert!(private);
|
||||||
|
assert_eq!(manager_destination(true), None);
|
||||||
|
assert_eq!(manager_destination(false), Some(SYSTEMD_NAME));
|
||||||
|
assert!(!owner_monitoring_enabled(true));
|
||||||
|
assert!(owner_monitoring_enabled(false));
|
||||||
|
|
||||||
|
let private_denied =
|
||||||
|
select_connection::<&str>(Err((RestartFailure::BusDisconnected, true)), || {
|
||||||
|
Err(RestartFailure::PermissionDenied)
|
||||||
|
});
|
||||||
|
assert_eq!(private_denied, Err(RestartFailure::PermissionDenied));
|
||||||
|
|
||||||
|
let private_timeout =
|
||||||
|
select_connection::<&str>(Err((RestartFailure::BusDisconnected, true)), || {
|
||||||
|
Err(RestartFailure::WaitTimedOutMayStillComplete)
|
||||||
|
});
|
||||||
|
assert_eq!(
|
||||||
|
private_timeout,
|
||||||
|
Err(RestartFailure::WaitTimedOutMayStillComplete)
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut private_attempted = false;
|
||||||
|
let denied_bus =
|
||||||
|
select_connection::<&str>(Err((RestartFailure::PermissionDenied, false)), || {
|
||||||
|
private_attempted = true;
|
||||||
|
Ok("private")
|
||||||
|
});
|
||||||
|
assert_eq!(denied_bus, Err(RestartFailure::PermissionDenied));
|
||||||
|
assert!(!private_attempted);
|
||||||
|
|
||||||
|
let timeout_bus = select_connection::<&str>(
|
||||||
|
Err((RestartFailure::WaitTimedOutMayStillComplete, false)),
|
||||||
|
|| {
|
||||||
|
private_attempted = true;
|
||||||
|
Ok("private")
|
||||||
|
},
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
timeout_bus,
|
||||||
|
Err(RestartFailure::WaitTimedOutMayStillComplete)
|
||||||
|
);
|
||||||
|
assert!(!private_attempted);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn private_fallback_is_limited_to_missing_or_refused_bus_endpoint() {
|
||||||
|
let unavailable = zbus::Error::Connection(
|
||||||
|
Arc::new(std::io::Error::from(std::io::ErrorKind::ConnectionRefused)),
|
||||||
|
zbus::Address::try_from("unix:path=/run/dbus/system_bus_socket")
|
||||||
|
.expect("valid synthetic bus address"),
|
||||||
|
);
|
||||||
|
assert!(private_fallback_allowed(&unavailable));
|
||||||
|
assert_eq!(
|
||||||
|
map_connect_error(&unavailable),
|
||||||
|
RestartFailure::BusDisconnected
|
||||||
|
);
|
||||||
|
assert!(private_fallback_allowed(&zbus::Error::InputOutput(
|
||||||
|
Arc::new(std::io::Error::from(std::io::ErrorKind::ConnectionReset))
|
||||||
|
)));
|
||||||
|
|
||||||
|
let denied = zbus::Error::Connection(
|
||||||
|
Arc::new(std::io::Error::from(std::io::ErrorKind::PermissionDenied)),
|
||||||
|
zbus::Address::try_from("unix:path=/run/dbus/system_bus_socket")
|
||||||
|
.expect("valid synthetic bus address"),
|
||||||
|
);
|
||||||
|
assert!(!private_fallback_allowed(&denied));
|
||||||
|
assert_eq!(map_connect_error(&denied), RestartFailure::PermissionDenied);
|
||||||
|
assert_eq!(
|
||||||
|
map_private_builder_error(denied),
|
||||||
|
RestartFailure::PermissionDenied
|
||||||
|
);
|
||||||
|
assert!(!private_fallback_allowed(&zbus::Error::InputOutput(
|
||||||
|
Arc::new(std::io::Error::from(std::io::ErrorKind::PermissionDenied))
|
||||||
|
)));
|
||||||
|
assert!(!private_fallback_allowed(&zbus::Error::Handshake(
|
||||||
|
"synthetic authentication rejection".into()
|
||||||
|
)));
|
||||||
|
assert!(!is_root_peer_uid(None));
|
||||||
|
assert!(!is_root_peer_uid(Some(1000)));
|
||||||
|
assert!(is_root_peer_uid(Some(0)));
|
||||||
|
assert_eq!(
|
||||||
|
map_peer_creds_error(std::io::Error::from(std::io::ErrorKind::PermissionDenied)),
|
||||||
|
RestartFailure::PermissionDenied
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
map_peer_creds_error(std::io::Error::from(std::io::ErrorKind::ConnectionReset)),
|
||||||
|
RestartFailure::BusDisconnected
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
fn removed(path: &str, unit: &str, result: JobResult) -> ServiceEvent {
|
fn removed(path: &str, unit: &str, result: JobResult) -> ServiceEvent {
|
||||||
ServiceEvent::JobRemoved {
|
ServiceEvent::JobRemoved {
|
||||||
job_path: path.to_owned(),
|
job_path: path.to_owned(),
|
||||||
@@ -747,6 +1049,21 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unit_resolution_keeps_permission_timeout_and_disconnect_categories() {
|
||||||
|
assert_eq!(
|
||||||
|
map_unit_resolution_failure(RestartFailure::RequestFailed),
|
||||||
|
RestartFailure::UnitResolutionFailed
|
||||||
|
);
|
||||||
|
for failure in [
|
||||||
|
RestartFailure::PermissionDenied,
|
||||||
|
RestartFailure::WaitTimedOutMayStillComplete,
|
||||||
|
RestartFailure::BusDisconnected,
|
||||||
|
] {
|
||||||
|
assert_eq!(map_unit_resolution_failure(failure), failure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct FakeUnitLoadApi {
|
struct FakeUnitLoadApi {
|
||||||
calls: Vec<String>,
|
calls: Vec<String>,
|
||||||
configured_unit_seen: Option<String>,
|
configured_unit_seen: Option<String>,
|
||||||
@@ -903,6 +1220,10 @@ mod tests {
|
|||||||
);
|
);
|
||||||
assert_eq!(execution.phase, RestartPhase::RequestRestart);
|
assert_eq!(execution.phase, RestartPhase::RequestRestart);
|
||||||
assert!(execution.submitted);
|
assert!(execution.submitted);
|
||||||
|
assert_eq!(
|
||||||
|
fake.calls.iter().filter(|call| **call == "restart").count(),
|
||||||
|
1
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
Reference in New Issue
Block a user