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,7 +1,9 @@
import { clientSupports, normalizeSsCipher, type ExcludedNode, type NodeDto } from '@proxy-station/shared';
import type { ProfileIR } from '../ir.js';
import { surgeFfsb } from './surge-ffsb.js';
import { surgeAiDns } from './surge-ai-dns.js';
import { surgeDeclaration, surgeParam, surgeValue as quoteName } from './surge-format.js';
import { WG_POLICY, WG_SECTION, surgeWireguardSection } from './wireguard.js';
export function surgeNodeLine(n: NodeDto): string {
const parts: string[] = [];
@@ -57,6 +59,11 @@ export interface SurgeOutput {
}
export function generateSurge(ir: ProfileIR, opts: { selfUrl: string }): SurgeOutput {
if (ir.wireguard && [...ir.nodes, ...ir.groups].some(n => n.name === WG_POLICY)) {
throw new Error('WireGuard 组网策略名称与现有节点或组冲突');
}
const ffsb = surgeFfsb(ir);
ir = ffsb.ir;
const dns = surgeAiDns(ir);
const excluded: ExcludedNode[] = [];
const usable = ir.nodes.filter((n) => {
@@ -74,6 +81,8 @@ export function generateSurge(ir: ProfileIR, opts: { selfUrl: string }): SurgeOu
lines.push(dns.general);
lines.push('', '[Proxy]');
lines.push(...ffsb.proxyLines);
if (ir.wireguard) lines.push(`${WG_POLICY} = wireguard, section-name=${WG_SECTION}`);
for (const n of usable) lines.push(surgeNodeLine(n));
lines.push('', '[Proxy Group]');
@@ -90,7 +99,9 @@ export function generateSurge(ir: ProfileIR, opts: { selfUrl: string }): SurgeOu
}
const sources = g.members.filter((m) => m.kind === 'group').map((m) => m.name);
if (sources.length) parts.push(`include-other-group=${quoteName(sources.join(','))}`);
parts.push(`policy-regex-filter=${quoteName(g.filterRegex)}`);
// Keep the original regex's unanchored search semantics while excluding only the WG policy.
const filter = ir.wireguard ? `^(?!${WG_POLICY}$).*?(?:${g.filterRegex})` : g.filterRegex;
parts.push(`policy-regex-filter=${quoteName(filter)}`);
} else if (g.includeAllNodes) {
// 「全部节点」组:显式展开当前启用且 Surge 支持的节点
parts.push(...[...new Set([...g.members.filter((m) => m.kind !== 'node' || usableNames.has(m.name)).map((m) => m.name), ...usable.map((n) => n.name)])].map(quoteName));
@@ -98,6 +109,8 @@ export function generateSurge(ir: ProfileIR, opts: { selfUrl: string }): SurgeOu
} else {
parts.push(...(memberNames.length ? memberNames : ['DIRECT']));
}
parts.push(...(ffsb.extraMembers[g.name] ?? []));
if (ffsb.hiddenGroups.includes(g.name)) parts.push('hidden=true');
if (g.type !== 'select') {
// Surge uses per-node test-url or General.proxy-test-url, never group url=.
if (g.interval) parts.push(`interval=${g.interval}`);
@@ -107,6 +120,10 @@ export function generateSurge(ir: ProfileIR, opts: { selfUrl: string }): SurgeOu
}
lines.push('', '[Rule]');
if (ir.wireguard) {
if (ir.wireguard.acceptIncoming) lines.push(`IP-CIDR,${ir.wireguard.address}/32,DIRECT,no-resolve`);
lines.push(`IP-CIDR,${ir.wireguard.subnet},${WG_POLICY},no-resolve`);
}
lines.push(...dns.rules);
for (const r of ir.rules.filter((r) => r.type !== 'FINAL')) {
const value = r.ruleset ? r.ruleset.surgeUrl : r.value;
@@ -123,5 +140,6 @@ export function generateSurge(ir: ProfileIR, opts: { selfUrl: string }): SurgeOu
}
}
if (ir.wireguard) lines.push('', surgeWireguardSection(ir.wireguard));
return { content: lines.join('\n') + '\n', excluded };
}