From 7dacb69275a6884d744475a8bf1385ab77b03186 Mon Sep 17 00:00:00 2001 From: xboard Date: Mon, 23 Mar 2026 14:56:41 +0800 Subject: [PATCH] feat: Trojan Reality support and protocol distribution optimizations --- .../V1/Client/ClientController.php | 3 +- app/Http/Requests/Admin/ServerSave.php | 7 + app/Models/Server.php | 28 +- app/Protocols/Clash.php | 5 +- app/Protocols/ClashMeta.php | 27 +- app/Protocols/General.php | 63 ++- app/Protocols/Loon.php | 73 +++- app/Protocols/QuantumultX.php | 229 +++++++---- app/Protocols/Shadowrocket.php | 62 ++- app/Protocols/SingBox.php | 370 +++++++++++------- app/Protocols/Stash.php | 118 ++++-- app/Protocols/Surfboard.php | 14 +- app/Protocols/Surge.php | 18 +- app/Services/ServerService.php | 7 +- app/Support/AbstractProtocol.php | 9 +- 15 files changed, 727 insertions(+), 306 deletions(-) diff --git a/app/Http/Controllers/V1/Client/ClientController.php b/app/Http/Controllers/V1/Client/ClientController.php index 31c6054..e4437bc 100644 --- a/app/Http/Controllers/V1/Client/ClientController.php +++ b/app/Http/Controllers/V1/Client/ClientController.php @@ -80,7 +80,8 @@ class ClientController extends Controller 'user' => $user, 'servers' => $serversFiltered, 'clientName' => $clientInfo['name'] ?? null, - 'clientVersion' => $clientInfo['version'] ?? null + 'clientVersion' => $clientInfo['version'] ?? null, + 'userAgent' => $clientInfo['flag'] ?? null ]); return $protocolInstance->handle(); diff --git a/app/Http/Requests/Admin/ServerSave.php b/app/Http/Requests/Admin/ServerSave.php index b017bff..9932302 100644 --- a/app/Http/Requests/Admin/ServerSave.php +++ b/app/Http/Requests/Admin/ServerSave.php @@ -42,10 +42,17 @@ class ServerSave extends FormRequest 'tls_settings.allow_insecure' => 'nullable|boolean', ], 'trojan' => [ + 'tls' => 'nullable|integer', 'network' => 'required|string', 'network_settings' => 'nullable|array', 'server_name' => 'nullable|string', 'allow_insecure' => 'nullable|boolean', + 'reality_settings.allow_insecure' => 'nullable|boolean', + 'reality_settings.server_name' => 'nullable|string', + 'reality_settings.server_port' => 'nullable|integer', + 'reality_settings.public_key' => 'nullable|string', + 'reality_settings.private_key' => 'nullable|string', + 'reality_settings.short_id' => 'nullable|string', ], 'hysteria' => [ 'version' => 'required|integer', diff --git a/app/Models/Server.php b/app/Models/Server.php index 21234b2..ff91889 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -148,6 +148,20 @@ class Server extends Model ] ]; + private const REALITY_CONFIGURATION = [ + 'reality_settings' => [ + 'type' => 'object', + 'fields' => [ + 'server_name' => ['type' => 'string', 'default' => null], + 'server_port' => ['type' => 'string', 'default' => null], + 'public_key' => ['type' => 'string', 'default' => null], + 'private_key' => ['type' => 'string', 'default' => null], + 'short_id' => ['type' => 'string', 'default' => null], + 'allow_insecure' => ['type' => 'boolean', 'default' => false], + ] + ] + ]; + private const UTLS_CONFIGURATION = [ 'utls' => [ 'type' => 'object', @@ -160,10 +174,12 @@ class Server extends Model private const PROTOCOL_CONFIGURATIONS = [ self::TYPE_TROJAN => [ + 'tls' => ['type' => 'integer', 'default' => 1], 'network' => ['type' => 'string', 'default' => null], 'network_settings' => ['type' => 'array', 'default' => null], 'server_name' => ['type' => 'string', 'default' => null], 'allow_insecure' => ['type' => 'boolean', 'default' => false], + ...self::REALITY_CONFIGURATION, ...self::MULTIPLEX_CONFIGURATION, ...self::UTLS_CONFIGURATION ], @@ -182,17 +198,7 @@ class Server extends Model 'flow' => ['type' => 'string', 'default' => null], 'network' => ['type' => 'string', 'default' => null], 'network_settings' => ['type' => 'array', 'default' => null], - 'reality_settings' => [ - 'type' => 'object', - 'fields' => [ - 'allow_insecure' => ['type' => 'boolean', 'default' => false], - 'server_port' => ['type' => 'string', 'default' => null], - 'server_name' => ['type' => 'string', 'default' => null], - 'public_key' => ['type' => 'string', 'default' => null], - 'private_key' => ['type' => 'string', 'default' => null], - 'short_id' => ['type' => 'string', 'default' => null] - ] - ], + ...self::REALITY_CONFIGURATION, ...self::MULTIPLEX_CONFIGURATION, ...self::UTLS_CONFIGURATION ], diff --git a/app/Protocols/Clash.php b/app/Protocols/Clash.php index 0b99664..f84d99f 100644 --- a/app/Protocols/Clash.php +++ b/app/Protocols/Clash.php @@ -199,8 +199,9 @@ class Clash extends AbstractProtocol switch (data_get($protocol_settings, 'network')) { case 'tcp': - $array['network'] = data_get($protocol_settings, 'network_settings.header.type'); - if (data_get($protocol_settings, 'network_settings.header.type', 'none') !== 'none') { + $headerType = data_get($protocol_settings, 'network_settings.header.type', 'none'); + $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', ['/']) diff --git a/app/Protocols/ClashMeta.php b/app/Protocols/ClashMeta.php index 8e73fe5..65d0e2e 100644 --- a/app/Protocols/ClashMeta.php +++ b/app/Protocols/ClashMeta.php @@ -271,8 +271,9 @@ class ClashMeta extends AbstractProtocol 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') { + $headerType = data_get($protocol_settings, 'network_settings.header.type', 'none'); + $array['network'] = ($headerType === 'http') ? 'http' : 'tcp'; + if ($headerType === 'http') { if ( $httpOpts = array_filter([ 'headers' => data_get($protocol_settings, 'network_settings.header.request.headers'), @@ -420,10 +421,26 @@ class ClashMeta extends AbstractProtocol 'port' => $server['port'], 'password' => $password, 'udp' => true, - 'skip-cert-verify' => (bool) data_get($protocol_settings, 'allow_insecure', false) ]; - if ($serverName = data_get($protocol_settings, 'server_name')) { - $array['sni'] = $serverName; + + $tlsMode = (int) data_get($protocol_settings, 'tls', 1); + switch ($tlsMode) { + case 2: // Reality + $array['skip-cert-verify'] = (bool) data_get($protocol_settings, 'reality_settings.allow_insecure', false); + if ($serverName = data_get($protocol_settings, 'reality_settings.server_name')) { + $array['sni'] = $serverName; + } + $array['reality-opts'] = [ + 'public-key' => data_get($protocol_settings, 'reality_settings.public_key'), + 'short-id' => data_get($protocol_settings, 'reality_settings.short_id'), + ]; + break; + default: // Standard TLS + $array['skip-cert-verify'] = (bool) data_get($protocol_settings, 'allow_insecure', false); + if ($serverName = data_get($protocol_settings, 'server_name')) { + $array['sni'] = $serverName; + } + break; } self::appendUtls($array, $protocol_settings); diff --git a/app/Protocols/General.php b/app/Protocols/General.php index 3ffe6e9..e94f688 100644 --- a/app/Protocols/General.php +++ b/app/Protocols/General.php @@ -48,7 +48,9 @@ class General extends AbstractProtocol default => '', }; } - return response(base64_encode($uri))->header('content-type', 'text/plain'); + return response(base64_encode($uri)) + ->header('content-type', 'text/plain') + ->header('subscription-userinfo', "upload={$user['u']}; download={$user['d']}; total={$user['transfer_enable']}; expire={$user['expired_at']}"); } public static function buildShadowsocks($password, $server) @@ -66,7 +68,7 @@ class General extends AbstractProtocol $plugin_opts = data_get($protocol_settings, 'plugin_opts'); $url = "ss://{$str}@{$addr}:{$server['port']}"; if ($plugin && $plugin_opts) { - $url .= '/?' . 'plugin=' . $plugin . ';' . rawurlencode($plugin_opts); + $url .= '/?' . 'plugin=' . rawurlencode($plugin . ';' . $plugin_opts); } $url .= "#{$name}\r\n"; return $url; @@ -91,6 +93,9 @@ class General extends AbstractProtocol if ($serverName = data_get($protocol_settings, 'tls_settings.server_name')) { $config['sni'] = $serverName; } + if ($fp = Helper::getTlsFingerprint(data_get($protocol_settings, 'utls'))) { + $config['fp'] = $fp; + } switch ($protocol_settings['network']) { case 'tcp': @@ -148,7 +153,7 @@ class General extends AbstractProtocol 'security' => '', //传输层安全 tls/reality 'encryption' => 'none', //加密方式 'type' => $server['protocol_settings']['network'], //传输协议 - 'flow' => $protocol_settings['flow'] ? $protocol_settings['flow'] : null, + 'flow' => data_get($protocol_settings, 'flow'), ]; // 处理TLS switch ($server['protocol_settings']['tls']) { @@ -160,6 +165,9 @@ class General extends AbstractProtocol if ($serverName = data_get($protocol_settings, 'tls_settings.server_name')) { $config['sni'] = $serverName; } + if (data_get($protocol_settings, 'tls_settings.allow_insecure')) { + $config['allowInsecure'] = '1'; + } break; case 2: //reality $config['security'] = "reality"; @@ -224,11 +232,30 @@ class General extends AbstractProtocol $protocol_settings = $server['protocol_settings']; $name = rawurlencode($server['name']); $array = []; - $array['allowInsecure'] = $protocol_settings['allow_insecure']; - if ($serverName = data_get($protocol_settings, 'server_name')) { - $array['peer'] = $serverName; - $array['sni'] = $serverName; + $tlsMode = (int) data_get($protocol_settings, 'tls', 1); + + switch ($tlsMode) { + case 2: // Reality + $array['security'] = 'reality'; + $array['pbk'] = data_get($protocol_settings, 'reality_settings.public_key'); + $array['sid'] = data_get($protocol_settings, 'reality_settings.short_id'); + $array['sni'] = data_get($protocol_settings, 'reality_settings.server_name'); + if ($fp = Helper::getTlsFingerprint(data_get($protocol_settings, 'utls'))) { + $array['fp'] = $fp; + } + break; + default: // Standard TLS + $array['allowInsecure'] = data_get($protocol_settings, 'allow_insecure', false); + if ($serverName = data_get($protocol_settings, 'server_name')) { + $array['peer'] = $serverName; + $array['sni'] = $serverName; + } + if ($fp = Helper::getTlsFingerprint(data_get($protocol_settings, 'utls'))) { + $array['fp'] = $fp; + } + break; } + switch ($server['protocol_settings']['network']) { case 'ws': $array['type'] = 'ws'; @@ -299,8 +326,10 @@ class General extends AbstractProtocol $params['upmbps'] = $upMbps; if ($downMbps = data_get($protocol_settings, 'bandwidth.down')) $params['downmbps'] = $downMbps; - if ($obfsPassword = data_get($protocol_settings, 'obfs.password')) + if (data_get($protocol_settings, 'obfs.open') && ($obfsPassword = data_get($protocol_settings, 'obfs.password'))) { + $params['obfs'] = 'xplus'; $params['obfsParam'] = $obfsPassword; + } $query = http_build_query($params); $uri = "hysteria://{$addr}:{$server['port']}?{$query}#{$name}"; @@ -309,8 +338,8 @@ class General extends AbstractProtocol return $uri; } - - + + public static function buildTuic($password, $server) { $protocol_settings = data_get($server, 'protocol_settings', []); @@ -344,6 +373,10 @@ class General extends AbstractProtocol $udpRelay = data_get($protocol_settings, 'udp_relay_mode', 'native'); $queryParams['udp-relay-mode'] = $udpRelay; + if (data_get($protocol_settings, 'tls.allow_insecure')) { + $queryParams['insecure'] = '1'; + } + $query = http_build_query($queryParams); // 构造完整URI,格式: @@ -361,7 +394,7 @@ class General extends AbstractProtocol - + public static function buildAnyTLS($password, $server) { @@ -372,16 +405,18 @@ class General extends AbstractProtocol 'insecure' => data_get($protocol_settings, 'tls.allow_insecure') ]; $query = http_build_query($params); - $uri = "anytls://{$password}@{$server['host']}:{$server['port']}?{$query}#{$name}"; + $addr = Helper::wrapIPv6($server['host']); + $uri = "anytls://{$password}@{$addr}:{$server['port']}?{$query}#{$name}"; $uri .= "\r\n"; return $uri; } - + public static function buildSocks($password, $server) { $name = rawurlencode($server['name']); $credentials = base64_encode("{$password}:{$password}"); - return "socks://{$credentials}@{$server['host']}:{$server['port']}#{$name}\r\n"; + $addr = Helper::wrapIPv6($server['host']); + return "socks://{$credentials}@{$addr}:{$server['port']}#{$name}\r\n"; } public static function buildHttp($password, $server) diff --git a/app/Protocols/Loon.php b/app/Protocols/Loon.php index 9e13b44..27c12b4 100644 --- a/app/Protocols/Loon.php +++ b/app/Protocols/Loon.php @@ -19,6 +19,7 @@ class Loon extends AbstractProtocol protected $protocolRequirements = [ 'loon.hysteria.protocol_settings.version' => [2 => '637'], + 'loon.trojan.protocol_settings.tls' => [0 => '3.2.1', 1 => '3.2.1',2 => '999.9.9'], ]; public function handle() @@ -115,11 +116,10 @@ class Loon extends AbstractProtocol ]; if (data_get($protocol_settings, 'tls')) { - if (data_get($protocol_settings, 'network') === 'tcp') - $config[] = 'over-tls=true'; + $config[] = 'over-tls=true'; if (data_get($protocol_settings, 'tls_settings')) { $tls_settings = data_get($protocol_settings, 'tls_settings'); - $config[] = 'skip-cert-verify=' . ($tls_settings['allow_insecure'] ? 'true' : 'false'); + $config[] = 'skip-cert-verify=' . (data_get($tls_settings, 'allow_insecure') ? 'true' : 'false'); if (data_get($tls_settings, 'server_name')) $config[] = "tls-name={$tls_settings['server_name']}"; } @@ -150,8 +150,25 @@ class Loon extends AbstractProtocol if (data_get($wsSettings, key: 'headers.Host')) $config[] = "host={$wsSettings['headers']['Host']}"; break; - - + case 'grpc': + $config[] = 'transport=grpc'; + if ($serviceName = data_get($protocol_settings, 'network_settings.serviceName')) + $config[] = "grpc-service-name={$serviceName}"; + break; + case 'h2': + $config[] = 'transport=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) ? $host[0] : $host); + break; + case 'httpupgrade': + $config[] = 'transport=httpupgrade'; + if ($path = data_get($protocol_settings, 'network_settings.path')) + $config[] = "path={$path}"; + if ($host = data_get($protocol_settings, 'network_settings.headers.Host')) + $config[] = "host={$host}"; + break; } $uri = implode(',', $config); @@ -167,13 +184,45 @@ class Loon extends AbstractProtocol "{$server['host']}", "{$server['port']}", "{$password}", - data_get($protocol_settings, 'server_name') ? "tls-name={$protocol_settings['server_name']}" : "", - 'fast-open=false', - 'udp=true' ]; - if (!empty($protocol_settings['allow_insecure'])) { - $config[] = data_get($protocol_settings, 'allow_insecure') ? 'skip-cert-verify=true' : 'skip-cert-verify=false'; + + $tlsMode = (int) data_get($protocol_settings, 'tls', 1); + switch ($tlsMode) { + case 2: // Reality + if ($serverName = data_get($protocol_settings, 'reality_settings.server_name')) { + $config[] = "tls-name={$serverName}"; + } + if ($pubkey = data_get($protocol_settings, 'reality_settings.public_key')) { + $config[] = "public-key={$pubkey}"; + } + if ($shortid = data_get($protocol_settings, 'reality_settings.short_id')) { + $config[] = "short-id={$shortid}"; + } + $config[] = 'skip-cert-verify=' . (data_get($protocol_settings, 'reality_settings.allow_insecure', false) ? 'true' : 'false'); + break; + default: // Standard TLS + if ($serverName = data_get($protocol_settings, 'server_name')) { + $config[] = "tls-name={$serverName}"; + } + $config[] = 'skip-cert-verify=' . (data_get($protocol_settings, 'allow_insecure') ? 'true' : 'false'); + break; } + + switch (data_get($protocol_settings, 'network', 'tcp')) { + case 'ws': + $config[] = 'transport=ws'; + if ($path = data_get($protocol_settings, 'network_settings.path')) + $config[] = "path={$path}"; + if ($host = data_get($protocol_settings, 'network_settings.headers.Host')) + $config[] = "host={$host}"; + break; + case 'grpc': + $config[] = 'transport=grpc'; + if ($serviceName = data_get($protocol_settings, 'network_settings.serviceName')) + $config[] = "grpc-service-name={$serviceName}"; + break; + } + $config = array_filter($config); $uri = implode(',', $config); $uri .= "\r\n"; @@ -267,7 +316,9 @@ class Loon extends AbstractProtocol ]; if (data_get($protocol_settings, 'tls.allow_insecure')) $config[] = "skip-cert-verify=true"; - $config[] = "download-bandwidth=" . data_get($protocol_settings, 'bandwidth.download_bandwidth'); + if ($down = data_get($protocol_settings, 'bandwidth.down')) { + $config[] = "download-bandwidth={$down}"; + } $config[] = "udp=true"; $config = array_filter($config); $uri = implode(',', $config); diff --git a/app/Protocols/QuantumultX.php b/app/Protocols/QuantumultX.php index 1e27168..475cb40 100644 --- a/app/Protocols/QuantumultX.php +++ b/app/Protocols/QuantumultX.php @@ -2,6 +2,7 @@ namespace App\Protocols; +use App\Utils\Helper; use App\Support\AbstractProtocol; use App\Models\Server; @@ -11,7 +12,10 @@ class QuantumultX extends AbstractProtocol public $allowedProtocols = [ Server::TYPE_SHADOWSOCKS, Server::TYPE_VMESS, + Server::TYPE_VLESS, Server::TYPE_TROJAN, + Server::TYPE_SOCKS, + Server::TYPE_HTTP, ]; public function handle() @@ -20,15 +24,15 @@ class QuantumultX extends AbstractProtocol $user = $this->user; $uri = ''; foreach ($servers as $item) { - if ($item['type'] === Server::TYPE_SHADOWSOCKS) { - $uri .= self::buildShadowsocks($item['password'], $item); - } - if ($item['type'] === Server::TYPE_VMESS) { - $uri .= self::buildVmess($item['password'], $item); - } - if ($item['type'] === Server::TYPE_TROJAN) { - $uri .= self::buildTrojan($item['password'], $item); - } + $uri .= match ($item['type']) { + Server::TYPE_SHADOWSOCKS => self::buildShadowsocks($item['password'], $item), + Server::TYPE_VMESS => self::buildVmess($item['password'], $item), + Server::TYPE_VLESS => self::buildVless($item['password'], $item), + Server::TYPE_TROJAN => self::buildTrojan($item['password'], $item), + Server::TYPE_SOCKS => self::buildSocks5($item['password'], $item), + Server::TYPE_HTTP => self::buildHttp($item['password'], $item), + default => '' + }; } return response(base64_encode($uri)) ->header('content-type', 'text/plain') @@ -39,18 +43,16 @@ class QuantumultX extends AbstractProtocol { $protocol_settings = $server['protocol_settings']; $password = data_get($server, 'password', $password); + $addr = Helper::wrapIPv6($server['host']); $config = [ - "shadowsocks={$server['host']}:{$server['port']}", + "shadowsocks={$addr}:{$server['port']}", "method={$protocol_settings['cipher']}", "password={$password}", - 'fast-open=true', - 'udp-relay=true', - "tag={$server['name']}" ]; + if (data_get($protocol_settings, 'plugin') && data_get($protocol_settings, 'plugin_opts')) { $plugin = data_get($protocol_settings, 'plugin'); $pluginOpts = data_get($protocol_settings, 'plugin_opts', ''); - // 解析插件选项 $parsedOpts = collect(explode(';', $pluginOpts)) ->filter() ->mapWithKeys(function ($pair) { @@ -61,83 +63,170 @@ class QuantumultX extends AbstractProtocol return [trim($key) => trim($value)]; }) ->all(); - switch ($plugin) { - case 'obfs': + if ($plugin === 'obfs') { + if (isset($parsedOpts['obfs'])) { $config[] = "obfs={$parsedOpts['obfs']}"; - if (isset($parsedOpts['obfs-host'])) { - $config[] = "obfs-host={$parsedOpts['obfs-host']}"; - } - if (isset($parsedOpts['path'])) { - $config[] = "obfs-uri={$parsedOpts['path']}"; - } - break; + } + if (isset($parsedOpts['obfs-host'])) { + $config[] = "obfs-host={$parsedOpts['obfs-host']}"; + } + if (isset($parsedOpts['path'])) { + $config[] = "obfs-uri={$parsedOpts['path']}"; + } } } - $uri = implode(',', $config); - $uri .= "\r\n"; - return $uri; + + self::applyCommonSettings($config, $server); + + return implode(',', array_filter($config)) . "\r\n"; } public static function buildVmess($uuid, $server) { $protocol_settings = $server['protocol_settings']; + $addr = Helper::wrapIPv6($server['host']); $config = [ - "vmess={$server['host']}:{$server['port']}", - 'method=chacha20-poly1305', + "vmess={$addr}:{$server['port']}", + "method=" . data_get($protocol_settings, 'cipher', 'auto'), "password={$uuid}", - 'fast-open=true', - 'udp-relay=true', - "tag={$server['name']}" ]; - if (data_get($protocol_settings, 'tls')) { - if (data_get($protocol_settings, 'network') === 'tcp') - array_push($config, 'obfs=over-tls'); - if (data_get($protocol_settings, 'tls_settings')) { - if (data_get($protocol_settings, 'tls_settings.allow_insecure')) - array_push($config, 'tls-verification=' . ($protocol_settings['tls_settings']['allow_insecure'] ? 'false' : 'true')); - if (data_get($protocol_settings, 'tls_settings.server_name')) - $host = data_get($protocol_settings, 'tls_settings.server_name'); - } - } - if (data_get($protocol_settings, 'network') === 'ws') { - if (data_get($protocol_settings, 'tls')) - array_push($config, 'obfs=wss'); - else - array_push($config, 'obfs=ws'); - if (data_get($protocol_settings, 'network_settings')) { - if (data_get($protocol_settings, 'network_settings.path')) - array_push($config, "obfs-uri={$protocol_settings['network_settings']['path']}"); - if (data_get($protocol_settings, 'network_settings.headers.Host') && !isset($host)) - $host = data_get($protocol_settings, 'network_settings.headers.Host'); - } - } - if (isset($host)) { - array_push($config, "obfs-host={$host}"); + self::applyTransportSettings($config, $protocol_settings); + self::applyCommonSettings($config, $server); + + return implode(',', array_filter($config)) . "\r\n"; + } + + public static function buildVless($uuid, $server) + { + $protocol_settings = $server['protocol_settings']; + $addr = Helper::wrapIPv6($server['host']); + $config = [ + "vless={$addr}:{$server['port']}", + 'method=none', + "password={$uuid}", + ]; + + self::applyTransportSettings($config, $protocol_settings); + + if ($flow = data_get($protocol_settings, 'flow')) { + $config[] = "vless-flow={$flow}"; } - $uri = implode(',', $config); - $uri .= "\r\n"; - return $uri; + self::applyCommonSettings($config, $server); + + return implode(',', array_filter($config)) . "\r\n"; + } + + private static function applyTransportSettings(&$config, $settings, bool $nativeTls = false, ?array $tlsData = null) + { + $tlsMode = (int) data_get($settings, 'tls', 0); + $network = data_get($settings, 'network', 'tcp'); + $host = null; + $isWs = $network === 'ws'; + + switch ($network) { + case 'ws': + $config[] = $tlsMode ? 'obfs=wss' : 'obfs=ws'; + if ($path = data_get($settings, 'network_settings.path')) { + $config[] = "obfs-uri={$path}"; + } + $host = data_get($settings, 'network_settings.headers.Host'); + break; + case 'tcp': + $headerType = data_get($settings, 'network_settings.header.type', 'tcp'); + if ($headerType === 'http') { + $config[] = 'obfs=http'; + $paths = data_get($settings, 'network_settings.header.request.path', ['/']); + $config[] = 'obfs-uri=' . (is_array($paths) ? ($paths[0] ?? '/') : $paths); + $hostVal = data_get($settings, 'network_settings.header.request.headers.Host'); + $host = is_array($hostVal) ? ($hostVal[0] ?? null) : $hostVal; + } elseif ($tlsMode) { + $config[] = $nativeTls ? 'over-tls=true' : 'obfs=over-tls'; + } + break; + } + + switch ($tlsMode) { + case 2: // Reality + $host = $host ?? data_get($settings, 'reality_settings.server_name'); + if ($pubKey = data_get($settings, 'reality_settings.public_key')) { + $config[] = "reality-base64-pubkey={$pubKey}"; + } + if ($shortId = data_get($settings, 'reality_settings.short_id')) { + $config[] = "reality-hex-shortid={$shortId}"; + } + break; + case 1: // TLS + $resolved = $tlsData ?? (array) data_get($settings, 'tls_settings', []); + $allowInsecure = (bool) ($resolved['allow_insecure'] ?? false); + $config[] = 'tls-verification=' . ($allowInsecure ? 'false' : 'true'); + $host = $host ?? ($resolved['server_name'] ?? null); + break; + } + + if ($host) { + $config[] = ($nativeTls && !$isWs) ? "tls-host={$host}" : "obfs-host={$host}"; + } + } + + private static function applyCommonSettings(&$config, $server) + { + $config[] = 'fast-open=true'; + if ($server['type'] !== Server::TYPE_HTTP) { + $config[] = 'udp-relay=true'; + } + $config[] = "tag={$server['name']}"; } public static function buildTrojan($password, $server) { $protocol_settings = $server['protocol_settings']; + $addr = Helper::wrapIPv6($server['host']); $config = [ - "trojan={$server['host']}:{$server['port']}", + "trojan={$addr}:{$server['port']}", "password={$password}", - 'over-tls=true', - $protocol_settings['server_name'] ? "tls-host={$protocol_settings['server_name']}" : "", - // Tips: allowInsecure=false = tls-verification=true - $protocol_settings['allow_insecure'] ? 'tls-verification=false' : 'tls-verification=true', - 'fast-open=true', - 'udp-relay=true', - "tag={$server['name']}" ]; - $config = array_filter($config); - $uri = implode(',', $config); - $uri .= "\r\n"; - return $uri; + + $tlsData = [ + 'allow_insecure' => data_get($protocol_settings, 'allow_insecure', false), + 'server_name' => data_get($protocol_settings, 'server_name'), + ]; + self::applyTransportSettings($config, $protocol_settings, true, $tlsData); + self::applyCommonSettings($config, $server); + + return implode(',', array_filter($config)) . "\r\n"; + } + + public static function buildSocks5($password, $server) + { + $protocol_settings = $server['protocol_settings']; + $addr = Helper::wrapIPv6($server['host']); + $config = [ + "socks5={$addr}:{$server['port']}", + "username={$password}", + "password={$password}", + ]; + + self::applyTransportSettings($config, $protocol_settings, true); + self::applyCommonSettings($config, $server); + + return implode(',', array_filter($config)) . "\r\n"; + } + + public static function buildHttp($password, $server) + { + $protocol_settings = $server['protocol_settings']; + $addr = Helper::wrapIPv6($server['host']); + $config = [ + "http={$addr}:{$server['port']}", + "username={$password}", + "password={$password}", + ]; + + self::applyTransportSettings($config, $protocol_settings, true); + self::applyCommonSettings($config, $server); + + return implode(',', array_filter($config)) . "\r\n"; } } diff --git a/app/Protocols/Shadowrocket.php b/app/Protocols/Shadowrocket.php index 1886aed..950cf7e 100644 --- a/app/Protocols/Shadowrocket.php +++ b/app/Protocols/Shadowrocket.php @@ -35,7 +35,7 @@ class Shadowrocket extends AbstractProtocol $upload = round($user['u'] / (1024 * 1024 * 1024), 2); $download = round($user['d'] / (1024 * 1024 * 1024), 2); $totalTraffic = round($user['transfer_enable'] / (1024 * 1024 * 1024), 2); - $expiredDate = date('Y-m-d', $user['expired_at']); + $expiredDate = $user['expired_at'] === null ? 'N/A' : date('Y-m-d', $user['expired_at']); $uri .= "STATUS=🚀↑:{$upload}GB,↓:{$download}GB,TOT:{$totalTraffic}GB💡Expires:{$expiredDate}\r\n"; foreach ($servers as $item) { if ($item['type'] === Server::TYPE_SHADOWSOCKS) { @@ -128,6 +128,25 @@ class Shadowrocket extends AbstractProtocol $config['path'] = data_get($protocol_settings, 'network_settings.serviceName'); $config['host'] = data_get($protocol_settings, 'tls_settings.server_name') ?? $server['host']; break; + case 'httpupgrade': + $config['obfs'] = "httpupgrade"; + if ($path = data_get($protocol_settings, 'network_settings.path')) { + $config['path'] = $path; + } + if ($host = data_get($protocol_settings, 'network_settings.host', $server['host'])) { + $config['obfsParam'] = $host; + } + break; + case 'h2': + $config['obfs'] = "h2"; + if ($path = data_get($protocol_settings, 'network_settings.path')) { + $config['path'] = $path; + } + if ($host = data_get($protocol_settings, 'network_settings.host')) { + $config['obfsParam'] = $host[0] ?? $server['host']; + $config['peer'] = $host [0] ?? $server['host']; + } + break; } $query = http_build_query($config, '', '&', PHP_QUERY_RFC3986); $uri = "vmess://{$userinfo}?{$query}"; @@ -157,7 +176,6 @@ class Shadowrocket extends AbstractProtocol $config['xtls'] = $xtlsMap[data_get($protocol_settings, 'flow')]; } } - switch (data_get($protocol_settings, 'tls')) { case 1: $config['tls'] = 1; @@ -211,6 +229,15 @@ class Shadowrocket extends AbstractProtocol } $config['type'] = data_get($protocol_settings, 'network_settings.header.type', 'none'); break; + case 'h2': + $config['obfs'] = "h2"; + if ($path = data_get($protocol_settings, 'network_settings.path')) { + $config['path'] = $path; + } + if ($host = data_get($protocol_settings, 'network_settings.host', $server['host'])) { + $config['obfsParam'] = $host; + } + break; case 'httpupgrade': $config['obfs'] = "httpupgrade"; if ($path = data_get($protocol_settings, 'network_settings.path')) { @@ -244,10 +271,24 @@ class Shadowrocket extends AbstractProtocol { $protocol_settings = $server['protocol_settings']; $name = rawurlencode($server['name']); - $params['allowInsecure'] = data_get($protocol_settings, 'allow_insecure'); - if ($serverName = data_get($protocol_settings, 'server_name')) { - $params['peer'] = $serverName; + $params = []; + $tlsMode = (int) data_get($protocol_settings, 'tls', 1); + + switch ($tlsMode) { + case 2: // Reality + $params['security'] = 'reality'; + $params['pbk'] = data_get($protocol_settings, 'reality_settings.public_key'); + $params['sid'] = data_get($protocol_settings, 'reality_settings.short_id'); + $params['sni'] = data_get($protocol_settings, 'reality_settings.server_name'); + break; + default: // Standard TLS + $params['allowInsecure'] = data_get($protocol_settings, 'allow_insecure'); + if ($serverName = data_get($protocol_settings, 'server_name')) { + $params['peer'] = $serverName; + } + break; } + switch (data_get($protocol_settings, 'network')) { case 'grpc': $params['obfs'] = 'grpc'; @@ -286,7 +327,7 @@ class Shadowrocket extends AbstractProtocol } if (data_get($protocol_settings, 'obfs.open')) { $params["obfs"] = "xplus"; - $params["obfsParam"] = data_get($protocol_settings, 'obfs_settings.password'); + $params["obfsParam"] = data_get($protocol_settings, 'obfs.password'); } $params['insecure'] = data_get($protocol_settings, 'tls.allow_insecure'); if (isset($server['ports'])) @@ -341,7 +382,8 @@ class Shadowrocket extends AbstractProtocol $params['password'] = $password; } $query = http_build_query($params); - $uri = "tuic://{$server['host']}:{$server['port']}?{$query}#{$name}"; + $addr = Helper::wrapIPv6($server['host']); + $uri = "tuic://{$addr}:{$server['port']}?{$query}#{$name}"; $uri .= "\r\n"; return $uri; } @@ -355,7 +397,8 @@ class Shadowrocket extends AbstractProtocol 'insecure' => data_get($protocol_settings, 'tls.allow_insecure') ]; $query = http_build_query($params); - $uri = "anytls://{$password}@{$server['host']}:{$server['port']}?{$query}#{$name}"; + $addr = Helper::wrapIPv6($server['host']); + $uri = "anytls://{$password}@{$addr}:{$server['port']}?{$query}#{$name}"; $uri .= "\r\n"; return $uri; } @@ -364,7 +407,8 @@ class Shadowrocket extends AbstractProtocol { $protocol_settings = $server['protocol_settings']; $name = rawurlencode($server['name']); - $uri = "socks://" . base64_encode("{$password}:{$password}@{$server['host']}:{$server['port']}") . "?method=auto#{$name}"; + $addr = Helper::wrapIPv6($server['host']); + $uri = 'socks://' . base64_encode("{$password}:{$password}@{$addr}:{$server['port']}") . "?method=auto#{$name}"; $uri .= "\r\n"; return $uri; } diff --git a/app/Protocols/SingBox.php b/app/Protocols/SingBox.php index 68dd511..89578c8 100644 --- a/app/Protocols/SingBox.php +++ b/app/Protocols/SingBox.php @@ -5,6 +5,7 @@ use App\Utils\Helper; use Illuminate\Support\Arr; use App\Support\AbstractProtocol; use App\Models\Server; +use Log; class SingBox extends AbstractProtocol { @@ -59,9 +60,6 @@ class SingBox extends AbstractProtocol 'anytls' => [ 'base_version' => '1.12.0' ], - 'mieru' => [ - 'base_version' => '1.12.0' - ] ] ]; @@ -133,10 +131,6 @@ 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'])) { @@ -155,23 +149,12 @@ class SingBox extends AbstractProtocol protected function buildRule() { $rules = $this->config['route']['rules']; - // Force the nodes ip to be a direct rule - // array_unshift($rules, [ - // 'ip_cidr' => collect($this->servers)->pluck('host')->map(function ($host) { - // return filter_var($host, FILTER_VALIDATE_IP) ? [$host] : Helper::getIpByDomainName($host); - // })->flatten()->unique()->values(), - // 'outbound' => 'direct', - // ]); $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 + * 模板基准格式: 1.13.0+ (最新) */ protected function adaptConfigForVersion(): void { @@ -180,57 +163,190 @@ class SingBox extends AbstractProtocol return; } - // >= 1.11.0: 移除已废弃字段,避免 "配置已过时" 警告 - if (version_compare($coreVersion, '1.11.0', '>=')) { - $this->removeDeprecatedFieldsV111(); + // >= 1.13.0: 移除已删除的 block/dns 出站 + if (version_compare($coreVersion, '1.13.0', '>=')) { + $this->upgradeSpecialOutboundsToActions(); } - // < 1.10.0: address 数组 → inet4_address/inet6_address + // < 1.11.0: rule action 降级为旧出站; 恢复废弃字段 + if (version_compare($coreVersion, '1.11.0', '<')) { + $this->downgradeActionsToSpecialOutbounds(); + $this->restoreDeprecatedInboundFields(); + } + + // < 1.12.0: DNS type+server → 旧 address 格式 + if (version_compare($coreVersion, '1.12.0', '<')) { + $this->convertDnsServersToLegacy(); + } + + // < 1.10.0: tun address 数组 → inet4_address/inet6_address if (version_compare($coreVersion, '1.10.0', '<')) { - $this->convertAddressToLegacy(); + $this->convertTunAddressToLegacy(); } } /** - * 获取实际 sing-box 核心版本 - * - * sing-box 客户端直接报核心版本,hiddify/sfm 等 wrapper 客户端 - * 报的是 app 版本,需要映射到对应的 sing-box 核心版本 + * 获取核心版本 (Hiddify/SFM 等映射到内核版本) */ private function getSingBoxCoreVersion(): ?string { + // 优先从 UA 提取核心版本 + if (!empty($this->userAgent)) { + if (preg_match('/sing-box\s+v?(\d+(?:\.\d+){0,2})/i', $this->userAgent, $matches)) { + return $matches[1]; + } + } + 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'; + return '1.13.0'; } /** - * sing-box >= 1.11.0: 移除废弃字段 + * sing-box >= 1.13.0: block/dns 出站升级为 action */ - private function removeDeprecatedFieldsV111(): void + private function upgradeSpecialOutboundsToActions(): void + { + $removedTags = []; + $this->config['outbounds'] = array_values(array_filter( + $this->config['outbounds'] ?? [], + function ($outbound) use (&$removedTags) { + if (in_array($outbound['type'] ?? '', ['block', 'dns'])) { + $removedTags[$outbound['tag']] = $outbound['type']; + return false; + } + return true; + } + )); + + if (empty($removedTags)) { + return; + } + + if (isset($this->config['route']['rules'])) { + foreach ($this->config['route']['rules'] as &$rule) { + if (!isset($rule['outbound']) || !isset($removedTags[$rule['outbound']])) { + continue; + } + $type = $removedTags[$rule['outbound']]; + unset($rule['outbound']); + $rule['action'] = $type === 'dns' ? 'hijack-dns' : 'reject'; + } + unset($rule); + } + } + + /** + * sing-box < 1.11.0: rule action 降级为旧 block/dns 出站 + */ + private function downgradeActionsToSpecialOutbounds(): void + { + $needsDnsOutbound = false; + $needsBlockOutbound = false; + + if (isset($this->config['route']['rules'])) { + foreach ($this->config['route']['rules'] as &$rule) { + if (!isset($rule['action'])) { + continue; + } + switch ($rule['action']) { + case 'hijack-dns': + unset($rule['action']); + $rule['outbound'] = 'dns-out'; + $needsDnsOutbound = true; + break; + case 'reject': + unset($rule['action']); + $rule['outbound'] = 'block'; + $needsBlockOutbound = true; + break; + } + } + unset($rule); + } + + if ($needsBlockOutbound) { + $this->config['outbounds'][] = ['type' => 'block', 'tag' => 'block']; + } + if ($needsDnsOutbound) { + $this->config['outbounds'][] = ['type' => 'dns', 'tag' => 'dns-out']; + } + } + + /** + * sing-box < 1.11.0: 恢复废弃的入站字段 + */ + private function restoreDeprecatedInboundFields(): void { if (!isset($this->config['inbounds'])) { return; } foreach ($this->config['inbounds'] as &$inbound) { - unset($inbound['endpoint_independent_nat']); - unset($inbound['sniff_override_destination']); + if ($inbound['type'] === 'tun') { + $inbound['endpoint_independent_nat'] = true; + } + if (!empty($inbound['sniff'])) { + $inbound['sniff_override_destination'] = true; + } } } + /** + * sing-box < 1.12.0: 将新 DNS server type+server 格式转换为旧 address 格式 + */ + private function convertDnsServersToLegacy(): void + { + if (!isset($this->config['dns']['servers'])) { + return; + } + foreach ($this->config['dns']['servers'] as &$server) { + if (!isset($server['type'])) { + continue; + } + $type = $server['type']; + $host = $server['server'] ?? null; + switch ($type) { + case 'https': + $server['address'] = "https://{$host}/dns-query"; + break; + case 'tls': + $server['address'] = "tls://{$host}"; + break; + case 'tcp': + $server['address'] = "tcp://{$host}"; + break; + case 'quic': + $server['address'] = "quic://{$host}"; + break; + case 'udp': + $server['address'] = $host; + break; + case 'block': + $server['address'] = 'rcode://refused'; + break; + case 'rcode': + $server['address'] = 'rcode://' . ($server['rcode'] ?? 'success'); + unset($server['rcode']); + break; + default: + $server['address'] = $host; + break; + } + unset($server['type'], $server['server']); + } + unset($server); + } + /** * sing-box < 1.10.0: 将 tun address 数组转换为 inet4_address/inet6_address */ - private function convertAddressToLegacy(): void + private function convertTunAddressToLegacy(): void { if (!isset($this->config['inbounds'])) { return; @@ -297,42 +413,8 @@ class SingBox extends AbstractProtocol $this->appendMultiplex($array, $protocol_settings); - $transport = match ($protocol_settings['network']) { - 'tcp' => data_get($protocol_settings, 'network_settings.header.type', 'none') !== 'none' ? [ - 'type' => 'http', - 'path' => Arr::random(data_get($protocol_settings, 'network_settings.header.request.path', ['/'])), - 'host' => data_get($protocol_settings, 'network_settings.header.request.headers.Host', []) - ] : null, - 'ws' => array_filter([ - 'type' => 'ws', - 'path' => data_get($protocol_settings, 'network_settings.path'), - 'headers' => ($host = data_get($protocol_settings, 'network_settings.headers.Host')) ? ['Host' => $host] : null, - 'max_early_data' => 2048, - 'early_data_header_name' => 'Sec-WebSocket-Protocol' - ]), - 'grpc' => [ - '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 - }; - - if ($transport) { - $array['transport'] = array_filter($transport, fn($value) => !is_null($value)); + if ($transport = $this->buildTransport($protocol_settings, $server)) { + $array['transport'] = $transport; } return $array; } @@ -347,18 +429,23 @@ class SingBox extends AbstractProtocol "server_port" => $server['port'], "uuid" => $password, "packet_encoding" => "xudp", - 'flow' => data_get($protocol_settings, 'flow', ''), ]; + if ($flow = data_get($protocol_settings, 'flow')) { + $array['flow'] = $flow; + } if ($protocol_settings['tls']) { + $tlsMode = (int) $protocol_settings['tls']; $tlsConfig = [ 'enabled' => true, - 'insecure' => (bool) data_get($protocol_settings, 'tls_settings.allow_insecure'), + 'insecure' => $tlsMode === 2 + ? (bool) data_get($protocol_settings, 'reality_settings.allow_insecure', false) + : (bool) data_get($protocol_settings, 'tls_settings.allow_insecure', false), ]; $this->appendUtls($tlsConfig, $protocol_settings); - switch ($protocol_settings['tls']) { + switch ($tlsMode) { case 1: if ($serverName = data_get($protocol_settings, 'tls_settings.server_name')) { $tlsConfig['server_name'] = $serverName; @@ -379,41 +466,8 @@ class SingBox extends AbstractProtocol $this->appendMultiplex($array, $protocol_settings); - $transport = match ($protocol_settings['network']) { - 'tcp' => data_get($protocol_settings, 'network_settings.header.type') == 'http' ? [ - 'type' => 'http', - 'path' => Arr::random(data_get($protocol_settings, 'network_settings.header.request.path', ['/'])) - ] : null, - 'ws' => array_filter([ - 'type' => 'ws', - 'path' => data_get($protocol_settings, 'network_settings.path'), - 'headers' => ($host = data_get($protocol_settings, 'network_settings.headers.Host')) ? ['Host' => $host] : null, - 'max_early_data' => 2048, - 'early_data_header_name' => 'Sec-WebSocket-Protocol' - ], fn($value) => !is_null($value)), - 'grpc' => [ - '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 - }; - - if ($transport) { - $array['transport'] = array_filter($transport, fn($value) => !is_null($value)); + if ($transport = $this->buildTransport($protocol_settings, $server)) { + $array['transport'] = $transport; } return $array; @@ -428,36 +482,36 @@ class SingBox extends AbstractProtocol 'server' => $server['host'], 'server_port' => $server['port'], 'password' => $password, - 'tls' => [ - 'enabled' => true, - 'insecure' => (bool) data_get($protocol_settings, 'allow_insecure', false), - ] ]; - $this->appendUtls($array['tls'], $protocol_settings); + $tlsMode = (int) data_get($protocol_settings, 'tls', 1); + $tlsConfig = ['enabled' => true]; - if ($serverName = data_get($protocol_settings, 'server_name')) { - $array['tls']['server_name'] = $serverName; + switch ($tlsMode) { + case 2: // Reality + $tlsConfig['insecure'] = (bool) data_get($protocol_settings, 'reality_settings.allow_insecure', false); + $tlsConfig['server_name'] = data_get($protocol_settings, 'reality_settings.server_name'); + $tlsConfig['reality'] = [ + 'enabled' => true, + 'public_key' => data_get($protocol_settings, 'reality_settings.public_key'), + 'short_id' => data_get($protocol_settings, 'reality_settings.short_id'), + ]; + break; + default: // Standard TLS + $tlsConfig['insecure'] = (bool) data_get($protocol_settings, 'allow_insecure', false); + if ($serverName = data_get($protocol_settings, 'server_name')) { + $tlsConfig['server_name'] = $serverName; + } + break; } + $this->appendUtls($tlsConfig, $protocol_settings); + $array['tls'] = $tlsConfig; + $this->appendMultiplex($array, $protocol_settings); - $transport = match (data_get($protocol_settings, 'network')) { - 'grpc' => [ - 'type' => 'grpc', - 'service_name' => data_get($protocol_settings, 'network_settings.serviceName') - ], - 'ws' => array_filter([ - 'type' => 'ws', - 'path' => data_get($protocol_settings, 'network_settings.path'), - 'headers' => data_get($protocol_settings, 'network_settings.headers.Host') ? ['Host' => [data_get($protocol_settings, 'network_settings.headers.Host')]] : null, - 'max_early_data' => 2048, - 'early_data_header_name' => 'Sec-WebSocket-Protocol' - ]), - default => null - }; - if ($transport) { - $array['transport'] = array_filter($transport, fn($value) => !is_null($value)); + if ($transport = $this->buildTransport($protocol_settings, $server)) { + $array['transport'] = $transport; } return $array; } @@ -508,10 +562,9 @@ class SingBox extends AbstractProtocol ] }; - return array_merge( - $baseConfig, - $speedConfig, - $versionConfig + return array_filter( + array_merge($baseConfig, $speedConfig, $versionConfig), + fn($v) => !is_null($v) ); } @@ -625,6 +678,47 @@ class SingBox extends AbstractProtocol return $array; } + protected function buildTransport(array $protocol_settings, array $server): ?array + { + $transport = match (data_get($protocol_settings, 'network')) { + 'tcp' => data_get($protocol_settings, 'network_settings.header.type') === 'http' ? [ + 'type' => 'http', + 'path' => Arr::random(data_get($protocol_settings, 'network_settings.header.request.path', ['/'])), + 'host' => data_get($protocol_settings, 'network_settings.header.request.headers.Host', []) + ] : null, + 'ws' => [ + 'type' => 'ws', + 'path' => data_get($protocol_settings, 'network_settings.path'), + 'headers' => ($host = data_get($protocol_settings, 'network_settings.headers.Host')) ? ['Host' => $host] : null, + 'max_early_data' => 0, + // 'early_data_header_name' => 'Sec-WebSocket-Protocol' + ], + 'grpc' => [ + '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 + }; + + if (!$transport) { + return null; + } + + return array_filter($transport, fn($v) => !is_null($v)); + } + protected function appendMultiplex(&$array, $protocol_settings) { if ($multiplex = data_get($protocol_settings, 'multiplex')) { diff --git a/app/Protocols/Stash.php b/app/Protocols/Stash.php index b83d2e1..8a3eadc 100644 --- a/app/Protocols/Stash.php +++ b/app/Protocols/Stash.php @@ -23,6 +23,19 @@ class Stash extends AbstractProtocol Server::TYPE_HTTP, ]; protected $protocolRequirements = [ + // Global rules applied regardless of client version (features Stash never supports) + '*' => [ + 'trojan' => [ + 'protocol_settings.tls' => [ + '2' => '9999.0.0', // Trojan Reality not supported in Stash + ], + ], + 'vmess' => [ + 'protocol_settings.network' => [ + 'httpupgrade' => '9999.0.0', // httpupgrade not supported in Stash + ], + ], + ], 'stash' => [ 'anytls' => [ 'base_version' => '3.3.0' // AnyTLS 协议在3.3.0版本中添加 @@ -110,10 +123,10 @@ class Stash extends AbstractProtocol array_push($proxy, self::buildTuic($item['password'], $item)); array_push($proxies, $item['name']); } - // if ($item['type'] === 'anytls') { - // array_push($proxy, self::buildAnyTLS($item['password'], $item)); - // array_push($proxies, $item['name']); - // } + if ($item['type'] === Server::TYPE_ANYTLS) { + array_push($proxy, self::buildAnyTLS($item['password'], $item)); + array_push($proxies, $item['name']); + } if ($item['type'] === Server::TYPE_SOCKS) { array_push($proxy, self::buildSocks5($item['password'], $item)); array_push($proxies, $item['name']); @@ -237,8 +250,8 @@ class Stash extends AbstractProtocol $array['cipher'] = 'auto'; $array['udp'] = true; - $array['tls'] = data_get($protocol_settings, 'tls'); - $array['skip-cert-verify'] = data_get($protocol_settings, 'tls_settings.allow_insecure'); + $array['tls'] = (bool) data_get($protocol_settings, 'tls'); + $array['skip-cert-verify'] = (bool) data_get($protocol_settings, 'tls_settings.allow_insecure', false); if ($serverName = data_get($protocol_settings, 'tls_settings.server_name')) { $array['servername'] = $serverName; } @@ -266,6 +279,15 @@ class Stash extends AbstractProtocol $array['grpc-opts'] = []; $array['grpc-opts']['grpc-service-name'] = data_get($protocol_settings, 'network_settings.serviceName'); break; + case 'h2': + $array['network'] = 'h2'; + $array['tls'] = true; + $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; default: break; } @@ -297,6 +319,7 @@ class Stash extends AbstractProtocol break; case 2: $array['tls'] = true; + $array['skip-cert-verify'] = (bool) data_get($protocol_settings, 'reality_settings.allow_insecure', false); if ($serverName = data_get($protocol_settings, 'reality_settings.server_name')) { $array['servername'] = $serverName; $array['sni'] = $serverName; @@ -335,11 +358,14 @@ 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'] = []; + 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; } return $array; @@ -348,13 +374,36 @@ class Stash extends AbstractProtocol public static function buildTrojan($password, $server) { $protocol_settings = $server['protocol_settings']; - $array = []; - $array['name'] = $server['name']; - $array['type'] = 'trojan'; - $array['server'] = $server['host']; - $array['port'] = $server['port']; - $array['password'] = $password; - $array['udp'] = true; + $array = [ + 'name' => $server['name'], + 'type' => 'trojan', + 'server' => $server['host'], + 'port' => $server['port'], + 'password' => $password, + 'udp' => true, + ]; + + $tlsMode = (int) data_get($protocol_settings, 'tls', 1); + switch ($tlsMode) { + case 2: // Reality + $array['tls'] = true; + $array['skip-cert-verify'] = (bool) data_get($protocol_settings, 'reality_settings.allow_insecure', false); + if ($serverName = data_get($protocol_settings, 'reality_settings.server_name')) { + $array['sni'] = $serverName; + } + $array['reality-opts'] = [ + 'public-key' => data_get($protocol_settings, 'reality_settings.public_key'), + 'short-id' => data_get($protocol_settings, 'reality_settings.short_id'), + ]; + break; + default: // Standard TLS + if ($serverName = data_get($protocol_settings, 'server_name')) { + $array['sni'] = $serverName; + } + $array['skip-cert-verify'] = (bool) data_get($protocol_settings, 'allow_insecure', false); + break; + } + switch (data_get($protocol_settings, 'network')) { case 'tcp': $headerType = data_get($protocol_settings, 'network_settings.header.type', 'tcp'); @@ -370,11 +419,13 @@ class Stash extends AbstractProtocol $array['ws-opts']['headers'] = ['Host' => $host]; } break; + case 'grpc': + $array['network'] = 'grpc'; + if ($serviceName = data_get($protocol_settings, 'network_settings.serviceName')) + $array['grpc-opts']['grpc-service-name'] = $serviceName; + break; } - if ($serverName = data_get($protocol_settings, 'server_name')) { - $array['sni'] = $serverName; - } - $array['skip-cert-verify'] = data_get($protocol_settings, 'allow_insecure'); + return $array; } @@ -398,12 +449,18 @@ class Stash extends AbstractProtocol $array['type'] = 'hysteria'; $array['auth-str'] = $password; $array['protocol'] = 'udp'; - $array['obfs'] = data_get($protocol_settings, 'obfs.open') ? data_get($protocol_settings, 'obfs.type') : null; + if (data_get($protocol_settings, 'obfs.open')) { + $array['obfs'] = data_get($protocol_settings, 'obfs.password'); + } break; case 2: $array['type'] = 'hysteria2'; $array['auth'] = $password; $array['fast-open'] = true; + if (data_get($protocol_settings, 'obfs.open')) { + $array['obfs'] = data_get($protocol_settings, 'obfs.type', 'salamander'); + $array['obfs-password'] = data_get($protocol_settings, 'obfs.password'); + } break; } return $array; @@ -417,8 +474,6 @@ class Stash extends AbstractProtocol 'type' => 'tuic', 'server' => $server['host'], 'port' => $server['port'], - 'uuid' => $password, - 'password' => $password, 'congestion-controller' => data_get($protocol_settings, 'congestion_control', 'cubic'), 'udp-relay-mode' => data_get($protocol_settings, 'udp_relay_mode', 'native'), 'alpn' => data_get($protocol_settings, 'alpn', ['h3']), @@ -430,6 +485,13 @@ class Stash extends AbstractProtocol 'version' => data_get($protocol_settings, 'version', 5), ]; + if (data_get($protocol_settings, 'version') === 4) { + $array['token'] = $password; + } else { + $array['uuid'] = $password; + $array['password'] = $password; + } + $array['skip-cert-verify'] = (bool) data_get($protocol_settings, 'tls.allow_insecure', false); if ($serverName = data_get($protocol_settings, 'tls.server_name')) { $array['sni'] = $serverName; @@ -440,15 +502,15 @@ class Stash extends AbstractProtocol public static function buildAnyTLS($password, $server) { - $protocol_settings = $server['protocol_settings']; + $protocol_settings = data_get($server, 'protocol_settings', []); $array = [ 'name' => $server['name'], 'type' => 'anytls', 'server' => $server['host'], 'port' => $server['port'], 'password' => $password, - 'sni' => data_get($protocol_settings, 'tls_settings.server_name'), - 'skip-cert-verify' => (bool) data_get($protocol_settings, 'tls_settings.allow_insecure', false), + 'sni' => data_get($protocol_settings, 'tls.server_name'), + 'skip-cert-verify' => (bool) data_get($protocol_settings, 'tls.allow_insecure', false), 'udp' => true, ]; diff --git a/app/Protocols/Surfboard.php b/app/Protocols/Surfboard.php index 18914c8..7e14566 100644 --- a/app/Protocols/Surfboard.php +++ b/app/Protocols/Surfboard.php @@ -74,7 +74,7 @@ class Surfboard extends AbstractProtocol $totalTraffic = round($user['transfer_enable'] / (1024 * 1024 * 1024), 2); $unusedTraffic = $totalTraffic - $useTraffic; $expireDate = $user['expired_at'] === NULL ? '长期有效' : date('Y-m-d H:i:s', $user['expired_at']); - $subscribeInfo = "title={$appName}订阅信息, content=上传流量:{$upload}GB\\n下载流量:{$download}GB\\n剩余流量: { $unusedTraffic }GB\\n套餐流量:{$totalTraffic}GB\\n到期时间:{$expireDate}"; + $subscribeInfo = "title={$appName}订阅信息, content=上传流量:{$upload}GB\\n下载流量:{$download}GB\\n剩余流量:{$unusedTraffic}GB\\n套餐流量:{$totalTraffic}GB\\n到期时间:{$expireDate}"; $config = str_replace('$subscribe_info', $subscribeInfo, $config); return response($config, 200) @@ -146,10 +146,12 @@ class Surfboard extends AbstractProtocol array_push($config, 'tls=true'); if (data_get($protocol_settings, 'tls_settings')) { $tlsSettings = data_get($protocol_settings, 'tls_settings'); - if (!!data_get($tlsSettings, 'allowInsecure')) - array_push($config, 'skip-cert-verify=' . ($tlsSettings['allowInsecure'] ? 'true' : 'false')); - if (!!data_get($tlsSettings, 'serverName')) - array_push($config, "sni={$tlsSettings['serverName']}"); + if (data_get($tlsSettings, 'allow_insecure')) { + array_push($config, 'skip-cert-verify=' . ($tlsSettings['allow_insecure'] ? 'true' : 'false')); + } + if ($sni = data_get($tlsSettings, 'server_name')) { + array_push($config, "sni={$sni}"); + } } } if (data_get($protocol_settings, 'network') === 'ws') { @@ -176,7 +178,7 @@ class Surfboard extends AbstractProtocol "{$server['host']}", "{$server['port']}", "password={$password}", - $protocol_settings['server_name'] ? "sni={$protocol_settings['server_name']}" : "", + data_get($protocol_settings, 'server_name') ? "sni=" . data_get($protocol_settings, 'server_name') : "", 'tfo=true', 'udp-relay=true' ]; diff --git a/app/Protocols/Surge.php b/app/Protocols/Surge.php index 3d57d6f..ee1ea62 100644 --- a/app/Protocols/Surge.php +++ b/app/Protocols/Surge.php @@ -95,7 +95,7 @@ class Surge extends AbstractProtocol $totalTraffic = round($user['transfer_enable'] / (1024 * 1024 * 1024), 2); $unusedTraffic = $totalTraffic - $useTraffic; $expireDate = $user['expired_at'] === NULL ? '长期有效' : date('Y-m-d H:i:s', $user['expired_at']); - $subscribeInfo = "title={$appName}订阅信息, content=上传流量:{$upload}GB\\n下载流量:{$download}GB\\n剩余流量:{ $unusedTraffic }GB\\n套餐流量:{$totalTraffic}GB\\n到期时间:{$expireDate}"; + $subscribeInfo = "title={$appName}订阅信息, content=上传流量:{$upload}GB\\n下载流量:{$download}GB\\n剩余流量:{$unusedTraffic}GB\\n套餐流量:{$totalTraffic}GB\\n到期时间:{$expireDate}"; $config = str_replace('$subscribe_info', $subscribeInfo, $config); return response($config, 200) @@ -108,7 +108,7 @@ class Surge extends AbstractProtocol { $protocol_settings = $server['protocol_settings']; $config = [ - "{$server['name']}=ss", + "{$server['name']} = ss", "{$server['host']}", "{$server['port']}", "encrypt-method={$protocol_settings['cipher']}", @@ -152,7 +152,7 @@ class Surge extends AbstractProtocol { $protocol_settings = $server['protocol_settings']; $config = [ - "{$server['name']}=vmess", + "{$server['name']} = vmess", "{$server['host']}", "{$server['port']}", "username={$uuid}", @@ -191,11 +191,11 @@ class Surge extends AbstractProtocol { $protocol_settings = $server['protocol_settings']; $config = [ - "{$server['name']}=trojan", + "{$server['name']} = trojan", "{$server['host']}", "{$server['port']}", "password={$password}", - $protocol_settings['server_name'] ? "sni={$protocol_settings['server_name']}" : "", + data_get($protocol_settings, 'server_name') ? "sni=" . data_get($protocol_settings, 'server_name') : "", 'tfo=true', 'udp-relay=true' ]; @@ -213,7 +213,7 @@ class Surge extends AbstractProtocol { $protocol_settings = data_get($server, 'protocol_settings', []); $config = [ - "{$server['name']}=anytls", + "{$server['name']} = anytls", "{$server['host']}", "{$server['port']}", "password={$password}", @@ -237,7 +237,7 @@ class Surge extends AbstractProtocol if ($protocol_settings['version'] != 2) return ''; $config = [ - "{$server['name']}=hysteria2", + "{$server['name']} = hysteria2", "{$server['host']}", "{$server['port']}", "password={$password}", @@ -266,7 +266,7 @@ class Surge extends AbstractProtocol $protocol_settings = data_get($server, 'protocol_settings', []); $type = data_get($protocol_settings, 'tls') ? 'socks5-tls' : 'socks5'; $config = [ - "{$server['name']}={$type}", + "{$server['name']} = {$type}", "{$server['host']}", "{$server['port']}", "{$password}", @@ -295,7 +295,7 @@ class Surge extends AbstractProtocol $protocol_settings = data_get($server, 'protocol_settings', []); $type = data_get($protocol_settings, 'tls') ? 'https' : 'http'; $config = [ - "{$server['name']}={$type}", + "{$server['name']} = {$type}", "{$server['host']}", "{$server['port']}", "{$password}", diff --git a/app/Services/ServerService.php b/app/Services/ServerService.php index 28f168d..739571f 100644 --- a/app/Services/ServerService.php +++ b/app/Services/ServerService.php @@ -168,6 +168,11 @@ class ServerService 'host' => $host, 'server_name' => $protocolSettings['server_name'], 'multiplex' => data_get($protocolSettings, 'multiplex'), + 'tls' => (int) $protocolSettings['tls'], + 'tls_settings' => match ((int) $protocolSettings['tls']) { + 2 => $protocolSettings['reality_settings'], + default => null, + }, ], 'vless' => [ ...$baseConfig, @@ -256,7 +261,7 @@ class ServerService $response['custom_routes'] = $node['custom_routes']; } - if (!empty($node['cert_config'])) { + if (!empty($node['cert_config']) && data_get($node['cert_config'],'cert_mode') !== 'none' ) { $response['cert_config'] = $node['cert_config']; } diff --git a/app/Support/AbstractProtocol.php b/app/Support/AbstractProtocol.php index 03d6c40..78a2c0d 100644 --- a/app/Support/AbstractProtocol.php +++ b/app/Support/AbstractProtocol.php @@ -26,6 +26,11 @@ abstract class AbstractProtocol */ protected $clientVersion; + /** + * @var string|null 原始 User-Agent + */ + protected $userAgent; + /** * @var array 协议标识 */ @@ -48,13 +53,15 @@ abstract class AbstractProtocol * @param array $servers 服务器信息 * @param string|null $clientName 客户端名称 * @param string|null $clientVersion 客户端版本 + * @param string|null $userAgent 原始 User-Agent */ - public function __construct($user, $servers, $clientName = null, $clientVersion = null) + public function __construct($user, $servers, $clientName = null, $clientVersion = null, $userAgent = null) { $this->user = $user; $this->servers = $servers; $this->clientName = $clientName; $this->clientVersion = $clientVersion; + $this->userAgent = $userAgent; $this->protocolRequirements = $this->normalizeProtocolRequirements($this->protocolRequirements); $this->servers = HookManager::filter('protocol.servers.filtered', $this->filterServersByVersion()); }