fix(dns,routes): keep DoH socket-handler routes runtime-only and prune stale persisted entries
This commit is contained in:
@@ -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.'
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user