fix traffic reboot

This commit is contained in:
infosave2007
2026-01-30 19:27:02 +03:00
parent f4fa6ec941
commit e90e3a8df2
3 changed files with 296 additions and 75 deletions
+77 -13
View File
@@ -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
View File
@@ -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 {