diff --git a/app/Console/Commands/NodeWebSocketServer.php b/app/Console/Commands/NodeWebSocketServer.php index 0eb1f71..4a63919 100644 --- a/app/Console/Commands/NodeWebSocketServer.php +++ b/app/Console/Commands/NodeWebSocketServer.php @@ -126,7 +126,7 @@ class NodeWebSocketServer extends Command NodeRegistry::add($nodeId, $conn); Cache::put("node_ws_alive:{$nodeId}", true, 86400); - Log::info("[WS] Node#{$nodeId} connected", [ + Log::debug("[WS] Node#{$nodeId} connected", [ 'remote' => $conn->getRemoteIp(), 'total' => NodeRegistry::count(), ]); @@ -137,8 +137,8 @@ class NodeWebSocketServer extends Command 'data' => ['node_id' => $nodeId], ])); - // Push full sync (config + users) immediately - $this->pushFullSync($nodeId, $node); + // Push full sync (config + users) immediately to this specific connection + $this->pushFullSync($conn, $node); }; $worker->onMessage = function (TcpConnection $conn, $data) { @@ -148,12 +148,18 @@ class NodeWebSocketServer extends Command } $event = $msg['event'] ?? ''; + $nodeId = $conn->nodeId ?? null; switch ($event) { case 'pong': // Heartbeat response — node is alive - if (!empty($conn->nodeId)) { - Cache::put("node_ws_alive:{$conn->nodeId}", true, 86400); + if ($nodeId) { + Cache::put("node_ws_alive:{$nodeId}", true, 86400); + } + break; + case 'node.status': + if ($nodeId && isset($msg['data'])) { + $this->handleNodeStatus($nodeId, $msg['data']); } break; default: @@ -167,7 +173,7 @@ class NodeWebSocketServer extends Command $nodeId = $conn->nodeId; NodeRegistry::remove($nodeId); Cache::forget("node_ws_alive:{$nodeId}"); - Log::info("[WS] Node#{$nodeId} disconnected", [ + Log::debug("[WS] Node#{$nodeId} disconnected", [ 'total' => NodeRegistry::count(), ]); } @@ -176,6 +182,25 @@ class NodeWebSocketServer extends Command Worker::runAll(); } + /** + * Handle status data pushed from node via WebSocket + */ + private function handleNodeStatus(int $nodeId, array $data): void + { + $node = Server::find($nodeId); + if (!$node) return; + + $nodeType = strtoupper($node->type); + + // Update last check-in cache + Cache::put(\App\Utils\CacheKey::get('SERVER_' . $nodeType . '_LAST_CHECK_AT', $nodeId), time(), 3600); + + // Update metrics cache via Service + ServerService::updateMetrics($node, $data); + + Log::debug("[WS] Node#{$nodeId} status updated via WebSocket"); + } + /** * Subscribe to Redis pub/sub channel for receiving push commands from Laravel. * Laravel app publishes to "node:push" channel, Workerman picks it up and forwards to the right node. @@ -229,15 +254,23 @@ class NodeWebSocketServer extends Command /** * Push full config + users to a newly connected node. */ - private function pushFullSync(int $nodeId, Server $node): void + private function pushFullSync(TcpConnection $conn, Server $node): void { + $nodeId = $conn->nodeId; // Push config $config = ServerService::buildNodeConfig($node); - NodeRegistry::send($nodeId, 'sync.config', ['config' => $config]); + Log::debug("[WS] Node#{$nodeId} config: ", $config); + $conn->send(json_encode([ + 'event' => 'sync.config', + 'data' => ['config' => $config] + ])); // Push users $users = ServerService::getAvailableUsers($node)->toArray(); - NodeRegistry::send($nodeId, 'sync.users', ['users' => $users]); + $conn->send(json_encode([ + 'event' => 'sync.users', + 'data' => ['users' => $users] + ])); Log::info("[WS] Full sync pushed to node#{$nodeId}", [ 'users' => count($users), diff --git a/app/Http/Controllers/V2/Server/ServerController.php b/app/Http/Controllers/V2/Server/ServerController.php index c90c2e0..d5ea600 100644 --- a/app/Http/Controllers/V2/Server/ServerController.php +++ b/app/Http/Controllers/V2/Server/ServerController.php @@ -4,6 +4,7 @@ namespace App\Http\Controllers\V2\Server; use App\Http\Controllers\Controller; use App\Jobs\UserAliveSyncJob; +use App\Services\ServerService; use App\Services\UserService; use App\Utils\CacheKey; use Illuminate\Http\Request; @@ -113,6 +114,7 @@ class ServerController extends Controller 'used' => (int) ($status['disk']['used'] ?? 0), ], 'updated_at' => now()->timestamp, + 'kernel_status' => $status['kernel_status'] ?? null, ]; $cacheTime = max(300, (int) admin_setting('server_push_interval', 60) * 3); @@ -125,24 +127,7 @@ class ServerController extends Controller // handle node metrics (Metrics) $metrics = $request->input('metrics'); if (is_array($metrics) && !empty($metrics)) { - $metricsData = [ - 'uptime' => (int) ($metrics['uptime'] ?? 0), - 'inbound_speed' => (int) ($metrics['inbound_speed'] ?? 0), - 'outbound_speed' => (int) ($metrics['outbound_speed'] ?? 0), - 'active_connections' => (int) ($metrics['active_connections'] ?? 0), - 'total_connections' => (int) ($metrics['total_connections'] ?? 0), - 'speed_limiter' => $metrics['speed_limiter'] ?? [], - 'cpu_per_core' => $metrics['cpu_per_core'] ?? [], - 'gc' => $metrics['gc'] ?? [], - 'api' => $metrics['api'] ?? [], - 'updated_at' => now()->timestamp, - ]; - $cacheTime = max(300, (int) admin_setting('server_push_interval', 60) * 3); - Cache::put( - CacheKey::get('SERVER_' . strtoupper($nodeType) . '_METRICS', $nodeId), - $metricsData, - $cacheTime - ); + ServerService::updateMetrics($node, $metrics); } return response()->json(['data' => true]); diff --git a/app/Http/Requests/Admin/ServerSave.php b/app/Http/Requests/Admin/ServerSave.php index df385dd..b017bff 100644 --- a/app/Http/Requests/Admin/ServerSave.php +++ b/app/Http/Requests/Admin/ServerSave.php @@ -8,6 +8,23 @@ use Illuminate\Foundation\Http\FormRequest; class ServerSave extends FormRequest { + private const UTLS_RULES = [ + 'utls.enabled' => 'nullable|boolean', + 'utls.fingerprint' => 'nullable|string', + ]; + + private const MULTIPLEX_RULES = [ + 'multiplex.enabled' => 'nullable|boolean', + 'multiplex.protocol' => 'nullable|string', + 'multiplex.max_connections' => 'nullable|integer', + 'multiplex.min_streams' => 'nullable|integer', + 'multiplex.max_streams' => 'nullable|integer', + 'multiplex.padding' => 'nullable|boolean', + 'multiplex.brutal.enabled' => 'nullable|boolean', + 'multiplex.brutal.up_mbps' => 'nullable|integer', + 'multiplex.brutal.down_mbps' => 'nullable|integer', + ]; + private const PROTOCOL_RULES = [ 'shadowsocks' => [ 'cipher' => 'required|string', @@ -67,8 +84,8 @@ class ServerSave extends FormRequest 'tls_settings' => 'nullable|array', ], 'mieru' => [ - 'transport' => 'required|string', - 'multiplexing' => 'required|string', + 'transport' => 'required|string|in:TCP,UDP', + 'traffic_pattern' => 'string' ], 'anytls' => [ 'tls' => 'nullable|array', @@ -112,13 +129,45 @@ class ServerSave extends FormRequest $type = $this->input('type'); $rules = $this->getBaseRules(); - foreach (self::PROTOCOL_RULES[$type] ?? [] as $field => $rule) { + $protocolRules = self::PROTOCOL_RULES[$type] ?? []; + if (in_array($type, ['vmess', 'vless', 'trojan', 'mieru'])) { + $protocolRules = array_merge($protocolRules, self::MULTIPLEX_RULES, self::UTLS_RULES); + } + + foreach ($protocolRules as $field => $rule) { $rules['protocol_settings.' . $field] = $rule; } return $rules; } + public function attributes(): array + { + return [ + 'protocol_settings.cipher' => '加密方式', + 'protocol_settings.obfs' => '混淆类型', + 'protocol_settings.network' => '传输协议', + 'protocol_settings.port_range' => '端口范围', + 'protocol_settings.traffic_pattern' => 'Traffic Pattern', + 'protocol_settings.transport' => '传输方式', + 'protocol_settings.version' => '协议版本', + 'protocol_settings.password' => '密码', + 'protocol_settings.handshake.server' => '握手服务器', + 'protocol_settings.handshake.server_port' => '握手端口', + 'protocol_settings.multiplex.enabled' => '多路复用', + 'protocol_settings.multiplex.protocol' => '复用协议', + 'protocol_settings.multiplex.max_connections' => '最大连接数', + 'protocol_settings.multiplex.min_streams' => '最小流数', + 'protocol_settings.multiplex.max_streams' => '最大流数', + 'protocol_settings.multiplex.padding' => '复用填充', + 'protocol_settings.multiplex.brutal.enabled' => 'Brutal加速', + 'protocol_settings.multiplex.brutal.up_mbps' => 'Brutal上行速率', + 'protocol_settings.multiplex.brutal.down_mbps' => 'Brutal下行速率', + 'protocol_settings.utls.enabled' => 'uTLS', + 'protocol_settings.utls.fingerprint' => 'uTLS指纹', + ]; + } + public function messages() { return [ @@ -139,7 +188,11 @@ class ServerSave extends FormRequest 'networkSettings.array' => '传输协议配置有误', 'ruleSettings.array' => '规则配置有误', 'tlsSettings.array' => 'tls配置有误', - 'dnsSettings.array' => 'dns配置有误' + 'dnsSettings.array' => 'dns配置有误', + 'protocol_settings.*.required' => ':attribute 不能为空', + 'protocol_settings.*.string' => ':attribute 必须是字符串', + 'protocol_settings.*.integer' => ':attribute 必须是整数', + 'protocol_settings.*.in' => ':attribute 的值不合法', ]; } } diff --git a/app/Models/Server.php b/app/Models/Server.php index fdd0ae4..21234b2 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -126,19 +126,55 @@ class Server extends Model 'rate_time_enable' => 'boolean', ]; + private const MULTIPLEX_CONFIGURATION = [ + 'multiplex' => [ + 'type' => 'object', + 'fields' => [ + 'enabled' => ['type' => 'boolean', 'default' => false], + 'protocol' => ['type' => 'string', 'default' => 'yamux'], + 'max_connections' => ['type' => 'integer', 'default' => null], + // 'min_streams' => ['type' => 'integer', 'default' => null], + // 'max_streams' => ['type' => 'integer', 'default' => null], + 'padding' => ['type' => 'boolean', 'default' => false], + 'brutal' => [ + 'type' => 'object', + 'fields' => [ + 'enabled' => ['type' => 'boolean', 'default' => false], + 'up_mbps' => ['type' => 'integer', 'default' => null], + 'down_mbps' => ['type' => 'integer', 'default' => null], + ] + ] + ] + ] + ]; + + private const UTLS_CONFIGURATION = [ + 'utls' => [ + 'type' => 'object', + 'fields' => [ + 'enabled' => ['type' => 'boolean', 'default' => false], + 'fingerprint' => ['type' => 'string', 'default' => 'chrome'], + ] + ] + ]; + private const PROTOCOL_CONFIGURATIONS = [ self::TYPE_TROJAN => [ - 'allow_insecure' => ['type' => 'boolean', 'default' => false], - 'server_name' => ['type' => 'string', 'default' => null], 'network' => ['type' => 'string', 'default' => null], - 'network_settings' => ['type' => 'array', 'default' => null] + 'network_settings' => ['type' => 'array', 'default' => null], + 'server_name' => ['type' => 'string', 'default' => null], + 'allow_insecure' => ['type' => 'boolean', 'default' => false], + ...self::MULTIPLEX_CONFIGURATION, + ...self::UTLS_CONFIGURATION ], self::TYPE_VMESS => [ 'tls' => ['type' => 'integer', 'default' => 0], 'network' => ['type' => 'string', 'default' => null], 'rules' => ['type' => 'array', 'default' => null], 'network_settings' => ['type' => 'array', 'default' => null], - 'tls_settings' => ['type' => 'array', 'default' => null] + 'tls_settings' => ['type' => 'array', 'default' => null], + ...self::MULTIPLEX_CONFIGURATION, + ...self::UTLS_CONFIGURATION ], self::TYPE_VLESS => [ 'tls' => ['type' => 'integer', 'default' => 0], @@ -156,7 +192,9 @@ class Server extends Model 'private_key' => ['type' => 'string', 'default' => null], 'short_id' => ['type' => 'string', 'default' => null] ] - ] + ], + ...self::MULTIPLEX_CONFIGURATION, + ...self::UTLS_CONFIGURATION ], self::TYPE_SHADOWSOCKS => [ 'cipher' => ['type' => 'string', 'default' => null], @@ -251,8 +289,9 @@ class Server extends Model ] ], self::TYPE_MIERU => [ - 'transport' => ['type' => 'string', 'default' => 'tcp'], - 'multiplexing' => ['type' => 'string', 'default' => 'MULTIPLEXING_LOW'] + 'transport' => ['type' => 'string', 'default' => 'TCP'], + 'traffic_pattern' => ['type' => 'string', 'default' => ''], + ...self::MULTIPLEX_CONFIGURATION, ] ]; diff --git a/app/Protocols/ClashMeta.php b/app/Protocols/ClashMeta.php index 9d730a3..f3ee57f 100644 --- a/app/Protocols/ClashMeta.php +++ b/app/Protocols/ClashMeta.php @@ -227,7 +227,7 @@ class ClashMeta extends AbstractProtocol $array['plugin-opts'] = array_filter([ 'host' => $parsedOpts['host'] ?? null, 'password' => $parsedOpts['password'] ?? null, - 'version' => isset($parsedOpts['version']) ? (int)$parsedOpts['version'] : 2 + 'version' => isset($parsedOpts['version']) ? (int) $parsedOpts['version'] : 2 ], fn($v) => $v !== null); break; @@ -266,14 +266,19 @@ class ClashMeta extends AbstractProtocol $array['servername'] = data_get($protocol_settings, 'tls_settings.server_name'); } + self::appendUtls($array, $protocol_settings); + self::appendMultiplex($array, $protocol_settings); + switch (data_get($protocol_settings, 'network')) { case 'tcp': $array['network'] = data_get($protocol_settings, 'network_settings.header.type', 'tcp'); if (data_get($protocol_settings, 'network_settings.header.type', 'none') !== 'none') { - if ($httpOpts = array_filter([ - 'headers' => data_get($protocol_settings, 'network_settings.header.request.headers'), - 'path' => data_get($protocol_settings, 'network_settings.header.request.path', ['/']) - ])) { + if ( + $httpOpts = array_filter([ + 'headers' => data_get($protocol_settings, 'network_settings.header.request.headers'), + 'path' => data_get($protocol_settings, 'network_settings.header.request.path', ['/']) + ]) + ) { $array['http-opts'] = $httpOpts; } } @@ -336,7 +341,7 @@ class ClashMeta extends AbstractProtocol if ($serverName = data_get($protocol_settings, 'tls_settings.server_name')) { $array['servername'] = $serverName; } - $array['client-fingerprint'] = Helper::getRandFingerprint(); + self::appendUtls($array, $protocol_settings); break; case 2: $array['tls'] = true; @@ -346,7 +351,7 @@ class ClashMeta extends AbstractProtocol 'public-key' => data_get($protocol_settings, 'reality_settings.public_key'), 'short-id' => data_get($protocol_settings, 'reality_settings.short_id') ]; - $array['client-fingerprint'] = Helper::getRandFingerprint(); + self::appendUtls($array, $protocol_settings); break; default: break; @@ -358,10 +363,12 @@ class ClashMeta extends AbstractProtocol $headerType = data_get($protocol_settings, 'network_settings.header.type', 'none'); if ($headerType === 'http') { $array['network'] = 'http'; - if ($httpOpts = array_filter([ - 'headers' => data_get($protocol_settings, 'network_settings.header.request.headers'), - 'path' => data_get($protocol_settings, 'network_settings.header.request.path', ['/']) - ])) { + if ( + $httpOpts = array_filter([ + 'headers' => data_get($protocol_settings, 'network_settings.header.request.headers'), + 'path' => data_get($protocol_settings, 'network_settings.header.request.path', ['/']) + ]) + ) { $array['http-opts'] = $httpOpts; } } @@ -398,6 +405,8 @@ class ClashMeta extends AbstractProtocol break; } + self::appendMultiplex($array, $protocol_settings); + return $array; } @@ -417,6 +426,9 @@ class ClashMeta extends AbstractProtocol $array['sni'] = $serverName; } + self::appendUtls($array, $protocol_settings); + self::appendMultiplex($array, $protocol_settings); + switch (data_get($protocol_settings, 'network')) { case 'tcp': $array['network'] = 'tcp'; @@ -565,8 +577,7 @@ class ClashMeta extends AbstractProtocol 'port' => $server['port'], 'username' => $password, 'password' => $password, - 'transport' => strtoupper(data_get($protocol_settings, 'transport', 'TCP')), - 'multiplexing' => data_get($protocol_settings, 'multiplexing', 'MULTIPLEXING_LOW') + 'transport' => strtoupper(data_get($protocol_settings, 'transport', 'TCP')) ]; // 如果配置了端口范围 @@ -640,4 +651,37 @@ class ClashMeta extends AbstractProtocol return false; } } -} + + protected static function appendMultiplex(&$array, $protocol_settings) + { + if ($multiplex = data_get($protocol_settings, 'multiplex')) { + if (data_get($multiplex, 'enabled')) { + $array['smux'] = array_filter([ + 'enabled' => true, + 'protocol' => data_get($multiplex, 'protocol', 'yamux'), + 'max-connections' => data_get($multiplex, 'max_connections'), + // 'min-streams' => data_get($multiplex, 'min_streams'), + // 'max-streams' => data_get($multiplex, 'max_streams'), + 'padding' => data_get($multiplex, 'padding') ? true : null, + ]); + + if (data_get($multiplex, 'brutal.enabled')) { + $array['smux']['brutal'] = [ + 'enabled' => true, + 'up' => data_get($multiplex, 'brutal.up_mbps'), + 'down' => data_get($multiplex, 'brutal.down_mbps'), + ]; + } + } + } + } + + protected static function appendUtls(&$array, $protocol_settings) + { + if ($utls = data_get($protocol_settings, 'utls')) { + if (data_get($utls, 'enabled')) { + $array['client-fingerprint'] = Helper::getTlsFingerprint($utls); + } + } + } +} \ No newline at end of file diff --git a/app/Protocols/General.php b/app/Protocols/General.php index 3ac519b..3ffe6e9 100644 --- a/app/Protocols/General.php +++ b/app/Protocols/General.php @@ -154,7 +154,9 @@ class General extends AbstractProtocol switch ($server['protocol_settings']['tls']) { case 1: $config['security'] = "tls"; - $config['fp'] = Helper::getRandFingerprint(); + if ($fp = Helper::getTlsFingerprint(data_get($protocol_settings, 'utls'))) { + $config['fp'] = $fp; + } if ($serverName = data_get($protocol_settings, 'tls_settings.server_name')) { $config['sni'] = $serverName; } @@ -166,7 +168,9 @@ class General extends AbstractProtocol $config['sni'] = data_get($protocol_settings, 'reality_settings.server_name'); $config['servername'] = data_get($protocol_settings, 'reality_settings.server_name'); $config['spx'] = "/"; - $config['fp'] = Helper::getRandFingerprint(); + if ($fp = Helper::getTlsFingerprint(data_get($protocol_settings, 'utls'))) { + $config['fp'] = $fp; + } break; default: break; diff --git a/app/Protocols/Shadowrocket.php b/app/Protocols/Shadowrocket.php index 9aaac5b..1886aed 100644 --- a/app/Protocols/Shadowrocket.php +++ b/app/Protocols/Shadowrocket.php @@ -165,14 +165,18 @@ class Shadowrocket extends AbstractProtocol if ($serverName = data_get($protocol_settings, 'tls_settings.server_name')) { $config['peer'] = $serverName; } - $config['fp'] = Helper::getRandFingerprint(); + if ($fp = Helper::getTlsFingerprint(data_get($protocol_settings, 'utls'))) { + $config['fp'] = $fp; + } break; case 2: $config['tls'] = 1; $config['sni'] = data_get($protocol_settings, 'reality_settings.server_name'); $config['pbk'] = data_get($protocol_settings, 'reality_settings.public_key'); $config['sid'] = data_get($protocol_settings, 'reality_settings.short_id'); - $config['fp'] = Helper::getRandFingerprint(); + if ($fp = Helper::getTlsFingerprint(data_get($protocol_settings, 'utls'))) { + $config['fp'] = $fp; + } break; default: break; diff --git a/app/Protocols/SingBox.php b/app/Protocols/SingBox.php index 4eb803e..68dd511 100644 --- a/app/Protocols/SingBox.php +++ b/app/Protocols/SingBox.php @@ -3,7 +3,6 @@ namespace App\Protocols; use App\Utils\Helper; use Illuminate\Support\Arr; -use Illuminate\Support\Facades\File; use App\Support\AbstractProtocol; use App\Models\Server; @@ -20,7 +19,6 @@ class SingBox extends AbstractProtocol Server::TYPE_ANYTLS, Server::TYPE_SOCKS, Server::TYPE_HTTP, - Server::TYPE_MIERU, ]; private $config; const CUSTOM_TEMPLATE_FILE = 'resources/rules/custom.sing-box.json'; @@ -55,9 +53,6 @@ class SingBox extends AbstractProtocol 'juicity' => [ 'base_version' => '1.7.0' ], - 'shadowtls' => [ - 'base_version' => '1.6.0' - ], 'wireguard' => [ 'base_version' => '1.5.0' ], @@ -292,11 +287,16 @@ class SingBox extends AbstractProtocol 'enabled' => true, 'insecure' => (bool) data_get($protocol_settings, 'tls_settings.allow_insecure'), ]; + + $this->appendUtls($array['tls'], $protocol_settings); + if ($serverName = data_get($protocol_settings, 'tls_settings.server_name')) { $array['tls']['server_name'] = $serverName; } } + $this->appendMultiplex($array, $protocol_settings); + $transport = match ($protocol_settings['network']) { 'tcp' => data_get($protocol_settings, 'network_settings.header.type', 'none') !== 'none' ? [ 'type' => 'http', @@ -354,12 +354,10 @@ class SingBox extends AbstractProtocol $tlsConfig = [ 'enabled' => true, 'insecure' => (bool) data_get($protocol_settings, 'tls_settings.allow_insecure'), - 'utls' => [ - 'enabled' => true, - 'fingerprint' => Helper::getRandFingerprint() - ] ]; + $this->appendUtls($tlsConfig, $protocol_settings); + switch ($protocol_settings['tls']) { case 1: if ($serverName = data_get($protocol_settings, 'tls_settings.server_name')) { @@ -379,6 +377,8 @@ class SingBox extends AbstractProtocol $array['tls'] = $tlsConfig; } + $this->appendMultiplex($array, $protocol_settings); + $transport = match ($protocol_settings['network']) { 'tcp' => data_get($protocol_settings, 'network_settings.header.type') == 'http' ? [ 'type' => 'http', @@ -433,9 +433,15 @@ class SingBox extends AbstractProtocol 'insecure' => (bool) data_get($protocol_settings, 'allow_insecure', false), ] ]; + + $this->appendUtls($array['tls'], $protocol_settings); + if ($serverName = data_get($protocol_settings, 'server_name')) { $array['tls']['server_name'] = $serverName; } + + $this->appendMultiplex($array, $protocol_settings); + $transport = match (data_get($protocol_settings, 'network')) { 'grpc' => [ 'type' => 'grpc', @@ -619,27 +625,39 @@ class SingBox extends AbstractProtocol return $array; } - protected function buildMieru($password, $server): array + protected function appendMultiplex(&$array, $protocol_settings) { - $protocol_settings = data_get($server, 'protocol_settings', []); - $array = [ - 'type' => 'mieru', - 'tag' => $server['name'], - 'server' => $server['host'], - 'server_port' => $server['port'], - 'username' => $password, - 'password' => $password, - 'transport' => strtolower(data_get($protocol_settings, 'transport', 'tcp')), - ]; - - if (isset($server['ports'])) { - $array['server_port_range'] = [$server['ports']]; + if ($multiplex = data_get($protocol_settings, 'multiplex')) { + if (data_get($multiplex, 'enabled')) { + $array['multiplex'] = [ + 'enabled' => true, + 'protocol' => data_get($multiplex, 'protocol', 'yamux'), + 'max_connections' => data_get($multiplex, 'max_connections'), + 'min_streams' => data_get($multiplex, 'min_streams'), + 'max_streams' => data_get($multiplex, 'max_streams'), + 'padding' => (bool) data_get($multiplex, 'padding', false), + ]; + if (data_get($multiplex, 'brutal.enabled')) { + $array['multiplex']['brutal'] = [ + 'enabled' => true, + 'up_mbps' => data_get($multiplex, 'brutal.up_mbps'), + 'down_mbps' => data_get($multiplex, 'brutal.down_mbps'), + ]; + } + $array['multiplex'] = array_filter($array['multiplex'], fn($v) => !is_null($v)); + } } + } - if ($multiplexing = data_get($protocol_settings, 'multiplexing')) { - $array['multiplexing'] = $multiplexing; + protected function appendUtls(&$tlsConfig, $protocol_settings) + { + if ($utls = data_get($protocol_settings, 'utls')) { + if (data_get($utls, 'enabled')) { + $tlsConfig['utls'] = [ + 'enabled' => true, + 'fingerprint' => Helper::getTlsFingerprint($utls) + ]; + } } - - return $array; } } diff --git a/app/Protocols/Stash.php b/app/Protocols/Stash.php index 8f6910d..b83d2e1 100644 --- a/app/Protocols/Stash.php +++ b/app/Protocols/Stash.php @@ -283,7 +283,9 @@ class Stash extends AbstractProtocol $array['uuid'] = $uuid; $array['udp'] = true; - $array['client-fingerprint'] = Helper::getRandFingerprint(); + if ($fingerprint = Helper::getTlsFingerprint(data_get($protocol_settings, 'utls'))) { + $array['client-fingerprint'] = $fingerprint; + } switch (data_get($protocol_settings, 'tls')) { case 1: @@ -312,10 +314,12 @@ class Stash extends AbstractProtocol $headerType = data_get($protocol_settings, 'network_settings.header.type', 'tcp'); $array['network'] = ($headerType === 'http') ? 'http' : 'tcp'; if ($headerType === 'http') { - if ($httpOpts = array_filter([ - 'headers' => data_get($protocol_settings, 'network_settings.header.request.headers'), - 'path' => data_get($protocol_settings, 'network_settings.header.request.path', ['/']) - ])) { + if ( + $httpOpts = array_filter([ + 'headers' => data_get($protocol_settings, 'network_settings.header.request.headers'), + 'path' => data_get($protocol_settings, 'network_settings.header.request.path', ['/']) + ]) + ) { $array['http-opts'] = $httpOpts; } } @@ -331,11 +335,11 @@ class Stash extends AbstractProtocol $array['network'] = 'grpc'; $array['grpc-opts']['grpc-service-name'] = data_get($protocol_settings, 'network_settings.serviceName'); break; - // case 'h2': - // $array['network'] = 'h2'; - // $array['h2-opts']['host'] = data_get($protocol_settings, 'network_settings.host'); - // $array['h2-opts']['path'] = data_get($protocol_settings, 'network_settings.path'); - // break; + // case 'h2': + // $array['network'] = 'h2'; + // $array['h2-opts']['host'] = data_get($protocol_settings, 'network_settings.host'); + // $array['h2-opts']['path'] = data_get($protocol_settings, 'network_settings.path'); + // break; } return $array; diff --git a/app/Services/ServerService.php b/app/Services/ServerService.php index 13a1680..00ed257 100644 --- a/app/Services/ServerService.php +++ b/app/Services/ServerService.php @@ -96,8 +96,41 @@ class ServerService } /** - * Build node config data + * Update node metrics and load status */ + public static function updateMetrics(Server $node, array $metrics): void + { + $nodeType = strtoupper($node->type); + $nodeId = $node->id; + $cacheTime = max(300, (int) admin_setting('server_push_interval', 60) * 3); + + $metricsData = [ + 'uptime' => (int) ($metrics['uptime'] ?? 0), + 'goroutines' => (int) ($metrics['goroutines'] ?? 0), + 'active_connections' => (int) ($metrics['active_connections'] ?? 0), + 'total_connections' => (int) ($metrics['total_connections'] ?? 0), + 'total_users' => (int) ($metrics['total_users'] ?? 0), + 'active_users' => (int) ($metrics['active_users'] ?? 0), + 'inbound_speed' => (int) ($metrics['inbound_speed'] ?? 0), + 'outbound_speed' => (int) ($metrics['outbound_speed'] ?? 0), + 'cpu_per_core' => $metrics['cpu_per_core'] ?? [], + 'load' => $metrics['load'] ?? [], + 'speed_limiter' => $metrics['speed_limiter'] ?? [], + 'gc' => $metrics['gc'] ?? [], + 'api' => $metrics['api'] ?? [], + 'ws' => $metrics['ws'] ?? [], + 'limits' => $metrics['limits'] ?? [], + 'updated_at' => now()->timestamp, + 'kernel_status' => (bool) ($metrics['kernel_status'] ?? false), + ]; + + \Illuminate\Support\Facades\Cache::put( + \App\Utils\CacheKey::get('SERVER_' . $nodeType . '_METRICS', $nodeId), + $metricsData, + $cacheTime + ); + } + public static function buildNodeConfig(Server $node): array { $nodeType = $node->type; @@ -120,28 +153,31 @@ class ServerService 'plugin' => $protocolSettings['plugin'], 'plugin_opts' => $protocolSettings['plugin_opts'], 'server_key' => match ($protocolSettings['cipher']) { - '2022-blake3-aes-128-gcm' => Helper::getServerKey($node->created_at, 16), - '2022-blake3-aes-256-gcm' => Helper::getServerKey($node->created_at, 32), - default => null, - }, + '2022-blake3-aes-128-gcm' => Helper::getServerKey($node->created_at, 16), + '2022-blake3-aes-256-gcm' => Helper::getServerKey($node->created_at, 32), + default => null, + }, ], 'vmess' => [ ...$baseConfig, 'tls' => (int) $protocolSettings['tls'], + 'multiplex' => data_get($protocolSettings, 'multiplex'), ], 'trojan' => [ ...$baseConfig, 'host' => $host, 'server_name' => $protocolSettings['server_name'], + 'multiplex' => data_get($protocolSettings, 'multiplex'), ], 'vless' => [ ...$baseConfig, 'tls' => (int) $protocolSettings['tls'], 'flow' => $protocolSettings['flow'], 'tls_settings' => match ((int) $protocolSettings['tls']) { - 2 => $protocolSettings['reality_settings'], - default => $protocolSettings['tls_settings'], - }, + 2 => $protocolSettings['reality_settings'], + default => $protocolSettings['tls_settings'], + }, + 'multiplex' => data_get($protocolSettings, 'multiplex'), ], 'hysteria' => [ ...$baseConfig, @@ -152,13 +188,13 @@ class ServerService 'up_mbps' => (int) $protocolSettings['bandwidth']['up'], 'down_mbps' => (int) $protocolSettings['bandwidth']['down'], ...match ((int) $protocolSettings['version']) { - 1 => ['obfs' => $protocolSettings['obfs']['password'] ?? null], - 2 => [ - 'obfs' => $protocolSettings['obfs']['open'] ? $protocolSettings['obfs']['type'] : null, - 'obfs-password' => $protocolSettings['obfs']['password'] ?? null, - ], - default => [], - }, + 1 => ['obfs' => $protocolSettings['obfs']['password'] ?? null], + 2 => [ + 'obfs' => $protocolSettings['obfs']['open'] ? $protocolSettings['obfs']['type'] : null, + 'obfs-password' => $protocolSettings['obfs']['password'] ?? null, + ], + default => [], + }, ], 'tuic' => [ ...$baseConfig, @@ -195,8 +231,10 @@ class ServerService ], 'mieru' => [ ...$baseConfig, - 'server_port' => (string) $serverPort, - 'protocol' => (int) $protocolSettings['protocol'], + 'server_port' => (int) $serverPort, + 'transport' => data_get($protocolSettings, 'transport', 'TCP'), + 'traffic_pattern' => $protocolSettings['traffic_pattern'], + // 'multiplex' => data_get($protocolSettings, 'multiplex'), ], default => [], }; diff --git a/app/Utils/Helper.php b/app/Utils/Helper.php index 17d0091..1cc1bf7 100644 --- a/app/Utils/Helper.php +++ b/app/Utils/Helper.php @@ -188,8 +188,20 @@ class Helper public static function getIpByDomainName($domain) { return gethostbynamel($domain) ?: []; } + + public static function getTlsFingerprint($utls = null) + { + + if (is_array($utls) || is_object($utls)) { + if (!data_get($utls, 'enabled')) { + return null; + } + $fingerprint = data_get($utls, 'fingerprint', 'chrome'); + if ($fingerprint !== 'random') { + return $fingerprint; + } + } - public static function getRandFingerprint() { $fingerprints = ['chrome', 'firefox', 'safari', 'ios', 'edge', 'qq']; return Arr::random($fingerprints); } diff --git a/docs/en/installation/aapanel-docker.md b/docs/en/installation/aapanel-docker.md index 7c604a1..348ed98 100644 --- a/docs/en/installation/aapanel-docker.md +++ b/docs/en/installation/aapanel-docker.md @@ -141,8 +141,9 @@ docker compose up -d ## Troubleshooting If you encounter any issues during installation or operation, please check: -1. System requirements are met -2. All required ports are available +1. **Empty Admin Dashboard**: If the admin panel is blank, run `git submodule update --init --recursive --force` to restore the theme files. +2. System requirements are met +3. All required ports are available 3. Docker services are running properly 4. Nginx configuration is correct 5. Check logs for detailed error messages diff --git a/docs/en/installation/aapanel.md b/docs/en/installation/aapanel.md index 49d6649..7394bc1 100644 --- a/docs/en/installation/aapanel.md +++ b/docs/en/installation/aapanel.md @@ -169,8 +169,9 @@ sh update.sh ## Troubleshooting ### Common Issues -1. Changes to admin path require service restart to take effect -2. Any code changes after enabling Octane require restart to take effect +1. **Empty Admin Dashboard**: If the admin panel is blank, run `git submodule update --init --recursive --force` to restore the theme files. +2. Changes to admin path require service restart to take effect +3. Any code changes after enabling Octane require restart to take effect 3. When PHP extension installation fails, check if PHP version is correct 4. For database connection failures, check database configuration and permissions diff --git a/public/assets/admin b/public/assets/admin index 155f00a..5296f66 160000 --- a/public/assets/admin +++ b/public/assets/admin @@ -1 +1 @@ -Subproject commit 155f00a9d7e214d1668eeb20c8dc5abd59a1ff5a +Subproject commit 5296f66eb99feb89d0f4c8bfd28c24c6dbf532ea