顶层端口字段没有 proxy 可写(listener/inbound/base.go:97),界面上 切不了订阅。升级删掉顶层键、建同端口的 listener:listen 按 allow-lan 推导后写死,全局规则里 MATCH 之前的部分复制成附加规则,来源照抄 MATCH 目标组的 use:,让流量去向尽量不变。udp 不写,命名入站解析时 默认就是开的(listener/parse.go:52)。 「默认」tag 随术语表改成 legacy,旧叫法进 avoid 列表。
This commit is contained in:
@@ -159,7 +159,7 @@ describe('按入站聚合', () => {
|
||||
])
|
||||
})
|
||||
|
||||
it('默认入站用内核给的内部名,附上端口', () => {
|
||||
it('legacy 入站用内核给的内部名,附上端口', () => {
|
||||
const list = [conn({ metadata: meta({ inboundName: 'DEFAULT-MIXED', inboundPort: '17890' }) })]
|
||||
expect(groupByInbound(list)[0]?.label).toBe('DEFAULT-MIXED(17890)')
|
||||
})
|
||||
|
||||
@@ -10,7 +10,7 @@ export interface ConnectionMeta {
|
||||
host: string
|
||||
destinationIP: string
|
||||
destinationPort: string
|
||||
/** 从哪一路入站进来:命名入站是它的名字,默认入站是内核造的 DEFAULT-* */
|
||||
/** 从哪一路入站进来:命名入站是它的名字,legacy 入站是内核造的 DEFAULT-* */
|
||||
inboundName: string
|
||||
inboundPort: string
|
||||
}
|
||||
@@ -100,7 +100,7 @@ export function filterConnections(list: ConnectionItem[], query: string): Connec
|
||||
|
||||
/** 一路入站当前有多少条连接。 */
|
||||
export interface InboundUse {
|
||||
/** 命名入站显示名字;默认入站内核只给 DEFAULT-* 这类内部名,附上端口好认 */
|
||||
/** 命名入站显示名字;legacy 入站内核只给 DEFAULT-* 这类内部名,附上端口好认 */
|
||||
label: string
|
||||
count: number
|
||||
upload: number
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
proxyGroupName,
|
||||
pruneGenerated,
|
||||
readSetup,
|
||||
upgradeLegacyInbound,
|
||||
} from './inbound-groups.ts'
|
||||
import type { OutboundSetup } from './inbound-groups.ts'
|
||||
import { collectInbounds } from './inbounds.ts'
|
||||
@@ -427,3 +428,73 @@ describe('附加规则', () => {
|
||||
expect((pruneGenerated(data)['sub-rules'] as ConfigObject).mine).toBeDefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('legacy 入站升级', () => {
|
||||
/** 形状仿照目标机器:一个 legacy 混合端口,全局规则是内网直连加 MATCH 兜底。 */
|
||||
function legacyBase(): ConfigObject {
|
||||
return {
|
||||
'mixed-port': 7890,
|
||||
'allow-lan': true,
|
||||
'proxy-providers': {
|
||||
subs: { type: 'http', url: URL, path: './providers/subs.yaml' },
|
||||
other: { type: 'http', url: `${URL}2`, path: './providers/other.yaml' },
|
||||
},
|
||||
'proxy-groups': [{ name: 'PROXY', type: 'select', proxies: ['DIRECT'], use: ['subs'] }],
|
||||
rules: [
|
||||
'IP-CIDR,192.168.0.0/16,DIRECT,no-resolve',
|
||||
'MATCH,PROXY',
|
||||
'DOMAIN,dead.example,DIRECT',
|
||||
],
|
||||
}
|
||||
}
|
||||
const legacyInbound = (data: ConfigObject) => collectInbounds(data).find((i) => i.isDefault)!
|
||||
|
||||
it('删掉顶层键,建同端口的命名入站,listen 按 allow-lan 推导写死', () => {
|
||||
const data = legacyBase()
|
||||
const next = upgradeLegacyInbound(data, legacyInbound(data))!
|
||||
expect(next['mixed-port']).toBeUndefined()
|
||||
expect((next.listeners as ConfigObject[]).at(-1)).toMatchObject({
|
||||
name: 'mixed-port',
|
||||
type: 'mixed',
|
||||
port: 7890,
|
||||
listen: '0.0.0.0',
|
||||
})
|
||||
})
|
||||
|
||||
it('allow-lan 关着时 listen 写 127.0.0.1', () => {
|
||||
const data = { ...legacyBase(), 'allow-lan': false }
|
||||
const next = upgradeLegacyInbound(data, legacyInbound(data))!
|
||||
expect(((next.listeners as ConfigObject[]).at(-1) as ConfigObject).listen).toBe('127.0.0.1')
|
||||
})
|
||||
|
||||
it('全局规则里第一条 MATCH 之前的部分复制成附加规则,全局规则原文不动', () => {
|
||||
const data = legacyBase()
|
||||
const next = upgradeLegacyInbound(data, legacyInbound(data))!
|
||||
expect(readSubRules(next, 'mixed-port')).toEqual(['IP-CIDR,192.168.0.0/16,DIRECT,no-resolve'])
|
||||
expect(next.rules).toEqual(legacyBase().rules)
|
||||
})
|
||||
|
||||
it('升级完就在专属组模式下,来源照抄 MATCH 目标组的 use', () => {
|
||||
const data = legacyBase()
|
||||
const next = upgradeLegacyInbound(data, legacyInbound(data))!
|
||||
expect(readSetup(next, 'mixed-port')).toEqual({
|
||||
proxy: { sources: ['subs'], filter: null },
|
||||
chain: null,
|
||||
})
|
||||
expect(managedWiring(next, collectInbounds(next).find((i) => i.name === 'mixed-port')!)).toBe(
|
||||
true,
|
||||
)
|
||||
})
|
||||
|
||||
it('MATCH 目标组没有 use 时退回全部订阅', () => {
|
||||
const data = legacyBase()
|
||||
;(data['proxy-groups'] as ConfigObject[])[0] = { name: 'PROXY', type: 'select', proxies: ['DIRECT'] }
|
||||
const next = upgradeLegacyInbound(data, legacyInbound(data))!
|
||||
expect(readSetup(next, 'mixed-port')?.proxy.sources).toEqual(['subs', 'other'])
|
||||
})
|
||||
|
||||
it('已有同名入站时返回 null,不写出重名的 listener', () => {
|
||||
const data = { ...legacyBase(), listeners: [{ name: 'mixed-port', type: 'mixed', port: 1 }] }
|
||||
expect(upgradeLegacyInbound(data, legacyInbound(data))).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
* 副本的来源 path 和哪个用户订阅相同(读同一个缓存文件)
|
||||
*/
|
||||
import type { ConfigObject } from '../vendor/config-editor/index.ts'
|
||||
import { resolveDestination } from './inbounds.ts'
|
||||
import { fallbackTarget, legacyListenAddress, resolveDestination } from './inbounds.ts'
|
||||
import type { Inbound } from './inbounds.ts'
|
||||
import { lastSwitchableGroup, resolveChain } from './outbound-chain.ts'
|
||||
import type { RuntimeNode } from './outbound-chain.ts'
|
||||
@@ -433,6 +433,56 @@ export function applySetup(data: ConfigObject, inbound: string, setup: OutboundS
|
||||
return next
|
||||
}
|
||||
|
||||
/**
|
||||
* 把一路 legacy 入站升级成命名入站,并直接进专属组模式。
|
||||
*
|
||||
* legacy 入站(顶层端口字段)没有 `proxy` 可写,只能顺全局规则走,在界面上
|
||||
* 切不了订阅。升级 = 删掉顶层端口键,在 `listeners:` 里建一个行为等价的入站:
|
||||
* 端口 原样
|
||||
* listen 按 allow-lan 推导后写死。升级后改 allow-lan 不再影响它,这是升级的
|
||||
* 语义变化之一,界面文案要说
|
||||
* udp 不写。命名入站解析时默认就是开的(内核源码 listener/parse.go:52)
|
||||
*
|
||||
* 行为等价还差分流:它原来查全局规则,升级后整表改查自己的附加规则,所以把
|
||||
* 全局规则里第一条 MATCH 之前的部分复制过来(MATCH 之后的行本来就永远轮不到)。
|
||||
* 全局规则原文不动——那是用户手写的段落,何况可能还有别的 legacy 入站在用。
|
||||
* 专属组的来源照抄原 MATCH 目标组的 `use:`,流量的实际去向尽量不变;
|
||||
* 目标组没有 `use:` 时退回全部订阅。
|
||||
*
|
||||
* 同名入站已存在时返回 null,调用方据此报错而不是写出重名的 listener。
|
||||
*/
|
||||
export function upgradeLegacyInbound(data: ConfigObject, inbound: Inbound): ConfigObject | null {
|
||||
if (inbound.legacyKey === null) return null
|
||||
const port = Number(inbound.port)
|
||||
if (!Number.isFinite(port) || port <= 0) return null
|
||||
const listeners = Array.isArray(data.listeners) ? data.listeners : []
|
||||
if (listeners.some((entry) => isPlainObject(entry) && entry.name === inbound.name)) return null
|
||||
|
||||
const next = { ...data }
|
||||
delete next[inbound.legacyKey]
|
||||
next.listeners = [
|
||||
...listeners,
|
||||
{ name: inbound.name, type: inbound.type, port, listen: legacyListenAddress(data) },
|
||||
]
|
||||
|
||||
const rules: string[] = []
|
||||
for (const rule of Array.isArray(data.rules) ? data.rules : []) {
|
||||
if (typeof rule !== 'string') continue
|
||||
if (rule.split(',')[0]?.trim().toUpperCase() === 'MATCH') break
|
||||
rules.push(rule)
|
||||
}
|
||||
|
||||
const target = fallbackTarget(data)
|
||||
const providers = new Set(userProviders(data))
|
||||
const uses = (target ? (groupUses(data, target) ?? []) : []).filter((name) => providers.has(name))
|
||||
const sources = uses.length > 0 ? uses : userProviders(data)
|
||||
|
||||
return applySetup(applySubRules(next, inbound.name, rules), inbound.name, {
|
||||
proxy: { sources, filter: null },
|
||||
chain: null,
|
||||
})
|
||||
}
|
||||
|
||||
/** 配置里指向某个名字的地方有几处。用来判断生成的东西还有没有人在用。 */
|
||||
function referencesTo(data: ConfigObject, name: string): number {
|
||||
let count = 0
|
||||
|
||||
@@ -42,7 +42,7 @@ describe('合并两套写法的入站', () => {
|
||||
expect(list[1]!.listenerIndex).toBe(0)
|
||||
})
|
||||
|
||||
it('默认入站的出站永远是「跟着规则走」,因为旧写法没处写', () => {
|
||||
it('legacy 入站的出站永远是「跟着规则走」,因为旧写法没处写', () => {
|
||||
expect(collectInbounds({ 'mixed-port': 7777 })[0]!.outbound).toEqual({ kind: 'rules' })
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user