feat: Enhance online client tracking by including recent handshake counts for WireGuard/AWG
This commit is contained in:
@@ -67,3 +67,4 @@ test_protocols.php
|
||||
scripts/regen_qr.php
|
||||
scripts/test_xray_install.sh
|
||||
scripts/test_online.php
|
||||
API_AWG_DOCS.md
|
||||
|
||||
+144
-93
@@ -27,19 +27,11 @@ class ServerMonitoring
|
||||
return true;
|
||||
}
|
||||
|
||||
$containerName = $this->serverData['container_name'];
|
||||
if (strpos($containerName, 'xray') === false) {
|
||||
$this->xrayStatsFetched = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Use --reset=true to get delta since last check and prevent counter reset on restart
|
||||
$xrayContainer = $this->getXrayContainerName();
|
||||
if (!$xrayContainer) {
|
||||
$this->xrayStatsFetched = true;
|
||||
return true; // Not an Xray server
|
||||
}
|
||||
$cmd = "docker exec $xrayContainer xray api statsquery --pattern 'user>>>' --reset=true --server=127.0.0.1:10085";
|
||||
// Always try to fetch from amnezia-xray container
|
||||
// Even if server's container_name is different, there may be xray clients
|
||||
$xrayContainer = $this->getXrayContainerName() ?? 'amnezia-xray';
|
||||
|
||||
$cmd = "docker exec $xrayContainer xray api statsquery --pattern 'user>>>' --reset=true --server=127.0.0.1:10085 2>/dev/null";
|
||||
$json = $this->execSSH($cmd);
|
||||
|
||||
if (!$json || trim($json) === '') {
|
||||
@@ -274,24 +266,40 @@ class ServerMonitoring
|
||||
$containerName = $this->serverData['container_name'];
|
||||
$bytesReceived = 0;
|
||||
$bytesSent = 0;
|
||||
$speedUp = 0;
|
||||
$speedDown = 0;
|
||||
|
||||
$protocol = $this->serverData['install_protocol'] ?? '';
|
||||
// Determine if this client is XRay based on protocol_id
|
||||
$isXrayClient = false;
|
||||
if (!empty($client['protocol_id'])) {
|
||||
$stmtProto = $db->prepare('SELECT slug FROM protocols WHERE id = ?');
|
||||
$stmtProto->execute([$client['protocol_id']]);
|
||||
$protoData = $stmtProto->fetch();
|
||||
if ($protoData && stripos($protoData['slug'], 'xray') !== false) {
|
||||
$isXrayClient = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: check config for vless URI
|
||||
if (!$isXrayClient && !empty($client['config']) && strpos($client['config'], 'vless://') !== false) {
|
||||
$isXrayClient = true;
|
||||
}
|
||||
|
||||
if (strpos($protocol, 'xray') !== false || strpos($protocol, 'vless') !== false) {
|
||||
if ($isXrayClient) {
|
||||
// Retrieve DELTA from cache
|
||||
if ($this->xrayStatsFetched) {
|
||||
// Try to find by UUID first (if we tracked it) or Email/Name
|
||||
// Our cache is keyed by "email" from the stats query "user>>>email>>>..."
|
||||
// In VpnClient.php, the X-ray config uses client 'id' (uuid) as 'id' and 'email' as 'email'.
|
||||
// Usually Amnezia sets email = uuid or name.
|
||||
// Let's try keys: client['id'], client['name'], client['email'] (if exists)
|
||||
|
||||
// In our previous fetchXrayStats, we keyed by $parts[1].
|
||||
|
||||
$key = $client['id']; // UUID
|
||||
// Try name first (matches email in xray config), then UUID from config
|
||||
$key = $client['name'];
|
||||
if (!isset($this->xrayStatsCache[$key])) {
|
||||
// Try name
|
||||
$key = $client['name'];
|
||||
// Try UUID from config
|
||||
if (!empty($client['config']) && preg_match('/vless:\/\/([0-9a-fA-F-]{36})@/i', $client['config'], $m)) {
|
||||
$key = $m[1];
|
||||
}
|
||||
}
|
||||
|
||||
if (!isset($this->xrayStatsCache[$key])) {
|
||||
// Try client['id'] as last resort
|
||||
$key = $client['id'];
|
||||
}
|
||||
|
||||
if (isset($this->xrayStatsCache[$key])) {
|
||||
@@ -307,32 +315,42 @@ class ServerMonitoring
|
||||
$bytesReceived = ($currentDbStats['bytes_received'] ?? 0) + (int) $xStats['down'];
|
||||
|
||||
// Calculate speed based on DELTA (since Reset=true, value IS the delta since last check)
|
||||
// If we check every 60s, speed = delta / 60.
|
||||
// But exact interval varies.
|
||||
// For now, let's trust the delta.
|
||||
|
||||
// Simple speed aproximation: Delta / (Now - LastCheck)
|
||||
// But we don't have exact LastCheck time per client easily here.
|
||||
// However, sparklines use a separate API.
|
||||
// The 'speed_up'/'speed_down' columns in DB are usually "Current Speed".
|
||||
// If we just gathered a delta over X seconds...
|
||||
// Let's approximate: X-ray stats delta.
|
||||
// We can just store the 'current speed' as calculated by (Delta Bytes / Interval).
|
||||
// But we don't know the exact interval since the LAST fetch was run by the cron.
|
||||
// Assuming cron runs every minute?
|
||||
// If we assume 1 minute (60s):
|
||||
// Assuming cron runs every minute (60s):
|
||||
$speedUp = round($xStats['up'] / 60);
|
||||
$speedDown = round($xStats['down'] / 60);
|
||||
} else {
|
||||
// No stats in cache, use current DB values
|
||||
$stmt = $db->prepare("SELECT bytes_sent, bytes_received FROM vpn_clients WHERE id = ?");
|
||||
$stmt->execute([$client['id']]);
|
||||
$currentDbStats = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
$bytesSent = $currentDbStats['bytes_sent'] ?? 0;
|
||||
$bytesReceived = $currentDbStats['bytes_received'] ?? 0;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// WireGuard Logic
|
||||
// WireGuard Logic - get bytes and handshake timestamp
|
||||
$publicKey = $client['public_key'];
|
||||
$cmd = "docker exec {$containerName} wg show all dump | grep '{$publicKey}' | awk '{print \$6, \$7}'";
|
||||
// wg show all dump format (tab-separated):
|
||||
// $1=interface $2=pubkey $3=psk $4=endpoint $5=allowed-ips $6=latest-handshake $7=rx-bytes $8=tx-bytes $9=keepalive
|
||||
// rx-bytes = bytes received by server = client's upload (bytes_sent)
|
||||
// tx-bytes = bytes transmitted by server = client's download (bytes_received)
|
||||
$cmd = "docker exec {$containerName} wg show all dump | grep '{$publicKey}' | awk '{print \$6, \$7, \$8}'";
|
||||
$result = $this->execSSH($cmd);
|
||||
|
||||
if ($result) {
|
||||
list($bytesReceived, $bytesSent) = explode(' ', trim($result));
|
||||
$parts = explode(' ', trim($result));
|
||||
if (count($parts) >= 3) {
|
||||
$handshakeTs = (int)$parts[0];
|
||||
$bytesSent = (int)$parts[1]; // server's rx = client's upload
|
||||
$bytesReceived = (int)$parts[2]; // server's tx = client's download
|
||||
|
||||
// Update last_handshake if there was a recent handshake
|
||||
if ($handshakeTs > 0) {
|
||||
$handshakeDate = date('Y-m-d H:i:s', $handshakeTs);
|
||||
$stmtHs = $db->prepare("UPDATE vpn_clients SET last_handshake = ? WHERE id = ?");
|
||||
$stmtHs->execute([$handshakeDate, $client['id']]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -425,10 +443,10 @@ class ServerMonitoring
|
||||
$stats['speed_down_kbps'],
|
||||
]);
|
||||
|
||||
// Update vpn_clients table with latest stats
|
||||
// Update vpn_clients table with latest stats (don't touch last_handshake - it's set separately for WG/AWG)
|
||||
$stmt = $db->prepare("
|
||||
UPDATE vpn_clients
|
||||
SET bytes_sent = ?, bytes_received = ?, speed_up = ?, speed_down = ?, current_speed = ?, last_handshake = NOW(), last_sync_at = NOW()
|
||||
SET bytes_sent = ?, bytes_received = ?, speed_up = ?, speed_down = ?, current_speed = ?, last_sync_at = NOW()
|
||||
WHERE id = ?
|
||||
");
|
||||
|
||||
@@ -785,11 +803,23 @@ class ServerMonitoring
|
||||
|
||||
// Get all active servers
|
||||
$servers = VpnServer::listAll();
|
||||
$db = DB::conn();
|
||||
|
||||
foreach ($servers as $serverData) {
|
||||
// Check if this is an Xray server
|
||||
// Check if this server has any XRay clients
|
||||
$stmt = $db->prepare("
|
||||
SELECT COUNT(*) as cnt FROM vpn_clients vc
|
||||
JOIN protocols p ON vc.protocol_id = p.id
|
||||
WHERE vc.server_id = ? AND p.slug LIKE '%xray%'
|
||||
");
|
||||
$stmt->execute([$serverData['id']]);
|
||||
$hasXrayClients = (int)$stmt->fetchColumn() > 0;
|
||||
|
||||
// Also check container_name as fallback
|
||||
$containerName = $serverData['container_name'] ?? '';
|
||||
if (strpos($containerName, 'xray') === false) {
|
||||
$isXrayServer = strpos($containerName, 'xray') !== false;
|
||||
|
||||
if (!$hasXrayClients && !$isXrayServer) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -799,7 +829,7 @@ class ServerMonitoring
|
||||
$username = $serverData['username'] ?? 'root';
|
||||
$password = $serverData['password'] ?? '';
|
||||
|
||||
$xrayContainer = $containerName ?: 'amnezia-xray';
|
||||
$xrayContainer = $isXrayServer ? $containerName : 'amnezia-xray';
|
||||
$cmd = "docker exec $xrayContainer xray api statsgetallonlineusers --server=127.0.0.1:10085";
|
||||
|
||||
$sshOptions = '-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=5';
|
||||
@@ -861,58 +891,79 @@ class ServerMonitoring
|
||||
public static function getOnlineClientsForServer(array $serverData): array
|
||||
{
|
||||
$result = [];
|
||||
$db = DB::conn();
|
||||
|
||||
// 1. Get XRay online clients from Xray API
|
||||
$stmt = $db->prepare("
|
||||
SELECT COUNT(*) as cnt FROM vpn_clients vc
|
||||
JOIN protocols p ON vc.protocol_id = p.id
|
||||
WHERE vc.server_id = ? AND p.slug LIKE '%xray%'
|
||||
");
|
||||
$stmt->execute([$serverData['id']]);
|
||||
$hasXrayClients = (int)$stmt->fetchColumn() > 0;
|
||||
|
||||
// Check if this is an Xray server
|
||||
$containerName = $serverData['container_name'] ?? '';
|
||||
if (strpos($containerName, 'xray') === false) {
|
||||
return $result;
|
||||
}
|
||||
$isXrayServer = strpos($containerName, 'xray') !== false;
|
||||
|
||||
// Build SSH command
|
||||
$host = $serverData['host'];
|
||||
$port = (int)($serverData['port'] ?? 22);
|
||||
$username = $serverData['username'] ?? 'root';
|
||||
$password = $serverData['password'] ?? '';
|
||||
|
||||
$xrayContainer = $containerName ?: 'amnezia-xray';
|
||||
$cmd = "docker exec $xrayContainer xray api statsgetallonlineusers --server=127.0.0.1:10085";
|
||||
|
||||
$sshOptions = '-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=5';
|
||||
$sshCmd = sprintf(
|
||||
"sshpass -p '%s' ssh -p %d %s %s@%s %s 2>/dev/null",
|
||||
$password,
|
||||
$port,
|
||||
$sshOptions,
|
||||
$username,
|
||||
$host,
|
||||
escapeshellarg($cmd)
|
||||
);
|
||||
|
||||
$output = shell_exec($sshCmd);
|
||||
if (!$output) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
$data = json_decode($output, true);
|
||||
if (!isset($data['users']) || !is_array($data['users'])) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
foreach ($data['users'] as $user) {
|
||||
// Parse format: "user>>>email>>>online"
|
||||
if (is_string($user)) {
|
||||
$parts = explode('>>>', $user);
|
||||
if (count($parts) >= 2) {
|
||||
$result[] = $parts[1];
|
||||
}
|
||||
} else {
|
||||
$email = $user['email'] ?? null;
|
||||
if ($email) {
|
||||
$result[] = $email;
|
||||
if ($hasXrayClients || $isXrayServer) {
|
||||
$host = $serverData['host'];
|
||||
$port = (int)($serverData['port'] ?? 22);
|
||||
$username = $serverData['username'] ?? 'root';
|
||||
$password = $serverData['password'] ?? '';
|
||||
|
||||
$xrayContainer = $isXrayServer ? $containerName : 'amnezia-xray';
|
||||
$cmd = "docker exec $xrayContainer xray api statsgetallonlineusers --server=127.0.0.1:10085";
|
||||
|
||||
$sshOptions = '-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=5';
|
||||
$sshCmd = sprintf(
|
||||
"sshpass -p '%s' ssh -p %d %s %s@%s %s 2>/dev/null",
|
||||
$password,
|
||||
$port,
|
||||
$sshOptions,
|
||||
$username,
|
||||
$host,
|
||||
escapeshellarg($cmd)
|
||||
);
|
||||
|
||||
$output = shell_exec($sshCmd);
|
||||
if ($output) {
|
||||
$data = json_decode($output, true);
|
||||
if (isset($data['users']) && is_array($data['users'])) {
|
||||
foreach ($data['users'] as $user) {
|
||||
if (is_string($user)) {
|
||||
$parts = explode('>>>', $user);
|
||||
if (count($parts) >= 2) {
|
||||
$result[] = $parts[1];
|
||||
}
|
||||
} else {
|
||||
$email = $user['email'] ?? null;
|
||||
if ($email) {
|
||||
$result[] = $email;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Add WireGuard/AWG clients with recent handshake (< 5 minutes)
|
||||
// Exclude XRay clients - they use Xray API for online status
|
||||
$stmt = $db->prepare("
|
||||
SELECT vc.name FROM vpn_clients vc
|
||||
LEFT JOIN protocols p ON vc.protocol_id = p.id
|
||||
WHERE vc.server_id = ?
|
||||
AND vc.status = 'active'
|
||||
AND vc.last_handshake IS NOT NULL
|
||||
AND vc.last_handshake >= DATE_SUB(NOW(), INTERVAL 300 SECOND)
|
||||
AND (p.slug IS NULL OR p.slug NOT LIKE '%xray%')
|
||||
");
|
||||
$stmt->execute([$serverData['id']]);
|
||||
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
|
||||
if (!in_array($row['name'], $result)) {
|
||||
$result[] = $row['name'];
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
+46
-9
@@ -1551,21 +1551,58 @@ class VpnClient
|
||||
// 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)
|
||||
// Determine protocol by client's protocol_id
|
||||
$isXray = false;
|
||||
$xrayContainerName = 'amnezia-xray'; // Default XRay container name
|
||||
|
||||
if (!empty($this->data['protocol_id'])) {
|
||||
$stmtProto = $pdo->prepare('SELECT slug FROM protocols WHERE id = ?');
|
||||
$stmtProto->execute([$this->data['protocol_id']]);
|
||||
$protoData = $stmtProto->fetch();
|
||||
if ($protoData && stripos($protoData['slug'], 'xray') !== false) {
|
||||
$isXray = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: check container_name or config for xray indicators
|
||||
if (!$isXray) {
|
||||
$containerName = $serverData['container_name'] ?? '';
|
||||
if (strpos($containerName, 'xray') !== false) {
|
||||
$isXray = true;
|
||||
$xrayContainerName = $containerName;
|
||||
} elseif (!empty($this->data['config']) && strpos($this->data['config'], 'vless://') !== false) {
|
||||
$isXray = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (strpos($containerName, 'xray') !== false) {
|
||||
// Extract UUID from config for XRay (vless://UUID@...)
|
||||
if ($isXray) {
|
||||
// XRay stats are tracked by email field in xray config
|
||||
// Try client name first (typically used as email), then UUID from config as fallback
|
||||
$identifier = null;
|
||||
$uuid = null;
|
||||
|
||||
// Extract UUID from config
|
||||
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'])) {
|
||||
$uuid = $m[1];
|
||||
}
|
||||
|
||||
// Override container_name for XRay stats
|
||||
$xrayServerData = $serverData;
|
||||
$xrayServerData['container_name'] = $xrayContainerName;
|
||||
|
||||
// Try name first (typically matches email in xray config)
|
||||
if (!empty($this->data['name'])) {
|
||||
$identifier = $this->data['name'];
|
||||
$stats = self::getXrayStats($xrayServerData, $identifier);
|
||||
}
|
||||
|
||||
// If no stats found by name, try UUID
|
||||
if ((empty($stats) || ($stats['bytes_sent'] == 0 && $stats['bytes_received'] == 0)) && $uuid) {
|
||||
$identifier = $uuid;
|
||||
$stats = self::getXrayStats($xrayServerData, $identifier);
|
||||
}
|
||||
|
||||
if ($identifier) {
|
||||
$stats = self::getXrayStats($serverData, $identifier);
|
||||
if ($identifier && !empty($stats)) {
|
||||
// Infer online status for XRay: if traffic increased, they are online.
|
||||
// Update last_handshake to NOW() if activity detected.
|
||||
if ($stats['bytes_sent'] > $prevSent || $stats['bytes_received'] > $prevReceived) {
|
||||
|
||||
+14
-1
@@ -267,11 +267,24 @@ Router::get('/dashboard', function () {
|
||||
|
||||
// Get real-time online clients count from Xray API
|
||||
$onlineData = ServerMonitoring::countOnlineClients();
|
||||
|
||||
// Also count clients with recent handshake (within 5 minutes) for WireGuard/AWG
|
||||
$pdo = DB::conn();
|
||||
$stmt = $pdo->query("
|
||||
SELECT COUNT(*) as cnt FROM vpn_clients
|
||||
WHERE last_handshake IS NOT NULL
|
||||
AND last_handshake > DATE_SUB(NOW(), INTERVAL 5 MINUTE)
|
||||
AND status = 'active'
|
||||
");
|
||||
$recentHandshakeCount = (int)$stmt->fetchColumn();
|
||||
|
||||
// Combine both counts (XRay online + recent handshake), avoiding duplicates
|
||||
$totalOnline = max($onlineData['total'], $recentHandshakeCount);
|
||||
|
||||
View::render('dashboard.twig', [
|
||||
'servers' => $servers,
|
||||
'clients' => $clients,
|
||||
'online_count' => $onlineData['total'],
|
||||
'online_count' => $totalOnline,
|
||||
'online_users' => $onlineData['users'],
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -276,8 +276,9 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="px-6 py-4">{{ client.client_ip }}</td>
|
||||
<td class="px-6 py-4" data-client-name="{{ client.name }}" data-client-status="{{ client.status }}">
|
||||
{% if client.name in online_logins %}
|
||||
<td class="px-6 py-4" data-client-name="{{ client.name }}" data-client-status="{{ client.status }}" data-last-handshake="{{ client.last_handshake }}">
|
||||
{% set is_online_by_handshake = client.last_handshake and (("now"|date('U') - client.last_handshake|date('U')) < 300) %}
|
||||
{% if client.name in online_logins or is_online_by_handshake %}
|
||||
<span class="online-badge px-2 py-1 bg-green-100 text-green-800 rounded text-xs"><i class="fas fa-wifi mr-1"></i>Online</span>
|
||||
{% elseif client.status == 'active' %}
|
||||
<span class="status-badge px-2 py-1 bg-gray-100 text-gray-600 rounded text-xs">{{ t('status.active') }}</span>
|
||||
|
||||
Reference in New Issue
Block a user