Skip to content

Commit 6ca5236

Browse files
committed
feat: 优化 sing-box network 转换逻辑, 兼容 UDP 开关, 区分不支持协议
- 调整 sing-box 的 network 解析:`_network` 显式为 `tcp/udp` 时直接生效,并提高优先级,避免和 `udp` 冲突。 - 增加 Mihomo 风格兼容:当节点配置 `udp: false` 时,对支持 `network` 的协议写入 `network: "tcp"`。 - 为 shadowTLS 与 SSR 补齐 network 解析,确保这两类协议也能应用 UDP/TCP 映射规则。 - 移除 anytls、tailscale、wireguard 的 network 映射,避免输出不支持的字段导致配置不合法。 - 新增结构化产物测试,验证: - `udp: true/false` 与 `_network` 的行为差异与优先级; - 不支持 network 的协议不会被误加 `network` 字段。 - 更新 demo 注释说明 `_network` 生效协议列表、`udp` 与 `_network` 的关系。
1 parent 495e88c commit 6ca5236

4 files changed

Lines changed: 101 additions & 5 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.6",
3+
"version": "2.23.7",
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: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,13 @@ const detourParser = (proxy, parsedProxy) => {
5050
parsedProxy.detour = proxy['dialer-proxy'] || proxy.detour;
5151
};
5252
const networkParser = (proxy, parsedProxy) => {
53-
if (['tcp', 'udp'].includes(proxy._network))
53+
if (['tcp', 'udp'].includes(proxy._network)) {
5454
parsedProxy.network = proxy._network;
55+
return;
56+
}
57+
if (proxy.udp === false) {
58+
parsedProxy.network = 'tcp';
59+
}
5560
};
5661
const tfoParser = (proxy, parsedProxy) => {
5762
parsedProxy.tcp_fast_open = false;
@@ -503,6 +508,7 @@ const shadowTLSParser = (proxy = {}) => {
503508
if (proxy['fast-open'] === true) stPart.udp_fragment = true;
504509
tfoParser(proxy, stPart);
505510
detourParser(proxy, stPart);
511+
networkParser(proxy, ssPart);
506512
smuxParser(proxy.smux, ssPart);
507513
ipVersionParser(proxy, stPart);
508514
domainResolverParser(proxy, stPart);
@@ -616,6 +622,7 @@ const ssrParser = (proxy = {}) => {
616622
if (proxy['protocol-param'] && proxy['protocol-param'] !== '')
617623
parsedProxy.protocol_param = proxy['protocol-param'];
618624
if (proxy['fast-open']) parsedProxy.udp_fragment = true;
625+
networkParser(proxy, parsedProxy);
619626
tfoParser(proxy, parsedProxy);
620627
detourParser(proxy, parsedProxy);
621628
smuxParser(proxy.smux, parsedProxy);
@@ -912,7 +919,6 @@ const anytlsParser = (proxy = {}) => {
912919
`${proxy['min-idle-session']}`,
913920
10,
914921
);
915-
networkParser(proxy, parsedProxy);
916922
detourParser(proxy, parsedProxy);
917923
tlsParser(proxy, parsedProxy);
918924
ipVersionParser(proxy, parsedProxy);
@@ -959,7 +965,6 @@ const tailscaleParser = (proxy = {}) => {
959965
`${proxy['relay-server-port']}`,
960966
10,
961967
);
962-
networkParser(proxy, parsedProxy);
963968
if (!useControlHTTPClient) {
964969
detourParser(proxy, parsedProxy);
965970
ipVersionParser(proxy, parsedProxy);
@@ -1051,7 +1056,6 @@ const wireguardParser = (proxy = {}) => {
10511056
parsedProxy.peers.push(peer);
10521057
}
10531058
}
1054-
networkParser(proxy, parsedProxy);
10551059
tfoParser(proxy, parsedProxy);
10561060
detourParser(proxy, parsedProxy);
10571061
smuxParser(proxy.smux, parsedProxy);

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

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2289,6 +2289,96 @@ describe('Proxy structured producers', function () {
22892289
expect(outbound.plugin_opts).to.include('mux=0');
22902290
});
22912291

2292+
it('maps mihomo udp capability flags to sing-box network only when disabling UDP', function () {
2293+
const output = loadProducedJson('sing-box', [
2294+
{
2295+
type: 'ss',
2296+
name: 'SS UDP On',
2297+
server: 'ss-on.example.com',
2298+
port: 8388,
2299+
cipher: 'aes-128-gcm',
2300+
password: 'secret',
2301+
udp: true,
2302+
},
2303+
{
2304+
type: 'ss',
2305+
name: 'SS UDP Off',
2306+
server: 'ss-off.example.com',
2307+
port: 8388,
2308+
cipher: 'aes-128-gcm',
2309+
password: 'secret',
2310+
udp: false,
2311+
},
2312+
{
2313+
type: 'ss',
2314+
name: 'SS Network Override',
2315+
server: 'ss-override.example.com',
2316+
port: 8388,
2317+
cipher: 'aes-128-gcm',
2318+
password: 'secret',
2319+
udp: false,
2320+
_network: 'udp',
2321+
},
2322+
]);
2323+
2324+
const udpOn = output.outbounds.find((item) => item.tag === 'SS UDP On');
2325+
const udpOff = output.outbounds.find(
2326+
(item) => item.tag === 'SS UDP Off',
2327+
);
2328+
const networkOverride = output.outbounds.find(
2329+
(item) => item.tag === 'SS Network Override',
2330+
);
2331+
2332+
expect(udpOn).to.not.have.property('network');
2333+
expect(udpOff).to.have.property('network', 'tcp');
2334+
expect(networkOverride).to.have.property('network', 'udp');
2335+
});
2336+
2337+
it('does not emit sing-box network for protocols without sing-box network options', function () {
2338+
const output = loadProducedJson('sing-box', [
2339+
{
2340+
type: 'anytls',
2341+
name: 'AnyTLS No Network',
2342+
server: 'anytls.example.com',
2343+
port: 443,
2344+
password: 'secret',
2345+
udp: false,
2346+
_network: 'udp',
2347+
},
2348+
{
2349+
type: 'tailscale',
2350+
name: 'Tailscale No Network',
2351+
udp: false,
2352+
_network: 'udp',
2353+
},
2354+
{
2355+
type: 'wireguard',
2356+
name: 'WireGuard No Network',
2357+
server: 'wg.example.com',
2358+
port: 51820,
2359+
'private-key': 'private-key',
2360+
'public-key': 'public-key',
2361+
ip: '10.0.0.2',
2362+
udp: false,
2363+
_network: 'udp',
2364+
},
2365+
]);
2366+
2367+
const anytls = output.outbounds.find(
2368+
(item) => item.tag === 'AnyTLS No Network',
2369+
);
2370+
const tailscale = output.endpoints.find(
2371+
(item) => item.tag === 'Tailscale No Network',
2372+
);
2373+
const wireguard = output.endpoints.find(
2374+
(item) => item.tag === 'WireGuard No Network',
2375+
);
2376+
2377+
expect(anytls).to.not.have.property('network');
2378+
expect(tailscale).to.not.have.property('network');
2379+
expect(wireguard).to.not.have.property('network');
2380+
});
2381+
22922382
it('emits sing-box outbounds with reality tls and websocket transport', function () {
22932383
const output = loadProducedJson('sing-box', {
22942384
type: 'vless',

scripts/demo.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ function operator(proxies = [], targetPlatform, context) {
2727
// 14. `ports` 为端口跳跃, `hop-interval` 变换端口号的时间间隔
2828
// 15. `ip-version` 设置节点使用 IP 版本,兼容各家的值. 会进行内部转换. sing-box 以外: 若无法匹配则使用原始值. sing-box: 需有匹配且节点上设置 `_dns_server` 字段, 将自动设置 `domain_resolver.server`. 同时, `sing-box` 支持使用完整的 `_domain_resolver` 结构设置 `domain_resolver` 字段
2929
// 16. `sing-box` 支持使用 `_network` 来设置 `network`, 例如 `tcp`, `udp`
30+
// 仅对 sing-box 源码里有 `network` 字段的协议生效: `ss`, `ssr`, `socks5`, `vmess`, `vless`, `trojan`, `hysteria`, `hysteria2`, `tuic`.
31+
// 注意: mihomo 风格的 `udp: true` 表示节点支持 UDP, 不会转换成 sing-box 的 `network: "udp"`; sing-box 默认就是 TCP+UDP. `udp: false` 会转换成 `network: "tcp"`. `_network` 是显式覆盖, 优先级高于 `udp`.
3032
// 17. `block-quic` 支持 `auto`, `on`, `off`. 不同的平台不一定都支持, 会自动转换
3133
// 18. `sing-box` 支持 `_fragment`, `_fragment_fallback_delay`, `_record_fragment` 设置 `tls` 的 `fragment`, `fragment_fallback_delay`, `record_fragment`
3234
// 19. `sing-box` 支持 `_certificate`, `_certificate_path`, `_certificate_public_key_sha256`, `_client_certificate`, `_client_certificate_path`, `_client_key`, `_client_key_path` 设置 `tls` 的 `certificate`, `certificate_path`, `certificate_public_key_sha256`, `client_certificate`, `client_certificate_path`, `client_key`, `client_key_path`

0 commit comments

Comments
 (0)