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; // 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('Test SOA with DNSSEC timing', async () => { const httpsData = await tapNodeTools.createHttpsCert(); const udpPort = 8754; dnsServer = new smartdns.DnsServer({ httpsKey: httpsData.key, httpsCert: httpsData.cert, httpsPort: 8755, udpPort: udpPort, dnssecZone: 'example.com', }); await dnsServer.start(); const client = dgram.createSocket('udp4'); // Test with DNSSEC enabled const query = dnsPacket.encode({ type: 'query', id: 1, 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, ], }); const startTime = Date.now(); console.log('Sending DNSSEC query for nonexistent domain...'); const responsePromise = new Promise((resolve, reject) => { const timeout = setTimeout(() => { client.close(); const elapsed = Date.now() - startTime; reject(new Error(`Query timed out after ${elapsed}ms`)); }, 3000); client.on('message', (msg) => { clearTimeout(timeout); const elapsed = Date.now() - startTime; console.log(`Response received in ${elapsed}ms`); 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); const elapsed = Date.now() - startTime; console.error(`Error after ${elapsed}ms:`, err); 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 details:'); console.log('- Answers:', dnsResponse.answers.length); console.log('- Answer types:', dnsResponse.answers.map(a => a.type)); const soaAnswers = dnsResponse.answers.filter(a => a.type === 'SOA'); const rrsigAnswers = dnsResponse.answers.filter(a => a.type === 'RRSIG'); console.log('- SOA records:', soaAnswers.length); console.log('- RRSIG records:', 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'); } catch (error) { console.error('DNSSEC SOA query failed:', error); throw error; } await stopServer(dnsServer); dnsServer = null; }); tap.test('DNSSEC signing completes within reasonable time', async () => { const httpsData = await tapNodeTools.createHttpsCert(); const udpPort = 8756; dnsServer = new smartdns.DnsServer({ httpsKey: httpsData.key, httpsCert: httpsData.cert, httpsPort: 8757, udpPort: udpPort, dnssecZone: 'perf.example.com', }); // No handlers registered — server returns SOA for nonexistent domain await dnsServer.start(); const client = dgram.createSocket('udp4'); const query = dnsPacket.encode({ type: 'query', id: 2, flags: dnsPacket.RECURSION_DESIRED, questions: [ { name: 'nonexistent.perf.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, ], }); const startTime = Date.now(); console.log('Sending DNSSEC query for performance test...'); const responsePromise = new Promise((resolve, reject) => { const timeout = setTimeout(() => { client.close(); const elapsed = Date.now() - startTime; reject(new Error(`Query timed out after ${elapsed}ms — exceeds 2s budget`)); }, 2000); client.on('message', (msg) => { clearTimeout(timeout); const elapsed = Date.now() - startTime; console.log(`DNSSEC response received in ${elapsed}ms`); 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 elapsed = Date.now() - startTime; // Response must arrive within 2 seconds (generous for CI) expect(elapsed).toBeLessThan(2000); // Verify correctness: SOA + RRSIG present const soaAnswers = dnsResponse.answers.filter(a => a.type === 'SOA'); const rrsigAnswers = dnsResponse.answers.filter(a => a.type === 'RRSIG'); expect(soaAnswers.length).toEqual(1); expect(rrsigAnswers.length).toBeGreaterThan(0); const rrsigData = (rrsigAnswers[0] as any).data; expect(rrsigData.typeCovered).toEqual('SOA'); console.log(`DNSSEC signing performance OK: ${elapsed}ms`); } catch (error) { console.error('DNSSEC performance test failed:', error); throw error; } await stopServer(dnsServer); dnsServer = null; }); export default tap.start();