feat: 添加 WireGuard 设备管理并完善客户端分流

实现独立加密设备身份、网关 agent、持久生命周期任务和 Surge/FlClash 专属订阅凭证。

完善 Shadowrocket 规则转换与 Clash AI DNS,增加 Surge FFSB 适配、规则快照、运维工具及验收文档。
This commit is contained in:
YANG JIANKUAN
2026-11-02 22:44:23 +08:00
parent f741aeeccd
commit a5d980a5e9
80 changed files with 4777 additions and 51 deletions

View File

@@ -1,6 +1,8 @@
import YAML from 'yaml';
import { clientSupports, normalizeSsCipher, splitRuleExpression, type ExcludedNode, type NodeDto } from '@proxy-station/shared';
import type { ProfileIR } from '../ir.js';
import { clashAiDns } from './clash-ai-dns.js';
import { WG_POLICY } from './wireguard.js';
export function clashProxy(n: NodeDto): Record<string, unknown> {
const base: Record<string, unknown> = {
@@ -119,6 +121,9 @@ export interface ClashOutput {
}
export function generateClash(ir: ProfileIR, opts: { convertBaseUrl: string }): ClashOutput {
if (ir.wireguard && [...ir.nodes, ...ir.groups].some(n => n.name === WG_POLICY)) {
throw new Error('WireGuard 组网策略名称与现有节点或组冲突');
}
const excluded: ExcludedNode[] = [];
const usable = ir.nodes.filter((n) => {
const verdict = clientSupports('clash', n);
@@ -128,6 +133,13 @@ export function generateClash(ir: ProfileIR, opts: { convertBaseUrl: string }):
const usableNames = new Set(usable.map((n) => n.name));
const proxies = usable.map(clashProxy);
if (ir.wireguard) {
const wg = ir.wireguard;
proxies.push({ name: WG_POLICY, type: 'wireguard', ip: wg.address, 'private-key': wg.privateKey,
mtu: wg.mtu, udp: true, 'persistent-keepalive': wg.keepalive,
peers: [{ server: wg.server, port: wg.port, 'public-key': wg.serverPublicKey,
...(wg.presharedKey ? { 'pre-shared-key': wg.presharedKey } : {}), 'allowed-ips': [wg.subnet] }] });
}
const proxyGroups = ir.groups.map((g) => {
const group: Record<string, unknown> = { name: g.name, type: g.type };
@@ -136,10 +148,12 @@ export function generateClash(ir: ProfileIR, opts: { convertBaseUrl: string }):
.map((m) => m.name);
if (g.filterRegex) {
group['include-all-proxies'] = true;
if (ir.wireguard) group['exclude-filter'] = `^${WG_POLICY}$`;
group.filter = g.filterRegex;
if (members.length) group.proxies = [...new Set(members)];
} else if (g.includeAllNodes) {
group['include-all-proxies'] = true;
if (ir.wireguard) group['exclude-filter'] = `^${WG_POLICY}$`;
if (members.length) group.proxies = [...new Set(members)];
} else {
group.proxies = members.length ? members : ['DIRECT'];
@@ -154,7 +168,7 @@ export function generateClash(ir: ProfileIR, opts: { convertBaseUrl: string }):
// rule-providers:从启用规则引用的规则集生成,名称唯一化
const providerNameByRulesetId = new Map<string, string>();
const providers: Record<string, unknown> = {};
const providers: Record<string, Record<string, unknown>> = {};
const takenNames = new Set<string>();
const usedRulesets = ir.rules.flatMap((r) => (r.ruleset ? [r.ruleset] : []));
for (const rs of usedRulesets) {
@@ -175,6 +189,7 @@ export function generateClash(ir: ProfileIR, opts: { convertBaseUrl: string }):
const skippedRules: string[] = [];
const ruleLines: string[] = [];
if (ir.wireguard) ruleLines.push(`IP-CIDR,${ir.wireguard.subnet},${WG_POLICY},no-resolve`);
for (const r of ir.rules) {
if (r.type === 'FINAL') {
ruleLines.push(`MATCH,${mapPolicy(r.policy)}`);
@@ -217,6 +232,8 @@ export function generateClash(ir: ProfileIR, opts: { convertBaseUrl: string }):
ruleLines.push([r.type, r.value, policy, ...params].join(','));
}
const defaultNameservers = ['https://doh.pub/dns-query', 'https://dns.alidns.com/dns-query'];
const aiDns = clashAiDns(ir, providers, providerNameByRulesetId, defaultNameservers);
const doc = {
'mixed-port': 7890,
'allow-lan': false,
@@ -228,7 +245,8 @@ export function generateClash(ir: ProfileIR, opts: { convertBaseUrl: string }):
enable: true,
listen: '0.0.0.0:1053',
'enhanced-mode': 'fake-ip',
nameserver: ['https://doh.pub/dns-query', 'https://dns.alidns.com/dns-query'],
nameserver: defaultNameservers,
...aiDns,
},
proxies,
'proxy-groups': proxyGroups,