fix traffic reboot
This commit is contained in:
@@ -1246,28 +1246,92 @@ class InstallProtocolManager
|
||||
|
||||
// Check if client exists
|
||||
$clients = &$config['inbounds'][0]['settings']['clients'];
|
||||
foreach ($clients as $c) {
|
||||
$duplicateFound = false;
|
||||
foreach ($clients as $k => $c) {
|
||||
if (($c['id'] ?? '') === $clientId) {
|
||||
// Already exists
|
||||
// Already exists by ID (exact match)
|
||||
Logger::appendInstall($server->getId(), "Client $clientId already exists in X-Ray config");
|
||||
return ['success' => true, 'message' => 'Client already exists'];
|
||||
}
|
||||
}
|
||||
if (($c['email'] ?? '') === (!empty($options['login']) ? $options['login'] : $clientId)) {
|
||||
// Email conflict! (Different ID but same email)
|
||||
// This happens if user re-adds a client with same login but new UUID (after deleting from DB)
|
||||
Logger::appendInstall($server->getId(), "Client email already exists in X-Ray config. Updating ID/Level.");
|
||||
|
||||
// Add client
|
||||
$email = !empty($options['login']) ? $options['login'] : $clientId;
|
||||
$newClient = ['id' => $clientId, 'email' => $email];
|
||||
// Update existing client entry with new UUID
|
||||
$clients[$k]['id'] = $clientId;
|
||||
$clients[$k]['level'] = 0; // Ensure level 0
|
||||
|
||||
// Detect flow from other clients or default
|
||||
$flow = 'xtls-rprx-vision'; // Default for Reality
|
||||
if (!empty($clients)) {
|
||||
if (isset($clients[0]['flow'])) {
|
||||
$flow = $clients[0]['flow'];
|
||||
$duplicateFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
$newClient['flow'] = $flow;
|
||||
|
||||
$clients[] = $newClient;
|
||||
if (!$duplicateFound) {
|
||||
// Add new client (no conflict)
|
||||
$email = !empty($options['login']) ? $options['login'] : $clientId;
|
||||
$newClient = ['id' => $clientId, 'email' => $email];
|
||||
|
||||
// Detect flow from other clients or default
|
||||
$flow = 'xtls-rprx-vision'; // Default for Reality
|
||||
if (!empty($clients)) {
|
||||
if (isset($clients[0]['flow'])) {
|
||||
$flow = $clients[0]['flow'];
|
||||
}
|
||||
}
|
||||
$newClient['flow'] = $flow;
|
||||
$newClient['level'] = 0; // Explicitly set level 0
|
||||
|
||||
$clients[] = $newClient;
|
||||
}
|
||||
|
||||
// Fix JSON encoding issues (empty objects becoming arrays)
|
||||
if (isset($config['stats']) && empty($config['stats'])) {
|
||||
$config['stats'] = new stdClass();
|
||||
}
|
||||
if (isset($config['policy']['levels']) && is_array($config['policy']['levels'])) {
|
||||
// Check if it's an indexed array (0, 1...) which is wrong for X-ray levels map
|
||||
if (array_keys($config['policy']['levels']) === range(0, count($config['policy']['levels']) - 1)) {
|
||||
$newLevels = new stdClass();
|
||||
foreach ($config['policy']['levels'] as $idx => $lvl) {
|
||||
$newLevels->{(string) $idx} = $lvl;
|
||||
}
|
||||
$config['policy']['levels'] = $newLevels;
|
||||
} elseif (empty($config['policy']['levels'])) {
|
||||
$config['policy']['levels'] = new stdClass();
|
||||
}
|
||||
} else {
|
||||
if (!isset($config['policy'])) {
|
||||
$config['policy'] = new stdClass();
|
||||
}
|
||||
if (!isset($config['policy']['levels'])) {
|
||||
$config['policy']['levels'] = new stdClass();
|
||||
}
|
||||
}
|
||||
|
||||
// Enforce Level 0 Policy with limitIp
|
||||
if (!isset($config['policy']['levels']->{'0'})) {
|
||||
$config['policy']['levels']->{'0'} = new stdClass();
|
||||
}
|
||||
$level0 = $config['policy']['levels']->{'0'};
|
||||
// Cast to object if array
|
||||
if (is_array($level0)) {
|
||||
$level0 = (object) $level0;
|
||||
$config['policy']['levels']->{'0'} = $level0;
|
||||
}
|
||||
|
||||
// Set restriction parameters
|
||||
$level0->limitIp = 1;
|
||||
$level0->handshake = 4;
|
||||
$level0->connIdle = 300;
|
||||
$level0->uplinkOnly = 2;
|
||||
$level0->downlinkOnly = 5;
|
||||
$level0->statsUserUplink = true;
|
||||
$level0->statsUserDownlink = true;
|
||||
$level0->bufferSize = 4;
|
||||
// It's an assoc array, duplicate it to stdClass to ensure object encoding
|
||||
$config['policy']['levels'] = (object) $config['policy']['levels'];
|
||||
|
||||
|
||||
// 3. Write config back
|
||||
$newJson = json_encode($config, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
|
||||
|
||||
+122
-31
@@ -14,6 +14,69 @@ class ServerMonitoring
|
||||
{
|
||||
private VpnServer $server;
|
||||
private array $serverData;
|
||||
private array $xrayStatsCache = [];
|
||||
private bool $xrayStatsFetched = false;
|
||||
|
||||
/**
|
||||
* Fetch all X-ray user stats in one batch
|
||||
* Returns true on success, false on failure (SSH / JSON error)
|
||||
*/
|
||||
private function fetchXrayStats(): bool
|
||||
{
|
||||
if ($this->xrayStatsFetched) {
|
||||
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
|
||||
// Note: The container name is hardcoded to 'amnezia-xray' in the provided snippet.
|
||||
// Assuming this is intentional or will be corrected by the user later.
|
||||
$cmd = "docker exec amnezia-xray xray api statsquery --pattern 'user>>>' --reset=true --server=127.0.0.1:10085";
|
||||
$json = $this->execSSH($cmd);
|
||||
|
||||
if (!$json || trim($json) === '') {
|
||||
// Assuming a log method exists or needs to be added, for now, using error_log
|
||||
error_log("Failed to fetch X-ray stats (empty response)");
|
||||
return false;
|
||||
}
|
||||
|
||||
$data = json_decode($json, true);
|
||||
if (!isset($data['stat'])) {
|
||||
// If empty stats, but successful connection, it's fine (just no traffic delta)
|
||||
$this->xrayStatsCache = [];
|
||||
$this->xrayStatsFetched = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
$stats = [];
|
||||
foreach ($data['stat'] as $item) {
|
||||
// "user>>>email>>>traffic>>>downlink"
|
||||
$parts = explode('>>>', $item['name']);
|
||||
if (count($parts) >= 4) {
|
||||
$email = $parts[1];
|
||||
$type = $parts[3]; // 'downlink' or 'uplink'
|
||||
|
||||
if (!isset($stats[$email])) {
|
||||
$stats[$email] = ['up' => 0, 'down' => 0];
|
||||
}
|
||||
|
||||
if ($type === 'uplink') {
|
||||
$stats[$email]['up'] += (int) $item['value'];
|
||||
} elseif ($type === 'downlink') {
|
||||
$stats[$email]['down'] += (int) $item['value'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->xrayStatsCache = $stats;
|
||||
$this->xrayStatsFetched = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
public function __construct(int $serverId)
|
||||
{
|
||||
@@ -46,6 +109,12 @@ class ServerMonitoring
|
||||
*/
|
||||
public function collectClientMetrics(): array
|
||||
{
|
||||
// Pre-fetch X-ray stats
|
||||
if (!$this->fetchXrayStats()) {
|
||||
error_log("Failed to fetch X-ray stats, preventing DB overwrite");
|
||||
return []; // Abort if stats collection failed
|
||||
}
|
||||
|
||||
$clients = VpnClient::listByServer($this->serverData['id']);
|
||||
$results = [];
|
||||
|
||||
@@ -55,6 +124,12 @@ class ServerMonitoring
|
||||
|
||||
$stats = $this->getClientStats($client);
|
||||
if ($stats) {
|
||||
// Check if speed values are excessively high (spike detection)
|
||||
// Use 10Gbps (1250 MB/s) as sanity limit. 1250 * 1024 * 1024 ~ 1.3e9
|
||||
// Actually ServerMonitoring calculates bytes/sec.
|
||||
// If speed is > 2 Gbit/s likely an error (unless on 10G link, but rare)
|
||||
// Let's rely on simple positive check for now.
|
||||
|
||||
$this->saveClientMetrics($client['id'], $stats);
|
||||
$results[] = [
|
||||
'client_id' => $client['id'],
|
||||
@@ -181,45 +256,61 @@ class ServerMonitoring
|
||||
private function getClientStats(array $client): ?array
|
||||
{
|
||||
$db = DB::conn();
|
||||
// this->fetchXrayStats() call moved to collectClientMetrics to handle failure gracefully
|
||||
|
||||
// Get current stats from server
|
||||
$containerName = $this->serverData['container_name'];
|
||||
$bytesReceived = 0;
|
||||
$bytesSent = 0;
|
||||
|
||||
if (strpos($containerName, 'xray') !== false) {
|
||||
// X-Ray Logic
|
||||
$identifier = null;
|
||||
// Best effort to find UUID/Email
|
||||
if (!empty($client['config']) && preg_match('/vless:\\/\\/([0-9a-fA-F-]{36})@/i', $client['config'], $m)) {
|
||||
$identifier = $m[1];
|
||||
} elseif (!empty($client['name'])) { // Often name IS the UUID for XRay
|
||||
$identifier = $client['name'];
|
||||
}
|
||||
$slug = $this->serverData['slug']; // Assuming 'slug' is available in serverData
|
||||
|
||||
if ($identifier) {
|
||||
// Query X-Ray API
|
||||
$cmd = sprintf(
|
||||
"docker exec %s xray api statsquery --server=127.0.0.1:10085 --pattern 'user>>>%s>>>traffic>>>' 2>/dev/null",
|
||||
escapeshellarg($containerName),
|
||||
escapeshellarg($identifier)
|
||||
);
|
||||
if ($slug === 'xray' || $slug === 'vless') {
|
||||
// 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)
|
||||
|
||||
$json = $this->execSSH($cmd);
|
||||
if ($json) {
|
||||
$data = json_decode($json, true);
|
||||
if (isset($data['stat']) && is_array($data['stat'])) {
|
||||
foreach ($data['stat'] as $row) {
|
||||
if (strpos($row['name'], '>>>uplink') !== false) {
|
||||
$bytesSent = (int) $row['value'];
|
||||
}
|
||||
if (strpos($row['name'], '>>>downlink') !== false) {
|
||||
$bytesReceived = (int) $row['value'];
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// SSH command failed or returned empty for X-Ray stats
|
||||
// In our previous fetchXrayStats, we keyed by $parts[1].
|
||||
|
||||
$key = $client['id']; // UUID
|
||||
if (!isset($this->xrayStatsCache[$key])) {
|
||||
// Try name
|
||||
$key = $client['name'];
|
||||
}
|
||||
|
||||
if (isset($this->xrayStatsCache[$key])) {
|
||||
$xStats = $this->xrayStatsCache[$key];
|
||||
|
||||
// CRITICAL FIX: Add DELTA to existing DB values
|
||||
// We need to get the current total bytes from the DB first
|
||||
$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) + (int) $xStats['up'];
|
||||
$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):
|
||||
$speedUp = round($xStats['up'] / 60);
|
||||
$speedDown = round($xStats['down'] / 60);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user