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:
+23
-13
@@ -799,26 +799,33 @@ class VpnClient
|
|||||||
$containerName = $serverData['container_name'];
|
$containerName = $serverData['container_name'];
|
||||||
$protocolSlug = (string) ($serverData['install_protocol'] ?? '');
|
$protocolSlug = (string) ($serverData['install_protocol'] ?? '');
|
||||||
$isAwg2 = (stripos($containerName, 'awg2') !== false || $protocolSlug === 'awg2');
|
$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';
|
$wgTool = $isAwg2 ? 'awg' : 'wg';
|
||||||
|
|
||||||
$cmd = sprintf(
|
// Inner script that runs inside the container shell. Generates a private
|
||||||
"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\"'",
|
// key, derives the public key and prints them separated by a "---" marker.
|
||||||
escapeshellarg($containerName),
|
$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,
|
||||||
$wgTool
|
$wgTool
|
||||||
);
|
);
|
||||||
|
|
||||||
$escaped = escapeshellarg($cmd);
|
$cmd = sprintf(
|
||||||
$sshCmd = sprintf(
|
'docker exec -i %s sh -lc %s',
|
||||||
"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($containerName),
|
||||||
escapeshellarg($serverData['password']),
|
escapeshellarg($script)
|
||||||
$serverData['port'],
|
|
||||||
$serverData['username'],
|
|
||||||
$serverData['host'],
|
|
||||||
$escaped
|
|
||||||
);
|
);
|
||||||
|
|
||||||
$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));
|
$parts = explode("---", trim($out));
|
||||||
|
|
||||||
if (count($parts) < 2) {
|
if (count($parts) < 2) {
|
||||||
@@ -860,8 +867,11 @@ class VpnClient
|
|||||||
try {
|
try {
|
||||||
$containerName = $serverData['container_name'] ?? 'amnezia-awg';
|
$containerName = $serverData['container_name'] ?? 'amnezia-awg';
|
||||||
$server = new VpnServer($serverData['id']);
|
$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(
|
$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)
|
escapeshellarg($containerName)
|
||||||
);
|
);
|
||||||
$serverConfig = $server->executeCommand($cmd, true);
|
$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%';
|
||||||
@@ -568,6 +568,13 @@ Router::post('/servers/{id}/deploy', function ($params) {
|
|||||||
requireAuth();
|
requireAuth();
|
||||||
header('Content-Type: application/json');
|
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'];
|
$serverId = (int) $params['id'];
|
||||||
$rawBody = file_get_contents('php://input');
|
$rawBody = file_get_contents('php://input');
|
||||||
$options = [];
|
$options = [];
|
||||||
|
|||||||
Reference in New Issue
Block a user