feat: 添加 WireGuard 设备管理并完善客户端分流
实现独立加密设备身份、网关 agent、持久生命周期任务和 Surge/FlClash 专属订阅凭证。 完善 Shadowrocket 规则转换与 Clash AI DNS,增加 Surge FFSB 适配、规则快照、运维工具及验收文档。
This commit is contained in:
@@ -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 };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user