Fix: Client deletion UI, Enable XRay stats, fix dns_servers schema
This commit is contained in:
@@ -1200,7 +1200,7 @@ class InstallProtocolManager
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add client
|
// Add client
|
||||||
$newClient = ['id' => $clientId];
|
$newClient = ['id' => $clientId, 'email' => $clientId];
|
||||||
|
|
||||||
// Detect flow from other clients or default
|
// Detect flow from other clients or default
|
||||||
$flow = 'xtls-rprx-vision'; // Default for Reality
|
$flow = 'xtls-rprx-vision'; // Default for Reality
|
||||||
|
|||||||
@@ -1439,6 +1439,49 @@ class VpnClient
|
|||||||
return $this->data['qr_code'] ?? '';
|
return $this->data['qr_code'] ?? '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get XRay client stats
|
||||||
|
*/
|
||||||
|
private static function getXrayStats(array $serverData, string $clientId): array
|
||||||
|
{
|
||||||
|
$stats = [
|
||||||
|
'bytes_sent' => 0,
|
||||||
|
'bytes_received' => 0,
|
||||||
|
'last_handshake' => 0 // XRay stats API does not provide handshake time
|
||||||
|
];
|
||||||
|
|
||||||
|
$containerName = $serverData['container_name'] ?? 'amnezia-xray';
|
||||||
|
|
||||||
|
// Command to query stats
|
||||||
|
// We query by email, which should be equal to client ID (UUID)
|
||||||
|
$cmd = sprintf(
|
||||||
|
"docker exec -i %s xray api statsquery --server=127.0.0.1:10085 --pattern 'user>>>%s>>>traffic>>>' 2>/dev/null",
|
||||||
|
escapeshellarg($containerName),
|
||||||
|
escapeshellarg($clientId)
|
||||||
|
);
|
||||||
|
|
||||||
|
$output = self::executeServerCommand($serverData, $cmd, true);
|
||||||
|
|
||||||
|
if (empty($output)) {
|
||||||
|
return $stats;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Output format example:
|
||||||
|
// 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];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $stats;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sync traffic statistics from server
|
* Sync traffic statistics from server
|
||||||
*/
|
*/
|
||||||
@@ -1456,7 +1499,33 @@ class VpnClient
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// XRay stats logic
|
||||||
|
$stats = [];
|
||||||
|
|
||||||
|
// Heuristic: if container name contains 'xray' or protocol slug suggests xray
|
||||||
|
$containerName = $serverData['container_name'] ?? '';
|
||||||
|
// Or better: try to detect protocol from config if container name is vague (but usually amnezia-xray)
|
||||||
|
|
||||||
|
if (strpos($containerName, 'xray') !== false) {
|
||||||
|
$uuid = null;
|
||||||
|
// Try to find UUID in config
|
||||||
|
// 1. Check for JSON format (server.json style or subsets)
|
||||||
|
if (preg_match('/"id":\s*"([0-9a-fA-F-]{36})"/', $this->data['config'] ?? '', $m)) {
|
||||||
|
$uuid = $m[1];
|
||||||
|
}
|
||||||
|
// 2. Check for VLESS URI
|
||||||
|
elseif (preg_match('/vless:\/\/([0-9a-fA-F-]{36})@/', $this->data['config'] ?? '', $m)) {
|
||||||
|
$uuid = $m[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($uuid) {
|
||||||
|
$stats = self::getXrayStats($serverData, $uuid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($stats)) {
|
||||||
$stats = self::getClientStatsFromServer($serverData, $this->data['public_key']);
|
$stats = self::getClientStatsFromServer($serverData, $this->data['public_key']);
|
||||||
|
}
|
||||||
|
|
||||||
$pdo = DB::conn();
|
$pdo = DB::conn();
|
||||||
$stmt = $pdo->prepare('
|
$stmt = $pdo->prepare('
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
-- Enable Stats and API for XRay VLESS protocol
|
||||||
|
-- This allows collecting traffic usage per user
|
||||||
|
|
||||||
|
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}"'
|
||||||
|
WHERE slug = 'xray-vless';
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
-- Add dns_servers column to vpn_servers table if missing
|
||||||
|
-- Needed for correct configuration regeneration
|
||||||
|
|
||||||
|
SET @dbname = DATABASE();
|
||||||
|
SET @tablename = "vpn_servers";
|
||||||
|
SET @columnname = "dns_servers";
|
||||||
|
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 vpn_servers ADD COLUMN dns_servers VARCHAR(255) DEFAULT '1.1.1.1, 1.0.0.1'"
|
||||||
|
));
|
||||||
|
PREPARE alterIfNotExists FROM @preparedStatement;
|
||||||
|
EXECUTE alterIfNotExists;
|
||||||
|
DEALLOCATE PREPARE alterIfNotExists;
|
||||||
@@ -348,7 +348,7 @@
|
|||||||
<a href="/clients/{{ client.id }}" class="text-purple-600 hover:text-purple-800 mr-2">{{ t('servers.view') }}</a>
|
<a href="/clients/{{ client.id }}" class="text-purple-600 hover:text-purple-800 mr-2">{{ t('servers.view') }}</a>
|
||||||
{% if client.status == 'active' %}
|
{% if client.status == 'active' %}
|
||||||
<form method="POST" action="/clients/{{ client.id }}/revoke" style="display:inline;">
|
<form method="POST" action="/clients/{{ client.id }}/revoke" style="display:inline;">
|
||||||
<button type="submit" class="text-orange-600 hover:text-orange-800 mr-2" onclick="return confirm('{{ t('clients.revoke_confirm') }}')">{{ t('clients.revoke') }}</button>
|
<button type="button" class="text-orange-600 hover:text-orange-800 mr-2" onclick="confirmAction(this, '{{ t('clients.revoke_confirm') }}')">{{ t('clients.revoke') }}</button>
|
||||||
</form>
|
</form>
|
||||||
{% else %}
|
{% else %}
|
||||||
<form method="POST" action="/clients/{{ client.id }}/restore" style="display:inline;">
|
<form method="POST" action="/clients/{{ client.id }}/restore" style="display:inline;">
|
||||||
@@ -356,7 +356,7 @@
|
|||||||
</form>
|
</form>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<form method="POST" action="/clients/{{ client.id }}/delete" style="display:inline;">
|
<form method="POST" action="/clients/{{ client.id }}/delete" style="display:inline;">
|
||||||
<button type="submit" class="text-red-600 hover:text-red-800" onclick="return confirm('{{ t('clients.delete_confirm') }}')">{{ t('clients.delete') }}</button>
|
<button type="button" class="text-red-600 hover:text-red-800" onclick="confirmAction(this, '{{ t('clients.delete_confirm') }}')">{{ t('clients.delete') }}</button>
|
||||||
</form>
|
</form>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -370,6 +370,12 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
async function confirmAction(btn, message) {
|
||||||
|
if (await showConfirmModal(message)) {
|
||||||
|
btn.closest('form').submit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function toggleExpirationInput() {
|
function toggleExpirationInput() {
|
||||||
const select = document.getElementById('expirationSelect');
|
const select = document.getElementById('expirationSelect');
|
||||||
const input = document.getElementById('expirationSeconds');
|
const input = document.getElementById('expirationSeconds');
|
||||||
|
|||||||
Reference in New Issue
Block a user