504 Commits
Author SHA1 Message Date
hellandClaude Opus 5 3803260905 fix(awg): run the host-tuning probe as a privileged script
get_host_tuning() passes a multi-line script to run_sudo_command(), which
builds `echo <pw> | sudo -S -p '' <script>`. Only the first line is elevated;
everything after the first newline runs as the unprivileged SSH user. The
`docker ps -a` loop is on line 8, so on hosts where the login user is not in
the docker group the container list comes back empty and the Host tuning modal
shows the host rows with no containers - no error, just missing data.

run_sudo_script() is what the rest of the file already uses for this (five
call sites in awg_manager alone): it uploads the script and runs the whole
thing under sudo. get_host_tuning was the one place that did not.

tests/test_host_tuning.py drives get_host_tuning through a fake SSH layer that
records which transport the caller used and fails if it is not the script one,
then checks the parsed host keys and the running/stopped container list.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-08 22:17:02 +07:00
hellandClaude Opus 5 3d827db346 fix: repair three defects that came in with v1.6.4
Review notes on the merge, all three in code that arrived from upstream:

app.py had the body of a second `protocol/rename` implementation sitting after
the `return` inside the wg-easy import handler's `except Exception`, followed
by a stray `except Exception` of its own. Unreachable, and it hid the real
control flow of the import endpoint - the surviving rename handler is the
earlier one, so nothing behaved differently, but the block is dead weight.
Removed.

That dead copy was the one that capped the name (`req.name.strip()[:64]`); the
live handler stores `req.name.strip()` as-is. The modal caps input at 64 chars,
so a direct API call was the only way in, and it parked an unbounded string in
data.json permanently. The cap is back as CUSTOM_PROTOCOL_NAME_MAX.

templates/server.html: saveRenameProtocol() restores `btn.textContent =
oldText` in its `finally`, but nothing declares oldText in that function - a
ReferenceError on every save, in both the success and the failure path. Five
other functions use the same pattern and declare it properly; this one never
touches the label at all, so the line is simply gone.

Verified against a running panel: POST protocol/rename with a 100-character
name now answers with 64, and both the rename and wgeasy endpoints are still
registered. 261 tests OK (1 skipped).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-08 22:13:41 +07:00
hellandClaude Opus 5 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
hellandClaude Opus 5 e537823184 fix(tests): skip the Playwright e2e module when pytest is missing
tests/test_playwright_e2e.py starts with a bare `import pytest`, but pytest is
not in requirements.txt - only playwright is. CI installs requirements.txt and
runs `python -m unittest discover -s tests`, which imports every module under
tests/, so collection aborts on that import and the whole suite fails.

`main` has been red since the module landed: the runs for #134, the v1.6.4 tag
and the follow-up commit all report `FAILED (errors=1)` with
`ModuleNotFoundError: No module named 'pytest'`, and none of the 152 real
unit tests are reported.

The import is now guarded and raises unittest.SkipTest, so `unittest discover`
skips the module and keeps running the rest. Nothing changes for pytest users:
with pytest installed the import succeeds and `pytest tests/test_playwright_e2e.py`
behaves exactly as before. The module docstring now also says it expects a
panel already serving on BASE_URL.

An alternative would be adding pytest to requirements.txt, but that pulls a
test runner into the runtime dependencies of the panel, and `unittest discover`
still would not run these tests - they are pytest fixtures and plain classes,
not unittest.TestCase.

Before: Ran 152 tests ... FAILED (errors=1)
After:  Ran 152 tests ... OK (skipped=1)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-08 22:05:33 +07:00
Public relations & Virtual technology 002380de35 Update app.py v1.6.4 2026-09-08 13:49:22 +03:00
Максимов Владимир Сергеевич 91c8d7ad45 v1.6.4 2026-09-08 13:48:41 +03:00
Public relations & Virtual technology da8fce7abc Merge pull request #134 from leonidorlov-hash/feat/wgeasy-import-upstream
feat: one-click migration from wg-easy / amnezia-wg-easy panels
2026-09-08 13:10:25 +03:00
Public relations & Virtual technology eb403b1a2e Merge branch 'main' into feat/wgeasy-import-upstream 2026-09-08 13:10:15 +03:00
Public relations & Virtual technology c427f9e9e3 Merge pull request #141 from leonidorlov-hash/fix/disabled-ip-reservation
fix(ipam): keep disabled clients' IPs reserved; validate on re-enable
2026-09-08 12:59:26 +03:00
Public relations & Virtual technology baf4b77c17 Merge pull request #137 from leonidorlov-hash/fix/rename-save-feedback
fix(ui): busy feedback and local cancel in connection rename editor
2026-09-08 12:58:51 +03:00
Public relations & Virtual technology 5bf293450b Merge pull request #136 from leonidorlov-hash/feat/protocol-logos
feat(ui): protocol logos and custom instance names
2026-09-08 12:58:11 +03:00
Public relations & Virtual technology e70c822503 Merge branch 'main' into feat/protocol-logos 2026-09-08 12:56:28 +03:00
Public relations & Virtual technology 11291c2226 Merge pull request #135 from vikbaranov/feature/pwa
feat(ui): design system and mobile layout overhaul
2026-09-08 12:55:04 +03:00
leonidorlov-hash 22dc4f1042 fix(ipam): tolerate missing clientsTable, fail only on broken container
cat exits non-zero both when docker exec fails and when the table file
does not exist yet (fresh instance). Distinguish via 'docker exec true':
raise only when the container itself is unusable; an absent table simply
means no reservations yet. Fixes the test regression in CI
2026-09-07 21:29:21 +03:00
leonidorlov-hash f836d6d6b2 fix(ipam): tolerate missing clientsTable, fail only on broken container
cat exits non-zero both when docker exec fails and when the table file
does not exist yet (fresh instance). Distinguish via 'docker exec true':
raise only when the container itself is unusable; an absent table simply
means no reservations yet. Fixes the test regression in CI
2026-09-07 21:29:18 +03:00
leonidorlov-hash 728dd5bfac fix(ipam): keep disabled clients' IPs reserved; validate on re-enable
Closes the conflict scenario from #138: disabling a client removed its
[Peer] from the active config while its IP stayed in clientsTable, so
the allocator (which read only the active config) could hand that IP to
a newly created client; re-enabling the original client then activated
two peers with the same address.

AWG (all versions) and WireGuard:
- allocation now counts reservations from clientsTable (disabled clients
  included) in addition to the active config; only deletion frees an IP
- re-enabling validates that the stored IP is not owned by another
  client (table) and not present in the active config; on conflict the
  operation is rejected with a clear error
- if clientsTable cannot be read, allocation fails loudly instead of
  treating the reservation pool as empty

WireGuard additionally derives the subnet from the live server config
instead of the built-in default, so imported instances with custom
subnets allocate correct addresses.

Xray needs no change: clients are identified by UUID, no IP allocation.

Fixes #138
2026-09-07 20:35:19 +03:00
leonidorlov-hash 21a29217ce fix(ipam): keep disabled clients' IPs reserved; validate on re-enable
Closes the conflict scenario from #138: disabling a client removed its
[Peer] from the active config while its IP stayed in clientsTable, so
the allocator (which read only the active config) could hand that IP to
a newly created client; re-enabling the original client then activated
two peers with the same address.

AWG (all versions) and WireGuard:
- allocation now counts reservations from clientsTable (disabled clients
  included) in addition to the active config; only deletion frees an IP
- re-enabling validates that the stored IP is not owned by another
  client (table) and not present in the active config; on conflict the
  operation is rejected with a clear error
- if clientsTable cannot be read, allocation fails loudly instead of
  treating the reservation pool as empty

WireGuard additionally derives the subnet from the live server config
instead of the built-in default, so imported instances with custom
subnets allocate correct addresses.

Xray needs no change: clients are identified by UUID, no IP allocation.

Fixes #138
2026-09-07 20:35:07 +03:00
leonidorlov-hash 8791c7e080 fix(import): import WgEasyError before try block
An exception raised before the inner import (e.g. SSH connect failure)
crashed the handler with UnboundLocalError and returned a plain-text 500
instead of JSON
2026-09-06 20:12:08 +03:00
leonidorlov-hash 5413b2fffa fix(import): import WgEasyError before try block
An exception raised before the inner import (e.g. SSH connect failure)
crashed the handler with UnboundLocalError and returned a plain-text 500
instead of JSON
2026-09-06 20:12:05 +03:00
leonidorlov-hash 85f7517176 fix(import): match source container by web port
With several wg-easy containers on one host, detect_source picked the
first running one - mixing the client list from the requested panel
with the server identity of a different container. Now the container
whose published TCP port matches the authenticated web panel wins
2026-09-06 18:50:01 +03:00
leonidorlov-hash d693140181 fix(import): match source container by web port
With several wg-easy containers on one host, detect_source picked the
first running one - mixing the client list from the requested panel
with the server identity of a different container. Now the container
whose published TCP port matches the authenticated web panel wins
2026-09-06 18:49:20 +03:00
leonidorlov-hash 2ced8dc7ee feat(import): multi-instance import + visible import log
- When AWG 2.0 is already installed on the server, wg-easy import now
  creates the next free instance (awg2__2, amnezia-awg2-2 container)
  instead of failing with a bare 400
- Import progress and errors are shown in a log pane inside the modal;
  the modal no longer closes silently on failure, and stays open on
  success until the user closes it (page reloads to show the instance)
- apiCall keeps the error payload so the UI can render the log
2026-09-06 17:50:14 +03:00
leonidorlov-hash 2a56bc05b8 feat(import): multi-instance import + visible import log
- When AWG 2.0 is already installed on the server, wg-easy import now
  creates the next free instance (awg2__2, amnezia-awg2-2 container)
  instead of failing with a bare 400
- Import progress and errors are shown in a log pane inside the modal;
  the modal no longer closes silently on failure, and stays open on
  success until the user closes it (page reloads to show the instance)
- apiCall keeps the error payload so the UI can render the log
2026-09-06 16:36:15 +03:00
leonidorlov-hash 8de6086c45 fix(ui): busy feedback and local cancel in connection rename editor
- Save click disables both buttons and inputs and swaps the checkmark
  for a spinner while the request is in flight (rename + tc apply
  takes a few seconds), making impatient double-clicks impossible
- Cancel / Escape restores the original row in place instead of
  reloading the whole connections list
- Buttons are restored on error so the editor stays usable
2026-09-06 15:42:02 +03:00
leonidorlov-hash 4869454eaf fix(ui): darker spinner accent on secondary save button 2026-09-06 15:31:48 +03:00
leonidorlov-hash e83e454a04 fix(ui): fully inline spinner styles in rename editors
Self-contained border/radius/animation so the indicator renders even if
the shared .btn .spinner rule is overridden or not applied
2026-09-06 15:16:56 +03:00
leonidorlov-hash 4411f9768a fix(ui): local cancel for rename editors, spinner inside save button
- connection rename: Cancel/Escape restores the original row in place
  instead of reloading the whole connections list
- save buttons (connection rename, protocol rename) show a spinning
  indicator inside the button while the request is in flight
- protocol rename modal locks its input during the request
2026-09-06 15:06:20 +03:00
leonidorlov-hash 429849ca24 fix(ui): instant busy feedback on connection rename/speed save
Buttons and inputs disable on click and the save button shows a spinner
while the request is in flight (rename + tc apply takes 2-4s), so
impatient double-clicks are impossible
2026-09-06 14:59:57 +03:00
leonidorlov-hash 23933d88d7 feat(wireguard): per-peer bandwidth limits (tc), same as AWG
- iproute2 added to the WG image; older containers get it installed on
  the fly when a limit is applied (no rebuild needed)
- bwlimits flat file + HTB/policer via tc on wg0, re-applied at boot
  by the start script and on every clientsTable save
- set_speed_limit(client_id, max_speed) mirrors the AWG API, so the
  rename/speed popover now offers a limit for WireGuard clients too
2026-09-06 14:19:01 +03:00
leonidorlov-hash 266f23d743 feat(ui): protocol logos and custom instance names
- WireGuard, AmneziaWG (all variants) and Xray cards show real logos
  (bundled in static/img) instead of emoji, on brand-colored tiles:
  WireGuard #871719, AmneziaWG #303030, Xray #010001
- installed instances can be renamed via a small pencil inside the
  instance name (fades in on hover); the custom name is stored per
  protocol in the server record (custom_name) and shown on the card
  title and in all instance pickers; empty input restores the default
2026-09-05 17:23:59 +03:00
Viktor Baranov 60b97304d7 Revert "chore: remove playwright from requirements"
This reverts commit f1477ad865.
2026-09-05 17:11:12 +03:00
Viktor Baranov f1477ad865 chore: remove playwright from requirements 2026-09-05 17:08:02 +03:00
Viktor Baranov 30156c5829 feat(ui): design system and mobile layout overhaul 2026-09-05 16:55:33 +03:00
leonidorlov-hash d8384d957f fix(ui): brand-colored logo tiles, pencil rename next to instance name
- logo tile backgrounds: WireGuard #871719, AmneziaWG #303030, Xray #010001
- rename control moved from the actions row to a small SVG pencil inside
  .protocol-name; hidden by default, fades in at 25% opacity on name hover,
  full opacity on its own hover (same 0.2s ease as buttons)
2026-09-05 16:43:02 +03:00
Public relations & Virtual technology c3410b9116 Merge pull request #133 from leonidorlov-hash/fix/kmod-failure-reason
fix(awg): report the actual reason when kernel module setup fails
2026-09-05 14:47:01 +03:00
Public relations & Virtual technology d33a4f73ae Merge pull request #132 from leonidorlov-hash/fix/ssh-pool-lock
fix(ssh): serialize command/SFTP execution on pooled connections
2026-09-05 14:46:40 +03:00
Public relations & Virtual technology 92df6ee9e3 Merge pull request #131 from leonidorlov-hash/fix/docker-ipv6-no-daemon-restart
fix(awg): never restart Docker daemon implicitly when enabling IPv6
2026-09-05 14:44:47 +03:00
leonidorlov-hash ee0966b9f5 feat(ui): protocol logos and custom instance names
- WireGuard, AmneziaWG (all variants) and Xray cards now show real
  logos (static/img) instead of emoji
- installed instances can be renamed via a pencil button on the card;
  the custom name is stored per protocol in the server record and
  shown on the card title and in all instance pickers
2026-09-05 13:51:25 +03:00
leonidorlov-hash 41351067a0 feat(wgeasy-import): auto-target AWG 2.0 when source has obfuscation
w0rng/amnezia-wg-easy sources carry AmneziaWG obfuscation params
(Jc/Jmin/Jmax/S1-S4/H1-H4) in the [Interface] section. Importing such
clients into a plain WireGuard instance would break their configs, so
the importer now reads the source wg0.conf from the container, detects
the obfuscation and installs an AWG 2.0 instance with the exact same
params instead. Plain wg-easy sources still land on WireGuard.

Preview now reports the detected listen port, obfuscation presence and
the resolved target protocol.
2026-09-05 02:55:14 +03:00
leonidorlov-hash eb12bf138b feat: import clients from wg-easy / amnezia-wg-easy panels
Adds an 'Import from wg-easy' button on the server page. The panel
fetches the client list from a running wg-easy (v14 / w0rng fork) or
wg-easy v15 instance through the server's own SSH connection (web API
on 127.0.0.1, nothing needs to be exposed), shows a preview with
selectable clients, and on confirmation:

- stops the old wg-easy container (not deleted)
- installs the panel-managed WireGuard instance on the same UDP port
- restores the original server key, subnet and all client keys/PSKs/IPs,
  so existing client configs keep working unchanged
- writes a backup copy of wg0.json to /root on the server

The WG container start script now accepts a custom subnet so NAT rules
match the imported network instead of the panel default 10.8.2.0/24.
2026-09-05 02:55:14 +03:00
leonidorlov-hash e6f76d5bb2 feat(awg): report the actual reason when kernel module setup fails
The DKMS script used 'set -e' and surfaced a bare 'build failed' on any
error. Now each step (headers install, repo clone, dkms add/build/
install, modprobe) reports a specific reason, e.g. 'headers for the
running kernel 6.1.0-9-amd64 are no longer in the repos - upgrade the
kernel and reboot', and the reason is shown in the install log.
2026-09-05 02:49:04 +03:00
leonidorlov-hash fb8d5f9ac3 feat(awg): report the actual reason when kernel module setup fails
The DKMS script used 'set -e' and surfaced a bare 'build failed' on any
error. Now each step (headers install, repo clone, dkms add/build/
install, modprobe) reports a specific reason, e.g. 'headers for the
running kernel 6.1.0-9-amd64 are no longer in the repos - upgrade the
kernel and reboot', and the reason is shown in the install log.
2026-09-05 02:46:10 +03:00
leonidorlov-hash fe3793749c feat(wgeasy-import): auto-target AWG 2.0 when source has obfuscation
w0rng/amnezia-wg-easy sources carry AmneziaWG obfuscation params
(Jc/Jmin/Jmax/S1-S4/H1-H4) in the [Interface] section. Importing such
clients into a plain WireGuard instance would break their configs, so
the importer now reads the source wg0.conf from the container, detects
the obfuscation and installs an AWG 2.0 instance with the exact same
params instead. Plain wg-easy sources still land on WireGuard.

Preview now reports the detected listen port, obfuscation presence and
the resolved target protocol.
2026-09-05 02:32:21 +03:00
leonidorlov-hash 19f3bb1d66 feat: import clients from wg-easy / amnezia-wg-easy panels
Adds an 'Import from wg-easy' button on the server page. The panel
fetches the client list from a running wg-easy (v14 / w0rng fork) or
wg-easy v15 instance through the server's own SSH connection (web API
on 127.0.0.1, nothing needs to be exposed), shows a preview with
selectable clients, and on confirmation:

- stops the old wg-easy container (not deleted)
- installs the panel-managed WireGuard instance on the same UDP port
- restores the original server key, subnet and all client keys/PSKs/IPs,
  so existing client configs keep working unchanged
- writes a backup copy of wg0.json to /root on the server

The WG container start script now accepts a custom subnet so NAT rules
match the imported network instead of the panel default 10.8.2.0/24.
2026-09-05 01:29:56 +03:00
leonidorlov-hash 0fed74d995 Merge fix/docker-ipv6-no-daemon-restart + fix/ssh-pool-lock into deploy/v160 2026-09-05 00:22:22 +03:00
leonidorlov-hash e9536a775a fix(ssh): serialize command/SFTP execution on pooled connections (#125)
The pooled SSHManager is shared between request handlers and background
threads (traffic sync, conn monitor). When one side's exec failed, its
reconnect closed the shared client while the other side was mid-command:
'NoneType open_session', EOFError in open_sftp, a burst of 500s, then
30s of fast-fail from the connect cooldown.

Add an RLock around exec_command and all SFTP operations (RLock because
run_command retries by re-entering itself). Lock ordering is
exec -> conn, connect/disconnect never take the exec lock, so no
deadlock. Failed execs now serialize the reconnect too: other threads
simply wait and then run on the fresh transport.
2026-09-05 00:13:43 +03:00
leonidorlov-hash 50e6288b17 fix(awg): never restart Docker daemon implicitly when enabling IPv6 (#124)
prepare_host() merged ipv6/ip6tables keys into /etc/docker/daemon.json and
ran 'systemctl restart docker' mid-install. That killed every container
on the host (running tunnels dropped ~20s) and, when the panel itself
runs in Docker on the managed host, killed the panel together with its
own install request.

Now the daemon restart happens only when daemon.json actually changed
AND no amnezia-* or panel containers are running. Otherwise the restart
is deferred and the install log warns the admin that tunnels stay
IPv4-only until 'systemctl restart docker' is run manually. A successful
restart is also logged.
2026-09-04 22:45:53 +03:00
leonidorlov-hash d616f1cd52 Merge remote-tracking branch 'upstream/main' into deploy/v160 2026-09-04 19:35:49 +03:00
Public relations & Virtual technology 5a74091b19 change version 1.6.3 app.py v1.6.3 2026-09-04 17:24:51 +03:00
Public relations & Virtual technology fce3d49ef1 Merge pull request #129 from vikbaranov/fix/telegram_admin_connection_creation
fix(telegram): allow admins to create connection in telegram
2026-09-04 16:33:52 +03:00