路由详情加附加规则:每路入站自己的分流表

用内核的 sub-rules 机制:listener 写 rule: 指向顶层 sub-rules 里的表
(listener/inbound/base.go:96,tunnel.go:713-721)。两个硬约束决定了
形态:proxy 优先级更高且完全短路规则匹配(tunnel.go:318-325),有附加
规则时 listener 只能写 rule;子规则表没命中时兜底是 DIRECT 而不是这一
路的出站(tunnel.go match 循环末尾),表底那条 MATCH,专属组 是承重的,
由 Patchbay 自动维护、界面上单独画不参与编辑。不变式:附加规则空 ⇔
proxy 直连专属组,非空 ⇔ rule 形态。

三处既有逻辑教会认 rule 形态:applySetup 不再无条件写 proxy(会把附加
规则静默短路掉);referencesTo 扫 sub-rules 里的规则——表底 MATCH 是
rule 形态下专属组活着的唯一理由,不数它就会被当孤儿清掉;pruneGenerated
清理没有 listener 引用的生成表。panelGroups 和 syncGenerated 的「是否
由 Patchbay 接线」收敛成一个 managedWiring。

listener 的 rule 指向不存在的表时内核静默退回全局规则(getRules 的
else 分支),-t 和重载都不拦,自检里补上这条(vendor 本地改动,记入
ORIGIN.md)。界面:详情列新增附加规则区块,逐条增删改和排序复用
RuleForm,带内网直连预设;生成形状过了 mihomo -t 实测。
This commit is contained in:
2026-08-12 04:01:08 +08:00
parent d1214d4cbd
commit c3c0f56e56
6 changed files with 507 additions and 11 deletions
+71
View File
@@ -1,10 +1,14 @@
import { describe, expect, it } from 'bun:test'
import {
applySetup,
applySubRules,
chainGroupName,
chainProviderName,
managedWiring,
panelGroups,
readSubRules,
sharedPeers,
subRuleName,
syncGenerated,
isGenerated,
linesMatching,
@@ -356,3 +360,70 @@ describe('清理', () => {
expect((pruneGenerated(orphaned)['proxy-groups'] as ConfigObject[]).length).toBe(3)
})
})
describe('附加规则', () => {
const managed = () => applySetup(base(), 'test1', plain)
const listenerOf = (data: ConfigObject) => (data.listeners as ConfigObject[])[0]!
it('写入后 listener 换成 rule 形态,表底自动补兜底 MATCH', () => {
const next = applySubRules(managed(), 'test1', ['DOMAIN-SUFFIX,lan,DIRECT'])
const listener = listenerOf(next)
expect(listener.rule).toBe(subRuleName('test1'))
expect(listener.proxy).toBeUndefined()
expect((next['sub-rules'] as ConfigObject)[subRuleName('test1')]).toEqual([
'DOMAIN-SUFFIX,lan,DIRECT',
'MATCH,patchbay:test1',
])
})
it('读出来不含兜底 MATCH,往返一致', () => {
const rules = ['DOMAIN-SUFFIX,lan,DIRECT', 'GEOIP,CN,DIRECT']
const next = applySubRules(managed(), 'test1', rules)
expect(readSubRules(next, 'test1')).toEqual(rules)
expect(readSubRules(managed(), 'test1')).toEqual([])
})
it('清空后恢复 proxy 直连形态,sub-rules 键整个消失', () => {
const withRules = applySubRules(managed(), 'test1', ['GEOIP,CN,DIRECT'])
const cleared = applySubRules(withRules, 'test1', [])
const listener = listenerOf(cleared)
expect(listener.proxy).toBe(proxyGroupName('test1'))
expect(listener.rule).toBeUndefined()
expect(cleared['sub-rules']).toBeUndefined()
})
it('rule 形态仍被认成 Patchbay 接线,重建设置不会把 proxy 加回来', () => {
// proxy 会短路规则匹配(tunnel.go:318-325),加回来附加规则就静默失效了
const withRules = applySubRules(managed(), 'test1', ['GEOIP,CN,DIRECT'])
const inbound = collectInbounds(withRules).find((i) => i.name === 'test1')!
expect(managedWiring(withRules, inbound)).toBe(true)
const rebuilt = syncGenerated(withRules, collectInbounds(withRules))
const listener = listenerOf(rebuilt)
expect(listener.rule).toBe(subRuleName('test1'))
expect(listener.proxy).toBeUndefined()
})
it('rule 形态下专属组靠表底 MATCH 活着,不会被当孤儿清掉', () => {
const withRules = applySubRules(managed(), 'test1', ['GEOIP,CN,DIRECT'])
const pruned = pruneGenerated(withRules)
const groupNames = (pruned['proxy-groups'] as ConfigObject[]).map((g) => g.name)
expect(groupNames).toContain(proxyGroupName('test1'))
})
it('删掉入站后,附加规则表和组连锁消失', () => {
const withRules = applySubRules(managed(), 'test1', ['GEOIP,CN,DIRECT'])
const removed: ConfigObject = { ...withRules, listeners: [] }
const cleaned = pruneGenerated(removed)
expect(cleaned['sub-rules']).toBeUndefined()
expect(cleaned['proxy-groups']).toBeUndefined()
})
it('用户自己的 sub-rules 表不被清理', () => {
const data: ConfigObject = { ...base(), 'sub-rules': { mine: ['MATCH,DIRECT'] } }
expect((pruneGenerated(data)['sub-rules'] as ConfigObject).mine).toBeDefined()
})
})