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();