fix(dns,routes): keep DoH socket-handler routes runtime-only and prune stale persisted entries

This commit is contained in:
2026-04-14 00:53:26 +00:00
parent 5aa07e81c7
commit 9bb5a8bcc1
9 changed files with 362 additions and 94 deletions

View File

@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@serve.zone/dcrouter',
version: '13.17.5',
version: '13.17.6',
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
}

View File

@@ -315,7 +315,8 @@ export class DcRouter {
// Seed routes assembled during setupSmartProxy, passed to RouteConfigManager for DB seeding
private seedConfigRoutes: plugins.smartproxy.IRouteConfig[] = [];
private seedEmailRoutes: plugins.smartproxy.IRouteConfig[] = [];
private seedDnsRoutes: plugins.smartproxy.IRouteConfig[] = [];
// Runtime-only DoH routes. These carry live socket handlers and must never be persisted.
private runtimeDnsRoutes: plugins.smartproxy.IRouteConfig[] = [];
// Environment access
private qenv = new plugins.qenv.Qenv('./', '.nogit/');
@@ -580,13 +581,13 @@ export class DcRouter {
this.tunnelManager.syncAllowedEdges();
}
},
() => this.runtimeDnsRoutes,
);
this.apiTokenManager = new ApiTokenManager();
await this.apiTokenManager.initialize();
await this.routeConfigManager.initialize(
this.seedConfigRoutes as import('../ts_interfaces/data/remoteingress.js').IDcRouterRouteConfig[],
this.seedEmailRoutes as import('../ts_interfaces/data/remoteingress.js').IDcRouterRouteConfig[],
this.seedDnsRoutes as import('../ts_interfaces/data/remoteingress.js').IDcRouterRouteConfig[],
);
await this.targetProfileManager.normalizeAllRouteRefs();
@@ -892,7 +893,7 @@ export class DcRouter {
this.smartProxy = undefined;
}
// Assemble seed routes from constructor config — these will be seeded into DB
// Assemble serializable seed routes from constructor config — these will be seeded into DB
// by RouteConfigManager.initialize() when the ConfigManagers service starts.
this.seedConfigRoutes = (this.options.smartProxyConfig?.routes || []) as plugins.smartproxy.IRouteConfig[];
logger.log('info', `Found ${this.seedConfigRoutes.length} routes in config`);
@@ -903,17 +904,17 @@ export class DcRouter {
logger.log('debug', 'Email routes generated', { routes: JSON.stringify(this.seedEmailRoutes) });
}
this.seedDnsRoutes = [];
this.runtimeDnsRoutes = [];
if (this.options.dnsNsDomains && this.options.dnsNsDomains.length > 0) {
this.seedDnsRoutes = this.generateDnsRoutes();
logger.log('debug', `DNS routes for nameservers ${this.options.dnsNsDomains.join(', ')}`, { routes: JSON.stringify(this.seedDnsRoutes) });
this.runtimeDnsRoutes = this.generateDnsRoutes();
logger.log('debug', `DNS routes for nameservers ${this.options.dnsNsDomains.join(', ')}`, { routes: JSON.stringify(this.runtimeDnsRoutes) });
}
// Combined routes for SmartProxy bootstrap (before DB routes are loaded)
let routes: plugins.smartproxy.IRouteConfig[] = [
...this.seedConfigRoutes,
...this.seedEmailRoutes,
...this.seedDnsRoutes,
...this.runtimeDnsRoutes,
];
// Build the ACME options for SmartProxy from the DB-backed AcmeConfigManager.
@@ -1463,7 +1464,6 @@ export class DcRouter {
await this.routeConfigManager.initialize(
this.seedConfigRoutes as import('../ts_interfaces/data/remoteingress.js').IDcRouterRouteConfig[],
this.seedEmailRoutes as import('../ts_interfaces/data/remoteingress.js').IDcRouterRouteConfig[],
this.seedDnsRoutes as import('../ts_interfaces/data/remoteingress.js').IDcRouterRouteConfig[],
);
}
@@ -2185,7 +2185,7 @@ export class DcRouter {
// Pass current bootstrap routes so the manager can derive edge ports initially.
// Once RouteConfigManager applies the full DB set, the onRoutesApplied callback
// will push the complete merged routes here.
const bootstrapRoutes = [...this.seedConfigRoutes, ...this.seedEmailRoutes, ...this.seedDnsRoutes];
const bootstrapRoutes = [...this.seedConfigRoutes, ...this.seedEmailRoutes, ...this.runtimeDnsRoutes];
this.remoteIngressManager.setRoutes(bootstrapRoutes as any[]);
// If ConfigManagers finished before us, re-apply routes

View File

@@ -55,6 +55,7 @@ export class RouteConfigManager {
private getVpnClientIpsForRoute?: (route: IDcRouterRouteConfig, routeId?: string) => TIpAllowEntry[],
private referenceResolver?: ReferenceResolver,
private onRoutesApplied?: (routes: plugins.smartproxy.IRouteConfig[]) => void,
private getRuntimeRoutes?: () => plugins.smartproxy.IRouteConfig[],
) {}
/** Expose routes map for reference resolution lookups. */
@@ -63,7 +64,8 @@ export class RouteConfigManager {
}
/**
* Load persisted routes, seed config/email/dns routes, compute warnings, apply to SmartProxy.
* Load persisted routes, seed serializable config/email/dns routes,
* compute warnings, and apply the combined DB-backed + runtime route set to SmartProxy.
*/
public async initialize(
configRoutes: IDcRouterRouteConfig[] = [],
@@ -284,23 +286,40 @@ export class RouteConfigManager {
private async loadRoutes(): Promise<void> {
const docs = await RouteDoc.findAll();
let prunedRuntimeRoutes = 0;
for (const doc of docs) {
if (doc.id) {
this.routes.set(doc.id, {
id: doc.id,
route: doc.route,
enabled: doc.enabled,
createdAt: doc.createdAt,
updatedAt: doc.updatedAt,
createdBy: doc.createdBy,
origin: doc.origin || 'api',
metadata: doc.metadata,
});
if (!doc.id) continue;
const storedRoute: IRoute = {
id: doc.id,
route: doc.route,
enabled: doc.enabled,
createdAt: doc.createdAt,
updatedAt: doc.updatedAt,
createdBy: doc.createdBy,
origin: doc.origin || 'api',
metadata: doc.metadata,
};
if (this.isPersistedRuntimeRoute(storedRoute)) {
await doc.delete();
prunedRuntimeRoutes++;
logger.log(
'warn',
`Removed persisted runtime-only route '${storedRoute.route.name || storedRoute.id}' (${storedRoute.id}) from RouteDoc`,
);
continue;
}
this.routes.set(doc.id, storedRoute);
}
if (this.routes.size > 0) {
logger.log('info', `Loaded ${this.routes.size} route(s) from database`);
}
if (prunedRuntimeRoutes > 0) {
logger.log('info', `Pruned ${prunedRuntimeRoutes} persisted runtime-only route(s) from RouteDoc`);
}
}
private async persistRoute(stored: IRoute): Promise<void> {
@@ -389,36 +408,18 @@ export class RouteConfigManager {
const enabledRoutes: plugins.smartproxy.IRouteConfig[] = [];
const http3Config = this.getHttp3Config?.();
const vpnCallback = this.getVpnClientIpsForRoute;
// Helper: inject VPN security into a vpnOnly route
const injectVpn = (route: plugins.smartproxy.IRouteConfig, routeId?: string): plugins.smartproxy.IRouteConfig => {
if (!vpnCallback) return route;
const dcRoute = route as IDcRouterRouteConfig;
if (!dcRoute.vpnOnly) return route;
const vpnEntries = vpnCallback(dcRoute, routeId);
const existingEntries = route.security?.ipAllowList || [];
return {
...route,
security: {
...route.security,
ipAllowList: [...existingEntries, ...vpnEntries],
},
};
};
// Add all enabled routes with HTTP/3 and VPN augmentation
for (const route of this.routes.values()) {
if (route.enabled) {
let r = route.route;
if (http3Config?.enabled !== false) {
r = augmentRouteWithHttp3(r, { enabled: true, ...http3Config });
}
enabledRoutes.push(injectVpn(r, route.id));
enabledRoutes.push(this.prepareRouteForApply(route.route, route.id));
}
}
const runtimeRoutes = this.getRuntimeRoutes?.() || [];
for (const route of runtimeRoutes) {
enabledRoutes.push(this.prepareRouteForApply(route));
}
await smartProxy.updateRoutes(enabledRoutes);
// Notify listeners (e.g. RemoteIngressManager) of the route set
@@ -429,4 +430,47 @@ export class RouteConfigManager {
logger.log('info', `Applied ${enabledRoutes.length} routes to SmartProxy (${this.routes.size} total)`);
});
}
private prepareRouteForApply(
route: plugins.smartproxy.IRouteConfig,
routeId?: string,
): plugins.smartproxy.IRouteConfig {
let preparedRoute = route;
const http3Config = this.getHttp3Config?.();
if (http3Config?.enabled !== false) {
preparedRoute = augmentRouteWithHttp3(preparedRoute, { enabled: true, ...http3Config });
}
return this.injectVpnSecurity(preparedRoute, routeId);
}
private injectVpnSecurity(
route: plugins.smartproxy.IRouteConfig,
routeId?: string,
): plugins.smartproxy.IRouteConfig {
const vpnCallback = this.getVpnClientIpsForRoute;
if (!vpnCallback) return route;
const dcRoute = route as IDcRouterRouteConfig;
if (!dcRoute.vpnOnly) return route;
const vpnEntries = vpnCallback(dcRoute, routeId);
const existingEntries = route.security?.ipAllowList || [];
return {
...route,
security: {
...route.security,
ipAllowList: [...existingEntries, ...vpnEntries],
},
};
}
private isPersistedRuntimeRoute(storedRoute: IRoute): boolean {
const routeName = storedRoute.route.name || '';
const actionType = storedRoute.route.action?.type;
return (routeName.startsWith('dns-over-https-') && actionType === 'socket-handler')
|| (storedRoute.origin === 'dns' && actionType === 'socket-handler');
}
}

View File

@@ -97,8 +97,8 @@ export class DnsManager {
if (hasLegacyConfig) {
logger.log(
'warn',
'DnsManager: DB has DomainDoc entries — ignoring legacy dnsScopes/dnsRecords/dnsNsDomains constructor config. ' +
'Manage DNS via the Domains UI instead.',
'DnsManager: DB has DomainDoc entries — ignoring legacy dnsScopes/dnsRecords constructor config. ' +
'dnsNsDomains is still required for nameserver and DoH bootstrap unless that moves into DB-backed config.',
);
}
return;