From bad0bd9053587ad62e65701b907f59866f3c8549 Mon Sep 17 00:00:00 2001 From: Juergen Kunz Date: Tue, 31 Mar 2026 01:10:19 +0000 Subject: [PATCH] fix(vpn): resolve VPN-gated route domains into per-client AllowedIPs with cached DNS lookups --- changelog.md | 7 +++++ ts/00_commitinfo_data.ts | 2 +- ts/classes.dcrouter.ts | 57 ++++++++++++++++++++++++----------- ts/vpn/classes.vpn-manager.ts | 6 ++-- ts_web/00_commitinfo_data.ts | 2 +- 5 files changed, 52 insertions(+), 22 deletions(-) diff --git a/changelog.md b/changelog.md index dcf4db5..16c910d 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,12 @@ # Changelog +## 2026-03-31 - 11.21.1 - fix(vpn) +resolve VPN-gated route domains into per-client AllowedIPs with cached DNS lookups + +- Derive WireGuard AllowedIPs from DNS A records of matched vpn.required route domains instead of only configured public proxy IPs. +- Cache resolved domain IPs for 5 minutes and fall back to stale results on DNS lookup failures. +- Make per-client AllowedIPs generation asynchronous throughout VPN config export and regeneration flows. + ## 2026-03-31 - 11.21.0 - feat(vpn) add tag-aware WireGuard AllowedIPs for VPN-gated routes diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index 723193a..3add3f6 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: '11.21.0', + version: '11.21.1', description: 'A multifaceted routing service handling mail and SMS delivery functions.' } diff --git a/ts/classes.dcrouter.ts b/ts/classes.dcrouter.ts index 5123a76..6a7c043 100644 --- a/ts/classes.dcrouter.ts +++ b/ts/classes.dcrouter.ts @@ -2105,34 +2105,35 @@ export class DcRouter { // Re-apply routes so tag-based ipAllowLists get updated this.routeConfigManager?.applyRoutes(); }, - getClientAllowedIPs: (clientTags: string[]) => { + getClientAllowedIPs: async (clientTags: string[]) => { const subnet = this.options.vpnConfig?.subnet || '10.8.0.0/24'; const ips = new Set([subnet]); - // Determine the server's public-facing IP(s) that VPN-gated domains resolve to - const publicIPs: string[] = []; - if (this.options.proxyIps?.length) { - publicIPs.push(...this.options.proxyIps); - } - if (this.options.publicIp) { - publicIPs.push(this.options.publicIp); - } else if (this.detectedPublicIp) { - publicIPs.push(this.detectedPublicIp); - } - if (!publicIPs.length) return [...ips]; - - // Check routes for VPN-gated tag match + // Check routes for VPN-gated tag match and collect domains const routes = this.options.smartProxyConfig?.routes || []; + const domainsToResolve = new Set(); for (const route of routes) { const dcRoute = route as import('../ts_interfaces/data/remoteingress.js').IDcRouterRouteConfig; if (!dcRoute.vpn?.required) continue; const routeTags = dcRoute.vpn.allowedServerDefinedClientTags; if (!routeTags?.length || clientTags.some(t => routeTags.includes(t))) { - for (const ip of publicIPs) { - ips.add(`${ip}/32`); + // 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(/^\*\./, '')); + } } - break; // All routes resolve to the same server IPs + } + } + + // Resolve DNS A records for matched domains (with caching) + for (const domain of domainsToResolve) { + const resolvedIps = await this.resolveVpnDomainIPs(domain); + for (const ip of resolvedIps) { + ips.add(`${ip}/32`); } } @@ -2143,6 +2144,28 @@ export class DcRouter { await this.vpnManager.start(); } + /** Cache for DNS-resolved IPs of VPN-gated domains. TTL: 5 minutes. */ + private vpnDomainIpCache = new Map(); + + /** + * Resolve a domain's A record(s) for VPN AllowedIPs, with a 5-minute cache. + */ + private async resolveVpnDomainIPs(domain: string): Promise { + const cached = this.vpnDomainIpCache.get(domain); + if (cached && cached.expiresAt > Date.now()) { + return cached.ips; + } + try { + const { promises: dnsPromises } = await import('dns'); + const ips = await dnsPromises.resolve4(domain); + this.vpnDomainIpCache.set(domain, { ips, expiresAt: Date.now() + 5 * 60 * 1000 }); + return ips; + } catch (err) { + logger.log('warn', `VPN: Failed to resolve ${domain} for AllowedIPs: ${(err as Error).message}`); + return cached?.ips || []; // Return stale cache on failure, or empty + } + } + /** * Inject VPN security into routes that have vpn.required === true. * Adds the VPN subnet to security.ipAllowList so only VPN clients can access them. diff --git a/ts/vpn/classes.vpn-manager.ts b/ts/vpn/classes.vpn-manager.ts index 2864f02..f30bf66 100644 --- a/ts/vpn/classes.vpn-manager.ts +++ b/ts/vpn/classes.vpn-manager.ts @@ -32,7 +32,7 @@ export interface IVpnManagerConfig { /** Compute per-client AllowedIPs based on the client's server-defined tags. * Called at config generation time (create/export). Returns CIDRs for WireGuard AllowedIPs. * When not set, defaults to [subnet]. */ - getClientAllowedIPs?: (clientTags: string[]) => string[]; + getClientAllowedIPs?: (clientTags: string[]) => Promise; } interface IPersistedServerKeys { @@ -196,7 +196,7 @@ export class VpnManager { // Override AllowedIPs with per-client values based on tag-matched routes if (this.config.getClientAllowedIPs && bundle.wireguardConfig) { - const allowedIPs = this.config.getClientAllowedIPs(opts.serverDefinedClientTags || []); + const allowedIPs = await this.config.getClientAllowedIPs(opts.serverDefinedClientTags || []); bundle.wireguardConfig = bundle.wireguardConfig.replace( /AllowedIPs\s*=\s*.+/, `AllowedIPs = ${allowedIPs.join(', ')}`, @@ -317,7 +317,7 @@ export class VpnManager { // Override AllowedIPs with per-client values based on tag-matched routes if (this.config.getClientAllowedIPs) { const clientTags = persisted?.serverDefinedClientTags || []; - const allowedIPs = this.config.getClientAllowedIPs(clientTags); + const allowedIPs = await this.config.getClientAllowedIPs(clientTags); config = config.replace( /AllowedIPs\s*=\s*.+/, `AllowedIPs = ${allowedIPs.join(', ')}`, diff --git a/ts_web/00_commitinfo_data.ts b/ts_web/00_commitinfo_data.ts index 723193a..3add3f6 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: '11.21.0', + version: '11.21.1', description: 'A multifaceted routing service handling mail and SMS delivery functions.' }