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>
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>
`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>
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>
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
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
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
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
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
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
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
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
- 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
- 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
- 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
- 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
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
- 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
- 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
- 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)
- 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
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.
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.
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.
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.
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.
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.
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.
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.