feat: Implement single IP enforcement for Xray VLESS protocol with online tracking

This commit is contained in:
infosave2007
2026-01-30 20:09:39 +03:00
parent e90e3a8df2
commit 28a6de5697
3 changed files with 279 additions and 8 deletions
+39 -5
View File
@@ -1222,15 +1222,49 @@ class InstallProtocolManager
$config['policy']['levels']['0'] = [];
}
// Enforce limitIp: 1 for user level 0
// Enforce stats and online tracking for user level 0
$config['policy']['levels']['0']['handshake'] = 4;
$config['policy']['levels']['0']['connIdle'] = 300;
$config['policy']['levels']['0']['uplinkOnly'] = 2;
$config['policy']['levels']['0']['downlinkOnly'] = 5;
$config['policy']['levels']['0']['statsUserUplink'] = true;
$config['policy']['levels']['0']['statsUserDownlink'] = true;
$config['policy']['levels']['0']['statsUserOnline'] = true; // Enable online tracking
$config['policy']['levels']['0']['bufferSize'] = 4;
$config['policy']['levels']['0']['limitIp'] = 1; // Enforce 1 IP per user
// Ensure API services include StatsService and RoutingService
if (!isset($config['api'])) {
$config['api'] = ['tag' => 'api', 'services' => []];
}
if (!isset($config['api']['services'])) {
$config['api']['services'] = [];
}
if (!in_array('StatsService', $config['api']['services'])) {
$config['api']['services'][] = 'StatsService';
}
if (!in_array('RoutingService', $config['api']['services'])) {
$config['api']['services'][] = 'RoutingService';
}
// Ensure blocked outbound exists for IP blocking
if (!isset($config['outbounds'])) {
$config['outbounds'] = [];
}
$hasBlocked = false;
foreach ($config['outbounds'] as $ob) {
if (($ob['tag'] ?? '') === 'blocked') {
$hasBlocked = true;
break;
}
}
if (!$hasBlocked) {
$config['outbounds'][] = ['protocol' => 'blackhole', 'tag' => 'blocked'];
}
// Ensure main inbound has a tag for routing rules
if (!isset($config['inbounds'][0]['tag'])) {
$config['inbounds'][0]['tag'] = 'vless-in';
}
// Assuming VLESS structure: inbounds[0] -> settings -> clients
@@ -1309,7 +1343,7 @@ class InstallProtocolManager
}
}
// Enforce Level 0 Policy with limitIp
// Enforce Level 0 Policy with online tracking
if (!isset($config['policy']['levels']->{'0'})) {
$config['policy']['levels']->{'0'} = new stdClass();
}
@@ -1320,14 +1354,14 @@ class InstallProtocolManager
$config['policy']['levels']->{'0'} = $level0;
}
// Set restriction parameters
$level0->limitIp = 1;
// Set restriction parameters (statsUserOnline enables connection counting)
$level0->handshake = 4;
$level0->connIdle = 300;
$level0->uplinkOnly = 2;
$level0->downlinkOnly = 5;
$level0->statsUserUplink = true;
$level0->statsUserDownlink = true;
$level0->statsUserOnline = true; // Enable online tracking for enforcement
$level0->bufferSize = 4;
// It's an assoc array, duplicate it to stdClass to ensure object encoding
$config['policy']['levels'] = (object) $config['policy']['levels'];
+109 -3
View File
@@ -34,9 +34,12 @@ class ServerMonitoring
}
// 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";
$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";
$json = $this->execSSH($cmd);
if (!$json || trim($json) === '') {
@@ -109,6 +112,15 @@ class ServerMonitoring
*/
public function collectClientMetrics(): array
{
// Enforce single IP per user for Xray before collecting stats
if ($this->isXrayServer()) {
try {
$this->enforceXraySingleIpPerUser();
} catch (Throwable $e) {
error_log("Xray enforcement error: " . $e->getMessage());
}
}
// Pre-fetch X-ray stats
if (!$this->fetchXrayStats()) {
error_log("Failed to fetch X-ray stats, preventing DB overwrite");
@@ -530,4 +542,98 @@ class ServerMonitoring
return $output ?: null;
}
/**
* Get Xray container name for this server
* @return string|null Container name or null if not an Xray server
*/
private function getXrayContainerName(): ?string
{
$containerName = $this->serverData['container_name'] ?? '';
// Check if this is an Xray server
if (stripos($containerName, 'xray') !== false) {
return $containerName;
}
// Also check slug
$slug = $this->serverData['slug'] ?? '';
if (stripos($slug, 'xray') !== false || stripos($slug, 'vless') !== false) {
return $containerName ?: 'amnezia-xray';
}
return null;
}
/**
* Check if this server is an Xray server
*/
private function isXrayServer(): bool
{
return $this->getXrayContainerName() !== null;
}
/**
* Enforce single IP per user for Xray connections
* If a user is connected from multiple IPs, block all but the first one
*/
public function enforceXraySingleIpPerUser(): void
{
$xrayContainer = $this->getXrayContainerName();
if (!$xrayContainer) {
return; // Not an Xray server
}
// Get all online users
$cmd = "docker exec $xrayContainer xray api statsgetallonlineusers --server=127.0.0.1:10085";
$result = $this->execSSH($cmd);
if (!$result) {
return;
}
$data = json_decode($result, true);
if (!isset($data['users']) || !is_array($data['users'])) {
return;
}
$ipsToBlock = [];
foreach ($data['users'] as $user) {
$email = $user['email'] ?? null;
if (!$email) {
continue;
}
// Get IP list for this user
$ipCmd = "docker exec $xrayContainer xray api statsonlineiplist --server=127.0.0.1:10085 --email=" . escapeshellarg($email);
$ipResult = $this->execSSH($ipCmd);
if (!$ipResult) {
continue;
}
$ipData = json_decode($ipResult, true);
if (!isset($ipData['ips']) || !is_array($ipData['ips'])) {
continue;
}
// If more than 1 IP, block all but the first (oldest by timestamp)
if (count($ipData['ips']) > 1) {
// Sort by timestamp (value) ascending
asort($ipData['ips']);
$first = true;
foreach ($ipData['ips'] as $ip => $timestamp) {
if ($first) {
$first = false;
continue; // Keep first IP
}
$ipsToBlock[] = $ip;
}
}
}
// Block collected IPs
if (!empty($ipsToBlock)) {
$ipList = implode(' ', array_unique($ipsToBlock));
$blockCmd = "docker exec $xrayContainer xray api sib --server=127.0.0.1:10085 -outbound=blocked -inbound=vless-in -reset $ipList";
$this->execSSH($blockCmd);
error_log("[Xray Enforcement] Blocked IPs: $ipList");
}
}
}