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)
This commit is contained in:
+1
-2
@@ -57,8 +57,7 @@ services:
|
|||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
DOCKER_TLS_CERTDIR: ""
|
DOCKER_TLS_CERTDIR: ""
|
||||||
ports:
|
|
||||||
- "2375:2375"
|
|
||||||
volumes:
|
volumes:
|
||||||
- dind_data:/var/lib/docker
|
- dind_data:/var/lib/docker
|
||||||
|
|
||||||
|
|||||||
@@ -726,6 +726,31 @@ class InstallProtocolManager
|
|||||||
: (isset($options['server_port']) ? (int) $options['server_port'] : ''),
|
: (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) {
|
foreach ($pairs as $key => $value) {
|
||||||
if ($value !== '' && $value !== null) {
|
if ($value !== '' && $value !== null) {
|
||||||
$exports[] = sprintf('export %s=%s', $key, escapeshellarg((string) $value));
|
$exports[] = sprintf('export %s=%s', $key, escapeshellarg((string) $value));
|
||||||
@@ -1102,6 +1127,10 @@ class InstallProtocolManager
|
|||||||
if ($publicKey) {
|
if ($publicKey) {
|
||||||
$res['reality_public_key'] = $publicKey;
|
$res['reality_public_key'] = $publicKey;
|
||||||
}
|
}
|
||||||
|
// Store private key for future restoration
|
||||||
|
if (is_string($privateKey) && $privateKey !== '') {
|
||||||
|
$res['reality_private_key'] = $privateKey;
|
||||||
|
}
|
||||||
if ($shortId) {
|
if ($shortId) {
|
||||||
$res['reality_short_id'] = $shortId;
|
$res['reality_short_id'] = $shortId;
|
||||||
}
|
}
|
||||||
@@ -1131,6 +1160,7 @@ class InstallProtocolManager
|
|||||||
'client_id' => $clientId,
|
'client_id' => $clientId,
|
||||||
'result' => $res,
|
'result' => $res,
|
||||||
'reality_public_key' => $res['reality_public_key'] ?? null,
|
'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_short_id' => $res['reality_short_id'] ?? null,
|
||||||
'reality_server_name' => $res['reality_server_name'] ?? null,
|
'reality_server_name' => $res['reality_server_name'] ?? null,
|
||||||
]
|
]
|
||||||
@@ -1238,32 +1268,33 @@ class InstallProtocolManager
|
|||||||
{
|
{
|
||||||
$serverId = $server->getId();
|
$serverId = $server->getId();
|
||||||
$pdo = DB::conn();
|
$pdo = DB::conn();
|
||||||
|
|
||||||
// Fetch active clients
|
// Fetch active clients
|
||||||
$stmt = $pdo->prepare("SELECT * FROM vpn_clients WHERE server_id = ? AND status = 'active'");
|
$stmt = $pdo->prepare("SELECT * FROM vpn_clients WHERE server_id = ? AND status = 'active'");
|
||||||
$stmt->execute([$serverId]);
|
$stmt->execute([$serverId]);
|
||||||
$clients = $stmt->fetchAll();
|
$clients = $stmt->fetchAll();
|
||||||
|
|
||||||
if (empty($clients)) {
|
if (empty($clients)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$containerName = $server->getData()['container_name'] ?? 'amnezia-awg';
|
$containerName = $server->getData()['container_name'] ?? 'amnezia-awg';
|
||||||
|
|
||||||
// Read existing config
|
// Read existing config
|
||||||
$conf = $server->executeCommand("docker exec -i $containerName cat /opt/amnezia/awg/wg0.conf", true);
|
$conf = $server->executeCommand("docker exec -i $containerName cat /opt/amnezia/awg/wg0.conf", true);
|
||||||
if (!$conf) return;
|
if (!$conf)
|
||||||
|
return;
|
||||||
|
|
||||||
$newPeersBlock = "";
|
$newPeersBlock = "";
|
||||||
$count = 0;
|
$count = 0;
|
||||||
|
|
||||||
foreach ($clients as $client) {
|
foreach ($clients as $client) {
|
||||||
$ip = $client['client_ip'];
|
$ip = $client['client_ip'];
|
||||||
// Check if peer already exists (simple check by IP)
|
// Check if peer already exists (simple check by IP)
|
||||||
if (strpos($conf, $ip) !== false) {
|
if (strpos($conf, $ip) !== false) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Append Peer
|
// Append Peer
|
||||||
$newPeersBlock .= "\n[Peer]\n";
|
$newPeersBlock .= "\n[Peer]\n";
|
||||||
$newPeersBlock .= "PublicKey = " . $client['public_key'] . "\n";
|
$newPeersBlock .= "PublicKey = " . $client['public_key'] . "\n";
|
||||||
@@ -1275,13 +1306,13 @@ class InstallProtocolManager
|
|||||||
$newPeersBlock .= "AllowedIPs = $allowed\n";
|
$newPeersBlock .= "AllowedIPs = $allowed\n";
|
||||||
$count++;
|
$count++;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($count > 0) {
|
if ($count > 0) {
|
||||||
Logger::appendInstall($serverId, "Syncing $count existing clients to server config");
|
Logger::appendInstall($serverId, "Syncing $count existing clients to server config");
|
||||||
$conf .= $newPeersBlock;
|
$conf .= $newPeersBlock;
|
||||||
$escaped = addslashes($conf);
|
$escaped = addslashes($conf);
|
||||||
$server->executeCommand("docker exec -i $containerName sh -c 'echo \"$escaped\" > /opt/amnezia/awg/wg0.conf'", true);
|
$server->executeCommand("docker exec -i $containerName sh -c 'echo \"$escaped\" > /opt/amnezia/awg/wg0.conf'", true);
|
||||||
|
|
||||||
// Reload interface
|
// Reload interface
|
||||||
$server->executeCommand("docker exec -i $containerName wg-quick down wg0 || true", true);
|
$server->executeCommand("docker exec -i $containerName wg-quick down wg0 || true", true);
|
||||||
$server->executeCommand("docker exec -i $containerName wg-quick up wg0", true);
|
$server->executeCommand("docker exec -i $containerName wg-quick up wg0", true);
|
||||||
|
|||||||
+30
-8
@@ -1479,15 +1479,31 @@ class VpnClient
|
|||||||
// user>>>uuid>>>traffic>>>uplink: 1024
|
// user>>>uuid>>>traffic>>>uplink: 1024
|
||||||
// user>>>uuid>>>traffic>>>downlink: 2048
|
// user>>>uuid>>>traffic>>>downlink: 2048
|
||||||
|
|
||||||
$lines = explode("\n", trim($output));
|
// Parse JSON output
|
||||||
foreach ($lines as $line) {
|
$json = json_decode($output, true);
|
||||||
if (preg_match('/user>>>.+>>>traffic>>>uplink:\s*(\d+)/', $line, $m)) {
|
if (is_array($json) && isset($json['stat']) && is_array($json['stat'])) {
|
||||||
$stats['bytes_sent'] = (int) $m[1];
|
foreach ($json['stat'] as $item) {
|
||||||
} elseif (preg_match('/user>>>.+>>>traffic>>>downlink:\s*(\d+)/', $line, $m)) {
|
if (!isset($item['name']) || !isset($item['value']))
|
||||||
$stats['bytes_received'] = (int) $m[1];
|
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;
|
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)
|
// Or better: try to detect protocol from config if container name is vague (but usually amnezia-xray)
|
||||||
|
|
||||||
if (strpos($containerName, 'xray') !== false) {
|
if (strpos($containerName, 'xray') !== false) {
|
||||||
// Use client Name (Login) as identifier strictly requested by user
|
// Extract UUID from config for XRay (vless://UUID@...)
|
||||||
$identifier = $this->data['name'] ?? null;
|
$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) {
|
if ($identifier) {
|
||||||
$stats = self::getXrayStats($serverData, $identifier);
|
$stats = self::getXrayStats($serverData, $identifier);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (empty($stats)) {
|
if (empty($stats)) {
|
||||||
|
|||||||
@@ -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 = '';
|
|
||||||
@@ -1,19 +1,41 @@
|
|||||||
CREATE TABLE IF NOT EXISTS protocols (
|
-- Safely update protocols table schema and data
|
||||||
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;
|
|
||||||
|
|
||||||
-- Insert default protocols (X-Ray, AWG)
|
-- 1. Ensure columns exist
|
||||||
-- We populate initial data so the panel is usable immediately
|
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
|
INSERT IGNORE INTO protocols (slug, name, description, definition, show_text_content, is_active) VALUES
|
||||||
('amnezia-wg', 'AmneziaWG', 'Amnezia WireGuard implementation', '{}', 0, 1),
|
('amnezia-wg', 'AmneziaWG', 'Amnezia WireGuard implementation', '{}', 0, 1),
|
||||||
('amnezia-xray', 'Amnezia XRay', 'XRay (VLESS/Reality)', '{"scripts":{}}', 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),
|
('shadowsocks', 'Shadowsocks', 'Shadowsocks proxy', '{}', 0, 1),
|
||||||
('cloak', 'Cloak', 'Cloak obfuscation', '{}', 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 @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"');
|
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;
|
PREPARE stmt FROM @sql;
|
||||||
EXECUTE stmt;
|
EXECUTE stmt;
|
||||||
DEALLOCATE PREPARE 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 (
|
CREATE TABLE IF NOT EXISTS server_protocols (
|
||||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||||
server_id INT UNSIGNED NOT NULL,
|
server_id INT UNSIGNED NOT NULL,
|
||||||
|
|||||||
@@ -1,5 +1,123 @@
|
|||||||
-- Enable Stats and API for XRay VLESS protocol
|
-- Enable Stats and API for XRay VLESS protocol
|
||||||
-- This allows collecting traffic usage per user
|
-- 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 <<EOJSON
|
||||||
|
{
|
||||||
|
"log": { "loglevel": "warning" },
|
||||||
|
"stats": {},
|
||||||
|
"api": {
|
||||||
|
"tag": "api",
|
||||||
|
"services": [ "StatsService" ]
|
||||||
|
},
|
||||||
|
"policy": {
|
||||||
|
"levels": {
|
||||||
|
"0": {
|
||||||
|
"statsUserUplink": true,
|
||||||
|
"statsUserDownlink": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"system": {
|
||||||
|
"statsInboundUplink": true,
|
||||||
|
"statsInboundDownlink": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"inbounds": [{
|
||||||
|
"listen": "0.0.0.0",
|
||||||
|
"port": ${XRAY_PORT},
|
||||||
|
"protocol": "vless",
|
||||||
|
"settings": {
|
||||||
|
"clients": [{ "id": "${CLIENT_ID}", "flow": "xtls-rprx-vision", "email": "${CLIENT_ID}" }],
|
||||||
|
"decryption": "none"
|
||||||
|
},
|
||||||
|
"streamSettings": {
|
||||||
|
"network": "tcp",
|
||||||
|
"security": "reality",
|
||||||
|
"realitySettings": {
|
||||||
|
"show": false,
|
||||||
|
"dest": "${SERVER_NAME}:443",
|
||||||
|
"xver": 0,
|
||||||
|
"serverNames": ["${SERVER_NAME}"],
|
||||||
|
"privateKey": "${PRIVATE_KEY}",
|
||||||
|
"shortIds": ["${SHORT_ID}"],
|
||||||
|
"fingerprint": "${FINGERPRINT}",
|
||||||
|
"spiderX": "${SPIDER_X}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"listen": "127.0.0.1",
|
||||||
|
"port": 10085,
|
||||||
|
"protocol": "dokodemo-door",
|
||||||
|
"tag": "api",
|
||||||
|
"settings": {
|
||||||
|
"address": "127.0.0.1"
|
||||||
|
}
|
||||||
|
}],
|
||||||
|
"outbounds": [{ "protocol": "freedom", "tag": "direct" }],
|
||||||
|
"routing": {
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"inboundTag": [ "api" ],
|
||||||
|
"outboundTag": "api",
|
||||||
|
"type": "field"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOJSON
|
||||||
|
|
||||||
|
docker run -d --name "$CONTAINER_NAME" --restart always -p "${XRAY_PORT}:${XRAY_PORT}" -v /opt/amnezia/xray:/opt/amnezia/xray teddysun/xray xray run -c /opt/amnezia/xray/server.json
|
||||||
|
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
echo "XrayPort: ${XRAY_PORT}"
|
||||||
|
echo "Port: ${XRAY_PORT}"
|
||||||
|
echo "ClientID: ${CLIENT_ID}"
|
||||||
|
echo "PublicKey: ${PUBLIC_KEY}"
|
||||||
|
echo "PrivateKey: ${PRIVATE_KEY}"
|
||||||
|
echo "ShortID: ${SHORT_ID}"
|
||||||
|
echo "ServerName: ${SERVER_NAME}"
|
||||||
|
echo "ContainerName: ${CONTAINER_NAME}"
|
||||||
|
'
|
||||||
WHERE slug = 'xray-vless';
|
WHERE slug = 'xray-vless';
|
||||||
|
|||||||
Reference in New Issue
Block a user