From 6271bb107994e57805240a406c73937c080d666c Mon Sep 17 00:00:00 2001 From: Juergen Kunz Date: Mon, 6 Apr 2026 07:51:25 +0000 Subject: [PATCH] fix(vpn,target-profiles): refresh VPN client security when target profiles change and include profile target IPs in direct destination allow-lists --- changelog.md | 6 ++++ ts/00_commitinfo_data.ts | 2 +- ts/classes.dcrouter.ts | 4 +++ ts/config/classes.target-profile-manager.ts | 21 ++++++++++++ .../handlers/target-profile.handler.ts | 6 ++-- ts/vpn/classes.vpn-manager.ts | 33 +++++++++++++++++-- ts_web/00_commitinfo_data.ts | 2 +- 7 files changed, 67 insertions(+), 7 deletions(-) diff --git a/changelog.md b/changelog.md index 584835c..b569039 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,11 @@ # Changelog +## 2026-04-06 - 13.0.7 - fix(vpn,target-profiles) +refresh VPN client security when target profiles change and include profile target IPs in direct destination allow-lists + +- Adds direct target IP resolution from target profiles so forced SmartProxy clients can bypass rewriting for explicit profile targets. +- Refreshes running VPN client security policies after target profile updates or deletions to keep destination access rules in sync. + ## 2026-04-05 - 13.0.6 - fix(certificates) resolve base-domain certificate lookups and route profile list inputs diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index dde9958..e528aad 100644 --- a/ts/00_commitinfo_data.ts +++ b/ts/00_commitinfo_data.ts @@ -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.' } diff --git a/ts/classes.dcrouter.ts b/ts/classes.dcrouter.ts index 30f9123..295d2ce 100644 --- a/ts/classes.dcrouter.ts +++ b/ts/classes.dcrouter.ts @@ -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([subnet]); diff --git a/ts/config/classes.target-profile-manager.ts b/ts/config/classes.target-profile-manager.ts index 06b16f3..3989afe 100644 --- a/ts/config/classes.target-profile-manager.ts +++ b/ts/config/classes.target-profile-manager.ts @@ -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(); + 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 // ========================================================================= diff --git a/ts/opsserver/handlers/target-profile.handler.ts b/ts/opsserver/handlers/target-profile.handler.ts index f0872f8..c1dce0b 100644 --- a/ts/opsserver/handlers/target-profile.handler.ts +++ b/ts/opsserver/handlers/target-profile.handler.ts @@ -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; }, diff --git a/ts/vpn/classes.vpn-manager.ts b/ts/vpn/classes.vpn-manager.ts index d112bae..7737d8c 100644 --- a/ts/vpn/classes.vpn-manager.ts +++ b/ts/vpn/classes.vpn-manager.ts @@ -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; + /** 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 { + 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 { diff --git a/ts_web/00_commitinfo_data.ts b/ts_web/00_commitinfo_data.ts index dde9958..e528aad 100644 --- a/ts_web/00_commitinfo_data.ts +++ b/ts_web/00_commitinfo_data.ts @@ -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.' }