1. The state directory was created too late and never verified.
ensure_state_dir only ran when the ownership record was written, well after
assert_swap_path_is_safe had already required every parent of swap.path to exist. On a
clean host that meant a perfectly legal /var/lib/awg2-harness/swapfile was rejected
because the directory this package is supposed to create did not exist yet.
Worse, when the directory did exist the check only confirmed it was a directory. A
pre-existing /var/lib/awg2-harness that was group- or world-writable defeated the whole
point of restricting swap.path to root-owned roots: another user could still swap the
swapfile out from under a root-owned fallocate or mkswap.
ensure_state_dir now runs first in ensure_swap and asserts what the restriction actually
depends on: the parent is a real directory and not a symlink, the state directory itself
is not a symlink, it is owned by uid 0, and it is not writable by group or others. A
mode that is merely narrower than 0700 is tightened rather than rejected; anything
group- or world-writable is a hard error, because the safety argument for the whole
allow-list rests on it.
The units also declare StateDirectory=awg2-harness with mode 0700 and
RuntimeDirectory=awg2-harness, so in the common case systemd creates and owns both and
the binary only has to verify. The runtime checks stay regardless: the CLI subcommands
run outside the service lifecycle.
2. The fstab copies could be more permissive than the file they mirror.
The backup and pristine copies were written at a hardcoded 0644 with a comment claiming
they must not be wider than the original. On a host where /etc/fstab is 0600 they were,
and the rollback then handed that wider mode to the live file too. FstabTransaction now
captures the live mode when it opens and uses it for the backup, the pristine copy, the
candidate and the rollback.
One test added, unix-only since it asserts a mode: a 0600 fstab produces 0600 copies and
stays 0600 after the commit.
cargo test --workspace: 180 + 4 passed. clippy -D warnings clean. Unit files not verified
here - no systemd on this host; tools/build.sh runs systemd-analyze verify on the build
machine.
awg2-harness.service ran ensure-swap from ExecStartPre while the daemon runs it again as
the first step of its own startup. Idempotency kept it harmless, but it cost two locks,
two reads of /proc/swaps, two persistence checks, and split the startup failure path
across two units so an operator reading systemctl status could not tell which invocation
failed. The daemon owns the startup sequence, its error contract and its log stream, so
the ExecStartPre is removed. The standalone ensure-swap subcommand is unaffected.
Type=simple reports a unit as started the moment systemd forks, before the binary is
known to be executable. Type=exec waits for the exec to succeed, so a missing or
non-executable /usr/sbin/awg2-harness fails the unit instead of appearing to start.
Restart policy: the daemon moves from Restart=always to on-failure, matching the
scheduler. A clean exit is deliberate and should not be respawned. For the scheduler this
was already load-bearing and is now documented - it exits 0 both when reboot.enabled is
false and after systemd accepts the reboot request, so Restart=always would have spun it
in a loop on any host with scheduled reboots disabled.
RestartSec=10s with StartLimitBurst=5 over StartLimitIntervalSec=60 could exhaust the
limit in about 50 seconds, which is easy to hit when the daemon is failing because Docker
or the container is still coming up - the restarts burn the budget before the dependency
can recover. Replaced with a stepped backoff, 10 -> 20 -> 40 -> 80 -> 160s via
RestartSteps=4 and RestartMaxDelaySec=160s, and the limit window widened to 15 minutes
with a burst of 10 so a full backoff sequence fits inside it. Both directives require
systemd 254+; Debian 13 ships 257.
Documented alongside, because it is the same lifecycle contract and had no home in the
docs: configuration is read once at process start and a config edit needs a restart of the
affected service. daemon-reload is not involved - it applies to unit files, not to the
application config. ExecReload is deliberately absent, since adding it without real SIGHUP
handling would imply a reload path that does not exist.
Unit files are installed from the repo root by tools/build.sh, which already runs
systemd-analyze verify on the build host when it is available, so these changes are
verified there. Not verifiable in this environment: no Debian, no systemd.
Acceptance checklist gained five checks covering unit verification, the restart-required
contract, single ensure-swap per start, growing backoff, and the scheduler not looping
when reboots are disabled.
- Полностью переписан swap-модуль в режиме exclusive ownership\n- Добавлены verify/checks, статус фактического swap-state и drift\n- Интегрирован ensure-swap в daemon и systemd ExecStartPre\n- Усилен lock API (таймауты) и применение в traffic/firewall/swap\n- Усилен parser firewall (JSON-first + устойчивый text fallback)\n- Обновлены config validation, runtime tools и default priority\n- Синхронизированы docs и build/package hygiene\n- Удалены member Cargo.lock в пользу корневого lockfile
Добавлен package-режим в tools/build.sh: --package, --dist-dir, --version. Реализована сборка Debian-пакета, metadata/control, postinst/postrm и SHA256SUMS. Установочные пути унифицированы под /usr/sbin и /lib/systemd/system для production-модели.