feat: Enhance protocol detection and restoration for AWG and X-Ray installations
This commit is contained in:
+449
-16
@@ -238,6 +238,18 @@ class InstallProtocolManager
|
|||||||
return self::detectBuiltinAwg($server, $protocol);
|
return self::detectBuiltinAwg($server, $protocol);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$slug = $protocol['slug'] ?? '';
|
||||||
|
|
||||||
|
// For AWG shell-based scenarios (amnezia-wg, amnezia-wg-advanced), use builtin AWG detection
|
||||||
|
if (self::isAwgProtocol($slug, $protocol)) {
|
||||||
|
return self::detectBuiltinAwg($server, $protocol);
|
||||||
|
}
|
||||||
|
|
||||||
|
// For X-Ray VLESS, use builtin detection
|
||||||
|
if ($slug === 'xray-vless') {
|
||||||
|
return self::detectBuiltinXray($server, $protocol);
|
||||||
|
}
|
||||||
|
|
||||||
return self::runScript($server, $protocol, 'detect', $options);
|
return self::runScript($server, $protocol, 'detect', $options);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -308,6 +320,18 @@ class InstallProtocolManager
|
|||||||
return self::restoreBuiltinAwg($server, $protocol, $detection, $options);
|
return self::restoreBuiltinAwg($server, $protocol, $detection, $options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$slug = $protocol['slug'] ?? '';
|
||||||
|
|
||||||
|
// For AWG shell-based scenarios, use builtin AWG restore
|
||||||
|
if (self::isAwgProtocol($slug, $protocol)) {
|
||||||
|
return self::restoreBuiltinAwg($server, $protocol, $detection, $options);
|
||||||
|
}
|
||||||
|
|
||||||
|
// For X-Ray VLESS, use builtin restore
|
||||||
|
if ($slug === 'xray-vless') {
|
||||||
|
return self::restoreBuiltinXray($server, $protocol, $detection, $options);
|
||||||
|
}
|
||||||
|
|
||||||
$result = self::runScript($server, $protocol, 'restore', array_merge($options, [
|
$result = self::runScript($server, $protocol, 'restore', array_merge($options, [
|
||||||
'detection' => $detection
|
'detection' => $detection
|
||||||
]));
|
]));
|
||||||
@@ -477,7 +501,7 @@ class InstallProtocolManager
|
|||||||
'',
|
'',
|
||||||
$details['preshared_key'] ?? null,
|
$details['preshared_key'] ?? null,
|
||||||
'',
|
'',
|
||||||
'disabled'
|
'active' // Import as active since they already work on the server
|
||||||
]);
|
]);
|
||||||
$restored++;
|
$restored++;
|
||||||
}
|
}
|
||||||
@@ -867,6 +891,22 @@ class InstallProtocolManager
|
|||||||
return $definition['engine'] ?? 'builtin_awg';
|
return $definition['engine'] ?? 'builtin_awg';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a protocol is an AWG variant (by slug or install_script content)
|
||||||
|
* Used to route shell-based AWG scenarios to builtin AWG detection/restore
|
||||||
|
*/
|
||||||
|
private static function isAwgProtocol(string $slug, array $protocol): bool
|
||||||
|
{
|
||||||
|
if (in_array($slug, ['amnezia-wg', 'amnezia-wg-advanced'], true)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
$installScript = (string) ($protocol['install_script'] ?? '');
|
||||||
|
if ($installScript !== '' && preg_match('/amneziavpn\/amnezia-wg|amnezia\/awg|amnezia-awg/i', $installScript)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
private static function fallbackProtocols(): array
|
private static function fallbackProtocols(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
@@ -940,6 +980,18 @@ class InstallProtocolManager
|
|||||||
return self::detectBuiltinAwg($server, $protocol);
|
return self::detectBuiltinAwg($server, $protocol);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$slug = $protocol['slug'] ?? '';
|
||||||
|
|
||||||
|
// For AWG shell-based scenarios (amnezia-wg, amnezia-wg-advanced), use builtin AWG detection
|
||||||
|
if (self::isAwgProtocol($slug, $protocol)) {
|
||||||
|
return self::detectBuiltinAwg($server, $protocol);
|
||||||
|
}
|
||||||
|
|
||||||
|
// For X-Ray VLESS, use builtin detection
|
||||||
|
if ($slug === 'xray-vless') {
|
||||||
|
return self::detectBuiltinXray($server, $protocol);
|
||||||
|
}
|
||||||
|
|
||||||
return self::runScript($server, $protocol, 'detect', $options);
|
return self::runScript($server, $protocol, 'detect', $options);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -956,9 +1008,7 @@ class InstallProtocolManager
|
|||||||
|
|
||||||
// For script-driven protocols, try to detect AWG scenario and fallback to builtin uninstall
|
// For script-driven protocols, try to detect AWG scenario and fallback to builtin uninstall
|
||||||
$slug = $protocol['slug'] ?? '';
|
$slug = $protocol['slug'] ?? '';
|
||||||
$installScript = (string) ($protocol['install_script'] ?? '');
|
if (self::isAwgProtocol($slug, $protocol)) {
|
||||||
$looksLikeAwg = (bool) preg_match('/amneziavpn\/amnezia-wg|amnezia\/awg|amnezia-awg/i', $installScript);
|
|
||||||
if (in_array($slug, ['amnezia-wg-advanced', 'amnezia-wg'], true) || $looksLikeAwg) {
|
|
||||||
// Prefer builtin AWG uninstall by default because script variants may have CRLF issues
|
// Prefer builtin AWG uninstall by default because script variants may have CRLF issues
|
||||||
// or leave behind the canonical container name, causing install conflicts.
|
// or leave behind the canonical container name, causing install conflicts.
|
||||||
if (!empty($options['use_script_uninstall'])) {
|
if (!empty($options['use_script_uninstall'])) {
|
||||||
@@ -1027,16 +1077,56 @@ class InstallProtocolManager
|
|||||||
$serverId = $server->getId();
|
$serverId = $server->getId();
|
||||||
try {
|
try {
|
||||||
Logger::appendInstall($serverId, 'Activate start for ' . ($protocol['slug'] ?? 'unknown') . ' engine ' . $engine);
|
Logger::appendInstall($serverId, 'Activate start for ' . ($protocol['slug'] ?? 'unknown') . ' engine ' . $engine);
|
||||||
|
|
||||||
|
// ── Check for existing installation before doing anything destructive ──
|
||||||
|
$slug = $protocol['slug'] ?? '';
|
||||||
|
$isAwg = $engine === 'builtin_awg' || self::isAwgProtocol($slug, $protocol);
|
||||||
|
$isXray = $slug === 'xray-vless';
|
||||||
|
|
||||||
|
if ($isAwg) {
|
||||||
|
$detection = self::detectBuiltinAwg($server, $protocol);
|
||||||
|
if (in_array($detection['status'] ?? '', ['existing', 'partial'], true)) {
|
||||||
|
Logger::appendInstall($serverId, 'Existing AWG installation detected, restoring instead of reinstalling');
|
||||||
|
$restoreResult = self::restoreBuiltinAwg($server, $protocol, $detection, $options);
|
||||||
|
// Import existing clients into DB
|
||||||
|
self::importExistingAwgClients($server, $protocol, $detection);
|
||||||
|
$pdo = DB::conn();
|
||||||
|
$pid = self::resolveProtocolId($protocol);
|
||||||
|
if ($pid) {
|
||||||
|
$details = $detection['details'] ?? [];
|
||||||
|
$config = [
|
||||||
|
'server_host' => $server->getData()['host'] ?? null,
|
||||||
|
'server_port' => $details['vpn_port'] ?? null,
|
||||||
|
'extras' => [
|
||||||
|
'vpn_port' => $details['vpn_port'] ?? null,
|
||||||
|
'server_public_key' => $details['server_public_key'] ?? null,
|
||||||
|
'preshared_key' => $details['preshared_key'] ?? null,
|
||||||
|
'awg_params' => $details['awg_params'] ?? null,
|
||||||
|
]
|
||||||
|
];
|
||||||
|
$stmt2 = $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()');
|
||||||
|
$stmt2->execute([$serverId, $pid, json_encode($config)]);
|
||||||
|
}
|
||||||
|
return array_merge($restoreResult, ['mode' => 'restore_existing']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($isXray) {
|
||||||
|
$xrayDetection = self::detectBuiltinXray($server, $protocol);
|
||||||
|
if (in_array($xrayDetection['status'] ?? '', ['existing', 'partial'], true)) {
|
||||||
|
Logger::appendInstall($serverId, 'Existing X-Ray installation detected, restoring instead of reinstalling');
|
||||||
|
$restoreResult = self::restoreBuiltinXray($server, $protocol, $xrayDetection, $options);
|
||||||
|
return array_merge($restoreResult, ['mode' => 'restore_existing']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── No existing installation found — proceed with fresh install ──
|
||||||
|
|
||||||
if ($engine === 'builtin_awg') {
|
if ($engine === 'builtin_awg') {
|
||||||
$res = $server->runAwgInstall($options);
|
$res = $server->runAwgInstall($options);
|
||||||
Logger::appendInstall($serverId, 'Builtin AWG install finished');
|
Logger::appendInstall($serverId, 'Builtin AWG install finished');
|
||||||
$pdo = DB::conn();
|
$pdo = DB::conn();
|
||||||
$pid = (int) ($protocol['id'] ?? 0);
|
$pid = self::resolveProtocolId($protocol);
|
||||||
if (!$pid) {
|
|
||||||
$stmt = $pdo->prepare('SELECT id FROM protocols WHERE slug = ? LIMIT 1');
|
|
||||||
$stmt->execute([$protocol['slug'] ?? self::DEFAULT_SLUG]);
|
|
||||||
$pid = (int) $stmt->fetchColumn();
|
|
||||||
}
|
|
||||||
if ($pid) {
|
if ($pid) {
|
||||||
$config = [
|
$config = [
|
||||||
'server_host' => $server->getData()['host'] ?? null,
|
'server_host' => $server->getData()['host'] ?? null,
|
||||||
@@ -1145,12 +1235,7 @@ class InstallProtocolManager
|
|||||||
}
|
}
|
||||||
Logger::appendInstall($serverId, 'Scripted install parsed port ' . ($port ?? 0) . ' password ' . ($password ?? ''));
|
Logger::appendInstall($serverId, 'Scripted install parsed port ' . ($port ?? 0) . ' password ' . ($password ?? ''));
|
||||||
$pdo = DB::conn();
|
$pdo = DB::conn();
|
||||||
$pid = (int) ($protocol['id'] ?? 0);
|
$pid = self::resolveProtocolId($protocol);
|
||||||
if (!$pid) {
|
|
||||||
$stmt = $pdo->prepare('SELECT id FROM protocols WHERE slug = ? LIMIT 1');
|
|
||||||
$stmt->execute([$protocol['slug'] ?? '']);
|
|
||||||
$pid = (int) $stmt->fetchColumn();
|
|
||||||
}
|
|
||||||
if ($pid) {
|
if ($pid) {
|
||||||
$config = [
|
$config = [
|
||||||
'server_host' => $server->getData()['host'] ?? null,
|
'server_host' => $server->getData()['host'] ?? null,
|
||||||
@@ -1439,4 +1524,352 @@ class InstallProtocolManager
|
|||||||
$server->executeCommand("docker exec -i $containerName wg-quick up wg0", true);
|
$server->executeCommand("docker exec -i $containerName wg-quick up wg0", true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve protocol ID from protocol array, looking up by slug if needed
|
||||||
|
*/
|
||||||
|
private static function resolveProtocolId(array $protocol): int
|
||||||
|
{
|
||||||
|
$pid = (int) ($protocol['id'] ?? 0);
|
||||||
|
if (!$pid) {
|
||||||
|
$slug = $protocol['slug'] ?? '';
|
||||||
|
if ($slug === '') {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
$pdo = DB::conn();
|
||||||
|
$stmt = $pdo->prepare('SELECT id FROM protocols WHERE slug = ? LIMIT 1');
|
||||||
|
$stmt->execute([$slug]);
|
||||||
|
$pid = (int) $stmt->fetchColumn();
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $pid;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detect existing X-Ray (VLESS Reality) installation on the server
|
||||||
|
*/
|
||||||
|
private static function detectBuiltinXray(VpnServer $server, array $protocol): array
|
||||||
|
{
|
||||||
|
$metadata = $protocol['definition']['metadata'] ?? [];
|
||||||
|
$containerName = $metadata['container_name'] ?? 'amnezia-xray';
|
||||||
|
$containerFilter = escapeshellarg('^' . $containerName . '$');
|
||||||
|
$containerArg = escapeshellarg($containerName);
|
||||||
|
|
||||||
|
$containerList = trim($server->executeCommand("docker ps -a --filter name={$containerFilter} --format '{{.Names}}'", true));
|
||||||
|
if ($containerList === '') {
|
||||||
|
return [
|
||||||
|
'status' => 'absent',
|
||||||
|
'message' => 'Контейнер X-Ray не найден на сервере'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$containerState = trim($server->executeCommand("docker inspect --format '{{.State.Status}}' {$containerArg}", true));
|
||||||
|
|
||||||
|
// Read X-Ray config
|
||||||
|
$configRaw = $server->executeCommand("docker exec -i {$containerArg} cat /opt/amnezia/xray/server.json 2>/dev/null", true);
|
||||||
|
if (trim($configRaw) === '') {
|
||||||
|
$configRaw = $server->executeCommand("docker exec -i {$containerArg} cat /etc/xray/config.json 2>/dev/null", true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (trim($configRaw) === '') {
|
||||||
|
return [
|
||||||
|
'status' => 'partial',
|
||||||
|
'message' => 'Контейнер X-Ray найден, но конфигурация server.json отсутствует',
|
||||||
|
'details' => [
|
||||||
|
'container_name' => $containerName,
|
||||||
|
'container_status' => $containerState,
|
||||||
|
]
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$config = json_decode(trim($configRaw), true);
|
||||||
|
if (!is_array($config)) {
|
||||||
|
return [
|
||||||
|
'status' => 'partial',
|
||||||
|
'message' => 'Не удалось разобрать JSON конфигурации X-Ray',
|
||||||
|
'details' => [
|
||||||
|
'container_name' => $containerName,
|
||||||
|
'container_status' => $containerState,
|
||||||
|
]
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract port, clients, Reality keys
|
||||||
|
$inbounds = $config['inbounds'] ?? [];
|
||||||
|
$port = 443;
|
||||||
|
$xrayClients = [];
|
||||||
|
$realityPublicKey = null;
|
||||||
|
$realityPrivateKey = null;
|
||||||
|
$realityShortId = null;
|
||||||
|
$realityServerName = null;
|
||||||
|
|
||||||
|
if (is_array($inbounds) && !empty($inbounds)) {
|
||||||
|
$port = (int) ($inbounds[0]['port'] ?? 443);
|
||||||
|
$settings = $inbounds[0]['settings'] ?? [];
|
||||||
|
$xrayClients = $settings['clients'] ?? [];
|
||||||
|
|
||||||
|
$stream = $inbounds[0]['streamSettings'] ?? [];
|
||||||
|
if (is_array($stream) && ($stream['security'] ?? '') === 'reality') {
|
||||||
|
$rs = $stream['realitySettings'] ?? [];
|
||||||
|
$serverNames = $rs['serverNames'] ?? ($rs['serverName'] ?? []);
|
||||||
|
$shortIds = $rs['shortIds'] ?? ($rs['shortId'] ?? []);
|
||||||
|
$realityServerName = is_array($serverNames) ? ($serverNames[0] ?? null) : (is_string($serverNames) ? $serverNames : null);
|
||||||
|
$realityShortId = is_array($shortIds) ? ($shortIds[0] ?? null) : (is_string($shortIds) ? $shortIds : null);
|
||||||
|
$realityPrivateKey = $rs['privateKey'] ?? null;
|
||||||
|
|
||||||
|
// Derive public key from private
|
||||||
|
if (is_string($realityPrivateKey) && $realityPrivateKey !== '' && function_exists('sodium_crypto_scalarmult_base')) {
|
||||||
|
$b64 = strtr($realityPrivateKey, '-_', '+/');
|
||||||
|
$bin = base64_decode($b64, true);
|
||||||
|
if ($bin === false) {
|
||||||
|
$bin = base64_decode($realityPrivateKey, true);
|
||||||
|
}
|
||||||
|
if (is_string($bin) && strlen($bin) === 32) {
|
||||||
|
$pub = sodium_crypto_scalarmult_base($bin);
|
||||||
|
$realityPublicKey = rtrim(strtr(base64_encode($pub), '+/', '-_'), '=');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read clientsTable for names
|
||||||
|
$clientsTableRaw = $server->executeCommand("docker exec -i {$containerArg} cat /opt/amnezia/xray/clientsTable 2>/dev/null", true);
|
||||||
|
$clientsTable = json_decode(trim($clientsTableRaw), true);
|
||||||
|
$clientsCount = is_array($xrayClients) ? count($xrayClients) : 0;
|
||||||
|
|
||||||
|
return [
|
||||||
|
'status' => 'existing',
|
||||||
|
'message' => 'Найдена установленная конфигурация X-Ray VLESS Reality',
|
||||||
|
'details' => [
|
||||||
|
'container_name' => $containerName,
|
||||||
|
'container_status' => $containerState,
|
||||||
|
'port' => $port,
|
||||||
|
'clients' => $xrayClients,
|
||||||
|
'clients_table' => is_array($clientsTable) ? $clientsTable : [],
|
||||||
|
'clients_count' => $clientsCount,
|
||||||
|
'reality_public_key' => $realityPublicKey,
|
||||||
|
'reality_private_key' => $realityPrivateKey,
|
||||||
|
'reality_short_id' => $realityShortId,
|
||||||
|
'reality_server_name' => $realityServerName,
|
||||||
|
'config' => $config,
|
||||||
|
'summary' => sprintf('Container %s (%s), port %d, clients %d', $containerName, $containerState ?: 'unknown', $port, $clientsCount)
|
||||||
|
]
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restore existing X-Ray installation: save config to DB, import clients
|
||||||
|
*/
|
||||||
|
private static function restoreBuiltinXray(VpnServer $server, array $protocol, array $detection, array $options): array
|
||||||
|
{
|
||||||
|
$details = $detection['details'] ?? [];
|
||||||
|
$containerName = $details['container_name'] ?? 'amnezia-xray';
|
||||||
|
$containerArg = escapeshellarg($containerName);
|
||||||
|
$serverId = $server->getId();
|
||||||
|
|
||||||
|
// Ensure container is running
|
||||||
|
$server->executeCommand("docker start {$containerArg} 2>/dev/null || true", true);
|
||||||
|
|
||||||
|
// Update vpn_servers with X-Ray data
|
||||||
|
$port = $details['port'] ?? 443;
|
||||||
|
$pdo = DB::conn();
|
||||||
|
$stmt = $pdo->prepare('
|
||||||
|
UPDATE vpn_servers
|
||||||
|
SET vpn_port = ?,
|
||||||
|
status = ?,
|
||||||
|
error_message = NULL,
|
||||||
|
deployed_at = COALESCE(deployed_at, NOW())
|
||||||
|
WHERE id = ?
|
||||||
|
');
|
||||||
|
$stmt->execute([$port, 'active', $serverId]);
|
||||||
|
$server->refresh();
|
||||||
|
|
||||||
|
// Save protocol binding
|
||||||
|
$pid = self::resolveProtocolId($protocol);
|
||||||
|
if ($pid) {
|
||||||
|
$config = [
|
||||||
|
'server_host' => $server->getData()['host'] ?? null,
|
||||||
|
'server_port' => $port,
|
||||||
|
'extras' => [
|
||||||
|
'reality_public_key' => $details['reality_public_key'] ?? null,
|
||||||
|
'reality_private_key' => $details['reality_private_key'] ?? null,
|
||||||
|
'reality_short_id' => $details['reality_short_id'] ?? null,
|
||||||
|
'reality_server_name' => $details['reality_server_name'] ?? null,
|
||||||
|
'container_name' => $containerName,
|
||||||
|
]
|
||||||
|
];
|
||||||
|
$stmt2 = $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()');
|
||||||
|
$stmt2->execute([$serverId, $pid, json_encode($config)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Import X-Ray clients into database
|
||||||
|
$xrayClients = $details['clients'] ?? [];
|
||||||
|
$clientsTable = $details['clients_table'] ?? [];
|
||||||
|
$serverData = $server->getData();
|
||||||
|
$imported = 0;
|
||||||
|
|
||||||
|
// Build name lookup from clientsTable
|
||||||
|
$nameById = [];
|
||||||
|
if (is_array($clientsTable)) {
|
||||||
|
foreach ($clientsTable as $entry) {
|
||||||
|
$cid = $entry['clientId'] ?? '';
|
||||||
|
$cname = $entry['userData']['clientName'] ?? null;
|
||||||
|
if ($cid !== '' && $cname) {
|
||||||
|
$nameById[$cid] = $cname;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_array($xrayClients)) {
|
||||||
|
foreach ($xrayClients as $xClient) {
|
||||||
|
$uuid = $xClient['id'] ?? '';
|
||||||
|
if ($uuid === '') continue;
|
||||||
|
|
||||||
|
// Check if client already exists by public_key (UUID used as identifier)
|
||||||
|
$chk = $pdo->prepare('SELECT id FROM vpn_clients WHERE server_id = ? AND public_key = ?');
|
||||||
|
$chk->execute([$serverId, $uuid]);
|
||||||
|
if ($chk->fetch()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also check by login
|
||||||
|
$email = $xClient['email'] ?? '';
|
||||||
|
if ($email !== '') {
|
||||||
|
$chk2 = $pdo->prepare('SELECT id FROM vpn_clients WHERE server_id = ? AND login = ?');
|
||||||
|
$chk2->execute([$serverId, $email]);
|
||||||
|
if ($chk2->fetch()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$name = $nameById[$uuid] ?? ($email !== '' ? $email : 'xray-' . substr($uuid, 0, 8));
|
||||||
|
|
||||||
|
// Generate VLESS config URL for the client
|
||||||
|
$host = $serverData['host'] ?? '';
|
||||||
|
$realityPub = $details['reality_public_key'] ?? '';
|
||||||
|
$shortId = $details['reality_short_id'] ?? '';
|
||||||
|
$sni = $details['reality_server_name'] ?? '';
|
||||||
|
$flow = $xClient['flow'] ?? 'xtls-rprx-vision';
|
||||||
|
|
||||||
|
$vlessUrl = sprintf(
|
||||||
|
'vless://%s@%s:%d?type=tcp&security=reality&pbk=%s&fp=chrome&sni=%s&sid=%s&spx=%%2F&flow=%s#%s',
|
||||||
|
$uuid,
|
||||||
|
$host,
|
||||||
|
$port,
|
||||||
|
urlencode($realityPub),
|
||||||
|
urlencode($sni),
|
||||||
|
urlencode($shortId),
|
||||||
|
urlencode($flow),
|
||||||
|
urlencode($name)
|
||||||
|
);
|
||||||
|
|
||||||
|
$ins = $pdo->prepare('INSERT INTO vpn_clients (server_id, user_id, name, client_ip, public_key, private_key, preshared_key, login, config, protocol_id, status, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW())');
|
||||||
|
$ins->execute([
|
||||||
|
$serverId,
|
||||||
|
$serverData['user_id'] ?? null,
|
||||||
|
$name,
|
||||||
|
'',
|
||||||
|
$uuid, // Store UUID as public_key for X-Ray clients
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
$email !== '' ? $email : $uuid,
|
||||||
|
$vlessUrl,
|
||||||
|
$pid ?: null,
|
||||||
|
'active' // Import as active since they work on the server
|
||||||
|
]);
|
||||||
|
$imported++;
|
||||||
|
Logger::appendInstall($serverId, "Imported X-Ray client: {$name} ({$uuid})");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger::appendInstall($serverId, "X-Ray restore complete: imported {$imported} clients");
|
||||||
|
|
||||||
|
return [
|
||||||
|
'success' => true,
|
||||||
|
'mode' => 'restore',
|
||||||
|
'message' => 'Существующая конфигурация X-Ray восстановлена',
|
||||||
|
'port' => $port,
|
||||||
|
'clients_count' => count($xrayClients),
|
||||||
|
'imported_clients' => $imported,
|
||||||
|
'reality_public_key' => $details['reality_public_key'] ?? null,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Import existing AWG clients from server into database (called during activate with existing config)
|
||||||
|
*/
|
||||||
|
private static function importExistingAwgClients(VpnServer $server, array $protocol, array $detection): void
|
||||||
|
{
|
||||||
|
$details = $detection['details'] ?? [];
|
||||||
|
$containerName = $details['container_name'] ?? 'amnezia-awg';
|
||||||
|
$containerArg = escapeshellarg($containerName);
|
||||||
|
$serverId = $server->getId();
|
||||||
|
$pdo = DB::conn();
|
||||||
|
$serverData = $server->getData();
|
||||||
|
$pid = self::resolveProtocolId($protocol);
|
||||||
|
|
||||||
|
// Read wg0.conf and clientsTable
|
||||||
|
$wgConfig = $server->executeCommand("docker exec -i {$containerArg} cat /opt/amnezia/awg/wg0.conf 2>/dev/null", true);
|
||||||
|
$tableRaw = $server->executeCommand("docker exec -i {$containerArg} cat /opt/amnezia/awg/clientsTable 2>/dev/null", true);
|
||||||
|
$clientsTable = json_decode(trim($tableRaw), true);
|
||||||
|
|
||||||
|
// Build name lookup
|
||||||
|
$nameByPub = [];
|
||||||
|
if (is_array($clientsTable)) {
|
||||||
|
foreach ($clientsTable as $entry) {
|
||||||
|
$cid = $entry['clientId'] ?? '';
|
||||||
|
$uname = $entry['userData']['clientName'] ?? null;
|
||||||
|
if ($cid !== '' && $uname) {
|
||||||
|
$nameByPub[$cid] = $uname;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$imported = 0;
|
||||||
|
if (trim($wgConfig) !== '') {
|
||||||
|
$pattern = '/\[Peer\][^\[]*?PublicKey\s*=\s*(.+?)\s*[\r\n]+[\s\S]*?AllowedIPs\s*=\s*(.+?)(?:\r?\n|$)/';
|
||||||
|
if (preg_match_all($pattern, $wgConfig, $matches, PREG_SET_ORDER)) {
|
||||||
|
foreach ($matches as $m) {
|
||||||
|
$pub = trim($m[1]);
|
||||||
|
$allowed = trim($m[2]);
|
||||||
|
$clientIp = null;
|
||||||
|
foreach (explode(',', $allowed) as $ipSpec) {
|
||||||
|
$ipSpec = trim($ipSpec);
|
||||||
|
if (preg_match('/^([0-9\.]+)\/32$/', $ipSpec, $mm)) {
|
||||||
|
$clientIp = $mm[1];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!$clientIp) continue;
|
||||||
|
|
||||||
|
// Check if client already exists
|
||||||
|
$chk = $pdo->prepare('SELECT id FROM vpn_clients WHERE server_id = ? AND (client_ip = ? OR public_key = ?)');
|
||||||
|
$chk->execute([$serverId, $clientIp, $pub]);
|
||||||
|
if ($chk->fetch()) continue;
|
||||||
|
|
||||||
|
$name = $nameByPub[$pub] ?? ('import-' . str_replace('.', '_', $clientIp));
|
||||||
|
$ins = $pdo->prepare('INSERT INTO vpn_clients (server_id, user_id, name, client_ip, public_key, private_key, preshared_key, config, protocol_id, status, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW())');
|
||||||
|
$ins->execute([
|
||||||
|
$serverId,
|
||||||
|
$serverData['user_id'] ?? null,
|
||||||
|
$name,
|
||||||
|
$clientIp,
|
||||||
|
$pub,
|
||||||
|
'',
|
||||||
|
$details['preshared_key'] ?? null,
|
||||||
|
'',
|
||||||
|
$pid ?: null,
|
||||||
|
'active' // Import as active since they exist on the server
|
||||||
|
]);
|
||||||
|
$imported++;
|
||||||
|
Logger::appendInstall($serverId, "Imported AWG client: {$name} ({$clientIp})");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger::appendInstall($serverId, "AWG client import complete: imported {$imported} clients");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user