mirror of
https://github.com/lkddi/Xboard.git
synced 2026-04-24 03:57:27 +08:00
feat: introduce WebSocket sync for XBoard nodes
- Implement Workerman-based `xboard:ws-server` for real-time node synchronization. - Support custom routes, outbounds, and certificate configurations via JSON. - Optimize scheduled tasks with `lazyById` to minimize memory footprint. - Enhance reactivity using Observers for `Plan`, `Server`, and `ServerRoute`. - Expand protocol support for `httpupgrade`, `h2`, and `mieru`.
This commit is contained in:
@@ -35,6 +35,7 @@ class ClashMeta extends AbstractProtocol
|
||||
'grpc' => '0.0.0',
|
||||
'http' => '0.0.0',
|
||||
'h2' => '0.0.0',
|
||||
'httpupgrade' => '0.0.0',
|
||||
],
|
||||
'strict' => true,
|
||||
],
|
||||
@@ -246,7 +247,7 @@ class ClashMeta extends AbstractProtocol
|
||||
];
|
||||
|
||||
if (data_get($protocol_settings, 'tls')) {
|
||||
$array['tls'] = true;
|
||||
$array['tls'] = (bool) data_get($protocol_settings, 'tls');
|
||||
$array['skip-cert-verify'] = (bool) data_get($protocol_settings, 'tls_settings.allow_insecure', false);
|
||||
$array['servername'] = data_get($protocol_settings, 'tls_settings.server_name');
|
||||
}
|
||||
@@ -275,6 +276,22 @@ class ClashMeta extends AbstractProtocol
|
||||
if ($serviceName = data_get($protocol_settings, 'network_settings.serviceName'))
|
||||
$array['grpc-opts']['grpc-service-name'] = $serviceName;
|
||||
break;
|
||||
case 'h2':
|
||||
$array['network'] = 'h2';
|
||||
$array['h2-opts'] = [];
|
||||
if ($path = data_get($protocol_settings, 'network_settings.path'))
|
||||
$array['h2-opts']['path'] = $path;
|
||||
if ($host = data_get($protocol_settings, 'network_settings.host'))
|
||||
$array['h2-opts']['host'] = is_array($host) ? $host : [$host];
|
||||
break;
|
||||
case 'httpupgrade':
|
||||
$array['network'] = 'ws';
|
||||
$array['ws-opts'] = ['v2ray-http-upgrade' => true];
|
||||
if ($path = data_get($protocol_settings, 'network_settings.path'))
|
||||
$array['ws-opts']['path'] = $path;
|
||||
if ($host = data_get($protocol_settings, 'network_settings.host'))
|
||||
$array['ws-opts']['headers'] = ['Host' => $host];
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -322,6 +339,19 @@ class ClashMeta extends AbstractProtocol
|
||||
}
|
||||
|
||||
switch (data_get($protocol_settings, 'network')) {
|
||||
case 'tcp':
|
||||
$array['network'] = 'tcp';
|
||||
$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', ['/'])
|
||||
])) {
|
||||
$array['http-opts'] = $httpOpts;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'ws':
|
||||
$array['network'] = 'ws';
|
||||
if ($path = data_get($protocol_settings, 'network_settings.path'))
|
||||
@@ -334,6 +364,22 @@ class ClashMeta extends AbstractProtocol
|
||||
if ($serviceName = data_get($protocol_settings, 'network_settings.serviceName'))
|
||||
$array['grpc-opts']['grpc-service-name'] = $serviceName;
|
||||
break;
|
||||
case 'h2':
|
||||
$array['network'] = 'h2';
|
||||
$array['h2-opts'] = [];
|
||||
if ($path = data_get($protocol_settings, 'network_settings.path'))
|
||||
$array['h2-opts']['path'] = $path;
|
||||
if ($host = data_get($protocol_settings, 'network_settings.host'))
|
||||
$array['h2-opts']['host'] = is_array($host) ? $host : [$host];
|
||||
break;
|
||||
case 'httpupgrade':
|
||||
$array['network'] = 'ws';
|
||||
$array['ws-opts'] = ['v2ray-http-upgrade' => true];
|
||||
if ($path = data_get($protocol_settings, 'network_settings.path'))
|
||||
$array['ws-opts']['path'] = $path;
|
||||
if ($host = data_get($protocol_settings, 'network_settings.host'))
|
||||
$array['ws-opts']['headers'] = ['Host' => $host];
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -373,6 +419,22 @@ class ClashMeta extends AbstractProtocol
|
||||
if ($serviceName = data_get($protocol_settings, 'network_settings.serviceName'))
|
||||
$array['grpc-opts']['grpc-service-name'] = $serviceName;
|
||||
break;
|
||||
case 'h2':
|
||||
$array['network'] = 'h2';
|
||||
$array['h2-opts'] = [];
|
||||
if ($path = data_get($protocol_settings, 'network_settings.path'))
|
||||
$array['h2-opts']['path'] = $path;
|
||||
if ($host = data_get($protocol_settings, 'network_settings.host'))
|
||||
$array['h2-opts']['host'] = is_array($host) ? $host : [$host];
|
||||
break;
|
||||
case 'httpupgrade':
|
||||
$array['network'] = 'ws';
|
||||
$array['ws-opts'] = ['v2ray-http-upgrade' => true];
|
||||
if ($path = data_get($protocol_settings, 'network_settings.path'))
|
||||
$array['ws-opts']['path'] = $path;
|
||||
if ($host = data_get($protocol_settings, 'network_settings.host'))
|
||||
$array['ws-opts']['headers'] = ['Host' => $host];
|
||||
break;
|
||||
default:
|
||||
$array['network'] = 'tcp';
|
||||
break;
|
||||
@@ -396,6 +458,9 @@ class ClashMeta extends AbstractProtocol
|
||||
if (isset($server['ports'])) {
|
||||
$array['ports'] = $server['ports'];
|
||||
}
|
||||
if ($hopInterval = data_get($protocol_settings, 'hop_interval')) {
|
||||
$array['hop-interval'] = (int) $hopInterval;
|
||||
}
|
||||
switch (data_get($protocol_settings, 'version')) {
|
||||
case 1:
|
||||
$array['type'] = 'hysteria';
|
||||
|
||||
+87
-17
@@ -20,6 +20,7 @@ class General extends AbstractProtocol
|
||||
Server::TYPE_ANYTLS,
|
||||
Server::TYPE_SOCKS,
|
||||
Server::TYPE_TUIC,
|
||||
Server::TYPE_HTTP,
|
||||
];
|
||||
|
||||
protected $protocolRequirements = [
|
||||
@@ -43,6 +44,7 @@ class General extends AbstractProtocol
|
||||
Server::TYPE_ANYTLS => self::buildAnyTLS($item['password'], $item),
|
||||
Server::TYPE_SOCKS => self::buildSocks($item['password'], $item),
|
||||
Server::TYPE_TUIC => self::buildTuic($item['password'], $item),
|
||||
Server::TYPE_HTTP => self::buildHttp($item['password'], $item),
|
||||
default => '',
|
||||
};
|
||||
}
|
||||
@@ -113,6 +115,21 @@ class General extends AbstractProtocol
|
||||
if ($path = data_get($protocol_settings, 'network_settings.serviceName'))
|
||||
$config['path'] = $path;
|
||||
break;
|
||||
case 'h2':
|
||||
$config['net'] = 'h2';
|
||||
$config['type'] = 'h2';
|
||||
if ($path = data_get($protocol_settings, 'network_settings.path'))
|
||||
$config['path'] = $path;
|
||||
if ($host = data_get($protocol_settings, 'network_settings.host'))
|
||||
$config['host'] = is_array($host) ? implode(',', $host) : $host;
|
||||
break;
|
||||
case 'httpupgrade':
|
||||
$config['net'] = 'httpupgrade';
|
||||
$config['type'] = 'httpupgrade';
|
||||
if ($path = data_get($protocol_settings, 'network_settings.path'))
|
||||
$config['path'] = $path;
|
||||
$config['host'] = data_get($protocol_settings, 'network_settings.host', $server['host']);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -166,6 +183,13 @@ class General extends AbstractProtocol
|
||||
if ($path = data_get($protocol_settings, 'network_settings.serviceName'))
|
||||
$config['serviceName'] = $path;
|
||||
break;
|
||||
case 'h2':
|
||||
$config['type'] = 'http';
|
||||
if ($path = data_get($protocol_settings, 'network_settings.path'))
|
||||
$config['path'] = $path;
|
||||
if ($h2Host = data_get($protocol_settings, 'network_settings.host'))
|
||||
$config['host'] = is_array($h2Host) ? implode(',', $h2Host) : $h2Host;
|
||||
break;
|
||||
case 'kcp':
|
||||
if ($path = data_get($protocol_settings, 'network_settings.seed'))
|
||||
$config['path'] = $path;
|
||||
@@ -215,6 +239,19 @@ class General extends AbstractProtocol
|
||||
if ($serviceName = data_get($protocol_settings, 'network_settings.serviceName'))
|
||||
$array['serviceName'] = $serviceName;
|
||||
break;
|
||||
case 'h2':
|
||||
$array['type'] = 'http';
|
||||
if ($path = data_get($protocol_settings, 'network_settings.path'))
|
||||
$array['path'] = $path;
|
||||
if ($host = data_get($protocol_settings, 'network_settings.host'))
|
||||
$array['host'] = is_array($host) ? implode(',', $host) : $host;
|
||||
break;
|
||||
case 'httpupgrade':
|
||||
$array['type'] = 'httpupgrade';
|
||||
if ($path = data_get($protocol_settings, 'network_settings.path'))
|
||||
$array['path'] = $path;
|
||||
$array['host'] = data_get($protocol_settings, 'network_settings.host', $server['host']);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -230,31 +267,40 @@ class General extends AbstractProtocol
|
||||
{
|
||||
$protocol_settings = $server['protocol_settings'];
|
||||
$params = [];
|
||||
// Return empty if version is not 2
|
||||
if ($server['protocol_settings']['version'] !== 2) {
|
||||
return '';
|
||||
}
|
||||
$version = data_get($protocol_settings, 'version', 2);
|
||||
|
||||
if ($serverName = data_get($protocol_settings, 'tls.server_name')) {
|
||||
$params['sni'] = $serverName;
|
||||
$params['security'] = 'tls';
|
||||
}
|
||||
$params['insecure'] = data_get($protocol_settings, 'tls.allow_insecure') ? '1' : '0';
|
||||
|
||||
if (data_get($protocol_settings, 'obfs.open')) {
|
||||
$params['obfs'] = 'salamander';
|
||||
$params['obfs-password'] = data_get($protocol_settings, 'obfs.password');
|
||||
}
|
||||
if (isset($server['ports'])) {
|
||||
$params['mport'] = $server['ports'];
|
||||
}
|
||||
|
||||
$params['insecure'] = data_get($protocol_settings, 'tls.allow_insecure');
|
||||
|
||||
$query = http_build_query($params);
|
||||
$name = rawurlencode($server['name']);
|
||||
$addr = Helper::wrapIPv6($server['host']);
|
||||
|
||||
$uri = "hysteria2://{$password}@{$addr}:{$server['port']}?{$query}#{$name}";
|
||||
if ($version === 2) {
|
||||
if (data_get($protocol_settings, 'obfs.open')) {
|
||||
$params['obfs'] = 'salamander';
|
||||
$params['obfs-password'] = data_get($protocol_settings, 'obfs.password');
|
||||
}
|
||||
if (isset($server['ports'])) {
|
||||
$params['mport'] = $server['ports'];
|
||||
}
|
||||
|
||||
$query = http_build_query($params);
|
||||
$uri = "hysteria2://{$password}@{$addr}:{$server['port']}?{$query}#{$name}";
|
||||
} else {
|
||||
$params['protocol'] = 'udp';
|
||||
$params['auth'] = $password;
|
||||
if ($upMbps = data_get($protocol_settings, 'bandwidth.up'))
|
||||
$params['upmbps'] = $upMbps;
|
||||
if ($downMbps = data_get($protocol_settings, 'bandwidth.down'))
|
||||
$params['downmbps'] = $downMbps;
|
||||
if ($obfsPassword = data_get($protocol_settings, 'obfs.password'))
|
||||
$params['obfsParam'] = $obfsPassword;
|
||||
|
||||
$query = http_build_query($params);
|
||||
$uri = "hysteria://{$addr}:{$server['port']}?{$query}#{$name}";
|
||||
}
|
||||
$uri .= "\r\n";
|
||||
|
||||
return $uri;
|
||||
@@ -333,4 +379,28 @@ class General extends AbstractProtocol
|
||||
$credentials = base64_encode("{$password}:{$password}");
|
||||
return "socks://{$credentials}@{$server['host']}:{$server['port']}#{$name}\r\n";
|
||||
}
|
||||
|
||||
public static function buildHttp($password, $server)
|
||||
{
|
||||
$protocol_settings = data_get($server, 'protocol_settings', []);
|
||||
$name = rawurlencode($server['name']);
|
||||
$addr = Helper::wrapIPv6($server['host']);
|
||||
$credentials = base64_encode("{$password}:{$password}");
|
||||
|
||||
$params = [];
|
||||
if (data_get($protocol_settings, 'tls')) {
|
||||
$params['security'] = 'tls';
|
||||
if ($serverName = data_get($protocol_settings, 'tls_settings.server_name')) {
|
||||
$params['sni'] = $serverName;
|
||||
}
|
||||
$params['allowInsecure'] = data_get($protocol_settings, 'tls_settings.allow_insecure') ? '1' : '0';
|
||||
}
|
||||
|
||||
$uri = "http://{$credentials}@{$addr}:{$server['port']}";
|
||||
if (!empty($params)) {
|
||||
$uri .= '?' . http_build_query($params);
|
||||
}
|
||||
$uri .= "#{$name}\r\n";
|
||||
return $uri;
|
||||
}
|
||||
}
|
||||
|
||||
+146
-7
@@ -20,6 +20,7 @@ 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';
|
||||
@@ -62,6 +63,9 @@ class SingBox extends AbstractProtocol
|
||||
],
|
||||
'anytls' => [
|
||||
'base_version' => '1.12.0'
|
||||
],
|
||||
'mieru' => [
|
||||
'base_version' => '1.12.0'
|
||||
]
|
||||
]
|
||||
];
|
||||
@@ -72,6 +76,7 @@ class SingBox extends AbstractProtocol
|
||||
$this->config = $this->loadConfig();
|
||||
$this->buildOutbounds();
|
||||
$this->buildRule();
|
||||
$this->adaptConfigForVersion();
|
||||
$user = $this->user;
|
||||
|
||||
return response()
|
||||
@@ -133,6 +138,10 @@ class SingBox extends AbstractProtocol
|
||||
$httpConfig = $this->buildHttp($this->user['uuid'], $item);
|
||||
$proxies[] = $httpConfig;
|
||||
}
|
||||
if ($item['type'] === Server::TYPE_MIERU) {
|
||||
$mieruConfig = $this->buildMieru($this->user['uuid'], $item);
|
||||
$proxies[] = $mieruConfig;
|
||||
}
|
||||
}
|
||||
foreach ($outbounds as &$outbound) {
|
||||
if (in_array($outbound['type'], ['urltest', 'selector'])) {
|
||||
@@ -161,6 +170,91 @@ class SingBox extends AbstractProtocol
|
||||
$this->config['route']['rules'] = $rules;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据客户端版本自适应配置格式
|
||||
*
|
||||
* sing-box 版本断点:
|
||||
* - 1.8.0: rule_set 替代 geoip/geosite db, cache_file 替代 clash_api.cache_file
|
||||
* - 1.10.0: address 数组替代 inet4_address/inet6_address
|
||||
* - 1.11.0: 移除 endpoint_independent_nat, sniff_override_destination
|
||||
*/
|
||||
protected function adaptConfigForVersion(): void
|
||||
{
|
||||
$coreVersion = $this->getSingBoxCoreVersion();
|
||||
if (empty($coreVersion)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// >= 1.11.0: 移除已废弃字段,避免 "配置已过时" 警告
|
||||
if (version_compare($coreVersion, '1.11.0', '>=')) {
|
||||
$this->removeDeprecatedFieldsV111();
|
||||
}
|
||||
|
||||
// < 1.10.0: address 数组 → inet4_address/inet6_address
|
||||
if (version_compare($coreVersion, '1.10.0', '<')) {
|
||||
$this->convertAddressToLegacy();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取实际 sing-box 核心版本
|
||||
*
|
||||
* sing-box 客户端直接报核心版本,hiddify/sfm 等 wrapper 客户端
|
||||
* 报的是 app 版本,需要映射到对应的 sing-box 核心版本
|
||||
*/
|
||||
private function getSingBoxCoreVersion(): ?string
|
||||
{
|
||||
if (empty($this->clientVersion)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// sing-box 原生客户端,版本即核心版本
|
||||
if ($this->clientName === 'sing-box') {
|
||||
return $this->clientVersion;
|
||||
}
|
||||
|
||||
// Hiddify/SFM 等 wrapper 默认内置较新的 sing-box 核心
|
||||
// 保守策略: 直接按最新格式输出(移除废弃字段),因为这些客户端普遍内置 >= 1.11 的核心
|
||||
return '1.11.0';
|
||||
}
|
||||
|
||||
/**
|
||||
* sing-box >= 1.11.0: 移除废弃字段
|
||||
*/
|
||||
private function removeDeprecatedFieldsV111(): void
|
||||
{
|
||||
if (!isset($this->config['inbounds'])) {
|
||||
return;
|
||||
}
|
||||
foreach ($this->config['inbounds'] as &$inbound) {
|
||||
unset($inbound['endpoint_independent_nat']);
|
||||
unset($inbound['sniff_override_destination']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* sing-box < 1.10.0: 将 tun address 数组转换为 inet4_address/inet6_address
|
||||
*/
|
||||
private function convertAddressToLegacy(): void
|
||||
{
|
||||
if (!isset($this->config['inbounds'])) {
|
||||
return;
|
||||
}
|
||||
foreach ($this->config['inbounds'] as &$inbound) {
|
||||
if ($inbound['type'] !== 'tun' || !isset($inbound['address'])) {
|
||||
continue;
|
||||
}
|
||||
foreach ($inbound['address'] as $addr) {
|
||||
if (str_contains($addr, ':')) {
|
||||
$inbound['inet6_address'] = $addr;
|
||||
} else {
|
||||
$inbound['inet4_address'] = $addr;
|
||||
}
|
||||
}
|
||||
unset($inbound['address']);
|
||||
}
|
||||
}
|
||||
|
||||
protected function buildShadowsocks($password, $server)
|
||||
{
|
||||
$protocol_settings = data_get($server, 'protocol_settings');
|
||||
@@ -191,14 +285,16 @@ class SingBox extends AbstractProtocol
|
||||
'uuid' => $uuid,
|
||||
'security' => 'auto',
|
||||
'alter_id' => 0,
|
||||
'transport' => [],
|
||||
'tls' => $protocol_settings['tls'] ? [
|
||||
];
|
||||
|
||||
if ($protocol_settings['tls']) {
|
||||
$array['tls'] = [
|
||||
'enabled' => true,
|
||||
'insecure' => (bool) data_get($protocol_settings, 'tls_settings.allow_insecure'),
|
||||
] : null
|
||||
];
|
||||
if ($serverName = data_get($protocol_settings, 'tls_settings.server_name')) {
|
||||
$array['tls']['server_name'] = $serverName;
|
||||
];
|
||||
if ($serverName = data_get($protocol_settings, 'tls_settings.server_name')) {
|
||||
$array['tls']['server_name'] = $serverName;
|
||||
}
|
||||
}
|
||||
|
||||
$transport = match ($protocol_settings['network']) {
|
||||
@@ -218,6 +314,20 @@ class SingBox extends AbstractProtocol
|
||||
'type' => 'grpc',
|
||||
'service_name' => data_get($protocol_settings, 'network_settings.serviceName')
|
||||
],
|
||||
'h2' => [
|
||||
'type' => 'http',
|
||||
'host' => data_get($protocol_settings, 'network_settings.host'),
|
||||
'path' => data_get($protocol_settings, 'network_settings.path')
|
||||
],
|
||||
'httpupgrade' => [
|
||||
'type' => 'httpupgrade',
|
||||
'path' => data_get($protocol_settings, 'network_settings.path'),
|
||||
'host' => data_get($protocol_settings, 'network_settings.host', $server['host']),
|
||||
'headers' => data_get($protocol_settings, 'network_settings.headers')
|
||||
],
|
||||
'quic' => [
|
||||
'type' => 'quic'
|
||||
],
|
||||
default => null
|
||||
};
|
||||
|
||||
@@ -296,6 +406,9 @@ class SingBox extends AbstractProtocol
|
||||
'host' => data_get($protocol_settings, 'network_settings.host', $server['host']),
|
||||
'headers' => data_get($protocol_settings, 'network_settings.headers')
|
||||
],
|
||||
'quic' => [
|
||||
'type' => 'quic'
|
||||
],
|
||||
default => null
|
||||
};
|
||||
|
||||
@@ -337,7 +450,9 @@ class SingBox extends AbstractProtocol
|
||||
]),
|
||||
default => null
|
||||
};
|
||||
$array['transport'] = $transport;
|
||||
if ($transport) {
|
||||
$array['transport'] = array_filter($transport, fn($value) => !is_null($value));
|
||||
}
|
||||
return $array;
|
||||
}
|
||||
|
||||
@@ -503,4 +618,28 @@ class SingBox extends AbstractProtocol
|
||||
|
||||
return $array;
|
||||
}
|
||||
|
||||
protected function buildMieru($password, $server): array
|
||||
{
|
||||
$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 ($multiplexing = data_get($protocol_settings, 'multiplexing')) {
|
||||
$array['multiplexing'] = $multiplexing;
|
||||
}
|
||||
|
||||
return $array;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user