From 9bb5a8bcc15d8bfb0b4e7d7b02cc68fcdec1cfd3 Mon Sep 17 00:00:00 2001 From: Juergen Kunz Date: Tue, 14 Apr 2026 00:53:26 +0000 Subject: [PATCH] fix(dns,routes): keep DoH socket-handler routes runtime-only and prune stale persisted entries --- changelog.md | 9 + package.json | 8 +- pnpm-lock.yaml | 67 +++---- test/test.dns-runtime-routes.node.ts | 230 ++++++++++++++++++++++ ts/00_commitinfo_data.ts | 2 +- ts/classes.dcrouter.ts | 18 +- ts/config/classes.route-config-manager.ts | 116 +++++++---- ts/dns/manager.dns.ts | 4 +- ts_web/00_commitinfo_data.ts | 2 +- 9 files changed, 362 insertions(+), 94 deletions(-) create mode 100644 test/test.dns-runtime-routes.node.ts diff --git a/changelog.md b/changelog.md index 36e93b2..f58805b 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,14 @@ # Changelog +## 2026-04-14 - 13.17.6 - fix(dns,routes) +keep DoH socket-handler routes runtime-only and prune stale persisted entries + +- stops persisting generated DNS-over-HTTPS routes that depend on live socket handlers +- removes stale persisted runtime-only DoH routes from RouteDoc during startup +- applies runtime DNS routes alongside DB-backed routes through RouteConfigManager +- updates DnsManager warning to clarify that dnsNsDomains is still required for nameserver and DoH bootstrap +- adds tests covering runtime DoH route application, stale route pruning, and updated DNS warning behavior + ## 2026-04-13 - 13.17.5 - fix(vpn,target-profiles) normalize target profile route references and stabilize VPN host-IP client routing behavior diff --git a/package.json b/package.json index 5907c21..68a7c50 100644 --- a/package.json +++ b/package.json @@ -51,10 +51,10 @@ "@push.rocks/smartmetrics": "^3.0.3", "@push.rocks/smartmigration": "1.2.0", "@push.rocks/smartmta": "^5.3.1", - "@push.rocks/smartnetwork": "^4.5.2", + "@push.rocks/smartnetwork": "^4.6.0", "@push.rocks/smartpath": "^6.0.0", "@push.rocks/smartpromise": "^4.2.3", - "@push.rocks/smartproxy": "^27.6.0", + "@push.rocks/smartproxy": "^27.7.0", "@push.rocks/smartradius": "^1.1.1", "@push.rocks/smartrequest": "^5.0.1", "@push.rocks/smartrx": "^3.0.10", @@ -62,12 +62,12 @@ "@push.rocks/smartunique": "^3.0.9", "@push.rocks/smartvpn": "1.19.2", "@push.rocks/taskbuffer": "^8.0.2", - "@serve.zone/catalog": "^2.12.3", + "@serve.zone/catalog": "^2.12.4", "@serve.zone/interfaces": "^5.3.0", "@serve.zone/remoteingress": "^4.15.3", "@tsclass/tsclass": "^9.5.0", "@types/qrcode": "^1.5.6", - "lru-cache": "^11.3.3", + "lru-cache": "^11.3.5", "qrcode": "^1.5.4", "uuid": "^13.0.0" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2592c25..2add49d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -72,8 +72,8 @@ importers: specifier: ^5.3.1 version: 5.3.1 '@push.rocks/smartnetwork': - specifier: ^4.5.2 - version: 4.5.2 + specifier: ^4.6.0 + version: 4.6.0 '@push.rocks/smartpath': specifier: ^6.0.0 version: 6.0.0 @@ -81,8 +81,8 @@ importers: specifier: ^4.2.3 version: 4.2.3 '@push.rocks/smartproxy': - specifier: ^27.6.0 - version: 27.6.0 + specifier: ^27.7.0 + version: 27.7.0 '@push.rocks/smartradius': specifier: ^1.1.1 version: 1.1.1 @@ -105,8 +105,8 @@ importers: specifier: ^8.0.2 version: 8.0.2 '@serve.zone/catalog': - specifier: ^2.12.3 - version: 2.12.3(@tiptap/pm@2.27.2) + specifier: ^2.12.4 + version: 2.12.4(@tiptap/pm@2.27.2) '@serve.zone/interfaces': specifier: ^5.3.0 version: 5.3.0 @@ -120,8 +120,8 @@ importers: specifier: ^1.5.6 version: 1.5.6 lru-cache: - specifier: ^11.3.3 - version: 11.3.3 + specifier: ^11.3.5 + version: 11.3.5 qrcode: specifier: ^1.5.4 version: 1.5.4 @@ -365,9 +365,6 @@ packages: '@design.estate/dees-element@2.2.4': resolution: {integrity: sha512-O9cA6flBMMd+pBwMQrZXwAWel9yVxgokolb+Em6gvkXxPJ0P/B5UDn4Vc2d4ts3ta55PTBm+l2dPeDVGx/bl7Q==} - '@design.estate/dees-wcctools@3.8.0': - resolution: {integrity: sha512-CC14iVKUrguzD9jIrdPBd9fZ4egVJEZMxl5y8iy0l7WLumeoYvGsoXj5INVkRPLRVLqziIdi4Je1hXqHt2NU+g==} - '@design.estate/dees-wcctools@3.9.0': resolution: {integrity: sha512-0vZBaGBEGIbl8hx+8BezIIea3U5T7iSHHF9VqlJZGf+nOFIW4zBAxcCljH8YzZ1Yayp6BEjxp/pQXjHN2YB3Jg==} @@ -1260,8 +1257,8 @@ packages: '@push.rocks/smartmustache@3.0.2': resolution: {integrity: sha512-G3LyRXoJhyM+iQhkvP/MR/2WYMvC9U7zc2J44JxUM5tPdkQ+o3++FbfRtnZj6rz5X/A7q03//vsxPitVQwoi2Q==} - '@push.rocks/smartnetwork@4.5.2': - resolution: {integrity: sha512-lbMMyc2f/WWd5+qzZyF1ynXndjCtasxPWmj/d8GUuis9rDrW7sLIT1PlAPC2F6Qsy4H/K32JrYU+01d/6sWObg==} + '@push.rocks/smartnetwork@4.6.0': + resolution: {integrity: sha512-ubaO/Qp8r30A+qwk33M/0+nQi+o8gNHEI9zq3jv1MwqiLxhiV1hnbr4CL9AUcvs4EhwUBiw0EswKjCJROwDqvQ==} '@push.rocks/smartnftables@1.1.0': resolution: {integrity: sha512-7JNzerlW20HEl2wKMBIHltwneCQRpXiD2lJkXZZc02ctnfjgFejXVDIeWomhPx6PZ0Z6zmqdF6rrFDtDHyqqfA==} @@ -1287,8 +1284,8 @@ packages: '@push.rocks/smartpromise@4.2.3': resolution: {integrity: sha512-Ycg/TJR+tMt+S3wSFurOpEoW6nXv12QBtKXgBcjMZ4RsdO28geN46U09osPn9N9WuwQy1PkmTV5J/V4F9U8qEw==} - '@push.rocks/smartproxy@27.6.0': - resolution: {integrity: sha512-1mPzabUKhlC0EdeI7Hjee/aiptTsOLftbq8oWBTlIg9JhCQwkIs5UNGTJV/VvlEflJKnay8TbzLzlr95gUr/1w==} + '@push.rocks/smartproxy@27.7.0': + resolution: {integrity: sha512-0u8HF5ocQ2xmfCN1FWyulGTddZ4ZkWaip1j0alT8Bc/LdIYerjKtNJCU4N2wMk/Zz0Wl5UQOmBm4qIWmgRiEcg==} '@push.rocks/smartpuppeteer@2.0.5': resolution: {integrity: sha512-yK/qSeWVHIGWRp3c8S5tfdGP6WCKllZC4DR8d8CQlEjszOSBmHtlTdyyqOMBZ/BA4kd+eU5f3A1r4K2tGYty1g==} @@ -1591,8 +1588,8 @@ packages: '@selderee/plugin-htmlparser2@0.11.0': resolution: {integrity: sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==} - '@serve.zone/catalog@2.12.3': - resolution: {integrity: sha512-/QLFjFcy/ig6cdr4517smSc/VCutW/qF/8lCM3v7tpQ5yLApjqiL314Dyvk9zzSwHpw69IeuM9EmPOeTuCY0iQ==} + '@serve.zone/catalog@2.12.4': + resolution: {integrity: sha512-GRfJZ0yQxChUy7Gp4mxhuN5y4GXZMOEk0W7rJiyZbezA938q+pFTplb9ahSaEHjiUht1MmTu/5WtoJFwgAP8SQ==} '@serve.zone/interfaces@5.3.0': resolution: {integrity: sha512-venO7wtDR9ixzD9NhdERBGjNKbFA5LL0yHw4eqGh0UpmvtXVc3SFG0uuHDilOKMZqZ8bttV88qVsFy1aSTJrtA==} @@ -3085,8 +3082,8 @@ packages: resolution: {integrity: sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - lru-cache@11.3.3: - resolution: {integrity: sha512-JvNw9Y81y33E+BEYPr0U7omo+U9AySnsMsEiXgwT6yqd31VQWTLNQqmT4ou5eqPFUrTfIDFta2wKhB1hyohtAQ==} + lru-cache@11.3.5: + resolution: {integrity: sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw==} engines: {node: 20 || >=22} lru-cache@7.18.3: @@ -4937,18 +4934,6 @@ snapshots: - supports-color - vue - '@design.estate/dees-wcctools@3.8.0': - dependencies: - '@design.estate/dees-domtools': 2.5.4 - '@design.estate/dees-element': 2.2.4 - '@push.rocks/smartdelay': 3.0.5 - lit: 3.3.2 - transitivePeerDependencies: - - '@nuxt/kit' - - react - - supports-color - - vue - '@design.estate/dees-wcctools@3.9.0': dependencies: '@design.estate/dees-domtools': 2.5.4 @@ -5169,7 +5154,7 @@ snapshots: '@push.rocks/smartjson': 6.0.0 '@push.rocks/smartlog': 3.2.2 '@push.rocks/smartmongo': 5.1.1(socks@2.8.7) - '@push.rocks/smartnetwork': 4.5.2 + '@push.rocks/smartnetwork': 4.6.0 '@push.rocks/smartpath': 6.0.0 '@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartrequest': 5.0.1 @@ -5972,7 +5957,7 @@ snapshots: '@push.rocks/smartdelay': 3.0.5 '@push.rocks/smartdns': 7.9.0 '@push.rocks/smartlog': 3.2.2 - '@push.rocks/smartnetwork': 4.5.2 + '@push.rocks/smartnetwork': 4.6.0 '@push.rocks/smartstring': 4.1.0 '@push.rocks/smarttime': 4.2.3 '@push.rocks/smartunique': 3.0.9 @@ -6429,7 +6414,7 @@ snapshots: '@push.rocks/smartpath': 6.0.0 '@push.rocks/smartrust': 1.3.2 '@tsclass/tsclass': 9.5.0 - lru-cache: 11.3.3 + lru-cache: 11.3.5 mailparser: 3.9.6 uuid: 13.0.0 transitivePeerDependencies: @@ -6439,7 +6424,7 @@ snapshots: dependencies: handlebars: 4.7.9 - '@push.rocks/smartnetwork@4.5.2': + '@push.rocks/smartnetwork@4.6.0': dependencies: '@push.rocks/smartdns': 7.9.0 '@push.rocks/smartrust': 1.3.2 @@ -6500,7 +6485,7 @@ snapshots: '@push.rocks/smartdelay': 3.0.5 '@push.rocks/smartfs': 1.5.0 '@push.rocks/smartjimp': 1.2.0 - '@push.rocks/smartnetwork': 4.5.2 + '@push.rocks/smartnetwork': 4.6.0 '@push.rocks/smartpath': 6.0.0 '@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartpuppeteer': 2.0.5(typescript@6.0.2) @@ -6521,7 +6506,7 @@ snapshots: '@push.rocks/smartpromise@4.2.3': {} - '@push.rocks/smartproxy@27.6.0': + '@push.rocks/smartproxy@27.7.0': dependencies: '@push.rocks/smartcrypto': 2.0.4 '@push.rocks/smartlog': 3.2.2 @@ -6923,12 +6908,12 @@ snapshots: domhandler: 5.0.3 selderee: 0.11.0 - '@serve.zone/catalog@2.12.3(@tiptap/pm@2.27.2)': + '@serve.zone/catalog@2.12.4(@tiptap/pm@2.27.2)': dependencies: '@design.estate/dees-catalog': 3.78.2(@tiptap/pm@2.27.2) '@design.estate/dees-domtools': 2.5.4 '@design.estate/dees-element': 2.2.4 - '@design.estate/dees-wcctools': 3.8.0 + '@design.estate/dees-wcctools': 3.9.0 transitivePeerDependencies: - '@nuxt/kit' - '@tiptap/pm' @@ -8659,7 +8644,7 @@ snapshots: lowercase-keys@3.0.0: {} - lru-cache@11.3.3: {} + lru-cache@11.3.5: {} lru-cache@7.18.3: {} @@ -9304,7 +9289,7 @@ snapshots: path-scurry@2.0.2: dependencies: - lru-cache: 11.3.3 + lru-cache: 11.3.5 minipass: 7.1.3 path-to-regexp@8.4.2: {} diff --git a/test/test.dns-runtime-routes.node.ts b/test/test.dns-runtime-routes.node.ts new file mode 100644 index 0000000..d91c21e --- /dev/null +++ b/test/test.dns-runtime-routes.node.ts @@ -0,0 +1,230 @@ +import { tap, expect } from '@git.zone/tstest/tapbundle'; +import { DcRouter } from '../ts/classes.dcrouter.js'; +import { RouteConfigManager } from '../ts/config/index.js'; +import { DcRouterDb, DomainDoc, RouteDoc } from '../ts/db/index.js'; +import { DnsManager } from '../ts/dns/manager.dns.js'; +import { logger } from '../ts/logger.js'; +import * as plugins from '../ts/plugins.js'; + +const createTestDb = async () => { + const storagePath = plugins.path.join( + plugins.os.tmpdir(), + `dcrouter-dns-runtime-routes-${Date.now()}-${Math.random().toString(16).slice(2)}`, + ); + + DcRouterDb.resetInstance(); + const db = DcRouterDb.getInstance({ + storagePath, + dbName: `dcrouter-test-${Date.now()}-${Math.random().toString(16).slice(2)}`, + }); + await db.start(); + await db.getDb().mongoDb.createCollection('__test_init'); + + return { + async cleanup() { + await db.stop(); + DcRouterDb.resetInstance(); + await plugins.fs.promises.rm(storagePath, { recursive: true, force: true }); + }, + }; +}; + +const testDbPromise = createTestDb(); + +const clearTestState = async () => { + for (const route of await RouteDoc.findAll()) { + await route.delete(); + } + for (const domain of await DomainDoc.findAll()) { + await domain.delete(); + } +}; + +tap.test('RouteConfigManager applies runtime DoH routes without persisting them', async () => { + await testDbPromise; + await clearTestState(); + + const dcRouter = new DcRouter({ + dnsNsDomains: ['ns1.example.com', 'ns2.example.com'], + dnsScopes: ['example.com'], + smartProxyConfig: { routes: [] }, + dbConfig: { enabled: false }, + }); + + const appliedRoutes: any[][] = []; + const smartProxy = { + updateRoutes: async (routes: any[]) => { + appliedRoutes.push(routes); + }, + }; + + const routeManager = new RouteConfigManager( + () => smartProxy as any, + undefined, + undefined, + undefined, + undefined, + () => (dcRouter as any).generateDnsRoutes(), + ); + + await routeManager.initialize([], [], []); + await routeManager.applyRoutes(); + + const persistedRoutes = await RouteDoc.findAll(); + expect(persistedRoutes.length).toEqual(0); + expect(appliedRoutes.length).toEqual(2); + + for (const routeSet of appliedRoutes) { + const dnsQueryRoute = routeSet.find((route) => route.name === 'dns-over-https-dns-query'); + const resolveRoute = routeSet.find((route) => route.name === 'dns-over-https-resolve'); + + expect(dnsQueryRoute).toBeDefined(); + expect(resolveRoute).toBeDefined(); + expect(typeof dnsQueryRoute.action.socketHandler).toEqual('function'); + expect(typeof resolveRoute.action.socketHandler).toEqual('function'); + } +}); + +tap.test('RouteConfigManager removes stale persisted DoH socket-handler routes on startup', async () => { + await testDbPromise; + await clearTestState(); + + const staleDnsQueryRoute = new RouteDoc(); + staleDnsQueryRoute.id = 'stale-doh-query'; + staleDnsQueryRoute.route = { + name: 'dns-over-https-dns-query', + match: { + ports: [443], + domains: ['ns1.example.com'], + path: '/dns-query', + }, + action: { + type: 'socket-handler' as any, + } as any, + }; + staleDnsQueryRoute.enabled = true; + staleDnsQueryRoute.createdAt = Date.now(); + staleDnsQueryRoute.updatedAt = Date.now(); + staleDnsQueryRoute.createdBy = 'test'; + staleDnsQueryRoute.origin = 'dns'; + await staleDnsQueryRoute.save(); + + const staleResolveRoute = new RouteDoc(); + staleResolveRoute.id = 'stale-doh-resolve'; + staleResolveRoute.route = { + name: 'dns-over-https-resolve', + match: { + ports: [443], + domains: ['ns1.example.com'], + path: '/resolve', + }, + action: { + type: 'socket-handler' as any, + } as any, + }; + staleResolveRoute.enabled = true; + staleResolveRoute.createdAt = Date.now(); + staleResolveRoute.updatedAt = Date.now(); + staleResolveRoute.createdBy = 'test'; + staleResolveRoute.origin = 'dns'; + await staleResolveRoute.save(); + + const validRoute = new RouteDoc(); + validRoute.id = 'valid-forward-route'; + validRoute.route = { + name: 'valid-forward-route', + match: { + ports: [443], + domains: ['app.example.com'], + }, + action: { + type: 'forward', + targets: [{ host: '127.0.0.1', port: 8443 }], + tls: { mode: 'terminate' as const }, + }, + } as any; + validRoute.enabled = true; + validRoute.createdAt = Date.now(); + validRoute.updatedAt = Date.now(); + validRoute.createdBy = 'test'; + validRoute.origin = 'api'; + await validRoute.save(); + + const appliedRoutes: any[][] = []; + const smartProxy = { + updateRoutes: async (routes: any[]) => { + appliedRoutes.push(routes); + }, + }; + + const routeManager = new RouteConfigManager(() => smartProxy as any); + await routeManager.initialize([], [], []); + + expect((await RouteDoc.findByName('dns-over-https-dns-query'))).toEqual(null); + expect((await RouteDoc.findByName('dns-over-https-resolve'))).toEqual(null); + + const remainingRoutes = await RouteDoc.findAll(); + expect(remainingRoutes.length).toEqual(1); + expect(remainingRoutes[0].route.name).toEqual('valid-forward-route'); + + expect(appliedRoutes.length).toEqual(1); + expect(appliedRoutes[0].length).toEqual(1); + expect(appliedRoutes[0][0].name).toEqual('valid-forward-route'); +}); + +tap.test('DnsManager warning keeps dnsNsDomains in scope', async () => { + await testDbPromise; + await clearTestState(); + const originalLog = logger.log.bind(logger); + const warningMessages: string[] = []; + + (logger as any).log = (level: 'error' | 'warn' | 'info' | 'success' | 'debug', message: string, context?: Record) => { + if (level === 'warn') { + warningMessages.push(message); + } + return originalLog(level, message, context || {}); + }; + + try { + const existingDomain = new DomainDoc(); + existingDomain.id = 'existing-domain'; + existingDomain.name = 'example.com'; + existingDomain.source = 'dcrouter'; + existingDomain.authoritative = true; + existingDomain.createdAt = Date.now(); + existingDomain.updatedAt = Date.now(); + existingDomain.createdBy = 'test'; + await existingDomain.save(); + + const dnsManager = new DnsManager({ + dnsNsDomains: ['ns1.example.com'], + dnsScopes: ['example.com'], + dnsRecords: [{ name: 'www.example.com', type: 'A', value: '127.0.0.1' }], + smartProxyConfig: { routes: [] }, + }); + + await dnsManager.start(); + + expect( + warningMessages.some((message) => + message.includes('ignoring legacy dnsScopes/dnsRecords constructor config') + && message.includes('dnsNsDomains is still required for nameserver and DoH bootstrap'), + ), + ).toEqual(true); + expect( + warningMessages.some((message) => + message.includes('ignoring legacy dnsScopes/dnsRecords/dnsNsDomains constructor config'), + ), + ).toEqual(false); + } finally { + (logger as any).log = originalLog; + } +}); + +tap.test('cleanup test db', async () => { + await clearTestState(); + const testDb = await testDbPromise; + await testDb.cleanup(); +}); + +export default tap.start(); diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index fe88eab..3d4df56 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: '13.17.5', + version: '13.17.6', description: 'A multifaceted routing service handling mail and SMS delivery functions.' } diff --git a/ts/classes.dcrouter.ts b/ts/classes.dcrouter.ts index cde4e47..5e6e49c 100644 --- a/ts/classes.dcrouter.ts +++ b/ts/classes.dcrouter.ts @@ -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 diff --git a/ts/config/classes.route-config-manager.ts b/ts/config/classes.route-config-manager.ts index 154375b..3189787 100644 --- a/ts/config/classes.route-config-manager.ts +++ b/ts/config/classes.route-config-manager.ts @@ -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 { 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 { @@ -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'); + } } diff --git a/ts/dns/manager.dns.ts b/ts/dns/manager.dns.ts index f0ab562..6ee59d9 100644 --- a/ts/dns/manager.dns.ts +++ b/ts/dns/manager.dns.ts @@ -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; diff --git a/ts_web/00_commitinfo_data.ts b/ts_web/00_commitinfo_data.ts index fe88eab..3d4df56 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: '13.17.5', + version: '13.17.6', description: 'A multifaceted routing service handling mail and SMS delivery functions.' }