SELECT 组的点名成员 AUTO 排第一,不再静默回退到直连

内核给没有选中记录的组默认用第一个成员,选中的线路被订阅刷新删掉时也
静默回退到第一个成员(selector.go:103-112)。之前第一位是 DIRECT,这两种
情况都会让一路入站在无人察觉时变成直连,对按端口定向出站的场景是泄露。

另加 sharedPeers():「共用」标记只在真有别的入站落在同一个组上时才该出现,
只有自己在用时它是噪音。
This commit is contained in:
2026-08-11 14:39:26 +08:00
parent 17f8b3f3fa
commit 6bedce215c
2 changed files with 73 additions and 9 deletions
+42 -5
View File
@@ -4,6 +4,7 @@ import {
chainGroupName,
chainProviderName,
panelGroups,
sharedPeers,
syncGenerated,
isGenerated,
linesMatching,
@@ -84,13 +85,14 @@ describe('筛选预览', () => {
})
describe('一路入站的三个组', () => {
it('SELECT 的成员是 DIRECT 加另外两个组,加上订阅', () => {
// 这三种挑法是所有 clash 配置里固定的那几种,让用户从零拼一遍没有意义
it('SELECT 的成员是另外两个组加 DIRECT,再加上订阅,AUTO 排第一', () => {
// AUTO 必须排第一:内核给没有选中记录的组默认用第一个成员,选中的线路被订阅删掉时
// 也回退到第一个成员。这两种情况落到 DIRECT 上就是静默直连(selector.go:103-112)
const groups = applySetup(base(), 'test1', plain)['proxy-groups'] as ConfigObject[]
expect(groups[0]).toEqual({
name: 'patchbay:test1',
type: 'select',
proxies: ['DIRECT', 'patchbay:test1 AUTO', 'patchbay:test1 FALLBACK'],
proxies: ['patchbay:test1 AUTO', 'patchbay:test1 FALLBACK', 'DIRECT'],
use: ['subs'],
})
})
@@ -147,6 +149,41 @@ describe('一路入站的三个组', () => {
])
})
it('两路没接管的入站落到同一个组时互为共用者', () => {
// 「共用」标记只该出现在这种情况:在其中一路上换选中会连带改掉另一路的去向
const data: ConfigObject = {
...base(),
'mixed-port': 7890,
rules: ['MATCH,PROXY'],
'proxy-groups': [{ name: 'PROXY', type: 'select', use: ['subs'] }],
}
const runtime = { PROXY: { type: 'Selector', now: 'A', all: ['A'] } }
const inbounds = collectInbounds(data)
const legacy = inbounds.find((i) => i.isDefault)!
expect(sharedPeers(data, runtime, inbounds, legacy)).toEqual(['test1'])
})
it('只有自己落在那个组上时没有共用者', () => {
const data: ConfigObject = {
'mixed-port': 7890,
rules: ['MATCH,PROXY'],
'proxy-groups': [{ name: 'PROXY', type: 'select', proxies: ['DIRECT'] }],
}
const runtime = { PROXY: { type: 'Selector', now: 'DIRECT', all: ['DIRECT'] } }
const inbounds = collectInbounds(data)
expect(sharedPeers(data, runtime, inbounds, inbounds[0]!)).toEqual([])
})
it('接管过的入站用的是自己的组,谈不上共用', () => {
const data = applySetup(base(), 'test1', plain)
const runtime = { 'patchbay:test1': { type: 'Selector' } }
const inbounds = collectInbounds(data)
expect(sharedPeers(data, runtime, inbounds, inbounds[0]!)).toEqual([])
})
it('筛选条件三个组共用一份', () => {
const groups = applySetup(base(), 'test1', {
proxy: { sources: ['subs'], filter: '(?i)hk' },
@@ -186,8 +223,8 @@ describe('读取设置', () => {
expect(readSetup(applySetup(base(), 'test1', setup), 'test1')).toEqual(setup)
})
it('开了链式代理时,来源从副本的 url 还原', () => {
// 代理组的 use 这时指向副本,用户选的订阅只能靠 url 相同认回来
it('开了链式代理时,来源从副本的 path 还原', () => {
// 代理组的 use 这时指向副本,用户选的订阅只能靠读同一个缓存文件认回来
const data = applySetup(base(), 'test1', chained)
expect(readSetup(data, 'test1')).toEqual(chained)
})