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.
Two modules wrote into /etc/sysctl.d and did it differently. sysctl_tuning refused to
touch a file whose first line lacked its marker and cleaned up on disable. swap wrote
/etc/sysctl.d/99-awg2-harness.conf with a different marker, no ownership check at all -
an administrator's file at that path was silently clobbered - and never removed it when
the module was turned off, so vm.swappiness stayed pinned forever.
New module managed_file.rs holds the rules once:
- a read that fails is never an absent file;
- ownership is Absent / Managed / Foreign, decided by the marker on the first line;
- write refuses Foreign, skips when the content already matches, and is otherwise atomic:
temp sibling created with create_new, sync_all, mode 0644, rename, directory flush;
- remove refuses Foreign and treats Absent as success.
Both modules now go through it. sysctl_tuning loses its private write_atomic,
ensure_managed_or_absent and remove_managed_sysctl_file_at.
The swappiness file moves to /etc/sysctl.d/90-awg2-harness-swap.conf with the marker
"# Managed by awg2-harness swap", matching the 90- prefix every other managed file uses.
The old path is removed on first run of the new version, but only when it still carries
the previous "# Managed by awg2-harness" marker; a foreign file there is left alone and
does not fail the migration, since the current file lives elsewhere.
Disable semantics are now explicit: swap.enabled = false removes the managed file and
does nothing else. Runtime vm.swappiness is deliberately not reset - picking a value
would be inventing a policy - and active swap is not deactivated, because merely turning
a feature off should not trigger a destructive action. Previously ensure_swap returned
early and left the persisted file behind.
Also replaced a vacuous test: swappiness_conf_render_is_stable asserted that a string
literal equals a format! of the same literal, touching no production code at all. It now
exercises render_swappiness_conf, and a companion test proves the rendered file is
recognised as managed by the same marker check that guards overwriting it - otherwise
every reconcile would refuse to touch its own output.
Twelve tests: the three ownership states, refusal to overwrite and refusal to remove a
foreign file with content asserted intact, idempotent write, stale temp handling, an
unreadable path reported as an error, plus legacy migration in all three cases - ours,
foreign, absent.
cargo test --workspace: 148 + 4 passed. clippy -D warnings clean.
Two more release blockers in the swap module.
1. Foreign files could be destroyed.
swap.path was validated against a blacklist of exact strings. /etc/../etc/fstab is not
string-equal to /etc/fstab but resolves to it, and nothing normalized the path or
checked the parent chain for symlinks. ensure_desired_swapfile then removed any
existing regular file whose size did not match the configured one, so a mistyped or
hostile path meant deleting /etc/passwd, an application config, or any other regular
file. disable_foreign_swaps additionally deleted foreign swapfiles outright.
Ownership is now explicit. A sidecar marker <swap.path>.awg2-harness-owned sits next to
the managed swapfile, and inspect_swapfile_ownership classifies the path as Missing,
ManagedValid, ManagedWrongSize or Foreign. Foreign is always a hard error - the file is
never resized, rewritten or removed. Foreign active swaps are now only swapped off;
deleting an administrator's swapfile is not this package's call, and swapoff plus the
fstab rewrite already produce the exclusive runtime state.
Path validation is split in two. The lexical half rejects relative paths, '.' and '..'
components, whitespace, control characters and backslashes, the system path list, and
anything under /proc, /sys or /dev; it touches no filesystem, so validate-config stays
side-effect free on a host where the file does not exist yet. It runs after '..' is
rejected, so the string is already normalized and the blacklist cannot be bypassed. The
filesystem half runs just before use and refuses a symlink anywhere in the parent chain,
not only at the final component.
One deliberate deviation from the audit: arbitrary absolute paths stay allowed rather
than being restricted to a fixed managed location. Pinning the path would break
deployments that already configured their own, and the ownership marker is the guard
that actually prevents data loss - a wrong path can now only fail, never destroy.
2. The migration order left the host swapless on failure.
ensure_swap ran disable_foreign_swaps first, then created the swapfile, then activated
it. Any failure in fallocate, dd, mkswap or swapon after that first step left the host
with no swap at all - on a box provisioned with swap precisely because it is short on
memory.
The sequence is now a transaction: preflight, prepare the managed swapfile, swapon,
verify it actually appears in the active list, and only then swapoff the others,
rewrite fstab, apply swappiness and verify. Nothing destructive happens until the
replacement is confirmed live. The one unavoidable gap is deactivating our own swap to
resize it, which cannot be done in place; foreign swap stays up throughout.
chmod 0600 now also happens before mkswap, so the file is never briefly world-readable
while it already holds swap structure.
config.rs no longer carries its own copy of the path blacklist and calls the shared
validator.
Twelve tests: the four ownership states, refusal to recreate a foreign file with its
content asserted intact afterwards, '..' traversal, dangerous and virtual paths,
whitespace and control characters, relative and directory paths, a missing parent
directory, an accepted chain, and a symlinked parent (unix only).
Docs updated: IMPLEMENTATION_SPEC section 17 gained the ownership table, the ordered
transaction and the path rules; CONFIGURATION and the acceptance checklist gained the
operator-facing constraints and nine checks.
cargo test --workspace: 136 + 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.
- добавлена секция host_firewall в конфиг и defaults\n- реализован host firewall reconcile в отдельной таблице\n- apply-firewall теперь делает host firewall + anti-torrent\n- убрана JSON-инспекция из full-table drift для anti-torrent\n- расширен status вывод drift по host firewall/anti-torrent
- Полностью переписан 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