The rollback claimed an invariant in its own comment - restore the original, confirm it,
and only then release staging - and did not enforce it. original_back was computed and then
ignored: the staging swapoff ran unconditionally.
The failure path that mattered: staging is active, the original is already off, reactivating
the original fails (swapon can fail under the memory pressure that made the resize
necessary), and the rollback then switched off the staging swap - the last thing carrying
load. Recovering from a failed resize left the host with no swap at all, which is worse
than the failure being recovered from.
original_back now gates the release. If the original cannot be brought back the rollback
returns early with the staging swap still active, and the error carries the two commands
that finish the job by hand. An extra temporary swap is always the better end state than
none. The staging file is also only unlinked once swapoff has actually succeeded - an
active swapfile cannot be removed, and attempting it produced an error that explained
nothing.
Asserting this needs a seam, because the invariant is a sequence of calls rather than a
value: a small SwapCommands trait covers swapon/swapoff, with the real implementation
behind it. Three tests: the original cannot be restored and swapoff is never called with
the message naming the preserved fallback; the original comes back and staging is released
in that order; nothing was taken down so nothing is touched.
Also from the same audit:
- StaleOwnership advice was wrong. It suggested deleting the ownership record, which turns
the file Foreign - refused just as firmly. The message now distinguishes the two cases: a
leftover .previous next to the swapfile is the signature of a replacement killed between
installing the new file and recording its identity, so the error says nothing is lost,
points at the intact previous swapfile, and gives the exact commands for going back or
keeping the new file. Full automatic crash recovery is deliberately not added here - it
needs a transition journal, and that belongs with the Debian acceptance work.
- The acceptance checklist still described the PID-file lock ("re-acquires by the absence
of /proc/<pid>"), which stopped being true when locking moved to flock. The kernel
releases it on process exit, and the leftover file means nothing.
- shellcheck was installed by --prepare-build-env but never run. It now runs in
prepare_build_gate: the script installs systemd units and writes to /usr/sbin, so it is
part of the release surface.
- The skip message for systemd-analyze verify now says what the check actually is - a
post-install gate, expected to be skipped on a clean builder - rather than reading like a
build gate that silently did not run.
One audit item did not reproduce: there is no duplicated -j in antitorrent_drop_counter,
the flag appears once.
Version raised to 0.3.0 in traffic_core/Cargo.toml, with the packaging examples in README
and INSTALLATION updated to match. The reboot scheduler keeps its own 0.2.0.
cargo fmt --check, clippy -D warnings, cargo test --locked --workspace --release: 191 + 4
passed. build.sh not syntax-checked here - no bash on this host; the shellcheck gate now
covers it on the builder.
Two release blockers, the first a regression introduced by dd5be47.
1. The anti-torrent batch was applied on the host.
Making the rebuild atomic moved it from run_step(transport, ..) to run_nft_batch(script),
and run_nft_batch spawned `nft` directly with Command::new. An nft transaction belongs to
the network namespace of the process that runs nft, so with the production default
apply_in_amnezia_netns = true the sequence became: inspect inside the container, install
the table on the host, verify inside the container, fail. The guard was silently
ineffective, a stray table accumulated on the host, and the next reconcile could fail on
`add table` because the host copy already existed.
NsTransport gains run_with_stdin, implemented for all three transports - host, nsenter,
and docker exec with -i, which is required or the child sees EOF immediately.
run_nft_batch now takes the transport, and both callers pass the one they inspected
with. A recording fake transport asserts the script goes through the transport rather
than to a host process, which is the regression this could not previously catch.
2. Swap replacement rolled back with the wrong priority and not from every branch.
The rollback reactivated with desired.priority - the value that may be exactly what the
kernel had just rejected - because the previously active priority was dropped at the
match arm and never passed in. The active entry's priority is now carried into
replace_active_swapfile and used by the rollback.
Three early failure branches also returned without undoing anything: a failed swapoff of
the original, a failed rename to .previous, and a failed swapoff of the staging file all
left the staging swap active and the transition half-applied, after which the next
reconcile tried to unlink a file that was still active and could not proceed.
The transition now records what it completed - staging_active, original_active,
original_moved, new_installed - and one rollback undoes exactly that much, in an order
that never leaves the host without swap: restore the original file, reactivate it at its
previous priority, confirm it, and only then release and remove the staging swap. The
returned error carries the original cause plus what the recovery achieved.
Leftovers from a killed process are no longer removed blindly. A .new or .previous that
is still active cannot be unlinked, so it is reported with the swapoff command that
clears it instead of failing the same way on every reconcile.
Also in this commit, from the same audit:
- Requires=docker.service dropped in favour of Wants=. The daemon is built to outlive
Docker being down, but Requires= had systemd stop the harness whenever Docker stopped,
and Restart=on-failure does not cover a dependency-ordered stop - so the reconnect loop
never got to run.
- apply_antitorrent_disabled swallowed every discover_target error, not just "no container
running". Docker being unreachable or a permission error on the socket reported success
for a cleanup that never happened. Only AppError::NotFound is tolerated now.
- FstabTransaction::begin asserts /etc/fstab is a regular non-symlink file. read_to_string
and metadata both follow links, so a symlinked fstab would be read through and then
replaced by the rename, detaching the real file from the path the system boots from.
- append_event serialises its read-modify-write behind a mutex; the reconcile loop and the
Docker events thread both append and could drop an entry.
- tool_exists loses the fallocate, chmod and dd arms, dead since mkswap --file replaced the
allocation chain.
Docs corrected where they no longer matched the code: findmnt is described as mandatory
rather than skippable, the swap creation wording is mkswap --file, and the README package
examples use 0.1.6 to match Cargo.toml. Remaining fallocate mentions are explicitly
historical ("the previous chain"), which is accurate.
Seven tests added: batch routed through the given transport, batch failure reported as
such, rollback state defaults and previous-priority carry, active leftovers reported rather
than deleted, inactive leftovers cleared.
cargo fmt --check, clippy -D warnings, cargo test --locked --workspace --release: 188 + 4
passed.
Three diagnostic defects from the follow-up audit.
1. The reboot plan was entirely best-effort.
persist_plan discarded the directory creation, the write and every error. The plan file
is the only thing on the host that says when it is going to reboot, so a failed write
produced a scheduler sitting on a timer that nothing could report. It now creates the
directory, writes through a temp file with sync_all and rename, and returns Result.
The plan also outlived its meaning in two places. With reboot.enabled = false the
scheduler exited without touching a plan written when it was still enabled, so status
kept advertising a reboot that would never happen. And after systemd accepted the reboot
request the plan stayed behind, leaving a past timestamp looking pending. Both paths now
clear it; an absent plan is the desired end state, not an error.
2. sysctl status turned a read failure into "file absent".
inspect_sysctl_tuning used fs::read_to_string(..).ok() for both managed files, so an
unreadable file was reported as missing - a diagnostic stating the opposite of the
truth, in the module whose whole job is to report drift accurately. Reads now go through
ManagedFile::read, which distinguishes absent from unreadable, and an unreadable file
produces its own drift reason naming the path and the error.
3. README described the removed ExecStartPre.
It still said the service runs ensure-swap before starting the daemon. The daemon
performs it as the first step of its own startup sequence.
Docs also state explicitly what survives swap.enabled = false, which was implied but never
written: the swapfile, its activation and the /etc/fstab entry all remain, so swap comes
back after a reboot. Only the vm.swappiness persistence is removed.
Acceptance checklist gained seven checks covering the priority-only path not recreating the
file, swap remaining active throughout a resize, foreign-swap rollback, the unreadable
status, and both reboot-plan lifecycle cases.
cargo test --workspace: 179 + 4 passed. clippy -D warnings clean.
Both the code comment and the docs said the interface row is "14 columns for AWG 2.0, 28
for AWG 3.0". That reads as a version signal and it is not one. The width of the row is
decided by the version of amneziawg-tools doing the printing, not by which protocol
parameters the tunnel has configured: AWG 3.0 tools emit the wide row for an AWG 2.0
configuration too, with the newer fields simply zero, empty or (none). Any rule of the
form "N columns means version X" is wrong by construction.
The classifier never relied on column count for that purpose - it discriminates on the
allowed-ips column - so this is a correction to what the code claims about itself and to
what the docs would lead a future change to assume.
Five fixtures added from the audit's list, all previously uncovered:
- AWG 3.0 interface row with header protection disabled: the key column is (none) and the
range fields collapse to single values;
- an AWG 2.0 configuration read through AWG 3.0 tools, asserted to have the same width as
a native AWG 3.0 row and to still be rejected while its peer parses;
- a dump containing several interfaces, each with its own peers;
- an interface with no peers at all;
- malformed rows - truncated, non-numeric handshake, non-CIDR allowed-ips - which must be
skipped rather than partially interpreted.
Docs also pin what was verified and against which versions, rather than stating it as
timeless: AmneziaVPN 5.0.0.5 and the master branches of amneziawg-tools and amneziawg-go
at the time of the change.
amnezia-awg3 is now described as what it is - not a confirmed official name. AmneziaVPN
5.0.0.5 added AWG 3.0 support but still exposes only Awg and Awg2 container variants. It
stays in the example list on purpose, and the reasoning is written down: a name that does
not match costs nothing because the search continues down the list, while a name missing
from the list produces a silent "container not found". This is a deliberate deviation from
the audit's suggestion to build defaults only from names in use today.
cargo fmt --check, clippy -D warnings, and cargo test --locked --workspace --release all
clean: 163 + 4 passed.
Peer discovery is now the single owner of how wg is invoked and parsed.
wg.rs:
- resolve the binary through ordered amnezia.wg_binaries (default [awg, wg]);
awg is the real binary in the official image and wg is a symlink to it, so awg is
tried first and wg stays for plain WireGuard containers. No probe call: resolution
is a property of the command being run anyway.
- drop the nsenter fallback. AmneziaWG is userspace and its UAPI socket lives under
RUNSTATEDIR/amneziawg/ in the container mount namespace, while host wireguard-tools
looks under /var/run/wireguard/. nsenter -n enters only the network namespace, so
that path could never return peers.
- classify dump lines structurally instead of by column count. The AWG interface line
is longer than a peer line (14 columns on AWG 2.0, 28 on AWG 3.0) and its
handshake/rx/tx positions hold junk-packet sizes that parse as integers, so only the
allowed-ips column discriminates. Column 1 of an interface line is the device private
key, which the previous heuristic rejected only incidentally.
- fix allowed-ips parsing: `show all allowed-ips` separates a peer's addresses with
spaces, not commas, so peers with more than one allowed IP were shaped on their first
address only and the rest ran unlimited.
- collapse the three duplicated /32-in-CIDR filters into one tokenizer shared by the
allowed-ips, dump and pretty-print parsers.
config.rs:
- add amnezia.wg_binaries with serde default, validated as bare command names
(non-empty, no whitespace, no path separators) since they execute inside the
container. Existing deployed configs keep parsing unchanged.
- remove host wg from the required-tool preflight. It gated daemon startup on
wireguard-tools for a code path that produced nothing.
status.rs:
- discover peers once instead of twice per status call and degrade gracefully like the
neighbouring sections instead of aborting the whole output.
Tests: real AWG 3.0 / AWG 2.0 / plain WireGuard interface-line fixtures, AWG 3.0 range
keepalive, space-separated allowed-ips, binary resolution order, and a test that parses
and validates the shipped config.example.toml.
Добавлен отдельный host-level модуль sysctl_tuning без легаси-подходов: managed sysctl файл, точечный reconcile через sysctl -w только для drifted ключей, режимы tcp_bbr off/auto/require, безопасное удаление только managed-файла и атомарная запись конфигурации.
Интегрированы новые CLI-команды apply-network-tuning/inspect-network-tuning, startup reconcile в daemon и секция Network tuning в status. Расширены config/defaults/валидация, обновлен пример конфига и документация. Добавлены unit-тесты для config и sysctl_tuning.
Добавлен package-режим в tools/build.sh: --package, --dist-dir, --version. Реализована сборка Debian-пакета, metadata/control, postinst/postrm и SHA256SUMS. Установочные пути унифицированы под /usr/sbin и /lib/systemd/system для production-модели.