Skip to content

Commit 3374eae

Browse files
committed
feat: sing-box 官方版支持 Snell
1 parent 6a55efb commit 3374eae

4 files changed

Lines changed: 89 additions & 54 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.33.2",
3+
"version": "2.34.0",
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: 38 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -538,10 +538,7 @@ const getShadowTLSPluginOpts = (proxy = {}) => {
538538
if (proxy.plugin === 'shadow-tls' && proxy['plugin-opts']) {
539539
return proxy['plugin-opts'];
540540
}
541-
if (
542-
proxy.type === 'snell' &&
543-
proxy['obfs-opts']?.mode === 'shadow-tls'
544-
) {
541+
if (proxy.type === 'snell' && proxy['obfs-opts']?.mode === 'shadow-tls') {
545542
return {
546543
host: proxy['obfs-opts'].host,
547544
password: proxy['obfs-opts'].password,
@@ -721,17 +718,22 @@ const getSnellVersion = (version) => {
721718
return parseInt(normalized, 10);
722719
};
723720

724-
const snellParser = (proxy = {}) => {
721+
const snellParser = (proxy = {}, includeUnsupportedProxy = false) => {
725722
const version = getSnellVersion(proxy.version);
726723
const shadowTLSPluginOpts = getShadowTLSPluginOpts(proxy);
724+
const supportedVersions = includeUnsupportedProxy
725+
? [1, 2, 3, 4, 5, 6]
726+
: [4, 5, 6];
727727
if (
728728
version != null &&
729-
(![1, 2, 3, 4, 5].includes(version) || Number.isNaN(version))
729+
(!supportedVersions.includes(version) || Number.isNaN(version))
730730
) {
731731
throw new Error(
732732
`Platform sing-box does not support snell version ${proxy.version}`,
733733
);
734734
}
735+
const outputVersion =
736+
!includeUnsupportedProxy && version === 5 ? 4 : version;
735737

736738
const parsedProxy = {
737739
tag: proxy.name,
@@ -742,17 +744,22 @@ const snellParser = (proxy = {}) => {
742744
};
743745
if (parsedProxy.server_port < 0 || parsedProxy.server_port > 65535)
744746
throw 'invalid port';
745-
if (version != null) parsedProxy.version = version;
746-
if (
747-
proxy['obfs-opts']?.mode &&
748-
proxy['obfs-opts'].mode !== 'shadow-tls'
749-
)
750-
parsedProxy.obfs_mode = proxy['obfs-opts'].mode;
751-
if (
752-
proxy['obfs-opts']?.host &&
753-
proxy['obfs-opts']?.mode !== 'shadow-tls'
754-
)
755-
parsedProxy.obfs_host = proxy['obfs-opts'].host;
747+
if (outputVersion != null) parsedProxy.version = outputVersion;
748+
if (proxy._userkey) parsedProxy.userkey = proxy._userkey;
749+
if (outputVersion === 6) {
750+
if (proxy.mode) parsedProxy.mode = proxy.mode;
751+
} else {
752+
if (
753+
proxy['obfs-opts']?.mode &&
754+
proxy['obfs-opts'].mode !== 'shadow-tls'
755+
)
756+
parsedProxy.obfs_mode = proxy['obfs-opts'].mode;
757+
if (
758+
proxy['obfs-opts']?.host &&
759+
proxy['obfs-opts']?.mode !== 'shadow-tls'
760+
)
761+
parsedProxy.obfs_host = proxy['obfs-opts'].host;
762+
}
756763
if (proxy.reuse && (version == null || version >= 4))
757764
parsedProxy.reuse = true;
758765
networkParser(proxy, parsedProxy);
@@ -1393,21 +1400,20 @@ export default function singbox_Producer() {
13931400
}
13941401
break;
13951402
case 'snell':
1396-
if (opts['include-unsupported-proxy']) {
1397-
list.push(snellParser(proxy));
1398-
const shadowTLSPluginOpts =
1399-
getShadowTLSPluginOpts(proxy);
1400-
if (shadowTLSPluginOpts) {
1401-
list.push(
1402-
shadowTLSOutboundParser(
1403-
proxy,
1404-
shadowTLSPluginOpts,
1405-
),
1406-
);
1407-
}
1408-
} else {
1409-
throw new Error(
1410-
`Platform sing-box does not support proxy type: ${proxy.type}`,
1403+
list.push(
1404+
snellParser(
1405+
proxy,
1406+
opts['include-unsupported-proxy'],
1407+
),
1408+
);
1409+
const shadowTLSPluginOpts =
1410+
getShadowTLSPluginOpts(proxy);
1411+
if (shadowTLSPluginOpts) {
1412+
list.push(
1413+
shadowTLSOutboundParser(
1414+
proxy,
1415+
shadowTLSPluginOpts,
1416+
),
14111417
);
14121418
}
14131419
break;

backend/src/test/proxy-producers/structured.spec.js

Lines changed: 48 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -334,14 +334,15 @@ describe('Proxy structured producers', function () {
334334
);
335335
});
336336

337-
it('keeps Snell in sing-box only when include-unsupported-proxy is enabled', function () {
337+
it('keeps supported Snell in sing-box by default', function () {
338338
const proxy = {
339339
type: 'snell',
340340
name: 'sing-box Snell',
341341
server: 'snell.example.com',
342342
port: 44046,
343343
psk: 'secret',
344-
version: 4,
344+
version: 5,
345+
_userkey: 'user-secret',
345346
udp: false,
346347
tfo: true,
347348
'fast-open': true,
@@ -359,21 +360,12 @@ describe('Proxy structured producers', function () {
359360
},
360361
};
361362

362-
const { result, errors } = captureErrors(() =>
363+
const { result: internal, errors } = captureErrors(() =>
363364
produceInternal('sing-box', proxy),
364365
);
365-
const internal = produceInternal('sing-box', proxy, {
366-
'include-unsupported-proxy': true,
367-
});
368-
const external = loadProducedJson('sing-box', proxy, {
369-
'include-unsupported-proxy': true,
370-
});
366+
const external = loadProducedJson('sing-box', proxy);
371367

372-
expect(result).to.deep.equal([]);
373-
expect(errors).to.have.length(1);
374-
expect(errors[0]).to.include(
375-
'Platform sing-box does not support proxy type: snell',
376-
);
368+
expect(errors).to.deep.equal([]);
377369
expect(internal).to.have.length(1);
378370
expectSubset(internal[0], {
379371
tag: 'sing-box Snell',
@@ -382,6 +374,7 @@ describe('Proxy structured producers', function () {
382374
server_port: 44046,
383375
psk: 'secret',
384376
version: 4,
377+
userkey: 'user-secret',
385378
reuse: true,
386379
network: 'tcp',
387380
obfs_mode: 'tls',
@@ -399,14 +392,49 @@ describe('Proxy structured producers', function () {
399392
expectSubset(external.outbounds[0], internal[0]);
400393
});
401394

402-
it('keeps sing-box Snell versions documented by reF1nd and rejects newer versions', function () {
395+
it('keeps sing-box supported Snell versions by default and maps v5 to v4', function () {
396+
const proxies = [1, 4, 5, 6, '4x'].map((version) => ({
397+
type: 'snell',
398+
name: `sing-box Snell ${version}`,
399+
server: 'snell.example.com',
400+
port: 44046,
401+
psk: 'secret',
402+
version,
403+
mode: 'unshaped',
404+
udp: true,
405+
reuse: true,
406+
'obfs-opts': {
407+
mode: 'http',
408+
host: 'obfs.example.com',
409+
},
410+
}));
411+
412+
const { result, errors } = captureErrors(() =>
413+
produceInternal('sing-box', proxies),
414+
);
415+
416+
expect(result.map((proxy) => proxy.version)).to.deep.equal([4, 4, 6]);
417+
expect(result[1].tag).to.equal('sing-box Snell 5');
418+
expect(result[2].mode).to.equal('unshaped');
419+
expect(result[2]).to.not.have.property('obfs_mode');
420+
expect(errors).to.have.length(2);
421+
expect(errors[0]).to.include(
422+
'Platform sing-box does not support snell version 1',
423+
);
424+
expect(errors[1]).to.include(
425+
'Platform sing-box does not support snell version 4x',
426+
);
427+
});
428+
429+
it('keeps legacy Snell versions when include-unsupported-proxy is enabled', function () {
403430
const proxies = [1, 2, 3, 4, 5, 6, '4x'].map((version) => ({
404431
type: 'snell',
405432
name: `sing-box Snell ${version}`,
406433
server: 'snell.example.com',
407434
port: 44046,
408435
psk: 'secret',
409436
version,
437+
_userkey: `user-${version}`,
410438
udp: true,
411439
reuse: true,
412440
}));
@@ -418,7 +446,7 @@ describe('Proxy structured producers', function () {
418446
);
419447

420448
expect(result.map((proxy) => proxy.version)).to.deep.equal([
421-
1, 2, 3, 4, 5,
449+
1, 2, 3, 4, 5, 6,
422450
]);
423451
expect(
424452
result.find((proxy) => proxy.version === 1),
@@ -429,11 +457,11 @@ describe('Proxy structured producers', function () {
429457
expect(result.find((proxy) => proxy.version === 4).reuse).to.equal(
430458
true,
431459
);
432-
expect(errors).to.have.length(2);
433-
expect(errors[0]).to.include(
434-
'Platform sing-box does not support snell version 6',
460+
expect(result.find((proxy) => proxy.version === 5).userkey).to.equal(
461+
'user-5',
435462
);
436-
expect(errors[1]).to.include(
463+
expect(errors).to.have.length(1);
464+
expect(errors[0]).to.include(
437465
'Platform sing-box does not support snell version 4x',
438466
);
439467
});

scripts/demo.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ function operator(proxies = [], targetPlatform, context) {
3030
// 14. `ports` 为端口跳跃, `hop-interval` 变换端口号的时间间隔
3131
// 15. `ip-version` 设置节点使用 IP 版本,兼容各家的值. 会进行内部转换. sing-box 以外: 若无法匹配则使用原始值. sing-box: 需有匹配且节点上设置 `_dns_server` 字段, 将自动设置 `domain_resolver.server`. 同时, `sing-box` 支持使用 `_domain_resolver` 设置 `domain_resolver`: 字符串会作为 `server`, 对象会合并到 `domain_resolver`
3232
// 16. `sing-box` 支持使用 `_network` 来设置 `network`, 例如 `tcp`, `udp`
33-
// 仅对 sing-box 源码里有 `network` 字段的协议生效: `ss`, `ssr`, `socks5`, `vmess`, `vless`, `trojan`, `hysteria`, `hysteria2`, `tuic`.
33+
// 仅对 sing-box 源码里有 `network` 字段的协议生效: `ss`, `ssr`, `socks5`, `vmess`, `vless`, `trojan`, `hysteria`, `hysteria2`, `tuic`, `snell`.
3434
// 注意: mihomo 风格的 `udp: true` 表示节点支持 UDP, 不会转换成 sing-box 的 `network: "udp"`; sing-box 默认就是 TCP+UDP. `udp: false` 会转换成 `network: "tcp"`. `_network` 是显式覆盖, 优先级高于 `udp`.
3535
// 17. `block-quic` 支持 `auto`, `on`, `off`. 不同的平台不一定都支持, 会自动转换
3636
// 18. `sing-box` 支持 `_fragment`, `_fragment_fallback_delay`, `_record_fragment` 设置 `tls` 的 `fragment`, `fragment_fallback_delay`, `record_fragment`
@@ -55,6 +55,7 @@ function operator(proxies = [], targetPlatform, context) {
5555
// 30. Loon 支持使用 `_loon_tls_profile` 设置 `tls-profile` 字段('default', 'chrome', 'ios18', 'ios26'), 否则则使用 client-fingerprint 自动转换部分对应的值
5656
// 31. `shadow-tls-password`/`shadow-tls-sni`/`shadow-tls-version` 这套旧字段已废弃. 请使用 `plugin: 'shadow-tls'` 和 `plugin-opts: { password, host, version }`
5757
// 32. mihomo 中 Snell shadow-tls 字段与 ss shadow-tls 字段不同, 使用的是 obfs-opts 而不是 plugin+plugin-opts, 不能与 obfs http/tls 共存. Sub-Store 内部有字段转换, 建议直接使用单行 Surge 格式 `1=snell,a.com,443,version=4,psk="1",obfs=http,obfs-host=a.com,shadow-tls-password="1",shadow-tls-sni=a.com,shadow-tls-version=3,alpn="http/1.1,h2",reuse=true` . 若想使用 JSON/JSON5/YAML 单行格式输入, 可使用 `{ "name": "1", "server": "a.com", "port": 443, "psk": "1", "version": 4, "reuse": true, "type": "snell", "obfs-opts": { "mode": "http", "host": "a.com" }, "plugin": "shadow-tls", "plugin-opts": { "host": "a.com", "password": "1", "version": 3, "alpn": [ "http/1.1", "h2" ] } }`
58+
// 33. sing-box Snell 出站默认允许 version 4/5/6, 其中 version 5 会按 sing-box 行为输出成 version 4. 开启“含不支持的协议”时保留 version 1/2/3/4/5/6. 节点上的 `_userkey` 会输出为 sing-box 的 `userkey`
5859

5960
// require 为 Node.js 的 require, 在 Node.js 运行环境下 可以用来引入模块
6061
// 例如在 Node.js 环境下, 将文件内容写入 /tmp/1.txt 文件

0 commit comments

Comments
 (0)