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:
@@ -1,5 +1,11 @@
|
|||||||
# Changelog
|
# 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)
|
## 2026-04-05 - 13.0.6 - fix(certificates)
|
||||||
resolve base-domain certificate lookups and route profile list inputs
|
resolve base-domain certificate lookups and route profile list inputs
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@serve.zone/dcrouter',
|
name: '@serve.zone/dcrouter',
|
||||||
version: '13.0.6',
|
version: '13.0.7',
|
||||||
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2151,6 +2151,10 @@ export class DcRouter {
|
|||||||
// Re-apply routes so profile-based ipAllowLists get updated
|
// Re-apply routes so profile-based ipAllowLists get updated
|
||||||
this.routeConfigManager?.applyRoutes();
|
this.routeConfigManager?.applyRoutes();
|
||||||
},
|
},
|
||||||
|
getClientDirectTargets: (targetProfileIds: string[]) => {
|
||||||
|
if (!this.targetProfileManager) return [];
|
||||||
|
return this.targetProfileManager.getDirectTargetIps(targetProfileIds);
|
||||||
|
},
|
||||||
getClientAllowedIPs: async (targetProfileIds: string[]) => {
|
getClientAllowedIPs: async (targetProfileIds: string[]) => {
|
||||||
const subnet = this.options.vpnConfig?.subnet || '10.8.0.0/24';
|
const subnet = this.options.vpnConfig?.subnet || '10.8.0.0/24';
|
||||||
const ips = new Set<string>([subnet]);
|
const ips = new Set<string>([subnet]);
|
||||||
|
|||||||
@@ -134,6 +134,27 @@ export class TargetProfileManager {
|
|||||||
.map((c) => ({ clientId: c.clientId, description: c.description }));
|
.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
|
// Core matching: route → client IPs
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|||||||
@@ -110,8 +110,9 @@ export class TargetProfileHandler {
|
|||||||
targets: dataArg.targets,
|
targets: dataArg.targets,
|
||||||
routeRefs: dataArg.routeRefs,
|
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.routeConfigManager?.applyRoutes();
|
||||||
|
this.opsServerRef.dcRouterRef.vpnManager?.refreshAllClientSecurity();
|
||||||
return { success: true };
|
return { success: true };
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -129,8 +130,9 @@ export class TargetProfileHandler {
|
|||||||
}
|
}
|
||||||
const result = await manager.deleteProfile(dataArg.id, dataArg.force);
|
const result = await manager.deleteProfile(dataArg.id, dataArg.force);
|
||||||
if (result.success) {
|
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.routeConfigManager?.applyRoutes();
|
||||||
|
this.opsServerRef.dcRouterRef.vpnManager?.refreshAllClientSecurity();
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -30,6 +30,9 @@ export interface IVpnManagerConfig {
|
|||||||
* Called at config generation time (create/export). Returns CIDRs for WireGuard AllowedIPs.
|
* Called at config generation time (create/export). Returns CIDRs for WireGuard AllowedIPs.
|
||||||
* When not set, defaults to [subnet]. */
|
* When not set, defaults to [subnet]. */
|
||||||
getClientAllowedIPs?: (targetProfileIds: string[]) => Promise<string[]>;
|
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),
|
/** Forwarding mode: 'socket' (default, userspace NAT), 'bridge' (L2 bridge to host LAN),
|
||||||
* or 'hybrid' (socket default, bridge for clients with useHostIp=true) */
|
* or 'hybrid' (socket default, bridge for clients with useHostIp=true) */
|
||||||
forwardingMode?: 'socket' | 'bridge' | 'hybrid';
|
forwardingMode?: 'socket' | 'bridge' | 'hybrid';
|
||||||
@@ -477,18 +480,28 @@ export class VpnManager {
|
|||||||
const security: plugins.smartvpn.IClientSecurity = {};
|
const security: plugins.smartvpn.IClientSecurity = {};
|
||||||
const forceSmartproxy = client.forceDestinationSmartproxy ?? true;
|
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) {
|
if (!forceSmartproxy) {
|
||||||
// Client traffic goes directly — not forced to SmartProxy
|
// Client traffic goes directly — not forced to SmartProxy
|
||||||
security.destinationPolicy = {
|
security.destinationPolicy = {
|
||||||
default: 'allow' as const,
|
default: 'allow' as const,
|
||||||
blockList: client.destinationBlockList,
|
blockList: client.destinationBlockList,
|
||||||
};
|
};
|
||||||
} else if (client.destinationAllowList?.length || client.destinationBlockList?.length) {
|
} else if (mergedAllowList.length || client.destinationBlockList?.length) {
|
||||||
// Client is forced to SmartProxy, but with per-client allow/block overrides
|
// Client is forced to SmartProxy, but with allow/block overrides
|
||||||
|
// (includes TargetProfile direct targets that bypass SmartProxy)
|
||||||
security.destinationPolicy = {
|
security.destinationPolicy = {
|
||||||
default: 'forceTarget' as const,
|
default: 'forceTarget' as const,
|
||||||
target: '127.0.0.1',
|
target: '127.0.0.1',
|
||||||
allowList: client.destinationAllowList,
|
allowList: mergedAllowList.length ? mergedAllowList : undefined,
|
||||||
blockList: client.destinationBlockList,
|
blockList: client.destinationBlockList,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -497,6 +510,20 @@ export class VpnManager {
|
|||||||
return security;
|
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 helpers ────────────────────────────────────────────────────
|
||||||
|
|
||||||
private async loadOrGenerateServerKeys(): Promise<VpnServerKeysDoc> {
|
private async loadOrGenerateServerKeys(): Promise<VpnServerKeysDoc> {
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@serve.zone/dcrouter',
|
name: '@serve.zone/dcrouter',
|
||||||
version: '13.0.6',
|
version: '13.0.7',
|
||||||
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user