Commit Graph

26 Commits

Author SHA1 Message Date
founder a9b6e3c15d fix(systemd): drop the duplicate ensure-swap, use Type=exec and stepped restart backoff [P1]
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.
2026-08-06 22:54:11 +05:00
founder e9f0e29f75 refactor: one ownership model for every file written under /etc [P1]
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.
2026-08-06 22:50:19 +05:00
founder a895295701 fix(swap): never touch an unowned swapfile, activate before deactivating [P0]
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.
2026-08-06 21:07:55 +05:00
founder 549fbcae31 fix(swap): make the /etc/fstab rewrite transactional [P0]
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.
2026-08-06 21:02:38 +05:00
founder d98ade42ce docs: unify language contract to Russian and reconcile the spec with the code
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.
2026-08-06 16:45:00 +05:00
founder 16214f3c66 AmneziaWG 3.0 readiness: centralize wg CLI contract, drop dead host-side fallback
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.
2026-08-06 15:32:26 +05:00
founder 2c27bff800 Реализован production-модуль sysctl_tuning и полная интеграция fix33
Добавлен отдельный 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.
2026-05-28 11:31:13 +05:00
founder 4ff2bb2c30 Документация: унификация проверок для amnezia-awg2 и amnezia-awg 2026-05-12 21:19:08 +05:00
founder c7b5da98f4 Документация DNS: динамический deep-check и уточнение критериев 2026-05-12 21:12:15 +05:00
founder 4757799f04 Документация: обязательный production-профиль AmneziaDNS и правила DNS/53 2026-05-12 20:36:17 +05:00
founder b3d274f89d Документация и UX установки: полный production-fix по fix32 2026-05-12 20:16:56 +05:00
founder bb37da72c0 fix30: очищены old-bash формулировки и уточнён class-id контракт 2026-05-12 17:50:15 +05:00
founder 44a62cf634 fix29: удалён legacy, введён Debian 13-only и обновлён rollback 2026-05-12 17:26:41 +05:00
founder 69c737bcf4 fix28: обновлена production-документация и упаковка docs в deb 2026-05-12 16:56:36 +05:00
founder c10fb775e9 Fix23: вернуть production ssh_port 2323 в конфиге и документации
- config.example: host_firewall.ssh_port=2323

- docs: синхронизированы CONFIGURATION/MIGRATION/ACCEPTANCE_CHECKLIST
2026-05-12 14:02:12 +05:00
founder 6e586c7af9 Fix23: финализация production-сборки 0.1.4
- Swap: сузил tolerance usable size до 64 KiB

- Cargo.lock: синхронизирована версия awg2-harness = 0.1.4

- Example/docs: ssh_port default приведен к 22

- Обновлен acceptance checklist по SSH default
2026-05-12 14:00:54 +05:00
founder 1e2a3b5b67 Добавлен fallback диагностики FORWARD через nft и расширен вывод status 2026-05-12 11:48:18 +05:00
founder f8c6f2e92d Уточнена диагностика UDP port mapping для Docker и обновлены checklist/docs 2026-05-11 19:30:25 +05:00
founder adc244daa7 Добавлена диагностика Docker/Amnezia port path в status и уточнена документация host firewall 2026-05-11 19:22:39 +05:00
founder 36600c0a5a fix10: стабилизация host firewall first-run и no-op reconcile
- исправлен first-run дефект: delete table вынесен из nft batch\n- добавлен no-op путь при отсутствии host firewall drift\n- исправлен trim для default_input_policy в drift-check\n- добавлены тесты для batch script и policy trim\n- уточнен release-gate в acceptance checklist по INPUT/FORWARD
2026-05-11 18:45:17 +05:00
founder ed4a571d71 fix9: безопасный host firewall apply и строгий конфиг
- host_firewall секция стала обязательной (убран silent enable)\n- host firewall применяется атомарным nft batch\n- добавлен verify-after-apply с hard-fail при drift\n- усилен tolerant parser drift (кавычки, ipv6-icmp alias)\n- расширены unit-тесты config/firewall\n- дополнен acceptance checklist блоком host firewall
2026-05-11 18:33:46 +05:00
founder 7a4e9764b1 fix7: продакшен-реализация полного reconcile swap и интеграций
- Полностью переписан 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
2026-05-11 17:30:15 +05:00
founder b83f2ee3a9 Пакет: убрать docker.io из metadata и улучшить help/docs layout
Удалён Recommends: docker.io из control. Ужесточено предупреждение, что --install не является production-path. В .deb docs переведены на repo-like структуру (/usr/share/doc/awg2-harness/repo/...), чтобы не ломались относительные ссылки.
2026-05-10 17:36:31 +05:00
founder 9d2d2bc0c2 Сборка: добавить production packaging flow через .deb
Добавлен package-режим в tools/build.sh: --package, --dist-dir, --version. Реализована сборка Debian-пакета, metadata/control, postinst/postrm и SHA256SUMS. Установочные пути унифицированы под /usr/sbin и /lib/systemd/system для production-модели.
2026-05-10 17:26:11 +05:00
founder 76c6b52f4e Полный продакшн-фикс: parity с old bash, drift-recovery, status/runtime и builder separation 2026-05-10 17:09:39 +05:00
founder d937f816bf feat: bootstrap awg2 harness rust workspace and docs 2026-05-09 02:17:25 +05:00