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

@@ -21,7 +21,7 @@ import { MetricsManager } from './monitoring/index.js';
import { RadiusServer, type IRadiusServerConfig } from './radius/index.js';
import { RemoteIngressManager, TunnelManager } from './remoteingress/index.js';
import { VpnManager, type IVpnManagerConfig } from './vpn/index.js';
import { RouteConfigManager, ApiTokenManager, ReferenceResolver, DbSeeder } from './config/index.js';
import { RouteConfigManager, ApiTokenManager, ReferenceResolver, DbSeeder, TargetProfileManager } from './config/index.js';
import { SecurityLogger, ContentScanner, IPReputationChecker } from './security/index.js';
import { type IHttp3Config, augmentRoutesWithHttp3 } from './http3/index.js';
@@ -180,8 +180,8 @@ export interface IDcRouterOptions {
/**
* VPN server configuration.
* Enables VPN-based access control: routes with vpn.enabled are only
* accessible from VPN clients. Supports WireGuard + native (WS/QUIC) transports.
* Enables VPN-based access control: routes with vpnOnly are only
* accessible from VPN clients whose TargetProfile matches. Supports WireGuard + native (WS/QUIC) transports.
*/
vpnConfig?: {
/** Enable VPN server (default: false) */
@@ -197,7 +197,7 @@ export interface IDcRouterOptions {
/** Pre-defined VPN clients created on startup */
clients?: Array<{
clientId: string;
serverDefinedClientTags?: string[];
targetProfileIds?: string[];
description?: string;
}>;
/** Destination routing policy for VPN client traffic.
@@ -274,6 +274,7 @@ export class DcRouter {
public routeConfigManager?: RouteConfigManager;
public apiTokenManager?: ApiTokenManager;
public referenceResolver?: ReferenceResolver;
public targetProfileManager?: TargetProfileManager;
// Auto-discovered public IP (populated by generateAuthoritativeRecords)
public detectedPublicIp: string | null = null;
@@ -465,16 +466,22 @@ export class DcRouter {
this.referenceResolver = new ReferenceResolver();
await this.referenceResolver.initialize();
// Initialize target profile manager
this.targetProfileManager = new TargetProfileManager();
await this.targetProfileManager.initialize();
this.routeConfigManager = new RouteConfigManager(
() => this.getConstructorRoutes(),
() => this.smartProxy,
() => this.options.http3,
this.options.vpnConfig?.enabled
? (tags?: string[]) => {
if (tags?.length && this.vpnManager) {
return this.vpnManager.getClientIpsForServerDefinedTags(tags);
? (route: import('../ts_interfaces/data/remoteingress.js').IDcRouterRouteConfig, routeId?: string) => {
if (!this.vpnManager || !this.targetProfileManager) {
return [this.options.vpnConfig?.subnet || '10.8.0.0/24'];
}
return [this.options.vpnConfig?.subnet || '10.8.0.0/24'];
return this.targetProfileManager.getMatchingClientIps(
route, routeId, this.vpnManager.listClients(),
);
}
: undefined,
this.referenceResolver,
@@ -504,6 +511,7 @@ export class DcRouter {
this.routeConfigManager = undefined;
this.apiTokenManager = undefined;
this.referenceResolver = undefined;
this.targetProfileManager = undefined;
})
.withRetry({ maxRetries: 2, baseDelayMs: 1000 }),
);
@@ -2137,56 +2145,31 @@ export class DcRouter {
bridgeIpRangeStart: this.options.vpnConfig.bridgeIpRangeStart,
bridgeIpRangeEnd: this.options.vpnConfig.bridgeIpRangeEnd,
onClientChanged: () => {
// Re-apply routes so tag-based ipAllowLists get updated
// Re-apply routes so profile-based ipAllowLists get updated
this.routeConfigManager?.applyRoutes();
},
getClientAllowedIPs: async (clientTags: string[]) => {
getClientAllowedIPs: async (targetProfileIds: string[]) => {
const subnet = this.options.vpnConfig?.subnet || '10.8.0.0/24';
const ips = new Set<string>([subnet]);
// Check routes for VPN-gated tag match and collect domains
const routes = this.options.smartProxyConfig?.routes || [];
const domainsToResolve = new Set<string>();
for (const route of routes) {
const dcRoute = route as import('../ts_interfaces/data/remoteingress.js').IDcRouterRouteConfig;
if (!dcRoute.vpn?.enabled) continue;
if (!this.targetProfileManager) return [...ips];
const routeTags = dcRoute.vpn.allowedServerDefinedClientTags;
if (!routeTags?.length || clientTags.some(t => routeTags.includes(t))) {
// Collect domains from this route
const domains = (route.match as any)?.domains;
if (Array.isArray(domains)) {
for (const d of domains) {
// Strip wildcard prefix for DNS resolution (*.example.com → example.com)
domainsToResolve.add(d.replace(/^\*\./, ''));
}
}
}
}
const routes = (this.options.smartProxyConfig?.routes || []) as import('../ts_interfaces/data/remoteingress.js').IDcRouterRouteConfig[];
const storedRoutes = this.routeConfigManager?.getStoredRoutes() || new Map();
// Also scan stored/programmatic routes
const storedRoutes = this.routeConfigManager?.getStoredRoutes();
if (storedRoutes) {
for (const [, stored] of storedRoutes) {
if (!stored.enabled) continue;
const dcRoute = stored.route as import('../ts_interfaces/data/remoteingress.js').IDcRouterRouteConfig;
if (!dcRoute.vpn?.enabled) continue;
const { domains, targetIps } = this.targetProfileManager.getClientAccessSpec(
targetProfileIds, routes, storedRoutes,
);
const routeTags = dcRoute.vpn.allowedServerDefinedClientTags;
if (!routeTags?.length || clientTags.some(t => routeTags.includes(t))) {
const domains = (stored.route.match as any)?.domains;
if (Array.isArray(domains)) {
for (const d of domains) {
domainsToResolve.add(d.replace(/^\*\./, ''));
}
}
}
}
// Add target IPs directly
for (const ip of targetIps) {
ips.add(`${ip}/32`);
}
// Resolve DNS A records for matched domains (with caching)
for (const domain of domainsToResolve) {
const resolvedIps = await this.resolveVpnDomainIPs(domain);
for (const domain of domains) {
const stripped = domain.replace(/^\*\./, '');
const resolvedIps = await this.resolveVpnDomainIPs(stripped);
for (const ip of resolvedIps) {
ips.add(`${ip}/32`);
}
@@ -2199,7 +2182,7 @@ export class DcRouter {
await this.vpnManager.start();
// Re-apply routes now that VPN clients are loaded — ensures hardcoded routes
// get correct tag-based ipAllowLists (not possible during setupSmartProxy since
// get correct profile-based ipAllowLists (not possible during setupSmartProxy since
// VPN server wasn't ready yet)
this.routeConfigManager?.applyRoutes();
}