BREAKING CHANGE(vpn): replace tag-based VPN access control with source and target profiles

This commit is contained in:
2026-04-05 00:37:37 +00:00
parent 25365678e0
commit 1ddf83b28d
38 changed files with 1546 additions and 321 deletions

View File

@@ -14,7 +14,7 @@ export interface IVpnManagerConfig {
/** Pre-defined VPN clients created on startup (idempotent — skips already-persisted clients) */
initialClients?: Array<{
clientId: string;
serverDefinedClientTags?: string[];
targetProfileIds?: string[];
description?: string;
}>;
/** Called when clients are created/deleted/toggled — triggers route re-application */
@@ -26,10 +26,10 @@ export interface IVpnManagerConfig {
allowList?: string[];
blockList?: string[];
};
/** Compute per-client AllowedIPs based on the client's server-defined tags.
/** Compute per-client AllowedIPs based on the client's target profile IDs.
* Called at config generation time (create/export). Returns CIDRs for WireGuard AllowedIPs.
* When not set, defaults to [subnet]. */
getClientAllowedIPs?: (clientTags: string[]) => Promise<string[]>;
getClientAllowedIPs?: (targetProfileIds: string[]) => Promise<string[]>;
/** Forwarding mode: 'socket' (default, userspace NAT), 'bridge' (L2 bridge to host LAN),
* or 'hybrid' (socket default, bridge for clients with useHostIp=true) */
forwardingMode?: 'socket' | 'bridge' | 'hybrid';
@@ -90,7 +90,6 @@ export class VpnManager {
publicKey: client.noisePublicKey,
wgPublicKey: client.wgPublicKey,
enabled: client.enabled,
serverDefinedClientTags: client.serverDefinedClientTags,
description: client.description,
assignedIp: client.assignedIp,
expiresAt: client.expiresAt,
@@ -163,7 +162,7 @@ export class VpnManager {
if (!this.clients.has(initial.clientId)) {
const bundle = await this.createClient({
clientId: initial.clientId,
serverDefinedClientTags: initial.serverDefinedClientTags,
targetProfileIds: initial.targetProfileIds,
description: initial.description,
});
logger.log('info', `VPN: Created initial client '${initial.clientId}' (IP: ${bundle.entry.assignedIp})`);
@@ -197,7 +196,7 @@ export class VpnManager {
*/
public async createClient(opts: {
clientId: string;
serverDefinedClientTags?: string[];
targetProfileIds?: string[];
description?: string;
forceDestinationSmartproxy?: boolean;
destinationAllowList?: string[];
@@ -214,13 +213,12 @@ export class VpnManager {
const bundle = await this.vpnServer.createClient({
clientId: opts.clientId,
serverDefinedClientTags: opts.serverDefinedClientTags,
description: opts.description,
});
// Override AllowedIPs with per-client values based on tag-matched routes
// Override AllowedIPs with per-client values based on target profiles
if (this.config.getClientAllowedIPs && bundle.wireguardConfig) {
const allowedIPs = await this.config.getClientAllowedIPs(opts.serverDefinedClientTags || []);
const allowedIPs = await this.config.getClientAllowedIPs(opts.targetProfileIds || []);
bundle.wireguardConfig = bundle.wireguardConfig.replace(
/AllowedIPs\s*=\s*.+/,
`AllowedIPs = ${allowedIPs.join(', ')}`,
@@ -231,7 +229,7 @@ export class VpnManager {
const doc = new VpnClientDoc();
doc.clientId = bundle.entry.clientId;
doc.enabled = bundle.entry.enabled ?? true;
doc.serverDefinedClientTags = bundle.entry.serverDefinedClientTags;
doc.targetProfileIds = opts.targetProfileIds;
doc.description = bundle.entry.description;
doc.assignedIp = bundle.entry.assignedIp;
doc.noisePublicKey = bundle.entry.publicKey;
@@ -332,11 +330,11 @@ export class VpnManager {
}
/**
* Update a client's metadata (description, tags) without rotating keys.
* Update a client's metadata (description, target profiles) without rotating keys.
*/
public async updateClient(clientId: string, update: {
description?: string;
serverDefinedClientTags?: string[];
targetProfileIds?: string[];
forceDestinationSmartproxy?: boolean;
destinationAllowList?: string[];
destinationBlockList?: string[];
@@ -349,7 +347,7 @@ export class VpnManager {
const client = this.clients.get(clientId);
if (!client) throw new Error(`Client not found: ${clientId}`);
if (update.description !== undefined) client.description = update.description;
if (update.serverDefinedClientTags !== undefined) client.serverDefinedClientTags = update.serverDefinedClientTags;
if (update.targetProfileIds !== undefined) client.targetProfileIds = update.targetProfileIds;
if (update.forceDestinationSmartproxy !== undefined) client.forceDestinationSmartproxy = update.forceDestinationSmartproxy;
if (update.destinationAllowList !== undefined) client.destinationAllowList = update.destinationAllowList;
if (update.destinationBlockList !== undefined) client.destinationBlockList = update.destinationBlockList;
@@ -409,10 +407,10 @@ export class VpnManager {
);
}
// Override AllowedIPs with per-client values based on tag-matched routes
// Override AllowedIPs with per-client values based on target profiles
if (this.config.getClientAllowedIPs) {
const clientTags = persisted?.serverDefinedClientTags || [];
const allowedIPs = await this.config.getClientAllowedIPs(clientTags);
const profileIds = persisted?.targetProfileIds || [];
const allowedIPs = await this.config.getClientAllowedIPs(profileIds);
config = config.replace(
/AllowedIPs\s*=\s*.+/,
`AllowedIPs = ${allowedIPs.join(', ')}`,
@@ -423,22 +421,6 @@ 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 ───────────────────────────────────────────────
/**
@@ -548,12 +530,6 @@ export class VpnManager {
private async loadPersistedClients(): Promise<void> {
const docs = await VpnClientDoc.findAll();
for (const doc of docs) {
// Migrate legacy `tags` → `serverDefinedClientTags`
if (!doc.serverDefinedClientTags && (doc as any).tags) {
doc.serverDefinedClientTags = (doc as any).tags;
(doc as any).tags = undefined;
await doc.save();
}
this.clients.set(doc.clientId, doc);
}
if (this.clients.size > 0) {