Skip to content

Commit 79f46b2

Browse files
committed
feat: 优化域名解析并发, 优化 sing-box Tailscale 转换, 支持 mihomo Tailscale
- Resolve Domain Operator 新增并发控制参数 `concurrency`(默认 15,超 20 会 warning),并将解析流程改为基于 worker 池执行,避免重复解析 - 新增域名解析结果缓存命中分流逻辑,命中缓存的域名不再参与并发解析,同时补强并发参数校验与空值兼容 - 调整 Domain Resolver 日志与 Settings 读取,兼容 `$.read(SETTINGS_KEY)` 可能为空的场景 - ClashMeta producer 调整 tailscale 兼容条件,不再把 tailscale 一律过滤掉 - sing-box producer 增加 `control-http-client` 支持映射到 `control_http_client`,新增 `state-dir` 兼容,且在存在 control client 时跳过 legacy 的 `detour`/`dialer-proxy`/`ip_version`/`domain_resolver` 映射以避免字段冲突 - 新增 `resolve-domain.spec.js` 覆盖并发上限、缓存命中、无效参数、告警场景,以及 sing-box 结构化测试补充 tailscale control_http_client 与 legacy 字段互斥校验 - 后端版本号升级到 `2.23.4`,并补充 demo 注释说明 `control_http_client` 的使用方式
1 parent e95a0f6 commit 79f46b2

7 files changed

Lines changed: 375 additions & 34 deletions

File tree

backend/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "sub-store",
3-
"version": "2.23.2",
3+
"version": "2.23.4",
44
"description": "Advanced Subscription Manager for QX, Loon, Surge, Stash and Shadowrocket.",
55
"main": "src/main.js",
66
"packageManager": "pnpm@11.0.9",

backend/src/core/proxy-utils/processors/index.js

Lines changed: 109 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -524,6 +524,67 @@ function parseIP4P(IP4P) {
524524
return { server, port };
525525
}
526526

527+
const DEFAULT_RESOLVE_DOMAIN_CONCURRENCY = 15;
528+
const RESOLVE_DOMAIN_CONCURRENCY_WARN_THRESHOLD = 20;
529+
530+
function normalizeResolveDomainConcurrency(concurrency) {
531+
if (
532+
typeof concurrency === 'undefined' ||
533+
concurrency === null ||
534+
(typeof concurrency === 'string' && concurrency.trim() === '')
535+
) {
536+
return DEFAULT_RESOLVE_DOMAIN_CONCURRENCY;
537+
}
538+
539+
const parsed = Number(concurrency);
540+
if (!Number.isInteger(parsed) || parsed < 1) {
541+
throw new Error('域名解析并发数应为大于 0 的整数');
542+
}
543+
544+
return parsed;
545+
}
546+
547+
async function resolveDomainsWithConcurrency(
548+
domains,
549+
concurrency,
550+
resolveDomain,
551+
) {
552+
let nextIndex = 0;
553+
const workerCount = Math.min(concurrency, domains.length);
554+
const workers = Array.from({ length: workerCount }, async () => {
555+
while (nextIndex < domains.length) {
556+
const domain = domains[nextIndex];
557+
nextIndex += 1;
558+
await resolveDomain(domain);
559+
}
560+
});
561+
562+
await Promise.all(workers);
563+
}
564+
565+
function getDomainResolverCacheId(provider, domain, type, url) {
566+
switch (provider) {
567+
case 'Custom':
568+
return hex_md5(`CUSTOM:${url}:${domain}:${type}`);
569+
case 'Google':
570+
return hex_md5(`GOOGLE:${domain}:${type}`);
571+
case 'IP-API':
572+
return hex_md5(`IP-API:${domain}`);
573+
case 'Cloudflare':
574+
return hex_md5(`CLOUDFLARE:${domain}:${type}`);
575+
case 'Ali':
576+
return hex_md5(`ALI:${domain}:${type}`);
577+
case 'Tencent':
578+
return hex_md5(`TENCENT:${domain}:${type}`);
579+
}
580+
}
581+
582+
function getCachedDomainResolverResult(provider, domain, type, cache, url) {
583+
if (cache === 'disabled') return null;
584+
const id = getDomainResolverCacheId(provider, domain, type, url);
585+
return id ? resourceCache.get(id) : null;
586+
}
587+
527588
const DOMAIN_RESOLVERS = {
528589
Custom: async function (domain, type, noCache, timeout, edns, url) {
529590
const id = hex_md5(`CUSTOM:${url}:${domain}:${type}`);
@@ -693,11 +754,12 @@ function ResolveDomainOperator({
693754
url,
694755
timeout,
695756
edns: _edns,
757+
concurrency: _concurrency,
696758
}) {
697759
if (['IPv6', 'IP4P'].includes(_type) && ['IP-API'].includes(provider)) {
698760
throw new Error(`域名解析服务提供方 ${provider} 不支持 ${_type}`);
699761
}
700-
const { defaultTimeout } = $.read(SETTINGS_KEY);
762+
const { defaultTimeout } = $.read(SETTINGS_KEY) || {};
701763
const requestTimeout = timeout || defaultTimeout || 8000;
702764
let type = ['IPv6', 'IP4P'].includes(_type) ? 'IPv6' : 'IPv4';
703765

@@ -707,8 +769,16 @@ function ResolveDomainOperator({
707769
}
708770
let edns = _edns || '223.6.6.6';
709771
if (!isIP(edns)) throw new Error(`域名解析 EDNS 应为 IP`);
772+
const concurrency = normalizeResolveDomainConcurrency(_concurrency);
773+
if (concurrency > RESOLVE_DOMAIN_CONCURRENCY_WARN_THRESHOLD) {
774+
$.warn(
775+
`域名解析并发数 ${concurrency} 超过建议值 ${RESOLVE_DOMAIN_CONCURRENCY_WARN_THRESHOLD}, 可能导致代理 App TCP 连接数激增`,
776+
);
777+
}
710778
$.info(
711-
`Domain Resolver: [${_type}] ${provider} ${edns || ''} ${url || ''}`,
779+
`Domain Resolver: [${_type}] ${provider} ${edns || ''} ${
780+
url || ''
781+
} concurrency=${concurrency}`,
712782
);
713783
return {
714784
name: 'Resolve Domain Operator',
@@ -719,42 +789,55 @@ function ResolveDomainOperator({
719789
}
720790
});
721791
const results = {};
722-
const limit = 15; // more than 20 concurrency may result in surge TCP connection shortage.
723-
const totalDomain = [
792+
const domains = [
724793
...new Set(
725794
proxies
726795
.filter((p) => !isIP(p.server) && !p['_no-resolve'])
727796
.map((c) => c.server),
728797
),
729798
];
730-
const totalBatch = Math.ceil(totalDomain.length / limit);
731-
for (let i = 0; i < totalBatch; i++) {
732-
const currentBatch = [];
733-
for (let domain of totalDomain.splice(0, limit)) {
734-
currentBatch.push(
735-
resolver(
799+
const domainsToResolve = [];
800+
domains.forEach((domain) => {
801+
const cached = getCachedDomainResolverResult(
802+
provider,
803+
domain,
804+
type,
805+
cache,
806+
url,
807+
);
808+
if (cached) {
809+
results[domain] = cached;
810+
$.info(
811+
`Using cached resolved domain: ${domain}${cached}`,
812+
);
813+
} else {
814+
domainsToResolve.push(domain);
815+
}
816+
});
817+
await resolveDomainsWithConcurrency(
818+
domainsToResolve,
819+
concurrency,
820+
async (domain) => {
821+
try {
822+
const ip = await resolver(
736823
domain,
737824
type,
738825
cache === 'disabled',
739826
requestTimeout,
740827
edns,
741828
url,
742-
)
743-
.then((ip) => {
744-
results[domain] = ip;
745-
$.info(
746-
`Successfully resolved domain: ${domain}${ip}`,
747-
);
748-
})
749-
.catch((err) => {
750-
$.error(
751-
`Failed to resolve domain: ${domain} with resolver [${provider}]: ${err}`,
752-
);
753-
}),
754-
);
755-
}
756-
await Promise.all(currentBatch);
757-
}
829+
);
830+
results[domain] = ip;
831+
$.info(
832+
`Successfully resolved domain: ${domain}${ip}`,
833+
);
834+
} catch (err) {
835+
$.error(
836+
`Failed to resolve domain: ${domain} with resolver [${provider}]: ${err}`,
837+
);
838+
}
839+
},
840+
);
758841
proxies.forEach((p) => {
759842
if (!p['_no-resolve']) {
760843
if (results[p.server]) {

backend/src/core/proxy-utils/producers/clashmeta.js

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,7 @@ export default function ClashMeta_Producer() {
5656
return false;
5757
} else if (proxy.type === 'snell' && proxy.version >= 4) {
5858
return false;
59-
} else if (
60-
['tailscale', 'juicity', 'naive'].includes(proxy.type)
61-
) {
59+
} else if (['juicity', 'naive'].includes(proxy.type)) {
6260
return false;
6361
} else if (
6462
['ss'].includes(proxy.type) &&

backend/src/core/proxy-utils/producers/sing-box.js

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,17 @@ const domainResolverParser = (proxy, parsedProxy) => {
3535
};
3636
}
3737
};
38+
const hasControlHTTPClient = (proxy) => {
39+
const value = proxy['control-http-client'];
40+
if (value === undefined || value === null) return false;
41+
if (typeof value === 'string') return value.trim() !== '';
42+
if (isPlainObject(value)) {
43+
return Object.values(value).some(
44+
(item) => item !== undefined && item !== null && item !== '',
45+
);
46+
}
47+
return true;
48+
};
3849
const detourParser = (proxy, parsedProxy) => {
3950
parsedProxy.detour = proxy['dialer-proxy'] || proxy.detour;
4051
};
@@ -909,11 +920,13 @@ const anytlsParser = (proxy = {}) => {
909920
return parsedProxy;
910921
};
911922
const tailscaleParser = (proxy = {}) => {
923+
const useControlHTTPClient = hasControlHTTPClient(proxy);
912924
const parsedProxy = {
913925
tag: proxy.name,
914926
type: 'tailscale',
927+
control_http_client: proxy['control-http-client'],
915928
udp_timeout: proxy['udp-timeout'],
916-
state_directory: proxy['state-directory'],
929+
state_directory: proxy['state-dir'] || proxy['state-directory'],
917930
auth_key: proxy['auth-key'],
918931
control_url: proxy['control-url'],
919932
ephemeral: proxy.ephemeral,
@@ -947,9 +960,11 @@ const tailscaleParser = (proxy = {}) => {
947960
10,
948961
);
949962
networkParser(proxy, parsedProxy);
950-
detourParser(proxy, parsedProxy);
951-
ipVersionParser(proxy, parsedProxy);
952-
domainResolverParser(proxy, parsedProxy);
963+
if (!useControlHTTPClient) {
964+
detourParser(proxy, parsedProxy);
965+
ipVersionParser(proxy, parsedProxy);
966+
domainResolverParser(proxy, parsedProxy);
967+
}
953968
return parsedProxy;
954969
};
955970

0 commit comments

Comments
 (0)