231 lines
6.9 KiB
TypeScript
231 lines
6.9 KiB
TypeScript
|
|
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<string, any>) => {
|
||
|
|
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();
|