feat(vpn): add tag-based VPN route access control and support configured initial VPN clients

This commit is contained in:
2026-03-30 12:07:58 +00:00
parent 43618abeba
commit eb211348d2
14 changed files with 125 additions and 33 deletions

View File

@@ -16,6 +16,14 @@ export interface IVpnManagerConfig {
serverEndpoint?: string;
/** Override forwarding mode. Default: auto-detect (tun if root, socket otherwise) */
forwardingMode?: 'tun' | 'socket';
/** Pre-defined VPN clients created on startup (idempotent — skips already-persisted clients) */
initialClients?: Array<{
clientId: string;
serverDefinedClientTags?: string[];
description?: string;
}>;
/** Called when clients are created/deleted/toggled — triggers route re-application */
onClientChanged?: () => void;
}
interface IPersistedServerKeys {
@@ -28,7 +36,7 @@ interface IPersistedServerKeys {
interface IPersistedClient {
clientId: string;
enabled: boolean;
tags?: string[];
serverDefinedClientTags?: string[];
description?: string;
assignedIp?: string;
noisePublicKey: string;
@@ -36,6 +44,8 @@ interface IPersistedClient {
createdAt: number;
updatedAt: number;
expiresAt?: string;
/** @deprecated Legacy field — migrated to serverDefinedClientTags on load */
tags?: string[];
}
/**
@@ -92,7 +102,7 @@ export class VpnManager {
publicKey: client.noisePublicKey,
wgPublicKey: client.wgPublicKey,
enabled: client.enabled,
tags: client.tags,
serverDefinedClientTags: client.serverDefinedClientTags,
description: client.description,
assignedIp: client.assignedIp,
expiresAt: client.expiresAt,
@@ -122,6 +132,21 @@ export class VpnManager {
};
await this.vpnServer.start(serverConfig);
// Create initial clients from config (idempotent — skip already-persisted)
if (this.config.initialClients) {
for (const initial of this.config.initialClients) {
if (!this.clients.has(initial.clientId)) {
const bundle = await this.createClient({
clientId: initial.clientId,
serverDefinedClientTags: initial.serverDefinedClientTags,
description: initial.description,
});
logger.log('info', `VPN: Created initial client '${initial.clientId}' (IP: ${bundle.entry.assignedIp})`);
}
}
}
logger.log('info', `VPN server started: mode=${this._forwardingMode}, subnet=${subnet}, wg=:${wgListenPort}, clients=${this.clients.size}`);
}
@@ -148,7 +173,7 @@ export class VpnManager {
*/
public async createClient(opts: {
clientId: string;
tags?: string[];
serverDefinedClientTags?: string[];
description?: string;
}): Promise<plugins.smartvpn.IClientConfigBundle> {
if (!this.vpnServer) {
@@ -157,7 +182,7 @@ export class VpnManager {
const bundle = await this.vpnServer.createClient({
clientId: opts.clientId,
tags: opts.tags,
serverDefinedClientTags: opts.serverDefinedClientTags,
description: opts.description,
});
@@ -174,7 +199,7 @@ export class VpnManager {
const persisted: IPersistedClient = {
clientId: bundle.entry.clientId,
enabled: bundle.entry.enabled ?? true,
tags: bundle.entry.tags,
serverDefinedClientTags: bundle.entry.serverDefinedClientTags,
description: bundle.entry.description,
assignedIp: bundle.entry.assignedIp,
noisePublicKey: bundle.entry.publicKey,
@@ -186,6 +211,7 @@ export class VpnManager {
this.clients.set(persisted.clientId, persisted);
await this.persistClient(persisted);
this.config.onClientChanged?.();
return bundle;
}
@@ -199,6 +225,7 @@ export class VpnManager {
await this.vpnServer.removeClient(clientId);
this.clients.delete(clientId);
await this.storageManager.delete(`${STORAGE_PREFIX_CLIENTS}${clientId}`);
this.config.onClientChanged?.();
}
/**
@@ -220,6 +247,7 @@ export class VpnManager {
client.updatedAt = Date.now();
await this.persistClient(client);
}
this.config.onClientChanged?.();
}
/**
@@ -234,6 +262,7 @@ export class VpnManager {
client.updatedAt = Date.now();
await this.persistClient(client);
}
this.config.onClientChanged?.();
}
/**
@@ -283,6 +312,22 @@ export class VpnManager {
return config;
}
// ── Tag-based access control ───────────────────────────────────────────
/**
* Get assigned IPs for all enabled clients matching any of the given server-defined tags.
*/
public getClientIpsForServerDefinedTags(tags: string[]): string[] {
const ips: string[] = [];
for (const client of this.clients.values()) {
if (!client.enabled || !client.assignedIp) continue;
if (client.serverDefinedClientTags?.some(t => tags.includes(t))) {
ips.push(client.assignedIp);
}
}
return ips;
}
// ── Status and telemetry ───────────────────────────────────────────────
/**
@@ -364,6 +409,12 @@ export class VpnManager {
for (const key of keys) {
const client = await this.storageManager.getJSON<IPersistedClient>(key);
if (client) {
// Migrate legacy `tags` → `serverDefinedClientTags`
if (!client.serverDefinedClientTags && client.tags) {
client.serverDefinedClientTags = client.tags;
delete client.tags;
await this.persistClient(client);
}
this.clients.set(client.clientId, client);
}
}