3 Commits

Author SHA1 Message Date
hell 539ef92197 test: cover the exit-node paths these two suites already touch
Both files were submitted in #143 and #147, before the exit node existed here,
so their exit-specific halves had to be stripped to keep them importable. That
code is in the tree since #149, and the stripped copies now test less than they
could:

- test_ssh_error_text.py gains ExitLinkMessageTests: an SSH whose sudo call
  returns -1 with no output at all (a dropped transport) must make exit_link
  and exit_unlink raise a message that names the exit code, not
  "exit link apply failed: " with nothing after the colon. That is the failure
  the empty str(EOFError) produced, and it is the reason #147 exists.
- test_sudo_command_chains.py pins amnezia-exit, not amnezia-awg2, as the
  representative container that DNSManager attaches to amnezia-dns-net. Both
  are in vpn_containers, but only the exit one silently breaks DNS on an exit
  node when it falls out of the list.

No production code changes.
2026-09-11 01:39:13 +07:00
hell 95bfd42ff8 fix: four defects in the code that came with #150-#152
- app.py handed every /static response `max-age=15552000, immutable`, but
  only the assets built through static_version() carry ?v=<mtime>. The
  favicon, the icons, qrcode.min.js, searchable-select.js, the vendored
  CodeMirror bundles and ReDoc keep one URL forever, so a redeploy could not
  reach a browser that had already cached them. Immutable is now limited to
  fingerprinted URLs; the rest get an hour plus a revalidation.
- WireGuardManager._apply_bw_limits ran `docker cp ... && docker exec ...`
  through run_sudo_command, which only privileges the head of the chain, so
  the exec ran as the unprivileged SSH user. Wrapped in one `sh -c`, the
  pattern tests/test_sudo_command_chains.py enforces.
- toggleConnection() restored the row only in the error path. When
  loadConnections() fails after a successful toggle, the row it was going to
  rebuild stays disabled with a spinning bulb. Restore now runs in `finally`.
- The add-user modal starts on role "none", where the password is optional,
  but the label said "required" until the select fired its change event.

Tests: the bandwidth chain joins the sudo-chain suite, and a new
tests/test_static_cache.py pins both cache lifetimes.
2026-09-10 00:17:21 +07:00
hell 04c623cc87 fix: run privileged command chains inside one shell
`run_sudo_command("a && b")` builds `echo <pw> | sudo -S -p '' a && b`, so
sudo elevates only the first command of the chain. Everything after `&&` runs
as the unprivileged SSH user and fails on anything root-owned. Four call sites
were affected:

- backup_manager: restoring a protocol backup copies the archive out of
  /opt/amnezia/backups, which is root-owned, so the restore died with
  `cp: cannot stat '/opt/amnezia/backups/awg2/...': Permission denied` right
  after reporting that it had found the archive.
- app.py: the inline backup-download chain has the same shape.
- dns_manager: `docker network create` and the per-container attach loop -
  the attach silently never happened, so containers started before AmneziaDNS
  were left off amnezia-dns-net.
- adguard_manager `_ensure_network`: same.

Each chain now goes to one shell via `sh -c '<chain>'` with the arguments
quoted, so the whole chain runs under sudo. tests/test_sudo_command_chains.py
records the commands a fake SSH layer receives and asserts every privileged
chain is wrapped, plus a unit check on the helper itself, so a new bare `&&`
in these paths fails the suite.

The one error in the suite on this branch is the pre-existing
`import pytest` in tests/test_playwright_e2e.py, unrelated to this change
and fixed separately.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-08 22:07:55 +07:00