feat(xray): Implement universal client addition with fallback
- Added InstallProtocolManager::addClient and fallback logic for X-Ray VLESS to update server configuration (server.json) and restart container. - Updated VpnClient::create to invoke InstallProtocolManager::addClient for scripted protocols, enabling dynamic user addition. - Ensured UUID generation for X-Ray clients.
This commit is contained in:
@@ -494,6 +494,11 @@ class InstallProtocolManager
|
||||
];
|
||||
}
|
||||
|
||||
public static function addClient(VpnServer $server, array $protocol, array $options = []): array
|
||||
{
|
||||
return self::runScript($server, $protocol, 'add_client', $options);
|
||||
}
|
||||
|
||||
private static function runScript(VpnServer $server, array $protocol, string $phase, array $options = []): array
|
||||
{
|
||||
$definition = $protocol['definition'] ?? [];
|
||||
@@ -503,6 +508,8 @@ class InstallProtocolManager
|
||||
$scripts = $protocol['install_script'] ?? null;
|
||||
} elseif ($phase === 'uninstall') {
|
||||
$scripts = $protocol['uninstall_script'] ?? null;
|
||||
} elseif ($phase === 'add_client' && ($protocol['slug'] ?? '') === 'xray-vless') {
|
||||
return self::runBuiltinXrayAddClient($server, $options);
|
||||
}
|
||||
}
|
||||
if (!$scripts) {
|
||||
@@ -518,6 +525,11 @@ class InstallProtocolManager
|
||||
'message' => 'Скрипт удаления не настроен для протокола'
|
||||
];
|
||||
}
|
||||
if ($phase === 'add_client') {
|
||||
// If no script and no builtin handler, we just skip it (assume not needed or manual)
|
||||
// Or throw generic error? Better return success to not break flow if not implemented for other protocols
|
||||
return ['success' => true, 'message' => 'No add_client script defined'];
|
||||
}
|
||||
throw new Exception('Скрипт ' . $phase . ' не настроен для протокола');
|
||||
}
|
||||
|
||||
@@ -1112,7 +1124,10 @@ class InstallProtocolManager
|
||||
$config = [
|
||||
'server_host' => $server->getData()['host'] ?? null,
|
||||
'server_port' => $port,
|
||||
'extras' => ['password' => $password, 'client_id' => $clientId, 'result' => $res,
|
||||
'extras' => [
|
||||
'password' => $password,
|
||||
'client_id' => $clientId,
|
||||
'result' => $res,
|
||||
'reality_public_key' => $res['reality_public_key'] ?? null,
|
||||
'reality_short_id' => $res['reality_short_id'] ?? null,
|
||||
'reality_server_name' => $res['reality_server_name'] ?? null,
|
||||
@@ -1128,4 +1143,88 @@ class InstallProtocolManager
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
private static function runBuiltinXrayAddClient(VpnServer $server, array $options): array
|
||||
{
|
||||
$clientId = $options['client_id'] ?? null;
|
||||
if (!$clientId) {
|
||||
throw new Exception("Client ID is required for X-Ray add_client");
|
||||
}
|
||||
|
||||
// Default container name if not provided
|
||||
$containerName = 'amnezia-xray';
|
||||
if (!empty($options['container_name'])) {
|
||||
$containerName = $options['container_name'];
|
||||
}
|
||||
|
||||
Logger::appendInstall($server->getId(), "Adding X-Ray client $clientId to container $containerName");
|
||||
|
||||
// 1. Read config
|
||||
$catCmd = "docker exec -i " . escapeshellarg($containerName) . " cat /opt/amnezia/xray/server.json 2>/dev/null";
|
||||
$configRaw = $server->executeCommand($catCmd, true);
|
||||
|
||||
if (trim($configRaw) === '') {
|
||||
$catCmd = "docker exec -i " . escapeshellarg($containerName) . " cat /etc/xray/config.json 2>/dev/null";
|
||||
$configRaw = $server->executeCommand($catCmd, true);
|
||||
}
|
||||
|
||||
if (trim($configRaw) === '') {
|
||||
throw new Exception("Could not read X-Ray config from $containerName");
|
||||
}
|
||||
|
||||
$config = json_decode($configRaw, true);
|
||||
if (!$config) {
|
||||
throw new Exception("Invalid JSON in X-Ray config");
|
||||
}
|
||||
|
||||
// 2. Modify config
|
||||
// Assuming VLESS structure: inbounds[0] -> settings -> clients
|
||||
if (!isset($config['inbounds'][0]['settings']['clients'])) {
|
||||
// Might be different structure? But we stick to standard Amnezia XRay config
|
||||
if (!isset($config['inbounds'][0]['settings'])) {
|
||||
$config['inbounds'][0]['settings'] = [];
|
||||
}
|
||||
if (!isset($config['inbounds'][0]['settings']['clients'])) {
|
||||
$config['inbounds'][0]['settings']['clients'] = [];
|
||||
}
|
||||
}
|
||||
|
||||
// Check if client exists
|
||||
$clients = &$config['inbounds'][0]['settings']['clients'];
|
||||
foreach ($clients as $c) {
|
||||
if (($c['id'] ?? '') === $clientId) {
|
||||
// Already exists
|
||||
Logger::appendInstall($server->getId(), "Client $clientId already exists in X-Ray config");
|
||||
return ['success' => true, 'message' => 'Client already exists'];
|
||||
}
|
||||
}
|
||||
|
||||
// Add client
|
||||
$newClient = ['id' => $clientId];
|
||||
|
||||
// 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;
|
||||
|
||||
$clients[] = $newClient;
|
||||
|
||||
// 3. Write config back
|
||||
$newJson = json_encode($config, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
|
||||
$b64 = base64_encode($newJson);
|
||||
$writeCmd = "docker exec -i " . escapeshellarg($containerName) . " sh -c 'echo \"$b64\" | base64 -d > /opt/amnezia/xray/server.json'";
|
||||
|
||||
$server->executeCommand($writeCmd, true);
|
||||
|
||||
// 4. Restart container
|
||||
$server->executeCommand("docker restart " . escapeshellarg($containerName), true);
|
||||
|
||||
Logger::appendInstall($server->getId(), "Updated X-Ray config and restarted container");
|
||||
|
||||
return ['success' => true];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -386,6 +386,28 @@ class VpnClient
|
||||
if (($slug ?? '') === 'smb' && empty($vars['password'])) {
|
||||
$vars['password'] = $pass;
|
||||
}
|
||||
|
||||
// Ensure client_id (UUID) for X-Ray
|
||||
if (empty($vars['client_id']) && (stripos($slug, 'xray') !== false || stripos($slug, 'vless') !== false)) {
|
||||
$data = random_bytes(16);
|
||||
$data[6] = chr(ord($data[6]) & 0x0f | 0x40);
|
||||
$data[8] = chr(ord($data[8]) & 0x3f | 0x80);
|
||||
$vars['client_id'] = vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4));
|
||||
}
|
||||
|
||||
// Try to add client to server via universal manager (supports scripts and builtins)
|
||||
if ($protoRow) {
|
||||
// We pass generic options. InstallProtocolManager will handle specific logic for 'add_client' phase.
|
||||
// For xray-vless it uses builtin fallback in runScript.
|
||||
try {
|
||||
require_once __DIR__ . '/InstallProtocolManager.php';
|
||||
InstallProtocolManager::addClient($server, $protoRow, $vars);
|
||||
} catch (Exception $e) {
|
||||
error_log("Failed to add client to server: " . $e->getMessage());
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
$config = $protoRow ? ProtocolService::generateProtocolOutput($protoRow, $vars) : '';
|
||||
|
||||
// Prepare last_config_json for QR code generation if config is JSON (XRay)
|
||||
|
||||
Reference in New Issue
Block a user