mirror of
https://github.com/cedar2025/Xboard.git
synced 2025-01-22 10:38:14 -05:00
feat: Singbox、clash订阅增加绕过服务器地址规则
1、优化订阅下发代码 2、singbox、clash、meta订阅下发增加绕过服务器地址规则,防止重复代理
This commit is contained in:
parent
3f264c17ba
commit
163d09c71b
@ -8,136 +8,65 @@ use App\Services\ServerService;
|
||||
use App\Services\UserService;
|
||||
use App\Utils\Helper;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class ClientController extends Controller
|
||||
{
|
||||
|
||||
// 支持hy2 的客户端版本列表
|
||||
const SupportedHy2ClientVersions = [
|
||||
'NekoBox' => '1.2.7',
|
||||
'sing-box' => '1.5.0',
|
||||
'stash' => '2.5.0',
|
||||
'Shadowrocket' => '1993',
|
||||
'ClashMetaForAndroid' => '2.9.0',
|
||||
'Nekoray' => '3.24',
|
||||
'verge' => '1.3.8',
|
||||
'ClashX Meta' => '1.3.5',
|
||||
'Hiddify' => '0.1.0',
|
||||
'loon' => '637',
|
||||
'v2rayN' => '6.31',
|
||||
'surge' => '2398'
|
||||
];
|
||||
// allowed types
|
||||
const AllowedTypes = ['vmess', 'vless', 'trojan', 'hysteria', 'shadowsocks'];
|
||||
|
||||
public function subscribe(Request $request)
|
||||
{
|
||||
// 节点类型筛选
|
||||
$allowedTypes = ['vmess', 'vless', 'trojan', 'hysteria', 'hysteria2', 'shadowsocks'];
|
||||
$types = $request->input('types', "vmess|vless|trojan|hysteria|shadowsocks");
|
||||
if ($types === "all") $types = implode('|', $allowedTypes);
|
||||
$typesArr = $types ? collect(explode('|', str_replace(['|','|',','], "|" , $types)))->reject(function($type) use ($allowedTypes){
|
||||
return !in_array($type, $allowedTypes);
|
||||
})->values()->all() : [];
|
||||
|
||||
// 节点关键词筛选字段获取
|
||||
$filterArr = (mb_strlen($request->input('filter')) > 20) ? null : explode("|" ,str_replace(['|','|',','], "|" , $request->input('filter')));
|
||||
|
||||
$flag = $request->input('flag') ?? $request->header('User-Agent', '');
|
||||
$flag = strtolower($flag);
|
||||
$ip = $request->input('ip') ?? $request->ip();
|
||||
|
||||
preg_match('/\/v?(\d+(\.\d+){0,2})/', $flag, $matches);
|
||||
$version = $matches[1]??null;
|
||||
$supportedClientVersions = [
|
||||
'NekoBox' => '1.2.7',
|
||||
'sing-box' => '1.5.0',
|
||||
'stash' => '2.5.0',
|
||||
'Shadowrocket' => '1993',
|
||||
'ClashMetaForAndroid' => '2.9.0',
|
||||
'Nekoray' => '3.24',
|
||||
'verge' => '1.3.8',
|
||||
'ClashX Meta' => '1.3.5',
|
||||
'Hiddify' => '0.1.0',
|
||||
'loon' => '637',
|
||||
'v2rayN' => '6.31',
|
||||
'surge' => '2398'
|
||||
];
|
||||
$supportHy2 = true;
|
||||
if ($version) {
|
||||
$supportHy2 = collect($supportedClientVersions)
|
||||
->contains(function ($minVersion, $client) use ($flag, $version) {
|
||||
return stripos($flag, $client) !== false && $this->versionCompare($version, $minVersion);
|
||||
});
|
||||
}
|
||||
// filter types
|
||||
$types = $request->input('types', 'all');
|
||||
$typesArr = $types === 'all' ? self::AllowedTypes : array_values(array_intersect(explode('|', str_replace(['|', '|', ','], "|", $types)), self::AllowedTypes));
|
||||
// filter keyword
|
||||
$filterArr = mb_strlen($filter = $request->input('filter')) > 20 ? null : explode("|", str_replace(['|', '|', ','], "|", $filter));
|
||||
$flag = strtolower($request->input('flag') ?? $request->header('User-Agent', ''));
|
||||
$ip = $request->input('ip', $request->ip());
|
||||
// get client version
|
||||
$version = preg_match('/\/v?(\d+(\.\d+){0,2})/', $flag, $matches) ? $matches[1] : null;
|
||||
$supportHy2 = $version
|
||||
&& collect(self::SupportedHy2ClientVersions)
|
||||
->contains(fn($minVersion, $client) => stripos($flag, $client) !== false && $this->versionCompare($version, $minVersion));
|
||||
$user = $request->user;
|
||||
// account not expired and is not banned.
|
||||
$userService = new UserService();
|
||||
if ($userService->isAvailable($user)) {
|
||||
// 获取IP地址信息
|
||||
// get ip location
|
||||
$ip2region = new \Ip2Region();
|
||||
$geo = filter_var($ip,FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) ? $ip2region->memorySearch($ip) : [];
|
||||
$region = $geo['region'] ?? null;
|
||||
|
||||
// 获取服务器列表
|
||||
$region = filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) ? ($ip2region->memorySearch($ip)['region'] ?? null) : null;
|
||||
// get available servers
|
||||
$servers = ServerService::getAvailableServers($user);
|
||||
|
||||
// 判断不满足,不满足的直接过滤掉
|
||||
$serversFiltered = collect($servers)->reject(function ($server) use ($typesArr, $filterArr, $region, $supportHy2){
|
||||
// 过滤类型
|
||||
if($typesArr){
|
||||
// 默认过滤掉hysteria2 线路
|
||||
if($server['type'] == "hysteria" && $server['version'] == 2 && !in_array('hysteria2', $typesArr)
|
||||
&& !$supportHy2
|
||||
){
|
||||
return true;
|
||||
}
|
||||
if(!in_array($server['type'], $typesArr) && !($server['type'] == "hysteria" && $server['version'] == 2 && in_array('hysteria2', $typesArr))) return true;
|
||||
}
|
||||
// 过滤关键词
|
||||
if($filterArr){
|
||||
$rejectFlag = true;
|
||||
foreach($filterArr as $filter){
|
||||
if(stripos($server['name'],$filter) !== false
|
||||
|| in_array($filter, $server['tags'] ?? [])
|
||||
) $rejectFlag = false;
|
||||
}
|
||||
if($rejectFlag) return true;
|
||||
}
|
||||
// 过滤地区
|
||||
if(strpos($region, '中国') !== false){
|
||||
$excludes = $server['excludes'];
|
||||
if(blank($excludes)) return false;
|
||||
foreach($excludes as $v){
|
||||
$excludeList = explode("|",str_replace(["|",","," ",","],"|",$v));
|
||||
$rejectFlag = false;
|
||||
foreach($excludeList as $needle){
|
||||
if(stripos($region, $needle) !== false){
|
||||
return true;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
})->values()->all();
|
||||
// filter servers
|
||||
$serversFiltered = $this->serverFilter($servers, $typesArr, $filterArr, $region, $supportHy2);
|
||||
$this->setSubscribeInfoToServers($serversFiltered, $user, count($servers) - count($serversFiltered));
|
||||
|
||||
$servers = $serversFiltered;
|
||||
|
||||
// 线路名称增加协议类型
|
||||
if (admin_setting('show_protocol_to_server_enable')){
|
||||
$typePrefixes = [
|
||||
'hysteria' => [1 => '[Hy]', 2 => '[Hy2]'],
|
||||
'vless' => '[vless]',
|
||||
'shadowsocks' => '[ss]',
|
||||
'vmess' => '[vmess]',
|
||||
'trojan' => '[trojan]',
|
||||
];
|
||||
$servers = collect($servers)->map(function($server)use ($typePrefixes){
|
||||
if (isset($typePrefixes[$server['type']])) {
|
||||
// 如果是 hysteria 类型,根据版本选择前缀
|
||||
$prefix = is_array($typePrefixes[$server['type']]) ? $typePrefixes[$server['type']][$server['version']] : $typePrefixes[$server['type']];
|
||||
// 设置服务器名称
|
||||
$server['name'] = $prefix . $server['name'];
|
||||
}
|
||||
return $server;
|
||||
})->toArray();
|
||||
}
|
||||
$this->addPrefixToServerName($servers);
|
||||
if ($flag) {
|
||||
foreach (array_reverse(glob(app_path('Protocols') . '/*.php')) as $file) {
|
||||
$file = 'App\\Protocols\\' . basename($file, '.php');
|
||||
$class = new $file($user, $servers);
|
||||
$classFlags = explode(',', $class->flag);
|
||||
$isMatch = function() use ($classFlags, $flag){
|
||||
foreach ($classFlags as $classFlag){
|
||||
if(stripos($flag, $classFlag) !== false) return true;
|
||||
foreach ($classFlags as $classFlag) {
|
||||
if (stripos($flag, $classFlag) !== false) {
|
||||
return $class->handle();
|
||||
}
|
||||
return false;
|
||||
};
|
||||
// 判断是否匹配
|
||||
if ($isMatch()) {
|
||||
return $class->handle();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -145,16 +74,93 @@ class ClientController extends Controller
|
||||
return $class->handle();
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Summary of serverFilter
|
||||
* @param mixed $typesArr
|
||||
* @param mixed $filterArr
|
||||
* @param mixed $region
|
||||
* @param mixed $supportHy2
|
||||
* @return array
|
||||
*/
|
||||
private function serverFilter($servers, $typesArr, $filterArr, $region, $supportHy2)
|
||||
{
|
||||
return collect($servers)->reject(function ($server) use ($typesArr, $filterArr, $region, $supportHy2) {
|
||||
if (
|
||||
$typesArr && (
|
||||
($server['type'] == "hysteria" && $server['version'] == 2 && !in_array('hysteria2', $typesArr) && !$supportHy2) ||
|
||||
(!in_array($server['type'], $typesArr) && !($server['type'] == "hysteria" && $server['version'] == 2 && in_array('hysteria2', $typesArr)))
|
||||
)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($filterArr) {
|
||||
foreach ($filterArr as $filter) {
|
||||
if (stripos($server['name'], $filter) !== false || in_array($filter, $server['tags'] ?? [])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (strpos($region, '中国') !== false) {
|
||||
$excludes = $server['excludes'] ?? [];
|
||||
if (empty($excludes)) {
|
||||
return false;
|
||||
}
|
||||
foreach ($excludes as $v) {
|
||||
$excludeList = explode("|", str_replace(["|", ",", " ", ","], "|", $v));
|
||||
foreach ($excludeList as $needle) {
|
||||
if (stripos($region, $needle) !== false) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})->values()->all();
|
||||
}
|
||||
/*
|
||||
* add prefix to server name
|
||||
*/
|
||||
private function addPrefixToServerName(&$servers)
|
||||
{
|
||||
// 线路名称增加协议类型
|
||||
if (admin_setting('show_protocol_to_server_enable')) {
|
||||
$typePrefixes = [
|
||||
'hysteria' => [1 => '[Hy]', 2 => '[Hy2]'],
|
||||
'vless' => '[vless]',
|
||||
'shadowsocks' => '[ss]',
|
||||
'vmess' => '[vmess]',
|
||||
'trojan' => '[trojan]',
|
||||
];
|
||||
$servers = collect($servers)->map(function ($server) use ($typePrefixes) {
|
||||
if (isset($typePrefixes[$server['type']])) {
|
||||
$prefix = is_array($typePrefixes[$server['type']]) ? $typePrefixes[$server['type']][$server['version']] : $typePrefixes[$server['type']];
|
||||
$server['name'] = $prefix . $server['name'];
|
||||
}
|
||||
return $server;
|
||||
})->toArray();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Summary of setSubscribeInfoToServers
|
||||
* @param mixed $servers
|
||||
* @param mixed $user
|
||||
* @param mixed $rejectServerCount
|
||||
* @return void
|
||||
*/
|
||||
private function setSubscribeInfoToServers(&$servers, $user, $rejectServerCount = 0)
|
||||
{
|
||||
if (!isset($servers[0])) return;
|
||||
if($rejectServerCount > 0){
|
||||
if (!isset($servers[0]))
|
||||
return;
|
||||
if ($rejectServerCount > 0) {
|
||||
array_unshift($servers, array_merge($servers[0], [
|
||||
'name' => "去除{$rejectServerCount}条不合适线路",
|
||||
'name' => "过滤掉{$rejectServerCount}条线路",
|
||||
]));
|
||||
}
|
||||
if (!(int)admin_setting('show_info_to_server_enable', 0)) return;
|
||||
if (!(int) admin_setting('show_info_to_server_enable', 0))
|
||||
return;
|
||||
$useTraffic = $user['u'] + $user['d'];
|
||||
$totalTraffic = $user['transfer_enable'];
|
||||
$remainingTraffic = Helper::trafficConvert($totalTraffic - $useTraffic);
|
||||
@ -180,7 +186,8 @@ class ClientController extends Controller
|
||||
* 判断版本号
|
||||
*/
|
||||
|
||||
function versionCompare($version1, $version2) {
|
||||
function versionCompare($version1, $version2)
|
||||
{
|
||||
if (!preg_match('/^\d+(\.\d+){0,2}/', $version1) || !preg_match('/^\d+(\.\d+){0,2}/', $version2)) {
|
||||
return false;
|
||||
}
|
||||
@ -190,8 +197,8 @@ class ClientController extends Controller
|
||||
$maxParts = max(count($v1Parts), count($v2Parts));
|
||||
|
||||
for ($i = 0; $i < $maxParts; $i++) {
|
||||
$part1 = isset($v1Parts[$i]) ? (int)$v1Parts[$i] : 0;
|
||||
$part2 = isset($v2Parts[$i]) ? (int)$v2Parts[$i] : 0;
|
||||
$part1 = isset($v1Parts[$i]) ? (int) $v1Parts[$i] : 0;
|
||||
$part2 = isset($v2Parts[$i]) ? (int) $v2Parts[$i] : 0;
|
||||
|
||||
if ($part1 < $part2) {
|
||||
return false;
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Protocols;
|
||||
|
||||
use App\Utils\Helper;
|
||||
use phpDocumentor\Reflection\Types\Self_;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
|
||||
@ -32,11 +33,6 @@ class Clash
|
||||
$proxy = [];
|
||||
$proxies = [];
|
||||
|
||||
// 增加不支持提示
|
||||
// array_push($proxy, [ "name" => "您的客户端不支持", "type" => "vmess", "server" => "1.1.1.1", "port" => 80, "uuid" => "aaaaaaaa-bbbb-cccc-cccc-dddddddddddd", "alterId" => 0, "cipher" => "auto", "udp" => false, "tls" => false]);
|
||||
// array_push($proxies, "您的客户端不支持");
|
||||
// array_push($proxy, [ "name" => "请使用clash Meta内核的客户端", "type" => "vmess", "server" => "1.1.1.1", "port" => 80, "uuid" => "aaaaaaaa-bbbb-cccc-cccc-dddddddddddd", "alterId" => 0, "cipher" => "auto", "udp" => false, "tls" => false]);
|
||||
// array_push($proxies, "请使用clash Meta内核的客户端");
|
||||
foreach ($servers as $item) {
|
||||
|
||||
if ($item['type'] === 'shadowsocks'
|
||||
@ -83,11 +79,9 @@ class Clash
|
||||
return $group['proxies'];
|
||||
});
|
||||
$config['proxy-groups'] = array_values($config['proxy-groups']);
|
||||
// Force the current subscription domain to be a direct rule
|
||||
$subsDomain = request()->header('Host');
|
||||
if ($subsDomain) {
|
||||
array_unshift($config['rules'], "DOMAIN,{$subsDomain},DIRECT");
|
||||
}
|
||||
|
||||
$config = $this->buildRules($config);
|
||||
|
||||
|
||||
$yaml = Yaml::dump($config, 2, 4, Yaml::DUMP_EMPTY_ARRAY_AS_SEQUENCE);
|
||||
$yaml = str_replace('$app_name', admin_setting('app_name', 'XBoard'), $yaml);
|
||||
@ -98,6 +92,27 @@ class Clash
|
||||
->header('profile-web-page-url', admin_setting('app_url'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the rules for Clash.
|
||||
*/
|
||||
public function buildRules($config)
|
||||
{
|
||||
// Force the current subscription domain to be a direct rule
|
||||
$subsDomain = request()->header('Host');
|
||||
if ($subsDomain) {
|
||||
array_unshift($config['rules'], "DOMAIN,{$subsDomain},DIRECT");
|
||||
}
|
||||
// Force the nodes ip to be a direct rule
|
||||
collect($this->servers)->pluck('host')->map(function($host){
|
||||
$host = trim($host);
|
||||
return filter_var($host, FILTER_VALIDATE_IP) ? [$host] : Helper::getIpByDomainName($host);
|
||||
})->flatten()->unique()->each(function($nodeIP) use ( &$config ) {
|
||||
array_unshift($config['rules'], "IP-CIDR,{$nodeIP}/32,DIRECT,no-resolve");
|
||||
});
|
||||
|
||||
return $config;
|
||||
}
|
||||
|
||||
public static function buildShadowsocks($uuid, $server)
|
||||
{
|
||||
$array = [];
|
||||
|
@ -81,11 +81,7 @@ class ClashMeta
|
||||
return $group['proxies'];
|
||||
});
|
||||
$config['proxy-groups'] = array_values($config['proxy-groups']);
|
||||
// Force the current subscription domain to be a direct rule
|
||||
$subsDomain = request()->header('Host');
|
||||
if ($subsDomain) {
|
||||
array_unshift($config['rules'], "DOMAIN,{$subsDomain},DIRECT");
|
||||
}
|
||||
$config = $this->buildRules($config);
|
||||
|
||||
$yaml = Yaml::dump($config, 2, 4, Yaml::DUMP_EMPTY_ARRAY_AS_SEQUENCE);
|
||||
$yaml = str_replace('$app_name', admin_setting('app_name', 'XBoard'), $yaml);
|
||||
@ -95,6 +91,27 @@ class ClashMeta
|
||||
->header('content-disposition', 'attachment;filename*=UTF-8\'\'' . rawurlencode($appName));
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the rules for Clash.
|
||||
*/
|
||||
public function buildRules($config)
|
||||
{
|
||||
// Force the current subscription domain to be a direct rule
|
||||
$subsDomain = request()->header('Host');
|
||||
if ($subsDomain) {
|
||||
array_unshift($config['rules'], "DOMAIN,{$subsDomain},DIRECT");
|
||||
}
|
||||
// Force the nodes ip to be a direct rule
|
||||
collect($this->servers)->pluck('host')->map(function($host){
|
||||
$host = trim($host);
|
||||
return filter_var($host, FILTER_VALIDATE_IP) ? [$host] : Helper::getIpByDomainName($host);
|
||||
})->flatten()->unique()->each(function($nodeIP) use ( &$config ) {
|
||||
array_unshift($config['rules'], "IP-CIDR,{$nodeIP}/32,DIRECT,no-resolve");
|
||||
});
|
||||
|
||||
return $config;
|
||||
}
|
||||
|
||||
public static function buildShadowsocks($password, $server)
|
||||
{
|
||||
$array = [];
|
||||
|
@ -21,12 +21,13 @@ class SingBox
|
||||
$appName = admin_setting('app_name', 'XBoard');
|
||||
$this->config = $this->loadConfig();
|
||||
$this->buildOutbounds();
|
||||
$this->buildRule();
|
||||
$user = $this->user;
|
||||
|
||||
return response($this->config, 200)
|
||||
return response()
|
||||
->json($this->config)
|
||||
->header('subscription-userinfo', "upload={$user['u']}; download={$user['d']}; total={$user['transfer_enable']}; expire={$user['expired_at']}")
|
||||
->header('profile-update-interval', '24')
|
||||
->header('content-disposition', 'attachment;filename*=UTF-8\'\'' . rawurlencode($appName));
|
||||
->header('profile-update-interval', '24');
|
||||
}
|
||||
|
||||
protected function loadConfig()
|
||||
@ -75,6 +76,21 @@ class SingBox
|
||||
return $outbounds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build rule
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
protected function buildShadowsocks($password, $server)
|
||||
{
|
||||
$array = [];
|
||||
|
@ -129,25 +129,34 @@ class Helper
|
||||
}
|
||||
|
||||
/**
|
||||
* 替换字符串中的 [num1-num2] 格式为介于 num1 和 num2 之间的随机数字
|
||||
* 根据规则替换域名中对应的字符串
|
||||
*
|
||||
* @param string $input 用户输入的字符串
|
||||
* @return string 替换后的字符串
|
||||
*/
|
||||
public static function replaceRandomNumber($input) {
|
||||
// 匹配 [1-4999] 格式的正则表达式
|
||||
$pattern = '/\[(\d+)-(\d+)\]/';
|
||||
|
||||
// 使用 preg_replace_callback 替换匹配到的内容
|
||||
$result = preg_replace_callback($pattern, function ($matches) {
|
||||
// 提取最小和最大值
|
||||
$min = intval($matches[1]);
|
||||
$max = intval($matches[2]);
|
||||
// 生成随机数
|
||||
$randomNumber = rand($min, $max);
|
||||
return $randomNumber;
|
||||
}, $input);
|
||||
|
||||
return $result;
|
||||
public static function replaceByPattern($input)
|
||||
{
|
||||
$patterns = [
|
||||
'/\[(\d+)-(\d+)\]/' => function ($matches) {
|
||||
$min = intval($matches[1]);
|
||||
$max = intval($matches[2]);
|
||||
if ($min > $max) {
|
||||
list($min, $max) = [$max, $min];
|
||||
}
|
||||
$randomNumber = rand($min, $max);
|
||||
return $randomNumber;
|
||||
},
|
||||
'/\[uuid\]/' => function () {
|
||||
return self::guid(true);
|
||||
}
|
||||
];
|
||||
foreach ($patterns as $pattern => $callback) {
|
||||
$input = preg_replace_callback($pattern, $callback, $input);
|
||||
}
|
||||
return $input;
|
||||
}
|
||||
|
||||
public static function getIpByDomainName($domain) {
|
||||
return gethostbynamel($domain) ?: [];
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user