Two release blockers, both in the most destructive operation the package performs.
1. An unreadable /etc/fstab was treated as an empty one.
let content = fs::read_to_string(fstab_path).unwrap_or_default();
PermissionDenied, an I/O error, a damaged filesystem and "file absent" all collapsed
into an empty string. The rewrite then emitted a fstab containing only the managed swap
line and installed it atomically and successfully, dropping the root and boot mounts.
Consequence: a host that does not boot, guarded by one unwrap_or_default. The same
masking was present in read_fstab_swap_entries, so an unreadable fstab was reported by
status as "no swap entries", which reads as healthy drift-free state.
2. The backup was best-effort.
let _ = fs::write(bak_path, &content);
If it failed, the rewrite proceeded anyway. IMPLEMENTATION_SPEC and the acceptance
checklist both asserted the backup exists. The scenario where the backup fails is
exactly the scenario where it is needed.
New module fstab.rs owns the operation:
- read_required distinguishes NotFound from every other error and fails on both. A
missing /etc/fstab on Debian is an anomaly, not a blank slate, so it is never created;
- a blank candidate is refused outright;
- the backup is mandatory and flushed before the temp file is created, so there is no
window where the target is about to be replaced and no recovery copy exists;
- two copies are kept: .awg2-harness.bak is the state before the current rewrite,
.awg2-harness.original is written once with create_new and holds the host as it was
before this package first touched it;
- the temp file is a sibling of the target, created with create_new, sync_all'd, and
inherits the original permissions; a stale temp from a killed run is removed explicitly
rather than reused;
- the candidate is validated with findmnt --verify --tab-file before the swap. The step is
skipped when findmnt cannot be spawned; a rejection is fatal;
- commit is an atomic rename followed by a directory fsync;
- after the rename the target is re-read and compared to what was written. A mismatch
restores the backup and fails.
Also fixed: render_fstab_line interpolated the path raw. fstab fields are
whitespace-separated, so a space or tab in swap.path would shift every following column.
Paths now go through octal escaping (\040, \011, \012, \134).
Ten tests, all portable - the "unreadable but present" case is exercised by pointing the
reader at a directory, which fails as a non-NotFound error on every platform. Covered:
unreadable target is an error rather than an empty file, missing target is an error,
backup failure leaves the target byte-identical, blank candidate refused, unchanged
candidate is a no-op that writes no backup, the pristine copy survives repeated rewrites,
a stale temp does not block commit, and field escaping.
Docs updated: IMPLEMENTATION_SPEC section 17 gained the full rewrite contract, the
acceptance checklist gained seven checks including the two failure modes above.
cargo test --workspace: 124 + 4 passed, ten new. clippy -D warnings clean.
firewall.rs held two subsystems that share nothing but the nft command layer: the host
INPUT firewall, applied in the host namespace and owning inet awg2_host_firewall, and the
anti-torrent guard, applied in the namespace chosen by config and owning inet
awg2_harness. The split follows that boundary rather than an inspect/apply one:
firewall/mod.rs apply_firewall, namespace selection, shared table constants
firewall/nft.rs text normalization and the nft command wrappers
firewall/host.rs host INPUT firewall and its 5 tests
firewall/antitorrent.rs connection-count guard and its 6 tests
Production logic is unchanged. Function sets before and after are identical: 25 against
25, verified by extracting names from the old blob and the four new files and diffing.
Not a pure move in one respect, stated here so it is not a surprise in review: the test
module contained four near-identical 45-line Config literals, about 180 lines of
copy-paste, differing only in the single field each test actually cared about. Carrying
four copies into the new files would have knowingly propagated the duplication, so they
collapse into one test_config() helper in firewall/mod.rs that tests mutate. This is why
the four files total 875 lines against the old 890 despite gaining doc comments.
Doc comments now record the reasoning that was only implicit before: why nft output is
compared after normalization (rendering differs across versions - quoting, comma-separated
ct state lists, brace layout), why the host firewall batch never contains a delete (a
partially applied INPUT chain could lock out SSH), why ct state new is load-bearing in the
anti-torrent rule, why connlimit_state_present accepts two renderings, and why the
anti-torrent rule defaults to the container namespace.
IMPLEMENTATION_SPEC section 3 is updated: it described the old flat layout and carried the
split as an unfulfilled intention. Both traffic and firewall now match what it documents.
cargo test --workspace: 110 + 4 passed, unchanged count. clippy -D warnings clean.
print_status and print_degraded_sections carried four byte-identical blocks - host
firewall (25 lines), anti-torrent (24), last actions (9) and recent events (10) - plus a
Conntrack block that was the same logic assembled differently and a Traffic desired block
of four printlns. Roughly 80 lines of copy-paste with no test covering either copy, so a
format change applied to one path would have silently diverged from the other. The
divergence would only ever be visible while the container is down, which is the worst
moment to discover the diagnostic output is wrong.
Every shared section is now one function called from both paths:
print_traffic_desired_section, print_host_firewall_section, print_antitorrent_section,
print_conntrack_section, print_last_actions_section, print_recent_events_section. The file
already had this shape for print_swap_status and print_reboot_section; the pattern is now
followed consistently. The Docker/Amnezia port path block keeps two variants because they
genuinely differ - real values against unknown - but the shared FORWARD ownership note is
a single constant.
The runtime state paths were also duplicated across modules: status.rs re-declared the
literals that daemon.rs already owns. daemon::LAST_STATE_FILE and daemon::EVENTS_FILE are
now pub and reused, and the recent-events window is a named constant instead of a bare 10.
Ordering fix in the same function: print_network_tuning_status ran between the service
line and the container lines, so container/container_id/pid/netns/interface appeared under
the "Network tuning:" header at the same two-space indent as its fields. Container
identity is now printed inside the discovery match, before any other section opens, in
both the healthy and degraded paths. Field names are unchanged; only the order of two
blocks moved. IMPLEMENTATION_SPEC section 15 documented the old order and is updated.
print_degraded_sections returned anyhow::Result<()> while never failing; it returns unit
now.
status.rs: 645 -> 602 lines. cargo test --workspace: 110 + 4 passed. clippy -D warnings
clean.
The docs folder had drifted into a mixed-language contract: IMPLEMENTATION_SPEC.md was
entirely in English while the rest was Russian with English headings. Prose is now
Russian throughout; English is kept only where it is technically load-bearing (commands,
tool output, config keys, file/module/table names, tc handles) and for terms with no
settled Russian equivalent in this context (drift, reconcile, peer, namespace, handshake).
docs/README.md now states that contract explicitly.
Rewriting the spec surfaced several places where it described intent rather than the
code. Corrected against traffic_core/src:
- module layout: the spec showed traffic/ and firewall/ as directories with
model/inspect/apply submodules. They are single files, and sysctl_tuning.rs was
missing from the tree entirely.
- data model: Peer was documented as carrying public key, handshake and counters. It
carries only ipv4; runtime state lives in a separate PeerRuntime, and there is no
desired-state struct at all - it is derived inline from config plus sorted peers.
RuntimeTarget.netns_path is a String, not a PathBuf.
- interface detection: the spec listed a third step taking the first non-loopback
interface with an IPv4 address. No such fallback exists; detection bails with an
explicit error, which is safer and is now documented as deliberate.
- reconcile: the pseudo-flow folded firewall into reconcile_traffic. They are two
independent reconciles with separate locks.
- locking: only traffic.lock was documented. There are three locks, and
sysctl_tuning.lock has a 30s timeout rather than 10s. Stale-lock recovery via
/proc/<pid> was undocumented.
- sysctl_tuning: the module also manages /etc/modules-load.d/90-awg2-harness-bbr.conf
when BBR is active, and refuses to touch either managed file without its marker.
Both docs and the acceptance checklist claimed only the sysctl.d file was written.
- status contract: the documented output was outdated. All 15 printed sections are now
listed in the order status.rs emits them, including the degraded path when no container
is found, and the fact that the Network tuning block is printed between the System
service line and the container lines.
- swap: size was documented as a constant 2048 MB instead of swap.size_mb, and the
/etc/fstab.awg2-harness.bak backup was undocumented.
- apply/verify: the tolerated-error lists are now the actual closed sets from the code.
Acceptance checklist gained checks for the daemon startup failure policy (sysctl/swap
fatal, traffic/firewall not), lock contention and stale-lock recovery, degraded status,
the modules-load file, and the marker guard. The stale host wg requirement was dropped
from its host-tools item.
Every path, table, counter and log string quoted in the docs was verified to exist in
the source. cargo test --workspace: 110 + 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.
- Полностью переписан 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-модели.