Skip to content

Commit 6682ab8

Browse files
committed
feat: sing-box 支持 Snell 并增强 ShadowTLS 配置兼容
- 重构 shadowsocks 的 ShadowTLS 解析,统一 plugin 配置读取逻辑,修复 detour tag 生成方式,并支持 plugin/alpn 的兼容处理 - 新增 shadowTLS 出站解析工具化函数,补齐缺失 plugin 配置兜底与 ALPN 转换(含数组/逗号字符串) - 新增 sing-box 的 snell parser:支持版本 1~5,非法版本直接报错,并兼容 obfs/reuse/network 等字段 - 在 sing-box 中新增 snell 处理路径:仅在 include-unsupported-proxy 开启时允许产出,并支持和 ShadowTLS 组合输出链式 outbounds - 补充结构化测试: - snell 默认/开启 include-unsupported-proxy 的行为 - snell 1~5 正常与 6/非法版本失败 - Surge、Mihomo 等来源的 snell 与 shadow-tls 场景 - Shadowsocks shadow-tls 的 ALPN 透传与 shadowtls 出站映射
1 parent 7188d29 commit 6682ab8

3 files changed

Lines changed: 437 additions & 9 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.19",
3+
"version": "2.23.20",
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/producers/sing-box.js

Lines changed: 122 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -469,12 +469,13 @@ const socks5Parser = (proxy = {}) => {
469469
};
470470

471471
const shadowTLSParser = (proxy = {}) => {
472+
const pluginOpts = getShadowTLSPluginOpts(proxy);
472473
const ssPart = {
473474
tag: proxy.name,
474475
type: 'shadowsocks',
475476
method: proxy.cipher,
476477
password: proxy.password,
477-
detour: `${proxy.name}_shadowtls`,
478+
detour: getShadowTLSTag(proxy),
478479
};
479480
if (proxy.uot) ssPart.udp_over_tcp = true;
480481
if (proxy['udp-over-tcp']) {
@@ -487,16 +488,59 @@ const shadowTLSParser = (proxy = {}) => {
487488
: 2,
488489
};
489490
}
491+
networkParser(proxy, ssPart);
492+
smuxParser(proxy.smux, ssPart);
493+
return {
494+
type: 'ss-with-st',
495+
ssPart,
496+
stPart: shadowTLSOutboundParser(proxy, pluginOpts),
497+
};
498+
};
499+
500+
const getShadowTLSTag = (proxy = {}) => `${proxy.name}_shadowtls`;
501+
502+
const getShadowTLSPluginOpts = (proxy = {}) => {
503+
if (proxy.plugin === 'shadow-tls' && proxy['plugin-opts']) {
504+
return proxy['plugin-opts'];
505+
}
506+
if (
507+
proxy['shadow-tls-password'] != null ||
508+
proxy['shadow-tls-sni'] != null ||
509+
proxy['shadow-tls-version'] != null
510+
) {
511+
return {
512+
host: proxy['shadow-tls-sni'],
513+
password: proxy['shadow-tls-password'],
514+
version: proxy['shadow-tls-version'],
515+
};
516+
}
517+
return undefined;
518+
};
519+
520+
const normalizeALPN = (alpn) => {
521+
if (typeof alpn === 'string') {
522+
return alpn
523+
.split(',')
524+
.map((item) => item.trim())
525+
.filter((item) => item !== '');
526+
}
527+
if (Array.isArray(alpn)) return alpn;
528+
return undefined;
529+
};
530+
531+
const shadowTLSOutboundParser = (proxy = {}, pluginOpts) => {
532+
if (!pluginOpts) throw new Error('shadow-tls plugin options are missing');
533+
490534
const stPart = {
491-
tag: `${proxy.name}_shadowtls`,
535+
tag: getShadowTLSTag(proxy),
492536
type: 'shadowtls',
493537
server: proxy.server,
494538
server_port: parseInt(`${proxy.port}`, 10),
495-
version: proxy['plugin-opts'].version,
496-
password: proxy['plugin-opts'].password,
539+
version: pluginOpts.version,
540+
password: pluginOpts.password,
497541
tls: {
498542
enabled: true,
499-
server_name: proxy['plugin-opts'].host,
543+
server_name: pluginOpts.host,
500544
utls: {
501545
enabled: true,
502546
fingerprint: proxy['client-fingerprint'],
@@ -505,15 +549,16 @@ const shadowTLSParser = (proxy = {}) => {
505549
};
506550
if (stPart.server_port < 0 || stPart.server_port > 65535)
507551
throw '端口值非法';
552+
const alpn = normalizeALPN(pluginOpts.alpn);
553+
if (alpn) stPart.tls.alpn = alpn;
508554
if (proxy['fast-open'] === true) stPart.udp_fragment = true;
509555
tfoParser(proxy, stPart);
510556
detourParser(proxy, stPart);
511-
networkParser(proxy, ssPart);
512-
smuxParser(proxy.smux, ssPart);
513557
ipVersionParser(proxy, stPart);
514558
domainResolverParser(proxy, stPart);
515-
return { type: 'ss-with-st', ssPart, stPart };
559+
return stPart;
516560
};
561+
517562
const ssParser = (proxy = {}) => {
518563
const parsedProxy = {
519564
tag: proxy.name,
@@ -631,6 +676,56 @@ const ssrParser = (proxy = {}) => {
631676
return parsedProxy;
632677
};
633678

679+
const getSnellVersion = (version) => {
680+
if (version == null) return undefined;
681+
const normalized = `${version}`.trim();
682+
if (!/^\d+$/.test(normalized)) return NaN;
683+
return parseInt(normalized, 10);
684+
};
685+
686+
const snellParser = (proxy = {}) => {
687+
const version = getSnellVersion(proxy.version);
688+
const shadowTLSPluginOpts = getShadowTLSPluginOpts(proxy);
689+
if (
690+
version != null &&
691+
(![1, 2, 3, 4, 5].includes(version) || Number.isNaN(version))
692+
) {
693+
throw new Error(
694+
`Platform sing-box does not support snell version ${proxy.version}`,
695+
);
696+
}
697+
698+
const parsedProxy = {
699+
tag: proxy.name,
700+
type: 'snell',
701+
server: proxy.server,
702+
server_port: parseInt(`${proxy.port}`, 10),
703+
psk: proxy.psk,
704+
};
705+
if (parsedProxy.server_port < 0 || parsedProxy.server_port > 65535)
706+
throw 'invalid port';
707+
if (version != null) parsedProxy.version = version;
708+
if (proxy['obfs-opts']?.mode)
709+
parsedProxy.obfs_mode = proxy['obfs-opts'].mode;
710+
if (proxy['obfs-opts']?.host)
711+
parsedProxy.obfs_host = proxy['obfs-opts'].host;
712+
if (proxy.reuse && (version == null || version >= 4))
713+
parsedProxy.reuse = true;
714+
networkParser(proxy, parsedProxy);
715+
if (shadowTLSPluginOpts) {
716+
parsedProxy.detour = getShadowTLSTag(proxy);
717+
delete parsedProxy.server;
718+
delete parsedProxy.server_port;
719+
} else {
720+
if (proxy['fast-open']) parsedProxy.udp_fragment = true;
721+
tfoParser(proxy, parsedProxy);
722+
detourParser(proxy, parsedProxy);
723+
ipVersionParser(proxy, parsedProxy);
724+
domainResolverParser(proxy, parsedProxy);
725+
}
726+
return parsedProxy;
727+
};
728+
634729
const vmessParser = (proxy = {}) => {
635730
const parsedProxy = {
636731
tag: proxy.name,
@@ -1145,6 +1240,25 @@ export default function singbox_Producer() {
11451240
);
11461241
}
11471242
break;
1243+
case 'snell':
1244+
if (opts['include-unsupported-proxy']) {
1245+
list.push(snellParser(proxy));
1246+
const shadowTLSPluginOpts =
1247+
getShadowTLSPluginOpts(proxy);
1248+
if (shadowTLSPluginOpts) {
1249+
list.push(
1250+
shadowTLSOutboundParser(
1251+
proxy,
1252+
shadowTLSPluginOpts,
1253+
),
1254+
);
1255+
}
1256+
} else {
1257+
throw new Error(
1258+
`Platform sing-box does not support proxy type: ${proxy.type}`,
1259+
);
1260+
}
1261+
break;
11481262
case 'vmess':
11491263
if (
11501264
!proxy.network ||

0 commit comments

Comments
 (0)