如果只是展示几个配置字段,Vue 3 集成 WireGuard 确实没有多少难度,但真正工程化意味着要把密钥派生、wg-quick 语法解析、隧道状态同步和浏览器安全边界都纳入统一的组件体系。本文会围绕这些环节给出可落地的实现思路。

一、前端与 WireGuard 的边界划分
WireGuard 本身是一个内核级或用户态的 VPN 实现,真正的数据面转发依赖操作系统网络栈。浏览器环境没有原始 UDP socket 权限,Vue 3 应用通常无法直接充当完整的 WireGuard 节点。因此先把职责拆开:前端负责配置生成、密钥管理、Peer 信息展示、隧道状态轮询;后端或本地代理负责加载内核模块、执行 wg-quick 命令、维护实际隧道。
在工程化项目中,可以定义一个清晰的 API 契约。例如,后端暴露 /api/wireguard/status 返回每个 Peer 的 latest handshake、transfer 数据,前端只消费这些状态。配置下发则通过受控接口完成,而不是让浏览器直接写 /etc/wireguard/wg0.conf。这样做既降低前端复杂度,也避免敏感文件系统操作暴露在 Web 层。
但部分场景仍然可以把更多逻辑搬进前端:例如使用 WebAssembly 运行 wireguard-go,配合 WebTransport 实现数据通道。这类方案适用于浏览器扩展或 Electron 应用,普通 Web 页面仍建议保持轻边界。
二、在 Vue 3 中生成 WireGuard 密钥与配置
WireGuard 使用 Curve25519 密钥对,私钥为 32 字节,公钥为 32 字节,通常用 Base64 编码。Web Crypto API 不支持 X25519,因此需要引入第三方库。推荐 @noble/curves,纯 TypeScript 实现且体积较小。安装后可以封装成一个 composable。
// composables/useWireGuardKeys.ts
import { x25519 } from '@noble/curves/ed25519';
import { randomBytes } from '@noble/hashes/utils';
export function useWireGuardKeys() {
function generateKeyPair() {
const privateKey = randomBytes(32);
const publicKey = x25519.getPublicKey(privateKey);
return {
privateKey: Buffer.from(privateKey).toString('base64'),
publicKey: Buffer.from(publicKey).toString('base64')
};
}
function derivePublicKey(privateKeyBase64: string) {
const privateKey = Buffer.from(privateKeyBase64, 'base64');
const publicKey = x25519.getPublicKey(privateKey);
return Buffer.from(publicKey).toString('base64');
}
return { generateKeyPair, derivePublicKey };
}
上面的代码用 Buffer 做 Base64 转换,在浏览器中需要引入 polyfill,或者直接使用 btoa 和 Uint8Array。关键点是私钥必须立即放入受控状态,不要挂到全局对象。
配置生成同样可以纯前端完成。wg-quick 的 Interface 段落包含 PrivateKey、Address、ListenPort、DNS 等键,Peer 段落包含 PublicKey、AllowedIPs、Endpoint、PersistentKeepalive。可以设计一个函数把结构化对象序列化为文本,并做基本校验。
// utils/wgConfig.ts
export interface WireGuardPeer {
publicKey: string;
allowedIPs: string[];
endpoint?: string;
persistentKeepalive?: number;
}
export interface WireGuardConfig {
privateKey: string;
address: string[];
dns?: string[];
mtu?: number;
peers: WireGuardPeer[];
}
export function buildWgQuickConfig(config: WireGuardConfig): string {
const lines: string[] = [];
lines.push('[Interface]');
lines.push(`PrivateKey = ${config.privateKey}`);
lines.push(`Address = ${config.address.join(', ')}`);
if (config.dns && config.dns.length) {
lines.push(`DNS = ${config.dns.join(', ')}`);
}
if (config.mtu) {
lines.push(`MTU = ${config.mtu}`);
}
for (const peer of config.peers) {
lines.push('');
lines.push('[Peer]');
lines.push(`PublicKey = ${peer.publicKey}`);
lines.push(`AllowedIPs = ${peer.allowedIPs.join(', ')}`);
if (peer.endpoint) {
lines.push(`Endpoint = ${peer.endpoint}`);
}
if (peer.persistentKeepalive) {
lines.push(`PersistentKeepalive = ${peer.persistentKeepalive}`);
}
}
return lines.join('\n');
}
这段函数把配置对象转换成 wg-quick 可识别的文本。实际项目中还需要反向解析:用户粘贴一份现成配置时,前端要能提取 Interface 和 Peer 字段。可以编写一个基于行解析的小型状态机,识别 [Interface] 和 [Peer] 段落,再把键值对映射到对象。解析器要处理好注释、空行和重复键。
三、借助 WASM 在浏览器中建立 WireGuard 隧道
如果要在 Vue 3 应用内真正运行 WireGuard,典型的路线是把 wireguard-go 编译为 WebAssembly。官方 wireguard-go 依赖系统 socket,直接编译到浏览器无法工作,但可以通过构建标签或修改网络栈,将数据收发替换为 WebTransport 或 WebRTC DataChannel。社区已有类似实验,也有商业产品使用这种方案。
在 Vue 3 中集成 WASM,可以使用动态 import 加载模块,并在 onMounted 时初始化。需要传递一个数据通道适配器,把浏览器的传输能力交给 Go 侧。示例代码如下。
// composables/useWireGuardWasm.ts
import { ref, onMounted } from 'vue';
export function useWireGuardWasm() {
const status = ref('idle');
onMounted(async () => {
try {
const wasmModule = await import('/wasm/wireguard.wasm');
const adapter = createWebTransportAdapter();
await wasmModule.initWireGuard({
config: currentConfig.value,
adapter
});
status.value = 'running';
} catch (err) {
status.value = 'error';
console.error('WASM init failed', err);
}
});
return { status };
}
注意 WASM 方案在纯浏览器页面中仍然受限:WebTransport 需要 HTTPS 且部分浏览器不支持,移动端支持更弱。因此更稳妥的工程化选择是采用 Electron 或浏览器扩展,利用 Node 环境调用系统 wg 命令,同时用 Vue 3 构建界面。这样既能复用前端组件,又不会受浏览器沙箱约束。
无论哪种方案,状态同步都是持续工作。可以通过 setInterval 轮询后端接口,或者使用 WebSocket 推送。每个 Peer 的 lastHandshake 和 transferRx/Tx 需要格式化为人类可读的时间与流量单位。这些可以封装成 composables 配合 Pinia 管理。
四、工程化封装与安全边界
在 Vue 3 项目中,建议把 WireGuard 相关逻辑集中到 Pinia store 中。store 维护配置对象、密钥对、peer 列表和连接状态。密钥对应该使用 reactive 或 ref 存储,但不要持久化到 localStorage。如果一定要缓存,应当使用 Web Crypto 的 AES-GCM 加密后存储到 IndexedDB,并把解密密钥放在内存中。
// stores/wireguard.ts
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
import { useWireGuardKeys } from '@/composables/useWireGuardKeys';
export const useWireGuardStore = defineStore('wireguard', () => {
const privateKey = ref('');
const publicKey = ref('');
const peers = ref<WireGuardPeer[]>([]);
const connectionStatus = ref<'idle' | 'connecting' | 'connected' | 'error'>('idle');
const { generateKeyPair } = useWireGuardKeys();
function generateKeys() {
const pair = generateKeyPair();
privateKey.value = pair.privateKey;
publicKey.value = pair.publicKey;
}
const hasPrivateKey = computed(() => privateKey.value.length > 0);
return { privateKey, publicKey, peers, connectionStatus, generateKeys, hasPrivateKey };
});
测试同样不可忽略。配置解析器和序列化函数是纯函数,适合用 Vitest 覆盖边界情况,例如 Peer 缺少 AllowedIPs、重复段落、注释行等。前端与 WireGuard 的边界应当通过类型定义固化,避免后端返回的字段名不一致导致运行时错误。
安全上,私钥绝不应该出现在日志、错误上报或 Vue Devtools 的持久化状态中。必要时可以使用 Proxy 包装,阻止序列化。端口扫描和未授权访问要由后端控制,前端只展示最小必要信息。总体而言,将 WireGuard 带进 Vue 3 工程,核心价值在于统一的配置管理和状态可视化,而不是完全替代系统级客户端。