fix(vpn,target-profiles): refresh VPN client security when target profiles change and include profile target IPs in direct destination allow-lists

This commit is contained in:
2026-04-06 07:51:25 +00:00
parent 0fa65f31c3
commit 6271bb1079
7 changed files with 67 additions and 7 deletions

View File

@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@serve.zone/dcrouter',
version: '13.0.6',
version: '13.0.7',
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
}

View File

@@ -2151,6 +2151,10 @@ export class DcRouter {
// Re-apply routes so profile-based ipAllowLists get updated
this.routeConfigManager?.applyRoutes();
},
getClientDirectTargets: (targetProfileIds: string[]) => {
if (!this.targetProfileManager) return [];
return this.targetProfileManager.getDirectTargetIps(targetProfileIds);
},
getClientAllowedIPs: async (targetProfileIds: string[]) => {
const subnet = this.options.vpnConfig?.subnet || '10.8.0.0/24';
const ips = new Set<string>([subnet]);

View File

@@ -134,6 +134,27 @@ export class TargetProfileManager {
.map((c) => ({ clientId: c.clientId, description: c.description }));
}
// =========================================================================
// Direct target IPs (bypass SmartProxy)
// =========================================================================
/**
* For a set of target profile IDs, collect all explicit target host IPs.
* These IPs bypass the SmartProxy forceTarget rewrite — VPN clients can
* connect to them directly through the tunnel.
*/
public getDirectTargetIps(targetProfileIds: string[]): string[] {
const ips = new Set<string>();
for (const profileId of targetProfileIds) {
const profile = this.profiles.get(profileId);
if (!profile?.targets?.length) continue;
for (const t of profile.targets) {
ips.add(t.host);
}
}
return [...ips];
}
// =========================================================================
// Core matching: route → client IPs
// =========================================================================

View File

@@ -110,8 +110,9 @@ export class TargetProfileHandler {
targets: dataArg.targets,
routeRefs: dataArg.routeRefs,
});
// Re-apply routes to update VPN access
// Re-apply routes and refresh VPN client security to update access
this.opsServerRef.dcRouterRef.routeConfigManager?.applyRoutes();
this.opsServerRef.dcRouterRef.vpnManager?.refreshAllClientSecurity();
return { success: true };
},
),
@@ -129,8 +130,9 @@ export class TargetProfileHandler {
}
const result = await manager.deleteProfile(dataArg.id, dataArg.force);
if (result.success) {
// Re-apply routes to update VPN access
// Re-apply routes and refresh VPN client security to update access
this.opsServerRef.dcRouterRef.routeConfigManager?.applyRoutes();
this.opsServerRef.dcRouterRef.vpnManager?.refreshAllClientSecurity();
}
return result;
},

View File

@@ -30,6 +30,9 @@ export interface IVpnManagerConfig {
* Called at config generation time (create/export). Returns CIDRs for WireGuard AllowedIPs.
* When not set, defaults to [subnet]. */
getClientAllowedIPs?: (targetProfileIds: string[]) => Promise<string[]>;
/** Resolve per-client destination allow-list IPs from target profile IDs.
* Returns IP strings that should bypass forceTarget and go direct to the real destination. */
getClientDirectTargets?: (targetProfileIds: string[]) => 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';
@@ -477,18 +480,28 @@ export class VpnManager {
const security: plugins.smartvpn.IClientSecurity = {};
const forceSmartproxy = client.forceDestinationSmartproxy ?? true;
// Collect direct targets from assigned TargetProfiles (bypass forceTarget for these IPs)
const profileDirectTargets = this.config.getClientDirectTargets?.(client.targetProfileIds || []) || [];
// Merge with per-client explicit allow list
const mergedAllowList = [
...(client.destinationAllowList || []),
...profileDirectTargets,
];
if (!forceSmartproxy) {
// Client traffic goes directly — not forced to SmartProxy
security.destinationPolicy = {
default: 'allow' as const,
blockList: client.destinationBlockList,
};
} else if (client.destinationAllowList?.length || client.destinationBlockList?.length) {
// Client is forced to SmartProxy, but with per-client allow/block overrides
} else if (mergedAllowList.length || client.destinationBlockList?.length) {
// Client is forced to SmartProxy, but with allow/block overrides
// (includes TargetProfile direct targets that bypass SmartProxy)
security.destinationPolicy = {
default: 'forceTarget' as const,
target: '127.0.0.1',
allowList: client.destinationAllowList,
allowList: mergedAllowList.length ? mergedAllowList : undefined,
blockList: client.destinationBlockList,
};
}
@@ -497,6 +510,20 @@ export class VpnManager {
return security;
}
/**
* Refresh all client security policies against the running daemon.
* Call this when TargetProfiles change so destination allow-lists stay in sync.
*/
public async refreshAllClientSecurity(): Promise<void> {
if (!this.vpnServer) return;
for (const client of this.clients.values()) {
const security = this.buildClientSecurity(client);
if (security.destinationPolicy) {
await this.vpnServer.updateClient(client.clientId, { security });
}
}
}
// ── Private helpers ────────────────────────────────────────────────────
private async loadOrGenerateServerKeys(): Promise<VpnServerKeysDoc> {