From 0d72579edd85e7241ac3ef890eda9986eb5d10bc Mon Sep 17 00:00:00 2001 From: infosave Date: Fri, 29 May 2026 12:12:55 +0300 Subject: [PATCH] fix(awg2): auto-detect wg/awg tool inside container (real cause of issue #50) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Live testing against an AmneziaWG 2.0 server revealed the actual root cause of "Failed to generate client keys": the official Amnezia container image ships the userspace tool only as `wg` (a patched AmneziaWG binary) and has NO `awg` binary, while the panel hardcoded `awg` for AWG2. `awg genkey` then failed with "sh: awg: not found". (amneziawg-go ships `awg` with `wg` symlinked, so both names work there — but the Amnezia image does not.) - generateClientKeys(): detect the tool inside the container (`command -v awg || command -v wg`) instead of hardcoding `awg`. - addClientToServer(): resolve the tool via new resolveWgTool() helper so `wg set` / `wg-quick up` (peer apply) also work on the Amnezia image. - executeServerCommand(): delegate to VpnServer::executeCommand so SSH key auth + docker sudo auto-detection apply to all 19 call sites (it was password-only before). Verified end-to-end on a live AWG2 server: pre-fix code fails with "Failed to generate client keys: sh: awg: not found"; fixed code creates the client, generates keys, and the peer appears in `wg show wg0`. Co-Authored-By: Claude Opus 4.8 (1M context) --- inc/VpnClient.php | 63 ++++++++++++++++++++++++++++++++++++----------- 1 file changed, 48 insertions(+), 15 deletions(-) diff --git a/inc/VpnClient.php b/inc/VpnClient.php index d94d093..043eca6 100644 --- a/inc/VpnClient.php +++ b/inc/VpnClient.php @@ -797,19 +797,20 @@ class VpnClient private static function generateClientKeys(array $serverData, string $clientName): array { $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'; - // 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 - ); + // Detect the WireGuard userspace tool INSIDE the container instead of + // hardcoding it. Different AWG2 images expose it under different names: + // the official Amnezia image ships only `wg` (a patched AmneziaWG binary), + // while amneziawg-go provides `awg` (with `wg` symlinked to it). Hardcoding + // `awg` made `awg genkey` fail with "awg: not found" on the Amnezia image, + // which is the actual cause of the "Failed to generate client keys" error + // in issue #50. Prefer `awg`, fall back to `wg`. + $script = 'set -e; umask 077; ' + . 'tool=$(command -v awg 2>/dev/null || command -v wg 2>/dev/null); ' + . '[ -n "$tool" ] || { echo no_wg_tool; exit 1; }; ' + . 'priv=$("$tool" genkey | tr -d "\r\n"); [ -n "$priv" ] || { echo empty_private_key; exit 1; }; ' + . 'pub=$(printf "%s\n" "$priv" | "$tool" pubkey | tr -d "\r\n"); [ -n "$pub" ] || { echo empty_public_key; exit 1; }; ' + . 'printf "%s\n---\n%s\n" "$priv" "$pub"'; $cmd = sprintf( 'docker exec -i %s sh -lc %s', @@ -1223,9 +1224,12 @@ class VpnClient throw new Exception('Refusing to add client with empty public key'); } - // Determine correct tool names (awg for AWG2, wg for standard) - $wgTool = $isAwg2 ? 'awg' : 'wg'; - $wgQuickTool = $isAwg2 ? 'awg-quick' : 'wg-quick'; + // Determine correct tool names by probing the container. The official + // Amnezia image exposes only `wg`/`wg-quick`; amneziawg-go provides + // `awg`/`awg-quick`. Hardcoding `awg` broke peer setup on the Amnezia + // image (issue #50). Prefer `awg`, fall back to `wg`. + $wgTool = $isAwg2 ? self::resolveWgTool($serverData, $containerName) : 'wg'; + $wgQuickTool = $wgTool . '-quick'; // 1. Create temp file for PSK (to avoid shell escaping issues) $pskFile = '/tmp/' . bin2hex(random_bytes(8)) . '.psk'; @@ -1302,11 +1306,40 @@ class VpnClient self::executeServerCommand($serverData, $updateCmd, true); } + /** + * Resolve the WireGuard userspace tool name available inside a container. + * Returns 'awg' when present, otherwise 'wg'. Used so AWG2 works on both the + * official Amnezia image (ships `wg`) and amneziawg-go (ships `awg`). + */ + private static function resolveWgTool(array $serverData, string $containerName): string + { + $probe = sprintf( + "docker exec -i %s sh -lc 'command -v awg >/dev/null 2>&1 && echo awg || echo wg'", + escapeshellarg($containerName) + ); + $tool = trim(self::executeServerCommand($serverData, $probe, true)); + return $tool === 'awg' ? 'awg' : 'wg'; + } + /** * Execute command on server */ private static function executeServerCommand(array $serverData, string $command, bool $sudo = false): string { + // Delegate to VpnServer::executeCommand so SSH key authentication, docker + // sudo auto-detection and retry logic are shared with the rest of the + // panel. The previous inline implementation was password-only and failed + // on key-based servers (contributing to issue #50). + if (!empty($serverData['id'])) { + try { + $server = new VpnServer((int) $serverData['id']); + return $server->executeCommand($command, $sudo ? null : false); + } catch (Exception $e) { + error_log('executeServerCommand: delegate failed, using legacy path: ' . $e->getMessage()); + } + } + + // Legacy fallback (no server id in $serverData): password-only SSH. $needsSudo = $sudo && strtolower((string) ($serverData['username'] ?? '')) !== 'root'; $baseCommand = $command;