import * as plugins from '../ts_server/plugins.js'; import { expect, tap } from '@git.zone/tstest/tapbundle'; import { tapNodeTools } from '@git.zone/tstest/tapbundle_node'; import * as dnsPacket from 'dns-packet'; import * as dgram from 'dgram'; import * as smartdns from '../ts_server/index.js'; let dnsServer: smartdns.DnsServer; // Port management for tests let nextHttpsPort = 8700; let nextUdpPort = 8701; function getUniqueHttpsPort() { return nextHttpsPort++; } function getUniqueUdpPort() { return nextUdpPort++; } // Cleanup function for servers async function stopServer(server: smartdns.DnsServer | null | undefined) { if (!server) { return; } try { await server.stop(); } catch (e) { console.log('Handled error when stopping server:', e.message || e); } } tap.test('Direct SOA query should work without timeout', async () => { const httpsData = await tapNodeTools.createHttpsCert(); const udpPort = getUniqueUdpPort(); dnsServer = new smartdns.DnsServer({ httpsKey: httpsData.key, httpsCert: httpsData.cert, httpsPort: getUniqueHttpsPort(), udpPort: udpPort, dnssecZone: 'example.com', }); // Register a SOA handler directly dnsServer.registerHandler('example.com', ['SOA'], (question) => { console.log('Direct SOA handler called for:', question.name); return { name: question.name, type: 'SOA', class: 'IN', ttl: 3600, data: { mname: 'ns1.example.com', rname: 'hostmaster.example.com', serial: 2024010101, refresh: 3600, retry: 600, expire: 604800, minimum: 86400, }, }; }); await dnsServer.start(); const client = dgram.createSocket('udp4'); const query = dnsPacket.encode({ type: 'query', id: 1, flags: dnsPacket.RECURSION_DESIRED, questions: [ { name: 'example.com', type: 'SOA', class: 'IN', }, ], }); console.log('Sending SOA query for example.com'); const responsePromise = new Promise((resolve, reject) => { const timeout = setTimeout(() => { client.close(); reject(new Error('Query timed out after 5 seconds')); }, 5000); client.on('message', (msg) => { clearTimeout(timeout); try { const dnsResponse = dnsPacket.decode(msg); resolve(dnsResponse); } catch (e) { reject(new Error(`Failed to decode response: ${e.message}`)); } client.close(); }); client.on('error', (err) => { clearTimeout(timeout); reject(err); client.close(); }); client.send(query, udpPort, 'localhost', (err) => { if (err) { clearTimeout(timeout); reject(err); client.close(); } }); }); try { const dnsResponse = await responsePromise; console.log('SOA response received:', dnsResponse.answers.length, 'answers'); const soaAnswers = dnsResponse.answers.filter(a => a.type === 'SOA'); expect(soaAnswers.length).toEqual(1); const soaData = (soaAnswers[0] as any).data; console.log('SOA data:', soaData); expect(soaData.mname).toEqual('ns1.example.com'); expect(soaData.serial).toEqual(2024010101); } catch (error) { console.error('SOA query failed:', error); throw error; } await stopServer(dnsServer); dnsServer = null; }); tap.test('SOA query with DNSSEC should work', async () => { const httpsData = await tapNodeTools.createHttpsCert(); const udpPort = getUniqueUdpPort(); dnsServer = new smartdns.DnsServer({ httpsKey: httpsData.key, httpsCert: httpsData.cert, httpsPort: getUniqueHttpsPort(), udpPort: udpPort, dnssecZone: 'example.com', }); await dnsServer.start(); const client = dgram.createSocket('udp4'); const query = dnsPacket.encode({ type: 'query', id: 2, flags: dnsPacket.RECURSION_DESIRED, questions: [ { name: 'nonexistent.example.com', type: 'A', class: 'IN', }, ], additionals: [ { name: '.', type: 'OPT', ttl: 0, flags: 0x8000, // DO bit set for DNSSEC data: Buffer.alloc(0), } as any, ], }); console.log('Sending query for nonexistent domain with DNSSEC'); const responsePromise = new Promise((resolve, reject) => { const timeout = setTimeout(() => { client.close(); reject(new Error('Query timed out after 5 seconds')); }, 5000); client.on('message', (msg) => { clearTimeout(timeout); try { const dnsResponse = dnsPacket.decode(msg); resolve(dnsResponse); } catch (e) { reject(new Error(`Failed to decode response: ${e.message}`)); } client.close(); }); client.on('error', (err) => { clearTimeout(timeout); reject(err); client.close(); }); client.send(query, udpPort, 'localhost', (err) => { if (err) { clearTimeout(timeout); reject(err); client.close(); } }); }); try { const dnsResponse = await responsePromise; console.log('Response received with', dnsResponse.answers.length, 'answers'); const soaAnswers = dnsResponse.answers.filter(a => a.type === 'SOA'); console.log('SOA records found:', soaAnswers.length); if (soaAnswers.length > 0) { const soaData = (soaAnswers[0] as any).data; console.log('SOA data:', soaData); } } catch (error) { console.error('SOA query with DNSSEC failed:', error); throw error; } await stopServer(dnsServer); dnsServer = null; }); tap.test('Test raw SOA serialization', async () => { const httpsData = await tapNodeTools.createHttpsCert(); dnsServer = new smartdns.DnsServer({ httpsKey: httpsData.key, httpsCert: httpsData.cert, httpsPort: getUniqueHttpsPort(), udpPort: getUniqueUdpPort(), dnssecZone: 'example.com', }); // Test the serializeRData method directly const soaData = { mname: 'ns1.example.com', rname: 'hostmaster.example.com', serial: 2024010101, refresh: 3600, retry: 600, expire: 604800, minimum: 86400, }; try { // @ts-ignore - accessing private method for testing const serialized = dnsServer.serializeRData('SOA', soaData); console.log('SOA serialized successfully, buffer length:', serialized.length); expect(serialized.length).toBeGreaterThan(0); // The buffer should contain the serialized domain names + 5 * 4 bytes for the numbers // Domain names have variable length, but should be at least 20 bytes total expect(serialized.length).toBeGreaterThan(20); } catch (error) { console.error('SOA serialization failed:', error); throw error; } }); export default tap.start();