Files
mihomo-patchbay/shared/inbounds.ts
T
im0o f97981b146 默认入站:显示可选出站,并给一条转成命名入站的路
上一版把默认入站的整个出站块都藏了,结果策略组也看不见。改成只藏 RULE/DIRECT/GLOBAL
那三张卡(它固定是 RULE),策略组和线路照常列出来当上下文,只是不可点。

顺带修一个显示错误:老式顶层端口没有 listen 字段,实际地址是内核从 allow-lan 和
bind-address 推出来的(listener/listener.go:709-718)——allow-lan 关着时绑的是
127.0.0.1,而界面一律显示 0.0.0.0,会让人以为局域网连得上。

转换时会把老式端口字段删掉并把推导出的地址显式写进 listen:不删会导致同一个端口被声明
两次内核起不来,不写 listen 则以后改 allow-lan 会连带影响这个端口。
2026-08-10 12:37:33 +08:00

163 lines
6.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 把配置文件里两套写法的入站合并成界面上的一张表。
*
* mihomo 有新旧两套声明入站的方式:
* 旧:顶层的 mixed-port / port / socks-port / redir-port / tproxy-port,
* 每种只能有一个,而且没法给它单独指定出站
* 新:listeners 数组,数量不限,每个都能有自己的出站和附加规则
*
* 界面上必须两边都画。只画 listeners 的话,一台只用 mixed-port 的机器打开是空板——
* 而实际上那个端口正在跑,这等于对用户撒谎。
*/
import type { ConfigObject } from '../vendor/config-editor/index.ts'
/** 内核自带、不需要配置就存在的出站。 */
export const BUILTIN_OUTBOUNDS = [
'DIRECT',
'REJECT',
'REJECT-DROP',
'PASS',
'PASS-RULE',
'COMPATIBLE',
'GLOBAL',
] as const
/** 旧写法的顶层端口字段,以及它们各自等价于哪种入站类型。 */
const LEGACY_PORTS: Array<{ key: string; type: string }> = [
{ key: 'mixed-port', type: 'mixed' },
{ key: 'port', type: 'http' },
{ key: 'socks-port', type: 'socks' },
{ key: 'redir-port', type: 'redir' },
{ key: 'tproxy-port', type: 'tproxy' },
]
/** 一路入站的出站去向。四选一,对应界面上那个下拉框。 */
export type OutboundMode =
| { kind: 'rules' } // 没写 proxy,流量进分流规则
| { kind: 'direct' } // proxy: DIRECT
| { kind: 'global' } // proxy: GLOBAL
| { kind: 'named'; name: string } // proxy: 某个策略组或自有线路
export interface Inbound {
/** 界面上用来定位这一行 */
id: string
name: string
type: string
/** 端口。旧写法一定是单个数字,新写法可以是 "200,302" 或 "401-429" 这类写法 */
port: string
listen: string | null
outbound: OutboundMode
/** 挂在这路入站上的附加规则组名,没有就是 null */
ruleSet: string | null
/** 旧写法声明的入站。界面上不可删除,也不能单独指定出站 */
isDefault: boolean
/** 旧写法对应的顶层字段名,用于写回。新写法为 null */
legacyKey: string | null
/** 在 listeners 数组里的下标,用于写回。旧写法为 null */
listenerIndex: number | null
/** 给人看的一句话说明,主要是给默认入站解释它为什么不能改出站 */
note: string | null
}
function outboundOf(proxy: unknown): OutboundMode {
if (typeof proxy !== 'string' || proxy === '') return { kind: 'rules' }
if (proxy === 'DIRECT') return { kind: 'direct' }
if (proxy === 'GLOBAL') return { kind: 'global' }
return { kind: 'named', name: proxy }
}
/** 把出站去向写回成配置里 proxy 字段该有的值。返回 undefined 表示不写这个字段。 */
export function outboundToProxyField(mode: OutboundMode): string | undefined {
if (mode.kind === 'rules') return undefined
if (mode.kind === 'direct') return 'DIRECT'
if (mode.kind === 'global') return 'GLOBAL'
return mode.name
}
/**
* 老式顶层端口实际绑在哪个地址上。
*
* 它没有自己的 listen 字段,地址是内核从两个全局开关推出来的
* (内核源码 `listener/listener.go:709-718`):
* allow-lan 关 → 127.0.0.1,只有本机连得上
* allow-lan 开 → bind-address 指定的地址,写 * 或没写就是所有网卡
* 界面上不能一律显示 0.0.0.0,那会让人以为局域网连得上而实际连不上。
*/
export function legacyListenAddress(data: ConfigObject): string {
if (data['allow-lan'] !== true) return '127.0.0.1'
const bind = data['bind-address']
if (typeof bind !== 'string' || bind === '' || bind === '*') return '0.0.0.0'
return bind
}
export function collectInbounds(data: ConfigObject): Inbound[] {
const result: Inbound[] = []
const legacyListen = legacyListenAddress(data)
for (const { key, type } of LEGACY_PORTS) {
const raw = data[key]
const port = typeof raw === 'number' ? raw : Number(raw)
// 0 和缺省都表示没启用这个端口
if (!Number.isFinite(port) || port <= 0) continue
result.push({
id: `legacy:${key}`,
// 名字直接用它来自的那个配置键。比「HTTP 和 SOCKS5 共用」这种描述好:
// 类型那一列已经写着协议了,名字再说一遍是重复,而配置键能直接对上文件里的行。
name: key,
type,
port: String(port),
listen: legacyListen,
outbound: { kind: 'rules' },
ruleSet: null,
isDefault: true,
legacyKey: key,
listenerIndex: null,
note: `由配置文件顶层的 ${key} 声明。这种写法没法单独指定出站,流量一律走分流规则。想让这个端口固定走某个出站,要把它改成命名入站。`,
})
}
const listeners = Array.isArray(data.listeners) ? data.listeners : []
listeners.forEach((entry, index) => {
if (typeof entry !== 'object' || entry === null) return
const item = entry as Record<string, unknown>
result.push({
id: `listener:${String(index)}`,
name: typeof item.name === 'string' ? item.name : `第 ${String(index + 1)} 个入站`,
type: typeof item.type === 'string' ? item.type : '',
port: item.port === undefined || item.port === null ? '' : String(item.port),
listen: typeof item.listen === 'string' ? item.listen : null,
outbound: outboundOf(item.proxy),
ruleSet: typeof item.rule === 'string' && item.rule ? item.rule : null,
isDefault: false,
legacyKey: null,
listenerIndex: index,
note: null,
})
})
return result
}
/**
* 所有可以被入站的出站字段引用的名字。
*
* 订阅带来的线路**不在这里**——内核按名字找出站时查的是一张由内置出站、顶层 proxies
* 和 proxy-groups 构成的表,订阅线路住在订阅对象里,不在表上。想让入站固定走订阅里的
* 某条线路,得先建一个只装它的策略组。
*/
export function collectOutboundNames(data: ConfigObject): {
builtins: string[]
groups: string[]
ownProxies: string[]
} {
const groups = (Array.isArray(data['proxy-groups']) ? data['proxy-groups'] : [])
.map((g) => (typeof g === 'object' && g !== null ? (g as Record<string, unknown>).name : null))
.filter((n): n is string => typeof n === 'string')
const ownProxies = (Array.isArray(data.proxies) ? data.proxies : [])
.map((p) => (typeof p === 'object' && p !== null ? (p as Record<string, unknown>).name : null))
.filter((n): n is string => typeof n === 'string')
return { builtins: [...BUILTIN_OUTBOUNDS], groups, ownProxies }
}