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