fix(awg2): auto-detect wg/awg tool inside container (real cause of issue #50)

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) <noreply@anthropic.com>
This commit is contained in:
infosave
2026-05-29 12:12:55 +03:00
parent b819eb35b0
commit 0d72579edd
+48 -15
View File
@@ -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;