import * as plugins from '../ts_server/plugins.js'; import { expect, tap } from '@git.zone/tstest/tapbundle'; import { tapNodeTools } from '@git.zone/tstest/tapbundle_serverside'; 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'); const rrsigAnswers = dnsResponse.answers.filter(a => a.type === 'RRSIG'); console.log('SOA records found:', soaAnswers.length); console.log('RRSIG records found:', rrsigAnswers.length); // Must have exactly 1 SOA for the zone expect(soaAnswers.length).toEqual(1); // Must have at least 1 RRSIG covering the SOA expect(rrsigAnswers.length).toBeGreaterThan(0); // Verify RRSIG covers SOA type const rrsigData = (rrsigAnswers[0] as any).data; expect(rrsigData.typeCovered).toEqual('SOA'); // Verify SOA data fields are present and valid const soaData = (soaAnswers[0] as any).data; console.log('SOA data:', soaData); expect(soaData.mname).toStartWith('ns'); // nameserver expect(soaData.rname).toInclude('.'); // responsible party email expect(typeof soaData.serial).toEqual('number'); expect(soaData.refresh).toBeGreaterThan(0); expect(soaData.retry).toBeGreaterThan(0); expect(soaData.expire).toBeGreaterThan(0); expect(soaData.minimum).toBeGreaterThan(0); } catch (error) { console.error('SOA query with DNSSEC failed:', error); throw error; } await stopServer(dnsServer); dnsServer = null; }); tap.test('SOA serialization produces correct wire format', async () => { const httpsData = await tapNodeTools.createHttpsCert(); const udpPort = getUniqueUdpPort(); dnsServer = new smartdns.DnsServer({ httpsKey: httpsData.key, httpsCert: httpsData.cert, httpsPort: getUniqueHttpsPort(), udpPort: udpPort, dnssecZone: 'roundtrip.example.com', }); // Register a handler with specific SOA data we can verify round-trips correctly const expectedSoa = { mname: 'ns1.roundtrip.example.com', rname: 'admin.roundtrip.example.com', serial: 2025020101, refresh: 7200, retry: 1800, expire: 1209600, minimum: 43200, }; dnsServer.registerHandler('roundtrip.example.com', ['SOA'], (question) => { return { name: question.name, type: 'SOA', class: 'IN', ttl: 3600, data: expectedSoa, }; }); await dnsServer.start(); const client = dgram.createSocket('udp4'); // Plain UDP query without DNSSEC to test pure SOA serialization const query = dnsPacket.encode({ type: 'query', id: 3, flags: dnsPacket.RECURSION_DESIRED, questions: [ { name: 'roundtrip.example.com', type: 'SOA', class: 'IN', }, ], }); console.log('Sending plain SOA query for serialization round-trip test'); 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; const soaAnswers = dnsResponse.answers.filter(a => a.type === 'SOA'); expect(soaAnswers.length).toEqual(1); const soaData = (soaAnswers[0] as any).data; console.log('Round-trip SOA data:', soaData); // Verify all 7 SOA fields survived the full round-trip: // handler → Rust encode_soa → wire → dns-packet decode expect(soaData.mname).toEqual(expectedSoa.mname); expect(soaData.rname).toEqual(expectedSoa.rname); expect(soaData.serial).toEqual(expectedSoa.serial); expect(soaData.refresh).toEqual(expectedSoa.refresh); expect(soaData.retry).toEqual(expectedSoa.retry); expect(soaData.expire).toEqual(expectedSoa.expire); expect(soaData.minimum).toEqual(expectedSoa.minimum); } catch (error) { console.error('SOA serialization round-trip test failed:', error); throw error; } await stopServer(dnsServer); dnsServer = null; }); export default tap.start();