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

@@ -95,12 +95,14 @@ export interface TokenDto {
name: string;
/** 分配给该订阅的节点 id;空数组 = 全部节点 */
nodeIds: string[];
/** 旧设备专属订阅兼容映射;不返回设备密钥。 */
wgDeviceId?: string | null;
enabled: boolean;
createdAt: number;
lastAccessAt: number | null;
accessCount: number;
/** 各客户端订阅路径;ShadowRocket 额外有 shadowrocketConf(策略组与规则) */
urls: Record<ClientType, string> & { shadowrocketConf: string };
urls: Record<ClientType, string> & { shadowrocketConf: string; wireguard?: string };
}
export interface ExcludedNode {
@@ -117,3 +119,16 @@ export interface PreviewResult {
export interface SettingsDto {
surgeSections: Record<string, string>;
}
export interface WgDeviceDto {
id: string; gatewayId: string; name: string; os: string; address: string; publicKey: string;
status: 'provisioning' | 'restoring' | 'revoking' | 'error' | 'pending' | 'online' | 'offline' | 'unknown' | 'paused' | 'archived';
canDistribute: boolean; createdAt: number; sampledAt: number | null; lastActivityAt: number | null;
uploadRate: number | null; downloadRate: number | null; uploadTotal: number; downloadTotal: number;
job: { id: string; action: string; state: string; error: string | null } | null;
}
export interface WgGatewayDto {
id: string; server: string; port: number; publicKey: string; subnet: string; mtu: number;
configured: boolean; receivedAt: number | null;
}
export interface WgBindingDto { id: string; deviceId: string; path: string }

View File

@@ -1,4 +1,5 @@
export * from './protocols.js';
export * from './schemas.js';
export * from './dto.js';
export * from './wireguard.js';
export { splitRuleExpression } from './rule-syntax.js';

View File

@@ -7,7 +7,7 @@ export type ClientType = (typeof CLIENTS)[number];
export const CLIENT_LABELS: Record<ClientType, string> = {
surge: 'Surge (macOS)',
shadowrocket: 'ShadowRocket (iOS)',
clash: 'ClashMeta (Android)',
clash: 'FlClash',
};
export type Egress = 'direct' | 'warp' | 'unknown';

36
shared/src/wireguard.ts Normal file
View File

@@ -0,0 +1,36 @@
import { z } from 'zod';
const key = z.string().regex(/^[A-Za-z0-9+/]{43}=$/, 'WireGuard 密钥必须为 32 字节 Base64');
const ipv4 = z.string().ip({ version: 'v4' });
const ipNumber = (ip: string) => ip.split('.').reduce((n, v) => n * 256 + Number(v), 0);
/** Device-specific private-network access; never an Internet proxy node. */
export const wireguardProfileSchema = z.object({
server: z.string().min(1).max(253).regex(/^[a-zA-Z0-9.-]+$/),
port: z.number().int().min(1).max(65535),
serverPublicKey: key,
privateKey: key,
presharedKey: key.optional(),
address: ipv4,
subnet: z.string().regex(/^\d+\.\d+\.\d+\.\d+\/\d+$/),
mtu: z.number().int().min(576).max(1420).default(1280),
keepalive: z.number().int().min(0).max(65535).default(25),
acceptIncoming: z.boolean().default(false),
}).strict().superRefine((v, ctx) => {
const [network, bits] = v.subnet.split('/');
const prefix = Number(bits);
if (!ipv4.safeParse(network).success || prefix < 8 || prefix > 30) {
ctx.addIssue({ code: 'custom', path: ['subnet'], message: '组网需要有效的 IPv4 私网网段(/8–/30)' });
return;
}
const size = 2 ** (32 - prefix);
const base = ipNumber(network);
const address = ipNumber(v.address);
const privateRange = [[167772160, 184549375], [2886729728, 2887778303], [3232235520, 3232301055]]
.some(([lo, hi]) => base >= lo && base + size - 1 <= hi);
if (!privateRange || base % size !== 0 || address <= base || address >= base + size - 1) {
ctx.addIssue({ code: 'custom', path: ['subnet'], message: '仅允许 RFC1918 私网,设备地址必须是该网段的有效主机地址' });
}
});
export type WireguardProfile = z.output<typeof wireguardProfileSchema>;