feat: Refactor uTLS & Multiplex Support, Node Status Push Optimization

- Server/ServerSave/Server.php: Unified utls and multiplex schema, validation, and defaults for vmess/vless/trojan/mieru protocols, enabling more flexible protocol configuration.
- Protocols (SingBox/ClashMeta/Shadowrocket/Stash/General): All protocol generators now support utls (client-fingerprint/fp) and multiplex options. Removed getRandFingerprint, replaced with getTlsFingerprint supporting random/custom fingerprints.
- Helper.php: Refactored TLS fingerprint utility to support object/string/random input.
- ServerService: Abstracted updateMetrics method to unify HTTP/WS node status caching logic.
- NodeWebSocketServer: Improved node connection, status push, and full sync logic; adjusted log levels; clarified push logic.
- ServerController: Reused ServerService for node metrics handling, reducing code duplication.
- Docs: Improved aapanel installation docs, added fix for empty admin dashboard.
This commit is contained in:
xboard
2026-03-16 23:09:56 +08:00
parent 65363ea918
commit b55091a066
14 changed files with 352 additions and 116 deletions

View File

@@ -126,7 +126,7 @@ class NodeWebSocketServer extends Command
NodeRegistry::add($nodeId, $conn); NodeRegistry::add($nodeId, $conn);
Cache::put("node_ws_alive:{$nodeId}", true, 86400); Cache::put("node_ws_alive:{$nodeId}", true, 86400);
Log::info("[WS] Node#{$nodeId} connected", [ Log::debug("[WS] Node#{$nodeId} connected", [
'remote' => $conn->getRemoteIp(), 'remote' => $conn->getRemoteIp(),
'total' => NodeRegistry::count(), 'total' => NodeRegistry::count(),
]); ]);
@@ -137,8 +137,8 @@ class NodeWebSocketServer extends Command
'data' => ['node_id' => $nodeId], 'data' => ['node_id' => $nodeId],
])); ]));
// Push full sync (config + users) immediately // Push full sync (config + users) immediately to this specific connection
$this->pushFullSync($nodeId, $node); $this->pushFullSync($conn, $node);
}; };
$worker->onMessage = function (TcpConnection $conn, $data) { $worker->onMessage = function (TcpConnection $conn, $data) {
@@ -148,12 +148,18 @@ class NodeWebSocketServer extends Command
} }
$event = $msg['event'] ?? ''; $event = $msg['event'] ?? '';
$nodeId = $conn->nodeId ?? null;
switch ($event) { switch ($event) {
case 'pong': case 'pong':
// Heartbeat response — node is alive // Heartbeat response — node is alive
if (!empty($conn->nodeId)) { if ($nodeId) {
Cache::put("node_ws_alive:{$conn->nodeId}", true, 86400); Cache::put("node_ws_alive:{$nodeId}", true, 86400);
}
break;
case 'node.status':
if ($nodeId && isset($msg['data'])) {
$this->handleNodeStatus($nodeId, $msg['data']);
} }
break; break;
default: default:
@@ -167,7 +173,7 @@ class NodeWebSocketServer extends Command
$nodeId = $conn->nodeId; $nodeId = $conn->nodeId;
NodeRegistry::remove($nodeId); NodeRegistry::remove($nodeId);
Cache::forget("node_ws_alive:{$nodeId}"); Cache::forget("node_ws_alive:{$nodeId}");
Log::info("[WS] Node#{$nodeId} disconnected", [ Log::debug("[WS] Node#{$nodeId} disconnected", [
'total' => NodeRegistry::count(), 'total' => NodeRegistry::count(),
]); ]);
} }
@@ -176,6 +182,25 @@ class NodeWebSocketServer extends Command
Worker::runAll(); 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. * 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. * 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. * 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 // Push config
$config = ServerService::buildNodeConfig($node); $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 // Push users
$users = ServerService::getAvailableUsers($node)->toArray(); $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}", [ Log::info("[WS] Full sync pushed to node#{$nodeId}", [
'users' => count($users), 'users' => count($users),

View File

@@ -4,6 +4,7 @@ namespace App\Http\Controllers\V2\Server;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Jobs\UserAliveSyncJob; use App\Jobs\UserAliveSyncJob;
use App\Services\ServerService;
use App\Services\UserService; use App\Services\UserService;
use App\Utils\CacheKey; use App\Utils\CacheKey;
use Illuminate\Http\Request; use Illuminate\Http\Request;
@@ -113,6 +114,7 @@ class ServerController extends Controller
'used' => (int) ($status['disk']['used'] ?? 0), 'used' => (int) ($status['disk']['used'] ?? 0),
], ],
'updated_at' => now()->timestamp, 'updated_at' => now()->timestamp,
'kernel_status' => $status['kernel_status'] ?? null,
]; ];
$cacheTime = max(300, (int) admin_setting('server_push_interval', 60) * 3); $cacheTime = max(300, (int) admin_setting('server_push_interval', 60) * 3);
@@ -125,24 +127,7 @@ class ServerController extends Controller
// handle node metrics (Metrics) // handle node metrics (Metrics)
$metrics = $request->input('metrics'); $metrics = $request->input('metrics');
if (is_array($metrics) && !empty($metrics)) { if (is_array($metrics) && !empty($metrics)) {
$metricsData = [ ServerService::updateMetrics($node, $metrics);
'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
);
} }
return response()->json(['data' => true]); return response()->json(['data' => true]);

View File

@@ -8,6 +8,23 @@ use Illuminate\Foundation\Http\FormRequest;
class ServerSave extends 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 = [ private const PROTOCOL_RULES = [
'shadowsocks' => [ 'shadowsocks' => [
'cipher' => 'required|string', 'cipher' => 'required|string',
@@ -67,8 +84,8 @@ class ServerSave extends FormRequest
'tls_settings' => 'nullable|array', 'tls_settings' => 'nullable|array',
], ],
'mieru' => [ 'mieru' => [
'transport' => 'required|string', 'transport' => 'required|string|in:TCP,UDP',
'multiplexing' => 'required|string', 'traffic_pattern' => 'string'
], ],
'anytls' => [ 'anytls' => [
'tls' => 'nullable|array', 'tls' => 'nullable|array',
@@ -112,13 +129,45 @@ class ServerSave extends FormRequest
$type = $this->input('type'); $type = $this->input('type');
$rules = $this->getBaseRules(); $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; $rules['protocol_settings.' . $field] = $rule;
} }
return $rules; 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() public function messages()
{ {
return [ return [
@@ -139,7 +188,11 @@ class ServerSave extends FormRequest
'networkSettings.array' => '传输协议配置有误', 'networkSettings.array' => '传输协议配置有误',
'ruleSettings.array' => '规则配置有误', 'ruleSettings.array' => '规则配置有误',
'tlsSettings.array' => 'tls配置有误', '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 的值不合法',
]; ];
} }
} }

View File

@@ -126,19 +126,55 @@ class Server extends Model
'rate_time_enable' => 'boolean', '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 = [ private const PROTOCOL_CONFIGURATIONS = [
self::TYPE_TROJAN => [ self::TYPE_TROJAN => [
'allow_insecure' => ['type' => 'boolean', 'default' => false],
'server_name' => ['type' => 'string', 'default' => null],
'network' => ['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 => [ self::TYPE_VMESS => [
'tls' => ['type' => 'integer', 'default' => 0], 'tls' => ['type' => 'integer', 'default' => 0],
'network' => ['type' => 'string', 'default' => null], 'network' => ['type' => 'string', 'default' => null],
'rules' => ['type' => 'array', 'default' => null], 'rules' => ['type' => 'array', 'default' => null],
'network_settings' => ['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 => [ self::TYPE_VLESS => [
'tls' => ['type' => 'integer', 'default' => 0], 'tls' => ['type' => 'integer', 'default' => 0],
@@ -156,7 +192,9 @@ class Server extends Model
'private_key' => ['type' => 'string', 'default' => null], 'private_key' => ['type' => 'string', 'default' => null],
'short_id' => ['type' => 'string', 'default' => null] 'short_id' => ['type' => 'string', 'default' => null]
] ]
] ],
...self::MULTIPLEX_CONFIGURATION,
...self::UTLS_CONFIGURATION
], ],
self::TYPE_SHADOWSOCKS => [ self::TYPE_SHADOWSOCKS => [
'cipher' => ['type' => 'string', 'default' => null], 'cipher' => ['type' => 'string', 'default' => null],
@@ -251,8 +289,9 @@ class Server extends Model
] ]
], ],
self::TYPE_MIERU => [ self::TYPE_MIERU => [
'transport' => ['type' => 'string', 'default' => 'tcp'], 'transport' => ['type' => 'string', 'default' => 'TCP'],
'multiplexing' => ['type' => 'string', 'default' => 'MULTIPLEXING_LOW'] 'traffic_pattern' => ['type' => 'string', 'default' => ''],
...self::MULTIPLEX_CONFIGURATION,
] ]
]; ];

View File

@@ -227,7 +227,7 @@ class ClashMeta extends AbstractProtocol
$array['plugin-opts'] = array_filter([ $array['plugin-opts'] = array_filter([
'host' => $parsedOpts['host'] ?? null, 'host' => $parsedOpts['host'] ?? null,
'password' => $parsedOpts['password'] ?? 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); ], fn($v) => $v !== null);
break; break;
@@ -266,14 +266,19 @@ class ClashMeta extends AbstractProtocol
$array['servername'] = data_get($protocol_settings, 'tls_settings.server_name'); $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')) { switch (data_get($protocol_settings, 'network')) {
case 'tcp': case 'tcp':
$array['network'] = data_get($protocol_settings, 'network_settings.header.type', 'tcp'); $array['network'] = data_get($protocol_settings, 'network_settings.header.type', 'tcp');
if (data_get($protocol_settings, 'network_settings.header.type', 'none') !== 'none') { if (data_get($protocol_settings, 'network_settings.header.type', 'none') !== 'none') {
if ($httpOpts = array_filter([ if (
'headers' => data_get($protocol_settings, 'network_settings.header.request.headers'), $httpOpts = array_filter([
'path' => data_get($protocol_settings, 'network_settings.header.request.path', ['/']) 'headers' => data_get($protocol_settings, 'network_settings.header.request.headers'),
])) { 'path' => data_get($protocol_settings, 'network_settings.header.request.path', ['/'])
])
) {
$array['http-opts'] = $httpOpts; $array['http-opts'] = $httpOpts;
} }
} }
@@ -336,7 +341,7 @@ class ClashMeta extends AbstractProtocol
if ($serverName = data_get($protocol_settings, 'tls_settings.server_name')) { if ($serverName = data_get($protocol_settings, 'tls_settings.server_name')) {
$array['servername'] = $serverName; $array['servername'] = $serverName;
} }
$array['client-fingerprint'] = Helper::getRandFingerprint(); self::appendUtls($array, $protocol_settings);
break; break;
case 2: case 2:
$array['tls'] = true; $array['tls'] = true;
@@ -346,7 +351,7 @@ class ClashMeta extends AbstractProtocol
'public-key' => data_get($protocol_settings, 'reality_settings.public_key'), 'public-key' => data_get($protocol_settings, 'reality_settings.public_key'),
'short-id' => data_get($protocol_settings, 'reality_settings.short_id') 'short-id' => data_get($protocol_settings, 'reality_settings.short_id')
]; ];
$array['client-fingerprint'] = Helper::getRandFingerprint(); self::appendUtls($array, $protocol_settings);
break; break;
default: default:
break; break;
@@ -358,10 +363,12 @@ class ClashMeta extends AbstractProtocol
$headerType = data_get($protocol_settings, 'network_settings.header.type', 'none'); $headerType = data_get($protocol_settings, 'network_settings.header.type', 'none');
if ($headerType === 'http') { if ($headerType === 'http') {
$array['network'] = 'http'; $array['network'] = 'http';
if ($httpOpts = array_filter([ if (
'headers' => data_get($protocol_settings, 'network_settings.header.request.headers'), $httpOpts = array_filter([
'path' => data_get($protocol_settings, 'network_settings.header.request.path', ['/']) 'headers' => data_get($protocol_settings, 'network_settings.header.request.headers'),
])) { 'path' => data_get($protocol_settings, 'network_settings.header.request.path', ['/'])
])
) {
$array['http-opts'] = $httpOpts; $array['http-opts'] = $httpOpts;
} }
} }
@@ -398,6 +405,8 @@ class ClashMeta extends AbstractProtocol
break; break;
} }
self::appendMultiplex($array, $protocol_settings);
return $array; return $array;
} }
@@ -417,6 +426,9 @@ class ClashMeta extends AbstractProtocol
$array['sni'] = $serverName; $array['sni'] = $serverName;
} }
self::appendUtls($array, $protocol_settings);
self::appendMultiplex($array, $protocol_settings);
switch (data_get($protocol_settings, 'network')) { switch (data_get($protocol_settings, 'network')) {
case 'tcp': case 'tcp':
$array['network'] = 'tcp'; $array['network'] = 'tcp';
@@ -565,8 +577,7 @@ class ClashMeta extends AbstractProtocol
'port' => $server['port'], 'port' => $server['port'],
'username' => $password, 'username' => $password,
'password' => $password, 'password' => $password,
'transport' => strtoupper(data_get($protocol_settings, 'transport', 'TCP')), 'transport' => strtoupper(data_get($protocol_settings, 'transport', 'TCP'))
'multiplexing' => data_get($protocol_settings, 'multiplexing', 'MULTIPLEXING_LOW')
]; ];
// 如果配置了端口范围 // 如果配置了端口范围
@@ -640,4 +651,37 @@ class ClashMeta extends AbstractProtocol
return false; 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);
}
}
}
}

View File

@@ -154,7 +154,9 @@ class General extends AbstractProtocol
switch ($server['protocol_settings']['tls']) { switch ($server['protocol_settings']['tls']) {
case 1: case 1:
$config['security'] = "tls"; $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')) { if ($serverName = data_get($protocol_settings, 'tls_settings.server_name')) {
$config['sni'] = $serverName; $config['sni'] = $serverName;
} }
@@ -166,7 +168,9 @@ class General extends AbstractProtocol
$config['sni'] = data_get($protocol_settings, 'reality_settings.server_name'); $config['sni'] = data_get($protocol_settings, 'reality_settings.server_name');
$config['servername'] = data_get($protocol_settings, 'reality_settings.server_name'); $config['servername'] = data_get($protocol_settings, 'reality_settings.server_name');
$config['spx'] = "/"; $config['spx'] = "/";
$config['fp'] = Helper::getRandFingerprint(); if ($fp = Helper::getTlsFingerprint(data_get($protocol_settings, 'utls'))) {
$config['fp'] = $fp;
}
break; break;
default: default:
break; break;

View File

@@ -165,14 +165,18 @@ class Shadowrocket extends AbstractProtocol
if ($serverName = data_get($protocol_settings, 'tls_settings.server_name')) { if ($serverName = data_get($protocol_settings, 'tls_settings.server_name')) {
$config['peer'] = $serverName; $config['peer'] = $serverName;
} }
$config['fp'] = Helper::getRandFingerprint(); if ($fp = Helper::getTlsFingerprint(data_get($protocol_settings, 'utls'))) {
$config['fp'] = $fp;
}
break; break;
case 2: case 2:
$config['tls'] = 1; $config['tls'] = 1;
$config['sni'] = data_get($protocol_settings, 'reality_settings.server_name'); $config['sni'] = data_get($protocol_settings, 'reality_settings.server_name');
$config['pbk'] = data_get($protocol_settings, 'reality_settings.public_key'); $config['pbk'] = data_get($protocol_settings, 'reality_settings.public_key');
$config['sid'] = data_get($protocol_settings, 'reality_settings.short_id'); $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; break;
default: default:
break; break;

View File

@@ -3,7 +3,6 @@ namespace App\Protocols;
use App\Utils\Helper; use App\Utils\Helper;
use Illuminate\Support\Arr; use Illuminate\Support\Arr;
use Illuminate\Support\Facades\File;
use App\Support\AbstractProtocol; use App\Support\AbstractProtocol;
use App\Models\Server; use App\Models\Server;
@@ -20,7 +19,6 @@ class SingBox extends AbstractProtocol
Server::TYPE_ANYTLS, Server::TYPE_ANYTLS,
Server::TYPE_SOCKS, Server::TYPE_SOCKS,
Server::TYPE_HTTP, Server::TYPE_HTTP,
Server::TYPE_MIERU,
]; ];
private $config; private $config;
const CUSTOM_TEMPLATE_FILE = 'resources/rules/custom.sing-box.json'; const CUSTOM_TEMPLATE_FILE = 'resources/rules/custom.sing-box.json';
@@ -55,9 +53,6 @@ class SingBox extends AbstractProtocol
'juicity' => [ 'juicity' => [
'base_version' => '1.7.0' 'base_version' => '1.7.0'
], ],
'shadowtls' => [
'base_version' => '1.6.0'
],
'wireguard' => [ 'wireguard' => [
'base_version' => '1.5.0' 'base_version' => '1.5.0'
], ],
@@ -292,11 +287,16 @@ class SingBox extends AbstractProtocol
'enabled' => true, 'enabled' => true,
'insecure' => (bool) data_get($protocol_settings, 'tls_settings.allow_insecure'), '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')) { if ($serverName = data_get($protocol_settings, 'tls_settings.server_name')) {
$array['tls']['server_name'] = $serverName; $array['tls']['server_name'] = $serverName;
} }
} }
$this->appendMultiplex($array, $protocol_settings);
$transport = match ($protocol_settings['network']) { $transport = match ($protocol_settings['network']) {
'tcp' => data_get($protocol_settings, 'network_settings.header.type', 'none') !== 'none' ? [ 'tcp' => data_get($protocol_settings, 'network_settings.header.type', 'none') !== 'none' ? [
'type' => 'http', 'type' => 'http',
@@ -354,12 +354,10 @@ class SingBox extends AbstractProtocol
$tlsConfig = [ $tlsConfig = [
'enabled' => true, 'enabled' => true,
'insecure' => (bool) data_get($protocol_settings, 'tls_settings.allow_insecure'), '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']) { switch ($protocol_settings['tls']) {
case 1: case 1:
if ($serverName = data_get($protocol_settings, 'tls_settings.server_name')) { if ($serverName = data_get($protocol_settings, 'tls_settings.server_name')) {
@@ -379,6 +377,8 @@ class SingBox extends AbstractProtocol
$array['tls'] = $tlsConfig; $array['tls'] = $tlsConfig;
} }
$this->appendMultiplex($array, $protocol_settings);
$transport = match ($protocol_settings['network']) { $transport = match ($protocol_settings['network']) {
'tcp' => data_get($protocol_settings, 'network_settings.header.type') == 'http' ? [ 'tcp' => data_get($protocol_settings, 'network_settings.header.type') == 'http' ? [
'type' => 'http', 'type' => 'http',
@@ -433,9 +433,15 @@ class SingBox extends AbstractProtocol
'insecure' => (bool) data_get($protocol_settings, 'allow_insecure', false), 'insecure' => (bool) data_get($protocol_settings, 'allow_insecure', false),
] ]
]; ];
$this->appendUtls($array['tls'], $protocol_settings);
if ($serverName = data_get($protocol_settings, 'server_name')) { if ($serverName = data_get($protocol_settings, 'server_name')) {
$array['tls']['server_name'] = $serverName; $array['tls']['server_name'] = $serverName;
} }
$this->appendMultiplex($array, $protocol_settings);
$transport = match (data_get($protocol_settings, 'network')) { $transport = match (data_get($protocol_settings, 'network')) {
'grpc' => [ 'grpc' => [
'type' => 'grpc', 'type' => 'grpc',
@@ -619,27 +625,39 @@ class SingBox extends AbstractProtocol
return $array; return $array;
} }
protected function buildMieru($password, $server): array protected function appendMultiplex(&$array, $protocol_settings)
{ {
$protocol_settings = data_get($server, 'protocol_settings', []); if ($multiplex = data_get($protocol_settings, 'multiplex')) {
$array = [ if (data_get($multiplex, 'enabled')) {
'type' => 'mieru', $array['multiplex'] = [
'tag' => $server['name'], 'enabled' => true,
'server' => $server['host'], 'protocol' => data_get($multiplex, 'protocol', 'yamux'),
'server_port' => $server['port'], 'max_connections' => data_get($multiplex, 'max_connections'),
'username' => $password, 'min_streams' => data_get($multiplex, 'min_streams'),
'password' => $password, 'max_streams' => data_get($multiplex, 'max_streams'),
'transport' => strtolower(data_get($protocol_settings, 'transport', 'tcp')), 'padding' => (bool) data_get($multiplex, 'padding', false),
]; ];
if (data_get($multiplex, 'brutal.enabled')) {
if (isset($server['ports'])) { $array['multiplex']['brutal'] = [
$array['server_port_range'] = [$server['ports']]; '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')) { protected function appendUtls(&$tlsConfig, $protocol_settings)
$array['multiplexing'] = $multiplexing; {
if ($utls = data_get($protocol_settings, 'utls')) {
if (data_get($utls, 'enabled')) {
$tlsConfig['utls'] = [
'enabled' => true,
'fingerprint' => Helper::getTlsFingerprint($utls)
];
}
} }
return $array;
} }
} }

View File

@@ -283,7 +283,9 @@ class Stash extends AbstractProtocol
$array['uuid'] = $uuid; $array['uuid'] = $uuid;
$array['udp'] = true; $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')) { switch (data_get($protocol_settings, 'tls')) {
case 1: case 1:
@@ -312,10 +314,12 @@ class Stash extends AbstractProtocol
$headerType = data_get($protocol_settings, 'network_settings.header.type', 'tcp'); $headerType = data_get($protocol_settings, 'network_settings.header.type', 'tcp');
$array['network'] = ($headerType === 'http') ? 'http' : 'tcp'; $array['network'] = ($headerType === 'http') ? 'http' : 'tcp';
if ($headerType === 'http') { if ($headerType === 'http') {
if ($httpOpts = array_filter([ if (
'headers' => data_get($protocol_settings, 'network_settings.header.request.headers'), $httpOpts = array_filter([
'path' => data_get($protocol_settings, 'network_settings.header.request.path', ['/']) 'headers' => data_get($protocol_settings, 'network_settings.header.request.headers'),
])) { 'path' => data_get($protocol_settings, 'network_settings.header.request.path', ['/'])
])
) {
$array['http-opts'] = $httpOpts; $array['http-opts'] = $httpOpts;
} }
} }
@@ -331,11 +335,11 @@ class Stash extends AbstractProtocol
$array['network'] = 'grpc'; $array['network'] = 'grpc';
$array['grpc-opts']['grpc-service-name'] = data_get($protocol_settings, 'network_settings.serviceName'); $array['grpc-opts']['grpc-service-name'] = data_get($protocol_settings, 'network_settings.serviceName');
break; break;
// case 'h2': // case 'h2':
// $array['network'] = 'h2'; // $array['network'] = 'h2';
// $array['h2-opts']['host'] = data_get($protocol_settings, 'network_settings.host'); // $array['h2-opts']['host'] = data_get($protocol_settings, 'network_settings.host');
// $array['h2-opts']['path'] = data_get($protocol_settings, 'network_settings.path'); // $array['h2-opts']['path'] = data_get($protocol_settings, 'network_settings.path');
// break; // break;
} }
return $array; return $array;

View File

@@ -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 public static function buildNodeConfig(Server $node): array
{ {
$nodeType = $node->type; $nodeType = $node->type;
@@ -120,28 +153,31 @@ class ServerService
'plugin' => $protocolSettings['plugin'], 'plugin' => $protocolSettings['plugin'],
'plugin_opts' => $protocolSettings['plugin_opts'], 'plugin_opts' => $protocolSettings['plugin_opts'],
'server_key' => match ($protocolSettings['cipher']) { 'server_key' => match ($protocolSettings['cipher']) {
'2022-blake3-aes-128-gcm' => Helper::getServerKey($node->created_at, 16), '2022-blake3-aes-128-gcm' => Helper::getServerKey($node->created_at, 16),
'2022-blake3-aes-256-gcm' => Helper::getServerKey($node->created_at, 32), '2022-blake3-aes-256-gcm' => Helper::getServerKey($node->created_at, 32),
default => null, default => null,
}, },
], ],
'vmess' => [ 'vmess' => [
...$baseConfig, ...$baseConfig,
'tls' => (int) $protocolSettings['tls'], 'tls' => (int) $protocolSettings['tls'],
'multiplex' => data_get($protocolSettings, 'multiplex'),
], ],
'trojan' => [ 'trojan' => [
...$baseConfig, ...$baseConfig,
'host' => $host, 'host' => $host,
'server_name' => $protocolSettings['server_name'], 'server_name' => $protocolSettings['server_name'],
'multiplex' => data_get($protocolSettings, 'multiplex'),
], ],
'vless' => [ 'vless' => [
...$baseConfig, ...$baseConfig,
'tls' => (int) $protocolSettings['tls'], 'tls' => (int) $protocolSettings['tls'],
'flow' => $protocolSettings['flow'], 'flow' => $protocolSettings['flow'],
'tls_settings' => match ((int) $protocolSettings['tls']) { 'tls_settings' => match ((int) $protocolSettings['tls']) {
2 => $protocolSettings['reality_settings'], 2 => $protocolSettings['reality_settings'],
default => $protocolSettings['tls_settings'], default => $protocolSettings['tls_settings'],
}, },
'multiplex' => data_get($protocolSettings, 'multiplex'),
], ],
'hysteria' => [ 'hysteria' => [
...$baseConfig, ...$baseConfig,
@@ -152,13 +188,13 @@ class ServerService
'up_mbps' => (int) $protocolSettings['bandwidth']['up'], 'up_mbps' => (int) $protocolSettings['bandwidth']['up'],
'down_mbps' => (int) $protocolSettings['bandwidth']['down'], 'down_mbps' => (int) $protocolSettings['bandwidth']['down'],
...match ((int) $protocolSettings['version']) { ...match ((int) $protocolSettings['version']) {
1 => ['obfs' => $protocolSettings['obfs']['password'] ?? null], 1 => ['obfs' => $protocolSettings['obfs']['password'] ?? null],
2 => [ 2 => [
'obfs' => $protocolSettings['obfs']['open'] ? $protocolSettings['obfs']['type'] : null, 'obfs' => $protocolSettings['obfs']['open'] ? $protocolSettings['obfs']['type'] : null,
'obfs-password' => $protocolSettings['obfs']['password'] ?? null, 'obfs-password' => $protocolSettings['obfs']['password'] ?? null,
], ],
default => [], default => [],
}, },
], ],
'tuic' => [ 'tuic' => [
...$baseConfig, ...$baseConfig,
@@ -195,8 +231,10 @@ class ServerService
], ],
'mieru' => [ 'mieru' => [
...$baseConfig, ...$baseConfig,
'server_port' => (string) $serverPort, 'server_port' => (int) $serverPort,
'protocol' => (int) $protocolSettings['protocol'], 'transport' => data_get($protocolSettings, 'transport', 'TCP'),
'traffic_pattern' => $protocolSettings['traffic_pattern'],
// 'multiplex' => data_get($protocolSettings, 'multiplex'),
], ],
default => [], default => [],
}; };

View File

@@ -188,8 +188,20 @@ class Helper
public static function getIpByDomainName($domain) { public static function getIpByDomainName($domain) {
return gethostbynamel($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']; $fingerprints = ['chrome', 'firefox', 'safari', 'ios', 'edge', 'qq'];
return Arr::random($fingerprints); return Arr::random($fingerprints);
} }

View File

@@ -141,8 +141,9 @@ docker compose up -d
## Troubleshooting ## Troubleshooting
If you encounter any issues during installation or operation, please check: If you encounter any issues during installation or operation, please check:
1. System requirements are met 1. **Empty Admin Dashboard**: If the admin panel is blank, run `git submodule update --init --recursive --force` to restore the theme files.
2. All required ports are available 2. System requirements are met
3. All required ports are available
3. Docker services are running properly 3. Docker services are running properly
4. Nginx configuration is correct 4. Nginx configuration is correct
5. Check logs for detailed error messages 5. Check logs for detailed error messages

View File

@@ -169,8 +169,9 @@ sh update.sh
## Troubleshooting ## Troubleshooting
### Common Issues ### Common Issues
1. Changes to admin path require service 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. Any code changes after enabling Octane require restart to take effect 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 3. When PHP extension installation fails, check if PHP version is correct
4. For database connection failures, check database configuration and permissions 4. For database connection failures, check database configuration and permissions