fix(awg2): resolve issue #50 client key generation and install timeout

Issue #50 (AmneziaWG 2.0 / awg2): "Failed to generate client keys" when
creating clients, and "Invalid server response" on first install.

- VpnClient::generateClientKeys() built its own password-only SSH command
  (PubkeyAuthentication=no, no sudo), bypassing VpnServer::executeCommand.
  That broke key-based servers and hosts where docker requires sudo. Route
  it through executeCommand so SSH-key auth and docker sudo auto-detection
  apply, matching every other remote operation.
- VpnClient::getNextClientIP() read /opt/amnezia/awg/wg0.conf only; AWG2
  uses awg0.conf. Read awg0.conf first, fall back to wg0.conf.
- deploy route: lift PHP time limit (set_time_limit(0) + ignore_user_abort)
  so the multi-minute awg2 docker build is not killed mid-request, which
  produced the truncated, non-JSON "Invalid server response".
- migration 070: drop `--no-cache` from the awg2 docker build so layers are
  reused, making installs and retries fast and idempotent.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
infosave
2026-05-29 10:31:59 +03:00
parent 809b0ca63d
commit b819eb35b0
3 changed files with 50 additions and 13 deletions
+23 -13
View File
@@ -799,26 +799,33 @@ class VpnClient
$containerName = $serverData['container_name'];
$protocolSlug = (string) ($serverData['install_protocol'] ?? '');
$isAwg2 = (stripos($containerName, 'awg2') !== false || $protocolSlug === 'awg2');
// The amneziawg-go image ships `awg` and a `wg -> awg` symlink, so either
// tool works there. Use `awg` for AWG2 and `wg` otherwise.
$wgTool = $isAwg2 ? 'awg' : 'wg';
$cmd = sprintf(
"docker exec -i %s sh -lc 'set -e; umask 077; priv=\$(%s genkey | tr -d " . '"' . "\\r\\n" . '"' . "); [ -n \"\$priv\" ] || { echo empty_private_key; exit 1; }; pub=\$(printf " . '"' . "%%s\\n" . '"' . " \"\$priv\" | %s pubkey | tr -d " . '"' . "\\r\\n" . '"' . "); [ -n \"\$pub\" ] || { echo empty_public_key; exit 1; }; printf " . '"' . "%%s\\n---\\n%%s\\n" . '"' . " \"\$priv\" \"\$pub\"'",
escapeshellarg($containerName),
// Inner script that runs inside the container shell. Generates a private
// key, derives the public key and prints them separated by a "---" marker.
$script = sprintf(
'set -e; umask 077; priv=$(%s genkey | tr -d "\r\n"); [ -n "$priv" ] || { echo empty_private_key; exit 1; }; pub=$(printf "%%s\n" "$priv" | %s pubkey | tr -d "\r\n"); [ -n "$pub" ] || { echo empty_public_key; exit 1; }; printf "%%s\n---\n%%s\n" "$priv" "$pub"',
$wgTool,
$wgTool
);
$escaped = escapeshellarg($cmd);
$sshCmd = sprintf(
"sshpass -p %s ssh -p %d -q -o LogLevel=ERROR -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -o PreferredAuthentications=password -o PubkeyAuthentication=no %s@%s %s 2>&1",
escapeshellarg($serverData['password']),
$serverData['port'],
$serverData['username'],
$serverData['host'],
$escaped
$cmd = sprintf(
'docker exec -i %s sh -lc %s',
escapeshellarg($containerName),
escapeshellarg($script)
);
$out = (string) shell_exec($sshCmd);
// Route the command through VpnServer::executeCommand so that SSH key
// authentication and automatic docker sudo detection are handled the same
// way as every other remote operation. The previous implementation built
// its own password-only SSH command (PubkeyAuthentication=no, no sudo),
// which failed on key-based servers and on hosts where docker needs sudo,
// producing the "Failed to generate client keys" error (issue #50).
$server = new VpnServer((int) $serverData['id']);
$out = (string) $server->executeCommand($cmd); // null sudo => auto-detect for docker
$parts = explode("---", trim($out));
if (count($parts) < 2) {
@@ -860,8 +867,11 @@ class VpnClient
try {
$containerName = $serverData['container_name'] ?? 'amnezia-awg';
$server = new VpnServer($serverData['id']);
// AWG2 stores its config as awg0.conf (inside the container the path is
// always /opt/amnezia/awg/). Read awg0.conf first, then fall back to the
// legacy wg0.conf so externally created peers are still detected.
$cmd = sprintf(
"docker exec %s cat /opt/amnezia/awg/wg0.conf 2>/dev/null",
"docker exec %s sh -c 'cat /opt/amnezia/awg/awg0.conf 2>/dev/null; cat /opt/amnezia/awg/wg0.conf 2>/dev/null'",
escapeshellarg($containerName)
);
$serverConfig = $server->executeCommand($cmd, true);
@@ -0,0 +1,20 @@
-- =====================================================================
-- Migration 070: Speed up / stabilize AmneziaWG 2.0 (awg2) installation
--
-- Issue #50: the first install of awg2 frequently failed with
-- "Invalid server response". Root cause: the install script ran
-- `docker build --no-cache` every time, forcing a full recompile of the
-- amneziawg-go Go sources on each attempt. That build can take several
-- minutes, exceeding the web request timeout, so the browser received a
-- truncated (non-JSON) response. On retry the work from the first attempt
-- had already produced the image/config, so it "magically" succeeded.
--
-- Dropping `--no-cache` lets Docker reuse cached layers, making installs
-- (and especially retries) fast and idempotent. The sources are pinned via
-- `git clone --depth=1`, so a cached build is the desired behaviour.
-- =====================================================================
UPDATE protocols
SET install_script = REPLACE(install_script, 'docker build --no-cache -t amnezia-awg2', 'docker build -t amnezia-awg2')
WHERE slug = 'awg2'
AND install_script LIKE '%docker build --no-cache -t amnezia-awg2%';
+7
View File
@@ -568,6 +568,13 @@ Router::post('/servers/{id}/deploy', function ($params) {
requireAuth();
header('Content-Type: application/json');
// Some protocols (e.g. AmneziaWG 2.0 / awg2) build a Docker image from source
// on the remote host, which can take several minutes. Without lifting the PHP
// time limit the request is killed mid-build and the browser receives a
// truncated, non-JSON body shown as "Invalid server response" (issue #50).
@set_time_limit(0);
@ignore_user_abort(true);
$serverId = (int) $params['id'];
$rawBody = file_get_contents('php://input');
$options = [];