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 = 8300; let nextUdpPort = 8301; function getUniqueHttpsPort() { return nextHttpsPort++; } function getUniqueUdpPort() { return nextUdpPort++; } // Cleanup function for servers async function stopServer(server: smartdns.DnsServer | null | undefined) { if (!server) { return; } try { const stopPromise = server.stop(); const timeoutPromise = new Promise((_, reject) => { setTimeout(() => reject(new Error('Stop operation timed out')), 5000); }); await Promise.race([stopPromise, timeoutPromise]); } catch (e) { console.log('Handled error when stopping server:', e.message || e); // Force close if normal stop fails try { // @ts-ignore - accessing private properties for emergency cleanup if (server.httpsServer) { (server as any).httpsServer.close(); (server as any).httpsServer = null; } // @ts-ignore - accessing private properties for emergency cleanup if (server.udpServer) { (server as any).udpServer.close(); (server as any).udpServer = null; } } catch (forceError) { console.log('Force cleanup error:', forceError.message || forceError); } } } tap.test('should now return multiple NS records after fix', 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 multiple NS record handlers for the same domain dnsServer.registerHandler('example.com', ['NS'], (question) => { console.log('First NS handler called'); return { name: question.name, type: 'NS', class: 'IN', ttl: 3600, data: 'ns1.example.com', }; }); dnsServer.registerHandler('example.com', ['NS'], (question) => { console.log('Second NS handler called'); return { name: question.name, type: 'NS', class: 'IN', ttl: 3600, data: 'ns2.example.com', }; }); 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: 'NS', class: 'IN', }, ], }); const responsePromise = new Promise((resolve, reject) => { client.on('message', (msg) => { const dnsResponse = dnsPacket.decode(msg); resolve(dnsResponse); client.close(); }); client.on('error', (err) => { reject(err); client.close(); }); client.send(query, udpPort, 'localhost', (err) => { if (err) { reject(err); client.close(); } }); }); const dnsResponse = await responsePromise; console.log('Fixed behavior - NS records returned:', dnsResponse.answers.length); console.log('NS records:', dnsResponse.answers.filter(a => a.type === 'NS').map(a => a.data)); // FIXED BEHAVIOR: Should now return both NS records const nsAnswers = dnsResponse.answers.filter(a => a.type === 'NS'); expect(nsAnswers.length).toEqual(2); expect(nsAnswers.map(a => a.data).sort()).toEqual(['ns1.example.com', 'ns2.example.com']); await stopServer(dnsServer); dnsServer = null; }); tap.test('should support round-robin DNS with multiple A records', 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 multiple A record handlers for round-robin DNS const ips = ['10.0.0.1', '10.0.0.2', '10.0.0.3']; for (const ip of ips) { dnsServer.registerHandler('www.example.com', ['A'], (question) => { console.log(`A handler for ${ip} called`); return { name: question.name, type: 'A', class: 'IN', ttl: 300, data: ip, }; }); } await dnsServer.start(); const client = dgram.createSocket('udp4'); const query = dnsPacket.encode({ type: 'query', id: 2, flags: dnsPacket.RECURSION_DESIRED, questions: [ { name: 'www.example.com', type: 'A', class: 'IN', }, ], }); const responsePromise = new Promise((resolve, reject) => { client.on('message', (msg) => { const dnsResponse = dnsPacket.decode(msg); resolve(dnsResponse); client.close(); }); client.on('error', (err) => { reject(err); client.close(); }); client.send(query, udpPort, 'localhost', (err) => { if (err) { reject(err); client.close(); } }); }); const dnsResponse = await responsePromise; console.log('Fixed behavior - A records returned:', dnsResponse.answers.length); console.log('A records:', dnsResponse.answers.filter(a => a.type === 'A').map(a => a.data)); // FIXED BEHAVIOR: Should return all A records for round-robin const aAnswers = dnsResponse.answers.filter(a => a.type === 'A'); expect(aAnswers.length).toEqual(3); expect(aAnswers.map(a => a.data).sort()).toEqual(['10.0.0.1', '10.0.0.2', '10.0.0.3']); await stopServer(dnsServer); dnsServer = null; }); tap.test('should return multiple TXT records', 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 multiple TXT record handlers const txtRecords = [ ['v=spf1 include:_spf.example.com ~all'], ['v=DKIM1; k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNA...'], ['google-site-verification=1234567890abcdef'] ]; for (const data of txtRecords) { dnsServer.registerHandler('example.com', ['TXT'], (question) => { console.log(`TXT handler for ${data[0].substring(0, 20)}... called`); return { name: question.name, type: 'TXT', class: 'IN', ttl: 3600, data: data, }; }); } await dnsServer.start(); const client = dgram.createSocket('udp4'); const query = dnsPacket.encode({ type: 'query', id: 3, flags: dnsPacket.RECURSION_DESIRED, questions: [ { name: 'example.com', type: 'TXT', class: 'IN', }, ], }); const responsePromise = new Promise((resolve, reject) => { client.on('message', (msg) => { const dnsResponse = dnsPacket.decode(msg); resolve(dnsResponse); client.close(); }); client.on('error', (err) => { reject(err); client.close(); }); client.send(query, udpPort, 'localhost', (err) => { if (err) { reject(err); client.close(); } }); }); const dnsResponse = await responsePromise; console.log('Fixed behavior - TXT records returned:', dnsResponse.answers.length); const txtAnswers = dnsResponse.answers.filter(a => a.type === 'TXT'); console.log('TXT records count:', txtAnswers.length); // FIXED BEHAVIOR: Should return all TXT records expect(txtAnswers.length).toEqual(3); // Check that all expected records are present const txtData = txtAnswers.map(a => a.data[0].toString()); expect(txtData.some(d => d.includes('spf1'))).toEqual(true); expect(txtData.some(d => d.includes('DKIM1'))).toEqual(true); expect(txtData.some(d => d.includes('google-site-verification'))).toEqual(true); await stopServer(dnsServer); dnsServer = null; }); tap.test('should handle DNSSEC correctly with multiple records', 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 multiple NS record handlers dnsServer.registerHandler('example.com', ['NS'], (question) => { return { name: question.name, type: 'NS', class: 'IN', ttl: 3600, data: 'ns1.example.com', }; }); dnsServer.registerHandler('example.com', ['NS'], (question) => { return { name: question.name, type: 'NS', class: 'IN', ttl: 3600, data: 'ns2.example.com', }; }); await dnsServer.start(); const client = dgram.createSocket('udp4'); // Create query with DNSSEC requested const query = dnsPacket.encode({ type: 'query', id: 4, flags: dnsPacket.RECURSION_DESIRED, questions: [ { name: 'example.com', type: 'NS', class: 'IN', }, ], additionals: [ { name: '.', type: 'OPT', ttl: 0, flags: 0x8000, // DO bit set for DNSSEC data: Buffer.alloc(0), } as any, ], }); const responsePromise = new Promise((resolve, reject) => { client.on('message', (msg) => { const dnsResponse = dnsPacket.decode(msg); resolve(dnsResponse); client.close(); }); client.on('error', (err) => { reject(err); client.close(); }); client.send(query, udpPort, 'localhost', (err) => { if (err) { reject(err); client.close(); } }); }); const dnsResponse = await responsePromise; console.log('DNSSEC response - total answers:', dnsResponse.answers.length); const nsAnswers = dnsResponse.answers.filter(a => a.type === 'NS'); const rrsigAnswers = dnsResponse.answers.filter(a => a.type === 'RRSIG'); console.log('NS records:', nsAnswers.length); console.log('RRSIG records:', rrsigAnswers.length); // With DNSSEC RRset signing, all NS records share ONE RRSIG (entire RRset signed together) expect(nsAnswers.length).toEqual(2); expect(rrsigAnswers.length).toEqual(1); await stopServer(dnsServer); dnsServer = null; }); tap.test('should not return duplicate records when same handler registered multiple times', 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 the same handler multiple times (edge case) const sameHandler = (question) => { return { name: question.name, type: 'A', class: 'IN', ttl: 300, data: '10.0.0.1', }; }; dnsServer.registerHandler('test.example.com', ['A'], sameHandler); dnsServer.registerHandler('test.example.com', ['A'], sameHandler); dnsServer.registerHandler('test.example.com', ['A'], sameHandler); await dnsServer.start(); const client = dgram.createSocket('udp4'); const query = dnsPacket.encode({ type: 'query', id: 5, flags: dnsPacket.RECURSION_DESIRED, questions: [ { name: 'test.example.com', type: 'A', class: 'IN', }, ], }); const responsePromise = new Promise((resolve, reject) => { client.on('message', (msg) => { const dnsResponse = dnsPacket.decode(msg); resolve(dnsResponse); client.close(); }); client.on('error', (err) => { reject(err); client.close(); }); client.send(query, udpPort, 'localhost', (err) => { if (err) { reject(err); client.close(); } }); }); const dnsResponse = await responsePromise; const aAnswers = dnsResponse.answers.filter(a => a.type === 'A'); console.log('Duplicate handler test - A records returned:', aAnswers.length); // Even though handler is registered 3 times, we get 3 identical records // This is expected behavior - the DNS server doesn't deduplicate expect(aAnswers.length).toEqual(3); expect(aAnswers.every(a => a.data === '10.0.0.1')).toEqual(true); await stopServer(dnsServer); dnsServer = null; }); export default tap.start();