fix(routing): serialize route updates and correct VPN-gated route application

This commit is contained in:
2026-04-06 10:23:18 +00:00
parent 07cfe76cac
commit 68473f8550
15 changed files with 145 additions and 80 deletions

View File

@@ -431,7 +431,15 @@ export class DcRouter {
// failed silently (SmartProxy doesn't emit certificate-failed for this path).
// Calling updateRoutes() re-triggers provisionCertificatesViaCallback internally,
// which calls certProvisionFunction again — now with smartAcmeReady === true.
if (this.smartProxy) {
if (this.routeConfigManager) {
// Go through RouteConfigManager to get the full merged route set
// and serialize via the route-update mutex (prevents stale overwrites)
logger.log('info', 'Re-triggering certificate provisioning via RouteConfigManager');
this.routeConfigManager.applyRoutes().catch((err: any) => {
logger.log('warn', `Failed to re-trigger cert provisioning: ${err?.message || err}`);
});
} else if (this.smartProxy) {
// No RouteConfigManager (DB disabled) — re-send current routes to trigger cert provisioning
if (this.certProvisionScheduler) {
this.certProvisionScheduler.clear();
}
@@ -477,7 +485,8 @@ export class DcRouter {
this.options.vpnConfig?.enabled
? (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'];
// VPN not ready yet — deny all until re-apply after VPN starts
return [];
}
return this.targetProfileManager.getMatchingClientIps(
route, routeId, this.vpnManager.listClients(),
@@ -2149,7 +2158,10 @@ export class DcRouter {
bridgeIpRangeEnd: this.options.vpnConfig.bridgeIpRangeEnd,
onClientChanged: () => {
// Re-apply routes so profile-based ipAllowLists get updated
this.routeConfigManager?.applyRoutes();
// (serialized by RouteConfigManager's mutex — safe as fire-and-forget)
this.routeConfigManager?.applyRoutes().catch((err) => {
logger.log('warn', `Failed to re-apply routes after VPN client change: ${err?.message || err}`);
});
},
getClientDirectTargets: (targetProfileIds: string[]) => {
if (!this.targetProfileManager) return [];
@@ -2191,7 +2203,7 @@ export class DcRouter {
// Re-apply routes now that VPN clients are loaded — ensures hardcoded routes
// get correct profile-based ipAllowLists (not possible during setupSmartProxy since
// VPN server wasn't ready yet)
this.routeConfigManager?.applyRoutes();
await this.routeConfigManager?.applyRoutes();
}
/** Cache for DNS-resolved IPs of VPN-gated domains. TTL: 5 minutes. */
@@ -2209,6 +2221,11 @@ export class DcRouter {
const { promises: dnsPromises } = await import('dns');
const ips = await dnsPromises.resolve4(domain);
this.vpnDomainIpCache.set(domain, { ips, expiresAt: Date.now() + 5 * 60 * 1000 });
// Evict oldest entries if cache exceeds 1000 entries
if (this.vpnDomainIpCache.size > 1000) {
const firstKey = this.vpnDomainIpCache.keys().next().value;
if (firstKey) this.vpnDomainIpCache.delete(firstKey);
}
return ips;
} catch (err) {
logger.log('warn', `VPN: Failed to resolve ${domain} for AllowedIPs: ${(err as Error).message}`);