From ca51fa62ddb6cc14a814e73ace10116480aa638e Mon Sep 17 00:00:00 2001 From: infosave2007 Date: Mon, 26 Jan 2026 21:41:26 +0300 Subject: [PATCH] feat: XRay Reality key backup and restoration - Modified migrations/048_enable_xray_stats.sql to accept existing keys via env vars (PRIVATE_KEY, SHORT_ID) - Updated InstallProtocolManager.php to extract and store reality_private_key after XRay installation - Added key restoration logic in buildExports() to reuse saved keys during reinstallation - Fixed VpnClient.php to correctly parse JSON stats output from XRay API - Security fix: removed exposed port 2375 from docker-compose.yml (dind container) --- docker-compose.yml | 3 +- inc/InstallProtocolManager.php | 47 +++++++-- inc/VpnClient.php | 38 +++++-- migrations/012_add_user_roles.sql | 32 ------ migrations/047_create_protocols_table.sql | 56 +++++++--- migrations/048_enable_xray_stats.sql | 120 +++++++++++++++++++++- 6 files changed, 228 insertions(+), 68 deletions(-) delete mode 100644 migrations/012_add_user_roles.sql diff --git a/docker-compose.yml b/docker-compose.yml index 5e108e6..b153990 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -57,8 +57,7 @@ services: restart: unless-stopped environment: DOCKER_TLS_CERTDIR: "" - ports: - - "2375:2375" + volumes: - dind_data:/var/lib/docker diff --git a/inc/InstallProtocolManager.php b/inc/InstallProtocolManager.php index d1b223a..f4abf95 100644 --- a/inc/InstallProtocolManager.php +++ b/inc/InstallProtocolManager.php @@ -726,6 +726,31 @@ class InstallProtocolManager : (isset($options['server_port']) ? (int) $options['server_port'] : ''), ]; + // Check for saved Reality keys in server_protocols table + $serverId = $serverData['id'] ?? null; + if ($serverId) { + try { + $pdo = DB::conn(); + $stmt = $pdo->prepare('SELECT config_data FROM server_protocols WHERE server_id = ? ORDER BY applied_at DESC LIMIT 1'); + $stmt->execute([$serverId]); + $configJson = $stmt->fetchColumn(); + if ($configJson) { + $config = json_decode($configJson, true); + $extras = $config['extras'] ?? []; + // Export saved Reality keys if reinstalling (allow script to reuse them) + if (!empty($extras['reality_private_key'])) { + $pairs['PRIVATE_KEY'] = $extras['reality_private_key']; + } + if (!empty($extras['reality_short_id'])) { + $pairs['SHORT_ID'] = $extras['reality_short_id']; + } + // Note: CLIENT_ID is per-client, not per-server, so we don't restore it here + } + } catch (Throwable $e) { + // Ignore errors, will generate new keys + } + } + foreach ($pairs as $key => $value) { if ($value !== '' && $value !== null) { $exports[] = sprintf('export %s=%s', $key, escapeshellarg((string) $value)); @@ -1102,6 +1127,10 @@ class InstallProtocolManager if ($publicKey) { $res['reality_public_key'] = $publicKey; } + // Store private key for future restoration + if (is_string($privateKey) && $privateKey !== '') { + $res['reality_private_key'] = $privateKey; + } if ($shortId) { $res['reality_short_id'] = $shortId; } @@ -1131,6 +1160,7 @@ class InstallProtocolManager 'client_id' => $clientId, 'result' => $res, 'reality_public_key' => $res['reality_public_key'] ?? null, + 'reality_private_key' => $res['reality_private_key'] ?? null, 'reality_short_id' => $res['reality_short_id'] ?? null, 'reality_server_name' => $res['reality_server_name'] ?? null, ] @@ -1238,32 +1268,33 @@ class InstallProtocolManager { $serverId = $server->getId(); $pdo = DB::conn(); - + // Fetch active clients $stmt = $pdo->prepare("SELECT * FROM vpn_clients WHERE server_id = ? AND status = 'active'"); $stmt->execute([$serverId]); $clients = $stmt->fetchAll(); - + if (empty($clients)) { return; } $containerName = $server->getData()['container_name'] ?? 'amnezia-awg'; - + // Read existing config $conf = $server->executeCommand("docker exec -i $containerName cat /opt/amnezia/awg/wg0.conf", true); - if (!$conf) return; + if (!$conf) + return; $newPeersBlock = ""; $count = 0; - + foreach ($clients as $client) { $ip = $client['client_ip']; // Check if peer already exists (simple check by IP) if (strpos($conf, $ip) !== false) { continue; } - + // Append Peer $newPeersBlock .= "\n[Peer]\n"; $newPeersBlock .= "PublicKey = " . $client['public_key'] . "\n"; @@ -1275,13 +1306,13 @@ class InstallProtocolManager $newPeersBlock .= "AllowedIPs = $allowed\n"; $count++; } - + if ($count > 0) { Logger::appendInstall($serverId, "Syncing $count existing clients to server config"); $conf .= $newPeersBlock; $escaped = addslashes($conf); $server->executeCommand("docker exec -i $containerName sh -c 'echo \"$escaped\" > /opt/amnezia/awg/wg0.conf'", true); - + // Reload interface $server->executeCommand("docker exec -i $containerName wg-quick down wg0 || true", true); $server->executeCommand("docker exec -i $containerName wg-quick up wg0", true); diff --git a/inc/VpnClient.php b/inc/VpnClient.php index ae195ed..a61def2 100644 --- a/inc/VpnClient.php +++ b/inc/VpnClient.php @@ -1479,15 +1479,31 @@ class VpnClient // user>>>uuid>>>traffic>>>uplink: 1024 // user>>>uuid>>>traffic>>>downlink: 2048 - $lines = explode("\n", trim($output)); - foreach ($lines as $line) { - if (preg_match('/user>>>.+>>>traffic>>>uplink:\s*(\d+)/', $line, $m)) { - $stats['bytes_sent'] = (int) $m[1]; - } elseif (preg_match('/user>>>.+>>>traffic>>>downlink:\s*(\d+)/', $line, $m)) { - $stats['bytes_received'] = (int) $m[1]; + // Parse JSON output + $json = json_decode($output, true); + if (is_array($json) && isset($json['stat']) && is_array($json['stat'])) { + foreach ($json['stat'] as $item) { + if (!isset($item['name']) || !isset($item['value'])) + continue; + if (strpos($item['name'], 'uplink') !== false) { + $stats['bytes_sent'] += (int) $item['value']; + } elseif (strpos($item['name'], 'downlink') !== false) { + $stats['bytes_received'] += (int) $item['value']; + } + } + } else { + // Fallback to text parsing (legacy) + $lines = explode("\n", trim($output)); + foreach ($lines as $line) { + if (preg_match('/user>>>.+>>>traffic>>>uplink:\s*(\d+)/', $line, $m)) { + $stats['bytes_sent'] = (int) $m[1]; + } elseif (preg_match('/user>>>.+>>>traffic>>>downlink:\s*(\d+)/', $line, $m)) { + $stats['bytes_received'] = (int) $m[1]; + } } } + return $stats; } @@ -1516,12 +1532,18 @@ class VpnClient // Or better: try to detect protocol from config if container name is vague (but usually amnezia-xray) if (strpos($containerName, 'xray') !== false) { - // Use client Name (Login) as identifier strictly requested by user - $identifier = $this->data['name'] ?? null; + // Extract UUID from config for XRay (vless://UUID@...) + $identifier = null; + if (!empty($this->data['config']) && preg_match('/vless:\/\/([0-9a-fA-F-]{36})@/i', $this->data['config'], $m)) { + $identifier = $m[1]; + } elseif (!empty($this->data['name'])) { + $identifier = $this->data['name']; + } if ($identifier) { $stats = self::getXrayStats($serverData, $identifier); } + } if (empty($stats)) { diff --git a/migrations/012_add_user_roles.sql b/migrations/012_add_user_roles.sql deleted file mode 100644 index dbb04a5..0000000 --- a/migrations/012_add_user_roles.sql +++ /dev/null @@ -1,32 +0,0 @@ --- Migration: Add user roles and permissions --- Date: 2025-11-10 - --- User roles table -CREATE TABLE IF NOT EXISTS user_roles ( - id INT PRIMARY KEY AUTO_INCREMENT, - name VARCHAR(50) NOT NULL UNIQUE, - display_name VARCHAR(100) NOT NULL, - description TEXT, - permissions JSON NOT NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; - --- Add role to users table -ALTER TABLE users -ADD COLUMN role VARCHAR(50) DEFAULT 'viewer' AFTER ldap_dn, -ADD INDEX idx_role (role); - --- Insert default roles -INSERT IGNORE INTO user_roles (name, display_name, description, permissions) VALUES -('admin', 'Administrator', 'Full access to all features', JSON_ARRAY('*')), -('manager', 'Manager', 'Can manage servers and clients', JSON_ARRAY('servers.view', 'servers.create', 'servers.edit', 'clients.view', 'clients.create', 'clients.edit', 'clients.delete')), -('viewer', 'Viewer', 'Can only view own clients', JSON_ARRAY('clients.view_own', 'clients.download_own')); - --- Insert default LDAP group mappings (examples) -INSERT IGNORE INTO ldap_group_mappings (ldap_group, role_name, description) VALUES -('vpn-admins', 'admin', 'VPN administrators with full access'), -('vpn-managers', 'manager', 'VPN managers who can create and manage clients'), -('vpn-users', 'viewer', 'Regular VPN users with view-only access'); - --- Update existing users to admin role (backward compatibility) -UPDATE users SET role = 'admin' WHERE role IS NULL OR role = ''; diff --git a/migrations/047_create_protocols_table.sql b/migrations/047_create_protocols_table.sql index 20ce14a..df6d718 100644 --- a/migrations/047_create_protocols_table.sql +++ b/migrations/047_create_protocols_table.sql @@ -1,19 +1,41 @@ -CREATE TABLE IF NOT EXISTS protocols ( - id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, - slug VARCHAR(50) NOT NULL UNIQUE, - name VARCHAR(100) NOT NULL, - description TEXT, - definition JSON, - show_text_content TINYINT(1) DEFAULT 0, - is_active TINYINT(1) DEFAULT 1, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - INDEX idx_slug (slug), - INDEX idx_active (is_active) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +-- Safely update protocols table schema and data --- Insert default protocols (X-Ray, AWG) --- We populate initial data so the panel is usable immediately +-- 1. Ensure columns exist +SET @dbname = DATABASE(); +SET @tablename = "protocols"; +SET @columnname = "definition"; +SET @preparedStatement = (SELECT IF( + ( + SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS + WHERE + (table_name = @tablename) + AND (table_schema = @dbname) + AND (column_name = @columnname) + ) > 0, + "SELECT 1", + "ALTER TABLE protocols ADD COLUMN definition JSON NULL AFTER description" +)); +PREPARE alterIfNotExists FROM @preparedStatement; +EXECUTE alterIfNotExists; +DEALLOCATE PREPARE alterIfNotExists; + +SET @columnname = "show_text_content"; +SET @preparedStatement = (SELECT IF( + ( + SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS + WHERE + (table_name = @tablename) + AND (table_schema = @dbname) + AND (column_name = @columnname) + ) > 0, + "SELECT 1", + "ALTER TABLE protocols ADD COLUMN show_text_content TINYINT(1) DEFAULT 0 AFTER definition" +)); +PREPARE alterIfNotExists FROM @preparedStatement; +EXECUTE alterIfNotExists; +DEALLOCATE PREPARE alterIfNotExists; + +-- 2. Insert Data INSERT IGNORE INTO protocols (slug, name, description, definition, show_text_content, is_active) VALUES ('amnezia-wg', 'AmneziaWG', 'Amnezia WireGuard implementation', '{}', 0, 1), ('amnezia-xray', 'Amnezia XRay', 'XRay (VLESS/Reality)', '{"scripts":{}}', 0, 1), @@ -22,14 +44,14 @@ INSERT IGNORE INTO protocols (slug, name, description, definition, show_text_con ('shadowsocks', 'Shadowsocks', 'Shadowsocks proxy', '{}', 0, 1), ('cloak', 'Cloak', 'Cloak obfuscation', '{}', 0, 1); --- Add protocol_id to vpn_clients if it does not exist +-- 3. Update vpn_clients structure (original logic from migration) SET @exist := (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='vpn_clients' AND COLUMN_NAME='protocol_id'); SET @sql := IF(@exist=0, 'ALTER TABLE vpn_clients ADD COLUMN protocol_id INT UNSIGNED NULL AFTER server_id, ADD INDEX idx_protocol_id (protocol_id), ADD CONSTRAINT fk_clients_protocol FOREIGN KEY (protocol_id) REFERENCES protocols(id) ON DELETE SET NULL', 'SELECT "Column protocol_id exists"'); PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; --- Also check server_protocols table existence (referenced in InstallProtocolManager) +-- 4. Create server_protocols if not exists CREATE TABLE IF NOT EXISTS server_protocols ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, server_id INT UNSIGNED NOT NULL, diff --git a/migrations/048_enable_xray_stats.sql b/migrations/048_enable_xray_stats.sql index 3d9cb7b..2c05cfc 100644 --- a/migrations/048_enable_xray_stats.sql +++ b/migrations/048_enable_xray_stats.sql @@ -1,5 +1,123 @@ -- Enable Stats and API for XRay VLESS protocol -- This allows collecting traffic usage per user +-- Supports restoration of existing keys via environment variables -UPDATE protocols SET install_script = '#!/bin/bash\n\nset -euo pipefail\nset -x\n\nCONTAINER_NAME="${CONTAINER_NAME:-amnezia-xray}"\nPORT_RANGE_START=${PORT_RANGE_START:-30000}\nPORT_RANGE_END=${PORT_RANGE_END:-65000}\nXRAY_PORT=$((RANDOM % (PORT_RANGE_END - PORT_RANGE_START + 1) + PORT_RANGE_START))\n\nPRIVATE_KEY=$(docker run --rm teddysun/xray xray x25519 | grep "Private key:" | awk ''{print $3}'')\nPUBLIC_KEY=$(docker run --rm teddysun/xray xray x25519 -i "$PRIVATE_KEY" | grep "Public key:" | awk ''{print $3}'')\nSHORT_ID=$(openssl rand -hex 8)\nCLIENT_ID=$(cat /proc/sys/kernel/random/uuid)\n\nSERVER_NAME="www.googletagmanager.com"\nFINGERPRINT="chrome"\nSPIDER_X="/"\n\ndocker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true\nmkdir -p /opt/amnezia/xray\n\ncat > /opt/amnezia/xray/server.json << EOF\n{\n "log": { "loglevel": "warning" },\n "stats": {},\n "api": {\n "tag": "api",\n "services": [\n "StatsService"\n ]\n },\n "policy": {\n "levels": {\n "0": {\n "statsUserUplink": true,\n "statsUserDownlink": true\n }\n },\n "system": {\n "statsInboundUplink": true,\n "statsInboundDownlink": true\n }\n },\n "inbounds": [\n {\n "listen": "0.0.0.0",\n "port": ${XRAY_PORT},\n "protocol": "vless",\n "settings": {\n "clients": [ { "id": "${CLIENT_ID}", "email": "${CLIENT_ID}" } ],\n "decryption": "none",\n "fallbacks": [ { "dest": 80 } ]\n },\n "streamSettings": {\n "network": "tcp",\n "security": "reality",\n "realitySettings": {\n "show": false,\n "dest": "${SERVER_NAME}:443",\n "xver": 0,\n "serverNames": [ "${SERVER_NAME}" ],\n "privateKey": "${PRIVATE_KEY}",\n "shortIds": [ "${SHORT_ID}" ],\n "fingerprint": "${FINGERPRINT}",\n "spiderX": "${SPIDER_X}"\n }\n }\n },\n {\n "listen": "127.0.0.1",\n "port": 10085,\n "protocol": "dokodemo-door",\n "tag": "api",\n "settings": {\n "address": "127.0.0.1"\n }\n }\n ],\n "outbounds": [ \n { "protocol": "freedom", "tag": "direct" }\n ],\n "routing": {\n "rules": [\n {\n "inboundTag": [\n "api"\n ],\n "outboundTag": "api",\n "type": "field"\n }\n ]\n }\n}\nEOF\n\n# start container\ndocker run -d \\\n --name "$CONTAINER_NAME" \\\n --restart always \\\n --network host \\\n -v /opt/amnezia/xray:/opt/amnezia/xray \\\n teddysun/xray xray run -c /opt/amnezia/xray/server.json\n\nsleep 2\n\n# panel output\necho "Port: ${XRAY_PORT}"\necho "ClientID: ${CLIENT_ID}"\necho "PublicKey: ${PUBLIC_KEY}"\necho "PrivateKey: ${PRIVATE_KEY}"\necho "ShortID: ${SHORT_ID}"\necho "ServerName: ${SERVER_NAME}"' +UPDATE protocols SET install_script = '#!/bin/bash +set -eu + +CONTAINER_NAME="${CONTAINER_NAME:-amnezia-xray}" +XRAY_PORT=${SERVER_PORT:-443} + +docker pull teddysun/xray >/dev/null 2>&1 || true + +# Use existing keys if provided, otherwise generate new ones +if [ -z "${PRIVATE_KEY:-}" ]; then + GEN=$(docker run --rm --entrypoint /usr/bin/xray teddysun/xray x25519 2>/dev/null || true) + PRIVATE_KEY=$(printf "%s\\n" "$GEN" | sed -n -E "s/^[Pp]rivate[Kk]ey:[[:space:]]*(.*)$/\\1/p" | tr -d " \\t\\r\\n") + if [ -z "$PRIVATE_KEY" ]; then + PRIVATE_KEY=$(printf "%s\\n" "$GEN" | grep -i "private" | head -1 | sed "s/.*:[[:space:]]*//" | tr -d " \\t\\r\\n") + fi +fi + +# Derive public key from private key +PUBLIC_KEY=$(docker run --rm --entrypoint /usr/bin/xray teddysun/xray x25519 -i "$PRIVATE_KEY" 2>/dev/null | sed -n -E "s/^[Pp]ublic[[:space:]]*[Kk]ey:[[:space:]]*(.*)$/\\1/p" | tr -d " \\t\\r\\n" || true) +if [ -z "$PUBLIC_KEY" ]; then + PUBLIC_KEY=$(docker run --rm --entrypoint /usr/bin/xray teddysun/xray x25519 -i "$PRIVATE_KEY" 2>/dev/null | sed -n -E "s/^[Pp]assword:[[:space:]]*(.*)$/\\1/p" | tr -d " \\t\\r\\n" || true) +fi + +# Use existing short_id or generate new one +if [ -z "${SHORT_ID:-}" ]; then + SHORT_ID=$(od -An -tx1 -N8 /dev/urandom | tr -d " \\n") +fi + +# Use existing client_id or generate new one +if [ -z "${CLIENT_ID:-}" ]; then + CLIENT_ID=$(cat /proc/sys/kernel/random/uuid) +fi + +SERVER_NAME="${SERVER_NAME:-www.googletagmanager.com}" +FINGERPRINT="${FINGERPRINT:-chrome}" +SPIDER_X="${SPIDER_X:-/}" + +docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true +mkdir -p /opt/amnezia/xray + +cat > /opt/amnezia/xray/server.json <