feat: add multi-protocol support for AWG2 with dynamic tool selection and configuration path resolution

This commit is contained in:
infosave2007
2026-04-23 18:00:09 +03:00
parent e2767b3af2
commit 6006628f64
4 changed files with 232 additions and 72 deletions
+108 -49
View File
@@ -427,14 +427,16 @@ class InstallProtocolManager
{ {
$metadata = $protocol['definition']['metadata'] ?? []; $metadata = $protocol['definition']['metadata'] ?? [];
$serverData = $server->getData(); $serverData = $server->getData();
$containerName = $serverData['container_name'] ?? ($metadata['container_name'] ?? 'amnezia-awg'); // For multi-protocol servers, use container_name from protocol metadata first
// (vpn_servers.container_name stores the primary protocol's container, e.g. 'aivpn-server')
$containerName = $metadata['container_name'] ?? ($serverData['container_name'] ?? 'amnezia-awg');
$containerFilter = escapeshellarg('^' . $containerName . '$'); $containerFilter = escapeshellarg('^' . $containerName . '$');
$containerArg = escapeshellarg($containerName); $containerArg = escapeshellarg($containerName);
// Для AWG2 конфигурация внутри контейнера находится в /opt/amnezia/awg/awg0.conf // AWG2 uses awg0.conf (standard, same as native Amnezia app)
// На хосте может быть /opt/amnezia/awg2/wg0.conf (монтируется как /opt/amnezia/awg внутри контейнера) // Old AWG uses wg0.conf
$isAwg2 = (stripos($containerName, 'awg2') !== false || ($protocol['slug'] ?? '') === 'awg2'); $isAwg2 = (stripos($containerName, 'awg2') !== false || ($protocol['slug'] ?? '') === 'awg2');
$configDir = $isAwg2 ? '/opt/amnezia/awg' : '/opt/amnezia/awg'; $configDir = '/opt/amnezia/awg';
$configFile = $isAwg2 ? 'awg0.conf' : 'wg0.conf'; $configFile = $isAwg2 ? 'awg0.conf' : 'wg0.conf';
$containerListRaw = trim($server->executeCommand("docker ps -a --filter name={$containerFilter} --format '{{.Names}}'", true)); $containerListRaw = trim($server->executeCommand("docker ps -a --filter name={$containerFilter} --format '{{.Names}}'", true));
@@ -466,13 +468,18 @@ class InstallProtocolManager
$containerState = trim($server->executeCommand("docker inspect --format '{{.State.Status}}' {$containerArg}", true)); $containerState = trim($server->executeCommand("docker inspect --format '{{.State.Status}}' {$containerArg}", true));
// Для AWG2 проверяем оба возможных имени файла конфигурации // AWG2: try awg0.conf first (standard), fall back to wg0.conf (legacy panel installs)
$configFile = ($protocol['slug'] ?? '') === 'awg2' ? 'awg0.conf' : 'wg0.conf'; $configFile = $isAwg2 ? 'awg0.conf' : 'wg0.conf';
$wgConfig = $server->executeCommand("docker exec -i {$containerArg} cat {$configDir}/{$configFile} 2>/dev/null", true); $wgConfig = $server->executeCommand("docker exec -i {$containerArg} cat {$configDir}/{$configFile} 2>/dev/null", true);
if (trim($wgConfig) === '') { if ($isAwg2 && (trim($wgConfig) === '' || strpos($wgConfig, '[Interface]') === false)) {
// Fallback to wg0.conf for legacy panel installs
$configFile = 'wg0.conf';
$wgConfig = $server->executeCommand("docker exec -i {$containerArg} cat {$configDir}/{$configFile} 2>/dev/null", true);
}
if (trim($wgConfig) === '' || strpos($wgConfig, '[Interface]') === false) {
return [ return [
'status' => 'partial', 'status' => 'partial',
'message' => "Контейнер найден, но конфигурация {$configFile} отсутствует", 'message' => "Контейнер найден, но конфигурация wg0.conf/awg0.conf отсутствует",
'details' => [ 'details' => [
'container_name' => $containerName, 'container_name' => $containerName,
'container_status' => $containerState, 'container_status' => $containerState,
@@ -532,11 +539,18 @@ class InstallProtocolManager
$containerName = $details['container_name'] ?? ($protocol['definition']['metadata']['container_name'] ?? 'amnezia-awg'); $containerName = $details['container_name'] ?? ($protocol['definition']['metadata']['container_name'] ?? 'amnezia-awg');
$containerArg = escapeshellarg($containerName); $containerArg = escapeshellarg($containerName);
// Для AWG2 конфигурация внутри контейнера находится в /opt/amnezia/awg/awg0.conf // Config is always wg0.conf — container CMD runs: awg-quick up /opt/amnezia/awg/wg0.conf
// На хосте может быть /opt/amnezia/awg2/wg0.conf (монтируется как /opt/amnezia/awg внутри контейнера)
$isAwg2 = (stripos($containerName, 'awg2') !== false || ($protocol['slug'] ?? '') === 'awg2'); $isAwg2 = (stripos($containerName, 'awg2') !== false || ($protocol['slug'] ?? '') === 'awg2');
$configDir = '/opt/amnezia/awg'; // Внутри контейнера всегда /opt/amnezia/awg $configDir = '/opt/amnezia/awg';
// AWG2: try awg0.conf first (standard), fall back to wg0.conf (legacy)
$configFile = $isAwg2 ? 'awg0.conf' : 'wg0.conf'; $configFile = $isAwg2 ? 'awg0.conf' : 'wg0.conf';
$testConf = trim($server->executeCommand("docker exec -i {$containerArg} cat {$configDir}/{$configFile} 2>/dev/null", true));
if ($isAwg2 && ($testConf === '' || strpos($testConf, '[Interface]') === false)) {
$configFile = 'wg0.conf';
}
// Determine interface name from config filename (wg0.conf -> wg0, awg0.conf -> awg0)
$ifaceName = str_replace('.conf', '', $configFile);
// Try to ensure container is running and wg is up // Try to ensure container is running and wg is up
$server->executeCommand("docker start {$containerArg} 2>/dev/null || true", true); $server->executeCommand("docker start {$containerArg} 2>/dev/null || true", true);
@@ -544,40 +558,64 @@ class InstallProtocolManager
$server->executeCommand("docker exec -i {$containerArg} wg-quick up {$configDir}/{$configFile} 2>/dev/null || true", true); $server->executeCommand("docker exec -i {$containerArg} wg-quick up {$configDir}/{$configFile} 2>/dev/null || true", true);
$pdo = DB::conn(); $pdo = DB::conn();
$stmt = $pdo->prepare(' $serverData = $server->getData();
UPDATE vpn_servers $serverId = $server->getId();
SET vpn_port = ?,
server_public_key = ?,
preshared_key = ?,
awg_params = ?,
status = ?,
error_message = NULL,
deployed_at = COALESCE(deployed_at, NOW()),
install_protocol = ?
WHERE id = ?
');
$stmt->execute([
$details['vpn_port'] ?? null,
$details['server_public_key'] ?? null,
$details['preshared_key'] ?? null,
isset($details['awg_params']) ? json_encode($details['awg_params']) : null,
'active',
$protocol['slug'] ?? ($isAwg2 ? 'awg2' : 'amnezia-wg'),
$server->getId()
]);
// Add entry to server_protocols table so protocol shows in installed list
$protocolId = self::resolveProtocolId($protocol); $protocolId = self::resolveProtocolId($protocol);
if ($protocolId) { $protocolSlug = $protocol['slug'] ?? ($isAwg2 ? 'awg2' : 'amnezia-wg');
// Check if server already has another primary protocol installed
$existingProtocol = $serverData['install_protocol'] ?? '';
$isSecondaryProtocol = ($existingProtocol !== '' && $existingProtocol !== $protocolSlug);
if (!$isSecondaryProtocol) {
// Primary protocol — update vpn_servers
$stmt = $pdo->prepare(' $stmt = $pdo->prepare('
INSERT INTO server_protocols (server_id, protocol_id, applied_at) UPDATE vpn_servers
VALUES (?, ?, NOW()) SET vpn_port = ?,
ON DUPLICATE KEY UPDATE applied_at = NOW() server_public_key = ?,
preshared_key = ?,
awg_params = ?,
status = ?,
error_message = NULL,
deployed_at = COALESCE(deployed_at, NOW()),
install_protocol = ?
WHERE id = ?
'); ');
$stmt->execute([ $stmt->execute([
$server->getId(), $details['vpn_port'] ?? null,
$protocolId $details['server_public_key'] ?? null,
$details['preshared_key'] ?? null,
isset($details['awg_params']) ? json_encode($details['awg_params']) : null,
'active',
$protocolSlug,
$serverId
]); ]);
} else {
// Secondary protocol — only ensure server is active, don't overwrite primary protocol data
$stmt = $pdo->prepare('UPDATE vpn_servers SET status = ?, error_message = NULL WHERE id = ?');
$stmt->execute(['active', $serverId]);
}
// Store protocol-specific config in server_protocols (works for both primary and secondary)
if ($protocolId) {
$configData = json_encode([
'server_host' => $serverData['ip_address'] ?? $serverData['hostname'] ?? null,
'server_port' => $details['vpn_port'] ?? null,
'extras' => [
'vpn_port' => $details['vpn_port'] ?? null,
'vpn_subnet' => $details['vpn_subnet'] ?? '10.8.1.0/24',
'server_public_key' => $details['server_public_key'] ?? null,
'preshared_key' => $details['preshared_key'] ?? null,
'awg_params' => $details['awg_params'] ?? null,
'container_name' => $containerName,
],
]);
$stmt = $pdo->prepare('
INSERT INTO server_protocols (server_id, protocol_id, config_data, applied_at, created_at)
VALUES (?, ?, ?, NOW(), NOW())
ON DUPLICATE KEY UPDATE config_data = VALUES(config_data), applied_at = NOW()
');
$stmt->execute([$serverId, $protocolId, $configData]);
} }
$server->refresh(); $server->refresh();
@@ -646,18 +684,28 @@ class InstallProtocolManager
// Use existing keys and config // Use existing keys and config
} else { } else {
// Generate new key pair for this client // Generate new key pair for this client
$newPrivateKey = trim($server->executeCommand("docker exec -i {$containerArg} /usr/bin/awg genkey 2>/dev/null", true)); // Use awg for AWG2, wg for standard
$newPublicKey = trim($server->executeCommand("docker exec -i {$containerArg} sh -c 'echo \"'\"$newPrivateKey\"'\" | /usr/bin/awg pubkey' 2>/dev/null", true)); $keyTool = $isAwg2 ? 'awg' : 'wg';
$newPrivateKey = trim($server->executeCommand("docker exec {$containerArg} {$keyTool} genkey", true));
if (!empty($newPrivateKey)) {
$escapedKey = escapeshellarg($newPrivateKey);
$newPublicKey = trim($server->executeCommand("docker exec {$containerArg} sh -c 'echo {$escapedKey} | {$keyTool} pubkey'", true));
} else {
$newPublicKey = '';
}
if (!empty($newPrivateKey) && !empty($newPublicKey)) { Logger::appendInstall($serverId, "Restore: keygen for {$clientIp}: privkey_len=" . strlen($newPrivateKey) . " pubkey_len=" . strlen($newPublicKey));
if (!empty($newPrivateKey) && !empty($newPublicKey) && strlen($newPublicKey) >= 40) {
$privateKey = $newPrivateKey; $privateKey = $newPrivateKey;
$protocolSlug = $protocol['slug'] ?? ''; $protocolSlug = $protocol['slug'] ?? '';
$serverHost = $serverData['host'] ?? $serverData['ip_address'] ?? $serverData['hostname'] ?? '';
$config = VpnClient::buildClientConfig( $config = VpnClient::buildClientConfig(
$privateKey, $privateKey,
$clientIp, $clientIp,
$details['server_public_key'] ?? '', $details['server_public_key'] ?? '',
$details['preshared_key'] ?? '', $details['preshared_key'] ?? '',
$serverData['ip_address'] ?? $serverData['hostname'] ?? '', $serverHost,
$details['vpn_port'] ?? 51820, $details['vpn_port'] ?? 51820,
$details['awg_params'] ?? [], $details['awg_params'] ?? [],
$protocolSlug $protocolSlug
@@ -667,6 +715,8 @@ class InstallProtocolManager
// Mark that we need to update server config with new public key // Mark that we need to update server config with new public key
$needsServerConfigUpdate = true; $needsServerConfigUpdate = true;
$keyUpdates[] = ['old' => $pub, 'new' => $newPublicKey]; $keyUpdates[] = ['old' => $pub, 'new' => $newPublicKey];
} else {
Logger::appendInstall($serverId, "Restore: WARNING keygen failed for {$clientIp}, keeping original public key");
} }
} }
@@ -693,7 +743,7 @@ class InstallProtocolManager
if ($needsServerConfigUpdate && !empty($keyUpdates)) { if ($needsServerConfigUpdate && !empty($keyUpdates)) {
Logger::appendInstall($serverId, "Restore: updating server config with " . count($keyUpdates) . " new public keys"); Logger::appendInstall($serverId, "Restore: updating server config with " . count($keyUpdates) . " new public keys");
// Update awg0.conf - replace old public keys with new ones // Update wg0.conf - replace old public keys with new ones
$updatedConfig = $wgConfig; $updatedConfig = $wgConfig;
foreach ($keyUpdates as $update) { foreach ($keyUpdates as $update) {
// Escape special characters for regex // Escape special characters for regex
@@ -1403,6 +1453,7 @@ class InstallProtocolManager
if ($isAwg) { if ($isAwg) {
$detection = self::detectBuiltinAwg($server, $protocol); $detection = self::detectBuiltinAwg($server, $protocol);
Logger::appendInstall($serverId, 'AWG detect result: status=' . ($detection['status'] ?? 'null') . ' message=' . ($detection['message'] ?? 'none'));
if (($detection['status'] ?? '') === 'existing') { if (($detection['status'] ?? '') === 'existing') {
Logger::appendInstall($serverId, 'Existing AWG installation detected, restoring instead of reinstalling'); Logger::appendInstall($serverId, 'Existing AWG installation detected, restoring instead of reinstalling');
$restoreResult = self::restoreBuiltinAwg($server, $protocol, $detection, $options); $restoreResult = self::restoreBuiltinAwg($server, $protocol, $detection, $options);
@@ -2104,11 +2155,15 @@ class InstallProtocolManager
$serverData = $server->getData(); $serverData = $server->getData();
$containerName = $serverData['container_name'] ?? 'amnezia-awg'; $containerName = $serverData['container_name'] ?? 'amnezia-awg';
// Для AWG2 конфигурация внутри контейнера находится в /opt/amnezia/awg/awg0.conf // AWG2: try awg0.conf first (standard), fall back to wg0.conf (legacy)
$isAwg2 = (stripos($containerName, 'awg2') !== false || ($protocol['slug'] ?? '') === 'awg2'); $isAwg2 = (stripos($containerName, 'awg2') !== false || ($protocol['slug'] ?? '') === 'awg2');
$configDir = '/opt/amnezia/awg'; // Внутри контейнера всегда /opt/amnezia/awg $configDir = '/opt/amnezia/awg';
$configFile = $isAwg2 ? 'awg0.conf' : 'wg0.conf'; $configFile = $isAwg2 ? 'awg0.conf' : 'wg0.conf';
$conf = $server->executeCommand("docker exec -i $containerName cat {$configDir}/{$configFile}", true); $conf = $server->executeCommand("docker exec -i $containerName cat {$configDir}/{$configFile}", true);
if ($isAwg2 && (!$conf || strpos($conf, '[Interface]') === false)) {
$configFile = 'wg0.conf';
$conf = $server->executeCommand("docker exec -i $containerName cat {$configDir}/{$configFile}", true);
}
if (!$conf) if (!$conf)
return; return;
@@ -2454,11 +2509,15 @@ class InstallProtocolManager
$serverData = $server->getData(); $serverData = $server->getData();
$pid = self::resolveProtocolId($protocol); $pid = self::resolveProtocolId($protocol);
// Для AWG2 конфигурация внутри контейнера находится в /opt/amnezia/awg/awg0.conf // AWG2: try awg0.conf first (standard), fall back to wg0.conf (legacy)
$isAwg2 = (stripos($containerName, 'awg2') !== false || ($protocol['slug'] ?? '') === 'awg2'); $isAwg2 = (stripos($containerName, 'awg2') !== false || ($protocol['slug'] ?? '') === 'awg2');
$configDir = '/opt/amnezia/awg'; // Внутри контейнера всегда /opt/amnezia/awg $configDir = '/opt/amnezia/awg';
$configFile = $isAwg2 ? 'awg0.conf' : 'wg0.conf'; $configFile = $isAwg2 ? 'awg0.conf' : 'wg0.conf';
$wgConfig = $server->executeCommand("docker exec -i {$containerArg} cat {$configDir}/{$configFile} 2>/dev/null", true); $wgConfig = $server->executeCommand("docker exec -i {$containerArg} cat {$configDir}/{$configFile} 2>/dev/null", true);
if ($isAwg2 && (trim($wgConfig) === '' || strpos($wgConfig, '[Interface]') === false)) {
$configFile = 'wg0.conf';
$wgConfig = $server->executeCommand("docker exec -i {$containerArg} cat {$configDir}/{$configFile} 2>/dev/null", true);
}
$tableRaw = $server->executeCommand("docker exec -i {$containerArg} cat {$configDir}/clientsTable 2>/dev/null", true); $tableRaw = $server->executeCommand("docker exec -i {$containerArg} cat {$configDir}/clientsTable 2>/dev/null", true);
$clientsTable = json_decode(trim($tableRaw), true); $clientsTable = json_decode(trim($tableRaw), true);
+100 -17
View File
@@ -100,6 +100,61 @@ class VpnClient
} }
} }
// For multi-protocol setups, override server data with protocol-specific settings
// (subnet, keys, port, AWG params) from server_protocols.config_data or protocol metadata
if ($protocolId) {
try {
$stmtSp = $pdo->prepare('SELECT config_data FROM server_protocols WHERE server_id = ? AND protocol_id = ? LIMIT 1');
$stmtSp->execute([$serverId, $protocolId]);
$spConfigRaw = $stmtSp->fetchColumn();
if ($spConfigRaw) {
$spConfig = is_string($spConfigRaw) ? json_decode($spConfigRaw, true) : $spConfigRaw;
if (is_array($spConfig)) {
$spExtras = $spConfig['extras'] ?? [];
// If extras has 'result' subarray, merge it
if (isset($spExtras['result']) && is_array($spExtras['result'])) {
$spExtras = array_merge($spExtras, $spExtras['result']);
}
// Override server data with protocol-specific values
if (!empty($spExtras['server_public_key'])) {
$serverData['server_public_key'] = $spExtras['server_public_key'];
}
if (!empty($spExtras['preshared_key'])) {
$serverData['preshared_key'] = $spExtras['preshared_key'];
}
if (!empty($spExtras['vpn_port'])) {
$serverData['vpn_port'] = $spExtras['vpn_port'];
}
if (!empty($spConfig['server_port'])) {
$serverData['vpn_port'] = $spConfig['server_port'];
}
// Override AWG params from protocol config
// AWG params can be at extras level (Jc, S1, etc.) or nested in extras.awg_params
$awgOverride = [];
$awgSource = $spExtras;
if (isset($spExtras['awg_params']) && is_array($spExtras['awg_params'])) {
$awgSource = array_merge($awgSource, $spExtras['awg_params']);
}
foreach (['Jc', 'Jmin', 'Jmax', 'S1', 'S2', 'S3', 'S4', 'H1', 'H2', 'H3', 'H4', 'I1', 'I2', 'I3', 'I4', 'I5'] as $ak) {
if (isset($awgSource[$ak]) && $awgSource[$ak] !== '' && $awgSource[$ak] !== null) {
$awgOverride[$ak] = $awgSource[$ak];
}
}
if (!empty($awgOverride)) {
$serverData['awg_params'] = json_encode($awgOverride);
}
}
}
} catch (Exception $e) {
error_log('Failed to load protocol config_data: ' . $e->getMessage());
}
// Override vpn_subnet from protocol definition metadata (e.g. AWG2 uses 10.8.1.0/24)
if (!empty($protoMetadata['vpn_subnet'])) {
$serverData['vpn_subnet'] = $protoMetadata['vpn_subnet'];
}
}
$clientIP = self::getNextClientIP($serverData); $clientIP = self::getNextClientIP($serverData);
$loginBase = $login !== null && $login !== '' ? $login : $name; $loginBase = $login !== null && $login !== '' ? $login : $name;
$loginBase = str_replace(' ', '_', trim($loginBase)); $loginBase = str_replace(' ', '_', trim($loginBase));
@@ -742,9 +797,15 @@ class VpnClient
private static function generateClientKeys(array $serverData, string $clientName): array private static function generateClientKeys(array $serverData, string $clientName): array
{ {
$containerName = $serverData['container_name']; $containerName = $serverData['container_name'];
$protocolSlug = (string) ($serverData['install_protocol'] ?? '');
$isAwg2 = (stripos($containerName, 'awg2') !== false || $protocolSlug === 'awg2');
$wgTool = $isAwg2 ? 'awg' : 'wg';
$cmd = sprintf( $cmd = sprintf(
"docker exec -i %s sh -lc 'set -e; umask 077; priv=\$(wg genkey | tr -d " . '"' . "\\r\\n" . '"' . "); [ -n \"\$priv\" ] || { echo empty_private_key; exit 1; }; pub=\$(printf " . '"' . "%%s\\n" . '"' . " \"\$priv\" | wg pubkey | tr -d " . '"' . "\\r\\n" . '"' . "); [ -n \"\$pub\" ] || { echo empty_public_key; exit 1; }; printf " . '"' . "%%s\\n---\\n%%s\\n" . '"' . " \"\$priv\" \"\$pub\"'", "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) escapeshellarg($containerName),
$wgTool,
$wgTool
); );
$escaped = escapeshellarg($cmd); $escaped = escapeshellarg($cmd);
@@ -1133,10 +1194,18 @@ class VpnClient
{ {
$containerName = $serverData['container_name']; $containerName = $serverData['container_name'];
$protocolSlug = (string) ($serverData['install_protocol'] ?? ''); $protocolSlug = (string) ($serverData['install_protocol'] ?? '');
// Для AWG2 конфигурация внутри контейнера находится в /opt/amnezia/awg/awg0.conf
$isAwg2 = (stripos($containerName, 'awg2') !== false || $protocolSlug === 'awg2'); $isAwg2 = (stripos($containerName, 'awg2') !== false || $protocolSlug === 'awg2');
$configDir = '/opt/amnezia/awg'; // Внутри контейнера всегда /opt/amnezia/awg $configDir = '/opt/amnezia/awg';
// AWG2: try awg0.conf first (standard), fall back to wg0.conf (legacy panel installs)
$configFile = $isAwg2 ? 'awg0.conf' : 'wg0.conf'; $configFile = $isAwg2 ? 'awg0.conf' : 'wg0.conf';
$testConf = trim(self::executeServerCommand($serverData, "docker exec -i {$containerName} cat {$configDir}/{$configFile} 2>/dev/null", true));
if ($isAwg2 && ($testConf === '' || strpos($testConf, '[Interface]') === false)) {
$configFile = 'wg0.conf';
}
// Interface name matches config filename (wg0.conf -> wg0, awg0.conf -> awg0)
$ifaceName = str_replace('.conf', '', $configFile);
$presharedKey = $serverData['preshared_key']; $presharedKey = $serverData['preshared_key'];
$publicKey = trim($publicKey); $publicKey = trim($publicKey);
@@ -1144,16 +1213,21 @@ class VpnClient
throw new Exception('Refusing to add client with empty public key'); 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';
// 1. Create temp file for PSK (to avoid shell escaping issues) // 1. Create temp file for PSK (to avoid shell escaping issues)
$pskFile = '/tmp/' . bin2hex(random_bytes(8)) . '.psk'; $pskFile = '/tmp/' . bin2hex(random_bytes(8)) . '.psk';
$cmd1 = sprintf("docker exec -i %s sh -c 'echo \"%s\" > %s'", $containerName, $presharedKey, $pskFile); $cmd1 = sprintf("docker exec -i %s sh -c 'echo \"%s\" > %s'", $containerName, $presharedKey, $pskFile);
self::executeServerCommand($serverData, $cmd1, true); self::executeServerCommand($serverData, $cmd1, true);
// 2. Add peer using wg set // 2. Add peer using wg/awg set
// wg set wg0 peer <PUBKEY> preshared-key <FILE> allowed-ips <IPS>
$cmd2 = sprintf( $cmd2 = sprintf(
"docker exec -i %s wg set wg0 peer %s preshared-key %s allowed-ips %s/32", "docker exec -i %s %s set %s peer %s preshared-key %s allowed-ips %s/32",
$containerName, $containerName,
$wgTool,
$ifaceName,
escapeshellarg($publicKey), escapeshellarg($publicKey),
$pskFile, $pskFile,
$clientIP $clientIP
@@ -1164,14 +1238,13 @@ class VpnClient
$cmd3 = sprintf("docker exec -i %s rm -f %s", $containerName, $pskFile); $cmd3 = sprintf("docker exec -i %s rm -f %s", $containerName, $pskFile);
self::executeServerCommand($serverData, $cmd3, true); self::executeServerCommand($serverData, $cmd3, true);
// 4. Persist to wg0.conf (append) // 4. Persist to config file (append)
$peerBlock = "\n[Peer]\n"; $peerBlock = "\n[Peer]\n";
$peerBlock .= "PublicKey = {$publicKey}\n"; $peerBlock .= "PublicKey = {$publicKey}\n";
$peerBlock .= "PresharedKey = {$presharedKey}\n"; $peerBlock .= "PresharedKey = {$presharedKey}\n";
$peerBlock .= "AllowedIPs = {$clientIP}/32\n"; $peerBlock .= "AllowedIPs = {$clientIP}/32\n";
$escapedBlock = addslashes($peerBlock); $escapedBlock = addslashes($peerBlock);
$configFile = (stripos($containerName, 'awg2') !== false || $protocolSlug === 'awg2') ? 'awg0.conf' : 'wg0.conf';
$cmd4 = sprintf("docker exec -i %s sh -c 'echo \"%s\" >> %s/%s'", $containerName, $escapedBlock, $configDir, $configFile); $cmd4 = sprintf("docker exec -i %s sh -c 'echo \"%s\" >> %s/%s'", $containerName, $escapedBlock, $configDir, $configFile);
self::executeServerCommand($serverData, $cmd4, true); self::executeServerCommand($serverData, $cmd4, true);
@@ -1180,7 +1253,7 @@ class VpnClient
// 6. CRITICAL: Reload WG interface to apply AWG obfuscation params // 6. CRITICAL: Reload WG interface to apply AWG obfuscation params
// Without this, the interface uses standard WireGuard without Jc/S1/S2/H1-H4 // Without this, the interface uses standard WireGuard without Jc/S1/S2/H1-H4
$cmd5 = sprintf("docker exec -i %s sh -c 'ip link del wg0 2>/dev/null || true; wg-quick up %s/%s 2>&1'", $containerName, $configDir, $configFile); $cmd5 = sprintf("docker exec -i %s sh -c 'ip link del %s 2>/dev/null || true; %s up %s/%s 2>&1'", $containerName, $ifaceName, $wgQuickTool, $configDir, $configFile);
self::executeServerCommand($serverData, $cmd5, true); self::executeServerCommand($serverData, $cmd5, true);
} }
@@ -1467,16 +1540,25 @@ class VpnClient
{ {
$containerName = $serverData['container_name']; $containerName = $serverData['container_name'];
$protocolSlug = (string) ($serverData['install_protocol'] ?? ''); $protocolSlug = (string) ($serverData['install_protocol'] ?? '');
// Для AWG2 конфигурация внутри контейнера находится в /opt/amnezia/awg/ // Config dir inside container is always /opt/amnezia/awg
$configDir = '/opt/amnezia/awg'; // Внутри контейнера всегда /opt/amnezia/awg $configDir = '/opt/amnezia/awg';
// Determine config filename // AWG2: try awg0.conf first (standard), fall back to wg0.conf (legacy panel installs)
$configFile = (stripos($containerName, 'awg2') !== false || $protocolSlug === 'awg2') ? 'awg0.conf' : 'wg0.conf'; $isAwg2 = (stripos($containerName, 'awg2') !== false || $protocolSlug === 'awg2');
$configFile = $isAwg2 ? 'awg0.conf' : 'wg0.conf';
$testConf = trim(self::executeServerCommand($serverData, "docker exec -i {$containerName} cat {$configDir}/{$configFile} 2>/dev/null", true));
if ($isAwg2 && ($testConf === '' || strpos($testConf, '[Interface]') === false)) {
$configFile = 'wg0.conf';
}
$ifaceName = str_replace('.conf', '', $configFile);
$wgTool = $isAwg2 ? 'awg' : 'wg';
// First, remove using wg command (live removal) // First, remove using wg/awg command (live removal)
$removeCmd = sprintf( $removeCmd = sprintf(
"docker exec -i %s wg set wg0 peer %s remove", "docker exec -i %s %s set %s peer %s remove",
$containerName, $containerName,
$wgTool,
$ifaceName,
escapeshellarg($publicKey) escapeshellarg($publicKey)
); );
@@ -1503,7 +1585,8 @@ class VpnClient
self::executeServerCommand($serverData, $writeCmd, true); self::executeServerCommand($serverData, $writeCmd, true);
// Save config // Save config
$saveCmd = sprintf("docker exec -i %s wg-quick save wg0", $containerName); $wgQuickTool = $isAwg2 ? 'awg-quick' : 'wg-quick';
$saveCmd = sprintf("docker exec -i %s %s save %s", $containerName, $wgQuickTool, $ifaceName);
self::executeServerCommand($serverData, $saveCmd, true); self::executeServerCommand($serverData, $saveCmd, true);
// Remove from clientsTable // Remove from clientsTable
@@ -51,7 +51,7 @@ docker build --no-cache -t amnezia-awg2 /opt/amnezia/awg2/src
EXISTING=$(docker ps -aq -f "name=$CONTAINER_NAME" 2>/dev/null | head -1) EXISTING=$(docker ps -aq -f "name=$CONTAINER_NAME" 2>/dev/null | head -1)
if [ -z "$EXISTING" ]; then if [ -z "$EXISTING" ]; then
docker run -d --name "$CONTAINER_NAME" --restart always --cap-add=NET_ADMIN --device /dev/net/tun -p "${VPN_PORT}:${VPN_PORT}/udp" -v /opt/amnezia/awg2:/opt/amnezia/awg amnezia-awg2 sh -c "while [ ! -f /opt/amnezia/awg/wg0.conf ]; do sleep 1; done; WG_QUICK_USERSPACE_IMPLEMENTATION=amneziawg-go awg-quick up /opt/amnezia/awg/wg0.conf && sleep infinity" docker run -d --name "$CONTAINER_NAME" --restart always --cap-add=NET_ADMIN --device /dev/net/tun -p "${VPN_PORT}:${VPN_PORT}/udp" -v /opt/amnezia/awg2:/opt/amnezia/awg amnezia-awg2 sh -c "while [ ! -f /opt/amnezia/awg/awg0.conf ]; do sleep 1; done; WG_QUICK_USERSPACE_IMPLEMENTATION=amneziawg-go awg-quick up /opt/amnezia/awg/awg0.conf && sleep infinity"
sleep 2 sleep 2
else else
STATUS=$(docker inspect --format="{{.State.Status}}" "$CONTAINER_NAME" 2>/dev/null || echo "") STATUS=$(docker inspect --format="{{.State.Status}}" "$CONTAINER_NAME" 2>/dev/null || echo "")
@@ -59,12 +59,28 @@ else
docker start "$CONTAINER_NAME" >/dev/null 2>&1 || true docker start "$CONTAINER_NAME" >/dev/null 2>&1 || true
fi fi
fi fi
# Check for existing config: first on host, then inside container (native Amnezia app installs)
CONF_FILE="/opt/amnezia/awg2/awg0.conf"
if [ ! -f "$CONF_FILE" ]; then
# Try to extract config from inside the container (native Amnezia app stores config without volume mount)
CONTAINER_CONF=$(docker exec "$CONTAINER_NAME" cat /opt/amnezia/awg/awg0.conf 2>/dev/null || true)
if [ -n "$CONTAINER_CONF" ] && echo "$CONTAINER_CONF" | grep -q "\\[Interface\\]"; then
mkdir -p /opt/amnezia/awg2
echo "$CONTAINER_CONF" > /opt/amnezia/awg2/awg0.conf
# Also extract keys if available
docker exec "$CONTAINER_NAME" cat /opt/amnezia/awg/wireguard_server_private_key.key > /opt/amnezia/awg2/wireguard_server_private_key.key 2>/dev/null || true
docker exec "$CONTAINER_NAME" cat /opt/amnezia/awg/wireguard_server_public_key.key > /opt/amnezia/awg2/wireguard_server_public_key.key 2>/dev/null || true
docker exec "$CONTAINER_NAME" cat /opt/amnezia/awg/wireguard_psk.key > /opt/amnezia/awg2/wireguard_psk.key 2>/dev/null || true
docker exec "$CONTAINER_NAME" cat /opt/amnezia/awg/clientsTable > /opt/amnezia/awg2/clientsTable 2>/dev/null || true
echo "Extracted existing config from container"
fi
fi
if [ -f /opt/amnezia/awg2/wg0.conf ]; then if [ -f /opt/amnezia/awg2/awg0.conf ]; then
PORT=$(grep -E "^ListenPort" /opt/amnezia/awg2/wg0.conf | cut -d= -f2 | tr -d "[:space:]") PORT=$(grep -E "^ListenPort" /opt/amnezia/awg2/awg0.conf | cut -d= -f2 | tr -d "[:space:]")
PSK=$(cat /opt/amnezia/awg2/wireguard_psk.key 2>/dev/null || true) PSK=$(cat /opt/amnezia/awg2/wireguard_psk.key 2>/dev/null || true)
if [ -z "$PSK" ]; then if [ -z "$PSK" ]; then
PSK=$(grep -E "^PresharedKey" /opt/amnezia/awg2/wg0.conf | cut -d= -f2 | tr -d "[:space:]") PSK=$(grep -E "^PresharedKey" /opt/amnezia/awg2/awg0.conf | head -1 | cut -d= -f2 | tr -d "[:space:]")
fi fi
PUBKEY=$(cat /opt/amnezia/awg2/wireguard_server_public_key.key 2>/dev/null || true) PUBKEY=$(cat /opt/amnezia/awg2/wireguard_server_public_key.key 2>/dev/null || true)
if [ -z "$PUBKEY" ]; then if [ -z "$PUBKEY" ]; then
@@ -83,7 +99,7 @@ if [ -f /opt/amnezia/awg2/wg0.conf ]; then
echo "Server Host: $EXTERNAL_IP" echo "Server Host: $EXTERNAL_IP"
for P in Jc Jmin Jmax S1 S2 S3 S4 H1 H2 H3 H4 I1 I2 I3 I4 I5; do for P in Jc Jmin Jmax S1 S2 S3 S4 H1 H2 H3 H4 I1 I2 I3 I4 I5; do
VAL=$(sed -n -E "s/^[[:space:]]*$P[[:space:]]*=[[:space:]]*//p" /opt/amnezia/awg2/wg0.conf | head -1 | tr -d "\r") VAL=$(sed -n -E "s/^[[:space:]]*$P[[:space:]]*=[[:space:]]*//p" /opt/amnezia/awg2/awg0.conf | head -1 | tr -d "\\r")
if [ -n "$VAL" ] || [[ "$P" =~ ^I[2-5]$ ]]; then echo "Variable: $P=$VAL"; fi if [ -n "$VAL" ] || [[ "$P" =~ ^I[2-5]$ ]]; then echo "Variable: $P=$VAL"; fi
done done
echo "Variable: dns_servers=1.1.1.1, 1.0.0.1" echo "Variable: dns_servers=1.1.1.1, 1.0.0.1"
@@ -130,7 +146,7 @@ echo "H4 = $H4_VAL"
echo "I1 = $I1_VAL" echo "I1 = $I1_VAL"
echo "PostUp = iptables -A FORWARD -i %i -j ACCEPT; iptables -A FORWARD -o %i -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE" echo "PostUp = iptables -A FORWARD -i %i -j ACCEPT; iptables -A FORWARD -o %i -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE"
echo "PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -D FORWARD -o %i -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE" echo "PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -D FORWARD -o %i -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE"
} > /opt/amnezia/awg2/wg0.conf } > /opt/amnezia/awg2/awg0.conf
echo "$PRIVATE_KEY" > /opt/amnezia/awg2/wireguard_server_private_key.key echo "$PRIVATE_KEY" > /opt/amnezia/awg2/wireguard_server_private_key.key
echo "$PUBLIC_KEY" > /opt/amnezia/awg2/wireguard_server_public_key.key echo "$PUBLIC_KEY" > /opt/amnezia/awg2/wireguard_server_public_key.key
+2
View File
@@ -58,8 +58,10 @@
<h3 class="font-bold mb-4">{{ t('servers.server_info') }}</h3> <h3 class="font-bold mb-4">{{ t('servers.server_info') }}</h3>
<dl class="space-y-2"> <dl class="space-y-2">
<div><dt class="text-sm text-gray-600">{{ t('common.status') }}</dt><dd><span class="px-2 py-1 bg-green-100 text-green-800 rounded text-sm">{{ server.status }}</span></dd></div> <div><dt class="text-sm text-gray-600">{{ t('common.status') }}</dt><dd><span class="px-2 py-1 bg-green-100 text-green-800 rounded text-sm">{{ server.status }}</span></dd></div>
{% if server_protocols|length <= 1 %}
<div><dt class="text-sm text-gray-600">VPN Port</dt><dd>{{ server.vpn_port }}</dd></div> <div><dt class="text-sm text-gray-600">VPN Port</dt><dd>{{ server.vpn_port }}</dd></div>
<div><dt class="text-sm text-gray-600">Subnet</dt><dd>{{ server.vpn_subnet }}</dd></div> <div><dt class="text-sm text-gray-600">Subnet</dt><dd>{{ server.vpn_subnet }}</dd></div>
{% endif %}
</dl> </dl>