Skip to content

Commit 201e0ee

Browse files
committed
fix: 修复下行 reality 合法性检查
1 parent 946cdaf commit 201e0ee

3 files changed

Lines changed: 133 additions & 4 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.22.8",
3+
"version": "2.22.9",
44
"description": "Advanced Subscription Manager for QX, Loon, Surge, Stash and Shadowrocket.",
55
"main": "src/main.js",
66
"scripts": {

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,23 @@ function produce(proxies, targetPlatform, type, opts = {}) {
297297
: [];
298298
for (const [realityLabel, realityOpts] of realityChecks) {
299299
if (realityOpts && !isNotBlank(realityOpts['public-key'])) {
300+
// When the main proxy (上行) uses Reality with a valid
301+
// public-key, an explicit empty-string public-key in xhttp
302+
// download-settings reality-opts (下行) is intentional:
303+
// it explicitly cancels Reality inheritance for the
304+
// download stream. This is a legitimate Mihomo config.
305+
// Distinguish from reality-opts:{} (missing public-key
306+
// entirely) which represents a broken/incomplete Reality
307+
// config parsed from a malformed URI and should still be
308+
// rejected.
309+
if (
310+
realityLabel ===
311+
'xhttp download-settings reality-opts' &&
312+
isNotBlank(proxy['reality-opts']?.['public-key']) &&
313+
realityOpts['public-key'] === ''
314+
) {
315+
continue;
316+
}
300317
// Intentional: a VLESS Reality node without public-key is
301318
// not a valid Mihomo export or regenerated share link. We
302319
// keep the marker while parsing so callers can inspect the

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

Lines changed: 115 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -129,8 +129,10 @@ describe('Proxy structured producers', function () {
129129

130130
expect(internal, platform).to.have.length(1);
131131
expect(internal[0].name, platform).to.equal('WS');
132-
expect(external.proxies.map((proxy) => proxy.name), platform).to
133-
.deep.equal(['WS', 'QUIC']);
132+
expect(
133+
external.proxies.map((proxy) => proxy.name),
134+
platform,
135+
).to.deep.equal(['WS', 'QUIC']);
134136
}
135137
});
136138

@@ -395,6 +397,117 @@ describe('Proxy structured producers', function () {
395397
expect(external.trim()).to.equal('proxies:');
396398
});
397399

400+
it('keeps VLESS xhttp proxy when download-settings reality-opts has empty public-key and main reality is valid', function () {
401+
// 上行(主代理)有合法 reality-opts,下行(download-settings)用
402+
// reality-opts: { public-key: '' } 取消继承上行 Reality,合法配置。
403+
const proxy = {
404+
type: 'vless',
405+
name: 'XHTTP Reality Cancel',
406+
server: 'vless-xhttp.example.com',
407+
port: 443,
408+
uuid: UUID,
409+
tls: true,
410+
sni: 'sni.example.com',
411+
network: 'xhttp',
412+
'reality-opts': {
413+
'public-key': 'pubkey',
414+
'short-id': '08',
415+
},
416+
'xhttp-opts': {
417+
path: '/xhttp',
418+
mode: 'stream-up',
419+
'download-settings': {
420+
server: 'download.example.com',
421+
port: 8443,
422+
tls: true,
423+
servername: 'download-sni.example.com',
424+
'reality-opts': { 'public-key': '' },
425+
},
426+
},
427+
};
428+
429+
const internal = produceInternal('Mihomo', proxy);
430+
expect(internal).to.have.length(1);
431+
expect(internal[0].name).to.equal('XHTTP Reality Cancel');
432+
});
433+
434+
it('filters out VLESS xhttp proxy when download-settings reality-opts is empty object (broken URI) even with valid main reality', function () {
435+
// 下行 download-settings.reality-opts 为 {} (没有 public-key 字段),
436+
// 代表 URI 里声明了 security=reality 但 pbk 缺失,属于残缺配置,应过滤。
437+
const proxy = {
438+
type: 'vless',
439+
name: 'XHTTP Broken Reality',
440+
server: 'vless-xhttp.example.com',
441+
port: 443,
442+
uuid: UUID,
443+
tls: true,
444+
sni: 'sni.example.com',
445+
network: 'xhttp',
446+
'reality-opts': {
447+
'public-key': 'pubkey',
448+
'short-id': '08',
449+
},
450+
'xhttp-opts': {
451+
path: '/xhttp',
452+
mode: 'stream-up',
453+
'download-settings': {
454+
server: 'download.example.com',
455+
port: 8443,
456+
tls: true,
457+
'reality-opts': {},
458+
},
459+
},
460+
};
461+
462+
const internal = produceInternal('Mihomo', proxy);
463+
expect(internal).to.have.length(0);
464+
});
465+
466+
it('filters out VLESS xhttp proxy when download-settings reality-opts has empty public-key and main has no valid reality', function () {
467+
// 下行 download-settings 有 reality-opts: { public-key: '' },
468+
// 但上行主代理本身没有合法 Reality,属于无效配置,应被过滤。
469+
const proxy = {
470+
type: 'vless',
471+
name: 'XHTTP Invalid Reality',
472+
server: 'vless-xhttp.example.com',
473+
port: 443,
474+
uuid: UUID,
475+
tls: true,
476+
sni: 'sni.example.com',
477+
network: 'xhttp',
478+
'xhttp-opts': {
479+
path: '/xhttp',
480+
mode: 'stream-up',
481+
'download-settings': {
482+
server: 'download.example.com',
483+
port: 8443,
484+
tls: true,
485+
'reality-opts': { 'public-key': '' },
486+
},
487+
},
488+
};
489+
490+
const internal = produceInternal('Mihomo', proxy);
491+
expect(internal).to.have.length(0);
492+
});
493+
494+
it('filters out VLESS proxy when main reality-opts has empty public-key', function () {
495+
// 主代理 reality-opts.public-key 为空,无论下行如何,都应被过滤。
496+
const proxy = {
497+
type: 'vless',
498+
name: 'Reality Empty Key',
499+
server: 'vless.example.com',
500+
port: 443,
501+
uuid: UUID,
502+
tls: true,
503+
network: 'tcp',
504+
'reality-opts': { 'public-key': '' },
505+
};
506+
507+
const internal = produceInternal('Mihomo', proxy);
508+
expect(internal).to.have.length(0);
509+
});
510+
398511
it('normalizes Stash TUIC defaults and external yaml wrapper', function () {
399512
const proxy = {
400513
type: 'tuic',
@@ -1843,5 +1956,4 @@ describe('Proxy structured producers', function () {
18431956

18441957
expect(internal.map((p) => p.name)).to.deep.equal(['VLESS WS']);
18451958
});
1846-
18471959
});

0 commit comments

Comments
 (0)